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