mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
Merge branch '3.1' into api-issue-05-2xftmd
This commit is contained in:
46 files changed
+2108
-210
No files matched your search
@@ -710,7 +710,9 @@ class Controller:
|
||||
except KeyError:
|
||||
if compute_id == "vm":
|
||||
raise ControllerNotFoundError("Cannot use a node on the GNS3 VM server with the GNS3 VM not configured")
|
||||
raise ControllerNotFoundError(f"Compute ID {compute_id} doesn't exist")
|
||||
raise ControllerNotFoundError(
|
||||
f"Compute ID {compute_id} doesn't exist", code="compute_not_found", details={"compute_id": compute_id}
|
||||
)
|
||||
|
||||
def has_compute(self, compute_id):
|
||||
"""
|
||||
@@ -748,7 +750,9 @@ class Controller:
|
||||
try:
|
||||
return self._projects[project_id]
|
||||
except KeyError:
|
||||
raise ControllerNotFoundError(f"Project ID {project_id} doesn't exist")
|
||||
raise ControllerNotFoundError(
|
||||
f"Project ID {project_id} doesn't exist", code="project_not_found", details={"project_id": project_id}
|
||||
)
|
||||
|
||||
async def get_loaded_project(self, project_id):
|
||||
"""
|
||||
|
||||
@@ -15,11 +15,17 @@
|
||||
# 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 typing import Any
|
||||
|
||||
|
||||
class ControllerError(Exception):
|
||||
def __init__(self, message: str):
|
||||
default_code = "conflict"
|
||||
|
||||
def __init__(self, message: str, code: str | None = None, details: dict[str, Any] | None = None):
|
||||
super().__init__()
|
||||
self._message = message
|
||||
self._code = code or self.default_code
|
||||
self._details = details
|
||||
|
||||
def __repr__(self):
|
||||
return self._message
|
||||
@@ -27,30 +33,33 @@ class ControllerError(Exception):
|
||||
def __str__(self):
|
||||
return self._message
|
||||
|
||||
@property
|
||||
def code(self) -> str:
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def details(self) -> dict[str, Any] | None:
|
||||
return self._details
|
||||
|
||||
|
||||
class ControllerNotFoundError(ControllerError):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
default_code = "not_found"
|
||||
|
||||
|
||||
class ControllerBadRequestError(ControllerError):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
default_code = "bad_request"
|
||||
|
||||
|
||||
class ControllerUnauthorizedError(ControllerError):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
default_code = "unauthorized"
|
||||
|
||||
|
||||
class ControllerForbiddenError(ControllerError):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
default_code = "forbidden"
|
||||
|
||||
|
||||
class ControllerTimeoutError(ControllerError):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
default_code = "timeout"
|
||||
|
||||
|
||||
class ComputeError(ControllerError):
|
||||
@@ -65,8 +74,10 @@ class ComputeConflictError(ComputeError):
|
||||
:param response: compute JSON response
|
||||
"""
|
||||
|
||||
default_code = "compute_conflict"
|
||||
|
||||
def __init__(self, url, response):
|
||||
super().__init__(response["message"])
|
||||
super().__init__(response["message"], code=response.get("code"), details=response.get("details"))
|
||||
self._url = url
|
||||
self._response = response
|
||||
|
||||
|
||||
@@ -274,9 +274,22 @@ class Link:
|
||||
|
||||
port = node.get_port(adapter_number, port_number)
|
||||
if port is None:
|
||||
raise ControllerNotFoundError(f"Port {adapter_number}/{port_number} for {node.name} not found")
|
||||
raise ControllerNotFoundError(
|
||||
f"Port {adapter_number}/{port_number} for {node.name} not found",
|
||||
code="port_not_found",
|
||||
details={"node_id": node.id, "adapter_number": adapter_number, "port_number": port_number},
|
||||
)
|
||||
if port.link is not None:
|
||||
raise ControllerError("Port is already used")
|
||||
raise ControllerError(
|
||||
"Port is already used",
|
||||
code="port_in_use",
|
||||
details={
|
||||
"node_id": node.id,
|
||||
"adapter_number": adapter_number,
|
||||
"port_number": port_number,
|
||||
"link_id": port.link.id,
|
||||
},
|
||||
)
|
||||
|
||||
self._link_type = port.link_type
|
||||
|
||||
@@ -313,17 +326,29 @@ class Link:
|
||||
{"node": node, "adapter_number": adapter_number, "port_number": port_number, "port": port, "label": label}
|
||||
)
|
||||
|
||||
if not batch:
|
||||
port.link = self
|
||||
|
||||
if len(self._nodes) == 2 and not batch:
|
||||
await self.create()
|
||||
try:
|
||||
await self.create()
|
||||
except BaseException:
|
||||
self.release_ports()
|
||||
raise
|
||||
for n in self._nodes:
|
||||
n["node"].add_link(self)
|
||||
n["port"].link = self
|
||||
self._created = True
|
||||
self._project.emit_notification("link.created", self.asdict())
|
||||
|
||||
if dump:
|
||||
self._project.dump()
|
||||
|
||||
def release_ports(self):
|
||||
for n in self._nodes:
|
||||
if n["port"].link == self:
|
||||
n["port"].link = None
|
||||
n["node"].links.discard(self)
|
||||
|
||||
async def update_nodes(self, nodes):
|
||||
for node_data in nodes:
|
||||
node = self._project.get_node(node_data["node_id"])
|
||||
@@ -352,11 +377,7 @@ class Link:
|
||||
"""
|
||||
Delete the link
|
||||
"""
|
||||
for n in self._nodes:
|
||||
# It could be different of self if we rollback an already existing link
|
||||
if n["port"].link == self:
|
||||
n["port"].link = None
|
||||
n["node"].remove_link(self)
|
||||
self.release_ports()
|
||||
|
||||
async def reset(self):
|
||||
"""
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import copy
|
||||
import html
|
||||
import logging
|
||||
@@ -137,6 +138,8 @@ class Node:
|
||||
self._command_line = None
|
||||
self._node_directory = None
|
||||
self._status = "stopped"
|
||||
self._status_changed = asyncio.Event()
|
||||
self._transition = None
|
||||
self._template_id = template_id
|
||||
self._x = 0
|
||||
self._y = 0
|
||||
@@ -788,7 +791,7 @@ class Node:
|
||||
elif key == "command_line":
|
||||
self._command_line = value
|
||||
elif key == "status":
|
||||
self._status = value
|
||||
self._set_status(value)
|
||||
elif key == "console_type":
|
||||
self._console_type = value
|
||||
elif key == "aux_type":
|
||||
@@ -904,6 +907,30 @@ class Node:
|
||||
async def destroy(self):
|
||||
await self.delete()
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def _transitioning(self, transition):
|
||||
"""
|
||||
Report a transitional status while a lifecycle call is in flight
|
||||
"""
|
||||
|
||||
self._transition = transition
|
||||
self.project.emit_notification("node.updated", self.asdict())
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._transition = None
|
||||
|
||||
async def wait_for_status(self, status):
|
||||
"""
|
||||
Wait until the controller side status of the node matches
|
||||
|
||||
:param status: expected status
|
||||
"""
|
||||
|
||||
while self._status != status:
|
||||
self._status_changed.clear()
|
||||
await self._status_changed.wait()
|
||||
|
||||
async def start(self, data=None):
|
||||
"""
|
||||
Start a node
|
||||
@@ -927,32 +954,56 @@ class Node:
|
||||
f"Cannot start node '{self._name}': {len(failed_links)} deferred link(s) "
|
||||
"could not be restored. Please try again."
|
||||
)
|
||||
try:
|
||||
# For IOU: we need to send the licence everytime we start a node
|
||||
if self.node_type == "iou":
|
||||
license_check = self._project.controller.iou_license.get("license_check", True)
|
||||
iourc_content = self._project.controller.iou_license.get("iourc_content", None)
|
||||
await self.post(
|
||||
"/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content}
|
||||
)
|
||||
else:
|
||||
await self.post("/start", data=data, timeout=240)
|
||||
except asyncio.TimeoutError:
|
||||
raise ControllerTimeoutError(f"Timeout when starting {self._name}")
|
||||
async with self._transitioning("starting"):
|
||||
try:
|
||||
# For IOU: we need to send the licence everytime we start a node
|
||||
if self.node_type == "iou":
|
||||
license_check = self._project.controller.iou_license.get("license_check", True)
|
||||
iourc_content = self._project.controller.iou_license.get("iourc_content", None)
|
||||
await self.post(
|
||||
"/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content}
|
||||
)
|
||||
else:
|
||||
await self.post("/start", data=data, timeout=240)
|
||||
except asyncio.TimeoutError:
|
||||
raise ControllerTimeoutError(f"Timeout when starting {self._name}")
|
||||
self._confirm_status("started")
|
||||
|
||||
async def stop(self):
|
||||
async def stop(self, strict=False):
|
||||
"""
|
||||
Stop a node
|
||||
|
||||
:param strict: Raise the errors reported by the compute instead of ignoring them
|
||||
"""
|
||||
if self.missing_image:
|
||||
return
|
||||
try:
|
||||
await self.post("/stop", timeout=240, dont_connect=True)
|
||||
# We don't care if a node is down at this step
|
||||
except (ComputeError, ControllerError):
|
||||
pass
|
||||
except asyncio.TimeoutError:
|
||||
raise ControllerTimeoutError(f"Timeout when stopping {self._name}")
|
||||
stopped = False
|
||||
async with self._transitioning("stopping"):
|
||||
try:
|
||||
await self.post("/stop", timeout=240, dont_connect=True)
|
||||
stopped = True
|
||||
except (ComputeError, ControllerError):
|
||||
# We don't care if a node is down at this step
|
||||
if strict:
|
||||
raise
|
||||
except asyncio.TimeoutError:
|
||||
raise ControllerTimeoutError(f"Timeout when stopping {self._name}")
|
||||
if stopped:
|
||||
self._confirm_status("stopped")
|
||||
|
||||
def _set_status(self, status):
|
||||
self._status = status
|
||||
self._status_changed.set()
|
||||
|
||||
def _confirm_status(self, status):
|
||||
"""
|
||||
Record the state reached once the compute accepted a lifecycle call,
|
||||
without waiting for the asynchronous compute notification
|
||||
"""
|
||||
|
||||
if not self.is_always_running():
|
||||
self._set_status(status)
|
||||
self.project.emit_notification("node.updated", self.asdict())
|
||||
|
||||
async def suspend(self):
|
||||
"""
|
||||
@@ -961,7 +1012,8 @@ class Node:
|
||||
try:
|
||||
await self.post("/suspend", timeout=240)
|
||||
except asyncio.TimeoutError:
|
||||
raise ControllerTimeoutError(f"Timeout when reloading {self._name}")
|
||||
raise ControllerTimeoutError(f"Timeout when suspending {self._name}")
|
||||
self._confirm_status("suspended")
|
||||
|
||||
async def reload(self):
|
||||
"""
|
||||
@@ -971,6 +1023,7 @@ class Node:
|
||||
await self.post("/reload", timeout=240)
|
||||
except asyncio.TimeoutError:
|
||||
raise ControllerTimeoutError(f"Timeout when reloading {self._name}")
|
||||
self._confirm_status("started")
|
||||
|
||||
async def reset_console(self):
|
||||
"""
|
||||
@@ -1145,6 +1198,27 @@ class Node:
|
||||
return port
|
||||
return None
|
||||
|
||||
def get_port_by_name(self, name):
|
||||
"""
|
||||
Return the port with this name or None if the port is not found
|
||||
"""
|
||||
for port in self.ports:
|
||||
if port.name == name:
|
||||
return port
|
||||
return None
|
||||
|
||||
def get_free_port(self, link_type=None):
|
||||
"""
|
||||
Return the lowest free port, optionally restricted to a link type,
|
||||
or None if there is no such port
|
||||
"""
|
||||
free_ports = [
|
||||
port for port in self.ports if port.link is None and (link_type is None or port.link_type == link_type)
|
||||
]
|
||||
if not free_ports:
|
||||
return None
|
||||
return min(free_ports, key=lambda port: (port.adapter_number, port.port_number))
|
||||
|
||||
def _list_ports(self):
|
||||
"""
|
||||
Generate the list of port display in the client
|
||||
@@ -1283,7 +1357,7 @@ class Node:
|
||||
additional_data = {
|
||||
"project_id": self._project.id,
|
||||
"command_line": self._command_line,
|
||||
"status": self._status,
|
||||
"status": self._transition or self._status,
|
||||
"console_host": str(self._compute.console_host),
|
||||
"node_directory": self._node_directory,
|
||||
"ports": [port.asdict() for port in self.ports],
|
||||
|
||||
@@ -41,6 +41,10 @@ class Port:
|
||||
def link(self, val):
|
||||
self._link = val
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def adapter_number(self):
|
||||
return self._adapter_number
|
||||
|
||||
@@ -757,7 +757,9 @@ class Project:
|
||||
try:
|
||||
return self._nodes[node_id]
|
||||
except KeyError:
|
||||
raise ControllerNotFoundError(f"Node ID {node_id} doesn't exist")
|
||||
raise ControllerNotFoundError(
|
||||
f"Node ID {node_id} doesn't exist", code="node_not_found", details={"node_id": node_id}
|
||||
)
|
||||
|
||||
def _get_closed_data(self, section, id_key):
|
||||
"""
|
||||
@@ -826,7 +828,9 @@ class Project:
|
||||
try:
|
||||
return self._drawings[drawing_id]
|
||||
except KeyError:
|
||||
raise ControllerNotFoundError(f"Drawing ID {drawing_id} doesn't exist")
|
||||
raise ControllerNotFoundError(
|
||||
f"Drawing ID {drawing_id} doesn't exist", code="drawing_not_found", details={"drawing_id": drawing_id}
|
||||
)
|
||||
|
||||
@open_required
|
||||
async def delete_drawing(self, drawing_id):
|
||||
@@ -1110,7 +1114,9 @@ class Project:
|
||||
try:
|
||||
return self._links[link_id]
|
||||
except KeyError:
|
||||
raise ControllerNotFoundError(f"Link ID {link_id} doesn't exist")
|
||||
raise ControllerNotFoundError(
|
||||
f"Link ID {link_id} doesn't exist", code="link_not_found", details={"link_id": link_id}
|
||||
)
|
||||
|
||||
@property
|
||||
def links(self):
|
||||
@@ -1471,7 +1477,11 @@ class Project:
|
||||
try:
|
||||
return self._snapshots[snapshot_id]
|
||||
except KeyError:
|
||||
raise ControllerNotFoundError(f"Snapshot ID {snapshot_id} doesn't exist")
|
||||
raise ControllerNotFoundError(
|
||||
f"Snapshot ID {snapshot_id} doesn't exist",
|
||||
code="snapshot_not_found",
|
||||
details={"snapshot_id": snapshot_id},
|
||||
)
|
||||
|
||||
def _load_snapshot_config(self):
|
||||
|
||||
|
||||
Reference in new issue
Block a user