Merge pull request #1137 from GNS3/duplicate

API for duplication a Node
This commit is contained in:
Jeremy Grossmann authored and GitHub committed 2017-07-24 01:52:34 -07:00
commit 34c27bc18a
18 files changed
+382 -32

No files matched your search

+31
View File
@@ -256,6 +256,37 @@ class BaseManager:
project.add_node(node)
return node
@asyncio.coroutine
def duplicate_node(self, source_node_id, destination_node_id):
"""
Duplicate a node
:param source_node_id: Source node identifier
:param destination_node_id: Destination node identifier
:returns: New node instance
"""
source_node = self.get_node(source_node_id)
destination_node = self.get_node(destination_node_id)
# Some node don't have working dir like switch
if not hasattr(destination_node, "working_dir"):
return destination_node
destination_dir = destination_node.working_dir
try:
shutil.rmtree(destination_dir)
shutil.copytree(source_node.working_dir, destination_dir)
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Can't duplicate node data: {}".format(e))
# We force a refresh of the name. This force the rewrite
# of some configuration files
node_name = destination_node.name
destination_node.name = node_name + str(uuid4())
destination_node.name = node_name
return destination_node
@asyncio.coroutine
def close_node(self, node_id):
"""
-1
View File
@@ -34,7 +34,6 @@ from ..ubridge.hypervisor import Hypervisor
from ..ubridge.ubridge_error import UbridgeError
from .nios.nio_udp import NIOUDP
from .error import NodeError
from ..config import Config
log = logging.getLogger(__name__)
+39 -6
View File
@@ -511,16 +511,12 @@ class Dynamips(BaseManager):
:param settings: VM settings
"""
module_workdir = vm.project.module_working_directory(self.module_name.lower())
default_startup_config_path = os.path.join(module_workdir, vm.id, "configs", "i{}_startup-config.cfg".format(vm.dynamips_id))
default_private_config_path = os.path.join(module_workdir, vm.id, "configs", "i{}_private-config.cfg".format(vm.dynamips_id))
startup_config_content = settings.get("startup_config_content")
if startup_config_content:
self._create_config(vm, default_startup_config_path, startup_config_content)
self._create_config(vm, vm.startup_config_path, startup_config_content)
private_config_content = settings.get("private_config_content")
if private_config_content:
self._create_config(vm, default_private_config_path, private_config_content)
self._create_config(vm, vm.private_config_path, private_config_content)
def _create_config(self, vm, path, content=None):
"""
@@ -605,3 +601,40 @@ class Dynamips(BaseManager):
if was_auto_started:
yield from vm.stop()
return validated_idlepc
@asyncio.coroutine
def duplicate_node(self, source_node_id, destination_node_id):
"""
Duplicate a node
:param node_id: Node identifier
:returns: New node instance
"""
source_node = self.get_node(source_node_id)
destination_node = self.get_node(destination_node_id)
# Not a Dynamips router
if not hasattr(source_node, "startup_config_path"):
return (yield from super().duplicate_node(source_node_id, destination_node_id))
try:
with open(source_node.startup_config_path) as f:
startup_config = f.read()
except OSError:
startup_config = None
try:
with open(source_node.private_config_path) as f:
private_config = f.read()
except OSError:
private_config = None
yield from self.set_vm_configs(destination_node, {
"startup_config_content": startup_config,
"private_config_content": private_config
})
# Force refresh of the name in configuration files
new_name = destination_node.name
yield from destination_node.set_name(source_node.name)
yield from destination_node.set_name(new_name)
return destination_node
+22 -10
View File
@@ -1474,6 +1474,20 @@ class Router(BaseNode):
return self._slots
@property
def startup_config_path(self):
"""
:returns: Path of the startup config
"""
return os.path.join(self._working_directory, "configs", "i{}_startup-config.cfg".format(self._dynamips_id))
@property
def private_config_path(self):
"""
:returns: Path of the private config
"""
return os.path.join(self._working_directory, "configs", "i{}_private-config.cfg".format(self._dynamips_id))
@asyncio.coroutine
def set_name(self, new_name):
"""
@@ -1483,28 +1497,26 @@ class Router(BaseNode):
"""
# change the hostname in the startup-config
startup_config_path = os.path.join(self._working_directory, "configs", "i{}_startup-config.cfg".format(self._dynamips_id))
if os.path.isfile(startup_config_path):
if os.path.isfile(self.startup_config_path):
try:
with open(startup_config_path, "r+", encoding="utf-8", errors="replace") as f:
with open(self.startup_config_path, "r+", encoding="utf-8", errors="replace") as f:
old_config = f.read()
new_config = re.sub(r"^hostname .+$", "hostname " + new_name, old_config, flags=re.MULTILINE)
f.seek(0)
f.write(new_config)
except OSError as e:
raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e))
raise DynamipsError("Could not amend the configuration {}: {}".format(self.startup_config_path, e))
# change the hostname in the private-config
private_config_path = os.path.join(self._working_directory, "configs", "i{}_private-config.cfg".format(self._dynamips_id))
if os.path.isfile(private_config_path):
if os.path.isfile(self.private_config_path):
try:
with open(private_config_path, "r+", encoding="utf-8", errors="replace") as f:
with open(self.private_config_path, "r+", encoding="utf-8", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self.name, new_name)
f.seek(0)
f.write(new_config)
except OSError as e:
raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e))
raise DynamipsError("Could not amend the configuration {}: {}".format(self.private_config_path, e))
yield from self._hypervisor.send('vm rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name))
log.info('Router "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name, id=self._id, new_name=new_name))
@@ -1543,7 +1555,7 @@ class Router(BaseNode):
startup_config_base64, private_config_base64 = yield from self.extract_config()
if startup_config_base64:
startup_config = os.path.join("configs", "i{}_startup-config.cfg".format(self._dynamips_id))
startup_config = self.startup_config_path
try:
config = base64.b64decode(startup_config_base64).decode("utf-8", errors="replace")
config = "!\n" + config.replace("\r", "")
@@ -1555,7 +1567,7 @@ class Router(BaseNode):
raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e))
if private_config_base64 and base64.b64decode(private_config_base64) != b'\nkerberos password \nend\n':
private_config = os.path.join("configs", "i{}_private-config.cfg".format(self._dynamips_id))
private_config = self.private_config_path
try:
config = base64.b64decode(private_config_base64).decode("utf-8", errors="replace")
config_path = os.path.join(self._working_directory, private_config)
+1 -1
View File
@@ -173,7 +173,7 @@ class VPCSVM(BaseNode):
if self.script_file:
content = self.startup_script
content = content.replace(self._name, new_name)
escaped_name = re.escape(new_name)
escaped_name = new_name.replace('\\', '')
content = re.sub(r"^set pcname .+$", "set pcname " + escaped_name, content, flags=re.MULTILINE)
self.startup_script = content
+10
View File
@@ -106,6 +106,16 @@ class Node:
if self._symbol is None:
self.symbol = ":/symbols/computer.svg"
def is_always_running(self):
"""
:returns: Boolean True if the node is always running
like ethernet switch
"""
return self.node_type not in (
"qemu", "docker", "dynamips",
"vpcs", "vmware", "virtualbox",
"iou")
@property
def id(self):
return self._id
+52 -1
View File
@@ -19,6 +19,7 @@ import re
import os
import json
import uuid
import copy
import shutil
import asyncio
import aiohttp
@@ -367,6 +368,8 @@ class Project:
if base_name is None:
return None
base_name = re.sub(r"[ ]", "", base_name)
base_name = re.sub(r"[0-9]+$", "{0}", base_name)
if '{0}' in base_name or '{id}' in base_name:
# base name is a template, replace {0} or {id} by an unique identifier
for number in range(1, 1000000):
@@ -834,7 +837,7 @@ class Project:
"""
for node in self._nodes.values():
# Some node type are always running we ignore them
if node.status != "stopped" and node.node_type in ("qemu", "docker", "dynamips", "vpcs", "vmware", "virtualbox", "iou"):
if node.status != "stopped" and not node.is_always_running():
return True
return False
@@ -882,6 +885,54 @@ class Project:
pool.append(node.suspend)
yield from pool.join()
@asyncio.coroutine
def duplicate_node(self, node, x, y, z):
"""
Duplicate a node
:param node: Node instance
:param x: X position
:param y: Y position
:param z: Z position
:returns: New node
"""
if node.status != "stopped" and not node.is_always_running():
raise aiohttp.web.HTTPConflict(text="Cannot duplicate node data while the node is running")
data = copy.deepcopy(node.__json__(topology_dump=True))
# Some properties like internal ID should not be duplicate
for unique_property in (
'node_id',
'name',
'compute_id',
'application_id',
'dynamips_id'):
data.pop(unique_property, None)
if 'properties' in data:
data['properties'].pop(unique_property, None)
node_type = data.pop('node_type')
data['x'] = x
data['y'] = y
data['z'] = z
new_node_uuid = str(uuid.uuid4())
new_node = yield from self.add_node(
node.compute,
node.name,
new_node_uuid,
node_type=node_type,
**data)
try:
yield from node.post("/duplicate", timeout=None, data={
"destination_node_id": new_node_uuid
})
except aiohttp.web.HTTPNotFound as e:
yield from self.delete_node(new_node_uuid)
raise aiohttp.web.HTTPConflict(text="This node type cannot be duplicated")
except aiohttp.web.HTTPConflict as e:
yield from self.delete_node(new_node_uuid)
raise e
return new_node
def __json__(self):
return {
"name": self._name,
@@ -447,3 +447,23 @@ class DynamipsVMHandler:
dynamips_manager = Dynamips.instance()
yield from dynamips_manager.write_image(request.match_info["filename"], request.content)
response.set_status(204)
@Route.post(
r"/projects/{project_id}/dynamips/nodes/{node_id}/duplicate",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
201: "Instance duplicated",
404: "Instance doesn't exist"
},
description="Duplicate a dynamips instance")
def duplicate(request, response):
new_node = yield from Dynamips.instance().duplicate_node(
request.match_info["node_id"],
request.json["destination_node_id"]
)
response.set_status(201)
response.json(new_node)
@@ -20,7 +20,6 @@ import os
from gns3server.web.route import Route
from gns3server.schemas.node import NODE_CAPTURE_SCHEMA
from gns3server.schemas.nio import NIO_SCHEMA
from gns3server.compute.builtin import Builtin
from gns3server.compute.dynamips import Dynamips
from gns3server.schemas.ethernet_switch import (
@@ -91,6 +90,26 @@ class EthernetSwitchHandler:
# node = builtin_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
response.json(node)
@Route.post(
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}/duplicate",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
201: "Instance duplicated",
404: "Instance doesn't exist"
},
description="Duplicate an ethernet switch instance")
def duplicate(request, response):
new_node = yield from Dynamips.instance().duplicate_node(
request.match_info["node_id"],
request.json["destination_node_id"]
)
response.set_status(201)
response.json(new_node)
@Route.put(
r"/projects/{project_id}/ethernet_switch/nodes/{node_id}",
parameters={
@@ -30,7 +30,6 @@ from gns3server.schemas.vpcs import (
class VPCSHandler:
"""
API entry points for VPCS.
"""
@@ -119,6 +118,26 @@ class VPCSHandler:
yield from VPCS.instance().delete_node(request.match_info["node_id"])
response.set_status(204)
@Route.post(
r"/projects/{project_id}/vpcs/nodes/{node_id}/duplicate",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
201: "Instance duplicated",
404: "Instance doesn't exist"
},
description="Duplicate a VPCS instance")
def duplicate(request, response):
new_node = yield from VPCS.instance().duplicate_node(
request.match_info["node_id"],
request.json["destination_node_id"]
)
response.set_status(201)
response.json(new_node)
@Route.post(
r"/projects/{project_id}/vpcs/nodes/{node_id}/start",
parameters={
@@ -25,7 +25,8 @@ from gns3server.utils import force_unix_path
from gns3server.schemas.node import (
NODE_OBJECT_SCHEMA,
NODE_UPDATE_SCHEMA,
NODE_CREATE_SCHEMA
NODE_CREATE_SCHEMA,
NODE_DUPLICATE_SCHEMA
)
@@ -180,6 +181,32 @@ class NodeHandler:
yield from project.start_all()
response.set_status(204)
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/duplicate",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
201: "Instance duplicated",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Duplicate a node instance",
input=NODE_DUPLICATE_SCHEMA,
output=NODE_OBJECT_SCHEMA)
def duplicate(request, response):
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
new_node = yield from project.duplicate_node(
node,
request.json["x"],
request.json["y"],
request.json.get("z", 0))
response.json(new_node)
response.set_status(201)
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/start",
parameters={
+23
View File
@@ -237,3 +237,26 @@ NODE_OBJECT_SCHEMA = {
NODE_CREATE_SCHEMA = NODE_OBJECT_SCHEMA
NODE_UPDATE_SCHEMA = copy.deepcopy(NODE_OBJECT_SCHEMA)
del NODE_UPDATE_SCHEMA["required"]
NODE_DUPLICATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Duplicate a node",
"type": "object",
"properties": {
"x": {
"description": "X position of the node",
"type": "integer"
},
"y": {
"description": "Y position of the node",
"type": "integer"
},
"z": {
"description": "Z position of the node",
"type": "integer"
}
},
"additionalProperties": False,
"required": ["x", "y"]
}