From 5ee71e711cbddfc4d98c3f8f876e5e79cee99ea1 Mon Sep 17 00:00:00 2001 From: Balazs Kocso Date: Thu, 20 Dec 2018 18:13:20 +0100 Subject: [PATCH] furnace/cli.py: add command line interface to furnace --- furnace/cli.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 3 ++ 2 files changed, 124 insertions(+) create mode 100644 furnace/cli.py diff --git a/furnace/cli.py b/furnace/cli.py new file mode 100644 index 0000000..2be820f --- /dev/null +++ b/furnace/cli.py @@ -0,0 +1,121 @@ +# +# Copyright (c) 2016-2018 Balabit +# +# This file is part of Furnace. +# +# Furnace 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. +# +# Furnace 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 Furnace. If not, see . +# + +import argparse +import sys +import tempfile +from pathlib import Path + +from . import version +from .config import BindMount +from .context import ContainerContext +from .utils import OverlayfsMountContext + + +DESCRIPTION = "A lightweight pure-python container implementation." + + +def parse_arguments(argv): + parser = argparse.ArgumentParser(description=DESCRIPTION) + parser.add_argument( + 'root_dir', + help="This directory will be the root directory of the container" + ) + parser.add_argument( + 'cmd', + nargs='*', + help="this command that will be run. If it is empty, than furnace will give an interactive shell" + ) + parser.add_argument( + '--version', + action='version', + version='%(prog)s {version}'.format(version=version.get_version()), + ) + parser.add_argument( + '--hostname', + default='container', + help="virtual hostname setting for interactive shell" + ) + parser.add_argument( + '-i', '--isolate-networking', + action='store_true', + help="create an isolated networking namespace for the container" + ) + parser.add_argument( + '-p', '--persistent', + action='store_true', + help="do not create a temporary overlay on top of the root directory, the changes will be persistent" + ) + parser.add_argument( + '-v', '--volume', + action='append', + metavar='src:dst:rw', + default=[], + help="add volumes from the host machine to the container in the following format: " + "/source/from/the/host:/path/in/the/container:rw, (readonly mount is the default)" + ) + return parser.parse_args(argv[1:]) + + +def create_bind_mount_list(volumes): + bind_mounts = [] + for volume in volumes: + source, destination = volume.split(':', 1) + readonly = True + if ':' in destination: + destination, readwrite = destination.split(':', 1) + if readwrite == 'rw': + readonly = False + + if destination.startswith('/'): + destination = destination[1:] + bind_mounts.append(BindMount(Path(source), Path(destination), readonly)) + + return bind_mounts + + +def run_container(root_dir, volumes, isolate_networking, hostname, cmd): + bind_mounts = create_bind_mount_list(volumes) + + with ContainerContext(root_dir, isolate_networking=isolate_networking, bind_mounts=bind_mounts) as container: + if cmd: + container.run(cmd) + else: + container.interactive_shell(virtual_hostname=hostname) + + +def main(argv=sys.argv): + args = parse_arguments(argv) + if args.persistent: + run_container(args.root_dir, args.volume, args.isolate_networking, args.hostname, args.cmd) + else: + with tempfile.TemporaryDirectory( + suffix="overlay_work" + ) as overlay_workdir, tempfile.TemporaryDirectory( + suffix="overlay_rw" + ) as overlay_rwdir, tempfile.TemporaryDirectory( + suffix="overlay_mount" + ) as overlay_mounted, OverlayfsMountContext( + [args.root_dir], overlay_rwdir, overlay_workdir, overlay_mounted + ): + run_container(overlay_mounted, args.volume, args.isolate_networking, args.hostname, args.cmd) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/setup.py b/setup.py index c81ba06..4f360be 100755 --- a/setup.py +++ b/setup.py @@ -51,6 +51,9 @@ 'Topic :: System', ], packages=find_packages(include=["furnace*"]), + entry_points={ + 'console_scripts': ['furnace=furnace.cli:main'], + }, extras_require={ 'dev': dev_requirements },