pb-plugin: Update to chroot-style plugins
[petitboot] / utils / pb-plugin
1 #!/bin/sh
2
3 __dest=/
4 __pb_mount_dir=/var/petitboot/mnt/dev/
5 __plugin_basedir=/tmp/
6 plugin_file=pb-plugin.cpio.gz
7 plugin_meta=pb-plugin.conf
8 plugin_meta_dir=etc/preboot-plugins/
9 plugin_meta_path=$plugin_meta_dir$plugin_meta
10
11 usage()
12 {
13         cat <<EOF
14 Usage: $0 <command>
15
16 Where <command> is one of:
17   run <FILE|URL>      - run 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 __run_init()
66 {
67         local base dir
68
69         base=$1
70
71         for dir in etc dev sys proc
72         do
73                 mkdir -p $base/$dir
74         done
75
76         cp /etc/resolv.conf $base/etc
77         mount -o bind /dev $base/dev
78         mount -o bind /sys $base/sys
79         mount -o bind /proc $base/proc
80 }
81
82 __run_cleanup()
83 {
84         local base
85
86         base=$1
87
88         [ -e $base/dev/null ] && umount $base/dev
89         [ -e $base/sys/kernel ] && umount $base/sys
90         [ -e $base/proc/stat ] && umount $base/proc
91         rm -rf $base
92 }
93
94 do_run()
95 {
96         local url executable
97
98         url=$1
99         executable=$2
100
101         if [ -z "$url" ]
102         then
103                 echo "error: install requires a file/URL argument." >&2
104                 exit 1
105         fi
106
107         name=${url##*/}
108
109         if is_url "$url"
110         then
111                 file=$(mktemp)
112                 trap "rm '$file'" EXIT
113                 download "$url" "$file"
114                 if [ $? -ne 0 ]
115                 then
116                         echo "error: failed to download $url" >&2
117                         exit 1
118                 fi
119         else
120                 file=$url
121                 if [ ! -r "$file" ]
122                 then
123                         echo "error: $file doesn't exist or is not readable" >&2
124                         exit 1
125                 fi
126         fi
127
128         echo "File '$name' has the following sha256 checksum:"
129         echo
130         sha256sum "$file" | cut -f1 -d' '
131         echo
132         echo "Do you want to run this plugin? (y/N)"
133         read resp
134
135         case $resp in
136         [yY]|[yY][eE][sS])
137                 ;;
138         *)
139                 echo "Cancelled"
140                 exit 0
141                 ;;
142         esac
143
144         __dest=$(mktemp -d)
145         gunzip -c "$file" | ( cd $__dest && cpio -i -d)
146
147         if [ $? -ne 0 ]
148         then
149                 echo "error: Failed to extract archive $url, exiting"
150                 rm -rf $__dest
151                 exit 1
152         fi
153
154         . $__dest/$plugin_meta_path
155
156         (
157                 executable=${PLUGIN_EXECUTABLE:-$executable}
158
159                 __run_init $__dest
160
161                 printf "Entering plugin\n"
162                 plugin_info
163
164                 chroot $__dest $executable
165
166                 printf "\nExiting plugin & cleaning up\n"
167                 __run_cleanup $__dest
168         )
169
170 }
171
172 do_scan()
173 {
174         local found dev plugin_path __meta_tmp
175         found=0
176         for mnt in $__pb_mount_dir/*
177         do
178                 dev=$(basename $mnt)
179                 plugin_path="$mnt/$plugin_file"
180
181                 [ -e "$plugin_path" ] || continue
182
183                 # extract plugin metadata to a temporary directory
184                 __meta_tmp=$(mktemp -d)
185                 [ -d $__meta_tmp ] || continue
186                 gunzip -c "$plugin_path" |
187                         (cd $__meta_tmp &&
188                                 cpio -i -d $plugin_meta_path 2>/dev/null)
189                 if ! [ $? = 0 -a -e "$plugin_path" ]
190                 then
191                         rm -rf $__meta_tmp
192                         continue
193                 fi
194
195                 (
196                         . $__meta_tmp/$plugin_meta_path
197
198                         printf "Plugin found on %s:\n" $dev
199                         plugin_info
200                         printf "\n"
201                         printf "To run this plugin:\n"
202                         printf "  $0 run $plugin_path\n"
203                         printf "\n"
204                 )
205                 rm -rf $__meta_tmp
206                 found=1
207         done
208
209         if [ "$found" = 0 ]
210         then
211                 echo "No plugins found"
212         fi
213 }
214
215 guided_meta()
216 {
217         local vendorname vendorshortname
218         local pluginname pluginnhortname
219         local version date executable
220         local file
221
222         file=$1
223
224 cat <<EOF
225
226 Enter the vendor company / author name. This can contain spaces.
227 (eg. 'Example Corporation')
228 EOF
229         read vendorname
230 cat <<EOF
231
232 Enter the vendor shortname. This should be a single-word abbreviation, in all
233 lower-case. This is only used in internal filenames.
234
235 Typically, a stock-ticker name is used for this (eg 'exco')
236 EOF
237         read vendorshortname
238
239 cat <<EOF
240
241 Enter the descriptive plugin name. This can contain spaces, but should only be
242 a few words in length (eg 'RAID configuration utility')
243 EOF
244         read pluginname
245
246 cat <<EOF
247
248 Enter the plugin shortname. This should not contain spaces, but hyphens are
249 fine (eg 'raid-config'). This is only used in internal filnames.
250 EOF
251         read pluginshortname
252
253
254 cat <<EOF
255
256 Enter the plugin version. This should not contain spaces (eg 1.2):
257 EOF
258         read version
259
260 cat <<EOF
261
262 Enter the full path (within the plugin root) to the plugin executable file.
263 This will be the default action when the plugin is run. (eg /usr/bin/my-util)
264 EOF
265         read executable
266
267         date=$(date +%Y-%m-%d)
268
269         mkdir -p $(dirname $file)
270
271         cat <<EOF > $file
272 PLUGIN_VENDOR='$vendorname'
273 PLUGIN_NAME='$pluginname'
274 PLUGIN_VERSION='$version'
275 PLUGIN_DATE='$date'
276 PLUGIN_EXECUTABLE='$executable'
277 EOF
278
279 }
280
281 do_create()
282 {
283         local src meta_dir_abs meta_file
284         src=$1
285
286         if [ -z "$src" ]
287         then
288                 echo "error: missing source directory" >&2
289                 usage
290                 exit 1
291         fi
292
293         if [ ! -d "$src" ]
294         then
295                 echo "error: source directory missing" >&2
296                 exit 1
297         fi
298
299         meta_file=$src/$plugin_meta_path
300
301         if [ ! -e $meta_file ]
302         then
303                 echo "No plugin metadata file found. " \
304                         "Would you like to create one? (Y/n)"
305                 read resp
306                 case "$resp" in
307                 [nN]|[nN][oO])
308                         echo "Cancelled, exiting"
309                         exit 1
310                         ;;
311                 esac
312                 guided_meta $meta_file || exit
313         fi
314
315         # Sanity check metadata file
316         (
317                 . $meta_file
318                 if [ ! -n "$PLUGIN_VENDOR" ]
319                 then
320                         echo "error: no PLUGIN_VENDOR defined in metadata" &>2
321                         exit 1
322                 fi
323                 if [ ! -n "$PLUGIN_NAME" ]
324                 then
325                         echo "error: no PLUGIN_NAME defined in metadata" &>2
326                         exit 1
327                 fi
328                 if [ ! -n "$PLUGIN_VERSION" ]
329                 then
330                         echo "error: no PLUGIN_VERSION defined in metadata" &>2
331                         exit 1
332                 fi
333                 if [ ! -n "$PLUGIN_DATE" ]
334                 then
335                         echo "error: no PLUGIN_DATE defined in metadata" &>2
336                         exit 1
337                 fi
338                 if [ ! -n "$PLUGIN_EXECUTABLE" ]
339                 then
340                         echo "error: no PLUGIN_EXECUTABLE defined in metadata" \
341                                 &>2
342                         exit 1
343                 fi
344
345         ) || exit 1
346
347         outfile=$plugin_file
348
349         (
350                 cd $src
351                 find -mindepth 1 | cpio -o -Hnewc -v
352         ) | gzip -c > $outfile
353
354         echo
355         echo "Plugin metadata:"
356         sed -e 's/^/  /' $meta_file
357         echo
358
359         echo "User-visible metadata:"
360
361         (
362                 . $meta_file
363                 plugin_info | sed -e 's/^/  /'
364         )
365
366         echo
367
368
369 cat <<EOF
370 Plugin created in:
371   $outfile
372
373 Ship this file in the top-level-directory of a USB device or CD to have it
374 automatically discoverable by 'pb-plugin scan'.
375 EOF
376 }
377
378 test_http_download()
379 {
380         local tmp ref
381
382         tmp=$(mktemp -p $test_tmpdir)
383         ref=$(mktemp -p $test_tmpdir)
384
385         echo $RANDOM > $ref
386
387         wget()
388         {
389                 cat $ref
390         }
391
392         download http://example.com/test $tmp
393         cmp -s "$ref" "$tmp"
394 }
395
396 test_ftp_download()
397 {
398         local tmp ref
399
400         tmp=$(mktemp -p $test_tmpdir)
401         ref=$(mktemp -p $test_tmpdir)
402
403         echo $RANDOM > $ref
404
405         ncftpget()
406         {
407                 cat $ref
408         }
409
410         download ftp://example.com/test $tmp
411         cmp -s "$ref" "$tmp"
412 }
413
414 test_scan()
415 {
416         __pb_mount_dir="$test_tmpdir/mnt"
417         mnt_dir="$__pb_mount_dir/sda"
418         mkdir -p $mnt_dir/$plugin_meta_dir
419         (
420                 echo "PLUGIN_NAME=test"
421                 echo "PLUGIN_VERSION=1"
422                 echo "PLUGIN_EXECUTABLE=/bin/sh"
423         ) > $mnt_dir/$plugin_meta_path
424         (
425                 cd $mnt_dir;
426                 find -mindepth 1 | cpio -o -Hnewc 2>/dev/null
427         ) | gzip -c > $mnt_dir/$plugin_file
428
429         do_scan | grep -q 'test 1'
430         rc=$?
431 }
432
433 test_empty_scan()
434 {
435         __pb_mount_dir="$test_tmpdir/mnt"
436         mkdir -p $__pb_mount_dir
437         do_scan | grep -q "No plugins"
438 }
439
440 test_setup()
441 {
442         n=$(($n+1))
443
444         test_tmpdir="$tests_tmpdir/$n"
445         mkdir "$test_tmpdir"
446 }
447
448 test_teardown()
449 {
450         true
451 }
452
453 test_failed=0
454 do_test()
455 {
456         local tstr op
457
458         tstr="$@"
459         op=-eq
460
461         if [ "x$1" = "x!" ]
462         then
463                 op=-ne
464                 shift
465         fi
466
467         test_setup
468         ( $@ )
469         local rc=$?
470         test_teardown
471
472         if [ $rc $op 0 ]
473         then
474                 echo PASS: "$tstr"
475         else
476                 echo FAIL: "$tstr"
477                 test_failed=1
478                 false
479         fi
480 }
481
482 do_tests()
483 {
484         local tests_tmpdir n
485
486         tests_tmpdir=$(mktemp -d)
487         n=0
488
489         do_test ! is_url "/test"
490         do_test ! is_url "./test"
491         do_test ! is_url "../test"
492         do_test ! is_url "test"
493         do_test is_url "http://example.com/path"
494         do_test is_url "git+ssh://example.com/path"
495         do_test test_http_download
496         do_test test_ftp_download
497         do_test test_scan
498         do_test test_empty_scan
499
500         if [ $test_failed = 0 ]
501         then
502                 echo "$n tests passed"
503         else
504                 echo "Tests failed"
505                 false
506         fi
507         rm -rf "$tests_tmpdir"
508 }
509
510 case "$1" in
511 run)
512         shift
513         do_run $@
514         ;;
515 scan)
516         shift
517         do_scan $@
518         ;;
519 create)
520         shift
521         do_create $@
522         ;;
523 __test)
524         shift
525         do_tests $@
526         ;;
527 "")
528         echo "error: Missing command" >&2
529         usage
530         exit 1
531         ;;
532 *)
533         echo "Invalid command: $s" >&2
534         usage
535         exit 1
536 esac
537
538