From 7e40eb02e6c042717f7ec5d012bbfba0ee5a4d21 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 27 Jul 2016 18:31:02 +0200 Subject: [PATCH] API for editing a file on a Node --- gns3server/controller/compute.py | 22 +++--- .../handlers/api/controller/node_handler.py | 75 +++++++++++++++++++ tests/handlers/api/controller/test_node.py | 25 +++++++ 3 files changed, 113 insertions(+), 9 deletions(-) diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index 14be2908..f175ab35 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -358,7 +358,7 @@ class Compute: return "{}://{}:{}/v2/compute{}".format(self._protocol, self._host, self._port, path) @asyncio.coroutine - def _run_http_query(self, method, path, data=None, timeout=10): + def _run_http_query(self, method, path, data=None, timeout=10, raw=False): with Timeout(timeout): url = self._getUrl(path) headers = {} @@ -370,20 +370,20 @@ class Compute: if hasattr(data, '__json__'): data = json.dumps(data.__json__()) # Stream the request - elif isinstance(data, aiohttp.streams.StreamReader) or isinstance(data, io.BufferedIOBase): + elif isinstance(data, aiohttp.streams.StreamReader) or isinstance(data, io.BufferedIOBase) or isinstance(data, bytes): chunked = True headers['content-type'] = 'application/octet-stream' else: data = json.dumps(data) - response = yield from self._session().request(method, url, headers=headers, data=data, auth=self._auth, chunked=chunked) + response = yield from self._session().request(method, url, headers=headers, data=data, auth=self._auth, chunked=chunked) body = yield from response.read() - if body: + if body and not raw: body = body.decode() if response.status >= 300: # Try to decode the GNS3 error - if body: + if body and not raw: try: msg = json.loads(body)["message"] except (KeyError, ValueError): @@ -412,12 +412,16 @@ class Compute: else: raise NotImplementedError("{} status code is not supported".format(response.status)) if body and len(body): - try: - response.json = json.loads(body) - except ValueError: - raise aiohttp.web.HTTPConflict(text="The server {} is not a GNS3 server".format(self._id)) + if raw: + response.body = body + else: + try: + response.json = json.loads(body) + except ValueError: + raise aiohttp.web.HTTPConflict(text="The server {} is not a GNS3 server".format(self._id)) else: response.json = {} + response.body = b"" return response @asyncio.coroutine diff --git a/gns3server/handlers/api/controller/node_handler.py b/gns3server/handlers/api/controller/node_handler.py index 079b6a99..0368dab4 100644 --- a/gns3server/handlers/api/controller/node_handler.py +++ b/gns3server/handlers/api/controller/node_handler.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os import aiohttp from gns3server.web.route import Route @@ -314,3 +315,77 @@ class NodeHandler: idle = yield from node.dynamips_idlepc_proposals() response.json(idle) response.set_status(200) + + @Route.get( + r"/projects/{project_id}/nodes/{node_id}/files/{path:.+}", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a file in the node directory") + def get_file(request, response): + + project = Controller.instance().get_project(request.match_info["project_id"]) + node = project.get_node(request.match_info["node_id"]) + path = request.match_info["path"] + path = os.path.normpath(path) + + # Raise error if user try to escape + if path[0] == ".": + raise aiohttp.web.HTTPForbidden + + node_type = node.node_type + if node_type == "dynamips": + path = "/project-files/{}/{}".format(node_type, path) + else: + path = "/project-files/{}/{}/{}".format(node_type, node.id, path) + + res = yield from node.compute.http_query("GET", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), timeout=None, raw=True) + response.set_status(200) + response.content_type = "application/octet-stream" + response.enable_chunked_encoding() + response.content_length = None + response.start(request) + + response.write(res.body) + yield from response.write_eof() + + @Route.post( + r"/projects/{project_id}/nodes/{node_id}/files/{path:.+}", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + raw=True, + description="Write a file in the node directory") + def post_file(request, response): + + project = Controller.instance().get_project(request.match_info["project_id"]) + node = project.get_node(request.match_info["node_id"]) + path = request.match_info["path"] + path = os.path.normpath(path) + + # Raise error if user try to escape + if path[0] == ".": + raise aiohttp.web.HTTPForbidden + + node_type = node.node_type + if node_type == "dynamips": + path = "/project-files/{}/{}".format(node_type, path) + else: + path = "/project-files/{}/{}/{}".format(node_type, node.id, path) + + data = yield from request.content.read() + + res = yield from node.compute.http_query("POST", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), data=data, timeout=None, raw=True) + response.set_status(201) diff --git a/tests/handlers/api/controller/test_node.py b/tests/handlers/api/controller/test_node.py index 81d54458..759a5ea2 100644 --- a/tests/handlers/api/controller/test_node.py +++ b/tests/handlers/api/controller/test_node.py @@ -199,3 +199,28 @@ def test_dynamips_idlepc_proposals(http_controller, tmpdir, project, compute, no response = http_controller.get("/projects/{}/nodes/{}/dynamips/idlepc_proposals".format(project.id, node.id), example=True) assert response.status == 200 assert response.json == ["0x60606f54", "0x33805a22"] + + +def test_get_file(http_controller, tmpdir, project, node, compute): + response = MagicMock() + response.body = b"world" + compute.http_query = AsyncioMagicMock(return_value=response) + response = http_controller.get("/projects/{project_id}/nodes/{node_id}/files/hello".format(project_id=project.id, node_id=node.id), raw=True) + assert response.status == 200 + assert response.body == b'world' + + compute.http_query.assert_called_with("GET", "/projects/{project_id}/files/project-files/vpcs/{node_id}/hello".format(project_id=project.id, node_id=node.id), timeout=None, raw=True) + + response = http_controller.get("/projects/{project_id}/nodes/{node_id}/files/../hello".format(project_id=project.id, node_id=node.id), raw=True) + assert response.status == 403 + + +def test_post_file(http_controller, tmpdir, project, node, compute): + compute.http_query = AsyncioMagicMock() + response = http_controller.post("/projects/{project_id}/nodes/{node_id}/files/hello".format(project_id=project.id, node_id=node.id), body=b"hello", raw=True) + assert response.status == 201 + + compute.http_query.assert_called_with("POST", "/projects/{project_id}/files/project-files/vpcs/{node_id}/hello".format(project_id=project.id, node_id=node.id), data=b'hello', timeout=None, raw=True) + + response = http_controller.get("/projects/{project_id}/nodes/{node_id}/files/../hello".format(project_id=project.id, node_id=node.id), raw=True) + assert response.status == 403