mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-05 06:41:13 +02:00
Merge pull request #3011 from cristian-ciobanu/feat-#3049
feat(qemu): support two CD/DVD images and consistent ISO handling
This commit is contained in:
22 files changed
+799
-53
No files matched your search
@@ -135,6 +135,10 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends
|
||||
"""
|
||||
|
||||
data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
# Reject interface changes before applying any other property or changing live media.
|
||||
for name in ("cdrom_interface", "cdrom2_interface"):
|
||||
if name in data:
|
||||
node.validate_cdrom_interface(name, data[name])
|
||||
# update the console first to avoid issue if updating console type
|
||||
node.console = data.pop("console", node.console)
|
||||
for name, value in data.items():
|
||||
|
||||
@@ -158,6 +158,9 @@ class QemuVM(KernelDatapathMixin, BaseNode):
|
||||
self._hdc_disk_interface = "none"
|
||||
self._hdd_disk_interface = "none"
|
||||
self._cdrom_image = ""
|
||||
self._cdrom2_image = ""
|
||||
self._cdrom_interface = "ide"
|
||||
self._cdrom2_interface = "ide"
|
||||
self._bios_image = ""
|
||||
self._boot_priority = "c"
|
||||
self._mac_address = ""
|
||||
@@ -524,33 +527,101 @@ class QemuVM(KernelDatapathMixin, BaseNode):
|
||||
else:
|
||||
self._cdrom_image = ""
|
||||
|
||||
@property
|
||||
def cdrom2_image(self):
|
||||
return self._cdrom2_image
|
||||
|
||||
@cdrom2_image.setter
|
||||
def cdrom2_image(self, image):
|
||||
self._cdrom2_image = self.manager.get_abs_image_path(image, self.working_dir) if image else ""
|
||||
|
||||
@property
|
||||
def cdrom_interface(self):
|
||||
return self._cdrom_interface
|
||||
|
||||
@cdrom_interface.setter
|
||||
def cdrom_interface(self, interface):
|
||||
self.validate_cdrom_interface("cdrom_interface", interface)
|
||||
self._cdrom_interface = interface
|
||||
|
||||
@property
|
||||
def cdrom2_interface(self):
|
||||
return self._cdrom2_interface
|
||||
|
||||
@cdrom2_interface.setter
|
||||
def cdrom2_interface(self, interface):
|
||||
self.validate_cdrom_interface("cdrom2_interface", interface)
|
||||
self._cdrom2_interface = interface
|
||||
|
||||
def validate_cdrom_interface(self, name, interface):
|
||||
"""Validate without mutating the VM, including during API preflight."""
|
||||
if interface not in ("ide", "sata"):
|
||||
raise QemuError("CD/DVD interface must be ide or sata")
|
||||
if interface != getattr(self, name) and self.is_running():
|
||||
raise QemuError("Stop the VM before changing a CD/DVD interface")
|
||||
|
||||
async def update_property(self, name, value):
|
||||
"""
|
||||
Update Qemu VM properties.
|
||||
"""
|
||||
|
||||
previous = getattr(self, name)
|
||||
setattr(self, name, value)
|
||||
if name == "cdrom_image":
|
||||
# let the guest know about the new cdrom image
|
||||
await self._update_cdrom_image()
|
||||
try:
|
||||
if name in {"cdrom_image", "cdrom2_image"}:
|
||||
await self._update_cdrom_image(name)
|
||||
except (Exception, asyncio.CancelledError):
|
||||
# Old media may have been deleted while QEMU still has it open.
|
||||
# Restore the stored path without resolving it again.
|
||||
setattr(self, f"_{name}", previous)
|
||||
raise
|
||||
self._command_line_changed = True
|
||||
|
||||
async def _update_cdrom_image(self):
|
||||
"""
|
||||
Update the cdrom image path for the Qemu guest OS
|
||||
"""
|
||||
async def _update_cdrom_image(self, name="cdrom_image"):
|
||||
"""Change media only on an existing drive, validating the affected slot."""
|
||||
if not self.is_running():
|
||||
return
|
||||
image = getattr(self, name)
|
||||
self._cdrom_option(name)
|
||||
slot = 0 if name == "cdrom_image" else 1
|
||||
interface = self.cdrom_interface if slot == 0 else self.cdrom2_interface
|
||||
device = f"sata-cdrom{slot}" if interface == "sata" else f"ide1-cd{slot}"
|
||||
reader, writer = await self._open_qemu_monitor_connection_vm()
|
||||
if reader is None or writer is None:
|
||||
raise QemuError("Could not connect to the QEMU monitor to change CD/DVD media")
|
||||
|
||||
if self.is_running():
|
||||
if self._cdrom_image:
|
||||
self._cdrom_option() # this will check the cdrom image is accessible
|
||||
await self._control_vm("eject -f ide1-cd0")
|
||||
await self._control_vm(f"change ide1-cd0 {self._cdrom_image}")
|
||||
log.debug(
|
||||
f'QEMU VM "{self._name}" [{self._id}] has changed the cdrom image path to {self._cdrom_image}'
|
||||
)
|
||||
async def command(text):
|
||||
writer.write((text + "\n").encode("utf-8"))
|
||||
await writer.drain()
|
||||
# Consume the echo first: a filename may itself contain "(qemu) ".
|
||||
await asyncio.wait_for(reader.readline(), timeout=3)
|
||||
output = await asyncio.wait_for(reader.readuntil(b"(qemu) "), timeout=3)
|
||||
response = output.decode("utf-8", errors="replace")
|
||||
response = response.removesuffix("(qemu) ").strip()
|
||||
if "Error:" in response or (text != "info block" and response):
|
||||
raise QemuError(response)
|
||||
return response
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(reader.readuntil(b"(qemu) "), timeout=3)
|
||||
blocks = await command("info block")
|
||||
if not re.search(rf"(?m)^{re.escape(device)}(?:\s+\([^\r\n]*\))?:", blocks):
|
||||
raise QemuError("Stop the VM before adding this CD/DVD drive")
|
||||
if image:
|
||||
filename = image
|
||||
if not image.isascii() or any(ord(char) < 32 for char in image):
|
||||
# HMP's line editor cannot reliably handle non-ASCII bytes.
|
||||
# A JSON block filename carries them as ASCII escapes, which
|
||||
# the block layer decodes after the HMP parser has run.
|
||||
filename = "json:" + json.dumps({"driver": "raw", "file": {"driver": "file", "filename": image}})
|
||||
await command(f"change {device} -f {json.dumps(filename)}")
|
||||
else:
|
||||
await self._control_vm("eject -f ide1-cd0")
|
||||
log.debug(f'QEMU VM "{self._name}" [{self._id}] has ejected the cdrom image')
|
||||
await command(f"eject -f {device}")
|
||||
log.debug('QEMU VM "%s" has changed %s to %s', self._name, name, image)
|
||||
except (asyncio.TimeoutError, asyncio.IncompleteReadError, OSError) as e:
|
||||
raise QemuError(f"Could not change CD/DVD media: {e}") from e
|
||||
finally:
|
||||
writer.close()
|
||||
|
||||
@property
|
||||
def bios_image(self):
|
||||
@@ -2387,20 +2458,45 @@ class QemuVM(KernelDatapathMixin, BaseNode):
|
||||
|
||||
return options
|
||||
|
||||
def _cdrom_option(self):
|
||||
|
||||
def _cdrom_option(self, name=None):
|
||||
options = []
|
||||
if self._cdrom_image:
|
||||
if not os.path.isfile(self._cdrom_image) or not os.path.exists(self._cdrom_image):
|
||||
if os.path.islink(self._cdrom_image):
|
||||
raise QemuError(
|
||||
f"cdrom image '{self._cdrom_image}' linked to '{os.path.realpath(self._cdrom_image)}' is not accessible"
|
||||
)
|
||||
else:
|
||||
raise QemuError(f"cdrom image '{self._cdrom_image}' is not accessible")
|
||||
if self._hdc_disk_image:
|
||||
raise QemuError("You cannot use a disk image on hdc disk and a CDROM image at the same time")
|
||||
options.extend(["-cdrom", self._cdrom_image.replace(",", ",,")])
|
||||
for slot, image_name in enumerate(("cdrom_image", "cdrom2_image")):
|
||||
image = getattr(self, image_name)
|
||||
if name not in (None, image_name) or not image:
|
||||
continue
|
||||
if not os.path.isfile(image):
|
||||
if os.path.islink(image):
|
||||
raise QemuError(f"cdrom image '{image}' linked to '{os.path.realpath(image)}' is not accessible")
|
||||
raise QemuError(f"cdrom image '{image}' is not accessible")
|
||||
interface = self.cdrom_interface if slot == 0 else self.cdrom2_interface
|
||||
if interface == "sata":
|
||||
# Separate IDs avoid collisions with SATA hard disk controllers.
|
||||
options.extend(
|
||||
[
|
||||
"-device",
|
||||
f"ich9-ahci,id=cdrom-ahci{slot}",
|
||||
"-drive",
|
||||
f"file={image.replace(',', ',,')},if=none,id=sata-cdrom{slot},media=cdrom,readonly=on",
|
||||
"-device",
|
||||
f"ide-cd,drive=sata-cdrom{slot},bus=cdrom-ahci{slot}.0",
|
||||
]
|
||||
)
|
||||
elif slot == 0:
|
||||
if self._hdc_disk_image and self._hdc_disk_interface == "ide":
|
||||
raise QemuError("You cannot use an IDE hdc disk and a CDROM image at the same time")
|
||||
options.extend(["-cdrom", image.replace(",", ",,")])
|
||||
else:
|
||||
disk_interface = self._hdd_disk_interface
|
||||
if self._create_config_disk and disk_interface == "none":
|
||||
disk_interface = self._hda_disk_interface
|
||||
if (self._hdd_disk_image or self._create_config_disk) and disk_interface == "ide":
|
||||
raise QemuError("You cannot use an IDE hdd disk and a second CDROM image at the same time")
|
||||
options.extend(
|
||||
[
|
||||
"-drive",
|
||||
f"file={image.replace(',', ',,')},if=ide,index=3,media=cdrom,readonly=on",
|
||||
]
|
||||
)
|
||||
return options
|
||||
|
||||
def _bios_option(self):
|
||||
@@ -2868,6 +2964,8 @@ class QemuVM(KernelDatapathMixin, BaseNode):
|
||||
|
||||
answer["cdrom_image"] = self.manager.get_relative_image_path(self._cdrom_image, self.working_dir)
|
||||
answer["cdrom_image_md5sum"] = md5sum(self._cdrom_image, self.working_dir)
|
||||
answer["cdrom2_image"] = self.manager.get_relative_image_path(self._cdrom2_image, self.working_dir)
|
||||
answer["cdrom2_image_md5sum"] = md5sum(self._cdrom2_image, self.working_dir)
|
||||
answer["bios_image"] = self.manager.get_relative_image_path(self._bios_image, self.working_dir)
|
||||
answer["bios_image_md5sum"] = md5sum(self._bios_image, self.working_dir)
|
||||
answer["initrd"] = self.manager.get_relative_image_path(self._initrd, self.working_dir)
|
||||
|
||||
@@ -208,6 +208,7 @@ async def _patch_project_file(
|
||||
|
||||
# image files that we need to include in the exported archive
|
||||
images = []
|
||||
portable_references = []
|
||||
|
||||
try:
|
||||
with open(path) as f:
|
||||
@@ -249,32 +250,54 @@ async def _patch_project_file(
|
||||
if value is None or value.strip() == "":
|
||||
continue
|
||||
|
||||
if not keep_compute_ids: # If we keep the original compute we can keep the image path
|
||||
if not keep_compute_ids:
|
||||
node["properties"][prop] = os.path.basename(value)
|
||||
|
||||
if include_images is True:
|
||||
images.append({"compute_id": compute_id, "image": value, "image_type": node["node_type"]})
|
||||
if include_images:
|
||||
image = {"compute_id": compute_id, "image": value, "image_type": node["node_type"]}
|
||||
images.append(image)
|
||||
if not keep_compute_ids:
|
||||
portable_references.append((node["properties"], prop, image))
|
||||
|
||||
if not keep_compute_ids:
|
||||
topology["topology"][
|
||||
"computes"
|
||||
] = [] # Strip compute information because could contain secret info like password
|
||||
|
||||
local_images = {i["image"] for i in images if i["compute_id"] == "local"}
|
||||
destinations = _image_export_names(images)
|
||||
for properties, prop, image in portable_references:
|
||||
properties[prop] = destinations[(image["compute_id"], image["image_type"], image["image"])]
|
||||
|
||||
for image in local_images:
|
||||
_export_local_image(image, zstream)
|
||||
|
||||
remote_images = {(i["compute_id"], i["image_type"], i["image"]) for i in images if i["compute_id"] != "local"}
|
||||
|
||||
for compute_id, image_type, image in remote_images:
|
||||
await _export_remote_images(project, compute_id, image_type, image, zstream, temporary_dir)
|
||||
for (compute_id, image_type, image), export_name in destinations.items():
|
||||
if compute_id == "local":
|
||||
_export_local_image(image, zstream, export_name, image_type)
|
||||
else:
|
||||
await _export_remote_images(project, compute_id, image_type, image, zstream, temporary_dir, export_name)
|
||||
|
||||
zstream.writestr("project.gns3", json.dumps(topology, indent=4, sort_keys=True).encode())
|
||||
return images
|
||||
|
||||
|
||||
def _export_local_image(image, zstream):
|
||||
def _image_export_names(images):
|
||||
"""Allocate distinct portable names without changing ordinary image names."""
|
||||
destinations = {}
|
||||
used = set()
|
||||
for image in images:
|
||||
key = (image["compute_id"], image["image_type"], image["image"])
|
||||
if key in destinations:
|
||||
continue
|
||||
filename = os.path.basename(image["image"])
|
||||
stem, extension = os.path.splitext(filename)
|
||||
index = 1
|
||||
while (image["image_type"], filename) in used:
|
||||
index += 1
|
||||
filename = f"{stem}-{index}{extension}"
|
||||
used.add((image["image_type"], filename))
|
||||
destinations[key] = filename
|
||||
return destinations
|
||||
|
||||
|
||||
def _export_local_image(image, zstream, export_name=None, image_type=None):
|
||||
"""
|
||||
Exports a local image to the zip file.
|
||||
|
||||
@@ -285,6 +308,8 @@ def _export_local_image(image, zstream):
|
||||
from ..compute import MODULES
|
||||
|
||||
for module in MODULES:
|
||||
if image_type and getattr(module, "_NODE_TYPE", None) != image_type:
|
||||
continue
|
||||
try:
|
||||
images_directory = module.instance().get_images_directory()
|
||||
except NotImplementedError:
|
||||
@@ -298,13 +323,15 @@ def _export_local_image(image, zstream):
|
||||
path = os.path.join(images_directory, image)
|
||||
|
||||
if os.path.exists(path):
|
||||
arcname = os.path.join("images", directory, os.path.basename(image))
|
||||
arcname = os.path.join("images", directory, export_name or os.path.basename(image))
|
||||
_patch_mtime(path)
|
||||
zstream.write(path, arcname)
|
||||
return
|
||||
|
||||
|
||||
async def _export_remote_images(project, compute_id, image_type, image, project_zipfile, temporary_dir):
|
||||
async def _export_remote_images(
|
||||
project, compute_id, image_type, image, project_zipfile, temporary_dir, export_name=None
|
||||
):
|
||||
"""
|
||||
Export specific image from remote compute.
|
||||
"""
|
||||
@@ -334,5 +361,6 @@ async def _export_remote_images(project, compute_id, image_type, image, project_
|
||||
break
|
||||
await f.write(data)
|
||||
response.close()
|
||||
arcname = os.path.join("images", image_type, image)
|
||||
directory = "QEMU" if image_type == "qemu" else image_type
|
||||
arcname = os.path.join("images", directory, export_name or image)
|
||||
project_zipfile.write(temp_path, arcname=arcname, compress_type=zipfile.ZIP_DEFLATED)
|
||||
@@ -58,6 +58,7 @@ IMAGE_PROPERTIES_BY_NODE_TYPE = {
|
||||
"hdc_disk_image": "qemu",
|
||||
"hdd_disk_image": "qemu",
|
||||
"cdrom_image": "qemu",
|
||||
"cdrom2_image": "qemu",
|
||||
"initrd": "qemu",
|
||||
"kernel_image": "qemu",
|
||||
"bios_image": "qemu",
|
||||
@@ -900,6 +901,7 @@ class Node:
|
||||
"hdc_disk_image",
|
||||
"hdd_disk_image",
|
||||
"cdrom_image",
|
||||
"cdrom2_image",
|
||||
"bios_image",
|
||||
"initrd",
|
||||
"kernel_image",
|
||||
|
||||
@@ -45,6 +45,23 @@ class Image(BaseTable):
|
||||
last_error = Column(String)
|
||||
templates = relationship("Template", secondary=image_template_map, back_populates="images")
|
||||
|
||||
@property
|
||||
def reference(self):
|
||||
"""An emulator-relative reference, or an absolute additional-root path."""
|
||||
import os
|
||||
|
||||
from gns3server.utils import force_unix_path
|
||||
from gns3server.utils.images import default_images_directory
|
||||
|
||||
root = os.path.abspath(default_images_directory(self.image_type))
|
||||
path = os.path.abspath(self.path)
|
||||
try:
|
||||
if os.path.commonpath([root, path]) == root:
|
||||
return force_unix_path(os.path.relpath(path, root))
|
||||
except ValueError: # Different drives on Windows.
|
||||
pass
|
||||
return force_unix_path(path)
|
||||
|
||||
|
||||
class ImageSyncJob(BaseTable):
|
||||
__tablename__ = "image_sync_jobs"
|
||||
|
||||
@@ -201,6 +201,9 @@ class QemuTemplate(Template):
|
||||
hdd_disk_image = Column(String)
|
||||
hdd_disk_interface = Column(String)
|
||||
cdrom_image = Column(String)
|
||||
cdrom2_image = Column(String)
|
||||
cdrom_interface = Column(String, nullable=False, default="ide", server_default="ide")
|
||||
cdrom2_interface = Column(String, nullable=False, default="ide", server_default="ide")
|
||||
initrd = Column(String)
|
||||
kernel_image = Column(String)
|
||||
bios_image = Column(String)
|
||||
|
||||
@@ -140,12 +140,20 @@ async def connect_to_db(app: FastAPI) -> None:
|
||||
def upgrade_inventory(connection):
|
||||
from alembic.operations import Operations
|
||||
|
||||
from gns3server.db_migrations.versions.a6c8e1b2d490_add_second_qemu_cdrom import (
|
||||
upgrade as upgrade_cdrom,
|
||||
)
|
||||
from gns3server.db_migrations.versions.d9e8a2b7c401_image_inventory_reconciliation import (
|
||||
upgrade,
|
||||
)
|
||||
from gns3server.db_migrations.versions.e2a7c9d4b601_add_cdrom_interfaces import (
|
||||
upgrade as upgrade_cdrom_interfaces,
|
||||
)
|
||||
|
||||
with Operations.context(MigrationContext.configure(connection)):
|
||||
upgrade()
|
||||
upgrade_cdrom()
|
||||
upgrade_cdrom_interfaces()
|
||||
|
||||
await conn.run_sync(upgrade_inventory)
|
||||
await conn.run_sync(run_stamp, alembic_cfg)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Add a second QEMU CD-ROM image.
|
||||
|
||||
Revision ID: a6c8e1b2d490
|
||||
Revises: f7d3a9c2e5b1
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "a6c8e1b2d490"
|
||||
down_revision = "f7d3a9c2e5b1"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
existing = {column["name"] for column in sa.inspect(op.get_bind()).get_columns("qemu_templates")}
|
||||
if "cdrom2_image" not in existing:
|
||||
op.add_column("qemu_templates", sa.Column("cdrom2_image", sa.String()))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column("qemu_templates", "cdrom2_image")
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Add QEMU CD/DVD interfaces."""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "e2a7c9d4b601"
|
||||
down_revision = "a6c8e1b2d490"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
existing = {column["name"] for column in sa.inspect(op.get_bind()).get_columns("qemu_templates")}
|
||||
for name in ("cdrom_interface", "cdrom2_interface"):
|
||||
if name in existing:
|
||||
continue
|
||||
op.add_column("qemu_templates", sa.Column(name, sa.String(), nullable=False, server_default="ide"))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column("qemu_templates", "cdrom2_interface")
|
||||
op.drop_column("qemu_templates", "cdrom_interface")
|
||||
@@ -15,7 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
from typing import List, Literal, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -152,6 +152,9 @@ class QemuDiskInterfaceType(str, Enum):
|
||||
none = "none"
|
||||
|
||||
|
||||
QemuCdromInterface = Literal["ide", "sata"]
|
||||
|
||||
|
||||
class QemuBase(BaseModel):
|
||||
"""
|
||||
Common Qemu node properties.
|
||||
@@ -183,8 +186,12 @@ class QemuBase(BaseModel):
|
||||
hdd_disk_image_backing_file: Optional[str] = Field(None, description="QEMU hdd backing file disk image path")
|
||||
hdd_disk_image_md5sum: Optional[str] = Field(None, description="QEMU hdd disk image checksum")
|
||||
hdd_disk_interface: Optional[QemuDiskInterfaceType] = Field(None, description="QEMU hdd interface")
|
||||
cdrom_image: Optional[str] = Field(None, description="QEMU cdrom image path")
|
||||
cdrom_image_md5sum: Optional[str] = Field(None, description="QEMU cdrom image checksum")
|
||||
cdrom_image: Optional[str] = Field(None, description="QEMU first cdrom image path")
|
||||
cdrom_interface: QemuCdromInterface = Field("ide", description="First CD/DVD interface")
|
||||
cdrom2_interface: QemuCdromInterface = Field("ide", description="Second CD/DVD interface")
|
||||
cdrom2_image: Optional[str] = Field(None, description="QEMU second cdrom image path")
|
||||
cdrom_image_md5sum: Optional[str] = Field(None, description="QEMU first cdrom image checksum")
|
||||
cdrom2_image_md5sum: Optional[str] = Field(None, description="QEMU second cdrom image checksum")
|
||||
bios_image: Optional[str] = Field(None, description="QEMU bios image path")
|
||||
bios_image_md5sum: Optional[str] = Field(None, description="QEMU bios image checksum")
|
||||
initrd: Optional[str] = Field(None, description="QEMU initrd path")
|
||||
|
||||
@@ -23,6 +23,7 @@ from uuid import UUID
|
||||
from pydantic import AnyUrl, BaseModel, BeforeValidator, Discriminator, EmailStr, Field, Tag
|
||||
|
||||
from ..common import ExtraConfig
|
||||
from ..compute.qemu_nodes import QemuCdromInterface
|
||||
|
||||
# ============================================================================
|
||||
# Shared Enums
|
||||
@@ -398,6 +399,8 @@ class Qemu(BaseModel):
|
||||
hdc_disk_interface: Optional[QemuDiskInterface] = Field(
|
||||
None, title="Disk interface for the installed hdc_disk_image"
|
||||
)
|
||||
cdrom_interface: Optional[QemuCdromInterface] = Field(None, title="First CD/DVD interface")
|
||||
cdrom2_interface: Optional[QemuCdromInterface] = Field(None, title="Second CD/DVD interface")
|
||||
hdd_disk_interface: Optional[QemuDiskInterface] = Field(
|
||||
None, title="Disk interface for the installed hdd_disk_image"
|
||||
)
|
||||
@@ -425,7 +428,8 @@ class ApplianceVersionImages(BaseModel):
|
||||
hdb_disk_image: Optional[str] = Field(None, title="Hdc disk image")
|
||||
hdc_disk_image: Optional[str] = Field(None, title="Hdd disk image")
|
||||
hdd_disk_image: Optional[str] = Field(None, title="Hdd diskimage")
|
||||
cdrom_image: Optional[str] = Field(None, title="cdrom image")
|
||||
cdrom_image: Optional[str] = Field(None, title="First cdrom image")
|
||||
cdrom2_image: Optional[str] = Field(None, title="Second cdrom image")
|
||||
|
||||
|
||||
class ApplianceVersion(BaseModel):
|
||||
@@ -578,6 +582,8 @@ class QemuPropertiesV8(BaseModel):
|
||||
hdc_disk_interface: Optional[QemuDiskInterface] = Field(
|
||||
None, title="Disk interface for the installed hdc_disk_image"
|
||||
)
|
||||
cdrom_interface: Optional[QemuCdromInterface] = Field(None, title="First CD/DVD interface")
|
||||
cdrom2_interface: Optional[QemuCdromInterface] = Field(None, title="Second CD/DVD interface")
|
||||
hdd_disk_interface: Optional[QemuDiskInterface] = Field(
|
||||
None, title="Disk interface for the installed hdd_disk_image"
|
||||
)
|
||||
|
||||
@@ -37,6 +37,9 @@ class ImageBase(BaseModel):
|
||||
|
||||
filename: str = Field(..., description="Image filename")
|
||||
path: str = Field(..., description="Image path")
|
||||
reference: Optional[str] = Field(
|
||||
None, description="Image reference relative to its emulator directory, or an absolute additional-root path"
|
||||
)
|
||||
image_type: ImageType = Field(..., description="Image type")
|
||||
image_size: int = Field(..., description="Image size in bytes")
|
||||
checksum: str = Field(..., description="Checksum value")
|
||||
|
||||
@@ -23,6 +23,7 @@ from gns3server.schemas.compute.qemu_nodes import (
|
||||
CustomAdapter,
|
||||
QemuAdapterType,
|
||||
QemuBootPriority,
|
||||
QemuCdromInterface,
|
||||
QemuConsoleType,
|
||||
QemuDiskInterfaceType,
|
||||
QemuOnCloseAction,
|
||||
@@ -79,7 +80,10 @@ class QemuTemplate(TemplateBase):
|
||||
hdd_disk_interface: Optional[QemuDiskInterfaceType] = Field(
|
||||
QemuDiskInterfaceType.none, description="QEMU hdd interface"
|
||||
)
|
||||
cdrom_image: Optional[str] = Field("", description="QEMU cdrom image path")
|
||||
cdrom_image: Optional[str] = Field("", description="QEMU first cdrom image path")
|
||||
cdrom_interface: QemuCdromInterface = Field("ide", description="First CD/DVD interface")
|
||||
cdrom2_interface: QemuCdromInterface = Field("ide", description="Second CD/DVD interface")
|
||||
cdrom2_image: Optional[str] = Field("", description="QEMU second cdrom image path")
|
||||
initrd: Optional[str] = Field("", description="QEMU initrd path")
|
||||
kernel_image: Optional[str] = Field("", description="QEMU kernel image path")
|
||||
bios_image: Optional[str] = Field("", description="QEMU bios image path")
|
||||
|
||||
@@ -49,6 +49,9 @@ from . import force_unix_path
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Include the first ISO 9660 volume descriptor in image type detection.
|
||||
IMAGE_HEADER_SIZE = 16 * 2048 + 7
|
||||
|
||||
|
||||
async def list_images(image_type):
|
||||
"""
|
||||
@@ -145,7 +148,7 @@ def inspect_image_file(path, expected_image_type=None, allow_raw_image=False, st
|
||||
info = os.fstat(f.fileno())
|
||||
if not stat.S_ISREG(info.st_mode) or stat_fingerprint(info) != before:
|
||||
raise ImageChangedError(f"Image changed while opening: {path}")
|
||||
header = f.read(7)
|
||||
header = f.read(IMAGE_HEADER_SIZE)
|
||||
if len(header) < 7:
|
||||
raise InvalidImageError(f"Image '{path}' is too small to be valid")
|
||||
image_type = check_valid_image_header(path, header, allow_raw_image)
|
||||
@@ -359,6 +362,8 @@ def check_valid_image_header(path: str, data: bytes, allow_raw_image: bool = Fal
|
||||
elif data[:4] == b"QFI\xfb" or data[:4] == b"KDMV":
|
||||
# for Qemy images: file must be QCOW2 or VMDK
|
||||
return "qemu"
|
||||
elif data[16 * 2048 + 1 : 16 * 2048 + 6] == b"CD001":
|
||||
return "qemu"
|
||||
else:
|
||||
if allow_raw_image is True:
|
||||
return "qemu"
|
||||
@@ -380,12 +385,14 @@ async def write_image(
|
||||
if subdirectory is not None:
|
||||
validate_image_upload_name(image_filename)
|
||||
subfolders = validate_image_subdirectory(subdirectory)
|
||||
# HTTP chunk boundaries need not align with the seven-byte image header.
|
||||
# HTTP chunk boundaries need not align with the image header or ISO descriptor.
|
||||
iterator = stream.__aiter__()
|
||||
prefix = bytearray()
|
||||
async for chunk in iterator:
|
||||
prefix.extend(chunk)
|
||||
if len(prefix) >= 7:
|
||||
# Known disk formats keep their original early detection behavior.
|
||||
header_size = 7 if prefix.startswith((b"\x7fELF", b"QFI\xfb", b"KDMV")) else IMAGE_HEADER_SIZE
|
||||
if len(prefix) >= header_size:
|
||||
break
|
||||
if len(prefix) < 7:
|
||||
raise InvalidImageError("The image content is empty or too small to be valid")
|
||||
|
||||
Reference in new issue
Block a user