]> git.proxmox.com Git - pve-storage.git/blob - PVE/API2/Storage/Status.pm
897b4a77e28b739a590c5ade6046dd87fd1f009f
[pve-storage.git] / PVE / API2 / Storage / Status.pm
1 package PVE::API2::Storage::Status;
2
3 use strict;
4 use warnings;
5
6 use File::Path;
7 use File::Basename;
8 use PVE::Tools;
9 use PVE::INotify;
10 use PVE::Cluster;
11 use PVE::RRD;
12 use PVE::Storage;
13 use PVE::API2::Storage::Content;
14 use PVE::API2::Storage::PruneBackups;
15 use PVE::API2::Storage::FileRestore;
16 use PVE::RESTHandler;
17 use PVE::RPCEnvironment;
18 use PVE::JSONSchema qw(get_standard_option);
19 use PVE::Exception qw(raise_param_exc);
20
21 use base qw(PVE::RESTHandler);
22
23 __PACKAGE__->register_method ({
24 subclass => "PVE::API2::Storage::PruneBackups",
25 path => '{storage}/prunebackups',
26 });
27
28 __PACKAGE__->register_method ({
29 subclass => "PVE::API2::Storage::Content",
30 # set fragment delimiter (no subdirs) - we need that, because volume
31 # IDs may contain a slash '/'
32 fragmentDelimiter => '',
33 path => '{storage}/content',
34 });
35
36 __PACKAGE__->register_method ({
37 subclass => "PVE::API2::Storage::FileRestore",
38 path => '{storage}/file-restore',
39 });
40
41 __PACKAGE__->register_method ({
42 name => 'index',
43 path => '',
44 method => 'GET',
45 description => "Get status for all datastores.",
46 permissions => {
47 description => "Only list entries where you have 'Datastore.Audit' or 'Datastore.AllocateSpace' permissions on '/storage/<storage>'",
48 user => 'all',
49 },
50 protected => 1,
51 proxyto => 'node',
52 parameters => {
53 additionalProperties => 0,
54 properties => {
55 node => get_standard_option('pve-node'),
56 storage => get_standard_option('pve-storage-id', {
57 description => "Only list status for specified storage",
58 optional => 1,
59 completion => \&PVE::Storage::complete_storage_enabled,
60 }),
61 content => {
62 description => "Only list stores which support this content type.",
63 type => 'string', format => 'pve-storage-content-list',
64 optional => 1,
65 completion => \&PVE::Storage::complete_content_type,
66 },
67 enabled => {
68 description => "Only list stores which are enabled (not disabled in config).",
69 type => 'boolean',
70 optional => 1,
71 default => 0,
72 },
73 target => get_standard_option('pve-node', {
74 description => "If target is different to 'node', we only lists shared storages which " .
75 "content is accessible on this 'node' and the specified 'target' node.",
76 optional => 1,
77 completion => \&PVE::Cluster::get_nodelist,
78 }),
79 'format' => {
80 description => "Include information about formats",
81 type => 'boolean',
82 optional => 1,
83 default => 0,
84 },
85 },
86 },
87 returns => {
88 type => 'array',
89 items => {
90 type => "object",
91 properties => {
92 storage => get_standard_option('pve-storage-id'),
93 type => {
94 description => "Storage type.",
95 type => 'string',
96 },
97 content => {
98 description => "Allowed storage content types.",
99 type => 'string', format => 'pve-storage-content-list',
100 },
101 enabled => {
102 description => "Set when storage is enabled (not disabled).",
103 type => 'boolean',
104 optional => 1,
105 },
106 active => {
107 description => "Set when storage is accessible.",
108 type => 'boolean',
109 optional => 1,
110 },
111 shared => {
112 description => "Shared flag from storage configuration.",
113 type => 'boolean',
114 optional => 1,
115 },
116 total => {
117 description => "Total storage space in bytes.",
118 type => 'integer',
119 renderer => 'bytes',
120 optional => 1,
121 },
122 used => {
123 description => "Used storage space in bytes.",
124 type => 'integer',
125 renderer => 'bytes',
126 optional => 1,
127 },
128 avail => {
129 description => "Available storage space in bytes.",
130 type => 'integer',
131 renderer => 'bytes',
132 optional => 1,
133 },
134 used_fraction => {
135 description => "Used fraction (used/total).",
136 type => 'number',
137 renderer => 'fraction_as_percentage',
138 optional => 1,
139 },
140 },
141 },
142 links => [ { rel => 'child', href => "{storage}" } ],
143 },
144 code => sub {
145 my ($param) = @_;
146
147 my $rpcenv = PVE::RPCEnvironment::get();
148 my $authuser = $rpcenv->get_user();
149
150 my $localnode = PVE::INotify::nodename();
151
152 my $target = $param->{target};
153
154 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
155
156 my $cfg = PVE::Storage::config();
157
158 my $info = PVE::Storage::storage_info($cfg, $param->{content}, $param->{format});
159
160 raise_param_exc({ storage => "No such storage." })
161 if $param->{storage} && !defined($info->{$param->{storage}});
162
163 my $res = {};
164 my @sids = PVE::Storage::storage_ids($cfg);
165 foreach my $storeid (@sids) {
166 my $data = $info->{$storeid};
167 next if !$data;
168 my $privs = [ 'Datastore.Audit', 'Datastore.AllocateSpace' ];
169 next if !$rpcenv->check_any($authuser, "/storage/$storeid", $privs, 1);
170 next if $param->{storage} && $param->{storage} ne $storeid;
171
172 my $scfg = PVE::Storage::storage_config($cfg, $storeid);
173
174 next if $param->{enabled} && $scfg->{disable};
175
176 if ($target) {
177 # check if storage content is accessible on local node and specified target node
178 # we use this on the Clone GUI
179
180 next if !$scfg->{shared};
181 next if !PVE::Storage::storage_check_node($cfg, $storeid, undef, 1);
182 next if !PVE::Storage::storage_check_node($cfg, $storeid, $target, 1);
183 }
184
185 if ($data->{total}) {
186 $data->{used_fraction} = ($data->{used} // 0) / $data->{total};
187 }
188
189 $res->{$storeid} = $data;
190 }
191
192 return PVE::RESTHandler::hash_to_array($res, 'storage');
193 }});
194
195 __PACKAGE__->register_method ({
196 name => 'diridx',
197 path => '{storage}',
198 method => 'GET',
199 description => "",
200 permissions => {
201 check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
202 },
203 parameters => {
204 additionalProperties => 0,
205 properties => {
206 node => get_standard_option('pve-node'),
207 storage => get_standard_option('pve-storage-id'),
208 },
209 },
210 returns => {
211 type => 'array',
212 items => {
213 type => "object",
214 properties => {
215 subdir => { type => 'string' },
216 },
217 },
218 links => [ { rel => 'child', href => "{subdir}" } ],
219 },
220 code => sub {
221 my ($param) = @_;
222
223 my $res = [
224 { subdir => 'status' },
225 { subdir => 'content' },
226 { subdir => 'upload' },
227 { subdir => 'rrd' },
228 { subdir => 'rrddata' },
229 { subdir => 'prunebackups' },
230 ];
231
232 return $res;
233 }});
234
235 __PACKAGE__->register_method ({
236 name => 'read_status',
237 path => '{storage}/status',
238 method => 'GET',
239 description => "Read storage status.",
240 permissions => {
241 check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
242 },
243 protected => 1,
244 proxyto => 'node',
245 parameters => {
246 additionalProperties => 0,
247 properties => {
248 node => get_standard_option('pve-node'),
249 storage => get_standard_option('pve-storage-id'),
250 },
251 },
252 returns => {
253 type => "object",
254 properties => {},
255 },
256 code => sub {
257 my ($param) = @_;
258
259 my $cfg = PVE::Storage::config();
260
261 my $info = PVE::Storage::storage_info($cfg, $param->{content});
262
263 my $data = $info->{$param->{storage}};
264
265 raise_param_exc({ storage => "No such storage." })
266 if !defined($data);
267
268 return $data;
269 }});
270
271 __PACKAGE__->register_method ({
272 name => 'rrd',
273 path => '{storage}/rrd',
274 method => 'GET',
275 description => "Read storage RRD statistics (returns PNG).",
276 permissions => {
277 check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
278 },
279 protected => 1,
280 proxyto => 'node',
281 parameters => {
282 additionalProperties => 0,
283 properties => {
284 node => get_standard_option('pve-node'),
285 storage => get_standard_option('pve-storage-id'),
286 timeframe => {
287 description => "Specify the time frame you are interested in.",
288 type => 'string',
289 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
290 },
291 ds => {
292 description => "The list of datasources you want to display.",
293 type => 'string', format => 'pve-configid-list',
294 },
295 cf => {
296 description => "The RRD consolidation function",
297 type => 'string',
298 enum => [ 'AVERAGE', 'MAX' ],
299 optional => 1,
300 },
301 },
302 },
303 returns => {
304 type => "object",
305 properties => {
306 filename => { type => 'string' },
307 },
308 },
309 code => sub {
310 my ($param) = @_;
311
312 return PVE::RRD::create_rrd_graph(
313 "pve2-storage/$param->{node}/$param->{storage}",
314 $param->{timeframe}, $param->{ds}, $param->{cf});
315 }});
316
317 __PACKAGE__->register_method ({
318 name => 'rrddata',
319 path => '{storage}/rrddata',
320 method => 'GET',
321 description => "Read storage RRD statistics.",
322 permissions => {
323 check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
324 },
325 protected => 1,
326 proxyto => 'node',
327 parameters => {
328 additionalProperties => 0,
329 properties => {
330 node => get_standard_option('pve-node'),
331 storage => get_standard_option('pve-storage-id'),
332 timeframe => {
333 description => "Specify the time frame you are interested in.",
334 type => 'string',
335 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
336 },
337 cf => {
338 description => "The RRD consolidation function",
339 type => 'string',
340 enum => [ 'AVERAGE', 'MAX' ],
341 optional => 1,
342 },
343 },
344 },
345 returns => {
346 type => "array",
347 items => {
348 type => "object",
349 properties => {},
350 },
351 },
352 code => sub {
353 my ($param) = @_;
354
355 return PVE::RRD::create_rrd_data(
356 "pve2-storage/$param->{node}/$param->{storage}",
357 $param->{timeframe}, $param->{cf});
358 }});
359
360 # makes no sense for big images and backup files (because it
361 # create a copy of the file).
362 __PACKAGE__->register_method ({
363 name => 'upload',
364 path => '{storage}/upload',
365 method => 'POST',
366 description => "Upload templates and ISO images.",
367 permissions => {
368 check => ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
369 },
370 protected => 1,
371 parameters => {
372 additionalProperties => 0,
373 properties => {
374 node => get_standard_option('pve-node'),
375 storage => get_standard_option('pve-storage-id'),
376 content => {
377 description => "Content type.",
378 type => 'string', format => 'pve-storage-content',
379 },
380 filename => {
381 description => "The name of the file to create.",
382 type => 'string',
383 },
384 tmpfilename => {
385 description => "The source file name. This parameter is usually set by the REST handler. You can only overwrite it when connecting to the trusted port on localhost.",
386 type => 'string',
387 optional => 1,
388 },
389 },
390 },
391 returns => { type => "string" },
392 code => sub {
393 my ($param) = @_;
394
395 my $rpcenv = PVE::RPCEnvironment::get();
396
397 my $user = $rpcenv->get_user();
398
399 my $cfg = PVE::Storage::config();
400
401 my $node = $param->{node};
402 my $scfg = PVE::Storage::storage_check_enabled($cfg, $param->{storage}, $node);
403
404 die "can't upload to storage type '$scfg->{type}'\n"
405 if !defined($scfg->{path});
406
407 my $content = $param->{content};
408
409 my $tmpfilename = $param->{tmpfilename};
410 die "missing temporary file name\n" if !$tmpfilename;
411
412 my $size = -s $tmpfilename;
413 die "temporary file '$tmpfilename' does not exist\n" if !defined($size);
414
415 my $filename = $param->{filename};
416
417 chomp $filename;
418 $filename =~ s/^.*[\/\\]//;
419 $filename =~ s/[^-a-zA-Z0-9_.]/_/g;
420
421 my $path;
422
423 if ($content eq 'iso') {
424 if ($filename !~ m![^/]+$PVE::Storage::iso_extension_re$!) {
425 raise_param_exc({ filename => "missing '.iso' or '.img' extension" });
426 }
427 $path = PVE::Storage::get_iso_dir($cfg, $param->{storage});
428 } elsif ($content eq 'vztmpl') {
429 if ($filename !~ m![^/]+\.tar\.[gx]z$!) {
430 raise_param_exc({ filename => "missing '.tar.gz' or '.tar.xz' extension" });
431 }
432 $path = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage});
433 } else {
434 raise_param_exc({ content => "upload content type '$content' not allowed" });
435 }
436
437 die "storage '$param->{storage}' does not support '$content' content\n"
438 if !$scfg->{content}->{$content};
439
440 my $dest = "$path/$filename";
441 my $dirname = dirname($dest);
442
443 # best effort to match apl_download behaviour
444 chmod 0644, $tmpfilename;
445
446 # we simply overwrite the destination file if it already exists
447
448 my $cmd;
449 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
450 my $remip = PVE::Cluster::remote_node_ip($node);
451
452 my @ssh_options = ('-o', 'BatchMode=yes');
453
454 my @remcmd = ('/usr/bin/ssh', @ssh_options, $remip, '--');
455
456 eval {
457 # activate remote storage
458 PVE::Tools::run_command([@remcmd, '/usr/sbin/pvesm', 'status',
459 '--storage', $param->{storage}]);
460 };
461 die "can't activate storage '$param->{storage}' on node '$node': $@\n" if $@;
462
463 PVE::Tools::run_command([@remcmd, '/bin/mkdir', '-p', '--', PVE::Tools::shell_quote($dirname)],
464 errmsg => "mkdir failed");
465
466 $cmd = ['/usr/bin/scp', @ssh_options, '-p', '--', $tmpfilename, "[$remip]:" . PVE::Tools::shell_quote($dest)];
467 } else {
468 PVE::Storage::activate_storage($cfg, $param->{storage});
469 File::Path::make_path($dirname);
470 $cmd = ['cp', '--', $tmpfilename, $dest];
471 }
472
473 my $worker = sub {
474 my $upid = shift;
475
476 print "starting file import from: $tmpfilename\n";
477 print "target node: $node\n";
478 print "target file: $dest\n";
479 print "file size is: $size\n";
480 print "command: " . join(' ', @$cmd) . "\n";
481
482 eval { PVE::Tools::run_command($cmd, errmsg => 'import failed'); };
483 if (my $err = $@) {
484 unlink $dest;
485 die $err;
486 }
487 print "finished file import successfully\n";
488 };
489
490 my $upid = $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
491
492 # apache removes the temporary file on return, so we need
493 # to wait here to make sure the worker process starts and
494 # opens the file before it gets removed.
495 sleep(1);
496
497 return $upid;
498 }});
499
500 1;