mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-06-07 11:06:28 +03:00
Compare commits
184 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b81a531a7b | ||
|
|
089b4108cc | ||
|
|
b89f70370a | ||
|
|
81b4ded30a | ||
|
|
b658eea427 | ||
|
|
da225ffdf9 | ||
|
|
b7fb6e6b13 | ||
|
|
078cef064b | ||
|
|
bec1c41f75 | ||
|
|
64f3516153 | ||
|
|
558e8ad8ce | ||
|
|
5f7408809e | ||
|
|
8359da3c76 | ||
|
|
c613e20971 | ||
|
|
34ab6c2e1b | ||
|
|
5382a8a397 | ||
|
|
507f104ae5 | ||
|
|
ada2f647a0 | ||
|
|
347b76d39e | ||
|
|
3749819016 | ||
|
|
4d4871d165 | ||
|
|
59f6a22e81 | ||
|
|
0982338e2c | ||
|
|
23c3576256 | ||
|
|
1dbf30c6cb | ||
|
|
2081689c12 | ||
|
|
983c55928e | ||
|
|
de625d6cfc | ||
|
|
523d791cac | ||
|
|
270518f294 | ||
|
|
7ab8d679f7 | ||
|
|
f518464eb2 | ||
|
|
321685acb8 | ||
|
|
747ca36a5a | ||
|
|
485844f8de | ||
|
|
b7c0a8c368 | ||
|
|
678c42f941 | ||
|
|
1eaf6c97e0 | ||
|
|
f92282f823 | ||
|
|
9ef90210d8 | ||
|
|
f280ea4c68 | ||
|
|
0067634990 | ||
|
|
4799fc7c93 | ||
|
|
829154fb1c | ||
|
|
7e0caba4b0 | ||
|
|
3c3890ff21 | ||
|
|
65411d1742 | ||
|
|
146a6a5af2 | ||
|
|
fc72140402 | ||
|
|
1b13d83e38 | ||
|
|
e3f073d74b | ||
|
|
8f6e84f8a9 | ||
|
|
c594c3d8a7 | ||
|
|
a550527e6d | ||
|
|
0ce377f321 | ||
|
|
ab37a6237c | ||
|
|
ecc57133c6 | ||
|
|
fc6c2c0304 | ||
|
|
92c731a9c9 | ||
|
|
bc433e5281 | ||
|
|
21eb0b0f03 | ||
|
|
3f70d0238f | ||
|
|
8fd3f67378 | ||
|
|
4e9cb90468 | ||
|
|
d38e62fa38 | ||
|
|
4b7df545aa | ||
|
|
6af5f5f3fb | ||
|
|
b9318dfe6a | ||
|
|
6ffc2c807b | ||
|
|
d177ea44bd | ||
|
|
39f3b22817 | ||
|
|
d157295550 | ||
|
|
d7b9465850 | ||
|
|
0766dac62b | ||
|
|
4f81dde2fd | ||
|
|
4a716000ff | ||
|
|
d8b0e9234e | ||
|
|
bf0af2a929 | ||
|
|
a05e47a4d2 | ||
|
|
996c5c927c | ||
|
|
dab7569575 | ||
|
|
2a0e8a3b4f | ||
|
|
872e7199e4 | ||
|
|
67b57a8d78 | ||
|
|
ba3c1e6969 | ||
|
|
1879172505 | ||
|
|
22fe51fe5a | ||
|
|
b5ac40896f | ||
|
|
2d6b53245b | ||
|
|
eee377c4fc | ||
|
|
c34b82c255 | ||
|
|
c750ce8d80 | ||
|
|
2d2e682540 | ||
|
|
e06cf5b9a1 | ||
|
|
d25258c47f | ||
|
|
1e40a36a48 | ||
|
|
d5059d22fc | ||
|
|
bae61bdcaa | ||
|
|
eb44226ee4 | ||
|
|
8ee251cbb2 | ||
|
|
a84e081a75 | ||
|
|
d92db4e99d | ||
|
|
873e04ed9d | ||
|
|
c0c41b99eb | ||
|
|
12b694047a | ||
|
|
59651a3fe5 | ||
|
|
02ad5d2f3a | ||
|
|
a31b98f781 | ||
|
|
e9a674c4e9 | ||
|
|
4b383e2b06 | ||
|
|
6d2ca353a3 | ||
|
|
d8b5caf679 | ||
|
|
d61088e3a7 | ||
|
|
a3f0569663 | ||
|
|
55b80cc9cb | ||
|
|
aec6c37016 | ||
|
|
117f6ec3b1 | ||
|
|
574d6b3792 | ||
|
|
9608614aa9 | ||
|
|
fe5f80382a | ||
|
|
a4ed59200d | ||
|
|
3ca05c7427 | ||
|
|
6a16bcedc0 | ||
|
|
56ebfc7fd0 | ||
|
|
9b559d43be | ||
|
|
ad7d06ef21 | ||
|
|
b88bf71be9 | ||
|
|
3b019edc82 | ||
|
|
f3504809ed | ||
|
|
23735f35ad | ||
|
|
3adc46fbe2 | ||
|
|
8a303e4563 | ||
|
|
842ad8ae26 | ||
|
|
466c349642 | ||
|
|
1356fd9c69 | ||
|
|
2d1c9444c5 | ||
|
|
22d7815d8e | ||
|
|
53487d5937 | ||
|
|
ab729d8f67 | ||
|
|
eb5c10de3d | ||
|
|
1b6d534b8e | ||
|
|
a9b5b9eda2 | ||
|
|
ce12eb86e8 | ||
|
|
d4ffbd9f97 | ||
|
|
4c01a465ac | ||
|
|
012bc1e406 | ||
|
|
05ba772715 | ||
|
|
9ea57f511b | ||
|
|
5aaa2d7280 | ||
|
|
497eb19369 | ||
|
|
70049aa877 | ||
|
|
ece7930cb1 | ||
|
|
c7df589857 | ||
|
|
8bcc92f319 | ||
|
|
dedde63b60 | ||
|
|
a81d1443f9 | ||
|
|
e69089f4cf | ||
|
|
0c052542b3 | ||
|
|
00e402f28c | ||
|
|
0742b282a3 | ||
|
|
2b588aa0bf | ||
|
|
92fb8418ab | ||
|
|
9c7dbc864e | ||
|
|
25aebaa46c | ||
|
|
0e30b3cf5f | ||
|
|
755667c4d5 | ||
|
|
16dbdf70d9 | ||
|
|
806c7479ee | ||
|
|
cc8b84725a | ||
|
|
e01701614e | ||
|
|
efaffac801 | ||
|
|
c58366e9cb | ||
|
|
068ebcdea0 | ||
|
|
51f2b4bfa8 | ||
|
|
168e4ab86e | ||
|
|
7ae18ff82a | ||
|
|
c694173f9d | ||
|
|
b58b92c9f0 | ||
|
|
3ddb2e70d4 | ||
|
|
05966a9119 | ||
|
|
8ea24e9920 | ||
|
|
47f34fd5af | ||
|
|
89321a6cad | ||
|
|
6690ba7108 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -60,3 +60,6 @@ keys
|
||||
updates
|
||||
.cache
|
||||
__pycache__
|
||||
|
||||
# Virtualenv
|
||||
env
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Change Log
|
||||
|
||||
## 2.1.0a1 24/07/2017
|
||||
|
||||
* Packet filtering
|
||||
* Suspend a link
|
||||
* Duplicate a node
|
||||
* Move config to central server
|
||||
* Appliance templates on server
|
||||
|
||||
## 2.0.3 13/06/2017
|
||||
|
||||
* Display error when we can't export files
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Run tests inside a container
|
||||
FROM ubuntu:vivid
|
||||
FROM ubuntu:yakkety
|
||||
|
||||
MAINTAINER GNS3 Team
|
||||
|
||||
#ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --force-yes python3.4 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3.4-dev xvfb
|
||||
RUN apt-get install -y --force-yes python3.5 python3-pyqt5 python3-pip python3-pyqt5.qtsvg python3-pyqt5.qtwebsockets python3.5-dev xvfb
|
||||
RUN apt-get clean
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ ADD . /src
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
CMD xvfb-run python3.4 -m pytest -vv
|
||||
CMD xvfb-run python3.5 -m pytest -vv
|
||||
|
||||
126
gns3/appliance_manager.py
Normal file
126
gns3/appliance_manager.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 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 .qt import QtCore
|
||||
from .controller import Controller
|
||||
from .utils.server_select import server_select
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApplianceManager(QtCore.QObject):
|
||||
|
||||
appliances_changed_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._appliance_templates = []
|
||||
self._appliances = []
|
||||
self._controller = Controller.instance()
|
||||
self._controller.connected_signal.connect(self.refresh)
|
||||
self._controller.disconnected_signal.connect(self._controllerDisconnectedSlot)
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
if self._controller.connected():
|
||||
self._controller.get("/appliances/templates", self._listApplianceTemplateCallback)
|
||||
self._controller.get("/appliances", self._listAppliancesCallback)
|
||||
|
||||
def _controllerDisconnectedSlot(self):
|
||||
self._appliance_templates = []
|
||||
self._appliances = []
|
||||
self.appliances_changed_signal.emit()
|
||||
|
||||
def appliance_templates(self):
|
||||
return self._appliance_templates
|
||||
|
||||
def appliances(self):
|
||||
return self._appliances
|
||||
|
||||
def getAppliance(self, appliance_id):
|
||||
"""
|
||||
Look for an appliance by appliance ID
|
||||
"""
|
||||
for appliance in self._appliances:
|
||||
if appliance["appliance_id"] == appliance_id:
|
||||
return appliance
|
||||
return None
|
||||
|
||||
def _listAppliancesCallback(self, result, error=False, **kwargs):
|
||||
if error is True:
|
||||
log.error("Error while getting appliances list: {}".format(result["message"]))
|
||||
return
|
||||
self._appliances = result
|
||||
self.appliances_changed_signal.emit()
|
||||
|
||||
def _listApplianceTemplateCallback(self, result, error=False, **kwargs):
|
||||
if error is True:
|
||||
log.error("Error while getting appliance templates list: {}".format(result["message"]))
|
||||
return
|
||||
self._appliance_templates = result
|
||||
self.appliances_changed_signal.emit()
|
||||
|
||||
def createNodeFromApplianceId(self, project, appliance_id, x, y):
|
||||
for appliance in self._appliances:
|
||||
if appliance["appliance_id"] == appliance_id:
|
||||
break
|
||||
if appliance.get("compute_id") is None:
|
||||
from .main_window import MainWindow
|
||||
server = server_select(MainWindow.instance(), node_type=appliance["node_type"])
|
||||
if server is None:
|
||||
return False
|
||||
self._controller.post("/projects/" + project.id() + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
|
||||
"compute_id": server.id(),
|
||||
"x": int(x),
|
||||
"y": int(y)
|
||||
})
|
||||
else:
|
||||
self._controller.post("/projects/" + project.id() + "/appliances/" + appliance_id, self._createNodeFromApplianceCallback, {
|
||||
"x": int(x),
|
||||
"y": int(y)
|
||||
})
|
||||
return True
|
||||
|
||||
def _createNodeFromApplianceCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while creating node: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def _controllerDisconnectedSlot(self):
|
||||
self._appliances = []
|
||||
|
||||
def appliances(self):
|
||||
return self._appliances
|
||||
|
||||
def _listAppliancesCallback(self, result, error=False, **kwargs):
|
||||
if error is True:
|
||||
log.error("Error while getting appliance list: {}".format(result["message"]))
|
||||
return
|
||||
self._appliances = result
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only on instance of ApplianceManager.
|
||||
:returns: instance of ApplianceManager
|
||||
"""
|
||||
|
||||
if not hasattr(ApplianceManager, '_instance') or ApplianceManager._instance is None:
|
||||
ApplianceManager._instance = ApplianceManager()
|
||||
return ApplianceManager._instance
|
||||
@@ -19,8 +19,12 @@
|
||||
Base class for node classes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from .qt import QtCore
|
||||
from .ports.port import Port
|
||||
from .utils.normalize_filename import normalize_filename
|
||||
|
||||
|
||||
import logging
|
||||
@@ -176,19 +180,16 @@ class BaseNode(QtCore.QObject):
|
||||
# set ports as started
|
||||
port.setStatus(Port.started)
|
||||
self.started_signal.emit()
|
||||
log.info("{} has started".format(self.name()))
|
||||
elif status == self.stopped:
|
||||
for port in self._ports:
|
||||
# set ports as stopped
|
||||
port.setStatus(Port.stopped)
|
||||
self.stopped_signal.emit()
|
||||
log.info("{} has stopped".format(self.name()))
|
||||
elif status == self.suspended:
|
||||
for port in self._ports:
|
||||
# set ports as suspended
|
||||
port.setStatus(Port.suspended)
|
||||
self.suspended_signal.emit()
|
||||
log.info("{} has suspended".format(self.name()))
|
||||
|
||||
def initialized(self):
|
||||
"""
|
||||
@@ -326,3 +327,73 @@ class BaseNode(QtCore.QObject):
|
||||
"""
|
||||
|
||||
self._project.delete(path, callback, context=context, **kwargs)
|
||||
|
||||
def exportConfigToDirectory(self, directory):
|
||||
"""
|
||||
Exports the initial-config to a directory.
|
||||
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
if not hasattr(self, "configFiles"):
|
||||
return
|
||||
for file in self.configFiles():
|
||||
self.controllerHttpGet("/nodes/{node_id}/files/{file}".format(node_id=self._node_id, file=file),
|
||||
self._exportConfigToDirectoryCallback,
|
||||
context={"directory": directory, "file": file},
|
||||
raw=True)
|
||||
|
||||
def _exportConfigToDirectoryCallback(self, result, error=False, raw_body=None, context={}, **kwargs):
|
||||
"""
|
||||
Callback for exportConfigToDirectory.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
# The file could be missing if you have not private config for
|
||||
# exemple
|
||||
return
|
||||
export_directory = context["directory"]
|
||||
|
||||
filename = normalize_filename(self.name()) + "_{}".format(context["file"].replace("/", "_")) # We can have / in the case of Docker
|
||||
config_path = os.path.join(export_directory, filename)
|
||||
try:
|
||||
with open(config_path, "wb") as f:
|
||||
log.debug("saving {} config to {}".format(self.name(), config_path))
|
||||
f.write(raw_body)
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not export config to {}: {}".format(config_path, e))
|
||||
|
||||
def importConfigFromDirectory(self, directory):
|
||||
"""
|
||||
Imports an initial-config from a directory.
|
||||
|
||||
:param directory: source directory path
|
||||
"""
|
||||
|
||||
if not hasattr(self, "configFiles"):
|
||||
return
|
||||
|
||||
try:
|
||||
contents = os.listdir(directory)
|
||||
except OSError as e:
|
||||
self.warning_signal.emit(self.id(), "Can't list file in {}: {}".format(directory, str(e)))
|
||||
return
|
||||
|
||||
for file in self.configFiles():
|
||||
filename = normalize_filename(self.name()) + "_{}".format(file.replace("/", "_")) # We can have / in the case of Docker
|
||||
if filename in contents:
|
||||
self.controllerHttpPost("/nodes/{node_id}/files/{file}".format(
|
||||
node_id=self._node_id,
|
||||
file=file), self._importConfigCallback,
|
||||
pathlib.Path(os.path.join(directory, filename)))
|
||||
else:
|
||||
self.warning_signal.emit(self.id(), "no script file could be found, expected file name: {}".format(filename))
|
||||
|
||||
def _importConfigCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while import config: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
!
|
||||
service timestamps debug datetime msec
|
||||
service timestamps log datetime msec
|
||||
no service password-encryption
|
||||
!
|
||||
hostname %h
|
||||
!
|
||||
ip cef
|
||||
no ip domain-lookup
|
||||
no ip icmp rate-limit unreachable
|
||||
ip tcp synwait 5
|
||||
no cdp log mismatch duplex
|
||||
!
|
||||
line con 0
|
||||
exec-timeout 0 0
|
||||
logging synchronous
|
||||
privilege level 15
|
||||
no login
|
||||
line aux 0
|
||||
exec-timeout 0 0
|
||||
logging synchronous
|
||||
privilege level 15
|
||||
no login
|
||||
!
|
||||
!
|
||||
end
|
||||
@@ -1,181 +0,0 @@
|
||||
!
|
||||
service timestamps debug datetime msec
|
||||
service timestamps log datetime msec
|
||||
no service password-encryption
|
||||
no service dhcp
|
||||
!
|
||||
hostname %h
|
||||
!
|
||||
ip cef
|
||||
no ip routing
|
||||
no ip domain-lookup
|
||||
no ip icmp rate-limit unreachable
|
||||
ip tcp synwait 5
|
||||
no cdp log mismatch duplex
|
||||
vtp file nvram:vlan.dat
|
||||
!
|
||||
!
|
||||
interface FastEthernet0/0
|
||||
description *** Unused for Layer2 EtherSwitch ***
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface FastEthernet0/1
|
||||
description *** Unused for Layer2 EtherSwitch ***
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface FastEthernet1/0
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/1
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/2
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/3
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/4
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/5
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/6
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/7
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/8
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/9
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/10
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/11
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/12
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/13
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/14
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface FastEthernet1/15
|
||||
no shutdown
|
||||
duplex full
|
||||
speed 100
|
||||
!
|
||||
interface Vlan1
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
!
|
||||
line con 0
|
||||
exec-timeout 0 0
|
||||
logging synchronous
|
||||
privilege level 15
|
||||
no login
|
||||
line aux 0
|
||||
exec-timeout 0 0
|
||||
logging synchronous
|
||||
privilege level 15
|
||||
no login
|
||||
!
|
||||
!
|
||||
banner exec $
|
||||
|
||||
***************************************************************
|
||||
This is a normal Router with a SW module inside (NM-16ESW)
|
||||
It has been preconfigured with hard coded speed and duplex
|
||||
|
||||
To create vlans use the command "vlan database" from exec mode
|
||||
After creating all desired vlans use "exit" to apply the config
|
||||
|
||||
To view existing vlans use the command "show vlan-switch brief"
|
||||
|
||||
Warning: You are using an old IOS image for this router.
|
||||
Please update the IOS to enable the "macro" command!
|
||||
***************************************************************
|
||||
|
||||
$
|
||||
!
|
||||
!Warning: If the IOS is old and doesn't support macro, it will stop the configuration loading from this point!
|
||||
!
|
||||
macro name add_vlan
|
||||
end
|
||||
vlan database
|
||||
vlan $v
|
||||
exit
|
||||
@
|
||||
macro name del_vlan
|
||||
end
|
||||
vlan database
|
||||
no vlan $v
|
||||
exit
|
||||
@
|
||||
!
|
||||
!
|
||||
banner exec $
|
||||
|
||||
***************************************************************
|
||||
This is a normal Router with a Switch module inside (NM-16ESW)
|
||||
It has been pre-configured with hard-coded speed and duplex
|
||||
|
||||
To create vlans use the command "vlan database" in exec mode
|
||||
After creating all desired vlans use "exit" to apply the config
|
||||
|
||||
To view existing vlans use the command "show vlan-switch brief"
|
||||
|
||||
Alias(exec) : vl - "show vlan-switch brief" command
|
||||
Alias(configure): va X - macro to add vlan X
|
||||
Alias(configure): vd X - macro to delete vlan X
|
||||
***************************************************************
|
||||
|
||||
$
|
||||
!
|
||||
alias configure va macro global trace add_vlan $v
|
||||
alias configure vd macro global trace del_vlan $v
|
||||
alias exec vl show vlan-switch brief
|
||||
!
|
||||
!
|
||||
end
|
||||
@@ -1,132 +0,0 @@
|
||||
!
|
||||
service timestamps debug datetime msec
|
||||
service timestamps log datetime msec
|
||||
no service password-encryption
|
||||
!
|
||||
hostname %h
|
||||
!
|
||||
!
|
||||
!
|
||||
logging discriminator EXCESS severity drops 6 msg-body drops EXCESSCOLL
|
||||
logging buffered 50000
|
||||
logging console discriminator EXCESS
|
||||
!
|
||||
no ip icmp rate-limit unreachable
|
||||
!
|
||||
ip cef
|
||||
no ip domain-lookup
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
ip tcp synwait-time 5
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
interface Ethernet0/0
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/1
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/2
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/3
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/0
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/1
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/2
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/3
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet2/0
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet2/1
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet2/2
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet2/3
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet3/0
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet3/1
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet3/2
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet3/3
|
||||
no ip address
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Vlan1
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
line con 0
|
||||
exec-timeout 0 0
|
||||
privilege level 15
|
||||
logging synchronous
|
||||
line aux 0
|
||||
exec-timeout 0 0
|
||||
privilege level 15
|
||||
logging synchronous
|
||||
!
|
||||
end
|
||||
@@ -1,108 +0,0 @@
|
||||
!
|
||||
service timestamps debug datetime msec
|
||||
service timestamps log datetime msec
|
||||
no service password-encryption
|
||||
!
|
||||
hostname %h
|
||||
!
|
||||
!
|
||||
!
|
||||
no ip icmp rate-limit unreachable
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
ip cef
|
||||
no ip domain-lookup
|
||||
!
|
||||
!
|
||||
ip tcp synwait-time 5
|
||||
!
|
||||
!
|
||||
!
|
||||
!
|
||||
interface Ethernet0/0
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet0/1
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet0/2
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet0/3
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet1/0
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet1/1
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet1/2
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Ethernet1/3
|
||||
no ip address
|
||||
shutdown
|
||||
!
|
||||
interface Serial2/0
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial2/1
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial2/2
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial2/3
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial3/0
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial3/1
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial3/2
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
interface Serial3/3
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
!
|
||||
!
|
||||
no cdp log mismatch duplex
|
||||
!
|
||||
line con 0
|
||||
exec-timeout 0 0
|
||||
privilege level 15
|
||||
logging synchronous
|
||||
line aux 0
|
||||
exec-timeout 0 0
|
||||
privilege level 15
|
||||
logging synchronous
|
||||
!
|
||||
end
|
||||
@@ -1 +0,0 @@
|
||||
set pcname %h
|
||||
@@ -21,7 +21,6 @@ Handles commands typed in the GNS3 console.
|
||||
|
||||
import sys
|
||||
import cmd
|
||||
import logging
|
||||
import struct
|
||||
import sip
|
||||
import json
|
||||
@@ -30,6 +29,9 @@ from .node import Node
|
||||
from .qt import QtCore
|
||||
from .version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
@@ -177,6 +179,24 @@ class ConsoleCmd(cmd.Cmd):
|
||||
print("Cannot console to {}".format(device))
|
||||
break
|
||||
|
||||
def do_log(self, args):
|
||||
"""
|
||||
Log a message
|
||||
|
||||
log level message
|
||||
"""
|
||||
|
||||
args = args.split()
|
||||
if len(args) == 0:
|
||||
return
|
||||
level = args.pop(0)
|
||||
if level == "info":
|
||||
log.info(" ".join(args))
|
||||
elif level == "warning":
|
||||
log.warning(" ".join(args))
|
||||
else:
|
||||
log.error(" ".join(args))
|
||||
|
||||
def _start_console(self, node):
|
||||
"""
|
||||
Starts a console application for a specific node.
|
||||
|
||||
@@ -197,6 +197,9 @@ class Controller(QtCore.QObject):
|
||||
def getSynchronous(self, endpoint, timeout=2):
|
||||
return self._http_client.getSynchronous(endpoint, timeout)
|
||||
|
||||
def connectWebSocket(self, path, *args):
|
||||
return self._http_client.connectWebSocket(path)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
@@ -208,12 +211,13 @@ class Controller(QtCore.QObject):
|
||||
Controller._instance = Controller()
|
||||
return Controller._instance
|
||||
|
||||
def getStatic(self, url, callback):
|
||||
def getStatic(self, url, callback, fallback=None):
|
||||
"""
|
||||
Get a URL from the /static on controller and cache it on disk
|
||||
|
||||
:param url: URL without the protocol and host part
|
||||
:param callback: Callback to call when file is ready
|
||||
:param fallback: Fallback url in case of error
|
||||
"""
|
||||
|
||||
if not self._http_client:
|
||||
@@ -229,16 +233,25 @@ class Controller(QtCore.QObject):
|
||||
if os.path.exists(path):
|
||||
callback(path)
|
||||
elif path in self._static_asset_download_queue:
|
||||
self._static_asset_download_queue[path].append(callback)
|
||||
self._static_asset_download_queue[path].append((callback, fallback, ))
|
||||
else:
|
||||
self._static_asset_download_queue[path] = [callback]
|
||||
self._static_asset_download_queue[path] = [(callback, fallback, )]
|
||||
self._http_client.createHTTPQuery("GET", url, qpartial(self._getStaticCallback, url, path))
|
||||
|
||||
def _getStaticCallback(self, url, path, result, error=False, raw_body=None, **kwargs):
|
||||
if path not in self._static_asset_download_queue:
|
||||
return
|
||||
|
||||
if error:
|
||||
fallback_used = False
|
||||
for callback, fallback in self._static_asset_download_queue[path]:
|
||||
if fallback:
|
||||
self.getStatic(fallback, callback)
|
||||
fallback_used = True
|
||||
if fallback_used:
|
||||
log.error("Error while downloading file: {}".format(url))
|
||||
log.error("Error while downloading file: {}".format(url))
|
||||
if path in self._static_asset_download_queue:
|
||||
del self._static_asset_download_queue[path]
|
||||
del self._static_asset_download_queue[path]
|
||||
return
|
||||
try:
|
||||
with open(path, "wb+") as f:
|
||||
@@ -247,18 +260,24 @@ class Controller(QtCore.QObject):
|
||||
log.error("Can't write to {}: {}".format(path, str(e)))
|
||||
return
|
||||
log.debug("File stored {} for {}".format(path, url))
|
||||
for callback in self._static_asset_download_queue[path]:
|
||||
for callback, fallback in self._static_asset_download_queue[path]:
|
||||
callback(path)
|
||||
del self._static_asset_download_queue[path]
|
||||
|
||||
def getSymbolIcon(self, symbol_id, callback):
|
||||
def getSymbolIcon(self, symbol_id, callback, fallback=None):
|
||||
"""
|
||||
Get a QIcon for a symbol from the controller
|
||||
|
||||
:param url: URL without the protocol and host part
|
||||
:param symbol_id: Symbol id
|
||||
:param callback: Callback to call when file is ready
|
||||
:param fallback: Fallback symbol if not found
|
||||
"""
|
||||
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback))
|
||||
if symbol_id is None:
|
||||
self.getStatic(Symbol(fallback).url(), qpartial(self._getIconCallback, callback))
|
||||
else:
|
||||
if fallback:
|
||||
fallback = Symbol(fallback).url()
|
||||
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback), fallback=fallback)
|
||||
|
||||
def _getIconCallback(self, callback, path):
|
||||
icon = QtGui.QIcon()
|
||||
|
||||
@@ -41,7 +41,7 @@ if __version_info__[3] != 0:
|
||||
import faulthandler
|
||||
# Display a traceback in case of segfault crash. Usefull when frozen
|
||||
# Not enabled by default for security reason
|
||||
log.info("Enable catching segfault")
|
||||
log.debug("Enable catching segfault")
|
||||
faulthandler.enable()
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "sync+https://cf90c68e9c81469499e1417714cf2e79:a8ac6a66779a433497c8e9cb32c2a668@sentry.io/38506"
|
||||
DSN = "sync+https://a6a4a48f46c84f2b9474cd48db6eeb66:f77e467cb5bf484496e4e6eb45700d13@sentry.io/38506"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = get_resource("cacert.pem")
|
||||
if cacert is not None and os.path.isfile(cacert):
|
||||
@@ -70,11 +70,18 @@ class CrashReport:
|
||||
|
||||
def captureException(self, exception, value, tb):
|
||||
from .local_server import LocalServer
|
||||
from .local_config import LocalConfig
|
||||
|
||||
local_server = LocalServer.instance().localServerSettings()
|
||||
if local_server["report_errors"]:
|
||||
if not RAVEN_AVAILABLE:
|
||||
return
|
||||
|
||||
if os.path.exists(LocalConfig.instance().runAsRootPath()):
|
||||
log.warning("User has run application as root. Crash reports are disabled.")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if os.path.exists(".git"):
|
||||
log.warning("A .git directory exist crash report is turn off for developers. Instant exit")
|
||||
sys.exit(1)
|
||||
@@ -104,7 +111,7 @@ class CrashReport:
|
||||
except Exception as e:
|
||||
log.error("Can't send crash report to Sentry: {}".format(e))
|
||||
return
|
||||
log.info("Crash report sent with event ID: {}".format(client.get_ident(report)))
|
||||
log.debug("Crash report sent with event ID: {}".format(client.get_ident(report)))
|
||||
|
||||
def _add_qt_information(self, context):
|
||||
try:
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import os
|
||||
import sip
|
||||
import shutil
|
||||
|
||||
from ..qt import QtWidgets, QtCore, QtGui, qpartial, qslot
|
||||
from ..ui.appliance_wizard_ui import Ui_ApplianceWizard
|
||||
@@ -71,7 +72,16 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
self.uiLocalRadioButton.toggled.connect(self._localToggledSlot)
|
||||
if Controller.instance().isRemote():
|
||||
self.uiLocalRadioButton.setText("Run the appliance on the main server")
|
||||
self.uiLocalRadioButton.setText("Install the appliance on the main server")
|
||||
else:
|
||||
if not path.endswith('.builtin.gns3a'):
|
||||
try:
|
||||
destination = Config().appliances_dir
|
||||
os.makedirs(destination, exist_ok=True)
|
||||
destination = os.path.join(destination, os.path.basename(path))
|
||||
shutil.copy(path, destination)
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.warning(self.parent(), "Can't copy {} to {}".format(path, destination), str(e))
|
||||
|
||||
self.uiServerWizardPage.isComplete = self._uiServerWizardPage_isComplete
|
||||
|
||||
@@ -128,6 +138,9 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
self.uiInfoTreeWidget.addTopLevelItem(item)
|
||||
|
||||
elif self.page(page_id) == self.uiServerWizardPage:
|
||||
is_mac = ComputeManager.instance().localPlatform().startswith("darwin")
|
||||
is_win = ComputeManager.instance().localPlatform().startswith("win")
|
||||
|
||||
self.uiRemoteServersComboBox.clear()
|
||||
if len(ComputeManager.instance().remoteComputes()) == 0:
|
||||
self.uiRemoteRadioButton.setEnabled(False)
|
||||
@@ -141,7 +154,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
|
||||
if ComputeManager.instance().localPlatform() is None:
|
||||
self.uiLocalRadioButton.setEnabled(False)
|
||||
elif (ComputeManager.instance().localPlatform().startswith("darwin") or ComputeManager.instance().localPlatform().startswith("win")):
|
||||
elif is_mac or is_win:
|
||||
if type == "qemu":
|
||||
# Qemu has issues on OSX and Windows we disallow usage of the local server
|
||||
if not LocalConfig.instance().experimental():
|
||||
@@ -158,6 +171,14 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
|
||||
else:
|
||||
self.uiRemoteRadioButton.setChecked(False)
|
||||
|
||||
if is_mac or is_win:
|
||||
if not self.uiRemoteRadioButton.isEnabled() \
|
||||
and not self.uiVMRadioButton.isEnabled() \
|
||||
and not self.uiLocalRadioButton.isEnabled():
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, "No GNS3 VM available.",
|
||||
"GNS3 VM is not available, please configure GNS3 VM before adding new Appliance.")
|
||||
|
||||
elif self.page(page_id) == self.uiFilesWizardPage:
|
||||
self._registry.getRemoteImageList(self._appliance.emulator(), self._compute_id)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from gns3.local_config import LocalConfig
|
||||
from gns3.ui.console_command_dialog_ui import Ui_uiConsoleCommandDialog
|
||||
from gns3.settings import PRECONFIGURED_TELNET_CONSOLE_COMMANDS, \
|
||||
PRECONFIGURED_VNC_CONSOLE_COMMANDS, \
|
||||
PRECONFIGURED_SPICE_CONSOLE_COMMANDS, \
|
||||
CUSTOM_CONSOLE_COMMANDS_SETTINGS
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
|
||||
def __init__(self, parent, console_type="telnet", current=None):
|
||||
"""
|
||||
:params console_type: telnet, serial or vnc
|
||||
:params console_type: telnet, serial, vnc or spice
|
||||
:params current: Current console command
|
||||
"""
|
||||
super().__init__(parent)
|
||||
@@ -62,6 +63,9 @@ class ConsoleCommandDialog(QtWidgets.QDialog, Ui_uiConsoleCommandDialog):
|
||||
elif self._console_type == "vnc":
|
||||
self._consoles = copy.copy(PRECONFIGURED_VNC_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
elif self._console_type == "spice":
|
||||
self._consoles = copy.copy(PRECONFIGURED_SPICE_CONSOLE_COMMANDS)
|
||||
self._consoles.update(self._settings[self._console_type])
|
||||
|
||||
self.uiCommandComboBox.clear()
|
||||
self.uiCommandComboBox.addItem("Custom", "")
|
||||
|
||||
@@ -136,22 +136,15 @@ class DoctorDialog(QtWidgets.QDialog, Ui_DoctorDialog):
|
||||
if not os.path.exists(path):
|
||||
return (2, "Ubridge path {path} doesn't exists".format(path=path))
|
||||
|
||||
request_setuid = False
|
||||
if sys.platform.startswith("linux"):
|
||||
try:
|
||||
if "security.capability" in os.listxattr(path):
|
||||
caps = os.getxattr(path, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
return (2, "Ubridge requires CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
|
||||
else:
|
||||
# capabilities not supported
|
||||
request_setuid = True
|
||||
except AttributeError:
|
||||
if "security.capability" not in os.listxattr(path) or not struct.unpack("<IIIII", os.getxattr(path, "security.capability"))[1] & 1 << 13:
|
||||
return (2, "Ubridge requires CAP_NET_RAW. Run sudo setcap cap_net_admin,cap_net_raw=ep {path}".format(path=path))
|
||||
except (OSError, AttributeError) as e:
|
||||
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
|
||||
return (1, "Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)".format(path=path))
|
||||
return (1, "Could not determine if CAP_NET_RAW capability is set for uBridge: {}".format(e))
|
||||
|
||||
if sys.platform.startswith("darwin") or request_setuid:
|
||||
if sys.platform.startswith("darwin"):
|
||||
if os.stat(path).st_uid != 0 or not os.stat(path).st_mode & stat.S_ISUID:
|
||||
return (2, "Ubridge should be setuid. Run sudo chown root:admin {path} and sudo chmod 4750 {path}".format(path=path))
|
||||
return (0, None)
|
||||
|
||||
@@ -41,9 +41,6 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
self.uiServerHostLineEdit.setText(self._compute.host())
|
||||
self.uiServerPortSpinBox.setValue(self._compute.port())
|
||||
|
||||
index = self.uiServerProtocolComboBox.findText(self._compute.protocol().upper())
|
||||
self.uiServerProtocolComboBox.setCurrentIndex(index)
|
||||
|
||||
if self._compute.user():
|
||||
self.uiEnableAuthenticationCheckBox.setChecked(True)
|
||||
self.uiServerUserLineEdit.setText(self._compute.user())
|
||||
@@ -81,7 +78,7 @@ class EditComputeDialog(QtWidgets.QDialog, Ui_EditComputeDialog):
|
||||
|
||||
host = self.uiServerHostLineEdit.text().strip()
|
||||
name = self.uiServerNameLineEdit.text().strip()
|
||||
protocol = self.uiServerProtocolComboBox.currentText().lower()
|
||||
protocol = "http"
|
||||
port = self.uiServerPortSpinBox.value()
|
||||
user = self.uiServerUserLineEdit.text().strip()
|
||||
password = self.uiServerPasswordLineEdit.text().strip()
|
||||
|
||||
@@ -62,7 +62,7 @@ class ExportDebugDialog(QtWidgets.QDialog, Ui_ExportDebugDialog):
|
||||
self._exportDebugCallback({}, error=True)
|
||||
|
||||
def _exportDebugCallback(self, result, error=False, **kwargs):
|
||||
log.info("Export debug information to %s", self._path)
|
||||
log.debug("Export debug information to %s", self._path)
|
||||
|
||||
try:
|
||||
with ZipFile(self._path, 'w') as zip:
|
||||
|
||||
174
gns3/dialogs/filter_dialog.py
Normal file
174
gns3/dialogs/filter_dialog.py
Normal file
@@ -0,0 +1,174 @@
|
||||
# -*- 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 ..qt import QtGui, QtWidgets, qslot
|
||||
from ..ui.filter_dialog_ui import Ui_FilterDialog
|
||||
|
||||
|
||||
class FilterDialog(QtWidgets.QDialog, Ui_FilterDialog):
|
||||
|
||||
"""
|
||||
Filter dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, link):
|
||||
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
self._link = link
|
||||
self._link.updated_link_signal.connect(self._updateUiSlot)
|
||||
self._link.listAvailableFilters(self._listAvailableFiltersCallback)
|
||||
self._initialized = False
|
||||
self._filter_items = {}
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._helpSlot)
|
||||
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(self._resetSlot)
|
||||
|
||||
def _listAvailableFiltersCallback(self, result, error=False, *args, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Link", "Error while listing information about the link: {}".format(result["message"]))
|
||||
return
|
||||
self._filters = result
|
||||
self._initialized = True
|
||||
self._updateUiSlot()
|
||||
|
||||
@qslot
|
||||
def _updateUiSlot(self, *args):
|
||||
|
||||
# Empty the main layout
|
||||
while True:
|
||||
item = self.uiVerticalLayout.takeAt(0)
|
||||
if item is None:
|
||||
break
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
if len(self._filters) == 0:
|
||||
QtWidgets.QMessageBox.critical(self, "Link", "No filter available for this link. Try with a different node type.")
|
||||
self.reject()
|
||||
|
||||
self._tabWidget = QtWidgets.QTabWidget(self)
|
||||
for i, filter in enumerate(self._filters):
|
||||
tab = QtWidgets.QWidget()
|
||||
self._tabWidget.addTab(tab, filter['name'])
|
||||
self._tabWidget.setTabToolTip(i, filter['description'])
|
||||
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_red.svg'))
|
||||
vlayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
gridLayout = QtWidgets.QGridLayout()
|
||||
line = 0
|
||||
filter["spinBoxes"] = []
|
||||
filter["textEdits"] = []
|
||||
|
||||
nb_spin = 0
|
||||
|
||||
for param in filter["parameters"]:
|
||||
label = QtWidgets.QLabel()
|
||||
label.setText(param["name"] + ":")
|
||||
gridLayout.addWidget(label, line, 0, 1, 1)
|
||||
|
||||
if param["type"] == "int":
|
||||
spinBox = QtWidgets.QSpinBox()
|
||||
filter["spinBoxes"].append(spinBox)
|
||||
spinBox.setMinimum(param["minimum"])
|
||||
spinBox.setMaximum(param["maximum"])
|
||||
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(spinBox.sizePolicy().hasHeightForWidth())
|
||||
spinBox.setSizePolicy(sizePolicy)
|
||||
try:
|
||||
value = self._link.filters()[filter["type"]][nb_spin]
|
||||
spinBox.setValue(value)
|
||||
if value != 0:
|
||||
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
except(KeyError, IndexError):
|
||||
pass
|
||||
nb_spin += 1
|
||||
gridLayout.addWidget(spinBox, line, 1, 1, 1)
|
||||
unit = QtWidgets.QLabel()
|
||||
unit.setText(param["unit"])
|
||||
gridLayout.addWidget(unit, line, 2, 1, 1)
|
||||
elif param["type"] == "text":
|
||||
textEdit = QtWidgets.QTextEdit()
|
||||
textEdit.setAcceptRichText(False)
|
||||
filter["textEdits"].append(textEdit)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
textEdit.setMinimumWidth(300)
|
||||
textEdit.setSizePolicy(sizePolicy)
|
||||
try:
|
||||
text = self._link.filters()[filter["type"]][0]
|
||||
textEdit.setPlainText(text)
|
||||
if text:
|
||||
self._tabWidget.setTabIcon(i, QtGui.QIcon(':/icons/led_green.svg'))
|
||||
except(KeyError, IndexError):
|
||||
pass
|
||||
gridLayout.addWidget(textEdit, line, 1, 1, 1)
|
||||
|
||||
line += 1
|
||||
|
||||
spacerItem = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
gridLayout.addItem(spacerItem, line, 0, 1, 1)
|
||||
vlayout.addLayout(gridLayout)
|
||||
tab.setLayout(vlayout)
|
||||
|
||||
self.uiVerticalLayout.addWidget(self._tabWidget)
|
||||
|
||||
@qslot
|
||||
def _applyPreferencesSlot(self, *args):
|
||||
new_filters = {}
|
||||
for filter in self._filters:
|
||||
new_filters[filter["type"]] = []
|
||||
for spinBox in filter["spinBoxes"]:
|
||||
new_filters[filter["type"]].append(spinBox.value())
|
||||
for spinBox in filter["textEdits"]:
|
||||
new_filters[filter["type"]].append(spinBox.toPlainText())
|
||||
self._link.setFilters(new_filters)
|
||||
self._link.update()
|
||||
|
||||
@qslot
|
||||
def _helpSlot(self, *args):
|
||||
help_text = "Filters are applied to packets in both direction.\n\n"
|
||||
|
||||
filter_nb = 0
|
||||
for filter in self._filters:
|
||||
help_text += "{}: {}".format(filter["name"], filter["description"])
|
||||
filter_nb += 1
|
||||
if len(self._filters) != filter_nb:
|
||||
help_text += "\n\n"
|
||||
|
||||
QtWidgets.QMessageBox.information(self, "Help for filters", help_text)
|
||||
|
||||
@qslot
|
||||
def _resetSlot(self, *args):
|
||||
|
||||
filters = {}
|
||||
self._link.setFilters(filters)
|
||||
self._link.update()
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
Called when the dialog is closed.
|
||||
|
||||
:param result: boolean (accepted or rejected)
|
||||
"""
|
||||
if result and self._initialized:
|
||||
self._applyPreferencesSlot()
|
||||
super().done(result)
|
||||
191
gns3/dialogs/notif_dialog.py
Normal file
191
gns3/dialogs/notif_dialog.py
Normal file
@@ -0,0 +1,191 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 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/>.
|
||||
|
||||
"""
|
||||
Display error to the user in an overlay popup
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore, qslot
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
MAX_ELEMENTS = 3
|
||||
DISPLAY_DURATION = {
|
||||
"CRITICAL": 120,
|
||||
"ERROR": 120,
|
||||
"WARNING": 20,
|
||||
"INFO": 5
|
||||
}
|
||||
|
||||
|
||||
class NotifDialogHandler(logging.StreamHandler):
|
||||
|
||||
def __init__(self, dialog):
|
||||
super().__init__()
|
||||
self._dialog = dialog
|
||||
self.setLevel(logging.INFO)
|
||||
self._dialog.show()
|
||||
|
||||
def emit(self, record):
|
||||
self._dialog.addNotif(record.levelname, record.getMessage())
|
||||
|
||||
|
||||
class NotifDialog(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self._notifs = []
|
||||
|
||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint |
|
||||
QtCore.Qt.WindowDoesNotAcceptFocus |
|
||||
QtCore.Qt.SubWindow)
|
||||
# QtCore.Qt.Tool)
|
||||
# QtCore.Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) # | QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
self._layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.setInterval(1000)
|
||||
self._timer.timeout.connect(self._refreshSlot)
|
||||
self._timer.start()
|
||||
|
||||
for i in range(0, MAX_ELEMENTS):
|
||||
l = QtWidgets.QLabel()
|
||||
l.setAlignment(QtCore.Qt.AlignTop)
|
||||
l.setWordWrap(True)
|
||||
l.hide()
|
||||
self._layout.addWidget(l)
|
||||
self.setLayout(self._layout)
|
||||
|
||||
@qslot
|
||||
def addNotif(self, level, message):
|
||||
if not self.parent().settings()["overlay_notifications"]:
|
||||
return
|
||||
|
||||
# This unicode char prevent the wordwrap at /
|
||||
message = message.replace("/", "\u2060/\u2060")
|
||||
if len(self._notifs) == MAX_ELEMENTS:
|
||||
self._notifs.pop(0)
|
||||
self._notifs.append((level, message, time.time()))
|
||||
self.update()
|
||||
|
||||
@qslot
|
||||
def _refreshSlot(self):
|
||||
"""
|
||||
Hide the notifs after some delay
|
||||
"""
|
||||
notifs = []
|
||||
for (i, (level, message, when)) in enumerate(self._notifs):
|
||||
if when + DISPLAY_DURATION[level] > time.time():
|
||||
notifs.append((level, message, when))
|
||||
if notifs != self._notifs:
|
||||
self._notifs = notifs
|
||||
self.update()
|
||||
elif len(notifs) > 0:
|
||||
self.resize()
|
||||
|
||||
def update(self):
|
||||
if len(self._notifs) == 0:
|
||||
self.hide()
|
||||
else:
|
||||
for (i, (level, message, when)) in enumerate(self._notifs):
|
||||
w = self._layout.itemAt(i).widget()
|
||||
w.setText(message)
|
||||
if level == "ERROR" or level == "CRITICAL":
|
||||
w.setStyleSheet("""
|
||||
color: black;
|
||||
padding-left: 12px;
|
||||
background-color: rgb(247, 205, 198);
|
||||
border-left: 10px solid red;
|
||||
""")
|
||||
elif level == "WARNING":
|
||||
w.setStyleSheet("""
|
||||
color: black;
|
||||
padding-left: 12px;
|
||||
background-color: #f4f2b5;
|
||||
border-left: 10px solid orange;
|
||||
""")
|
||||
elif level == "INFO":
|
||||
w.setStyleSheet("""
|
||||
color: black;
|
||||
padding-left: 12px;
|
||||
background-color: #cfffc9;
|
||||
border-left: 10px solid green;
|
||||
""")
|
||||
|
||||
w.show()
|
||||
for i in range(i + 1, MAX_ELEMENTS):
|
||||
w = self._layout.itemAt(i).widget()
|
||||
w.hide()
|
||||
|
||||
self.resize()
|
||||
self.show()
|
||||
|
||||
def resize(self):
|
||||
x = self.parent().width() - self.width() - 10
|
||||
y = 10
|
||||
self.setGeometry(x, y, self.sizeHint().width(), self.sizeHint().height())
|
||||
|
||||
@qslot
|
||||
def mousePressEvent(self, event):
|
||||
self._notifs.clear()
|
||||
self.update()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
"""
|
||||
A demo main for testing the features
|
||||
"""
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
class MainWindow(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
l1 = QtWidgets.QLabel()
|
||||
l1.setText("Hello World")
|
||||
|
||||
vbox = QtWidgets.QVBoxLayout()
|
||||
vbox.addWidget(l1)
|
||||
self.setLayout(vbox)
|
||||
self.setStyleSheet("background-color:blue;")
|
||||
self._dialog = NotifDialog(self)
|
||||
log.addHandler(NotifDialogHandler(self._dialog))
|
||||
log.info("test")
|
||||
|
||||
def moveEvent(self, event):
|
||||
log.error("An error")
|
||||
log.info("An info with an url http://test")
|
||||
log.warning("A warning with a long long long longlong longlong longlong longlong longlong longlong longlong long message")
|
||||
self._dialog.update()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._dialog.update()
|
||||
|
||||
main = MainWindow()
|
||||
main.setMinimumWidth(600)
|
||||
main.setMinimumHeight(600)
|
||||
main.show()
|
||||
exit_code = app.exec_()
|
||||
@@ -144,7 +144,7 @@ class ProjectDialog(QtWidgets.QDialog, Ui_ProjectDialog):
|
||||
|
||||
def _duplicateCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
log.error("Error while duplicate project: {}".format(result["message"]))
|
||||
log.error("Error while duplicating project: {}".format(result["message"]))
|
||||
return
|
||||
Controller.instance().refreshProjectList()
|
||||
|
||||
|
||||
@@ -216,7 +216,6 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
self.uiRemoteMainServerUserLineEdit.setText(local_server_settings["user"])
|
||||
self.uiRemoteMainServerPasswordLineEdit.setText(local_server_settings["password"])
|
||||
self.uiRemoteMainServerPortSpinBox.setValue(local_server_settings["port"])
|
||||
self.uiRemoteMainServerProtocolComboBox.setCurrentText(local_server_settings["protocol"])
|
||||
elif self.page(page_id) == self.uiLocalServerStatusWizardPage:
|
||||
self._refreshLocalServerStatusSlot()
|
||||
|
||||
@@ -331,7 +330,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
|
||||
local_server_settings["auto_start"] = False
|
||||
local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
|
||||
local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
|
||||
local_server_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText()
|
||||
local_server_settings["protocol"] = "http"
|
||||
local_server_settings["user"] = self.uiRemoteMainServerUserLineEdit.text()
|
||||
local_server_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
|
||||
local_server_settings["auth"] = self.uiRemoteMainServerAuthCheckBox.isChecked()
|
||||
|
||||
@@ -52,12 +52,18 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
# use the first item in the list as the model
|
||||
first_item = items[0]
|
||||
pen = first_item.pen()
|
||||
brush = first_item.brush()
|
||||
self._color = brush.color()
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
|
||||
self._color.green(),
|
||||
self._color.blue(),
|
||||
self._color.alpha()))
|
||||
if hasattr(first_item, "brush"): # Line don't have brush
|
||||
brush = first_item.brush()
|
||||
self._color = brush.color()
|
||||
self.uiColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._color.red(),
|
||||
self._color.green(),
|
||||
self._color.blue(),
|
||||
self._color.alpha()))
|
||||
else:
|
||||
self.uiColorLabel.hide()
|
||||
self.uiColorPushButton.hide()
|
||||
self._color = None
|
||||
|
||||
self._border_color = pen.color()
|
||||
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
|
||||
self._border_color.green(),
|
||||
@@ -102,11 +108,15 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
|
||||
|
||||
border_style = QtCore.Qt.PenStyle(self.uiBorderStyleComboBox.itemData(self.uiBorderStyleComboBox.currentIndex()))
|
||||
pen = QtGui.QPen(self._border_color, self.uiBorderWidthSpinBox.value(), border_style, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
brush = QtGui.QBrush(self._color)
|
||||
if self._color:
|
||||
brush = QtGui.QBrush(self._color)
|
||||
else:
|
||||
brush = None
|
||||
|
||||
for item in self._items:
|
||||
item.setPen(pen)
|
||||
item.setBrush(brush)
|
||||
if brush:
|
||||
item.setBrush(brush)
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
|
||||
def done(self, result):
|
||||
|
||||
@@ -22,7 +22,7 @@ Graphical view on the scene where items are drawn.
|
||||
import logging
|
||||
import os
|
||||
import sip
|
||||
import pickle
|
||||
import sys
|
||||
|
||||
from .qt import QtCore, QtGui, QtNetwork, QtWidgets, qpartial, qslot
|
||||
from .items.node_item import NodeItem
|
||||
@@ -33,6 +33,7 @@ from .modules import MODULES
|
||||
from .modules.module_error import ModuleError
|
||||
from .settings import GRAPHICS_VIEW_SETTINGS
|
||||
from .topology import Topology
|
||||
from .appliance_manager import ApplianceManager
|
||||
from .dialogs.style_editor_dialog import StyleEditorDialog
|
||||
from .dialogs.text_editor_dialog import TextEditorDialog
|
||||
from .dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
@@ -55,6 +56,7 @@ from .items.text_item import TextItem
|
||||
from .items.shape_item import ShapeItem
|
||||
from .items.drawing_item import DrawingItem
|
||||
from .items.rectangle_item import RectangleItem
|
||||
from .items.line_item import LineItem
|
||||
from .items.ellipse_item import EllipseItem
|
||||
from .items.image_item import ImageItem
|
||||
|
||||
@@ -82,6 +84,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_note = False
|
||||
self._adding_rectangle = False
|
||||
self._adding_ellipse = False
|
||||
self._adding_line = False
|
||||
self._newlink = None
|
||||
self._dragging = False
|
||||
self._last_mouse_position = None
|
||||
@@ -113,6 +116,16 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
def setSceneSize(self, width, height):
|
||||
self.scene().setSceneRect(-(width / 2), -(height / 2), width, height)
|
||||
|
||||
def setZoom(self, zoom):
|
||||
"""
|
||||
Sets zoom of the Graphics View
|
||||
:param zoom:
|
||||
:return:
|
||||
"""
|
||||
if zoom:
|
||||
factor = zoom / 100.
|
||||
self.scale(factor, factor)
|
||||
|
||||
def setEnabled(self, enabled):
|
||||
|
||||
if enabled is False:
|
||||
@@ -233,6 +246,20 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._adding_ellipse = False
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addLine(self, state):
|
||||
"""
|
||||
Adds a line.
|
||||
|
||||
:param state: boolean
|
||||
"""
|
||||
|
||||
if state:
|
||||
self._adding_line = True
|
||||
self.setCursor(QtCore.Qt.PointingHandCursor)
|
||||
else:
|
||||
self._adding_line = False
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def addImage(self, image_path):
|
||||
"""
|
||||
Adds an image.
|
||||
@@ -437,6 +464,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self._main_window.uiDrawEllipseAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_ellipse = False
|
||||
elif event.button() == QtCore.Qt.LeftButton and self._adding_line:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.createDrawingItem("line", pos.x(), pos.y(), 0)
|
||||
self._main_window.uiDrawLineAction.setChecked(False)
|
||||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self._adding_line = False
|
||||
else:
|
||||
super().mousePressEvent(event)
|
||||
|
||||
@@ -474,6 +507,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if delta is not None and delta.x() == 0:
|
||||
# CTRL is pressed then use the mouse wheel to zoom in or out.
|
||||
self.scaleView(pow(2.0, delta.y() / 240.0))
|
||||
self._topology.project().setZoom(round(self.transform().m11() * 100))
|
||||
self._topology.project().update()
|
||||
else:
|
||||
super().wheelEvent(event)
|
||||
|
||||
@@ -486,6 +521,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
if factor < 0.10 or factor > 10:
|
||||
return
|
||||
self.scale(scale_factor, scale_factor)
|
||||
self._main_window.uiStatusBar.showMessage("Zoom: {}%".format(round(self.transform().m11() * 100)), 2000)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
@@ -554,6 +590,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
self.configureSlot()
|
||||
return
|
||||
else:
|
||||
if sys.platform.startswith("win") and item.node().bringToFront():
|
||||
return
|
||||
self.consoleFromItems(self.scene().selectedItems())
|
||||
return
|
||||
elif isinstance(item, NoteItem) and isinstance(item.parentItem(), NodeItem):
|
||||
@@ -587,7 +625,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
# check if what is dragged is handled by this view
|
||||
if event.mimeData().hasFormat("application/x-gns3-node") or event.mimeData().hasFormat("text/uri-list"):
|
||||
if event.mimeData().hasFormat("text/uri-list") \
|
||||
or event.mimeData().hasFormat("application/x-gns3-appliance"):
|
||||
event.acceptProposedAction()
|
||||
event.accept()
|
||||
else:
|
||||
@@ -601,10 +640,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
"""
|
||||
|
||||
# check if what has been dropped is handled by this view
|
||||
if event.mimeData().hasFormat("application/x-gns3-node"):
|
||||
data = event.mimeData().data("application/x-gns3-node")
|
||||
# load the pickled node data
|
||||
node_data = pickle.loads(data)
|
||||
if event.mimeData().hasFormat("application/x-gns3-appliance"):
|
||||
appliance_id = event.mimeData().data("application/x-gns3-appliance").data().decode()
|
||||
event.setDropAction(QtCore.Qt.CopyAction)
|
||||
event.accept()
|
||||
if event.keyboardModifiers() == QtCore.Qt.ShiftModifier:
|
||||
@@ -615,12 +652,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
for node_number in range(integer):
|
||||
x = event.pos().x() - (150 / 2) + (node_number % max_nodes_per_line) * offset
|
||||
y = event.pos().y() - (70 / 2) + (node_number // max_nodes_per_line) * offset
|
||||
node_item = self.createNode(node_data, QtCore.QPoint(x, y))
|
||||
if node_item is None:
|
||||
# stop if there is any error
|
||||
if self.createNodeFromApplianceId(appliance_id, QtCore.QPoint(x, y)) is False:
|
||||
event.ignore()
|
||||
break
|
||||
else:
|
||||
self.createNode(node_data, event.pos())
|
||||
if self.createNodeFromApplianceId(appliance_id, event.pos()) is False:
|
||||
event.ignore()
|
||||
elif event.mimeData().hasFormat("text/uri-list") and event.mimeData().hasUrls():
|
||||
# This should not arrive but we received bug report with it...
|
||||
if len(event.mimeData().urls()) == 0:
|
||||
@@ -678,6 +715,12 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
change_symbol_action.triggered.connect(self.changeSymbolActionSlot)
|
||||
menu.addAction(change_symbol_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, DrawingItem) or isinstance(item, NodeItem), items)):
|
||||
duplicate_action = QtWidgets.QAction("Duplicate", menu)
|
||||
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
|
||||
duplicate_action.triggered.connect(self.duplicateActionSlot)
|
||||
menu.addAction(duplicate_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "nodeDir"), items)):
|
||||
# Action: Show in file manager
|
||||
show_in_file_manager_action = QtWidgets.QAction("Show in file manager", menu)
|
||||
@@ -703,6 +746,13 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
|
||||
menu.addAction(aux_console_action)
|
||||
|
||||
if sys.platform.startswith("win") and True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"), items)):
|
||||
# Action: bring console or window to front (Windows only)
|
||||
bring_to_front_action = QtWidgets.QAction("Bring to front", menu)
|
||||
bring_to_front_action.setIcon(QtGui.QIcon(':/icons/front.svg'))
|
||||
bring_to_front_action.triggered.connect(self.bringToFrontSlot)
|
||||
menu.addAction(bring_to_front_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configFiles"), items)):
|
||||
import_config_action = QtWidgets.QAction("Import config", menu)
|
||||
import_config_action.setIcon(QtGui.QIcon(':/icons/import_config.svg'))
|
||||
@@ -757,12 +807,6 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
reload_action.triggered.connect(self.reloadActionSlot)
|
||||
menu.addAction(reload_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, DrawingItem), items)):
|
||||
duplicate_action = QtWidgets.QAction("Duplicate", menu)
|
||||
duplicate_action.setIcon(QtGui.QIcon(':/icons/new.svg'))
|
||||
duplicate_action.triggered.connect(self.duplicateActionSlot)
|
||||
menu.addAction(duplicate_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NoteItem), items)):
|
||||
text_edit_action = QtWidgets.QAction("Text edit", menu)
|
||||
text_edit_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
|
||||
@@ -775,7 +819,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
text_edit_action.triggered.connect(self.textEditActionSlot)
|
||||
menu.addAction(text_edit_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, ShapeItem), items)):
|
||||
if True in list(map(lambda item: isinstance(item, ShapeItem) or isinstance(item, LineItem), items)):
|
||||
style_action = QtWidgets.QAction("Style", menu)
|
||||
style_action.setIcon(QtGui.QIcon(':/icons/drawing.svg'))
|
||||
style_action.triggered.connect(self.styleActionSlot)
|
||||
@@ -952,6 +996,11 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
# returns True to ignore this node.
|
||||
return True
|
||||
|
||||
# TightVNC has lack support of IPv6 host at this moment
|
||||
if "vncviewer" in node.consoleCommand() and ":" in node.consoleHost():
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, "TightVNC", "TightVNC (vncviewer) may not start because of lack of IPv6 support.")
|
||||
|
||||
try:
|
||||
node.openConsole(aux=aux)
|
||||
except (OSError, ValueError) as e:
|
||||
@@ -1006,7 +1055,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
console_type = "telnet"
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
|
||||
if item.node().consoleType() not in ("telnet", "serial", "vnc"):
|
||||
if item.node().consoleType() not in ("telnet", "serial", "vnc", "spice"):
|
||||
continue
|
||||
current_cmd = item.node().consoleCommand()
|
||||
console_type = item.node().consoleType()
|
||||
@@ -1016,7 +1065,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
|
||||
node = item.node()
|
||||
if node.consoleType() not in ("telnet", "serial", "vnc"):
|
||||
if node.consoleType() not in ("telnet", "serial", "vnc", "spice"):
|
||||
continue
|
||||
try:
|
||||
node.openConsole(command=cmd)
|
||||
@@ -1171,6 +1220,16 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
def bringToFrontSlot(self):
|
||||
"""
|
||||
Slot to receive events from the bring to front action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"):
|
||||
item.node().bringToFront()
|
||||
|
||||
def idlepcActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the idlepc action in the
|
||||
@@ -1195,7 +1254,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC", "Error: {}".format(result["message"]))
|
||||
else:
|
||||
router = context["router"]
|
||||
log.info("{} has received Idle-PC proposals".format(router.name()))
|
||||
log.debug("{} has received Idle-PC proposals".format(router.name()))
|
||||
idlepcs = result
|
||||
if idlepcs and idlepcs[0] != "0x0":
|
||||
dialog = IdlePCDialog(router, idlepcs, parent=self)
|
||||
@@ -1229,7 +1288,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
router = context["router"]
|
||||
idlepc = result["idlepc"]
|
||||
log.info("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
|
||||
log.debug("{} has received the auto idle-pc value: {}".format(router.name(), idlepc))
|
||||
router.setIdlepc(idlepc)
|
||||
# apply Idle-PC to all routers with the same IOS image
|
||||
ios_image = os.path.basename(router.settings()["image"])
|
||||
@@ -1259,6 +1318,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
type = "image"
|
||||
self.createDrawingItem(type, item.pos().x() + 20, item.pos().y() + 20, item.zValue(), rotation=item.rotation(), svg=item.toSvg())
|
||||
elif isinstance(item, NodeItem):
|
||||
item.node().duplicate(item.pos().x() + 20, item.pos().y() + 20, item.zValue())
|
||||
|
||||
def styleActionSlot(self):
|
||||
"""
|
||||
@@ -1268,7 +1329,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, ShapeItem):
|
||||
if isinstance(item, ShapeItem) or isinstance(item, LineItem):
|
||||
items.append(item)
|
||||
if items:
|
||||
style_dialog = StyleEditorDialog(self._main_window, items)
|
||||
@@ -1413,48 +1474,18 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
raise ModuleError("Please select a server")
|
||||
return server
|
||||
|
||||
def createNode(self, node_data, pos):
|
||||
def createNodeFromApplianceId(self, appliance_id, pos):
|
||||
"""
|
||||
Creates a new node on the scene.
|
||||
|
||||
:param node_data: node data to create a new node
|
||||
:param pos: position of the drop event
|
||||
|
||||
:returns: NodeItem instance
|
||||
Ask the server to create a node using this appliance
|
||||
"""
|
||||
try:
|
||||
node_module = None
|
||||
for module in MODULES:
|
||||
instance = module.instance()
|
||||
node_class = module.getNodeClass(node_data["class"])
|
||||
if node_class in instance.classes():
|
||||
node_module = instance
|
||||
break
|
||||
|
||||
if not node_module:
|
||||
raise ModuleError("Could not find any module for {}".format(node_class))
|
||||
if self._topology.project() is None:
|
||||
return
|
||||
node = node_module.instantiateNode(node_class, self.allocateCompute(node_data, instance), self._topology.project())
|
||||
# If no server is available a ValueError is raised
|
||||
except (ModuleError, ValueError) as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
|
||||
return
|
||||
|
||||
pos = self.mapToScene(pos)
|
||||
node_item = self.createNodeItem(node, node_data["symbol"], pos.x(), pos.y())
|
||||
node.setGraphics(node_item)
|
||||
try:
|
||||
node_module.createNode(node, node_data["name"])
|
||||
except ModuleError as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Node creation", "{}".format(e))
|
||||
return
|
||||
return node_item
|
||||
return ApplianceManager().instance().createNodeFromApplianceId(self._topology.project(), appliance_id, pos.x(), pos.y())
|
||||
|
||||
def createNodeItem(self, node, symbol, x, y):
|
||||
node.setSymbol(symbol)
|
||||
node.setPos(x, y)
|
||||
node_item = NodeItem(node)
|
||||
|
||||
self.scene().addItem(node_item)
|
||||
self._topology.addNode(node)
|
||||
|
||||
@@ -1482,6 +1513,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
|
||||
item = EllipseItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "rect":
|
||||
item = RectangleItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "line":
|
||||
item = LineItem(pos=QtCore.QPoint(x, y), dst=QtCore.QPoint(200, 0), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "image":
|
||||
item = ImageItem(pos=QtCore.QPoint(x, y), z=z, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg)
|
||||
elif type == "text":
|
||||
|
||||
@@ -27,7 +27,7 @@ import ipaddress
|
||||
import urllib.request
|
||||
|
||||
from .version import __version__, __version_info__
|
||||
from .qt import QtCore, QtNetwork, qpartial, sip_is_deleted
|
||||
from .qt import QtCore, QtNetwork, qpartial, sip_is_deleted, QtWebSockets
|
||||
from .utils import parse_version
|
||||
|
||||
import logging
|
||||
@@ -95,6 +95,8 @@ class HTTPClient(QtCore.QObject):
|
||||
# List of query waiting for the connection
|
||||
self._query_waiting_connections = []
|
||||
|
||||
self._websocket = QtWebSockets.QWebSocket()
|
||||
|
||||
def setMaxTimeDifferenceBetweenQueries(self, value):
|
||||
self._max_time_difference_between_queries = value
|
||||
|
||||
@@ -309,7 +311,7 @@ class HTTPClient(QtCore.QObject):
|
||||
self._query_waiting_connections.append((request, callback))
|
||||
# If we are not connected and we enqueue the first query we open the conection
|
||||
if len(self._query_waiting_connections) == 1:
|
||||
log.info("Connection to {}".format(self.url()))
|
||||
log.debug("Connection to {}".format(self.url()))
|
||||
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5, showProgress=False)
|
||||
|
||||
def _connectionError(self, callback, msg="", server=None):
|
||||
@@ -441,6 +443,30 @@ class HTTPClient(QtCore.QObject):
|
||||
request.setRawHeader(b"Authorization", auth_string.encode())
|
||||
return request
|
||||
|
||||
def connectWebSocket(self, path, prefix="/v2"):
|
||||
"""
|
||||
Path of the websocket endpoint
|
||||
"""
|
||||
host = self._getHostForQuery()
|
||||
request = self._websocket.request()
|
||||
request.setUrl(QtCore.QUrl("ws://{host}:{port}{prefix}{path}".format(host=host, port=self._port, path=path, prefix=prefix)))
|
||||
self._addAuth(request)
|
||||
self._websocket.open(request)
|
||||
return self._websocket
|
||||
|
||||
def _getHostForQuery(self):
|
||||
"""
|
||||
Get hostname that could be use by Qt
|
||||
"""
|
||||
try:
|
||||
ip = self._host.rsplit('%', 1)[0]
|
||||
ipaddress.IPv6Address(ip) # remove any scope ID
|
||||
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
|
||||
host = "[{}]".format(ip)
|
||||
except ipaddress.AddressValueError:
|
||||
host = self._host
|
||||
return host
|
||||
|
||||
def _executeHTTPQuery(self, method, path, callback, body, context={}, downloadProgressCallback=None, showProgress=True, ignoreErrors=False, progressText=None, server=None, timeout=120, prefix="/v2", params={}, networkManager=None, **kwargs):
|
||||
"""
|
||||
Call the remote server
|
||||
@@ -461,14 +487,7 @@ class HTTPClient(QtCore.QObject):
|
||||
:returns: QNetworkReply
|
||||
"""
|
||||
|
||||
try:
|
||||
ip = self._host.rsplit('%', 1)[0]
|
||||
ipaddress.IPv6Address(ip) # remove any scope ID
|
||||
# this is an IPv6 address, we must surround it with brackets to be used with QUrl.
|
||||
host = "[{}]".format(ip)
|
||||
except ipaddress.AddressValueError:
|
||||
host = self._host
|
||||
|
||||
host = self._getHostForQuery()
|
||||
if params == {}:
|
||||
query_string = ""
|
||||
else:
|
||||
@@ -607,7 +626,7 @@ class HTTPClient(QtCore.QObject):
|
||||
if not body or content_type != "application/json":
|
||||
callback({"message": error_message}, error=True, server=server, context=context)
|
||||
else:
|
||||
log.debug(body)
|
||||
# log.debug(body)
|
||||
try:
|
||||
callback(json.loads(body), error=True, server=server, context=context)
|
||||
except ValueError:
|
||||
@@ -637,7 +656,7 @@ class HTTPClient(QtCore.QObject):
|
||||
except UnicodeDecodeError:
|
||||
body = None
|
||||
content_type = response.header(QtNetwork.QNetworkRequest.ContentTypeHeader)
|
||||
log.debug(body)
|
||||
# log.debug(body)
|
||||
if body and len(body.strip(" \n\t")) > 0 and content_type == "application/json":
|
||||
try:
|
||||
params = json.loads(body)
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
# 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 ..qt import QtCore, QtWidgets, qslot
|
||||
from ..qt import QtCore, QtWidgets, qslot, QtGui
|
||||
from .utils import colorFromSvg
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
@@ -24,6 +25,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DrawingItem:
|
||||
# Map QT stroke to SVG style
|
||||
QT_DASH_TO_SVG = {
|
||||
QtCore.Qt.SolidLine: "",
|
||||
QtCore.Qt.NoPen: None,
|
||||
QtCore.Qt.DashLine: "25, 25",
|
||||
QtCore.Qt.DotLine: "5, 25",
|
||||
QtCore.Qt.DashDotLine: "5, 25, 25",
|
||||
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
|
||||
}
|
||||
|
||||
show_layer = False
|
||||
|
||||
@@ -215,3 +225,48 @@ class DrawingItem:
|
||||
painter.setPen(QtCore.Qt.black)
|
||||
zval = str(int(self.zValue()))
|
||||
painter.drawText(QtCore.QPointF(center.x() - 4, center.y() + 4), zval)
|
||||
|
||||
def _styleSvg(self, element):
|
||||
"""
|
||||
Add style from the shape item to the SVG element that we will
|
||||
export
|
||||
"""
|
||||
style = ""
|
||||
pen = self.pen()
|
||||
if hasattr(self, "brush"): # Line don't have a brush
|
||||
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
|
||||
element.set("fill-opacity", str(self.brush().color().alphaF()))
|
||||
|
||||
dasharray = self.QT_DASH_TO_SVG[pen.style()]
|
||||
if dasharray is None: # No border to the element
|
||||
return element
|
||||
elif dasharray == "":
|
||||
pass # Solid line
|
||||
else:
|
||||
element.set("stroke-dasharray", dasharray)
|
||||
element.set("stroke-width", str(pen.width()))
|
||||
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
|
||||
return element
|
||||
|
||||
def _penFromSVGElement(self, svg):
|
||||
"""
|
||||
Get a pen from a SVG element
|
||||
|
||||
:param svg:
|
||||
"""
|
||||
pen = QtGui.QPen()
|
||||
if svg.get("stroke-width"):
|
||||
pen.setWidth(int(svg.get("stroke-width")))
|
||||
if svg.get("stroke"):
|
||||
pen.setColor(colorFromSvg(svg.get("stroke")))
|
||||
# Map SVG stroke style (border of the element to the Qt version)
|
||||
if not svg.get("stroke"):
|
||||
pen.setStyle(QtCore.Qt.NoPen)
|
||||
else:
|
||||
pen.setStyle(QtCore.Qt.SolidLine)
|
||||
stroke = svg.get("stroke-dasharray")
|
||||
if stroke:
|
||||
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
|
||||
if svg_stroke == stroke:
|
||||
pen.setStyle(qt_stroke)
|
||||
return pen
|
||||
|
||||
@@ -112,14 +112,14 @@ class EthernetLinkItem(LinkItem):
|
||||
if self.length < 100:
|
||||
return
|
||||
|
||||
if self._source_port.status() == Port.started:
|
||||
if self._link.suspended() or self._source_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._source_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._source_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
@@ -153,14 +153,14 @@ class EthernetLinkItem(LinkItem):
|
||||
|
||||
painter.drawPoint(point1)
|
||||
|
||||
if self._destination_port.status() == Port.started:
|
||||
if self._link.suspended() or self._destination_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
@@ -194,4 +194,4 @@ class EthernetLinkItem(LinkItem):
|
||||
|
||||
painter.drawPoint(point2)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
self._drawSymbol()
|
||||
|
||||
216
gns3/items/line_item.py
Normal file
216
gns3/items/line_item.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Graphical representation of a rectangle on the QGraphicsScene.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWidgets
|
||||
from .drawing_item import DrawingItem
|
||||
|
||||
|
||||
class LineItem(QtWidgets.QGraphicsLineItem, DrawingItem):
|
||||
|
||||
"""
|
||||
Class to draw a rectangle on the scene.
|
||||
"""
|
||||
|
||||
def __init__(self, dst=None, svg=None, **kws):
|
||||
super().__init__(svg=svg, **kws)
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
self._edge = None
|
||||
self._border = 20
|
||||
|
||||
if svg is None:
|
||||
if dst is not None:
|
||||
self.setLine(0,
|
||||
0,
|
||||
dst.x(),
|
||||
dst.y())
|
||||
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.setPen(pen)
|
||||
else:
|
||||
self.fromSvg(svg)
|
||||
if self._id is None:
|
||||
self.create()
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""
|
||||
Paints the contents of an item in local coordinates.
|
||||
|
||||
:param painter: QPainter instance
|
||||
:param option: QStyleOptionGraphicsItem instance
|
||||
:param widget: QWidget instance
|
||||
"""
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
self.drawLayerInfo(painter)
|
||||
|
||||
def toSvg(self):
|
||||
"""
|
||||
Return an SVG version of the shape
|
||||
"""
|
||||
svg = ET.Element("svg")
|
||||
width = abs(self.line().x1() - self.line().x2())
|
||||
height = abs(self.line().y1() - self.line().y2())
|
||||
svg.set("width", str(int(width)))
|
||||
svg.set("height", str(int(height)))
|
||||
|
||||
line = ET.SubElement(svg, "line")
|
||||
line.set("x1", str(int(self.line().x1())))
|
||||
line.set("x2", str(int(self.line().x2())))
|
||||
line.set("y1", str(int(self.line().y1())))
|
||||
line.set("y2", str(int(self.line().y2())))
|
||||
line = self._styleSvg(line)
|
||||
|
||||
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
|
||||
|
||||
def fromSvg(self, svg):
|
||||
"""
|
||||
Import element informations from an SVG
|
||||
"""
|
||||
svg = ET.fromstring(svg)
|
||||
width = float(svg.get("width", 0))
|
||||
height = float(svg.get("height", 0))
|
||||
|
||||
# Backup the pos and restore it
|
||||
pos = self.pos()
|
||||
y1 = self.line().y1()
|
||||
self.setLine(0, 0, width, height)
|
||||
|
||||
pen = QtGui.QPen()
|
||||
|
||||
if len(svg):
|
||||
pen = self._penFromSVGElement(svg[0])
|
||||
self.setLine(
|
||||
float(svg[0].get("x1")),
|
||||
float(svg[0].get("y1")),
|
||||
float(svg[0].get("x2")),
|
||||
float(svg[0].get("y2"))
|
||||
)
|
||||
self.setPos(pos)
|
||||
self.setPen(pen)
|
||||
self.update()
|
||||
|
||||
def _isHorizontalLine(self):
|
||||
return abs(self.line().x1() - self.line().x2()) > abs(self.line().y1() - self.line().y2())
|
||||
|
||||
def hoverMoveEvent(self, event):
|
||||
"""
|
||||
Handles all hover move events.
|
||||
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
# objects on the background layer don't need cursors
|
||||
if self.zValue() >= 0:
|
||||
|
||||
if self._isHorizontalLine():
|
||||
if event.pos().x() > (self.line().x2() - self._border):
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
|
||||
elif event.pos().x() < self._border:
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeHorCursor)
|
||||
else:
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
|
||||
# Vertical line
|
||||
else:
|
||||
if event.pos().y() > (self.line().y2() - self._border):
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
|
||||
elif event.pos().y() < self._border:
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeVerCursor)
|
||||
else:
|
||||
self._graphics_view.setCursor(QtCore.Qt.SizeAllCursor)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""
|
||||
Handles all mouse move events.
|
||||
|
||||
:param event: QMouseEvent instance
|
||||
"""
|
||||
|
||||
self.update()
|
||||
if self._edge:
|
||||
scenePos = event.scenePos()
|
||||
if self._edge == "left" or self._edge == "bottom":
|
||||
diff_x = self.x() - scenePos.x()
|
||||
diff_y = self.y() - scenePos.y()
|
||||
self.setPos(scenePos.x(), scenePos.y())
|
||||
self.setLine(
|
||||
0,
|
||||
0,
|
||||
self.line().x2() + diff_x,
|
||||
self.line().y2() + diff_y)
|
||||
elif self._edge == "right" or self._edge == "top":
|
||||
pos = self.mapFromScene(scenePos)
|
||||
self.setLine(
|
||||
0,
|
||||
0,
|
||||
pos.x(),
|
||||
pos.y())
|
||||
self.setPos(self.x(), self.y())
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
Handles all mouse press events.
|
||||
|
||||
:param event: QMouseEvent instance
|
||||
"""
|
||||
|
||||
self.update()
|
||||
if self._isHorizontalLine():
|
||||
if event.pos().x() > (self.line().x2() - self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "right"
|
||||
elif event.pos().x() < (self.line().x1() + self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "left"
|
||||
else:
|
||||
if event.pos().y() > (self.line().y2() - self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "top"
|
||||
elif event.pos().y() < (self.line().y1() + self._border):
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False)
|
||||
self._edge = "bottom"
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
Handles all mouse release events.
|
||||
|
||||
:param: QMouseEvent instance
|
||||
"""
|
||||
|
||||
self.update()
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
|
||||
|
||||
self._edge = None
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def hoverLeaveEvent(self, event):
|
||||
"""
|
||||
Handles all hover leave events.
|
||||
|
||||
:param event: QGraphicsSceneHoverEvent instance
|
||||
"""
|
||||
|
||||
# objects on the background layer don't need cursors
|
||||
if self.zValue() >= 0:
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
@@ -24,9 +24,10 @@ import math
|
||||
from ..qt import QtCore, QtGui, QtWidgets, QtSvg, qslot
|
||||
|
||||
from ..packet_capture import PacketCapture
|
||||
from ..dialogs.filter_dialog import FilterDialog
|
||||
|
||||
|
||||
class SvgCaptureItem(QtSvg.QGraphicsSvgItem):
|
||||
class SvgIconItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
def __init__(self, symbol, parent):
|
||||
|
||||
@@ -86,11 +87,17 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
|
||||
# QGraphicsSvgItem to indicate a capture
|
||||
self._capturing_item = None
|
||||
# QGraphicsSvgItem to indicate a filter is applied
|
||||
self._filter_item = None
|
||||
# QGraphicsSvgItem to indicate we suspend a link
|
||||
self._suspend_item = None
|
||||
# QGraphicsSvgItem to indicate a filter is applied and a capture is active
|
||||
self._filter_capturing_item = None
|
||||
|
||||
if not self._adding_flag:
|
||||
# there is a destination
|
||||
self._link = link
|
||||
self._link.updated_link_signal.connect(self._drawCaptureSymbol)
|
||||
self._link.updated_link_signal.connect(self._drawSymbol)
|
||||
self._link.delete_link_signal.connect(self._linkDeletedSlot)
|
||||
self.setFlag(self.ItemIsFocusable)
|
||||
source_item.addLink(self)
|
||||
@@ -118,6 +125,16 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
if self in self.scene().items():
|
||||
self.scene().removeItem(self)
|
||||
|
||||
@qslot
|
||||
def _filterActionSlot(self, *args):
|
||||
dialog = FilterDialog(self._main_window, self._link)
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
@qslot
|
||||
def _suspendActionSlot(self, *args):
|
||||
self._link.toggleSuspend()
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete this link
|
||||
@@ -228,12 +245,32 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
analyze_action.triggered.connect(self._analyzeCaptureActionSlot)
|
||||
menu.addAction(analyze_action)
|
||||
|
||||
if self._link.suspended() is False:
|
||||
# Edit filters
|
||||
filter_action = QtWidgets.QAction("Packet filters", menu)
|
||||
filter_action.setIcon(QtGui.QIcon(':/icons/filter.svg'))
|
||||
filter_action.triggered.connect(self._filterActionSlot)
|
||||
menu.addAction(filter_action)
|
||||
|
||||
# Suspend link
|
||||
suspend_action = QtWidgets.QAction("Suspend", menu)
|
||||
suspend_action.setIcon(QtGui.QIcon(':/icons/pause.svg'))
|
||||
suspend_action.triggered.connect(self._suspendActionSlot)
|
||||
menu.addAction(suspend_action)
|
||||
else:
|
||||
# Resume link
|
||||
resume_action = QtWidgets.QAction("Resume", menu)
|
||||
resume_action.setIcon(QtGui.QIcon(':/icons/start.svg'))
|
||||
resume_action.triggered.connect(self._suspendActionSlot)
|
||||
menu.addAction(resume_action)
|
||||
|
||||
# delete
|
||||
delete_action = QtWidgets.QAction("Delete", menu)
|
||||
delete_action.setIcon(QtGui.QIcon(':/icons/delete.svg'))
|
||||
delete_action.triggered.connect(self._deleteActionSlot)
|
||||
menu.addAction(delete_action)
|
||||
|
||||
@qslot
|
||||
def mousePressEvent(self, event):
|
||||
"""
|
||||
Called when the link is clicked and shows a contextual menu.
|
||||
@@ -433,19 +470,90 @@ class LinkItem(QtWidgets.QGraphicsPathItem):
|
||||
self.update()
|
||||
|
||||
@qslot
|
||||
def _drawCaptureSymbol(self, *args):
|
||||
def _drawSymbol(self, *args):
|
||||
"""
|
||||
Draws a capture symbol in the middle of the link to indicate a capture is active.
|
||||
Draws a symbol in the middle of the link to indicate a capture, a suspend or a filter is active.
|
||||
"""
|
||||
|
||||
#FIXME: refactor ugly symbol management
|
||||
if not self._adding_flag:
|
||||
if self._link.capturing() and self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._capturing_item is None:
|
||||
self._capturing_item = SvgCaptureItem(':/icons/inspect.svg', self)
|
||||
self._capturing_item.setScale(0.6)
|
||||
self._capturing_item.setPos(link_center)
|
||||
if not self._capturing_item.isVisible():
|
||||
self._capturing_item.show()
|
||||
elif self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
|
||||
if self._link.suspended():
|
||||
if self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._suspend_item is None:
|
||||
self._suspend_item = SvgIconItem(':/icons/pause.svg', self)
|
||||
self._suspend_item.setScale(0.6)
|
||||
if not self._suspend_item.isVisible():
|
||||
self._suspend_item.show()
|
||||
self._suspend_item.setPos(link_center)
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
elif self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
|
||||
elif self._link.capturing() and len(self._link.filters()) > 0:
|
||||
if self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._filter_capturing_item is None:
|
||||
self._filter_capturing_item = SvgIconItem(':/icons/filter-capture.svg', self)
|
||||
self._filter_capturing_item.setScale(0.6)
|
||||
if not self._filter_capturing_item.isVisible():
|
||||
self._filter_capturing_item.show()
|
||||
self._filter_capturing_item.setPos(link_center)
|
||||
elif self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
|
||||
elif self._link.capturing():
|
||||
if self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._capturing_item is None:
|
||||
self._capturing_item = SvgIconItem(':/icons/inspect.svg', self)
|
||||
self._capturing_item.setScale(0.6)
|
||||
self._capturing_item.setPos(link_center)
|
||||
if not self._capturing_item.isVisible():
|
||||
self._capturing_item.show()
|
||||
elif self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
|
||||
elif len(self._link.filters()) > 0:
|
||||
if self.length >= 150:
|
||||
link_center = QtCore.QPointF(self.source.x() + self.dx / 2.0 - 11, self.source.y() + self.dy / 2.0 - 11)
|
||||
if self._filter_item is None:
|
||||
self._filter_item = SvgIconItem(':/icons/filter.svg', self)
|
||||
self._filter_item.setScale(0.6)
|
||||
if not self._filter_item.isVisible():
|
||||
self._filter_item.show()
|
||||
self._filter_item.setPos(link_center)
|
||||
elif self._filter_item:
|
||||
self._filter_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
|
||||
else:
|
||||
if self._capturing_item:
|
||||
self._capturing_item.hide()
|
||||
if self._suspend_item:
|
||||
self._suspend_item.hide()
|
||||
if self._filter_item:
|
||||
self._filter_item.hide()
|
||||
if self._filter_capturing_item:
|
||||
self._filter_capturing_item.hide()
|
||||
|
||||
@@ -114,19 +114,19 @@ class SerialLinkItem(LinkItem):
|
||||
return
|
||||
|
||||
# source point color
|
||||
if self._source_port.status() == Port.started:
|
||||
if self._link.suspended() or self._source_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
elif self._source_port.status() == Port.started:
|
||||
# port is active
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.green
|
||||
elif self._source_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
shape = QtCore.Qt.RoundCap
|
||||
color = QtCore.Qt.yellow
|
||||
else:
|
||||
shape = QtCore.Qt.SquareCap
|
||||
color = QtCore.Qt.red
|
||||
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.MiterJoin))
|
||||
painter.setPen(QtGui.QPen(color, self._point_size, QtCore.Qt.SolidLine, shape, QtCore.Qt.MiterJoin))
|
||||
|
||||
source_port_label = self._source_port.label()
|
||||
if source_port_label is None:
|
||||
@@ -143,14 +143,14 @@ class SerialLinkItem(LinkItem):
|
||||
painter.drawPoint(self.source_point)
|
||||
|
||||
# destination point color
|
||||
if self._destination_port.status() == Port.started:
|
||||
if self._link.suspended() or self._destination_port.status() == Port.suspended:
|
||||
# link or port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.started:
|
||||
# port is active
|
||||
color = QtCore.Qt.green
|
||||
shape = QtCore.Qt.RoundCap
|
||||
elif self._destination_port.status() == Port.suspended:
|
||||
# port is suspended
|
||||
color = QtCore.Qt.yellow
|
||||
shape = QtCore.Qt.RoundCap
|
||||
else:
|
||||
color = QtCore.Qt.red
|
||||
shape = QtCore.Qt.SquareCap
|
||||
@@ -172,4 +172,4 @@ class SerialLinkItem(LinkItem):
|
||||
|
||||
painter.drawPoint(self.destination_point)
|
||||
|
||||
self._drawCaptureSymbol()
|
||||
self._drawSymbol()
|
||||
|
||||
@@ -30,16 +30,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class ShapeItem(DrawingItem):
|
||||
|
||||
# Map QT stroke to SVG style
|
||||
QT_DASH_TO_SVG = {
|
||||
QtCore.Qt.SolidLine: "",
|
||||
QtCore.Qt.NoPen: None,
|
||||
QtCore.Qt.DashLine: "25, 25",
|
||||
QtCore.Qt.DotLine: "5, 25",
|
||||
QtCore.Qt.DashDotLine: "5, 25, 25",
|
||||
QtCore.Qt.DashDotDotLine: "25, 25, 5, 25, 5"
|
||||
}
|
||||
|
||||
"""
|
||||
Base class to draw shapes on the scene.
|
||||
"""
|
||||
@@ -180,27 +170,6 @@ class ShapeItem(DrawingItem):
|
||||
if self.zValue() >= 0:
|
||||
self._graphics_view.setCursor(QtCore.Qt.ArrowCursor)
|
||||
|
||||
def _styleSvg(self, element):
|
||||
"""
|
||||
Add style from the shape item to the SVG element that we will
|
||||
export
|
||||
"""
|
||||
style = ""
|
||||
pen = self.pen()
|
||||
element.set("fill", "#{}".format(hex(self.brush().color().rgba())[4:]))
|
||||
element.set("fill-opacity", str(self.brush().color().alphaF()))
|
||||
|
||||
dasharray = self.QT_DASH_TO_SVG[pen.style()]
|
||||
if dasharray is None: # No border to the element
|
||||
return element
|
||||
elif dasharray == "":
|
||||
pass # Solid line
|
||||
else:
|
||||
element.set("stroke-dasharray", dasharray)
|
||||
element.set("stroke-width", str(pen.width()))
|
||||
element.set("stroke", "#" + hex(pen.color().rgba())[4:])
|
||||
return element
|
||||
|
||||
def fromSvg(self, svg):
|
||||
"""
|
||||
Import element informations from an SVG
|
||||
@@ -214,10 +183,7 @@ class ShapeItem(DrawingItem):
|
||||
brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
|
||||
|
||||
if len(svg):
|
||||
if svg[0].get("stroke-width"):
|
||||
pen.setWidth(int(svg[0].get("stroke-width")))
|
||||
if svg[0].get("stroke"):
|
||||
pen.setColor(colorFromSvg(svg[0].get("stroke")))
|
||||
pen = self._penFromSVGElement(svg[0])
|
||||
if svg[0].get("fill"):
|
||||
new_color = colorFromSvg(svg[0].get("fill"))
|
||||
color = brush.color()
|
||||
@@ -230,17 +196,6 @@ class ShapeItem(DrawingItem):
|
||||
color.setAlphaF(float(svg[0].get("fill-opacity")))
|
||||
brush.setColor(color)
|
||||
|
||||
# Map SVG stroke style (border of the element to the Qt version)
|
||||
if not svg[0].get("stroke"):
|
||||
pen.setStyle(QtCore.Qt.NoPen)
|
||||
else:
|
||||
pen.setStyle(QtCore.Qt.SolidLine)
|
||||
stroke = svg[0].get("stroke-dasharray")
|
||||
if stroke:
|
||||
for (qt_stroke, svg_stroke) in self.QT_DASH_TO_SVG.items():
|
||||
if svg_stroke == stroke:
|
||||
pen.setStyle(qt_stroke)
|
||||
|
||||
self.setPen(pen)
|
||||
self.setBrush(brush)
|
||||
self.update()
|
||||
|
||||
83
gns3/link.py
83
gns3/link.py
@@ -59,10 +59,10 @@ class Link(QtCore.QObject):
|
||||
|
||||
super().__init__()
|
||||
|
||||
log.info("adding link from {} {} to {} {}".format(source_node.name(),
|
||||
source_port.name(),
|
||||
destination_node.name(),
|
||||
destination_port.name()))
|
||||
log.debug("adding link from {} {} to {} {}".format(source_node.name(),
|
||||
source_port.name(),
|
||||
destination_node.name(),
|
||||
destination_port.name()))
|
||||
|
||||
# create an unique ID
|
||||
self._id = Link._instance_count
|
||||
@@ -79,10 +79,12 @@ class Link(QtCore.QObject):
|
||||
self._capture_file_path = None
|
||||
self._capture_file = None
|
||||
self._initialized = False
|
||||
self._filters = {}
|
||||
self._suspend = False
|
||||
|
||||
# Boolean if True we are creatin the first instance of this node
|
||||
# Boolean if True we are creating the first instance of this node
|
||||
# if false the node already exist in the topology
|
||||
# use to avoid erasing informations when reloading
|
||||
# use to avoid erasing information when reloading
|
||||
self._creator = False
|
||||
|
||||
self._nodes = []
|
||||
@@ -124,11 +126,22 @@ class Link(QtCore.QObject):
|
||||
if "nodes" in result:
|
||||
self._nodes = result["nodes"]
|
||||
self._updateLabels()
|
||||
if "filters" in result:
|
||||
self._filters = result["filters"]
|
||||
if "suspend" in result:
|
||||
self._suspend = result["suspend"]
|
||||
self.updated_link_signal.emit(self._id)
|
||||
|
||||
def creator(self):
|
||||
return self._creator
|
||||
|
||||
def suspended(self):
|
||||
return self._suspend
|
||||
|
||||
def toggleSuspend(self):
|
||||
self._suspend = not self._suspend
|
||||
self.update()
|
||||
|
||||
def initialized(self):
|
||||
return self._initialized
|
||||
|
||||
@@ -149,6 +162,12 @@ class Link(QtCore.QObject):
|
||||
body = self._prepareParams()
|
||||
Controller.instance().put("/projects/{project_id}/links/{link_id}".format(project_id=self._source_node.project().id(), link_id=self._link_id), self.updateLinkCallback, body=body)
|
||||
|
||||
def listAvailableFilters(self, callback):
|
||||
"""
|
||||
Get the list of available filters
|
||||
"""
|
||||
Controller.instance().get("/projects/{project_id}/links/{link_id}/available_filters".format(project_id=self._source_node.project().id(), link_id=self._link_id), callback)
|
||||
|
||||
def updateLinkCallback(self, result, error=False, *args, **kwargs):
|
||||
if error:
|
||||
QtWidgets.QMessageBox.warning(None, "Update link", "Error while updating link: {}".format(result["message"]))
|
||||
@@ -167,10 +186,14 @@ class Link(QtCore.QObject):
|
||||
def _updateLabel(self, label, label_data):
|
||||
if not label or sip.isdeleted(label):
|
||||
return
|
||||
label.setPlainText(label_data["text"])
|
||||
label.setPos(label_data["x"], label_data["y"])
|
||||
label.setStyle(label_data["style"])
|
||||
label.setRotation(label_data["rotation"])
|
||||
if "text" in label_data:
|
||||
label.setPlainText(label_data["text"])
|
||||
if "x" in label_data and "y" in label_data:
|
||||
label.setPos(label_data["x"], label_data["y"])
|
||||
if "style" in label_data:
|
||||
label.setStyle(label_data["style"])
|
||||
if "rotation" in label_data:
|
||||
label.setRotation(label_data["rotation"])
|
||||
|
||||
def _prepareParams(self):
|
||||
body = {
|
||||
@@ -185,7 +208,9 @@ class Link(QtCore.QObject):
|
||||
"adapter_number": self._destination_port.adapterNumber(),
|
||||
"port_number": self._destination_port.portNumber()
|
||||
}
|
||||
]
|
||||
],
|
||||
"filters": self._filters,
|
||||
"suspend": self._suspend
|
||||
}
|
||||
if self._source_port.label():
|
||||
body["nodes"][0]["label"] = self._source_port.label().dump()
|
||||
@@ -243,10 +268,18 @@ class Link(QtCore.QObject):
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Link from {} port {} to {} port {}".format(self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
description = "Link from {} port {} to {} port {}".format(self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name())
|
||||
|
||||
if self.capturing():
|
||||
description += "\nPacket capture is active"
|
||||
|
||||
for filter_type in self._filters.keys():
|
||||
description += "\nPacket filter '{}' is active".format(filter_type)
|
||||
|
||||
return description
|
||||
|
||||
def capture_file_name(self):
|
||||
"""
|
||||
@@ -264,10 +297,10 @@ class Link(QtCore.QObject):
|
||||
Deletes this link.
|
||||
"""
|
||||
|
||||
log.info("deleting link from {} {} to {} {}".format(self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name()))
|
||||
log.debug("deleting link from {} {} to {} {}".format(self._source_node.name(),
|
||||
self._source_port.name(),
|
||||
self._destination_node.name(),
|
||||
self._destination_port.name()))
|
||||
|
||||
if skip_controller:
|
||||
self._linkDeletedCallback({})
|
||||
@@ -409,3 +442,15 @@ class Link(QtCore.QObject):
|
||||
if self._destination_node == node:
|
||||
return self._destination_port
|
||||
return self._source_port
|
||||
|
||||
def filters(self):
|
||||
"""
|
||||
:returns: List the filters active on the node
|
||||
"""
|
||||
return self._filters
|
||||
|
||||
def setFilters(self, filters):
|
||||
"""
|
||||
:params filters: List of filters
|
||||
"""
|
||||
self._filters = filters
|
||||
|
||||
@@ -183,6 +183,13 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
return os.path.normpath(path)
|
||||
|
||||
def runAsRootPath(self):
|
||||
"""
|
||||
Gets run as root filename
|
||||
:return: string
|
||||
"""
|
||||
return os.path.join(self.configDirectory(), "run_as_root")
|
||||
|
||||
def _migrateOldConfigPath(self):
|
||||
"""
|
||||
Migrate pre 1.4 config path
|
||||
@@ -269,7 +276,7 @@ class LocalConfig(QtCore.QObject):
|
||||
Read the configuration file.
|
||||
"""
|
||||
|
||||
log.info("Load config from %s", config_path)
|
||||
log.debug("Load config from %s", config_path)
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
self._last_config_changed = os.stat(config_path).st_mtime
|
||||
@@ -296,7 +303,7 @@ class LocalConfig(QtCore.QObject):
|
||||
with open(temporary, "w", encoding="utf-8") as f:
|
||||
json.dump(self._settings, f, sort_keys=True, indent=4)
|
||||
shutil.move(temporary, self._config_file)
|
||||
log.info("Configuration save to %s", self._config_file)
|
||||
log.debug("Configuration save to %s", self._config_file)
|
||||
self._last_config_changed = os.stat(self._config_file).st_mtime
|
||||
except (ValueError, OSError) as e:
|
||||
log.error("Could not write the config file {}: {}".format(self._config_file, e))
|
||||
@@ -324,7 +331,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
try:
|
||||
if self._last_config_changed and self._last_config_changed < os.stat(self._config_file).st_mtime:
|
||||
log.info("Client config has changed, reloading it...")
|
||||
log.debug("Client config has changed, reloading it...")
|
||||
self._readConfig(self._config_file)
|
||||
self.config_changed_signal.emit()
|
||||
except OSError as e:
|
||||
@@ -402,9 +409,8 @@ class LocalConfig(QtCore.QObject):
|
||||
self._settings[section] = settings
|
||||
|
||||
if changed:
|
||||
log.info("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
|
||||
log.debug("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
|
||||
self.writeConfig()
|
||||
|
||||
return copy.deepcopy(settings)
|
||||
|
||||
def saveSectionSettings(self, section, settings):
|
||||
@@ -420,7 +426,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
if self._settings[section] != settings:
|
||||
self._settings[section].update(copy.deepcopy(settings))
|
||||
log.info("Section %s has changed. Saving configuration", section)
|
||||
log.debug("Section %s has changed. Saving configuration", section)
|
||||
self.writeConfig()
|
||||
else:
|
||||
log.debug("Section %s has not changed. Skip saving configuration", section)
|
||||
|
||||
@@ -30,7 +30,7 @@ import signal
|
||||
import subprocess
|
||||
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore
|
||||
from gns3.qt import QtWidgets, QtCore, qslot
|
||||
from gns3.settings import LOCAL_SERVER_SETTINGS
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
@@ -59,17 +59,20 @@ class StopLocalServerWorker(QtCore.QObject):
|
||||
def __init__(self, local_server_process):
|
||||
super().__init__()
|
||||
self._local_server_process = local_server_process
|
||||
self._precision = 100 # In MS
|
||||
self._remaining_trial = int(10 * (1000 / self._precision))
|
||||
|
||||
@qslot
|
||||
def _callbackSlot(self, *params):
|
||||
self._local_server_process.poll()
|
||||
if self._local_server_process.returncode is None and self._remaining_trial > 0:
|
||||
self._remaining_trial -= 1
|
||||
QtCore.QTimer.singleShot(self._precision, self._callbackSlot)
|
||||
else:
|
||||
self.finished.emit()
|
||||
|
||||
def run(self):
|
||||
precision = 1
|
||||
remaining_trial = 4 / precision # 4 Seconds
|
||||
while remaining_trial > 0:
|
||||
if self._local_server_process.returncode is None:
|
||||
remaining_trial -= 1
|
||||
self.thread().sleep(precision)
|
||||
else:
|
||||
break
|
||||
self.finished.emit()
|
||||
QtCore.QTimer.singleShot(1000, self._callbackSlot)
|
||||
|
||||
def cancel(self):
|
||||
return
|
||||
@@ -157,28 +160,23 @@ class LocalServer(QtCore.QObject):
|
||||
if sys.platform.startswith("linux"):
|
||||
# test if the executable has the CAP_NET_RAW capability (Linux only)
|
||||
try:
|
||||
if "security.capability" in os.listxattr(path):
|
||||
caps = os.getxattr(path, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if not struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["setcap", "cap_net_admin,cap_net_raw=ep"])
|
||||
else:
|
||||
# capabilities not supported
|
||||
request_setuid = True
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if "security.capability" not in os.listxattr(path) or not struct.unpack("<IIIII", os.getxattr(path, "security.capability"))[1] & 1 << 13:
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires CAP_NET_RAW capability to interact with network interfaces. Set the capability to uBridge? All users on the system will be able to read packet from the network interfaces.",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
sudo(["setcap", "cap_net_admin,cap_net_raw=ep", path])
|
||||
except AttributeError:
|
||||
# Due to a Python bug, os.listxattr could be missing: https://github.com/GNS3/gns3-gui/issues/2010
|
||||
log.warning("Could not determine if CAP_NET_RAW capability is set for uBridge (Python bug)")
|
||||
return True
|
||||
except OSError as e:
|
||||
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set CAP_NET_RAW capability to uBridge {}: {}".format(path, str(e)))
|
||||
return False
|
||||
request_setuid = True
|
||||
|
||||
if sys.platform.startswith("darwin") or request_setuid:
|
||||
try:
|
||||
@@ -186,7 +184,7 @@ class LocalServer(QtCore.QObject):
|
||||
proceed = QtWidgets.QMessageBox.question(
|
||||
self.parent(),
|
||||
"uBridge",
|
||||
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
|
||||
"uBridge requires root permissions to interact with network interfaces. Set root permissions to uBridge? All admin users on the system will be able to read packet from the network interfaces.",
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if proceed == QtWidgets.QMessageBox.Yes:
|
||||
@@ -332,7 +330,7 @@ class LocalServer(QtCore.QObject):
|
||||
return True
|
||||
|
||||
if self.isLocalServerRunning():
|
||||
log.info("A local server already running on this host")
|
||||
log.debug("A local server already running on this host")
|
||||
# Try to kill the server. The server can be still running after
|
||||
# if the server was started by hand
|
||||
self._killAlreadyRunningServer()
|
||||
@@ -467,7 +465,7 @@ class LocalServer(QtCore.QObject):
|
||||
log.warn("could not delete server log file {}: {}".format(logpath, e))
|
||||
command += ' --log="{}" --pid="{}"'.format(logpath, self._pid_path())
|
||||
|
||||
log.info("Starting local server process with {}".format(command))
|
||||
log.debug("Starting local server process with {}".format(command))
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
# use the string on Windows
|
||||
@@ -480,7 +478,7 @@ class LocalServer(QtCore.QObject):
|
||||
log.warning('Could not start local server "{}": {}'.format(command, e))
|
||||
return False
|
||||
|
||||
log.info("Local server process has started (PID={})".format(self._local_server_process.pid))
|
||||
log.debug("Local server process has started (PID={})".format(self._local_server_process.pid))
|
||||
return True
|
||||
|
||||
def _checkLocalServerRunningSlot(self):
|
||||
@@ -539,7 +537,7 @@ class LocalServer(QtCore.QObject):
|
||||
|
||||
if self.localServerProcessIsRunning():
|
||||
self._stopping = True
|
||||
log.info("Stopping local server (PID={})".format(self._local_server_process.pid))
|
||||
log.debug("Stopping local server (PID={})".format(self._local_server_process.pid))
|
||||
# local server is running, let's stop it
|
||||
if self._http_client:
|
||||
self._http_client.shutdown()
|
||||
|
||||
10
gns3/main.py
10
gns3/main.py
@@ -184,8 +184,8 @@ def main():
|
||||
if sys.version_info < (3, 4):
|
||||
raise SystemExit("Python 3.4 or higher is required")
|
||||
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.0.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.0.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.5.0"):
|
||||
raise SystemExit("Requirement is PyQt5 version 5.5.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))
|
||||
|
||||
if parse_version(psutil.__version__) < parse_version("2.2.1"):
|
||||
raise SystemExit("Requirement is psutil version 2.2.1 or higher, got version {}".format(psutil.__version__))
|
||||
@@ -228,8 +228,10 @@ def main():
|
||||
if local_config.multiProfiles() and not options.profile:
|
||||
profile_select = ProfileSelectDialog()
|
||||
profile_select.show()
|
||||
profile_select.exec_()
|
||||
options.profile = profile_select.profile()
|
||||
if profile_select.exec_():
|
||||
options.profile = profile_select.profile()
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
# Init the config
|
||||
if options.config:
|
||||
|
||||
@@ -51,6 +51,8 @@ from .update_manager import UpdateManager
|
||||
from .utils.analytics import AnalyticsClient
|
||||
from .dialogs.appliance_wizard import ApplianceWizard
|
||||
from .dialogs.new_appliance_dialog import NewApplianceDialog
|
||||
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
|
||||
from .status_bar import StatusBarHandler
|
||||
from .registry.appliance import ApplianceError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -75,6 +77,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._notif_dialog = NotifDialog(self)
|
||||
# Setup logger
|
||||
logging.getLogger().addHandler(NotifDialogHandler(self._notif_dialog))
|
||||
logging.getLogger().addHandler(StatusBarHandler(self.uiStatusBar))
|
||||
|
||||
self._open_file_at_startup = open_file
|
||||
|
||||
MainWindow._instance = self
|
||||
@@ -82,6 +89,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
topology.setMainWindow(self)
|
||||
topology.project_changed_signal.connect(self._projectChangedSlot)
|
||||
Controller.instance().setParent(self)
|
||||
LocalServer.instance().setParent(self)
|
||||
|
||||
self._settings = {}
|
||||
HTTPClient.setProgressCallback(Progress.instance(self))
|
||||
@@ -212,6 +220,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiResetPortLabelsAction.triggered.connect(self._resetPortLabelsActionSlot)
|
||||
self.uiShowPortNamesAction.triggered.connect(self._showPortNamesActionSlot)
|
||||
self.uiShowGridAction.triggered.connect(self._showGridActionSlot)
|
||||
self.uiSnapToGridAction.triggered.connect(self._snapToGridActionSlot)
|
||||
|
||||
# tool menu connections
|
||||
self.uiWebInterfaceAction.triggered.connect(self._openWebInterfaceActionSlot)
|
||||
@@ -232,6 +241,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiInsertImageAction.triggered.connect(self._insertImageActionSlot)
|
||||
self.uiDrawRectangleAction.triggered.connect(self._drawRectangleActionSlot)
|
||||
self.uiDrawEllipseAction.triggered.connect(self._drawEllipseActionSlot)
|
||||
self.uiDrawLineAction.triggered.connect(self._drawLineActionSlot)
|
||||
self.uiEditReadmeAction.triggered.connect(self._editReadmeActionSlot)
|
||||
|
||||
# help menu connections
|
||||
@@ -299,8 +309,26 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Called when we ask to display the grid
|
||||
"""
|
||||
self.showGrid(self.uiShowGridAction.isChecked())
|
||||
|
||||
self.uiGraphicsView.viewport().update()
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setShowGrid(self.uiShowGridAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def _snapToGridActionSlot(self):
|
||||
"""
|
||||
Called when user click on the snap to grid menu item
|
||||
:return: None
|
||||
"""
|
||||
self.snapToGrid(self.uiSnapToGridAction.isChecked())
|
||||
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setSnapToGrid(self.uiSnapToGridAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def analyticsClient(self):
|
||||
"""
|
||||
@@ -313,6 +341,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Slot called to create a new project.
|
||||
"""
|
||||
|
||||
# prevents race condition
|
||||
if self._project_dialog is not None:
|
||||
return
|
||||
|
||||
self._project_dialog = ProjectDialog(self)
|
||||
self._project_dialog.show()
|
||||
create_new_project = self._project_dialog.exec_()
|
||||
@@ -321,7 +354,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.uiNodesDockWidget.setWindowTitle("")
|
||||
|
||||
if create_new_project:
|
||||
Topology.instance().createLoadProject(self._project_dialog.getProjectSettings())
|
||||
Topology.instance().createLoadProject(
|
||||
self._project_dialog.getProjectSettings())
|
||||
|
||||
self._project_dialog = None
|
||||
|
||||
@@ -537,6 +571,60 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
# TODO: quality option
|
||||
return image.save(path)
|
||||
|
||||
def showLayers(self, show_layers):
|
||||
"""
|
||||
Shows layers in GUI
|
||||
:param show_layers: boolean
|
||||
:return: None
|
||||
"""
|
||||
NodeItem.show_layer = show_layers
|
||||
ShapeItem.show_layer = show_layers
|
||||
for item in self.uiGraphicsView.items():
|
||||
item.update()
|
||||
|
||||
def showGrid(self, show_grid):
|
||||
"""
|
||||
Shows grid in GUI
|
||||
:param show_grid: boolean
|
||||
:return: None
|
||||
"""
|
||||
self.uiGraphicsView.viewport().update()
|
||||
|
||||
def snapToGrid(self, snap_to_grid):
|
||||
"""
|
||||
Snap to grid in GUI
|
||||
:param snap_to_grid: boolean
|
||||
:return: None
|
||||
"""
|
||||
self.uiGraphicsView.viewport().update()
|
||||
|
||||
def showInterfaceLabels(self, show_interface_labels):
|
||||
"""
|
||||
Show interface labels in GUI
|
||||
:param show_interface_labels: boolean
|
||||
:return: None
|
||||
"""
|
||||
LinkItem.showPortLabels(show_interface_labels)
|
||||
for item in self.uiGraphicsView.scene().items():
|
||||
if isinstance(item, LinkItem):
|
||||
item.adjust()
|
||||
|
||||
def _updateZoomSettings(self, zoom=None):
|
||||
"""
|
||||
Updates zoom settings
|
||||
:param zoom integer optional, when not provided then calculated from current view
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if zoom is None:
|
||||
zoom = round(self.uiGraphicsView.transform().m11() * 100)
|
||||
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setZoom(zoom)
|
||||
project.update()
|
||||
|
||||
def _screenshotActionSlot(self):
|
||||
"""
|
||||
Slot called to take a screenshot of the scene.
|
||||
@@ -605,6 +693,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
factor_in = pow(2.0, 120 / 240.0)
|
||||
self.uiGraphicsView.scaleView(factor_in)
|
||||
self._updateZoomSettings()
|
||||
|
||||
def _zoomOutActionSlot(self):
|
||||
"""
|
||||
@@ -613,6 +702,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
factor_out = pow(2.0, -120 / 240.0)
|
||||
self.uiGraphicsView.scaleView(factor_out)
|
||||
self._updateZoomSettings()
|
||||
|
||||
def _zoomResetActionSlot(self):
|
||||
"""
|
||||
@@ -620,6 +710,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
self.uiGraphicsView.resetTransform()
|
||||
self._updateZoomSettings()
|
||||
|
||||
def _fitInViewActionSlot(self):
|
||||
"""
|
||||
@@ -635,11 +726,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Slot called to show the layer positions on the scene.
|
||||
"""
|
||||
self.showLayers(self.uiShowLayersAction.isChecked())
|
||||
|
||||
NodeItem.show_layer = self.uiShowLayersAction.isChecked()
|
||||
ShapeItem.show_layer = self.uiShowLayersAction.isChecked()
|
||||
for item in self.uiGraphicsView.items():
|
||||
item.update()
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setShowLayers(self.uiShowLayersAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def _resetPortLabelsActionSlot(self):
|
||||
"""
|
||||
@@ -656,10 +749,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
Slot called to show the port names on the scene.
|
||||
"""
|
||||
|
||||
LinkItem.showPortLabels(self.uiShowPortNamesAction.isChecked())
|
||||
for item in self.uiGraphicsView.scene().items():
|
||||
if isinstance(item, LinkItem):
|
||||
item.adjust()
|
||||
self.showInterfaceLabels(self.uiShowPortNamesAction.isChecked())
|
||||
|
||||
# save settings
|
||||
project = Topology.instance().project()
|
||||
if project is not None:
|
||||
project.setShowInterfaceLabels(self.uiShowPortNamesAction.isChecked())
|
||||
project.update()
|
||||
|
||||
def _startAllActionSlot(self):
|
||||
"""
|
||||
@@ -737,7 +833,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
self._pictures_dir = os.path.dirname(path)
|
||||
|
||||
image = QtGui.QPixmap(path)
|
||||
QtGui.QPixmap(path)
|
||||
self.uiGraphicsView.addImage(path)
|
||||
|
||||
def _drawRectangleActionSlot(self):
|
||||
@@ -754,6 +850,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
|
||||
self.uiGraphicsView.addEllipse(self.uiDrawEllipseAction.isChecked())
|
||||
|
||||
def _drawLineActionSlot(self):
|
||||
"""
|
||||
Slot called when adding a line on the scene.
|
||||
"""
|
||||
|
||||
self.uiGraphicsView.addLine(self.uiDrawLineAction.isChecked())
|
||||
|
||||
def _onlineHelpActionSlot(self):
|
||||
"""
|
||||
Slot to launch a browser pointing to the documentation page.
|
||||
@@ -842,8 +945,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
else:
|
||||
self.uiNodesDockWidget.setWindowTitle(title)
|
||||
self.uiNodesDockWidget.setVisible(True)
|
||||
self.uiNodesView.clear()
|
||||
self.uiNodesView.populateNodesView(category)
|
||||
self.uiNodesDockWidget.populateNodesView(category)
|
||||
|
||||
def _localConfigChangedSlot(self):
|
||||
"""
|
||||
@@ -918,6 +1020,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
Topology.instance().editReadme()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._notif_dialog.resize()
|
||||
super().resizeEvent(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Handles all key press events for the main window.
|
||||
@@ -1000,9 +1106,28 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
run_as_root_path = LocalConfig.instance().runAsRootPath()
|
||||
|
||||
if not sys.platform.startswith("win") and os.geteuid() == 0:
|
||||
# touches file to know that user has run GNS3 as root and to prevent
|
||||
# from running as user
|
||||
if not os.path.exists(run_as_root_path):
|
||||
try:
|
||||
open(run_as_root_path, 'a').close()
|
||||
except OSError as e:
|
||||
log.warning("Cannot write `run_as_root` file due to: {}".format(str(e)))
|
||||
|
||||
QtWidgets.QMessageBox.warning(self, "Root", "Running GNS3 as root is not recommended and could be dangerous")
|
||||
|
||||
if not sys.platform.startswith("win") and os.geteuid() != 0 and os.path.exists(run_as_root_path):
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self, "Run as user",
|
||||
"GNS3 has been previously run as root. It is not possible "
|
||||
"to change to another user and GNS3 will be shutdown. Please delete the '{}' file "
|
||||
"and start the program again.".format(run_as_root_path))
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
# restore debug level
|
||||
if self._settings["debug_level"]:
|
||||
root = logging.getLogger()
|
||||
|
||||
@@ -33,7 +33,6 @@ from .atm_switch import ATMSwitch
|
||||
from .settings import (
|
||||
BUILTIN_SETTINGS,
|
||||
CLOUD_SETTINGS,
|
||||
NAT_SETTINGS,
|
||||
ETHERNET_HUB_SETTINGS,
|
||||
ETHERNET_SWITCH_SETTINGS
|
||||
)
|
||||
@@ -224,45 +223,9 @@ class Builtin(Module):
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node))
|
||||
if isinstance(node, Cloud):
|
||||
for key, info in self._cloud_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
|
||||
return
|
||||
elif isinstance(node, Nat):
|
||||
for key, info in self._nat_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(default_name_format=default_name_format)
|
||||
return
|
||||
elif isinstance(node, EthernetHub):
|
||||
for key, info in self._ethernet_hubs.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
|
||||
return
|
||||
elif isinstance(node, EthernetSwitch):
|
||||
for key, info in self._ethernet_switches.items():
|
||||
if node_name == info["name"]:
|
||||
default_name_format = info["default_name_format"].replace('{name}', node_name)
|
||||
node.create(ports=info["ports_mapping"], default_name_format=default_name_format)
|
||||
return
|
||||
node.create()
|
||||
|
||||
@staticmethod
|
||||
def findAlternativeInterface(node, missing_interface):
|
||||
|
||||
@@ -330,17 +293,6 @@ class Builtin(Module):
|
||||
"""
|
||||
|
||||
nodes = []
|
||||
for node_class in Builtin.classes():
|
||||
nodes.append(
|
||||
{"class": node_class.__name__,
|
||||
"name": node_class.symbolName(),
|
||||
"categories": node_class.categories(),
|
||||
"symbol": node_class.defaultSymbol(),
|
||||
"builtin": True,
|
||||
"node_type": node_class.URL_PREFIX
|
||||
}
|
||||
)
|
||||
|
||||
# add custom cloud node templates
|
||||
for cloud_node in self._cloud_nodes.values():
|
||||
nodes.append(
|
||||
@@ -371,9 +323,8 @@ class Builtin(Module):
|
||||
"server": switch["server"],
|
||||
"symbol": switch["symbol"],
|
||||
"categories": [switch["category"]]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -44,20 +44,6 @@ class ATMSwitch(Node):
|
||||
self._always_on = True
|
||||
self.settings().update({"mappings": {}})
|
||||
|
||||
def create(self, name=None, node_id=None, mappings=None, default_name_format="ATM{0}"):
|
||||
"""
|
||||
Creates this ATM switch.
|
||||
|
||||
:param name: optional name for this switch.
|
||||
:param node_id: Node identifier on the server
|
||||
:param mappings: mappings to be automatically added when creating this ATM switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if mappings:
|
||||
params["mappings"] = mappings
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
@@ -167,28 +153,6 @@ class ATMSwitch(Node):
|
||||
atmsw["properties"]["mappings"] = self._settings["mappings"]
|
||||
return atmsw
|
||||
|
||||
def load(self, node_info):
|
||||
"""
|
||||
Loads an ATM switch representation
|
||||
(from a topology file).
|
||||
|
||||
:param node_info: representation of the node (dictionary)
|
||||
"""
|
||||
|
||||
super().load(node_info)
|
||||
properties = node_info["properties"]
|
||||
name = properties.pop("name")
|
||||
|
||||
# ATM switches do not have an UUID before version 2.0
|
||||
node_id = properties.get("node_id", str(uuid.uuid4()))
|
||||
|
||||
mappings = {}
|
||||
if "mappings" in properties:
|
||||
mappings = properties["mappings"]
|
||||
|
||||
log.info("ATM switch {} is loading".format(name))
|
||||
self.create(name, node_id, mappings)
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
@@ -46,20 +46,6 @@ class Cloud(Node):
|
||||
|
||||
return self._interfaces
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="Cloud{0}"):
|
||||
"""
|
||||
Creates this cloud.
|
||||
|
||||
:param name: optional name for this cloud
|
||||
:param node_id: Node identifier on the server
|
||||
:param ports: ports to be automatically added when creating this cloud
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
@@ -39,20 +39,6 @@ class EthernetHub(Node):
|
||||
self._always_on = True
|
||||
self.settings().update({"ports_mapping": []})
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="Hub{0}"):
|
||||
"""
|
||||
Creates this hub.
|
||||
|
||||
:param name: optional name for this hub
|
||||
:param node_id: node identifier on the server
|
||||
:param ports: ports to automatically be added when creating this hub
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
@@ -38,21 +38,7 @@ class EthernetSwitch(Node):
|
||||
# this is an always-on node
|
||||
self.setStatus(Node.started)
|
||||
self._always_on = True
|
||||
self.settings().update({"ports_mapping": []})
|
||||
|
||||
def create(self, name=None, node_id=None, ports=None, default_name_format="SW{0}"):
|
||||
"""
|
||||
Creates this Ethernet switch.
|
||||
|
||||
:param name: optional name for this switch
|
||||
:param node_id: node identifier on the server
|
||||
:param ports: ports to be automatically added when creating this switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if ports:
|
||||
params["ports_mapping"] = ports
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
self.settings().update({"ports_mapping": [], "console": None, "console_type": "telnet"})
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
@@ -61,6 +47,10 @@ class EthernetSwitch(Node):
|
||||
:param result: server response (dict)
|
||||
"""
|
||||
self.settings()["ports_mapping"] = result["ports_mapping"]
|
||||
self.settings()["console"] = result["console"]
|
||||
|
||||
def console(self):
|
||||
return self.settings()["console"]
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
|
||||
@@ -41,20 +41,6 @@ class FrameRelaySwitch(Node):
|
||||
self._always_on = True
|
||||
self.settings().update({"mappings": {}})
|
||||
|
||||
def create(self, name=None, node_id=None, mappings={}, default_name_format="FR{0}"):
|
||||
"""
|
||||
Creates this Frame Relay switch.
|
||||
|
||||
:param name: name for this switch.
|
||||
:param node_id: node identifier on the server
|
||||
:param mappings: mappings to be automatically added when creating this Frame relay switch
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if mappings:
|
||||
params["mappings"] = mappings
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
@@ -46,17 +46,6 @@ class Nat(Node):
|
||||
|
||||
return self._interfaces
|
||||
|
||||
def create(self, name=None, node_id=None, default_name_format="Nat{0}"):
|
||||
"""
|
||||
Creates this nat.
|
||||
|
||||
:param name: optional name for this nat
|
||||
:param node_id: Node identifier on the server
|
||||
"""
|
||||
|
||||
params = {}
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
@@ -25,14 +25,6 @@ BUILTIN_SETTINGS = {
|
||||
}
|
||||
|
||||
|
||||
NAT_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Nat{0}",
|
||||
"symbol": ":/symbols/cloud.svg",
|
||||
"category": Node.end_devices,
|
||||
"ports_mapping": [],
|
||||
}
|
||||
|
||||
CLOUD_SETTINGS = {
|
||||
"name": "",
|
||||
"default_name_format": "Cloud{0}",
|
||||
|
||||
@@ -137,60 +137,9 @@ class Docker(Module):
|
||||
:param node_class: Node object
|
||||
:param server: HTTPClient instance
|
||||
"""
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
log.info("creating node {} with id {}".format(node, node.id()))
|
||||
|
||||
image = None
|
||||
if node_name:
|
||||
for image_key, info in self._docker_containers.items():
|
||||
if node_name == info["name"]:
|
||||
image = image_key
|
||||
if not image:
|
||||
selected_images = []
|
||||
for image, info in self._docker_containers.items():
|
||||
if info["server"] == node.compute().id():
|
||||
selected_images.append(image)
|
||||
|
||||
if not selected_images:
|
||||
raise ModuleError("No Docker VM on server {}".format(
|
||||
node.server().url()))
|
||||
elif len(selected_images) > 1:
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
|
||||
(selection, ok) = QtWidgets.QInputDialog.getItem(
|
||||
mainwindow, "Docker Image", "Please choose an image",
|
||||
selected_images, 0, False)
|
||||
if ok:
|
||||
image = selection
|
||||
else:
|
||||
raise ModuleError("Please select a Docker Image")
|
||||
else:
|
||||
image = selected_images[0]
|
||||
|
||||
image_settings = {}
|
||||
for setting_name, value in self._docker_containers[image].items():
|
||||
if setting_name in node.settings() and value != "" and value is not None:
|
||||
if setting_name not in ['name', 'image']:
|
||||
image_settings[setting_name] = value
|
||||
|
||||
default_name_format = DOCKER_CONTAINER_SETTINGS["default_name_format"]
|
||||
if self._docker_containers[image]["default_name_format"]:
|
||||
default_name_format = self._docker_containers[image]["default_name_format"]
|
||||
|
||||
image = self._docker_containers[image]["image"]
|
||||
node.create(image, base_name=node_name, additional_settings=image_settings, default_name_format=default_name_format)
|
||||
|
||||
def reset(self):
|
||||
"""Resets the servers."""
|
||||
self._nodes.clear()
|
||||
|
||||
@@ -38,7 +38,6 @@ class DockerVM(Node):
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
log.info("Docker VM is being created")
|
||||
|
||||
docker_vm_settings = {"image": "",
|
||||
"adapters": DOCKER_CONTAINER_SETTINGS["adapters"],
|
||||
@@ -54,23 +53,6 @@ class DockerVM(Node):
|
||||
|
||||
self.settings().update(docker_vm_settings)
|
||||
|
||||
def create(self, image, name=None, base_name=None, node_id=None, additional_settings={}, default_name_format="{name}-{0}"):
|
||||
"""Creates this Docker container.
|
||||
|
||||
:param image: image name
|
||||
:param name: optional name
|
||||
:param additional_settings: additional settings for this VM
|
||||
"""
|
||||
|
||||
params = {
|
||||
"image": image,
|
||||
"adapters": self._settings["adapters"]
|
||||
}
|
||||
params.update(additional_settings)
|
||||
if base_name:
|
||||
default_name_format = default_name_format.replace('{name}', base_name)
|
||||
self._create(name=name, node_id=node_id, params=params, default_name_format=default_name_format, timeout=None)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for Docker container creating.
|
||||
|
||||
@@ -245,56 +245,9 @@ class Dynamips(Module):
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node))
|
||||
|
||||
if isinstance(node, Router):
|
||||
ios_router = None
|
||||
if node_name:
|
||||
for ios_key, info in self._ios_routers.items():
|
||||
if node_name == info["name"]:
|
||||
ios_router = self._ios_routers[ios_key]
|
||||
break
|
||||
|
||||
if not ios_router:
|
||||
raise ModuleError("No IOS router for platform {}".format(node.settings()["platform"]))
|
||||
|
||||
vm_settings = {}
|
||||
for setting_name, value in ios_router.items():
|
||||
if setting_name in node.settings() and setting_name != "name" and value != "" and value is not None:
|
||||
vm_settings[setting_name] = value
|
||||
|
||||
default_name_format = IOS_ROUTER_SETTINGS["default_name_format"]
|
||||
if ios_router["default_name_format"]:
|
||||
default_name_format = ios_router["default_name_format"]
|
||||
|
||||
# Older GNS3 versions may have the following invalid settings in the VM template
|
||||
if "console" in vm_settings:
|
||||
del vm_settings["console"]
|
||||
if "sensors" in vm_settings:
|
||||
del vm_settings["sensors"]
|
||||
if "power_supplies" in vm_settings:
|
||||
del vm_settings["power_supplies"]
|
||||
|
||||
ram = vm_settings.pop("ram")
|
||||
image = vm_settings.pop("image", None)
|
||||
if image is None:
|
||||
raise ModuleError("No IOS image has been associated with this IOS router")
|
||||
node.create(image, ram, additional_settings=vm_settings, default_name_format=default_name_format)
|
||||
else:
|
||||
node.create()
|
||||
|
||||
def updateImageIdlepc(self, image_path, idlepc):
|
||||
"""
|
||||
Updates the Idle-PC for an IOS image.
|
||||
@@ -307,7 +260,7 @@ class Dynamips(Module):
|
||||
if os.path.basename(ios_router["image"]) == image_path:
|
||||
if ios_router["idlepc"] != idlepc:
|
||||
ios_router["idlepc"] = idlepc
|
||||
log.info("Idle-PC value {} saved into '{}' template".format(idlepc, ios_router["name"]))
|
||||
log.debug("Idle-PC value {} saved into '{}' template".format(idlepc, ios_router["name"]))
|
||||
self._saveIOSRouters()
|
||||
|
||||
def reset(self):
|
||||
@@ -317,28 +270,6 @@ class Dynamips(Module):
|
||||
|
||||
self._nodes.clear()
|
||||
|
||||
def exportConfigs(self, directory):
|
||||
"""
|
||||
Exports all configs for all nodes to a directory.
|
||||
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if isinstance(node, Router) and node.initialized():
|
||||
node.exportConfigToDirectory(directory)
|
||||
|
||||
def importConfigs(self, directory):
|
||||
"""
|
||||
Imports configs to all nodes from a directory.
|
||||
|
||||
:param directory: source directory path
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if isinstance(node, Router) and node.initialized():
|
||||
node.importConfigFromDirectory(directory)
|
||||
|
||||
def findAlternativeIOSImage(self, image, node):
|
||||
"""
|
||||
Tries to find an alternative IOS image.
|
||||
|
||||
@@ -21,17 +21,13 @@ Wizard for IOS routers.
|
||||
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
|
||||
|
||||
from gns3.qt import QtCore, QtGui, QtWidgets, qslot
|
||||
from gns3.node import Node
|
||||
from gns3.topology import Topology
|
||||
from gns3.utils.run_in_terminal import RunInTerminal
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from gns3.utils.get_default_base_config import get_default_base_config
|
||||
from gns3.dialogs.vm_with_images_wizard import VMWithImagesWizard
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.controller import Controller
|
||||
|
||||
from ..ui.ios_router_wizard_ui import Ui_IOSRouterWizard
|
||||
from ..settings import PLATFORMS_DEFAULT_RAM, PLATFORMS_DEFAULT_NVRAM, CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
|
||||
@@ -92,8 +88,8 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
|
||||
self.uiIdlepcLineEdit.textChanged.emit(self.uiIdlepcLineEdit.text())
|
||||
|
||||
# location of the base config templates
|
||||
self._base_startup_config_template = get_resource(os.path.join("configs", "ios_base_startup-config.txt"))
|
||||
self._base_etherswitch_startup_config_template = get_resource(os.path.join("configs", "ios_etherswitch_startup-config.txt"))
|
||||
self._base_startup_config_template = "ios_base_startup-config.txt"
|
||||
self._base_etherswitch_startup_config_template = "ios_etherswitch_startup-config.txt"
|
||||
|
||||
# FIXME: hide because of issue on Windows.
|
||||
self.uiTestIOSImagePushButton.hide()
|
||||
@@ -215,36 +211,20 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
|
||||
self._idle_valid = False
|
||||
self.uiIdlepcLineEdit.setStyleSheet('QLineEdit { background-color: %s }' % color)
|
||||
|
||||
def _idlePCFinderSlot(self):
|
||||
def _idlePCFinderSlot(self, ):
|
||||
"""
|
||||
Slot for the idle-PC finder.
|
||||
"""
|
||||
if Topology.instance().project() is None:
|
||||
project = Topology.instance().createLoadProject({"project_name": str(uuid.uuid4())})
|
||||
project.project_updated_signal.connect(self._projectCreatedSlot)
|
||||
else:
|
||||
self._projectCreatedSlot()
|
||||
|
||||
@qslot
|
||||
def _projectCreatedSlot(self, *args):
|
||||
if Topology.instance().project() is None:
|
||||
return
|
||||
try:
|
||||
Topology.instance().project().project_updated_signal.disconnect(self._projectCreatedSlot)
|
||||
self._project_created = True
|
||||
except TypeError:
|
||||
pass # If the slot is not connected (project already created)
|
||||
|
||||
module = Dynamips.instance()
|
||||
image = self.uiIOSImageLineEdit.text()
|
||||
platform = self.uiPlatformComboBox.currentText()
|
||||
ios_image = self.uiIOSImageLineEdit.text()
|
||||
ram = self.uiRamSpinBox.value()
|
||||
router_class = PLATFORM_TO_CLASS[platform]
|
||||
|
||||
self._router = router_class(module, ComputeManager.instance().getCompute(self._compute_id), Topology.instance().project())
|
||||
self._router.create(ios_image, ram, name="AUTOIDLEPC")
|
||||
self._router.created_signal.connect(self.createdSlot)
|
||||
self._router.server_error_signal.connect(self.serverErrorSlot)
|
||||
Controller.instance().postCompute("/autoidlepc",
|
||||
self._compute_id,
|
||||
self._computeAutoIdlepcCallback,
|
||||
timeout=None,
|
||||
body={
|
||||
"image": image,
|
||||
"platform": platform
|
||||
})
|
||||
self.uiIdlePCFinderPushButton.setEnabled(False)
|
||||
|
||||
def _etherSwitchSlot(self, state):
|
||||
@@ -261,15 +241,6 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
|
||||
self.uiNameLineEdit.setText(self.uiPlatformComboBox.currentText())
|
||||
# self.uiNameLineEdit.setEnabled(True)
|
||||
|
||||
def createdSlot(self, base_node_id):
|
||||
"""
|
||||
The node for the auto Idle-PC has been created.
|
||||
|
||||
:param base_node_id: not used
|
||||
"""
|
||||
|
||||
self._router.computeAutoIdlepc(self._computeAutoIdlepcCallback)
|
||||
|
||||
def serverErrorSlot(self, base_node_id, message):
|
||||
"""
|
||||
The auto idle-pc node could not be created.
|
||||
@@ -289,13 +260,6 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if self._project_created:
|
||||
Topology.instance().deleteProject()
|
||||
self._project_created = False
|
||||
self._router = None
|
||||
elif self._router:
|
||||
self._router.delete()
|
||||
self._router = None
|
||||
if error:
|
||||
QtWidgets.QMessageBox.critical(self, "Idle-PC finder", "Error: {}".format(result["message"]))
|
||||
else:
|
||||
@@ -420,7 +384,7 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
|
||||
settings = {
|
||||
"name": self.uiNameLineEdit.text(),
|
||||
"image": image,
|
||||
"startup_config": get_default_base_config(self._base_startup_config_template),
|
||||
"startup_config": self._base_startup_config_template,
|
||||
"ram": self.uiRamSpinBox.value(),
|
||||
"nvram": PLATFORMS_DEFAULT_NVRAM[platform],
|
||||
"idlepc": self.uiIdlepcLineEdit.text(),
|
||||
@@ -431,7 +395,7 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
|
||||
|
||||
if self.uiEtherSwitchCheckBox.isChecked():
|
||||
settings["default_name_format"] = "ESW{0}"
|
||||
settings["startup_config"] = get_default_base_config(self._base_etherswitch_startup_config_template)
|
||||
settings["startup_config"] = self._base_etherswitch_startup_config_template
|
||||
settings["symbol"] = ":/symbols/multilayer_switch.svg"
|
||||
settings["disk0"] = 1 # adds 1MB disk to store vlan.dat
|
||||
settings["category"] = Node.switches
|
||||
|
||||
@@ -48,7 +48,6 @@ class Router(Node):
|
||||
def __init__(self, module, server, project, platform="c7200"):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
log.info("Router {} is being created".format(platform))
|
||||
self._dynamips_id = None
|
||||
|
||||
router_settings = {"platform": platform,
|
||||
@@ -88,48 +87,6 @@ class Router(Node):
|
||||
|
||||
self.settings().update(router_settings)
|
||||
|
||||
def create(self, image, ram, name=None, node_id=None, dynamips_id=None, additional_settings={}, default_name_format="R{0}"):
|
||||
"""
|
||||
Creates this router.
|
||||
|
||||
:param image: IOS image path
|
||||
:param ram: amount of RAM
|
||||
:param name: optional name for this router
|
||||
:param node_id: Node identifier on the server
|
||||
:param dynamips_id: Dynamips identifier on the server
|
||||
:param additional_settings: other additional and not mandatory settings
|
||||
"""
|
||||
|
||||
platform = self._settings["platform"]
|
||||
self._settings["ram"] = ram
|
||||
self._settings["image"] = image
|
||||
|
||||
# Minimum settings to send to the server in order to create a new router
|
||||
params = {"name": name,
|
||||
"platform": platform,
|
||||
"ram": ram,
|
||||
"image": image}
|
||||
|
||||
if dynamips_id:
|
||||
params["dynamips_id"] = dynamips_id
|
||||
|
||||
# push the startup-config
|
||||
if not node_id and "startup_config" in additional_settings:
|
||||
base_config_content = self._readBaseConfig(additional_settings["startup_config"])
|
||||
if base_config_content is not None:
|
||||
params["startup_config_content"] = base_config_content
|
||||
del additional_settings["startup_config"]
|
||||
|
||||
# push the private-config
|
||||
if not node_id and "private_config" in additional_settings:
|
||||
base_config_content = self._readBaseConfig(additional_settings["private_config"])
|
||||
if base_config_content is not None:
|
||||
params["private_config_content"] = base_config_content
|
||||
del additional_settings["private_config"]
|
||||
|
||||
params.update(additional_settings)
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
@@ -147,18 +104,6 @@ class Router(Node):
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if "startup_config" in new_settings:
|
||||
base_config_content = self._readBaseConfig(new_settings["startup_config"])
|
||||
if base_config_content is not None:
|
||||
params["startup_config_content"] = base_config_content
|
||||
del new_settings["startup_config"]
|
||||
|
||||
if "private_config" in new_settings:
|
||||
if new_settings["private_config"] and os.path.isfile(new_settings["private_config"]):
|
||||
base_config_content = self._readBaseConfig(new_settings["private_config"])
|
||||
if base_config_content is not None:
|
||||
params["private_config_content"] = base_config_content
|
||||
del new_settings["private_config"]
|
||||
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings:
|
||||
@@ -181,9 +126,9 @@ class Router(Node):
|
||||
for name, value in result.items():
|
||||
if name in self._settings:
|
||||
if self._settings[name] != value:
|
||||
log.info("{}: updating {} from '{}' to '{}'".format(self.name(), name, self._settings[name], value))
|
||||
log.debug("{}: updating {} from '{}' to '{}'".format(self.name(), name, self._settings[name], value))
|
||||
self._settings[name] = value
|
||||
elif name not in ("project_id", "port_name_format", "port_segment_size", "first_port_name", "node_directory", "status", "node_id", "width", "height", "compute_id", "node_type", "startup_config_content", "private_config_content", "dynamips_id", "command_line"):
|
||||
elif name not in ("project_id", "port_name_format", "port_segment_size", "first_port_name", "node_directory", "status", "node_id", "width", "height", "compute_id", "node_type", "dynamips_id", "command_line"):
|
||||
# All key should be known, but we raise error only in debug
|
||||
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
||||
raise ValueError(name)
|
||||
@@ -369,50 +314,6 @@ class Router(Node):
|
||||
slot_info = self._slot_info()
|
||||
return info + slot_info
|
||||
|
||||
def exportConfigToDirectory(self, directory):
|
||||
"""
|
||||
Exports the startup-config and private-config to a directory.
|
||||
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
self.controllerHttpGet("/nodes/{node_id}".format(node_id=self._node_id),
|
||||
self._exportConfigToDirectoryCallback,
|
||||
context={"directory": directory})
|
||||
|
||||
def _exportConfigToDirectoryCallback(self, result, error=False, context={}, **kwargs):
|
||||
"""
|
||||
Callback for exportConfigToDirectory.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("error while exporting {} configs: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["message"])
|
||||
else:
|
||||
result = result["properties"]
|
||||
directory = context["directory"]
|
||||
if "startup_config_content" in result:
|
||||
config_path = os.path.join(directory, normalize_filename(self.name())) + "_startup-config.cfg"
|
||||
try:
|
||||
with open(config_path, "wb") as f:
|
||||
log.info("saving {} startup-config to {}".format(self.name(), config_path))
|
||||
if result["startup_config_content"]:
|
||||
f.write(result["startup_config_content"].encode("utf-8"))
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "Could not export startup-config to {}: {}".format(config_path, e))
|
||||
if "private_config_content" in result:
|
||||
config_path = os.path.join(directory, normalize_filename(self.name())) + "_private-config.cfg"
|
||||
try:
|
||||
with open(config_path, "wb") as f:
|
||||
log.info("saving {} private-config to {}".format(self.name(), config_path))
|
||||
if result["private_config_content"]:
|
||||
f.write(result["private_config_content"].encode("utf-8"))
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "Could not export private-config to {}: {}".format(config_path, e))
|
||||
|
||||
def configFiles(self):
|
||||
"""
|
||||
Name of the configuration files
|
||||
@@ -422,52 +323,6 @@ class Router(Node):
|
||||
"configs/i{}_private-config.cfg".format(self._dynamips_id)
|
||||
]
|
||||
|
||||
def importConfig(self, path):
|
||||
"""
|
||||
Imports a startup-config.
|
||||
|
||||
:param path: path to the startup-config
|
||||
"""
|
||||
|
||||
new_settings = {"startup_config": path}
|
||||
self.update(new_settings)
|
||||
|
||||
def importPrivateConfig(self, path):
|
||||
"""
|
||||
Imports a private-config.
|
||||
|
||||
:param path: path to the private-config
|
||||
"""
|
||||
|
||||
new_settings = {"private_config": path}
|
||||
self.update(new_settings)
|
||||
|
||||
def importConfigFromDirectory(self, directory):
|
||||
"""
|
||||
Imports a startup-config and a private-config from a directory.
|
||||
|
||||
:param directory: source directory path
|
||||
"""
|
||||
|
||||
try:
|
||||
contents = os.listdir(directory)
|
||||
except OSError as e:
|
||||
return
|
||||
startup_config = normalize_filename(self.name()) + "_startup-config.cfg"
|
||||
private_config = normalize_filename(self.name()) + "_private-config.cfg"
|
||||
new_settings = {}
|
||||
if startup_config in contents:
|
||||
new_settings["startup_config"] = os.path.join(directory, startup_config)
|
||||
|
||||
if private_config in contents:
|
||||
new_settings["private_config"] = os.path.join(directory, private_config)
|
||||
else:
|
||||
# private-config is optional
|
||||
log.debug("{}: no private-config file could be found, expected file name: {}".format(self.name(), private_config))
|
||||
|
||||
if new_settings:
|
||||
self.update(new_settings)
|
||||
|
||||
def console(self):
|
||||
"""
|
||||
Returns the console port for this router.
|
||||
|
||||
@@ -26,6 +26,7 @@ from gns3.qt import QtCore, QtGui, QtWidgets
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.dialogs.node_properties_dialog import ConfigurationError
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.controller import Controller
|
||||
from gns3.node import Node
|
||||
from ..ui.ios_router_configuration_page_ui import Ui_iosRouterConfigPageWidget
|
||||
from ..settings import CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
|
||||
@@ -71,6 +72,10 @@ class IOSRouterConfigurationPage(QtWidgets.QWidget, Ui_iosRouterConfigPageWidget
|
||||
for name, category in Node.defaultCategories().items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
|
||||
if Controller.instance().isRemote():
|
||||
self.uiStartupConfigToolButton.hide()
|
||||
self.uiPrivateConfigToolButton.hide()
|
||||
|
||||
def _idlePCValidateSlot(self):
|
||||
"""
|
||||
Slot to validate the entered Idle-PC Value
|
||||
|
||||
@@ -24,7 +24,6 @@ import os
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.local_config import LocalConfig
|
||||
|
||||
from ..module import Module
|
||||
@@ -175,69 +174,9 @@ class IOU(Module):
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node))
|
||||
|
||||
iouimage = None
|
||||
if node_name:
|
||||
for iou_key, info in self._iou_devices.items():
|
||||
if node_name == info["name"]:
|
||||
iouimage = iou_key
|
||||
|
||||
if not iouimage:
|
||||
selected_images = []
|
||||
for image, info in self._iou_devices.items():
|
||||
if info["server"] == node.compute().id():
|
||||
selected_images.append(image)
|
||||
|
||||
if not selected_images:
|
||||
raise ModuleError("No IOU image found for this device")
|
||||
elif len(selected_images) > 1:
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
|
||||
(selection, ok) = QtWidgets.QInputDialog.getItem(mainwindow, "IOU image", "Please choose an image", selected_images, 0, False)
|
||||
if ok:
|
||||
iouimage = selection
|
||||
else:
|
||||
raise ModuleError("Please select an IOU image")
|
||||
|
||||
else:
|
||||
iouimage = selected_images[0]
|
||||
|
||||
vm_settings = {}
|
||||
for setting_name, value in self._iou_devices[iouimage].items():
|
||||
if setting_name in node.settings() and setting_name != "name" and value != "" and value is not None:
|
||||
vm_settings[setting_name] = value
|
||||
|
||||
default_name_format = IOU_DEVICE_SETTINGS["default_name_format"]
|
||||
if self._iou_devices[iouimage]["default_name_format"]:
|
||||
default_name_format = self._iou_devices[iouimage]["default_name_format"]
|
||||
|
||||
if vm_settings["use_default_iou_values"]:
|
||||
del vm_settings["ram"]
|
||||
del vm_settings["nvram"]
|
||||
|
||||
if "console" in vm_settings:
|
||||
# Older GNS3 versions may have a console setting in the VM template
|
||||
del vm_settings["console"]
|
||||
|
||||
iou_path = vm_settings.pop("path")
|
||||
node.create(iou_path, additional_settings=vm_settings, default_name_format=default_name_format)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the servers.
|
||||
@@ -245,28 +184,6 @@ class IOU(Module):
|
||||
|
||||
self._nodes.clear()
|
||||
|
||||
def exportConfigs(self, directory):
|
||||
"""
|
||||
Exports all configs for all nodes to a directory.
|
||||
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if node.initialized():
|
||||
node.exportConfigToDirectory(directory)
|
||||
|
||||
def importConfigs(self, directory):
|
||||
"""
|
||||
Imports configs to all nodes from a directory.
|
||||
|
||||
:param directory: source directory path
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if node.initialized():
|
||||
node.importConfigFromDirectory(directory)
|
||||
|
||||
def findAlternativeIOUImage(self, image):
|
||||
"""
|
||||
Tries to find an alternative IOU image
|
||||
|
||||
@@ -25,7 +25,6 @@ import sys
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.node import Node
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from gns3.utils.get_default_base_config import get_default_base_config
|
||||
from gns3.dialogs.vm_with_images_wizard import VMWithImagesWizard
|
||||
from gns3.compute_manager import ComputeManager
|
||||
|
||||
@@ -63,8 +62,8 @@ class IOUDeviceWizard(VMWithImagesWizard, Ui_IOUDeviceWizard):
|
||||
self.uiIOUImageLineEdit.textChanged.connect(self._imageLineEditTextChangedSlot)
|
||||
|
||||
# location of the base config templates
|
||||
self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_startup-config.txt"))
|
||||
self._base_iou_l3_config_template = get_resource(os.path.join("configs", "iou_l3_base_startup-config.txt"))
|
||||
self._base_iou_l2_config_template = "iou_l2_base_startup-config.txt"
|
||||
self._base_iou_l3_config_template = "iou_l3_base_startup-config.txt"
|
||||
|
||||
from ..pages.iou_device_preferences_page import IOUDevicePreferencesPage
|
||||
self.addImageSelector(self.uiExistingImageRadioButton, self.uiIOUImageListComboBox, self.uiIOUImageLineEdit, self.uiIOUImageToolButton, IOUDevicePreferencesPage.getIOUImage)
|
||||
@@ -113,7 +112,7 @@ class IOUDeviceWizard(VMWithImagesWizard, Ui_IOUDeviceWizard):
|
||||
startup_config = ""
|
||||
if self.uiTypeComboBox.currentText() == "L2 image":
|
||||
# set the default L2 base startup-config
|
||||
default_base_config = get_default_base_config(self._base_iou_l2_config_template)
|
||||
default_base_config = self._base_iou_l2_config_template
|
||||
if default_base_config:
|
||||
startup_config = default_base_config
|
||||
symbol = ":/symbols/multilayer_switch.svg"
|
||||
@@ -122,7 +121,7 @@ class IOUDeviceWizard(VMWithImagesWizard, Ui_IOUDeviceWizard):
|
||||
serial_adapters = 0
|
||||
else:
|
||||
# set the default L3 base startup-config
|
||||
default_base_config = get_default_base_config(self._base_iou_l3_config_template)
|
||||
default_base_config = self._base_iou_l3_config_template
|
||||
if default_base_config:
|
||||
startup_config = default_base_config
|
||||
symbol = ":/symbols/router.svg"
|
||||
@@ -133,7 +132,6 @@ class IOUDeviceWizard(VMWithImagesWizard, Ui_IOUDeviceWizard):
|
||||
settings = {
|
||||
"name": self.uiNameLineEdit.text(),
|
||||
"path": path,
|
||||
"image": os.path.basename(path),
|
||||
"startup_config": startup_config,
|
||||
"ethernet_adapters": ethernet_adapters,
|
||||
"serial_adapters": serial_adapters,
|
||||
|
||||
@@ -23,7 +23,6 @@ import os
|
||||
import re
|
||||
from gns3.node import Node
|
||||
from gns3.utils.normalize_filename import normalize_filename
|
||||
from gns3.image_manager import ImageManager
|
||||
from .settings import IOU_DEVICE_SETTINGS
|
||||
|
||||
import logging
|
||||
@@ -45,8 +44,6 @@ class IOUDevice(Node):
|
||||
def __init__(self, module, server, project):
|
||||
super().__init__(module, server, project)
|
||||
|
||||
log.info("IOU instance is being created")
|
||||
|
||||
iou_device_settings = {"path": "",
|
||||
"md5sum": "",
|
||||
"startup_config": "",
|
||||
@@ -62,33 +59,6 @@ class IOUDevice(Node):
|
||||
|
||||
self.settings().update(iou_device_settings)
|
||||
|
||||
def create(self, iou_path, name=None, node_id=None, additional_settings={}, default_name_format="IOU{0}"):
|
||||
"""
|
||||
Creates this IOU device.
|
||||
|
||||
:param iou_path: path to an IOU image
|
||||
:param name: optional name
|
||||
:param console: optional TCP console port
|
||||
"""
|
||||
|
||||
params = {"path": iou_path}
|
||||
# push the startup-config
|
||||
if "startup_config" in additional_settings:
|
||||
base_config_content = self._readBaseConfig(additional_settings["startup_config"])
|
||||
if base_config_content is not None:
|
||||
params["startup_config_content"] = base_config_content
|
||||
del additional_settings["startup_config"]
|
||||
|
||||
# push the startup-config
|
||||
if "private_config" in additional_settings:
|
||||
base_config_content = self._readBaseConfig(additional_settings["private_config"])
|
||||
if base_config_content is not None:
|
||||
params["private_config_content"] = base_config_content
|
||||
del additional_settings["private_config"]
|
||||
|
||||
params.update(additional_settings)
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
@@ -106,8 +76,6 @@ class IOUDevice(Node):
|
||||
log.debug("{} is already running".format(self.name()))
|
||||
return
|
||||
|
||||
params = {}
|
||||
|
||||
log.debug("{} is starting".format(self.name()))
|
||||
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, progressText="{} is starting".format(self.name()))
|
||||
|
||||
@@ -119,18 +87,6 @@ class IOUDevice(Node):
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if "startup_config" in new_settings:
|
||||
base_config_content = self._readBaseConfig(new_settings["startup_config"])
|
||||
if base_config_content is not None:
|
||||
params["startup_config_content"] = base_config_content
|
||||
del new_settings["startup_config"]
|
||||
|
||||
if "private_config" in new_settings:
|
||||
base_config_content = self._readBaseConfig(new_settings["private_config"])
|
||||
if base_config_content is not None:
|
||||
params["private_config_content"] = base_config_content
|
||||
del new_settings["private_config"]
|
||||
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
@@ -189,95 +145,6 @@ class IOUDevice(Node):
|
||||
"""
|
||||
return ["startup-config.cfg", "private-config.cfg"]
|
||||
|
||||
def exportConfigToDirectory(self, directory):
|
||||
"""
|
||||
Exports the initial-config to a directory.
|
||||
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
self.controllerHttpGet("/nodes/{node_id}".format(node_id=self._node_id),
|
||||
self._exportConfigToDirectoryCallback,
|
||||
context={"directory": directory})
|
||||
|
||||
def _exportConfigToDirectoryCallback(self, result, error=False, context={}, **kwargs):
|
||||
"""
|
||||
Callback for exportConfigToDirectory.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("error while exporting {} IOU configs: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["message"])
|
||||
return
|
||||
export_directory = context["directory"]
|
||||
|
||||
result = result["properties"]
|
||||
if "startup_config_content" in result:
|
||||
startup_config_path = os.path.join(export_directory, normalize_filename(self.name())) + "_startup-config.cfg"
|
||||
try:
|
||||
with open(startup_config_path, "wb") as f:
|
||||
log.info("saving {} startup-config to {}".format(self.name(), startup_config_path))
|
||||
if result["startup_config_content"]:
|
||||
f.write(result["startup_config_content"].encode("utf-8"))
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not export startup-config to {}: {}".format(startup_config_path, e))
|
||||
|
||||
if "private_config_content" in result and result["private_config_content"] is not None and len(result["private_config_content"]) > 0:
|
||||
private_config_path = os.path.join(export_directory, normalize_filename(self.name())) + "_private-config.cfg"
|
||||
try:
|
||||
with open(private_config_path, "wb") as f:
|
||||
log.info("saving {} private-config to {}".format(self.name(), private_config_path))
|
||||
if result["private_config_content"]:
|
||||
f.write(result["private_config_content"].encode("utf-8"))
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not export private-config to {}: {}".format(startup_config_path, e))
|
||||
|
||||
def importConfig(self, path):
|
||||
"""
|
||||
Imports a startup-config.
|
||||
|
||||
:param path: path to the startup-config
|
||||
"""
|
||||
|
||||
new_settings = {"startup_config": path}
|
||||
self.update(new_settings)
|
||||
|
||||
def importPrivateConfig(self, path):
|
||||
"""
|
||||
Imports a private-config.
|
||||
|
||||
:param path: path to the private-config
|
||||
"""
|
||||
|
||||
new_settings = {"private_config": path}
|
||||
self.update(new_settings)
|
||||
|
||||
def importConfigFromDirectory(self, directory):
|
||||
"""
|
||||
Imports IOU configs from a directory.
|
||||
|
||||
:param directory: source directory path
|
||||
"""
|
||||
|
||||
contents = os.listdir(directory)
|
||||
startup_config = normalize_filename(self.name()) + "_startup-config.cfg"
|
||||
private_config = normalize_filename(self.name()) + "_private-config.cfg"
|
||||
new_settings = {}
|
||||
if startup_config in contents:
|
||||
new_settings["startup_config"] = os.path.join(directory, startup_config)
|
||||
|
||||
if private_config in contents:
|
||||
new_settings["private_config"] = os.path.join(directory, private_config)
|
||||
else:
|
||||
# private-config is optional
|
||||
log.debug("{}: no private-config file could be found, expected file name: {}".format(self.name(), private_config))
|
||||
|
||||
if new_settings:
|
||||
self.update(new_settings)
|
||||
|
||||
def console(self):
|
||||
"""
|
||||
Returns the console port for this IOU device.
|
||||
|
||||
@@ -26,8 +26,8 @@ from gns3.local_server import LocalServer
|
||||
from gns3.dialogs.node_properties_dialog import ConfigurationError
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.node import Node
|
||||
from gns3.controller import Controller
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from gns3.utils.get_default_base_config import get_default_base_config
|
||||
from ..ui.iou_device_configuration_page_ui import Ui_iouDeviceConfigPageWidget
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
|
||||
self.uiDefaultValuesCheckBox.stateChanged.connect(self._useDefaultValuesSlot)
|
||||
self._current_iou_image = ""
|
||||
self._compute_id = None
|
||||
if Controller.instance().isRemote():
|
||||
self.uiStartupConfigToolButton.hide()
|
||||
self.uiPrivateConfigToolButton.hide()
|
||||
|
||||
# location of the base config templates
|
||||
self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_startup-config.txt"))
|
||||
@@ -86,12 +89,12 @@ class iouDeviceConfigurationPage(QtWidgets.QWidget, Ui_iouDeviceConfigPageWidget
|
||||
if len(self.uiStartupConfigLineEdit.text().strip()) == 0:
|
||||
if "l2" in path:
|
||||
# set the default L2 base startup-config
|
||||
default_base_config = get_default_base_config(self._base_iou_l2_config_template)
|
||||
default_base_config = self._base_iou_l2_config_template
|
||||
if default_base_config:
|
||||
self.uiStartupConfigLineEdit.setText(default_base_config)
|
||||
else:
|
||||
# set the default L3 base startup-config
|
||||
default_base_config = get_default_base_config(self._base_iou_l3_config_template)
|
||||
default_base_config = self._base_iou_l3_config_template
|
||||
if default_base_config:
|
||||
self.uiStartupConfigLineEdit.setText(default_base_config)
|
||||
|
||||
|
||||
@@ -256,9 +256,9 @@ class IOUDevicePreferencesPage(QtWidgets.QWidget, Ui_IOUDevicePreferencesPageWid
|
||||
QtWidgets.QMessageBox.critical(parent, "IOU image", "Cannot read ELF magic number: {}".format(e))
|
||||
return
|
||||
|
||||
# file 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':
|
||||
# file must start with the ELF magic number, be 32-bit or 64-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' and elf_header_start != b'\x7fELF\x02\x01\x01':
|
||||
QtWidgets.QMessageBox.critical(parent, "IOU image", "Sorry, this is not a valid IOU image!")
|
||||
return
|
||||
|
||||
|
||||
@@ -70,3 +70,25 @@ class Module(QtCore.QObject):
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def exportConfigs(self, directory):
|
||||
"""
|
||||
Exports all configs for all nodes to a directory.
|
||||
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if hasattr(node, "initialized") and node.initialized():
|
||||
node.exportConfigToDirectory(directory)
|
||||
|
||||
def importConfigs(self, directory):
|
||||
"""
|
||||
Imports configs to all nodes from a directory.
|
||||
|
||||
:param directory: source directory path
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if hasattr(node, "initialized") and node.initialized():
|
||||
node.importConfigFromDirectory(directory)
|
||||
|
||||
@@ -174,73 +174,9 @@ class Qemu(Module):
|
||||
:param server: HTTPClient instance
|
||||
"""
|
||||
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("creating node {} with id {}".format(node, node.id()))
|
||||
|
||||
vm = None
|
||||
if node_name:
|
||||
for vm_key, info in self._qemu_vms.items():
|
||||
if node_name == info["name"]:
|
||||
vm = vm_key
|
||||
|
||||
if not vm:
|
||||
selected_vms = []
|
||||
for vm, info in self._qemu_vms.items():
|
||||
if info["server"] == node.compute().id():
|
||||
selected_vms.append(vm)
|
||||
|
||||
if not selected_vms:
|
||||
raise ModuleError("No QEMU VM on server {}".format(node.server().host()))
|
||||
elif len(selected_vms) > 1:
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
|
||||
(selection, ok) = QtWidgets.QInputDialog.getItem(mainwindow, "QEMU VM", "Please choose a VM", selected_vms, 0, False)
|
||||
if ok:
|
||||
vm = selection
|
||||
else:
|
||||
raise ModuleError("Please select a QEMU VM")
|
||||
else:
|
||||
vm = selected_vms[0]
|
||||
|
||||
vm_settings = {}
|
||||
for setting_name, value in self._qemu_vms[vm].items():
|
||||
if setting_name in node.settings() and value != "" and value is not None:
|
||||
vm_settings[setting_name] = value
|
||||
|
||||
qemu_path = vm_settings.pop("qemu_path")
|
||||
name = vm_settings.pop("name")
|
||||
port_name_format = self._qemu_vms[vm]["port_name_format"]
|
||||
port_segment_size = self._qemu_vms[vm]["port_segment_size"]
|
||||
first_port_name = self._qemu_vms[vm]["first_port_name"]
|
||||
|
||||
default_name_format = QEMU_VM_SETTINGS["default_name_format"]
|
||||
if self._qemu_vms[vm]["default_name_format"]:
|
||||
default_name_format = self._qemu_vms[vm]["default_name_format"]
|
||||
if self._qemu_vms[vm]["linked_base"]:
|
||||
name = default_name_format.replace('{name}', name)
|
||||
|
||||
node.create(qemu_path,
|
||||
name=name,
|
||||
port_name_format=port_name_format,
|
||||
port_segment_size=port_segment_size,
|
||||
first_port_name=first_port_name,
|
||||
linked_clone=self._qemu_vms[vm]["linked_base"],
|
||||
additional_settings=vm_settings,
|
||||
default_name_format=default_name_format)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the servers.
|
||||
|
||||
@@ -332,8 +332,8 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
# set the device name
|
||||
self.uiNameLineEdit.setText(settings["name"])
|
||||
|
||||
if "linked_base" in settings:
|
||||
self.uiBaseVMCheckBox.setChecked(settings["linked_base"])
|
||||
if "linked_clone" in settings:
|
||||
self.uiBaseVMCheckBox.setChecked(settings["linked_clone"])
|
||||
else:
|
||||
self.uiBaseVMCheckBox.hide()
|
||||
|
||||
@@ -457,8 +457,8 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
else:
|
||||
settings["name"] = name
|
||||
|
||||
if "linked_base" in settings:
|
||||
settings["linked_base"] = self.uiBaseVMCheckBox.isChecked()
|
||||
if "linked_clone" in settings:
|
||||
settings["linked_clone"] = self.uiBaseVMCheckBox.isChecked()
|
||||
|
||||
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text().strip()
|
||||
settings["hdb_disk_image"] = self.uiHdbDiskImageLineEdit.text().strip()
|
||||
|
||||
@@ -70,7 +70,7 @@ class QemuVMPreferencesPage(QtWidgets.QWidget, Ui_QemuVMPreferencesPageWidget):
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", qemu_vm["name"]])
|
||||
if qemu_vm["linked_base"]:
|
||||
if qemu_vm["linked_clone"]:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", qemu_vm["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(qemu_vm["server"]).name()])
|
||||
@@ -79,7 +79,7 @@ class QemuVMPreferencesPage(QtWidgets.QWidget, Ui_QemuVMPreferencesPageWidget):
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Console type:", qemu_vm["console_type"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["CPUs:", str(qemu_vm["cpus"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Memory:", "{} MB".format(qemu_vm["ram"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(qemu_vm["linked_base"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(qemu_vm["linked_clone"])])
|
||||
|
||||
if qemu_vm["qemu_path"]:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["QEMU binary:", os.path.basename(qemu_vm["qemu_path"])])
|
||||
|
||||
@@ -41,7 +41,6 @@ class QemuVM(Node):
|
||||
def __init__(self, module, server, project):
|
||||
super().__init__(module, server, project)
|
||||
|
||||
log.info("QEMU VM instance is being created")
|
||||
self._linked_clone = True
|
||||
|
||||
qemu_vm_settings = {"usage": "",
|
||||
@@ -88,24 +87,6 @@ class QemuVM(Node):
|
||||
|
||||
self.settings().update(qemu_vm_settings)
|
||||
|
||||
def create(self, qemu_path, name=None, node_id=None, port_name_format="Ethernet{0}", port_segment_size=0,
|
||||
first_port_name="", linked_clone=True, additional_settings={}, default_name_format=None):
|
||||
"""
|
||||
Creates this QEMU VM.
|
||||
|
||||
:param name: optional name
|
||||
:param node_id: Node identifier
|
||||
"""
|
||||
|
||||
self._linked_clone = linked_clone
|
||||
params = {"qemu_path": qemu_path,
|
||||
"linked_clone": linked_clone,
|
||||
"port_name_format": port_name_format,
|
||||
"port_segment_size": port_segment_size,
|
||||
"first_port_name": first_port_name}
|
||||
params.update(additional_settings)
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
@@ -61,6 +61,6 @@ QEMU_VM_SETTINGS = {
|
||||
"kernel_image": "",
|
||||
"initrd": "",
|
||||
"kernel_command_line": "",
|
||||
"linked_base": True,
|
||||
"linked_clone": True,
|
||||
"server": "local"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,16 @@
|
||||
<string>General settings</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item row="4" column="0">
|
||||
@@ -92,6 +101,11 @@
|
||||
<string>vnc</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>spice</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
@@ -189,7 +203,16 @@
|
||||
<string>HDD</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -412,7 +435,16 @@
|
||||
<string>CD/DVD</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -468,7 +500,16 @@
|
||||
<string>Network</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item row="6" column="1">
|
||||
@@ -551,7 +592,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>32</number>
|
||||
<number>275</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -592,7 +633,16 @@
|
||||
<string>Advanced settings</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
|
||||
#
|
||||
# Created: Thu Jan 5 14:49:45 2017
|
||||
# by: PyQt5 UI code generator 5.2.1
|
||||
# Created by: PyQt5 UI code generator 5.8
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -54,6 +53,7 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiConsoleTypeComboBox.setObjectName("uiConsoleTypeComboBox")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.uiConsoleTypeComboBox.addItem("")
|
||||
self.gridLayout_4.addWidget(self.uiConsoleTypeComboBox, 8, 1, 1, 1)
|
||||
self.uiConsoleTypeLabel = QtWidgets.QLabel(self.uiGeneralSettingsTab)
|
||||
self.uiConsoleTypeLabel.setObjectName("uiConsoleTypeLabel")
|
||||
@@ -286,7 +286,7 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
sizePolicy.setHeightForWidth(self.uiAdaptersSpinBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiAdaptersSpinBox.setSizePolicy(sizePolicy)
|
||||
self.uiAdaptersSpinBox.setMinimum(0)
|
||||
self.uiAdaptersSpinBox.setMaximum(32)
|
||||
self.uiAdaptersSpinBox.setMaximum(275)
|
||||
self.uiAdaptersSpinBox.setObjectName("uiAdaptersSpinBox")
|
||||
self.gridLayout_5.addWidget(self.uiAdaptersSpinBox, 0, 1, 1, 1)
|
||||
self.uiPortNameFormatLineEdit = QtWidgets.QLineEdit(self.uiNetworkTab)
|
||||
@@ -404,6 +404,10 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiACPIShutdownCheckBox = QtWidgets.QCheckBox(self.groupBox)
|
||||
self.uiACPIShutdownCheckBox.setObjectName("uiACPIShutdownCheckBox")
|
||||
self.gridLayout_3.addWidget(self.uiACPIShutdownCheckBox, 2, 0, 1, 2)
|
||||
self.uiQemuOptionsLineEdit.raise_()
|
||||
self.uiQemuOptionsLabel.raise_()
|
||||
self.uiACPIShutdownCheckBox.raise_()
|
||||
self.uiBaseVMCheckBox.raise_()
|
||||
self.verticalLayout_2.addWidget(self.groupBox)
|
||||
spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.verticalLayout_2.addItem(spacerItem4)
|
||||
@@ -424,6 +428,7 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiSymbolLabel.setText(_translate("QemuVMConfigPageWidget", "Symbol:"))
|
||||
self.uiConsoleTypeComboBox.setItemText(0, _translate("QemuVMConfigPageWidget", "telnet"))
|
||||
self.uiConsoleTypeComboBox.setItemText(1, _translate("QemuVMConfigPageWidget", "vnc"))
|
||||
self.uiConsoleTypeComboBox.setItemText(2, _translate("QemuVMConfigPageWidget", "spice"))
|
||||
self.uiConsoleTypeLabel.setText(_translate("QemuVMConfigPageWidget", "Console type:"))
|
||||
self.uiBootPriorityLabel.setText(_translate("QemuVMConfigPageWidget", "Boot priority:"))
|
||||
self.uiQemuListLabel.setText(_translate("QemuVMConfigPageWidget", "Qemu binary:"))
|
||||
|
||||
@@ -185,6 +185,11 @@
|
||||
<string>vnc</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>spice</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/noplay/code/gns3/gns3-gui/gns3/modules/qemu/ui/qemu_vm_wizard.ui'
|
||||
# Form implementation generated from reading ui file '/home/dominik/projects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_wizard.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.6
|
||||
# Created by: PyQt5 UI code generator 5.8.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_QemuVMWizard(object):
|
||||
|
||||
def setupUi(self, QemuVMWizard):
|
||||
QemuVMWizard.setObjectName("QemuVMWizard")
|
||||
QemuVMWizard.resize(623, 417)
|
||||
@@ -99,6 +97,7 @@ class Ui_QemuVMWizard(object):
|
||||
self.uiQemuConsoleTypeComboBox.setObjectName("uiQemuConsoleTypeComboBox")
|
||||
self.uiQemuConsoleTypeComboBox.addItem("")
|
||||
self.uiQemuConsoleTypeComboBox.addItem("")
|
||||
self.uiQemuConsoleTypeComboBox.addItem("")
|
||||
self.verticalLayout.addWidget(self.uiQemuConsoleTypeComboBox)
|
||||
self.label = QtWidgets.QLabel(self.uiConsoleTypeWizardPage)
|
||||
self.label.setObjectName("label")
|
||||
@@ -234,6 +233,7 @@ class Ui_QemuVMWizard(object):
|
||||
self.uiConsoleTypeWizardPage.setSubTitle(_translate("QemuVMWizard", "Please choose the console type. Telnet will connect to the serial console of the machine. VNC will connect to graphical output of the machine."))
|
||||
self.uiQemuConsoleTypeComboBox.setItemText(0, _translate("QemuVMWizard", "telnet"))
|
||||
self.uiQemuConsoleTypeComboBox.setItemText(1, _translate("QemuVMWizard", "vnc"))
|
||||
self.uiQemuConsoleTypeComboBox.setItemText(2, _translate("QemuVMWizard", "spice"))
|
||||
self.label.setText(_translate("QemuVMWizard", "Note: You don\'t need to install anything on the VM itself."))
|
||||
self.uiDiskWizardPage.setTitle(_translate("QemuVMWizard", "Disk image"))
|
||||
self.uiDiskWizardPage.setSubTitle(_translate("QemuVMWizard", "Please choose a base disk image for your virtual machine."))
|
||||
@@ -250,3 +250,4 @@ class Ui_QemuVMWizard(object):
|
||||
self.uiKernelImageLabel.setText(_translate("QemuVMWizard", "Kernel image (vmlinuz):"))
|
||||
self.uiKernelImageToolButton.setText(_translate("QemuVMWizard", "&Browse..."))
|
||||
self.uiInitrdLabel.setText(_translate("QemuVMWizard", "Initial RAM disk (initrd):"))
|
||||
|
||||
|
||||
@@ -216,74 +216,9 @@ class VirtualBox(Module):
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("creating node {} with id {}".format(node, node.id()))
|
||||
|
||||
vm = None
|
||||
if node_name:
|
||||
for vm_key, info in self._virtualbox_vms.items():
|
||||
if node_name == info["name"]:
|
||||
vm = vm_key
|
||||
|
||||
if not vm:
|
||||
selected_vms = []
|
||||
for vm, info in self._virtualbox_vms.items():
|
||||
if info["server"] == node.compute().id():
|
||||
selected_vms.append(vm)
|
||||
|
||||
if not selected_vms:
|
||||
raise ModuleError("No VirtualBox VM on server {}".format(node.server().url()))
|
||||
elif len(selected_vms) > 1:
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
|
||||
(selection, ok) = QtWidgets.QInputDialog.getItem(mainwindow, "VirtualBox VM", "Please choose a VM", selected_vms, 0, False)
|
||||
if ok:
|
||||
vm = selection
|
||||
else:
|
||||
raise ModuleError("Please select a VirtualBox VM")
|
||||
|
||||
else:
|
||||
vm = selected_vms[0]
|
||||
|
||||
vm_settings = {}
|
||||
for setting_name, value in self._virtualbox_vms[vm].items():
|
||||
if setting_name != "name" and setting_name in node.settings() and value != "" and value is not None:
|
||||
vm_settings[setting_name] = value
|
||||
|
||||
name = self._virtualbox_vms[vm]["name"]
|
||||
vmname = self._virtualbox_vms[vm]["vmname"]
|
||||
port_name_format = self._virtualbox_vms[vm]["port_name_format"]
|
||||
port_segment_size = self._virtualbox_vms[vm]["port_segment_size"]
|
||||
first_port_name = self._virtualbox_vms[vm]["first_port_name"]
|
||||
|
||||
default_name_format = VBOX_VM_SETTINGS["default_name_format"]
|
||||
if self._virtualbox_vms[vm]["default_name_format"]:
|
||||
default_name_format = self._virtualbox_vms[vm]["default_name_format"]
|
||||
if self._virtualbox_vms[vm]["linked_base"]:
|
||||
name = default_name_format.replace('{name}', name)
|
||||
|
||||
node.create(vmname,
|
||||
name=name,
|
||||
port_name_format=port_name_format,
|
||||
port_segment_size=port_segment_size,
|
||||
first_port_name=first_port_name,
|
||||
linked_clone=self._virtualbox_vms[vm]["linked_base"],
|
||||
additional_settings=vm_settings,
|
||||
default_name_format=default_name_format)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the module.
|
||||
|
||||
@@ -100,7 +100,7 @@ class VirtualBoxVMWizard(VMWizard, Ui_VirtualBoxVMWizard):
|
||||
"vmname": vmname,
|
||||
"server": self._compute_id,
|
||||
"ram": vminfo["ram"],
|
||||
"linked_base": self.uiBaseVMCheckBox.isChecked()
|
||||
"linked_clone": self.uiBaseVMCheckBox.isChecked()
|
||||
}
|
||||
|
||||
return settings
|
||||
|
||||
@@ -86,8 +86,8 @@ class VirtualBoxVMConfigurationPage(QtWidgets.QWidget, Ui_virtualBoxVMConfigPage
|
||||
self.uiNameLabel.hide()
|
||||
self.uiNameLineEdit.hide()
|
||||
|
||||
if "linked_base" in settings:
|
||||
self.uiBaseVMCheckBox.setChecked(settings["linked_base"])
|
||||
if "linked_clone" in settings:
|
||||
self.uiBaseVMCheckBox.setChecked(settings["linked_clone"])
|
||||
else:
|
||||
self.uiBaseVMCheckBox.hide()
|
||||
|
||||
@@ -163,8 +163,8 @@ class VirtualBoxVMConfigurationPage(QtWidgets.QWidget, Ui_virtualBoxVMConfigPage
|
||||
else:
|
||||
settings["name"] = name
|
||||
|
||||
if "linked_base" in settings:
|
||||
settings["linked_base"] = self.uiBaseVMCheckBox.isChecked()
|
||||
if "linked_clone" in settings:
|
||||
settings["linked_clone"] = self.uiBaseVMCheckBox.isChecked()
|
||||
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
@@ -70,7 +70,7 @@ class VirtualBoxVMPreferencesPage(QtWidgets.QWidget, Ui_VirtualBoxVMPreferencesP
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", vbox_vm["name"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["VirtualBox name:", vbox_vm["vmname"]])
|
||||
if vbox_vm["linked_base"]:
|
||||
if vbox_vm["linked_clone"]:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", vbox_vm["default_name_format"]])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["RAM:", str(vbox_vm["ram"])])
|
||||
try:
|
||||
@@ -79,7 +79,7 @@ class VirtualBoxVMPreferencesPage(QtWidgets.QWidget, Ui_VirtualBoxVMPreferencesP
|
||||
pass
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Headless mode enabled:", "{}".format(vbox_vm["headless"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["ACPI shutdown enabled:", "{}".format(vbox_vm["acpi_shutdown"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(vbox_vm["linked_base"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(vbox_vm["linked_clone"])])
|
||||
|
||||
# fill out the Network section
|
||||
section_item = self._createSectionItem("Network")
|
||||
|
||||
@@ -40,6 +40,6 @@ VBOX_VM_SETTINGS = {
|
||||
"adapter_type": "Intel PRO/1000 MT Desktop (82540EM)",
|
||||
"headless": False,
|
||||
"acpi_shutdown": False,
|
||||
"linked_base": False,
|
||||
"linked_clone": False,
|
||||
"server": "local"
|
||||
}
|
||||
|
||||
@@ -19,11 +19,8 @@
|
||||
VirtualBox VM implementation.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from gns3.node import Node
|
||||
from gns3.utils.bring_to_front import bring_window_to_front_from_process_name
|
||||
from .settings import VBOX_VM_SETTINGS
|
||||
|
||||
import logging
|
||||
@@ -45,7 +42,6 @@ class VirtualBoxVM(Node):
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
log.info("VirtualBox VM instance is being created")
|
||||
self._linked_clone = False
|
||||
|
||||
virtualbox_vm_settings = {"vmname": "",
|
||||
@@ -63,30 +59,6 @@ class VirtualBoxVM(Node):
|
||||
|
||||
self.settings().update(virtualbox_vm_settings)
|
||||
|
||||
def create(self, vmname, name=None, node_id=None, port_name_format="Ethernet{0}", port_segment_size=0,
|
||||
first_port_name="", linked_clone=False, additional_settings={}, default_name_format=None):
|
||||
"""
|
||||
Creates this VirtualBox VM.
|
||||
|
||||
:param vmname: VM name in VirtualBox
|
||||
:param name: optional name
|
||||
:param node_id: Node identifier
|
||||
:param linked_clone: either the VM is a linked clone
|
||||
:param additional_settings: additional settings for this VM
|
||||
"""
|
||||
|
||||
if not name:
|
||||
name = vmname
|
||||
|
||||
self._linked_clone = linked_clone
|
||||
params = {"vmname": vmname,
|
||||
"linked_clone": linked_clone,
|
||||
"port_name_format": port_name_format,
|
||||
"port_segment_size": port_segment_size,
|
||||
"first_port_name": first_port_name}
|
||||
params.update(additional_settings)
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
@@ -155,6 +127,19 @@ class VirtualBoxVM(Node):
|
||||
"""
|
||||
return self._settings["console"]
|
||||
|
||||
def bringToFront(self):
|
||||
"""
|
||||
Bring the VM window to front.
|
||||
"""
|
||||
|
||||
if self.status() == Node.started:
|
||||
# try 2 different window title formats
|
||||
bring_window_to_front_from_process_name("VirtualBox.exe", title="{} [".format(self._settings["vmname"]))
|
||||
bring_window_to_front_from_process_name("VirtualBox.exe", title="{} (".format(self._settings["vmname"]))
|
||||
|
||||
# bring any console to front
|
||||
return Node.bringToFront(self)
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
@@ -23,13 +23,13 @@ import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
import codecs
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.local_config import LocalConfig
|
||||
from collections import OrderedDict
|
||||
|
||||
from gns3.modules.module import Module
|
||||
from gns3.modules.module_error import ModuleError
|
||||
from gns3.modules.vmware.vmware_vm import VMwareVM
|
||||
from gns3.modules.vmware.settings import VMWARE_SETTINGS
|
||||
from gns3.modules.vmware.settings import VMWARE_VM_SETTINGS
|
||||
@@ -147,6 +147,46 @@ class VMware(Module):
|
||||
# Workstation is the default
|
||||
return "ws"
|
||||
|
||||
@staticmethod
|
||||
def parseVMwareFile(path):
|
||||
"""
|
||||
Parses a VMware file (VMX, preferences or inventory).
|
||||
|
||||
:param path: path to the VMware file
|
||||
|
||||
:returns: dict
|
||||
"""
|
||||
|
||||
pairs = OrderedDict()
|
||||
encoding = "utf-8"
|
||||
# get the first line to read the .encoding value
|
||||
with open(path, "rb") as f:
|
||||
line = f.readline().decode(encoding, errors="ignore")
|
||||
if line.startswith("#!"):
|
||||
# skip the shebang
|
||||
line = f.readline().decode(encoding, errors="ignore")
|
||||
try:
|
||||
key, value = line.split('=', 1)
|
||||
if key.strip().lower() == ".encoding":
|
||||
file_encoding = value.strip('" ')
|
||||
try:
|
||||
codecs.lookup(file_encoding)
|
||||
encoding = file_encoding
|
||||
except LookupError:
|
||||
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
|
||||
except ValueError:
|
||||
log.warning("Couldn't find file encoding in {}, using {}...".format(path, encoding))
|
||||
|
||||
# read the file with the correct encoding
|
||||
with open(path, encoding=encoding, errors="ignore") as f:
|
||||
for line in f.read().splitlines():
|
||||
try:
|
||||
key, value = line.split('=', 1)
|
||||
pairs[key.strip().lower()] = value.strip('" ')
|
||||
except ValueError:
|
||||
continue
|
||||
return pairs
|
||||
|
||||
def _loadSettings(self):
|
||||
"""
|
||||
Loads the settings from the server settings file.
|
||||
@@ -283,75 +323,9 @@ class VMware(Module):
|
||||
:param project: Project instance
|
||||
"""
|
||||
|
||||
log.info("instantiating node {}".format(node_class))
|
||||
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("creating node {} with id {}".format(node, node.id()))
|
||||
|
||||
vm = None
|
||||
if node_name:
|
||||
for vm_key, info in self._vmware_vms.items():
|
||||
if node_name == info["name"]:
|
||||
vm = vm_key
|
||||
|
||||
if not vm:
|
||||
selected_vms = []
|
||||
for vm, info in self._vmware_vms.items():
|
||||
if info["server"] == node.compute().id():
|
||||
selected_vms.append(vm)
|
||||
|
||||
if not selected_vms:
|
||||
raise ModuleError("No VMware VM on server {}".format(node.server().url()))
|
||||
elif len(selected_vms) > 1:
|
||||
|
||||
from gns3.main_window import MainWindow
|
||||
mainwindow = MainWindow.instance()
|
||||
|
||||
(selection, ok) = QtWidgets.QInputDialog.getItem(mainwindow, "VMware VM", "Please choose a VM", selected_vms, 0, False)
|
||||
if ok:
|
||||
vm = selection
|
||||
else:
|
||||
raise ModuleError("Please select a VMware VM")
|
||||
else:
|
||||
vm = selected_vms[0]
|
||||
|
||||
linked_base = self._vmware_vms[vm]["linked_base"]
|
||||
vm_settings = {}
|
||||
for setting_name, value in self._vmware_vms[vm].items():
|
||||
if setting_name in node.settings():
|
||||
vm_settings[setting_name] = value
|
||||
|
||||
vmx_path = vm_settings.pop("vmx_path")
|
||||
name = vm_settings.pop("name")
|
||||
port_name_format = self._vmware_vms[vm]["port_name_format"]
|
||||
port_segment_size = self._vmware_vms[vm]["port_segment_size"]
|
||||
first_port_name = self._vmware_vms[vm]["first_port_name"]
|
||||
|
||||
default_name_format = VMWARE_VM_SETTINGS["default_name_format"]
|
||||
if self._vmware_vms[vm]["default_name_format"]:
|
||||
default_name_format = self._vmware_vms[vm]["default_name_format"]
|
||||
if linked_base:
|
||||
name = default_name_format.replace('{name}', name)
|
||||
|
||||
node.create(vmx_path,
|
||||
name=name,
|
||||
port_name_format=port_name_format,
|
||||
port_segment_size=port_segment_size,
|
||||
first_port_name=first_port_name,
|
||||
linked_clone=linked_base,
|
||||
additional_settings=vm_settings,
|
||||
default_name_format=default_name_format)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the module.
|
||||
|
||||
@@ -99,7 +99,7 @@ class VMwareVMWizard(VMWizard, Ui_VMwareVMWizard):
|
||||
"name": vmname,
|
||||
"server": self._compute_id,
|
||||
"vmx_path": vminfo["vmx_path"],
|
||||
"linked_base": self.uiBaseVMCheckBox.isChecked()
|
||||
"linked_clone": self.uiBaseVMCheckBox.isChecked()
|
||||
}
|
||||
|
||||
return settings
|
||||
|
||||
@@ -84,8 +84,8 @@ class VMwareVMConfigurationPage(QtWidgets.QWidget, Ui_VMwareVMConfigPageWidget):
|
||||
self.uiNameLabel.hide()
|
||||
self.uiNameLineEdit.hide()
|
||||
|
||||
if "linked_base" in settings:
|
||||
self.uiBaseVMCheckBox.setChecked(settings["linked_base"])
|
||||
if "linked_clone" in settings:
|
||||
self.uiBaseVMCheckBox.setChecked(settings["linked_clone"])
|
||||
else:
|
||||
self.uiBaseVMCheckBox.hide()
|
||||
|
||||
@@ -160,8 +160,8 @@ class VMwareVMConfigurationPage(QtWidgets.QWidget, Ui_VMwareVMConfigPageWidget):
|
||||
else:
|
||||
settings["name"] = name
|
||||
|
||||
if "linked_base" in settings:
|
||||
settings["linked_base"] = self.uiBaseVMCheckBox.isChecked()
|
||||
if "linked_clone" in settings:
|
||||
settings["linked_clone"] = self.uiBaseVMCheckBox.isChecked()
|
||||
|
||||
if not node:
|
||||
# these are template settings
|
||||
|
||||
@@ -69,7 +69,7 @@ class VMwareVMPreferencesPage(QtWidgets.QWidget, Ui_VMwareVMPreferencesPageWidge
|
||||
# fill out the General section
|
||||
section_item = self._createSectionItem("General")
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Template name:", vmware_vm["name"]])
|
||||
if vmware_vm["linked_base"]:
|
||||
if vmware_vm["linked_clone"]:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Default name format:", vmware_vm["default_name_format"]])
|
||||
try:
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Server:", ComputeManager.instance().getCompute(vmware_vm["server"]).name()])
|
||||
@@ -77,7 +77,7 @@ class VMwareVMPreferencesPage(QtWidgets.QWidget, Ui_VMwareVMPreferencesPageWidge
|
||||
pass
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Headless mode enabled:", "{}".format(vmware_vm["headless"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["ACPI shutdown enabled:", "{}".format(vmware_vm["acpi_shutdown"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(vmware_vm["linked_base"])])
|
||||
QtWidgets.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(vmware_vm["linked_clone"])])
|
||||
|
||||
# fill out the Network section
|
||||
section_item = self._createSectionItem("Network")
|
||||
|
||||
@@ -48,6 +48,6 @@ VMWARE_VM_SETTINGS = {
|
||||
"use_any_adapter": False,
|
||||
"headless": False,
|
||||
"acpi_shutdown": False,
|
||||
"linked_base": False,
|
||||
"linked_clone": False,
|
||||
"server": "local"
|
||||
}
|
||||
|
||||
@@ -19,12 +19,9 @@
|
||||
VMware VM implementation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from gns3.qt import QtCore
|
||||
from gns3.node import Node
|
||||
from gns3.utils.bring_to_front import bring_window_to_front_from_process_name
|
||||
from .settings import VMWARE_VM_SETTINGS
|
||||
|
||||
import logging
|
||||
@@ -47,7 +44,6 @@ class VMwareVM(Node):
|
||||
def __init__(self, module, server, project):
|
||||
|
||||
super().__init__(module, server, project)
|
||||
log.info("VMware VM instance is being created")
|
||||
self._linked_clone = False
|
||||
|
||||
vmware_vm_settings = {"vmx_path": "",
|
||||
@@ -64,27 +60,6 @@ class VMwareVM(Node):
|
||||
|
||||
self.settings().update(vmware_vm_settings)
|
||||
|
||||
def create(self, vmx_path, name=None, node_id=None, port_name_format="Ethernet{0}", port_segment_size=0,
|
||||
first_port_name="", linked_clone=False, additional_settings={}, default_name_format=None):
|
||||
"""
|
||||
Creates this VMware VM.
|
||||
|
||||
:param vmx_path: path to the vmx file
|
||||
:param name: optional name
|
||||
:param node_id: Node identifier
|
||||
:param linked_clone: either the VM is a linked clone
|
||||
:param additional_settings: additional settings for this VM
|
||||
"""
|
||||
|
||||
self._linked_clone = linked_clone
|
||||
params = {"vmx_path": vmx_path,
|
||||
"linked_clone": linked_clone,
|
||||
"port_name_format": port_name_format,
|
||||
"port_segment_size": port_segment_size,
|
||||
"first_port_name": first_port_name}
|
||||
params.update(additional_settings)
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Callback for create.
|
||||
@@ -175,6 +150,26 @@ class VMwareVM(Node):
|
||||
|
||||
return self._settings["console"]
|
||||
|
||||
def bringToFront(self):
|
||||
"""
|
||||
Bring the VM window to front.
|
||||
"""
|
||||
|
||||
if self.status() == Node.started:
|
||||
try:
|
||||
vmx_pairs = self.module().parseVMwareFile(self.settings()["vmx_path"])
|
||||
except OSError as e:
|
||||
log.debug("Could not read VMX file: {}".format(e))
|
||||
return
|
||||
if "displayname" in vmx_pairs:
|
||||
window_name = "{} -".format(vmx_pairs["displayname"])
|
||||
# try for both VMware Player and Workstation
|
||||
bring_window_to_front_from_process_name("vmplayer.exe", title=window_name)
|
||||
bring_window_to_front_from_process_name("vmware.exe", title=window_name)
|
||||
|
||||
# bring any console to front
|
||||
return Node.bringToFront(self)
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node properties dialog.
|
||||
|
||||
@@ -23,14 +23,10 @@ import os
|
||||
import copy
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.local_server_config import LocalServerConfig
|
||||
from gns3.utils.get_default_base_config import get_default_base_config
|
||||
from gns3.utils.get_resource import get_resource
|
||||
|
||||
from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .vpcs_node import VPCSNode
|
||||
from .settings import VPCS_SETTINGS
|
||||
from .settings import VPCS_NODES_SETTINGS
|
||||
@@ -63,9 +59,6 @@ class VPCS(Module):
|
||||
Loads the settings from the persistent settings file.
|
||||
"""
|
||||
|
||||
# Copy the default base config in the final location
|
||||
get_default_base_config(get_resource(os.path.join("configs", "vpcs_base_config.txt")))
|
||||
|
||||
self._settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, VPCS_SETTINGS)
|
||||
if not os.path.exists(self._settings["vpcs_path"]):
|
||||
vpcs_path = shutil.which("vpcs")
|
||||
@@ -167,33 +160,6 @@ class VPCS(Module):
|
||||
# create an instance of the node class
|
||||
return node_class(self, server, project)
|
||||
|
||||
def createNode(self, node, node_name):
|
||||
"""
|
||||
Creates a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param node_name: Node name
|
||||
"""
|
||||
|
||||
log.info("creating node {}".format(node))
|
||||
|
||||
if node_name:
|
||||
for node_key, info in self._vpcs_nodes.items():
|
||||
if node_name == info["name"]:
|
||||
vm_settings = {}
|
||||
for setting_name, value in self._vpcs_nodes[node_key].items():
|
||||
|
||||
if setting_name in node.settings() and setting_name != "name" and value != "" and value is not None:
|
||||
vm_settings[setting_name] = value
|
||||
|
||||
node.create(default_name_format=info["default_name_format"], additional_settings=vm_settings)
|
||||
return
|
||||
|
||||
vm_settings = {
|
||||
"base_script_file": self._settings.get("base_script_file", get_default_base_config(get_resource(os.path.join("configs", "vpcs_base_config.txt"))))
|
||||
}
|
||||
node.create(additional_settings=vm_settings)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the module.
|
||||
@@ -201,40 +167,6 @@ class VPCS(Module):
|
||||
|
||||
self._nodes.clear()
|
||||
|
||||
def exportConfigs(self, directory):
|
||||
"""
|
||||
Exports all configs for all nodes to a directory.
|
||||
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if node.initialized():
|
||||
node.exportConfigToDirectory(directory)
|
||||
|
||||
def importConfigs(self, directory):
|
||||
"""
|
||||
Imports configs to all nodes from a directory.
|
||||
|
||||
:param directory: source directory path
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if node.initialized():
|
||||
node.importConfigFromDirectory(directory)
|
||||
|
||||
@staticmethod
|
||||
def getNodeClass(name):
|
||||
"""
|
||||
Returns the object with the corresponding name.
|
||||
|
||||
:param name: object name
|
||||
"""
|
||||
|
||||
if name in globals():
|
||||
return globals()[name]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def getNodeType(name, platform=None):
|
||||
if name == "vpcs":
|
||||
@@ -291,16 +223,6 @@ class VPCS(Module):
|
||||
"builtin": True
|
||||
}
|
||||
)
|
||||
|
||||
for node in self._vpcs_nodes.values():
|
||||
nodes.append(
|
||||
{"class": VPCSNode.__name__,
|
||||
"name": node["name"],
|
||||
"server": node["server"],
|
||||
"symbol": node["symbol"],
|
||||
"categories": [node["category"]]
|
||||
}
|
||||
)
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -19,16 +19,12 @@
|
||||
Wizard for VPCS nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from gns3.qt import QtGui, QtWidgets
|
||||
from gns3.node import Node
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from gns3.utils.get_default_base_config import get_default_base_config
|
||||
from gns3.dialogs.vm_wizard import VMWizard
|
||||
|
||||
from ..ui.vpcs_node_wizard_ui import Ui_VPCSNodeWizard
|
||||
from .. import VPCS
|
||||
|
||||
|
||||
class VPCSNodeWizard(VMWizard, Ui_VPCSNodeWizard):
|
||||
@@ -54,7 +50,7 @@ class VPCSNodeWizard(VMWizard, Ui_VPCSNodeWizard):
|
||||
"""
|
||||
|
||||
settings = {"name": self.uiNameLineEdit.text(),
|
||||
"base_script_file": get_default_base_config(get_resource(os.path.join("configs", "vpcs_base_config.txt"))),
|
||||
"base_script_file": "vpcs_base_config.txt",
|
||||
"symbol": ":/symbols/vpcs_guest.svg",
|
||||
"category": Node.end_devices,
|
||||
"server": self._compute_id}
|
||||
|
||||
@@ -23,6 +23,7 @@ import os
|
||||
from gns3.qt import QtWidgets
|
||||
from gns3.local_server import LocalServer
|
||||
from gns3.node import Node
|
||||
from gns3.controller import Controller
|
||||
|
||||
from ..ui.vpcs_node_configuration_page_ui import Ui_VPCSNodeConfigPageWidget
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
@@ -42,6 +43,8 @@ class VPCSNodeConfigurationPage(QtWidgets.QWidget, Ui_VPCSNodeConfigPageWidget):
|
||||
self.uiSymbolToolButton.clicked.connect(self._symbolBrowserSlot)
|
||||
self.uiScriptFileToolButton.clicked.connect(self._scriptFileBrowserSlot)
|
||||
self._default_configs_dir = LocalServer.instance().localServerSettings()["configs_path"]
|
||||
if Controller.instance().isRemote():
|
||||
self.uiScriptFileToolButton.hide()
|
||||
|
||||
# add the categories
|
||||
for name, category in Node.defaultCategories().items():
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
VPCS node implementation.
|
||||
"""
|
||||
|
||||
import os
|
||||
from gns3.node import Node
|
||||
from gns3.utils.normalize_filename import normalize_filename
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -41,43 +39,12 @@ class VPCSNode(Node):
|
||||
def __init__(self, module, server, project):
|
||||
super().__init__(module, server, project)
|
||||
|
||||
log.info("VPCS instance is being created")
|
||||
|
||||
vpcs_settings = {"console_host": None,
|
||||
"startup_script": None,
|
||||
"startup_script_path": None,
|
||||
"base_script_file": None,
|
||||
"console": None}
|
||||
|
||||
self.settings().update(vpcs_settings)
|
||||
|
||||
def create(self, name=None, node_id=None, additional_settings={}, default_name_format="PC{0}"):
|
||||
"""
|
||||
Creates this VPCS node.
|
||||
|
||||
:param name: optional name
|
||||
:param node_id: Node identifier
|
||||
:param additional_settings: additional settings for this node
|
||||
"""
|
||||
|
||||
params = {}
|
||||
if "base_script_file" in additional_settings:
|
||||
if os.path.isfile(additional_settings["base_script_file"]):
|
||||
base_config_content = self._readBaseConfig(additional_settings["base_script_file"])
|
||||
if base_config_content is not None:
|
||||
additional_settings["startup_script"] = base_config_content
|
||||
del additional_settings["base_script_file"]
|
||||
|
||||
if "startup_script_path" in additional_settings:
|
||||
del additional_settings["startup_script_path"]
|
||||
|
||||
# If we have an vm id that mean the VM already exits and we should not send startup_script
|
||||
if "startup_script" in additional_settings and node_id is not None:
|
||||
del additional_settings["startup_script"]
|
||||
|
||||
params.update(additional_settings)
|
||||
self._create(name, node_id, params, default_name_format)
|
||||
|
||||
def update(self, new_settings):
|
||||
"""
|
||||
Updates the settings for this VPCS node.
|
||||
@@ -85,19 +52,6 @@ class VPCSNode(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
if "script_file" in new_settings:
|
||||
if os.path.isfile(new_settings["script_file"]):
|
||||
base_config_content = self._readBaseConfig(new_settings["script_file"])
|
||||
if base_config_content is not None:
|
||||
new_settings["startup_script"] = base_config_content
|
||||
del new_settings["script_file"]
|
||||
|
||||
if "base_script_file" in new_settings:
|
||||
del new_settings["base_script_file"]
|
||||
|
||||
if "startup_script_path" in new_settings:
|
||||
del new_settings["startup_script_path"]
|
||||
|
||||
params = {}
|
||||
for name, value in new_settings.items():
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
@@ -139,74 +93,12 @@ class VPCSNode(Node):
|
||||
|
||||
return info + port_info
|
||||
|
||||
def exportConfigToDirectory(self, directory):
|
||||
"""
|
||||
Exports the script-file to a directory.
|
||||
|
||||
:param directory: destination directory path
|
||||
"""
|
||||
|
||||
self.controllerHttpGet("/nodes/{node_id}".format(node_id=self._node_id),
|
||||
self._exportConfigToDirectoryCallback,
|
||||
context={"directory": directory})
|
||||
|
||||
def _exportConfigToDirectoryCallback(self, result, error=False, context={}, **kwargs):
|
||||
"""
|
||||
Callback for exportConfigToDirectory.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("error while exporting {} configs: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["message"])
|
||||
elif "startup_script" in result["properties"]:
|
||||
export_directory = context["directory"]
|
||||
config_path = os.path.join(export_directory, normalize_filename(self.name())) + "_startup.vpc"
|
||||
try:
|
||||
with open(config_path, "wb") as f:
|
||||
log.info("saving {} script file to {}".format(self.name(), config_path))
|
||||
if result["properties"]["startup_script"]:
|
||||
f.write(result["properties"]["startup_script"].encode("utf-8"))
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not export the script file to {}: {}".format(config_path, e))
|
||||
|
||||
def configFiles(self):
|
||||
"""
|
||||
Name of the configuration files
|
||||
"""
|
||||
return ["startup.vpc"]
|
||||
|
||||
def importConfig(self, path):
|
||||
"""
|
||||
Imports a script-file.
|
||||
|
||||
:param path: path to the script file
|
||||
"""
|
||||
|
||||
new_settings = {"script_file": path}
|
||||
self.update(new_settings)
|
||||
|
||||
def importConfigFromDirectory(self, directory):
|
||||
"""
|
||||
Imports an initial-config from a directory.
|
||||
|
||||
:param directory: source directory path
|
||||
"""
|
||||
|
||||
try:
|
||||
contents = os.listdir(directory)
|
||||
except OSError as e:
|
||||
return
|
||||
script_file = normalize_filename(self.name()) + "_startup.vpc"
|
||||
new_settings = {}
|
||||
if script_file in contents:
|
||||
new_settings["script_file"] = os.path.join(directory, script_file)
|
||||
else:
|
||||
return
|
||||
self.update(new_settings)
|
||||
|
||||
def console(self):
|
||||
"""
|
||||
Returns the console port for this VPCS node.
|
||||
|
||||
175
gns3/node.py
175
gns3/node.py
@@ -16,13 +16,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import pathlib
|
||||
from gns3.local_server import LocalServer
|
||||
|
||||
from gns3.controller import Controller
|
||||
from gns3.ports.ethernet_port import EthernetPort
|
||||
from gns3.ports.serial_port import SerialPort
|
||||
from gns3.utils.bring_to_front import bring_window_to_front_from_title
|
||||
from gns3.qt import QtGui, QtCore
|
||||
|
||||
from .base_node import BaseNode
|
||||
@@ -72,10 +72,6 @@ class Node(BaseNode):
|
||||
except OSError as e:
|
||||
log.erro("Can't write %s: %s", context["path"], str(e))
|
||||
|
||||
|
||||
def creator(self):
|
||||
return self._creator
|
||||
|
||||
def settings(self):
|
||||
return self._settings
|
||||
|
||||
@@ -154,6 +150,8 @@ class Node(BaseNode):
|
||||
console_type = self.consoleType()
|
||||
if console_type == "vnc":
|
||||
return general_settings["vnc_console_command"]
|
||||
if console_type == "spice":
|
||||
return general_settings["spice_console_command"]
|
||||
return general_settings["telnet_console_command"]
|
||||
|
||||
def consoleType(self):
|
||||
@@ -224,66 +222,6 @@ class Node(BaseNode):
|
||||
|
||||
return body
|
||||
|
||||
def _create(self, name=None, node_id=None, params=None, default_name_format="Node{0}", timeout=120):
|
||||
"""
|
||||
Create the node on the controller
|
||||
"""
|
||||
|
||||
self._creator = True
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
if "symbol" in self._settings:
|
||||
params["symbol"] = self._settings["symbol"]
|
||||
params["x"] = self._settings["x"]
|
||||
params["y"] = self._settings["y"]
|
||||
if "label" in self._settings:
|
||||
params["label"] = self._settings["label"]
|
||||
|
||||
if not name:
|
||||
# use the default name format if no name is provided
|
||||
name = default_name_format
|
||||
|
||||
params["name"] = name
|
||||
if node_id is not None:
|
||||
self._node_id = node_id
|
||||
|
||||
body = self._prepareBody(params)
|
||||
self.controllerHttpPost("/nodes", self.createNodeCallback, body=body, timeout=timeout)
|
||||
|
||||
def createNodeCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
if error:
|
||||
self.server_error_signal.emit(self.id(), "Error while setting up node: {}".format(result["message"]))
|
||||
self.deleted_signal.emit()
|
||||
self._module.removeNode(self)
|
||||
return False
|
||||
|
||||
result = self._parseResponse(result)
|
||||
self._created = True
|
||||
self._createCallback(result)
|
||||
|
||||
if self._loading:
|
||||
self.loaded_signal.emit()
|
||||
else:
|
||||
self.setInitialized(True)
|
||||
log.info("Node instance {} has been created".format(self.name()))
|
||||
self.created_signal.emit(self.id())
|
||||
self._module.addNode(self)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Create callback compatible with the compute api.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def _update(self, params, timeout=60):
|
||||
"""
|
||||
Update the node on the controller
|
||||
@@ -303,7 +241,6 @@ class Node(BaseNode):
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("error while updating {}: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["message"])
|
||||
return False
|
||||
|
||||
@@ -313,6 +250,27 @@ class Node(BaseNode):
|
||||
self.updated_signal.emit()
|
||||
return True
|
||||
|
||||
def duplicate(self, x, y, z):
|
||||
"""
|
||||
Duplicate the node
|
||||
"""
|
||||
body = {
|
||||
"x": int(x),
|
||||
"y": int(y),
|
||||
"z": int(z)
|
||||
}
|
||||
self.controllerHttpPost("/nodes/{node_id}/duplicate".format(
|
||||
node_id=self._node_id),
|
||||
self._duplicateCallback,
|
||||
body=body,
|
||||
timeout=None)
|
||||
|
||||
def _duplicateCallback(self, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
log.error("Error while duplicating: {}".format(result["message"]))
|
||||
return
|
||||
|
||||
def _parseResponse(self, result):
|
||||
"""
|
||||
Parse node object from API
|
||||
@@ -382,6 +340,37 @@ class Node(BaseNode):
|
||||
new_port.setStatus(self.status())
|
||||
self._ports.append(new_port)
|
||||
|
||||
def createNodeCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
Callback for create.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
:returns: Boolean success or not
|
||||
"""
|
||||
if error:
|
||||
self.server_error_signal.emit(self.id(), "Error while setting up node: {}".format(result["message"]))
|
||||
self.deleted_signal.emit()
|
||||
self._module.removeNode(self)
|
||||
return False
|
||||
|
||||
result = self._parseResponse(result)
|
||||
self._created = True
|
||||
self._createCallback(result)
|
||||
|
||||
if self._loading:
|
||||
self.loaded_signal.emit()
|
||||
else:
|
||||
self.setInitialized(True)
|
||||
self.created_signal.emit(self.id())
|
||||
self._module.addNode(self)
|
||||
|
||||
def _createCallback(self, result):
|
||||
"""
|
||||
Create callback compatible with the compute api.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _updateCallback(self, result):
|
||||
"""
|
||||
Update callback compatible with the compute api.
|
||||
@@ -396,7 +385,6 @@ class Node(BaseNode):
|
||||
:param skip_controller: True to not delete on the controller (often it's when it's already deleted on the server)
|
||||
"""
|
||||
|
||||
log.info("{} is being deleted".format(self.name()))
|
||||
if not skip_controller:
|
||||
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback)
|
||||
else:
|
||||
@@ -415,7 +403,6 @@ class Node(BaseNode):
|
||||
log.error("error while deleting {}: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["message"])
|
||||
return
|
||||
log.info("{} has been deleted".format(self.name()))
|
||||
self.deleted_signal.emit()
|
||||
self._module.removeNode(self)
|
||||
|
||||
@@ -482,7 +469,7 @@ class Node(BaseNode):
|
||||
|
||||
def suspend(self):
|
||||
"""
|
||||
Suspends this router.
|
||||
Suspends this node.
|
||||
"""
|
||||
|
||||
if self.status() == Node.suspended:
|
||||
@@ -525,39 +512,6 @@ class Node(BaseNode):
|
||||
if error:
|
||||
log.error("error while reloading {}: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["message"])
|
||||
else:
|
||||
log.info("{} has reloaded".format(self.name()))
|
||||
|
||||
def _readBaseConfig(self, config_path):
|
||||
"""
|
||||
Returns a base config content.
|
||||
|
||||
:param config_path: path to the configuration file.
|
||||
|
||||
:returns: config content
|
||||
"""
|
||||
|
||||
if config_path is None or len(config_path.strip()) == 0:
|
||||
return None
|
||||
|
||||
if not os.path.isabs(config_path):
|
||||
config_path = os.path.join(LocalServer.instance().localServerSettings()["configs_path"], config_path)
|
||||
|
||||
if not os.path.isfile(config_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(config_path, "rb") as f:
|
||||
log.info("Opening configuration file: {}".format(config_path))
|
||||
config = f.read().decode("utf-8")
|
||||
config = config.replace('\r', "")
|
||||
return config
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "Could not read configuration file {}: {}".format(config_path, e))
|
||||
return None
|
||||
except UnicodeDecodeError as e:
|
||||
self.error_signal.emit(self.id(), "Invalid configuration file {}: {}".format(config_path, e))
|
||||
return None
|
||||
|
||||
def openConsole(self, command=None, aux=False):
|
||||
if command is None:
|
||||
@@ -585,9 +539,24 @@ class Node(BaseNode):
|
||||
elif console_type == "vnc":
|
||||
from .vnc_console import vncConsole
|
||||
vncConsole(self.consoleHost(), console_port, command)
|
||||
elif console_type == "spice":
|
||||
from .spice_console import spiceConsole
|
||||
spiceConsole(self.consoleHost(), console_port, command)
|
||||
elif console_type == "http" or console_type == "https":
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl("{console_type}://{host}:{port}{path}".format(console_type=console_type, host=self.consoleHost(), port=console_port, path=self.consoleHttpPath())))
|
||||
|
||||
def bringToFront(self):
|
||||
"""
|
||||
Bring the console window to front.
|
||||
"""
|
||||
|
||||
if self.status() == Node.started:
|
||||
if bring_window_to_front_from_title(self.name()):
|
||||
return True
|
||||
else:
|
||||
log.debug("Could not find window title '{}' to bring it to front".format(self.name()))
|
||||
return False
|
||||
|
||||
def setName(self, name):
|
||||
"""
|
||||
Set a name for a node.
|
||||
|
||||
63
gns3/nodes_dock_widget.py
Normal file
63
gns3/nodes_dock_widget.py
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2016 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 .qt import QtWidgets
|
||||
from .settings import NODES_VIEW_SETTINGS
|
||||
from .local_config import LocalConfig
|
||||
|
||||
|
||||
class NodesDockWidget(QtWidgets.QDockWidget):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._settings = LocalConfig.instance().loadSectionSettings("NodesView", NODES_VIEW_SETTINGS)
|
||||
|
||||
def _filterTextChangedSlot(self, text):
|
||||
self.window().uiNodesView.setCurrentSearch(text)
|
||||
self.window().uiNodesView.refresh()
|
||||
|
||||
def _filterIndexChangedSlot(self, index):
|
||||
self._settings["nodes_view_filter"] = index
|
||||
LocalConfig.instance().saveSectionSettings("NodesView", self._settings)
|
||||
|
||||
if index == 0:
|
||||
self.window().uiNodesView.setShowInstalledAppliances(True)
|
||||
self.window().uiNodesView.setShowBuiltinAvailableAppliances(True)
|
||||
self.window().uiNodesView.setShowMyAvailableAppliances(True)
|
||||
elif index == 1:
|
||||
self.window().uiNodesView.setShowInstalledAppliances(True)
|
||||
self.window().uiNodesView.setShowBuiltinAvailableAppliances(False)
|
||||
self.window().uiNodesView.setShowMyAvailableAppliances(False)
|
||||
elif index == 2:
|
||||
self.window().uiNodesView.setShowInstalledAppliances(False)
|
||||
self.window().uiNodesView.setShowBuiltinAvailableAppliances(True)
|
||||
self.window().uiNodesView.setShowMyAvailableAppliances(True)
|
||||
else:
|
||||
self.window().uiNodesView.setShowInstalledAppliances(False)
|
||||
self.window().uiNodesView.setShowBuiltinAvailableAppliances(False)
|
||||
self.window().uiNodesView.setShowMyAvailableAppliances(True)
|
||||
self.window().uiNodesView.refresh()
|
||||
|
||||
def populateNodesView(self, category):
|
||||
if self.window().uiNodesFilterComboBox.currentIndex() != self._settings["nodes_view_filter"]:
|
||||
self.window().uiNodesFilterComboBox.setCurrentIndex(self._settings["nodes_view_filter"])
|
||||
self._filterIndexChangedSlot(self._settings["nodes_view_filter"])
|
||||
self.window().uiNodesFilterComboBox.activated.connect(self._filterIndexChangedSlot)
|
||||
self.window().uiNodesFilterLineEdit.textChanged.connect(self._filterTextChangedSlot)
|
||||
self.window().uiNodesView.clear()
|
||||
text = self.window().uiNodesFilterLineEdit.text().strip().lower()
|
||||
self.window().uiNodesView.populateNodesView(category, text)
|
||||
@@ -16,20 +16,31 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Nodes view that list all the available nodes to be dragged and dropped on the QGraphics scene.
|
||||
Nodes view that list all the available nodes to be dragged and dropped
|
||||
on the QGraphics scene.
|
||||
"""
|
||||
|
||||
import pickle
|
||||
import tempfile
|
||||
import json
|
||||
import sip
|
||||
|
||||
from .qt import QtCore, QtGui, QtWidgets, qpartial
|
||||
from .modules import MODULES
|
||||
from .node import Node
|
||||
from .controller import Controller
|
||||
from .appliance_manager import ApplianceManager
|
||||
from .dialogs.configuration_dialog import ConfigurationDialog
|
||||
from .local_config import LocalConfig
|
||||
|
||||
|
||||
CATEGORY_TO_ID = {
|
||||
"firewall": 3,
|
||||
"guest": 2,
|
||||
"switch": 1,
|
||||
"multilayer_switch": 1,
|
||||
"router": 0
|
||||
}
|
||||
|
||||
|
||||
class NodesView(QtWidgets.QTreeWidget):
|
||||
|
||||
"""
|
||||
@@ -42,40 +53,84 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
|
||||
super().__init__(parent)
|
||||
self._current_category = None
|
||||
self._current_search = ""
|
||||
self._show_installed_appliances = True
|
||||
self._show_builtin_available_appliances = True
|
||||
self._show_my_available_appliances = True
|
||||
|
||||
# enables the possibility to drag items.
|
||||
self.setDragEnabled(True)
|
||||
|
||||
Controller.instance().connected_signal.connect(self.refresh)
|
||||
ApplianceManager.instance().appliances_changed_signal.connect(self.refresh)
|
||||
|
||||
def setCurrentSearch(self, search):
|
||||
self._current_search = search
|
||||
|
||||
def setShowInstalledAppliances(self, value):
|
||||
self._show_installed_appliances = value
|
||||
|
||||
def setShowBuiltinAvailableAppliances(self, value):
|
||||
self._show_builtin_available_appliances = value
|
||||
|
||||
def setShowMyAvailableAppliances(self, value):
|
||||
self._show_my_available_appliances = value
|
||||
|
||||
def refresh(self):
|
||||
self.clear()
|
||||
self.populateNodesView(self._current_category)
|
||||
self.populateNodesView(self._current_category, self._current_search)
|
||||
|
||||
def populateNodesView(self, category):
|
||||
def populateNodesView(self, category, search):
|
||||
"""
|
||||
Populates the nodes view with the device list of the specified
|
||||
category (None = all devices).
|
||||
|
||||
:param category: category of device to list
|
||||
:param search: filter
|
||||
"""
|
||||
|
||||
if not Controller.instance().connected():
|
||||
return
|
||||
self.setIconSize(QtCore.QSize(32, 32))
|
||||
self._current_category = category
|
||||
for module in MODULES:
|
||||
for node in module.instance().nodes():
|
||||
if category is not None and category not in node["categories"]:
|
||||
continue
|
||||
item = QtWidgets.QTreeWidgetItem(self)
|
||||
item.setText(0, node["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, node)
|
||||
item.setSizeHint(0, QtCore.QSize(32, 32))
|
||||
Controller.instance().getSymbolIcon(node["symbol"], qpartial(self._setItemIcon, item))
|
||||
self._current_search = search
|
||||
|
||||
if not self.topLevelItemCount() and category == Node.routers:
|
||||
QtWidgets.QMessageBox.warning(self, 'Routers', 'No routers have been configured.<br>You must provide your own router images in order to use GNS3.<br><br><a href="https://gns3.com/support/docs">Show documentation</a>')
|
||||
display_appliances = set()
|
||||
|
||||
if self._show_installed_appliances:
|
||||
for appliance in ApplianceManager.instance().appliances():
|
||||
if category is not None and category != CATEGORY_TO_ID[appliance["category"]]:
|
||||
continue
|
||||
if search != "" and search.lower() not in appliance["name"].lower():
|
||||
continue
|
||||
|
||||
display_appliances.add(appliance["name"])
|
||||
item = QtWidgets.QTreeWidgetItem(self)
|
||||
item.setText(0, appliance["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, appliance["appliance_id"])
|
||||
item.setData(1, QtCore.Qt.UserRole, "appliance")
|
||||
item.setSizeHint(0, QtCore.QSize(32, 32))
|
||||
Controller.instance().getSymbolIcon(appliance.get("symbol"), qpartial(self._setItemIcon, item), fallback=":/symbols/" + appliance["category"] + ".svg")
|
||||
|
||||
for appliance in ApplianceManager.instance().appliance_templates():
|
||||
if not appliance["builtin"] and not self._show_my_available_appliances:
|
||||
continue
|
||||
if appliance["builtin"] and not self._show_builtin_available_appliances:
|
||||
continue
|
||||
|
||||
if category is not None and category != CATEGORY_TO_ID[appliance["category"]]:
|
||||
continue
|
||||
if search != "" and search.lower() not in appliance["name"].lower():
|
||||
continue
|
||||
if appliance["name"] in display_appliances:
|
||||
continue
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem(self)
|
||||
item.setForeground(0, QtGui.QBrush(QtGui.QColor("gray")))
|
||||
item.setText(0, appliance["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, appliance)
|
||||
item.setData(1, QtCore.Qt.UserRole, "appliance_template")
|
||||
item.setSizeHint(0, QtCore.QSize(32, 32))
|
||||
Controller.instance().getSymbolIcon(appliance.get("symbol"), qpartial(self._setItemIcon, item), fallback=":/symbols/" + appliance["category"] + ".svg")
|
||||
|
||||
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
@@ -108,16 +163,25 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
# Check that an item has been selected and left button clicked
|
||||
if self.currentItem() is not None and event.buttons() == QtCore.Qt.LeftButton:
|
||||
item = self.currentItem()
|
||||
icon = item.icon(0)
|
||||
|
||||
# retrieve the node class from the item data
|
||||
node = item.data(0, QtCore.Qt.UserRole)
|
||||
if item.data(1, QtCore.Qt.UserRole) == "appliance_template":
|
||||
f = tempfile.NamedTemporaryFile(mode="w+", suffix=".builtin.gns3a", delete=False)
|
||||
json.dump(item.data(0, QtCore.Qt.UserRole), f)
|
||||
f.close()
|
||||
self.window().loadPath(f.name)
|
||||
return
|
||||
|
||||
icon = item.icon(0)
|
||||
mimedata = QtCore.QMimeData()
|
||||
|
||||
# pickle the node class, set the Mime type and data
|
||||
# and start dragging the item.
|
||||
data = pickle.dumps(node)
|
||||
mimedata.setData("application/x-gns3-node", data)
|
||||
if item.data(1, QtCore.Qt.UserRole) == "appliance":
|
||||
appliance_id = item.data(0, QtCore.Qt.UserRole)
|
||||
mimedata.setData("application/x-gns3-appliance", appliance_id.encode())
|
||||
elif item.data(1, QtCore.Qt.UserRole) == "node":
|
||||
appliance_id = item.data(0, QtCore.Qt.UserRole)
|
||||
mimedata.setData("application/x-gns3-appliance", appliance_id.encode())
|
||||
|
||||
drag = QtGui.QDrag(self)
|
||||
drag.setMimeData(mimedata)
|
||||
drag.setPixmap(icon.pixmap(self.iconSize()))
|
||||
@@ -127,15 +191,17 @@ class NodesView(QtWidgets.QTreeWidget):
|
||||
|
||||
def _showContextualMenu(self):
|
||||
item = self.currentItem()
|
||||
node = item.data(0, QtCore.Qt.UserRole)
|
||||
node = ApplianceManager.instance().getAppliance(item.data(0, QtCore.Qt.UserRole))
|
||||
if not node:
|
||||
return
|
||||
for module in MODULES:
|
||||
node_class = module.getNodeClass(node["class"])
|
||||
node_class = module.getNodeType(node["node_type"])
|
||||
if node_class:
|
||||
break
|
||||
|
||||
# We can not edit stuff like EthernetSwitch
|
||||
# or without config template like VPCS
|
||||
if "builtin" not in node and hasattr(module, "vmConfigurationPage"):
|
||||
if not node["builtin"] and hasattr(module, "vmConfigurationPage"):
|
||||
for vm_key, vm in module.instance().VMs().items():
|
||||
if vm["name"] == node["name"]:
|
||||
break
|
||||
|
||||
@@ -91,9 +91,9 @@ class PacketCapture:
|
||||
|
||||
if link:
|
||||
if link.capturing():
|
||||
if self._autostart[link]:
|
||||
if self._autostart[link] and link not in self._tail_process:
|
||||
self.startPacketCaptureReader(link)
|
||||
log.info("Has successfully started capturing packets on {} to {}".format(link.id(), link.capture_file_path()))
|
||||
log.debug("Has successfully started capturing packets on {} to {}".format(link.id(), link.capture_file_path()))
|
||||
else:
|
||||
self.stopPacketCaptureReader(link)
|
||||
|
||||
@@ -106,7 +106,7 @@ class PacketCapture:
|
||||
"""
|
||||
|
||||
link.stopCapture()
|
||||
log.info("Has successfully stopped capturing packets on {}".format(link.id()))
|
||||
log.debug("Has successfully stopped capturing packets on {}".format(link.id()))
|
||||
|
||||
def startPacketCaptureReader(self, link):
|
||||
"""
|
||||
|
||||
@@ -52,11 +52,13 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
self.uiSymbolsPathToolButton.clicked.connect(self._symbolsPathSlot)
|
||||
self.uiImagesPathToolButton.clicked.connect(self._imagesPathSlot)
|
||||
self.uiConfigsPathToolButton.clicked.connect(self._configsPathSlot)
|
||||
self.uiAppliancesPathToolButton.clicked.connect(self._appliancesPathSlot)
|
||||
self.uiImportConfigurationFilePushButton.clicked.connect(self._importConfigurationFileSlot)
|
||||
self.uiExportConfigurationFilePushButton.clicked.connect(self._exportConfigurationFileSlot)
|
||||
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
|
||||
self.uiTelnetConsolePreconfiguredCommandPushButton.clicked.connect(self._telnetConsolePreconfiguredCommandSlot)
|
||||
self.uiVNCConsolePreconfiguredCommandPushButton.clicked.connect(self._vncConsolePreconfiguredCommandSlot)
|
||||
self.uiSPICEConsolePreconfiguredCommandPushButton.clicked.connect(self._spiceConsolePreconfiguredCommandSlot)
|
||||
self.uiDefaultLabelFontPushButton.clicked.connect(self._setDefaultLabelFontSlot)
|
||||
self.uiDefaultLabelColorPushButton.clicked.connect(self._setDefaultLabelColorSlot)
|
||||
self.uiBrowseConfigurationPushButton.clicked.connect(self._browseConfigurationDirectorySlot)
|
||||
@@ -132,6 +134,18 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
self.uiConfigsPathLineEdit.setText(path)
|
||||
self.uiConfigsPathLineEdit.setCursorPosition(0)
|
||||
|
||||
def _appliancesPathSlot(self):
|
||||
"""
|
||||
Slot to select the appliances directory path.
|
||||
"""
|
||||
|
||||
local_server = LocalServer.instance().localServerSettings()
|
||||
directory = local_server["appliances_path"]
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, "My custom appliances directory", directory, QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
if path:
|
||||
self.uiAppliancesPathLineEdit.setText(path)
|
||||
self.uiAppliancesPathLineEdit.setCursorPosition(0)
|
||||
|
||||
def _restoreDefaultsSlot(self):
|
||||
"""
|
||||
Slot to restore default settings
|
||||
@@ -160,6 +174,16 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
if ok:
|
||||
self.uiVNCConsoleCommandLineEdit.setText(cmd)
|
||||
|
||||
def _spiceConsolePreconfiguredCommandSlot(self):
|
||||
"""
|
||||
Slot to set a chosen pre-configured SPICE console command.
|
||||
"""
|
||||
|
||||
cmd = self.uiSPICEConsoleCommandLineEdit.text()
|
||||
(ok, cmd) = ConsoleCommandDialog.getCommand(self, console_type="spice", current=cmd)
|
||||
if ok:
|
||||
self.uiSPICEConsoleCommandLineEdit.setText(cmd)
|
||||
|
||||
def _importConfigurationFileSlot(self):
|
||||
"""
|
||||
Slot to import a configuration file.
|
||||
@@ -254,7 +278,9 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
self.uiSymbolsPathLineEdit.setText(local_server["symbols_path"])
|
||||
self.uiImagesPathLineEdit.setText(local_server["images_path"])
|
||||
self.uiConfigsPathLineEdit.setText(local_server["configs_path"])
|
||||
self.uiAppliancesPathLineEdit.setText(local_server["appliances_path"])
|
||||
self.uiStatsCheckBox.setChecked(settings["send_stats"])
|
||||
self.uiOverlayNotificationsCheckBox.setChecked(settings["overlay_notifications"])
|
||||
self.uiCrashReportCheckBox.setChecked(local_server["report_errors"])
|
||||
self.uiCheckForUpdateCheckBox.setChecked(settings["check_for_update"])
|
||||
self.uiExperimentalFeaturesCheckBox.setChecked(settings["experimental_features"])
|
||||
@@ -269,6 +295,9 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
self.uiVNCConsoleCommandLineEdit.setText(settings["vnc_console_command"])
|
||||
self.uiVNCConsoleCommandLineEdit.setCursorPosition(0)
|
||||
|
||||
self.uiSPICEConsoleCommandLineEdit.setText(settings["spice_console_command"])
|
||||
self.uiSPICEConsoleCommandLineEdit.setCursorPosition(0)
|
||||
|
||||
self.uiMultiProfilesCheckBox.setChecked(settings["multi_profiles"])
|
||||
|
||||
self.uiImageDirectoriesListWidget.clear()
|
||||
@@ -322,6 +351,7 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
"projects_path": self.uiProjectsPathLineEdit.text(),
|
||||
"symbols_path": self.uiSymbolsPathLineEdit.text(),
|
||||
"configs_path": self.uiConfigsPathLineEdit.text(),
|
||||
"appliances_path": self.uiAppliancesPathLineEdit.text(),
|
||||
"report_errors": self.uiCrashReportCheckBox.isChecked(),
|
||||
"additional_images_paths": ":".join(additional_images_paths)}
|
||||
LocalServer.instance().updateLocalServerSettings(new_local_server_settings)
|
||||
@@ -331,8 +361,10 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
"experimental_features": self.uiExperimentalFeaturesCheckBox.isChecked(),
|
||||
"hdpi": self.uiHdpiCheckBox.isChecked(),
|
||||
"check_for_update": self.uiCheckForUpdateCheckBox.isChecked(),
|
||||
"overlay_notifications": self.uiOverlayNotificationsCheckBox.isChecked(),
|
||||
"telnet_console_command": self.uiTelnetConsoleCommandLineEdit.text(),
|
||||
"vnc_console_command": self.uiVNCConsoleCommandLineEdit.text(),
|
||||
"spice_console_command": self.uiSPICEConsoleCommandLineEdit.text(),
|
||||
"delay_console_all": self.uiDelayConsoleAllSpinBox.value(),
|
||||
"send_stats": self.uiStatsCheckBox.isChecked(),
|
||||
"multi_profiles": self.uiMultiProfilesCheckBox.isChecked()
|
||||
|
||||
@@ -30,11 +30,9 @@ log = logging.getLogger(__name__)
|
||||
from gns3.qt import QtNetwork, QtWidgets
|
||||
from ..ui.server_preferences_page_ui import Ui_ServerPreferencesPageWidget
|
||||
from ..topology import Topology
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
from ..settings import LOCAL_SERVER_SETTINGS
|
||||
from ..dialogs.edit_compute_dialog import EditComputeDialog
|
||||
from ..local_server import LocalServer
|
||||
from ..local_config import LocalConfig
|
||||
from ..compute_manager import ComputeManager
|
||||
|
||||
|
||||
@@ -63,10 +61,11 @@ class ServerPreferencesPage(QtWidgets.QWidget, Ui_ServerPreferencesPageWidget):
|
||||
|
||||
# load all available addresses
|
||||
for address in QtNetwork.QNetworkInterface.allAddresses():
|
||||
if address.protocol() == QtNetwork.QAbstractSocket.IPv4Protocol:
|
||||
if address.protocol() in [QtNetwork.QAbstractSocket.IPv4Protocol, QtNetwork.QAbstractSocket.IPv6Protocol]:
|
||||
address_string = address.toString()
|
||||
self.uiLocalServerHostComboBox.addItem(address_string, address_string)
|
||||
self.uiLocalServerHostComboBox.addItem("0.0.0.0", "0.0.0.0")
|
||||
self.uiLocalServerHostComboBox.addItem("::", "::") # all IPv6 addresses
|
||||
self.uiLocalServerHostComboBox.addItem("0.0.0.0", "0.0.0.0") # all IPv4 addresses
|
||||
|
||||
# default is 127.0.0.1
|
||||
index = self.uiLocalServerHostComboBox.findText("127.0.0.1")
|
||||
@@ -190,7 +189,6 @@ class ServerPreferencesPage(QtWidgets.QWidget, Ui_ServerPreferencesPageWidget):
|
||||
self.uiRemoteMainServerPortSpinBox.setValue(servers_settings["port"])
|
||||
self.uiRemoteMainServerUserLineEdit.setText(servers_settings["user"])
|
||||
self.uiRemoteMainServerPasswordLineEdit.setText(servers_settings["password"])
|
||||
self.uiRemoteMainServerProtocolComboBox.setCurrentText(servers_settings["protocol"])
|
||||
self.uiRemoteMainServerAuthCheckBox.setChecked(servers_settings["auth"])
|
||||
|
||||
self.uiLocalServerAutoStartCheckBox.setChecked(servers_settings["auto_start"])
|
||||
@@ -283,7 +281,7 @@ class ServerPreferencesPage(QtWidgets.QWidget, Ui_ServerPreferencesPageWidget):
|
||||
else:
|
||||
new_local_server_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
|
||||
new_local_server_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
|
||||
new_local_server_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText()
|
||||
new_local_server_settings["protocol"] = "http"
|
||||
new_local_server_settings["user"] = self.uiRemoteMainServerUserLineEdit.text()
|
||||
new_local_server_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
|
||||
new_local_server_settings["auth"] = self.uiRemoteMainServerAuthCheckBox.isChecked()
|
||||
|
||||
136
gns3/project.py
136
gns3/project.py
@@ -16,14 +16,16 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from .qt import QtCore, qpartial, QtWidgets, QtNetwork
|
||||
import json
|
||||
from .qt import QtCore, qpartial, QtWidgets, QtNetwork, qslot
|
||||
|
||||
from gns3.controller import Controller
|
||||
from gns3.compute_manager import ComputeManager
|
||||
from gns3.topology import Topology
|
||||
from gns3.local_config import LocalConfig
|
||||
from gns3.settings import GRAPHICS_VIEW_SETTINGS
|
||||
|
||||
from gns3.appliance_manager import ApplianceManager
|
||||
from gns3.utils import parse_version
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -44,6 +46,9 @@ class Project(QtCore.QObject):
|
||||
|
||||
project_updated_signal = QtCore.Signal()
|
||||
|
||||
# Called when project is fully loaded
|
||||
project_loaded_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._id = None
|
||||
@@ -58,6 +63,11 @@ class Project(QtCore.QObject):
|
||||
graphic_settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, GRAPHICS_VIEW_SETTINGS)
|
||||
self._scene_width = graphic_settings["scene_width"]
|
||||
self._scene_height = graphic_settings["scene_height"]
|
||||
self._zoom = graphic_settings.get("zoom", None)
|
||||
self._show_layers = graphic_settings.get("show_layers", False)
|
||||
self._snap_to_grid = graphic_settings.get("snap_to_grid", False)
|
||||
self._show_grid = graphic_settings.get("show_grid", False)
|
||||
self._show_interface_labels = graphic_settings.get("show_interface_labels", False)
|
||||
|
||||
self._name = "untitled"
|
||||
self._filename = None
|
||||
@@ -114,6 +124,71 @@ class Project(QtCore.QObject):
|
||||
def autoStart(self):
|
||||
return self._auto_start
|
||||
|
||||
def setZoom(self, zoom):
|
||||
"""
|
||||
Sets zoom factor of the view
|
||||
"""
|
||||
self._zoom = zoom
|
||||
|
||||
def zoom(self):
|
||||
"""
|
||||
Returns zoom factor of project
|
||||
:return: float or None when not defined
|
||||
"""
|
||||
return self._zoom
|
||||
|
||||
def setShowLayers(self, show_layers):
|
||||
"""
|
||||
Sets show layers mode
|
||||
"""
|
||||
self._show_layers = show_layers
|
||||
|
||||
def showLayers(self):
|
||||
"""
|
||||
Returns if show layers mode is ON
|
||||
:return: boolean
|
||||
"""
|
||||
return self._show_layers
|
||||
|
||||
def setSnapToGrid(self, snap_to_grid):
|
||||
"""
|
||||
Sets snap to grid mode
|
||||
"""
|
||||
self._snap_to_grid = snap_to_grid
|
||||
|
||||
def snapToGrid(self):
|
||||
"""
|
||||
Returns if snap to grid mode is ON
|
||||
:return: boolean
|
||||
"""
|
||||
return self._snap_to_grid
|
||||
|
||||
def setShowGrid(self, show_grid):
|
||||
"""
|
||||
Sets show grid mode
|
||||
"""
|
||||
self._show_grid = show_grid
|
||||
|
||||
def showGrid(self):
|
||||
"""
|
||||
Returns if show grid mode is ON
|
||||
:return: boolean
|
||||
"""
|
||||
return self._show_grid
|
||||
|
||||
def setShowInterfaceLabels(self, show_interface_labels):
|
||||
"""
|
||||
Sets show interface labels mode
|
||||
"""
|
||||
self._show_interface_labels = show_interface_labels
|
||||
|
||||
def showInterfaceLabels(self):
|
||||
"""
|
||||
Returns if show interface labels mode is ON
|
||||
:return: boolean
|
||||
"""
|
||||
return self._show_interface_labels
|
||||
|
||||
def setName(self, name):
|
||||
"""
|
||||
Set project name
|
||||
@@ -195,7 +270,7 @@ class Project(QtCore.QObject):
|
||||
def _duplicateCallback(self, callback, result, error=False, **kwargs):
|
||||
if error:
|
||||
if "message" in result:
|
||||
QtWidgets.QMessageBox.critical(None, "Duplicate project", "Error while duplicate: {}".format(result["message"]))
|
||||
QtWidgets.QMessageBox.critical(None, "Duplicate project", "Error while duplicating: {}".format(result["message"]))
|
||||
return
|
||||
if callback:
|
||||
callback(result["project_id"])
|
||||
@@ -312,7 +387,12 @@ class Project(QtCore.QObject):
|
||||
"auto_close": self._auto_close,
|
||||
"auto_start": self._auto_start,
|
||||
"scene_width": self._scene_width,
|
||||
"scene_height": self._scene_height
|
||||
"scene_height": self._scene_height,
|
||||
"zoom": self._zoom,
|
||||
"show_layers": self._show_layers,
|
||||
"snap_to_grid": self._snap_to_grid,
|
||||
"show_grid": self._show_grid,
|
||||
"show_interface_labels": self._show_interface_labels
|
||||
}
|
||||
self.put("", self._projectUpdatedCallback, body=body)
|
||||
|
||||
@@ -347,6 +427,11 @@ class Project(QtCore.QObject):
|
||||
self._auto_close = result.get("auto_close", False)
|
||||
self._scene_width = result.get("scene_width", 2000)
|
||||
self._scene_height = result.get("scene_height", 1000)
|
||||
self._zoom = result.get("zoom", None)
|
||||
self._show_layers = result.get("show_layers", False)
|
||||
self._snap_to_grid = result.get("snap_to_grid", False)
|
||||
self._show_grid = result.get("show_grid", False)
|
||||
self._show_interface_labels = result.get("show_interface_labels", False)
|
||||
|
||||
def load(self, path=None):
|
||||
if not path:
|
||||
@@ -397,6 +482,7 @@ class Project(QtCore.QObject):
|
||||
topo = Topology.instance()
|
||||
for drawing in result:
|
||||
topo.createDrawing(drawing)
|
||||
self.project_loaded_signal.emit()
|
||||
|
||||
def close(self, local_server_shutdown=False):
|
||||
"""Close project"""
|
||||
@@ -427,7 +513,7 @@ class Project(QtCore.QObject):
|
||||
log.error("Error while closing project {}: {}".format(self._id, result["message"]))
|
||||
else:
|
||||
self.stopListenNotifications()
|
||||
log.info("Project {} closed".format(self._id))
|
||||
log.debug("Project {} closed".format(self._id))
|
||||
|
||||
self._closed = True
|
||||
self.project_closed_signal.emit()
|
||||
@@ -443,13 +529,22 @@ class Project(QtCore.QObject):
|
||||
def _startListenNotifications(self):
|
||||
if not Controller.instance().connected():
|
||||
return
|
||||
path = "/projects/{project_id}/notifications".format(project_id=self._id)
|
||||
self._notification_stream = Controller.instance().createHTTPQuery("GET", path, self._endListenNotificationCallback,
|
||||
downloadProgressCallback=self._event_received,
|
||||
networkManager=self._notification_network_manager,
|
||||
timeout=None,
|
||||
showProgress=False,
|
||||
ignoreErrors=True)
|
||||
|
||||
# Qt websocket before Qt 5.6 doesn't support auth
|
||||
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.6.0"):
|
||||
path = "/projects/{project_id}/notifications".format(project_id=self._id)
|
||||
self._notification_stream = Controller.instance().createHTTPQuery("GET", path, self._endListenNotificationCallback,
|
||||
downloadProgressCallback=self._event_received,
|
||||
networkManager=self._notification_network_manager,
|
||||
timeout=None,
|
||||
showProgress=False,
|
||||
ignoreErrors=True)
|
||||
|
||||
else:
|
||||
path = "/projects/{project_id}/notifications/ws".format(project_id=self._id)
|
||||
self._notification_stream = Controller.instance().connectWebSocket(path)
|
||||
self._notification_stream.textMessageReceived.connect(self._websocket_event_received)
|
||||
self._notification_stream.error.connect(self._websocket_error)
|
||||
|
||||
def _endListenNotificationCallback(self, result, error=False, **kwargs):
|
||||
"""
|
||||
@@ -459,9 +554,21 @@ class Project(QtCore.QObject):
|
||||
self._notification_stream = None
|
||||
self._startListenNotifications()
|
||||
|
||||
def _event_received(self, result, server=None, **kwargs):
|
||||
@qslot
|
||||
def _websocket_error(self, error):
|
||||
if self._notification_stream:
|
||||
log.error(self._notification_stream.errorString())
|
||||
self._notification_stream = None
|
||||
self._startListenNotifications()
|
||||
|
||||
log.debug("Event received: %s", result)
|
||||
@qslot
|
||||
def _websocket_event_received(self, event):
|
||||
self._event_received(json.loads(event))
|
||||
|
||||
def _event_received(self, result, *args, **kwargs):
|
||||
# Log only relevant events
|
||||
if result["action"] not in ("ping", "compute.updated"):
|
||||
log.debug("Event received: %s", result)
|
||||
if result["action"] == "node.created":
|
||||
node = Topology.instance().getNodeFromUuid(result["event"]["node_id"])
|
||||
if node is None:
|
||||
@@ -515,5 +622,6 @@ class Project(QtCore.QObject):
|
||||
cm.computeDataReceivedCallback(result["event"])
|
||||
elif result["action"] == "settings.updated":
|
||||
LocalConfig.instance().refreshConfigFromController()
|
||||
ApplianceManager.instance().refresh()
|
||||
elif result["action"] == "ping":
|
||||
pass
|
||||
|
||||
@@ -46,12 +46,18 @@ try:
|
||||
except ImportError:
|
||||
raise SystemExit("Please install the PyQt5.QtSvg module")
|
||||
|
||||
try:
|
||||
from PyQt5 import QtWebSockets
|
||||
sys.modules[__name__ + '.QtWebSockets'] = QtWebSockets
|
||||
except ImportError:
|
||||
raise SystemExit("Please install the PyQt5.QtWebSockets module")
|
||||
|
||||
QtCore.Signal = QtCore.pyqtSignal
|
||||
QtCore.Slot = QtCore.pyqtSlot
|
||||
QtCore.Property = QtCore.pyqtProperty
|
||||
|
||||
from PyQt5.QtWidgets import QFileDialog as OldFileDialog
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager
|
||||
|
||||
# Do not use system proxy because it could be a parental control, virus or "Security software"...
|
||||
QtNetwork.QNetworkProxyFactory.setUseSystemConfiguration(False)
|
||||
@@ -155,14 +161,12 @@ if hasattr(sys, '_called_from_test'):
|
||||
self._instances.add(self)
|
||||
|
||||
def connect(self, func, style=None):
|
||||
log.debug("{caller} connect to signal".format(caller=sys._getframe(1).f_code.co_name))
|
||||
self._callbacks.add(func)
|
||||
|
||||
def disconnect(self, func):
|
||||
self._callbacks.remove(func)
|
||||
|
||||
def emit(self, *args):
|
||||
log.debug("{caller} emit signal".format(caller=sys._getframe(1).f_code.co_name))
|
||||
for callback in list(self._callbacks):
|
||||
callback(*args)
|
||||
|
||||
@@ -224,6 +228,31 @@ class StatsQtWidgetsQDialog(QtWidgets.QDialog):
|
||||
QtWidgets.QDialog = StatsQtWidgetsQDialog
|
||||
|
||||
|
||||
class PatchNetworkAccessManager(QNetworkAccessManager):
|
||||
"""
|
||||
Patch the network acces manager in order to solve
|
||||
hibernation issues on windows and Linux
|
||||
|
||||
See: https://github.com/GNS3/gns3-gui/issues/2104
|
||||
"""
|
||||
|
||||
def __init__(self, *params, **kwargs):
|
||||
super().__init__(*params, **kwargs)
|
||||
self.setNetworkAccessible(QNetworkAccessManager.Accessible)
|
||||
self.networkAccessibleChanged.connect(self.networkAccessibleChangedSlot)
|
||||
|
||||
def networkAccessibleChangedSlot(self, status):
|
||||
"""
|
||||
When we lost the network we switch to another available network
|
||||
"""
|
||||
if status == QtNetwork.QNetworkAccessManager.Accessible:
|
||||
return
|
||||
self.setConfiguration(QtNetwork.QNetworkConfigurationManager().defaultConfiguration())
|
||||
|
||||
|
||||
QtNetwork.QNetworkAccessManager = PatchNetworkAccessManager
|
||||
|
||||
|
||||
def qpartial(func, *args, **kwargs):
|
||||
"""
|
||||
A functools partial that you can use on qobject. If the targeted qobject is
|
||||
|
||||
@@ -42,18 +42,20 @@ class QImageSvgRenderer(QtSvg.QSvgRenderer):
|
||||
|
||||
def load(self, path_or_data):
|
||||
try:
|
||||
if not os.path.exists(path_or_data) and not path_or_data.startswith(":"):
|
||||
self._svg = path_or_data
|
||||
path_or_data = path_or_data.encode("utf-8")
|
||||
return super().load(path_or_data)
|
||||
except ValueError:
|
||||
pass # On windows we can get an error because the path is too long (it's the svg data)
|
||||
path_exists = os.path.exists(path_or_data)
|
||||
except ValueError: # On windows we can get an error because the path is too long (it's the svg data)
|
||||
path_exists = False
|
||||
|
||||
if not path_exists and not path_or_data.startswith(":"):
|
||||
self._svg = path_or_data
|
||||
path_or_data = path_or_data.encode("utf-8")
|
||||
return super().load(path_or_data)
|
||||
|
||||
try:
|
||||
# We load the SVG with ElementTree before
|
||||
# because Qt when failing loading send noise to logs
|
||||
# and their is no way to prevent that
|
||||
if not path_or_data.startswith(":") and os.path.exists(path_or_data):
|
||||
if not path_or_data.startswith(":") and path_exists:
|
||||
ET.parse(path_or_data)
|
||||
res = super().load(path_or_data)
|
||||
# If we can't render a SVG we load and base64 the image to create a SVG
|
||||
|
||||
@@ -35,15 +35,21 @@ class Appliance(collections.Mapping):
|
||||
def __init__(self, registry, path):
|
||||
"""
|
||||
:params registry: Instance of the registry where images are located
|
||||
:params path: Path of the appliance file on disk
|
||||
:params path: Path of the appliance file on disk or file content
|
||||
"""
|
||||
self._registry = registry
|
||||
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
self._appliance = json.load(f)
|
||||
except (OSError, ValueError) as e:
|
||||
raise ApplianceError("Could not read appliance {}: {}".format(os.path.abspath(path), str(e)))
|
||||
if os.path.isabs(path):
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
self._appliance = json.load(f)
|
||||
except (OSError, ValueError) as e:
|
||||
raise ApplianceError("Could not read appliance {}: {}".format(os.path.abspath(path), str(e)))
|
||||
else:
|
||||
try:
|
||||
self._appliance = json.loads(path)
|
||||
except ValueError as e:
|
||||
raise ApplianceError("Could not read appliance {}: {}".format(os.path.abspath(path), str(e)))
|
||||
self._check_config()
|
||||
self._resolve_version()
|
||||
|
||||
@@ -113,7 +119,7 @@ class Appliance(collections.Mapping):
|
||||
"""
|
||||
Duplicate a version in order to create a new version
|
||||
"""
|
||||
if len(self._appliance["versions"]) == 0:
|
||||
if 'versions' not in self._appliance.keys() or len(self._appliance["versions"]) == 0:
|
||||
raise ApplianceError("Your appliance file doesn't contain any versions")
|
||||
|
||||
ref = self._appliance["versions"][0]
|
||||
|
||||
@@ -57,6 +57,13 @@ class Config:
|
||||
"""
|
||||
return LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)["images_path"]
|
||||
|
||||
@property
|
||||
def appliances_dir(self):
|
||||
"""
|
||||
:returns: Location of the images directory on the server
|
||||
"""
|
||||
return LocalServerConfig.instance().loadSettings("Server", LOCAL_SERVER_SETTINGS)["appliances_path"]
|
||||
|
||||
@property
|
||||
def symbols_dir(self):
|
||||
"""
|
||||
@@ -262,8 +269,8 @@ class Config:
|
||||
if "port_segment_size" in appliance_config:
|
||||
new_config["port_segment_size"] = appliance_config["port_segment_size"]
|
||||
|
||||
if "linked_base" in appliance_config:
|
||||
new_config["linked_base"] = appliance_config["linked_base"]
|
||||
if "linked_clone" in appliance_config:
|
||||
new_config["linked_clone"] = appliance_config["linked_clone"]
|
||||
|
||||
log.debug("Add appliance QEMU: %s", str(new_config))
|
||||
self._config["Qemu"].setdefault("vms", [])
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"type": "integer",
|
||||
"title": "Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2"
|
||||
},
|
||||
"linked_base": {
|
||||
"linked_clone": {
|
||||
"type": "boolean",
|
||||
"title": "False if you don't want to use a single image for all nodes"
|
||||
},
|
||||
|
||||
@@ -36,6 +36,9 @@ DEFAULT_SYMBOLS_PATH = os.path.normpath(os.path.expanduser("~/GNS3/symbols"))
|
||||
# Default configs directory location
|
||||
DEFAULT_CONFIGS_PATH = os.path.normpath(os.path.expanduser("~/GNS3/configs"))
|
||||
|
||||
# Default appliances location
|
||||
DEFAULT_APPLIANCES_PATH = os.path.normpath(os.path.expanduser("~/GNS3/appliances"))
|
||||
|
||||
DEFAULT_LOCAL_SERVER_HOST = "127.0.0.1"
|
||||
DEFAULT_LOCAL_SERVER_PORT = 3080
|
||||
|
||||
@@ -163,7 +166,8 @@ elif sys.platform.startswith("darwin"):
|
||||
" -e ' display dialog \"WARNING OSX VNC support is limited if you have trouble connecting to a device please use an alternative client like Chicken of the VNC.\" buttons {\"OK\"} default button 1 with icon caution with title \"GNS3\"'"
|
||||
" -e ' open location \"vnc://%h:%p\"'"
|
||||
" -e 'end tell'",
|
||||
'Chicken of the VNC': "/Applications/Chicken\ of\ the\ VNC.app/Contents/MacOS/Chicken\ of\ the\ VNC %h:%p",
|
||||
'Chicken of the VNC': "/Applications/Chicken.app/Contents/MacOS/Chicken %h:%p",
|
||||
'Chicken of the VNC < 2.2': "/Applications/Chicken\ of\ the\ VNC.app/Contents/MacOS/Chicken\ of\ the\ VNC %h:%p",
|
||||
'Royal TSX': "open 'rtsx://vnc%3A%2F%2F%h:%p'",
|
||||
}
|
||||
|
||||
@@ -180,6 +184,33 @@ else:
|
||||
# default VNC console command on other systems
|
||||
DEFAULT_VNC_CONSOLE_COMMAND = PRECONFIGURED_VNC_CONSOLE_COMMANDS['TightVNC']
|
||||
|
||||
# Pre-configured SPICE console commands on various OSes
|
||||
if sys.platform.startswith("win"):
|
||||
# Windows
|
||||
PRECONFIGURED_SPICE_CONSOLE_COMMANDS = {
|
||||
'Remote Viewer (included with GNS3)': '"c:\\Program Files\\VirtViewer v5.0-256\\bin\\remote-viewer.exe" spice://%h:%p',
|
||||
}
|
||||
|
||||
# default Windows SPICE console command
|
||||
DEFAULT_SPICE_CONSOLE_COMMAND = PRECONFIGURED_SPICE_CONSOLE_COMMANDS['Remote Viewer (included with GNS3)']
|
||||
|
||||
elif sys.platform.startswith("darwin"):
|
||||
# Mac OS X
|
||||
PRECONFIGURED_SPICE_CONSOLE_COMMANDS = {
|
||||
'Remote Viewer': '/Applications/RemoteViewer.app/Contents/MacOS/RemoteViewer spice://%h:%p',
|
||||
}
|
||||
|
||||
# default Mac OS X SPICE console command
|
||||
DEFAULT_SPICE_CONSOLE_COMMAND = PRECONFIGURED_SPICE_CONSOLE_COMMANDS['Remote Viewer']
|
||||
|
||||
else:
|
||||
PRECONFIGURED_SPICE_CONSOLE_COMMANDS = {
|
||||
'Remote Viewer': 'remote-viewer spice://%h:%p',
|
||||
}
|
||||
|
||||
# default SPICE console command on other systems
|
||||
DEFAULT_SPICE_CONSOLE_COMMAND = PRECONFIGURED_SPICE_CONSOLE_COMMANDS['Remote Viewer']
|
||||
|
||||
# Pre-configured packet capture reader commands on various OSes
|
||||
WIRESHARK_NORMAL_CAPTURE = "Wireshark Traditional Capture"
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE = "Wireshark Live Traffic Capture"
|
||||
@@ -219,12 +250,14 @@ else:
|
||||
GENERAL_SETTINGS = {
|
||||
"style": DEFAULT_STYLE,
|
||||
"check_for_update": True,
|
||||
"overlay_notifications": True,
|
||||
"experimental_features": False,
|
||||
"send_stats": True,
|
||||
"stats_visitor_id": str(uuid.uuid4()), # An anonymous id for stats
|
||||
"last_check_for_update": 0,
|
||||
"telnet_console_command": DEFAULT_TELNET_CONSOLE_COMMAND,
|
||||
"vnc_console_command": DEFAULT_VNC_CONSOLE_COMMAND,
|
||||
"spice_console_command": DEFAULT_SPICE_CONSOLE_COMMAND,
|
||||
"delay_console_all": 500,
|
||||
"hide_getting_started_dialog": False,
|
||||
"hide_setup_wizard": False,
|
||||
@@ -239,6 +272,10 @@ GENERAL_SETTINGS = {
|
||||
"hdpi": not sys.platform.startswith("linux")
|
||||
}
|
||||
|
||||
NODES_VIEW_SETTINGS = {
|
||||
"nodes_view_filter": 0,
|
||||
}
|
||||
|
||||
GRAPHICS_VIEW_SETTINGS = {
|
||||
"scene_width": 2000,
|
||||
"scene_height": 1000,
|
||||
@@ -246,6 +283,11 @@ GRAPHICS_VIEW_SETTINGS = {
|
||||
"draw_link_status_points": True,
|
||||
"default_label_font": "TypeWriter,10,-1,5,75,0,0,0,0,0",
|
||||
"default_label_color": "#000000",
|
||||
"zoom": None,
|
||||
"show_layers": False,
|
||||
"snap_to_grid": False,
|
||||
"show_grid": False,
|
||||
"show_interface_labels": False
|
||||
}
|
||||
|
||||
LOCAL_SERVER_SETTINGS = {
|
||||
@@ -255,6 +297,7 @@ LOCAL_SERVER_SETTINGS = {
|
||||
"port": DEFAULT_LOCAL_SERVER_PORT,
|
||||
"images_path": DEFAULT_IMAGES_PATH,
|
||||
"projects_path": DEFAULT_PROJECTS_PATH,
|
||||
"appliances_path": DEFAULT_APPLIANCES_PATH,
|
||||
"additional_images_paths": "",
|
||||
"symbols_path": DEFAULT_SYMBOLS_PATH,
|
||||
"configs_path": DEFAULT_CONFIGS_PATH,
|
||||
@@ -281,5 +324,6 @@ PACKET_CAPTURE_SETTINGS = {
|
||||
CUSTOM_CONSOLE_COMMANDS_SETTINGS = {
|
||||
"telnet": {},
|
||||
"vnc": {},
|
||||
"serial": {}
|
||||
"serial": {},
|
||||
"spice": {}
|
||||
}
|
||||
|
||||
63
gns3/spice_console.py
Normal file
63
gns3/spice_console.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2017 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/>.
|
||||
|
||||
"""
|
||||
Functions to start SPICE console programs.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def spiceConsole(host, port, command):
|
||||
"""
|
||||
Start a SPICE console program.
|
||||
|
||||
:param host: host or IP address
|
||||
:param port: port number
|
||||
:param command: command to be executed
|
||||
"""
|
||||
|
||||
if len(command.strip(' ')) == 0:
|
||||
log.warning('SPICE client is not configured')
|
||||
return
|
||||
|
||||
# ipv6 support
|
||||
if ":" in host:
|
||||
host = "[{}]".format(host)
|
||||
|
||||
# replace the place-holders by the actual values
|
||||
command = command.replace("%h", host)
|
||||
command = command.replace("%p", str(port))
|
||||
|
||||
try:
|
||||
log.info('starting SPICE program "{}"'.format(command))
|
||||
if sys.platform.startswith("win"):
|
||||
# use the string on Windows
|
||||
subprocess.Popen(command)
|
||||
else:
|
||||
# use arguments on other platforms
|
||||
args = shlex.split(command)
|
||||
subprocess.Popen(args, env=os.environ)
|
||||
except (OSError, ValueError, subprocess.SubprocessError) as e:
|
||||
log.warning('could not start SPICE program "{}": {}'.format(command, e))
|
||||
raise
|
||||
92
gns3/status_bar.py
Normal file
92
gns3/status_bar.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2017 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from .qt import QtWidgets, QtGui, qslot
|
||||
|
||||
|
||||
class StatusBarHandler(logging.StreamHandler):
|
||||
|
||||
def __init__(self, widget):
|
||||
super().__init__()
|
||||
self._widget = widget
|
||||
self.setLevel(logging.WARNING)
|
||||
|
||||
def emit(self, record):
|
||||
if record.levelno > logging.WARNING:
|
||||
self._widget.addError()
|
||||
else:
|
||||
self._widget.addWarning()
|
||||
|
||||
|
||||
class StatusBar(QtWidgets.QStatusBar):
|
||||
"""
|
||||
The status bar
|
||||
"""
|
||||
|
||||
def __init__(self, parent, *args, **kwargs):
|
||||
super().__init__(parent, *args, **kwargs)
|
||||
self._parent = parent
|
||||
self._errors = 0
|
||||
self._warnings = 0
|
||||
|
||||
self._errors_button = QtWidgets.QPushButton()
|
||||
self._errors_button.setFlat(True)
|
||||
self._errors_button.setIcon(QtGui.QIcon(":/icons/warning.svg"))
|
||||
self._errors_button.clicked.connect(self._errorButtonPushedSlot)
|
||||
self.addPermanentWidget(self._errors_button)
|
||||
|
||||
self._refresh()
|
||||
|
||||
def addError(self):
|
||||
"""
|
||||
Increment error count
|
||||
"""
|
||||
self._errors += 1
|
||||
self._refresh()
|
||||
|
||||
def addWarning(self):
|
||||
"""
|
||||
Increment warning count
|
||||
"""
|
||||
self._warnings += 1
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self):
|
||||
if self._errors == 0 and self._warnings == 0:
|
||||
self._errors_button.setText("")
|
||||
self._errors_button.hide()
|
||||
return
|
||||
self._errors_button.show()
|
||||
text = ""
|
||||
if self._errors == 1:
|
||||
text += "1 error"
|
||||
elif self._errors > 1:
|
||||
text += "{} errors".format(self._errors)
|
||||
if self._warnings == 1:
|
||||
text += " 1 warning"
|
||||
elif self._warnings > 1:
|
||||
text += " {} warnings".format(self._warnings)
|
||||
self._errors_button.setText(text)
|
||||
|
||||
@qslot
|
||||
def _errorButtonPushedSlot(self, *args, **kwargs):
|
||||
self._parent.uiConsoleDockWidget.toggleViewAction().trigger()
|
||||
self._warnings = 0
|
||||
self._errors = 0
|
||||
self._refresh()
|
||||
@@ -72,6 +72,7 @@ class Style:
|
||||
self._mw.uiInsertImageAction.setIcon(QtGui.QIcon(":/icons/image.svg"))
|
||||
self._mw.uiDrawRectangleAction.setIcon(self._getStyleIcon(":/icons/rectangle.svg", ":/icons/rectangle-hover.svg"))
|
||||
self._mw.uiDrawEllipseAction.setIcon(self._getStyleIcon(":/icons/ellipse.svg", ":/icons/ellipse-hover.svg"))
|
||||
self._mw.uiDrawLineAction.setIcon(QtGui.QIcon(":/icons/vertically.svg"))
|
||||
self._mw.uiEditReadmeAction.setIcon(QtGui.QIcon(":/icons/edit.svg"))
|
||||
self._mw.uiOnlineHelpAction.setIcon(QtGui.QIcon(":/icons/help.svg"))
|
||||
self._mw.uiBrowseRoutersAction.setIcon(self._getStyleIcon(":/icons/router.png", ":/icons/router-hover.png"))
|
||||
@@ -100,7 +101,7 @@ class Style:
|
||||
self._mw.uiPreferencesAction.setIcon(self._getStyleIcon(":/classic_icons/preferences.svg", ":/classic_icons/preferences-hover.svg"))
|
||||
self._mw.uiZoomInAction.setIcon(self._getStyleIcon(":/classic_icons/zoom-in.svg", ":/classic_icons/zoom-in-hover.svg"))
|
||||
self._mw.uiZoomOutAction.setIcon(self._getStyleIcon(":/classic_icons/zoom-out.svg", ":/classic_icons/zoom-out-hover.svg"))
|
||||
self._mw.uiShowPortNamesAction.setIcon(self._getStyleIcon(":/classic_icons/show-interface-names.svg",":/classic_icons/show-interface-names-hover.svg"))
|
||||
self._mw.uiShowPortNamesAction.setIcon(self._getStyleIcon(":/classic_icons/show-interface-names.svg", ":/classic_icons/show-interface-names-hover.svg"))
|
||||
self._mw.uiStartAllAction.setIcon(self._getStyleIcon(":/classic_icons/start.svg", ":/classic_icons/start-hover.svg"))
|
||||
self._mw.uiSuspendAllAction.setIcon(self._getStyleIcon(":/classic_icons/pause.svg", ":/classic_icons/pause-hover.svg"))
|
||||
self._mw.uiStopAllAction.setIcon(self._getStyleIcon(":/classic_icons/stop.svg", ":/classic_icons/stop-hover.svg"))
|
||||
@@ -111,6 +112,7 @@ class Style:
|
||||
self._mw.uiInsertImageAction.setIcon(self._getStyleIcon(":/classic_icons/image.svg", ":/classic_icons/image-hover.svg"))
|
||||
self._mw.uiDrawRectangleAction.setIcon(self._getStyleIcon(":/classic_icons/rectangle.svg", ":/classic_icons/rectangle-hover.svg"))
|
||||
self._mw.uiDrawEllipseAction.setIcon(self._getStyleIcon(":/classic_icons/ellipse.svg", ":/classic_icons/ellipse-hover.svg"))
|
||||
self._mw.uiDrawLineAction.setIcon(self._getStyleIcon(":/classic_icons/line.svg", ":/classic_icons/line-hover.svg"))
|
||||
self._mw.uiEditReadmeAction.setIcon(self._getStyleIcon(":/classic_icons/edit.svg", ":/classic_icons/edit.svg"))
|
||||
self._mw.uiOnlineHelpAction.setIcon(self._getStyleIcon(":/classic_icons/help.svg", ":/classic_icons/help-hover.svg"))
|
||||
self._mw.uiBrowseRoutersAction.setIcon(self._getStyleIcon(":/classic_icons/router.svg", ":/classic_icons/router-hover.svg"))
|
||||
@@ -161,6 +163,7 @@ class Style:
|
||||
self._mw.uiInsertImageAction.setIcon(self._getStyleIcon(":/charcoal_icons/image.svg", ":/charcoal_icons/image-hover.svg"))
|
||||
self._mw.uiDrawRectangleAction.setIcon(self._getStyleIcon(":/charcoal_icons/rectangle.svg", ":/charcoal_icons/rectangle-hover.svg"))
|
||||
self._mw.uiDrawEllipseAction.setIcon(self._getStyleIcon(":/charcoal_icons/ellipse.svg", ":/charcoal_icons/ellipse-hover.svg"))
|
||||
self._mw.uiDrawLineAction.setIcon(self._getStyleIcon(":/charcoal_icons/line.svg", ":/charcoal_icons/line-hover.svg"))
|
||||
self._mw.uiEditReadmeAction.setIcon(self._getStyleIcon(":/charcoal_icons/edit.svg", ":/charcoal_icons/edit.svg"))
|
||||
self._mw.uiOnlineHelpAction.setIcon(self._getStyleIcon(":/charcoal_icons/help.svg", ":/charcoal_icons/help-hover.svg"))
|
||||
self._mw.uiBrowseRoutersAction.setIcon(self._getStyleIcon(":/charcoal_icons/router.svg", ":/charcoal_icons/router-hover.svg"))
|
||||
|
||||
@@ -33,6 +33,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
console_mutex = QtCore.QMutex()
|
||||
|
||||
|
||||
class ConsoleThread(QtCore.QThread):
|
||||
|
||||
consoleError = QtCore.pyqtSignal(str)
|
||||
@@ -71,6 +72,7 @@ class ConsoleThread(QtCore.QThread):
|
||||
command = command.replace("%p", str(port))
|
||||
command = command.replace("%d", self._name)
|
||||
command = command.replace("%i", self._node.project().id())
|
||||
command = command.replace("%n", str(self._node.id()))
|
||||
command = command.replace("%c", Controller.instance().httpClient().fullUrl())
|
||||
|
||||
# If the console use an apple script we lock to avoid multiple console
|
||||
@@ -84,7 +86,7 @@ class ConsoleThread(QtCore.QThread):
|
||||
pass
|
||||
# log.warning('could not start Telnet console "{}": {}'.format(self._command, e))
|
||||
finally:
|
||||
log.info('Telnet console {}:{} closed'.format(host, port))
|
||||
log.debug('Telnet console {}:{} closed'.format(host, port))
|
||||
if sys.platform.startswith("darwin") and "osascript" in command:
|
||||
console_mutex.unlock()
|
||||
|
||||
@@ -106,7 +108,7 @@ def nodeTelnetConsole(node, port, command=None):
|
||||
if not command:
|
||||
return
|
||||
|
||||
log.info('Starting telnet console in thread "{}"'.format(command))
|
||||
log.debug('Starting telnet console in thread "{}"'.format(command))
|
||||
console_thread = ConsoleThread(MainWindow.instance(), command, node, port)
|
||||
console_thread.consoleError.connect(_consoleErrorSlot)
|
||||
console_thread.start()
|
||||
@@ -114,5 +116,3 @@ def nodeTelnetConsole(node, port, command=None):
|
||||
|
||||
def _consoleErrorSlot(message):
|
||||
QtWidgets.QMessageBox.critical(MainWindow.instance(), "Error", message)
|
||||
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ class Topology(QtCore.QObject):
|
||||
if project:
|
||||
self._project.project_updated_signal.connect(self._projectUpdatedSlot)
|
||||
self._project.project_creation_error_signal.connect(self._projectCreationErrorSlot)
|
||||
self._project.project_loaded_signal.connect(self._projectLoadedSlot)
|
||||
self._main_window.setWindowTitle("{name} - GNS3".format(name=self._project.name()))
|
||||
self._main_window.uiGraphicsView.setSceneSize(project.sceneWidth(), project.sceneHeight())
|
||||
else:
|
||||
@@ -146,6 +147,23 @@ class Topology(QtCore.QObject):
|
||||
self._main_window.updateRecentProjectsSettings(self._project.id(), self._project.name(), self._project.path())
|
||||
self._main_window.updateRecentProjectActions()
|
||||
|
||||
def _projectLoadedSlot(self):
|
||||
# when project is loaded we can make updates in GUI
|
||||
if self._project is not None:
|
||||
self._main_window.uiShowLayersAction.setChecked(self._project.showLayers())
|
||||
self._main_window.showLayers(self._project.showLayers())
|
||||
|
||||
self._main_window.uiShowGridAction.setChecked(self._project.showGrid())
|
||||
self._main_window.showGrid(self._project.showGrid())
|
||||
|
||||
self._main_window.uiSnapToGridAction.setChecked(self._project.snapToGrid())
|
||||
self._main_window.snapToGrid(self._project.snapToGrid())
|
||||
|
||||
self._main_window.uiShowPortNamesAction.setChecked(self._project.showInterfaceLabels())
|
||||
self._main_window.showInterfaceLabels(self._project.showInterfaceLabels())
|
||||
|
||||
self._main_window.uiGraphicsView.setZoom(self._project.zoom())
|
||||
|
||||
def createLoadProject(self, project_settings):
|
||||
"""
|
||||
Create load a project based on settings, not on the .gns3
|
||||
@@ -198,8 +216,10 @@ class Topology(QtCore.QObject):
|
||||
dialog.exec_()
|
||||
|
||||
def _projectCreationErrorSlot(self, message):
|
||||
self.setProject(None)
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "New project", message)
|
||||
if self._project:
|
||||
self._project.project_creation_error_signal.disconnect(self._projectCreationErrorSlot)
|
||||
self.setProject(None)
|
||||
QtWidgets.QMessageBox.critical(self._main_window, "New project", message)
|
||||
|
||||
def exportProject(self):
|
||||
include_image_question = """Would you like to include any base image?
|
||||
@@ -542,6 +562,8 @@ It is your responsability to check if you have the right to distribute the image
|
||||
type = "rect"
|
||||
elif tag == "text":
|
||||
type = "text"
|
||||
elif tag == "line":
|
||||
type = "line"
|
||||
else:
|
||||
type = "image"
|
||||
except IndexError:
|
||||
|
||||
@@ -107,6 +107,7 @@ class TopologyNodeItem(QtWidgets.QTreeWidgetItem):
|
||||
self.takeChildren()
|
||||
|
||||
capturing = False
|
||||
filtering = False
|
||||
for link in self._node.links():
|
||||
item = QtWidgets.QTreeWidgetItem()
|
||||
port = link.getNodePort(self._node)
|
||||
@@ -115,10 +116,19 @@ class TopologyNodeItem(QtWidgets.QTreeWidgetItem):
|
||||
if link.capturing():
|
||||
item.setIcon(0, QtGui.QIcon(':/icons/inspect.svg'))
|
||||
capturing = True
|
||||
if len(link.filters()) > 0:
|
||||
item.setIcon(0, QtGui.QIcon(':/icons/filter.svg'))
|
||||
filtering = True
|
||||
if link.capturing() and len(link.filters()) > 0:
|
||||
item.setIcon(0, QtGui.QIcon(':/icons/filter-capture.svg'))
|
||||
if link.suspended():
|
||||
item.setIcon(0, QtGui.QIcon(':/icons/pause.svg'))
|
||||
self.addChild(item)
|
||||
|
||||
if self._parent.show_only_devices_with_capture and capturing is False:
|
||||
self.setHidden(True)
|
||||
elif self._parent.show_only_devices_with_filters and filtering is False:
|
||||
self.setHidden(True)
|
||||
else:
|
||||
self.setHidden(False)
|
||||
|
||||
@@ -156,6 +166,7 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
|
||||
self._topology.project_changed_signal.connect(self._projectChangedSlot)
|
||||
self.itemSelectionChanged.connect(self._itemSelectionChangedSlot)
|
||||
self.show_only_devices_with_capture = False
|
||||
self.show_only_devices_with_filters = False
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
self.itemDoubleClicked.connect(self._itemDoubleClickedSlot)
|
||||
|
||||
@@ -221,8 +232,8 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
|
||||
elif isinstance(item, LinkItem):
|
||||
item.setHovered(False)
|
||||
if not isinstance(current_item, TopologyNodeItem):
|
||||
port = current_item.data(0, QtCore.Qt.UserRole)
|
||||
if item.sourcePort() == port or item.destinationPort() == port:
|
||||
link = current_item.data(0, QtCore.Qt.UserRole)
|
||||
if item.link() == link:
|
||||
item.setHovered(True)
|
||||
|
||||
@qslot
|
||||
@@ -239,8 +250,8 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
|
||||
view.centerOn(item)
|
||||
elif isinstance(item, LinkItem):
|
||||
if not isinstance(current_item, TopologyNodeItem):
|
||||
port = current_item.data(0, QtCore.Qt.UserRole)
|
||||
if item.sourcePort() == port or item.destinationPort() == port:
|
||||
link = current_item.data(0, QtCore.Qt.UserRole)
|
||||
if item.link() == link:
|
||||
view.centerOn(item)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
@@ -271,11 +282,17 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
|
||||
collapse_all.triggered.connect(self._collapseAllSlot)
|
||||
menu.addAction(collapse_all)
|
||||
|
||||
if self.show_only_devices_with_capture is False:
|
||||
if self.show_only_devices_with_capture is False and self.show_only_devices_with_filters is False:
|
||||
devices_with_capture = QtWidgets.QAction("Show devices with capture(s)", menu)
|
||||
devices_with_capture.setIcon(QtGui.QIcon(":/icons/inspect.svg"))
|
||||
devices_with_capture.triggered.connect(self._devicesWithCaptureSlot)
|
||||
menu.addAction(devices_with_capture)
|
||||
|
||||
devices_with_filters = QtWidgets.QAction("Show devices with packet filter(s)", menu)
|
||||
devices_with_filters.setIcon(QtGui.QIcon(":/icons/filter.svg"))
|
||||
devices_with_filters.triggered.connect(self._devicesWithFiltersSlot)
|
||||
menu.addAction(devices_with_filters)
|
||||
|
||||
else:
|
||||
show_all_devices = QtWidgets.QAction("Show all devices", menu)
|
||||
# show_all_devices.setIcon(QtGui.QIcon(":/icons/inspect.svg"))
|
||||
@@ -287,6 +304,11 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
|
||||
stop_all_captures.triggered.connect(self._stopAllCapturesSlot)
|
||||
menu.addAction(stop_all_captures)
|
||||
|
||||
reset_all_filters = QtWidgets.QAction("Reset all packet filters", menu)
|
||||
reset_all_filters.setIcon(QtGui.QIcon(":/icons/filter-reset.svg"))
|
||||
reset_all_filters.triggered.connect(self._resetAllFiltersSlot)
|
||||
menu.addAction(reset_all_filters)
|
||||
|
||||
current_item = self.currentItem()
|
||||
from .main_window import MainWindow
|
||||
view = MainWindow.instance().uiGraphicsView
|
||||
@@ -295,9 +317,9 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
|
||||
if isinstance(current_item, TopologyNodeItem):
|
||||
view.populateDeviceContextualMenu(menu)
|
||||
else:
|
||||
port = current_item.data(0, QtCore.Qt.UserRole)
|
||||
link = current_item.data(0, QtCore.Qt.UserRole)
|
||||
for item in view.scene().items():
|
||||
if isinstance(item, LinkItem) and (item.sourcePort() == port or item.destinationPort() == port):
|
||||
if isinstance(item, LinkItem) and item.link() == link:
|
||||
item.populateLinkContextualMenu(menu)
|
||||
break
|
||||
|
||||
@@ -328,6 +350,15 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
|
||||
self.show_only_devices_with_capture = True
|
||||
self.refreshAllLinks()
|
||||
|
||||
@qslot
|
||||
def _devicesWithFiltersSlot(self, *args):
|
||||
"""
|
||||
Show only devices with filters.
|
||||
"""
|
||||
|
||||
self.show_only_devices_with_filters = True
|
||||
self.refreshAllLinks()
|
||||
|
||||
@qslot
|
||||
def _showAllDevicesSlot(self, *args):
|
||||
"""
|
||||
@@ -335,6 +366,7 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
|
||||
"""
|
||||
|
||||
self.show_only_devices_with_capture = False
|
||||
self.show_only_devices_with_filters = False
|
||||
self.refreshAllLinks()
|
||||
|
||||
@qslot
|
||||
@@ -346,3 +378,15 @@ class TopologySummaryView(QtWidgets.QTreeWidget):
|
||||
for link in self._topology.links():
|
||||
if link.capturing():
|
||||
PacketCapture.instance().stopCapture(link)
|
||||
|
||||
@qslot
|
||||
def _resetAllFiltersSlot(self, *args):
|
||||
"""
|
||||
Reset all packet filters
|
||||
"""
|
||||
|
||||
for link in self._topology.links():
|
||||
if len(link.filters()) > 0:
|
||||
filters = {}
|
||||
link.setFilters(filters)
|
||||
link.update()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user