From 3497deaa31ab4d29b579a89d0fa7186d2e529108 Mon Sep 17 00:00:00 2001
From: grossmj <grossmj@gns3.net>
Date: Mon, 19 Nov 2018 15:53:43 +0700
Subject: [PATCH] Allow virtual machines to use files in project directory as
 disk images.

---
 gns3server/compute/base_manager.py          | 30 ++++++++++++++-------
 gns3server/compute/dynamips/nodes/router.py |  5 ++--
 gns3server/compute/iou/iou_vm.py            |  7 +++--
 gns3server/compute/qemu/qemu_vm.py          | 30 ++++++++++-----------
 tests/handlers/api/compute/test_qemu.py     | 11 ++++++++
 5 files changed, 52 insertions(+), 31 deletions(-)

diff --git a/gns3server/compute/base_manager.py b/gns3server/compute/base_manager.py
index 5e9a0d50..cccab216 100644
--- a/gns3server/compute/base_manager.py
+++ b/gns3server/compute/base_manager.py
@@ -469,12 +469,14 @@ class BaseManager:
         except PermissionError:
             raise aiohttp.web.HTTPForbidden()
 
-    def get_abs_image_path(self, path):
+    def get_abs_image_path(self, path, extra_dir=None):
         """
         Get the absolute path of an image
 
         :param path: file path
-        :return: file path
+        :param extra_dir: an additional directory to be added to the search path
+
+        :returns: file path
         """
 
         if not path:
@@ -483,6 +485,9 @@ class BaseManager:
 
         server_config = self.config.get_section_config("Server")
         img_directory = self.get_images_directory()
+        valid_directory_prefices = images_directories(self._NODE_TYPE)
+        if extra_dir:
+            valid_directory_prefices.append(extra_dir)
 
         # Windows path should not be send to a unix server
         if not sys.platform.startswith("win"):
@@ -490,7 +495,7 @@ class BaseManager:
                 raise NodeError("{} is not allowed on this remote server. Please use only a filename in {}.".format(path, img_directory))
 
         if not os.path.isabs(path):
-            for directory in images_directories(self._NODE_TYPE):
+            for directory in valid_directory_prefices:
                 path = self._recursive_search_file_in_directory(directory, orig_path)
                 if path:
                     return force_unix_path(path)
@@ -502,15 +507,16 @@ class BaseManager:
                 return path
             raise ImageMissingError(orig_path)
 
-        # For non local server we disallow using absolute path outside image directory
+        # For local server we allow using absolute path outside image directory
         if server_config.getboolean("local", False) is True:
             path = force_unix_path(path)
             if os.path.exists(path):
                 return path
             raise ImageMissingError(orig_path)
 
+        # Check to see if path is an absolute path to a valid directory
         path = force_unix_path(path)
-        for directory in images_directories(self._NODE_TYPE):
+        for directory in valid_directory_prefices:
             if os.path.commonprefix([directory, path]) == directory:
                 if os.path.exists(path):
                     return path
@@ -534,23 +540,29 @@ class BaseManager:
                         return path
         return None
 
-    def get_relative_image_path(self, path):
+    def get_relative_image_path(self, path, extra_dir=None):
         """
         Get a path relative to images directory path
         or an abspath if the path is not located inside
         image directory
 
         :param path: file path
-        :return: file path
+        :param extra_dir: an additional directory to be added to the search path
+
+        :returns: file path
         """
 
         if not path:
             return ""
-        path = force_unix_path(self.get_abs_image_path(path))
 
+        path = force_unix_path(self.get_abs_image_path(path, extra_dir))
         img_directory = self.get_images_directory()
 
-        for directory in images_directories(self._NODE_TYPE):
+        valid_directory_prefices = images_directories(self._NODE_TYPE)
+        if extra_dir:
+            valid_directory_prefices.append(extra_dir)
+
+        for directory in valid_directory_prefices:
             if os.path.commonprefix([directory, path]) == directory:
                 relpath = os.path.relpath(path, directory)
                 # We don't allow to recurse search from the top image directory just for image type directory (compatibility with old releases)
diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py
index 19aa36fe..eb4be4dd 100644
--- a/gns3server/compute/dynamips/nodes/router.py
+++ b/gns3server/compute/dynamips/nodes/router.py
@@ -169,8 +169,7 @@ class Router(BaseNode):
                        "mac_addr": self._mac_addr,
                        "system_id": self._system_id}
 
-        # return the relative path if the IOS image is in the images_path directory
-        router_info["image"] = self.manager.get_relative_image_path(self._image)
+        router_info["image"] = self.manager.get_relative_image_path(self._image, self.project.path)
 
         # add the slots
         slot_number = 0
@@ -484,7 +483,7 @@ class Router(BaseNode):
         :param image: path to IOS image file
         """
 
-        image = self.manager.get_abs_image_path(image)
+        image = self.manager.get_abs_image_path(image, self.project.path)
 
         await self._hypervisor.send('vm set_ios "{name}" "{image}"'.format(name=self._name, image=image))
 
diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py
index 170d3560..ad5096f4 100644
--- a/gns3server/compute/iou/iou_vm.py
+++ b/gns3server/compute/iou/iou_vm.py
@@ -75,7 +75,7 @@ class IOUVM(BaseNode):
         self._iou_stdout_file = ""
         self._started = False
         self._nvram_watcher = None
-        self._path = self.manager.get_abs_image_path(path)
+        self._path = self.manager.get_abs_image_path(path, project.path)
         self._license_check = True
 
         # IOU settings
@@ -137,7 +137,7 @@ class IOUVM(BaseNode):
         :param path: path to the IOU image executable
         """
 
-        self._path = self.manager.get_abs_image_path(path)
+        self._path = self.manager.get_abs_image_path(path, self.project.path)
         log.info('IOU "{name}" [{id}]: IOU image updated to "{path}"'.format(name=self._name, id=self._id, path=self._path))
 
     @property
@@ -232,8 +232,7 @@ class IOUVM(BaseNode):
                        "command_line": self.command_line,
                        "application_id": self.application_id}
 
-        # return the relative path if the IOU image is in the images_path directory
-        iou_vm_info["path"] = self.manager.get_relative_image_path(self.path)
+        iou_vm_info["path"] = self.manager.get_relative_image_path(self.path, self.project.path)
         return iou_vm_info
 
     @property
diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py
index 081df9e0..fe122230 100644
--- a/gns3server/compute/qemu/qemu_vm.py
+++ b/gns3server/compute/qemu/qemu_vm.py
@@ -212,7 +212,7 @@ class QemuVM(BaseNode):
         :param value: New disk value
         """
 
-        value = self.manager.get_abs_image_path(value)
+        value = self.manager.get_abs_image_path(value, self.project.path)
         if not self.linked_clone:
             for node in self.manager.nodes:
                 if node != self and getattr(node, variable) == value:
@@ -412,7 +412,8 @@ class QemuVM(BaseNode):
 
         :param cdrom_image: QEMU cdrom image path
         """
-        self._cdrom_image = self.manager.get_abs_image_path(cdrom_image)
+
+        self._cdrom_image = self.manager.get_abs_image_path(cdrom_image, self.project.path)
         log.info('QEMU VM "{name}" [{id}] has set the QEMU cdrom image path to {cdrom_image}'.format(name=self._name,
                                                                                                      id=self._id,
                                                                                                      cdrom_image=self._cdrom_image))
@@ -434,7 +435,8 @@ class QemuVM(BaseNode):
 
         :param bios_image: QEMU bios image path
         """
-        self._bios_image = self.manager.get_abs_image_path(bios_image)
+
+        self._bios_image = self.manager.get_abs_image_path(bios_image, self.project.path)
         log.info('QEMU VM "{name}" [{id}] has set the QEMU bios image path to {bios_image}'.format(name=self._name,
                                                                                                    id=self._id,
                                                                                                    bios_image=self._bios_image))
@@ -739,7 +741,7 @@ class QemuVM(BaseNode):
         :param initrd: QEMU initrd path
         """
 
-        initrd = self.manager.get_abs_image_path(initrd)
+        initrd = self.manager.get_abs_image_path(initrd, self.project.path)
 
         log.info('QEMU VM "{name}" [{id}] has set the QEMU initrd path to {initrd}'.format(name=self._name,
                                                                                            id=self._id,
@@ -766,7 +768,7 @@ class QemuVM(BaseNode):
         :param kernel_image: QEMU kernel image path
         """
 
-        kernel_image = self.manager.get_abs_image_path(kernel_image)
+        kernel_image = self.manager.get_abs_image_path(kernel_image, self.project.path)
         log.info('QEMU VM "{name}" [{id}] has set the QEMU kernel image path to {kernel_image}'.format(name=self._name,
                                                                                                        id=self._id,
                                                                                                        kernel_image=kernel_image))
@@ -1938,22 +1940,20 @@ class QemuVM(BaseNode):
                     answer[field] = getattr(self, field)
                 except AttributeError:
                     pass
-        answer["hda_disk_image"] = self.manager.get_relative_image_path(self._hda_disk_image)
+        answer["hda_disk_image"] = self.manager.get_relative_image_path(self._hda_disk_image, self.project.path)
         answer["hda_disk_image_md5sum"] = md5sum(self._hda_disk_image)
-        answer["hdb_disk_image"] = self.manager.get_relative_image_path(self._hdb_disk_image)
+        answer["hdb_disk_image"] = self.manager.get_relative_image_path(self._hdb_disk_image, self.project.path)
         answer["hdb_disk_image_md5sum"] = md5sum(self._hdb_disk_image)
-        answer["hdc_disk_image"] = self.manager.get_relative_image_path(self._hdc_disk_image)
+        answer["hdc_disk_image"] = self.manager.get_relative_image_path(self._hdc_disk_image, self.project.path)
         answer["hdc_disk_image_md5sum"] = md5sum(self._hdc_disk_image)
-        answer["hdd_disk_image"] = self.manager.get_relative_image_path(self._hdd_disk_image)
+        answer["hdd_disk_image"] = self.manager.get_relative_image_path(self._hdd_disk_image, self.project.path)
         answer["hdd_disk_image_md5sum"] = md5sum(self._hdd_disk_image)
-        answer["cdrom_image"] = self.manager.get_relative_image_path(self._cdrom_image)
+        answer["cdrom_image"] = self.manager.get_relative_image_path(self._cdrom_image, self.project.path)
         answer["cdrom_image_md5sum"] = md5sum(self._cdrom_image)
-        answer["bios_image"] = self.manager.get_relative_image_path(self._bios_image)
+        answer["bios_image"] = self.manager.get_relative_image_path(self._bios_image, self.project.path)
         answer["bios_image_md5sum"] = md5sum(self._bios_image)
-        answer["initrd"] = self.manager.get_relative_image_path(self._initrd)
+        answer["initrd"] = self.manager.get_relative_image_path(self._initrd, self.project.path)
         answer["initrd_md5sum"] = md5sum(self._initrd)
-
-        answer["kernel_image"] = self.manager.get_relative_image_path(self._kernel_image)
+        answer["kernel_image"] = self.manager.get_relative_image_path(self._kernel_image, self.project.path)
         answer["kernel_image_md5sum"] = md5sum(self._kernel_image)
-
         return answer
diff --git a/tests/handlers/api/compute/test_qemu.py b/tests/handlers/api/compute/test_qemu.py
index 7c63edcd..a7636ee1 100644
--- a/tests/handlers/api/compute/test_qemu.py
+++ b/tests/handlers/api/compute/test_qemu.py
@@ -101,6 +101,17 @@ def test_qemu_create_with_params(http_compute, project, base_params, fake_qemu_v
     assert response.json["hda_disk_image_md5sum"] == "c4ca4238a0b923820dcc509a6f75849b"
 
 
+def test_qemu_create_with_project_file(http_compute, project, base_params, fake_qemu_vm):
+    response = http_compute.post("/projects/{project_id}/files/hello.img".format(project_id=project.id), body="world", raw=True)
+    assert response.status == 200
+    params = base_params
+    params["hda_disk_image"] = "hello.img"
+    response = http_compute.post("/projects/{project_id}/qemu/nodes".format(project_id=project.id), params, example=True)
+    assert response.status == 201
+    assert response.json["hda_disk_image"] == "hello.img"
+    assert response.json["hda_disk_image_md5sum"] == "7d793037a0760186574b0282f2f435e7"
+
+
 def test_qemu_get(http_compute, project, vm):
     response = http_compute.get("/projects/{project_id}/qemu/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True)
     assert response.status == 200