]> git.ozlabs.org Git - petitboot/blobdiff - utils/pb-plugin
In GRUB2 parser save_env, treat unset variable value as empty
[petitboot] / utils / pb-plugin
index 4e4165257e21b6bfa11b215c1ace2fa79317d220..e107f9648ba2e165aa31d6223e69eec5d8f8812f 100755 (executable)
@@ -2,11 +2,12 @@
 
 __dest=/
 __pb_mount_dir=/var/petitboot/mnt/dev/
-__plugin_basedir=/tmp/
-plugin_file=pb-plugin.cpio.gz
+plugin_abi=1
+plugin_ext=pb-plugin
 plugin_meta=pb-plugin.conf
 plugin_meta_dir=etc/preboot-plugins/
 plugin_meta_path=$plugin_meta_dir$plugin_meta
+plugin_wrapper_dir=/var/lib/pb-plugins/bin
 
 usage()
 {
@@ -14,9 +15,10 @@ usage()
 Usage: $0 <command>
 
 Where <command> is one of:
-  run <FILE|URL>      - run plugin from FILE/URL
-  scan                - look for available plugins on attached devices
-  create <DIR>        - create a new plugin archive from DIR
+  install <FILE|URL> - install plugin from FILE/URL
+  scan               - look for available plugins on attached devices
+  create <DIR>       - create a new plugin archive from DIR
+  lint <FILE>        - perform a pre-distribution check on FILE
 EOF
 }
 
@@ -62,41 +64,95 @@ plugin_info()
        echo "  (version $PLUGIN_VERSION)"
 }
 
-__run_init()
+parse_meta()
 {
-       local base dir
+       local file name value IFS
+
+       file=$1
+
+       IFS='='
+       while read -r name value
+       do
+               # Ensure we have a sensible variable name
+               echo "$name" | grep -q '^PLUGIN_[A-Z_]*$' || continue
+
+               # we know that $name has no quoting/expansion chars, but we
+               # may need to do some basic surrounding-quote removal for
+               # $value, without evaluating it
+               value=$(echo "$value" | sed "s/^\([\"']\)\(.*\)\1\$/\2/g")
+
+               export $name="$value"
+       done < $file
+}
+
+# How the ABI versioning works:
+#
+#  - This script has an ABI defined ($plugin_abi)
+#
+#  - Plugins have a current ABI number ($PLUGIN_ABI), and a minimum supported
+#    ABI number ($PLUGIN_ABI_MIN).
+#
+#  - A plugin is OK to run if:
+#    - the plugin's ABI matches the script ABI, or
+#    - the plugin's minimum ABI is lower than or equal to the script ABI
+plugin_abi_check()
+{
+       [ -n "$PLUGIN_ABI" ] &&
+               ( [ $PLUGIN_ABI -eq $plugin_abi ] ||
+                 [ $PLUGIN_ABI_MIN -le $plugin_abi ] )
+}
+
+do_wrap()
+{
+       local base binary dir
 
        base=$1
+       binary=$2
+       shift 2
 
-       for dir in etc dev sys proc
+       for dir in etc dev sys proc var
        do
-               mkdir -p $base/$dir
+               [ -e "$base/$dir" ] || mkdir -p "$base/$dir"
        done
 
        cp /etc/resolv.conf $base/etc
        mount -o bind /dev $base/dev
        mount -o bind /sys $base/sys
        mount -o bind /proc $base/proc
+       mount -o bind /var $base/var
+
+       chroot "$base" "$binary" "$@"
+
+       umount $base/dev
+       umount $base/sys
+       umount $base/proc
+       umount $base/var
 }
 
-__run_cleanup()
+__create_wrapper()
 {
-       local base
+       local base binary wrapper
 
        base=$1
+       binary=$2
+       wrapper=$plugin_wrapper_dir/$(basename $binary)
+
+       mkdir -p $plugin_wrapper_dir
 
-       [ -e $base/dev/null ] && umount $base/dev
-       [ -e $base/sys/kernel ] && umount $base/sys
-       [ -e $base/proc/stat ] && umount $base/proc
-       rm -rf $base
+       cat <<EOF > $wrapper
+#!/bin/sh
+
+exec $(realpath $0) __wrap '$base' '$binary' "\$@"
+EOF
+
+       chmod a+x $wrapper
 }
 
-do_run()
+do_install()
 {
-       local url executable
+       local url name file __dest
 
        url=$1
-       executable=$2
 
        if [ -z "$url" ]
        then
@@ -129,7 +185,8 @@ do_run()
        echo
        sha256sum "$file" | cut -f1 -d' '
        echo
-       echo "Do you want to run this plugin? (y/N)"
+
+       echo "Do you want to install this plugin? (y/N)"
        read resp
 
        case $resp in
@@ -151,39 +208,39 @@ do_run()
                exit 1
        fi
 
-       . $__dest/$plugin_meta_path
-
-       (
-               executable=${PLUGIN_EXECUTABLE:-$executable}
-
-               __run_init $__dest
+       parse_meta $__dest/$plugin_meta_path
 
-               printf "Entering plugin\n"
-               plugin_info
-
-               chroot $__dest $executable
+       if ! plugin_abi_check
+       then
+               echo "Plugin at $url is incompatible with this firmware," \
+                       "exiting."
+               rm -rf $__dest
+               exit 1
+       fi
 
-               printf "\nExiting plugin & cleaning up\n"
-               __run_cleanup $__dest
-       )
+       for binary in ${PLUGIN_EXECUTABLES}
+       do
+               __create_wrapper "$__dest" "$binary"
+       done
 
+       echo "Plugin installed"
+       plugin_info
 }
 
-do_scan()
+do_scan_mount()
 {
-       local found dev plugin_path __meta_tmp
-       found=0
-       for mnt in $__pb_mount_dir/*
-       do
-               dev=$(basename $mnt)
-               plugin_path="$mnt/$plugin_file"
+       local mnt dev plugin_path __meta_tmp
+       mnt=$1
+       dev=$(basename $mnt)
 
+       for plugin_path in $mnt/*.$plugin_ext
+       do
                [ -e "$plugin_path" ] || continue
 
                # extract plugin metadata to a temporary directory
                __meta_tmp=$(mktemp -d)
                [ -d $__meta_tmp ] || continue
-               gunzip -c "$plugin_path" |
+               gunzip -c "$plugin_path" 2>/dev/null |
                        (cd $__meta_tmp &&
                                cpio -i -d $plugin_meta_path 2>/dev/null)
                if ! [ $? = 0 -a -e "$plugin_path" ]
@@ -193,17 +250,32 @@ do_scan()
                fi
 
                (
-                       . $__meta_tmp/$plugin_meta_path
+                       parse_meta $__meta_tmp/$plugin_meta_path
+
+                       plugin_abi_check || exit 1
 
                        printf "Plugin found on %s:\n" $dev
                        plugin_info
                        printf "\n"
                        printf "To run this plugin:\n"
-                       printf "  $0 run $plugin_path\n"
+                       printf "  $0 install $plugin_path\n"
                        printf "\n"
                )
+               if [ $? = 0 ]
+               then
+                       found=1
+               fi
                rm -rf $__meta_tmp
-               found=1
+       done
+}
+
+do_scan()
+{
+       local found mnt
+       found=0
+       for mnt in $__pb_mount_dir/*
+       do
+               do_scan_mount $mnt
        done
 
        if [ "$found" = 0 ]
@@ -259,21 +331,28 @@ EOF
 
 cat <<EOF
 
-Enter the full path (within the plugin root) to the plugin executable file.
-This will be the default action when the plugin is run. (eg /usr/bin/my-util)
+Enter the full path (within the plugin root) to the plugin executable file(s).
+These will be exposed as wrapper scripts, to be run from the standard petitboot
+shell environment (eg, /usr/bin/my-raid-config).
+
+If multiple executables are provided, separate with a space.
 EOF
-       read executable
+       read executables
 
        date=$(date +%Y-%m-%d)
 
        mkdir -p $(dirname $file)
 
        cat <<EOF > $file
+PLUGIN_ABI='$plugin_abi'
+PLUGIN_ABI_MIN='1'
 PLUGIN_VENDOR='$vendorname'
+PLUGIN_VENDOR_ID='$vendorshortname'
 PLUGIN_NAME='$pluginname'
+PLUGIN_ID='$pluginshortname'
 PLUGIN_VERSION='$version'
 PLUGIN_DATE='$date'
-PLUGIN_EXECUTABLE='$executable'
+PLUGIN_EXECUTABLES='$executables'
 EOF
 
 }
@@ -313,38 +392,19 @@ do_create()
        fi
 
        # Sanity check metadata file
-       (
-               . $meta_file
-               if [ ! -n "$PLUGIN_VENDOR" ]
-               then
-                       echo "error: no PLUGIN_VENDOR defined in metadata" &>2
-                       exit 1
-               fi
-               if [ ! -n "$PLUGIN_NAME" ]
-               then
-                       echo "error: no PLUGIN_NAME defined in metadata" &>2
-                       exit 1
-               fi
-               if [ ! -n "$PLUGIN_VERSION" ]
-               then
-                       echo "error: no PLUGIN_VERSION defined in metadata" &>2
-                       exit 1
-               fi
-               if [ ! -n "$PLUGIN_DATE" ]
-               then
-                       echo "error: no PLUGIN_DATE defined in metadata" &>2
-                       exit 1
-               fi
-               if [ ! -n "$PLUGIN_EXECUTABLE" ]
-               then
-                       echo "error: no PLUGIN_EXECUTABLE defined in metadata" \
-                               &>2
-                       exit 1
-               fi
+       parse_meta $meta_file
 
-       ) || exit 1
+       errors=0
+       warnings=0
+
+       lint_metadata
+
+       if [ $errors -ne 0 ]
+       then
+               exit 1
+       fi
 
-       outfile=$plugin_file
+       outfile=${PLUGIN_ID}-${PLUGIN_VERSION}.${plugin_ext}
 
        (
                cd $src
@@ -357,24 +417,145 @@ do_create()
        echo
 
        echo "User-visible metadata:"
-
-       (
-               . $meta_file
-               plugin_info | sed -e 's/^/  /'
-       )
+       plugin_info | sed -e 's/^/  /'
 
        echo
 
-
 cat <<EOF
 Plugin created in:
   $outfile
 
 Ship this file in the top-level-directory of a USB device or CD to have it
-automatically discoverable by 'pb-plugin scan'.
+automatically discoverable by 'pb-plugin scan'. This file can be re-named,
+but must retain the .$plugin_ext extension to be discoverable.
 EOF
 }
 
+lint_fatal()
+{
+       echo "fatal:" "$@"
+       [ -d "$__dest" ] && rm -rf "$__dest"
+       exit 1
+}
+
+lint_err()
+{
+       echo "error:" "$@"
+       errors=$(($errors+1))
+}
+
+lint_warn()
+{
+       echo "warning:" "$@"
+       warnings=$(($warnings+1))
+}
+
+lint_metadata()
+{
+       [ -n "$PLUGIN_ABI" ] ||
+               lint_err "no PLUGIN_ABI defined in metadata"
+
+       printf '%s' "$PLUGIN_ABI" | grep -q '[^0-9]' &&
+               lint_err "PLUGIN_ABI has non-numeric characters"
+
+       [ -n "$PLUGIN_ABI_MIN" ] ||
+               lint_err "no PLUGIN_ABI_MIN defined in metadata"
+
+       printf '%s' "$PLUGIN_ABI_MIN" | grep -q '[^0-9]' &&
+               lint_err "PLUGIN_ABI_MIN has non-numeric characters"
+
+       [ "$PLUGIN_ABI" = "$plugin_abi" ] ||
+               lint_warn "PLUGIN_ABI (=$PLUGIN_ABI) is not $plugin_abi"
+
+       [ -n "$PLUGIN_VENDOR" ] ||
+               lint_err "no PLUGIN_VENDOR defined in metadata"
+
+       [ -n "$PLUGIN_VENDOR_ID" ] ||
+               lint_err "no PLUGIN_VENDOR_ID defined in metadata"
+
+       printf '%s' "$PLUGIN_VENDOR_ID" | grep -q '[^a-z0-9-]' &&
+               lint_err "PLUGIN_VENDOR_ID should only contain lowercase" \
+                       "alphanumerics and hyphens"
+
+       [ -n "$PLUGIN_NAME" ] ||
+               lint_err "no PLUGIN_NAME defined in metadata"
+
+       [ -n "$PLUGIN_ID" ] ||
+               lint_err "no PLUGIN_ID defined in metadata"
+
+       printf '%s' "$PLUGIN_ID" | grep -q '[^a-z0-9-]' &&
+               lint_err "PLUGIN_ID should only contain lowercase" \
+                       "alphanumerics and hyphens"
+
+       [ "$PLUGIN_VERSION" ] ||
+               lint_err "no PLUGIN_VERSION defined in metadata"
+
+       [ -n "$PLUGIN_DATE" ] ||
+               lint_err "no PLUGIN_DATE defined in metadata"
+
+       [ -n "$PLUGIN_EXECUTABLES" ] ||
+               lint_err "no PLUGIN_EXECUTABLES defined in metadata"
+}
+
+do_lint()
+{
+       local plugin_file errors warnings __dest executable dir
+
+       plugin_file=$1
+       errors=0
+       warnings=0
+       __dest=
+
+       [ "${plugin_file##*.}" = $plugin_ext ] ||
+               lint_err "Plugin file does not end with $plugin_ext"
+
+       gunzip -c "$plugin_file" > /dev/null 2>&1 ||
+               lint_fatal "Plugin can't be gunzipped"
+
+       gunzip -c "$plugin_file" 2>/dev/null | cpio -t >/dev/null 2>&1 ||
+               lint_fatal "Plugin can't be cpioed"
+
+       __dest=$(mktemp -d)
+       gunzip -c "$plugin_file" | ( cd $__dest && cpio -i -d 2>/dev/null)
+
+       [ -e "$__dest/$plugin_meta_path" ] ||
+               lint_fatal "No metadata file present (expecting" \
+                       "$plugin_meta_path)"
+
+       parse_meta "$__dest/$plugin_meta_path"
+       lint_metadata
+
+       for executable in ${PLUGIN_EXECUTABLES}
+       do
+               exec_path="$__dest/$executable"
+               [ -e "$exec_path" ] || {
+                       lint_err "PLUGIN_EXECUTABLES item $executable" \
+                               "doesn't exist"
+                       continue
+               }
+
+               [ -x "$exec_path" ] ||
+                       lint_err "PLUGIN_EXECUTABLES item $executable" \
+                               "isn't executable"
+       done
+
+       for dir in dev sys proc var
+       do
+               [ -e "$__dest/$dir" ] || continue
+
+               [ -d "$__dest/$dir" ] ||
+                       lint_err "/$dir exists, but isn't a directory"
+
+               [ "$(find $__dest/$dir -mindepth 1)" ] &&
+                       lint_warn "/$dir contains files/directories," \
+                               "these will be lost during chroot setup"
+       done
+
+       printf '%s: %d errors, %d warnings\n' $plugin_file $errors $warnings
+       rm -rf $__dest
+       [ $errors = 0 ]
+}
+
 test_http_download()
 {
        local tmp ref
@@ -411,23 +592,126 @@ test_ftp_download()
        cmp -s "$ref" "$tmp"
 }
 
+test_abi_check()
+{
+       (
+               plugin_abi=$1
+               PLUGIN_ABI=$2
+               PLUGIN_ABI_MIN=$3
+               plugin_abi_check
+       )
+}
+
 test_scan()
 {
        __pb_mount_dir="$test_tmpdir/mnt"
        mnt_dir="$__pb_mount_dir/sda"
        mkdir -p $mnt_dir/$plugin_meta_dir
        (
+               echo "PLUGIN_ABI=$plugin_abi"
                echo "PLUGIN_NAME=test"
                echo "PLUGIN_VERSION=1"
-               echo "PLUGIN_EXECUTABLE=/bin/sh"
+               echo "PLUGIN_EXECUTABLES=/bin/sh"
        ) > $mnt_dir/$plugin_meta_path
        (
                cd $mnt_dir;
                find -mindepth 1 | cpio -o -Hnewc 2>/dev/null
-       ) | gzip -c > $mnt_dir/$plugin_file
+       ) | gzip -c > $mnt_dir/test.$plugin_ext
 
-       do_scan | grep -q 'test 1'
-       rc=$?
+       do_scan | grep -q 'test'
+}
+
+test_scan_nogzip()
+{
+       __pb_mount_dir="$test_tmpdir/mnt"
+       mnt_dir="$__pb_mount_dir/sda"
+       stderr_file="$test_tmpdir/stderr"
+
+       mkdir -p $mnt_dir
+       echo "invalid" > $mnt_dir/nogzip.$plugin_ext
+
+       do_scan 2>$stderr_file | grep -q 'No plugins'
+
+       [ $? = 0 ] || return 1
+
+       if [ -s "$stderr_file" ]
+       then
+               echo "Scan with invalid (non-gzip) file produced error output" \
+                       >&2
+               cat "$stderr_file"
+               return 1
+       fi
+       true
+}
+
+test_scan_nocpio()
+{
+       __pb_mount_dir="$test_tmpdir/mnt"
+       mnt_dir="$__pb_mount_dir/sda"
+       stderr_file="$test_tmpdir/stderr"
+
+       mkdir -p $mnt_dir
+       echo "invalid" | gzip -c > $mnt_dir/nogzip.$plugin_ext
+
+       do_scan 2>$stderr_file | grep -q 'No plugins'
+
+       [ $? = 0 ] || return 1
+
+       if [ -s "$stderr_file" ]
+       then
+               echo "Scan with invalid (non-cpio) file produced error output" \
+                       >&2
+               cat "$stderr_file"
+               return 1
+       fi
+       true
+}
+
+test_scan_multiple()
+{
+       __pb_mount_dir="$test_tmpdir/mnt"
+       mnt_dir="$__pb_mount_dir/sda"
+       outfile=$test_tmpdir/scan.out
+
+       for i in 1 2
+       do
+               mkdir -p $mnt_dir/$plugin_meta_dir
+               (
+                       echo "PLUGIN_ABI=$plugin_abi"
+                       echo "PLUGIN_NAME=test-$i"
+                       echo "PLUGIN_VERSION=1"
+                       echo "PLUGIN_EXECUTABLES=/bin/sh"
+               ) > $mnt_dir/$plugin_meta_path
+               (
+                       cd $mnt_dir;
+                       find -mindepth 1 | cpio -o -Hnewc 2>/dev/null
+               ) | gzip -c > $mnt_dir/test-${i}.$plugin_ext
+               rm -rf $mnt_dir/$plugin_meta_dir
+       done
+
+       do_scan >$outfile
+
+       grep -q 'test-1' $outfile && grep -q 'test-2' $outfile
+}
+
+test_scan_wrongabi()
+{
+       __pb_mount_dir="$test_tmpdir/mnt"
+       mnt_dir="$__pb_mount_dir/sda"
+       mkdir -p $mnt_dir/$plugin_meta_dir
+       (
+               echo "PLUGIN_ABI=$(($plugin_abi + 1))"
+               echo "PLUGIN_ABI_MIN=$(($plugin_abi + 1))"
+               echo "PLUGIN_NAME=test"
+               echo "PLUGIN_VERSION=1"
+               echo "PLUGIN_EXECUTABLES=/bin/sh"
+       ) > $mnt_dir/$plugin_meta_path
+       (
+               cd $mnt_dir;
+               find -mindepth 1 | cpio -o -Hnewc 2>/dev/null
+       ) | gzip -c > $mnt_dir/test.$plugin_ext
+
+       do_scan | grep -q 'No plugins'
 }
 
 test_empty_scan()
@@ -494,7 +778,18 @@ do_tests()
        do_test is_url "git+ssh://example.com/path"
        do_test test_http_download
        do_test test_ftp_download
+       do_test ! test_abi_check
+       do_test ! test_abi_check 1
+       do_test test_abi_check 1 1
+       do_test test_abi_check 1 1 1
+       do_test test_abi_check 1 2 0
+       do_test test_abi_check 1 2 1
+       do_test ! test_abi_check 1 2 2
        do_test test_scan
+       do_test test_scan_nogzip
+       do_test test_scan_nocpio
+       do_test test_scan_multiple
+       do_test test_scan_wrongabi
        do_test test_empty_scan
 
        if [ $test_failed = 0 ]
@@ -502,15 +797,16 @@ do_tests()
                echo "$n tests passed"
        else
                echo "Tests failed"
-               false
        fi
        rm -rf "$tests_tmpdir"
+
+       [ $test_failed = 0 ]
 }
 
 case "$1" in
-run)
+install)
        shift
-       do_run $@
+       do_install $@
        ;;
 scan)
        shift
@@ -520,6 +816,14 @@ create)
        shift
        do_create $@
        ;;
+lint)
+       shift
+       do_lint $@
+       ;;
+__wrap)
+       shift
+       do_wrap $@
+       ;;
 __test)
        shift
        do_tests $@
@@ -530,7 +834,7 @@ __test)
        exit 1
        ;;
 *)
-       echo "Invalid command: $s" >&2
+       echo "Invalid command: $1" >&2
        usage
        exit 1
 esac