]>
Commit | Line | Data |
---|---|---|
793d720c DC |
1 | package PVE::API2::Disks::Directory; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
a83d8eb1 FE |
6 | use POSIX; |
7 | ||
793d720c DC |
8 | use PVE::Diskmanage; |
9 | use PVE::JSONSchema qw(get_standard_option); | |
b0373adc TL |
10 | use PVE::RESTHandler; |
11 | use PVE::RPCEnvironment; | |
1022a7c4 | 12 | use PVE::Systemd; |
793d720c DC |
13 | use PVE::Tools qw(run_command trim file_set_contents file_get_contents dir_glob_foreach lock_file); |
14 | ||
b0373adc | 15 | use PVE::API2::Storage::Config; |
793d720c DC |
16 | |
17 | use base qw(PVE::RESTHandler); | |
18 | ||
19 | my $SGDISK = '/sbin/sgdisk'; | |
20 | my $MKFS = '/sbin/mkfs'; | |
21 | my $BLKID = '/sbin/blkid'; | |
22 | ||
23 | my $read_ini = sub { | |
24 | my ($filename) = @_; | |
25 | ||
26 | my $content = file_get_contents($filename); | |
27 | my @lines = split /\n/, $content; | |
28 | ||
29 | my $result = {}; | |
30 | my $section; | |
31 | ||
32 | foreach my $line (@lines) { | |
33 | $line = trim($line); | |
34 | if ($line =~ m/^\[([^\]]+)\]/) { | |
35 | $section = $1; | |
36 | if (!defined($result->{$section})) { | |
37 | $result->{$section} = {}; | |
38 | } | |
39 | } elsif ($line =~ m/^(.*?)=(.*)$/) { | |
40 | my ($key, $val) = ($1, $2); | |
41 | if (!$section) { | |
42 | warn "key value pair found without section, skipping\n"; | |
43 | next; | |
44 | } | |
45 | ||
46 | if ($result->{$section}->{$key}) { | |
47 | # make duplicate properties to arrays to keep the order | |
48 | my $prop = $result->{$section}->{$key}; | |
49 | if (ref($prop) eq 'ARRAY') { | |
50 | push @$prop, $val; | |
51 | } else { | |
52 | $result->{$section}->{$key} = [$prop, $val]; | |
53 | } | |
54 | } else { | |
55 | $result->{$section}->{$key} = $val; | |
56 | } | |
57 | } | |
58 | # ignore everything else | |
59 | } | |
60 | ||
61 | return $result; | |
62 | }; | |
63 | ||
64 | my $write_ini = sub { | |
65 | my ($ini, $filename) = @_; | |
66 | ||
67 | my $content = ""; | |
68 | ||
69 | foreach my $sname (sort keys %$ini) { | |
70 | my $section = $ini->{$sname}; | |
71 | ||
72 | $content .= "[$sname]\n"; | |
73 | ||
74 | foreach my $pname (sort keys %$section) { | |
75 | my $prop = $section->{$pname}; | |
76 | ||
77 | if (!ref($prop)) { | |
78 | $content .= "$pname=$prop\n"; | |
79 | } elsif (ref($prop) eq 'ARRAY') { | |
80 | foreach my $val (@$prop) { | |
81 | $content .= "$pname=$val\n"; | |
82 | } | |
83 | } else { | |
84 | die "invalid property '$pname'\n"; | |
85 | } | |
86 | } | |
87 | $content .= "\n"; | |
88 | } | |
89 | ||
90 | file_set_contents($filename, $content); | |
91 | }; | |
92 | ||
93 | __PACKAGE__->register_method ({ | |
94 | name => 'index', | |
95 | path => '', | |
96 | method => 'GET', | |
97 | proxyto => 'node', | |
98 | protected => 1, | |
99 | permissions => { | |
100 | check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1], | |
101 | }, | |
102 | description => "PVE Managed Directory storages.", | |
103 | parameters => { | |
104 | additionalProperties => 0, | |
105 | properties => { | |
106 | node => get_standard_option('pve-node'), | |
107 | }, | |
108 | }, | |
109 | returns => { | |
110 | type => 'array', | |
111 | items => { | |
112 | type => 'object', | |
113 | properties => { | |
114 | unitfile => { | |
115 | type => 'string', | |
fd87487b | 116 | description => 'The path of the mount unit.', |
793d720c DC |
117 | }, |
118 | path => { | |
119 | type => 'string', | |
120 | description => 'The mount path.', | |
121 | }, | |
122 | device => { | |
123 | type => 'string', | |
124 | description => 'The mounted device.', | |
125 | }, | |
126 | type => { | |
127 | type => 'string', | |
128 | description => 'The filesystem type.', | |
129 | }, | |
130 | options => { | |
131 | type => 'string', | |
132 | description => 'The mount options.', | |
133 | }, | |
134 | }, | |
135 | }, | |
136 | }, | |
137 | code => sub { | |
138 | my ($param) = @_; | |
139 | ||
140 | my $result = []; | |
141 | ||
142 | dir_glob_foreach('/etc/systemd/system', '^mnt-pve-(.+)\.mount$', sub { | |
143 | my ($filename, $storid) = @_; | |
1022a7c4 | 144 | $storid = PVE::Systemd::unescape_unit($storid); |
793d720c DC |
145 | |
146 | my $unitfile = "/etc/systemd/system/$filename"; | |
147 | my $unit = $read_ini->($unitfile); | |
148 | ||
149 | push @$result, { | |
150 | unitfile => $unitfile, | |
151 | path => "/mnt/pve/$storid", | |
152 | device => $unit->{'Mount'}->{'What'}, | |
153 | type => $unit->{'Mount'}->{'Type'}, | |
154 | options => $unit->{'Mount'}->{'Options'}, | |
155 | }; | |
156 | }); | |
157 | ||
158 | return $result; | |
159 | }}); | |
160 | ||
161 | __PACKAGE__->register_method ({ | |
162 | name => 'create', | |
163 | path => '', | |
164 | method => 'POST', | |
165 | proxyto => 'node', | |
166 | protected => 1, | |
167 | permissions => { | |
168 | check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']], | |
169 | }, | |
170 | description => "Create a Filesystem on an unused disk. Will be mounted under '/mnt/pve/NAME'.", | |
171 | parameters => { | |
172 | additionalProperties => 0, | |
173 | properties => { | |
174 | node => get_standard_option('pve-node'), | |
175 | name => get_standard_option('pve-storage-id'), | |
176 | device => { | |
177 | type => 'string', | |
fdc863c7 | 178 | description => 'The block device you want to create the filesystem on.', |
793d720c DC |
179 | }, |
180 | add_storage => { | |
181 | description => "Configure storage using the directory.", | |
182 | type => 'boolean', | |
183 | optional => 1, | |
184 | default => 0, | |
185 | }, | |
186 | filesystem => { | |
187 | description => "The desired filesystem.", | |
188 | type => 'string', | |
189 | enum => ['ext4', 'xfs'], | |
190 | optional => 1, | |
191 | default => 'ext4', | |
192 | }, | |
193 | }, | |
194 | }, | |
195 | returns => { type => 'string' }, | |
196 | code => sub { | |
197 | my ($param) = @_; | |
198 | ||
199 | my $rpcenv = PVE::RPCEnvironment::get(); | |
200 | my $user = $rpcenv->get_user(); | |
201 | ||
202 | my $name = $param->{name}; | |
203 | my $dev = $param->{device}; | |
204 | my $node = $param->{node}; | |
205 | my $type = $param->{filesystem} // 'ext4'; | |
206 | ||
207 | $dev = PVE::Diskmanage::verify_blockdev_path($dev); | |
0370861c | 208 | PVE::Diskmanage::assert_disk_unused($dev); |
9280153e | 209 | PVE::Storage::assert_sid_unused($name) if $param->{add_storage}; |
793d720c DC |
210 | |
211 | my $worker = sub { | |
212 | my $path = "/mnt/pve/$name"; | |
1022a7c4 | 213 | my $mountunitname = PVE::Systemd::escape_unit($path, 1) . ".mount"; |
793d720c DC |
214 | my $mountunitpath = "/etc/systemd/system/$mountunitname"; |
215 | ||
e39e8ee2 | 216 | PVE::Diskmanage::locked_disk_action(sub { |
e99bc248 FE |
217 | PVE::Diskmanage::assert_disk_unused($dev); |
218 | ||
a2c34371 FE |
219 | my $part = $dev; |
220 | ||
05d91712 FE |
221 | if (PVE::Diskmanage::is_partition($dev)) { |
222 | eval { PVE::Diskmanage::change_parttype($dev, '8300'); }; | |
223 | warn $@ if $@; | |
224 | } else { | |
a2c34371 FE |
225 | # create partition |
226 | my $cmd = [$SGDISK, '-n1', '-t1:8300', $dev]; | |
227 | print "# ", join(' ', @$cmd), "\n"; | |
228 | run_command($cmd); | |
229 | ||
230 | my ($devname) = $dev =~ m|^/dev/(.*)$|; | |
231 | $part = "/dev/"; | |
232 | dir_glob_foreach("/sys/block/$devname", qr/\Q$devname\E.+/, sub { | |
233 | my ($partition) = @_; | |
234 | $part .= $partition; | |
235 | }); | |
236 | } | |
793d720c DC |
237 | |
238 | # create filesystem | |
a2c34371 | 239 | my $cmd = [$MKFS, '-t', $type, $part]; |
793d720c DC |
240 | print "# ", join(' ', @$cmd), "\n"; |
241 | run_command($cmd); | |
242 | ||
243 | # create systemd mount unit and enable & start it | |
244 | my $ini = { | |
245 | 'Unit' => { | |
246 | 'Description' => "Mount storage '$name' under /mnt/pve", | |
247 | }, | |
248 | 'Install' => { | |
249 | 'WantedBy' => 'multi-user.target', | |
250 | }, | |
251 | }; | |
252 | ||
253 | my $uuid_path; | |
254 | my $uuid; | |
255 | ||
256 | $cmd = [$BLKID, $part, '-o', 'export']; | |
257 | print "# ", join(' ', @$cmd), "\n"; | |
258 | run_command($cmd, outfunc => sub { | |
259 | my ($line) = @_; | |
260 | ||
261 | if ($line =~ m/^UUID=(.*)$/) { | |
262 | $uuid = $1; | |
263 | $uuid_path = "/dev/disk/by-uuid/$uuid"; | |
264 | } | |
265 | }); | |
266 | ||
267 | die "could not get UUID of device '$part'\n" if !$uuid; | |
268 | ||
269 | $ini->{'Mount'} = { | |
270 | 'What' => $uuid_path, | |
271 | 'Where' => $path, | |
272 | 'Type' => $type, | |
273 | 'Options' => 'defaults', | |
274 | }; | |
275 | ||
276 | $write_ini->($ini, $mountunitpath); | |
277 | ||
26082b7d | 278 | PVE::Diskmanage::udevadm_trigger($part); |
21a75847 | 279 | |
793d720c DC |
280 | run_command(['systemctl', 'daemon-reload']); |
281 | run_command(['systemctl', 'enable', $mountunitname]); | |
282 | run_command(['systemctl', 'start', $mountunitname]); | |
283 | ||
284 | if ($param->{add_storage}) { | |
285 | my $storage_params = { | |
286 | type => 'dir', | |
287 | storage => $name, | |
4ec588fe | 288 | content => 'rootdir,images,iso,backup,vztmpl,snippets', |
793d720c DC |
289 | is_mountpoint => 1, |
290 | path => $path, | |
291 | nodes => $node, | |
292 | }; | |
293 | ||
294 | PVE::API2::Storage::Config->create($storage_params); | |
295 | } | |
296 | }); | |
793d720c DC |
297 | }; |
298 | ||
793d720c DC |
299 | return $rpcenv->fork_worker('dircreate', $name, $user, $worker); |
300 | }}); | |
301 | ||
a83d8eb1 FE |
302 | __PACKAGE__->register_method ({ |
303 | name => 'delete', | |
304 | path => '{name}', | |
305 | method => 'DELETE', | |
306 | proxyto => 'node', | |
307 | protected => 1, | |
308 | permissions => { | |
309 | check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']], | |
310 | }, | |
311 | description => "Unmounts the storage and removes the mount unit.", | |
312 | parameters => { | |
313 | additionalProperties => 0, | |
314 | properties => { | |
315 | node => get_standard_option('pve-node'), | |
316 | name => get_standard_option('pve-storage-id'), | |
f81908eb FE |
317 | 'cleanup-disks' => { |
318 | description => "Also wipe disk so it can be repurposed afterwards.", | |
319 | type => 'boolean', | |
320 | optional => 1, | |
321 | default => 0, | |
322 | }, | |
a83d8eb1 FE |
323 | }, |
324 | }, | |
325 | returns => { type => 'string' }, | |
326 | code => sub { | |
327 | my ($param) = @_; | |
328 | ||
329 | my $rpcenv = PVE::RPCEnvironment::get(); | |
330 | my $user = $rpcenv->get_user(); | |
331 | ||
332 | my $name = $param->{name}; | |
333 | ||
334 | my $worker = sub { | |
335 | my $path = "/mnt/pve/$name"; | |
336 | my $mountunitname = PVE::Systemd::escape_unit($path, 1) . ".mount"; | |
337 | my $mountunitpath = "/etc/systemd/system/$mountunitname"; | |
338 | ||
339 | PVE::Diskmanage::locked_disk_action(sub { | |
f81908eb FE |
340 | my $to_wipe; |
341 | if ($param->{'cleanup-disks'}) { | |
342 | my $unit = $read_ini->($mountunitpath); | |
343 | ||
344 | my $dev = PVE::Diskmanage::verify_blockdev_path($unit->{'Mount'}->{'What'}); | |
345 | $to_wipe = $dev; | |
346 | ||
347 | # clean up whole device if this is the only partition | |
348 | $dev =~ s|^/dev/||; | |
349 | my $info = PVE::Diskmanage::get_disks($dev, 1, 1); | |
350 | die "unable to obtain information for disk '$dev'\n" if !$info->{$dev}; | |
351 | $to_wipe = $info->{$dev}->{parent} | |
352 | if $info->{$dev}->{parent} && scalar(keys $info->%*) == 2; | |
353 | } | |
354 | ||
a83d8eb1 FE |
355 | run_command(['systemctl', 'stop', $mountunitname]); |
356 | run_command(['systemctl', 'disable', $mountunitname]); | |
357 | ||
358 | unlink $mountunitpath or $! == ENOENT or die "cannot remove $mountunitpath - $!\n"; | |
f81908eb FE |
359 | |
360 | if ($to_wipe) { | |
361 | PVE::Diskmanage::wipe_blockdev($to_wipe); | |
362 | PVE::Diskmanage::udevadm_trigger($to_wipe); | |
363 | } | |
a83d8eb1 FE |
364 | }); |
365 | }; | |
366 | ||
367 | return $rpcenv->fork_worker('dirremove', $name, $user, $worker); | |
368 | }}); | |
369 | ||
793d720c | 370 | 1; |