Skip to content

Commit 2b96ed3

Browse files
authored
feat: domain resume and suspend (#47)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added the ability to suspend and resume virtual machines, providing enhanced lifecycle management. - **Tests** - Expanded testing to verify that virtual machines properly transition between running, paused, and shutdown states. - **Chores** - Streamlined the Docker testing workflow by simplifying build and run commands for improved efficiency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent c995368 commit 2b96ed3

File tree

9 files changed

+288
-51
lines changed

9 files changed

+288
-51
lines changed

__tests__/domain-lifecycle.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,119 @@ describe('Domain Lifecycle Tests', () => {
172172
domain = null;
173173
}
174174
}, 60000);
175+
176+
it('should handle suspend and resume operations', async () => {
177+
// Create a copy of the NVRAM file for this VM
178+
if (archConfig.firmware) {
179+
execSync(`cp "${archConfig.firmware.vars}" "/tmp/test-vm.nvram"`);
180+
}
181+
182+
// Create a minimal VM configuration
183+
const domainDesc: DomainDesc = {
184+
type: 'qemu',
185+
name: TEST_VM_NAME,
186+
memory: { value: 512 * 1024 }, // 512MB RAM
187+
vcpu: { value: 1 },
188+
os: {
189+
type: {
190+
arch: archConfig.arch,
191+
machine: archConfig.machine,
192+
value: 'hvm'
193+
},
194+
boot: { dev: 'hd' }
195+
},
196+
devices: [
197+
{
198+
type: 'emulator',
199+
emulator: {
200+
value: archConfig.emulator
201+
}
202+
},
203+
{
204+
type: 'disk',
205+
disk: {
206+
type: 'file',
207+
device: 'disk',
208+
driver: {
209+
name: 'qemu',
210+
type: 'qcow2'
211+
},
212+
source: {
213+
file: DISK_IMAGE
214+
},
215+
target: {
216+
dev: 'vda',
217+
bus: 'virtio'
218+
}
219+
}
220+
},
221+
{
222+
type: 'console',
223+
console: {
224+
type: 'pty'
225+
}
226+
}
227+
]
228+
};
229+
230+
// Convert domain description to XML
231+
const xml = domainDescToXml(domainDesc);
232+
console.log('Generated XML:', xml);
233+
234+
try {
235+
// Define and start the domain
236+
domain = await connection.domainDefineXML(xml);
237+
console.log('Domain defined successfully');
238+
239+
if (!domain) {
240+
throw new Error('Failed to create domain');
241+
}
242+
243+
// Start the domain
244+
await domain.create();
245+
console.log('Domain created successfully');
246+
247+
// Verify initial running state
248+
const initialInfo = await connection.domainGetInfo(domain);
249+
console.log('Initial domain state:', initialInfo.state);
250+
expect(initialInfo.state).toBe(DomainState.RUNNING);
251+
252+
// Suspend the domain
253+
await domain.suspend();
254+
console.log('Domain suspended');
255+
256+
// Verify state after suspend
257+
const stateAfterSuspend = await connection.domainGetInfo(domain);
258+
console.log('Domain state after suspend:', stateAfterSuspend.state);
259+
expect(stateAfterSuspend.state).toBe(DomainState.PAUSED);
260+
261+
// Resume the domain
262+
await domain.resume();
263+
console.log('Domain resumed');
264+
265+
// Verify state after resume
266+
const stateAfterResume = await connection.domainGetInfo(domain);
267+
console.log('Domain state after resume:', stateAfterResume.state);
268+
expect(stateAfterResume.state).toBe(DomainState.RUNNING);
269+
270+
// Clean up by shutting down
271+
console.log('Performing cleanup shutdown...');
272+
await domain.destroy();
273+
274+
// Verify final state
275+
const finalState = (await domain.getInfo()).state;
276+
console.log('Final domain state:', finalState);
277+
expect(finalState).toBe(DomainState.SHUTOFF);
278+
279+
// Undefine the domain
280+
await domain.undefine();
281+
282+
} catch (error) {
283+
console.error('Error during VM operations:', error);
284+
throw error;
285+
} finally {
286+
// Clear the domain reference before cleanup
287+
domain = null;
288+
}
289+
}, 60000);
175290
});

lib/domain.spec.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, beforeEach, vi, expect } from 'vitest';
22
import { Domain } from './domain.js';
33
import { Hypervisor } from './hypervisor.js';
4-
import { DomainGetXMLDescFlags } from './types.js';
4+
import { DomainGetXMLDescFlags, DomainState } from './types.js';
55

66
describe('Domain', () => {
77
let domain: Domain;
@@ -16,7 +16,7 @@ describe('Domain', () => {
1616
const domainUndefine = vi.fn();
1717
const domainGetXMLDesc = vi.fn().mockResolvedValue('<domain/>');
1818
const domainGetInfo = vi.fn().mockResolvedValue({
19-
state: 1,
19+
state: DomainState.RUNNING,
2020
maxMem: 1024,
2121
memory: 512,
2222
nrVirtCpu: 1,
@@ -25,6 +25,8 @@ describe('Domain', () => {
2525
const domainGetID = vi.fn().mockResolvedValue(1);
2626
const domainGetName = vi.fn().mockResolvedValue('test-vm');
2727
const domainGetUUIDString = vi.fn().mockResolvedValue('123e4567-e89b-12d3-a456-426614174000');
28+
const domainSuspend = vi.fn();
29+
const domainResume = vi.fn();
2830

2931
// Create mock hypervisor
3032
hypervisor = {
@@ -37,7 +39,9 @@ describe('Domain', () => {
3739
domainGetInfo,
3840
domainGetID,
3941
domainGetName,
40-
domainGetUUIDString
42+
domainGetUUIDString,
43+
domainSuspend,
44+
domainResume
4145
} as unknown as Hypervisor;
4246

4347
// Mock the native domain
@@ -83,4 +87,18 @@ describe('Domain', () => {
8387
expect(hypervisor.domainGetUUIDString).toHaveBeenCalledWith(domain);
8488
});
8589
});
90+
91+
describe('suspend', () => {
92+
it('should suspend the domain', async () => {
93+
await domain.suspend();
94+
expect(hypervisor.domainSuspend).toHaveBeenCalledWith(domain);
95+
});
96+
});
97+
98+
describe('resume', () => {
99+
it('should resume the domain', async () => {
100+
await domain.resume();
101+
expect(hypervisor.domainResume).toHaveBeenCalledWith(domain);
102+
});
103+
});
86104
});

lib/domain.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@ export class Domain {
4747
return this.hypervisor.domainShutdown(this);
4848
}
4949

50+
/**
51+
* Pauses the domain's execution
52+
* @throws {LibvirtError} If pausing the domain fails
53+
*/
54+
async suspend(): Promise<void> {
55+
return this.hypervisor.domainSuspend(this);
56+
}
57+
58+
/**
59+
* Resumes a paused domain.
60+
* This operation restarts execution of a domain that was previously paused.
61+
* @throws {LibvirtError} If resuming the domain fails
62+
*/
63+
async resume(): Promise<void> {
64+
return this.hypervisor.domainResume(this);
65+
}
66+
5067
/**
5168
* Forcefully terminates the domain.
5269
* This is equivalent to pulling the power plug on a physical machine.

lib/hypervisor.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,30 @@ export class Hypervisor {
180180
);
181181
}
182182

183+
/**
184+
* Pauses a domain.
185+
* @param domain - The domain to pause
186+
* @throws {LibvirtError} If pausing the domain fails
187+
*/
188+
async domainSuspend(domain: Domain): Promise<void> {
189+
return wrapMethod(
190+
this.nativeHypervisor.domainSuspend.bind(this.nativeHypervisor),
191+
domain.getNativeDomain()
192+
);
193+
}
194+
195+
/**
196+
* Resumes a paused domain.
197+
* @param domain - The domain to resume
198+
* @throws {LibvirtError} If resuming the domain fails
199+
*/
200+
async domainResume(domain: Domain): Promise<void> {
201+
return wrapMethod(
202+
this.nativeHypervisor.domainResume.bind(this.nativeHypervisor),
203+
domain.getNativeDomain()
204+
);
205+
}
206+
183207
/**
184208
* Forcefully terminates a domain.
185209
* @param domain - The domain to destroy

scripts/test-docker.sh

Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,55 +16,26 @@ NC='\033[0m' # No Color
1616
run_tests() {
1717
local arch=$1
1818
local tag="libvirt-test-${arch}"
19-
local builder_tag="libvirt-builder-${arch}"
2019

21-
echo -e "${GREEN}Running tests on node:20-slim (${arch})${NC}"
22-
23-
# Try to build with caching first
24-
if docker buildx build \
25-
--platform linux/$arch \
26-
--target builder \
27-
-t $builder_tag \
28-
--cache-from $builder_tag \
29-
--cache-to $builder_tag \
20+
docker buildx build \
21+
--platform linux/"$arch" \
22+
-t "$tag" \
3023
--load \
31-
. 2>/dev/null; then
32-
33-
# If caching worked, build test stage with cache
34-
docker buildx build \
35-
--platform linux/$arch \
36-
--target test \
37-
-t $tag \
38-
--cache-from $tag \
39-
--cache-to $tag \
40-
--load \
41-
.
42-
else
43-
echo -e "${YELLOW}Caching not supported by Docker driver, using simple build${NC}"
44-
# Fall back to simple multi-stage build without caching
45-
docker buildx build \
46-
--platform linux/$arch \
47-
--target test \
48-
-t $tag \
49-
--load \
50-
.
51-
fi
24+
.
25+
5226

53-
# Run the container with necessary privileges
27+
# Run the container with necessary privileges and network setup
5428
docker run --rm \
55-
--platform linux/$arch \
29+
--platform linux/"$arch" \
5630
--privileged \
31+
--cap-add SYS_ADMIN \
32+
--cap-add NET_ADMIN \
33+
--device /dev/kvm \
34+
--network host \
5735
-v /var/run/libvirt/libvirt-sock:/var/run/libvirt/libvirt-sock \
5836
-v /var/lib/libvirt:/var/lib/libvirt \
59-
$tag
60-
61-
# Check the exit code
62-
if [ $? -eq 0 ]; then
63-
echo -e "${GREEN}Tests passed on node:20-slim (${arch})${NC}"
64-
else
65-
echo -e "${RED}Tests failed on node:20-slim (${arch})${NC}"
66-
exit 1
67-
fi
37+
-v /sys/fs/cgroup:/sys/fs/cgroup:ro \
38+
"$tag"
6839
}
6940

7041
# Main script
@@ -76,12 +47,7 @@ if ! docker buildx version >/dev/null 2>&1; then
7647
docker buildx install
7748
fi
7849

79-
# Create a temporary directory for test artifacts
80-
mkdir -p test-artifacts
81-
8250
# Run tests for each architecture
8351
for arch in "${ARCHES[@]}"; do
8452
run_tests "$arch"
8553
done
86-
87-
echo "All tests completed!"

src/domain.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class Domain : public Napi::ObjectWrap<Domain> {
4545
friend class DomainRestoreWorker;
4646
friend class DomainGetXMLDescWorker;
4747
friend class DomainUndefineWorker;
48+
friend class DomainSuspendWorker;
49+
friend class DomainResumeWorker;
4850
};
4951

5052
#endif // SRC_DOMAIN_H_

0 commit comments

Comments
 (0)