gns3-qemu-config - QEMU config disk
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

199 lines
10.0KB

  1. From 7781d29bb20e305d7acfabd2239fa092bd73fce5 Mon Sep 17 00:00:00 2001
  2. From: Bernhard Ehlers <none@bernhard-ehlers.de>
  3. Date: Mon, 6 Apr 2020 12:56:00 +0200
  4. Subject: [PATCH 1/4] QEMU config disk - initial implementation. Ref #2958
  5. ---
  6. gns3server/compute/qemu/qemu_vm.py | 146 +++++++++++++++++++++++++++++++++----
  7. 1 file changed, 130 insertions(+), 16 deletions(-)
  8. diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py
  9. index 6b0a61fa..88698ed4 100644
  10. --- a/gns3server/compute/qemu/qemu_vm.py
  11. +++ b/gns3server/compute/qemu/qemu_vm.py
  12. @@ -38,6 +38,7 @@ from gns3server.utils.asyncio import subprocess_check_output, cancellable_wait_r
  13. from .qemu_error import QemuError
  14. from .utils.qcow2 import Qcow2, Qcow2Error
  15. from ..adapters.ethernet_adapter import EthernetAdapter
  16. +from ..error import NodeError, ImageMissingError
  17. from ..nios.nio_udp import NIOUDP
  18. from ..nios.nio_tap import NIOTAP
  19. from ..base_node import BaseNode
  20. @@ -123,6 +124,22 @@ class QemuVM(BaseNode):
  21. self.mac_address = "" # this will generate a MAC address
  22. self.adapters = 1 # creates 1 adapter by default
  23. +
  24. + # config disk
  25. + self.config_disk_name = "config.img"
  26. + if not shutil.which("mcopy"):
  27. + log.warning("Config disk: 'mtools' are not installed.")
  28. + self.config_disk_name = ""
  29. + self.config_disk_image = ""
  30. + else:
  31. + try:
  32. + self.config_disk_image = self.manager.get_abs_image_path(
  33. + self.config_disk_name, self.project.path)
  34. + except (NodeError, ImageMissingError) as e:
  35. + log.warning("Config disk: {}".format(e))
  36. + self.config_disk_name = ""
  37. + self.config_disk_image = ""
  38. +
  39. log.info('QEMU VM "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
  40. @property
  41. @@ -1077,6 +1094,7 @@ class QemuVM(BaseNode):
  42. self._stop_cpulimit()
  43. if self.on_close != "save_vm_state":
  44. await self._clear_save_vm_stated()
  45. + await self._export_config()
  46. await super().stop()
  47. async def _open_qemu_monitor_connection_vm(self, timeout=10):
  48. @@ -1578,6 +1596,92 @@ class QemuVM(BaseNode):
  49. log.info("{} returned with {}".format(self._get_qemu_img(), retcode))
  50. return retcode
  51. + async def _mcopy(self, *args):
  52. + env = os.environ
  53. + env["MTOOLSRC"] = 'mtoolsrc'
  54. + try:
  55. + process = await asyncio.create_subprocess_exec("mcopy", *args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.working_dir, env=env)
  56. + (stdout, _) = await process.communicate()
  57. + retcode = process.returncode
  58. + except (OSError, subprocess.SubprocessError) as e:
  59. + log.error("mcopy failure: {}".format(e))
  60. + return 1
  61. + if retcode != 0:
  62. + stdout = stdout.decode("utf-8").rstrip()
  63. + if stdout:
  64. + log.error("mcopy failure: {}".format(stdout))
  65. + else:
  66. + log.error("mcopy failure: return code {}".format(retcode))
  67. + return retcode
  68. +
  69. + async def _export_config(self):
  70. + disk_name = getattr(self, "config_disk_name")
  71. + if not disk_name or \
  72. + not os.path.exists(os.path.join(self.working_dir, disk_name)):
  73. + return
  74. + config_dir = os.path.join(self.working_dir, "configs")
  75. + zip_file = os.path.join(self.working_dir, "config.zip")
  76. + try:
  77. + shutil.rmtree(config_dir, ignore_errors=True)
  78. + os.mkdir(config_dir)
  79. + if os.path.exists(zip_file):
  80. + os.remove(zip_file)
  81. + if await self._mcopy("-s", "-m", "x:/", config_dir) == 0:
  82. + shutil.make_archive(zip_file[:-4], "zip", config_dir)
  83. + except OSError as e:
  84. + log.error("Can't export config: {}".format(e))
  85. + finally:
  86. + shutil.rmtree(config_dir, ignore_errors=True)
  87. +
  88. + async def _import_config(self):
  89. + disk_name = getattr(self, "config_disk_name")
  90. + zip_file = os.path.join(self.working_dir, "config.zip")
  91. + if not disk_name or not os.path.exists(zip_file):
  92. + return
  93. + config_dir = os.path.join(self.working_dir, "configs")
  94. + disk = os.path.join(self.working_dir, disk_name)
  95. + try:
  96. + shutil.rmtree(config_dir, ignore_errors=True)
  97. + os.mkdir(config_dir)
  98. + shutil.unpack_archive(zip_file, config_dir)
  99. + shutil.copyfile(getattr(self, "config_disk_image"), disk)
  100. + config_files = [os.path.join(config_dir, fname)
  101. + for fname in os.listdir(config_dir)]
  102. + if config_files:
  103. + if await self._mcopy("-s", "-m", *config_files, "x:/") != 0:
  104. + os.remove(disk)
  105. + os.remove(zip_file)
  106. + except OSError as e:
  107. + log.error("Can't import config: {}".format(e))
  108. + os.remove(zip_file)
  109. + finally:
  110. + shutil.rmtree(config_dir, ignore_errors=True)
  111. +
  112. + def _disk_interface_options(self, disk, disk_index, interface, format=None):
  113. + options = []
  114. + extra_drive_options = ""
  115. + if format:
  116. + extra_drive_options += ",format={}".format(format)
  117. +
  118. + if interface == "sata":
  119. + # special case, sata controller doesn't exist in Qemu
  120. + options.extend(["-device", 'ahci,id=ahci{}'.format(disk_index)])
  121. + options.extend(["-drive", 'file={},if=none,id=drive{},index={},media=disk{}'.format(disk, disk_index, disk_index, extra_drive_options)])
  122. + options.extend(["-device", 'ide-drive,drive=drive{},bus=ahci{}.0,id=drive{}'.format(disk_index, disk_index, disk_index)])
  123. + elif interface == "nvme":
  124. + options.extend(["-drive", 'file={},if=none,id=drive{},index={},media=disk{}'.format(disk, disk_index, disk_index, extra_drive_options)])
  125. + options.extend(["-device", 'nvme,drive=drive{},serial={}'.format(disk_index, disk_index)])
  126. + elif interface == "scsi":
  127. + options.extend(["-device", 'virtio-scsi-pci,id=scsi{}'.format(disk_index)])
  128. + options.extend(["-drive", 'file={},if=none,id=drive{},index={},media=disk{}'.format(disk, disk_index, disk_index, extra_drive_options)])
  129. + options.extend(["-device", 'scsi-hd,drive=drive{}'.format(disk_index)])
  130. + #elif interface == "sd":
  131. + # options.extend(["-drive", 'file={},id=drive{},index={}{}'.format(disk, disk_index, disk_index, extra_drive_options)])
  132. + # options.extend(["-device", 'sd-card,drive=drive{},id=drive{}'.format(disk_index, disk_index, disk_index)])
  133. + else:
  134. + options.extend(["-drive", 'file={},if={},index={},media=disk,id=drive{}{}'.format(disk, interface, disk_index, disk_index, extra_drive_options)])
  135. + return options
  136. +
  137. async def _disk_options(self):
  138. options = []
  139. qemu_img_path = self._get_qemu_img()
  140. @@ -1642,23 +1746,33 @@ class QemuVM(BaseNode):
  141. else:
  142. disk = disk_image
  143. - if interface == "sata":
  144. - # special case, sata controller doesn't exist in Qemu
  145. - options.extend(["-device", 'ahci,id=ahci{}'.format(disk_index)])
  146. - options.extend(["-drive", 'file={},if=none,id=drive{},index={},media=disk'.format(disk, disk_index, disk_index)])
  147. - options.extend(["-device", 'ide-drive,drive=drive{},bus=ahci{}.0,id=drive{}'.format(disk_index, disk_index, disk_index)])
  148. - elif interface == "nvme":
  149. - options.extend(["-drive", 'file={},if=none,id=drive{},index={},media=disk'.format(disk, disk_index, disk_index)])
  150. - options.extend(["-device", 'nvme,drive=drive{},serial={}'.format(disk_index, disk_index)])
  151. - elif interface == "scsi":
  152. - options.extend(["-device", 'virtio-scsi-pci,id=scsi{}'.format(disk_index)])
  153. - options.extend(["-drive", 'file={},if=none,id=drive{},index={},media=disk'.format(disk, disk_index, disk_index)])
  154. - options.extend(["-device", 'scsi-hd,drive=drive{}'.format(disk_index)])
  155. - #elif interface == "sd":
  156. - # options.extend(["-drive", 'file={},id=drive{},index={}'.format(disk, disk_index, disk_index)])
  157. - # options.extend(["-device", 'sd-card,drive=drive{},id=drive{}'.format(disk_index, disk_index, disk_index)])
  158. + options.extend(self._disk_interface_options(disk, disk_index, interface))
  159. +
  160. + # config disk
  161. + disk_image = getattr(self, "config_disk_image")
  162. + if disk_image:
  163. + if getattr(self, "_hdd_disk_image"):
  164. + log.warning("Config disk: blocked by disk image 'hdd'")
  165. else:
  166. - options.extend(["-drive", 'file={},if={},index={},media=disk,id=drive{}'.format(disk, interface, disk_index, disk_index)])
  167. + disk_name = getattr(self, "config_disk_name")
  168. + disk = os.path.join(self.working_dir, disk_name)
  169. + interface = getattr(self, "hda_disk_interface", "ide")
  170. + await self._import_config()
  171. + if not os.path.exists(disk):
  172. + try:
  173. + shutil.copyfile(disk_image, disk)
  174. + except OSError as e:
  175. + raise QemuError("Could not create '{}' disk image: {}".format(disk_name, e))
  176. + mtoolsrc = os.path.join(self.working_dir, "mtoolsrc")
  177. + if not os.path.exists(mtoolsrc):
  178. + try:
  179. + with open(mtoolsrc, 'w') as outfile:
  180. + outfile.write('drive x:\n')
  181. + outfile.write(' file="{}"\n'.format(disk))
  182. + outfile.write(' partition=1\n')
  183. + except OSError as e:
  184. + raise QemuError("Could not create 'mtoolsrc': {}".format(e))
  185. + options.extend(self._disk_interface_options(disk, 3, interface, "raw"))
  186. return options
  187. --
  188. 2.15.1 (Apple Git-101)