]> git.proxmox.com Git - pve-storage.git/blob - PVE/API2/Disks/Directory.pm
4fdb06874d0754a55ac4e9beba80664db45960b9
[pve-storage.git] / PVE / API2 / Disks / Directory.pm
1 package PVE::API2::Disks::Directory;
2
3 use strict;
4 use warnings;
5
6 use POSIX;
7
8 use PVE::Diskmanage;
9 use PVE::JSONSchema qw(get_standard_option);
10 use PVE::RESTHandler;
11 use PVE::RPCEnvironment;
12 use PVE::Systemd;
13 use PVE::Tools qw(run_command trim file_set_contents file_get_contents dir_glob_foreach lock_file);
14
15 use PVE::API2::Storage::Config;
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',
116 description => 'The path of the mount unit.',
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) = @_;
144 $storid = PVE::Systemd::unescape_unit($storid);
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',
178 description => 'The block device you want to create the filesystem on.',
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 my $path = "/mnt/pve/$name";
207 my $mountunitname = PVE::Systemd::escape_unit($path, 1) . ".mount";
208 my $mountunitpath = "/etc/systemd/system/$mountunitname";
209
210 $dev = PVE::Diskmanage::verify_blockdev_path($dev);
211 PVE::Diskmanage::assert_disk_unused($dev);
212
213 my $storage_params = {
214 type => 'dir',
215 storage => $name,
216 content => 'rootdir,images,iso,backup,vztmpl,snippets',
217 is_mountpoint => 1,
218 path => $path,
219 nodes => $node,
220 };
221 my $verify_params = [qw(path)];
222
223 if ($param->{add_storage}) {
224 PVE::API2::Storage::Config->create_or_update(
225 $name,
226 $node,
227 $storage_params,
228 $verify_params,
229 1,
230 );
231 }
232
233 my $mounted = PVE::Diskmanage::mounted_paths();
234 die "the path for '${name}' is already mounted: ${path} ($mounted->{$path})\n"
235 if $mounted->{$path};
236 die "a systemd mount unit already exists: ${mountunitpath}\n" if -e $mountunitpath;
237
238 my $worker = sub {
239 PVE::Diskmanage::locked_disk_action(sub {
240 PVE::Diskmanage::assert_disk_unused($dev);
241
242 my $part = $dev;
243
244 if (PVE::Diskmanage::is_partition($dev)) {
245 eval { PVE::Diskmanage::change_parttype($dev, '8300'); };
246 warn $@ if $@;
247 } else {
248 # create partition
249 my $cmd = [$SGDISK, '-n1', '-t1:8300', $dev];
250 print "# ", join(' ', @$cmd), "\n";
251 run_command($cmd);
252
253 my ($devname) = $dev =~ m|^/dev/(.*)$|;
254 $part = "/dev/";
255 dir_glob_foreach("/sys/block/$devname", qr/\Q$devname\E.+/, sub {
256 my ($partition) = @_;
257 $part .= $partition;
258 });
259 }
260
261 # create filesystem
262 my $cmd = [$MKFS, '-t', $type, $part];
263 print "# ", join(' ', @$cmd), "\n";
264 run_command($cmd);
265
266 # create systemd mount unit and enable & start it
267 my $ini = {
268 'Unit' => {
269 'Description' => "Mount storage '$name' under /mnt/pve",
270 },
271 'Install' => {
272 'WantedBy' => 'multi-user.target',
273 },
274 };
275
276 my $uuid_path;
277 my $uuid;
278
279 $cmd = [$BLKID, $part, '-o', 'export'];
280 print "# ", join(' ', @$cmd), "\n";
281 run_command($cmd, outfunc => sub {
282 my ($line) = @_;
283
284 if ($line =~ m/^UUID=(.*)$/) {
285 $uuid = $1;
286 $uuid_path = "/dev/disk/by-uuid/$uuid";
287 }
288 });
289
290 die "could not get UUID of device '$part'\n" if !$uuid;
291
292 $ini->{'Mount'} = {
293 'What' => $uuid_path,
294 'Where' => $path,
295 'Type' => $type,
296 'Options' => 'defaults',
297 };
298
299 $write_ini->($ini, $mountunitpath);
300
301 PVE::Diskmanage::udevadm_trigger($part);
302
303 run_command(['systemctl', 'daemon-reload']);
304 run_command(['systemctl', 'enable', $mountunitname]);
305 run_command(['systemctl', 'start', $mountunitname]);
306
307 if ($param->{add_storage}) {
308 PVE::API2::Storage::Config->create_or_update(
309 $name,
310 $node,
311 $storage_params,
312 $verify_params,
313 );
314 }
315 });
316 };
317
318 return $rpcenv->fork_worker('dircreate', $name, $user, $worker);
319 }});
320
321 __PACKAGE__->register_method ({
322 name => 'delete',
323 path => '{name}',
324 method => 'DELETE',
325 proxyto => 'node',
326 protected => 1,
327 permissions => {
328 check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
329 },
330 description => "Unmounts the storage and removes the mount unit.",
331 parameters => {
332 additionalProperties => 0,
333 properties => {
334 node => get_standard_option('pve-node'),
335 name => get_standard_option('pve-storage-id'),
336 'cleanup-config' => {
337 description => "Marks associated storage(s) as not available on this node anymore ".
338 "or removes them from the configuration (if configured for this node only).",
339 type => 'boolean',
340 optional => 1,
341 default => 0,
342 },
343 'cleanup-disks' => {
344 description => "Also wipe disk so it can be repurposed afterwards.",
345 type => 'boolean',
346 optional => 1,
347 default => 0,
348 },
349 },
350 },
351 returns => { type => 'string' },
352 code => sub {
353 my ($param) = @_;
354
355 my $rpcenv = PVE::RPCEnvironment::get();
356 my $user = $rpcenv->get_user();
357
358 my $name = $param->{name};
359 my $node = $param->{node};
360
361 my $worker = sub {
362 my $path = "/mnt/pve/$name";
363 my $mountunitname = PVE::Systemd::escape_unit($path, 1) . ".mount";
364 my $mountunitpath = "/etc/systemd/system/$mountunitname";
365
366 PVE::Diskmanage::locked_disk_action(sub {
367 my $to_wipe;
368 if ($param->{'cleanup-disks'}) {
369 my $unit = $read_ini->($mountunitpath);
370
371 my $dev = PVE::Diskmanage::verify_blockdev_path($unit->{'Mount'}->{'What'});
372 $to_wipe = $dev;
373
374 # clean up whole device if this is the only partition
375 $dev =~ s|^/dev/||;
376 my $info = PVE::Diskmanage::get_disks($dev, 1, 1);
377 die "unable to obtain information for disk '$dev'\n" if !$info->{$dev};
378 $to_wipe = $info->{$dev}->{parent}
379 if $info->{$dev}->{parent} && scalar(keys $info->%*) == 2;
380 }
381
382 run_command(['systemctl', 'stop', $mountunitname]);
383 run_command(['systemctl', 'disable', $mountunitname]);
384
385 unlink $mountunitpath or $! == ENOENT or die "cannot remove $mountunitpath - $!\n";
386
387 my $config_err;
388 if ($param->{'cleanup-config'}) {
389 my $match = sub {
390 my ($scfg) = @_;
391 return $scfg->{type} eq 'dir' && $scfg->{path} eq $path;
392 };
393 eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
394 warn $config_err = $@ if $@;
395 }
396
397 if ($to_wipe) {
398 PVE::Diskmanage::wipe_blockdev($to_wipe);
399 PVE::Diskmanage::udevadm_trigger($to_wipe);
400 }
401
402 die "config cleanup failed - $config_err" if $config_err;
403 });
404 };
405
406 return $rpcenv->fork_worker('dirremove', $name, $user, $worker);
407 }});
408
409 1;