]> git.ipfire.org Git - people/pmueller/ipfire-2.x.git/blob - html/cgi-bin/extrahd.cgi
Merge branch 'next'
[people/pmueller/ipfire-2.x.git] / html / cgi-bin / extrahd.cgi
1 #!/usr/bin/perl
2 ###############################################################################
3 # #
4 # IPFire.org - A linux based firewall #
5 # Copyright (C) 2023 IPFire Team <info@ipfire.org> #
6 # #
7 # This program is free software: you can redistribute it and/or modify #
8 # it under the terms of the GNU General Public License as published by #
9 # the Free Software Foundation, either version 3 of the License, or #
10 # (at your option) any later version. #
11 # #
12 # This program is distributed in the hope that it will be useful, #
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15 # GNU General Public License for more details. #
16 # #
17 # You should have received a copy of the GNU General Public License #
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
19 # #
20 ###############################################################################
21
22 use strict;
23 # enable only the following on debugging purpose
24 #use warnings;
25 #use CGI::Carp 'fatalsToBrowser';
26
27 require '/var/ipfire/general-functions.pl';
28 require "${General::swroot}/lang.pl";
29 require "${General::swroot}/header.pl";
30
31 my %extrahdsettings = ();
32 my $errormessage = "";
33
34 # Hash to store the configured drives.
35 my %configured_drives;
36
37 # SYSFS directory which contains all block device data.
38 my $sysfs_block_dir = "/sys/class/block";
39
40 # Array which contains the valid mount directories.
41 # Only mounting to subdirectories inside them is allowed.
42 my @valid_mount_dirs = (
43 "/data",
44 "/media",
45 "/mnt",
46 );
47
48 # Grab all available block devices.
49 my @devices = &get_block_devices();
50
51 # Grab all known UUID's.
52 my %uuids = &get_uuids();
53
54 # Grab all mountpoints.
55 my %mountpoints = &get_mountpoints();
56
57 # Omit the file system types of the mounted devices.
58 my %filesystems = &get_mountedfs();
59
60 # Gather all used swap devices.
61 my @swaps = &get_swaps();
62
63 # The config file which contains the configured devices.
64 my $devicefile = "/var/ipfire/extrahd/devices";
65
66 #workaround to suppress a warning when a variable is used only once
67 my @dummy = ( ${Header::colourgreen}, ${Header::colourred} );
68 undef (@dummy);
69
70 &Header::showhttpheaders();
71
72 ### Values that have to be initialized
73 $extrahdsettings{'PATH'} = '';
74 $extrahdsettings{'FS'} = '';
75 $extrahdsettings{'DEVICE'} = '';
76 $extrahdsettings{'ACTION'} = '';
77 $extrahdsettings{'UUID'} = '';
78
79 &Header::getcgihash(\%extrahdsettings);
80
81 &Header::openpage('ExtraHD', 1, '');
82 &Header::openbigbox('100%', 'left', '', $errormessage);
83
84 ############################################################################################################################
85 ############################################################################################################################
86
87 #
88 ## Add a new device.
89 #
90 if ($extrahdsettings{'ACTION'} eq $Lang::tr{'add'}) {
91 # Check if a mount path has been given.
92 if (not $extrahdsettings{'PATH'}) {
93 $errormessage = "$Lang::tr{'extrahd no mount point given'}.";
94
95 # Check if a valid mount path has been choosen.
96 } elsif(not &is_valid_dir("$extrahdsettings{'PATH'}")) {
97 $errormessage = "$Lang::tr{'extrahd you cant mount'} $extrahdsettings{'DEVICE'} $Lang::tr{'extrahd to'} $extrahdsettings{'PATH'} $Lang::tr{'extrahd because it is outside the allowed mount path'}.";
98
99 # Check if the given path allready is mounted somewhere.
100 } elsif(&is_mounted("$extrahdsettings{'PATH'}")) {
101 $errormessage = "$Lang::tr{'extrahd you cant mount'} $extrahdsettings{'DEVICE'} $Lang::tr{'extrahd to'} $extrahdsettings{'PATH'} $Lang::tr{'extrahd because there is already a device mounted'}.";
102 }
103
104 # Check against may previously configured drives.
105 unless ($errormessage) {
106 # Open device file for reading.
107 open( FILE, "< $devicefile" ) or die "Unable to read $devicefile";
108 my @devices = <FILE>;
109 close FILE;
110
111 # Loop through the entries line-by-line.
112 foreach my $entry (sort @devices) {
113 # Split the line into pieces and assign nice variables.
114 my ($uuid, $fs, $path) = split( /\;/, $entry );
115
116 # Check if the path is allready used.
117 if ( "$extrahdsettings{'PATH'}" eq "$path" ) {
118 $errormessage = "$Lang::tr{'extrahd you cant mount'} $extrahdsettings{'DEVICE'} $Lang::tr{'extrahd to'} $extrahdsettings{'PATH'} $Lang::tr{'extrahd because there is already a device mounted'}.";
119 }
120
121 # Check if the uuid is allready used.
122 if ("$extrahdsettings{'DEVICE'} eq $uuid") {
123 $errormessage = "$extrahdsettings{'DEVICE'} is allready mounted.";
124 }
125 }
126 }
127
128 # Go further if there was no error message.
129 unless($errormessage) {
130 # Re-open the device file for writing.
131 open(FILE, ">> $devicefile" ) or die "Unable to write $devicefile";
132
133 # Write the config line.
134 print FILE "UUID=$extrahdsettings{'UUID'};$extrahdsettings{'FS'};$extrahdsettings{'PATH'};\n";
135
136 # Close file handle.
137 close(FILE);
138
139 # Call helper binary to mount the device.
140 &General::system("/usr/local/bin/extrahdctrl", "mount", "$extrahdsettings{'PATH'}");
141 }
142
143 #
144 # Remove an existing one.
145 #
146 } elsif ($extrahdsettings{'ACTION'} eq $Lang::tr{'delete'}) {
147 # Call helper binary to unmount the device.
148 unless(&General::system("/usr/local/bin/extrahdctrl", "umount", "$extrahdsettings{'PATH'}")) {
149 # Open the device file for reading.
150 open(FILE, "< $devicefile" ) or die "Unable to read $devicefile";
151
152 # Read the file content into a temporary array.
153 my @tmp = <FILE>;
154
155 # Close file handle.
156 close(FILE);
157
158 # Re-open device file for writing.
159 open(FILE, "> $devicefile" ) or die "Unable to write $devicefile";
160
161 # Loop through the previous read file content.
162 foreach my $line (sort @tmp) {
163 # Split line content and assign nice variables.
164 my ($uuid, $fs, $path) = split( /\;/, $line );
165
166 # Write the line in case it does not contain our element to delete.
167 if ($path ne $extrahdsettings{'PATH'}) {
168 print FILE "$line";
169 }
170 }
171
172 # Close file handle.
173 close(FILE);
174 } else {
175 $errormessage = "$Lang::tr{'extrahd cant umount'} $extrahdsettings{'PATH'}$Lang::tr{'extrahd maybe the device is in use'}?";
176 }
177 }
178
179 if ($errormessage) {
180 &Header::openbox('100%', 'left', $Lang::tr{'error messages'});
181 print "<class name='base'>$errormessage\n";
182 print "&nbsp;</class>\n";
183 &Header::closebox();
184 }
185
186 ############################################################################################################################
187 ############################################################################################################################
188
189 &Header::openbox('100%', 'center', $Lang::tr{'extrahd detected drives'});
190
191 # Re-read mountpoints.
192 %mountpoints = &get_mountpoints();
193
194 # Read-in the device config file.
195 open( FILE, "< $devicefile" ) or die "Unable to read $devicefile";
196
197 # Loop through the file content.
198 while (<FILE>) {
199 # Cut the line into pieces.
200 my ($uuid, $fs, $path) = split( /\;/, $_ );
201
202 # Add the found entry to the hash of configured drives.
203 $configured_drives{$uuid} = $path;
204 }
205
206 # Close the file handle.
207 close(FILE);
208
209 print <<END
210 <table border='0' width='600' cellspacing="0">
211 END
212 ;
213 foreach my $device (sort @devices) {
214 # Grab the device details.
215 my $vendor = &get_device_vendor($device);
216 my $model = &get_device_model($device);
217 my $bsize = &get_device_size($device);
218
219 # Convert size into human-readable format.
220 my $size = &General::formatBytes($bsize);
221
222 print <<END
223 <tr><td colspan="5">&nbsp;</td></tr>
224 <tr><td align='left' colspan="2"><b>/dev/$device</b></td>
225 <td align='center' colspan="2">$vendor $model</td>
226
227 <td align='center'>$Lang::tr{'size'} $size</td>
228 <td>&nbsp;</td></tr>
229 <tr><td colspan="5">&nbsp;</td></tr>
230 END
231 ;
232
233 # Grab the known partitions of the current block device.
234 my @partitions = &get_device_partitions($device);
235
236 foreach my $partition (@partitions) {
237 my $disabled;
238
239 # Omit the partition size.
240 my $bsize = &get_device_size($partition);
241
242 # Convert into human-readable format.
243 my $size = &General::formatBytes($bsize);
244
245 # Try to omit the used filesystem.
246 my $fs = $filesystems{$partition};
247
248 # Get the mountpoint.
249 my $mountpoint = $mountpoints{$partition};
250
251 # If no mountpoint could be determined try to grab from
252 # configured drives.
253 unless($mountpoint) {
254 my $uuid = $uuids{$partition};
255
256 # Build uuid string.
257 $uuid = "UUID=" . $uuid;
258
259 # Try to obtain a possible moutpoint from configured drives.
260 $mountpoint = $configured_drives{$uuid} if ($configured_drives{$uuid});
261 }
262
263 # Check if the mountpoint is used as root or boot device.
264 if ($mountpoint eq "/" or $mountpoint =~ "^/boot") {
265 $disabled = "disabled";
266
267 # Check if it is mounted.
268 } elsif(&is_mounted($mountpoint)) {
269 $disabled = "disabled";
270
271 # Check if the device is used as swap.
272 } elsif (&is_swap($partition)) {
273 $disabled = "disabled";
274 $mountpoint = "swap";
275 $fs = "swap";
276 }
277
278 print <<END
279
280 <form method='post' action='$ENV{'SCRIPT_NAME'}'>
281 <tr><td align="left" colspan=5><strong>UUID=$uuids{$partition}</strong></td></tr>
282 <tr>
283 <td align="list">/dev/$partition</td>
284 <td align="center">$Lang::tr{'size'} $size</td>
285 <td align="center">$fs</td>
286 <td align="center"><input type='text' name='PATH' value='$mountpoint' $disabled></td>
287 <td align="center">
288 <input type='hidden' name='DEVICE' value='/dev/$partition' />
289 <input type='hidden' name='UUID' value='$uuids{$partition}' />
290 END
291 ;
292 # Check if the mountpoint refers to a known configured drive.
293 if(&is_configured($mountpoint)) {
294 print "<input type='hidden' name='ACTION' value='$Lang::tr{'delete'}'>\n";
295 print "<input type='hidden' name='PATH' value='$mountpoint'>\n";
296
297 # Check if the device is mounted properly.
298 if(&is_mounted($mountpoint)) {
299 print "<img src='/images/updbooster/updxl-led-green.gif' alt='$Lang::tr{'extrahd mounted'}' title='$Lang::tr{'extrahd mounted'}'>&nbsp;\n";
300 } else {
301 print "<img src='/images/updbooster/updxl-led-red.gif' alt='$Lang::tr{'extrahd not mounted'}' title='$Lang::tr{'extrahd not mounted'}'>&nbsp;\n";
302 }
303
304 print "<input type='image' alt='$Lang::tr{'delete'}' title='$Lang::tr{'delete'}' src='/images/delete.gif'>\n";
305 } else {
306 unless($disabled) {
307 print "<input type='hidden' name='ACTION' value='$Lang::tr{'add'}'>\n";
308 print "<input type='hidden' name='FS' value='auto'>\n";
309 print "<img src='/images/updbooster/updxl-led-gray.gif' alt='$Lang::tr{'extrahd not configured'}' title='$Lang::tr{'extrahd not configured'}'>&nbsp;\n";
310 print "<input type='image' alt='$Lang::tr{'add'}' title='$Lang::tr{'add'}' src='/images/add.gif'>\n";
311 }
312 }
313
314 print <<END
315 </form></td></tr>
316 END
317 ; }
318
319 }
320
321 print <<END
322 <tr><td align="center" colspan="5">&nbsp;</td></tr>
323 <tr><td align="center" colspan="5">&nbsp;</td></tr>
324 <tr><td align="center" colspan="5">$Lang::tr{'extrahd install or load driver'}</td></tr>
325 </table>
326 END
327 ;
328 &Header::closebox();
329
330 &Header::closebigbox();
331 &Header::closepage();
332
333 #
334 ## Function which return an array with all available block devices.
335 #
336 sub get_block_devices () {
337 my @devices;
338
339 # Open directory from kernel sysfs.
340 opendir(DEVICES, "/sys/block");
341
342 # Loop through the directory.
343 while(readdir(DEVICES)) {
344 # Skip . and ..
345 next if($_ =~ /^\.$/);
346 next if($_ =~ /^\..$/);
347
348 # Skip any loopback and ram devices.
349 next if($_ =~ "^loop");
350 next if($_ =~ "^ram");
351
352 # Add the device to the array of found devices.
353 push(@devices, $_);
354 }
355
356 # Close directory handle.
357 closedir(DEVICES);
358
359 # Return the devices array.
360 return @devices;
361 }
362
363 #
364 ## Function which return all partitions of a given block device.
365 #
366 sub get_device_partitions ($) {
367 my ($device) = @_;
368
369 # Array to store the known partitions for the given
370 # device.
371 my @partitions;
372
373 # Assign device directory.
374 my $device_dir = "$sysfs_block_dir/$device";
375
376 # Abort and return nothing if the device dir does not exist.
377 return unless(-d "$device_dir");
378
379 opendir(DEVICE, "$sysfs_block_dir/$device");
380 while(readdir(DEVICE)) {
381 next unless($_ =~ "^$device");
382
383 push(@partitions, $_);
384 }
385
386 closedir(DEVICE);
387
388 @partitions = sort(@partitions);
389
390 return @partitions;
391 }
392
393 #
394 ## Returns the vendor of a given block device.
395 #
396 sub get_device_vendor ($) {
397 my ($device) = @_;
398
399 # Assign device directory.
400 my $device_dir = "$sysfs_block_dir/$device";
401
402 # Abort and return nothing if the device dir does not exist
403 # or no vendor file exists.
404 return unless(-d "$device_dir");
405 return unless(-f "$device_dir/device/vendor");
406
407 # Open and read-in the device vendor.
408 open(VENDOR, "$device_dir/device/vendor");
409 my $vendor = <VENDOR>;
410 close(VENDOR);
411
412 # Abort and return nothing if no vendor could be read.
413 return unless($vendor);
414
415 # Remove any newlines from the vendor string.
416 chomp($vendor);
417
418 # Return the omited vendor.
419 return $vendor;
420 }
421
422 #
423 ## Returns the model name (string) of a given block device.
424 #
425 sub get_device_model ($) {
426 my ($device) = @_;
427
428 # Assign device directory.
429 my $device_dir = "$sysfs_block_dir/$device";
430
431 # Abort and return nothing if the device dir does not exist
432 # or no model file exists.
433 return unless(-d "$device_dir");
434 return unless(-f "$device_dir/device/model");
435
436 # Open and read-in the device model.
437 open(MODEL, "$device_dir/device/model");
438 my $model = <MODEL>;
439 close(MODEL);
440
441 # Abort and return nothing if no model could be read.
442 return unless($model);
443
444 # Remove any newlines from the model string.
445 chomp($model);
446
447 # Return the model string.
448 return $model;
449 }
450
451 #
452 ## Returns the size of a given device in bytes.
453 #
454 sub get_device_size ($) {
455 my ($device) = @_;
456
457 # Assign device directory.
458 my $device_dir = "$sysfs_block_dir/$device";
459
460 # Abort and return nothing if the device dir does not exist
461 # or no size file exists.
462 return unless(-d "$device_dir");
463 return unless(-f "$device_dir/size");
464
465 # Open and read-in the device size.
466 open(SIZE, "$device_dir/size");
467 my $size = <SIZE>;
468 close(SIZE);
469
470 # Abort and return nothing if the size could not be read.
471 return unless($size);
472
473 # Remove any newlines for the size string.
474 chomp($size);
475
476 # The omited size only contains the amount of blocks from the
477 # given device. To convert this into bytes we have to multiply this
478 # value with 512 bytes for each block. This is a static value used by
479 # the linux kernel.
480 $size = $size * 512;
481
482 # Return the size in bytes.
483 return $size;
484 }
485
486 #
487 ## Function which returns all currently mounted devices as a hash.
488 ## example: "sda1" -> "/boot"
489 #
490 sub get_mountpoints () {
491 my %mounts;
492
493 # Open and read-in the current mounts from the
494 # kernel file system.
495 open(MOUNT, "/proc/mounts");
496
497 # Loop through the known mounts.
498 while(<MOUNT>) {
499 # Skip mounts which does not belong to a device.
500 next unless ($_ =~ "^/dev");
501
502 # Cut the line into pieces and assign nice variables.
503 my ($dev, $mpoint, $fs, $options, $a, $b) = split(/ /, $_);
504
505 # Split the device name.
506 my @tmp = split("/", $dev);
507
508 # Assign the plain device name to a new variable.
509 # It is the last element of the array.
510 my $device = $tmp[-1];
511
512 # Add the mountpoint to the hash of mountpoints.
513 $mounts{"$device"} = $mpoint;
514 }
515
516 # Close file handle.
517 close(MOUNT);
518
519 # Return the hash of known mountpoints.
520 return %mounts;
521 }
522
523 sub get_swaps () {
524 my @swaps;
525
526 # Open and read the swaps file.
527 open(SWAP, "/proc/swaps");
528
529 # Loop though the file content.
530 while(<SWAP>) {
531 # Skip lines which does not belong to a device.
532 next unless ($_ =~ "^/dev");
533
534 # Split the line and assign nice variables.
535 my ($dev, $type, $size, $used, $prio) = split(/ /, $_);
536
537 # Cut the device line into pieces.
538 my @tmp = split("/", $dev);
539
540 my $device = @tmp[-1];
541
542 # Add the found swap to the array of swaps.
543 push(@swaps, $device);
544 }
545
546 # Close file handle.
547 close(SWAP);
548
549 # Sort the array.
550 @swaps = sort(@swaps);
551
552 # Return the array.
553 return @swaps;
554 }
555
556 #
557 ## Function with returns the mounted devices and the used filesystems as a hash.
558 ## Example: "sda1" -> "ext4"
559 #
560 sub get_mountedfs () {
561 my %mountedfs;
562
563 # Open and read the current mounts from the kernel
564 # file system.
565 open(MOUNT, "/proc/mounts");
566
567 # Loop through the known mounts.
568 while(<MOUNT>) {
569 # Skip mounts which does not belong to a device.
570 next unless ($_ =~ "^/dev");
571
572 # Split line and assign nice variables.
573 my ($dev, $mpoint, $fs, $options, $a, $b) = split(/ /, $_);
574
575 # Cut the device line into pieces.
576 my @tmp = split("/", $dev);
577
578 # Assign the plain device name to a variable
579 # It is the last element of the temporary array.
580 my $device = $tmp[-1];
581
582 # Convert the filesystem into lower case format.
583 $fs = lc($fs);
584
585 # Add the mounted file system.
586 $mountedfs{$device} = $fs;
587 }
588
589 # Close file handle.
590 close(MOUNT);
591
592 # Return the hash with the mounted filesystems.
593 return %mountedfs;
594 }
595
596 #
597 ## Function which returns all known UUID's as a hash.
598 ## Example: "sda1" -> "1234-5678-abcd"
599 #
600 sub get_uuids () {
601 my %uuids;
602
603 # Directory where the uuid mappings can be found.
604 my $uuid_dir = "/dev/disk/by-uuid";
605
606 # Open uuid directory and read-in the current known uuids.
607 opendir(UUIDS, "$uuid_dir");
608
609 # Loop through the uuids.
610 foreach my $uuid (readdir(UUIDS)) {
611 # Skip . and ..
612 next if($uuid eq "." or $uuid eq "..");
613
614 # Skip everything which is not a symbolic link.
615 next unless(-l "$uuid_dir/$uuid");
616
617 # Resolve the target of the symbolic link.
618 my $target = readlink("$uuid_dir/$uuid");
619
620 # Split the link target into pieces.
621 my @tmp = split("/", $target);
622
623 # Assign the last element of the array to the dev variable.
624 my $dev = "$tmp[-1]";
625
626 # Add the device and uuid to the hash of uuids.
627 $uuids{$dev} = $uuid;
628 }
629
630 # Close directory handle.
631 closedir(UUIDS);
632
633 # Return the hash of uuids.
634 return %uuids;
635 }
636
637 #
638 ## Returns the device name of a given uuid.
639 #
640 sub device_by_uuid ($) {
641 my ($uuid) = @_;
642
643 # Reverse the main uuids hash.
644 my %uuids = reverse %uuids;
645
646 # Lookup and return the device name.
647 return $uuids{$uuid};
648 }
649
650 #
651 ## Returns "True" in case a given path is a known mountpoint.
652 #
653 sub is_mounted ($) {
654 my ($mpoint) = @_;
655
656 my %mountpoints = reverse %mountpoints;
657
658 # Return "True" if the requested mountpoint is known and
659 # therefore mounted.
660 return 1 if($mountpoints{$mpoint});
661 }
662
663 #
664 ## Returns "True" if a given mountpoint is a subdirectory of one
665 ## of the directories specified by the valid_mount_dirs array abouve.
666 #
667 sub is_valid_dir ($) {
668 my ($mpoint) = @_;
669
670 # Do not allow "/mnt" or "/media" as mount points.
671 return if($mpoint eq "/mnt");
672 return if($mpoint eq "/media");
673
674 # Split the given mountpoint into pieces and store them
675 # in a temporay array.
676 my @tmp = split("/", $mpoint);
677
678 # Exit and return nothing if the temporary array is empty.
679 return unless(@tmp);
680
681 # Build the root path based on the given mount point.
682 my $root_path = "/" . @tmp[1];
683
684 # Check if the root path is valid.
685 return 1 if(grep /$root_path/, @valid_mount_dirs);
686 }
687
688 #
689 # Returns "True" if a device is used as swap.
690 #
691 sub is_swap ($) {
692 my ($device) = @_;
693
694 return 1 if(grep /$device/, @swaps);
695 }
696
697 #
698 ## Returns "True" if a drive is a configured one.
699 #
700 sub is_configured ($) {
701 my ($path) = @_;
702
703 # Loop through the hash of configured drives.
704 foreach my $uuid (keys %configured_drives) {
705 return 1 if($configured_drives{$uuid} eq "$path");
706 }
707 }