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