Skip to content

Commit 92d7570

Browse files
committed
Create and apply networks
1 parent b900a51 commit 92d7570

5 files changed

Lines changed: 240 additions & 10 deletions

File tree

backend/vm_manager/network.py

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pydantic import BaseModel, ConfigDict
33
from db import get_session
44
import libvirt
5+
from host_manager import libvirt_connection
56

67

78
class LibvirtNetworkBridge(SQLModel, table=True):
@@ -23,6 +24,14 @@ class LibvirtNetworkBridgeResponse(BaseModel):
2324
active: bool
2425

2526

27+
class LibvirtNetworkBridgeApplyResponse(BaseModel):
28+
model_config = ConfigDict(from_attributes=True)
29+
checked: int
30+
removed: int
31+
defined: int
32+
started: int
33+
errors: list[str]
34+
2635
class LibvirtNetworkCustom(SQLModel, table=True):
2736
__tablename__ = "libvirtnetworkcustom"
2837
id: int | None = Field(primary_key=True)
@@ -47,15 +56,13 @@ def get_network_bridge(id: int) -> LibvirtNetworkBridgeResponse | None:
4756
statement = select(LibvirtNetworkBridge).where(LibvirtNetworkBridge.id == id)
4857
result = session.exec(statement).first()
4958
if result:
50-
# Check if active in libvirt
51-
conn = libvirt.open(None)
59+
# Check if active in libvirt using shared connection
5260
try:
61+
conn = libvirt_connection.connection
5362
network = conn.networkLookupByName(result.name)
5463
active = network.isActive() == 1
5564
except libvirt.libvirtError:
5665
active = False
57-
finally:
58-
conn.close()
5966
return LibvirtNetworkBridgeResponse(
6067
id=result.id,
6168
name=result.name,
@@ -128,15 +135,13 @@ def get_network_custom(id: int) -> LibvirtNetworkCustomResponse | None:
128135
statement = select(LibvirtNetworkCustom).where(LibvirtNetworkCustom.id == id)
129136
result = session.exec(statement).first()
130137
if result:
131-
# Check if active in libvirt
132-
conn = libvirt.open(None)
138+
# Check if active in libvirt using shared connection
133139
try:
140+
conn = libvirt_connection.connection
134141
network = conn.networkLookupByName(result.name)
135142
active = network.isActive() == 1
136143
except libvirt.libvirtError:
137144
active = False
138-
finally:
139-
conn.close()
140145
return LibvirtNetworkCustomResponse(
141146
id=result.id,
142147
name=result.name,
@@ -202,3 +207,95 @@ def delete_network_custom(id: int) -> bool:
202207
session.commit()
203208
return True
204209
return False
210+
211+
212+
def apply_network_bridges() -> LibvirtNetworkBridgeApplyResponse:
213+
"""Apply network bridge settings from the database to libvirt.
214+
215+
For every bridge entry in the database:
216+
- If a libvirt network with the same name exists, destroy and undefine it.
217+
- Define a new network XML for the bridge and register it with libvirt.
218+
- If the DB entry has `autostart=True`, set autostart and start the network.
219+
220+
Returns a summary dict with counts and any errors encountered.
221+
"""
222+
summary = {
223+
"checked": 0,
224+
"removed": 0,
225+
"defined": 0,
226+
"started": 0,
227+
"errors": [],
228+
}
229+
230+
try:
231+
conn = libvirt_connection.connection
232+
except Exception as e:
233+
summary["errors"].append(f"failed-to-get-libvirt-connection:{e}")
234+
return summary
235+
236+
with get_session() as session:
237+
stmt = select(LibvirtNetworkBridge)
238+
bridges = session.exec(stmt).all()
239+
for b in bridges:
240+
summary["checked"] += 1
241+
# Remove existing definition if present
242+
try:
243+
try:
244+
existing = conn.networkLookupByName(b.name)
245+
except libvirt.libvirtError:
246+
existing = None
247+
248+
if existing is not None:
249+
try:
250+
if existing.isActive() == 1:
251+
existing.destroy()
252+
except libvirt.libvirtError as e:
253+
# record but continue
254+
summary["errors"].append(f"destroy:{b.name}:{e}")
255+
try:
256+
existing.undefine()
257+
summary["removed"] += 1
258+
except libvirt.libvirtError as e:
259+
summary["errors"].append(f"undefine:{b.name}:{e}")
260+
261+
# Build network XML for a bridged network
262+
xml = (
263+
f"<network>"
264+
f"<name>{b.name}</name>"
265+
f"<forward mode='bridge'/>"
266+
f"<bridge name='{b.bridge_name}'/>"
267+
f"</network>"
268+
)
269+
270+
try:
271+
net = conn.networkDefineXML(xml)
272+
if net is None:
273+
summary["errors"].append(f"define-failed:{b.name}")
274+
else:
275+
summary["defined"] += 1
276+
# Set autostart and start if requested
277+
if b.autostart:
278+
try:
279+
net.setAutostart(1)
280+
except libvirt.libvirtError as e:
281+
summary["errors"].append(f"set-autostart:{b.name}:{e}")
282+
try:
283+
if net.isActive() != 1:
284+
net.create()
285+
summary["started"] += 1
286+
except libvirt.libvirtError as e:
287+
summary["errors"].append(f"start:{b.name}:{e}")
288+
except libvirt.libvirtError as e:
289+
summary["errors"].append(f"defineXML:{b.name}:{e}")
290+
291+
except Exception as e:
292+
# Catch-all per-bridge to avoid aborting the whole operation
293+
summary["errors"].append(f"unexpected:{b.name}:{e}")
294+
continue
295+
return LibvirtNetworkBridgeApplyResponse(
296+
checked=summary["checked"],
297+
removed=summary["removed"],
298+
defined=summary["defined"],
299+
started=summary["started"],
300+
errors=summary["errors"],
301+
)

backend/vm_manager/routes.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .vmbasic import VirtualMachineBasic, VirtualMachineBasicTemplate, OvmfPath, VirtualMachineXmlTemplate, VirtualMachineDeviceDiskFile, VirtualMachineDeviceNetwork, VirtualMachineDeviceDiskBlock, VirtualMachineDeviceDiskIscsi, VirtualMachineDevicePci
77
from .network import (
88
LibvirtNetworkBridge,
9+
LibvirtNetworkBridgeApplyResponse,
910
LibvirtNetworkBridgeResponse,
1011
LibvirtNetworkCustom,
1112
LibvirtNetworkCustomResponse,
@@ -14,6 +15,7 @@
1415
update_network_bridge,
1516
delete_network_bridge,
1617
create_network_bridge,
18+
apply_network_bridges,
1719
get_network_custom,
1820
get_network_custom_all,
1921
update_network_custom,
@@ -215,6 +217,16 @@ async def api_vm_network_bridge_delete(bridge_id: int, username: str = Depends(c
215217
raise HTTPException(status_code=404, detail="Network bridge not found")
216218
return
217219

220+
221+
@router.post("/network/bridge/apply", response_model=LibvirtNetworkBridgeApplyResponse)
222+
async def api_vm_network_bridges_apply(username: str = Depends(check_auth)):
223+
"""Apply all network bridge settings from the database to libvirt."""
224+
try:
225+
result = apply_network_bridges()
226+
except Exception as e:
227+
raise HTTPException(status_code=500, detail=str(e))
228+
return result
229+
218230
@router.get("/network/custom", response_model=list[LibvirtNetworkCustomResponse])
219231
async def api_vm_network_customs_get(username: str = Depends(check_auth)):
220232
return get_network_custom_all()

frontend/src/composables/useApi.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ export function useApi() {
188188
return data
189189
},
190190

191+
async applyBridgeNetworks() {
192+
const { data, error } = await client.POST('/api/vm/network/bridge/apply')
193+
if (error) throw error
194+
return data
195+
},
196+
191197
async getCustomNetworks() {
192198
const { data, error } = await client.GET('/api/vm/network/custom')
193199
if (error) throw error

frontend/src/pages/vm/NetworkOverviewPage.vue

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@
3232
>
3333
<ToolTip content="Add" />
3434
</q-btn>
35+
<q-btn
36+
flat
37+
round
38+
color="primary"
39+
icon="mdi-play-circle"
40+
@click="applyBridgeNetworks"
41+
>
42+
<ToolTip content="Apply" />
43+
</q-btn>
3544
<q-btn
3645
flat
3746
round
@@ -99,7 +108,8 @@
99108
<q-dialog v-model="networkBridgeEditDialog">
100109
<q-card style="min-width: 70vw">
101110
<q-card-section class="row items-center q-pb-none">
102-
<div class="text-h6">Edit bridge network '{{ editedBridge.name }}'</div>
111+
<div v-if="editedBridge && editedBridge.id" class="text-h6">Edit bridge network '{{ editedBridge.name }}'</div>
112+
<div v-else class="text-h6">Create bridge network</div>
103113
<q-space />
104114
<q-btn icon="close" flat round dense @click="networkBridgeEditDialog = false" />
105115
</q-card-section>
@@ -131,7 +141,8 @@
131141
<q-dialog v-model="networkCustomEditDialog">
132142
<q-card style="min-width: 70vw">
133143
<q-card-section class="row items-center q-pb-none">
134-
<div class="text-h6">Edit custom network '{{ editedCustom.name }}'</div>
144+
<div v-if="editedCustom && editedCustom.id" class="text-h6">Edit custom network '{{ editedCustom.name }}'</div>
145+
<div v-else class="text-h6">Create custom network</div>
135146
<q-space />
136147
<q-btn icon="close" flat round dense @click="networkCustomEditDialog = false" />
137148
</q-card-section>
@@ -163,6 +174,33 @@
163174
<ErrorDialog ref="errorDialog" />
164175
<ConfirmDialog ref="confirmDialog" />
165176
<ToolTip ref="toolTip" />
177+
<q-dialog v-model="applyResultDialog">
178+
<q-card>
179+
<q-card-section class="row items-center q-pb-none">
180+
<div class="text-h6">Apply Result</div>
181+
<q-space />
182+
<q-btn icon="close" flat round dense v-close-popup @click="applyResultDialog = false" />
183+
</q-card-section>
184+
<q-separator color="transparent" spaced="lg" inset />
185+
<q-card-section class="q-pt-none">
186+
<div v-if="applyResult">
187+
<div>Checked: {{ applyResult.checked }}</div>
188+
<div>Removed: {{ applyResult.removed }}</div>
189+
<div>Defined: {{ applyResult.defined }}</div>
190+
<div>Started: {{ applyResult.started }}</div>
191+
<div v-if="applyResult.errors && applyResult.errors.length">
192+
<div class="q-mt-sm text-subtitle2">Errors:</div>
193+
<ul>
194+
<li v-for="(err, idx) in applyResult.errors" :key="idx">{{ err }}</li>
195+
</ul>
196+
</div>
197+
</div>
198+
</q-card-section>
199+
<q-card-actions align="right">
200+
<q-btn flat label="Close" color="primary" @click="applyResultDialog = false" />
201+
</q-card-actions>
202+
</q-card>
203+
</q-dialog>
166204
</template>
167205

168206
<script>
@@ -223,6 +261,8 @@ export default {
223261
],
224262
networkBridgesLoading: false,
225263
networkBridgesSelected: [],
264+
applyResultDialog: false,
265+
applyResult: null,
226266
227267
networkCustomEditDialog: false,
228268
editedCustom: {},
@@ -456,6 +496,28 @@ export default {
456496
})
457497
},
458498
499+
applyBridgeNetworks() {
500+
this.$refs.confirmDialog.show(
501+
'Apply bridge networks',
502+
['This will remove any existing libvirt definitions for the bridges and (re)define and start networks configured in the database. Continue?'],
503+
() => {
504+
const api = useApi()
505+
api.vm
506+
.applyBridgeNetworks()
507+
.then((res) => {
508+
this.applyResult = res
509+
this.applyResultDialog = true
510+
// refresh data after applying
511+
this.getData()
512+
})
513+
.catch((error) => {
514+
this.$refs.errorDialog.show('Error applying bridge networks', [error?.detail || error.message])
515+
})
516+
},
517+
() => {}
518+
)
519+
},
520+
459521
},
460522
mounted() {
461523
this.getData()

frontend/src/types/api.d.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,26 @@ export interface paths {
196196
patch?: never;
197197
trace?: never;
198198
};
199+
"/api/vm/network/bridge/apply": {
200+
parameters: {
201+
query?: never;
202+
header?: never;
203+
path?: never;
204+
cookie?: never;
205+
};
206+
get?: never;
207+
put?: never;
208+
/**
209+
* Api Vm Network Bridges Apply
210+
* @description Apply all network bridge settings from the database to libvirt.
211+
*/
212+
post: operations["api_vm_network_bridges_apply_api_vm_network_bridge_apply_post"];
213+
delete?: never;
214+
options?: never;
215+
head?: never;
216+
patch?: never;
217+
trace?: never;
218+
};
199219
"/api/vm/network/custom": {
200220
parameters: {
201221
query?: never;
@@ -1119,6 +1139,19 @@ export interface components {
11191139
*/
11201140
autostart: boolean;
11211141
};
1142+
/** LibvirtNetworkBridgeApplyResponse */
1143+
LibvirtNetworkBridgeApplyResponse: {
1144+
/** Checked */
1145+
checked: number;
1146+
/** Removed */
1147+
removed: number;
1148+
/** Defined */
1149+
defined: number;
1150+
/** Started */
1151+
started: number;
1152+
/** Errors */
1153+
errors: string[];
1154+
};
11221155
/** LibvirtNetworkBridgeResponse */
11231156
LibvirtNetworkBridgeResponse: {
11241157
/** Id */
@@ -1962,6 +1995,26 @@ export interface operations {
19621995
};
19631996
};
19641997
};
1998+
api_vm_network_bridges_apply_api_vm_network_bridge_apply_post: {
1999+
parameters: {
2000+
query?: never;
2001+
header?: never;
2002+
path?: never;
2003+
cookie?: never;
2004+
};
2005+
requestBody?: never;
2006+
responses: {
2007+
/** @description Successful Response */
2008+
200: {
2009+
headers: {
2010+
[name: string]: unknown;
2011+
};
2012+
content: {
2013+
"application/json": components["schemas"]["LibvirtNetworkBridgeApplyResponse"];
2014+
};
2015+
};
2016+
};
2017+
};
19652018
api_vm_network_customs_get_api_vm_network_custom_get: {
19662019
parameters: {
19672020
query?: never;

0 commit comments

Comments
 (0)