]> git.proxmox.com Git - pve-storage.git/blob - PVE/API2/Storage/Content.pm
Fix #582: Add DELETE delay logic
[pve-storage.git] / PVE / API2 / Storage / Content.pm
1 package PVE::API2::Storage::Content;
2
3 use strict;
4 use warnings;
5 use Data::Dumper;
6
7 use PVE::SafeSyslog;
8 use PVE::Cluster;
9 use PVE::Storage;
10 use PVE::INotify;
11 use PVE::Exception qw(raise_param_exc);
12 use PVE::RPCEnvironment;
13 use PVE::RESTHandler;
14 use PVE::JSONSchema qw(get_standard_option);
15
16 use base qw(PVE::RESTHandler);
17
18 __PACKAGE__->register_method ({
19 name => 'index',
20 path => '',
21 method => 'GET',
22 description => "List storage content.",
23 permissions => {
24 check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
25 },
26 protected => 1,
27 proxyto => 'node',
28 parameters => {
29 additionalProperties => 0,
30 properties => {
31 node => get_standard_option('pve-node'),
32 storage => get_standard_option('pve-storage-id', {
33 completion => \&PVE::Storage::complete_storage_enabled,
34 }),
35 content => {
36 description => "Only list content of this type.",
37 type => 'string', format => 'pve-storage-content',
38 optional => 1,
39 completion => \&PVE::Storage::complete_content_type,
40 },
41 vmid => get_standard_option('pve-vmid', {
42 description => "Only list images for this VM",
43 optional => 1,
44 completion => \&PVE::Cluster::complete_vmid,
45 }),
46 },
47 },
48 returns => {
49 type => 'array',
50 items => {
51 type => "object",
52 properties => {
53 volid => {
54 description => "Volume identifier.",
55 type => 'string',
56 },
57 vmid => {
58 description => "Associated Owner VMID.",
59 type => 'integer',
60 optional => 1,
61 },
62 parent => {
63 description => "Volume identifier of parent (for linked cloned).",
64 type => 'string',
65 optional => 1,
66 },
67 'format' => {
68 description => "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)",
69 type => 'string',
70 },
71 size => {
72 description => "Volume size in bytes.",
73 type => 'integer',
74 renderer => 'bytes',
75 },
76 used => {
77 description => "Used space. Please note that most storage plugins " .
78 "does not report anything useful here.",
79 type => 'integer',
80 renderer => 'bytes',
81 optional => 1,
82 },
83 },
84 },
85 links => [ { rel => 'child', href => "{volid}" } ],
86 },
87 code => sub {
88 my ($param) = @_;
89
90 my $rpcenv = PVE::RPCEnvironment::get();
91
92 my $authuser = $rpcenv->get_user();
93
94 my $storeid = $param->{storage};
95
96 my $cfg = PVE::Storage::config();
97
98 my $vollist = PVE::Storage::volume_list($cfg, $storeid, $param->{vmid}, $param->{content});
99
100 my $res = [];
101 foreach my $item (@$vollist) {
102 eval { PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $item->{volid}); };
103 next if $@;
104 push @$res, $item;
105 }
106
107 return $res;
108 }});
109
110 __PACKAGE__->register_method ({
111 name => 'create',
112 path => '',
113 method => 'POST',
114 description => "Allocate disk images.",
115 permissions => {
116 check => ['perm', '/storage/{storage}', ['Datastore.AllocateSpace']],
117 },
118 protected => 1,
119 proxyto => 'node',
120 parameters => {
121 additionalProperties => 0,
122 properties => {
123 node => get_standard_option('pve-node'),
124 storage => get_standard_option('pve-storage-id', {
125 completion => \&PVE::Storage::complete_storage_enabled,
126 }),
127 filename => {
128 description => "The name of the file to create.",
129 type => 'string',
130 },
131 vmid => get_standard_option('pve-vmid', {
132 description => "Specify owner VM",
133 completion => \&PVE::Cluster::complete_vmid,
134 }),
135 size => {
136 description => "Size in kilobyte (1024 bytes). Optional suffixes 'M' (megabyte, 1024K) and 'G' (gigabyte, 1024M)",
137 type => 'string',
138 pattern => '\d+[MG]?',
139 },
140 'format' => {
141 type => 'string',
142 enum => ['raw', 'qcow2', 'subvol'],
143 requires => 'size',
144 optional => 1,
145 },
146 },
147 },
148 returns => {
149 description => "Volume identifier",
150 type => 'string',
151 },
152 code => sub {
153 my ($param) = @_;
154
155 my $storeid = $param->{storage};
156 my $name = $param->{filename};
157 my $sizestr = $param->{size};
158
159 my $size;
160 if ($sizestr =~ m/^\d+$/) {
161 $size = $sizestr;
162 } elsif ($sizestr =~ m/^(\d+)M$/) {
163 $size = $1 * 1024;
164 } elsif ($sizestr =~ m/^(\d+)G$/) {
165 $size = $1 * 1024 * 1024;
166 } else {
167 raise_param_exc({ size => "unable to parse size '$sizestr'" });
168 }
169
170 # extract FORMAT from name
171 if ($name =~ m/\.(raw|qcow2|vmdk)$/) {
172 my $fmt = $1;
173
174 raise_param_exc({ format => "different storage formats ($param->{format} != $fmt)" })
175 if $param->{format} && $param->{format} ne $fmt;
176
177 $param->{format} = $fmt;
178 }
179
180 my $cfg = PVE::Storage::config();
181
182 my $volid = PVE::Storage::vdisk_alloc ($cfg, $storeid, $param->{vmid},
183 $param->{format},
184 $name, $size);
185
186 return $volid;
187 }});
188
189 # we allow to pass volume names (without storage prefix) if the storage
190 # is specified as separate parameter.
191 my $real_volume_id = sub {
192 my ($storeid, $volume) = @_;
193
194 my $volid;
195
196 if ($volume =~ m/:/) {
197 eval {
198 my ($sid, $volname) = PVE::Storage::parse_volume_id ($volume);
199 die "storage ID missmatch ($sid != $storeid)\n"
200 if $storeid && $sid ne $storeid;
201 $volid = $volume;
202 $storeid = $sid;
203 };
204 raise_param_exc({ volume => $@ }) if $@;
205
206 } else {
207 raise_param_exc({ volume => "no storage speficied - incomplete volume ID" })
208 if !$storeid;
209
210 $volid = "$storeid:$volume";
211 }
212
213 return wantarray ? ($volid, $storeid) : $volid;
214 };
215
216 __PACKAGE__->register_method ({
217 name => 'info',
218 path => '{volume}',
219 method => 'GET',
220 description => "Get volume attributes",
221 permissions => {
222 description => "You need read access for the volume.",
223 user => 'all',
224 },
225 protected => 1,
226 proxyto => 'node',
227 parameters => {
228 additionalProperties => 0,
229 properties => {
230 node => get_standard_option('pve-node'),
231 storage => get_standard_option('pve-storage-id', { optional => 1 }),
232 volume => {
233 description => "Volume identifier",
234 type => 'string',
235 },
236 },
237 },
238 returns => { type => 'object' },
239 code => sub {
240 my ($param) = @_;
241
242 my $rpcenv = PVE::RPCEnvironment::get();
243 my $authuser = $rpcenv->get_user();
244
245 my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
246
247 my $cfg = PVE::Storage::config();
248
249 PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $volid);
250
251 my $path = PVE::Storage::path($cfg, $volid);
252 my ($size, $format, $used, $parent) = PVE::Storage::file_size_info($path);
253 die "file_size_info on '$volid' failed\n" if !($format && $size);
254
255 # fixme: return more attributes?
256 return {
257 path => $path,
258 size => $size,
259 used => $used,
260 format => $format,
261 };
262 }});
263
264 __PACKAGE__->register_method ({
265 name => 'delete',
266 path => '{volume}',
267 method => 'DELETE',
268 description => "Delete volume",
269 permissions => {
270 description => "You need 'Datastore.Allocate' privilege on the storage (or 'Datastore.AllocateSpace' for backup volumes if you have VM.Backup privilege on the VM).",
271 user => 'all',
272 },
273 protected => 1,
274 proxyto => 'node',
275 parameters => {
276 additionalProperties => 0,
277 properties => {
278 node => get_standard_option('pve-node'),
279 storage => get_standard_option('pve-storage-id', {
280 optional => 1,
281 completion => \&PVE::Storage::complete_storage,
282 }),
283 volume => {
284 description => "Volume identifier",
285 type => 'string',
286 completion => \&PVE::Storage::complete_volume,
287 },
288 delay => {
289 type => 'integer',
290 description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
291 minimum => 1,
292 maximum => 30,
293 optional => 1,
294 },
295 },
296 },
297 returns => { type => 'string', optional => 1, },
298 code => sub {
299 my ($param) = @_;
300
301 my $rpcenv = PVE::RPCEnvironment::get();
302 my $authuser = $rpcenv->get_user();
303
304 my $cfg = PVE::Storage::config();
305
306 my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
307
308 my ($path, $ownervm, $vtype) = PVE::Storage::path($cfg, $volid);
309 if ($vtype eq 'backup' && $ownervm) {
310 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
311 $rpcenv->check($authuser, "/vms/$ownervm", ['VM.Backup']);
312 } else {
313 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Allocate']);
314 }
315
316 my $worker = sub {
317 PVE::Storage::vdisk_free ($cfg, $volid);
318 print "Removed volume '$volid'\n";
319 if ($vtype eq 'backup'
320 && $path =~ /(.*\/vzdump-\w+-\d+-\d{4}_\d{2}_\d{2}-\d{2}_\d{2}_\d{2})[^\/]+$/) {
321 my $logpath = "$1.log";
322 # try to cleanup our backup log file too, if still exisiting, #318
323 unlink($logpath) if -e $logpath;
324 }
325 };
326
327 my $id = (defined $ownervm ? "$ownervm@" : '') . $storeid;
328 my $upid = $rpcenv->fork_worker('imgdel', $id, $authuser, $worker);
329 my $background_delay = $param->{delay};
330 if ($background_delay) {
331 my $end_time = time() + $background_delay;
332 my $currently_deleting; # not necessarily true, e.g. sequential api call from cli
333 do {
334 my $task = PVE::Tools::upid_decode($upid);
335 $currently_deleting = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
336 sleep 1 if $currently_deleting;
337 } while (time() < $end_time && $currently_deleting);
338
339 if (!$currently_deleting) {
340 my $status = PVE::Tools::upid_read_status($upid);
341 return undef if $status eq 'OK';
342 die $status;
343 }
344 }
345 return $upid;
346 }});
347
348 __PACKAGE__->register_method ({
349 name => 'copy',
350 path => '{volume}',
351 method => 'POST',
352 description => "Copy a volume. This is experimental code - do not use.",
353 protected => 1,
354 proxyto => 'node',
355 parameters => {
356 additionalProperties => 0,
357 properties => {
358 node => get_standard_option('pve-node'),
359 storage => get_standard_option('pve-storage-id', { optional => 1}),
360 volume => {
361 description => "Source volume identifier",
362 type => 'string',
363 },
364 target => {
365 description => "Target volume identifier",
366 type => 'string',
367 },
368 target_node => get_standard_option('pve-node', {
369 description => "Target node. Default is local node.",
370 optional => 1,
371 }),
372 },
373 },
374 returns => {
375 type => 'string',
376 },
377 code => sub {
378 my ($param) = @_;
379
380 my $rpcenv = PVE::RPCEnvironment::get();
381
382 my $user = $rpcenv->get_user();
383
384 my $target_node = $param->{target_node} || PVE::INotify::nodename();
385 # pvesh examples
386 # cd /nodes/localhost/storage/local/content
387 # pve:/> create local:103/vm-103-disk-1.raw -target local:103/vm-103-disk-2.raw
388 # pve:/> create 103/vm-103-disk-1.raw -target 103/vm-103-disk-3.raw
389
390 my $src_volid = &$real_volume_id($param->{storage}, $param->{volume});
391 my $dst_volid = &$real_volume_id($param->{storage}, $param->{target});
392
393 print "DEBUG: COPY $src_volid TO $dst_volid\n";
394
395 my $cfg = PVE::Storage::config();
396
397 # do all parameter checks first
398
399 # then do all short running task (to raise errors befor we go to background)
400
401 # then start the worker task
402 my $worker = sub {
403 my $upid = shift;
404
405 print "DEBUG: starting worker $upid\n";
406
407 my ($target_sid, $target_volname) = PVE::Storage::parse_volume_id($dst_volid);
408 #my $target_ip = PVE::Cluster::remote_node_ip($target_node);
409
410 # you need to get this working (fails currently, because storage_migrate() uses
411 # ssh to connect to local host (which is not needed
412 my $sshinfo = PVE::Cluster::get_ssh_info($target_node);
413 PVE::Storage::storage_migrate($cfg, $src_volid, $sshinfo, $target_sid, $target_volname);
414
415 print "DEBUG: end worker $upid\n";
416
417 };
418
419 return $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
420 }});
421
422 1;