[libvirt] [sandbox PATCH v4 00/21] *** virt-sandbox-image v4 ***

Hi, Running sandbox now has its own disk layer. virt-sandbox now using GHashMap to store key value pairs for environment variables and some minor changes. Daniel P Berrange (1): Add virt-sandbox-image Eren Yagdiran (20): Fix virt-sandbox-image Image: Add Hooking Mechanism Image: virt-sandbox-image default dir constants Image: Discard caching bytecode Image: Add check_writable and runtime resolver Image: Add download function Image: Refactor create function Image: Add delete function Image: Add get_command function to Source Image: Add run args Image: Add check_connect function Image: Add get_disk function to Source Image: Add run function Image: Add network support Image: Add Volume Support Image: man file for virt-sandbox-image Add config for environment variables Add environment parameter to virt-sandbox init-common: Exporting custom environment variables Image: Add custom environment support .gitignore | 1 + bin/Makefile.am | 21 +- bin/virt-sandbox-image.in | 3 + bin/virt-sandbox-image.pod | 172 +++++++++++ bin/virt-sandbox.c | 14 + configure.ac | 2 + libvirt-sandbox/libvirt-sandbox-config.c | 171 ++++++++++- libvirt-sandbox/libvirt-sandbox-config.h | 13 + libvirt-sandbox/libvirt-sandbox-init-common.c | 18 ++ libvirt-sandbox/libvirt-sandbox.sym | 6 + libvirt-sandbox/tests/test-config.c | 10 + po/POTFILES.in | 1 + virt-sandbox-image/Makefile.am | 14 + virt-sandbox-image/sources/DockerSource.py | 419 ++++++++++++++++++++++++++ virt-sandbox-image/sources/Source.py | 55 ++++ virt-sandbox-image/sources/__init__.py | 26 ++ virt-sandbox-image/virt-sandbox-image.py | 306 +++++++++++++++++++ 17 files changed, 1247 insertions(+), 5 deletions(-) create mode 100644 bin/virt-sandbox-image.in create mode 100644 bin/virt-sandbox-image.pod create mode 100644 virt-sandbox-image/Makefile.am create mode 100644 virt-sandbox-image/sources/DockerSource.py create mode 100644 virt-sandbox-image/sources/Source.py create mode 100644 virt-sandbox-image/sources/__init__.py create mode 100755 virt-sandbox-image/virt-sandbox-image.py -- 2.1.0

From: Daniel P Berrange <berrange@redhat.com> virt-sandbox-image.py is a python script that lets you download Docker images easily. It is a proof of concept code and consumes Docker Rest API. --- po/POTFILES.in | 1 + virt-sandbox-image/virt-sandbox-image.py | 394 +++++++++++++++++++++++++++++++ 2 files changed, 395 insertions(+) create mode 100644 virt-sandbox-image/virt-sandbox-image.py diff --git a/po/POTFILES.in b/po/POTFILES.in index afcb050..7204112 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -11,3 +11,4 @@ libvirt-sandbox/libvirt-sandbox-context-interactive.c libvirt-sandbox/libvirt-sandbox-init-common.c libvirt-sandbox/libvirt-sandbox-rpcpacket.c libvirt-sandbox/libvirt-sandbox-util.c +virt-sandbox-image/virt-sandbox-image.py diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py new file mode 100644 index 0000000..4f5443b --- /dev/null +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -0,0 +1,394 @@ +#!/usr/bin/python -Es +# +# Authors: Daniel P. Berrange <berrange@redhat.com> +# +# Copyright (C) 2013 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# + +import argparse +import gettext +import hashlib +import json +import os +import os.path +import shutil +import sys +import urllib2 +import subprocess + +default_index_server = "index.docker.io" +default_template_dir = "/var/lib/libvirt/templates" + +debug = True +verbose = True + +gettext.bindtextdomain("libvirt-sandbox", "/usr/share/locale") +gettext.textdomain("libvirt-sandbox") +try: + gettext.install("libvirt-sandbox", + localedir="/usr/share/locale", + unicode=False, + codeset = 'utf-8') +except IOError: + import __builtin__ + __builtin__.__dict__['_'] = unicode + + +def debug(msg): + sys.stderr.write(msg) + +def info(msg): + sys.stdout.write(msg) + +def get_url(server, path, headers): + url = "https://" + server + path + debug(" Fetching %s..." % url) + req = urllib2.Request(url=url) + + if json: + req.add_header("Accept", "application/json") + + for h in headers.keys(): + req.add_header(h, headers[h]) + + return urllib2.urlopen(req) + +def get_json(server, path, headers): + try: + res = get_url(server, path, headers) + data = json.loads(res.read()) + debug("OK\n") + return (data, res) + except Exception, e: + debug("FAIL %s\n" % str(e)) + raise + +def save_data(server, path, headers, dest, checksum=None, datalen=None): + try: + res = get_url(server, path, headers) + + csum = None + if checksum is not None: + csum = hashlib.sha256() + + pattern = [".", "o", "O", "o"] + patternIndex = 0 + donelen = 0 + + with open(dest, "w") as f: + while 1: + buf = res.read(1024*64) + if not buf: + break + if csum is not None: + csum.update(buf) + f.write(buf) + + if datalen is not None: + donelen = donelen + len(buf) + debug("\x1b[s%s (%5d Kb of %5d Kb)\x1b8" % ( + pattern[patternIndex], (donelen/1024), (datalen/1024) + )) + patternIndex = (patternIndex + 1) % 4 + + debug("\x1b[K") + if csum is not None: + csumstr = "sha256:" + csum.hexdigest() + if csumstr != checksum: + debug("FAIL checksum '%s' does not match '%s'" % (csumstr, checksum)) + os.remove(dest) + raise IOError("Checksum '%s' for data does not match '%s'" % (csumstr, checksum)) + debug("OK\n") + return res + except Exception, e: + debug("FAIL %s\n" % str(e)) + raise + + +def download_template(name, server, destdir): + tag = "latest" + + offset = name.find(':') + if offset != -1: + tag = name[offset + 1:] + name = name[0:offset] + + # First we must ask the index server about the image name. THe + # index server will return an auth token we can use when talking + # to the registry server. We need this token even when anonymous + try: + (data, res) = get_json(server, "/v1/repositories/" + name + "/images", + {"X-Docker-Token": "true"}) + except urllib2.HTTPError, e: + raise ValueError(["Image '%s' does not exist" % name]) + + registryserver = res.info().getheader('X-Docker-Endpoints') + token = res.info().getheader('X-Docker-Token') + checksums = {} + for layer in data: + pass + # XXX Checksums here don't appear to match the data in + # image download later. Find out what these are sums of + #checksums[layer["id"]] = layer["checksum"] + + # Now we ask the registry server for the list of tags associated + # with the image. Tags usually reflect some kind of version of + # the image, but they aren't officially "versions". There is + # always a "latest" tag which is the most recent upload + # + # We must pass in the auth token from the index server. This + # token can only be used once, and we're given a cookie back + # in return to use for later RPC calls. + (data, res) = get_json(registryserver, "/v1/repositories/" + name + "/tags", + { "Authorization": "Token " + token }) + + cookie = res.info().getheader('Set-Cookie') + + if not tag in data: + raise ValueError(["Tag '%s' does not exist for image '%s'" % (tag, name)]) + imagetagid = data[tag] + + # Only base images are self-contained, most images reference one + # or more parents, in a linear stack. Here we are getting the list + # of layers for the image with the tag we used. + (data, res) = get_json(registryserver, "/v1/images/" + imagetagid + "/ancestry", + { "Cookie": cookie }) + + if data[0] != imagetagid: + raise ValueError(["Expected first layer id '%s' to match image id '%s'", + data[0], imagetagid]) + + try: + createdFiles = [] + createdDirs = [] + + for layerid in data: + templatedir = destdir + "/" + layerid + if not os.path.exists(templatedir): + os.mkdir(templatedir) + createdDirs.append(templatedir) + + jsonfile = templatedir + "/template.json" + datafile = templatedir + "/template.tar.gz" + + if not os.path.exists(jsonfile) or not os.path.exists(datafile): + # The '/json' URL gives us some metadata about the layer + res = save_data(registryserver, "/v1/images/" + layerid + "/json", + { "Cookie": cookie }, jsonfile) + createdFiles.append(jsonfile) + layersize = int(res.info().getheader("x-docker-size")) + + datacsum = None + if layerid in checksums: + datacsum = checksums[layerid] + + # and the '/layer' URL is the actual payload, provided + # as a tar.gz archive + save_data(registryserver, "/v1/images/" + layerid + "/layer", + { "Cookie": cookie }, datafile, datacsum, layersize) + createdFiles.append(datafile) + + # Strangely the 'json' data for a layer doesn't include + # its actual name, so we save that in a json file of our own + index = { + "name": name, + } + + indexfile = destdir + "/" + imagetagid + "/index.json" + with open(indexfile, "w") as f: + f.write(json.dumps(index)) + except Exception, e: + for f in createdFiles: + try: + os.remove(f) + except: + pass + for d in createdDirs: + try: + os.rmdir(d) + except: + pass + + +def delete_template(name, destdir): + imageusage = {} + imageparent = {} + imagenames = {} + imagedirs = os.listdir(destdir) + for imagetagid in imagedirs: + indexfile = destdir + "/" + imagetagid + "/index.json" + if os.path.exists(indexfile): + with open(indexfile, "r") as f: + index = json.load(f) + imagenames[index["name"]] = imagetagid + jsonfile = destdir + "/" + imagetagid + "/template.json" + if os.path.exists(jsonfile): + with open(jsonfile, "r") as f: + template = json.load(f) + + parent = template.get("parent", None) + if parent: + if parent not in imageusage: + imageusage[parent] = [] + imageusage[parent].append(imagetagid) + imageparent[imagetagid] = parent + + if not name in imagenames: + raise ValueError(["Image %s does not exist locally" % name]) + + imagetagid = imagenames[name] + while imagetagid != None: + debug("Remove %s\n" % imagetagid) + parent = imageparent.get(imagetagid, None) + + indexfile = destdir + "/" + imagetagid + "/index.json" + if os.path.exists(indexfile): + os.remove(indexfile) + jsonfile = destdir + "/" + imagetagid + "/template.json" + if os.path.exists(jsonfile): + os.remove(jsonfile) + datafile = destdir + "/" + imagetagid + "/template.tar.gz" + if os.path.exists(datafile): + os.remove(datafile) + imagedir = destdir + "/" + imagetagid + os.rmdir(imagedir) + + if parent: + if len(imageusage[parent]) != 1: + debug("Parent %s is shared\n" % parent) + parent = None + imagetagid = parent + + +def get_image_list(name, destdir): + imageparent = {} + imagenames = {} + imagedirs = os.listdir(destdir) + for imagetagid in imagedirs: + indexfile = destdir + "/" + imagetagid + "/index.json" + if os.path.exists(indexfile): + with open(indexfile, "r") as f: + index = json.load(f) + imagenames[index["name"]] = imagetagid + jsonfile = destdir + "/" + imagetagid + "/template.json" + if os.path.exists(jsonfile): + with open(jsonfile, "r") as f: + template = json.load(f) + + parent = template.get("parent", None) + if parent: + imageparent[imagetagid] = parent + + if not name in imagenames: + raise ValueError(["Image %s does not exist locally" % name]) + + imagetagid = imagenames[name] + imagelist = [] + while imagetagid != None: + imagelist.append(imagetagid) + parent = imageparent.get(imagetagid, None) + imagetagid = parent + + return imagelist + +def create_template(name, imagepath, format, destdir): + if not format in ["qcow2"]: + raise ValueError(["Unsupported image format %s" % format]) + + imagelist = get_image_list(name, destdir) + imagelist.reverse() + + parentImage = None + for imagetagid in imagelist: + templateImage = destdir + "/" + imagetagid + "/template." + format + cmd = ["qemu-img", "create", "-f", "qcow2"] + if parentImage is not None: + cmd.append("-o") + cmd.append("backing_fmt=qcow2,backing_file=%s" % parentImage) + cmd.append(templateImage) + if parentImage is None: + cmd.append("10G") + debug("Run %s\n" % " ".join(cmd)) + subprocess.call(cmd) + parentImage = templateImage + +def download(args): + info("Downloading %s from %s to %s\n" % (args.name, default_index_server, default_template_dir)) + download_template(args.name, default_index_server, default_template_dir) + +def delete(args): + info("Deleting %s from %s\n" % (args.name, default_template_dir)) + delete_template(args.name, default_template_dir) + +def create(args): + info("Creating %s from %s in format %s\n" % (args.imagepath, args.name, args.format)) + create_template(args.name, args.imagepath, args.format, default_template_dir) + +def requires_name(parser): + parser.add_argument("name", + help=_("name of the template")) + +def gen_download_args(subparser): + parser = subparser.add_parser("download", + help=_("Download template data")) + requires_name(parser) + parser.set_defaults(func=download) + +def gen_delete_args(subparser): + parser = subparser.add_parser("delete", + help=_("Delete template data")) + requires_name(parser) + parser.set_defaults(func=delete) + +def gen_create_args(subparser): + parser = subparser.add_parser("create", + help=_("Create image from template data")) + requires_name(parser) + parser.add_argument("imagepath", + help=_("path for image")) + parser.add_argument("format", + help=_("format")) + parser.set_defaults(func=create) + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Sandbox Container Image Tool') + + subparser = parser.add_subparsers(help=_("commands")) + gen_download_args(subparser) + gen_delete_args(subparser) + gen_create_args(subparser) + + try: + args = parser.parse_args() + args.func(args) + sys.exit(0) + except KeyboardInterrupt, e: + sys.exit(0) + except ValueError, e: + for line in e: + for l in line: + sys.stderr.write("%s: %s\n" % (sys.argv[0], l)) + sys.stderr.flush() + sys.exit(1) + except IOError, e: + sys.stderr.write("%s: %s: %s\n" % (sys.argv[0], e.filename, e.reason)) + sys.stderr.flush() + sys.exit(1) + except OSError, e: + sys.stderr.write("%s: %s\n" % (sys.argv[0], e)) + sys.stderr.flush() + sys.exit(1) -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:29PM +0000, Eren Yagdiran wrote:
From: Daniel P Berrange <berrange@redhat.com>
virt-sandbox-image.py is a python script that lets you download Docker images easily. It is a proof of concept code and consumes Docker Rest API. --- po/POTFILES.in | 1 + virt-sandbox-image/virt-sandbox-image.py | 394 +++++++++++++++++++++++++++++++ 2 files changed, 395 insertions(+) create mode 100644 virt-sandbox-image/virt-sandbox-image.py
ACK Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

On Fri, Aug 28, 2015 at 01:47:29PM +0000, Eren Yagdiran wrote:
From: Daniel P Berrange <berrange@redhat.com>
virt-sandbox-image.py is a python script that lets you download Docker images easily. It is a proof of concept code and consumes Docker Rest API. --- po/POTFILES.in | 1 + virt-sandbox-image/virt-sandbox-image.py | 394 +++++++++++++++++++++++++++++++ 2 files changed, 395 insertions(+) create mode 100644 virt-sandbox-image/virt-sandbox-image.py
I'm thinking that this file would be better off going in libvirt-sandbox/image/cli.py and then adding a bin/virt-sandbox-image file that merely does #!/usr/bin/python # -*- coding: utf-8 -*- from libvirt_sandbox.image import cli import sys if __name__ == '__main__': sys.exit(cli.main()) and also updating libvirt-sandbox.spec.in to hold the new file Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Authentication fix for Docker REST API. --- virt-sandbox-image/virt-sandbox-image.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 4f5443b..a9cb0ff 100644 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -1,8 +1,10 @@ #!/usr/bin/python -Es # # Authors: Daniel P. Berrange <berrange@redhat.com> +# Eren Yagdiran <erenyagdiran@gmail.com> # # Copyright (C) 2013 Red Hat, Inc. +# Copyright (C) 2015 Universitat Polit��cnica de Catalunya. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -166,7 +168,7 @@ def download_template(name, server, destdir): # or more parents, in a linear stack. Here we are getting the list # of layers for the image with the tag we used. (data, res) = get_json(registryserver, "/v1/images/" + imagetagid + "/ancestry", - { "Cookie": cookie }) + { "Authorization": "Token " + token }) if data[0] != imagetagid: raise ValueError(["Expected first layer id '%s' to match image id '%s'", @@ -188,9 +190,9 @@ def download_template(name, server, destdir): if not os.path.exists(jsonfile) or not os.path.exists(datafile): # The '/json' URL gives us some metadata about the layer res = save_data(registryserver, "/v1/images/" + layerid + "/json", - { "Cookie": cookie }, jsonfile) + { "Authorization": "Token " + token }, jsonfile) createdFiles.append(jsonfile) - layersize = int(res.info().getheader("x-docker-size")) + layersize = int(res.info().getheader("Content-Length")) datacsum = None if layerid in checksums: @@ -199,7 +201,7 @@ def download_template(name, server, destdir): # and the '/layer' URL is the actual payload, provided # as a tar.gz archive save_data(registryserver, "/v1/images/" + layerid + "/layer", - { "Cookie": cookie }, datafile, datacsum, layersize) + { "Authorization": "Token " + token }, datafile, datacsum, layersize) createdFiles.append(datafile) # Strangely the 'json' data for a layer doesn't include -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:30PM +0000, Eren Yagdiran wrote:
Authentication fix for Docker REST API. --- virt-sandbox-image/virt-sandbox-image.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-)
ACK Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Any custom source provider can be added to virt-sandbox-image as a source --- .gitignore | 1 + bin/Makefile.am | 16 ++++++++++++---- bin/virt-sandbox-image.in | 3 +++ configure.ac | 2 ++ virt-sandbox-image/Makefile.am | 13 +++++++++++++ virt-sandbox-image/sources/Source.py | 27 +++++++++++++++++++++++++++ virt-sandbox-image/sources/__init__.py | 26 ++++++++++++++++++++++++++ virt-sandbox-image/virt-sandbox-image.py | 15 +++++++++------ 8 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 bin/virt-sandbox-image.in create mode 100644 virt-sandbox-image/Makefile.am create mode 100644 virt-sandbox-image/sources/Source.py create mode 100644 virt-sandbox-image/sources/__init__.py mode change 100644 => 100755 virt-sandbox-image/virt-sandbox-image.py diff --git a/.gitignore b/.gitignore index f77ea12..ef5b5aa 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ bin/virt-sandbox bin/virt-sandbox-service-util build/ bin/*.1 +bin/virt-sandbox-image diff --git a/bin/Makefile.am b/bin/Makefile.am index 416f86f..df4c7dc 100644 --- a/bin/Makefile.am +++ b/bin/Makefile.am @@ -3,7 +3,11 @@ bin_PROGRAMS = virt-sandbox libexec_PROGRAMS = virt-sandbox-service-util -bin_SCRIPTS = virt-sandbox-service +bin_SCRIPTS = virt-sandbox-service \ + virt-sandbox-image + +virt-sandbox-image: virt-sandbox-image.in + sed -e 's,[@]pkgpythondir[@],$(pkgpythondir),g' < $< >$@ virtsandboxcompdir = $(datarootdir)/bash-completion/completions/ @@ -20,8 +24,11 @@ POD_FILES = \ virt-sandbox-service-reload.pod \ virt-sandbox-service-upgrade.pod \ $(NULL) -EXTRA_DIST = $(bin_SCRIPTS) $(POD_FILES) virt-sandbox-service-bash-completion.sh virt-sandbox-service.logrotate -EXTRA_DIST += virt-sandbox-service-bash-completion.sh +EXTRA_DIST = virt-sandbox-service \ + virt-sandbox-image.in \ + $(POD_FILES) \ + virt-sandbox-service-bash-completion.sh \ + virt-sandbox-service.logrotate man1_MANS = \ virt-sandbox.1 \ @@ -64,7 +71,8 @@ virt-sandbox-service-reload.1: virt-sandbox-service-reload.pod Makefile virt-sandbox-service-upgrade.1: virt-sandbox-service-upgrade.pod Makefile $(AM_V_GEN)$(POD2MAN) $< $(srcdir)/$@ -CLEANFILES = $(man1_MANS) +CLEANFILES = $(man1_MANS) \ + virt-sandbox-image virt_sandbox_SOURCES = virt-sandbox.c virt_sandbox_CFLAGS = \ diff --git a/bin/virt-sandbox-image.in b/bin/virt-sandbox-image.in new file mode 100644 index 0000000..732bb38 --- /dev/null +++ b/bin/virt-sandbox-image.in @@ -0,0 +1,3 @@ +#!/bin/sh + +exec "@pkgpythondir@/virt-sandbox-image.py" "$@" diff --git a/configure.ac b/configure.ac index 8f6da04..69b5870 100644 --- a/configure.ac +++ b/configure.ac @@ -124,11 +124,13 @@ dnl Should be in m4/virt-gettext.m4 but intltoolize is too dnl dumb to find it there IT_PROG_INTLTOOL([0.35.0]) +AM_PATH_PYTHON AC_OUTPUT(Makefile libvirt-sandbox/Makefile libvirt-sandbox/tests/Makefile bin/Makefile + virt-sandbox-image/Makefile examples/Makefile docs/Makefile docs/libvirt-sandbox/Makefile diff --git a/virt-sandbox-image/Makefile.am b/virt-sandbox-image/Makefile.am new file mode 100644 index 0000000..5ab4d2e --- /dev/null +++ b/virt-sandbox-image/Makefile.am @@ -0,0 +1,13 @@ + +EXTRA_DIST = \ + virt-sandbox-image.py \ + sources + +install-data-local: + $(mkinstalldirs) $(DESTDIR)/$(pkgpythondir)/sources + $(INSTALL) -m 0755 $(srcdir)/virt-sandbox-image.py $(DESTDIR)$(pkgpythondir) + $(INSTALL) -m 0644 $(srcdir)/sources/__init__.py $(DESTDIR)$(pkgpythondir)/sources + $(INSTALL) -m 0644 $(srcdir)/sources/Source.py $(DESTDIR)$(pkgpythondir)/sources + +uninstall-local: + rm -f $(DESTDIR)$(pkgpythondir) diff --git a/virt-sandbox-image/sources/Source.py b/virt-sandbox-image/sources/Source.py new file mode 100644 index 0000000..508ca80 --- /dev/null +++ b/virt-sandbox-image/sources/Source.py @@ -0,0 +1,27 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 Universitat Polit��cnica de Catalunya. +# +# 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.1 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Author: Eren Yagdiran <erenyagdiran@gmail.com> + +from abc import ABCMeta, abstractmethod + +class Source(): + __metaclass__ = ABCMeta + def __init__(self): + pass diff --git a/virt-sandbox-image/sources/__init__.py b/virt-sandbox-image/sources/__init__.py new file mode 100644 index 0000000..df8603b --- /dev/null +++ b/virt-sandbox-image/sources/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 Universitat Polit��cnica de Catalunya. +# +# 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.1 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Author: Eren Yagdiran <erenyagdiran@gmail.com> +# + +import os +import glob +modules = glob.glob(os.path.dirname(__file__)+"/*.py") +__all__ = [ os.path.basename(f)[:-3] for f in modules] diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py old mode 100644 new mode 100755 index a9cb0ff..fa9e1c8 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -1,5 +1,5 @@ #!/usr/bin/python -Es -# +# -*- coding: utf-8 -*- # Authors: Daniel P. Berrange <berrange@redhat.com> # Eren Yagdiran <erenyagdiran@gmail.com> # @@ -32,11 +32,14 @@ import sys import urllib2 import subprocess -default_index_server = "index.docker.io" -default_template_dir = "/var/lib/libvirt/templates" - -debug = True -verbose = True +import importlib +def dynamic_source_loader(name): + name = name[0].upper() + name[1:] + modname = "sources." + name + "Source" + mod = importlib.import_module(modname) + classname = name + "Source" + classimpl = getattr(mod,classname) + return classimpl() gettext.bindtextdomain("libvirt-sandbox", "/usr/share/locale") gettext.textdomain("libvirt-sandbox") -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:31PM +0000, Eren Yagdiran wrote:
Any custom source provider can be added to virt-sandbox-image as a source --- .gitignore | 1 + bin/Makefile.am | 16 ++++++++++++---- bin/virt-sandbox-image.in | 3 +++ configure.ac | 2 ++ virt-sandbox-image/Makefile.am | 13 +++++++++++++ virt-sandbox-image/sources/Source.py | 27 +++++++++++++++++++++++++++ virt-sandbox-image/sources/__init__.py | 26 ++++++++++++++++++++++++++ virt-sandbox-image/virt-sandbox-image.py | 15 +++++++++------ 8 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 bin/virt-sandbox-image.in create mode 100644 virt-sandbox-image/Makefile.am create mode 100644 virt-sandbox-image/sources/Source.py create mode 100644 virt-sandbox-image/sources/__init__.py mode change 100644 => 100755 virt-sandbox-image/virt-sandbox-image.py
ACK Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

On Fri, Aug 28, 2015 at 01:47:31PM +0000, Eren Yagdiran wrote:
Any custom source provider can be added to virt-sandbox-image as a source --- .gitignore | 1 + bin/Makefile.am | 16 ++++++++++++---- bin/virt-sandbox-image.in | 3 +++ configure.ac | 2 ++ virt-sandbox-image/Makefile.am | 13 +++++++++++++ virt-sandbox-image/sources/Source.py | 27 +++++++++++++++++++++++++++ virt-sandbox-image/sources/__init__.py | 26 ++++++++++++++++++++++++++ virt-sandbox-image/virt-sandbox-image.py | 15 +++++++++------ 8 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 bin/virt-sandbox-image.in create mode 100644 virt-sandbox-image/Makefile.am create mode 100644 virt-sandbox-image/sources/Source.py create mode 100644 virt-sandbox-image/sources/__init__.py mode change 100644 => 100755 virt-sandbox-image/virt-sandbox-image.py
diff --git a/.gitignore b/.gitignore index f77ea12..ef5b5aa 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ bin/virt-sandbox bin/virt-sandbox-service-util build/ bin/*.1 +bin/virt-sandbox-image diff --git a/bin/Makefile.am b/bin/Makefile.am index 416f86f..df4c7dc 100644 --- a/bin/Makefile.am +++ b/bin/Makefile.am @@ -3,7 +3,11 @@ bin_PROGRAMS = virt-sandbox
libexec_PROGRAMS = virt-sandbox-service-util
-bin_SCRIPTS = virt-sandbox-service +bin_SCRIPTS = virt-sandbox-service \ + virt-sandbox-image + +virt-sandbox-image: virt-sandbox-image.in + sed -e 's,[@]pkgpythondir[@],$(pkgpythondir),g' < $< >$@
virtsandboxcompdir = $(datarootdir)/bash-completion/completions/
@@ -20,8 +24,11 @@ POD_FILES = \ virt-sandbox-service-reload.pod \ virt-sandbox-service-upgrade.pod \ $(NULL) -EXTRA_DIST = $(bin_SCRIPTS) $(POD_FILES) virt-sandbox-service-bash-completion.sh virt-sandbox-service.logrotate -EXTRA_DIST += virt-sandbox-service-bash-completion.sh +EXTRA_DIST = virt-sandbox-service \ + virt-sandbox-image.in \ + $(POD_FILES) \ + virt-sandbox-service-bash-completion.sh \ + virt-sandbox-service.logrotate
man1_MANS = \ virt-sandbox.1 \ @@ -64,7 +71,8 @@ virt-sandbox-service-reload.1: virt-sandbox-service-reload.pod Makefile virt-sandbox-service-upgrade.1: virt-sandbox-service-upgrade.pod Makefile $(AM_V_GEN)$(POD2MAN) $< $(srcdir)/$@
-CLEANFILES = $(man1_MANS) +CLEANFILES = $(man1_MANS) \ + virt-sandbox-image
virt_sandbox_SOURCES = virt-sandbox.c virt_sandbox_CFLAGS = \ diff --git a/bin/virt-sandbox-image.in b/bin/virt-sandbox-image.in new file mode 100644 index 0000000..732bb38 --- /dev/null +++ b/bin/virt-sandbox-image.in @@ -0,0 +1,3 @@ +#!/bin/sh + +exec "@pkgpythondir@/virt-sandbox-image.py" "$@"
Per my comment on patch 1, we can actually do this in python #!/usr/bin/python # -*- coding: utf-8 -*- from libvirt_sandbox.image import cli import sys if __name__ == '__main__': sys.exit(cli.main()) which nicely avoids the need to have a .in file text substitution.
diff --git a/virt-sandbox-image/sources/Source.py b/virt-sandbox-image/sources/Source.py new file mode 100644 index 0000000..508ca80 --- /dev/null +++ b/virt-sandbox-image/sources/Source.py
I'm thinking these will be best in libvirt-sandbox/image/sources/Source.py so we get a python import module of 'libvirt_sandbox.image.sources.Source" Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Conflicts: virt-sandbox-image/virt-sandbox-image.py --- virt-sandbox-image/virt-sandbox-image.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index fa9e1c8..55aea6a 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -32,6 +32,14 @@ import sys import urllib2 import subprocess +default_privileged_template_dir = "/var/lib/libvirt/templates" +default_home_dir = os.environ['HOME'] +default_unprivileged_template_dir = default_home_dir + "/.local/share/libvirt/templates" +default_privileged_storage_dir = default_privileged_template_dir + "/storage" +default_unprivileged_storage_dir = default_unprivileged_template_dir + "/storage" +debug = False +verbose = False + import importlib def dynamic_source_loader(name): name = name[0].upper() + name[1:] -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:32PM +0000, Eren Yagdiran wrote:
Conflicts: virt-sandbox-image/virt-sandbox-image.py --- virt-sandbox-image/virt-sandbox-image.py | 8 ++++++++ 1 file changed, 8 insertions(+)
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index fa9e1c8..55aea6a 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -32,6 +32,14 @@ import sys import urllib2 import subprocess
+default_privileged_template_dir = "/var/lib/libvirt/templates" +default_home_dir = os.environ['HOME'] +default_unprivileged_template_dir = default_home_dir + "/.local/share/libvirt/templates" +default_privileged_storage_dir = default_privileged_template_dir + "/storage"
This should really be "/var/lib/libvirt/images"
+default_unprivileged_storage_dir = default_unprivileged_template_dir + "/storage"
And default_home_dir + "/.local/share/libvirt/images" Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

--- virt-sandbox-image/virt-sandbox-image.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 55aea6a..9e98bf2 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -40,6 +40,8 @@ default_unprivileged_storage_dir = default_unprivileged_template_dir + "/storage debug = False verbose = False +sys.dont_write_byte_code = True + import importlib def dynamic_source_loader(name): name = name[0].upper() + name[1:] -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:33PM +0000, Eren Yagdiran wrote:
--- virt-sandbox-image/virt-sandbox-image.py | 2 ++ 1 file changed, 2 insertions(+)
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 55aea6a..9e98bf2 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -40,6 +40,8 @@ default_unprivileged_storage_dir = default_unprivileged_template_dir + "/storage debug = False verbose = False
+sys.dont_write_byte_code = True +
As I've said before, do *not* do this. Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

These helper functions are for selecting right directories according to running user privileges --- virt-sandbox-image/virt-sandbox-image.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 9e98bf2..5917dd6 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -32,6 +32,8 @@ import sys import urllib2 import subprocess +template_dir = None +storage_dir = None default_privileged_template_dir = "/var/lib/libvirt/templates" default_home_dir = os.environ['HOME'] default_unprivileged_template_dir = default_home_dir + "/.local/share/libvirt/templates" @@ -40,6 +42,29 @@ default_unprivileged_storage_dir = default_unprivileged_template_dir + "/storage debug = False verbose = False +def check_dir_writable(path): + if not os.access(path,os.W_OK): + return False + return True + +def runtime_dir_resolver(): + global default_privileged_template_dir + global default_privileged_storage_dir + global default_unprivileged_template_dir + global default_unprivileged_storage_dir + global template_dir + global storage_dir + if(check_dir_writable(default_privileged_template_dir)): + template_dir = default_privileged_template_dir + storage_dir = default_privileged_storage_dir + return + template_dir = default_unprivileged_template_dir + storage_dir = default_unprivileged_storage_dir + if not os.path.exists(template_dir): + os.makedirs(template_dir) + if not os.path.exists(storage_dir): + os.makedirs(storage_dir) + sys.dont_write_byte_code = True import importlib @@ -380,8 +405,8 @@ def gen_create_args(subparser): parser.set_defaults(func=create) if __name__ == '__main__': + runtime_dir_resolver() parser = argparse.ArgumentParser(description='Sandbox Container Image Tool') - subparser = parser.add_subparsers(help=_("commands")) gen_download_args(subparser) gen_delete_args(subparser) -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:34PM +0000, Eren Yagdiran wrote:
These helper functions are for selecting right directories according to running user privileges --- virt-sandbox-image/virt-sandbox-image.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 9e98bf2..5917dd6 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -32,6 +32,8 @@ import sys import urllib2 import subprocess
+template_dir = None +storage_dir = None default_privileged_template_dir = "/var/lib/libvirt/templates" default_home_dir = os.environ['HOME'] default_unprivileged_template_dir = default_home_dir + "/.local/share/libvirt/templates" @@ -40,6 +42,29 @@ default_unprivileged_storage_dir = default_unprivileged_template_dir + "/storage debug = False verbose = False
+def check_dir_writable(path): + if not os.access(path,os.W_OK): + return False + return True + +def runtime_dir_resolver(): + global default_privileged_template_dir + global default_privileged_storage_dir + global default_unprivileged_template_dir + global default_unprivileged_storage_dir + global template_dir + global storage_dir + if(check_dir_writable(default_privileged_template_dir)): + template_dir = default_privileged_template_dir + storage_dir = default_privileged_storage_dir + return + template_dir = default_unprivileged_template_dir + storage_dir = default_unprivileged_storage_dir + if not os.path.exists(template_dir): + os.makedirs(template_dir) + if not os.path.exists(storage_dir): + os.makedirs(storage_dir)
This code is called in early startup before we've even decided what command to run. We should not be calling os.makedirs() at this point. eg if the user asks virt-sandbox-image --help they would not expect us to create any files/dirs. We should only create the directories when we actually need to storage some data in them, eg for the 'download' command, but not for the 'delete' command for example.
+ sys.dont_write_byte_code = True
import importlib @@ -380,8 +405,8 @@ def gen_create_args(subparser): parser.set_defaults(func=create)
if __name__ == '__main__': + runtime_dir_resolver() parser = argparse.ArgumentParser(description='Sandbox Container Image Tool') - subparser = parser.add_subparsers(help=_("commands")) gen_download_args(subparser) gen_delete_args(subparser)
Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Refactor download function from virt-sandbox-image to use the newly introduced Source abstract class. The docker-specific download code is moved to a new DockerSource class. --- virt-sandbox-image/Makefile.am | 1 + virt-sandbox-image/sources/DockerSource.py | 214 +++++++++++++++++++++++++++++ virt-sandbox-image/sources/Source.py | 4 + virt-sandbox-image/virt-sandbox-image.py | 204 +++++---------------------- 4 files changed, 251 insertions(+), 172 deletions(-) create mode 100644 virt-sandbox-image/sources/DockerSource.py diff --git a/virt-sandbox-image/Makefile.am b/virt-sandbox-image/Makefile.am index 5ab4d2e..8188c80 100644 --- a/virt-sandbox-image/Makefile.am +++ b/virt-sandbox-image/Makefile.am @@ -8,6 +8,7 @@ install-data-local: $(INSTALL) -m 0755 $(srcdir)/virt-sandbox-image.py $(DESTDIR)$(pkgpythondir) $(INSTALL) -m 0644 $(srcdir)/sources/__init__.py $(DESTDIR)$(pkgpythondir)/sources $(INSTALL) -m 0644 $(srcdir)/sources/Source.py $(DESTDIR)$(pkgpythondir)/sources + $(INSTALL) -m 0644 $(srcdir)/sources/DockerSource.py $(DESTDIR)$(pkgpythondir)/sources uninstall-local: rm -f $(DESTDIR)$(pkgpythondir) diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py new file mode 100644 index 0000000..f3cf5f3 --- /dev/null +++ b/virt-sandbox-image/sources/DockerSource.py @@ -0,0 +1,214 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 Universitat Polit��cnica de Catalunya. +# +# 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.1 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Author: Eren Yagdiran <erenyagdiran@gmail.com> +# + +from Source import Source +import urllib2 +import sys +import json +import traceback +import os +import subprocess +import shutil + +class DockerSource(Source): + + www_auth_username = None + www_auth_password = None + + def __init__(self): + self.default_index_server = "index.docker.io" + + def _check_cert_validate(self): + major = sys.version_info.major + SSL_WARNING = "SSL certificates couldn't be validated by default. You need to have 2.7.9/3.4.3 or higher" + SSL_WARNING +="\nSee https://bugs.python.org/issue22417\n" + py2_7_9_hexversion = 34015728 + py3_4_3_hexversion = 50594800 + if (major == 2 and sys.hexversion < py2_7_9_hexversion) or (major == 3 and sys.hexversion < py3_4_3_hexversion): + sys.stderr.write(SSL_WARNING) + + def download_template(self,**args): + name = args['name'] + registry = args['registry'] if args['registry'] is not None else self.default_index_server + username = args['username'] + password = args['password'] + templatedir = args['templatedir'] + self._download_template(name,registry,username,password,templatedir) + + def _download_template(self,name, server,username,password,destdir): + + if username is not None: + self.www_auth_username = username + self.www_auth_password = password + + self._check_cert_validate() + tag = "latest" + offset = name.find(':') + if offset != -1: + tag = name[offset + 1:] + name = name[0:offset] + try: + (data, res) = self._get_json(server, "/v1/repositories/" + name + "/images", + {"X-Docker-Token": "true"}) + except urllib2.HTTPError, e: + raise ValueError(["Image '%s' does not exist" % name]) + + registryserver = res.info().getheader('X-Docker-Endpoints') + token = res.info().getheader('X-Docker-Token') + checksums = {} + for layer in data: + pass + (data, res) = self._get_json(registryserver, "/v1/repositories/" + name + "/tags", + { "Authorization": "Token " + token }) + + cookie = res.info().getheader('Set-Cookie') + + if not tag in data: + raise ValueError(["Tag '%s' does not exist for image '%s'" % (tag, name)]) + imagetagid = data[tag] + + (data, res) = self._get_json(registryserver, "/v1/images/" + imagetagid + "/ancestry", + { "Authorization": "Token "+token }) + + if data[0] != imagetagid: + raise ValueError(["Expected first layer id '%s' to match image id '%s'", + data[0], imagetagid]) + + try: + createdFiles = [] + createdDirs = [] + + for layerid in data: + templatedir = destdir + "/" + layerid + if not os.path.exists(templatedir): + os.mkdir(templatedir) + createdDirs.append(templatedir) + + jsonfile = templatedir + "/template.json" + datafile = templatedir + "/template.tar.gz" + + if not os.path.exists(jsonfile) or not os.path.exists(datafile): + res = self._save_data(registryserver, "/v1/images/" + layerid + "/json", + { "Authorization": "Token " + token }, jsonfile) + createdFiles.append(jsonfile) + + layersize = int(res.info().getheader("Content-Length")) + + datacsum = None + if layerid in checksums: + datacsum = checksums[layerid] + + self._save_data(registryserver, "/v1/images/" + layerid + "/layer", + { "Authorization": "Token "+token }, datafile, datacsum, layersize) + createdFiles.append(datafile) + + index = { + "name": name, + } + + indexfile = destdir + "/" + imagetagid + "/index.json" + print("Index file " + indexfile) + with open(indexfile, "w") as f: + f.write(json.dumps(index)) + except Exception as e: + traceback.print_exc() + for f in createdFiles: + try: + os.remove(f) + except: + pass + for d in createdDirs: + try: + shutil.rmtree(d) + except: + pass + def _save_data(self,server, path, headers, dest, checksum=None, datalen=None): + try: + res = self._get_url(server, path, headers) + + csum = None + if checksum is not None: + csum = hashlib.sha256() + + pattern = [".", "o", "O", "o"] + patternIndex = 0 + donelen = 0 + + with open(dest, "w") as f: + while 1: + buf = res.read(1024*64) + if not buf: + break + if csum is not None: + csum.update(buf) + f.write(buf) + + if datalen is not None: + donelen = donelen + len(buf) + debug("\x1b[s%s (%5d Kb of %5d Kb)\x1b8" % ( + pattern[patternIndex], (donelen/1024), (datalen/1024) + )) + patternIndex = (patternIndex + 1) % 4 + + debug("\x1b[K") + if csum is not None: + csumstr = "sha256:" + csum.hexdigest() + if csumstr != checksum: + debug("FAIL checksum '%s' does not match '%s'" % (csumstr, checksum)) + os.remove(dest) + raise IOError("Checksum '%s' for data does not match '%s'" % (csumstr, checksum)) + debug("OK\n") + return res + except Exception, e: + debug("FAIL %s\n" % str(e)) + raise + + def _get_url(self,server, path, headers): + url = "https://" + server + path + debug("Fetching %s..." % url) + + req = urllib2.Request(url=url) + if json: + req.add_header("Accept", "application/json") + for h in headers.keys(): + req.add_header(h, headers[h]) + + #www Auth header starts + if self.www_auth_username is not None: + base64string = base64.encodestring('%s:%s' % (self.www_auth_username, self.www_auth_password)).replace('\n', '') + req.add_header("Authorization", "Basic %s" % base64string) + #www Auth header finish + + return urllib2.urlopen(req) + + def _get_json(self,server, path, headers): + try: + res = self._get_url(server, path, headers) + data = json.loads(res.read()) + debug("OK\n") + return (data, res) + except Exception, e: + debug("FAIL %s\n" % str(e)) + raise + +def debug(msg): + sys.stderr.write(msg) diff --git a/virt-sandbox-image/sources/Source.py b/virt-sandbox-image/sources/Source.py index 508ca80..8751689 100644 --- a/virt-sandbox-image/sources/Source.py +++ b/virt-sandbox-image/sources/Source.py @@ -25,3 +25,7 @@ class Source(): __metaclass__ = ABCMeta def __init__(self): pass + + @abstractmethod + def download_template(self,**args): + pass diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 5917dd6..4f371d7 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -94,176 +94,6 @@ def debug(msg): def info(msg): sys.stdout.write(msg) -def get_url(server, path, headers): - url = "https://" + server + path - debug(" Fetching %s..." % url) - req = urllib2.Request(url=url) - - if json: - req.add_header("Accept", "application/json") - - for h in headers.keys(): - req.add_header(h, headers[h]) - - return urllib2.urlopen(req) - -def get_json(server, path, headers): - try: - res = get_url(server, path, headers) - data = json.loads(res.read()) - debug("OK\n") - return (data, res) - except Exception, e: - debug("FAIL %s\n" % str(e)) - raise - -def save_data(server, path, headers, dest, checksum=None, datalen=None): - try: - res = get_url(server, path, headers) - - csum = None - if checksum is not None: - csum = hashlib.sha256() - - pattern = [".", "o", "O", "o"] - patternIndex = 0 - donelen = 0 - - with open(dest, "w") as f: - while 1: - buf = res.read(1024*64) - if not buf: - break - if csum is not None: - csum.update(buf) - f.write(buf) - - if datalen is not None: - donelen = donelen + len(buf) - debug("\x1b[s%s (%5d Kb of %5d Kb)\x1b8" % ( - pattern[patternIndex], (donelen/1024), (datalen/1024) - )) - patternIndex = (patternIndex + 1) % 4 - - debug("\x1b[K") - if csum is not None: - csumstr = "sha256:" + csum.hexdigest() - if csumstr != checksum: - debug("FAIL checksum '%s' does not match '%s'" % (csumstr, checksum)) - os.remove(dest) - raise IOError("Checksum '%s' for data does not match '%s'" % (csumstr, checksum)) - debug("OK\n") - return res - except Exception, e: - debug("FAIL %s\n" % str(e)) - raise - - -def download_template(name, server, destdir): - tag = "latest" - - offset = name.find(':') - if offset != -1: - tag = name[offset + 1:] - name = name[0:offset] - - # First we must ask the index server about the image name. THe - # index server will return an auth token we can use when talking - # to the registry server. We need this token even when anonymous - try: - (data, res) = get_json(server, "/v1/repositories/" + name + "/images", - {"X-Docker-Token": "true"}) - except urllib2.HTTPError, e: - raise ValueError(["Image '%s' does not exist" % name]) - - registryserver = res.info().getheader('X-Docker-Endpoints') - token = res.info().getheader('X-Docker-Token') - checksums = {} - for layer in data: - pass - # XXX Checksums here don't appear to match the data in - # image download later. Find out what these are sums of - #checksums[layer["id"]] = layer["checksum"] - - # Now we ask the registry server for the list of tags associated - # with the image. Tags usually reflect some kind of version of - # the image, but they aren't officially "versions". There is - # always a "latest" tag which is the most recent upload - # - # We must pass in the auth token from the index server. This - # token can only be used once, and we're given a cookie back - # in return to use for later RPC calls. - (data, res) = get_json(registryserver, "/v1/repositories/" + name + "/tags", - { "Authorization": "Token " + token }) - - cookie = res.info().getheader('Set-Cookie') - - if not tag in data: - raise ValueError(["Tag '%s' does not exist for image '%s'" % (tag, name)]) - imagetagid = data[tag] - - # Only base images are self-contained, most images reference one - # or more parents, in a linear stack. Here we are getting the list - # of layers for the image with the tag we used. - (data, res) = get_json(registryserver, "/v1/images/" + imagetagid + "/ancestry", - { "Authorization": "Token " + token }) - - if data[0] != imagetagid: - raise ValueError(["Expected first layer id '%s' to match image id '%s'", - data[0], imagetagid]) - - try: - createdFiles = [] - createdDirs = [] - - for layerid in data: - templatedir = destdir + "/" + layerid - if not os.path.exists(templatedir): - os.mkdir(templatedir) - createdDirs.append(templatedir) - - jsonfile = templatedir + "/template.json" - datafile = templatedir + "/template.tar.gz" - - if not os.path.exists(jsonfile) or not os.path.exists(datafile): - # The '/json' URL gives us some metadata about the layer - res = save_data(registryserver, "/v1/images/" + layerid + "/json", - { "Authorization": "Token " + token }, jsonfile) - createdFiles.append(jsonfile) - layersize = int(res.info().getheader("Content-Length")) - - datacsum = None - if layerid in checksums: - datacsum = checksums[layerid] - - # and the '/layer' URL is the actual payload, provided - # as a tar.gz archive - save_data(registryserver, "/v1/images/" + layerid + "/layer", - { "Authorization": "Token " + token }, datafile, datacsum, layersize) - createdFiles.append(datafile) - - # Strangely the 'json' data for a layer doesn't include - # its actual name, so we save that in a json file of our own - index = { - "name": name, - } - - indexfile = destdir + "/" + imagetagid + "/index.json" - with open(indexfile, "w") as f: - f.write(json.dumps(index)) - except Exception, e: - for f in createdFiles: - try: - os.remove(f) - except: - pass - for d in createdDirs: - try: - os.rmdir(d) - except: - pass - - def delete_template(name, destdir): imageusage = {} imageparent = {} @@ -367,8 +197,16 @@ def create_template(name, imagepath, format, destdir): parentImage = templateImage def download(args): - info("Downloading %s from %s to %s\n" % (args.name, default_index_server, default_template_dir)) - download_template(args.name, default_index_server, default_template_dir) + try: + dynamic_source_loader(args.source).download_template(name=args.name, + registry=args.registry, + username=args.username, + password=args.password, + templatedir=args.template_dir) + except IOError,e: + print "Source %s cannot be found in given path" %args.source + except Exception,e: + print "Download Error %s" % str(e) def delete(args): info("Deleting %s from %s\n" % (args.name, default_template_dir)) @@ -382,10 +220,32 @@ def requires_name(parser): parser.add_argument("name", help=_("name of the template")) +def requires_source(parser): + parser.add_argument("-s","--source", + default="docker", + help=_("name of the template")) + +def requires_auth_conn(parser): + parser.add_argument("-r","--registry", + help=_("Url of the custom registry")) + parser.add_argument("-u","--username", + help=_("Username for the custom registry")) + parser.add_argument("-p","--password", + help=_("Password for the custom registry")) + +def requires_template_dir(parser): + global template_dir + parser.add_argument("-t","--template-dir", + default=template_dir, + help=_("Template directory for saving templates")) + def gen_download_args(subparser): parser = subparser.add_parser("download", help=_("Download template data")) + requires_source(parser) requires_name(parser) + requires_auth_conn(parser) + requires_template_dir(parser) parser.set_defaults(func=download) def gen_delete_args(subparser): -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:35PM +0000, Eren Yagdiran wrote:
Refactor download function from virt-sandbox-image to use the newly introduced Source abstract class. The docker-specific download code is moved to a new DockerSource class. --- virt-sandbox-image/Makefile.am | 1 + virt-sandbox-image/sources/DockerSource.py | 214 +++++++++++++++++++++++++++++ virt-sandbox-image/sources/Source.py | 4 + virt-sandbox-image/virt-sandbox-image.py | 204 +++++---------------------- 4 files changed, 251 insertions(+), 172 deletions(-) create mode 100644 virt-sandbox-image/sources/DockerSource.py diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py new file mode 100644 index 0000000..f3cf5f3 --- /dev/null +++ b/virt-sandbox-image/sources/DockerSource.py + + def download_template(self,**args): + name = args['name'] + registry = args['registry'] if args['registry'] is not None else self.default_index_server + username = args['username'] + password = args['password'] + templatedir = args['templatedir'] + self._download_template(name,registry,username,password,templatedir)
Using '**args' is a bit of a wierd way todo named parameters in python. We should jsut list the named parameters explicitly as you've done in the folloiwing _download_template method. In fact we don't need separate download_template and _download_template methods at all.
+ def _download_template(self,name, server,username,password,destdir): +
diff --git a/virt-sandbox-image/sources/Source.py b/virt-sandbox-image/sources/Source.py index 508ca80..8751689 100644 --- a/virt-sandbox-image/sources/Source.py +++ b/virt-sandbox-image/sources/Source.py @@ -25,3 +25,7 @@ class Source(): __metaclass__ = ABCMeta def __init__(self): pass + + @abstractmethod + def download_template(self,**args): + pass
We actually want to list the named parameters here, as the base class is defining the API contract that the subclass must satisfy. It is also worth having python docs inline. Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Move the docker-related code to the DockerSource and use the Source mechanism --- virt-sandbox-image/sources/DockerSource.py | 100 +++++++++++++++++++++++++++++ virt-sandbox-image/sources/Source.py | 4 ++ virt-sandbox-image/virt-sandbox-image.py | 76 +++++----------------- 3 files changed, 121 insertions(+), 59 deletions(-) diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py index f3cf5f3..09eea85 100644 --- a/virt-sandbox-image/sources/DockerSource.py +++ b/virt-sandbox-image/sources/DockerSource.py @@ -210,5 +210,105 @@ class DockerSource(Source): debug("FAIL %s\n" % str(e)) raise + def create_template(self,**args): + name = args['name'] + connect = args['connect'] + templatedir = args['templatedir'] + format = args['format'] + format = format if format is not None else self.default_disk_format + + self._create_template(name, + connect, + templatedir, + format) + + def _create_template(self,name,connect,templatedir,format): + self._check_disk_format(format) + imagelist = self._get_image_list(name,templatedir) + imagelist.reverse() + + parentImage = None + for imagetagid in imagelist: + templateImage = templatedir + "/" + imagetagid + "/template." + format + cmd = ["qemu-img","create","-f","qcow2"] + if parentImage is not None: + cmd.append("-o") + cmd.append("backing_fmt=qcow2,backing_file=%s" % parentImage) + cmd.append(templateImage) + if parentImage is None: + cmd.append("10G") + subprocess.call(cmd) + + if parentImage is None: + self._format_disk(templateImage,format,connect) + + self._extract_tarballs(templatedir + "/" + imagetagid + "/template.",format,connect) + parentImage = templateImage + + + def _check_disk_format(self,format): + supportedFormats = ['qcow2'] + if not format in supportedFormats: + raise ValueError(["Unsupported image format %s" % format]) + + def _get_image_list(self,name,destdir): + imageparent = {} + imagenames = {} + imagedirs = os.listdir(destdir) + for imagetagid in imagedirs: + indexfile = destdir + "/" + imagetagid + "/index.json" + if os.path.exists(indexfile): + with open(indexfile,"r") as f: + index = json.load(f) + imagenames[index["name"]] = imagetagid + jsonfile = destdir + "/" + imagetagid + "/template.json" + if os.path.exists(jsonfile): + with open(jsonfile,"r") as f: + template = json.load(f) + parent = template.get("parent",None) + if parent: + imageparent[imagetagid] = parent + if not name in imagenames: + raise ValueError(["Image %s does not exist locally" %name]) + imagetagid = imagenames[name] + imagelist = [] + while imagetagid != None: + imagelist.append(imagetagid) + parent = imageparent.get(imagetagid,None) + imagetagid = parent + return imagelist + + def _format_disk(self,disk,format,connect): + cmd = ['virt-sandbox'] + if connect is not None: + cmd.append("-c") + cmd.append(connect) + cmd.append("-p") + params = ['--disk=file:disk_image=%s,format=%s' %(disk,format), + '/sbin/mkfs.ext3', + '/dev/disk/by-tag/disk_image'] + cmd = cmd + params + subprocess.call(cmd) + + def _extract_tarballs(self,directory,format,connect): + tempdir = "/mnt" + tarfile = directory + "tar.gz" + diskfile = directory + "qcow2" + cmd = ['virt-sandbox'] + if connect is not None: + cmd.append("-c") + cmd.append(connect) + cmd.append("-p") + params = ['-m', + 'host-image:/mnt=%s,format=%s' %(diskfile,format), + '--', + '/bin/tar', + 'zxf', + '%s' %tarfile, + '-C', + '/mnt'] + cmd = cmd + params + subprocess.call(cmd) + def debug(msg): sys.stderr.write(msg) diff --git a/virt-sandbox-image/sources/Source.py b/virt-sandbox-image/sources/Source.py index 8751689..6ac08dc 100644 --- a/virt-sandbox-image/sources/Source.py +++ b/virt-sandbox-image/sources/Source.py @@ -29,3 +29,7 @@ class Source(): @abstractmethod def download_template(self,**args): pass + + @abstractmethod + def create_template(self,**args): + pass diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 4f371d7..745401c 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -143,59 +143,6 @@ def delete_template(name, destdir): parent = None imagetagid = parent - -def get_image_list(name, destdir): - imageparent = {} - imagenames = {} - imagedirs = os.listdir(destdir) - for imagetagid in imagedirs: - indexfile = destdir + "/" + imagetagid + "/index.json" - if os.path.exists(indexfile): - with open(indexfile, "r") as f: - index = json.load(f) - imagenames[index["name"]] = imagetagid - jsonfile = destdir + "/" + imagetagid + "/template.json" - if os.path.exists(jsonfile): - with open(jsonfile, "r") as f: - template = json.load(f) - - parent = template.get("parent", None) - if parent: - imageparent[imagetagid] = parent - - if not name in imagenames: - raise ValueError(["Image %s does not exist locally" % name]) - - imagetagid = imagenames[name] - imagelist = [] - while imagetagid != None: - imagelist.append(imagetagid) - parent = imageparent.get(imagetagid, None) - imagetagid = parent - - return imagelist - -def create_template(name, imagepath, format, destdir): - if not format in ["qcow2"]: - raise ValueError(["Unsupported image format %s" % format]) - - imagelist = get_image_list(name, destdir) - imagelist.reverse() - - parentImage = None - for imagetagid in imagelist: - templateImage = destdir + "/" + imagetagid + "/template." + format - cmd = ["qemu-img", "create", "-f", "qcow2"] - if parentImage is not None: - cmd.append("-o") - cmd.append("backing_fmt=qcow2,backing_file=%s" % parentImage) - cmd.append(templateImage) - if parentImage is None: - cmd.append("10G") - debug("Run %s\n" % " ".join(cmd)) - subprocess.call(cmd) - parentImage = templateImage - def download(args): try: dynamic_source_loader(args.source).download_template(name=args.name, @@ -213,8 +160,13 @@ def delete(args): delete_template(args.name, default_template_dir) def create(args): - info("Creating %s from %s in format %s\n" % (args.imagepath, args.name, args.format)) - create_template(args.name, args.imagepath, args.format, default_template_dir) + try: + dynamic_source_loader(args.source).create_template(name=args.name, + connect=args.connect, + templatedir=args.template_dir, + format=args.format) + except Exception,e: + print "Create Error %s" % str(e) def requires_name(parser): parser.add_argument("name", @@ -225,6 +177,10 @@ def requires_source(parser): default="docker", help=_("name of the template")) +def requires_connect(parser): + parser.add_argument("-c","--connect", + help=_("Connect string for libvirt")) + def requires_auth_conn(parser): parser.add_argument("-r","--registry", help=_("Url of the custom registry")) @@ -258,10 +214,12 @@ def gen_create_args(subparser): parser = subparser.add_parser("create", help=_("Create image from template data")) requires_name(parser) - parser.add_argument("imagepath", - help=_("path for image")) - parser.add_argument("format", - help=_("format")) + requires_source(parser) + requires_connect(parser) + requires_template_dir(parser) + parser.add_argument("-f","--format", + default="qcow2", + help=_("format format for image")) parser.set_defaults(func=create) if __name__ == '__main__': -- 2.1.0

Refactoring delete function from virt-sandbox-image to DockerSource. Delete function can delete templates by name. --- virt-sandbox-image/sources/DockerSource.py | 53 +++++++++++++++++++++++++++ virt-sandbox-image/sources/Source.py | 4 +++ virt-sandbox-image/virt-sandbox-image.py | 58 ++++-------------------------- 3 files changed, 64 insertions(+), 51 deletions(-) diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py index 09eea85..760ba6c 100644 --- a/virt-sandbox-image/sources/DockerSource.py +++ b/virt-sandbox-image/sources/DockerSource.py @@ -310,5 +310,58 @@ class DockerSource(Source): cmd = cmd + params subprocess.call(cmd) + def delete_template(self,**args): + imageusage = {} + imageparent = {} + imagenames = {} + name = args['name'] + destdir = args['templatedir'] + destdir = destdir if destdir is not None else default_template_dir + imagedirs = os.listdir(destdir) + for imagetagid in imagedirs: + indexfile = destdir + "/" + imagetagid + "/index.json" + if os.path.exists(indexfile): + with open(indexfile,"r") as f: + index = json.load(f) + imagenames[index["name"]] = imagetagid + jsonfile = destdir + "/" + imagetagid + "/template.json" + if os.path.exists(jsonfile): + with open(jsonfile,"r") as f: + template = json.load(f) + + parent = template.get("parent",None) + if parent: + if parent not in imageusage: + imageusage[parent] = [] + imageusage[parent].append(imagetagid) + imageparent[imagetagid] = parent + + + if not name in imagenames: + raise ValueError(["Image %s does not exist locally" %name]) + + imagetagid = imagenames[name] + while imagetagid != None: + debug("Remove %s\n" % imagetagid) + parent = imageparent.get(imagetagid,None) + + indexfile = destdir + "/" + imagetagid + "/index.json" + if os.path.exists(indexfile): + os.remove(indexfile) + jsonfile = destdir + "/" + imagetagid + "/template.json" + if os.path.exists(jsonfile): + os.remove(jsonfile) + datafile = destdir + "/" + imagetagid + "/template.tar.gz" + if os.path.exists(datafile): + os.remove(datafile) + imagedir = destdir + "/" + imagetagid + shutil.rmtree(imagedir) + + if parent: + if len(imageusage[parent]) != 1: + debug("Parent %s is shared\n" % parent) + parent = None + imagetagid = parent + def debug(msg): sys.stderr.write(msg) diff --git a/virt-sandbox-image/sources/Source.py b/virt-sandbox-image/sources/Source.py index 6ac08dc..d66e61e 100644 --- a/virt-sandbox-image/sources/Source.py +++ b/virt-sandbox-image/sources/Source.py @@ -33,3 +33,7 @@ class Source(): @abstractmethod def create_template(self,**args): pass + + @abstractmethod + def delete_template(self,**args): + pass diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 745401c..1da5150 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -94,55 +94,6 @@ def debug(msg): def info(msg): sys.stdout.write(msg) -def delete_template(name, destdir): - imageusage = {} - imageparent = {} - imagenames = {} - imagedirs = os.listdir(destdir) - for imagetagid in imagedirs: - indexfile = destdir + "/" + imagetagid + "/index.json" - if os.path.exists(indexfile): - with open(indexfile, "r") as f: - index = json.load(f) - imagenames[index["name"]] = imagetagid - jsonfile = destdir + "/" + imagetagid + "/template.json" - if os.path.exists(jsonfile): - with open(jsonfile, "r") as f: - template = json.load(f) - - parent = template.get("parent", None) - if parent: - if parent not in imageusage: - imageusage[parent] = [] - imageusage[parent].append(imagetagid) - imageparent[imagetagid] = parent - - if not name in imagenames: - raise ValueError(["Image %s does not exist locally" % name]) - - imagetagid = imagenames[name] - while imagetagid != None: - debug("Remove %s\n" % imagetagid) - parent = imageparent.get(imagetagid, None) - - indexfile = destdir + "/" + imagetagid + "/index.json" - if os.path.exists(indexfile): - os.remove(indexfile) - jsonfile = destdir + "/" + imagetagid + "/template.json" - if os.path.exists(jsonfile): - os.remove(jsonfile) - datafile = destdir + "/" + imagetagid + "/template.tar.gz" - if os.path.exists(datafile): - os.remove(datafile) - imagedir = destdir + "/" + imagetagid - os.rmdir(imagedir) - - if parent: - if len(imageusage[parent]) != 1: - debug("Parent %s is shared\n" % parent) - parent = None - imagetagid = parent - def download(args): try: dynamic_source_loader(args.source).download_template(name=args.name, @@ -156,8 +107,11 @@ def download(args): print "Download Error %s" % str(e) def delete(args): - info("Deleting %s from %s\n" % (args.name, default_template_dir)) - delete_template(args.name, default_template_dir) + try: + dynamic_source_loader(args.source).delete_template(name=args.name, + templatedir=args.template_dir) + except Exception,e: + print "Delete Error %s", str(e) def create(args): try: @@ -208,6 +162,8 @@ def gen_delete_args(subparser): parser = subparser.add_parser("delete", help=_("Delete template data")) requires_name(parser) + requires_source(parser) + requires_template_dir(parser) parser.set_defaults(func=delete) def gen_create_args(subparser): -- 2.1.0

Provide a way to know how a template can be started depending on the used source DockerSource will need to parse the topmost config file in order to find the igniter command --- virt-sandbox-image/sources/DockerSource.py | 14 ++++++++++++++ virt-sandbox-image/sources/Source.py | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py index 760ba6c..3e0362b 100644 --- a/virt-sandbox-image/sources/DockerSource.py +++ b/virt-sandbox-image/sources/DockerSource.py @@ -29,6 +29,15 @@ import os import subprocess import shutil +class DockerConfParser(): + + def __init__(self,jsonfile): + with open(jsonfile) as json_file: + self.json_data = json.load(json_file) + def getRunCommand(self): + cmd = self.json_data['container_config']['Cmd'][2] + return cmd[cmd.index('"') + 1:cmd.rindex('"')] + class DockerSource(Source): www_auth_username = None @@ -363,5 +372,10 @@ class DockerSource(Source): parent = None imagetagid = parent + def get_command(self,configfile): + configParser = DockerConfParser(configfile) + commandToRun = configParser.getRunCommand() + return commandToRun + def debug(msg): sys.stderr.write(msg) diff --git a/virt-sandbox-image/sources/Source.py b/virt-sandbox-image/sources/Source.py index d66e61e..9daf62d 100644 --- a/virt-sandbox-image/sources/Source.py +++ b/virt-sandbox-image/sources/Source.py @@ -37,3 +37,7 @@ class Source(): @abstractmethod def delete_template(self,**args): pass + + @abstractmethod + def get_command(self,**args): + pass -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:38PM +0000, Eren Yagdiran wrote:
Provide a way to know how a template can be started depending on the used source DockerSource will need to parse the topmost config file in order to find the igniter command --- virt-sandbox-image/sources/DockerSource.py | 14 ++++++++++++++ virt-sandbox-image/sources/Source.py | 4 ++++ 2 files changed, 18 insertions(+)
diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py index 760ba6c..3e0362b 100644 --- a/virt-sandbox-image/sources/DockerSource.py +++ b/virt-sandbox-image/sources/DockerSource.py @@ -29,6 +29,15 @@ import os import subprocess import shutil
+class DockerConfParser(): + + def __init__(self,jsonfile): + with open(jsonfile) as json_file: + self.json_data = json.load(json_file) + def getRunCommand(self): + cmd = self.json_data['container_config']['Cmd'][2] + return cmd[cmd.index('"') + 1:cmd.rindex('"')]
In testing, I found out we should be using 'config' here not 'container_config', since the latter refers to the configuration used when the image was built, which is subtly different from the config that is intended to be used when in it deployed. In particular the 'Cmd' field does not need this string munging when we use 'config' instead of 'container_config'. In researching this, I found out that docker also has an optional 'Entrypoints' array and if both Entrypoints and Cmd are specified, we should concatenate them. If the user provides command line args, then should replace the 'Cmd' list, but not the 'Entrypoints' list. Finally some images don'pt define any 'Cmd' or 'Entrypoints' at all, in which case it seems we should just try using /bin/sh, in the absence of any user supplied command. Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

On Tue, 2015-09-08 at 17:21 +0100, Daniel P. Berrange wrote:
On Fri, Aug 28, 2015 at 01:47:38PM +0000, Eren Yagdiran wrote:
Provide a way to know how a template can be started depending on the used source DockerSource will need to parse the topmost config file in order to find the igniter command --- virt-sandbox-image/sources/DockerSource.py | 14 ++++++++++++++ virt-sandbox-image/sources/Source.py | 4 ++++ 2 files changed, 18 insertions(+)
diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py index 760ba6c..3e0362b 100644 --- a/virt-sandbox-image/sources/DockerSource.py +++ b/virt-sandbox-image/sources/DockerSource.py @@ -29,6 +29,15 @@ import os import subprocess import shutil
+class DockerConfParser(): + + def __init__(self,jsonfile): + with open(jsonfile) as json_file: + self.json_data = json.load(json_file) + def getRunCommand(self): + cmd = self.json_data['container_config']['Cmd'][2] + return cmd[cmd.index('"') + 1:cmd.rindex('"')]
In testing, I found out we should be using 'config' here not 'container_config', since the latter refers to the configuration used when the image was built, which is subtly different from the config that is intended to be used when in it deployed.
In particular the 'Cmd' field does not need this string munging when we use 'config' instead of 'container_config'.
In researching this, I found out that docker also has an optional 'Entrypoints' array and if both Entrypoints and Cmd are specified, we should concatenate them. If the user provides command line args, then should replace the 'Cmd' list, but not the 'Entrypoints' list. Finally some images don'pt define any 'Cmd' or 'Entrypoints' at all, in which case it seems we should just try using /bin/sh, in the absence of any user supplied command.
Do we really want to have a default to /bin/sh when docker is giving an error to the user? Error response from daemon: No command specified -- Cedric
Regards, Daniel

On Wed, Sep 09, 2015 at 09:43:35AM +0200, Cedric Bosdonnat wrote:
On Tue, 2015-09-08 at 17:21 +0100, Daniel P. Berrange wrote:
On Fri, Aug 28, 2015 at 01:47:38PM +0000, Eren Yagdiran wrote:
Provide a way to know how a template can be started depending on the used source DockerSource will need to parse the topmost config file in order to find the igniter command --- virt-sandbox-image/sources/DockerSource.py | 14 ++++++++++++++ virt-sandbox-image/sources/Source.py | 4 ++++ 2 files changed, 18 insertions(+)
diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py index 760ba6c..3e0362b 100644 --- a/virt-sandbox-image/sources/DockerSource.py +++ b/virt-sandbox-image/sources/DockerSource.py @@ -29,6 +29,15 @@ import os import subprocess import shutil
+class DockerConfParser(): + + def __init__(self,jsonfile): + with open(jsonfile) as json_file: + self.json_data = json.load(json_file) + def getRunCommand(self): + cmd = self.json_data['container_config']['Cmd'][2] + return cmd[cmd.index('"') + 1:cmd.rindex('"')]
In testing, I found out we should be using 'config' here not 'container_config', since the latter refers to the configuration used when the image was built, which is subtly different from the config that is intended to be used when in it deployed.
In particular the 'Cmd' field does not need this string munging when we use 'config' instead of 'container_config'.
In researching this, I found out that docker also has an optional 'Entrypoints' array and if both Entrypoints and Cmd are specified, we should concatenate them. If the user provides command line args, then should replace the 'Cmd' list, but not the 'Entrypoints' list. Finally some images don'pt define any 'Cmd' or 'Entrypoints' at all, in which case it seems we should just try using /bin/sh, in the absence of any user supplied command.
Do we really want to have a default to /bin/sh when docker is giving an error to the user?
Yeah, I'm not sure really.
Error response from daemon: No command specified
I guess the error is nicer in the csae where the user does not have a shell inside their image Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Commandline parameters for running a template --- virt-sandbox-image/virt-sandbox-image.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 1da5150..d6b682f 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -178,6 +178,18 @@ def gen_create_args(subparser): help=_("format format for image")) parser.set_defaults(func=create) +def gen_run_args(subparser): + parser = subparser.add_parser("run", + help=_("Run a already built image")) + requires_name(parser) + requires_source(parser) + requires_connect(parser) + parser.add_argument("-t","--template-dir", + help=_("Template directory for saving templates")) + parser.add_argument("-i","--igniter", + help=_("Igniter command for image")) + parser.set_defaults(func=run) + if __name__ == '__main__': runtime_dir_resolver() parser = argparse.ArgumentParser(description='Sandbox Container Image Tool') @@ -185,6 +197,7 @@ if __name__ == '__main__': gen_download_args(subparser) gen_delete_args(subparser) gen_create_args(subparser) + gen_run_args(subparser) try: args = parser.parse_args() -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:39PM +0000, Eren Yagdiran wrote:
Commandline parameters for running a template --- virt-sandbox-image/virt-sandbox-image.py | 13 +++++++++++++ 1 file changed, 13 insertions(+)
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 1da5150..d6b682f 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -178,6 +178,18 @@ def gen_create_args(subparser): help=_("format format for image")) parser.set_defaults(func=create)
+def gen_run_args(subparser): + parser = subparser.add_parser("run", + help=_("Run a already built image")) + requires_name(parser) + requires_source(parser) + requires_connect(parser) + parser.add_argument("-t","--template-dir", + help=_("Template directory for saving templates"))
This should have been requires_template(parser) otherwise we miss out the default value setting Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

On Fri, Aug 28, 2015 at 01:47:39PM +0000, Eren Yagdiran wrote:
Commandline parameters for running a template --- virt-sandbox-image/virt-sandbox-image.py | 13 +++++++++++++ 1 file changed, 13 insertions(+)
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 1da5150..d6b682f 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -178,6 +178,18 @@ def gen_create_args(subparser): help=_("format format for image")) parser.set_defaults(func=create)
+def gen_run_args(subparser): + parser = subparser.add_parser("run", + help=_("Run a already built image")) + requires_name(parser) + requires_source(parser) + requires_connect(parser) + parser.add_argument("-t","--template-dir", + help=_("Template directory for saving templates")) + parser.add_argument("-i","--igniter", + help=_("Igniter command for image"))
Rather than having an --igniter arg, which only allows a single binary name, we should just allow an arbitrary number of positional args, so the user can pass full arg lists, eg virt-sandbox-image run ubuntu /bin/somefile somearg otherarg .... Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Check if user-specified connect argument is valid --- virt-sandbox-image/virt-sandbox-image.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index d6b682f..c46abd4 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -122,6 +122,12 @@ def create(args): except Exception,e: print "Create Error %s" % str(e) +def check_connect(connectstr): + supportedDrivers = ['lxc:///','qemu:///session','qemu:///system'] + if not connectstr in supportedDrivers: + raise ValueError("%s is not supported by Virt-sandbox" %connectstr) + return True + def requires_name(parser): parser.add_argument("name", help=_("name of the template")) -- 2.1.0

Provide a way to know which disk image to use for the sandbox depending on the used source DockerSource will need to locate the topmost disk image among all the layers images --- virt-sandbox-image/sources/DockerSource.py | 16 ++++++++++++++++ virt-sandbox-image/sources/Source.py | 4 ++++ virt-sandbox-image/virt-sandbox-image.py | 9 +++++++++ 3 files changed, 29 insertions(+) diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py index 3e0362b..87fbcf3 100644 --- a/virt-sandbox-image/sources/DockerSource.py +++ b/virt-sandbox-image/sources/DockerSource.py @@ -372,6 +372,22 @@ class DockerSource(Source): parent = None imagetagid = parent + def get_disk(self,**args): + name = args['name'] + destdir = args['templatedir'] + sandboxid = args['id'] + imageList = self._get_image_list(name,destdir) + toplayer = imageList[0] + diskfile = destdir + "/" + toplayer + "/template.qcow2" + configfile = destdir + "/" + toplayer + "/template.json" + tempfile = destdir + "/" + toplayer + "/" + sandboxid + ".qcow2" + cmd = ["qemu-img","create","-q","-f","qcow2"] + cmd.append("-o") + cmd.append("backing_fmt=qcow2,backing_file=%s" % diskfile) + cmd.append(tempfile) + subprocess.call(cmd) + return (tempfile,configfile) + def get_command(self,configfile): configParser = DockerConfParser(configfile) commandToRun = configParser.getRunCommand() diff --git a/virt-sandbox-image/sources/Source.py b/virt-sandbox-image/sources/Source.py index 9daf62d..9a3da59 100644 --- a/virt-sandbox-image/sources/Source.py +++ b/virt-sandbox-image/sources/Source.py @@ -41,3 +41,7 @@ class Source(): @abstractmethod def get_command(self,**args): pass + + @abstractmethod + def get_disk(self,**args): + pass diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index c46abd4..a73619c 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -31,6 +31,8 @@ import shutil import sys import urllib2 import subprocess +import random +import string template_dir = None storage_dir = None @@ -128,6 +130,12 @@ def check_connect(connectstr): raise ValueError("%s is not supported by Virt-sandbox" %connectstr) return True +def requires_id(parser): + randomid = ''.join(random.choice(string.lowercase) for i in range(10)) + parser.add_argument("-d","--id", + default=randomid, + help=_("id of the running sandbox")) + def requires_name(parser): parser.add_argument("name", help=_("name of the template")) @@ -187,6 +195,7 @@ def gen_create_args(subparser): def gen_run_args(subparser): parser = subparser.add_parser("run", help=_("Run a already built image")) + requires_id(parser) requires_name(parser) requires_source(parser) requires_connect(parser) -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:41PM +0000, Eren Yagdiran wrote:
Provide a way to know which disk image to use for the sandbox depending on the used source DockerSource will need to locate the topmost disk image among all the layers images --- virt-sandbox-image/sources/DockerSource.py | 16 ++++++++++++++++ virt-sandbox-image/sources/Source.py | 4 ++++ virt-sandbox-image/virt-sandbox-image.py | 9 +++++++++ 3 files changed, 29 insertions(+)
diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py index 3e0362b..87fbcf3 100644 --- a/virt-sandbox-image/sources/DockerSource.py +++ b/virt-sandbox-image/sources/DockerSource.py @@ -372,6 +372,22 @@ class DockerSource(Source): parent = None imagetagid = parent
+ def get_disk(self,**args): + name = args['name'] + destdir = args['templatedir'] + sandboxid = args['id'] + imageList = self._get_image_list(name,destdir) + toplayer = imageList[0] + diskfile = destdir + "/" + toplayer + "/template.qcow2" + configfile = destdir + "/" + toplayer + "/template.json" + tempfile = destdir + "/" + toplayer + "/" + sandboxid + ".qcow2"
This is storing the per-sandbox image file inside the template directory which is not what we want. We should be using the storage_dir instead of template_dir for the per instance image.
+ cmd = ["qemu-img","create","-q","-f","qcow2"] + cmd.append("-o") + cmd.append("backing_fmt=qcow2,backing_file=%s" % diskfile) + cmd.append(tempfile) + subprocess.call(cmd) + return (tempfile,configfile) + def get_command(self,configfile): configParser = DockerConfParser(configfile) commandToRun = configParser.getRunCommand()
+def requires_id(parser): + randomid = ''.join(random.choice(string.lowercase) for i in range(10)) + parser.add_argument("-d","--id", + default=randomid, + help=_("id of the running sandbox"))
This is currently just used to form the per-instance image filename. We should also use this for the libvirt guest name too. So I'd suggest we just call this '-n' / '--name' to match the virt-sandbox arg syntax and have a prefix on it eg sandboxXXXXXXX where XXXXX is the random suffix Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Run an already-built template If there is no execution command specified by user, source.get_command will find the command to invoke --- virt-sandbox-image/virt-sandbox-image.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index a73619c..c182874 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -130,6 +130,31 @@ def check_connect(connectstr): raise ValueError("%s is not supported by Virt-sandbox" %connectstr) return True +def run(args): + try: + if args.connect is not None: + check_connect(args.connect) + source = dynamic_source_loader(args.source) + diskfile,configfile = source.get_disk(name=args.name,templatedir=args.template_dir,id=args.id) + + format = "qcow2" + commandToRun = args.igniter + if commandToRun is None: + commandToRun = source.get_command(configfile) + cmd = ['virt-sandbox'] + if args.connect is not None: + cmd.append("-c") + cmd.append(args.connect) + params = ['-m','host-image:/=%s,format=%s' %(diskfile,format), + '--', + commandToRun] + cmd = cmd + params + subprocess.call(cmd) + subprocess.call(["rm", "-rf", diskfile]) + + except Exception,e: + print "Run Error %s" % str(e) + def requires_id(parser): randomid = ''.join(random.choice(string.lowercase) for i in range(10)) parser.add_argument("-d","--id", -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:42PM +0000, Eren Yagdiran wrote:
Run an already-built template If there is no execution command specified by user, source.get_command will find the command to invoke --- virt-sandbox-image/virt-sandbox-image.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+)
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index a73619c..c182874 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -130,6 +130,31 @@ def check_connect(connectstr): raise ValueError("%s is not supported by Virt-sandbox" %connectstr) return True
+def run(args): + try: + if args.connect is not None: + check_connect(args.connect) + source = dynamic_source_loader(args.source) + diskfile,configfile = source.get_disk(name=args.name,templatedir=args.template_dir,id=args.id) + + format = "qcow2" + commandToRun = args.igniter + if commandToRun is None: + commandToRun = source.get_command(configfile) + cmd = ['virt-sandbox'] + if args.connect is not None: + cmd.append("-c") + cmd.append(args.connect)
We must also pass '-n' to give the instance name, otherwise we'll never be able to run more than one docker image at once
+ params = ['-m','host-image:/=%s,format=%s' %(diskfile,format), + '--', + commandToRun] + cmd = cmd + params + subprocess.call(cmd) + subprocess.call(["rm", "-rf", diskfile])
We should just use os.unlink(diskfile) - no need to shell out to the rm command to just delete a single file Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Virt-sandbox-image will pass exact network arguments to virt-sandbox --- virt-sandbox-image/virt-sandbox-image.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index c182874..058738a 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -145,9 +145,13 @@ def run(args): if args.connect is not None: cmd.append("-c") cmd.append(args.connect) - params = ['-m','host-image:/=%s,format=%s' %(diskfile,format), - '--', - commandToRun] + params = ['-m','host-image:/=%s,format=%s' %(diskfile,format)] + networkArgs = args.network + if networkArgs is not None: + params.append('-N') + params.append(networkArgs) + params.append('--') + params.append(commandToRun) cmd = cmd + params subprocess.call(cmd) subprocess.call(["rm", "-rf", diskfile]) @@ -228,6 +232,8 @@ def gen_run_args(subparser): help=_("Template directory for saving templates")) parser.add_argument("-i","--igniter", help=_("Igniter command for image")) + parser.add_argument("-n","--network", + help=_("Network params for running template")) parser.set_defaults(func=run) if __name__ == '__main__': -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:43PM +0000, Eren Yagdiran wrote:
Virt-sandbox-image will pass exact network arguments to virt-sandbox --- virt-sandbox-image/virt-sandbox-image.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index c182874..058738a 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -145,9 +145,13 @@ def run(args): if args.connect is not None: cmd.append("-c") cmd.append(args.connect) - params = ['-m','host-image:/=%s,format=%s' %(diskfile,format), - '--', - commandToRun] + params = ['-m','host-image:/=%s,format=%s' %(diskfile,format)] + networkArgs = args.network + if networkArgs is not None: + params.append('-N') + params.append(networkArgs) + params.append('--') + params.append(commandToRun) cmd = cmd + params subprocess.call(cmd) subprocess.call(["rm", "-rf", diskfile]) @@ -228,6 +232,8 @@ def gen_run_args(subparser): help=_("Template directory for saving templates")) parser.add_argument("-i","--igniter", help=_("Igniter command for image")) + parser.add_argument("-n","--network", + help=_("Network params for running template"))
We should use -N here to match virt-sandbox arg syntax, as -n is used for shorthand of --name, which we also need to support in virt-sandbox-image Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Volumes let user to map host-paths into sandbox. Docker containers need volumes for data persistence. --- virt-sandbox-image/sources/DockerSource.py | 12 ++++++++++++ virt-sandbox-image/sources/Source.py | 4 ++++ virt-sandbox-image/virt-sandbox-image.py | 22 ++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py index 87fbcf3..1022107 100644 --- a/virt-sandbox-image/sources/DockerSource.py +++ b/virt-sandbox-image/sources/DockerSource.py @@ -28,6 +28,7 @@ import traceback import os import subprocess import shutil +import collections class DockerConfParser(): @@ -37,6 +38,13 @@ class DockerConfParser(): def getRunCommand(self): cmd = self.json_data['container_config']['Cmd'][2] return cmd[cmd.index('"') + 1:cmd.rindex('"')] + def getVolumes(self): + volumes = self.json_data['container_config']['Volumes'] + volumelist = [] + if isinstance(volumes,collections.Iterable): + for key,value in volumes.iteritems(): + volumelist.append(key) + return volumelist class DockerSource(Source): @@ -393,5 +401,9 @@ class DockerSource(Source): commandToRun = configParser.getRunCommand() return commandToRun + def get_volume(self,configfile): + configParser = DockerConfParser(configfile) + return configParser.getVolumes() + def debug(msg): sys.stderr.write(msg) diff --git a/virt-sandbox-image/sources/Source.py b/virt-sandbox-image/sources/Source.py index 9a3da59..8cc508e 100644 --- a/virt-sandbox-image/sources/Source.py +++ b/virt-sandbox-image/sources/Source.py @@ -45,3 +45,7 @@ class Source(): @abstractmethod def get_disk(self,**args): pass + + @abstractmethod + def get_volume(self,**args): + pass diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 058738a..79f8d8c 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -132,6 +132,7 @@ def check_connect(connectstr): def run(args): try: + global storage_dir if args.connect is not None: check_connect(args.connect) source = dynamic_source_loader(args.source) @@ -150,6 +151,25 @@ def run(args): if networkArgs is not None: params.append('-N') params.append(networkArgs) + allVolumes = source.get_volume(configfile) + volumeArgs = args.volume + if volumeArgs is not None: + allVolumes = allVolumes + volumeArgs + for volume in allVolumes: + volumeSplit = volume.split(":") + volumelen = len(volumeSplit) + if volumelen == 2: + hostPath = volumeSplit[0] + guestPath = volumeSplit[1] + elif volumelen == 1: + guestPath = volumeSplit[0] + hostPath = storage_dir + guestPath + if not os.path.exists(hostPath): + os.makedirs(hostPath) + else: + pass + params.append("--mount") + params.append("host-bind:%s=%s" %(guestPath,hostPath)) params.append('--') params.append(commandToRun) cmd = cmd + params @@ -234,6 +254,8 @@ def gen_run_args(subparser): help=_("Igniter command for image")) parser.add_argument("-n","--network", help=_("Network params for running template")) + parser.add_argument("-v","--volume",action="append", + help=_("Volume params for running template")) parser.set_defaults(func=run) if __name__ == '__main__': -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:44PM +0000, Eren Yagdiran wrote:
Volumes let user to map host-paths into sandbox. Docker containers need volumes for data persistence.
I'm a little bit puzzelled about how this feature is supposed to be used. IIUC, in the Docker json file we have something like { "/var/my-app-data/": {}, "/etc/some-config.d/": {}, }
+ def getVolumes(self): + volumes = self.json_data['container_config']['Volumes'] + volumelist = [] + if isinstance(volumes,collections.Iterable): + for key,value in volumes.iteritems(): + volumelist.append(key) + return volumelist
This will just return a python list ["/var/my-app-data/", "/etc/some-config.d"]
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 058738a..79f8d8c 100755 --- a/virt-sandbox-image/virt-sandbox-image.py
@@ -150,6 +151,25 @@ def run(args): if networkArgs is not None: params.append('-N') params.append(networkArgs) + allVolumes = source.get_volume(configfile) + volumeArgs = args.volume + if volumeArgs is not None: + allVolumes = allVolumes + volumeArgs + for volume in allVolumes: + volumeSplit = volume.split(":")
We don't have any ':' in our returned list from getVolumes()
+ volumelen = len(volumeSplit) + if volumelen == 2: + hostPath = volumeSplit[0] + guestPath = volumeSplit[1] + elif volumelen == 1: + guestPath = volumeSplit[0] + hostPath = storage_dir + guestPath + if not os.path.exists(hostPath): + os.makedirs(hostPath) + else: + pass
So we seem to just skip this ?
+ params.append("--mount") + params.append("host-bind:%s=%s" %(guestPath,hostPath)) params.append('--') params.append(commandToRun) cmd = cmd + params
Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

On Tue, 2015-09-08 at 17:27 +0100, Daniel P. Berrange wrote:
On Fri, Aug 28, 2015 at 01:47:44PM +0000, Eren Yagdiran wrote:
Volumes let user to map host-paths into sandbox. Docker containers need volumes for data persistence.
I'm a little bit puzzelled about how this feature is supposed to be used. IIUC, in the Docker json file we have something like
{ "/var/my-app-data/": {}, "/etc/some-config.d/": {}, }
See here for the full syntax / documentation of volume mounts: http://docs.docker.com/userguide/dockervolumes/#volume
+ def getVolumes(self): + volumes = self.json_data['container_config']['Volumes']
It seems this commit needs quite some rework: the Config/Volumes section only gets the destination of the bind mounts created by docker, while HostConfig/Mounts gets all the details: "Mounts": [ { "Source": "/public", "Destination": "/home/public", "Mode": "", "RW": true }, { "Source": "/home", "Destination": "ro", "Mode": "", "RW": true }, { "Name": "43c4b4472e74bc5d1cf0a390f8707decade340df5e10cdea468bb191cac14a76", "Source": "/var/lib/docker/volumes/43c4b4472e74bc5d1cf0a390f8707decade340df5e10cdea468bb191cac14a76/_data", "Destination": "/srv/www", "Driver": "local", "Mode": "", "RW": true } ], Note in this sample, that at least some versions of docker can't properly handle the -v /path:ro flag (documented) and try to create a bind mount to /ro ;) In the same config, I'm having this for the Volumes: "Volumes": { "/srv/www": {} }, It really seems like volumes are just bind mounts to an automatically created image. The purpose of this feature is easy data storing outside the rootfs image. Volumes and Mounts are both created with the docker run -v option. The --volumes-from will just add the required items in the Mounts section of the docker container config.
+ volumelist = [] + if isinstance(volumes,collections.Iterable): + for key,value in volumes.iteritems(): + volumelist.append(key) + return volumelist
This will just return a python list
["/var/my-app-data/", "/etc/some-config.d"]
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 058738a..79f8d8c 100755 --- a/virt-sandbox-image/virt-sandbox-image.py
@@ -150,6 +151,25 @@ def run(args): if networkArgs is not None: params.append('-N') params.append(networkArgs) + allVolumes = source.get_volume(configfile) + volumeArgs = args.volume + if volumeArgs is not None: + allVolumes = allVolumes + volumeArgs + for volume in allVolumes: + volumeSplit = volume.split(":")
We don't have any ':' in our returned list from getVolumes()
Indeed there is no ':' in the Volumes list, and there will be none in the mounts section too. I think Eren mixed up the docker parameters and the config file values. -- Cedric
+ volumelen = len(volumeSplit) + if volumelen == 2: + hostPath = volumeSplit[0] + guestPath = volumeSplit[1] + elif volumelen == 1: + guestPath = volumeSplit[0] + hostPath = storage_dir + guestPath + if not os.path.exists(hostPath): + os.makedirs(hostPath) + else: + pass
So we seem to just skip this ?
+ params.append("--mount") + params.append("host-bind:%s=%s" %(guestPath,hostPath)) params.append('--') params.append(commandToRun) cmd = cmd + params
Regards, Daniel

On Wed, Sep 09, 2015 at 10:41:10AM +0200, Cedric Bosdonnat wrote:
On Tue, 2015-09-08 at 17:27 +0100, Daniel P. Berrange wrote:
On Fri, Aug 28, 2015 at 01:47:44PM +0000, Eren Yagdiran wrote:
Volumes let user to map host-paths into sandbox. Docker containers need volumes for data persistence.
I'm a little bit puzzelled about how this feature is supposed to be used. IIUC, in the Docker json file we have something like
{ "/var/my-app-data/": {}, "/etc/some-config.d/": {}, }
See here for the full syntax / documentation of volume mounts: http://docs.docker.com/userguide/dockervolumes/#volume
+ def getVolumes(self): + volumes = self.json_data['container_config']['Volumes']
It seems this commit needs quite some rework: the Config/Volumes section only gets the destination of the bind mounts created by docker, while HostConfig/Mounts gets all the details:
IIUC, we shouldn't really look at the HostConfig section at all - that reflects the host-specific configuration of the image when it was deployed on the original build host. We should work entirely from the Config section, to formulate our own host-specific configuration as needed by our tools.
"Mounts": [ { "Source": "/public", "Destination": "/home/public", "Mode": "", "RW": true }, { "Source": "/home", "Destination": "ro", "Mode": "", "RW": true }, { "Name": "43c4b4472e74bc5d1cf0a390f8707decade340df5e10cdea468bb191cac14a76", "Source": "/var/lib/docker/volumes/43c4b4472e74bc5d1cf0a390f8707decade340df5e10cdea468bb191cac14a76/_data", "Destination": "/srv/www", "Driver": "local", "Mode": "", "RW": true } ],
Note in this sample, that at least some versions of docker can't properly handle the -v /path:ro flag (documented) and try to create a bind mount to /ro ;)
In the same config, I'm having this for the Volumes:
"Volumes": { "/srv/www": {} },
It really seems like volumes are just bind mounts to an automatically created image. The purpose of this feature is easy data storing outside the rootfs image.
Volumes and Mounts are both created with the docker run -v option. The --volumes-from will just add the required items in the Mounts section of the docker container config.
+ volumelist = [] + if isinstance(volumes,collections.Iterable): + for key,value in volumes.iteritems(): + volumelist.append(key) + return volumelist
This will just return a python list
["/var/my-app-data/", "/etc/some-config.d"]
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 058738a..79f8d8c 100755 --- a/virt-sandbox-image/virt-sandbox-image.py
@@ -150,6 +151,25 @@ def run(args): if networkArgs is not None: params.append('-N') params.append(networkArgs) + allVolumes = source.get_volume(configfile) + volumeArgs = args.volume + if volumeArgs is not None: + allVolumes = allVolumes + volumeArgs + for volume in allVolumes: + volumeSplit = volume.split(":")
We don't have any ':' in our returned list from getVolumes()
Indeed there is no ':' in the Volumes list, and there will be none in the mounts section too. I think Eren mixed up the docker parameters and the config file values.
I think my confusion was due to this code block handling both the user supplied command line arguments, which can include the ':', and the image supplied mounts which don't include the ':' When I reposted this series, I put this patch last of all, so my inclination at this point is to not merge it at all yet. We'll just focus on the rest of the series, and investigate the volume stuff some more before considering it for merge. Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

On Wed, 2015-09-09 at 09:54 +0100, Daniel P. Berrange wrote:
On Wed, Sep 09, 2015 at 10:41:10AM +0200, Cedric Bosdonnat wrote:
On Tue, 2015-09-08 at 17:27 +0100, Daniel P. Berrange wrote:
On Fri, Aug 28, 2015 at 01:47:44PM +0000, Eren Yagdiran wrote:
Volumes let user to map host-paths into sandbox. Docker containers need volumes for data persistence.
I'm a little bit puzzelled about how this feature is supposed to be used. IIUC, in the Docker json file we have something like
{ "/var/my-app-data/": {}, "/etc/some-config.d/": {}, }
See here for the full syntax / documentation of volume mounts: http://docs.docker.com/userguide/dockervolumes/#volume
+ def getVolumes(self): + volumes = self.json_data['container_config']['Volumes']
It seems this commit needs quite some rework: the Config/Volumes section only gets the destination of the bind mounts created by docker, while HostConfig/Mounts gets all the details:
IIUC, we shouldn't really look at the HostConfig section at all - that reflects the host-specific configuration of the image when it was deployed on the original build host. We should work entirely from the Config section, to formulate our own host-specific configuration as needed by our tools.
Yes, looks like docker build files VOLUME directory only can handle the automatically created data volume: http://docs.docker.com/reference/builder/#volume Thus may be better to ignore bind mounts.
"Mounts": [ { "Source": "/public", "Destination": "/home/public", "Mode": "", "RW": true }, { "Source": "/home", "Destination": "ro", "Mode": "", "RW": true }, { "Name": "43c4b4472e74bc5d1cf0a390f8707decade340df5e10cdea468bb191cac14a76", "Source": "/var/lib/docker/volumes/43c4b4472e74bc5d1cf0a390f8707decade340df5e10cdea468bb191cac14a76/_data", "Destination": "/srv/www", "Driver": "local", "Mode": "", "RW": true } ],
Note in this sample, that at least some versions of docker can't properly handle the -v /path:ro flag (documented) and try to create a bind mount to /ro ;)
In the same config, I'm having this for the Volumes:
"Volumes": { "/srv/www": {} },
It really seems like volumes are just bind mounts to an automatically created image. The purpose of this feature is easy data storing outside the rootfs image.
Volumes and Mounts are both created with the docker run -v option. The --volumes-from will just add the required items in the Mounts section of the docker container config.
+ volumelist = [] + if isinstance(volumes,collections.Iterable): + for key,value in volumes.iteritems(): + volumelist.append(key) + return volumelist
This will just return a python list
["/var/my-app-data/", "/etc/some-config.d"]
diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 058738a..79f8d8c 100755 --- a/virt-sandbox-image/virt-sandbox-image.py
@@ -150,6 +151,25 @@ def run(args): if networkArgs is not None: params.append('-N') params.append(networkArgs) + allVolumes = source.get_volume(configfile) + volumeArgs = args.volume + if volumeArgs is not None: + allVolumes = allVolumes + volumeArgs + for volume in allVolumes: + volumeSplit = volume.split(":")
We don't have any ':' in our returned list from getVolumes()
Indeed there is no ':' in the Volumes list, and there will be none in the mounts section too. I think Eren mixed up the docker parameters and the config file values.
I think my confusion was due to this code block handling both the user supplied command line arguments, which can include the ':', and the image supplied mounts which don't include the ':'
When I reposted this series, I put this patch last of all, so my inclination at this point is to not merge it at all yet. We'll just focus on the rest of the series, and investigate the volume stuff some more before considering it for merge.
Indeed. -- Cedric

--- bin/Makefile.am | 5 ++ bin/virt-sandbox-image.pod | 172 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 bin/virt-sandbox-image.pod diff --git a/bin/Makefile.am b/bin/Makefile.am index df4c7dc..5d7ff8a 100644 --- a/bin/Makefile.am +++ b/bin/Makefile.am @@ -23,6 +23,7 @@ POD_FILES = \ virt-sandbox-service-delete.pod \ virt-sandbox-service-reload.pod \ virt-sandbox-service-upgrade.pod \ + virt-sandbox-image.pod \ $(NULL) EXTRA_DIST = virt-sandbox-service \ virt-sandbox-image.in \ @@ -40,6 +41,7 @@ man1_MANS = \ virt-sandbox-service-delete.1 \ virt-sandbox-service-reload.1 \ virt-sandbox-service-upgrade.1 \ + virt-sandbox-image.1 \ $(NULL) POD2MAN = pod2man -c "Virtualization Support" -r "$(PACKAGE)-$(VERSION)" @@ -71,6 +73,9 @@ virt-sandbox-service-reload.1: virt-sandbox-service-reload.pod Makefile virt-sandbox-service-upgrade.1: virt-sandbox-service-upgrade.pod Makefile $(AM_V_GEN)$(POD2MAN) $< $(srcdir)/$@ +virt-sandbox-image.1: virt-sandbox-image.pod Makefile + $(AM_V_GEN)$(POD2MAN) $< $(srcdir)/$@ + CLEANFILES = $(man1_MANS) \ virt-sandbox-image diff --git a/bin/virt-sandbox-image.pod b/bin/virt-sandbox-image.pod new file mode 100644 index 0000000..a85fcd9 --- /dev/null +++ b/bin/virt-sandbox-image.pod @@ -0,0 +1,172 @@ +=head1 NAME + +virt-sandbox-image - Sandbox Container Image Tool + +=head1 SYNOPSIS + + {download,create,run,delete} + + commands: + + download Download template data + + create Create image from template data + + run Run an already built image + + delete Delete template data + +=head1 DESCRIPTION + +virt-sandbox-image.py is a sandbox container image tool developed in python. +This tool can download,create,run and delete templates which are provided by +different sources. This tool comes with Docker source by default. Other sources +can be implemented by extending source class + +=head1 OPTIONS + +=over 4 + +=item B<download name -s source -r registry -u username -p password -t template_directory> + +Download a template by given name with a specified source. + +=over 6 + +=item B<name> + +Template name to download + +=item B<-s or --source> + +Source parameter will try load source module under sources/ directory. Each source has to implement Source.py base class and register itself with a proper name +Default source is Docker. + +=item B<-r or --registry> + +Custom registry url for downloading data. This might need privileged credentials which can be specified by --username and --password parameters. + +=item B<-u or --username> + +Username for custom registry authentication + +=item B<-p or --password> + +Password for custom registry authentication + +=item B<-t or --template-dir> + +Custom directory for downloading template data + +=back + +=item B<create name imagepath format -s source -d driver> + +Create already downloaded template into image with given format. + +=over 5 + +=item B<name> + +Template name to download. + +=item B<imagepath> + +Image path where template image will be stored. + +=item B<format> + +Image format e.g qcow2 + +=item B<-s or --source> + +Source parameter will try load source module under sources/ directory. Each source has to implement Source.py base class and register itself with a proper name +Default source is Docker. + +=item B<-d or --driver> + +Driver parameter can be specified with only supported driver by libvirt-sandbox. These are lxc:///, qemu:///session, qemu:///system. + +=back + +=item B<run name imagepath format -c command -n network -v volume -s source -d driver> + +Run already built image. + +=over 6 + +=item B<name> + +Template name to download. + +=item B<imagepath> + +Image path where template image will be stored. + +=item B<-c or --command> + +Command for running a image. If it is not specified, virt-sandbox-image will try to load command params from specified source. E.g /bin/bash + +=item B<-n or --network> + +Network params will be passed directly to the virt-sandbox. More information about network params, See C<virt-sandbox(8)> + +=item B<-v or --volume> + +Volume params are for binding host-paths to the guest. E.g -v /home:/home will map /home directory from host to the guest. + +=item B<-d or --driver> + +Driver parameter can be specified with only supported driver by libvirt-sandbox. These are lxc:///, qemu:///session, qemu:///system. + +=back + +=item B<delete name imagepath -s source > + +Delete downloaded template data and its built image. + +=over 3 + +=item B<name> + +Template name to delete. + +=item B<imagepath> + +Image path where template data or image stays. + +=item B<-s or --source> + +Source parameter will try load source module under sources/ directory. Each source has to implement Source.py base class and register itself with a proper name +Default source is Docker. + +=back + +=back + +=head1 SEE ALSO + +C<virt-sandbox(8)> + +=head1 FILES + +Container content will be stored in subdirectories of +/var/lib/libvirt/templates, by default. + +=head1 AUTHORS + +Daniel P. Berrange <dan@berrange.com> + +Eren Yagdiran <erenyagdiran@gmail.com> + +=head1 COPYRIGHT + +Copyright (C) 2013 Red Hat, Inc. +Copyright (C) 2015 Universitat Politecnica de Catalunya. + +=head1 LICENSE + +virt-sandbox-image is distributed under the terms of the GNU LGPL v2+. +This is free software; see the source for copying conditions. +There is NO warranty; not even for MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:45PM +0000, Eren Yagdiran wrote:
--- bin/Makefile.am | 5 ++ bin/virt-sandbox-image.pod | 172 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 bin/virt-sandbox-image.pod
This needs to also update libvirt-sandbox.spec.in to add the new file to the RPM
diff --git a/bin/virt-sandbox-image.pod b/bin/virt-sandbox-image.pod new file mode 100644 index 0000000..a85fcd9 --- /dev/null +++ b/bin/virt-sandbox-image.pod
The virt-sandbox-service command split each subcommand up into a separate man page. We should probably do the same for this too, but that can wait until after merge.
+=head1 OPTIONS + +=over 4 + +=item B<download name -s source -r registry -u username -p password -t template_directory>
Typically the options would be listed before the position args. eg B<download -s source -r registry -u username -p password -t template_directory name> Also we should probably use 'templatename' instead of just 'name' to make things unambiguous
+=item B<run name imagepath format -c command -n network -v volume -s source -d driver> + +Run already built image. + +=over 6 + +=item B<name> + +Template name to download. + +=item B<imagepath> + +Image path where template image will be stored. + +=item B<-c or --command> + +Command for running a image. If it is not specified, virt-sandbox-image will try to load command params from specified source. E.g /bin/bash
This is called -i / --ignitor in the code
+ +=item B<-n or --network> + +Network params will be passed directly to the virt-sandbox. More information about network params, See C<virt-sandbox(8)> + +=item B<-v or --volume> + +Volume params are for binding host-paths to the guest. E.g -v /home:/home will map /home directory from host to the guest. + +=item B<-d or --driver> + +Driver parameter can be specified with only supported driver by libvirt-sandbox. These are lxc:///, qemu:///session, qemu:///system.
And this is -c / --connect Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Add the config gobject to store custom environment variables. This will allow creating custom environment variables on a sandbox with a parameter formatted like --env key1=val1 Add testcase for custom environment variables "make check" now includes testcase for environment variables --- libvirt-sandbox/libvirt-sandbox-config.c | 171 ++++++++++++++++++++++++++++++- libvirt-sandbox/libvirt-sandbox-config.h | 13 +++ libvirt-sandbox/libvirt-sandbox.sym | 6 ++ libvirt-sandbox/tests/test-config.c | 10 ++ 4 files changed, 199 insertions(+), 1 deletion(-) diff --git a/libvirt-sandbox/libvirt-sandbox-config.c b/libvirt-sandbox/libvirt-sandbox-config.c index 2506072..780d174 100644 --- a/libvirt-sandbox/libvirt-sandbox-config.c +++ b/libvirt-sandbox/libvirt-sandbox-config.c @@ -64,6 +64,7 @@ struct _GVirSandboxConfigPrivate GList *networks; GList *mounts; GList *disks; + GHashTable* envs; gchar *secLabel; gboolean secDynamic; @@ -276,6 +277,8 @@ static void gvir_sandbox_config_finalize(GObject *object) g_list_foreach(priv->networks, (GFunc)g_object_unref, NULL); g_list_free(priv->networks); + g_hash_table_destroy(priv->envs); + g_list_foreach(priv->disks, (GFunc)g_object_unref, NULL); g_list_free(priv->disks); @@ -483,6 +486,7 @@ static void gvir_sandbox_config_init(GVirSandboxConfig *config) priv->arch = g_strdup(uts.machine); priv->secDynamic = TRUE; + priv->envs = g_hash_table_new(g_str_hash, g_str_equal); priv->uid = geteuid(); priv->gid = getegid(); priv->username = g_strdup(g_get_user_name()); @@ -1144,6 +1148,102 @@ gboolean gvir_sandbox_config_has_networks(GVirSandboxConfig *config) return priv->networks ? TRUE : FALSE; } +/** + * gvir_sandbox_config_add_env: + * @config: (transfer none): the sandbox config + * @key: (transfer none): the key for environment variable + * @value: (transfer none): the value for environment variable + * + * Adds a new environment variable to the sandbox + * + */ +void gvir_sandbox_config_add_env(GVirSandboxConfig *config, + gchar *k, + gchar *v) +{ + gchar * key = g_strdup(k); + gchar * value = g_strdup(v); + GVirSandboxConfigPrivate *priv = config->priv; + g_hash_table_insert(priv->envs,key,value); +} + +/** + * gvir_sandbox_config_get_envs: + * @config: (transfer none): the sandbox config + * + * Retrieves the hashtable of custom environment variables in the sandbox + * + * Returns: (transfer full) (element-type gchar gchar): the hashtable of environment variables + */ +GHashTable *gvir_sandbox_config_get_envs(GVirSandboxConfig *config) +{ + GVirSandboxConfigPrivate *priv = config->priv; + return priv->envs; +} + +/** + * gvir_sandbox_config_add_env_strv: + * @config: (transfer none): the sandbox config + * @envs: (transfer none)(array zero-terminated=1): the list of environment variables + * + * Parses @envs whose elements are in the format KEY=VALUE + * + * --env KEY=VALUE + */ +gboolean gvir_sandbox_config_add_env_strv(GVirSandboxConfig *config, + gchar **envs, + GError **error) +{ + gsize i = 0; + while (envs && envs[i]) { + if (!gvir_sandbox_config_add_env_opts(config, + envs[i], + error)) + return FALSE; + i++; + } + return TRUE; +} + +/** + * gvir_sandbox_config_add_env_opts: + * @config: (transfer none): the sandbox config + * @env: (transfer none): the env config + * + * Parses @env in the format KEY=VALUE + * creating #GVirSandboxConfigEnv instances for each element. For + * example + * + * --env KEY=VALUE + */ + +gboolean gvir_sandbox_config_add_env_opts(GVirSandboxConfig *config, + const char *opt, + GError **error) +{ + gchar *tmp = NULL; + gchar *key = NULL; + gchar *value = NULL; + + if (!(tmp = g_strdup(opt))) + return FALSE; + + key = strchr(tmp, '='); + + if (!key) { + g_set_error(error, GVIR_SANDBOX_CONFIG_ERROR, 0, + _("Wrong environment format on %s"), opt); + return FALSE; + } + + *key = '\0'; + value = key + 1; + + gvir_sandbox_config_add_env(config, tmp, value); + + g_free(tmp); + return TRUE; +} /** * gvir_sandbox_config_add_disk: @@ -1163,7 +1263,6 @@ void gvir_sandbox_config_add_disk(GVirSandboxConfig *config, priv->disks = g_list_append(priv->disks, dsk); } - /** * gvir_sandbox_config_get_disks: * @config: (transfer none): the sandbox config @@ -1172,6 +1271,7 @@ void gvir_sandbox_config_add_disk(GVirSandboxConfig *config, * * Returns: (transfer full) (element-type GVirSandboxConfigMount): the list of disks */ + GList *gvir_sandbox_config_get_disks(GVirSandboxConfig *config) { GVirSandboxConfigPrivate *priv = config->priv; @@ -1949,6 +2049,39 @@ static GVirSandboxConfigMount *gvir_sandbox_config_load_config_mount(GKeyFile *f goto cleanup; } +static gboolean gvir_sandbox_config_load_config_env(GKeyFile *file, + guint i, + GError **error, + gchar **key, + gchar **value) +{ + gboolean ret = TRUE; + gchar *index = NULL; + GError *e = NULL; + index = g_strdup_printf("env.%u", i); + if ((*key = g_key_file_get_string(file, index, "key", &e)) == NULL) { + if (e->code == G_KEY_FILE_ERROR_GROUP_NOT_FOUND) { + g_error_free(e); + ret = FALSE; + goto cleanup; + } + g_error_free(e); + g_set_error(error, GVIR_SANDBOX_CONFIG_ERROR, 0, + "%s", _("Missing environment key in config file")); + ret = FALSE; + goto cleanup; + } + if ((*value = g_key_file_get_string(file, index, "value", NULL)) == NULL) { + g_set_error(error, GVIR_SANDBOX_CONFIG_ERROR, 0, + "%s", _("Missing environment value in config file")); + ret = FALSE; + goto cleanup; + } + + cleanup: + g_free(index); + return ret; +} static GVirSandboxConfigDisk *gvir_sandbox_config_load_config_disk(GKeyFile *file, guint i, @@ -2244,6 +2377,19 @@ static gboolean gvir_sandbox_config_load_config(GVirSandboxConfig *config, priv->mounts = g_list_append(priv->mounts, mount); } + for (i = 0 ; i < 1024; i++) { + gchar *key = NULL; + gchar *value = NULL; + if (!(gvir_sandbox_config_load_config_env(file, i, error,&key,&value)) && *error){ + g_free(key); + g_free(value); + ret = FALSE; + goto cleanup; + } + if (key){ + g_hash_table_insert(priv->envs,key,value); + } + } for (i = 0 ; i < 1024 ; i++) { GVirSandboxConfigDisk *disk; @@ -2274,6 +2420,19 @@ static gboolean gvir_sandbox_config_load_config(GVirSandboxConfig *config, return ret; } +static void gvir_sandbox_config_save_config_env(gchar *key, + gchar *value, + GKeyFile *file, + guint i) +{ + gchar *index = NULL; + index = g_strdup_printf("env.%u", i); + g_key_file_set_string(file, index, "key",key); + g_key_file_set_string(file, index, "value",value); + g_free(index); + g_free(key); + g_free(value); +} static void gvir_sandbox_config_save_config_disk(GVirSandboxConfigDisk *config, GKeyFile *file, @@ -2484,6 +2643,16 @@ static void gvir_sandbox_config_save_config(GVirSandboxConfig *config, i++; } + + GHashTableIter iter; + gpointer key, value; + i = 0; + g_hash_table_iter_init (&iter, priv->envs); + while (g_hash_table_iter_next (&iter, &key, &value)){ + gvir_sandbox_config_save_config_env(key,value,file,i); + i++; + } + i = 0; tmp = priv->disks; while (tmp) { diff --git a/libvirt-sandbox/libvirt-sandbox-config.h b/libvirt-sandbox/libvirt-sandbox-config.h index 2c5f0a6..e5e53f7 100644 --- a/libvirt-sandbox/libvirt-sandbox-config.h +++ b/libvirt-sandbox/libvirt-sandbox-config.h @@ -122,6 +122,19 @@ gboolean gvir_sandbox_config_add_network_strv(GVirSandboxConfig *config, GError **error); gboolean gvir_sandbox_config_has_networks(GVirSandboxConfig *config); +void gvir_sandbox_config_add_env(GVirSandboxConfig *config, + gchar *key, + gchar *value); +GHashTable *gvir_sandbox_config_get_envs(GVirSandboxConfig *config); +gboolean gvir_sandbox_config_add_env_strv(GVirSandboxConfig *config, + gchar **envs, + GError **error); +gboolean gvir_sandbox_config_add_env_opts(GVirSandboxConfig *config, + const char *env, + GError **error); +gboolean gvir_sandbox_config_has_envs(GVirSandboxConfig *config); + + void gvir_sandbox_config_add_disk(GVirSandboxConfig *config, GVirSandboxConfigDisk *dsk); GList *gvir_sandbox_config_get_disks(GVirSandboxConfig *config); diff --git a/libvirt-sandbox/libvirt-sandbox.sym b/libvirt-sandbox/libvirt-sandbox.sym index 6f8097a..a09a6c2 100644 --- a/libvirt-sandbox/libvirt-sandbox.sym +++ b/libvirt-sandbox/libvirt-sandbox.sym @@ -26,6 +26,12 @@ LIBVIRT_SANDBOX_0.6.0 { gvir_sandbox_config_disk_get_type; gvir_sandbox_config_has_disks; + gvir_sandbox_config_add_env; + gvir_sandbox_config_add_env_strv; + gvir_sandbox_config_add_env_opts; + gvir_sandbox_config_env_get_type; + gvir_sandbox_config_has_envs; + gvir_sandbox_config_mount_add_include; gvir_sandbox_config_mount_get_includes; gvir_sandbox_config_mount_get_target; diff --git a/libvirt-sandbox/tests/test-config.c b/libvirt-sandbox/tests/test-config.c index da05187..ac10bab 100644 --- a/libvirt-sandbox/tests/test-config.c +++ b/libvirt-sandbox/tests/test-config.c @@ -58,6 +58,13 @@ int main(int argc, char **argv) "host-bind:/tmp=", NULL }; + + const gchar *envs[] = { + "key1=val1", + "key2=val2", + NULL + }; + const gchar *disks[] = { "file:dbdata=/tmp/img.blah,format=qcow2", "file:cache=/tmp/img.qcow2", @@ -103,6 +110,9 @@ int main(int argc, char **argv) if (!gvir_sandbox_config_add_mount_strv(cfg1, (gchar**)mounts, &err)) goto cleanup; + if (!gvir_sandbox_config_add_env_strv(cfg1, (gchar**)envs, &err)) + goto cleanup; + if (!gvir_sandbox_config_add_disk_strv(cfg1, (gchar**)disks, &err)) goto cleanup; -- 2.1.0

On Fri, Aug 28, 2015 at 01:47:46PM +0000, Eren Yagdiran wrote:
Add the config gobject to store custom environment variables. This will allow creating custom environment variables on a sandbox with a parameter formatted like --env key1=val1
Add testcase for custom environment variables
"make check" now includes testcase for environment variables
diff --git a/libvirt-sandbox/libvirt-sandbox.sym b/libvirt-sandbox/libvirt-sandbox.sym index 6f8097a..a09a6c2 100644 --- a/libvirt-sandbox/libvirt-sandbox.sym +++ b/libvirt-sandbox/libvirt-sandbox.sym @@ -26,6 +26,12 @@ LIBVIRT_SANDBOX_0.6.0 { gvir_sandbox_config_disk_get_type; gvir_sandbox_config_has_disks;
+ gvir_sandbox_config_add_env; + gvir_sandbox_config_add_env_strv; + gvir_sandbox_config_add_env_opts; + gvir_sandbox_config_env_get_type; + gvir_sandbox_config_has_envs; + gvir_sandbox_config_mount_add_include; gvir_sandbox_config_mount_get_includes; gvir_sandbox_config_mount_get_target;
This should be added to a new version block 0.6.1, rather than changing the existing version. Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

Allow users to add custom environment variables to their sandbox. --- bin/virt-sandbox.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bin/virt-sandbox.c b/bin/virt-sandbox.c index 195515f..e90b698 100644 --- a/bin/virt-sandbox.c +++ b/bin/virt-sandbox.c @@ -64,6 +64,7 @@ int main(int argc, char **argv) { GError *error = NULL; gchar *name = NULL; gchar **disks = NULL; + gchar **envs = NULL; gchar **mounts = NULL; gchar **includes = NULL; gchar *includefile = NULL; @@ -95,6 +96,8 @@ int main(int argc, char **argv) { N_("root directory of the sandbox"), "DIR" }, { "disk", ' ', 0, G_OPTION_ARG_STRING_ARRAY, &disks, N_("add a disk in the guest"), "TYPE:TAGNAME=SOURCE,format=FORMAT" }, + { "env", 'e', 0, G_OPTION_ARG_STRING_ARRAY, &envs, + N_("add a environment variable for the sandbox"), "KEY=VALUE" }, { "mount", 'm', 0, G_OPTION_ARG_STRING_ARRAY, &mounts, N_("mount a filesystem in the guest"), "TYPE:TARGET=SOURCE" }, { "include", 'i', 0, G_OPTION_ARG_STRING_ARRAY, &includes, @@ -185,6 +188,13 @@ int main(int argc, char **argv) { gvir_sandbox_config_set_username(cfg, "root"); } + if (envs && + !gvir_sandbox_config_add_env_strv(cfg, envs, &error)) { + g_printerr(_("Unable to parse custom environment variables: %s\n"), + error && error->message ? error->message : _("Unknown failure")); + goto cleanup; + } + if (disks && !gvir_sandbox_config_add_disk_strv(cfg, disks, &error)) { g_printerr(_("Unable to parse disks: %s\n"), @@ -329,6 +339,10 @@ inheriting the host's root filesystem. NB. C<DIR> must contain a matching install of the libvirt-sandbox package. This restriction may be lifted in a future version. +=item B<--env key=value> + +Sets up a custom environment variable on a running sandbox. + =item B<--disk TYPE:TAGNAME=SOURCE,format=FORMAT> Sets up a disk inside the sandbox by using B<SOURCE> with a symlink named as B<TAGNAME> -- 2.1.0

Common-init reads config file and exports custom environment variables from config file and applies them to the running sandbox. --- libvirt-sandbox/libvirt-sandbox-init-common.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libvirt-sandbox/libvirt-sandbox-init-common.c b/libvirt-sandbox/libvirt-sandbox-init-common.c index d35f760..16abf25 100644 --- a/libvirt-sandbox/libvirt-sandbox-init-common.c +++ b/libvirt-sandbox/libvirt-sandbox-init-common.c @@ -336,6 +336,21 @@ static gboolean setup_network(GVirSandboxConfig *config, GError **error) } +static gboolean setup_custom_env(GVirSandboxConfig *config, GError **error) +{ + gboolean ret = FALSE; + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, gvir_sandbox_config_get_envs(config)); + while (g_hash_table_iter_next (&iter, &key, &value)){ + if(setenv(key,value,1)!=0) + goto cleanup; + } + ret = TRUE; + cleanup: + return ret; +} + static int change_user(const gchar *user, uid_t uid, gid_t gid, @@ -1262,6 +1277,9 @@ int main(int argc, char **argv) { if (!setup_disk_tags()) exit(EXIT_FAILURE); + if (!setup_custom_env(config, &error)) + goto error; + if (!setup_network(config, &error)) goto error; -- 2.1.0

Any custom key=value pair can be used as a custom environment variable in virt-sandbox-image. e.g virt-sandbox-image run ubuntu /var/lib/libvirt/templates -c lxc:/// -i /bin/bash -e key1=val1 --- virt-sandbox-image/sources/DockerSource.py | 10 ++++++++++ virt-sandbox-image/sources/Source.py | 4 ++++ virt-sandbox-image/virt-sandbox-image.py | 17 +++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/virt-sandbox-image/sources/DockerSource.py b/virt-sandbox-image/sources/DockerSource.py index 1022107..122e595 100644 --- a/virt-sandbox-image/sources/DockerSource.py +++ b/virt-sandbox-image/sources/DockerSource.py @@ -45,6 +45,12 @@ class DockerConfParser(): for key,value in volumes.iteritems(): volumelist.append(key) return volumelist + def getEnvs(self): + lst = self.json_data['container_config']['Env'] + if lst is not None and isinstance(lst,list): + return lst + else: + return [] class DockerSource(Source): @@ -405,5 +411,9 @@ class DockerSource(Source): configParser = DockerConfParser(configfile) return configParser.getVolumes() + def get_env(self,configfile): + configParser = DockerConfParser(configfile) + return configParser.getEnvs() + def debug(msg): sys.stderr.write(msg) diff --git a/virt-sandbox-image/sources/Source.py b/virt-sandbox-image/sources/Source.py index 8cc508e..c467ce2 100644 --- a/virt-sandbox-image/sources/Source.py +++ b/virt-sandbox-image/sources/Source.py @@ -49,3 +49,7 @@ class Source(): @abstractmethod def get_volume(self,**args): pass + + @abstractmethod + def get_env(self,**args): + pass diff --git a/virt-sandbox-image/virt-sandbox-image.py b/virt-sandbox-image/virt-sandbox-image.py index 79f8d8c..285e99c 100755 --- a/virt-sandbox-image/virt-sandbox-image.py +++ b/virt-sandbox-image/virt-sandbox-image.py @@ -170,6 +170,20 @@ def run(args): pass params.append("--mount") params.append("host-bind:%s=%s" %(guestPath,hostPath)) + + allEnvs = source.get_env(configfile) + envArgs = args.env + if envArgs is not None: + allEnvs = allEnvs + envArgs + for env in allEnvs: + envsplit = env.split("=") + envlen = len(envsplit) + if envlen == 2: + params.append("--env") + params.append(env) + else: + pass + params.append('--') params.append(commandToRun) cmd = cmd + params @@ -256,6 +270,9 @@ def gen_run_args(subparser): help=_("Network params for running template")) parser.add_argument("-v","--volume",action="append", help=_("Volume params for running template")) + parser.add_argument("-e","--env",action="append", + help=_("Environment params for running template")) + parser.set_defaults(func=run) if __name__ == '__main__': -- 2.1.0
participants (3)
-
Cedric Bosdonnat
-
Daniel P. Berrange
-
Eren Yagdiran