pb-plugin: Fix invalid command reference
[petitboot] / utils / pb-plugin
1 #!/bin/sh
2
3 __dest=/
4 __pb_mount_dir=/var/petitboot/mnt/dev/
5 plugin_abi=1
6 plugin_ext=pb-plugin
7 plugin_meta=pb-plugin.conf
8 plugin_meta_dir=etc/preboot-plugins/
9 plugin_meta_path=$plugin_meta_dir$plugin_meta
10 plugin_wrapper_dir=/var/lib/pb-plugins/bin
11
12 usage()
13 {
14         cat <<EOF
15 Usage: $0 <command>
16
17 Where <command> is one of:
18   install <FILE|URL> - install plugin from FILE/URL
19   scan               - look for available plugins on attached devices
20   create <DIR>       - create a new plugin archive from DIR
21   lint <FILE>        - perform a pre-distribution check on FILE
22 EOF
23 }
24
25 is_url()
26 {
27         local url tmp
28         url=$1
29         tmp=${url#*://}
30         [ "$url" != "$tmp" ]
31 }
32
33 download()
34 {
35         local url file proto
36         url=$1
37         file=$2
38         proto=${url%://*}
39
40         case "$proto" in
41         http)
42                 wget -O - "$url" > $file
43                 ;;
44         ftp)
45                 ncftpget -c "$url" > $file
46                 ;;
47         *)
48                 echo "error: Unsuported protocol $proto" >&2
49                 false
50         esac
51 }
52
53 plugin_info()
54 {
55         local title
56         if [ "$PLUGIN_VENDOR" ]
57         then
58                 title="$PLUGIN_VENDOR: $PLUGIN_NAME"
59         else
60                 title="$PLUGIN_NAME"
61         fi
62
63         echo "$title"
64         echo "  (version $PLUGIN_VERSION)"
65 }
66
67 # How the ABI versioning works:
68 #
69 #  - This script has an ABI defined ($plugin_abi)
70 #
71 #  - Plugins have a current ABI number ($PLUGIN_ABI), and a minimum supported
72 #    ABI number ($PLUGIN_ABI_MIN).
73 #
74 #  - A plugin is OK to run if:
75 #    - the plugin's ABI matches the script ABI, or
76 #    - the plugin's minimum ABI is lower than or equal to the script ABI
77 plugin_abi_check()
78 {
79         [ -n "$PLUGIN_ABI" ] &&
80                 ( [ $PLUGIN_ABI -eq $plugin_abi ] ||
81                   [ $PLUGIN_ABI_MIN -le $plugin_abi ] )
82 }
83
84 do_wrap()
85 {
86         local base binary dir
87
88         base=$1
89         binary=$2
90         shift 2
91
92         for dir in etc dev sys proc var
93         do
94                 [ -e "$base/$dir" ] || mkdir -p "$base/$dir"
95         done
96
97         cp /etc/resolv.conf $base/etc
98         mount -o bind /dev $base/dev
99         mount -o bind /sys $base/sys
100         mount -o bind /proc $base/proc
101         mount -o bind /var $base/var
102
103         chroot "$base" "$binary" "$@"
104
105         umount $base/dev
106         umount $base/sys
107         umount $base/proc
108         umount $base/var
109 }
110
111 __create_wrapper()
112 {
113         local base binary wrapper
114
115         base=$1
116         binary=$2
117         wrapper=$plugin_wrapper_dir/$(basename $binary)
118
119         mkdir -p $plugin_wrapper_dir
120
121         cat <<EOF > $wrapper
122 #!/bin/sh
123
124 exec $(realpath $0) __wrap '$base' '$binary' "\$@"
125 EOF
126
127         chmod a+x $wrapper
128 }
129
130 do_install()
131 {
132         local url name file __dest
133
134         url=$1
135
136         if [ -z "$url" ]
137         then
138                 echo "error: install requires a file/URL argument." >&2
139                 exit 1
140         fi
141
142         name=${url##*/}
143
144         if is_url "$url"
145         then
146                 file=$(mktemp)
147                 trap "rm '$file'" EXIT
148                 download "$url" "$file"
149                 if [ $? -ne 0 ]
150                 then
151                         echo "error: failed to download $url" >&2
152                         exit 1
153                 fi
154         else
155                 file=$url
156                 if [ ! -r "$file" ]
157                 then
158                         echo "error: $file doesn't exist or is not readable" >&2
159                         exit 1
160                 fi
161         fi
162
163         echo "File '$name' has the following sha256 checksum:"
164         echo
165         sha256sum "$file" | cut -f1 -d' '
166         echo
167
168         echo "Do you want to install this plugin? (y/N)"
169         read resp
170
171         case $resp in
172         [yY]|[yY][eE][sS])
173                 ;;
174         *)
175                 echo "Cancelled"
176                 exit 0
177                 ;;
178         esac
179
180         __dest=$(mktemp -d)
181         gunzip -c "$file" | ( cd $__dest && cpio -i -d)
182
183         if [ $? -ne 0 ]
184         then
185                 echo "error: Failed to extract archive $url, exiting"
186                 rm -rf $__dest
187                 exit 1
188         fi
189
190         . $__dest/$plugin_meta_path
191
192         if ! plugin_abi_check
193         then
194                 echo "Plugin at $url is incompatible with this firmware," \
195                         "exiting."
196                 rm -rf $__dest
197                 exit 1
198         fi
199
200         for binary in ${PLUGIN_EXECUTABLES}
201         do
202                 __create_wrapper "$__dest" "$binary"
203         done
204
205         echo "Plugin installed"
206         plugin_info
207 }
208
209 do_scan_mount()
210 {
211         local mnt dev plugin_path __meta_tmp
212         mnt=$1
213         dev=$(basename $mnt)
214
215         for plugin_path in $mnt/*.$plugin_ext
216         do
217                 [ -e "$plugin_path" ] || continue
218
219                 # extract plugin metadata to a temporary directory
220                 __meta_tmp=$(mktemp -d)
221                 [ -d $__meta_tmp ] || continue
222                 gunzip -c "$plugin_path" 2>/dev/null |
223                         (cd $__meta_tmp &&
224                                 cpio -i -d $plugin_meta_path 2>/dev/null)
225                 if ! [ $? = 0 -a -e "$plugin_path" ]
226                 then
227                         rm -rf $__meta_tmp
228                         continue
229                 fi
230
231                 (
232                         . $__meta_tmp/$plugin_meta_path
233
234                         plugin_abi_check || exit 1
235
236                         printf "Plugin found on %s:\n" $dev
237                         plugin_info
238                         printf "\n"
239                         printf "To run this plugin:\n"
240                         printf "  $0 install $plugin_path\n"
241                         printf "\n"
242                 )
243                 if [ $? = 0 ]
244                 then
245                         found=1
246                 fi
247                 rm -rf $__meta_tmp
248         done
249 }
250
251 do_scan()
252 {
253         local found mnt
254         found=0
255         for mnt in $__pb_mount_dir/*
256         do
257                 do_scan_mount $mnt
258         done
259
260         if [ "$found" = 0 ]
261         then
262                 echo "No plugins found"
263         fi
264 }
265
266 guided_meta()
267 {
268         local vendorname vendorshortname
269         local pluginname pluginnhortname
270         local version date executable
271         local file
272
273         file=$1
274
275 cat <<EOF
276
277 Enter the vendor company / author name. This can contain spaces.
278 (eg. 'Example Corporation')
279 EOF
280         read vendorname
281 cat <<EOF
282
283 Enter the vendor shortname. This should be a single-word abbreviation, in all
284 lower-case. This is only used in internal filenames.
285
286 Typically, a stock-ticker name is used for this (eg 'exco')
287 EOF
288         read vendorshortname
289
290 cat <<EOF
291
292 Enter the descriptive plugin name. This can contain spaces, but should only be
293 a few words in length (eg 'RAID configuration utility')
294 EOF
295         read pluginname
296
297 cat <<EOF
298
299 Enter the plugin shortname. This should not contain spaces, but hyphens are
300 fine (eg 'raid-config'). This is only used in internal filnames.
301 EOF
302         read pluginshortname
303
304
305 cat <<EOF
306
307 Enter the plugin version. This should not contain spaces (eg 1.2):
308 EOF
309         read version
310
311 cat <<EOF
312
313 Enter the full path (within the plugin root) to the plugin executable file(s).
314 These will be exposed as wrapper scripts, to be run from the standard petitboot
315 shell environment (eg, /usr/bin/my-raid-config).
316
317 If multiple executables are provided, separate with a space.
318 EOF
319         read executables
320
321         date=$(date +%Y-%m-%d)
322
323         mkdir -p $(dirname $file)
324
325         cat <<EOF > $file
326 PLUGIN_ABI='$plugin_abi'
327 PLUGIN_ABI_MIN='1'
328 PLUGIN_VENDOR='$vendorname'
329 PLUGIN_VENDOR_ID='$vendorshortname'
330 PLUGIN_NAME='$pluginname'
331 PLUGIN_ID='$pluginshortname'
332 PLUGIN_VERSION='$version'
333 PLUGIN_DATE='$date'
334 PLUGIN_EXECUTABLES='$executables'
335 EOF
336
337 }
338
339 do_create()
340 {
341         local src meta_dir_abs meta_file
342         src=$1
343
344         if [ -z "$src" ]
345         then
346                 echo "error: missing source directory" >&2
347                 usage
348                 exit 1
349         fi
350
351         if [ ! -d "$src" ]
352         then
353                 echo "error: source directory missing" >&2
354                 exit 1
355         fi
356
357         meta_file=$src/$plugin_meta_path
358
359         if [ ! -e $meta_file ]
360         then
361                 echo "No plugin metadata file found. " \
362                         "Would you like to create one? (Y/n)"
363                 read resp
364                 case "$resp" in
365                 [nN]|[nN][oO])
366                         echo "Cancelled, exiting"
367                         exit 1
368                         ;;
369                 esac
370                 guided_meta $meta_file || exit
371         fi
372
373         # Sanity check metadata file
374         . $meta_file
375
376         errors=0
377         warnings=0
378
379         lint_metadata
380
381         if [ $errors -ne 0 ]
382         then
383                 exit 1
384         fi
385
386         outfile=${PLUGIN_ID}-${PLUGIN_VERSION}.${plugin_ext}
387
388         (
389                 cd $src
390                 find -mindepth 1 | cpio -o -Hnewc -v
391         ) | gzip -c > $outfile
392
393         echo
394         echo "Plugin metadata:"
395         sed -e 's/^/  /' $meta_file
396         echo
397
398         echo "User-visible metadata:"
399         plugin_info | sed -e 's/^/  /'
400
401         echo
402
403 cat <<EOF
404 Plugin created in:
405   $outfile
406
407 Ship this file in the top-level-directory of a USB device or CD to have it
408 automatically discoverable by 'pb-plugin scan'. This file can be re-named,
409 but must retain the .$plugin_ext extension to be discoverable.
410 EOF
411 }
412
413 lint_fatal()
414 {
415         echo "fatal:" "$@"
416         [ -d "$__dest" ] && rm -rf "$__dest"
417         exit 1
418 }
419
420 lint_err()
421 {
422         echo "error:" "$@"
423         errors=$(($errors+1))
424 }
425
426 lint_warn()
427 {
428         echo "warning:" "$@"
429         warnings=$(($warnings+1))
430 }
431
432 lint_metadata()
433 {
434         [ -n "$PLUGIN_ABI" ] ||
435                 lint_err "no PLUGIN_ABI defined in metadata"
436
437         printf '%s' "$PLUGIN_ABI" | grep -q '[^0-9]' &&
438                 lint_err "PLUGIN_ABI has non-numeric characters"
439
440         [ -n "$PLUGIN_ABI_MIN" ] ||
441                 lint_err "no PLUGIN_ABI_MIN defined in metadata"
442
443         printf '%s' "$PLUGIN_ABI_MIN" | grep -q '[^0-9]' &&
444                 lint_err "PLUGIN_ABI_MIN has non-numeric characters"
445
446         [ "$PLUGIN_ABI" = "$plugin_abi" ] ||
447                 lint_warn "PLUGIN_ABI (=$PLUGIN_ABI) is not $plugin_abi"
448
449         [ -n "$PLUGIN_VENDOR" ] ||
450                 lint_err "no PLUGIN_VENDOR defined in metadata"
451
452         [ -n "$PLUGIN_VENDOR_ID" ] ||
453                 lint_err "no PLUGIN_VENDOR_ID defined in metadata"
454
455         printf '%s' "$PLUGIN_VENDOR_ID" | grep -q '[^a-z0-9-]' &&
456                 lint_err "PLUGIN_VENDOR_ID should only contain lowercase" \
457                         "alphanumerics and hyphens"
458
459         [ -n "$PLUGIN_NAME" ] ||
460                 lint_err "no PLUGIN_NAME defined in metadata"
461
462         [ -n "$PLUGIN_ID" ] ||
463                 lint_err "no PLUGIN_ID defined in metadata"
464
465         printf '%s' "$PLUGIN_ID" | grep -q '[^a-z0-9-]' &&
466                 lint_err "PLUGIN_ID should only contain lowercase" \
467                         "alphanumerics and hyphens"
468
469         [ "$PLUGIN_VERSION" ] ||
470                 lint_err "no PLUGIN_VERSION defined in metadata"
471
472         [ -n "$PLUGIN_DATE" ] ||
473                 lint_err "no PLUGIN_DATE defined in metadata"
474
475         [ -n "$PLUGIN_EXECUTABLES" ] ||
476                 lint_err "no PLUGIN_EXECUTABLES defined in metadata"
477 }
478
479 do_lint()
480 {
481         local plugin_file errors warnings __dest executable dir
482
483         plugin_file=$1
484         errors=0
485         warnings=0
486         __dest=
487
488         [ "${plugin_file##*.}" = $plugin_ext ] ||
489                 lint_err "Plugin file does not end with $plugin_ext"
490
491         gunzip -c "$plugin_file" > /dev/null 2>&1 ||
492                 lint_fatal "Plugin can't be gunzipped"
493
494         gunzip -c "$plugin_file" 2>/dev/null | cpio -t >/dev/null 2>&1 ||
495                 lint_fatal "Plugin can't be cpioed"
496
497         __dest=$(mktemp -d)
498         gunzip -c "$plugin_file" | ( cd $__dest && cpio -i -d 2>/dev/null)
499
500         [ -e "$__dest/$plugin_meta_path" ] ||
501                 lint_fatal "No metadata file present (expecting" \
502                         "$plugin_meta_path)"
503
504         (sh -e "$__dest/$plugin_meta_path") >/dev/null 2>&1  ||
505                 lint_err "Plugin metadata file has shell errors"
506
507         . "$__dest/$plugin_meta_path"
508         lint_metadata
509
510         for executable in ${PLUGIN_EXECUTABLES}
511         do
512                 exec_path="$__dest/$executable"
513                 [ -e "$exec_path" ] || {
514                         lint_err "PLUGIN_EXECUTABLES item $executable" \
515                                 "doesn't exist"
516                         continue
517                 }
518
519                 [ -x "$exec_path" ] ||
520                         lint_err "PLUGIN_EXECUTABLES item $executable" \
521                                 "isn't executable"
522         done
523
524         for dir in dev sys proc var
525         do
526                 [ -e "$__dest/$dir" ] || continue
527
528                 [ -d "$__dest/$dir" ] ||
529                         lint_err "/$dir exists, but isn't a directory"
530
531                 [ "$(find $__dest/$dir -mindepth 1)" ] &&
532                         lint_warn "/$dir contains files/directories," \
533                                 "these will be lost during chroot setup"
534         done
535
536         printf '%s: %d errors, %d warnings\n' $plugin_file $errors $warnings
537         rm -rf $__dest
538         [ $errors = 0 ]
539 }
540
541 test_http_download()
542 {
543         local tmp ref
544
545         tmp=$(mktemp -p $test_tmpdir)
546         ref=$(mktemp -p $test_tmpdir)
547
548         echo $RANDOM > $ref
549
550         wget()
551         {
552                 cat $ref
553         }
554
555         download http://example.com/test $tmp
556         cmp -s "$ref" "$tmp"
557 }
558
559 test_ftp_download()
560 {
561         local tmp ref
562
563         tmp=$(mktemp -p $test_tmpdir)
564         ref=$(mktemp -p $test_tmpdir)
565
566         echo $RANDOM > $ref
567
568         ncftpget()
569         {
570                 cat $ref
571         }
572
573         download ftp://example.com/test $tmp
574         cmp -s "$ref" "$tmp"
575 }
576
577 test_abi_check()
578 {
579         (
580                 plugin_abi=$1
581                 PLUGIN_ABI=$2
582                 PLUGIN_ABI_MIN=$3
583                 plugin_abi_check
584         )
585 }
586
587 test_scan()
588 {
589         __pb_mount_dir="$test_tmpdir/mnt"
590         mnt_dir="$__pb_mount_dir/sda"
591         mkdir -p $mnt_dir/$plugin_meta_dir
592         (
593                 echo "PLUGIN_ABI=$plugin_abi"
594                 echo "PLUGIN_NAME=test"
595                 echo "PLUGIN_VERSION=1"
596                 echo "PLUGIN_EXECUTABLES=/bin/sh"
597         ) > $mnt_dir/$plugin_meta_path
598         (
599                 cd $mnt_dir;
600                 find -mindepth 1 | cpio -o -Hnewc 2>/dev/null
601         ) | gzip -c > $mnt_dir/test.$plugin_ext
602
603         do_scan | grep -q 'test'
604 }
605
606 test_scan_nogzip()
607 {
608         __pb_mount_dir="$test_tmpdir/mnt"
609         mnt_dir="$__pb_mount_dir/sda"
610         stderr_file="$test_tmpdir/stderr"
611
612         mkdir -p $mnt_dir
613         echo "invalid" > $mnt_dir/nogzip.$plugin_ext
614
615         do_scan 2>$stderr_file | grep -q 'No plugins'
616
617         [ $? = 0 ] || return 1
618
619         if [ -s "$stderr_file" ]
620         then
621                 echo "Scan with invalid (non-gzip) file produced error output" \
622                         >&2
623                 cat "$stderr_file"
624                 return 1
625         fi
626         true
627 }
628
629 test_scan_nocpio()
630 {
631         __pb_mount_dir="$test_tmpdir/mnt"
632         mnt_dir="$__pb_mount_dir/sda"
633         stderr_file="$test_tmpdir/stderr"
634
635         mkdir -p $mnt_dir
636         echo "invalid" | gzip -c > $mnt_dir/nogzip.$plugin_ext
637
638         do_scan 2>$stderr_file | grep -q 'No plugins'
639
640         [ $? = 0 ] || return 1
641
642         if [ -s "$stderr_file" ]
643         then
644                 echo "Scan with invalid (non-cpio) file produced error output" \
645                         >&2
646                 cat "$stderr_file"
647                 return 1
648         fi
649         true
650 }
651
652 test_scan_multiple()
653 {
654         __pb_mount_dir="$test_tmpdir/mnt"
655         mnt_dir="$__pb_mount_dir/sda"
656         outfile=$test_tmpdir/scan.out
657
658         for i in 1 2
659         do
660                 mkdir -p $mnt_dir/$plugin_meta_dir
661                 (
662                         echo "PLUGIN_ABI=$plugin_abi"
663                         echo "PLUGIN_NAME=test-$i"
664                         echo "PLUGIN_VERSION=1"
665                         echo "PLUGIN_EXECUTABLES=/bin/sh"
666                 ) > $mnt_dir/$plugin_meta_path
667                 (
668                         cd $mnt_dir;
669                         find -mindepth 1 | cpio -o -Hnewc 2>/dev/null
670                 ) | gzip -c > $mnt_dir/test-${i}.$plugin_ext
671                 rm -rf $mnt_dir/$plugin_meta_dir
672         done
673
674         do_scan >$outfile
675
676         grep -q 'test-1' $outfile && grep -q 'test-2' $outfile
677 }
678
679 test_scan_wrongabi()
680 {
681         __pb_mount_dir="$test_tmpdir/mnt"
682         mnt_dir="$__pb_mount_dir/sda"
683         mkdir -p $mnt_dir/$plugin_meta_dir
684         (
685                 echo "PLUGIN_ABI=$(($plugin_abi + 1))"
686                 echo "PLUGIN_ABI_MIN=$(($plugin_abi + 1))"
687                 echo "PLUGIN_NAME=test"
688                 echo "PLUGIN_VERSION=1"
689                 echo "PLUGIN_EXECUTABLES=/bin/sh"
690         ) > $mnt_dir/$plugin_meta_path
691         (
692                 cd $mnt_dir;
693                 find -mindepth 1 | cpio -o -Hnewc 2>/dev/null
694         ) | gzip -c > $mnt_dir/test.$plugin_ext
695
696         do_scan | grep -q 'No plugins'
697 }
698
699 test_empty_scan()
700 {
701         __pb_mount_dir="$test_tmpdir/mnt"
702         mkdir -p $__pb_mount_dir
703         do_scan | grep -q "No plugins"
704 }
705
706 test_setup()
707 {
708         n=$(($n+1))
709
710         test_tmpdir="$tests_tmpdir/$n"
711         mkdir "$test_tmpdir"
712 }
713
714 test_teardown()
715 {
716         true
717 }
718
719 test_failed=0
720 do_test()
721 {
722         local tstr op
723
724         tstr="$@"
725         op=-eq
726
727         if [ "x$1" = "x!" ]
728         then
729                 op=-ne
730                 shift
731         fi
732
733         test_setup
734         ( $@ )
735         local rc=$?
736         test_teardown
737
738         if [ $rc $op 0 ]
739         then
740                 echo PASS: "$tstr"
741         else
742                 echo FAIL: "$tstr"
743                 test_failed=1
744                 false
745         fi
746 }
747
748 do_tests()
749 {
750         local tests_tmpdir n
751
752         tests_tmpdir=$(mktemp -d)
753         n=0
754
755         do_test ! is_url "/test"
756         do_test ! is_url "./test"
757         do_test ! is_url "../test"
758         do_test ! is_url "test"
759         do_test is_url "http://example.com/path"
760         do_test is_url "git+ssh://example.com/path"
761         do_test test_http_download
762         do_test test_ftp_download
763         do_test ! test_abi_check
764         do_test ! test_abi_check 1
765         do_test test_abi_check 1 1
766         do_test test_abi_check 1 1 1
767         do_test test_abi_check 1 2 0
768         do_test test_abi_check 1 2 1
769         do_test ! test_abi_check 1 2 2
770         do_test test_scan
771         do_test test_scan_nogzip
772         do_test test_scan_nocpio
773         do_test test_scan_multiple
774         do_test test_scan_wrongabi
775         do_test test_empty_scan
776
777         if [ $test_failed = 0 ]
778         then
779                 echo "$n tests passed"
780         else
781                 echo "Tests failed"
782         fi
783         rm -rf "$tests_tmpdir"
784
785         [ $test_failed = 0 ]
786 }
787
788 case "$1" in
789 install)
790         shift
791         do_install $@
792         ;;
793 scan)
794         shift
795         do_scan $@
796         ;;
797 create)
798         shift
799         do_create $@
800         ;;
801 lint)
802         shift
803         do_lint $@
804         ;;
805 __wrap)
806         shift
807         do_wrap $@
808         ;;
809 __test)
810         shift
811         do_tests $@
812         ;;
813 "")
814         echo "error: Missing command" >&2
815         usage
816         exit 1
817         ;;
818 *)
819         echo "Invalid command: $1" >&2
820         usage
821         exit 1
822 esac
823
824