[gnome-ostree] qa_make_disk/qa_pull_deploy: Scripts to generate disk images as non-root



commit 866a417dcbb37eceabff9158a0d0b37053798e7e
Author: Colin Walters <walters verbum org>
Date:   Sat Jan 5 08:11:25 2013 -0500

    qa_make_disk/qa_pull_deploy: Scripts to generate disk images as non-root
    
    Using guestfish.

 Makefile-ostbuild.am                       |    6 +-
 src/ostbuild/js/builtins/qa_make_disk.js   |  102 +++++++++++++
 src/ostbuild/js/builtins/qa_pull_deploy.js |  225 ++++++++++++++++++++++++++++
 src/ostbuild/js/main.js                    |    4 +-
 src/ostbuild/js/procutil.js                |   78 ++++++++++
 5 files changed, 413 insertions(+), 2 deletions(-)
---
diff --git a/Makefile-ostbuild.am b/Makefile-ostbuild.am
index 7a685c4..9740386 100644
--- a/Makefile-ostbuild.am
+++ b/Makefile-ostbuild.am
@@ -27,7 +27,9 @@ ostbuild: src/ostbuild/ostbuild.in Makefile
 EXTRA_DIST += ostbuild/ostbuild.in
 
 if BUILDSYSTEM
-bin_SCRIPTS += ostbuild $(srcdir)/src/ostbuild/ostbuild-qemu-pull-deploy
+bin_SCRIPTS += ostbuild \
+	$(srcdir)/src/ostbuild/ostbuild-qemu-pull-deploy \
+	$(NULL)
 
 utils_SCRIPTS = \
 	src/ostbuild/ostree-build-compile-one \
@@ -57,6 +59,8 @@ jsostbuiltins_DATA= \
 	src/ostbuild/js/builtins/build.js \
 	src/ostbuild/js/builtins/checkout.js \
 	src/ostbuild/js/builtins/git_mirror.js \
+	src/ostbuild/js/builtins/qa_make_disk.js \
+	src/ostbuild/js/builtins/qa_pull_deploy.js \
 	src/ostbuild/js/builtins/prefix.js \
 	src/ostbuild/js/builtins/resolve.js \
 	$(NULL)
diff --git a/src/ostbuild/js/builtins/qa_make_disk.js b/src/ostbuild/js/builtins/qa_make_disk.js
new file mode 100644
index 0000000..f6ca0fc
--- /dev/null
+++ b/src/ostbuild/js/builtins/qa_make_disk.js
@@ -0,0 +1,102 @@
+// -*- indent-tabs-mode: nil; tab-width: 2; -*-
+// Copyright (C) 2013 Colin Walters <walters verbum org>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+// Boston, MA 02111-1307, USA.
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Lang = imports.lang;
+const Format = imports.format;
+
+const GSystem = imports.gi.GSystem;
+
+const ArgParse = imports.argparse;
+const ProcUtil = imports.procutil;
+
+const loop = GLib.MainLoop.new(null, true);
+
+const QaMakeDisk = new Lang.Class({
+    Name: 'QaMakeDisk',
+
+    execute: function(argv) {
+        let cancellable = null;
+        let parser = new ArgParse.ArgumentParser("Generate a disk image");
+        parser.addArgument('diskpath');
+        
+        let args = parser.parse(argv);
+
+        let path = Gio.File.new_for_path(args.diskpath);
+        if (path.query_exists(null))
+            throw new Error("" + path.get_path() + " exists");
+
+        let tmppath = path.get_parent().get_child(path.get_basename() + '.tmp');
+        GSystem.shutil_rm_rf(tmppath, cancellable);
+        let sizeMb = 8 * 1024;
+        let bootsizeMb = 200;
+        let swapsizeMb = 64;
+
+        let guestfishProcess;
+        
+        ProcUtil.runSync(['qemu-img', 'create', tmppath.get_path(), '' + sizeMb + 'M'], cancellable);
+        let lines = ProcUtil.runProcWithInputSyncGetLines(['guestfish', '-a', tmppath.get_path()], 
+                                                          cancellable,
+                                                          'launch\n\
+part-init /dev/vda mbr\n\
+blockdev-getsize64 /dev/vda\n\
+blockdev-getss /dev/vda\n');
+                                                 
+        if (lines.length != 2)
+            throw new Error("guestfish returned unexpected output lines (" + lines.length + ", expected 2");
+        let diskBytesize = parseInt(lines[0]);
+        let diskSectorsize = parseInt(lines[1]);
+        print(Format.vprintf("bytesize: %s sectorsize: %s", [diskBytesize, diskSectorsize]));
+        let bootsizeSectors = bootsizeMb * 1024 / diskSectorsize * 1024;
+        let swapsizeSectors = swapsizeMb * 1024 / diskSectorsize * 1024;
+        let rootsizeSectors = diskBytesize / diskSectorsize - bootsizeSectors - swapsizeSectors - 64;
+        let bootOffset = 64;
+        let swapOffset = bootOffset + bootsizeSectors;
+        let rootOffset = swapOffset + swapsizeSectors;
+        let endOffset = rootOffset + rootsizeSectors;
+
+        let partconfig = Format.vprintf('launch\n\
+part-add /dev/vda p %s %s\n\
+part-add /dev/vda p %s %s\n\
+part-add /dev/vda p %s %s\n\
+mkfs ext4 /dev/vda1\n\
+set-e2label /dev/vda1 gnostree-boot\n\
+mkswap-L gnostree-swap /dev/vda2\n\
+mkfs ext4 /dev/vda3\n\
+set-e2label /dev/vda3 gnostree-root\n\
+mount /dev/vda3 /\n\
+mkdir /boot\n\
+', [bootOffset, swapOffset - 1,
+    swapOffset, rootOffset - 1,
+    rootOffset, endOffset - 1]);
+        print("partition config: ", partconfig);
+        lines = ProcUtil.runProcWithInputSyncGetLines(['guestfish', '-a', tmppath.get_path()], cancellable, partconfig);
+        GSystem.file_rename(tmppath, path, cancellable);
+        print("Created: " + path.get_path());
+    }
+});
+
+function main(argv) {
+    let ecode = 1;
+    var app = new QaMakeDisk();
+    GLib.idle_add(GLib.PRIORITY_DEFAULT,
+                  function() { try { app.execute(argv); ecode = 0; } finally { loop.quit(); }; return false; });
+    loop.run();
+    return ecode;
+}
diff --git a/src/ostbuild/js/builtins/qa_pull_deploy.js b/src/ostbuild/js/builtins/qa_pull_deploy.js
new file mode 100644
index 0000000..9bf872f
--- /dev/null
+++ b/src/ostbuild/js/builtins/qa_pull_deploy.js
@@ -0,0 +1,225 @@
+// -*- indent-tabs-mode: nil; tab-width: 2; -*-
+// Copyright (C) 2013 Colin Walters <walters verbum org>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+// Boston, MA 02111-1307, USA.
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Lang = imports.lang;
+const Format = imports.format;
+
+const GSystem = imports.gi.GSystem;
+
+const ArgParse = imports.argparse;
+const ProcUtil = imports.procutil;
+
+const loop = GLib.MainLoop.new(null, true);
+
+const QaPullDeploy = new Lang.Class({
+    Name: 'QaPullDeploy',
+
+    _findCurrentKernel: function(mntdir, osname, cancellable) {
+        let deployBootdir = mntdir.resolve_relative_path('ostree/deploy/' + osname + '/current/boot');
+        let d = deployBootdir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
+	      let finfo;
+	      while ((finfo = d.next_file(cancellable)) != null) {
+	          let child = deployBootdir.get_child(finfo.get_name());
+	          if (child.get_basename().indexOf('vmlinuz-') == 0) {
+                return child;
+            }
+        }
+        d.close(cancellable);
+        throw new Error("Couldn't find vmlinuz- in " + deployBootdir.get_path());
+    },
+
+    _parseKernelRelease: function(kernelPath) {
+        let name = kernelPath.get_basename();
+        let idx = name.indexOf('-');
+        if (idx == -1) throw new Error("Invalid kernel name " + kernelPath.get_path());
+        let kernelRelease = name.substr(idx + 1);
+        return kernelRelease;
+    },
+
+    _getInitramfsPath: function(mntdir, kernelRelease) {
+        let bootdir = mntdir.get_child('boot');
+        let initramfsName = 'initramfs-' + kernelRelease + '.img';
+        let path = bootdir.resolve_relative_path('ostree/' + initramfsName);
+        if (!path.query_exists(null))
+            throw new Error("Couldn't find initramfs " + path.get_path());
+        return path;
+    },
+
+    // https://bugzilla.redhat.com/show_bug.cgi?id=892834
+    // Also; we have to recreate it as a directory, then
+    // delete that again to avoid further fuse/guestfs bugs.
+    _workaroundGuestfsFuseBug: function(symlinkPath, cancellable) {
+        GSystem.shutil_rm_rf(symlinkPath, cancellable);
+        GSystem.file_ensure_directory(symlinkPath, true, cancellable);
+        let dummyFile = symlinkPath.get_child('dummy');
+        dummyFile.replace_contents('hello world', null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
+        GSystem.shutil_rm_rf(symlinkPath, cancellable);
+    },
+
+    execute: function(argv) {
+        let cancellable = null;
+        let parser = new ArgParse.ArgumentParser("Generate a disk image");
+        parser.addArgument('diskpath');
+        parser.addArgument('srcrepo');
+        parser.addArgument('osname');
+        parser.addArgument('target');
+        
+        let args = parser.parse(argv);
+
+        let diskpath = Gio.File.new_for_path(args.diskpath);
+
+        let workdir = Gio.File.new_for_path('.');
+        let mntdir = workdir.get_child('mnt');
+        GSystem.file_ensure_directory(mntdir, true, cancellable);
+        let bootdir = mntdir.get_child('boot');
+        let ostreedir = mntdir.get_child('ostree');
+        let ostree_osdir = ostreedir.resolve_relative_path('deploy/' + args.osname);
+        let guestmountPidFile = workdir.get_child('guestmount.pid');
+
+        if (guestmountPidFile.query_exists(null)) {
+            throw new Error("guestmount pid file exists (unclean shutdown?): " + guestmountPidFile.get_path());
+        }
+
+        try {
+            let procContext = new GSystem.SubprocessContext({ argv: ['guestmount', '--rw', '-o', 'allow_root',
+                                                                     '--pid-file', guestmountPidFile.get_path(),
+                                                                     '-a', diskpath.get_path(),
+                                                                     '-m', '/dev/sda3',
+                                                                     '-m', '/dev/sda1:/boot',
+                                                                     mntdir.get_path()] });
+            let guestfishProc = new GSystem.Subprocess({context: procContext});
+            print("starting guestfish");
+            guestfishProc.init(cancellable);
+            guestfishProc.wait_sync_check(cancellable);
+            // guestfish should have daemonized now (unfortunately, if
+            // there was a way to avoid that we would).
+
+            let adminCmd = ['ostree', 'admin', '--ostree-dir=' + ostreedir.get_path(),
+                            '--boot-dir=' + mntdir.get_child('boot').get_path()];
+            let adminEnv = GLib.get_environ();
+            adminEnv.push('LIBGSYSTEM_ENABLE_GUESTFS_FUSE_WORKAROUND=1');
+            let procdir = mntdir.get_child('proc');
+            if (!procdir.query_exists(cancellable)) {
+                ProcUtil.runSync(adminCmd.concat(['init-fs', mntdir.get_path()]), cancellable,
+                                 {logInitiation: true, env: adminEnv});
+            }
+            ProcUtil.runSync(adminCmd.concat(['os-init', args.osname]), cancellable,
+                             {logInitiation: true, env: adminEnv});
+            ProcUtil.runSync(adminCmd.concat(['os-init', args.osname]), cancellable,
+                             {logInitiation: true, env: adminEnv});
+            ProcUtil.runSync(['ostree', '--repo=' + ostreedir.get_child('repo').get_path(),
+                              'pull-local', args.srcrepo, args.target], cancellable,
+                             {logInitiation: true, env: adminEnv});
+
+            let currentDeployLink = ostree_osdir.get_child('current');
+            let currentEtcDeployLink = ostree_osdir.get_child('current-etc');
+            this._workaroundGuestfsFuseBug(currentDeployLink, cancellable);
+            this._workaroundGuestfsFuseBug(currentEtcDeployLink, cancellable);
+            
+            ProcUtil.runSync(adminCmd.concat(['deploy', '--no-kernel', args.osname, args.target]), cancellable,
+                             {logInitiation: true, env: adminEnv});
+            ProcUtil.runSync(adminCmd.concat(['update-kernel', '--no-bootloader', args.osname]), cancellable,
+                             {logInitiation: true, env: adminEnv});
+            ProcUtil.runSync(adminCmd.concat(['prune', args.osname]), cancellable,
+                             {logInitiation: true, env: adminEnv});
+
+            let deployKernelPath = this._findCurrentKernel(mntdir, args.osname, cancellable);
+            let bootKernelPath = bootdir.resolve_relative_path('ostree/' + deployKernelPath.get_basename());
+            if (!bootKernelPath.query_exists(cancellable))
+                throw new Error("" + bootKernelPath.get_path() + " doesn't exist");
+            let kernelRelease = this._parseKernelRelease(deployKernelPath);
+            let initramfsPath = this._getInitramfsPath(mntdir, kernelRelease);
+
+            let defaultFstab = 'LABEL=gnostree-root / ext4 defaults 1 1\n\
+LABEL=gnostree-boot /boot ext4 defaults 1 2\n\
+LABEL=gnostree-swap swap swap defaults 0 0\n';
+            let fstabPath = ostreedir.resolve_relative_path('deploy/gnome-ostree/current-etc/fstab'); 
+            fstabPath.replace_contents(defaultFstab, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
+            
+            let grubDir = mntdir.resolve_relative_path('boot/grub');
+            GSystem.file_ensure_directory(grubDir, false, cancellable);
+            let bootRelativeKernelPath = bootdir.get_relative_path(bootKernelPath);
+            if (bootRelativeKernelPath == null)
+                throw new Error("" + bootKernelPath.get_path() + " is not relative to " + bootdir.get_path());
+            let bootRelativeInitramfsPath = bootdir.get_relative_path(initramfsPath);
+            let grubConfPath = grubDir.get_child('grub.conf');
+            let grubConf = Format.vprintf('default=0\n\
+timeout=5\n\
+title GNOME-OSTree\n\
+        root (hd0,0)\n\
+        kernel /%s root=LABEL=gnostree-root\n\
+        initrd /%s\n', [bootRelativeKernelPath, bootRelativeInitramfsPath]);
+            grubConfPath.replace_contents(grubConf, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
+        } finally {
+            if (guestmountPidFile.query_exists(null)) {
+                let pidStr = GSystem.file_load_contents_utf8(guestmountPidFile, cancellable);
+                if (pidStr.length > 0) {
+                    for (let i = 0; i < 30; i++) {
+                        // See "man guestmount" for why retry loops here might be needed if this
+                        // script is running on a client machine with programs that watch for new mounts
+                        try {
+                            ProcUtil.runSync(['fusermount', '-u', mntdir.get_path()], cancellable,
+                                             {logInitiation: true});
+                            break;
+                        } catch (e) {
+                            if (!(e.origError && e.origError.domain == GLib.spawn_exit_error_quark()))
+                                throw e;
+                            else
+                                GLib.usleep(GLib.USEC_PER_SEC);
+                        }
+                    }
+                    let pid = parseInt(pidStr);
+                    for (let i = 0; i < 30; i++) {
+                        let killContext = new GSystem.SubprocessContext({argv: ['kill', '-0', ''+pid]});
+                        killContext.set_stderr_disposition(GSystem.SubprocessStreamDisposition.NULL);
+                        let killProc = new GSystem.Subprocess({context: killContext});
+                        killProc.init(null);
+                        let [waitSuccess, ecode] = killProc.wait_sync(null);
+                        let [killSuccess, statusStr] = ProcUtil.getExitStatusAndString(ecode);
+                        if (killSuccess) {
+                            print("Awaiting termination of guestfish, pid=" + pid + " timeout=" + (30 - i) + "s");
+                            GLib.usleep(GLib.USEC_PER_SEC);
+                        } else {
+                            break;
+                            print("guestmount exited");
+                        }
+                    }
+                }
+            }
+        }
+
+        let grubInstallCmds = 'grub-install / /dev/vda\n';
+        let lines = ProcUtil.runProcWithInputSyncGetLines(['guestfish', '-a', args.diskpath,
+                                                           '-m', '/dev/sda3',
+                                                           '-m', '/dev/sda1:/boot'],
+                                                          cancellable, grubInstallCmds);
+        
+        print("Complete!");
+    }
+});
+
+function main(argv) {
+    let ecode = 1;
+    var app = new QaPullDeploy();
+    GLib.idle_add(GLib.PRIORITY_DEFAULT,
+                  function() { try { app.execute(argv); ecode = 0; } finally { loop.quit(); }; return false; });
+    loop.run();
+    return ecode;
+}
diff --git a/src/ostbuild/js/main.js b/src/ostbuild/js/main.js
index 55ed988..4f4a319 100755
--- a/src/ostbuild/js/main.js
+++ b/src/ostbuild/js/main.js
@@ -22,7 +22,9 @@ const BUILTINS = {'autobuilder': "Run resolve and build",
                   'prefix': "Display or modify \"prefix\" (build target)",
                   'git-mirror': "Update internal git mirror for one or more components",
                   'resolve': "Expand git revisions in source to exact targets",
-                  'build': "Build multiple components and generate trees"};
+                  'build': "Build multiple components and generate trees",
+                  'qa-make-disk': "Generate a bare disk image",
+		  'qa-pull-deploy': "Copy OSTree repo into virtual disk and deploy it"};
 
 function usage(ecode) {
     print("Builtins:");
diff --git a/src/ostbuild/js/procutil.js b/src/ostbuild/js/procutil.js
index f1a6480..0af1ab3 100644
--- a/src/ostbuild/js/procutil.js
+++ b/src/ostbuild/js/procutil.js
@@ -141,3 +141,81 @@ function asyncWaitCheckFinish(process, result) {
     let [waitSuccess, ecode] = process.wait_finish(result);
     return getExitStatusAndString(ecode);
 }
+
+function runWithTempContextAndLoop(func) {
+    let mainContext = new GLib.MainContext();
+    let mainLoop = GLib.MainLoop.new(mainContext, true);
+    try {
+        mainContext.push_thread_default();
+        return func(mainLoop);
+    } finally {
+        mainContext.pop_thread_default();
+    }
+}
+
+function _runProcWithInputSyncGetLinesInternal(mainLoop, argv, cancellable, input) {
+    let context = new GSystem.SubprocessContext({ argv: argv });
+    context.set_stdout_disposition(GSystem.SubprocessStreamDisposition.PIPE);
+    context.set_stdin_disposition(GSystem.SubprocessStreamDisposition.PIPE);
+    let proc = new GSystem.Subprocess({context: context});
+    proc.init(cancellable);
+    let stdinPipe = proc.get_stdin_pipe();
+    let memStream = Gio.MemoryInputStream.new_from_bytes(new GLib.Bytes(input));
+    let asyncOps = 3;
+    function asyncOpComplete() {
+        asyncOps--;
+        if (asyncOps == 0)
+            mainLoop.quit();
+    }
+    function onSpliceComplete(stdinPipe, result) {
+        try {
+            let bytesWritten = stdinPipe.splice_finish(result);
+        } finally {
+            asyncOpComplete();
+        }
+    }
+    let closeBoth = Gio.OutputStreamSpliceFlags.CLOSE_SOURCE | Gio.OutputStreamSpliceFlags.CLOSE_TARGET;
+    stdinPipe.splice_async(memStream, closeBoth, GLib.PRIORITY_DEFAULT,
+                           cancellable, onSpliceComplete);
+
+    let procException = null;
+    function onProcExited(proc, result) {
+        try {
+            let [success, statusText] = asyncWaitCheckFinish(proc, result);
+            if (!success)
+                procException = statusText;
+        } finally {
+            asyncOpComplete();
+        }
+    }
+    proc.wait(cancellable, onProcExited);
+    
+    let stdoutPipe = proc.get_stdout_pipe();
+    let stdoutData = Gio.DataInputStream.new(stdoutPipe);
+    let lines = [];
+    function onReadLine(datastream, result) {
+        try {
+            let [line, len] = stdoutData.read_line_finish_utf8(result);
+            if (line == null)
+                asyncOpComplete();
+            else {
+                lines.push(line);
+                stdoutData.read_line_async(GLib.PRIORITY_DEFAULT, cancellable, onReadLine);
+            }
+        } catch (e) {
+            asyncOpComplete();
+            throw e;
+        }
+    }
+    stdoutData.read_line_async(GLib.PRIORITY_DEFAULT, cancellable, onReadLine);
+
+    mainLoop.run();
+
+    return lines;
+}
+
+function runProcWithInputSyncGetLines(argv, cancellable, input) {
+    return runWithTempContextAndLoop(function (loop) {
+        return  _runProcWithInputSyncGetLinesInternal(loop, argv, cancellable, input);
+    });
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]