]> git.proxmox.com Git - pve-storage.git/blame_incremental - PVE/API2/Storage/Content.pm
fix #3307: make it possible to set protection for backups
[pve-storage.git] / PVE / API2 / Storage / Content.pm
... / ...
CommitLineData
1package PVE::API2::Storage::Content;
2
3use strict;
4use warnings;
5use Data::Dumper;
6
7use PVE::SafeSyslog;
8use PVE::Cluster;
9use PVE::Storage;
10use PVE::INotify;
11use PVE::Exception qw(raise_param_exc);
12use PVE::RPCEnvironment;
13use PVE::RESTHandler;
14use PVE::JSONSchema qw(get_standard_option);
15use PVE::SSHInfo;
16
17use base qw(PVE::RESTHandler);
18
19__PACKAGE__->register_method ({
20 name => 'index',
21 path => '',
22 method => 'GET',
23 description => "List storage content.",
24 permissions => {
25 check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
26 },
27 protected => 1,
28 proxyto => 'node',
29 parameters => {
30 additionalProperties => 0,
31 properties => {
32 node => get_standard_option('pve-node'),
33 storage => get_standard_option('pve-storage-id', {
34 completion => \&PVE::Storage::complete_storage_enabled,
35 }),
36 content => {
37 description => "Only list content of this type.",
38 type => 'string', format => 'pve-storage-content',
39 optional => 1,
40 completion => \&PVE::Storage::complete_content_type,
41 },
42 vmid => get_standard_option('pve-vmid', {
43 description => "Only list images for this VM",
44 optional => 1,
45 completion => \&PVE::Cluster::complete_vmid,
46 }),
47 },
48 },
49 returns => {
50 type => 'array',
51 items => {
52 type => "object",
53 properties => {
54 volid => {
55 description => "Volume identifier.",
56 type => 'string',
57 },
58 vmid => {
59 description => "Associated Owner VMID.",
60 type => 'integer',
61 optional => 1,
62 },
63 parent => {
64 description => "Volume identifier of parent (for linked cloned).",
65 type => 'string',
66 optional => 1,
67 },
68 'format' => {
69 description => "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)",
70 type => 'string',
71 },
72 size => {
73 description => "Volume size in bytes.",
74 type => 'integer',
75 renderer => 'bytes',
76 },
77 used => {
78 description => "Used space. Please note that most storage plugins " .
79 "do not report anything useful here.",
80 type => 'integer',
81 renderer => 'bytes',
82 optional => 1,
83 },
84 ctime => {
85 description => "Creation time (seconds since the UNIX Epoch).",
86 type => 'integer',
87 minimum => 0,
88 optional => 1,
89 },
90 notes => {
91 description => "Optional notes. If they contain multiple lines, only the first one is returned here.",
92 type => 'string',
93 optional => 1,
94 },
95 encrypted => {
96 description => "If whole backup is encrypted, value is the fingerprint or '1' "
97 ." if encrypted. Only useful for the Proxmox Backup Server storage type.",
98 type => 'string',
99 optional => 1,
100 },
101 verification => {
102 description => "Last backup verification result, only useful for PBS storages.",
103 type => 'object',
104 properties => {
105 state => {
106 description => "Last backup verification state.",
107 type => 'string',
108 },
109 upid => {
110 description => "Last backup verification UPID.",
111 type => 'string',
112 },
113 },
114 optional => 1,
115 },
116 protected => {
117 description => "Protection status. Currently only supported for backups.",
118 type => 'boolean',
119 optional => 1,
120 },
121 },
122 },
123 links => [ { rel => 'child', href => "{volid}" } ],
124 },
125 code => sub {
126 my ($param) = @_;
127
128 my $rpcenv = PVE::RPCEnvironment::get();
129
130 my $authuser = $rpcenv->get_user();
131
132 my $storeid = $param->{storage};
133
134 my $cfg = PVE::Storage::config();
135
136 my $vollist = PVE::Storage::volume_list($cfg, $storeid, $param->{vmid}, $param->{content});
137
138 my $res = [];
139 foreach my $item (@$vollist) {
140 eval { PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $item->{volid}); };
141 next if $@;
142 $item->{vmid} = int($item->{vmid}) if (defined($item->{vmid}));
143 push @$res, $item;
144 }
145
146 return $res;
147 }});
148
149__PACKAGE__->register_method ({
150 name => 'create',
151 path => '',
152 method => 'POST',
153 description => "Allocate disk images.",
154 permissions => {
155 check => ['perm', '/storage/{storage}', ['Datastore.AllocateSpace']],
156 },
157 protected => 1,
158 proxyto => 'node',
159 parameters => {
160 additionalProperties => 0,
161 properties => {
162 node => get_standard_option('pve-node'),
163 storage => get_standard_option('pve-storage-id', {
164 completion => \&PVE::Storage::complete_storage_enabled,
165 }),
166 filename => {
167 description => "The name of the file to create.",
168 type => 'string',
169 },
170 vmid => get_standard_option('pve-vmid', {
171 description => "Specify owner VM",
172 completion => \&PVE::Cluster::complete_vmid,
173 }),
174 size => {
175 description => "Size in kilobyte (1024 bytes). Optional suffixes 'M' (megabyte, 1024K) and 'G' (gigabyte, 1024M)",
176 type => 'string',
177 pattern => '\d+[MG]?',
178 },
179 'format' => {
180 type => 'string',
181 enum => ['raw', 'qcow2', 'subvol'],
182 requires => 'size',
183 optional => 1,
184 },
185 },
186 },
187 returns => {
188 description => "Volume identifier",
189 type => 'string',
190 },
191 code => sub {
192 my ($param) = @_;
193
194 my $storeid = $param->{storage};
195 my $name = $param->{filename};
196 my $sizestr = $param->{size};
197
198 my $size;
199 if ($sizestr =~ m/^\d+$/) {
200 $size = $sizestr;
201 } elsif ($sizestr =~ m/^(\d+)M$/) {
202 $size = $1 * 1024;
203 } elsif ($sizestr =~ m/^(\d+)G$/) {
204 $size = $1 * 1024 * 1024;
205 } else {
206 raise_param_exc({ size => "unable to parse size '$sizestr'" });
207 }
208
209 # extract FORMAT from name
210 if ($name =~ m/\.(raw|qcow2|vmdk)$/) {
211 my $fmt = $1;
212
213 raise_param_exc({ format => "different storage formats ($param->{format} != $fmt)" })
214 if $param->{format} && $param->{format} ne $fmt;
215
216 $param->{format} = $fmt;
217 }
218
219 my $cfg = PVE::Storage::config();
220
221 my $volid = PVE::Storage::vdisk_alloc ($cfg, $storeid, $param->{vmid},
222 $param->{format},
223 $name, $size);
224
225 return $volid;
226 }});
227
228# we allow to pass volume names (without storage prefix) if the storage
229# is specified as separate parameter.
230my $real_volume_id = sub {
231 my ($storeid, $volume) = @_;
232
233 my $volid;
234
235 if ($volume =~ m/:/) {
236 eval {
237 my ($sid, $volname) = PVE::Storage::parse_volume_id ($volume);
238 die "storage ID mismatch ($sid != $storeid)\n"
239 if $storeid && $sid ne $storeid;
240 $volid = $volume;
241 $storeid = $sid;
242 };
243 raise_param_exc({ volume => $@ }) if $@;
244
245 } else {
246 raise_param_exc({ volume => "no storage specified - incomplete volume ID" })
247 if !$storeid;
248
249 $volid = "$storeid:$volume";
250 }
251
252 return wantarray ? ($volid, $storeid) : $volid;
253};
254
255__PACKAGE__->register_method ({
256 name => 'info',
257 path => '{volume}',
258 method => 'GET',
259 description => "Get volume attributes",
260 permissions => {
261 description => "You need read access for the volume.",
262 user => 'all',
263 },
264 protected => 1,
265 proxyto => 'node',
266 parameters => {
267 additionalProperties => 0,
268 properties => {
269 node => get_standard_option('pve-node'),
270 storage => get_standard_option('pve-storage-id', { optional => 1 }),
271 volume => {
272 description => "Volume identifier",
273 type => 'string',
274 },
275 },
276 },
277 returns => {
278 type => 'object',
279 properties => {
280 path => {
281 description => "The Path",
282 type => 'string',
283 },
284 size => {
285 description => "Volume size in bytes.",
286 type => 'integer',
287 renderer => 'bytes',
288 },
289 used => {
290 description => "Used space. Please note that most storage plugins " .
291 "do not report anything useful here.",
292 type => 'integer',
293 renderer => 'bytes',
294 },
295 format => {
296 description => "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)",
297 type => 'string',
298 },
299 notes => {
300 description => "Optional notes.",
301 optional => 1,
302 type => 'string',
303 },
304 protected => {
305 description => "Protection status. Currently only supported for backups.",
306 type => 'boolean',
307 optional => 1,
308 },
309 },
310 },
311 code => sub {
312 my ($param) = @_;
313
314 my $rpcenv = PVE::RPCEnvironment::get();
315 my $authuser = $rpcenv->get_user();
316
317 my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
318
319 my $cfg = PVE::Storage::config();
320
321 PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $volid);
322
323 my $path = PVE::Storage::path($cfg, $volid);
324 my ($size, $format, $used, $parent) = PVE::Storage::volume_size_info($cfg, $volid);
325 die "volume_size_info on '$volid' failed\n" if !($format && $size);
326
327 my $entry = {
328 path => $path,
329 size => $size,
330 used => $used,
331 format => $format,
332 };
333
334 for my $attribute (qw(notes protected)) {
335 # keep going if fetching an optional attribute fails
336 eval {
337 my $value = PVE::Storage::get_volume_attribute($cfg, $volid, $attribute);
338 $entry->{$attribute} = $value if defined($value);
339 };
340 warn $@ if $@;
341 }
342
343 return $entry;
344 }});
345
346__PACKAGE__->register_method ({
347 name => 'updateattributes',
348 path => '{volume}',
349 method => 'PUT',
350 description => "Update volume attributes",
351 permissions => {
352 description => "You need read access for the volume.",
353 user => 'all',
354 },
355 protected => 1,
356 proxyto => 'node',
357 parameters => {
358 additionalProperties => 0,
359 properties => {
360 node => get_standard_option('pve-node'),
361 storage => get_standard_option('pve-storage-id', { optional => 1 }),
362 volume => {
363 description => "Volume identifier",
364 type => 'string',
365 },
366 notes => {
367 description => "The new notes.",
368 type => 'string',
369 optional => 1,
370 },
371 protected => {
372 description => "Protection status. Currently only supported for backups.",
373 type => 'boolean',
374 optional => 1,
375 },
376 },
377 },
378 returns => { type => 'null' },
379 code => sub {
380 my ($param) = @_;
381
382 my $rpcenv = PVE::RPCEnvironment::get();
383 my $authuser = $rpcenv->get_user();
384
385 my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
386
387 my $cfg = PVE::Storage::config();
388
389 PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $volid);
390
391 for my $attr (qw(notes protected)) {
392 if (exists $param->{$attr}) {
393 PVE::Storage::update_volume_attribute($cfg, $volid, $attr, $param->{$attr});
394 }
395 }
396
397 return undef;
398 }});
399
400__PACKAGE__->register_method ({
401 name => 'delete',
402 path => '{volume}',
403 method => 'DELETE',
404 description => "Delete volume",
405 permissions => {
406 description => "You need 'Datastore.Allocate' privilege on the storage (or 'Datastore.AllocateSpace' for backup volumes if you have VM.Backup privilege on the VM).",
407 user => 'all',
408 },
409 protected => 1,
410 proxyto => 'node',
411 parameters => {
412 additionalProperties => 0,
413 properties => {
414 node => get_standard_option('pve-node'),
415 storage => get_standard_option('pve-storage-id', {
416 optional => 1,
417 completion => \&PVE::Storage::complete_storage,
418 }),
419 volume => {
420 description => "Volume identifier",
421 type => 'string',
422 completion => \&PVE::Storage::complete_volume,
423 },
424 delay => {
425 type => 'integer',
426 description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
427 minimum => 1,
428 maximum => 30,
429 optional => 1,
430 },
431 },
432 },
433 returns => { type => 'string', optional => 1, },
434 code => sub {
435 my ($param) = @_;
436
437 my $rpcenv = PVE::RPCEnvironment::get();
438 my $authuser = $rpcenv->get_user();
439
440 my $cfg = PVE::Storage::config();
441
442 my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
443
444 my ($path, $ownervm, $vtype) = PVE::Storage::path($cfg, $volid);
445 if ($vtype eq 'backup' && $ownervm) {
446 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
447 $rpcenv->check($authuser, "/vms/$ownervm", ['VM.Backup']);
448 } else {
449 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Allocate']);
450 }
451
452 my $worker = sub {
453 PVE::Storage::vdisk_free ($cfg, $volid);
454 print "Removed volume '$volid'\n";
455 if ($vtype eq 'backup'
456 && $path =~ /(.*\/vzdump-\w+-\d+-\d{4}_\d{2}_\d{2}-\d{2}_\d{2}_\d{2})[^\/]+$/) {
457 my $logpath = "$1.log";
458 # try to cleanup our backup log file too, if still existing, #318
459 unlink($logpath) if -e $logpath;
460 }
461 };
462
463 my $id = (defined $ownervm ? "$ownervm@" : '') . $storeid;
464 my $upid = $rpcenv->fork_worker('imgdel', $id, $authuser, $worker);
465 my $background_delay = $param->{delay};
466 if ($background_delay) {
467 my $end_time = time() + $background_delay;
468 my $currently_deleting; # not necessarily true, e.g. sequential api call from cli
469 do {
470 my $task = PVE::Tools::upid_decode($upid);
471 $currently_deleting = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
472 sleep 1 if $currently_deleting;
473 } while (time() < $end_time && $currently_deleting);
474
475 if (!$currently_deleting) {
476 my $status = PVE::Tools::upid_read_status($upid);
477 chomp $status;
478 return undef if !PVE::Tools::upid_status_is_error($status);
479 die "$status\n";
480 }
481 }
482 return $upid;
483 }});
484
485__PACKAGE__->register_method ({
486 name => 'copy',
487 path => '{volume}',
488 method => 'POST',
489 description => "Copy a volume. This is experimental code - do not use.",
490 protected => 1,
491 proxyto => 'node',
492 parameters => {
493 additionalProperties => 0,
494 properties => {
495 node => get_standard_option('pve-node'),
496 storage => get_standard_option('pve-storage-id', { optional => 1}),
497 volume => {
498 description => "Source volume identifier",
499 type => 'string',
500 },
501 target => {
502 description => "Target volume identifier",
503 type => 'string',
504 },
505 target_node => get_standard_option('pve-node', {
506 description => "Target node. Default is local node.",
507 optional => 1,
508 }),
509 },
510 },
511 returns => {
512 type => 'string',
513 },
514 code => sub {
515 my ($param) = @_;
516
517 my $rpcenv = PVE::RPCEnvironment::get();
518
519 my $user = $rpcenv->get_user();
520
521 my $target_node = $param->{target_node} || PVE::INotify::nodename();
522 # pvesh examples
523 # cd /nodes/localhost/storage/local/content
524 # pve:/> create local:103/vm-103-disk-1.raw -target local:103/vm-103-disk-2.raw
525 # pve:/> create 103/vm-103-disk-1.raw -target 103/vm-103-disk-3.raw
526
527 my $src_volid = &$real_volume_id($param->{storage}, $param->{volume});
528 my $dst_volid = &$real_volume_id($param->{storage}, $param->{target});
529
530 print "DEBUG: COPY $src_volid TO $dst_volid\n";
531
532 my $cfg = PVE::Storage::config();
533
534 # do all parameter checks first
535
536 # then do all short running task (to raise errors before we go to background)
537
538 # then start the worker task
539 my $worker = sub {
540 my $upid = shift;
541
542 print "DEBUG: starting worker $upid\n";
543
544 my ($target_sid, $target_volname) = PVE::Storage::parse_volume_id($dst_volid);
545 #my $target_ip = PVE::Cluster::remote_node_ip($target_node);
546
547 # you need to get this working (fails currently, because storage_migrate() uses
548 # ssh to connect to local host (which is not needed
549 my $sshinfo = PVE::SSHInfo::get_ssh_info($target_node);
550 PVE::Storage::storage_migrate($cfg, $src_volid, $sshinfo, $target_sid, {'target_volname' => $target_volname});
551
552 print "DEBUG: end worker $upid\n";
553
554 };
555
556 return $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
557 }});
558
5591;