Skip to content

Commit

Permalink
furnace/cli.py: add command line interface to furnace
Browse files Browse the repository at this point in the history
  • Loading branch information
Balazs Kocso authored and Balazs Kocso committed Apr 14, 2020
1 parent 0bdf662 commit 4369c3b
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 0 deletions.
124 changes: 124 additions & 0 deletions furnace/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#
# 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 <http://www.gnu.org/licenses/>.
#

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="the command that will be run. If empty, furnace will drop into an interactive shell"
)
parser.add_argument(
'-V', '--version',
action='version',
version='%(prog)s {version}'.format(version=version.get_version()),
)
parser.add_argument(
'-H', '--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,ro}',
default=[],
type=create_bind_mount_from_string,
dest='volumes',
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_from_string(volume):
if ':' not in volume:
raise argparse.ArgumentTypeError("Volume specification should have the following format: '/source/from/the/host:/path/in/the/container:rw'")

source, destination = volume.split(':', 1)
readonly = True
if ':' in destination:
destination, readwrite = destination.split(':', 1)
if readwrite == 'ro':
readonly = True
elif readwrite == 'rw':
readonly = False
else:
raise argparse.ArgumentTypeError('For creating volumes use the "ro", "rw" labels.')

return BindMount(Path(source), Path(destination), readonly)


def run_container(root_dir, bind_mounts, isolate_networking, hostname, cmd):
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.volumes, 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.volumes, args.isolate_networking, args.hostname, args.cmd)

return 0


if __name__ == '__main__':
sys.exit(main())
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
'Topic :: System',
],
packages=find_packages(include=["furnace*"]),
entry_points={
'console_scripts': ['furnace=furnace.cli:main'],
},
extras_require={
'dev': Path('requirements-dev.txt').read_text()
},
Expand Down
59 changes: 59 additions & 0 deletions test/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#
# Copyright (c) 2016-2020 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 <http://www.gnu.org/licenses/>.
#

import argparse
import pytest
from pathlib import Path

from furnace.cli import create_bind_mount_from_string
from furnace.config import BindMount


def test_create_bind_mount_from_sting():
not_value_bind_mount_string = "/not/valid/volume/description"
with pytest.raises(argparse.ArgumentTypeError):
create_bind_mount_from_string(not_value_bind_mount_string)

simple_bind_mount_string = "/home/test1:/home/test2"
simple_bind_mount = BindMount(
source=Path('/home/test1'),
destination=Path('/home/test2'),
readonly=True,
)
assert create_bind_mount_from_string(simple_bind_mount_string) == simple_bind_mount

read_only_bind_mount_string = "/home/test3:/home/test4:ro"
read_only_bind_mount = BindMount(
source=Path('/home/test3'),
destination=Path('/home/test4'),
readonly=True,
)
assert create_bind_mount_from_string(read_only_bind_mount_string) == read_only_bind_mount

read_write_bind_mount_string = "/home/test5:/home/test6:rw"
read_write_bind_mount = BindMount(
source=Path('/home/test5'),
destination=Path('/home/test6'),
readonly=False,
)
assert create_bind_mount_from_string(read_write_bind_mount_string) == read_write_bind_mount

valid_bind_mount_string_with_not_valid_option = "/home/test7:/home/test8:not_valid_option"
with pytest.raises(argparse.ArgumentTypeError):
create_bind_mount_from_string(valid_bind_mount_string_with_not_valid_option)

0 comments on commit 4369c3b

Please sign in to comment.