mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-10 09:04:22 +02:00
Compare commits
218
Commits
1.0-alpha2
..
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0c344939b | ||
|
|
e261263aab | ||
|
|
b88abb7c91 | ||
|
|
5f4b3c547b | ||
|
|
f854752c84 | ||
|
|
4195bdc7dd | ||
|
|
b68c11e33e | ||
|
|
b3e86be182 | ||
|
|
5802c2b9f5 | ||
|
|
83cef60c0f | ||
|
|
e39c93c91a | ||
|
|
1a96a150bc | ||
|
|
c66fbbdb36 | ||
|
|
03fb75437b | ||
|
|
3833803244 | ||
|
|
7c446796fe | ||
|
|
ee88d6f808 | ||
|
|
46495b9265 | ||
|
|
a8193fa063 | ||
|
|
e3eecb6584 | ||
|
|
35f3434b2f | ||
|
|
20dc779fd8 | ||
|
|
04f670cb50 | ||
|
|
6dce005594 | ||
|
|
a49f107af2 | ||
|
|
e7141685cc | ||
|
|
aca9e0de56 | ||
|
|
3b465890b6 | ||
|
|
cf59240bef | ||
|
|
d1715baae1 | ||
|
|
b132c901c9 | ||
|
|
a0e2fe551a | ||
|
|
800d4d91f9 | ||
|
|
6c6c9200e4 | ||
|
|
4fa87005bc | ||
|
|
17e4b51d18 | ||
|
|
6421367259 | ||
|
|
6ff2c654d9 | ||
|
|
f876a862c4 | ||
|
|
ef492d4690 | ||
|
|
36e539382c | ||
|
|
6f9e0f6d2e | ||
|
|
b84dda3c8e | ||
|
|
e2f3d2aca8 | ||
|
|
382e693fc8 | ||
|
|
a95cc678e9 | ||
|
|
bcf0aae531 | ||
|
|
b483f87c2f | ||
|
|
9d2e18328b | ||
|
|
174013da80 | ||
|
|
99a8f5f21a | ||
|
|
5e72fcbe14 | ||
|
|
e688d96c36 | ||
|
|
3845cab84b | ||
|
|
98e3a2e088 | ||
|
|
76b357c1ce | ||
|
|
80ab81190c | ||
|
|
934404cc90 | ||
|
|
6e39630b9b | ||
|
|
569a68a486 | ||
|
|
77c583ca39 | ||
|
|
ea05744e1c | ||
|
|
e0f0c98ffd | ||
|
|
a8d740ef21 | ||
|
|
90c8c4312c | ||
|
|
e5642546f1 | ||
|
|
4a33b2021c | ||
|
|
a4bc96af28 | ||
|
|
d8f622d438 | ||
|
|
ad287d3434 | ||
|
|
4a4a57e1a3 | ||
|
|
9b010d6388 | ||
|
|
8fc4667d2c | ||
|
|
7cbce0f81b | ||
|
|
578bb5741d | ||
|
|
0cfb446146 | ||
|
|
ae0feb8c9c | ||
|
|
7ff218a5fc | ||
|
|
cdc778c159 | ||
|
|
19d24975c2 | ||
|
|
65d70bacfa | ||
|
|
9ac2716826 | ||
|
|
53ee3dc5cc | ||
|
|
64f78bb847 | ||
|
|
f5cf7363da | ||
|
|
1fb4ab7e33 | ||
|
|
0ef727ce4b | ||
|
|
88e03ae312 | ||
|
|
d6efd9ced0 | ||
|
|
6ffba35742 | ||
|
|
9fc7650f3f | ||
|
|
551b98880b | ||
|
|
7c99ee9de8 | ||
|
|
9ac5b86431 | ||
|
|
329891fc83 | ||
|
|
15bc2221b9 | ||
|
|
4b5c38495a | ||
|
|
c9a729b369 | ||
|
|
47d43a91ff | ||
|
|
9bc0287540 | ||
|
|
fd6e4954e4 | ||
|
|
ab42b41d39 | ||
|
|
33787d486a | ||
|
|
ff0201bb4f | ||
|
|
868935f5e8 | ||
|
|
d6c5dffae6 | ||
|
|
e16c269f6d | ||
|
|
7d8aee98fd | ||
|
|
b4f4bad21a | ||
|
|
606f773f3d | ||
|
|
2d4656621b | ||
|
|
aedd750251 | ||
|
|
14bb12d3fb | ||
|
|
49506ada3f | ||
|
|
587ddf7646 | ||
|
|
8dc3f288b3 | ||
|
|
8b3c5c54a4 | ||
|
|
a01cdd9978 | ||
|
|
cb763e0926 | ||
|
|
f9ee38dd55 | ||
|
|
9ef715e341 | ||
|
|
3a57539f77 | ||
|
|
e5f5228329 | ||
|
|
61ef750da3 | ||
|
|
e817c13738 | ||
|
|
a0a5705fd8 | ||
|
|
7b58f14681 | ||
|
|
a39a693cda | ||
|
|
3a0439c9ae | ||
|
|
d7b9ed33f8 | ||
|
|
f2fbdf618f | ||
|
|
9da5aa1107 | ||
|
|
0f5d2927df | ||
|
|
f1d346f958 | ||
|
|
909915ceeb | ||
|
|
72b204dfe6 | ||
|
|
3d6ec140b7 | ||
|
|
b42d751e89 | ||
|
|
566c48ffed | ||
|
|
77b845a17f | ||
|
|
08cb3de683 | ||
|
|
119eb635cf | ||
|
|
e41afbb5c6 | ||
|
|
babdfd5086 | ||
|
|
7182e59892 | ||
|
|
6c0918312c | ||
|
|
3b05d89837 | ||
|
|
0af4ea81ff | ||
|
|
85ef421d72 | ||
|
|
f4ab8e2dd0 | ||
|
|
0f8d1bc6da | ||
|
|
6a839c4b7b | ||
|
|
cef8a3f116 | ||
|
|
9b55a8623c | ||
|
|
f79b2b061b | ||
|
|
34fda76831 | ||
|
|
ec44d70c7b | ||
|
|
6981f82b7b | ||
|
|
562e5c4c43 | ||
|
|
ff6c864294 | ||
|
|
9cc5131024 | ||
|
|
0412e051b2 | ||
|
|
64e09a19d9 | ||
|
|
43fa46779f | ||
|
|
db4280713c | ||
|
|
87c3a41398 | ||
|
|
0d944d39ef | ||
|
|
5926bfbd07 | ||
|
|
588ee8eed0 | ||
|
|
0b47b5b41e | ||
|
|
2360a5415f | ||
|
|
879362d09f | ||
|
|
13f469f9d6 | ||
|
|
29be7ac4e2 | ||
|
|
c0a876af68 | ||
|
|
44bcc94e3e | ||
|
|
34ad75860d | ||
|
|
51c1f15853 | ||
|
|
62da0a5ec2 | ||
|
|
17b56810d1 | ||
|
|
bbce6b2c5c | ||
|
|
476a3c42b6 | ||
|
|
a50c4c112e | ||
|
|
0fc019da03 | ||
|
|
76982ddbbb | ||
|
|
81b11403db | ||
|
|
975e5db82f | ||
|
|
ec08a5a72a | ||
|
|
ac70b5d48a | ||
|
|
46c653998e | ||
|
|
c6b4ac04e1 | ||
|
|
a61686a436 | ||
|
|
cd47d96de7 | ||
|
|
f185c4d489 | ||
|
|
f9b0f11221 | ||
|
|
4ae7875010 | ||
|
|
2a888f93ce | ||
|
|
ed8b93dee2 | ||
|
|
93058f92d4 | ||
|
|
97e19ac7a5 | ||
|
|
b54e554497 | ||
|
|
e7d9f73e1e | ||
|
|
31866113e4 | ||
|
|
03f002b348 | ||
|
|
3bcf13328b | ||
|
|
0d856ae180 | ||
|
|
e0488bb31f | ||
|
|
6632af9776 | ||
|
|
32436b0051 | ||
|
|
85336e0fca | ||
|
|
28ca12367a | ||
|
|
223407c596 | ||
|
|
80d6b181dd | ||
|
|
724e3051fd | ||
|
|
7ebd451dda | ||
|
|
9be5625c38 | ||
|
|
2ebac2f20a | ||
|
|
f5d1184b29 |
No files matched your search
@@ -34,3 +34,6 @@ nosetests.xml
|
||||
.project
|
||||
.pydevproject
|
||||
.settings
|
||||
|
||||
# Gedit Backup Files
|
||||
*~
|
||||
+11
-6
@@ -1,14 +1,19 @@
|
||||
language: python
|
||||
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
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:
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ include INSTALL
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include tox.ini
|
||||
recursive-include tests *
|
||||
recursive-exclude tests *
|
||||
recursive-include docs *
|
||||
recursive-include gns3server *
|
||||
recursive-exclude * __pycache__
|
||||
|
||||
+17
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1,3 @@
|
||||
fabric
|
||||
pygithub
|
||||
python-novaclient
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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__
|
||||
File renamed without changes.
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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()."""
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Whitespace-only changes.
@@ -0,0 +1,80 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Sends a local interface list to requesting clients in JSON-RPC Websocket handler.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from ..jsonrpc import JSONRPCResponse
|
||||
from ..jsonrpc import JSONRPCCustomError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_windows_interfaces():
|
||||
"""
|
||||
Get Windows interfaces.
|
||||
|
||||
:returns: list of windows interfaces
|
||||
"""
|
||||
|
||||
import win32com.client
|
||||
locator = win32com.client.Dispatch("WbemScripting.SWbemLocator")
|
||||
service = locator.ConnectServer(".", "root\cimv2")
|
||||
interfaces = []
|
||||
# more info on Win32_NetworkAdapter: http://msdn.microsoft.com/en-us/library/aa394216%28v=vs.85%29.aspx
|
||||
for adapter in service.InstancesOf("Win32_NetworkAdapter"):
|
||||
if adapter.NetConnectionStatus == 2 or adapter.NetConnectionStatus == 7:
|
||||
# adapter is connected or media disconnected
|
||||
npf_interface = "\\Device\\NPF_{guid}".format(guid=adapter.GUID)
|
||||
interfaces.append({"id": npf_interface,
|
||||
"name": adapter.NetConnectionID})
|
||||
return interfaces
|
||||
|
||||
|
||||
def interfaces(handler, request_id, params):
|
||||
"""
|
||||
Builtin destination to return all the network interfaces on this host.
|
||||
|
||||
:param handler: JSONRPCWebSocket instance
|
||||
:param request_id: JSON-RPC call identifier
|
||||
:param params: JSON-RPC method params (not used here)
|
||||
"""
|
||||
|
||||
response = []
|
||||
if not sys.platform.startswith("win"):
|
||||
try:
|
||||
import netifaces
|
||||
for interface in netifaces.interfaces():
|
||||
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()
|
||||
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)())
|
||||
except Exception as e:
|
||||
log.error("uncaught exception {type}".format(type=type(e)), exc_info=1)
|
||||
|
||||
handler.write_message(JSONRPCResponse(response, request_id)())
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
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.
|
||||
|
||||
:param handler: JSONRPCWebSocket instance
|
||||
:param request_id: JSON-RPC call identifier
|
||||
:param params: JSON-RPC method params (not used here)
|
||||
"""
|
||||
|
||||
json_message = {"version": __version__}
|
||||
handler.write_message(JSONRPCResponse(json_message, request_id)())
|
||||
Executable
+99
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -23,13 +23,15 @@ 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
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileUploadHandler(tornado.web.RequestHandler):
|
||||
class FileUploadHandler(GNS3BaseHandler):
|
||||
"""
|
||||
File upload handler.
|
||||
|
||||
@@ -37,14 +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))
|
||||
@@ -53,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.
|
||||
@@ -65,8 +66,13 @@ class FileUploadHandler(tornado.web.RequestHandler):
|
||||
for filename in os.listdir(path):
|
||||
items.append(filename)
|
||||
|
||||
self.render("upload.html", path=path, items=items)
|
||||
self.render("upload.html",
|
||||
version=__version__,
|
||||
host=self._host,
|
||||
path=path,
|
||||
items=items)
|
||||
|
||||
@tornado.web.authenticated
|
||||
def post(self):
|
||||
"""
|
||||
Invoked on POST request.
|
||||
@@ -75,8 +81,12 @@ class FileUploadHandler(tornado.web.RequestHandler):
|
||||
if "file" in self.request.files:
|
||||
fileinfo = self.request.files["file"][0]
|
||||
destination_path = os.path.join(self._upload_dir, fileinfo['filename'])
|
||||
with open(destination_path, 'wb') as f:
|
||||
f.write(fileinfo['body'])
|
||||
try:
|
||||
with open(destination_path, 'wb') as f:
|
||||
f.write(fileinfo['body'])
|
||||
except OSError as e:
|
||||
self.write("Could not upload {}: {}".format(fileinfo['filename'], e))
|
||||
return
|
||||
st = os.stat(destination_path)
|
||||
os.chmod(destination_path, st.st_mode | stat.S_IXUSR)
|
||||
self.redirect("/upload")
|
||||
@@ -22,14 +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, JSONRPCInvalidRequest, JSONRPCMethodNotFound, JSONRPCNotification
|
||||
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.
|
||||
@@ -48,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):
|
||||
"""
|
||||
@@ -100,8 +108,10 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
|
||||
# Make sure the destination is not already registered
|
||||
# by another module for instance
|
||||
assert destination not in cls.destinations
|
||||
log.debug("registering {} as a destination for the {} module".format(destination,
|
||||
module))
|
||||
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))
|
||||
cls.destinations[destination] = module
|
||||
|
||||
def open(self):
|
||||
@@ -110,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):
|
||||
"""
|
||||
@@ -121,6 +139,10 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
|
||||
|
||||
log.debug("Received Websocket message: {}".format(message))
|
||||
|
||||
if self.zmq_router.closed:
|
||||
# no need to proceed, the ZeroMQ router has been closed.
|
||||
return
|
||||
|
||||
try:
|
||||
request = json_decode(message)
|
||||
jsonrpc_version = request["jsonrpc"]
|
||||
@@ -133,16 +155,24 @@ 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...
|
||||
return
|
||||
|
||||
if method.startswith("builtin"):
|
||||
if method.startswith("builtin") and request_id:
|
||||
log.info("calling built-in method {}".format(method))
|
||||
self.destinations[method]()
|
||||
self.destinations[method](self, request_id, request.get("params"))
|
||||
return
|
||||
|
||||
module = self.destinations[method]
|
||||
@@ -164,7 +194,7 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
|
||||
|
||||
# Reset the modules if there are no clients anymore
|
||||
# Modules must implement a reset destination
|
||||
if not self.clients:
|
||||
if not self.clients and not self.zmq_router.closed:
|
||||
for destination, module in self.destinations.items():
|
||||
if destination.endswith("reset"):
|
||||
# Route to the correct module
|
||||
|
||||
@@ -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__}
|
||||
|
||||
@@ -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
|
||||
|
||||
+65
-7
@@ -20,15 +20,59 @@ import os
|
||||
import datetime
|
||||
import sys
|
||||
import multiprocessing
|
||||
import locale
|
||||
import tornado.options
|
||||
from gns3server.server import Server
|
||||
from gns3server.version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# command line options
|
||||
from tornado.options import define
|
||||
define("host", default="0.0.0.0", help="run on the given host/IP address", type=str)
|
||||
define("port", default=8000, help="run on the given port", type=int)
|
||||
define("ipc", default=False, help="use IPC for module communication", type=bool)
|
||||
define("version", default=False, help="show the version", type=bool)
|
||||
|
||||
|
||||
def locale_check():
|
||||
"""
|
||||
Checks if this application runs with a correct locale (i.e. supports UTF-8 encoding) and attempt to fix
|
||||
if this is not the case.
|
||||
|
||||
This is to prevent UnicodeEncodeError with unicode paths when using standard library I/O operation
|
||||
methods (e.g. os.stat() or os.path.*) which rely on the system or user locale.
|
||||
|
||||
More information can be found there: http://seasonofcode.com/posts/unicode-i-o-and-locales-in-python.html
|
||||
or there: http://robjwells.com/post/61198832297/get-your-us-ascii-out-of-my-face
|
||||
"""
|
||||
|
||||
# no need to check on Windows or when frozen
|
||||
if sys.platform.startswith("win") or hasattr(sys, "frozen"):
|
||||
return
|
||||
|
||||
language = encoding = None
|
||||
try:
|
||||
language, encoding = locale.getlocale()
|
||||
except ValueError as e:
|
||||
log.error("could not determine the current locale: {}".format(e))
|
||||
if not language and not encoding:
|
||||
try:
|
||||
log.warn("could not find a default locale, switching to C.UTF-8...")
|
||||
locale.setlocale(locale.LC_ALL, ("C", "UTF-8"))
|
||||
except locale.Error as e:
|
||||
log.error("could not switch to the C.UTF-8 locale: {}".format(e))
|
||||
raise SystemExit
|
||||
elif encoding != "UTF-8":
|
||||
log.warn("your locale {}.{} encoding is not UTF-8, switching to the UTF-8 version...".format(language, encoding))
|
||||
try:
|
||||
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))
|
||||
|
||||
|
||||
def main():
|
||||
@@ -37,9 +81,20 @@ def main():
|
||||
"""
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
# necessary on Windows to use freezing software
|
||||
# necessary on Windows to freeze the application
|
||||
multiprocessing.freeze_support()
|
||||
|
||||
try:
|
||||
tornado.options.parse_command_line()
|
||||
except (tornado.options.Error, ValueError):
|
||||
tornado.options.print_help()
|
||||
raise SystemExit
|
||||
|
||||
from tornado.options import options
|
||||
if options.version:
|
||||
print(__version__)
|
||||
raise SystemExit
|
||||
|
||||
current_year = datetime.date.today().year
|
||||
print("GNS3 server version {}".format(__version__))
|
||||
print("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year))
|
||||
@@ -53,13 +108,16 @@ def main():
|
||||
micro=sys.version_info[2],
|
||||
pid=os.getpid()))
|
||||
|
||||
try:
|
||||
tornado.options.parse_command_line()
|
||||
except (tornado.options.Error, ValueError):
|
||||
tornado.options.print_help()
|
||||
raise SystemExit
|
||||
# check for the correct locale
|
||||
# (UNIX/Linux only)
|
||||
locale_check()
|
||||
|
||||
try:
|
||||
os.getcwd()
|
||||
except FileNotFoundError:
|
||||
log.critical("the current working directory doesn't exist")
|
||||
return
|
||||
|
||||
from tornado.options import options
|
||||
server = Server(options.host,
|
||||
options.port,
|
||||
ipc=options.ipc)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,9 +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 = [DeadMan, Dynamips, VPCS, VirtualBox, Qemu]
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
# IOU runs only on Linux
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Useful functions... in the attic ;)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import struct
|
||||
import socket
|
||||
import stat
|
||||
import errno
|
||||
import time
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP", ignore_ports=[]):
|
||||
"""
|
||||
Finds an unused port in a range.
|
||||
|
||||
:param start_port: first port in the range
|
||||
:param end_port: last port in the range
|
||||
:param host: host/address for bind()
|
||||
:param socket_type: TCP (default) or UDP
|
||||
:param ignore_ports: list of port to ignore within the range
|
||||
"""
|
||||
|
||||
if end_port < start_port:
|
||||
raise Exception("Invalid port range {}-{}".format(start_port, end_port))
|
||||
|
||||
if socket_type == "UDP":
|
||||
socket_type = socket.SOCK_DGRAM
|
||||
else:
|
||||
socket_type = socket.SOCK_STREAM
|
||||
|
||||
for port in range(start_port, end_port + 1):
|
||||
if port in ignore_ports:
|
||||
continue
|
||||
try:
|
||||
if ":" in host:
|
||||
# IPv6 address support
|
||||
with socket.socket(socket.AF_INET6, socket_type) as s:
|
||||
s.bind((host, port)) # the port is available if bind is a success
|
||||
else:
|
||||
with socket.socket(socket.AF_INET, socket_type) as s:
|
||||
s.bind((host, port)) # the port is available if bind is a success
|
||||
return port
|
||||
except OSError as e:
|
||||
if e.errno == errno.EADDRINUSE: # socket already in use
|
||||
if port + 1 == end_port:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
raise Exception("Could not find an unused port: {}".format(e))
|
||||
|
||||
raise Exception("Could not find a free port between {0} and {1}".format(start_port, end_port))
|
||||
|
||||
|
||||
def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10):
|
||||
"""
|
||||
Waits for a socket to be ready for wait time.
|
||||
|
||||
:param host: host/address to connect to
|
||||
:param port: port to connect to
|
||||
:param wait: maximum wait time
|
||||
:param socket_timeout: timeout for the socket
|
||||
|
||||
:returns: tuple with boolean indicating if the socket is ready and the last exception
|
||||
that occurred when connecting to the socket
|
||||
"""
|
||||
|
||||
# connect to a local address by default
|
||||
# if listening to all addresses (IPv4 or IPv6)
|
||||
if host == "0.0.0.0":
|
||||
host = "127.0.0.1"
|
||||
elif host == "::":
|
||||
host = "::1"
|
||||
|
||||
connection_success = False
|
||||
begin = time.time()
|
||||
last_exception = None
|
||||
while time.time() - begin < wait:
|
||||
time.sleep(0.01)
|
||||
try:
|
||||
with socket.create_connection((host, port), socket_timeout):
|
||||
pass
|
||||
except OSError as e:
|
||||
last_exception = e
|
||||
continue
|
||||
connection_success = True
|
||||
break
|
||||
|
||||
return connection_success, last_exception
|
||||
|
||||
|
||||
def has_privileged_access(executable):
|
||||
"""
|
||||
Check if an executable can access Ethernet and TAP devices in
|
||||
RAW mode.
|
||||
|
||||
:param executable: executable path
|
||||
|
||||
:returns: True or False
|
||||
"""
|
||||
|
||||
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)
|
||||
if sys.platform.startswith("linux") and "security.capability" in os.listxattr(executable):
|
||||
try:
|
||||
caps = os.getxattr(executable, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error("could not determine if CAP_NET_RAW capability is set for {}: {}".format(executable, e))
|
||||
|
||||
return False
|
||||
+84
-11
@@ -19,6 +19,7 @@
|
||||
Base class (interface) for modules
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import gns3server.jsonrpc as jsonrpc
|
||||
@@ -26,6 +27,9 @@ import multiprocessing
|
||||
import zmq
|
||||
import signal
|
||||
|
||||
from gns3server.config import Config
|
||||
from jsonschema import validate, ValidationError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,15 +47,20 @@ 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
|
||||
self._stream = None
|
||||
self._dealer = None
|
||||
self._zmq_host = args[0] # ZeroMQ server address
|
||||
self._zmq_port = args[1] # ZeroMQ server port
|
||||
self._current_session = None
|
||||
self._current_destination = None
|
||||
self._current_call_id = None
|
||||
self._stopping = False
|
||||
|
||||
def _setup(self):
|
||||
"""
|
||||
@@ -69,24 +78,24 @@ class IModule(multiprocessing.Process):
|
||||
:returns: ZMQ stream instance
|
||||
"""
|
||||
|
||||
socket = self._context.socket(zmq.DEALER)
|
||||
socket.setsockopt(zmq.IDENTITY, self.name.encode("utf-8"))
|
||||
self._dealer = self._context.socket(zmq.DEALER)
|
||||
self._dealer.setsockopt(zmq.IDENTITY, self.name.encode("utf-8"))
|
||||
if host and port:
|
||||
log.info("ZeroMQ client ({}) connecting to {}:{}".format(self.name, host, port))
|
||||
try:
|
||||
socket.connect("tcp://{}:{}".format(host, port))
|
||||
self._dealer.connect("tcp://{}:{}".format(host, port))
|
||||
except zmq.error.ZMQError as e:
|
||||
log.critical("Could not connect to ZeroMQ server on {}:{}, reason: {}".format(host, port, e))
|
||||
raise SystemExit
|
||||
else:
|
||||
log.info("ZeroMQ client ({}) connecting to ipc:///tmp/gns3.ipc".format(self.name))
|
||||
try:
|
||||
socket.connect("ipc:///tmp/gns3.ipc")
|
||||
self._dealer.connect("ipc:///tmp/gns3.ipc")
|
||||
except zmq.error.ZMQError as e:
|
||||
log.critical("Could not connect to ZeroMQ server on ipc:///tmp/gns3.ipc, reason: {}".format(e))
|
||||
raise SystemExit
|
||||
|
||||
stream = zmq.eventloop.zmqstream.ZMQStream(socket, self._ioloop)
|
||||
stream = zmq.eventloop.zmqstream.ZMQStream(self._dealer, self._ioloop)
|
||||
if callback:
|
||||
stream.on_recv(callback)
|
||||
return stream
|
||||
@@ -109,7 +118,7 @@ class IModule(multiprocessing.Process):
|
||||
|
||||
def signal_handler(signum=None, frame=None):
|
||||
log.warning("Module {} got signal {}, exiting...".format(self.name, signum))
|
||||
self.stop()
|
||||
self.stop(signum)
|
||||
|
||||
signals = [signal.SIGTERM, signal.SIGINT]
|
||||
if not sys.platform.startswith("win"):
|
||||
@@ -126,13 +135,36 @@ class IModule(multiprocessing.Process):
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
|
||||
def stop(self):
|
||||
log.info("{} module has stopped".format(self.name))
|
||||
|
||||
def _shutdown(self):
|
||||
"""
|
||||
Stops the event loop.
|
||||
Shutdowns the I/O loop and the ZeroMQ stream & socket
|
||||
"""
|
||||
|
||||
if self._ioloop:
|
||||
self._ioloop.stop()
|
||||
self._ioloop.stop()
|
||||
|
||||
if self._stream and not self._stream.closed:
|
||||
# close the zeroMQ stream
|
||||
self._stream.close()
|
||||
|
||||
if self._dealer and not self._dealer.closed:
|
||||
# close the ZeroMQ dealer socket
|
||||
self._dealer.close()
|
||||
|
||||
def stop(self, signum=None):
|
||||
"""
|
||||
Adds a callback to stop the event loop & ZeroMQ.
|
||||
|
||||
:param signum: signal number (if called by the signal handler)
|
||||
"""
|
||||
|
||||
if not self._stopping:
|
||||
self._stopping = True
|
||||
if signum:
|
||||
self._ioloop.add_callback_from_signal(self._shutdown)
|
||||
else:
|
||||
self._ioloop.add_callback(self._shutdown)
|
||||
|
||||
def send_response(self, results):
|
||||
"""
|
||||
@@ -208,6 +240,18 @@ class IModule(multiprocessing.Process):
|
||||
:param request: request from ZeroMQ server
|
||||
"""
|
||||
|
||||
# server is shutting down, do not process
|
||||
# more request
|
||||
if self._stopping:
|
||||
return
|
||||
|
||||
# handle special request to stop the module
|
||||
# e.g. useful on Windows where the
|
||||
# SIGBREAK signal cound't be propagated
|
||||
if request[0] == b"stop":
|
||||
self.stop()
|
||||
return
|
||||
|
||||
try:
|
||||
request = zmq.utils.jsonapi.loads(request[0])
|
||||
except ValueError:
|
||||
@@ -233,11 +277,35 @@ class IModule(multiprocessing.Process):
|
||||
log.error("uncaught exception {type}".format(type=type(e)), exc_info=1)
|
||||
exc_type, exc_value, exc_tb = sys.exc_info()
|
||||
lines = traceback.format_exception(exc_type, exc_value, exc_tb)
|
||||
tb = "\n" . join(lines)
|
||||
tb = "".join(lines)
|
||||
self.send_custom_error("uncaught exception {type}: {string}\n{tb}".format(type=type(e),
|
||||
string=str(e),
|
||||
tb=tb))
|
||||
|
||||
def validate_request(self, request, schema):
|
||||
"""
|
||||
Validates a request.
|
||||
|
||||
:param request: request (JSON-RPC params)
|
||||
:param schema: JSON-SCHEMA to validate the request
|
||||
|
||||
:returns: True or False
|
||||
"""
|
||||
|
||||
# check if we have a request
|
||||
if request is None:
|
||||
self.send_param_error()
|
||||
return False
|
||||
log.debug("received request {}".format(request))
|
||||
|
||||
# validate the request
|
||||
try:
|
||||
validate(request, schema)
|
||||
except ValidationError as e:
|
||||
self.send_custom_error("request validation error: {}".format(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def destinations(self):
|
||||
"""
|
||||
Destinations handled by this module.
|
||||
@@ -265,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
|
||||
@@ -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()
|
||||
@@ -24,12 +24,16 @@ import os
|
||||
import base64
|
||||
import tempfile
|
||||
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
|
||||
@@ -102,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 = {}
|
||||
@@ -114,24 +137,30 @@ 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
|
||||
self._callback = self.add_periodic_callback(self._check_hypervisors, 5000)
|
||||
self._callback.start()
|
||||
|
||||
def stop(self):
|
||||
def stop(self, signum=None):
|
||||
"""
|
||||
Properly stops the module.
|
||||
|
||||
:param signum: signal number (if called by the signal handler)
|
||||
"""
|
||||
|
||||
if not sys.platform.startswith("win32"):
|
||||
self._callback.stop()
|
||||
|
||||
# stop all Dynamips hypervisors
|
||||
if self._hypervisor_manager:
|
||||
self._hypervisor_manager.stop_all_hypervisors()
|
||||
IModule.stop(self) # this will stop the I/O loop
|
||||
|
||||
self.delete_dynamips_files()
|
||||
|
||||
IModule.stop(self, signum) # this will stop the I/O loop
|
||||
|
||||
def _check_hypervisors(self):
|
||||
"""
|
||||
@@ -154,12 +183,46 @@ class Dynamips(IModule):
|
||||
self.send_notification("{}.dynamips_stopped".format(self.name), notification)
|
||||
hypervisor.stop()
|
||||
|
||||
@IModule.route("dynamips.reset")
|
||||
def reset(self, request):
|
||||
def get_device_instance(self, device_id, instance_dict):
|
||||
"""
|
||||
Resets the module.
|
||||
Returns a device instance.
|
||||
|
||||
:param request: JSON request
|
||||
:param device_id: device identifier
|
||||
:param instance_dict: dictionary containing the instances
|
||||
|
||||
:returns: device instance
|
||||
"""
|
||||
|
||||
if device_id not in instance_dict:
|
||||
log.debug("device ID {} doesn't exist".format(device_id), exc_info=1)
|
||||
self.send_custom_error("Device ID {} doesn't exist".format(device_id))
|
||||
return None
|
||||
return instance_dict[device_id]
|
||||
|
||||
def delete_dynamips_files(self):
|
||||
"""
|
||||
Deletes useless Dynamips files from the working directory
|
||||
"""
|
||||
|
||||
files = glob.glob(os.path.join(self._working_dir, "dynamips", "*.ghost"))
|
||||
files += glob.glob(os.path.join(self._working_dir, "dynamips", "*_lock"))
|
||||
files += glob.glob(os.path.join(self._working_dir, "dynamips", "ilt_*"))
|
||||
files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_rommon_vars"))
|
||||
files += glob.glob(os.path.join(self._working_dir, "dynamips", "c[0-9][0-9][0-9][0-9]_*_ssa"))
|
||||
for file in files:
|
||||
try:
|
||||
log.debug("deleting file {}".format(file))
|
||||
os.remove(file)
|
||||
except OSError as e:
|
||||
log.warn("could not delete file {}: {}".format(file, e))
|
||||
continue
|
||||
|
||||
@IModule.route("dynamips.reset")
|
||||
def reset(self, request=None):
|
||||
"""
|
||||
Resets the module (JSON-RPC notification).
|
||||
|
||||
:param request: JSON request (not used)
|
||||
"""
|
||||
|
||||
# stop all Dynamips hypervisors
|
||||
@@ -188,6 +251,8 @@ class Dynamips(IModule):
|
||||
self._frame_relay_switches.clear()
|
||||
self._atm_switches.clear()
|
||||
|
||||
self.delete_dynamips_files()
|
||||
|
||||
self._hypervisor_manager = None
|
||||
log.info("dynamips module has been reset")
|
||||
|
||||
@@ -204,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
|
||||
@@ -233,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
|
||||
|
||||
@@ -258,7 +324,9 @@ class Dynamips(IModule):
|
||||
|
||||
else:
|
||||
if "project_name" in request:
|
||||
# for remote server
|
||||
new_working_dir = os.path.join(self._projects_dir, request["project_name"])
|
||||
|
||||
if self._projects_dir != self._working_dir != new_working_dir:
|
||||
|
||||
# trick to avoid file locks by Dynamips on Windows
|
||||
@@ -267,6 +335,7 @@ class Dynamips(IModule):
|
||||
|
||||
if not os.path.isdir(new_working_dir):
|
||||
try:
|
||||
self.delete_dynamips_files()
|
||||
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,
|
||||
@@ -274,8 +343,12 @@ class Dynamips(IModule):
|
||||
e))
|
||||
return
|
||||
|
||||
self._hypervisor_manager.working_dir = new_working_dir
|
||||
self._working_dir = new_working_dir
|
||||
elif "working_dir" in request:
|
||||
# for local server
|
||||
new_working_dir = request.pop("working_dir")
|
||||
|
||||
self._working_dir = new_working_dir
|
||||
self._hypervisor_manager.working_dir = new_working_dir
|
||||
|
||||
# apply settings to the hypervisor manager
|
||||
for name, value in request.items():
|
||||
@@ -290,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))
|
||||
@@ -307,36 +380,55 @@ class Dynamips(IModule):
|
||||
:returns: a NIO object
|
||||
"""
|
||||
|
||||
#TODO: JSON schema validation
|
||||
nio = None
|
||||
if request["nio"] == "NIO_UDP":
|
||||
lport = request["lport"]
|
||||
rhost = request["rhost"]
|
||||
rport = request["rport"]
|
||||
nio = NIO_UDP(node.hypervisor, lport, rhost, rport)
|
||||
# elif request["nio"] == "NIO_UDP_Auto":
|
||||
# lhost = request["lhost"]
|
||||
# lport_start = request["lport_start"]
|
||||
# lport_end = request["lport_end"]
|
||||
# nio = NIO_UDP_auto(node.hypervisor, lhost, lport_start, lport_end)
|
||||
elif request["nio"] == "NIO_GenericEthernet":
|
||||
ethernet_device = request["ethernet_device"]
|
||||
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 DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||
# check if we have an allocated NIO UDP auto
|
||||
nio = node.hypervisor.get_nio_udp_auto(lport)
|
||||
if not nio:
|
||||
# otherwise create an NIO UDP
|
||||
nio = NIO_UDP(node.hypervisor, lport, rhost, rport)
|
||||
else:
|
||||
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"] == "NIO_LinuxEthernet":
|
||||
ethernet_device = request["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"] == "NIO_TAP":
|
||||
tap_device = request["tap_device"]
|
||||
elif request["nio"]["type"] == "nio_tap":
|
||||
tap_device = request["nio"]["tap_device"]
|
||||
nio = NIO_TAP(node.hypervisor, tap_device)
|
||||
elif request["nio"] == "NIO_UNIX":
|
||||
local_file = request["local_file"]
|
||||
remote_file = request["remote_file"]
|
||||
elif request["nio"]["type"] == "nio_unix":
|
||||
local_file = request["nio"]["local_file"]
|
||||
remote_file = request["nio"]["remote_file"]
|
||||
nio = NIO_UNIX(node.hypervisor, local_file, remote_file)
|
||||
elif request["nio"] == "NIO_VDE":
|
||||
control_file = request["control_file"]
|
||||
local_file = request["local_file"]
|
||||
elif request["nio"]["type"] == "nio_vde":
|
||||
control_file = request["nio"]["control_file"]
|
||||
local_file = request["nio"]["local_file"]
|
||||
nio = NIO_VDE(node.hypervisor, control_file, local_file)
|
||||
elif request["nio"] == "NIO_Null":
|
||||
elif request["nio"]["type"] == "nio_null":
|
||||
nio = NIO_Null(node.hypervisor)
|
||||
return nio
|
||||
|
||||
@@ -357,23 +449,8 @@ class Dynamips(IModule):
|
||||
port,
|
||||
host))
|
||||
response = {"lport": port}
|
||||
|
||||
return response
|
||||
|
||||
# def allocate_udp_port_auto(self, node, lport_start, lport_end):
|
||||
# """
|
||||
# Allocates a UDP port in order to create an UDP NIO Auto.
|
||||
#
|
||||
# :param node: the node that needs to allocate an UDP port
|
||||
# """
|
||||
#
|
||||
# self._nio_udp_auto = NIO_UDP_auto(node.hypervisor, node.hypervisor.host, lport_start, lport_end)
|
||||
#
|
||||
# response = {"lport": self._nio_udp_auto.lport,
|
||||
# "lhost": self._nio_udp_auto.lhost}
|
||||
#
|
||||
# return response
|
||||
|
||||
def set_ghost_ios(self, router):
|
||||
"""
|
||||
Manages Ghost IOS support.
|
||||
@@ -401,37 +478,75 @@ class Dynamips(IModule):
|
||||
ghost.ghost_status = 1
|
||||
ghost.ghost_file = ghost_instance
|
||||
ghost.ram = router.ram
|
||||
ghost.start()
|
||||
ghost.stop()
|
||||
ghost.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
|
||||
router.ghost_status = 2
|
||||
router.ghost_file = ghost_instance
|
||||
|
||||
def save_base64config(self, config_base64, router, config_filename):
|
||||
def create_config_from_file(self, local_base_config, router, destination_config_path):
|
||||
"""
|
||||
Saves a base64 encoded config (decoded) to a file.
|
||||
Creates a config file from a local base config
|
||||
|
||||
:param config_base64: base64 encoded config
|
||||
:param local_base_config: path the a local base config
|
||||
:param router: router instance
|
||||
:param config_filename: file name to save the config
|
||||
:param destination_config_path: path to the destination config file
|
||||
|
||||
:returns: relative path to the config file
|
||||
:returns: relative path to the created config file
|
||||
"""
|
||||
|
||||
config = base64.decodestring(config_base64.encode("utf-8")).decode("utf-8")
|
||||
config = "!\n" + config.replace("\r", "")
|
||||
config = config.replace('%h', router.name)
|
||||
config_dir = os.path.join(router.hypervisor.working_dir, "configs")
|
||||
log.info("creating config file {} from {}".format(destination_config_path, local_base_config))
|
||||
config_path = destination_config_path
|
||||
config_dir = os.path.dirname(destination_config_path)
|
||||
try:
|
||||
os.makedirs(config_dir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not create configs directory: {}".format(e))
|
||||
config_path = os.path.join(config_dir, config_filename)
|
||||
|
||||
try:
|
||||
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", "")
|
||||
config = config.replace('%h', router.name)
|
||||
f.write(config)
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not save the configuration from {} to {}: {}".format(local_base_config, config_path, e))
|
||||
return "configs" + os.sep + os.path.basename(config_path)
|
||||
|
||||
def create_config_from_base64(self, config_base64, router, destination_config_path):
|
||||
"""
|
||||
Creates a config file from a base64 encoded config.
|
||||
|
||||
:param config_base64: base64 encoded config
|
||||
:param router: router instance
|
||||
:param destination_config_path: path to the destination config file
|
||||
|
||||
:returns: relative path to the created config file
|
||||
"""
|
||||
|
||||
log.info("creating config file {} from base64".format(destination_config_path))
|
||||
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)
|
||||
try:
|
||||
os.makedirs(config_dir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not create configs directory: {}".format(e))
|
||||
|
||||
config_path = destination_config_path
|
||||
try:
|
||||
with open(config_path, "w") as f:
|
||||
log.info("saving startup-config to {}".format(config_path))
|
||||
@@ -439,17 +554,3 @@ class Dynamips(IModule):
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not save the configuration {}: {}".format(config_path, e))
|
||||
return "configs" + os.sep + os.path.basename(config_path)
|
||||
|
||||
@IModule.route("dynamips.nio.get_interfaces")
|
||||
def nio_get_interfaces(self, request):
|
||||
"""
|
||||
Get all the network interfaces on this host.
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
try:
|
||||
import netifaces
|
||||
except ImportError:
|
||||
raise DynamipsError("The netifaces module is not installed")
|
||||
self.send_response(netifaces.interfaces())
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -16,10 +16,20 @@
|
||||
# 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
|
||||
|
||||
from ..schemas.atmsw import ATMSW_CREATE_SCHEMA
|
||||
from ..schemas.atmsw import ATMSW_DELETE_SCHEMA
|
||||
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__)
|
||||
|
||||
@@ -31,7 +41,7 @@ class ATMSW(object):
|
||||
"""
|
||||
Creates a new ATM switch.
|
||||
|
||||
Optional request parameters:
|
||||
Mandatory request parameters:
|
||||
- name (switch name)
|
||||
|
||||
Response parameters:
|
||||
@@ -41,11 +51,11 @@ class ATMSW(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
name = None
|
||||
if request and "name" in request:
|
||||
name = request["name"]
|
||||
# validate the request
|
||||
if not self.validate_request(request, ATMSW_CREATE_SCHEMA):
|
||||
return
|
||||
|
||||
name = request["name"]
|
||||
try:
|
||||
if not self._hypervisor_manager:
|
||||
self.start_hypervisor_manager()
|
||||
@@ -71,22 +81,20 @@ class ATMSW(object):
|
||||
- id (switch identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ATMSW_DELETE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
# get the ATM switch instance
|
||||
atmsw_id = request["id"]
|
||||
if atmsw_id not in self._atm_switches:
|
||||
self.send_custom_error("ATM switch id {} doesn't exist".format(atmsw_id))
|
||||
atmsw = self.get_device_instance(atmsw_id, self._atm_switches)
|
||||
if not atmsw:
|
||||
return
|
||||
atmsw = self._atm_switches[atmsw_id]
|
||||
|
||||
try:
|
||||
atmsw.delete()
|
||||
@@ -95,7 +103,7 @@ class ATMSW(object):
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("dynamips.atmsw.update")
|
||||
def atmsw_update(self, request):
|
||||
@@ -109,32 +117,31 @@ class ATMSW(object):
|
||||
- name (new switch name)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- name if changed
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ATMSW_UPDATE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
atmsw_id = request["id"]
|
||||
if atmsw_id not in self._atm_switches:
|
||||
self.send_custom_error("ATM switch id {} doesn't exist".format(atmsw_id))
|
||||
# get the ATM switch instance
|
||||
atmsw = self.get_device_instance(request["id"], self._atm_switches)
|
||||
if not atmsw:
|
||||
return
|
||||
atmsw = self._atm_switches[atmsw_id]
|
||||
|
||||
response = {}
|
||||
# rename the switch if requested
|
||||
if "name" in request and atmsw.name != request["name"]:
|
||||
try:
|
||||
atmsw.name = request["name"]
|
||||
response["name"] = atmsw.name
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
self.send_response(request)
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("dynamips.atmsw.allocate_udp_port")
|
||||
def atmsw_allocate_udp_port(self, request):
|
||||
@@ -153,17 +160,14 @@ class ATMSW(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ATMSW_ALLOCATE_UDP_PORT_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
atmsw_id = request["id"]
|
||||
if atmsw_id not in self._atm_switches:
|
||||
self.send_custom_error("ATM switch id {} doesn't exist".format(atmsw_id))
|
||||
# get the ATM switch instance
|
||||
atmsw = self.get_device_instance(request["id"], self._atm_switches)
|
||||
if not atmsw:
|
||||
return
|
||||
atmsw = self._atm_switches[atmsw_id]
|
||||
|
||||
try:
|
||||
# allocate a new UDP port
|
||||
@@ -185,42 +189,39 @@ class ATMSW(object):
|
||||
- port (port identifier)
|
||||
- port_id (port identifier)
|
||||
- mappings (VCs/VPs mapped to the port)
|
||||
- nio (nio type, one of the following)
|
||||
- "NIO_UDP"
|
||||
- nio (one of the following)
|
||||
- type "nio_udp"
|
||||
- lport (local port)
|
||||
- rhost (remote host)
|
||||
- rport (remote port)
|
||||
- "NIO_GenericEthernet"
|
||||
- type "nio_generic_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_LinuxEthernet"
|
||||
- type "nio_linux_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_TAP"
|
||||
- type "nio_tap"
|
||||
- tap_device (TAP device name e.g. tap0)
|
||||
- "NIO_UNIX"
|
||||
- type "nio_unix"
|
||||
- local_file (path to UNIX socket file)
|
||||
- remote_file (path to UNIX socket file)
|
||||
- "NIO_VDE"
|
||||
- type "nio_vde"
|
||||
- control_file (path to VDE control file)
|
||||
- local_file (path to VDE local file)
|
||||
- "NIO_Null"
|
||||
- type "nio_null"
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- port_id (unique port identifier)
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ATMSW_ADD_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
atmsw_id = request["id"]
|
||||
if atmsw_id not in self._atm_switches:
|
||||
self.send_custom_error("ATM switch id {} doesn't exist".format(atmsw_id))
|
||||
# get the ATM switch instance
|
||||
atmsw = self.get_device_instance(request["id"], self._atm_switches)
|
||||
if not atmsw:
|
||||
return
|
||||
atmsw = self._atm_switches[atmsw_id]
|
||||
|
||||
port = request["port"]
|
||||
mappings = request["mappings"]
|
||||
@@ -261,8 +262,7 @@ class ATMSW(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
self.send_response({"port_id": request["port_id"]})
|
||||
|
||||
@IModule.route("dynamips.atmsw.delete_nio")
|
||||
def atmsw_delete_nio(self, request):
|
||||
@@ -274,24 +274,21 @@ class ATMSW(object):
|
||||
- port (port identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ATMSW_DELETE_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
atmsw_id = request["id"]
|
||||
if atmsw_id not in self._atm_switches:
|
||||
self.send_custom_error("ATM switch id {} doesn't exist".format(atmsw_id))
|
||||
# get the ATM switch instance
|
||||
atmsw = self.get_device_instance(request["id"], self._atm_switches)
|
||||
if not atmsw:
|
||||
return
|
||||
atmsw = self._atm_switches[atmsw_id]
|
||||
|
||||
port = request["port"]
|
||||
|
||||
try:
|
||||
for source, destination in atmsw.mapping.copy().items():
|
||||
if len(source) == 3 and len(destination) == 3:
|
||||
@@ -315,5 +312,84 @@ class ATMSW(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
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)
|
||||
@@ -15,10 +15,20 @@
|
||||
# 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
|
||||
|
||||
from ..schemas.ethhub import ETHHUB_CREATE_SCHEMA
|
||||
from ..schemas.ethhub import ETHHUB_DELETE_SCHEMA
|
||||
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__)
|
||||
|
||||
@@ -30,7 +40,7 @@ class ETHHUB(object):
|
||||
"""
|
||||
Creates a new Ethernet hub.
|
||||
|
||||
Optional request parameters:
|
||||
Mandatory request parameters:
|
||||
- name (hub name)
|
||||
|
||||
Response parameters:
|
||||
@@ -40,11 +50,11 @@ class ETHHUB(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
name = None
|
||||
if request and "name" in request:
|
||||
name = request["name"]
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHHUB_CREATE_SCHEMA):
|
||||
return
|
||||
|
||||
name = request["name"]
|
||||
try:
|
||||
if not self._hypervisor_manager:
|
||||
self.start_hypervisor_manager()
|
||||
@@ -70,22 +80,20 @@ class ETHHUB(object):
|
||||
- id (hub identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHHUB_DELETE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
# get the Ethernet hub instance
|
||||
ethhub_id = request["id"]
|
||||
if ethhub_id not in self._ethernet_hubs:
|
||||
self.send_custom_error("Ethernet hub id {} doesn't exist".format(ethhub_id))
|
||||
ethhub = self.get_device_instance(ethhub_id, self._ethernet_hubs)
|
||||
if not ethhub:
|
||||
return
|
||||
ethhub = self._ethernet_hubs[ethhub_id]
|
||||
|
||||
try:
|
||||
ethhub.delete()
|
||||
@@ -108,27 +116,26 @@ class ETHHUB(object):
|
||||
- name (new hub name)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- name if changed
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHHUB_UPDATE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
ethhub_id = request["id"]
|
||||
if ethhub_id not in self._ethernet_hubs:
|
||||
self.send_custom_error("Ethernet hub id {} doesn't exist".format(ethhub_id))
|
||||
# get the Ethernet hub instance
|
||||
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
|
||||
if not ethhub:
|
||||
return
|
||||
ethhub = self._ethernet_hubs[ethhub_id]
|
||||
|
||||
response = {}
|
||||
# rename the hub if requested
|
||||
if "name" in request and ethhub.name != request["name"]:
|
||||
try:
|
||||
ethhub.name = request["name"]
|
||||
response["name"] = ethhub.name
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
@@ -152,17 +159,14 @@ class ETHHUB(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHHUB_ALLOCATE_UDP_PORT_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
ethhub_id = request["id"]
|
||||
if ethhub_id not in self._ethernet_hubs:
|
||||
self.send_custom_error("Ethernet hub id {} doesn't exist".format(ethhub_id))
|
||||
# get the Ethernet hub instance
|
||||
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
|
||||
if not ethhub:
|
||||
return
|
||||
ethhub = self._ethernet_hubs[ethhub_id]
|
||||
|
||||
try:
|
||||
# allocate a new UDP port
|
||||
@@ -183,44 +187,41 @@ class ETHHUB(object):
|
||||
- id (hub identifier)
|
||||
- port (port identifier)
|
||||
- port_id (port identifier)
|
||||
- nio (nio type, one of the following)
|
||||
- "NIO_UDP"
|
||||
- nio (one of the following)
|
||||
- type "nio_udp"
|
||||
- lport (local port)
|
||||
- rhost (remote host)
|
||||
- rport (remote port)
|
||||
- "NIO_GenericEthernet"
|
||||
- type "nio_generic_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_LinuxEthernet"
|
||||
- type "nio_linux_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_TAP"
|
||||
- type "nio_tap"
|
||||
- tap_device (TAP device name e.g. tap0)
|
||||
- "NIO_UNIX"
|
||||
- type "nio_unix"
|
||||
- local_file (path to UNIX socket file)
|
||||
- remote_file (path to UNIX socket file)
|
||||
- "NIO_VDE"
|
||||
- type "nio_vde"
|
||||
- control_file (path to VDE control file)
|
||||
- local_file (path to VDE local file)
|
||||
- "NIO_Null"
|
||||
- type "nio_null"
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- port_id (unique port identifier)
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHHUB_ADD_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
ethhub_id = request["id"]
|
||||
if ethhub_id not in self._ethernet_hubs:
|
||||
self.send_custom_error("Ethernet hub id {} doesn't exist".format(ethhub_id))
|
||||
# get the Ethernet hub instance
|
||||
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
|
||||
if not ethhub:
|
||||
return
|
||||
ethhub = self._ethernet_hubs[ethhub_id]
|
||||
|
||||
port = request["port"]
|
||||
|
||||
try:
|
||||
nio = self.create_nio(ethhub, request)
|
||||
if not nio:
|
||||
@@ -235,11 +236,10 @@ class ETHHUB(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
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).
|
||||
|
||||
@@ -248,24 +248,21 @@ class ETHHUB(object):
|
||||
- port (port identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHHUB_DELETE_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
ethhub_id = request["id"]
|
||||
if ethhub_id not in self._ethernet_hubs:
|
||||
self.send_custom_error("Ethernet hub id {} doesn't exist".format(ethhub_id))
|
||||
# get the Ethernet hub instance
|
||||
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
|
||||
if not ethhub:
|
||||
return
|
||||
ethhub = self._ethernet_hubs[ethhub_id]
|
||||
|
||||
port = request["port"]
|
||||
|
||||
try:
|
||||
nio = ethhub.remove_nio(port)
|
||||
nio.delete()
|
||||
@@ -273,5 +270,84 @@ class ETHHUB(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
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)
|
||||
@@ -15,10 +15,20 @@
|
||||
# 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
|
||||
|
||||
from ..schemas.ethsw import ETHSW_CREATE_SCHEMA
|
||||
from ..schemas.ethsw import ETHSW_DELETE_SCHEMA
|
||||
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__)
|
||||
|
||||
@@ -30,7 +40,7 @@ class ETHSW(object):
|
||||
"""
|
||||
Creates a new Ethernet switch.
|
||||
|
||||
Optional request parameters:
|
||||
Mandatory request parameters:
|
||||
- name (switch name)
|
||||
|
||||
Response parameters:
|
||||
@@ -40,11 +50,11 @@ class ETHSW(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
name = None
|
||||
if request and "name" in request:
|
||||
name = request["name"]
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHSW_CREATE_SCHEMA):
|
||||
return
|
||||
|
||||
name = request["name"]
|
||||
try:
|
||||
if not self._hypervisor_manager:
|
||||
self.start_hypervisor_manager()
|
||||
@@ -70,22 +80,20 @@ class ETHSW(object):
|
||||
- id (switch identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHSW_DELETE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
# get the Ethernet switch instance
|
||||
ethsw_id = request["id"]
|
||||
if ethsw_id not in self._ethernet_switches:
|
||||
self.send_custom_error("Ethernet switch id {} doesn't exist".format(ethsw_id))
|
||||
ethsw = self.get_device_instance(ethsw_id, self._ethernet_switches)
|
||||
if not ethsw:
|
||||
return
|
||||
ethsw = self._ethernet_switches[ethsw_id]
|
||||
|
||||
try:
|
||||
ethsw.delete()
|
||||
@@ -94,7 +102,7 @@ class ETHSW(object):
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("dynamips.ethsw.update")
|
||||
def ethsw_update(self, request):
|
||||
@@ -109,22 +117,19 @@ class ETHSW(object):
|
||||
- ports (ports settings)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- name if changed
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHSW_UPDATE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
ethsw_id = request["id"]
|
||||
if ethsw_id not in self._ethernet_switches:
|
||||
self.send_custom_error("Ethernet switch id {} doesn't exist".format(ethsw_id))
|
||||
# get the Ethernet switch instance
|
||||
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
|
||||
if not ethsw:
|
||||
return
|
||||
ethsw = self._ethernet_switches[ethsw_id]
|
||||
|
||||
if "ports" in request:
|
||||
ports = request["ports"]
|
||||
@@ -144,15 +149,17 @@ class ETHSW(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
response = {}
|
||||
# rename the switch if requested
|
||||
if "name" in request and ethsw.name != request["name"]:
|
||||
try:
|
||||
ethsw.name = request["name"]
|
||||
response["name"] = ethsw.name
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
self.send_response(request)
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("dynamips.ethsw.allocate_udp_port")
|
||||
def ethsw_allocate_udp_port(self, request):
|
||||
@@ -171,17 +178,14 @@ class ETHSW(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHSW_ALLOCATE_UDP_PORT_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
ethsw_id = request["id"]
|
||||
if ethsw_id not in self._ethernet_switches:
|
||||
self.send_custom_error("Ethernet switch id {} doesn't exist".format(ethsw_id))
|
||||
# get the Ethernet switch instance
|
||||
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
|
||||
if not ethsw:
|
||||
return
|
||||
ethsw = self._ethernet_switches[ethsw_id]
|
||||
|
||||
try:
|
||||
# allocate a new UDP port
|
||||
@@ -204,42 +208,39 @@ class ETHSW(object):
|
||||
- port_id (port identifier)
|
||||
- vlan (vlan identifier)
|
||||
- port_type ("access", "dot1q" or "qinq")
|
||||
- nio (nio type, one of the following)
|
||||
- "NIO_UDP"
|
||||
- nio (one of the following)
|
||||
- type "nio_udp"
|
||||
- lport (local port)
|
||||
- rhost (remote host)
|
||||
- rport (remote port)
|
||||
- "NIO_GenericEthernet"
|
||||
- type "nio_generic_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_LinuxEthernet"
|
||||
- type "nio_linux_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_TAP"
|
||||
- type "nio_tap"
|
||||
- tap_device (TAP device name e.g. tap0)
|
||||
- "NIO_UNIX"
|
||||
- type "nio_unix"
|
||||
- local_file (path to UNIX socket file)
|
||||
- remote_file (path to UNIX socket file)
|
||||
- "NIO_VDE"
|
||||
- type "nio_vde"
|
||||
- control_file (path to VDE control file)
|
||||
- local_file (path to VDE local file)
|
||||
- "NIO_Null"
|
||||
- type "nio_null"
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- port_id (unique port identifier)
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHSW_ADD_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
ethsw_id = request["id"]
|
||||
if ethsw_id not in self._ethernet_switches:
|
||||
self.send_custom_error("Ethernet switch id {} doesn't exist".format(ethsw_id))
|
||||
# get the Ethernet switch instance
|
||||
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
|
||||
if not ethsw:
|
||||
return
|
||||
ethsw = self._ethernet_switches[ethsw_id]
|
||||
|
||||
port = request["port"]
|
||||
vlan = request["vlan"]
|
||||
@@ -264,8 +265,7 @@ class ETHSW(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
self.send_response({"port_id": request["port_id"]})
|
||||
|
||||
@IModule.route("dynamips.ethsw.delete_nio")
|
||||
def ethsw_delete_nio(self, request):
|
||||
@@ -277,24 +277,21 @@ class ETHSW(object):
|
||||
- port (port identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, ETHSW_DELETE_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
ethsw_id = request["id"]
|
||||
if ethsw_id not in self._ethernet_switches:
|
||||
self.send_custom_error("Ethernet switch id {} doesn't exist".format(ethsw_id))
|
||||
# get the Ethernet switch instance
|
||||
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
|
||||
if not ethsw:
|
||||
return
|
||||
ethsw = self._ethernet_switches[ethsw_id]
|
||||
|
||||
port = request["port"]
|
||||
|
||||
try:
|
||||
nio = ethsw.remove_nio(port)
|
||||
nio.delete()
|
||||
@@ -302,5 +299,84 @@ class ETHSW(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
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)
|
||||
@@ -15,10 +15,20 @@
|
||||
# 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
|
||||
|
||||
from ..schemas.frsw import FRSW_CREATE_SCHEMA
|
||||
from ..schemas.frsw import FRSW_DELETE_SCHEMA
|
||||
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__)
|
||||
|
||||
@@ -30,7 +40,7 @@ class FRSW(object):
|
||||
"""
|
||||
Creates a new Frame-Relay switch.
|
||||
|
||||
Optional request parameters:
|
||||
Mandatory request parameters:
|
||||
- name (switch name)
|
||||
|
||||
Response parameters:
|
||||
@@ -40,11 +50,11 @@ class FRSW(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
name = None
|
||||
if request and "name" in request:
|
||||
name = request["name"]
|
||||
# validate the request
|
||||
if not self.validate_request(request, FRSW_CREATE_SCHEMA):
|
||||
return
|
||||
|
||||
name = request["name"]
|
||||
try:
|
||||
if not self._hypervisor_manager:
|
||||
self.start_hypervisor_manager()
|
||||
@@ -70,22 +80,20 @@ class FRSW(object):
|
||||
- id (switch identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, FRSW_DELETE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
# get the Frame relay switch instance
|
||||
frsw_id = request["id"]
|
||||
if frsw_id not in self._frame_relay_switches:
|
||||
self.send_custom_error("Frame relay switch id {} doesn't exist".format(frsw_id))
|
||||
frsw = self.get_device_instance(frsw_id, self._frame_relay_switches)
|
||||
if not frsw:
|
||||
return
|
||||
frsw = self._frame_relay_switches[frsw_id]
|
||||
|
||||
try:
|
||||
frsw.delete()
|
||||
@@ -94,7 +102,8 @@ class FRSW(object):
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("dynamips.frsw.update")
|
||||
def frsw_update(self, request):
|
||||
@@ -108,30 +117,30 @@ class FRSW(object):
|
||||
- name (new switch name)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- name if updated
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, FRSW_UPDATE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
frsw_id = request["id"]
|
||||
if frsw_id not in self._frame_relay_switches:
|
||||
self.send_custom_error("Frame relay switch id {} doesn't exist".format(frsw_id))
|
||||
# get the Frame relay switch instance
|
||||
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
|
||||
if not frsw:
|
||||
return
|
||||
frsw = self._frame_relay_switches[frsw_id]
|
||||
|
||||
response = {}
|
||||
# rename the switch if requested
|
||||
if "name" in request and frsw.name != request["name"]:
|
||||
try:
|
||||
frsw.name = request["name"]
|
||||
response["name"] = frsw.name
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
self.send_response(request)
|
||||
|
||||
@IModule.route("dynamips.frsw.allocate_udp_port")
|
||||
@@ -151,17 +160,14 @@ class FRSW(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, FRSW_ALLOCATE_UDP_PORT_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
frsw_id = request["id"]
|
||||
if frsw_id not in self._frame_relay_switches:
|
||||
self.send_custom_error("Frame relay switch id {} doesn't exist".format(frsw_id))
|
||||
# get the Frame relay switch instance
|
||||
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
|
||||
if not frsw:
|
||||
return
|
||||
frsw = self._frame_relay_switches[frsw_id]
|
||||
|
||||
try:
|
||||
# allocate a new UDP port
|
||||
@@ -183,42 +189,39 @@ class FRSW(object):
|
||||
- port (port identifier)
|
||||
- port_id (port identifier)
|
||||
- mappings (VCs mapped to the port)
|
||||
- nio (nio type, one of the following)
|
||||
- "NIO_UDP"
|
||||
- nio (one of the following)
|
||||
- type "nio_udp"
|
||||
- lport (local port)
|
||||
- rhost (remote host)
|
||||
- rport (remote port)
|
||||
- "NIO_GenericEthernet"
|
||||
- type "nio_generic_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_LinuxEthernet"
|
||||
- type "nio_linux_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_TAP"
|
||||
- type "nio_tap"
|
||||
- tap_device (TAP device name e.g. tap0)
|
||||
- "NIO_UNIX"
|
||||
- type "nio_unix"
|
||||
- local_file (path to UNIX socket file)
|
||||
- remote_file (path to UNIX socket file)
|
||||
- "NIO_VDE"
|
||||
- type "nio_vde"
|
||||
- control_file (path to VDE control file)
|
||||
- local_file (path to VDE local file)
|
||||
- "NIO_Null"
|
||||
- type "nio_null"
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- port_id (unique port identifier)
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, FRSW_ADD_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
frsw_id = request["id"]
|
||||
if frsw_id not in self._frame_relay_switches:
|
||||
self.send_custom_error("Frame relay switch id {} doesn't exist".format(frsw_id))
|
||||
# get the Frame relay switch instance
|
||||
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
|
||||
if not frsw:
|
||||
return
|
||||
frsw = self._frame_relay_switches[frsw_id]
|
||||
|
||||
port = request["port"]
|
||||
mappings = request["mappings"]
|
||||
@@ -246,8 +249,7 @@ class FRSW(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
self.send_response({"port_id": request["port_id"]})
|
||||
|
||||
@IModule.route("dynamips.frsw.delete_nio")
|
||||
def frsw_delete_nio(self, request):
|
||||
@@ -259,24 +261,21 @@ class FRSW(object):
|
||||
- port (port identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, FRSW_DELETE_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
frsw_id = request["id"]
|
||||
if frsw_id not in self._frame_relay_switches:
|
||||
self.send_custom_error("Frame relay switch id {} doesn't exist".format(frsw_id))
|
||||
# get the Frame relay switch instance
|
||||
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
|
||||
if not frsw:
|
||||
return
|
||||
frsw = self._frame_relay_switches[frsw_id]
|
||||
|
||||
port = request["port"]
|
||||
|
||||
try:
|
||||
# remove the VCs mapped with this port/nio
|
||||
for source, destination in frsw.mapping.copy().items():
|
||||
@@ -292,5 +291,84 @@ class FRSW(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
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)
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import os
|
||||
import base64
|
||||
import time
|
||||
from gns3server.modules import IModule
|
||||
from ..dynamips_error import DynamipsError
|
||||
|
||||
@@ -49,6 +50,23 @@ from ..adapters.wic_1enet import WIC_1ENET
|
||||
from ..adapters.wic_1t import WIC_1T
|
||||
from ..adapters.wic_2t import WIC_2T
|
||||
|
||||
from ..schemas.vm import VM_CREATE_SCHEMA
|
||||
from ..schemas.vm import VM_DELETE_SCHEMA
|
||||
from ..schemas.vm import VM_START_SCHEMA
|
||||
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
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -92,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)
|
||||
@@ -110,38 +128,40 @@ class VM(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_CREATE_SCHEMA):
|
||||
return
|
||||
|
||||
log.debug("received request {}".format(request))
|
||||
|
||||
#TODO: JSON schema validation
|
||||
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:
|
||||
|
||||
if platform not in PLATFORMS:
|
||||
raise DynamipsError("Unknown router platform: {}".format(platform))
|
||||
|
||||
if not self._hypervisor_manager:
|
||||
self.start_hypervisor_manager()
|
||||
|
||||
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"]
|
||||
@@ -197,31 +217,30 @@ class VM(object):
|
||||
- id (vm identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_DELETE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
# get the router instance
|
||||
router_id = request["id"]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
router = self.get_device_instance(router_id, self._routers)
|
||||
if not router:
|
||||
return
|
||||
router = self._routers[router_id]
|
||||
|
||||
try:
|
||||
router.delete()
|
||||
router.clean_delete()
|
||||
self._hypervisor_manager.unallocate_hypervisor_for_router(router)
|
||||
del self._routers[router_id]
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("dynamips.vm.start")
|
||||
def vm_start(self, request):
|
||||
@@ -232,29 +251,26 @@ class VM(object):
|
||||
- id (vm identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_START_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
router_id = request["id"]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
# get the router instance
|
||||
router = self.get_device_instance(request["id"], self._routers)
|
||||
if not router:
|
||||
return
|
||||
router = self._routers[router_id]
|
||||
|
||||
try:
|
||||
router.start()
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("dynamips.vm.stop")
|
||||
def vm_stop(self, request):
|
||||
@@ -265,29 +281,27 @@ class VM(object):
|
||||
- id (vm identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_STOP_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
router_id = request["id"]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
# get the router instance
|
||||
router = self.get_device_instance(request["id"], self._routers)
|
||||
if not router:
|
||||
return
|
||||
router = self._routers[router_id]
|
||||
|
||||
try:
|
||||
router.stop()
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("dynamips.vm.suspend")
|
||||
def vm_suspend(self, request):
|
||||
@@ -298,29 +312,27 @@ class VM(object):
|
||||
- id (vm identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_SUSPEND_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
router_id = request["id"]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
# get the router instance
|
||||
router = self.get_device_instance(request["id"], self._routers)
|
||||
if not router:
|
||||
return
|
||||
router = self._routers[router_id]
|
||||
|
||||
try:
|
||||
router.suspend()
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("dynamips.vm.reload")
|
||||
def vm_reload(self, request):
|
||||
@@ -331,22 +343,20 @@ class VM(object):
|
||||
- id (vm identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_RELOAD_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
router_id = request["id"]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
# get the router instance
|
||||
router = self.get_device_instance(request["id"], self._routers)
|
||||
if not router:
|
||||
return
|
||||
router = self._routers[router_id]
|
||||
|
||||
try:
|
||||
if router.get_status() != "inactive":
|
||||
router.stop()
|
||||
@@ -354,7 +364,8 @@ class VM(object):
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("dynamips.vm.update")
|
||||
def vm_update(self, request):
|
||||
@@ -370,37 +381,52 @@ class VM(object):
|
||||
- private_config_base64 (private-config base64 encoded)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- updated settings
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_UPDATE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
router_id = request["id"]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
# get the router instance
|
||||
router = self.get_device_instance(request["id"], self._routers)
|
||||
if not router:
|
||||
return
|
||||
router = self._routers[router_id]
|
||||
|
||||
response = {}
|
||||
try:
|
||||
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:
|
||||
config_filename = "{}.cfg".format(router.name)
|
||||
request["startup_config"] = self.save_base64config(request["startup_config_base64"], router, config_filename)
|
||||
if "startup_config" in request:
|
||||
router.set_config(request["startup_config"])
|
||||
# update the request with the new local startup-config path
|
||||
request["startup_config"] = self.create_config_from_base64(request["startup_config_base64"], router, startup_config_path)
|
||||
|
||||
# a new private-config has been pushed
|
||||
if "private_config_base64" in request:
|
||||
config_filename = "{}-private.cfg".format(router.name)
|
||||
request["private_config"] = self.save_base64config(request["private_config_base64"], router, config_filename)
|
||||
# update the request with the new local private-config path
|
||||
request["private_config"] = self.create_config_from_base64(request["private_config_base64"], router, private_config_path)
|
||||
|
||||
if "startup_config" in request:
|
||||
if os.path.isfile(request["startup_config"]) and request["startup_config"] != startup_config_path:
|
||||
# this is a local file set in the GUI
|
||||
request["startup_config"] = self.create_config_from_file(request["startup_config"], router, startup_config_path)
|
||||
router.set_config(request["startup_config"])
|
||||
else:
|
||||
router.set_config(request["startup_config"])
|
||||
response["startup_config"] = request["startup_config"]
|
||||
|
||||
if "private_config" in request:
|
||||
router.set_config(router.startup_config, request["private_config"])
|
||||
if os.path.isfile(request["private_config"]) and request["private_config"] != private_config_path:
|
||||
# this is a local file set in the GUI
|
||||
request["private_config"] = self.create_config_from_file(request["private_config"], router, private_config_path)
|
||||
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"]
|
||||
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
@@ -411,6 +437,7 @@ class VM(object):
|
||||
if hasattr(router, name) and getattr(router, name) != value:
|
||||
try:
|
||||
setattr(router, name, value)
|
||||
response[name] = value
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
@@ -422,6 +449,7 @@ class VM(object):
|
||||
if router.slots[slot_id] and type(router.slots[slot_id]) != type(adapter):
|
||||
router.slot_remove_binding(slot_id)
|
||||
router.slot_add_binding(slot_id, adapter)
|
||||
response[name] = value
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
@@ -430,6 +458,7 @@ class VM(object):
|
||||
if router.slots[slot_id]:
|
||||
try:
|
||||
router.slot_remove_binding(slot_id)
|
||||
response[name] = value
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
@@ -441,6 +470,7 @@ class VM(object):
|
||||
if router.slots[0].wics[wic_slot_id] and type(router.slots[0].wics[wic_slot_id]) != type(wic):
|
||||
router.uninstall_wic(wic_slot_id)
|
||||
router.install_wic(wic_slot_id, wic)
|
||||
response[name] = value
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
@@ -449,16 +479,104 @@ class VM(object):
|
||||
if router.slots[0].wics and router.slots[0].wics[wic_slot_id]:
|
||||
try:
|
||||
router.uninstall_wic(wic_slot_id)
|
||||
response[name] = value
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# Update the ghost IOS file in case the RAM size has changed
|
||||
if self._hypervisor_manager.ghost_ios_support:
|
||||
self.set_ghost_ios(router)
|
||||
try:
|
||||
self.set_ghost_ios(router)
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
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):
|
||||
@@ -469,17 +587,14 @@ class VM(object):
|
||||
- id (vm identifier)
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_SAVE_CONFIG_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
router_id = request["id"]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
# get the router instance
|
||||
router = self.get_device_instance(request["id"], self._routers)
|
||||
if not router:
|
||||
return
|
||||
router = self._routers[router_id]
|
||||
|
||||
try:
|
||||
if router.startup_config or router.private_config:
|
||||
@@ -510,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):
|
||||
"""
|
||||
@@ -528,17 +682,14 @@ class VM(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_IDLEPCS_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
router_id = request["id"]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
# get the router instance
|
||||
router = self.get_device_instance(request["id"], self._routers)
|
||||
if not router:
|
||||
return
|
||||
router = self._routers[router_id]
|
||||
|
||||
try:
|
||||
if "compute" in request and request["compute"] == False:
|
||||
@@ -551,10 +702,80 @@ class VM(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
response = {"id": router_id,
|
||||
response = {"id": router.id,
|
||||
"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):
|
||||
"""
|
||||
@@ -571,17 +792,14 @@ class VM(object):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_ALLOCATE_UDP_PORT_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
router_id = request["id"]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
# get the router instance
|
||||
router = self.get_device_instance(request["id"], self._routers)
|
||||
if not router:
|
||||
return
|
||||
router = self._routers[router_id]
|
||||
|
||||
try:
|
||||
# allocate a new UDP port
|
||||
@@ -603,46 +821,42 @@ class VM(object):
|
||||
- slot (slot number)
|
||||
- port (port number)
|
||||
- port_id (unique port identifier)
|
||||
- nio (nio type, one of the following)
|
||||
- "NIO_UDP"
|
||||
- nio (one of the following)
|
||||
- type "nio_udp"
|
||||
- lport (local port)
|
||||
- rhost (remote host)
|
||||
- rport (remote port)
|
||||
- "NIO_GenericEthernet"
|
||||
- type "nio_generic_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_LinuxEthernet"
|
||||
- type "nio_linux_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_TAP"
|
||||
- type "nio_tap"
|
||||
- tap_device (TAP device name e.g. tap0)
|
||||
- "NIO_UNIX"
|
||||
- type "nio_unix"
|
||||
- local_file (path to UNIX socket file)
|
||||
- remote_file (path to UNIX socket file)
|
||||
- "NIO_VDE"
|
||||
- type "nio_vde"
|
||||
- control_file (path to VDE control file)
|
||||
- local_file (path to VDE local file)
|
||||
- "NIO_Null"
|
||||
- type "nio_null"
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- port_id (unique port identifier)
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_ADD_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
router_id = request["id"]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
# get the router instance
|
||||
router = self.get_device_instance(request["id"], self._routers)
|
||||
if not router:
|
||||
return
|
||||
router = self._routers[router_id]
|
||||
|
||||
slot = request["slot"]
|
||||
port = request["port"]
|
||||
|
||||
try:
|
||||
nio = self.create_nio(router, request)
|
||||
if not nio:
|
||||
@@ -657,8 +871,7 @@ class VM(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
self.send_response({"port_id": request["port_id"]})
|
||||
|
||||
@IModule.route("dynamips.vm.delete_nio")
|
||||
def vm_delete_nio(self, request):
|
||||
@@ -671,25 +884,22 @@ class VM(object):
|
||||
- port (port identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, VM_DELETE_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
router_id = request["id"]
|
||||
router = self._routers[router_id]
|
||||
if router_id not in self._routers:
|
||||
self.send_custom_error("IOS router id {} doesn't exist".format(router_id))
|
||||
# get the router instance
|
||||
router = self.get_device_instance(request["id"], self._routers)
|
||||
if not router:
|
||||
return
|
||||
|
||||
slot = request["slot"]
|
||||
port = request["port"]
|
||||
|
||||
try:
|
||||
nio = router.slot_remove_nio_binding(slot, port)
|
||||
nio.delete()
|
||||
@@ -697,5 +907,4 @@ class VM(object):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
self.send_response(True)
|
||||
@@ -25,6 +25,8 @@ class DynamipsError(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
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L46
|
||||
"""
|
||||
|
||||
import socket
|
||||
import errno
|
||||
import re
|
||||
import logging
|
||||
from .dynamips_error import DynamipsError
|
||||
from .nios.nio_udp_auto import NIO_UDP_auto
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -53,12 +53,15 @@ class DynamipsHypervisor(object):
|
||||
self._ghosts = {}
|
||||
self._jitsharing_groups = {}
|
||||
self._working_dir = working_dir
|
||||
self._baseconsole = 2000
|
||||
self._baseaux = 2100
|
||||
self._baseudp = 10000
|
||||
self._current_udp_port = self._baseudp
|
||||
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._nio_udp_auto_instances = {}
|
||||
self._version = "N/A"
|
||||
self._timeout = 30
|
||||
self._timeout = timeout
|
||||
self._socket = None
|
||||
self._uuid = None
|
||||
|
||||
@@ -77,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))
|
||||
|
||||
@@ -90,6 +91,9 @@ class DynamipsHypervisor(object):
|
||||
|
||||
self._uuid = self.send("hypervisor uuid")
|
||||
|
||||
# this forces to send the working dir to Dynamips
|
||||
self.working_dir = self._working_dir
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""
|
||||
@@ -125,6 +129,7 @@ class DynamipsHypervisor(object):
|
||||
"""
|
||||
|
||||
self.send("hypervisor close")
|
||||
self._socket.shutdown(socket.SHUT_RDWR)
|
||||
self._socket.close()
|
||||
self._socket = None
|
||||
|
||||
@@ -134,8 +139,10 @@ class DynamipsHypervisor(object):
|
||||
"""
|
||||
|
||||
self.send("hypervisor stop")
|
||||
self._socket.shutdown(socket.SHUT_RDWR)
|
||||
self._socket.close()
|
||||
self._socket = None
|
||||
self._nio_udp_auto_instances.clear()
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
@@ -143,6 +150,7 @@ class DynamipsHypervisor(object):
|
||||
"""
|
||||
|
||||
self.send('hypervisor reset')
|
||||
self._nio_udp_auto_instances.clear()
|
||||
|
||||
@property
|
||||
def working_dir(self):
|
||||
@@ -220,68 +228,124 @@ class DynamipsHypervisor(object):
|
||||
self._devices = devices
|
||||
|
||||
@property
|
||||
def baseconsole(self):
|
||||
def console_start_port_range(self):
|
||||
"""
|
||||
Returns base console TCP port for this hypervisor.
|
||||
Returns the console start port range value
|
||||
|
||||
:returns: base console value (integer)
|
||||
:returns: console start port range value (integer)
|
||||
"""
|
||||
|
||||
return self._baseconsole
|
||||
return self._console_start_port_range
|
||||
|
||||
@baseconsole.setter
|
||||
def baseconsole(self, baseconsole):
|
||||
@console_start_port_range.setter
|
||||
def console_start_port_range(self, console_start_port_range):
|
||||
"""
|
||||
Sets the base console TCP port for this hypervisor.
|
||||
Set a new console start port range value
|
||||
|
||||
:param baseconsole: base console value (integer)
|
||||
:param console_start_port_range: console start port range value (integer)
|
||||
"""
|
||||
|
||||
self._baseconsole = baseconsole
|
||||
self._console_start_port_range = console_start_port_range
|
||||
|
||||
@property
|
||||
def baseaux(self):
|
||||
def console_end_port_range(self):
|
||||
"""
|
||||
Returns base auxiliary port for this hypervisor.
|
||||
Returns the console end port range value
|
||||
|
||||
:returns: base auxiliary port value (integer)
|
||||
:returns: console end port range value (integer)
|
||||
"""
|
||||
|
||||
return self._baseaux
|
||||
return self._console_end_port_range
|
||||
|
||||
@baseaux.setter
|
||||
def baseaux(self, baseaux):
|
||||
@console_end_port_range.setter
|
||||
def console_end_port_range(self, console_end_port_range):
|
||||
"""
|
||||
Sets the base auxiliary TCP port for this hypervisor.
|
||||
Set a new console end port range value
|
||||
|
||||
:param baseaux: base auxiliary port value (integer)
|
||||
:param console_end_port_range: console end port range value (integer)
|
||||
"""
|
||||
|
||||
self._baseaux = baseaux
|
||||
self._console_end_port_range = console_end_port_range
|
||||
|
||||
@property
|
||||
def baseudp(self):
|
||||
def aux_start_port_range(self):
|
||||
"""
|
||||
Returns the next available UDP port for UDP NIOs.
|
||||
Returns the auxiliary console start port range value
|
||||
|
||||
:returns: base UDP port value (integer)
|
||||
:returns: auxiliary console start port range value (integer)
|
||||
"""
|
||||
|
||||
return self._baseudp
|
||||
return self._aux_start_port_range
|
||||
|
||||
@baseudp.setter
|
||||
def baseudp(self, baseudp):
|
||||
@aux_start_port_range.setter
|
||||
def aux_start_port_range(self, aux_start_port_range):
|
||||
"""
|
||||
Sets the next open UDP port for NIOs for this hypervisor.
|
||||
Sets a new auxiliary console start port range value
|
||||
|
||||
:param baseudp: base UDP port value (integer)
|
||||
:param aux_start_port_range: auxiliary console start port range value (integer)
|
||||
"""
|
||||
|
||||
self._baseudp = baseudp
|
||||
self._current_udp_port = self._baseudp
|
||||
self._aux_start_port_range = aux_start_port_range
|
||||
|
||||
#FIXME
|
||||
log.info("hypervisor a new base UDP {}".format(self._baseudp))
|
||||
@property
|
||||
def aux_end_port_range(self):
|
||||
"""
|
||||
Returns the auxiliary console end port range value
|
||||
|
||||
:returns: auxiliary console end port range value (integer)
|
||||
"""
|
||||
|
||||
return self._aux_end_port_range
|
||||
|
||||
@aux_end_port_range.setter
|
||||
def aux_end_port_range(self, aux_end_port_range):
|
||||
"""
|
||||
Sets a new auxiliary console end port range value
|
||||
|
||||
:param aux_end_port_range: auxiliary console end port range value (integer)
|
||||
"""
|
||||
|
||||
self._aux_end_port_range = aux_end_port_range
|
||||
|
||||
@property
|
||||
def udp_start_port_range(self):
|
||||
"""
|
||||
Returns the UDP start port range value
|
||||
|
||||
:returns: UDP start port range value (integer)
|
||||
"""
|
||||
|
||||
return self._udp_start_port_range
|
||||
|
||||
@udp_start_port_range.setter
|
||||
def udp_start_port_range(self, udp_start_port_range):
|
||||
"""
|
||||
Sets a new UDP start port range value
|
||||
|
||||
:param udp_start_port_range: UDP start port range value (integer)
|
||||
"""
|
||||
|
||||
self._udp_start_port_range = udp_start_port_range
|
||||
|
||||
@property
|
||||
def udp_end_port_range(self):
|
||||
"""
|
||||
Returns the UDP end port range value
|
||||
|
||||
:returns: UDP end port range value (integer)
|
||||
"""
|
||||
|
||||
return self._udp_end_port_range
|
||||
|
||||
@udp_end_port_range.setter
|
||||
def udp_end_port_range(self, udp_end_port_range):
|
||||
"""
|
||||
Sets an new UDP end port range value
|
||||
|
||||
:param udp_end_port_range: UDP end port range value (integer)
|
||||
"""
|
||||
|
||||
self._udp_end_port_range = udp_end_port_range
|
||||
|
||||
@property
|
||||
def ghosts(self):
|
||||
@@ -343,58 +407,29 @@ class DynamipsHypervisor(object):
|
||||
|
||||
return self._port
|
||||
|
||||
@staticmethod
|
||||
def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP"):
|
||||
def get_nio_udp_auto(self, port):
|
||||
"""
|
||||
Finds an unused port in a range.
|
||||
Returns an allocated NIO UDP auto instance.
|
||||
|
||||
:param start_port: first port in the range
|
||||
:param end_port: last port in the range
|
||||
:param host: host/address for bind()
|
||||
:param socket_type: TCP (default) or UDP
|
||||
:returns: NIO UDP auto instance
|
||||
"""
|
||||
|
||||
if socket_type == "UDP":
|
||||
socket_type = socket.SOCK_DGRAM
|
||||
if port in self._nio_udp_auto_instances:
|
||||
return self._nio_udp_auto_instances.pop(port)
|
||||
else:
|
||||
socket_type = socket.SOCK_STREAM
|
||||
return None
|
||||
|
||||
for port in range(start_port, end_port):
|
||||
try:
|
||||
if ":" in host:
|
||||
# IPv6 address support
|
||||
with socket.socket(socket.AF_INET6, socket_type) as s:
|
||||
s.bind((host, port)) # the port is available if bind is a success
|
||||
else:
|
||||
with socket.socket(socket.AF_INET, socket_type) as s:
|
||||
s.bind((host, port)) # the port is available if bind is a success
|
||||
return port
|
||||
except OSError as e:
|
||||
if e.errno == errno.EADDRINUSE: # socket already in use
|
||||
if port + 1 == end_port:
|
||||
raise DynamipsError("Could not find a free port between {0} and {1}".format(start_port, end_port))
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
raise DynamipsError("Could not find an unused port: {}".format(e))
|
||||
|
||||
def allocate_udp_port(self, max_port=100):
|
||||
def allocate_udp_port(self):
|
||||
"""
|
||||
Allocates a new UDP port for creating an UDP NIO.
|
||||
|
||||
:param max_port: maximum number of port to scan in
|
||||
order to find one available for use.
|
||||
Allocates a new UDP port for creating an UDP NIO Auto.
|
||||
|
||||
:returns: port number (integer)
|
||||
"""
|
||||
|
||||
start_port = self._current_udp_port
|
||||
end_port = start_port + max_port
|
||||
allocated_port = DynamipsHypervisor.find_unused_port(start_port, end_port, self._host, socket_type="UDP")
|
||||
if allocated_port - self._current_udp_port > 1:
|
||||
self._current_udp_port += allocated_port - self._current_udp_port
|
||||
else:
|
||||
self._current_udp_port += 1
|
||||
# use Dynamips's NIO UDP auto back-end.
|
||||
nio = NIO_UDP_auto(self, self._host, self._udp_start_port_range, self._udp_end_port_range)
|
||||
self._nio_udp_auto_instances[nio.lport] = nio
|
||||
allocated_port = nio.lport
|
||||
return allocated_port
|
||||
|
||||
def send_raw(self, string):
|
||||
@@ -440,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 = []
|
||||
@@ -451,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -276,8 +276,9 @@ class Hypervisor(DynamipsHypervisor):
|
||||
|
||||
command = [self._path]
|
||||
command.extend(["-N1"]) # use instance IDs for filenames
|
||||
command.extend(["-l", "dynamips_log_{}.txt".format(self._port)]) # log file
|
||||
if self._host != "0.0.0.0" and self._host != "::":
|
||||
command.extend(["-H", self._host + ":" + str(self._port)])
|
||||
command.extend(["-H", "{}:{}".format(self._host, self._port)])
|
||||
else:
|
||||
command.extend(["-H", str(self._port)])
|
||||
return command
|
||||
@@ -19,12 +19,14 @@
|
||||
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
|
||||
from ..attic import wait_socket_is_ready
|
||||
from pkg_resources import parse_version
|
||||
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
import logging
|
||||
|
||||
@@ -38,39 +40,32 @@ 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',
|
||||
base_hypervisor_port=7200,
|
||||
base_console_port=2000,
|
||||
base_aux_port=3000,
|
||||
base_udp_port=10000):
|
||||
def __init__(self, path, working_dir, host='127.0.0.1'):
|
||||
|
||||
self._hypervisors = []
|
||||
self._path = path
|
||||
self._working_dir = working_dir
|
||||
self._host = host
|
||||
self._base_hypervisor_port = base_hypervisor_port
|
||||
self._current_port = self._base_hypervisor_port
|
||||
self._base_console_port = base_console_port
|
||||
self._base_aux_port = base_aux_port
|
||||
self._base_udp_port = base_udp_port
|
||||
self._current_base_udp_port = self._base_udp_port
|
||||
self._udp_incrementation_per_hypervisor = 100
|
||||
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):
|
||||
"""
|
||||
@@ -136,94 +131,204 @@ class HypervisorManager(object):
|
||||
hypervisor.working_dir = self._working_dir
|
||||
|
||||
@property
|
||||
def base_hypervisor_port(self):
|
||||
def hypervisor_start_port_range(self):
|
||||
"""
|
||||
Returns the base hypervisor port.
|
||||
Returns the hypervisor start port range value
|
||||
|
||||
:returns: base hypervisor port (integer)
|
||||
:returns: hypervisor start port range value (integer)
|
||||
"""
|
||||
|
||||
return self._base_hypervisor_port
|
||||
return self._hypervisor_start_port_range
|
||||
|
||||
@base_hypervisor_port.setter
|
||||
def base_hypervisor_port(self, base_hypervisor_port):
|
||||
@hypervisor_start_port_range.setter
|
||||
def hypervisor_start_port_range(self, hypervisor_start_port_range):
|
||||
"""
|
||||
Set a new base hypervisor port.
|
||||
Sets a new hypervisor start port range value
|
||||
|
||||
:param base_hypervisor_port: base hypervisor port (integer)
|
||||
:param hypervisor_start_port_range: hypervisor start port range value (integer)
|
||||
"""
|
||||
|
||||
if self._base_hypervisor_port != base_hypervisor_port:
|
||||
self._base_hypervisor_port = base_hypervisor_port
|
||||
self._current_port = self._base_hypervisor_port
|
||||
log.info("base hypervisor port set to {}".format(self._base_hypervisor_port))
|
||||
if self._hypervisor_start_port_range != hypervisor_start_port_range:
|
||||
self._hypervisor_start_port_range = hypervisor_start_port_range
|
||||
log.info("hypervisor start port range value set to {}".format(self._hypervisor_start_port_range))
|
||||
|
||||
@property
|
||||
def base_console_port(self):
|
||||
def hypervisor_end_port_range(self):
|
||||
"""
|
||||
Returns the base console port.
|
||||
Returns the hypervisor end port range value
|
||||
|
||||
:returns: base console port (integer)
|
||||
:returns: hypervisor end port range value (integer)
|
||||
"""
|
||||
|
||||
return self._base_console_port
|
||||
return self._hypervisor_end_port_range
|
||||
|
||||
@base_console_port.setter
|
||||
def base_console_port(self, base_console_port):
|
||||
@hypervisor_end_port_range.setter
|
||||
def hypervisor_end_port_range(self, hypervisor_end_port_range):
|
||||
"""
|
||||
Set a new base console port.
|
||||
Sets a new hypervisor end port range value
|
||||
|
||||
:param base_console_port: base console port (integer)
|
||||
:param hypervisor_end_port_range: hypervisor end port range value (integer)
|
||||
"""
|
||||
|
||||
if self._base_console_port != base_console_port:
|
||||
self._base_console_port = base_console_port
|
||||
log.info("base console port set to {}".format(self._base_console_port))
|
||||
if self._hypervisor_end_port_range != hypervisor_end_port_range:
|
||||
self._hypervisor_end_port_range = hypervisor_end_port_range
|
||||
log.info("hypervisor end port range value set to {}".format(self._hypervisor_end_port_range))
|
||||
|
||||
@property
|
||||
def base_aux_port(self):
|
||||
def console_start_port_range(self):
|
||||
"""
|
||||
Returns the base auxiliary console port.
|
||||
Returns the console start port range value
|
||||
|
||||
:returns: base auxiliary console port (integer)
|
||||
:returns: console start port range value (integer)
|
||||
"""
|
||||
|
||||
return self._base_aux_port
|
||||
return self._console_start_port_range
|
||||
|
||||
@base_aux_port.setter
|
||||
def base_aux_port(self, base_aux_port):
|
||||
@console_start_port_range.setter
|
||||
def console_start_port_range(self, console_start_port_range):
|
||||
"""
|
||||
Set a new base auxiliary console port.
|
||||
Sets a new console start port range value
|
||||
|
||||
:param base_aux_port: base auxiliary console port (integer)
|
||||
:param console_start_port_range: console start port range value (integer)
|
||||
"""
|
||||
|
||||
if self._base_aux_port != base_aux_port:
|
||||
self._base_aux_port = base_aux_port
|
||||
log.info("base aux port set to {}".format(self._base_aux_port))
|
||||
if self._console_start_port_range != console_start_port_range:
|
||||
self._console_start_port_range = console_start_port_range
|
||||
log.info("console start port range value set to {}".format(self._console_start_port_range))
|
||||
|
||||
# update all existing hypervisors with the new value
|
||||
for hypervisor in self._hypervisors:
|
||||
hypervisor.console_start_port_range = console_start_port_range
|
||||
|
||||
@property
|
||||
def base_udp_port(self):
|
||||
def console_end_port_range(self):
|
||||
"""
|
||||
Returns the base UDP port.
|
||||
Returns the console end port range value
|
||||
|
||||
:returns: base UDP port (integer)
|
||||
:returns: console end port range value (integer)
|
||||
"""
|
||||
|
||||
return self._base_udp_port
|
||||
return self._console_end_port_range
|
||||
|
||||
@base_udp_port.setter
|
||||
def base_udp_port(self, base_udp_port):
|
||||
@console_end_port_range.setter
|
||||
def console_end_port_range(self, console_end_port_range):
|
||||
"""
|
||||
Set a new base UDP port.
|
||||
Sets a new console end port range value
|
||||
|
||||
:param base_udp_port: base UDP port (integer)
|
||||
:param console_end_port_range: console end port range value (integer)
|
||||
"""
|
||||
|
||||
if self._base_udp_port != base_udp_port:
|
||||
self._base_udp_port = base_udp_port
|
||||
self._current_base_udp_port = self._base_udp_port
|
||||
log.info("base UDP port set to {}".format(self._base_udp_port))
|
||||
if self._console_end_port_range != console_end_port_range:
|
||||
self._console_end_port_range = console_end_port_range
|
||||
log.info("console end port range value set to {}".format(self._console_end_port_range))
|
||||
|
||||
# update all existing hypervisors with the new value
|
||||
for hypervisor in self._hypervisors:
|
||||
hypervisor.console_end_port_range = console_end_port_range
|
||||
|
||||
@property
|
||||
def aux_start_port_range(self):
|
||||
"""
|
||||
Returns the auxiliary console start port range value
|
||||
|
||||
:returns: auxiliary console start port range value (integer)
|
||||
"""
|
||||
|
||||
return self._aux_start_port_range
|
||||
|
||||
@aux_start_port_range.setter
|
||||
def aux_start_port_range(self, aux_start_port_range):
|
||||
"""
|
||||
Sets a new auxiliary console start port range value
|
||||
|
||||
:param aux_start_port_range: auxiliary console start port range value (integer)
|
||||
"""
|
||||
|
||||
if self._aux_start_port_range != aux_start_port_range:
|
||||
self._aux_start_port_range = aux_start_port_range
|
||||
log.info("auxiliary console start port range value set to {}".format(self._aux_start_port_range))
|
||||
|
||||
# update all existing hypervisors with the new value
|
||||
for hypervisor in self._hypervisors:
|
||||
hypervisor.aux_start_port_range = aux_start_port_range
|
||||
|
||||
@property
|
||||
def aux_end_port_range(self):
|
||||
"""
|
||||
Returns the auxiliary console end port range value
|
||||
|
||||
:returns: auxiliary console end port range value (integer)
|
||||
"""
|
||||
|
||||
return self._aux_end_port_range
|
||||
|
||||
@aux_end_port_range.setter
|
||||
def aux_end_port_range(self, aux_end_port_range):
|
||||
"""
|
||||
Sets a new auxiliary console end port range value
|
||||
|
||||
:param aux_end_port_range: auxiliary console end port range value (integer)
|
||||
"""
|
||||
|
||||
if self._aux_end_port_range != aux_end_port_range:
|
||||
self._aux_end_port_range = aux_end_port_range
|
||||
log.info("auxiliary console end port range value set to {}".format(self._aux_end_port_range))
|
||||
|
||||
# update all existing hypervisors with the new value
|
||||
for hypervisor in self._hypervisors:
|
||||
hypervisor.aux_end_port_range = aux_end_port_range
|
||||
|
||||
@property
|
||||
def udp_start_port_range(self):
|
||||
"""
|
||||
Returns the UDP start port range value
|
||||
|
||||
:returns: UDP start port range value (integer)
|
||||
"""
|
||||
|
||||
return self._udp_start_port_range
|
||||
|
||||
@udp_start_port_range.setter
|
||||
def udp_start_port_range(self, udp_start_port_range):
|
||||
"""
|
||||
Sets a new UDP start port range value
|
||||
|
||||
:param udp_start_port_range: UDP start port range value (integer)
|
||||
"""
|
||||
|
||||
if self._udp_start_port_range != udp_start_port_range:
|
||||
self._udp_start_port_range = udp_start_port_range
|
||||
log.info("UDP start port range value set to {}".format(self._udp_start_port_range))
|
||||
|
||||
# update all existing hypervisors with the new value
|
||||
for hypervisor in self._hypervisors:
|
||||
hypervisor.udp_start_port_range = udp_start_port_range
|
||||
|
||||
@property
|
||||
def udp_end_port_range(self):
|
||||
"""
|
||||
Returns the UDP end port range value
|
||||
|
||||
:returns: UDP end port range value (integer)
|
||||
"""
|
||||
|
||||
return self._udp_end_port_range
|
||||
|
||||
@udp_end_port_range.setter
|
||||
def udp_end_port_range(self, udp_end_port_range):
|
||||
"""
|
||||
Sets a new UDP end port range value
|
||||
|
||||
:param udp_end_port_range: UDP end port range value (integer)
|
||||
"""
|
||||
|
||||
if self._udp_end_port_range != udp_end_port_range:
|
||||
self._udp_end_port_range = udp_end_port_range
|
||||
log.info("UDP end port range value set to {}".format(self._udp_end_port_range))
|
||||
|
||||
# update all existing hypervisors with the new value
|
||||
for hypervisor in self._hypervisors:
|
||||
hypervisor.udp_end_port_range = udp_end_port_range
|
||||
|
||||
@property
|
||||
def ghost_ios_support(self):
|
||||
@@ -399,35 +504,17 @@ 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)
|
||||
"""
|
||||
|
||||
# connect to a local address by default
|
||||
# if listening to all addresses (IPv4 or IPv6)
|
||||
if host == "0.0.0.0":
|
||||
host = "127.0.0.1"
|
||||
elif host == "::":
|
||||
host = "::1"
|
||||
|
||||
connection_success = False
|
||||
begin = time.time()
|
||||
# try to connect for 10 seconds
|
||||
while(time.time() - begin < 10.0):
|
||||
time.sleep(0.01)
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout):
|
||||
pass
|
||||
except OSError as e:
|
||||
last_exception = e
|
||||
continue
|
||||
connection_success = True
|
||||
break
|
||||
# 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:
|
||||
# FIXME: throw exception here
|
||||
@@ -436,25 +523,6 @@ class HypervisorManager(object):
|
||||
else:
|
||||
log.info("Dynamips server ready after {:.4f} seconds".format(time.time() - begin))
|
||||
|
||||
def allocate_tcp_port(self, max_port=100):
|
||||
"""
|
||||
Allocates a new TCP port for a Dynamips hypervisor.
|
||||
|
||||
:param max_port: maximum number of port to scan in
|
||||
order to find one available for use.
|
||||
|
||||
:returns: port number (integer)
|
||||
"""
|
||||
|
||||
start_port = self._current_port
|
||||
end_port = start_port + max_port
|
||||
allocated_port = Hypervisor.find_unused_port(start_port, end_port, self._host)
|
||||
if allocated_port - self._current_port > 1:
|
||||
self._current_port += allocated_port - self._current_port
|
||||
else:
|
||||
self._current_port += 1
|
||||
return allocated_port
|
||||
|
||||
def start_new_hypervisor(self):
|
||||
"""
|
||||
Creates a new Dynamips process and start it.
|
||||
@@ -462,13 +530,17 @@ class HypervisorManager(object):
|
||||
:returns: the new hypervisor instance
|
||||
"""
|
||||
|
||||
port = self.allocate_tcp_port()
|
||||
try:
|
||||
port = find_unused_port(self._hypervisor_start_port_range, self._hypervisor_end_port_range, self._host)
|
||||
except Exception as e:
|
||||
raise DynamipsError(e)
|
||||
|
||||
hypervisor = Hypervisor(self._path,
|
||||
self._working_dir,
|
||||
self._host,
|
||||
port)
|
||||
|
||||
log.info("creating new hypervisor {}:{}".format(hypervisor.host, hypervisor.port))
|
||||
log.info("creating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, self._working_dir))
|
||||
hypervisor.start()
|
||||
|
||||
self.wait_for_hypervisor(self._host, port)
|
||||
@@ -477,10 +549,13 @@ class HypervisorManager(object):
|
||||
hypervisor.connect()
|
||||
if parse_version(hypervisor.version) < parse_version('0.2.11'):
|
||||
raise DynamipsError("Dynamips version must be >= 0.2.11, detected version is {}".format(hypervisor.version))
|
||||
hypervisor.baseconsole = self._base_console_port
|
||||
hypervisor.baseaux = self._base_aux_port
|
||||
hypervisor.baseudp = self._current_base_udp_port
|
||||
self._current_base_udp_port += self._udp_incrementation_per_hypervisor
|
||||
|
||||
hypervisor.console_start_port_range = self._console_start_port_range
|
||||
hypervisor.console_end_port_range = self._console_end_port_range
|
||||
hypervisor.aux_start_port_range = self._aux_start_port_range
|
||||
hypervisor.aux_end_port_range = self._aux_end_port_range
|
||||
hypervisor.udp_start_port_range = self._udp_start_port_range
|
||||
hypervisor.udp_end_port_range = self._udp_end_port_range
|
||||
self._hypervisors.append(hypervisor)
|
||||
return hypervisor
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -20,8 +20,9 @@ Interface for Dynamips virtual Machine module ("vm")
|
||||
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L77
|
||||
"""
|
||||
|
||||
from ..dynamips_hypervisor import DynamipsHypervisor
|
||||
from ..dynamips_error import DynamipsError
|
||||
from ...attic import find_unused_port
|
||||
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
@@ -36,34 +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 = []
|
||||
_instance_count = 1
|
||||
_instances = []
|
||||
_allocated_console_ports = []
|
||||
_allocated_aux_ports = []
|
||||
_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):
|
||||
|
||||
# create an unique ID
|
||||
self._id = Router._instance_count
|
||||
Router._instance_count += 1
|
||||
if not ghost_flag:
|
||||
|
||||
# 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:
|
||||
break
|
||||
name_id += 1
|
||||
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
|
||||
|
||||
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
|
||||
@@ -103,27 +115,45 @@ class Router(object):
|
||||
platform=platform,
|
||||
id=self._id))
|
||||
|
||||
# get console and aux ports
|
||||
self.console = (self._hypervisor.baseconsole - 1) + self._id
|
||||
self.aux = (self._hypervisor.baseaux - 1) + self._id
|
||||
try:
|
||||
# allocate a console port
|
||||
self._console = find_unused_port(self._hypervisor.console_start_port_range,
|
||||
self._hypervisor.console_end_port_range,
|
||||
self._hypervisor.host,
|
||||
ignore_ports=self._allocated_console_ports)
|
||||
|
||||
self._hypervisor.send("vm set_con_tcp_port {name} {console}".format(name=self._name,
|
||||
console=self._console))
|
||||
self._allocated_console_ports.append(self._console)
|
||||
|
||||
# allocate a auxiliary console port
|
||||
self._aux = find_unused_port(self._hypervisor.aux_start_port_range,
|
||||
self._hypervisor.aux_end_port_range,
|
||||
self._hypervisor.host,
|
||||
ignore_ports=self._allocated_aux_ports)
|
||||
|
||||
self._hypervisor.send("vm set_aux_tcp_port {name} {aux}".format(name=self._name,
|
||||
aux=self._aux))
|
||||
|
||||
self._allocated_aux_ports.append(self._aux)
|
||||
except Exception as e:
|
||||
raise DynamipsError(e)
|
||||
|
||||
# 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]
|
||||
else:
|
||||
log.info("creating a new ghost IOS file")
|
||||
Router._instance_count -= 1
|
||||
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()
|
||||
|
||||
def defaults(self):
|
||||
"""
|
||||
@@ -195,10 +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", "i{}_startup-config.cfg".format(self.id))
|
||||
if os.path.isfile(startup_config_path):
|
||||
try:
|
||||
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)
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e))
|
||||
|
||||
if self._private_config:
|
||||
# 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+", errors="replace") as f:
|
||||
old_config = f.read()
|
||||
new_config = old_config.replace(self.name, new_name)
|
||||
f.seek(0)
|
||||
f.write(new_config)
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e))
|
||||
|
||||
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))
|
||||
@@ -206,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):
|
||||
@@ -255,11 +304,43 @@ class Router(object):
|
||||
Deletes this router.
|
||||
"""
|
||||
|
||||
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))
|
||||
if self._id in self._instances:
|
||||
self._instances.remove(self._id)
|
||||
if self.console:
|
||||
self._allocated_console_ports.remove(self.console)
|
||||
if self.aux:
|
||||
self._allocated_aux_ports.remove(self.aux)
|
||||
|
||||
def clean_delete(self):
|
||||
"""
|
||||
Deletes this router & associated files (nvram, disks etc.)
|
||||
"""
|
||||
|
||||
self._hypervisor.send("vm clean_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._startup_config:
|
||||
# delete the startup-config
|
||||
startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}.cfg".format(self.name))
|
||||
if os.path.isfile(startup_config_path):
|
||||
os.remove(startup_config_path)
|
||||
|
||||
if self._private_config:
|
||||
# delete the private-config
|
||||
private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}-private.cfg".format(self.name))
|
||||
if os.path.isfile(private_config_path):
|
||||
os.remove(private_config_path)
|
||||
|
||||
log.info("router {name} [id={id}] has been deleted (including associated files)".format(name=self._name, id=self._id))
|
||||
if self._id in self._instances:
|
||||
self._instances.remove(self._id)
|
||||
if self.console:
|
||||
self._allocated_console_ports.remove(self.console)
|
||||
if self.aux:
|
||||
self._allocated_aux_ports.remove(self.aux)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
@@ -267,23 +348,24 @@ class Router(object):
|
||||
At least the IOS image must be set before starting it.
|
||||
"""
|
||||
|
||||
if self.get_status() == "suspended":
|
||||
status = self.get_status()
|
||||
if status == "suspended":
|
||||
self.resume()
|
||||
else:
|
||||
elif status == "inactive":
|
||||
|
||||
if self.console and self.aux:
|
||||
# check that console and aux ports are available
|
||||
try:
|
||||
#FIXME: use a defined range
|
||||
DynamipsHypervisor.find_unused_port(self.console, self.console + 100, self._hypervisor.host)
|
||||
except DynamipsError:
|
||||
raise DynamipsError("console port {} is not available".format(self.console))
|
||||
if not os.path.isfile(self._image):
|
||||
raise DynamipsError("IOS image '{}' is not accessible".format(self._image))
|
||||
|
||||
try:
|
||||
#FIXME: use a defined range
|
||||
DynamipsHypervisor.find_unused_port(self.aux, self.aux + 100, self._hypervisor.host)
|
||||
except DynamipsError:
|
||||
raise DynamipsError("aux port {} is not available".format(self.aux))
|
||||
try:
|
||||
with open(self._image, "rb") as f:
|
||||
# read the first 7 bytes of the file.
|
||||
elf_header_start = f.read(7)
|
||||
except OSError as e:
|
||||
raise DynamipsError("Cannot read ELF header for IOS image {}: {}".format(self._image, e))
|
||||
|
||||
# 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 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))
|
||||
@@ -294,8 +376,9 @@ class Router(object):
|
||||
The settings are kept.
|
||||
"""
|
||||
|
||||
self._hypervisor.send("vm stop {}".format(self._name))
|
||||
log.info("router {name} [id={id}] has been stopped".format(name=self._name, id=self._id))
|
||||
if self.get_status() != "inactive":
|
||||
self._hypervisor.send("vm stop {}".format(self._name))
|
||||
log.info("router {name} [id={id}] has been stopped".format(name=self._name, id=self._id))
|
||||
|
||||
def suspend(self):
|
||||
"""
|
||||
@@ -356,8 +439,11 @@ class Router(object):
|
||||
if not self._image:
|
||||
raise DynamipsError("Register an IOS image fist")
|
||||
|
||||
self._hypervisor.send("vm set_tsg {name} {group_id}".format(name=self._name,
|
||||
group_id=group_id))
|
||||
try:
|
||||
self._hypervisor.send("vm set_tsg {name} {group_id}".format(name=self._name,
|
||||
group_id=group_id))
|
||||
except DynamipsError:
|
||||
raise DynamipsError("JIT sharing is only supported in Dynamips >= 0.2.8-RC3 unstable")
|
||||
|
||||
log.info("router {name} [id={id}]: set in JIT sharing group {group_id}".format(name=self._name,
|
||||
id=self._id,
|
||||
@@ -486,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)'):
|
||||
"""
|
||||
@@ -632,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,
|
||||
@@ -688,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,
|
||||
@@ -892,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,
|
||||
@@ -1007,6 +1096,9 @@ class Router(object):
|
||||
if console == self._console:
|
||||
return
|
||||
|
||||
if console in self._allocated_console_ports:
|
||||
raise DynamipsError("Console port {} is already used by another router".format(console))
|
||||
|
||||
self._hypervisor.send("vm set_con_tcp_port {name} {console}".format(name=self._name,
|
||||
console=console))
|
||||
|
||||
@@ -1014,7 +1106,9 @@ class Router(object):
|
||||
id=self._id,
|
||||
old_console=self._console,
|
||||
new_console=console))
|
||||
self._allocated_console_ports.remove(self._console)
|
||||
self._console = console
|
||||
self._allocated_console_ports.append(self._console)
|
||||
|
||||
@property
|
||||
def aux(self):
|
||||
@@ -1037,6 +1131,9 @@ class Router(object):
|
||||
if aux == self._aux:
|
||||
return
|
||||
|
||||
if aux in self._allocated_aux_ports:
|
||||
raise DynamipsError("Auxiliary console port {} is already used by another router".format(aux))
|
||||
|
||||
self._hypervisor.send("vm set_aux_tcp_port {name} {aux}".format(name=self._name,
|
||||
aux=aux))
|
||||
|
||||
@@ -1044,7 +1141,10 @@ class Router(object):
|
||||
id=self._id,
|
||||
old_aux=self._aux,
|
||||
new_aux=aux))
|
||||
|
||||
self._allocated_aux_ports.remove(self._aux)
|
||||
self._aux = aux
|
||||
self._allocated_aux_ports.append(self._aux)
|
||||
|
||||
def get_cpu_info(self, cpu_id=0):
|
||||
"""
|
||||
@@ -1155,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.
|
||||
@@ -1162,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):
|
||||
"""
|
||||
@@ -1178,12 +1287,19 @@ 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')):
|
||||
raise DynamipsError("Adapter {adapter} cannot be added while router {name} is running".format(adapter=adapter,
|
||||
name=self._name))
|
||||
|
||||
self._hypervisor.send("vm slot_add_binding {name} {slot_id} 0 {adapter}".format(name=self._name,
|
||||
slot_id=slot_id,
|
||||
adapter=adapter))
|
||||
@@ -1195,11 +1311,8 @@ class Router(object):
|
||||
|
||||
self._slots[slot_id] = adapter
|
||||
|
||||
# Generate an OIR event if the router is running and
|
||||
# only for c7200, c3600 and c3745 (NM-4T only)
|
||||
if self.is_running() and (self._platform == 'c7200' \
|
||||
or (self._platform == 'c3600' and self.chassis == '3660') \
|
||||
or (self._platform == 'c3745' and adapter == 'NM-4T')):
|
||||
# Generate an OIR event if the router is running
|
||||
if self.is_running():
|
||||
|
||||
self._hypervisor.send("vm slot_oir_start {name} {slot_id} 0".format(name=self._name,
|
||||
slot_id=slot_id))
|
||||
@@ -1221,17 +1334,19 @@ 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))
|
||||
|
||||
#FIXME: check if adapter can be removed!
|
||||
# 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')):
|
||||
raise DynamipsError("Adapter {adapter} cannot be removed while router {name} is running".format(adapter=adapter,
|
||||
name=self._name))
|
||||
|
||||
# Generate an OIR event if the router is running and
|
||||
# only for c7200, c3600 and c3745 (NM-4T only)
|
||||
if self.is_running() and (self._platform == 'c7200' \
|
||||
or (self._platform == 'c3600' and self.chassis == '3660') \
|
||||
or (self._platform == 'c3745' and adapter == 'NM-4T')):
|
||||
# Generate an OIR event if the router is running
|
||||
if self.is_running():
|
||||
|
||||
self._hypervisor.send("vm slot_oir_stop {name} {slot_id} 0".format(name=self._name,
|
||||
slot_id=slot_id))
|
||||
@@ -1312,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,
|
||||
@@ -1436,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.
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,322 @@
|
||||
# -*- 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/>.
|
||||
|
||||
ATMSW_CREATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to create a new ATM switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "ATM switch name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["name"]
|
||||
}
|
||||
|
||||
ATMSW_DELETE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete an ATM switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "ATM switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
ATMSW_UPDATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to update an ATM switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "ATM switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "ATM switch name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
ATMSW_ALLOCATE_UDP_PORT_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to allocate an UDP port for an ATM switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "ATM switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port_id": {
|
||||
"description": "Unique port identifier for the ATM switch instance",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id", "port_id"]
|
||||
}
|
||||
|
||||
ATMSW_ADD_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to add a NIO for an ATM switch 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": "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,
|
||||
},
|
||||
"mappings": {
|
||||
"type": "object",
|
||||
},
|
||||
"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", "port_id", "mappings", "nio"],
|
||||
}
|
||||
|
||||
ATMSW_DELETE_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete a NIO for an ATM switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "ATM switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port": {
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
},
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
# -*- 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/>.
|
||||
|
||||
ETHHUB_CREATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to create a new Ethernet hub instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Ethernet hub name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["name"]
|
||||
}
|
||||
|
||||
ETHHUB_DELETE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete an Ethernet hub instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Ethernet hub instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
ETHHUB_UPDATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to update an Ethernet hub instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Ethernet hub instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "Ethernet hub name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
ETHHUB_ALLOCATE_UDP_PORT_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to allocate an UDP port for an Ethernet hub instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Ethernet hub instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port_id": {
|
||||
"description": "Unique port identifier for the Ethernet hub instance",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id", "port_id"]
|
||||
}
|
||||
|
||||
ETHHUB_ADD_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to add a NIO for an Ethernet hub 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": "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,
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
|
||||
ETHHUB_DELETE_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete a NIO for an Ethernet hub instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Ethernet hub instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port": {
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
},
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
# -*- 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/>.
|
||||
|
||||
ETHSW_CREATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to create a new Ethernet switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Ethernet switch name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["name"]
|
||||
}
|
||||
|
||||
ETHSW_DELETE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete an Ethernet switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Ethernet switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
#TODO: ports {'1': {'vlan': 1, 'type': 'qinq'}
|
||||
ETHSW_UPDATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to update an Ethernet switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Ethernet switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "Ethernet switch name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
# "ports": {
|
||||
# "type": "object",
|
||||
# "properties": {
|
||||
# "type": {
|
||||
# "description": "Port type",
|
||||
# "enum": ["access", "dot1q", "qinq"],
|
||||
# },
|
||||
# "vlan": {
|
||||
# "description": "VLAN number",
|
||||
# "type": "integer",
|
||||
# "minimum": 1
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
},
|
||||
#"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
ETHSW_ALLOCATE_UDP_PORT_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to allocate an UDP port for an Ethernet switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Ethernet switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port_id": {
|
||||
"description": "Unique port identifier for the Ethernet switch instance",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id", "port_id"]
|
||||
}
|
||||
|
||||
ETHSW_ADD_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to add a NIO for an Ethernet switch 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": "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,
|
||||
},
|
||||
"port_type": {
|
||||
"description": "Port type",
|
||||
"enum": ["access", "dot1q", "qinq"],
|
||||
},
|
||||
"vlan": {
|
||||
"description": "VLAN number",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"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", "port_type", "vlan", "nio"],
|
||||
|
||||
"dependencies": {
|
||||
"port_type": ["vlan"],
|
||||
"vlan": ["port_type"]
|
||||
}
|
||||
}
|
||||
|
||||
ETHSW_DELETE_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete a NIO for an Ethernet switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Ethernet switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port": {
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
},
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
# -*- 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/>.
|
||||
|
||||
FRSW_CREATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to create a new Frame relay switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Frame relay switch name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["name"]
|
||||
}
|
||||
|
||||
FRSW_DELETE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete a Frame relay switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Frame relay switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
FRSW_UPDATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to update a Frame relay switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Frame relay switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "Frame relay switch name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
FRSW_ALLOCATE_UDP_PORT_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to allocate an UDP port for a Frame relay switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Frame relay switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port_id": {
|
||||
"description": "Unique port identifier for the Frame relay switch instance",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id", "port_id"]
|
||||
}
|
||||
|
||||
FRSW_ADD_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to add a NIO for a Frame relay switch 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": "Frame relay switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port_id": {
|
||||
"description": "Unique port identifier for the Frame relay switch instance",
|
||||
"type": "integer"
|
||||
},
|
||||
"port": {
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
},
|
||||
"mappings": {
|
||||
"type": "object",
|
||||
},
|
||||
"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", "port_id", "mappings", "nio"],
|
||||
}
|
||||
|
||||
FRSW_DELETE_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete a NIO for a Frame relay switch instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Frame relay switch instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port": {
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
},
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
# -*- 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/>.
|
||||
|
||||
VM_CREATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to create a new VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Router name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"router_id": {
|
||||
"description": "VM/router instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"platform": {
|
||||
"description": "router platform",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^c[0-9]{4}$"
|
||||
},
|
||||
"chassis": {
|
||||
"description": "router chassis model",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[0-9]{4}(XM)?$"
|
||||
},
|
||||
"image": {
|
||||
"description": "path to the IOS image file",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"ram": {
|
||||
"description": "amount of RAM in MB",
|
||||
"type": "integer"
|
||||
},
|
||||
"console": {
|
||||
"description": "console TCP port",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"aux": {
|
||||
"description": "auxiliary console TCP port",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"mac_addr": {
|
||||
"description": "base MAC address",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["name", "platform", "image", "ram"]
|
||||
}
|
||||
|
||||
VM_DELETE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete a VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
VM_START_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to start a VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
VM_STOP_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to stop a VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
VM_SUSPEND_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to suspend a VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
VM_RELOAD_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to reload a VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
#TODO: improve platform specific properties (dependencies?)
|
||||
VM_UPDATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to update a VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "Router name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"platform": {
|
||||
"description": "platform",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^c[0-9]{4}$"
|
||||
},
|
||||
"image": {
|
||||
"description": "path to the IOS image",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"startup_config": {
|
||||
"description": "path to the IOS startup configuration file",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"private_config": {
|
||||
"description": "path to the IOS private configuration file",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"ram": {
|
||||
"description": "amount of RAM in MB",
|
||||
"type": "integer"
|
||||
},
|
||||
"nvram": {
|
||||
"description": "amount of NVRAM in KB",
|
||||
"type": "integer"
|
||||
},
|
||||
"mmap": {
|
||||
"description": "MMAP feature",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sparsemem": {
|
||||
"description": "sparse memory feature",
|
||||
"type": "boolean"
|
||||
},
|
||||
"clock_divisor": {
|
||||
"description": "clock divisor",
|
||||
"type": "integer"
|
||||
},
|
||||
"idlepc": {
|
||||
"description": "idle-pc value",
|
||||
"type": "string",
|
||||
"pattern": "^(0x[0-9a-fA-F]+)?$"
|
||||
},
|
||||
"idlemax": {
|
||||
"description": "idlemax value",
|
||||
"type": "integer",
|
||||
},
|
||||
"idlesleep": {
|
||||
"description": "idlesleep value",
|
||||
"type": "integer",
|
||||
},
|
||||
"exec_area": {
|
||||
"description": "exec area value",
|
||||
"type": "integer",
|
||||
},
|
||||
"jit_sharing_group": {
|
||||
"description": "JIT sharing group",
|
||||
"type": "integer",
|
||||
},
|
||||
"disk0": {
|
||||
"description": "disk0 size in MB",
|
||||
"type": "integer"
|
||||
},
|
||||
"disk1": {
|
||||
"description": "disk1 size in MB",
|
||||
"type": "integer"
|
||||
},
|
||||
"confreg": {
|
||||
"description": "configuration register",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^0x[0-9a-fA-F]{4}$"
|
||||
},
|
||||
"console": {
|
||||
"description": "console TCP port",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"aux": {
|
||||
"description": "auxiliary console TCP port",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"mac_addr": {
|
||||
"description": "base MAC address",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
|
||||
},
|
||||
"system_id": {
|
||||
"description": "system ID",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"slot0": {
|
||||
"description": "Network module slot 0",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"slot1": {
|
||||
"description": "Network module slot 1",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"slot2": {
|
||||
"description": "Network module slot 2",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"slot3": {
|
||||
"description": "Network module slot 3",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"slot4": {
|
||||
"description": "Network module slot 4",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"slot5": {
|
||||
"description": "Network module slot 5",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"slot6": {
|
||||
"description": "Network module slot 6",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"wic0": {
|
||||
"description": "Network module WIC slot 0",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"wic1": {
|
||||
"description": "Network module WIC slot 0",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"wic2": {
|
||||
"description": "Network module WIC slot 0",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "null"}
|
||||
]
|
||||
},
|
||||
"startup_config_base64": {
|
||||
"description": "startup configuration base64 encoded",
|
||||
"type": "string"
|
||||
},
|
||||
"private_config_base64": {
|
||||
"description": "private configuration base64 encoded",
|
||||
"type": "string"
|
||||
},
|
||||
# C7200 properties
|
||||
"npe": {
|
||||
"description": "NPE model",
|
||||
"enum": ["npe-100",
|
||||
"npe-150",
|
||||
"npe-175",
|
||||
"npe-200",
|
||||
"npe-225",
|
||||
"npe-300",
|
||||
"npe-400",
|
||||
"npe-g2"]
|
||||
},
|
||||
"midplane": {
|
||||
"description": "Midplane model",
|
||||
"enum": ["std", "vxr"]
|
||||
},
|
||||
"sensors": {
|
||||
"description": "Temperature sensors",
|
||||
"type": "array"
|
||||
},
|
||||
"power_supplies": {
|
||||
"description": "Power supplies status",
|
||||
"type": "array"
|
||||
},
|
||||
# I/O memory property for all platforms but C7200
|
||||
"iomem": {
|
||||
"description": "I/O memory percentage",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"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",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"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",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"compute": {
|
||||
"description": "indicates to compute new idle-pc values",
|
||||
"type": "boolean"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"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",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port_id": {
|
||||
"description": "Unique port identifier for the VM instance",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id", "port_id"]
|
||||
}
|
||||
|
||||
VM_ADD_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to add a NIO for a 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": "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
|
||||
},
|
||||
"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", "slot", "port", "nio"]
|
||||
}
|
||||
|
||||
VM_DELETE_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete a NIO for a VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "VM instance ID",
|
||||
"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", "slot", "port"]
|
||||
}
|
||||
+349
-208
@@ -20,13 +20,11 @@ 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
|
||||
from .iou_device import IOUDevice
|
||||
@@ -34,7 +32,21 @@ from .iou_error import IOUError
|
||||
from .nios.nio_udp import NIO_UDP
|
||||
from .nios.nio_tap import NIO_TAP
|
||||
from .nios.nio_generic_ethernet import NIO_GenericEthernet
|
||||
import gns3server.jsonrpc as jsonrpc
|
||||
from ..attic import find_unused_port
|
||||
from ..attic import has_privileged_access
|
||||
|
||||
from .schemas import IOU_CREATE_SCHEMA
|
||||
from .schemas import IOU_DELETE_SCHEMA
|
||||
from .schemas import IOU_UPDATE_SCHEMA
|
||||
from .schemas import IOU_START_SCHEMA
|
||||
from .schemas import IOU_STOP_SCHEMA
|
||||
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__)
|
||||
@@ -54,20 +66,17 @@ 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):
|
||||
iouyap_in_cwd = os.path.join(os.getcwd(), "iouyap")
|
||||
if os.path.isfile(iouyap_in_cwd):
|
||||
self._iouyap = iouyap_in_cwd
|
||||
else:
|
||||
# look for iouyap if none is defined or accessible
|
||||
for path in os.environ["PATH"].split(":"):
|
||||
try:
|
||||
if "iouyap" in os.listdir(path) and os.access(os.path.join(path, "iouyap"), os.X_OK):
|
||||
self._iouyap = os.path.join(path, "iouyap")
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||
# look for iouyap in the current working directory and $PATH
|
||||
for path in paths:
|
||||
try:
|
||||
if "iouyap" in os.listdir(path) and os.access(os.path.join(path, "iouyap"), os.X_OK):
|
||||
self._iouyap = os.path.join(path, "iouyap")
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
if not self._iouyap:
|
||||
log.warning("iouyap binary couldn't be found!")
|
||||
@@ -77,13 +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._current_console_port = self._console_start_port_range
|
||||
self._udp_start_port_range = 30001
|
||||
self._udp_end_port_range = 40001
|
||||
self._current_udp_port = self._udp_start_port_range
|
||||
self._host = kwargs["host"]
|
||||
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 = 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
|
||||
@@ -93,18 +101,23 @@ class IOU(IModule):
|
||||
self._iou_callback = self.add_periodic_callback(self._check_iou_is_alive, 5000)
|
||||
self._iou_callback.start()
|
||||
|
||||
def stop(self):
|
||||
def stop(self, signum=None):
|
||||
"""
|
||||
Properly stops the module.
|
||||
|
||||
:param signum: signal number (if called by the signal handler)
|
||||
"""
|
||||
|
||||
self._iou_callback.stop()
|
||||
|
||||
# delete all IOU instances
|
||||
for iou_id in self._iou_instances:
|
||||
iou_instance = self._iou_instances[iou_id]
|
||||
iou_instance.delete()
|
||||
|
||||
IModule.stop(self) # this will stop the I/O loop
|
||||
self.delete_iourc_file()
|
||||
|
||||
IModule.stop(self, signum) # this will stop the I/O loop
|
||||
|
||||
def _check_iou_is_alive(self):
|
||||
"""
|
||||
@@ -132,12 +145,39 @@ class IOU(IModule):
|
||||
self.send_notification("{}.iouyap_stopped".format(self.name), notification)
|
||||
iou_instance.stop()
|
||||
|
||||
@IModule.route("iou.reset")
|
||||
def reset(self, request):
|
||||
def get_iou_instance(self, iou_id):
|
||||
"""
|
||||
Resets the module.
|
||||
Returns an IOU device instance.
|
||||
|
||||
:param request: JSON request
|
||||
:param iou_id: IOU device identifier
|
||||
|
||||
:returns: IOUDevice instance
|
||||
"""
|
||||
|
||||
if iou_id not in self._iou_instances:
|
||||
log.debug("IOU device ID {} doesn't exist".format(iou_id), exc_info=1)
|
||||
self.send_custom_error("IOU device ID {} doesn't exist".format(iou_id))
|
||||
return None
|
||||
return self._iou_instances[iou_id]
|
||||
|
||||
def delete_iourc_file(self):
|
||||
"""
|
||||
Deletes the IOURC file.
|
||||
"""
|
||||
|
||||
if self._iourc and os.path.isfile(self._iourc):
|
||||
try:
|
||||
log.info("deleting iourc file {}".format(self._iourc))
|
||||
os.remove(self._iourc)
|
||||
except OSError as e:
|
||||
log.warn("could not delete iourc file {}: {}".format(self._iourc, e))
|
||||
|
||||
@IModule.route("iou.reset")
|
||||
def reset(self, request=None):
|
||||
"""
|
||||
Resets the module (JSON-RPC notification).
|
||||
|
||||
:param request: JSON request (not used)
|
||||
"""
|
||||
|
||||
# delete all IOU instances
|
||||
@@ -149,16 +189,8 @@ class IOU(IModule):
|
||||
IOUDevice.reset()
|
||||
|
||||
self._iou_instances.clear()
|
||||
self._remote_server = False
|
||||
self._current_console_port = self._console_start_port_range
|
||||
self._current_udp_port = self._udp_start_port_range
|
||||
|
||||
if self._iourc and os.path.isfile(self._iourc):
|
||||
try:
|
||||
log.info("deleting iourc file {}".format(self._iourc))
|
||||
os.remove(self._iourc)
|
||||
except OSError as e:
|
||||
log.warn("could not delete iourc file {}: {}".format(self._iourc, e))
|
||||
self._allocated_udp_ports.clear()
|
||||
self.delete_iourc_file()
|
||||
|
||||
log.info("IOU module has been reset")
|
||||
|
||||
@@ -171,6 +203,7 @@ class IOU(IModule):
|
||||
- iourc (base64 encoded iourc file)
|
||||
|
||||
Optional request parameters:
|
||||
- iouyap (path to iouyap)
|
||||
- working_dir (path to a working directory)
|
||||
- project_name
|
||||
- console_start_port_range
|
||||
@@ -181,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:
|
||||
@@ -194,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"]
|
||||
@@ -221,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"]
|
||||
@@ -238,38 +271,44 @@ class IOU(IModule):
|
||||
"""
|
||||
Creates a new IOU instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- path (path to the IOU executable)
|
||||
|
||||
Optional request parameters:
|
||||
- name (IOU name)
|
||||
- path (path to the IOU executable)
|
||||
- console (IOU console port)
|
||||
|
||||
Response parameters:
|
||||
- id (IOU instance identifier)
|
||||
- name (IOU name)
|
||||
- default settings
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
name = None
|
||||
if request and "name" in request:
|
||||
name = request["name"]
|
||||
# validate the request
|
||||
if not self.validate_request(request, IOU_CREATE_SCHEMA):
|
||||
return
|
||||
|
||||
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(name,
|
||||
iou_path,
|
||||
self._working_dir,
|
||||
self._host,
|
||||
iou_id,
|
||||
console,
|
||||
self._console_start_port_range,
|
||||
self._console_end_port_range)
|
||||
|
||||
iou_instance = IOUDevice(iou_path, self._working_dir, host=self._host, name=name)
|
||||
# find a console port
|
||||
if self._current_console_port >= self._console_end_port_range:
|
||||
self._current_console_port = self._console_start_port_range
|
||||
iou_instance.console = IOUDevice.find_unused_port(self._current_console_port, self._console_end_port_range, self._host)
|
||||
self._current_console_port += 1
|
||||
except IOUError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
@@ -290,31 +329,29 @@ class IOU(IModule):
|
||||
Mandatory request parameters:
|
||||
- id (IOU instance identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
Response parameter:
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, IOU_DELETE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
iou_id = request["id"]
|
||||
if iou_id not in self._iou_instances:
|
||||
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
|
||||
# get the instance
|
||||
iou_instance = self.get_iou_instance(request["id"])
|
||||
if not iou_instance:
|
||||
return
|
||||
iou_instance = self._iou_instances[iou_id]
|
||||
|
||||
try:
|
||||
iou_instance.delete()
|
||||
del self._iou_instances[iou_id]
|
||||
iou_instance.clean_delete()
|
||||
del self._iou_instances[request["id"]]
|
||||
except IOUError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("iou.update")
|
||||
def iou_update(self, request):
|
||||
@@ -326,55 +363,69 @@ 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:
|
||||
- same as original request
|
||||
- updated settings
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, IOU_UPDATE_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
iou_id = request["id"]
|
||||
if iou_id not in self._iou_instances:
|
||||
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
|
||||
# get the instance
|
||||
iou_instance = self.get_iou_instance(request["id"])
|
||||
if not iou_instance:
|
||||
return
|
||||
iou_instance = self._iou_instances[iou_id]
|
||||
|
||||
config_path = os.path.join(iou_instance.working_dir, "initial-config.cfg")
|
||||
try:
|
||||
# a new startup-config has been pushed
|
||||
if "startup_config_base64" in request:
|
||||
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)
|
||||
config_path = os.path.join(iou_instance.working_dir, "startup-config")
|
||||
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))
|
||||
request["startup_config"] = os.path.basename(config_path)
|
||||
if "startup_config" in request:
|
||||
iou_instance.startup_config = request["startup_config"]
|
||||
# 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["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["initial_config"] = os.path.basename(config_path)
|
||||
except OSError as 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["initial_config"]))
|
||||
except IOUError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# update the IOU settings
|
||||
response = {}
|
||||
for name, value in request.items():
|
||||
if hasattr(iou_instance, name) and getattr(iou_instance, name) != value:
|
||||
try:
|
||||
setattr(iou_instance, name, value)
|
||||
response[name] = value
|
||||
except IOUError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
self.send_response(request)
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("iou.start")
|
||||
def vm_start(self, request):
|
||||
@@ -385,32 +436,28 @@ class IOU(IModule):
|
||||
- id (IOU instance identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, IOU_START_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
iou_id = request["id"]
|
||||
if iou_id not in self._iou_instances:
|
||||
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
|
||||
# get the instance
|
||||
iou_instance = self.get_iou_instance(request["id"])
|
||||
if not iou_instance:
|
||||
return
|
||||
iou_instance = self._iou_instances[iou_id]
|
||||
|
||||
try:
|
||||
log.debug("starting IOU with command: {}".format(iou_instance.command()))
|
||||
iou_instance.iouyap = self._iouyap
|
||||
iou_instance.iourc = self._iourc
|
||||
iou_instance.start()
|
||||
except IOUError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("iou.stop")
|
||||
def vm_stop(self, request):
|
||||
@@ -421,29 +468,26 @@ class IOU(IModule):
|
||||
- id (IOU instance identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, IOU_STOP_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
iou_id = request["id"]
|
||||
if iou_id not in self._iou_instances:
|
||||
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
|
||||
# get the instance
|
||||
iou_instance = self.get_iou_instance(request["id"])
|
||||
if not iou_instance:
|
||||
return
|
||||
iou_instance = self._iou_instances[iou_id]
|
||||
|
||||
try:
|
||||
iou_instance.stop()
|
||||
except IOUError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("iou.reload")
|
||||
def vm_reload(self, request):
|
||||
@@ -454,22 +498,19 @@ class IOU(IModule):
|
||||
- id (IOU identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, IOU_RELOAD_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
iou_id = request["id"]
|
||||
if iou_id not in self._iou_instances:
|
||||
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
|
||||
# get the instance
|
||||
iou_instance = self.get_iou_instance(request["id"])
|
||||
if not iou_instance:
|
||||
return
|
||||
iou_instance = self._iou_instances[iou_id]
|
||||
|
||||
try:
|
||||
if iou_instance.is_running():
|
||||
@@ -478,7 +519,7 @@ class IOU(IModule):
|
||||
except IOUError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(request)
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("iou.allocate_udp_port")
|
||||
def allocate_udp_port(self, request):
|
||||
@@ -496,37 +537,32 @@ class IOU(IModule):
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, IOU_ALLOCATE_UDP_PORT_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
iou_id = request["id"]
|
||||
if iou_id not in self._iou_instances:
|
||||
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
|
||||
# get the instance
|
||||
iou_instance = self.get_iou_instance(request["id"])
|
||||
if not iou_instance:
|
||||
return
|
||||
iou_instance = self._iou_instances[iou_id]
|
||||
|
||||
try:
|
||||
|
||||
# find a UDP port
|
||||
if self._current_udp_port >= self._udp_end_port_range:
|
||||
self._current_udp_port = self._udp_start_port_range
|
||||
port = IOUDevice.find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP")
|
||||
self._current_udp_port += 1
|
||||
|
||||
log.info("{} [id={}] has allocated UDP port {} with host {}".format(iou_instance .name,
|
||||
iou_instance .id,
|
||||
port,
|
||||
self._host))
|
||||
response = {"lport": port}
|
||||
|
||||
except IOUError as e:
|
||||
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
|
||||
|
||||
response["port_id"] = request["port_id"]
|
||||
self._allocated_udp_ports.append(port)
|
||||
log.info("{} [id={}] has allocated UDP port {} with host {}".format(iou_instance.name,
|
||||
iou_instance.id,
|
||||
port,
|
||||
self._host))
|
||||
response = {"lport": port,
|
||||
"port_id": request["port_id"]}
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("iou.add_nio")
|
||||
@@ -539,75 +575,58 @@ class IOU(IModule):
|
||||
- slot (slot number)
|
||||
- port (port number)
|
||||
- port_id (unique port identifier)
|
||||
- nio (nio type, one of the following)
|
||||
- "NIO_UDP"
|
||||
- nio (one of the following)
|
||||
- type "nio_udp"
|
||||
- lport (local port)
|
||||
- rhost (remote host)
|
||||
- rport (remote port)
|
||||
- "NIO_GenericEthernet"
|
||||
- type "nio_generic_ethernet"
|
||||
- ethernet_device (Ethernet device name e.g. eth0)
|
||||
- "NIO_TAP"
|
||||
- type "nio_tap"
|
||||
- tap_device (TAP device name e.g. tap0)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- port_id (unique port identifier)
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, IOU_ADD_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
iou_id = request["id"]
|
||||
if iou_id not in self._iou_instances:
|
||||
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
|
||||
# get the instance
|
||||
iou_instance = self.get_iou_instance(request["id"])
|
||||
if not iou_instance:
|
||||
return
|
||||
iou_instance = self._iou_instances[iou_id]
|
||||
|
||||
slot = request["slot"]
|
||||
port = request["port"]
|
||||
|
||||
try:
|
||||
nio = None
|
||||
if request["nio"] == "NIO_UDP":
|
||||
lport = request["lport"]
|
||||
rhost = request["rhost"]
|
||||
rport = request["rport"]
|
||||
nio = NIO_UDP(lport, rhost, rport)
|
||||
elif request["nio"] == "NIO_TAP":
|
||||
tap_device = request["tap_device"]
|
||||
|
||||
# check that we have access to the tap device
|
||||
TUNSETIFF = 0x400454ca
|
||||
IFF_TAP = 0x0002
|
||||
IFF_NO_PI = 0x1000
|
||||
if request["nio"]["type"] == "nio_udp":
|
||||
lport = request["nio"]["lport"]
|
||||
rhost = request["nio"]["rhost"]
|
||||
rport = request["nio"]["rport"]
|
||||
try:
|
||||
tun = os.open("/dev/net/tun", os.O_RDWR)
|
||||
#TODO: handle IPv6
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.connect((rhost, rport))
|
||||
except OSError as e:
|
||||
raise IOUError("Could not open /dev/net/tun: {}".format(e))
|
||||
ifr = struct.pack("16sH", tap_device.encode("utf-8"), IFF_TAP | IFF_NO_PI)
|
||||
try:
|
||||
fcntl.ioctl(tun, TUNSETIFF, ifr)
|
||||
os.close(tun)
|
||||
except IOError as e:
|
||||
raise IOUError("TAP NIO {}: {}".format(tap_device, e))
|
||||
|
||||
raise IOUError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||
nio = NIO_UDP(lport, rhost, rport)
|
||||
elif request["nio"]["type"] == "nio_tap":
|
||||
tap_device = request["nio"]["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"] == "NIO_GenericEthernet":
|
||||
ethernet_device = request["ethernet_device"]
|
||||
|
||||
# check that we have access to the Ethernet device
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW):
|
||||
pass
|
||||
except socket.error as e:
|
||||
raise IOUError("Generic Ethernet NIO {}: {}".format(ethernet_device, e))
|
||||
elif request["nio"]["type"] == "nio_generic_ethernet":
|
||||
ethernet_device = request["nio"]["ethernet_device"]
|
||||
if not has_privileged_access(self._iouyap):
|
||||
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, ethernet_device))
|
||||
nio = NIO_GenericEthernet(ethernet_device)
|
||||
if not nio:
|
||||
raise IOUError("Requested NIO doesn't exist or is not supported: {}".format(request["nio"]))
|
||||
raise IOUError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
|
||||
except IOUError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
@@ -618,8 +637,7 @@ class IOU(IModule):
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
self.send_response({"port_id": request["port_id"]})
|
||||
|
||||
@IModule.route("iou.delete_nio")
|
||||
def delete_nio(self, request):
|
||||
@@ -632,33 +650,156 @@ class IOU(IModule):
|
||||
- port (port identifier)
|
||||
|
||||
Response parameters:
|
||||
- same as original request
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request == None:
|
||||
self.send_param_error()
|
||||
# validate the request
|
||||
if not self.validate_request(request, IOU_DELETE_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
#TODO: JSON schema validation for the request
|
||||
log.debug("received request {}".format(request))
|
||||
iou_id = request["id"]
|
||||
iou_instance = self._iou_instances[iou_id]
|
||||
if iou_id not in self._iou_instances:
|
||||
self.send_custom_error("IOU device id {} doesn't exist".format(iou_id))
|
||||
# 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.slot_remove_nio_binding(slot, port)
|
||||
nio = iou_instance.slot_remove_nio_binding(slot, port)
|
||||
if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
|
||||
self._allocated_udp_ports.remove(nio.lport)
|
||||
except IOUError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
# for now send back the original request
|
||||
self.send_response(request)
|
||||
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):
|
||||
@@ -668,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))
|
||||
|
||||
@@ -21,13 +21,14 @@ order to run an IOU instance.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import errno
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import argparse
|
||||
import threading
|
||||
import configparser
|
||||
import shutil
|
||||
|
||||
from .ioucon import start_ioucon
|
||||
from .iou_error import IOUError
|
||||
from .adapters.ethernet_adapter import EthernetAdapter
|
||||
@@ -35,6 +36,7 @@ from .adapters.serial_adapter import SerialAdapter
|
||||
from .nios.nio_udp import NIO_UDP
|
||||
from .nios.nio_tap import NIO_TAP
|
||||
from .nios.nio_generic_ethernet import NIO_GenericEthernet
|
||||
from ..attic import find_unused_port
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -44,35 +46,51 @@ 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
|
||||
"""
|
||||
|
||||
_instances = []
|
||||
_allocated_console_ports = []
|
||||
|
||||
def __init__(self, path, working_dir, host="127.0.0.1", name=None):
|
||||
def __init__(self,
|
||||
name,
|
||||
path,
|
||||
working_dir,
|
||||
host="127.0.0.1",
|
||||
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 = ""
|
||||
self._console = None
|
||||
self._console = console
|
||||
self._working_dir = None
|
||||
self._command = []
|
||||
self._process = None
|
||||
@@ -83,17 +101,40 @@ class IOUDevice(object):
|
||||
self._ioucon_thread_stop_event = None
|
||||
self._host = host
|
||||
self._started = False
|
||||
self._console_start_port_range = console_start_port_range
|
||||
self._console_end_port_range = console_end_port_range
|
||||
|
||||
# IOU settings
|
||||
self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces
|
||||
self._serial_adapters = [SerialAdapter(), SerialAdapter()] # one adapter = 4 interfaces
|
||||
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
|
||||
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 IOUError(e)
|
||||
|
||||
if self._console in self._allocated_console_ports:
|
||||
raise IOUError("Console port {} is already in used another IOU device".format(console))
|
||||
self._allocated_console_ports.append(self._console)
|
||||
|
||||
log.info("IOU device {name} [id={id}] has been created".format(name=self._name,
|
||||
id=self._id))
|
||||
@@ -107,12 +148,14 @@ 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,
|
||||
"ethernet_adapters": len(self._ethernet_adapters),
|
||||
"serial_adapters": len(self._serial_adapters),
|
||||
"console": self._console}
|
||||
"console": self._console,
|
||||
"l1_keepalives": self._l1_keepalives}
|
||||
|
||||
return iou_defaults
|
||||
|
||||
@@ -124,7 +167,7 @@ class IOUDevice(object):
|
||||
:returns: id (integer)
|
||||
"""
|
||||
|
||||
return(self._id)
|
||||
return self._id
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
@@ -133,6 +176,7 @@ class IOUDevice(object):
|
||||
"""
|
||||
|
||||
cls._instances.clear()
|
||||
cls._allocated_console_ports.clear()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -152,10 +196,23 @@ class IOUDevice(object):
|
||||
:param new_name: name
|
||||
"""
|
||||
|
||||
self._name = new_name
|
||||
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+", errors="replace") as f:
|
||||
old_config = f.read()
|
||||
new_config = old_config.replace(self._name, new_name)
|
||||
f.seek(0)
|
||||
f.write(new_config)
|
||||
except OSError as e:
|
||||
raise IOUError("Could not amend the configuration {}: {}".format(config_path, e))
|
||||
|
||||
log.info("IOU {name} [id={id}]: renamed to {new_name}".format(name=self._name,
|
||||
id=self._id,
|
||||
new_name=new_name))
|
||||
self._name = new_name
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
@@ -165,7 +222,7 @@ class IOUDevice(object):
|
||||
:returns: path to IOU
|
||||
"""
|
||||
|
||||
return(self._path)
|
||||
return self._path
|
||||
|
||||
@path.setter
|
||||
def path(self, path):
|
||||
@@ -177,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):
|
||||
@@ -188,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
|
||||
@@ -211,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
|
||||
@@ -244,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:
|
||||
@@ -255,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):
|
||||
@@ -276,10 +331,15 @@ class IOUDevice(object):
|
||||
:param console: console port (integer)
|
||||
"""
|
||||
|
||||
if console in self._allocated_console_ports:
|
||||
raise IOUError("Console port {} is already used by another IOU device".format(console))
|
||||
|
||||
self._allocated_console_ports.remove(self._console)
|
||||
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):
|
||||
"""
|
||||
@@ -296,10 +356,38 @@ class IOUDevice(object):
|
||||
"""
|
||||
|
||||
self.stop()
|
||||
self._instances.remove(self._id)
|
||||
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("IOU device {name} [id={id}] has been deleted".format(name=self._name,
|
||||
id=self._id))
|
||||
|
||||
def clean_delete(self):
|
||||
"""
|
||||
Deletes this IOU device & all files (nvram, initial-config etc.)
|
||||
"""
|
||||
|
||||
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 IOU device {name} [id={id}]: {error}".format(name=self._name,
|
||||
id=self._id,
|
||||
error=e))
|
||||
return
|
||||
|
||||
log.info("IOU device {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
|
||||
id=self._id))
|
||||
|
||||
@property
|
||||
def started(self):
|
||||
"""
|
||||
@@ -327,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,
|
||||
@@ -340,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
|
||||
|
||||
@@ -377,7 +484,7 @@ class IOUDevice(object):
|
||||
"""
|
||||
|
||||
if not self._ioucon_thead:
|
||||
telnet_server = "{}:{}".format(self._host, self._console)
|
||||
telnet_server = "{}:{}".format(self._host, self.console)
|
||||
log.info("starting ioucon for IOU instance {} to accept Telnet connections on {}".format(self._name, telnet_server))
|
||||
args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server)
|
||||
self._ioucon_thread_stop_event = threading.Event()
|
||||
@@ -391,7 +498,7 @@ class IOUDevice(object):
|
||||
|
||||
try:
|
||||
self._update_iouyap_config()
|
||||
command = [self._iouyap, str(self._id + 512)] # iouyap has always IOU ID + 512
|
||||
command = [self._iouyap, "-q", str(self._id + 512)] # iouyap has always IOU ID + 512
|
||||
log.info("starting iouyap: {}".format(command))
|
||||
self._iouyap_stdout_file = os.path.join(self._working_dir, "iouyap.log")
|
||||
log.info("logging to {}".format(self._iouyap_stdout_file))
|
||||
@@ -407,6 +514,23 @@ class IOUDevice(object):
|
||||
log.error("could not start iouyap: {}\n{}".format(e, iouyap_stdout))
|
||||
raise IOUError("Could not start iouyap: {}\n{}".format(e, iouyap_stdout))
|
||||
|
||||
def _library_check(self):
|
||||
"""
|
||||
Checks for missing shared library dependencies in the IOU image.
|
||||
"""
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(["ldd", self._path])
|
||||
except (FileNotFoundError, subprocess.CalledProcessError) as e:
|
||||
log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e))
|
||||
return
|
||||
|
||||
p = re.compile("([\.\w]+)\s=>\s+not found")
|
||||
missing_libs = p.findall(output.decode("utf-8"))
|
||||
if missing_libs:
|
||||
raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path,
|
||||
", ".join(missing_libs)))
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts the IOU process.
|
||||
@@ -414,8 +538,25 @@ class IOUDevice(object):
|
||||
|
||||
if not self.is_running():
|
||||
|
||||
if not os.path.isfile(self._path):
|
||||
raise IOUError("IOU '{}' is not accessible".format(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:
|
||||
with open(self._path, "rb") as f:
|
||||
# read the first 7 bytes of the file.
|
||||
elf_header_start = f.read(7)
|
||||
except OSError as e:
|
||||
raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._path, e))
|
||||
|
||||
# IOU images must start with the ELF magic number, be 32-bit, little endian
|
||||
# and have an ELF version of 1 normal IOS image are big endian!
|
||||
if elf_header_start != b'\x7fELF\x01\x01\x01':
|
||||
raise IOUError("'{}' is not a valid IOU image".format(self._path))
|
||||
|
||||
if not os.access(self._path, os.X_OK):
|
||||
raise IOUError("IOU image '{}' is not executable".format(self._path))
|
||||
|
||||
self._library_check()
|
||||
|
||||
if not self._iourc or not os.path.isfile(self._iourc):
|
||||
raise IOUError("A iourc file is necessary to start IOU")
|
||||
@@ -440,10 +581,12 @@ class IOUDevice(object):
|
||||
env=env)
|
||||
log.info("IOU instance {} started PID={}".format(self._id, self._process.pid))
|
||||
self._started = True
|
||||
except FileNotFoundError as e:
|
||||
raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e))
|
||||
except OSError as e:
|
||||
iou_stdout = self.read_iou_stdout()
|
||||
log.error("could not start IOU: {}\n{}".format(e, iou_stdout))
|
||||
raise IOUError("could not start IOU: {}\n{}".format(e, iou_stdout))
|
||||
log.error("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
|
||||
raise IOUError("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
|
||||
|
||||
# start console support
|
||||
self._start_ioucon()
|
||||
@@ -455,25 +598,11 @@ class IOUDevice(object):
|
||||
Stops the IOU process.
|
||||
"""
|
||||
|
||||
# stop the IOU process
|
||||
if self.is_running():
|
||||
log.info("stopping IOU 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() == None:
|
||||
log.warn("IOU instance {} PID={} is still running".format(self._id,
|
||||
self._process.pid))
|
||||
self._process = None
|
||||
self._started = False
|
||||
|
||||
# stop console support
|
||||
if self._ioucon_thead:
|
||||
self._ioucon_thread_stop_event.set()
|
||||
if self._ioucon_thead.is_alive():
|
||||
self._ioucon_thead.join(timeout=0.10)
|
||||
self._ioucon_thead.join(timeout=3.0) # wait for the thread to free the console port
|
||||
self._ioucon_thead = None
|
||||
|
||||
# stop iouyap
|
||||
@@ -484,11 +613,25 @@ 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
|
||||
|
||||
# stop the IOU process
|
||||
if self.is_running():
|
||||
log.info("stopping IOU 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("IOU instance {} PID={} is still running".format(self._id,
|
||||
self._process.pid))
|
||||
self._process = None
|
||||
self._started = False
|
||||
|
||||
def read_iou_stdout(self):
|
||||
"""
|
||||
Reads the standard output of the IOU process.
|
||||
@@ -498,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))
|
||||
@@ -513,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))
|
||||
@@ -526,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
|
||||
|
||||
@@ -537,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
|
||||
|
||||
@@ -576,6 +719,8 @@ class IOUDevice(object):
|
||||
|
||||
:param slot_id: slot ID
|
||||
:param port_id: port ID
|
||||
|
||||
:returns: NIO instance
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -599,6 +744,26 @@ class IOUDevice(object):
|
||||
self._update_iouyap_config()
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
|
||||
return nio
|
||||
|
||||
def _enable_l1_keepalives(self, command):
|
||||
"""
|
||||
Enables L1 keepalive messages if supported.
|
||||
|
||||
:param command: command line
|
||||
"""
|
||||
|
||||
env = os.environ.copy()
|
||||
env["IOURC"] = self._iourc
|
||||
try:
|
||||
output = subprocess.check_output([self._path, "-h"], stderr=subprocess.STDOUT, cwd=self._working_dir, env=env)
|
||||
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):
|
||||
"""
|
||||
Command to start the IOU process.
|
||||
@@ -629,20 +794,70 @@ 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))])
|
||||
if len(self._serial_adapters) != 2:
|
||||
command.extend(["-s", str(len(self._serial_adapters))])
|
||||
command.extend(["-n", str(self._nvram)])
|
||||
command.extend(["-m", str(self._ram)])
|
||||
if not self.use_default_iou_values:
|
||||
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)])
|
||||
return command
|
||||
|
||||
@property
|
||||
def use_default_iou_values(self):
|
||||
"""
|
||||
Returns if this device uses the default IOU image values.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._use_default_iou_values
|
||||
|
||||
@use_default_iou_values.setter
|
||||
def use_default_iou_values(self, state):
|
||||
"""
|
||||
Sets if this device uses the default IOU image values.
|
||||
|
||||
:param state: boolean
|
||||
"""
|
||||
|
||||
self._use_default_iou_values = state
|
||||
if state:
|
||||
log.info("IOU {name} [id={id}]: uses the default IOU image values".format(name=self._name, id=self._id))
|
||||
else:
|
||||
log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id))
|
||||
|
||||
@property
|
||||
def l1_keepalives(self):
|
||||
"""
|
||||
Returns either layer 1 keepalive messages option is enabled or disabled.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._l1_keepalives
|
||||
|
||||
@l1_keepalives.setter
|
||||
def l1_keepalives(self, state):
|
||||
"""
|
||||
Enables or disables layer 1 keepalive messages.
|
||||
|
||||
:param state: boolean
|
||||
"""
|
||||
|
||||
self._l1_keepalives = state
|
||||
if state:
|
||||
log.info("IOU {name} [id={id}]: has activated layer 1 keepalive messages".format(name=self._name, id=self._id))
|
||||
else:
|
||||
log.info("IOU {name} [id={id}]: has deactivated layer 1 keepalive messages".format(name=self._name, id=self._id))
|
||||
|
||||
@property
|
||||
def ram(self):
|
||||
"""
|
||||
@@ -699,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):
|
||||
@@ -777,37 +992,74 @@ class IOUDevice(object):
|
||||
|
||||
self._slots = self._ethernet_adapters + self._serial_adapters
|
||||
|
||||
@staticmethod
|
||||
def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP"):
|
||||
def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"):
|
||||
"""
|
||||
Finds an unused port in the specified range.
|
||||
Starts a packet capture.
|
||||
|
||||
:param start_port: first port in the range
|
||||
:param end_port: last port in the range
|
||||
:param host: host/address for bind()
|
||||
:param socket_type: TCP (default) or UDP
|
||||
: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
|
||||
"""
|
||||
|
||||
if socket_type == "UDP":
|
||||
socket_type = socket.SOCK_DGRAM
|
||||
else:
|
||||
socket_type = socket.SOCK_STREAM
|
||||
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))
|
||||
|
||||
for port in range(start_port, end_port):
|
||||
try:
|
||||
if ":" in host:
|
||||
# IPv6 address support
|
||||
with socket.socket(socket.AF_INET6, socket_type) as s:
|
||||
s.bind((host, port)) # the port is available if bind is a success
|
||||
else:
|
||||
with socket.socket(socket.AF_INET, socket_type) as s:
|
||||
s.bind((host, port)) # the port is available if bind is a success
|
||||
return port
|
||||
except OSError as e:
|
||||
if e.errno == errno.EADDRINUSE: # socket already in use
|
||||
if port + 1 == end_port:
|
||||
raise IOUError("Could not find a free port between {0} and {1}".format(start_port, end_port))
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
raise IOUError("Could not find an unused port: {}".format(e))
|
||||
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)
|
||||
@@ -25,6 +25,8 @@ class IOUError(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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -139,7 +139,10 @@ class FileLock:
|
||||
def unlock(self):
|
||||
if self.fd:
|
||||
# Deleting first prevents a race condition
|
||||
os.unlink(self.fd.name)
|
||||
try:
|
||||
os.unlink(self.fd.name)
|
||||
except FileNotFoundError as e:
|
||||
log.debug("{}".format(e))
|
||||
self.fd.close()
|
||||
|
||||
def __enter__(self):
|
||||
@@ -296,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:
|
||||
@@ -323,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')
|
||||
@@ -334,7 +335,10 @@ class TelnetServer(Console):
|
||||
def _disconnect(self, fileno):
|
||||
fd = self.fd_dict.pop(fileno)
|
||||
log.info("Telnet client disconnected")
|
||||
fd.shutdown(socket.SHUT_RDWR)
|
||||
try:
|
||||
fd.shutdown(socket.SHUT_RDWR)
|
||||
except OSError as e:
|
||||
log.warn("shutdown: {}".format(e))
|
||||
fd.close()
|
||||
|
||||
def __enter__(self):
|
||||
@@ -375,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)
|
||||
@@ -387,14 +394,12 @@ class IOU(Router):
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise NetioError("Couldn't unlink socket {}: {}"
|
||||
.format(self.ttyC, e))
|
||||
raise NetioError("Couldn't unlink socket {}: {}".format(self.ttyC, e))
|
||||
|
||||
try:
|
||||
self.fd.bind(self.ttyC)
|
||||
except Exception as e:
|
||||
raise NetioError("Couldn't create socket {}: {}"
|
||||
.format(self.ttyC, e))
|
||||
raise NetioError("Couldn't create socket {}: {}".format(self.ttyC, e))
|
||||
|
||||
def _connect(self):
|
||||
# Keep trying until we connect or die trying
|
||||
@@ -402,12 +407,10 @@ class IOU(Router):
|
||||
try:
|
||||
self.fd.connect(self.ttyS)
|
||||
except FileNotFoundError:
|
||||
log.debug("Waiting to connect to {}".format(self.ttyS),
|
||||
file=sys.stderr)
|
||||
log.debug("Waiting to connect to {}".format(self.ttyS))
|
||||
time.sleep(RETRY_DELAY)
|
||||
except Exception as e:
|
||||
raise NetioError("Couldn't connect to socket {}: {}"
|
||||
.format(self.ttyS, e))
|
||||
raise NetioError("Couldn't connect to socket {}: {}".format(self.ttyS, e))
|
||||
else:
|
||||
break
|
||||
|
||||
@@ -460,8 +463,7 @@ def mkdir_netio(netio_dir):
|
||||
except FileExistsError:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise NetioError("Couldn't create directory {}: {}"
|
||||
.format(netio_dir, e))
|
||||
raise NetioError("Couldn't create directory {}: {}".format(netio_dir, e))
|
||||
|
||||
|
||||
def send_recv_loop(console, router, esc_char, stop_event):
|
||||
@@ -600,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)')
|
||||
|
||||
@@ -626,7 +628,7 @@ def start_ioucon(cmdline_args, stop_event):
|
||||
if args.debug:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
else:
|
||||
print(e, file=sys.stderr)
|
||||
log.error("ioucon: {}".format(e))
|
||||
sys.exit(EXIT_FAILURE)
|
||||
|
||||
log.info("exiting...")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
IOU_CREATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to create a new IOU instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "IOU device name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"iou_id": {
|
||||
"description": "IOU device instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"console": {
|
||||
"description": "console TCP port",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"description": "path to the IOU executable",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
}
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["name", "path"],
|
||||
}
|
||||
|
||||
IOU_DELETE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete an IOU instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "IOU device instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
IOU_UPDATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to update an IOU instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "IOU device instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "IOU device name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"path": {
|
||||
"description": "path to the IOU executable",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"initial_config": {
|
||||
"description": "path to the IOU initial configuration file",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"ram": {
|
||||
"description": "amount of RAM in MB",
|
||||
"type": "integer"
|
||||
},
|
||||
"nvram": {
|
||||
"description": "amount of NVRAM in KB",
|
||||
"type": "integer"
|
||||
},
|
||||
"ethernet_adapters": {
|
||||
"description": "number of Ethernet adapters",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 16,
|
||||
},
|
||||
"serial_adapters": {
|
||||
"description": "number of serial adapters",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 16,
|
||||
},
|
||||
"console": {
|
||||
"description": "console TCP port",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"type": "integer"
|
||||
},
|
||||
"use_default_iou_values": {
|
||||
"description": "use the default IOU RAM & NVRAM values",
|
||||
"type": "boolean"
|
||||
},
|
||||
"l1_keepalives": {
|
||||
"description": "enable or disable layer 1 keepalive messages",
|
||||
"type": "boolean"
|
||||
},
|
||||
"initial_config_base64": {
|
||||
"description": "initial configuration base64 encoded",
|
||||
"type": "string"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
IOU_START_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to start an IOU instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "IOU device instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
IOU_STOP_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to stop an IOU instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "IOU device instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
IOU_RELOAD_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to reload an IOU instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "IOU device instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
IOU_ALLOCATE_UDP_PORT_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to allocate an UDP port for an IOU instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "IOU device instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port_id": {
|
||||
"description": "Unique port identifier for the IOU instance",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id", "port_id"]
|
||||
}
|
||||
|
||||
IOU_ADD_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to add a NIO for an IOU 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": "IOU device instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port_id": {
|
||||
"description": "Unique port identifier for the IOU instance",
|
||||
"type": "integer"
|
||||
},
|
||||
"slot": {
|
||||
"description": "Slot number",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 15
|
||||
},
|
||||
"port": {
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 3
|
||||
},
|
||||
"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", "slot", "port", "nio"]
|
||||
}
|
||||
|
||||
|
||||
IOU_DELETE_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete a NIO for an IOU instance",
|
||||
"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
|
||||
},
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
@@ -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.
@@ -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.
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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.
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user