Skip to content

Commit 3d2398f

Browse files
committed
VmManager: Allow external iscsi volumes
Closes #204
1 parent 0795010 commit 3d2398f

5 files changed

Lines changed: 214 additions & 1 deletion

File tree

backend/insert_vm_templates.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def insert_vm_templates():
3636
<emulator>{%qemu_path%}</emulator>
3737
{%devices_disk_file%}
3838
{%devices_disk_block%}
39+
{%devices_disk_iscsi%}
3940
{%devices_network%}
4041
<controller type='pci' index='0' model='pcie-root'/>
4142
<input type='tablet' bus='usb'>
@@ -81,6 +82,7 @@ def insert_vm_templates():
8182
<emulator>{%qemu_path%}</emulator>
8283
{%devices_disk_file%}
8384
{%devices_disk_block%}
85+
{%devices_disk_iscsi%}
8486
{%devices_network%}
8587
<controller type='pci' index='0' model='pcie-root'/>
8688
<controller type="usb" index="0" model="ich9-ehci1"/>

backend/vm_manager/routes.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from auth_manager.auth import check_auth
44
from db.database import get_session
55
from sqlmodel import select
6-
from .vmbasic import VirtualMachineBasic, VirtualMachineBasicTemplate, OvmfPath, VirtualMachineBasicLibvirtConfig, VirtualMachineXmlTemplate, VirtualMachineDeviceDiskFile, VirtualMachineDeviceNetwork, VirtualMachineDeviceDiskBlock, get_vm_networks
6+
from .vmbasic import VirtualMachineBasic, VirtualMachineBasicTemplate, OvmfPath, VirtualMachineBasicLibvirtConfig, VirtualMachineXmlTemplate, VirtualMachineDeviceDiskFile, VirtualMachineDeviceNetwork, VirtualMachineDeviceDiskBlock, VirtualMachineDeviceDiskIscsi, get_vm_networks
77

88
router = APIRouter()
99

@@ -198,3 +198,25 @@ async def api_vm_add_device_disk_block(vm_id: int, device: VirtualMachineDeviceD
198198
session.commit()
199199
return device
200200
return
201+
202+
@router.delete("/{vm_id}/devices/disk-iscsi/{device_id}")
203+
async def api_vm_delete_device_disk_iscsi(vm_id: int, device_id: int, username: str = Depends(check_auth)):
204+
with get_session() as session:
205+
disk_device_iscsi = session.exec(select(VirtualMachineDeviceDiskIscsi).where(VirtualMachineDeviceDiskIscsi.id == device_id)).first()
206+
if disk_device_iscsi is None:
207+
raise HTTPException(status_code=404, detail="iSCSI disk device not found")
208+
session.delete(disk_device_iscsi)
209+
session.commit()
210+
211+
@router.post("/{vm_id}/devices/disk-iscsi")
212+
async def api_vm_add_device_disk_iscsi(vm_id: int, device: VirtualMachineDeviceDiskIscsi, username: str = Depends(check_auth)):
213+
print(f"Adding iSCSI disk device {device}")
214+
with get_session() as session:
215+
vm = session.exec(select(VirtualMachineBasic).where(VirtualMachineBasic.id == vm_id)).first()
216+
if vm is None:
217+
raise HTTPException(status_code=404, detail="VM not found")
218+
device.vm = vm
219+
session.add(device)
220+
session.commit()
221+
return device
222+
return

backend/vm_manager/vmbasic.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class VirtualMachineBasic(SQLModel, table=True):
104104
devices_pci: list["VirtualMachineDevicePci"] = Relationship(back_populates="vm")
105105
devices_disk_file: list["VirtualMachineDeviceDiskFile"] = Relationship(back_populates="vm")
106106
devices_disk_block: list["VirtualMachineDeviceDiskBlock"] = Relationship(back_populates="vm")
107+
devices_disk_iscsi: list["VirtualMachineDeviceDiskIscsi"] = Relationship(back_populates="vm")
107108
devices_network: list["VirtualMachineDeviceNetwork"] = Relationship(back_populates="vm")
108109
xml_template_id: int | None = Field(nullable=False, foreign_key="virtualmachinexmltemplate.id")
109110
xml_template: VirtualMachineXmlTemplate | None = Relationship()
@@ -142,6 +143,18 @@ class VirtualMachineDeviceDiskBlock(SQLModel, table=True):
142143
vm_id: int | None = Field(foreign_key="virtualmachine.id")
143144
vm: VirtualMachineBasic | None = Relationship(back_populates="devices_disk_block")
144145

146+
class VirtualMachineDeviceDiskIscsi(SQLModel, table=True):
147+
__tablename__ = "virtualmachinedevicediskiscsi"
148+
id: int | None = Field(primary_key=True)
149+
name: str = Field(nullable=False)
150+
disk_bus: str = Field(nullable=False) # VirtualMachineBasicDiskBusTypes
151+
device_type: str = Field(nullable=False) # VirtualMachineBasicDiskDeviceTypes
152+
iscsi_name: str = Field(nullable=False) # iqn.2013-07.com.example:iscsi-nopool/2
153+
iscsi_host: str = Field(nullable=False) # example.com
154+
iscsi_port: int = Field(nullable=False, default=3260) # 3260
155+
vm_id: int | None = Field(foreign_key="virtualmachine.id")
156+
vm: VirtualMachineBasic | None = Relationship(back_populates="devices_disk_iscsi")
157+
145158
class VirtualMachineDeviceNetwork(SQLModel, table=True):
146159
__tablename__ = "virtualmachinedevicenetwork"
147160
id: int | None = Field(primary_key=True)
@@ -163,6 +176,7 @@ def to_dict(self):
163176
vm_dict["devices_pci"] = [device.model_dump() for device in self.vm.devices_pci]
164177
vm_dict["devices_disk_file"] = [device.model_dump() for device in self.vm.devices_disk_file]
165178
vm_dict["devices_disk_block"] = [device.model_dump() for device in self.vm.devices_disk_block]
179+
vm_dict["devices_disk_iscsi"] = [device.model_dump() for device in self.vm.devices_disk_iscsi]
166180
vm_dict["devices_network"] = [device.model_dump() for device in self.vm.devices_network]
167181
return vm_dict
168182

@@ -213,6 +227,22 @@ def gen_devices_disk_block(self):
213227
</disk>"""
214228
return devices_disk_block
215229

230+
def gen_devices_disk_iscsi(self):
231+
devices_disk_iscsi = ""
232+
# Calculate starting index based on number of file and block disks
233+
start_index = len(self.vm.devices_disk_file) + len(self.vm.devices_disk_block)
234+
for index, device in enumerate(self.vm.devices_disk_iscsi):
235+
targetdev = "sd"
236+
targetdev += chr(ord("a") + start_index + index)
237+
devices_disk_iscsi += f"""<disk type='network' device='{device.device_type}'>
238+
<driver name='qemu' type='raw'/>
239+
<source protocol='iscsi' name='{device.iscsi_name}'>
240+
<host name='{device.iscsi_host}' port='{device.iscsi_port}'/>
241+
</source>
242+
<target dev='{targetdev}' bus='{device.disk_bus}'/>
243+
</disk>"""
244+
return devices_disk_iscsi
245+
216246
def gen_devices_network(self):
217247
devices_network = ""
218248
for index, device in enumerate(self.vm.devices_network):
@@ -244,6 +274,7 @@ def gen_xml(self):
244274
xml = xml.replace("{%qemu_path%}", "/usr/bin/qemu-system-x86_64")
245275
xml = xml.replace("{%devices_disk_file%}", self.gen_devices_disk_file())
246276
xml = xml.replace("{%devices_disk_block%}", self.gen_devices_disk_block())
277+
xml = xml.replace("{%devices_disk_iscsi%}", self.gen_devices_disk_iscsi())
247278
xml = xml.replace("{%devices_network%}", self.gen_devices_network())
248279
xml = xml.replace("{%graphics%}", self.gen_graphics())
249280
xml = xml.replace("{%video%}", self.gen_video())

frontend/src/components/vm/AddDevice.vue

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@
3333
</q-item-section>
3434
<q-item-section>Disk (Block)</q-item-section>
3535
</q-item>
36+
<q-item
37+
clickable
38+
v-ripple
39+
:active="option === 'disk-iscsi'"
40+
@click="option = 'disk-iscsi'"
41+
active-class="my-menu-link"
42+
>
43+
<q-item-section avatar>
44+
<q-icon color="primary" name="mdi-harddisk" />
45+
</q-item-section>
46+
<q-item-section>Disk (iSCSI)</q-item-section>
47+
</q-item>
3648
<q-item
3749
clickable
3850
v-ripple
@@ -92,6 +104,44 @@
92104
hint="Enter the path to the physical disk device"
93105
/>
94106
</q-card-section>
107+
<q-card-section v-if="diskiscsiLayout">
108+
<q-input filled v-model="diskIscsiName" label="Disk Name" class="q-pb-md" />
109+
<q-select
110+
class="q-pb-md"
111+
v-model="diskIscsiBusType"
112+
:options="diskBusTypes"
113+
label="Disk Bus"
114+
filled
115+
/>
116+
<q-select
117+
class="q-pb-md"
118+
v-model="diskIscsiDeviceType"
119+
:options="diskDeviceTypes"
120+
label="Device Type"
121+
filled
122+
/>
123+
<q-input
124+
filled
125+
v-model="diskIscsiIqn"
126+
label="iSCSI Name (IQN)"
127+
hint="e.g., iqn.2013-07.com.example:iscsi-nopool/2"
128+
class="q-pb-md"
129+
/>
130+
<q-input
131+
filled
132+
v-model="diskIscsiHost"
133+
label="iSCSI Host"
134+
hint="e.g., example.com or 192.168.1.100"
135+
class="q-pb-md"
136+
/>
137+
<q-input
138+
filled
139+
v-model="diskIscsiPort"
140+
type="number"
141+
label="iSCSI Port"
142+
hint="Default: 3260"
143+
/>
144+
</q-card-section>
95145
<q-card-section v-if="networkLayout" class="q-py-none">
96146
<q-select
97147
class="q-pb-md"
@@ -128,6 +178,9 @@
128178
<q-btn color="primary" icon="check" flat @click="diskBlockCreate" v-if="diskblockLayout">
129179
<ToolTip content="Create" />
130180
</q-btn>
181+
<q-btn color="primary" icon="check" flat @click="diskIscsiCreate" v-if="diskiscsiLayout">
182+
<ToolTip content="Create" />
183+
</q-btn>
131184
</q-card-actions>
132185
</q-card>
133186
</q-dialog>
@@ -155,6 +208,7 @@ export default {
155208
optionLayout: true,
156209
diskfileLayout: false,
157210
diskblockLayout: false,
211+
diskiscsiLayout: false,
158212
networkLayout: false,
159213
networkInterfaceTypes: [
160214
{ label: 'VirtIO', value: 'virtio' },
@@ -184,6 +238,12 @@ export default {
184238
diskBlockBusType: 'virtio',
185239
diskBlockDeviceType: 'disk',
186240
diskBlockSource: '',
241+
diskIscsiName: '',
242+
diskIscsiBusType: 'virtio',
243+
diskIscsiDeviceType: 'disk',
244+
diskIscsiIqn: '',
245+
diskIscsiHost: '',
246+
diskIscsiPort: 3260,
187247
}
188248
},
189249
emits: ['finished'],
@@ -199,6 +259,7 @@ export default {
199259
this.optionLayout = true
200260
this.diskfileLayout = false
201261
this.diskblockLayout = false
262+
this.diskiscsiLayout = false
202263
this.networkLayout = false
203264
},
204265
optionChose() {
@@ -216,6 +277,15 @@ export default {
216277
this.diskBlockDeviceType = this.diskDeviceTypes[0]
217278
this.diskBlockName = ''
218279
this.diskBlockSource = ''
280+
} else if (this.option === 'disk-iscsi') {
281+
this.optionLayout = false
282+
this.diskiscsiLayout = true
283+
this.diskIscsiBusType = this.diskBusTypes[0]
284+
this.diskIscsiDeviceType = this.diskDeviceTypes[0]
285+
this.diskIscsiName = ''
286+
this.diskIscsiIqn = ''
287+
this.diskIscsiHost = ''
288+
this.diskIscsiPort = 3260
219289
} else if (this.option === 'network') {
220290
this.optionLayout = false
221291
this.networkLayout = true
@@ -294,6 +364,38 @@ export default {
294364
])
295365
})
296366
},
367+
diskIscsiCreate() {
368+
if (this.diskIscsiIqn == '') {
369+
this.$refs.errorDialog.show('Error', ['iSCSI IQN is required'])
370+
return
371+
}
372+
if (this.diskIscsiHost == '') {
373+
this.$refs.errorDialog.show('Error', ['iSCSI Host is required'])
374+
return
375+
}
376+
if (this.diskIscsiName == '') {
377+
this.$refs.errorDialog.show('Error', ['Disk name is required'])
378+
return
379+
}
380+
this.$api
381+
.post('/vm/' + this.vmid + '/devices/disk-iscsi', {
382+
name: this.diskIscsiName,
383+
disk_bus: this.diskIscsiBusType.value,
384+
device_type: this.diskIscsiDeviceType.value,
385+
iscsi_name: this.diskIscsiIqn,
386+
iscsi_host: this.diskIscsiHost,
387+
iscsi_port: this.diskIscsiPort,
388+
})
389+
.then(() => {
390+
this.layout = false
391+
this.$emit('finished')
392+
})
393+
.catch((error) => {
394+
this.$refs.errorDialog.show('Error creating iSCSI device', [
395+
error.response?.data?.detail || error.message,
396+
])
397+
})
398+
},
297399
},
298400
}
299401
</script>

frontend/src/components/vm/EditVmDialog.vue

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,34 @@
246246
</q-item-label>
247247
</q-item-section>
248248
</q-item>
249+
<q-item
250+
clickable
251+
v-ripple
252+
v-for="(disk, index) in vm.devices_disk_iscsi"
253+
:key="disk"
254+
:active="disk.active"
255+
@click="deviceSelect('disk-iscsi', disk, index)"
256+
>
257+
<q-item-section thumbnail class="q-pr-sm">
258+
<q-icon
259+
color="primary"
260+
:name="disk.device_type == 'cdrom' ? 'mdi-disc' : 'mdi-harddisk'"
261+
/>
262+
</q-item-section>
263+
<q-item-section>
264+
<q-item-label>
265+
{{ disk.disk_bus == 'virtio' ? 'VirtIO' : disk.disk_bus.toUpperCase() }}
266+
{{
267+
disk.device_type == 'cdrom'
268+
? 'CDROM'
269+
: disk.device_type == 'disk'
270+
? 'iSCSI Disk'
271+
: disk.device_type
272+
}}
273+
{{ disk.index }}
274+
</q-item-label>
275+
</q-item-section>
276+
</q-item>
249277
<q-item
250278
clickable
251279
v-ripple
@@ -316,6 +344,18 @@
316344
readonly
317345
/>
318346
</div>
347+
<div v-if="selectedDeviceType == 'disk-iscsi'">
348+
<q-input label="Name" v-model="selectedDevice.name" readonly />
349+
<q-input label="Device Type" v-model="selectedDevice.device_type" readonly />
350+
<q-input label="Disk Bus" v-model="selectedDevice.disk_bus" readonly />
351+
<q-input
352+
label="iSCSI Name (IQN)"
353+
v-model="selectedDevice.iscsi_name"
354+
readonly
355+
/>
356+
<q-input label="iSCSI Host" v-model="selectedDevice.iscsi_host" readonly />
357+
<q-input label="iSCSI Port" v-model="selectedDevice.iscsi_port" readonly />
358+
</div>
319359
<div v-if="selectedDeviceType == 'network'">
320360
<q-input label="Type" v-model="selectedDevice.type" readonly />
321361
<q-input
@@ -448,6 +488,9 @@ export default {
448488
this.vm.devices_disk_block.forEach((element) => {
449489
element.active = false
450490
})
491+
this.vm.devices_disk_iscsi.forEach((element) => {
492+
element.active = false
493+
})
451494
}
452495
},
453496
deviceSelect(type, device, index = null) {
@@ -461,6 +504,10 @@ export default {
461504
this.resetSelectedDevices()
462505
this.vm.devices_disk_block[index].active = true
463506
this.selectedDeviceTitle = 'Disk (Block)' + (index + 1)
507+
} else if (this.selectedDeviceType == 'disk-iscsi') {
508+
this.resetSelectedDevices()
509+
this.vm.devices_disk_iscsi[index].active = true
510+
this.selectedDeviceTitle = 'Disk (iSCSI) ' + (index + 1)
464511
} else if (this.selectedDeviceType == 'network') {
465512
this.resetSelectedDevices()
466513
this.vm.devices_network[index].active = true
@@ -505,6 +552,15 @@ export default {
505552
.catch((error) => {
506553
this.$refs.errorDialog.show('Error deleting disk device', [error.response.data.detail])
507554
})
555+
} else if (this.selectedDeviceType == 'disk-iscsi') {
556+
this.$api
557+
.delete('/vm/' + this.vmid + '/devices/disk-iscsi/' + this.selectedDevice.id)
558+
.then(() => {
559+
this.getdata()
560+
})
561+
.catch((error) => {
562+
this.$refs.errorDialog.show('Error deleting iSCSI disk device', [error.response.data.detail])
563+
})
508564
}
509565
},
510566
deviceAdd() {

0 commit comments

Comments
 (0)