diff --git a/builds/any/installer/grub/builds/Makefile b/builds/any/installer/grub/builds/Makefile index a89a27cd..933f727f 100644 --- a/builds/any/installer/grub/builds/Makefile +++ b/builds/any/installer/grub/builds/Makefile @@ -10,8 +10,20 @@ endif include $(ONL)/make/versions/version-onl.mk INSTALLER_NAME=$(FNAME_PRODUCT_VERSION)_ONL-OS_$(FNAME_BUILD_ID)_$(UARCH)_$(BOOTMODE)_INSTALLER +MKINSTALLER_OPTS = \ + --arch $(ARCH) \ + --boot-config boot-config \ + --add-dir config \ + --initrd onl-loader-initrd:$(ARCH) onl-loader-initrd-$(ARCH).cpio.gz \ + --swi onl-swi:$(ARCH) \ + --preinstall-script $(ONL)/builds/any/installer/sample-preinstall.sh \ + --postinstall-script $(ONL)/builds/any/installer/sample-postinstall.sh \ + --preinstall-plugin $(ONL)/builds/any/installer/sample-preinstall.py \ + --postinstall-plugin $(ONL)/builds/any/installer/sample-postinstall.py \ + # THIS LINE INTENTIONALLY LEFT BLANK + __installer: - $(ONL)/tools/mkinstaller.py --arch $(ARCH) --boot-config boot-config --add-dir config --initrd onl-loader-initrd:$(ARCH) onl-loader-initrd-$(ARCH).cpio.gz --swi onl-swi:$(ARCH) --out $(INSTALLER_NAME) + $(ONL)/tools/mkinstaller.py $(MKINSTALLER_OPTS) --out $(INSTALLER_NAME) md5sum "$(INSTALLER_NAME)" | awk '{ print $$1 }' > "$(INSTALLER_NAME).md5sum" diff --git a/builds/any/installer/installer.sh.in b/builds/any/installer/installer.sh.in index 8a988924..da7a24cb 100644 --- a/builds/any/installer/installer.sh.in +++ b/builds/any/installer/installer.sh.in @@ -361,16 +361,44 @@ else installer_list=$initrd_archive fi +installer_unzip() { + local zip tmp dummy + zip=$1; shift + + installer_say "Extracting from $zip: $@ ..." + + tmp=$(mktemp -d -t "unzip-XXXXXX") + if test "$SFX_PAD"; then + # ha ha, busybox cannot exclude multiple files + unzip -o $zip "$@" -x $SFX_PAD -d $tmp + elif test "$SFX_UNZIP"; then + unzip -o $zip "$@" -x $installer_script -d $tmp + else + dd if=$zip bs=$SFX_BLOCKSIZE skip=$SFX_BLOCKS \ + | unzip -o - "$@" -x $installer_script -d $tmp + fi + + rm -f $tmp/$installer_script + if test "$SFX_PAD"; then + rm -f $tmp/$SFX_PAD + fi + + set dummy $tmp/* + if test -e "$2"; then + shift + while test $# -gt 0; do + mv "$1" . + shift + done + else + installer_say "Extracting from $zip: no files extracted" + fi + + return 0 +} + installer_say "Unpacking ONL installer files..." -if test "$SFX_PAD"; then - # ha ha, busybox cannot exclude multiple files - unzip -o $installer_zip $installer_list -x $SFX_PAD -elif test "$SFX_UNZIP"; then - unzip -o $installer_zip $installer_list -x $installer_script -else - dd if=$installer_zip bs=$SFX_BLOCKSIZE skip=$SFX_BLOCKS \ - | unzip -o - $installer_list -x $installer_script -fi +installer_unzip $installer_zip $installer_list # Developer debugging if has_boot_env onl_installer_unpack_only; then installer_unpack_only=1; fi @@ -513,6 +541,13 @@ else installer_say "*** watch out for lingering mount-points" fi +installer_unzip $installer_zip preinstall.sh || : +if test -f preinstall.sh; then + installer_say "Invoking pre-install actions" + chmod +x preinstall.sh + ./preinstall.sh $rootdir +fi + chroot "${rootdir}" $installer_shell if test -f "$postinst"; then @@ -522,6 +557,12 @@ if test -f "$postinst"; then set +x fi +installer_unzip $installer_zip postinstall.sh || : +if test -f preinstall.sh; then + chmod +x postinstall.sh + ./postinstall.sh $rootdir +fi + trap - 0 1 installer_umount diff --git a/builds/any/installer/sample-postinstall.py b/builds/any/installer/sample-postinstall.py new file mode 100644 index 00000000..1f5605d2 --- /dev/null +++ b/builds/any/installer/sample-postinstall.py @@ -0,0 +1,59 @@ +"""sample-postinstall.py + +Example Python script for post-install hooks. + +Add this as a postinstall hook to your installer via +the 'mkinstaller.py' command line: + +$ mkinstaller.py ... --postinstall-plugin sample-postinstall.py ... + +At install time, this script will + +1. be extracted into a temporary working directory +2. be imported as a module, in the same process as the installer + script + +Importing the module should not trigger any side-effects. + +At the appropriate time during the install (a chrooted invocation +of the installer Python script) will + +1. scrape the top-level plugin's namespace for subclasses of + onl.install.Plugin.Plugin. + Implementors should declare classes here + (inheriting from onl.install.Plugin.Plugin) to embed the plugin + functionality. +2. instantiate an instance of each class, with the installer + object initialized as the 'installer' attribute +3. invoke the 'run' method (which must be overridden by implementors) +4. invoke the 'shutdown' method (by default, a no-op) + +The 'run' method should return zero on success. In any other case, the +installer terminates. + +The post-install plugins are invoked after the installer is complete +and after the boot loader is updated. + +An exception to this is for proxy GRUB configurations. In that case, the +post-install plugins are invoked after the install is finished, but before +the boot loader has been updated. + +At the time the post-install plugin is invoked, none of the +filesystems are mounted. If the implementor needs to manipulate the +disk, the filesystems should be re-mounted temporarily with +e.g. MountContext. The OnlMountContextReadWrite object and their +siblings won't work here because the mtab.yml file is not populated +within the loader environment. + +When using MountContxt, the system state in the installer object can help +(self.installer.blkidParts in particular). + +""" + +import onl.install.Plugin + +class Plugin(onl.install.Plugin.Plugin): + + def run(self): + self.log.info("hello from postinstall plugin") + return 0 diff --git a/builds/any/installer/sample-postinstall.sh b/builds/any/installer/sample-postinstall.sh new file mode 100644 index 00000000..36a0da25 --- /dev/null +++ b/builds/any/installer/sample-postinstall.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# +###################################################################### +# +# sample-postinstall.sh +# +# Example script for post-install hooks. +# +# Add this as a postinstall hook to your installer via +# the 'mkinstaller.py' command line: +# +# $ mkinstaller.py ... --postinstall-script sample-postinstall.sh ... +# +# At install time, this script will +# +# 1. be extracted into the working directory with the other installer +# collateral +# 2. have the execute bit set +# 3. run in-place with the installer chroot directory passed +# as the first command line parameter +# +# If the script fails (returns a non-zero exit code) then +# the install is aborted. +# +# This script is executed using the ONIE runtime (outside the chroot), +# after the actual installer (chrooted Python script) has finished. +# +# This script is run after the postinstall actions (e.g. proxy GRUB +# commands) +# +# At the time the script is run, the installer environment (chroot) +# is fully prepared, including filesystem mount-points. +# That is, the chroot mount points have not been unmounted yet. +# +###################################################################### + +rootdir=$1; shift + +echo "Hello from postinstall" +echo "Chroot is $rootdir" + +exit 0 diff --git a/builds/any/installer/sample-preinstall.py b/builds/any/installer/sample-preinstall.py new file mode 100644 index 00000000..09b2b524 --- /dev/null +++ b/builds/any/installer/sample-preinstall.py @@ -0,0 +1,49 @@ +"""sample-preinstall.py + +Example Python script for pre-install hooks. + +Add this as a preinstall hook to your installer via +the 'mkinstaller.py' command line: + +$ mkinstaller.py ... --preinstall-plugin sample-preinstall.py ... + +At install time, this script will + +1. be extracted into a temporary working directory +2. be imported as a module, in the same process as the installer + script + +Importing the module should not trigger any side-effects. + +At the appropriate time during the install (a chrooted invocation +of the installer Python script) will + +1. scrape the top-level plugin's namespace for subclasses of + onl.install.Plugin.Plugin. + Implementors should declare classes here + (inheriting from onl.install.Plugin.Plugin) to embed the plugin + functionality. +2. instantiate an instance of each class, with the installer + object initialized as the 'installer' attribute +3. invoke the 'run' method (which must be overridden by implementors) +4. invoke the 'shutdown' method (by default, a no-op) + +The 'run' method should return zero on success. In any other case, the +installer terminates. + +The 'installer' object has a handle onto the installer ZIP archive +(self.installer.zf) but otherwise the install has not been +started. That is, the install disk has not been +prepped/initialized/scanned yet. As per the ONL installer API, the +installer starts with *no* filesystems mounted, not even the ones from +a prior install. + +""" + +import onl.install.Plugin + +class Plugin(onl.install.Plugin.Plugin): + + def run(self): + self.log.info("hello from preinstall plugin") + return 0 diff --git a/builds/any/installer/sample-preinstall.sh b/builds/any/installer/sample-preinstall.sh new file mode 100644 index 00000000..13532aae --- /dev/null +++ b/builds/any/installer/sample-preinstall.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# +###################################################################### +# +# sample-preinstall.sh +# +# Example script for pre-install hooks. +# +# Add this as a preinstall hook to your installer via +# the 'mkinstaller.py' command line: +# +# $ mkinstaller.py ... --preinstall-script sample-preinstall.sh ... +# +# At install time, this script will +# +# 1. be extracted into the working directory with the other installer +# collateral +# 2. have the execute bit set +# 3. run in-place with the installer chroot directory passed +# as the first command line parameter +# +# If the script fails (returns a non-zero exit code) then +# the install is aborted. +# +# This script is executed using the ONIE runtime (outside the chroot), +# before the actual installer (chrooted Python script) +# +# At the time the script is run, the installer environment (chroot) +# has been fully prepared, including filesystem mount-points. +# +###################################################################### + +rootdir=$1; shift + +echo "Hello from preinstall" +echo "Chroot is $rootdir" + +exit 0 diff --git a/builds/any/installer/uboot/builds/Makefile b/builds/any/installer/uboot/builds/Makefile index b6073d5c..800463e4 100644 --- a/builds/any/installer/uboot/builds/Makefile +++ b/builds/any/installer/uboot/builds/Makefile @@ -10,8 +10,20 @@ endif include $(ONL)/make/versions/version-onl.mk INSTALLER_NAME=$(FNAME_PRODUCT_VERSION)_ONL-OS_$(FNAME_BUILD_ID)_$(UARCH)_$(BOOTMODE)_INSTALLER +MKINSTALLER_OPTS = \ + --arch $(ARCH) \ + --boot-config boot-config \ + --add-dir config \ + --fit onl-loader-fit:$(ARCH) onl-loader-fit.itb \ + --swi onl-swi:$(ARCH) \ + --preinstall-script $(ONL)/builds/any/installer/sample-preinstall.sh \ + --postinstall-script $(ONL)/builds/any/installer/sample-postinstall.sh \ + --preinstall-plugin $(ONL)/builds/any/installer/sample-preinstall.py \ + --postinstall-plugin $(ONL)/builds/any/installer/sample-postinstall.py \ + # THIS LINE INTENTIONALLY LEFT BLANK + __installer: - $(ONL)/tools/mkinstaller.py --arch $(ARCH) --boot-config boot-config --add-dir config --fit onl-loader-fit:$(ARCH) onl-loader-fit.itb --swi onl-swi:$(ARCH) --out $(INSTALLER_NAME) + $(ONL)/tools/mkinstaller.py $(MKINSTALLER_OPTS) --out $(INSTALLER_NAME) md5sum "$(INSTALLER_NAME)" | awk '{ print $$1 }' > "$(INSTALLER_NAME).md5sum" diff --git a/packages/base/all/vendor-config-onl/src/python/onl/install/BaseInstall.py b/packages/base/all/vendor-config-onl/src/python/onl/install/BaseInstall.py index 5c46f0b9..b9528b4d 100644 --- a/packages/base/all/vendor-config-onl/src/python/onl/install/BaseInstall.py +++ b/packages/base/all/vendor-config-onl/src/python/onl/install/BaseInstall.py @@ -3,7 +3,7 @@ Base classes for installers. """ -import os, stat +import os, sys, stat import subprocess import re import tempfile @@ -13,10 +13,12 @@ import parted import yaml import zipfile import shutil +import imp from InstallUtils import SubprocessMixin from InstallUtils import MountContext, BlkidParser, PartedParser from InstallUtils import ProcMountsParser +from Plugin import Plugin import onl.YamlUtils from onl.sysconfig import sysconfig @@ -413,6 +415,50 @@ class Base: return 0 + def preinstall(self): + return self.runPlugin("preinstall.py") + + def postinstall(self): + return self.runPlugin("postinstall.py") + + def runPluginFile(self, pyPath): + with open(pyPath) as fd: + sfx = ('.py', 'U', imp.PY_SOURCE,) + mod = imp.load_module("plugin", fd, pyPath, sfx) + for attr in dir(mod): + klass = getattr(mod, attr) + if isinstance(klass, type) and issubclass(klass, Plugin): + self.log.info("%s: running plugin %s", pyPath, attr) + plugin = klass(self) + try: + code = plugin.run() + except: + self.log.exception("plugin failed") + code = 1 + plugin.shutdown() + if code: return code + + return 0 + + def runPlugin(self, basename): + + src = os.path.join(self.im.installerConf.installer_dir, basename) + if os.path.exists(src): + return self.runPluginFile(src) + + if basename in self.zf.namelist(): + try: + src = None + with self.zf.open(basename, "r") as rfd: + wfno, src = tempfile.mkstemp(prefix="plugin-", + suffix=".py") + with os.fdopen(wfno, "w") as wfd: + shutil.copyfileobj(rfd, wfd) + return self.runPluginFile(src) + finally: + if src and os.path.exists(src): + os.unlink(src) + GRUB_TPL = """\ serial %(serial)s terminal_input serial @@ -603,6 +649,14 @@ class GrubInstaller(SubprocessMixin, Base): def installGpt(self): + # get a handle to the installer zip + p = os.path.join(self.im.installerConf.installer_dir, + self.im.installerConf.installer_zip) + self.zf = zipfile.ZipFile(p) + + code = self.preinstall() + if code: return code + code = self.findGpt() if code: return code @@ -640,11 +694,6 @@ class GrubInstaller(SubprocessMixin, Base): self.im.grubEnv.__dict__['bootPart'] = dev.device self.im.grubEnv.__dict__['bootDir'] = None - # get a handle to the installer zip - p = os.path.join(self.im.installerConf.installer_dir, - self.im.installerConf.installer_zip) - self.zf = zipfile.ZipFile(p) - code = self.installSwi() if code: return code @@ -663,6 +712,9 @@ class GrubInstaller(SubprocessMixin, Base): code = self.installGrub() if code: return code + code = self.postinstall() + if code: return code + self.log.info("ONL loader install successful.") self.log.info("GRUB installation is required next.") @@ -839,6 +891,14 @@ class UbootInstaller(SubprocessMixin, Base): self.log.error("not a block device: %s", self.device) return 1 + # get a handle to the installer zip + p = os.path.join(self.im.installerConf.installer_dir, + self.im.installerConf.installer_zip) + self.zf = zipfile.ZipFile(p) + + code = self.preinstall() + if code: return code + code = self.assertUnmounted() if code: return code @@ -884,11 +944,6 @@ class UbootInstaller(SubprocessMixin, Base): self.rawLoaderDevice = self.device + str(partIdx+1) break - # get a handle to the installer zip - p = os.path.join(self.im.installerConf.installer_dir, - self.im.installerConf.installer_zip) - self.zf = zipfile.ZipFile(p) - code = self.installSwi() if code: return code @@ -913,6 +968,9 @@ class UbootInstaller(SubprocessMixin, Base): code = self.installUbootEnv() if code: return code + code = self.postinstall() + if code: return code + return 0 def run(self): diff --git a/packages/base/all/vendor-config-onl/src/python/onl/install/Plugin.py b/packages/base/all/vendor-config-onl/src/python/onl/install/Plugin.py new file mode 100644 index 00000000..f1e97713 --- /dev/null +++ b/packages/base/all/vendor-config-onl/src/python/onl/install/Plugin.py @@ -0,0 +1,17 @@ +"""Plugin.py + +Base class for installer plugins. +""" + +class Plugin(object): + + def __init__(self, installer): + self.installer = installer + self.log = self.installer.log.getChild("plugin") + + def run(self): + self.log.warn("not implemented") + return 0 + + def shutdown(self): + pass diff --git a/tools/mkinstaller.py b/tools/mkinstaller.py index e3945bd2..6348a252 100755 --- a/tools/mkinstaller.py +++ b/tools/mkinstaller.py @@ -106,6 +106,18 @@ class InstallerShar(object): self.files.append(filename) self.files = list(set(self.files)) + def add_file_as(self, source, basename): + if not os.path.exists(source): + self.abort("File %s does not exist." % source) + + tmpdir = os.path.join(self.work_dir, "tmp") + if not os.path.exists(tmpdir): + os.mkdir(tmpdir) + + dst = os.path.join(tmpdir, basename) + shutil.copy(source, dst) + self.add_file(dst) + def add_dir(self, dir_): if not os.path.isdir(dir_): self.abort("Directory %s does not exist." % dir_) @@ -174,6 +186,16 @@ if __name__ == '__main__': ap.add_argument("--verbose", '-v', help="Verbose output.", action='store_true') ap.add_argument("--out", help="Destination Filename") + ap.add_argument("--preinstall-script", + help="Specify a preinstall script (runs before installer)") + ap.add_argument("--postinstall-script", + help="Specify a preinstall script (runs after installer)") + + ap.add_argument("--preinstall-plugin", + help="Specify a preinstall plugin (runs from within the installer chroot)") + ap.add_argument("--postinstall-plugin", + help="Specify a postinstall plugin (runs from within the installer chroot)") + ops = ap.parse_args() installer = InstallerShar(ops.arch, ops.work_dir) @@ -209,6 +231,20 @@ if __name__ == '__main__': if ops.swi: installer.add_swi(ops.swi) + hookdir = os.path.join(installer.work_dir, "tmp") + if not os.path.exists(hookdir): + os.makedirs(hookdir) + + if ops.preinstall_script: + installer.add_file_as(ops.preinstall_script, "preinstall.sh") + if ops.postinstall_script: + installer.add_file_as(ops.postinstall_script, "postinstall.sh") + + if ops.preinstall_plugin: + installer.add_file_as(ops.preinstall_plugin, "preinstall.py") + if ops.postinstall_plugin: + installer.add_file_as(ops.postinstall_plugin, "postinstall.py") + iname = os.path.abspath(ops.out) installer.build(iname) logger.info("installer: %s" % iname)