]>
Commit | Line | Data |
---|---|---|
4ebf0eab | 1 | use failure::*; |
fc189b19 | 2 | use ::serde::{Deserialize, Serialize}; |
4ebf0eab | 3 | |
9ea4bce4 WB |
4 | use proxmox::api::{api, schema::*}; |
5 | use proxmox::const_regex; | |
6 | use proxmox::{IPRE, IPV4RE, IPV6RE, IPV4OCTET, IPV6H16, IPV6LS32}; | |
255f378a DM |
7 | |
8 | // File names: may not contain slashes, may not start with "." | |
9 | pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| { | |
10 | if name.starts_with('.') { | |
11 | bail!("file names may not start with '.'"); | |
12 | } | |
13 | if name.contains('/') { | |
14 | bail!("file names may not contain slashes"); | |
15 | } | |
16 | Ok(()) | |
17 | }); | |
18 | ||
b25f313d DM |
19 | macro_rules! DNS_LABEL { () => (r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)") } |
20 | macro_rules! DNS_NAME { () => (concat!(r"(?:", DNS_LABEL!() , r"\.)*", DNS_LABEL!())) } | |
255f378a | 21 | |
163dc16c DM |
22 | // we only allow a limited set of characters |
23 | // colon is not allowed, because we store usernames in | |
24 | // colon separated lists)! | |
25 | // slash is not allowed because it is used as pve API delimiter | |
26 | // also see "man useradd" | |
ae62c4fe | 27 | macro_rules! USER_NAME_REGEX_STR { () => (r"(?:[^\s:/[:cntrl:]]+)") } |
163dc16c DM |
28 | |
29 | macro_rules! PROXMOX_SAFE_ID_REGEX_STR { () => (r"(?:[A-Za-z0-9_][A-Za-z0-9._\-]*)") } | |
30 | ||
255f378a DM |
31 | const_regex!{ |
32 | pub IP_FORMAT_REGEX = IPRE!(); | |
33 | pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$"; // fixme: define in common_regex ? | |
34 | pub SYSTEMD_DATETIME_REGEX = r"^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$"; // fixme: define in common_regex ? | |
d0adf270 | 35 | |
da4a15a3 DM |
36 | pub PASSWORD_REGEX = r"^[[:^cntrl:]]*$"; // everything but control characters |
37 | ||
d0adf270 DM |
38 | /// Regex for safe identifiers. |
39 | /// | |
40 | /// This | |
41 | /// [article](https://dwheeler.com/essays/fixing-unix-linux-filenames.html) | |
42 | /// contains further information why it is reasonable to restict | |
43 | /// names this way. This is not only useful for filenames, but for | |
44 | /// any identifier command line tools work with. | |
163dc16c | 45 | pub PROXMOX_SAFE_ID_REGEX = concat!(r"^", PROXMOX_SAFE_ID_REGEX_STR!(), r"$"); |
454c13ed DM |
46 | |
47 | pub SINGLE_LINE_COMMENT_REGEX = r"^[[:^cntrl:]]*$"; | |
b25f313d DM |
48 | |
49 | pub HOSTNAME_REGEX = r"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)$"; | |
50 | ||
ae62c4fe | 51 | pub DNS_NAME_REGEX = concat!(r"^", DNS_NAME!(), r"$"); |
b25f313d | 52 | |
ae62c4fe | 53 | pub DNS_NAME_OR_IP_REGEX = concat!(r"^", DNS_NAME!(), "|", IPRE!(), r"$"); |
163dc16c DM |
54 | |
55 | pub PROXMOX_USER_ID_REGEX = concat!(r"^", USER_NAME_REGEX_STR!(), r"@", PROXMOX_SAFE_ID_REGEX_STR!(), r"$"); | |
dcb8db66 DM |
56 | |
57 | pub CERT_FINGERPRINT_SHA256_REGEX = r"^(?:[0-9a-fA-F][0-9a-fA-F])(?::[0-9a-fA-F][0-9a-fA-F]){31}$"; | |
255f378a | 58 | } |
4ebf0eab | 59 | |
255f378a DM |
60 | pub const SYSTEMD_DATETIME_FORMAT: ApiStringFormat = |
61 | ApiStringFormat::Pattern(&SYSTEMD_DATETIME_REGEX); | |
4ebf0eab | 62 | |
255f378a DM |
63 | pub const IP_FORMAT: ApiStringFormat = |
64 | ApiStringFormat::Pattern(&IP_FORMAT_REGEX); | |
bbf9e7e9 | 65 | |
255f378a DM |
66 | pub const PVE_CONFIG_DIGEST_FORMAT: ApiStringFormat = |
67 | ApiStringFormat::Pattern(&SHA256_HEX_REGEX); | |
68 | ||
dcb8db66 DM |
69 | pub const CERT_FINGERPRINT_SHA256_FORMAT: ApiStringFormat = |
70 | ApiStringFormat::Pattern(&CERT_FINGERPRINT_SHA256_REGEX); | |
71 | ||
d0adf270 DM |
72 | pub const PROXMOX_SAFE_ID_FORMAT: ApiStringFormat = |
73 | ApiStringFormat::Pattern(&PROXMOX_SAFE_ID_REGEX); | |
74 | ||
454c13ed DM |
75 | pub const SINGLE_LINE_COMMENT_FORMAT: ApiStringFormat = |
76 | ApiStringFormat::Pattern(&SINGLE_LINE_COMMENT_REGEX); | |
77 | ||
b25f313d DM |
78 | pub const HOSTNAME_FORMAT: ApiStringFormat = |
79 | ApiStringFormat::Pattern(&HOSTNAME_REGEX); | |
80 | ||
81 | pub const DNS_NAME_FORMAT: ApiStringFormat = | |
82 | ApiStringFormat::Pattern(&DNS_NAME_REGEX); | |
83 | ||
84 | pub const DNS_NAME_OR_IP_FORMAT: ApiStringFormat = | |
85 | ApiStringFormat::Pattern(&DNS_NAME_OR_IP_REGEX); | |
86 | ||
163dc16c DM |
87 | pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat = |
88 | ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX); | |
89 | ||
7e7b781a DM |
90 | pub const PASSWORD_FORMAT: ApiStringFormat = |
91 | ApiStringFormat::Pattern(&PASSWORD_REGEX); | |
92 | ||
454c13ed | 93 | |
dcb8db66 DM |
94 | |
95 | pub const CERT_FINGERPRINT_SHA256_SCHEMA: Schema = StringSchema::new( | |
96 | "X509 certificate fingerprint (sha256)." | |
97 | ) | |
98 | .format(&CERT_FINGERPRINT_SHA256_FORMAT) | |
99 | .schema(); | |
100 | ||
002a191a | 101 | pub const PROXMOX_CONFIG_DIGEST_SCHEMA: Schema = StringSchema::new(r#"\ |
255f378a DM |
102 | Prevent changes if current configuration file has different SHA256 digest. |
103 | This can be used to prevent concurrent modifications. | |
104 | "# | |
105 | ) | |
106 | .format(&PVE_CONFIG_DIGEST_FORMAT) | |
107 | .schema(); | |
108 | ||
109 | ||
110 | pub const CHUNK_DIGEST_FORMAT: ApiStringFormat = | |
111 | ApiStringFormat::Pattern(&SHA256_HEX_REGEX); | |
112 | ||
113 | pub const CHUNK_DIGEST_SCHEMA: Schema = StringSchema::new("Chunk digest (SHA256).") | |
114 | .format(&CHUNK_DIGEST_FORMAT) | |
115 | .schema(); | |
116 | ||
117 | pub const NODE_SCHEMA: Schema = StringSchema::new("Node name (or 'localhost')") | |
118 | .format(&ApiStringFormat::VerifyFn(|node| { | |
119 | if node == "localhost" || node == proxmox::tools::nodename() { | |
120 | Ok(()) | |
121 | } else { | |
122 | bail!("no such node '{}'", node); | |
123 | } | |
124 | })) | |
125 | .schema(); | |
126 | ||
127 | pub const SEARCH_DOMAIN_SCHEMA: Schema = | |
128 | StringSchema::new("Search domain for host-name lookup.").schema(); | |
129 | ||
130 | pub const FIRST_DNS_SERVER_SCHEMA: Schema = | |
131 | StringSchema::new("First name server IP address.") | |
132 | .format(&IP_FORMAT) | |
133 | .schema(); | |
134 | ||
135 | pub const SECOND_DNS_SERVER_SCHEMA: Schema = | |
136 | StringSchema::new("Second name server IP address.") | |
137 | .format(&IP_FORMAT) | |
138 | .schema(); | |
139 | ||
140 | pub const THIRD_DNS_SERVER_SCHEMA: Schema = | |
141 | StringSchema::new("Third name server IP address.") | |
142 | .format(&IP_FORMAT) | |
143 | .schema(); | |
144 | ||
145 | pub const BACKUP_ARCHIVE_NAME_SCHEMA: Schema = | |
146 | StringSchema::new("Backup archive name.") | |
1ae5677d | 147 | .format(&PROXMOX_SAFE_ID_FORMAT) |
255f378a DM |
148 | .schema(); |
149 | ||
150 | pub const BACKUP_TYPE_SCHEMA: Schema = | |
151 | StringSchema::new("Backup type.") | |
152 | .format(&ApiStringFormat::Enum(&["vm", "ct", "host"])) | |
153 | .schema(); | |
154 | ||
155 | pub const BACKUP_ID_SCHEMA: Schema = | |
156 | StringSchema::new("Backup ID.") | |
1ae5677d | 157 | .format(&PROXMOX_SAFE_ID_FORMAT) |
255f378a DM |
158 | .schema(); |
159 | ||
160 | pub const BACKUP_TIME_SCHEMA: Schema = | |
161 | IntegerSchema::new("Backup time (Unix epoch.)") | |
162 | .minimum(1_547_797_308) | |
163 | .schema(); | |
5830c205 DM |
164 | |
165 | pub const UPID_SCHEMA: Schema = StringSchema::new("Unique Process/Task ID.") | |
166 | .max_length(256) | |
167 | .schema(); | |
66c49c21 DM |
168 | |
169 | pub const DATASTORE_SCHEMA: Schema = StringSchema::new("Datastore name.") | |
d0adf270 | 170 | .format(&PROXMOX_SAFE_ID_FORMAT) |
688fbe07 | 171 | .min_length(3) |
66c49c21 DM |
172 | .max_length(32) |
173 | .schema(); | |
fc189b19 | 174 | |
167971ed DM |
175 | pub const REMOTE_ID_SCHEMA: Schema = StringSchema::new("Remote ID.") |
176 | .format(&PROXMOX_SAFE_ID_FORMAT) | |
177 | .min_length(3) | |
178 | .max_length(32) | |
179 | .schema(); | |
180 | ||
454c13ed DM |
181 | pub const SINGLE_LINE_COMMENT_SCHEMA: Schema = StringSchema::new("Comment (single line).") |
182 | .format(&SINGLE_LINE_COMMENT_FORMAT) | |
183 | .schema(); | |
fc189b19 | 184 | |
b25f313d DM |
185 | pub const HOSTNAME_SCHEMA: Schema = StringSchema::new("Hostname (as defined in RFC1123).") |
186 | .format(&HOSTNAME_FORMAT) | |
187 | .schema(); | |
188 | ||
189 | pub const DNS_NAME_OR_IP_SCHEMA: Schema = StringSchema::new("DNS name or IP address.") | |
190 | .format(&DNS_NAME_OR_IP_FORMAT) | |
191 | .schema(); | |
192 | ||
163dc16c DM |
193 | pub const PROXMOX_AUTH_REALM_SCHEMA: Schema = StringSchema::new("Authentication domain ID") |
194 | .format(&PROXMOX_SAFE_ID_FORMAT) | |
195 | .min_length(3) | |
196 | .max_length(32) | |
197 | .schema(); | |
198 | ||
199 | pub const PROXMOX_USER_ID_SCHEMA: Schema = StringSchema::new("User ID") | |
200 | .format(&PROXMOX_USER_ID_FORMAT) | |
201 | .min_length(3) | |
202 | .max_length(64) | |
203 | .schema(); | |
204 | ||
fc189b19 DM |
205 | |
206 | // Complex type definitions | |
207 | ||
b31c8019 DM |
208 | #[api( |
209 | properties: { | |
210 | "backup-type": { | |
211 | schema: BACKUP_TYPE_SCHEMA, | |
212 | }, | |
213 | "backup-id": { | |
214 | schema: BACKUP_ID_SCHEMA, | |
215 | }, | |
216 | "last-backup": { | |
217 | schema: BACKUP_TIME_SCHEMA, | |
218 | }, | |
219 | "backup-count": { | |
220 | type: Integer, | |
221 | }, | |
222 | files: { | |
223 | items: { | |
224 | schema: BACKUP_ARCHIVE_NAME_SCHEMA | |
225 | }, | |
226 | }, | |
227 | }, | |
228 | )] | |
229 | #[derive(Serialize, Deserialize)] | |
230 | #[serde(rename_all="kebab-case")] | |
231 | /// Basic information about a backup group. | |
232 | pub struct GroupListItem { | |
233 | pub backup_type: String, // enum | |
234 | pub backup_id: String, | |
235 | pub last_backup: i64, | |
236 | /// Number of contained snapshots | |
237 | pub backup_count: u64, | |
238 | /// List of contained archive files. | |
239 | pub files: Vec<String>, | |
240 | } | |
241 | ||
fc189b19 | 242 | #[api( |
fc189b19 DM |
243 | properties: { |
244 | "backup-type": { | |
245 | schema: BACKUP_TYPE_SCHEMA, | |
246 | }, | |
247 | "backup-id": { | |
248 | schema: BACKUP_ID_SCHEMA, | |
249 | }, | |
250 | "backup-time": { | |
251 | schema: BACKUP_TIME_SCHEMA, | |
252 | }, | |
71da3d6a DM |
253 | files: { |
254 | items: { | |
255 | schema: BACKUP_ARCHIVE_NAME_SCHEMA | |
256 | }, | |
257 | }, | |
fc189b19 DM |
258 | }, |
259 | )] | |
260 | #[derive(Serialize, Deserialize)] | |
261 | #[serde(rename_all="kebab-case")] | |
71da3d6a | 262 | /// Basic information about backup snapshot. |
fc189b19 DM |
263 | pub struct SnapshotListItem { |
264 | pub backup_type: String, // enum | |
265 | pub backup_id: String, | |
266 | pub backup_time: i64, | |
71da3d6a | 267 | /// List of contained archive files. |
fc189b19 | 268 | pub files: Vec<String>, |
71da3d6a | 269 | /// Overall snapshot size (sum of all archive sizes). |
fc189b19 DM |
270 | #[serde(skip_serializing_if="Option::is_none")] |
271 | pub size: Option<u64>, | |
272 | } | |
ff620a3d | 273 | |
09b1f7b2 DM |
274 | #[api( |
275 | properties: { | |
276 | "filename": { | |
277 | schema: BACKUP_ARCHIVE_NAME_SCHEMA, | |
278 | }, | |
279 | }, | |
280 | )] | |
281 | #[derive(Serialize, Deserialize)] | |
282 | #[serde(rename_all="kebab-case")] | |
283 | /// Basic information about archive files inside a backup snapshot. | |
284 | pub struct BackupContent { | |
285 | pub filename: String, | |
286 | /// Archive size (from backup manifest). | |
287 | #[serde(skip_serializing_if="Option::is_none")] | |
288 | pub size: Option<u64>, | |
289 | } | |
290 | ||
a92830dc DM |
291 | #[api( |
292 | properties: { | |
293 | "upid": { | |
294 | optional: true, | |
295 | schema: UPID_SCHEMA, | |
296 | }, | |
297 | }, | |
298 | )] | |
299 | #[derive(Clone, Serialize, Deserialize)] | |
300 | #[serde(rename_all="kebab-case")] | |
301 | /// Garbage collection status. | |
302 | pub struct GarbageCollectionStatus { | |
303 | pub upid: Option<String>, | |
304 | /// Number of processed index files. | |
305 | pub index_file_count: usize, | |
306 | /// Sum of bytes referred by index files. | |
307 | pub index_data_bytes: u64, | |
308 | /// Bytes used on disk. | |
309 | pub disk_bytes: u64, | |
310 | /// Chunks used on disk. | |
311 | pub disk_chunks: usize, | |
312 | /// Sum of removed bytes. | |
313 | pub removed_bytes: u64, | |
314 | /// Number of removed chunks. | |
315 | pub removed_chunks: usize, | |
cf459b19 DM |
316 | /// Sum of pending bytes (pending removal - kept for safety). |
317 | pub pending_bytes: u64, | |
318 | /// Number of pending chunks (pending removal - kept for safety). | |
319 | pub pending_chunks: usize, | |
a92830dc DM |
320 | } |
321 | ||
322 | impl Default for GarbageCollectionStatus { | |
323 | fn default() -> Self { | |
324 | GarbageCollectionStatus { | |
325 | upid: None, | |
326 | index_file_count: 0, | |
327 | index_data_bytes: 0, | |
328 | disk_bytes: 0, | |
329 | disk_chunks: 0, | |
330 | removed_bytes: 0, | |
331 | removed_chunks: 0, | |
cf459b19 DM |
332 | pending_bytes: 0, |
333 | pending_chunks: 0, | |
a92830dc DM |
334 | } |
335 | } | |
336 | } | |
337 | ||
338 | ||
1dc117bb DM |
339 | #[api()] |
340 | #[derive(Serialize, Deserialize)] | |
341 | /// Storage space usage information. | |
342 | pub struct StorageStatus { | |
343 | /// Total space (bytes). | |
344 | pub total: u64, | |
345 | /// Used space (bytes). | |
346 | pub used: u64, | |
347 | /// Available space (bytes). | |
348 | pub avail: u64, | |
349 | } | |
ff620a3d | 350 | |
99384f79 DM |
351 | #[api( |
352 | properties: { | |
353 | "upid": { schema: UPID_SCHEMA }, | |
354 | }, | |
355 | )] | |
356 | #[derive(Serialize, Deserialize)] | |
357 | /// Task properties. | |
358 | pub struct TaskListItem { | |
359 | pub upid: String, | |
360 | /// The node name where the task is running on. | |
361 | pub node: String, | |
362 | /// The Unix PID | |
363 | pub pid: i64, | |
364 | /// The task start time (Epoch) | |
365 | pub pstart: u64, | |
366 | /// The task start time (Epoch) | |
367 | pub starttime: i64, | |
368 | /// Worker type (arbitrary ASCII string) | |
369 | pub worker_type: String, | |
370 | /// Worker ID (arbitrary ASCII string) | |
371 | pub worker_id: Option<String>, | |
372 | /// The user who started the task | |
373 | pub user: String, | |
374 | /// The task end time (Epoch) | |
375 | #[serde(skip_serializing_if="Option::is_none")] | |
376 | pub endtime: Option<i64>, | |
377 | /// Task end status | |
378 | #[serde(skip_serializing_if="Option::is_none")] | |
379 | pub status: Option<String>, | |
380 | } | |
381 | ||
ff620a3d DM |
382 | // Regression tests |
383 | ||
dcb8db66 DM |
384 | #[test] |
385 | fn test_cert_fingerprint_schema() -> Result<(), Error> { | |
386 | ||
387 | let schema = CERT_FINGERPRINT_SHA256_SCHEMA; | |
388 | ||
389 | let invalid_fingerprints = [ | |
390 | "86:88:7c:be:26:77:a5:62:67:d9:06:f5:e4::61:3e:20:dc:cd:43:92:07:7f:fb:65:54:6c:ff:d2:96:36:f8", | |
391 | "88:7C:BE:26:77:a5:62:67:D9:06:f5:e4:14:61:3e:20:dc:cd:43:92:07:7f:fb:65:54:6c:ff:d2:96:36:f8", | |
392 | "86:88:7c:be:26:77:a5:62:67:d9:06:f5:e4::14:61:3e:20:dc:cd:43:92:07:7f:fb:65:54:6c:ff:d2:96:36:f8:ff", | |
393 | "XX:88:7c:be:26:77:a5:62:67:d9:06:f5:e4::14:61:3e:20:dc:cd:43:92:07:7f:fb:65:54:6c:ff:d2:96:36:f8", | |
394 | "86:88:Y4:be:26:77:a5:62:67:d9:06:f5:e4:14:61:3e:20:dc:cd:43:92:07:7f:fb:65:54:6c:ff:d2:96:36:f8", | |
395 | "86:88:0:be:26:77:a5:62:67:d9:06:f5:e4:14:61:3e:20:dc:cd:43:92:07:7f:fb:65:54:6c:ff:d2:96:36:f8", | |
396 | ]; | |
397 | ||
398 | for fingerprint in invalid_fingerprints.iter() { | |
399 | if let Ok(_) = parse_simple_value(fingerprint, &schema) { | |
400 | bail!("test fingerprint '{}' failed - got Ok() while expection an error.", fingerprint); | |
401 | } | |
402 | } | |
403 | ||
404 | let valid_fingerprints = [ | |
405 | "86:88:7c:be:26:77:a5:62:67:d9:06:f5:e4:14:61:3e:20:dc:cd:43:92:07:7f:fb:65:54:6c:ff:d2:96:36:f8", | |
406 | "86:88:7C:BE:26:77:a5:62:67:D9:06:f5:e4:14:61:3e:20:dc:cd:43:92:07:7f:fb:65:54:6c:ff:d2:96:36:f8", | |
407 | ]; | |
408 | ||
409 | for fingerprint in valid_fingerprints.iter() { | |
410 | let v = match parse_simple_value(fingerprint, &schema) { | |
411 | Ok(v) => v, | |
412 | Err(err) => { | |
413 | bail!("unable to parse fingerprint '{}' - {}", fingerprint, err); | |
414 | } | |
415 | }; | |
416 | ||
417 | if v != serde_json::json!(fingerprint) { | |
418 | bail!("unable to parse fingerprint '{}' - got wrong value {:?}", fingerprint, v); | |
419 | } | |
420 | } | |
421 | ||
422 | Ok(()) | |
423 | } | |
424 | ||
ff620a3d DM |
425 | #[test] |
426 | fn test_proxmox_user_id_schema() -> Result<(), Error> { | |
427 | ||
428 | let schema = PROXMOX_USER_ID_SCHEMA; | |
429 | ||
430 | let invalid_user_ids = [ | |
431 | "x", // too short | |
432 | "xx", // too short | |
433 | "xxx", // no realm | |
434 | "xxx@", // no realm | |
435 | "xx x@test", // contains space | |
436 | "xx\nx@test", // contains control character | |
437 | "x:xx@test", // contains collon | |
438 | "xx/x@test", // contains slash | |
439 | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@test", // too long | |
440 | ]; | |
441 | ||
442 | for name in invalid_user_ids.iter() { | |
443 | if let Ok(_) = parse_simple_value(name, &schema) { | |
444 | bail!("test userid '{}' failed - got Ok() while expection an error.", name); | |
445 | } | |
446 | } | |
447 | ||
448 | let valid_user_ids = [ | |
449 | "xxx@y", | |
450 | "name@y", | |
451 | "xxx@test-it.com", | |
452 | "xxx@_T_E_S_T-it.com", | |
453 | "x_x-x.x@test-it.com", | |
454 | ]; | |
455 | ||
456 | for name in valid_user_ids.iter() { | |
457 | let v = match parse_simple_value(name, &schema) { | |
458 | Ok(v) => v, | |
459 | Err(err) => { | |
460 | bail!("unable to parse userid '{}' - {}", name, err); | |
461 | } | |
462 | }; | |
463 | ||
464 | if v != serde_json::json!(name) { | |
465 | bail!("unable to parse userid '{}' - got wrong value {:?}", name, v); | |
466 | } | |
467 | } | |
468 | ||
469 | Ok(()) | |
470 | } |