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