Merge pull request #1332 from GNS3/global-vars

Docker `ExtraHosts`, global variables for project and supplier support, Fixes: #2482
This commit is contained in:
ziajka authored and GitHub committed 2018-05-15 09:23:54 +02:00
commit c52342907a
20 files changed
+507 -32

No files matched your search

+49 -4
View File
@@ -61,11 +61,12 @@ class DockerVM(BaseNode):
:param console_resolution: Resolution of the VNC display
:param console_http_port: Port to redirect HTTP queries
:param console_http_path: Url part with the path of the web interface
:param extra_hosts: Hosts which will be written into /etc/hosts into docker conainer
"""
def __init__(self, name, node_id, project, manager, image, console=None, aux=None, start_command=None,
adapters=None, environment=None, console_type="telnet", console_resolution="1024x768",
console_http_port=80, console_http_path="/"):
console_http_port=80, console_http_path="/", extra_hosts=None):
super().__init__(name, node_id, project, manager, console=console, aux=aux, allocate_aux=True, console_type=console_type)
@@ -84,6 +85,8 @@ class DockerVM(BaseNode):
self._console_http_path = console_http_path
self._console_http_port = console_http_port
self._console_websocket = None
self._extra_hosts = extra_hosts
self._volumes = []
# Keep a list of created bridge
self._bridges = set()
@@ -114,7 +117,8 @@ class DockerVM(BaseNode):
"start_command": self.start_command,
"status": self.status,
"environment": self.environment,
"node_directory": self.working_path
"node_directory": self.working_path,
"extra_hosts": self.extra_hosts
}
def _get_free_display_port(self):
@@ -178,6 +182,14 @@ class DockerVM(BaseNode):
def environment(self, command):
self._environment = command
@property
def extra_hosts(self):
return self._extra_hosts
@extra_hosts.setter
def extra_hosts(self, extra_hosts):
self._extra_hosts = extra_hosts
@asyncio.coroutine
def _get_container_state(self):
"""Returns the container state (e.g. running, paused etc.)
@@ -288,7 +300,7 @@ class DockerVM(BaseNode):
"HostConfig": {
"CapAdd": ["ALL"],
"Privileged": True,
"Binds": self._mount_binds(image_infos)
"Binds": self._mount_binds(image_infos),
},
"Volumes": {},
"Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573
@@ -313,11 +325,20 @@ class DockerVM(BaseNode):
# Give the information to the container the list of volume path mounted
params["Env"].append("GNS3_VOLUMES={}".format(":".join(self._volumes)))
variables = self.project.variables
if not variables:
variables = []
for var in variables:
formatted = self._format_env(variables, var.get('value', ''))
params["Env"].append("{}={}".format(var["name"], formatted))
if self._environment:
for e in self._environment.strip().split("\n"):
e = e.strip()
if not e.startswith("GNS3_"):
params["Env"].append(e)
formatted = self._format_env(variables, e)
params["Env"].append(formatted)
if self._console_type == "vnc":
yield from self._start_vnc()
@@ -325,12 +346,36 @@ class DockerVM(BaseNode):
params["Env"].append("DISPLAY=:{}".format(self._display))
params["HostConfig"]["Binds"].append("/tmp/.X11-unix/:/tmp/.X11-unix/")
if self._extra_hosts:
extra_hosts = self._format_extra_hosts(self._extra_hosts)
if extra_hosts:
params["Env"].append("GNS3_EXTRA_HOSTS={}".format(extra_hosts))
result = yield from self.manager.query("POST", "containers/create", data=params)
self._cid = result['Id']
log.info("Docker container '{name}' [{id}] created".format(
name=self._name, id=self._id))
return True
def _format_env(self, variables, env):
for variable in variables:
env = env.replace('${' + variable["name"] + '}', variable.get("value", ""))
return env
def _format_extra_hosts(self, extra_hosts):
lines = [h.strip() for h in self._extra_hosts.split("\n") if h.strip() != ""]
hosts = []
try:
for host in lines:
hostname, ip = host.split(":")
hostname = hostname.strip()
ip = ip.strip()
if hostname and ip:
hosts.append((hostname, ip))
except ValueError:
raise DockerError("Can't apply `ExtraHosts`, wrong format: {}".format(extra_hosts))
return "\n".join(["{}\t{}".format(h[1], h[0]) for h in hosts])
@asyncio.coroutine
def update(self):
"""
@@ -60,6 +60,14 @@ ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
__EOF__
# imitate docker's `ExtraHosts` behaviour
sed -i '/GNS3_EXTRA_HOSTS_START/,/GNS3_EXTRA_HOSTS_END/d' /etc/hosts
[ -n "$GNS3_EXTRA_HOSTS" ] && cat >> /etc/hosts << __EOF__
# GNS3_EXTRA_HOSTS_START
$GNS3_EXTRA_HOSTS
# GNS3_EXTRA_HOSTS_END
__EOF__
# configure loopback interface
ip link set dev lo up
+24 -3
View File
@@ -25,13 +25,13 @@ import zipfile
import json
from uuid import UUID, uuid4
from .port_manager import PortManager
from .notification_manager import NotificationManager
from ..config import Config
from ..utils.asyncio import wait_run_in_executor
from ..utils.path import check_path_allowed, get_default_project_directory
import logging
log = logging.getLogger(__name__)
@@ -46,7 +46,7 @@ class Project:
:param path: path of the project. (None use the standard directory)
"""
def __init__(self, name=None, project_id=None, path=None):
def __init__(self, name=None, project_id=None, path=None, variables=None):
self._name = name
if project_id:
@@ -61,6 +61,7 @@ class Project:
self._nodes = set()
self._used_tcp_ports = set()
self._used_udp_ports = set()
self._variables = variables
if path is None:
location = get_default_project_directory()
@@ -83,7 +84,8 @@ class Project:
return {
"name": self._name,
"project_id": self._id
"project_id": self._id,
"variables": self._variables
}
def _config(self):
@@ -131,6 +133,14 @@ class Project:
return self._nodes
@property
def variables(self):
return self._variables
@variables.setter
def variables(self, variables):
self._variables = variables
def record_tcp_port(self, port):
"""
Associate a reserved TCP port number with this project.
@@ -287,6 +297,17 @@ class Project:
yield from node.delete()
self._nodes.remove(node)
@asyncio.coroutine
def update(self, variables=None, **kwargs):
original_variables = self.variables
self.variables = variables
# we need to update docker nodes when variables changes
if original_variables != variables:
for node in self.nodes:
if hasattr(node, 'update'):
yield from node.update()
@asyncio.coroutine
def close(self):
"""
+4 -3
View File
@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import aiohttp
import asyncio
import psutil
import platform
from .project import Project
@@ -95,16 +96,16 @@ class ProjectManager:
log.warning(message)
project.emit("log.warning", {"message": message})
def create_project(self, name=None, project_id=None, path=None):
def create_project(self, name=None, project_id=None, path=None, variables=None):
"""
Create a project and keep a references to it in project manager.
See documentation of Project for arguments
"""
if project_id is not None and project_id in self._projects:
return self._projects[project_id]
project = Project(name=name, project_id=project_id, path=path)
project = Project(name=name, project_id=project_id,
path=path, variables=variables)
self._check_available_disk_space(project)
self._projects[project.id] = project
return project
-1
View File
@@ -460,7 +460,6 @@ class Compute:
msg = json.loads(response.data)
action = msg.pop("action")
event = msg.pop("event")
if action == "ping":
self._cpu_usage_percent = event["cpu_usage_percent"]
self._memory_usage_percent = event["memory_usage_percent"]
+58 -3
View File
@@ -69,7 +69,7 @@ class Project:
def __init__(self, name=None, project_id=None, path=None, controller=None, status="opened",
filename=None, auto_start=False, auto_open=False, auto_close=True,
scene_height=1000, scene_width=2000, zoom=100, show_layers=False, snap_to_grid=False, show_grid=False,
grid_size=0, show_interface_labels=False):
grid_size=0, show_interface_labels=False, variables=None, supplier=None):
self._controller = controller
assert name is not None
@@ -86,6 +86,9 @@ class Project:
self._show_grid = show_grid
self._grid_size = grid_size
self._show_interface_labels = show_interface_labels
self._variables = variables
self._supplier = supplier
self._loading = False
# Disallow overwrite of existing project
@@ -134,6 +137,15 @@ class Project:
self.controller.notification.emit("project.updated", self.__json__())
self.dump()
# update on computes
for compute in list(self._project_created_on_compute):
yield from compute.put(
"/projects/{}".format(self._id), {
"variables": self.variables
}
)
def reset(self):
"""
Called when open/close a project. Cleanup internal stuff
@@ -267,6 +279,36 @@ class Project:
"""
self._show_interface_labels = show_interface_labels
@property
def variables(self):
"""
Variables applied to the project
:return: list
"""
return self._variables
@variables.setter
def variables(self, variables):
"""
Setter for variables applied to the project
"""
self._variables = variables
@property
def supplier(self):
"""
Supplier of the project
:return: dict
"""
return self._supplier
@supplier.setter
def supplier(self, supplier):
"""
Setter for supplier of the project
"""
self._supplier = supplier
@property
def auto_start(self):
"""
@@ -461,12 +503,14 @@ class Project:
yield from compute.post("/projects", data={
"name": self._name,
"project_id": self._id,
"path": self._path
"path": self._path,
"variables": self._variables
})
else:
yield from compute.post("/projects", data={
"name": self._name,
"project_id": self._id,
"variables": self._variables
})
self._project_created_on_compute.add(compute)
@@ -676,6 +720,15 @@ class Project:
except KeyError:
pass
# don't remove supplier's logo
if self.supplier:
try:
logo = self.supplier['logo']
pictures.remove(logo)
except KeyError:
pass
for pict in pictures:
os.remove(os.path.join(self.pictures_directory, pict))
except OSError as e:
@@ -1004,7 +1057,9 @@ class Project:
"snap_to_grid": self._snap_to_grid,
"show_grid": self._show_grid,
"grid_size": self._grid_size,
"show_interface_labels": self._show_interface_labels
"show_interface_labels": self._show_interface_labels,
"supplier": self._supplier,
"variables": self._variables
}
def __repr__(self):
+2
View File
@@ -85,6 +85,8 @@ def project_to_topology(project):
"show_grid": project.show_grid,
"grid_size": project.grid_size,
"show_interface_labels": project.show_interface_labels,
"variables": project.variables,
"supplier": project.supplier,
"topology": {
"nodes": [],
"links": [],
@@ -60,7 +60,8 @@ class DockerHandler:
console_resolution=request.json.get("console_resolution", "1024x768"),
console_http_port=request.json.get("console_http_port", 80),
console_http_path=request.json.get("console_http_path", "/"),
aux=request.json.get("aux"))
aux=request.json.get("aux"),
extra_hosts=request.json.get("extra_hosts"))
for name, value in request.json.items():
if name != "node_id":
if hasattr(container, name) and getattr(container, name) != value:
@@ -312,7 +313,7 @@ class DockerHandler:
props = [
"name", "console", "aux", "console_type", "console_resolution",
"console_http_port", "console_http_path", "start_command",
"environment", "adapters"
"environment", "adapters", "extra_hosts"
]
changed = False
@@ -73,11 +73,31 @@ class ProjectHandler:
p = pm.create_project(
name=request.json.get("name"),
path=request.json.get("path"),
project_id=request.json.get("project_id")
project_id=request.json.get("project_id"),
variables=request.json.get("variables", None)
)
response.set_status(201)
response.json(p)
@Route.put(
r"/projects/{project_id}",
description="Update the project on the server",
status_codes={
201: "Project updated",
403: "Forbidden to update a project"
},
output=PROJECT_OBJECT_SCHEMA,
input=PROJECT_UPDATE_SCHEMA)
def update_project(request, response):
pm = ProjectManager.instance()
project = pm.get_project(request.match_info["project_id"])
yield from project.update(
variables=request.json.get("variables", None)
)
response.set_status(200)
response.json(project)
@Route.get(
r"/projects/{project_id}",
description="Get project information",
+10
View File
@@ -87,6 +87,11 @@ DOCKER_CREATE_SCHEMA = {
"type": ["string", "null"],
"minLength": 0,
},
"extra_hosts": {
"description": "Docker extra hosts (added to /etc/hosts)",
"type": ["string", "null"],
"minLength": 0,
},
"container_id": {
"description": "Docker container ID Read only",
"type": "string",
@@ -184,6 +189,11 @@ DOCKER_OBJECT_SCHEMA = {
"type": ["string", "null"],
"minLength": 0,
},
"extra_hosts": {
"description": "Docker extra hosts (added to /etc/hosts)",
"type": ["string", "null"],
"minLength": 0,
},
"node_directory": {
"description": "Path to the node working directory Read only",
"type": "string"
+43 -3
View File
@@ -15,6 +15,40 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
SUPPLIER_OBJECT_SCHEMA = {
"type": ["object", "null"],
"description": "Supplier of the project",
"properties": {
"logo": {
"type": "string",
"description": "Path to the project supplier logo"
},
"url": {
"type": "string",
"description": "URL to the project supplier site"
}
}
}
VARIABLES_OBJECT_SCHEMA = {
"type": ["array", "null"],
"description": "Variables required to run the project",
"items": {
"properties": {
"name": {
"type": "string",
"description": "Variable name"
},
"value": {
"type": "string",
"description": "Variable value"
}
},
"required": ["name"]
}
}
PROJECT_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
@@ -73,7 +107,9 @@ PROJECT_CREATE_SCHEMA = {
"show_interface_labels": {
"type": "boolean",
"description": "Show interface labels on the drawing area"
}
},
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA
},
"additionalProperties": False,
"required": ["name"]
@@ -136,7 +172,9 @@ PROJECT_UPDATE_SCHEMA = {
"show_interface_labels": {
"type": "boolean",
"description": "Show interface labels on the drawing area"
}
},
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA
},
"additionalProperties": False,
}
@@ -215,7 +253,9 @@ PROJECT_OBJECT_SCHEMA = {
"show_interface_labels": {
"type": "boolean",
"description": "Show interface labels on the drawing area"
}
},
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA
},
"additionalProperties": False,
"required": ["project_id"]
+4
View File
@@ -23,6 +23,8 @@ from gns3server.schemas.compute import COMPUTE_OBJECT_SCHEMA
from gns3server.schemas.drawing import DRAWING_OBJECT_SCHEMA
from gns3server.schemas.link import LINK_OBJECT_SCHEMA
from gns3server.schemas.node import NODE_OBJECT_SCHEMA
from gns3server.schemas.project import VARIABLES_OBJECT_SCHEMA
from gns3server.schemas.project import SUPPLIER_OBJECT_SCHEMA
TOPOLOGY_SCHEMA = {
@@ -97,6 +99,8 @@ TOPOLOGY_SCHEMA = {
"type": "boolean",
"description": "Show interface labels on the drawing area"
},
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA,
"topology": {
"description": "The topology content",
"type": "object",