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