diff --git a/src/lxc/Makefile.am b/src/lxc/Makefile.am index b2f2128c4..7d86ad661 100644 --- a/src/lxc/Makefile.am +++ b/src/lxc/Makefile.am @@ -97,6 +97,10 @@ bin_SCRIPTS = \ lxc-shutdown \ lxc-destroy +if ENABLE_PYTHON + bin_SCRIPTS += lxc-start-ephemeral +endif + bin_PROGRAMS = \ lxc-attach \ lxc-unshare \ diff --git a/src/lxc/lxc-start-ephemeral b/src/lxc/lxc-start-ephemeral new file mode 100644 index 000000000..4f238a361 --- /dev/null +++ b/src/lxc/lxc-start-ephemeral @@ -0,0 +1,290 @@ +#!/usr/bin/python3 +# +# lxc-start-ephemeral: Start a copy of a container using an overlay +# +# This python implementation is based on the work done in the original +# shell implementation done by Serge Hallyn in Ubuntu (and other contributors) +# +# (C) Copyright Canonical Ltd. 2012 +# +# Authors: +# Stéphane Graber +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# NOTE: To remove once the API is stabilized +import warnings +warnings.filterwarnings("ignore", "The python-lxc API isn't yet stable") + +import argparse +import gettext +import lxc +import os +import sys +import subprocess +import tempfile + +_ = gettext.gettext +gettext.textdomain("lxc-start-ephemeral") + + +# Other functions +def randomMAC(): + import random + + mac = [0x00, 0x16, 0x3e, + random.randint(0x00, 0x7f), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff)] + return ':'.join(map(lambda x: "%02x" % x, mac)) + +# Begin parsing the command line +parser = argparse.ArgumentParser( + description=_("LXC: Start an ephemeral container"), + formatter_class=argparse.RawTextHelpFormatter, epilog=_( +"""If a COMMAND is given, then the container will run only as long +as the command runs. +If no COMMAND is given, this command will attach to tty1 and stop the +container when exiting (with ctrl-a-q). + +If no COMMAND is given and -d is used, the name and IP addresses of the +container will be printed to the console.""")) + +parser.add_argument("--orig", "-o", type=str, required=True, + help=_("name of the original container")) + +parser.add_argument("--bdir", "-b", type=str, + help=_("directory to bind mount into container")) + +parser.add_argument("--user", "-u", type=str, + help=_("the user to connect to the container as")) + +parser.add_argument("--key", "-S", type=str, + help=_("the path to the SSH key to use to connect")) + +parser.add_argument("--daemon", "-d", action="store_true", + help=_("run in the background")) + +parser.add_argument("--union-type", "-U", type=str, default="overlayfs", + choices=("overlayfs", "aufs"), + help=_("type of union (overlayfs or aufs), defaults to overlayfs.")) + +parser.add_argument("--keep-data", "-k", action="store_true", + help=_("Use a persistent backend instead of tmpfs.")) + +parser.add_argument("command", metavar='CMD', type=str, nargs="*", + help=_("Run specific command in container (command as argument)")) + +args = parser.parse_args() + +# Basic requirements check +## Check that -d and CMD aren't used at the same time +if args.command and args.daemon: + print(_("You can't use -d and a command at the same time.")) + sys.exit(1) + +## The user needs to be uid 0 +if not os.geteuid() == 0: + print(_("You must be root to run this script. Try running: sudo %s" % + (sys.argv[0]))) + sys.exit(1) + +# Load the orig container +orig = lxc.Container(args.orig) +if not orig.defined: + print(_("Source container '%s' doesn't exist." % args.orig)) + sys.exit(1) + +# Create the new container paths +dest_path = tempfile.mkdtemp(prefix="%s-" % args.orig, dir="/var/lib/lxc/") +os.mkdir(os.path.join(dest_path, "rootfs")) + +# Setup the new container's configuration +dest = lxc.Container(os.path.basename(dest_path)) +dest.load_config(orig.config_file_name) +dest.set_config_item("lxc.utsname", dest.name) +dest.set_config_item("lxc.rootfs", os.path.join(dest_path, "rootfs")) +dest.set_config_item("lxc.network.hwaddr", randomMAC()) + +overlay_dirs = [(orig.get_config_item("lxc.rootfs"), "%s/rootfs/" % dest_path)] + +# Generate a new fstab +if orig.get_config_item("lxc.mount"): + dest.set_config_item("lxc.mount", os.path.join(dest_path, "fstab")) + with open(orig.get_config_item("lxc.mount"), "r") as orig_fd: + with open(dest.get_config_item("lxc.mount"), "w+") as dest_fd: + for line in orig_fd.read().split("\n"): + # Start by replacing any reference to the container rootfs + line.replace(orig.get_config_item("lxc.rootfs"), + dest.get_config_item("lxc.rootfs")) + + # Skip any line that's not a bind mount + fields = line.split() + if len(fields) < 4: + dest_fd.write("%s\n" % line) + continue + + if fields[2] != "bind" and "bind" not in fields[3]: + dest_fd.write("%s\n" % line) + continue + + # Process any remaining line + dest_mount = os.path.abspath(os.path.join("%s/rootfs/" % ( + dest_path), fields[1])) + + if dest_mount == os.path.abspath("%s/rootfs/%s" % ( + dest_path, args.bdir)): + + dest_fd.write("%s\n" % line) + continue + + if "%s/rootfs/" % dest_path not in dest_mount: + print(_( +"Skipping mount entry '%s' as it's outside of the container rootfs.") % line) + + overlay_dirs += [(fields[0], dest_mount)] + +# Generate pre-mount script +with open(os.path.join(dest_path, "pre-mount"), "w+") as fd: + os.fchmod(fd.fileno(), 0o755) + fd.write("""#!/bin/sh +LXC_DIR="%s" +LXC_BASE="%s" +LXC_NAME="%s" +""" % (dest_path, orig.name, dest.name)) + + count = 0 + for entry in overlay_dirs: + target = "%s/delta%s" % (dest_path, count) + fd.write("mkdir -p %s %s\n" % (target, entry[1])) + + if not args.keep_data: + fd.write("mount -n -t tmpfs none %s\n" % (target)) + + if args.union_type == "overlayfs": + fd.write("mount -n -t overlayfs" + " -oupperdir=%s,lowerdir=%s none %s\n" % ( + target, + entry[0], + entry[1])) + elif args.union_type == "aufs": + fd.write("mount -n -t aufs " + "-o br=${upper}=rw:${lower}=ro,noplink none %s\n" % ( + target, + entry[0], + entry[1])) + count += 1 + + if args.bdir: + if not os.path.exists(args.bdir): + print(_("Path '%s' doesn't exist, won't be bind-mounted.") % + args.bdir) + else: + src_path = os.path.abspath(args.bdir) + dst_path = "%s/rootfs/%s" % (dest_path, os.path.abspath(args.bdir)) + fd.write("mkdir -p %s\nmount -n --bind %s %s\n" % ( + dst_path, src_path, dst_path)) + + fd.write(""" +[ -e $LXC_DIR/configured ] && exit 0 +for file in $LXC_DIR/rootfs/etc/hostname \\ + $LXC_DIR/rootfs/etc/hosts \\ + $LXC_DIR/rootfs/etc/sysconfig/network \\ + $LXC_DIR/rootfs/etc/sysconfig/network-scripts/ifcfg-eth0; do + [ -f "$file" ] && sed -i -e "s/$LXC_BASE/$LXC_NAME/" $file +done +touch $LXC_DIR/configured +""") + +dest.set_config_item("lxc.hook.pre-mount", + os.path.join(dest_path, "pre-mount")) + +# Generate post-stop script +if not args.keep_data: + with open(os.path.join(dest_path, "post-stop"), "w+") as fd: + os.fchmod(fd.fileno(), 0o755) + fd.write("""#!/bin/sh +[ -n "$1" ] && [ -d "/var/lib/lxc/$1/" ] && rm -Rf /var/lib/lxc/$1/ +""") + + dest.set_config_item("lxc.hook.post-stop", + os.path.join(dest_path, "post-stop")) + +dest.save_config() + +# Start the container +if not dest.start() or not dest.wait("RUNNING", timeout=5): + print(_("The container '%s' failed to start.") % dest.name) + dest.stop() + if dest.defined: + dest.destroy() + sys.exit(1) + +# Try to get the IP addresses +ips = dest.get_ips(timeout=5) + +# Deal with the case where we don't start a command in the container +if not args.command: + if args.daemon: + print(_("""The ephemeral container is now started. + +You can enter it from the command line with: lxc-console -n %s +The following IP addresses have be found in the container: +%s""") % ( + dest.name, + "\n".join([" - %s" % entry for entry in ips] + or [" - %s" % _("No address could be found")]) + )) + sys.exit(0) + else: + dest.console(tty=1) + if not dest.shutdown(timeout=5): + dest.stop() + sys.exit(0) + +# Now deal with the case where we want to run a command in the container +if not ips: + print(_("Failed to get an IP for container '%s'.") % dest.name) + dest.stop() + if dest.defined: + dest.destroy() + sys.exit(1) + +# NOTE: To replace by .attach() once the kernel supports it +cmd = ["ssh", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null"] + +if args.user: + cmd += ["-l", args.user] + +if args.key: + cmd += ["-k", args.key] + +for ip in ips: + ssh_cmd = cmd + [ip] + args.command + retval = subprocess.call(ssh_cmd, universal_newlines=True) + if retval == 255: + print(_("SSH failed to connect, trying next IP address.")) + continue + + if retval != 0: + print(_("Command returned with non-zero return code: %s") % retval) + break + +# Shutdown the container +if not dest.shutdown(timeout=5): + dest.stop()