clonebox 0.1.2__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Clone your workstation environment to an isolated VM with selective apps, paths and services
5
5
  Author: CloneBox Team
6
6
  License: Apache-2.0
@@ -65,7 +65,28 @@ CloneBox lets you create isolated virtual machines with only the applications, d
65
65
 
66
66
  ## Installation
67
67
 
68
- ### Prerequisites
68
+ ### Quick Setup (Recommended)
69
+
70
+ Run the setup script to automatically install dependencies and configure the environment:
71
+
72
+ ```bash
73
+ # Clone the repository
74
+ git clone https://github.com/wronai/clonebox.git
75
+ cd clonebox
76
+
77
+ # Run the setup script
78
+ ./setup.sh
79
+ ```
80
+
81
+ The setup script will:
82
+ - Install all required packages (QEMU, libvirt, Python, etc.)
83
+ - Add your user to the necessary groups
84
+ - Configure libvirt networks
85
+ - Install clonebox in development mode
86
+
87
+ ### Manual Installation
88
+
89
+ #### Prerequisites
69
90
 
70
91
  ```bash
71
92
  # Install libvirt and QEMU/KVM
@@ -82,7 +103,7 @@ newgrp libvirt
82
103
  sudo apt install genisoimage
83
104
  ```
84
105
 
85
- ### Install CloneBox
106
+ #### Install CloneBox
86
107
 
87
108
  ```bash
88
109
  # From source
@@ -296,6 +317,63 @@ clonebox detect --yaml --dedupe -o my-config.yaml
296
317
  - Python 3.8+
297
318
  - User in `libvirt` group
298
319
 
320
+ ## Troubleshooting
321
+
322
+ ### Network Issues
323
+
324
+ If you encounter "Network not found" or "network 'default' is not active" errors:
325
+
326
+ ```bash
327
+ # Run the network fix script
328
+ ./fix-network.sh
329
+
330
+ # Or manually fix:
331
+ virsh --connect qemu:///session net-destroy default 2>/dev/null
332
+ virsh --connect qemu:///session net-undefine default 2>/dev/null
333
+ virsh --connect qemu:///session net-define /tmp/default-network.xml
334
+ virsh --connect qemu:///session net-start default
335
+ ```
336
+
337
+ ### Permission Issues
338
+
339
+ If you get permission errors:
340
+
341
+ ```bash
342
+ # Ensure user is in libvirt and kvm groups
343
+ sudo usermod -aG libvirt $USER
344
+ sudo usermod -aG kvm $USER
345
+
346
+ # Log out and log back in for groups to take effect
347
+ ```
348
+
349
+ ### VM Already Exists
350
+
351
+ If you get "domain already exists" error:
352
+
353
+ ```bash
354
+ # List VMs
355
+ clonebox list
356
+
357
+ # Stop and delete the existing VM
358
+ clonebox delete <vm-name>
359
+
360
+ # Or use virsh directly
361
+ virsh --connect qemu:///session destroy <vm-name>
362
+ virsh --connect qemu:///session undefine <vm-name>
363
+ ```
364
+
365
+ ### virt-viewer not found
366
+
367
+ If GUI doesn't open:
368
+
369
+ ```bash
370
+ # Install virt-viewer
371
+ sudo apt install virt-viewer
372
+
373
+ # Then connect manually
374
+ virt-viewer --connect qemu:///session <vm-name>
375
+ ```
376
+
299
377
  ## License
300
378
 
301
379
  MIT License - see [LICENSE](LICENSE) file.
@@ -26,7 +26,28 @@ CloneBox lets you create isolated virtual machines with only the applications, d
26
26
 
27
27
  ## Installation
28
28
 
29
- ### Prerequisites
29
+ ### Quick Setup (Recommended)
30
+
31
+ Run the setup script to automatically install dependencies and configure the environment:
32
+
33
+ ```bash
34
+ # Clone the repository
35
+ git clone https://github.com/wronai/clonebox.git
36
+ cd clonebox
37
+
38
+ # Run the setup script
39
+ ./setup.sh
40
+ ```
41
+
42
+ The setup script will:
43
+ - Install all required packages (QEMU, libvirt, Python, etc.)
44
+ - Add your user to the necessary groups
45
+ - Configure libvirt networks
46
+ - Install clonebox in development mode
47
+
48
+ ### Manual Installation
49
+
50
+ #### Prerequisites
30
51
 
31
52
  ```bash
32
53
  # Install libvirt and QEMU/KVM
@@ -43,7 +64,7 @@ newgrp libvirt
43
64
  sudo apt install genisoimage
44
65
  ```
45
66
 
46
- ### Install CloneBox
67
+ #### Install CloneBox
47
68
 
48
69
  ```bash
49
70
  # From source
@@ -257,6 +278,63 @@ clonebox detect --yaml --dedupe -o my-config.yaml
257
278
  - Python 3.8+
258
279
  - User in `libvirt` group
259
280
 
281
+ ## Troubleshooting
282
+
283
+ ### Network Issues
284
+
285
+ If you encounter "Network not found" or "network 'default' is not active" errors:
286
+
287
+ ```bash
288
+ # Run the network fix script
289
+ ./fix-network.sh
290
+
291
+ # Or manually fix:
292
+ virsh --connect qemu:///session net-destroy default 2>/dev/null
293
+ virsh --connect qemu:///session net-undefine default 2>/dev/null
294
+ virsh --connect qemu:///session net-define /tmp/default-network.xml
295
+ virsh --connect qemu:///session net-start default
296
+ ```
297
+
298
+ ### Permission Issues
299
+
300
+ If you get permission errors:
301
+
302
+ ```bash
303
+ # Ensure user is in libvirt and kvm groups
304
+ sudo usermod -aG libvirt $USER
305
+ sudo usermod -aG kvm $USER
306
+
307
+ # Log out and log back in for groups to take effect
308
+ ```
309
+
310
+ ### VM Already Exists
311
+
312
+ If you get "domain already exists" error:
313
+
314
+ ```bash
315
+ # List VMs
316
+ clonebox list
317
+
318
+ # Stop and delete the existing VM
319
+ clonebox delete <vm-name>
320
+
321
+ # Or use virsh directly
322
+ virsh --connect qemu:///session destroy <vm-name>
323
+ virsh --connect qemu:///session undefine <vm-name>
324
+ ```
325
+
326
+ ### virt-viewer not found
327
+
328
+ If GUI doesn't open:
329
+
330
+ ```bash
331
+ # Install virt-viewer
332
+ sudo apt install virt-viewer
333
+
334
+ # Then connect manually
335
+ virt-viewer --connect qemu:///session <vm-name>
336
+ ```
337
+
260
338
  ## License
261
339
 
262
340
  MIT License - see [LICENSE](LICENSE) file.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clonebox"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Clone your workstation environment to an isolated VM with selective apps, paths and services"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -492,7 +492,7 @@ def deduplicate_list(items: list, key=None) -> list:
492
492
 
493
493
 
494
494
  def generate_clonebox_yaml(snapshot, detector, deduplicate: bool = True,
495
- target_path: str = None, vm_name: str = None) -> str:
495
+ target_path: str = None, vm_name: str = None, network_mode: str = "auto") -> str:
496
496
  """Generate YAML config from system snapshot."""
497
497
  sys_info = detector.get_system_info()
498
498
 
@@ -559,6 +559,7 @@ def generate_clonebox_yaml(snapshot, detector, deduplicate: bool = True,
559
559
  "vcpus": vcpus,
560
560
  "gui": True,
561
561
  "base_image": None,
562
+ "network_mode": network_mode,
562
563
  },
563
564
  "services": services,
564
565
  "packages": [
@@ -605,6 +606,7 @@ def create_vm_from_config(config: dict, start: bool = False, user_session: bool
605
606
  packages=config.get("packages", []),
606
607
  services=config.get("services", []),
607
608
  user_session=user_session,
609
+ network_mode=config["vm"].get("network_mode", "auto"),
608
610
  )
609
611
 
610
612
  cloner = SelectiveVMCloner(user_session=user_session)
@@ -655,7 +657,8 @@ def cmd_clone(args):
655
657
  snapshot, detector,
656
658
  deduplicate=args.dedupe,
657
659
  target_path=str(target_path),
658
- vm_name=vm_name
660
+ vm_name=vm_name,
661
+ network_mode=args.network
659
662
  )
660
663
 
661
664
  # Save config file
@@ -863,6 +866,8 @@ def main():
863
866
  clone_parser.add_argument("--dedupe", action="store_true", default=True, help="Remove duplicate entries")
864
867
  clone_parser.add_argument("--user", "-u", action="store_true",
865
868
  help="Use user session (qemu:///session) - no root required, stores in ~/.local/share/libvirt/")
869
+ clone_parser.add_argument("--network", choices=["auto", "default", "user"], default="auto",
870
+ help="Network mode: auto (default), default (libvirt network), user (slirp)")
866
871
  clone_parser.set_defaults(func=cmd_clone)
867
872
 
868
873
  args = parser.parse_args()
@@ -32,6 +32,7 @@ class VMConfig:
32
32
  packages: list = field(default_factory=list)
33
33
  services: list = field(default_factory=list)
34
34
  user_session: bool = False # Use qemu:///session instead of qemu:///system
35
+ network_mode: str = "auto" # auto|default|user
35
36
 
36
37
  def to_dict(self) -> dict:
37
38
  return {
@@ -91,6 +92,25 @@ class SelectiveVMCloner:
91
92
  return self.USER_IMAGES_DIR
92
93
  return self.SYSTEM_IMAGES_DIR
93
94
 
95
+ def _default_network_active(self) -> bool:
96
+ """Check if libvirt default network is active."""
97
+ try:
98
+ net = self.conn.networkLookupByName("default")
99
+ return net.isActive() == 1
100
+ except libvirt.libvirtError:
101
+ return False
102
+
103
+ def resolve_network_mode(self, config: VMConfig) -> str:
104
+ """Resolve network mode based on config and session type."""
105
+ mode = (config.network_mode or "auto").lower()
106
+ if mode == "auto":
107
+ if self.user_session and not self._default_network_active():
108
+ return "user"
109
+ return "default"
110
+ if mode in {"default", "user"}:
111
+ return mode
112
+ return "default"
113
+
94
114
  def check_prerequisites(self) -> dict:
95
115
  """Check system prerequisites for VM creation."""
96
116
  images_dir = self.get_images_dir()
@@ -123,8 +143,11 @@ class SelectiveVMCloner:
123
143
  except libvirt.libvirtError:
124
144
  checks["network_error"] = (
125
145
  "Default network not found or inactive.\n"
126
- " Start it with: sudo virsh net-start default\n"
127
- " Or create it: sudo virsh net-define /usr/share/libvirt/networks/default.xml"
146
+ " For user session, CloneBox can use user-mode networking (slirp) automatically.\n"
147
+ " Or create a user network:\n"
148
+ " virsh --connect qemu:///session net-define /tmp/default-network.xml\n"
149
+ " virsh --connect qemu:///session net-start default\n"
150
+ " Or use system session: clonebox clone . (without --user)\n"
128
151
  )
129
152
 
130
153
  # Check images directory
@@ -215,6 +238,13 @@ class SelectiveVMCloner:
215
238
  cloudinit_iso = self._create_cloudinit_iso(vm_dir, config)
216
239
  log(f"[cyan]☁️ Created cloud-init ISO with {len(config.packages)} packages[/]")
217
240
 
241
+ # Resolve network mode
242
+ network_mode = self.resolve_network_mode(config)
243
+ if network_mode == "user":
244
+ log("[yellow]⚠️ Using user-mode networking (slirp) because default libvirt network is unavailable[/]")
245
+ else:
246
+ log(f"[dim]Network mode: {network_mode}[/]")
247
+
218
248
  # Generate VM XML
219
249
  vm_xml = self._generate_vm_xml(config, root_disk, cloudinit_iso)
220
250
 
@@ -288,9 +318,14 @@ class SelectiveVMCloner:
288
318
  ET.SubElement(fs, "target", dir=tag)
289
319
 
290
320
  # Network interface
291
- iface = ET.SubElement(devices, "interface", type="network")
292
- ET.SubElement(iface, "source", network="default")
293
- ET.SubElement(iface, "model", type="virtio")
321
+ network_mode = self.resolve_network_mode(config)
322
+ if network_mode == "user":
323
+ iface = ET.SubElement(devices, "interface", type="user")
324
+ ET.SubElement(iface, "model", type="virtio")
325
+ else:
326
+ iface = ET.SubElement(devices, "interface", type="network")
327
+ ET.SubElement(iface, "source", network="default")
328
+ ET.SubElement(iface, "model", type="virtio")
294
329
 
295
330
  # Serial console
296
331
  serial = ET.SubElement(devices, "serial", type="pty")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Clone your workstation environment to an isolated VM with selective apps, paths and services
5
5
  Author: CloneBox Team
6
6
  License: Apache-2.0
@@ -65,7 +65,28 @@ CloneBox lets you create isolated virtual machines with only the applications, d
65
65
 
66
66
  ## Installation
67
67
 
68
- ### Prerequisites
68
+ ### Quick Setup (Recommended)
69
+
70
+ Run the setup script to automatically install dependencies and configure the environment:
71
+
72
+ ```bash
73
+ # Clone the repository
74
+ git clone https://github.com/wronai/clonebox.git
75
+ cd clonebox
76
+
77
+ # Run the setup script
78
+ ./setup.sh
79
+ ```
80
+
81
+ The setup script will:
82
+ - Install all required packages (QEMU, libvirt, Python, etc.)
83
+ - Add your user to the necessary groups
84
+ - Configure libvirt networks
85
+ - Install clonebox in development mode
86
+
87
+ ### Manual Installation
88
+
89
+ #### Prerequisites
69
90
 
70
91
  ```bash
71
92
  # Install libvirt and QEMU/KVM
@@ -82,7 +103,7 @@ newgrp libvirt
82
103
  sudo apt install genisoimage
83
104
  ```
84
105
 
85
- ### Install CloneBox
106
+ #### Install CloneBox
86
107
 
87
108
  ```bash
88
109
  # From source
@@ -296,6 +317,63 @@ clonebox detect --yaml --dedupe -o my-config.yaml
296
317
  - Python 3.8+
297
318
  - User in `libvirt` group
298
319
 
320
+ ## Troubleshooting
321
+
322
+ ### Network Issues
323
+
324
+ If you encounter "Network not found" or "network 'default' is not active" errors:
325
+
326
+ ```bash
327
+ # Run the network fix script
328
+ ./fix-network.sh
329
+
330
+ # Or manually fix:
331
+ virsh --connect qemu:///session net-destroy default 2>/dev/null
332
+ virsh --connect qemu:///session net-undefine default 2>/dev/null
333
+ virsh --connect qemu:///session net-define /tmp/default-network.xml
334
+ virsh --connect qemu:///session net-start default
335
+ ```
336
+
337
+ ### Permission Issues
338
+
339
+ If you get permission errors:
340
+
341
+ ```bash
342
+ # Ensure user is in libvirt and kvm groups
343
+ sudo usermod -aG libvirt $USER
344
+ sudo usermod -aG kvm $USER
345
+
346
+ # Log out and log back in for groups to take effect
347
+ ```
348
+
349
+ ### VM Already Exists
350
+
351
+ If you get "domain already exists" error:
352
+
353
+ ```bash
354
+ # List VMs
355
+ clonebox list
356
+
357
+ # Stop and delete the existing VM
358
+ clonebox delete <vm-name>
359
+
360
+ # Or use virsh directly
361
+ virsh --connect qemu:///session destroy <vm-name>
362
+ virsh --connect qemu:///session undefine <vm-name>
363
+ ```
364
+
365
+ ### virt-viewer not found
366
+
367
+ If GUI doesn't open:
368
+
369
+ ```bash
370
+ # Install virt-viewer
371
+ sudo apt install virt-viewer
372
+
373
+ # Then connect manually
374
+ virt-viewer --connect qemu:///session <vm-name>
375
+ ```
376
+
299
377
  ## License
300
378
 
301
379
  MIT License - see [LICENSE](LICENSE) file.
@@ -13,4 +13,5 @@ src/clonebox.egg-info/requires.txt
13
13
  src/clonebox.egg-info/top_level.txt
14
14
  tests/test_cli.py
15
15
  tests/test_cloner.py
16
- tests/test_detector.py
16
+ tests/test_detector.py
17
+ tests/test_network.py
@@ -104,9 +104,16 @@ class TestSelectiveVMClonerInit:
104
104
 
105
105
  @patch('clonebox.cloner.libvirt')
106
106
  def test_init_connection_failed(self, mock_libvirt):
107
- import libvirt as real_libvirt
108
- mock_libvirt.libvirtError = real_libvirt.libvirtError
109
- mock_libvirt.open.side_effect = real_libvirt.libvirtError("Connection refused")
107
+ try:
108
+ import libvirt as real_libvirt
109
+ mock_libvirt.libvirtError = real_libvirt.libvirtError
110
+ mock_libvirt.open.side_effect = real_libvirt.libvirtError("Connection refused")
111
+ except ImportError:
112
+ # If libvirt is not installed, create a mock exception
113
+ class MockLibvirtError(Exception):
114
+ pass
115
+ mock_libvirt.libvirtError = MockLibvirtError
116
+ mock_libvirt.open.side_effect = MockLibvirtError("Connection refused")
110
117
 
111
118
  with pytest.raises(ConnectionError) as exc_info:
112
119
  SelectiveVMCloner()
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for network mode functionality."""
3
+
4
+ import pytest
5
+ from pathlib import Path
6
+ from unittest.mock import patch, MagicMock
7
+
8
+ from clonebox.cloner import VMConfig, SelectiveVMCloner
9
+
10
+
11
+ class TestNetworkMode:
12
+ """Test network mode resolution and fallback."""
13
+
14
+ @patch('clonebox.cloner.libvirt')
15
+ def test_vm_config_network_mode_default(self, mock_libvirt):
16
+ config = VMConfig()
17
+ assert config.network_mode == "auto"
18
+
19
+ @patch('clonebox.cloner.libvirt')
20
+ def test_vm_config_network_mode_custom(self, mock_libvirt):
21
+ config = VMConfig(network_mode="user")
22
+ assert config.network_mode == "user"
23
+
24
+ @patch('clonebox.cloner.libvirt')
25
+ def test_resolve_network_mode_auto_system(self, mock_libvirt):
26
+ """Test auto mode with system session uses default network."""
27
+ mock_conn = MagicMock()
28
+ mock_libvirt.open.return_value = mock_conn
29
+
30
+ cloner = SelectiveVMCloner(user_session=False)
31
+ config = VMConfig(network_mode="auto")
32
+
33
+ mode = cloner.resolve_network_mode(config)
34
+ assert mode == "default"
35
+
36
+ @patch('clonebox.cloner.libvirt')
37
+ def test_resolve_network_mode_auto_user_with_default(self, mock_libvirt):
38
+ """Test auto mode with user session and default network available."""
39
+ mock_conn = MagicMock()
40
+ mock_net = MagicMock()
41
+ mock_net.isActive.return_value = 1
42
+ mock_conn.networkLookupByName.return_value = mock_net
43
+ mock_libvirt.open.return_value = mock_conn
44
+
45
+ cloner = SelectiveVMCloner(user_session=True)
46
+ config = VMConfig(network_mode="auto")
47
+
48
+ mode = cloner.resolve_network_mode(config)
49
+ assert mode == "default"
50
+
51
+ @patch('clonebox.cloner.libvirt')
52
+ def test_resolve_network_mode_auto_user_no_default(self, mock_libvirt):
53
+ """Test auto mode with user session and no default network falls back to user."""
54
+ import libvirt as real_libvirt
55
+ mock_conn = MagicMock()
56
+ mock_conn.networkLookupByName.side_effect = real_libvirt.libvirtError("No network")
57
+ mock_libvirt.open.return_value = mock_conn
58
+
59
+ cloner = SelectiveVMCloner(user_session=True)
60
+ config = VMConfig(network_mode="auto")
61
+
62
+ mode = cloner.resolve_network_mode(config)
63
+ assert mode == "user"
64
+
65
+ @patch('clonebox.cloner.libvirt')
66
+ def test_resolve_network_mode_explicit_default(self, mock_libvirt):
67
+ """Test explicit default mode."""
68
+ mock_conn = MagicMock()
69
+ mock_libvirt.open.return_value = mock_conn
70
+
71
+ cloner = SelectiveVMCloner(user_session=True)
72
+ config = VMConfig(network_mode="default")
73
+
74
+ mode = cloner.resolve_network_mode(config)
75
+ assert mode == "default"
76
+
77
+ @patch('clonebox.cloner.libvirt')
78
+ def test_resolve_network_mode_explicit_user(self, mock_libvirt):
79
+ """Test explicit user mode."""
80
+ mock_conn = MagicMock()
81
+ mock_libvirt.open.return_value = mock_conn
82
+
83
+ cloner = SelectiveVMCloner(user_session=False)
84
+ config = VMConfig(network_mode="user")
85
+
86
+ mode = cloner.resolve_network_mode(config)
87
+ assert mode == "user"
88
+
89
+ @patch('clonebox.cloner.libvirt')
90
+ def test_resolve_network_mode_invalid(self, mock_libvirt):
91
+ """Test invalid network mode falls back to default."""
92
+ mock_conn = MagicMock()
93
+ mock_libvirt.open.return_value = mock_conn
94
+
95
+ cloner = SelectiveVMCloner()
96
+ config = VMConfig(network_mode="invalid")
97
+
98
+ mode = cloner.resolve_network_mode(config)
99
+ assert mode == "default"
100
+
101
+ @patch('clonebox.cloner.libvirt')
102
+ def test_default_network_active_true(self, mock_libvirt):
103
+ """Test _default_network_active returns True when network is active."""
104
+ mock_conn = MagicMock()
105
+ mock_net = MagicMock()
106
+ mock_net.isActive.return_value = 1
107
+ mock_conn.networkLookupByName.return_value = mock_net
108
+ mock_libvirt.open.return_value = mock_conn
109
+
110
+ cloner = SelectiveVMCloner()
111
+ assert cloner._default_network_active() is True
112
+
113
+ @patch('clonebox.cloner.libvirt')
114
+ def test_default_network_active_false(self, mock_libvirt):
115
+ """Test _default_network_active returns False when network is inactive."""
116
+ import libvirt as real_libvirt
117
+ mock_conn = MagicMock()
118
+ mock_net = MagicMock()
119
+ mock_net.isActive.return_value = 0
120
+ mock_conn.networkLookupByName.return_value = mock_net
121
+ mock_libvirt.open.return_value = mock_conn
122
+
123
+ cloner = SelectiveVMCloner()
124
+ assert cloner._default_network_active() is False
125
+
126
+ @patch('clonebox.cloner.libvirt')
127
+ def test_default_network_active_not_found(self, mock_libvirt):
128
+ """Test _default_network_active returns False when network not found."""
129
+ import libvirt as real_libvirt
130
+ mock_conn = MagicMock()
131
+ mock_conn.networkLookupByName.side_effect = real_libvirt.libvirtError("Not found")
132
+ mock_libvirt.open.return_value = mock_conn
133
+
134
+ cloner = SelectiveVMCloner()
135
+ assert cloner._default_network_active() is False
136
+
137
+ @patch('clonebox.cloner.libvirt')
138
+ def test_generate_vm_xml_user_network(self, mock_libvirt):
139
+ """Test VM XML generation with user network."""
140
+ mock_conn = MagicMock()
141
+ mock_libvirt.open.return_value = mock_conn
142
+
143
+ cloner = SelectiveVMCloner()
144
+ config = VMConfig(name="test-vm", network_mode="user")
145
+
146
+ xml = cloner._generate_vm_xml(config, Path("/tmp/root.qcow2"), None)
147
+
148
+ assert '<interface type="user">' in xml
149
+ assert '<interface type="network">' not in xml
150
+
151
+ @patch('clonebox.cloner.libvirt')
152
+ def test_generate_vm_xml_default_network(self, mock_libvirt):
153
+ """Test VM XML generation with default network."""
154
+ mock_conn = MagicMock()
155
+ mock_libvirt.open.return_value = mock_conn
156
+
157
+ cloner = SelectiveVMCloner()
158
+ config = VMConfig(name="test-vm", network_mode="default")
159
+
160
+ xml = cloner._generate_vm_xml(config, Path("/tmp/root.qcow2"), None)
161
+
162
+ assert '<interface type="network">' in xml
163
+ assert '<source network="default"/>' in xml
164
+ assert '<interface type="user">' not in xml
File without changes
File without changes
File without changes