__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()
{
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 <DIR> - 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
}
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
+
+ cat <<EOF > $wrapper
+#!/bin/sh
+
+exec $(realpath $0) __wrap '$base' '$binary' "\$@"
+EOF
- [ -e $base/dev/null ] && umount $base/dev
- [ -e $base/sys/kernel ] && umount $base/sys
- [ -e $base/proc/stat ] && umount $base/proc
- rm -rf $base
+ chmod a+x $wrapper
}
-do_run()
+do_install()
{
- local url executable
+ local url name file __dest auto
+
+ if [ "$1" == "auto" ]; then
+ auto=y
+ shift;
+ fi
url=$1
- executable=$2
if [ -z "$url" ]
then
echo
sha256sum "$file" | cut -f1 -d' '
echo
- echo "Do you want to run this plugin? (y/N)"
- read resp
- case $resp in
- [yY]|[yY][eE][sS])
- ;;
- *)
- echo "Cancelled"
- exit 0
- ;;
- esac
+ if [ -z "$auto" ]
+ then
+ echo "Do you want to install this plugin? (y/N)"
+ read resp
+
+ case $resp in
+ [yY]|[yY][eE][sS])
+ ;;
+ *)
+ echo "Cancelled"
+ exit 0
+ ;;
+ esac
+ fi
__dest=$(mktemp -d)
gunzip -c "$file" | ( cd $__dest && cpio -i -d)
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
+ if ! plugin_abi_check
+ then
+ echo "Plugin at $url is incompatible with this firmware," \
+ "exiting."
+ rm -rf $__dest
+ exit 1
+ fi
- chroot $__dest $executable
+ for binary in ${PLUGIN_EXECUTABLES}
+ do
+ __create_wrapper "$__dest" "$binary"
+ done
- printf "\nExiting plugin & cleaning up\n"
- __run_cleanup $__dest
- )
+ pb-event plugin@local \
+ name=$PLUGIN_NAME id=$PLUGIN_ID version=$PLUGIN_VERSION \
+ vendor=$PLUGIN_VENDOR vendor_id=$PLUGIN_VENDOR_ID \
+ date=$PLUGIN_DATE executables="$PLUGIN_EXECUTABLES" \
+ source_file=$url installed="yes"
+ 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" ]
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"
+
+ pb-event plugin@$dev name=$PLUGIN_NAME \
+ path=$plugin_path installed="no"
)
+ if [ $? = 0 ]
+ then
+ found=1
+ fi
rm -rf $__meta_tmp
- found=1
+ done
+}
+
+do_scan()
+{
+ local found mnt dev locations
+ found=0
+ dev=$1
+
+ if [ -z $dev ]; then
+ locations=$__pb_mount_dir/*
+ else
+ echo "Scanning device $dev"
+ locations=$dev
+ fi
+
+ for mnt in $locations
+ do
+ do_scan_mount $mnt
done
if [ "$found" = 0 ]
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
}
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
+
+ errors=0
+ warnings=0
+
+ lint_metadata
- ) || exit 1
+ if [ $errors -ne 0 ]
+ then
+ exit 1
+ fi
- outfile=$plugin_file
+ outfile=${PLUGIN_ID}-${PLUGIN_VERSION}.${plugin_ext}
(
cd $src
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
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_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 '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_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 'No plugins'
}
test_empty_scan()
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 ]
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
shift
do_create $@
;;
+lint)
+ shift
+ do_lint $@
+ ;;
+__wrap)
+ shift
+ do_wrap $@
+ ;;
__test)
shift
do_tests $@
exit 1
;;
*)
- echo "Invalid command: $s" >&2
+ echo "Invalid command: $1" >&2
usage
exit 1
esac