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