1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
use thiserror::Error;
/// Core VMM error type.
#[derive(Debug, Error)]
pub enum VmmError {
/// The requested VM was not found.
#[error("VM not found: {0}")]
NotFound(String),
/// A VM with the given name already exists.
#[error("VM already exists: {0}")]
AlreadyExists(String),
/// The VM is not in a state that allows the requested operation.
#[error("VM '{id}' is in wrong state: expected {expected}, got {actual}")]
WrongState {
id: String,
expected: String,
actual: String,
},
/// The sandbox is paused. Distinct from [`Self::WrongState`] so callers
/// (the daemon's transparent auto-resume, CORE-21) can recognise
/// "paused" machine-readably instead of parsing state strings.
#[error("sandbox '{0}' is paused")]
Paused(String),
/// I/O error (file system, sockets, etc.).
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
/// JSON serialisation/deserialisation error.
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
/// Error from the Firecracker SDK.
#[error("fc-sdk error: {0}")]
Sdk(#[from] fc_sdk::Error),
/// Network-related error (TAP creation, IP allocation, etc.).
#[error("network error: {0}")]
Network(String),
/// Snapshot catalog error.
#[error("snapshot error: {0}")]
Snapshot(String),
/// Device-mapper / dm-snapshot error.
#[error("device-mapper error: {0}")]
DeviceMapper(String),
/// Process lifecycle error.
#[error("process error: {0}")]
Process(String),
/// Configuration error.
#[error("configuration error: {0}")]
Config(String),
/// Vsock / guest-agent communication error.
#[error("vsock error: {0}")]
Vsock(String),
/// A path inside a sandbox does not exist. The message shape is a
/// contract: the daemon's error classifier keys on the "path not
/// found:" prefix to attach the `FILE_NOT_FOUND` registry code.
#[error("path not found: {0}")]
PathNotFound(String),
/// A directory operation addressed a non-directory path.
#[error("not a directory: {0}")]
NotADirectory(String),
/// Refused to remove a non-empty directory without `recursive`
/// (`FAILED_PRECONDITION` per the filesystem contract).
#[error("directory not empty: {0}")]
DirectoryNotEmpty(String),
/// A catalog template reference did not resolve. The message shape is a
/// contract: the daemon's classifier keys on the "template not found:"
/// prefix to attach the `TEMPLATE_NOT_FOUND` registry code (CORE-107).
#[error("template not found: {0}")]
TemplateNotFound(String),
/// Publishing would repoint an existing immutable template version at
/// different content (409 on the daemon surface).
#[error("template version already exists: {0}")]
TemplateVersionExists(String),
/// A required precondition does not hold and retrying the same request
/// never helps (`FAILED_PRECONDITION` on the daemon surface).
#[error("failed precondition: {0}")]
FailedPrecondition(String),
/// A bounded wait elapsed before the awaited condition held
/// (`DEADLINE_EXCEEDED` on the daemon surface).
#[error("deadline exceeded: {0}")]
DeadlineExceeded(String),
/// A retryable operation whose durable result could not be confirmed.
#[error("service unavailable: {0}")]
Unavailable(String),
/// The operation's side effects committed — the sandbox exists and is
/// running — but the acknowledging record's durability is unconfirmed.
/// Distinct from [`VmmError::Unavailable`] so callers with a fallback
/// (warm create) can tell "nothing happened, retry freely" from "it
/// happened, do NOT re-execute".
#[error("sandbox {id} committed, but ACK durability is unconfirmed: {detail}")]
AckUnconfirmed {
/// The sandbox whose operation committed.
id: String,
/// The underlying durability failure.
detail: String,
},
/// A stdin write starts past the accepted byte count — the caller must
/// resume from `accepted` (its offsets have a gap).
#[error("stdin offset {offset} is past the {accepted} accepted bytes")]
StdinGap {
/// Bytes accepted so far — the offset the next write must start at.
accepted: u64,
/// The rejected write's offset.
offset: u64,
},
/// Generic catch-all error.
#[error("{0}")]
Other(String),
}
/// Convenience alias.
pub type Result<T> = std::result::Result<T, VmmError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_not_found_display() {
let e = VmmError::NotFound("vm-123".into());
assert_eq!(e.to_string(), "VM not found: vm-123");
}
#[test]
fn test_already_exists_display() {
let e = VmmError::AlreadyExists("my-vm".into());
assert_eq!(e.to_string(), "VM already exists: my-vm");
}
#[test]
fn test_wrong_state_display() {
let e = VmmError::WrongState {
id: "vm-1".into(),
expected: "running".into(),
actual: "stopped".into(),
};
let s = e.to_string();
assert!(s.contains("vm-1"));
assert!(s.contains("running"));
assert!(s.contains("stopped"));
}
#[test]
fn test_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let vmm_err = VmmError::from(io_err);
assert!(matches!(vmm_err, VmmError::Io(_)));
assert!(vmm_err.to_string().contains("I/O error"));
}
#[test]
fn test_network_error_display() {
let e = VmmError::Network("TAP creation failed".into());
assert_eq!(e.to_string(), "network error: TAP creation failed");
}
}