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