├── libioc ├── ioc_cli ├── shared │ ├── __init__.py │ ├── click.py │ ├── output.py │ └── jail.py ├── promote.py ├── rename.py ├── console.py ├── deactivate.py ├── import.py ├── activate.py ├── clone.py ├── pkg.py ├── update.py ├── restart.py ├── export.py ├── set.py ├── exec.py ├── provision.py ├── get.py ├── destroy.py ├── fetch.py ├── migrate.py ├── stop.py ├── start.py ├── snapshot.py ├── create.py ├── __init__.py ├── fstab.py └── list.py ├── .travis ├── constraints.txt └── mypy-stubs │ ├── ucl.pyi │ ├── texttable.pyi │ ├── pytest.pyi │ └── libzfs.pyi ├── requirements.txt ├── .gitmodules ├── requirements-dev.txt ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── bin └── ioc ├── setup.cfg ├── LICENSE.txt ├── rc.d └── ioc ├── .gitignore ├── Makefile ├── __main__.py ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── setup.py └── README.md /libioc: -------------------------------------------------------------------------------- 1 | .libioc/libioc -------------------------------------------------------------------------------- /ioc_cli/shared/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis/constraints.txt: -------------------------------------------------------------------------------- 1 | libucl==* 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.0 2 | texttable==1.6.1 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".libioc"] 2 | path = .libioc 3 | url = https://github.com/bsdci/libioc 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8-docstrings 2 | flake8-mutable 3 | flake8-builtins 4 | flake8-mypy 5 | bandit 6 | bandit-high-entropy-string 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Make sure to follow and check these boxes before submitting a PR! Thank you. 2 | 3 | - [ ] Explain the feature 4 | - [ ] Read [CONTRIBUTING.md](https://github.com/iocage/iocage/blob/master/CONTRIBUTING.md) 5 | -------------------------------------------------------------------------------- /bin/ioc: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3.6 2 | import sys 3 | import os.path 4 | 5 | LIB_DIR = "../" 6 | 7 | if LIB_DIR.startswith("/") is False: 8 | __dirname = os.path.dirname(os.path.abspath(__file__)) 9 | LIB_DIR = f"{__dirname}/{LIB_DIR}" 10 | 11 | sys.path.insert(0, os.path.abspath(LIB_DIR)) 12 | 13 | from ioc_cli import cli 14 | 15 | if __name__ == '__main__': 16 | sys.dd:exit(cli()) 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Make sure to follow and check these boxes before submitting an issue! Thank you. 2 | 3 | - [ ] Supply `ioc --version` 4 | - [ ] Supply the commands used, along with any steps to recreate it. 5 | - [ ] Provide the output from the command you issued. 6 | - [ ] Supply what you expected the result or output to be 7 | - [ ] Checked that the problem has not already been fixed on `master` if using 8 | a stable release. 9 | -------------------------------------------------------------------------------- /.travis/mypy-stubs/ucl.pyi: -------------------------------------------------------------------------------- 1 | # Stubs for ucl (Python 3.6) 2 | # 3 | # NOTE: This dynamically typed stub was automatically generated by stubgen. 4 | 5 | from typing import Dict, Any, Optional, Union 6 | 7 | UCL_EMIT_CONFIG = ... # type: int 8 | UCL_EMIT_JSON = ... # type: int 9 | UCL_EMIT_JSON_COMPACT = ... # type: int 10 | UCL_EMIT_MSGPACK = ... # type: int 11 | UCL_EMIT_YAML = ... # type: int 12 | 13 | def dump(data: Optional[Dict[str, Any]], *args: Any) -> str: ... 14 | def load(uclstr: str) -> Dict[str, Any]: ... 15 | def validate(uclstr: str) -> Optional[bool]: ... 16 | -------------------------------------------------------------------------------- /.travis/mypy-stubs/texttable.pyi: -------------------------------------------------------------------------------- 1 | # Stubs for texttable (Python 3.6) 2 | # 3 | # NOTE: This dynamically typed stub was automatically generated by stubgen. 4 | 5 | from typing import Any, List 6 | 7 | class ArraySizeError(Exception): 8 | msg = ... # type: Any 9 | def __init__(self, msg: Any) -> None: ... 10 | 11 | class Texttable: 12 | BORDER = ... # type: int 13 | HEADER = ... # type: Any 14 | HLINES = ... # type: Any 15 | VLINES = ... # type: Any 16 | def __init__(self, max_width: int = ...) -> None: ... 17 | def reset(self) -> None: ... 18 | def set_chars(self, array: List) -> None: ... 19 | def set_deco(self, deco: Any) -> None: ... 20 | def set_cols_align(self, array: List) -> None: ... 21 | def set_cols_valign(self, array: List) -> None: ... 22 | def set_cols_dtype(self, array: List) -> None: ... 23 | def set_cols_width(self, array: List) -> None: ... 24 | def set_precision(self, width: int) -> None: ... 25 | def header(self, array: List) -> None: ... 26 | def add_row(self, array: List) -> None: ... 27 | def add_rows( 28 | self, 29 | rows: List[List[str]], 30 | header: bool = ... 31 | ) -> None: ... 32 | def draw(self) -> str: ... 33 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [mypy] 5 | python_version = 3.6 6 | # override flake8-mypy defaults, since we provide (missing) types 7 | platform='freebsd' 8 | 9 | # flake8-mypy expects the two following for sensible formatting 10 | show_column_numbers=True 11 | show_error_context=False 12 | 13 | ignore_missing_imports=False 14 | follow_imports=True 15 | cache_dir=.mypy_cache 16 | 17 | # disallow untyped calls since we provide our own typedefs 18 | disallow_untyped_calls=True 19 | 20 | # don't allow just Any thing. 21 | warn_return_any=True 22 | 23 | # treat Optional per PEP 484 24 | strict_optional=True 25 | 26 | # ensure all execution paths are returning 27 | warn_no_return=True 28 | 29 | # lint-style cleanliness for typing needs to be disabled; returns more errors 30 | # than the full run. 31 | warn_redundant_casts=True 32 | warn_unused_ignores=True 33 | 34 | # The following are off by default. Flip them on if you feel 35 | # adventurous. 36 | 37 | check_untyped_defs=True 38 | disallow_untyped_defs=True 39 | 40 | [tool:pytest] 41 | addopts = -v -x -rs --ignore=setup.py --pep8 --cov-report term-missing --cov=iocage iocage tests 42 | pep8maxlinelength = 80 43 | pep8ignore = * ALL 44 | [aliases] 45 | test=pytest 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2019, Stefan Grönke 2 | Copyright (c) 2014-2018, iocage 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted providing that the following conditions 7 | are met: 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /.travis/mypy-stubs/pytest.pyi: -------------------------------------------------------------------------------- 1 | # Stubs for pytest (Python 3.6) 2 | # 3 | # NOTE: This dynamically typed stub was automatically generated by stubgen. 4 | 5 | from typing import Any 6 | from _pytest.config import main as main, UsageError as UsageError, cmdline as cmdline, hookspec as hookspec, hookimpl as hookimpl 7 | from _pytest.fixtures import fixture as fixture, yield_fixture as yield_fixture 8 | from _pytest.assertion import register_assert_rewrite as register_assert_rewrite 9 | from _pytest.freeze_support import freeze_includes as freeze_includes 10 | from _pytest import __version__ as __version__ 11 | from _pytest.debugging import pytestPDB as __pytestPDB 12 | from _pytest.recwarn import warns as warns, deprecated_call as deprecated_call 13 | from _pytest.runner import fail as fail, skip as skip, importorskip as importorskip, exit as exit 14 | from _pytest.mark import param as param 15 | from _pytest.mark import MARK_GEN as mark 16 | from _pytest.skipping import xfail as xfail 17 | from _pytest.main import Item as Item, Collector as Collector, File as File, Session as Session 18 | from _pytest.fixtures import fillfixtures as _fillfuncargs 19 | from _pytest.python import raises as raises, approx as approx, Module as Module, Class as Class, Instance as Instance, Function as Function, Generator as Generator 20 | 21 | set_trace = ... # type: Any 22 | 23 | # Names in __all__ with no definition: 24 | # _fillfuncargs 25 | # mark 26 | -------------------------------------------------------------------------------- /rc.d/ioc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # $FreeBSD$ 4 | # 5 | 6 | # PROVIDE: ioc 7 | # REQUIRE: LOGIN NETWORK cleanvar ZFS 8 | # BEFORE: securelevel 9 | # KEYWORD: shutdown 10 | 11 | # Add the following lines to /etc/rc.conf to enable ioc start on boot: 12 | # 13 | # ioc_enable="YES" 14 | # 15 | 16 | . /etc/rc.subr 17 | 18 | name="ioc" 19 | rcvar=ioc_enable 20 | 21 | # read configuration and set defaults 22 | load_rc_config "$name" 23 | : ${ioc_enable="NO"} 24 | : ${ioc_lang="en_US.UTF-8"} 25 | 26 | start_cmd="ioc_start" 27 | stop_cmd="ioc_stop" 28 | status_cmd="ioc_status" 29 | extra_commands="status" 30 | export LANG=$ioc_lang 31 | 32 | [ $# -ne 1 ] && rc_usage $_keywords 33 | 34 | ioc_start() 35 | { 36 | if checkyesno ${rcvar}; then 37 | echo "* [ioc] starting jails... " 38 | /usr/local/bin/ioc start --rc 39 | fi 40 | } 41 | 42 | ioc_stop() 43 | { 44 | if checkyesno ${rcvar}; then 45 | echo "* [ioc] stopping jails... " 46 | /usr/local/bin/ioc stop --rc 47 | fi 48 | } 49 | 50 | ioc_status() 51 | { 52 | if checkyesno ${rcvar}; then 53 | echo -n "* [ioc] checking jails status..." 54 | test -z "$(/usr/local/bin/ioc list boot=yes running=no template=no,- --no-header --output=name --output-format=list)" 55 | status=$? 56 | if test ${status} -eq 0; then 57 | echo " OK" 58 | else 59 | echo " Failed!" 60 | fi 61 | exit $status 62 | fi 63 | } 64 | 65 | run_rc_command "$1" 66 | -------------------------------------------------------------------------------- /ioc_cli/shared/click.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Click helpers for the CLI.""" 26 | import typing 27 | import click.core 28 | 29 | import libioc.events 30 | import libioc.Logger 31 | import libioc.Host 32 | 33 | 34 | class IocClickContext(click.core.Context): 35 | """ioc ctx for Click CLI.""" 36 | 37 | logger: libioc.Logger.Logger 38 | host: libioc.Host.Host 39 | parent: 'IocClickContext' 40 | print_events: typing.Callable[ 41 | [typing.Generator[libioc.events.IocEvent, None, None]], 42 | None 43 | ] 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated man pages are just for packaging 2 | man/*.gz 3 | 4 | # core dumps 5 | python3.6.core 6 | 7 | # Editor and OS Files 8 | *.swp 9 | .#* 10 | ._* 11 | .DS_Store 12 | .idea/ 13 | 14 | # Github Ignore patterns for Python 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | env/ 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | ./lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | wheels/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | .hypothesis/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | docs/*.rst 81 | docs/make.bat 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # dotenv 99 | .env 100 | 101 | # virtualenv 102 | .venv 103 | venv/ 104 | venv-*/ 105 | ENV/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | 120 | *.orig 121 | *.rej 122 | .pytest_cache/ 123 | .vscode/ 124 | docs/_build/ 125 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON_VERSION ?= $(TRAVIS_PYTHON_VERSION) 2 | SELECTED_PYTHON_VERSION != if [ "$(PYTHON_VERSION)" != "" ]; then echo $(PYTHON_VERSION); else pkg info -g 'python3*' | cut -d'-' -f1 | sed 's/^python//' | sort -n | tail -n1 | sed -r 's/^([0-9])([0-9]+)/\1.\2/'; fi 3 | PYTHON ?= python${SELECTED_PYTHON_VERSION} 4 | # turn python3.7 -> 3.7 -> 37 5 | pyver= ${PYTHON:S/^python//:S/.//:C/\([0-9]+\)/\1/} 6 | 7 | .if $(pyver) < 35 8 | . error "ioc cannot run with a Python version < 3.5" 9 | .endif 10 | 11 | deps: 12 | $(PYTHON) -m ensurepip 13 | $(PYTHON) -m pip install -Ur requirements.txt 14 | install-libioc: 15 | git submodule init 16 | git submodule update 17 | make -C .libioc/ install 18 | install-ioc: deps install-service 19 | $(PYTHON) -m pip install -U . 20 | install-service: 21 | @if [ -f /usr/local/etc/init.d ]; then \ 22 | install -m 0755 rc.d/ioc /usr/local/etc/init.d; \ 23 | else \ 24 | install -m 0755 rc.d/ioc /usr/local/etc/rc.d; \ 25 | fi 26 | install: install-libioc install-ioc 27 | install-dev: deps 28 | if [ "`uname`" = "FreeBSD" ]; then pkg install -y gmake; fi 29 | $(PYTHON) -m pip install -Ur requirements-dev.txt 30 | $(PYTHON) -m pip install -e . 31 | @if [ -f /usr/local/etc/init.d ]; then \ 32 | install -m 0755 -o root -g wheel rc.d/ioc /usr/local/etc/init.d; \ 33 | else \ 34 | install -m 0755 -o root -g wheel rc.d/ioc /usr/local/etc/rc.d; \ 35 | fi 36 | install-travis: 37 | $(PYTHON) -m pip install -U -r requirements-dev.txt 38 | uninstall: 39 | $(PYTHON) -m pip uninstall -y ioc_cli 40 | @if [ -f /usr/local/etc/rc.d/ioc ]; then \ 41 | rm /usr/local/etc/rc.d/ioc; \ 42 | fi 43 | check: 44 | flake8 --version 45 | flake8 --exclude=".eggs,__init__.py,docs" --ignore=E203,E252,W391,D107,A001,A002,A003,A004 46 | bandit --skip B404 --exclude tests/ -r . 47 | help: 48 | @echo " install" 49 | @echo " Installs ioc" 50 | @echo " uninstall" 51 | @echo " Removes ioc." 52 | @echo " check" 53 | @echo " Run static linters & other static analysis tests" 54 | @echo " install-dev" 55 | @echo " Install dependencies needed to run `check`" 56 | -------------------------------------------------------------------------------- /ioc_cli/promote.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Clone and promote jails.""" 26 | import click 27 | 28 | import libioc.errors 29 | import libioc.ZFS 30 | import libioc.Jail 31 | 32 | from .shared.click import IocClickContext 33 | 34 | __rootcmd__ = True 35 | 36 | 37 | @click.command(name="promote") 38 | @click.pass_context 39 | @click.argument( 40 | "jail", 41 | nargs=1, 42 | required=True 43 | ) 44 | def cli( 45 | ctx: IocClickContext, 46 | jail: str 47 | ) -> None: 48 | """Clone and promote jails.""" 49 | logger = ctx.parent.logger 50 | 51 | ioc_jail = libioc.Jail.JailGenerator( 52 | dict(id=jail), 53 | logger=logger, 54 | zfs=ctx.parent.zfs, 55 | host=ctx.parent.host 56 | ) 57 | 58 | try: 59 | ioc_jail.promote() 60 | except libioc.errors.IocException: 61 | exit(1) 62 | -------------------------------------------------------------------------------- /ioc_cli/rename.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Rename a jail.""" 26 | import click 27 | 28 | import libioc.Jail 29 | 30 | from .shared.click import IocClickContext 31 | 32 | __rootcmd__ = True 33 | 34 | 35 | @click.command(name="rename", help="Rename a stopped jail.") 36 | @click.pass_context 37 | @click.argument("jail", nargs=1) 38 | @click.argument("name", nargs=1) 39 | def cli( 40 | ctx: IocClickContext, 41 | jail: str, 42 | name: str 43 | ) -> None: 44 | """Rename a stopped jail.""" 45 | logger = ctx.parent.logger 46 | print_function = ctx.parent.print_events 47 | 48 | try: 49 | ioc_jail = libioc.Jail.Jail( 50 | jail, 51 | logger=logger, 52 | zfs=ctx.parent.zfs, 53 | host=ctx.parent.host, 54 | skip_invalid_config=True 55 | ) 56 | print_function(ioc_jail.rename(name)) 57 | except libioc.errors.IocException: 58 | exit(1) 59 | 60 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """The main CLI for ioc.""" 26 | import os 27 | import sys 28 | 29 | 30 | from ioc_cli import cli 31 | 32 | 33 | def main_safe(): 34 | """Wrap the main function and return raised errors.""" 35 | try: 36 | main() 37 | except BaseException as e: 38 | return e 39 | 40 | 41 | def main(): 42 | """Execute the main Click program.""" 43 | cli(prog_name="iocage") 44 | 45 | 46 | if __name__ == "__main__": 47 | coverdir = os.environ.get("IOCAGE_TRACE", None) 48 | if coverdir is None: 49 | main() 50 | else: 51 | import trace 52 | tracer = trace.Trace( 53 | ignoredirs=[sys.prefix, sys.exec_prefix], 54 | trace=0, 55 | count=1 56 | ) 57 | tracer.run("main_safe()") 58 | r = tracer.results() 59 | r.write_results(show_missing=True, coverdir=coverdir) 60 | print(f"Iocage Trace written to: {coverdir}") 61 | -------------------------------------------------------------------------------- /ioc_cli/console.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Console subcommand for the CLI.""" 26 | import click 27 | 28 | import libioc.Jail 29 | import libioc.Logger 30 | import libioc.errors 31 | 32 | __rootcmd__ = True 33 | 34 | 35 | @click.command(name="console", help="Login to a jail.") 36 | @click.pass_context 37 | @click.argument("jail") 38 | @click.option("--start", "-s", is_flag=True, default=False) 39 | def cli(ctx, jail, start): 40 | """Run jexec to login into the specified jail.""" 41 | logger = ctx.parent.logger 42 | 43 | try: 44 | ioc_jail = libioc.Jail.JailGenerator( 45 | jail, 46 | logger=logger, 47 | zfs=ctx.parent.zfs, 48 | host=ctx.parent.host 49 | ) 50 | except libioc.errors.JailNotFound: 51 | exit(1) 52 | 53 | try: 54 | if not ioc_jail.running: 55 | if start is True: 56 | ctx.parent.print_events(ioc_jail.start()) 57 | 58 | ioc_jail.exec_console() 59 | except libioc.errors.IocException: 60 | exit(1) 61 | 62 | -------------------------------------------------------------------------------- /ioc_cli/shared/output.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Use CLI helper functions for console output.""" 26 | import typing 27 | import texttable 28 | 29 | 30 | def print_table( 31 | data: typing.List[typing.List[str]], 32 | columns: typing.List[str], 33 | show_header: bool=True, 34 | sort_key: typing.Optional[str]=None 35 | ) -> None: 36 | """Print a table to stdout.""" 37 | table = texttable.Texttable(max_width=0) 38 | table.set_cols_dtype(["t"] * len(columns)) 39 | 40 | table_head = (list(x.upper() for x in columns)) 41 | table_data = data 42 | 43 | if sort_key is None: 44 | sort_index = -1 45 | else: 46 | try: 47 | sort_index = columns.index(sort_key) 48 | except ValueError: 49 | sort_index = -1 50 | 51 | if sort_index > -1: 52 | table_data.sort(key=lambda x: x[sort_index]) 53 | 54 | if show_header is True: 55 | table.add_rows([table_head] + table_data) 56 | else: 57 | table.add_rows(table_data, header=False) 58 | 59 | print(table.draw()) 60 | -------------------------------------------------------------------------------- /ioc_cli/deactivate.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Deactivate ZFS pools for iocage with the CLI.""" 26 | import click 27 | 28 | import libioc.errors 29 | import libioc.Datasets 30 | import libioc.Logger 31 | import libioc.ZFS 32 | 33 | __rootcmd__ = True 34 | 35 | 36 | @click.command(name="deactivate", help="Disable a ZFS pool for libioc.") 37 | @click.pass_context 38 | @click.argument("zpool") 39 | def cli(ctx, zpool): 40 | """Call ZFS set to change the property org.freebsd.ioc:active to no.""" 41 | logger = ctx.parent.logger 42 | zfs = ctx.parent.zfs 43 | 44 | try: 45 | iocage_pool = zfs.get(zpool) 46 | except Exception: 47 | logger.error(f"ZFS pool '{zpool}' not found") 48 | exit(1) 49 | 50 | try: 51 | datasets = libioc.Datasets.Datasets( 52 | zfs=zfs, 53 | logger=logger 54 | ) 55 | datasets.attach_source("iocage", f"{iocage_pool.name}/iocage") 56 | if datasets.is_pool_active(): 57 | datasets.deactivate() 58 | logger.log(f"ZFS pool '{zpool}' deactivated") 59 | else: 60 | logger.warn(f"ZFS pool '{zpool}' is not active") 61 | except libioc.errors.IocException: 62 | exit(1) 63 | -------------------------------------------------------------------------------- /ioc_cli/import.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Export a jail from the CLI.""" 26 | import click 27 | 28 | import libioc.errors 29 | import libioc.Jail 30 | import libioc.Host 31 | import libioc.ZFS 32 | 33 | from .shared.click import IocClickContext 34 | 35 | __rootcmd__ = True 36 | 37 | 38 | @click.command(name="import", help="Import a jail from a backup archive") 39 | @click.pass_context 40 | @click.argument("jail", required=True) 41 | @click.argument("source", required=True) 42 | def cli( 43 | ctx: IocClickContext, 44 | jail: str, 45 | source: str 46 | ) -> None: 47 | """Restore a jail from a backup archive.""" 48 | logger = ctx.parent.logger 49 | zfs: libioc.ZFS.ZFS = ctx.parent.zfs 50 | host: libioc.Host.HostGenerator = ctx.parent.host 51 | print_events = ctx.parent.print_events 52 | 53 | ioc_jail = libioc.Jail.JailGenerator( 54 | dict(name=jail), 55 | logger=logger, 56 | zfs=zfs, 57 | host=host, 58 | new=True 59 | ) 60 | 61 | if ioc_jail.exists is True: 62 | logger.error(f"The jail {jail} already exists") 63 | exit(1) 64 | 65 | try: 66 | print_events(ioc_jail.backup.restore(source)) 67 | except libioc.errors.IocException: 68 | exit(1) 69 | -------------------------------------------------------------------------------- /ioc_cli/activate.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Activate zfs pools for iocage with the CLI.""" 26 | import click 27 | 28 | import libioc.errors 29 | import libioc.Datasets 30 | import libioc.Logger 31 | import libioc.ZFS 32 | 33 | __rootcmd__ = True 34 | 35 | 36 | @click.command(name="activate", help="Set a zpool active for iocage usage.") 37 | @click.pass_context 38 | @click.argument("zpool") 39 | @click.option("--mountpoint", "-m", default="/iocage") 40 | def cli(ctx, zpool, mountpoint): 41 | """Call ZFS set to change the property org.freebsd.ioc:active to yes.""" 42 | logger = ctx.parent.logger 43 | zfs = ctx.parent.zfs 44 | 45 | if ctx.parent.user_sources is not None: 46 | logger.error("Cannot activate when executed with explicit sources.") 47 | exit(1) 48 | 49 | try: 50 | iocage_pool = zfs.get(zpool) 51 | except Exception: 52 | logger.error(f"ZFS pool '{zpool}' not found") 53 | exit(1) 54 | 55 | try: 56 | datasets = libioc.Datasets.Datasets( 57 | zfs=zfs, 58 | logger=logger 59 | ) 60 | datasets.activate_pool( 61 | pool=iocage_pool, 62 | mountpoint=mountpoint 63 | ) 64 | logger.log(f"ZFS pool '{zpool}' activated") 65 | except libioc.errors.IocException: 66 | exit(1) 67 | -------------------------------------------------------------------------------- /ioc_cli/clone.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Clone and promote jails.""" 26 | import click 27 | 28 | import libioc.errors 29 | import libioc.ZFS 30 | import libioc.Jail 31 | 32 | from .shared.click import IocClickContext 33 | 34 | __rootcmd__ = True 35 | 36 | 37 | def _is_dataset_name(name: str) -> bool: 38 | return "/" in name 39 | 40 | 41 | @click.command(name="clone") 42 | @click.pass_context 43 | @click.argument( 44 | "source", 45 | nargs=1, 46 | required=True 47 | ) 48 | @click.argument( 49 | "destination", 50 | nargs=1, 51 | required=True 52 | ) 53 | def cli( 54 | ctx: IocClickContext, 55 | source: str, 56 | destination: str 57 | ) -> None: 58 | """Clone and promote jails.""" 59 | print_function = ctx.parent.print_events 60 | logger = ctx.parent.logger 61 | 62 | ioc_source_jail = libioc.Jail.JailGenerator( 63 | dict(id=source), 64 | logger=logger, 65 | zfs=ctx.parent.zfs, 66 | host=ctx.parent.host 67 | ) 68 | 69 | ioc_destination_jail = libioc.Jail.JailGenerator( 70 | dict(id=destination), 71 | new=True, 72 | logger=logger, 73 | zfs=ctx.parent.zfs, 74 | host=ctx.parent.host 75 | ) 76 | 77 | try: 78 | print_function(ioc_destination_jail.clone_from_jail(ioc_source_jail)) 79 | except libioc.errors.IocException: 80 | exit(1) 81 | -------------------------------------------------------------------------------- /ioc_cli/shared/jail.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Get a specific jails with this CLI helper function.""" 26 | import typing 27 | 28 | import libioc.errors 29 | import libioc.Jail 30 | import libioc.Logger 31 | 32 | from .click import IocClickContext 33 | 34 | 35 | def get_jail( 36 | jail_name: str, 37 | ctx: IocClickContext, 38 | **jail_args: typing.Any 39 | ) -> libioc.Jail.JailGenerator: 40 | """Return the jail matching the given name.""" 41 | try: 42 | return libioc.Jail.JailGenerator( 43 | jail_name, 44 | logger=ctx.logger, 45 | host=ctx.host, 46 | **jail_args 47 | ) 48 | except libioc.errors.IocException: 49 | exit(1) 50 | 51 | 52 | def set_properties( 53 | properties: typing.Iterable[str], 54 | target: 'libioc.LaunchableResource.LaunchableResource', 55 | autosave: bool=True 56 | ) -> set: 57 | """Set a bunch of jail properties from a Click option tuple.""" 58 | new_data = dict() 59 | updated_properties = set() 60 | 61 | for prop in properties: 62 | 63 | if _is_setter_property(prop): 64 | key, value = prop.split("=", maxsplit=1) 65 | new_data[key] = value 66 | else: 67 | key = prop 68 | try: 69 | del target.config[key] 70 | updated_properties.add(key) 71 | except (libioc.errors.IocException, KeyError): 72 | pass 73 | 74 | updated_properties |= target.config.set_dict(new_data) 75 | 76 | if (len(updated_properties) > 0) and (autosave is True): 77 | target.save() 78 | 79 | return updated_properties 80 | 81 | 82 | def _is_setter_property(property_string: str) -> bool: 83 | return ("=" in property_string) 84 | -------------------------------------------------------------------------------- /ioc_cli/pkg.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Jail package management subcommand for the CLI.""" 26 | import typing 27 | import click 28 | 29 | import libioc.Jail 30 | import libioc.Pkg 31 | import libioc.Logger 32 | import libioc.errors 33 | 34 | from .shared.click import IocClickContext 35 | 36 | 37 | @click.command(name="pkg", help="Manage packages in a jail.") 38 | @click.pass_context 39 | @click.option( 40 | "--remove", "-r", 41 | "remove", 42 | is_flag=True, 43 | default=False, 44 | help="Remove the packages instead of installing/updating them." 45 | ) 46 | @click.argument("jail") 47 | @click.argument("packages", nargs=-1) 48 | def cli( 49 | ctx: IocClickContext, 50 | remove: bool, 51 | jail: str, 52 | packages: typing.Tuple[str, ...] 53 | ) -> None: 54 | """Manage packages within jails using an offline mirror.""" 55 | logger = ctx.parent.logger 56 | 57 | try: 58 | ioc_jail = libioc.Jail.JailGenerator( 59 | jail, 60 | logger=logger, 61 | zfs=ctx.parent.zfs, 62 | host=ctx.parent.host 63 | ) 64 | except libioc.errors.JailNotFound: 65 | exit(1) 66 | 67 | try: 68 | pkg = libioc.Pkg.Pkg( 69 | logger=logger, 70 | zfs=ctx.parent.zfs, 71 | host=ctx.parent.host 72 | ) 73 | if remove is False: 74 | events = pkg.fetch_and_install( 75 | jail=ioc_jail, 76 | packages=list(packages) 77 | ) 78 | else: 79 | events = pkg.remove( 80 | jail=ioc_jail, 81 | packages=list(packages) 82 | ) 83 | ctx.parent.print_events(events) 84 | except libioc.errors.IocException: 85 | exit(1) 86 | 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | Following the contribution guidelines saves everyone time, requires less back 4 | and forth during the review process, and helps to ensures a consistent codebase. 5 | I use PyCharm for all of my programming, and these are what I use for my settings, adapt to your editor of choice. 6 | 7 | ## A few general rules first: 8 | 9 | - Open any pull request against the `master` branch 10 | - Keep code to 80 characters or less. 11 | - Comment your code 12 | - Pull request description should clearly show what the change is including output if relevant. 13 | - Squash commits before opening a pull request. 14 | - Test and then test again! Make sure it works with the latest changes in `master`. 15 | - If adding a function, it must have a type signature. Examples can be found [here](https://www.python.org/dev/peps/pep-0484/) and [here](https://www.python.org/dev/peps/pep-3107/) 16 | - Spaces instead of Tabs 17 | - 4 spaces for first indent and each from then on. 18 | - Spaces around Assignment `(=, +=, …)`, Equality `(==, !=)`, Relational `(<, >, <=, >=)`, Bitwise `(*, |,^)`, Additive `(+, -)`, Multiplicative `(*, @, /, %)`, Shift `(<<, >>, >>>)` and Power operators `(**)`. 19 | - Spaces after: 20 | 21 | ```python 22 | , 23 | : 24 | # 25 | ``` 26 | 27 | - Spaces before: 28 | 29 | ```python 30 | \ 31 | # 32 | ``` 33 | 34 | - Align multiline method call arguments and method declaration parameters 35 | - New line after a colon 36 | - Align multiline import statements 37 | - Align multiline collections and comprehensions 38 | - Place `}` on a new line after a dictionary assignment 39 | - 1 line between declarations and code 40 | - 1 line after top level `imports` 41 | - 1 line around `class` 42 | - 1 line around `method` 43 | - 2 lines around top-level `classes` and `functions` 44 | - 1 line before and after `if` statements 45 | - 1 line before and after `for` loops 46 | - 1 line before and after `while` loops 47 | - Run isort on `import` statements, including `from imports`. 48 | - Keep `from imports` within their own group: 49 | 50 | ```python 51 | import bar 52 | import foo 53 | 54 | from baz import foo 55 | from foobar import morefoo 56 | ``` 57 | 58 | - Join `from imports` with the same source 59 | - Align dictionaries on colons: 60 | 61 | ```python 62 | x = max( 63 | 1, 64 | 2, 65 | 3) 66 | 67 | { 68 | “green” : 42, 69 | "eggs and ham": -0.0e0 70 | } 71 | ``` 72 | 73 | - Add a linefeed at the end of the file 74 | 75 | ## Running Tests 76 | 77 | Please run our Test suit before submitting a pull request. 78 | To do this you can run 79 | 80 | ```sh 81 | make check-deps # install dependencies needed to run check 82 | make check 83 | ``` 84 | 85 | The linters and static code analysis tools run in `check ` can be executed on _any_ system. 86 | For regression tests, you'll need a \*BSD system with a ZFS pool you can give `iocage` access to: 87 | 88 | ```sh 89 | setenv ZPOOL=jails 90 | make test 91 | ``` 92 | 93 | ## Documentation for Read The Docs 94 | 95 | If you wish to update some of our [documentation](http://iocage.readthedocs.org), you only need to submit a PR for the files you change in iocage/doc/source. They will automatically be updated when the changes are merged. 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ioc@bsd.ci. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /ioc_cli/update.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """ 26 | Apply operating system updates to jails. 27 | 28 | When a release was fetched, patches are downloaded. Standalone jails that 29 | were forked from a release that received new updates needs to apply them. 30 | No network connection is required, as the previously downloaded patches 31 | are temporarily mounted and applied with securelevel=0. 32 | """ 33 | import click 34 | import typing 35 | 36 | import libioc.errors 37 | import libioc.Jails 38 | import libioc.Logger 39 | import libioc.Config.Jail.File.Fstab 40 | 41 | from .shared.click import IocClickContext 42 | 43 | __rootcmd__ = True 44 | 45 | 46 | @click.command( 47 | name="update", 48 | help="Update a jail to a new release or patchlevel." 49 | ) 50 | @click.pass_context 51 | @click.argument("jails", nargs=-1) 52 | def cli( 53 | ctx: IocClickContext, 54 | jails: typing.Tuple[str, ...] 55 | ) -> typing.Optional[bool]: 56 | """Update jails with patches from their releases.""" 57 | logger = ctx.parent.logger 58 | print_function = ctx.parent.print_events 59 | 60 | if len(jails) == 0: 61 | logger.error("No jail selector provided") 62 | exit(1) 63 | 64 | filters = jails + ("template=no,-",) 65 | try: 66 | ioc_jails = libioc.Jails.JailsGenerator( 67 | logger=logger, 68 | host=ctx.parent.host, 69 | zfs=ctx.parent.zfs, 70 | filters=filters 71 | ) 72 | except libioc.errors.IocException: 73 | exit(1) 74 | 75 | changed_jails = [] 76 | failed_jails = [] 77 | for jail in ioc_jails: 78 | try: 79 | changed = print_function(jail.updater.apply()) 80 | if changed is True: 81 | changed_jails.append(jail) 82 | except libioc.errors.UpdateFailure: 83 | failed_jails.append(jail) 84 | 85 | if len(failed_jails) > 0: 86 | return False 87 | 88 | if len(changed_jails) == 0: 89 | jails_input = " ".join(list(jails)) 90 | logger.error( 91 | f"No non-basejail was updated or matched your input: {jails_input}" 92 | ) 93 | return False 94 | 95 | return True 96 | -------------------------------------------------------------------------------- /ioc_cli/restart.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Restart a jail with the CLI.""" 26 | import typing 27 | import click 28 | 29 | import libioc.errors 30 | import libioc.events 31 | import libioc.Jails 32 | import libioc.Logger 33 | 34 | from .shared.click import IocClickContext 35 | 36 | __rootcmd__ = True 37 | 38 | 39 | @click.command(name="restart", help="Restarts the specified jails.") 40 | @click.pass_context 41 | @click.option( 42 | '--shutdown', 43 | '-s', 44 | default=False, 45 | is_flag=True, 46 | help="Entirely shutdown jail during restart" 47 | ) 48 | @click.option( 49 | '--force', 50 | '-f', 51 | default=False, 52 | is_flag=True, 53 | help="Force jail shutdown during restart" 54 | ) 55 | @click.argument("jails", nargs=-1) 56 | def cli( 57 | ctx: IocClickContext, 58 | shutdown: bool, 59 | force: bool, 60 | jails: typing.Tuple[str, ...] 61 | ) -> None: 62 | """Restart a jail.""" 63 | logger = ctx.parent.logger 64 | print_function = ctx.parent.print_events 65 | 66 | # force implies shutdown 67 | if force is True: 68 | shutdown = True 69 | 70 | if len(jails) == 0: 71 | logger.error("No jail selector provided") 72 | exit(1) 73 | 74 | try: 75 | ioc_jails = libioc.Jails.JailsGenerator( 76 | host=ctx.parent.host, 77 | zfs=ctx.parent.zfs, 78 | logger=logger, 79 | filters=jails 80 | ) 81 | except libioc.errors.IocException: 82 | exit(1) 83 | 84 | changed_jails = [] 85 | failed_jails = [] 86 | for jail in ioc_jails: 87 | 88 | try: 89 | print_function(jail.restart( 90 | shutdown=shutdown, 91 | force=force 92 | )) 93 | changed_jails.append(jail) 94 | except StopIteration: 95 | failed_jails.append(jail) 96 | 97 | if len(failed_jails) > 0: 98 | exit(1) 99 | 100 | if len(changed_jails) == 0: 101 | jails_input = " ".join(list(jails)) 102 | logger.error(f"No jails matched your input: {jails_input}") 103 | exit(1) 104 | 105 | exit(0) 106 | -------------------------------------------------------------------------------- /ioc_cli/export.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Export a jail from the CLI.""" 26 | import click 27 | import os.path 28 | 29 | import libioc.errors 30 | import libioc.Filter 31 | import libioc.Jail 32 | import libioc.Jails 33 | import libioc.Logger 34 | import libioc.Releases 35 | import libioc.Resource 36 | 37 | from .shared.click import IocClickContext 38 | 39 | __rootcmd__ = True 40 | 41 | 42 | @click.command(name="export", help="Export a jail to a backup destination") 43 | @click.pass_context 44 | @click.argument("jail", required=True) 45 | @click.argument("destination", required=True) 46 | @click.option( 47 | "-s", "--standalone", 48 | default=False, 49 | is_flag=True, 50 | help="Exports the jails root dataset independently" 51 | ) 52 | @click.option( 53 | "--format", 54 | "_format", 55 | default="txz", 56 | type=click.Choice(["txz", "directory"]), 57 | help="Export the jail to a Tar Archive (compressed) or a new directory." 58 | ) 59 | # @click.option( 60 | # "-r", "--recursive", 61 | # default=False, 62 | # is_flag=True, 63 | # help="Include ZFS snapshots. Implies --standalone" 64 | # ) 65 | def cli( 66 | ctx: IocClickContext, 67 | jail: str, 68 | destination: str, 69 | standalone: bool, 70 | _format: str 71 | ) -> None: 72 | """ 73 | Backup a jail. 74 | 75 | The selected jail will be exported to a gzip compressed tar archive stored 76 | as the destination path. 77 | """ 78 | logger = ctx.parent.logger 79 | zfs: libioc.ZFS.ZFS = ctx.parent.zfs 80 | host: libioc.Host.HostGenerator = ctx.parent.host 81 | print_events = ctx.parent.print_events 82 | 83 | # Recursive exports cannot be imported at the current time 84 | recursive = False 85 | 86 | ioc_jail = libioc.Jail.JailGenerator( 87 | jail, 88 | logger=logger, 89 | zfs=zfs, 90 | host=host 91 | ) 92 | 93 | if os.path.isfile(destination) is True: 94 | logger.error(f"The destination {destination} already exists") 95 | exit(1) 96 | 97 | try: 98 | print_events(ioc_jail.backup.export( 99 | destination, 100 | standalone=standalone, 101 | recursive=recursive, 102 | backup_format=_format 103 | )) 104 | except libioc.errors.IocException: 105 | exit(1) 106 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2018, iocage 2 | # Copyright (c) 2017-2018, Stefan Grönke 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Installs libiocage using easy_install.""" 26 | import sys 27 | import typing 28 | from setuptools import find_packages, setup 29 | from setuptools.command import easy_install 30 | try: 31 | from pip._internal.req import parse_requirements 32 | except ModuleNotFoundError: 33 | from pip.req import parse_requirements 34 | 35 | 36 | def _read_requirements( 37 | filename: str="requirements.txt" 38 | ) -> typing.Dict[str, typing.List[str]]: 39 | reqs = list(parse_requirements(filename, session="ioc_cli")) 40 | return dict( 41 | install_requires=list(map(lambda x: f"{x.name}{x.specifier}", reqs)), 42 | dependency_links=list(map( 43 | lambda x: str(x.link), 44 | filter(lambda x: x.link, reqs) 45 | )) 46 | ) 47 | 48 | 49 | cli_requirements = _read_requirements("requirements.txt") 50 | 51 | TEMPLATE = '''\ 52 | # -*- coding: utf-8 -*- 53 | # EASY-INSTALL-ENTRY-SCRIPT: '{0}' 54 | __requires__ = '{0}' 55 | import sys 56 | 57 | from ioc_cli import cli 58 | 59 | if __name__ == '__main__': 60 | sys.dd:exit(cli())''' 61 | 62 | 63 | @classmethod # noqa: T484 64 | def get_args(cls, dist, header=None): # noqa: T484 65 | """Handle arguments for easy_install.""" 66 | if header is None: 67 | header = cls.get_header() 68 | 69 | script_text = TEMPLATE.format(str(dist.as_requirement())) 70 | args = cls._get_script_args("console", "ioc", header, script_text) 71 | 72 | for res in args: 73 | yield res 74 | 75 | 76 | easy_install.ScriptWriter.get_args = get_args 77 | 78 | 79 | if sys.version_info < (3, 6): 80 | exit("Only Python 3.6 and higher is supported.") 81 | 82 | setup( 83 | name='ioc_cli', 84 | license='BSD', 85 | version='0.8.2', 86 | description='A Python library to manage jails with iocage', 87 | keywords='FreeBSD jail iocage ioc', 88 | author='ioc Contributors', 89 | author_email='ioc@bsd.ci', 90 | url='https://github.com/iocage/libiocage', 91 | python_requires='>=3.6', 92 | packages=find_packages(include=["ioc_cli", "ioc_cli.*"]), 93 | include_package_data=True, 94 | install_requires=cli_requirements["install_requires"], 95 | dependency_links=cli_requirements["dependency_links"], 96 | entry_points={ 97 | 'console_scripts': [ 98 | 'ioc=ioc_cli:cli' 99 | ] 100 | } 101 | ) 102 | -------------------------------------------------------------------------------- /ioc_cli/set.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Set configuration values from the CLI.""" 26 | import typing 27 | import click 28 | 29 | import libioc.errors 30 | import libioc.Logger 31 | import libioc.helpers 32 | import libioc.Resource 33 | import libioc.Jails 34 | 35 | from .shared.jail import set_properties 36 | 37 | __rootcmd__ = True 38 | 39 | 40 | @click.command( 41 | context_settings=dict(max_content_width=400,), 42 | name="set", 43 | help="Sets the specified property." 44 | ) 45 | @click.pass_context 46 | @click.argument("props", nargs=-1) 47 | @click.argument("jail", nargs=1, required=True) 48 | def cli( 49 | ctx: click.core.Context, 50 | props: typing.Tuple[str, ...], 51 | jail: str 52 | ) -> None: 53 | """Set one or many configuration properties of one jail.""" 54 | parent: typing.Any = ctx.parent 55 | logger: libioc.Logger.Logger = parent.logger 56 | host: libioc.Host.HostGenerator = parent.host 57 | 58 | # Defaults 59 | if jail == "defaults": 60 | try: 61 | updated_properties = set_properties( 62 | properties=props, 63 | target=host.defaults 64 | ) 65 | except libioc.errors.IocException: 66 | exit(1) 67 | 68 | if len(updated_properties) > 0: 69 | logger.screen("Defaults updated: " + ", ".join(updated_properties)) 70 | else: 71 | logger.screen("Defaults unchanged") 72 | return 73 | 74 | filters = (f"name={jail}",) 75 | 76 | try: 77 | ioc_jails = libioc.Jails.JailsGenerator( 78 | filters, 79 | host=host, 80 | logger=logger, 81 | skip_invalid_config=True 82 | ) 83 | except libioc.errors.IocException: 84 | exit(1) 85 | 86 | updated_jail_count = 0 87 | 88 | for ioc_jail in ioc_jails: # type: libioc.Jail.JailGenerator 89 | 90 | try: 91 | updated_properties = set_properties( 92 | properties=props, 93 | target=ioc_jail 94 | ) 95 | except libioc.errors.IocException: 96 | exit(1) 97 | 98 | if len(updated_properties) == 0: 99 | logger.screen(f"Jail '{ioc_jail.humanreadable_name}' unchanged") 100 | else: 101 | _properties = ", ".join(updated_properties) 102 | logger.screen( 103 | f"Jail '{ioc_jail.humanreadable_name}' updated: {_properties}" 104 | ) 105 | 106 | updated_jail_count += 1 107 | 108 | if updated_jail_count == 0: 109 | logger.error("No jails to update") 110 | exit(1) 111 | 112 | exit(0) 113 | -------------------------------------------------------------------------------- /ioc_cli/exec.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Execute commands in jails from the CLI.""" 26 | import click 27 | import typing 28 | import shlex 29 | 30 | import libioc.Jail 31 | import libioc.Logger 32 | 33 | from .shared.click import IocClickContext 34 | 35 | __rootcmd__ = True 36 | 37 | 38 | @click.command( 39 | context_settings=dict(ignore_unknown_options=True), 40 | name="exec" 41 | ) 42 | @click.pass_context 43 | @click.option( 44 | "--user", 45 | "-u", 46 | help="The jail user who executes the command." 47 | ) 48 | @click.option( 49 | "--fork", 50 | "-f", 51 | is_flag=True, 52 | default=False, 53 | help="Spawns a jail to execute the command." 54 | ) 55 | @click.argument("jail", required=True, nargs=1) 56 | @click.argument("command", nargs=-1, type=click.UNPROCESSED) 57 | def cli( 58 | ctx: IocClickContext, 59 | command: typing.List[str], 60 | jail: str, 61 | user: typing.Optional[str], 62 | fork: bool, 63 | ) -> None: 64 | """ 65 | Run the given command inside the specified jail. 66 | 67 | When executing commands with own options or flags the end of ioc options 68 | can be marked with a double-dash or the full command can be quoted: 69 | 70 | ioc exec myjail -- ps -aux 71 | """ 72 | logger = ctx.parent.logger 73 | 74 | if jail.startswith("-"): 75 | logger.error("Please specify a jail first!") 76 | exit(1) 77 | 78 | command_list = list(command) 79 | 80 | if user is not None: 81 | user_command = " ".join(command_list) 82 | command_list = [ 83 | "/usr/bin/su", 84 | "-m", 85 | shlex.quote(user), 86 | "-c", 87 | shlex.quote(user_command) 88 | ] 89 | 90 | ioc_jail = libioc.Jail.JailGenerator( 91 | jail, 92 | logger=logger, 93 | zfs=ctx.parent.zfs, 94 | host=ctx.parent.host 95 | ) 96 | 97 | if not ioc_jail.exists: 98 | logger.error(f"The jail {ioc_jail.humanreadable_name} does not exist") 99 | exit(1) 100 | 101 | if (fork is False) and (ioc_jail.running is False): 102 | logger.error(f"The jail {ioc_jail.humanreadable_name} is not running") 103 | exit(1) 104 | 105 | try: 106 | if fork is True: 107 | events = ioc_jail.fork_exec( 108 | " ".join(command_list), 109 | passthru=True, 110 | exec_timeout=0 111 | ) 112 | for event in events: 113 | continue 114 | else: 115 | ioc_jail.passthru(command_list) 116 | except libioc.errors.IocException: 117 | exit(1) 118 | -------------------------------------------------------------------------------- /ioc_cli/provision.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Provision jails from the CLI..""" 26 | import typing 27 | import click 28 | 29 | import libioc.errors 30 | import libioc.Jails 31 | import libioc.Logger 32 | 33 | from .shared.click import IocClickContext 34 | from .shared.jail import set_properties 35 | 36 | __rootcmd__ = True 37 | 38 | 39 | @click.command(name="start", help="Trigger provisioning of jails.") 40 | @click.pass_context 41 | @click.argument("jails", nargs=-1) 42 | @click.option( 43 | "--option", "-o", 44 | "temporary_config_override", 45 | multiple=True, 46 | help="Temporarily override jail config options" 47 | ) 48 | def cli( 49 | ctx: IocClickContext, 50 | jails: typing.Tuple[str, ...], 51 | temporary_config_override: typing.Tuple[str, ...] 52 | ) -> None: 53 | """Run jail provisioner as defined in jail config.""" 54 | logger = ctx.parent.logger 55 | start_args = { 56 | "zfs": ctx.parent.zfs, 57 | "host": ctx.parent.host, 58 | "logger": logger, 59 | "print_function": ctx.parent.print_events 60 | } 61 | 62 | if not _provision( 63 | filters=jails, 64 | temporary_config_override=temporary_config_override, 65 | **start_args 66 | ): 67 | exit(1) 68 | 69 | 70 | def _provision( 71 | filters: typing.Tuple[str, ...], 72 | temporary_config_override: typing.Tuple[str, ...], 73 | zfs: libioc.ZFS.ZFS, 74 | host: libioc.Host.HostGenerator, 75 | logger: libioc.Logger.Logger, 76 | print_function: typing.Callable[ 77 | [typing.Generator[libioc.events.IocEvent, None, None]], 78 | None 79 | ] 80 | ) -> bool: 81 | 82 | jails = libioc.Jails.JailsGenerator( 83 | logger=logger, 84 | zfs=zfs, 85 | host=host, 86 | filters=filters 87 | ) 88 | 89 | changed_jails = [] 90 | failed_jails = [] 91 | for jail in jails: 92 | try: 93 | set_properties( 94 | properties=temporary_config_override, 95 | target=jail, 96 | autosave=False 97 | ) 98 | except libioc.errors.IocException: 99 | exit(1) 100 | 101 | try: 102 | print_function(_execute_provisioner(jail)) 103 | except libioc.errors.IocException: 104 | failed_jails.append(jail) 105 | continue 106 | 107 | changed_jails.append(jail) 108 | 109 | if len(failed_jails) > 0: 110 | return False 111 | 112 | if len(changed_jails) == 0: 113 | jails_input = " ".join(list(jails)) 114 | logger.error(f"No jails started your input: {jails_input}") 115 | return False 116 | 117 | return True 118 | 119 | 120 | def _execute_provisioner( 121 | jail: 'libioc.Jail.JailsGenerator' 122 | ) -> typing.Generator['libioc.events.IocEvent', None, None]: 123 | for event in jail.provisioner.provision(): 124 | yield event 125 | if isinstance(event, libioc.events.JailCommand): 126 | if event.done is True: 127 | print(event.stdout) 128 | -------------------------------------------------------------------------------- /ioc_cli/get.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Get a configuration value from the CLI.""" 26 | import typing 27 | import click 28 | 29 | import libioc.errors 30 | import libioc.Host 31 | import libioc.Jail 32 | import libioc.Logger 33 | 34 | from .shared.click import IocClickContext 35 | 36 | 37 | @click.command( 38 | context_settings=dict(max_content_width=400,), 39 | name="get", 40 | help="""Gets the specified property. 41 | 42 | Specify an individual jail by its name or use `defaults` to get the host's 43 | defaults from the main source dataset. 44 | """ 45 | ) 46 | @click.pass_context 47 | @click.argument("prop", nargs=-1, required=False, default=None) 48 | @click.argument("jail", nargs=1, required=True) 49 | @click.option( 50 | "--all", "-a", "_all", 51 | help="Get all properties for the specified jail.", 52 | is_flag=True 53 | ) 54 | def cli( 55 | ctx: IocClickContext, 56 | prop: typing.Tuple[str], 57 | _all: bool, 58 | jail: typing.Optional[str] 59 | ) -> None: 60 | """Get a list of jails and print the property.""" 61 | logger = ctx.parent.logger 62 | host = libioc.Host.Host(logger=logger) 63 | 64 | _prop = None if len(prop) == 0 else prop[0] 65 | 66 | if _all is True: 67 | if jail is None: 68 | jail = _prop 69 | _prop = None 70 | 71 | if _prop == "all": 72 | _prop = None 73 | 74 | if jail == "defaults": 75 | source_resource = host.defaults 76 | source_resource.read_config() 77 | lookup_method = _lookup_config_value 78 | else: 79 | lookup_method = _lookup_jail_value 80 | try: 81 | source_resource = libioc.Jail.Jail( 82 | jail, 83 | host=host, 84 | logger=logger, 85 | skip_invalid_config=True 86 | ) 87 | except libioc.errors.JailNotFound: 88 | exit(1) 89 | 90 | if (_prop is None) and (jail == "") and not _all: 91 | logger.error("Missing arguments property and jail") 92 | exit(1) 93 | elif (_prop is not None) and (jail == ""): 94 | logger.error("Missing argument property name or -a/--all argument") 95 | exit(1) 96 | 97 | if _prop: 98 | try: 99 | value = lookup_method(source_resource, _prop) 100 | except libioc.errors.IocException: 101 | exit(1) 102 | 103 | if value: 104 | print(value) 105 | return 106 | else: 107 | logger.error(f"Unknown property '{_prop}'") 108 | exit(1) 109 | 110 | for key in source_resource.config.all_properties: 111 | if (_prop is None) or (key == _prop): 112 | value = source_resource.config.get_string(key) 113 | _print_property(key, value) 114 | 115 | 116 | def _print_property(key: str, value: str) -> None: 117 | print(f"{key}:{value}") 118 | 119 | 120 | def _lookup_config_value( 121 | resource: 'libioc.Resource.Resource', 122 | key: str 123 | ) -> str: 124 | return str(libioc.helpers.to_string(resource.config[key])) 125 | 126 | 127 | def _lookup_jail_value( 128 | resource: 'libioc.LaunchableResource.LaunchableResource', 129 | key: str 130 | ) -> str: 131 | 132 | if key == "running": 133 | value = resource.running 134 | else: 135 | value = resource.getstring(key) 136 | 137 | return str(libioc.helpers.to_string(value)) 138 | -------------------------------------------------------------------------------- /ioc_cli/destroy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Destroy a jail from the CLI.""" 26 | import click 27 | import typing 28 | 29 | import libioc.errors 30 | import libioc.Filter 31 | import libioc.Jail 32 | import libioc.Jails 33 | import libioc.Logger 34 | import libioc.Releases 35 | import libioc.Resource 36 | 37 | from .shared.click import IocClickContext 38 | 39 | __rootcmd__ = True 40 | 41 | 42 | @click.command(name="destroy", help="Destroy specified resource") 43 | @click.pass_context 44 | @click.option("--force", "-f", default=False, is_flag=True, 45 | help="Destroy the jail without warnings or more user input.") 46 | @click.option( 47 | "--template", "-t", 48 | "dataset_type", 49 | flag_value="template", 50 | help="List all templates." 51 | ) 52 | @click.option( 53 | "--release", "-r", 54 | "dataset_type", 55 | flag_value="release", 56 | help="Destroy a specified RELEASE dataset." 57 | ) 58 | @click.option("--recursive", "-R", default=False, is_flag=True, 59 | help="Bypass the children prompt, best used with --force (-f).") 60 | @click.argument("filters", nargs=-1) 61 | def cli( 62 | ctx: IocClickContext, 63 | force: bool, 64 | dataset_type: typing.Optional[str], 65 | recursive: bool, 66 | filters: typing.Tuple[str, ...] 67 | ) -> None: 68 | """ 69 | Destroy a jail, release or template. 70 | 71 | Looks for the jail supplied and passes the uuid, path and configuration 72 | location to stop_jail. 73 | """ 74 | logger = ctx.parent.logger 75 | 76 | if filters is None or len(filters) == 0: 77 | logger.error("No filter specified - cannot select a target to delete") 78 | exit(1) 79 | 80 | if dataset_type is None: 81 | filters += ("template=no,-",) 82 | dataset_type = "jail" 83 | elif dataset_type == "template": 84 | filters += ("template=yes",) 85 | 86 | release = (dataset_type == "release") is True 87 | 88 | resources_class: typing.Union[ 89 | typing.Type[libioc.Releases.ReleasesGenerator], 90 | typing.Type[libioc.Jails.JailsGenerator] 91 | ] 92 | if release is True: 93 | resources_class = libioc.Releases.ReleasesGenerator 94 | resource_arguments = dict() 95 | else: 96 | resources_class = libioc.Jails.JailsGenerator 97 | resource_arguments = dict(skip_invalid_config=True) 98 | 99 | try: 100 | resources = list(resources_class( 101 | filters=filters, 102 | zfs=ctx.parent.zfs, 103 | host=ctx.parent.host, 104 | logger=logger, 105 | **resource_arguments 106 | )) 107 | except libioc.errors.IocException: 108 | exit(1) 109 | 110 | if len(resources) == 0: 111 | logger.error("No target matched your input") 112 | exit(1) 113 | 114 | if not force: 115 | _msg = f"These {dataset_type}s will be deleted" 116 | message = "\n- ".join( 117 | [_msg] + [r.getstring('full_name') for r in resources] 118 | ) + "\nAre you sure?" 119 | click.confirm(message, default=False, abort=True) 120 | 121 | failed_items = [] 122 | 123 | for item in resources: 124 | 125 | old_mountpoint = item.dataset.mountpoint 126 | 127 | if (not release and force and item.running) is True: 128 | ctx.parent.print_events(item.stop(force=True)) 129 | 130 | try: 131 | ctx.parent.print_events(item.destroy()) 132 | logger.screen(f"{old_mountpoint} destroyed") 133 | except libioc.errors.IocException: 134 | failed_items.append(item) 135 | 136 | if len(failed_items) > 0: 137 | exit(1) 138 | -------------------------------------------------------------------------------- /ioc_cli/fetch.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Fetch releases and updates with the CLI.""" 26 | import click 27 | import typing 28 | 29 | import libioc.Host 30 | import libioc.Prompts 31 | import libioc.Release 32 | import libioc.errors 33 | 34 | from .shared.click import IocClickContext 35 | 36 | __rootcmd__ = True 37 | 38 | 39 | @click.command( 40 | # context_settings=dict(max_content_width=400), 41 | name="fetch", 42 | help="Fetch and update a Release to create Jails from them." 43 | ) 44 | @click.pass_context 45 | @click.option( 46 | "--url", "-u", 47 | help="Remote URL with path to the release/snapshot directory" 48 | ) 49 | @click.option( # noqa: T484 50 | "--file", "-F", # noqa: T484 51 | multiple=True, 52 | help="Specify the files to fetch from the mirror." 53 | ) 54 | @click.option( 55 | "--release", "-r", 56 | help="The FreeBSD release to fetch." 57 | ) 58 | @click.option( 59 | "--update/--no-update", "-U/-NU", 60 | default=True, 61 | help="Update the release to the latest patch level." 62 | ) 63 | @click.option( 64 | "--fetch-updates/--no-fetch-updates", 65 | default=True, 66 | help="Skip fetching release updates" 67 | ) 68 | @click.option( # Compatibility 69 | "--http", "-h", 70 | default=False, 71 | is_flag=True, 72 | help="Have --server define a HTTP server instead." 73 | ) 74 | @click.option( # Basejail Update 75 | "--copy-basejail-only", 76 | "-b", 77 | is_flag=True, 78 | default=False, 79 | help="Update basejail after changes" 80 | ) 81 | @click.option( # Compatibility 82 | "--files", 83 | multiple=True, 84 | help=( 85 | "Specify the files to fetch from the mirror. " 86 | "(Deprecared: renamed to --file)" 87 | ) 88 | ) 89 | def cli( # noqa: T484 90 | ctx: IocClickContext, 91 | **kwargs 92 | ) -> None: 93 | """Fetch and update releases.""" 94 | logger = ctx.parent.logger 95 | host = ctx.parent.host 96 | zfs = ctx.parent.zfs 97 | prompts = libioc.Prompts.Prompts(host=host, logger=logger) 98 | 99 | release_input = kwargs["release"] 100 | if release_input is None: 101 | try: 102 | release = prompts.release() 103 | except libioc.errors.DefaultReleaseNotFound: 104 | exit(1) 105 | else: 106 | try: 107 | release = libioc.Release.ReleaseGenerator( 108 | name=release_input, 109 | host=host, 110 | zfs=zfs, 111 | logger=logger 112 | ) 113 | except libioc.errors.IocException: 114 | exit(1) 115 | 116 | if kwargs["copy_basejail_only"] is True: 117 | try: 118 | release.update_base_release() 119 | exit(0) 120 | except libioc.errors.IocException: 121 | exit(1) 122 | 123 | url_or_files_selected = False 124 | 125 | if _is_option_enabled(kwargs, "url"): 126 | release.mirror_url = kwargs["url"] 127 | url_or_files_selected = True 128 | 129 | if _is_option_enabled(kwargs, "files"): 130 | release.assets = list(kwargs["files"]) 131 | url_or_files_selected = True 132 | 133 | if (url_or_files_selected is False) and (release.available is False): 134 | logger.error(f"The release '{release.name}' is not available") 135 | exit(1) 136 | 137 | fetch_updates = bool(kwargs["fetch_updates"]) 138 | try: 139 | ctx.parent.print_events(release.fetch( 140 | update=kwargs["update"], 141 | fetch_updates=fetch_updates 142 | )) 143 | except libioc.errors.IocException: 144 | exit(1) 145 | 146 | exit(0) 147 | 148 | 149 | def _is_option_enabled(args: typing.Dict[str, typing.Any], name: str) -> bool: 150 | try: 151 | value = args[name] 152 | if value: 153 | return True 154 | except KeyError: 155 | pass 156 | 157 | return False 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ioc 2 | 3 | **The libioc command line tool for FreeBSD / HardenedBSD jail management** 4 | 5 | ioc originates from the FreeBSD jail manager iocage. 6 | 7 | ## Compatibility 8 | 9 | - python-iocage < 1.0 (JSON file) 10 | - iocell (UCL file) 11 | - iocage\_legacy @ master (UCL file) 12 | - iocage\_legacy @ v1.7.6 (ZFS properties) 13 | 14 | Jails created with either or mixed versions of the above implementations can be modified and used with ioc. 15 | For performance reasons a migration to the latest configuration format is recommended: 16 | 17 | ```sh 18 | ioc set config_type=json basejail=yes basejail_type=nullfs 19 | ``` 20 | 21 | ## Install 22 | 23 | ```sh 24 | git clone https://github.com/bsdci/ioc 25 | cd ioc 26 | make install 27 | ``` 28 | 29 | At the current time ioc is not packaged or available in FreeBSD ports yet. A request for [sysutilc/ioc](https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234816) is in review on bugs.freebsd.org. 30 | 31 | This Python module does not ship with a CLI tool. The project an installation instructions can be foun on [bsdci/ioc](https://github.com/bsdci/ioc). 32 | 33 | ## Documentation 34 | 35 | - ioc Handbook: https://bsdci.github.io/handbook 36 | - libioc Reference Documentation: https://bsdci.github.io/libioc 37 | - Gitter Chat: https://gitter.im/libioc/community 38 | 39 | ## Configuration 40 | 41 | ### Active ZFS pool 42 | 43 | libioc iterates over existing ZFS pools and stops at the first one with ZFS property `org.freebsd.ioc:active` set to `yes`. 44 | This behavior is the default used by prior iocage variants and is restricted to one pool managed by iocage. 45 | 46 | One or many datasets can be activated from rc.conf entries, replacing ZFS property activated pools. 47 | 48 | ### Root Datasets configured in /etc/rc.conf 49 | 50 | When ioc datasets are specified in the jail hosts `/etc/rc.conf`, libioc prefers them over activated pool lookups. 51 | Every ZFS filesystem that ioc should use as root dataset has a distinct name and is configured as `ioc_dataset_="zroot/some-dataset/ioc"`, for example: 52 | 53 | ``` 54 | $ cat /etc/rc.conf | grep ^ioc_dataset 55 | ioc_dataset_mysource="zroot/mysource/ioc" 56 | ioc_dataset_iocage="zroot/iocage" 57 | ``` 58 | 59 | ioc commands default to the first root data source specified in the file. 60 | Operations can be pointed to an alternative root by prefixing the subject with the source name followed by a slash. 61 | 62 | ```sh 63 | ioc create othersource/myjail 64 | ioc rename othersource/myjail myjail2 65 | ``` 66 | 67 | When `othersource` is the only datasource with a jail named `myjail` the above operation would have worked without explicitly stating the dataset name. 68 | 69 | ## Command Line Interface 70 | 71 | The CLI tool called `ioc` is powered by libioc. 72 | It is inspired by the command line interface of [iocage](https://github.com/iocage/iocage) but meant to be developed along with [libioc](https://github.com/bsdci/libioc) and aims to improve stability and performance of prior implementations. 73 | 74 | ``` 75 | Usage: ioc [OPTIONS] COMMAND [ARGS]... 76 | 77 | A jail manager. 78 | 79 | Options: 80 | --version Show the version and exit. 81 | --source TEXT Globally override the activated iocage dataset(s) 82 | -d, --log-level TEXT Set the CLI log level ('critical', 'error', 'warn', 83 | 'info', 'notice', 'verbose', 'debug', 'spam', 84 | 'screen') 85 | --help Show this message and exit. 86 | 87 | Commands: 88 | activate Set a zpool active for iocage usage. 89 | clone Clone and promote jails. 90 | console Login to a jail. 91 | create Create a jail. 92 | deactivate Disable a ZFS pool for iocage. 93 | destroy Destroy specified resource 94 | exec Run a command inside a specified jail. 95 | export Export a jail to a backup archive 96 | fetch Fetch and update a Release to create Jails... 97 | fstab View and manipulate a jails fstab file. 98 | get Gets the specified property. 99 | import Import a jail from a backup archive 100 | list List a specified dataset type, by default... 101 | migrate Migrate jails to the latest format. 102 | pkg Manage packages in a jail. 103 | promote Clone and promote jails. 104 | provision Trigger provisioning of jails. 105 | rename Rename a stopped jail. 106 | restart Restarts the specified jails. 107 | set Sets the specified property. 108 | snapshot Take and manage resource snapshots. 109 | start Starts the specified jails or ALL. 110 | stop Stops the specified jails or ALL. 111 | update Starts the specified jails or ALL. 112 | ``` 113 | 114 | ### Custom Release (e.g. running -CURRENT) 115 | 116 | #### Initially create the release dataset 117 | 118 | ```sh 119 | zfs create zroot/ioc/releases/custom/root 120 | cd /usr/src 121 | # install your source tree 122 | make installworld DESTDIR=/ioc/releases/custom/root 123 | make distribution DESTDIR=/ioc/releases/custom/root 124 | ioc fetch -r custom -b 125 | ``` 126 | 127 | #### Update the installation after recompile 128 | ```sh 129 | make installworld DESTDIR=/ioc/releases/custom/root 130 | ioc fetch -r custom -b 131 | ``` 132 | 133 | ## Development 134 | 135 | ### Static Code Analysis 136 | 137 | The project enforces PEP-8 code style and MyPy strong typing via flake8, that is required to pass before merging any changes. 138 | Together with Bandit checks for common security issues the static code analysis can be ran on Linux and BSD as both do not require py-libzfs or code execution. 139 | 140 | ``` 141 | make install-dev 142 | make check 143 | ``` 144 | 145 | -------------------------------------------------------------------------------- /ioc_cli/migrate.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2017, iocage 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted providing that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 14 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 17 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 21 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 22 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | # POSSIBILITY OF SUCH DAMAGE. 24 | """Migrate jails to the latest format (python-iocage).""" 25 | import typing 26 | import click 27 | 28 | import libioc.events 29 | import libioc.errors 30 | import libioc.helpers 31 | import libioc.Jails 32 | import libioc.Logger 33 | 34 | from .shared.click import IocClickContext 35 | 36 | __rootcmd__ = True 37 | 38 | 39 | class JailMigrationEvent(libioc.events.IocEvent): 40 | """CLI event that occurs when a jail is migrated from legacy format.""" 41 | 42 | def __init__( 43 | self, 44 | jail: 'libioc.Jail.JailGenerator' 45 | ) -> None: 46 | self.identifier = jail.full_name 47 | libioc.events.IocEvent.__init__(self) 48 | 49 | 50 | @click.command(name="migrate", help="Migrate jails to the latest format.") 51 | @click.pass_context 52 | @click.argument("jails", nargs=-1) 53 | def cli( 54 | ctx: IocClickContext, 55 | jails: typing.Tuple[str, ...] 56 | ) -> None: 57 | """Start one or many jails.""" 58 | logger = ctx.parent.logger 59 | zfs = libioc.ZFS.get_zfs(logger=logger) 60 | host = libioc.Host.HostGenerator(logger=logger, zfs=zfs) 61 | 62 | filters = jails + ("template=no,-",) 63 | 64 | ioc_jails = libioc.Jails.JailsGenerator( 65 | filters, 66 | logger=logger, 67 | host=host, 68 | zfs=zfs 69 | ) 70 | 71 | if len(ioc_jails) == 0: 72 | logger.error(f"No jails started your input: {jails}") 73 | exit(1) 74 | 75 | ctx.parent.print_events(_migrate_jails( 76 | ioc_jails, 77 | logger=logger, 78 | zfs=zfs, 79 | host=host 80 | )) 81 | 82 | 83 | def _migrate_jails( 84 | jails: 'libioc.Jails.JailsGenerator', 85 | logger: 'libioc.Logger.Logger', 86 | host: 'libioc.Host.HostGenerator', 87 | zfs: 'libioc.ZFS.ZFS' 88 | ) -> typing.Generator['libioc.events.IocEvent', None, None]: 89 | 90 | for jail in jails: 91 | 92 | event = JailMigrationEvent(jail=jail) 93 | yield event.begin() 94 | 95 | if jail.config.legacy is False: 96 | yield event.skip() 97 | continue 98 | 99 | if jail.running is True: 100 | yield event.fail(libioc.errors.JailAlreadyRunning( 101 | jail=jail, 102 | logger=logger 103 | )) 104 | continue 105 | 106 | if libioc.helpers.validate_name(jail.config["tag"]): 107 | name = jail.config["tag"] 108 | temporary_name = name 109 | else: 110 | name = jail.humanreadable_name 111 | temporary_name = "import-" + str(hash(name) % (1 << 32)) 112 | 113 | try: 114 | new_jail = libioc.Jail.JailGenerator( 115 | dict(name=temporary_name), 116 | root_datasets_name=jail.root_datasets_name, 117 | new=True, 118 | logger=logger, 119 | zfs=zfs, 120 | host=host 121 | ) 122 | if new_jail.exists is True: 123 | raise libioc.errors.JailAlreadyExists( 124 | jail=new_jail, 125 | logger=logger 126 | ) 127 | 128 | def _destroy_unclean_migration() -> typing.Generator[ 129 | 'libioc.events.IocEvents', 130 | None, 131 | None 132 | ]: 133 | _name = new_jail.humanreadable_name 134 | logger.verbose( 135 | f"Destroying unfinished migration target jail {_name}" 136 | ) 137 | yield from new_jail.destroy( 138 | force=True, 139 | event_scope=event.scope 140 | ) 141 | event.add_rollback_step(_destroy_unclean_migration) 142 | 143 | yield from new_jail.clone_from_jail(jail, event_scope=event.scope) 144 | new_jail.save() 145 | new_jail.promote() 146 | yield from jail.destroy( 147 | force=True, 148 | force_stop=True, 149 | event_scope=event.scope 150 | ) 151 | 152 | except libioc.errors.IocException as e: 153 | yield event.fail(e) 154 | continue 155 | 156 | if name != temporary_name: 157 | # the jail takes the old jails name 158 | yield from new_jail.rename(name, event_scope=event.scope) 159 | 160 | yield event.end() 161 | -------------------------------------------------------------------------------- /ioc_cli/stop.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Stop jails with the CLI.""" 26 | import typing 27 | import click 28 | 29 | import libioc.errors 30 | import libioc.Jails 31 | import libioc.Logger 32 | 33 | from .shared.click import IocClickContext 34 | 35 | __rootcmd__ = True 36 | 37 | 38 | @click.command(name="stop", help="Stops the specified jails or ALL.") 39 | @click.pass_context 40 | @click.option("--rc", default=False, is_flag=True, 41 | help="Will stop all jails with boot=on, in the specified" 42 | " order with higher value for priority stopping first.") 43 | @click.option("--force", "-f", is_flag=True, default=False, 44 | help="Skip checks and enforce jail shutdown") 45 | @click.argument("jails", nargs=-1) 46 | def cli( 47 | ctx: IocClickContext, 48 | rc: bool, 49 | force: bool, 50 | jails: typing.Tuple[str, ...] 51 | ) -> None: 52 | """ 53 | Stop a jail. 54 | 55 | Looks for the jail supplied and passes the uuid, path and configuration 56 | location to stop_jail. 57 | """ 58 | logger = ctx.parent.logger 59 | stop_args = { 60 | "logger": logger, 61 | "zfs": ctx.parent.zfs, 62 | "host": ctx.parent.host, 63 | "print_function": ctx.parent.print_events, 64 | "force": force 65 | } 66 | 67 | if (rc is False) and (len(jails) == 0): 68 | logger.error("No jail selector provided") 69 | exit(1) 70 | 71 | elif rc is True: 72 | if len(jails) > 0: 73 | logger.error("Cannot use --rc and jail selectors simultaniously") 74 | exit(1) 75 | 76 | _autostop( 77 | host=ctx.parent.host, 78 | zfs=ctx.parent.zfs, 79 | logger=logger, 80 | print_function=ctx.parent.print_events, 81 | force=force 82 | ) 83 | else: 84 | if not _normal(jails, **stop_args): 85 | exit(1) 86 | 87 | 88 | def _normal( 89 | filters: typing.Tuple[str, ...], 90 | zfs: libioc.Host.HostGenerator, 91 | host: libioc.Host.HostGenerator, 92 | logger: libioc.Logger.Logger, 93 | print_function: typing.Callable[ 94 | [typing.Generator[libioc.events.IocEvent, None, None]], 95 | None 96 | ], 97 | force: bool 98 | ) -> bool: 99 | 100 | filters += ("template=no,-",) 101 | 102 | try: 103 | jails = libioc.Jails.JailsGenerator( 104 | zfs=zfs, 105 | host=host, 106 | logger=logger, 107 | filters=filters, 108 | skip_invalid_config=True 109 | ) 110 | except libioc.errors.IocException: 111 | exit(1) 112 | 113 | changed_jails = [] 114 | failed_jails = [] 115 | for jail in jails: 116 | try: 117 | print_function(jail.stop(force=force)) 118 | except libioc.errors.IocException: 119 | failed_jails.append(jail) 120 | continue 121 | 122 | logger.log(f"{jail.name} stopped") 123 | changed_jails.append(jail) 124 | 125 | if len(failed_jails) > 0: 126 | return False 127 | 128 | if len(changed_jails) == 0: 129 | jails_input = " ".join(list(jails)) 130 | logger.error(f"No jails matched your input: {jails_input}") 131 | return False 132 | 133 | return True 134 | 135 | 136 | def _autostop( 137 | zfs: libioc.ZFS.ZFS, 138 | host: libioc.Host.HostGenerator, 139 | logger: libioc.Logger.Logger, 140 | print_function: typing.Callable[ 141 | [typing.Generator[libioc.events.IocEvent, None, None]], 142 | None 143 | ], 144 | force: bool=True 145 | ) -> None: 146 | 147 | filters = ("running=yes", "template=no,-",) 148 | 149 | try: 150 | ioc_jails = libioc.Jails.Jails( 151 | host=host, 152 | zfs=zfs, 153 | logger=logger, 154 | filters=filters, 155 | skip_invalid_config=True 156 | ) 157 | except libioc.errors.IocException: 158 | exit(1) 159 | 160 | # sort jails by their priority 161 | jails = reversed(sorted( 162 | list(ioc_jails), 163 | key=lambda x: x.config["priority"] 164 | )) 165 | 166 | failed_jails = [] 167 | for jail in jails: 168 | try: 169 | jail.stop(force=force) 170 | except libioc.errors.IocException: 171 | failed_jails.append(jail) 172 | continue 173 | 174 | logger.log(f"{jail.name} stopped") 175 | 176 | if len(failed_jails) > 0: 177 | exit(1) 178 | -------------------------------------------------------------------------------- /ioc_cli/start.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Start jails from the CLI..""" 26 | import typing 27 | import click 28 | 29 | import libioc.errors 30 | import libioc.Jails 31 | import libioc.Logger 32 | 33 | from .shared.click import IocClickContext 34 | from .shared.jail import set_properties 35 | 36 | __rootcmd__ = True 37 | 38 | 39 | @click.command(name="start", help="Starts the specified jails or ALL.") 40 | @click.pass_context 41 | @click.option( 42 | "--rc", 43 | default=False, 44 | is_flag=True, 45 | help=( 46 | "Will start all jails with boot=on, in the specified " 47 | "order with smaller value for priority starting first." 48 | ) 49 | ) 50 | @click.option( 51 | "--option", "-o", 52 | "temporary_config_override", 53 | multiple=True, 54 | help="Temporarily override jail config options" 55 | ) 56 | @click.argument("jails", nargs=-1) 57 | def cli( 58 | ctx: IocClickContext, 59 | rc: bool, 60 | temporary_config_override: typing.Tuple[str, ...], 61 | jails: typing.Tuple[str, ...] 62 | ) -> None: 63 | """Start one or many jails.""" 64 | logger = ctx.parent.logger 65 | start_args = { 66 | "zfs": ctx.parent.zfs, 67 | "host": ctx.parent.host, 68 | "logger": logger, 69 | "print_function": ctx.parent.print_events 70 | } 71 | 72 | if (rc is False) and (len(jails) == 0): 73 | logger.error("No jail selector provided") 74 | exit(1) 75 | 76 | elif rc is True: 77 | if len(jails) > 0: 78 | logger.error("Cannot use --rc and jail selectors simultaniously") 79 | exit(1) 80 | _autostart(**start_args) 81 | else: 82 | start_normal_successful = _normal( 83 | jails, 84 | temporary_config_override=temporary_config_override, 85 | **start_args 86 | ) 87 | if start_normal_successful is False: 88 | exit(1) 89 | 90 | 91 | def _autostart( 92 | zfs: libioc.ZFS.ZFS, 93 | host: libioc.Host.HostGenerator, 94 | logger: libioc.Logger.Logger, 95 | print_function: typing.Callable[ 96 | [typing.Generator[libioc.events.IocEvent, None, None]], 97 | None 98 | ] 99 | ) -> None: 100 | 101 | filters = ("boot=yes", "running=no", "template=no,-",) 102 | 103 | ioc_jails = libioc.Jails.Jails( 104 | zfs=zfs, 105 | host=host, 106 | logger=logger, 107 | filters=filters 108 | ) 109 | 110 | # sort jails by their priority 111 | jails = sorted( 112 | list(ioc_jails), 113 | key=lambda x: x.config["priority"] 114 | ) 115 | 116 | failed_jails = [] 117 | for jail in jails: 118 | try: 119 | if jail.running is True: 120 | logger.log(f"{jail.name} is already running - skipping start") 121 | continue 122 | elif jail.hostid_check_ok is False: 123 | logger.log(f"{jail.name} hostid mismatch - skipping start") 124 | continue 125 | jail.start() 126 | except libioc.errors.IocException: 127 | failed_jails.append(jail) 128 | continue 129 | 130 | logger.log(f"{jail.humanreadable_name} running as JID {jail.jid}") 131 | 132 | if len(failed_jails) > 0: 133 | exit(1) 134 | 135 | exit(0) 136 | 137 | 138 | def _normal( 139 | filters: typing.Tuple[str, ...], 140 | temporary_config_override: typing.Tuple[str, ...], 141 | zfs: libioc.ZFS.ZFS, 142 | host: libioc.Host.HostGenerator, 143 | logger: libioc.Logger.Logger, 144 | print_function: typing.Callable[ 145 | [typing.Generator[libioc.events.IocEvent, None, None]], 146 | None 147 | ] 148 | ) -> bool: 149 | 150 | filters += ("template=no,-",) 151 | 152 | jails = libioc.Jails.JailsGenerator( 153 | logger=logger, 154 | zfs=zfs, 155 | host=host, 156 | filters=filters 157 | ) 158 | 159 | changed_jails = [] 160 | skipped_jails = [] 161 | failed_jails = [] 162 | for jail in jails: 163 | try: 164 | set_properties( 165 | properties=temporary_config_override, 166 | target=jail, 167 | autosave=False 168 | ) 169 | except libioc.errors.IocException: 170 | exit(1) 171 | try: 172 | jail.require_jail_not_template() 173 | if jail.running is True: 174 | logger.log(f"{jail.name} is already running - skipping start") 175 | skipped_jails.append(jail) 176 | continue 177 | elif jail.hostid_check_ok is False: 178 | logger.log(f"{jail.name} hostid mismatch - skipping start") 179 | skipped_jails.append(jail) 180 | continue 181 | print_function(jail.start()) 182 | except libioc.errors.IocException: 183 | failed_jails.append(jail) 184 | continue 185 | 186 | logger.log(f"{jail.humanreadable_name} running as JID {jail.jid}") 187 | changed_jails.append(jail) 188 | 189 | if len(failed_jails) > 0: 190 | return False 191 | 192 | if len(changed_jails) == 0: 193 | jails_input = " ".join(list(filters)) 194 | if len(skipped_jails) == 0: 195 | logger.error(f"No jails matched your input: {jails_input}") 196 | else: 197 | logger.error(f"No jails were started: {jails_input}") 198 | return False 199 | 200 | return True 201 | -------------------------------------------------------------------------------- /ioc_cli/snapshot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Create and manage jail snapshots with the CLI.""" 26 | import typing 27 | import click 28 | 29 | import libioc.Jail 30 | import libioc.Logger 31 | 32 | from .shared.click import IocClickContext 33 | from .shared.jail import get_jail 34 | from .shared.output import print_table 35 | 36 | __rootcmd__ = True 37 | 38 | 39 | @click.command( 40 | name="list_or_create" 41 | ) 42 | @click.pass_context 43 | def cli_list_or_create( 44 | ctx: IocClickContext 45 | ) -> None: 46 | """ 47 | Choose whether to list or create a snapshot. 48 | 49 | When a full snapshot identifier `@` is seen, the 50 | snapshot will be created. Otherwise existing snapshots are listed. 51 | """ 52 | if "@" in ctx.info_name: 53 | return _cli_create(ctx, ctx.info_name) 54 | else: 55 | return _cli_list(ctx, ctx.info_name) 56 | 57 | 58 | @click.command( 59 | name="create", 60 | help="Create a snapshot" 61 | ) 62 | @click.pass_context 63 | @click.argument("identifier", nargs=1, required=True) 64 | def cli_create(ctx: IocClickContext, identifier: str) -> None: 65 | """Create a snapshot.""" 66 | _cli_create(ctx, identifier) 67 | 68 | 69 | def _cli_create(ctx: IocClickContext, identifier: str) -> None: 70 | try: 71 | ioc_jail, snapshot_name = _parse_identifier( 72 | ctx=ctx.parent, 73 | identifier=identifier, 74 | require_full_identifier=True 75 | ) 76 | ioc_jail.snapshots.create(snapshot_name) 77 | except libioc.errors.IocException: 78 | pass 79 | 80 | 81 | @click.command( 82 | name="rollback", 83 | help="Rollback to a snapshot" 84 | ) 85 | @click.pass_context 86 | @click.argument("identifier", nargs=1, required=True) 87 | @click.option("--force", "-f", is_flag=True, help="Force ZFS rollback") 88 | def cli_rollback( 89 | ctx: IocClickContext, 90 | identifier: str, 91 | force: bool 92 | ) -> None: 93 | """Rollback to a previously taken snapshot.""" 94 | try: 95 | ioc_jail, snapshot_name = _parse_identifier( 96 | ctx=ctx.parent, 97 | identifier=identifier, 98 | require_full_identifier=True 99 | ) 100 | ioc_jail.snapshots.rollback(snapshot_name, force=force) 101 | except libioc.errors.IocException: 102 | pass 103 | 104 | 105 | @click.command( 106 | name="list", 107 | help="List all snapshots" 108 | ) 109 | @click.pass_context 110 | @click.argument("jail", nargs=1, required=True) 111 | def cli_list(ctx: IocClickContext, jail: str) -> None: 112 | """List existing snapshots.""" 113 | _cli_list(ctx, jail) 114 | 115 | 116 | def _cli_list(ctx: IocClickContext, jail: str) -> None: 117 | try: 118 | ioc_jail, snapshot_name = _parse_identifier( 119 | ctx=ctx.parent, 120 | identifier=jail, 121 | require_full_identifier=False 122 | ) 123 | columns = ["NAME"] 124 | data = [[x.name.split("@", maxsplit=1)[1]] for x in ioc_jail.snapshots] 125 | print_table(data, columns) 126 | except libioc.errors.IocException: 127 | pass 128 | 129 | 130 | @click.command( 131 | name="remove", 132 | help="Delete existing snapshots" 133 | ) 134 | @click.argument("identifier", nargs=1, required=True) 135 | @click.pass_context 136 | def cli_remove(ctx: IocClickContext, identifier: str) -> None: 137 | """Remove a snapshot.""" 138 | try: 139 | ioc_jail, snapshot_name = _parse_identifier( 140 | ctx=ctx.parent, 141 | identifier=identifier, 142 | require_full_identifier=True 143 | ) 144 | ioc_jail.snapshots.delete(snapshot_name) 145 | except libioc.errors.IocException: 146 | pass 147 | 148 | 149 | class SnapshotCli(click.MultiCommand): 150 | """Python Click snapshot subcommand boilerplate.""" 151 | 152 | def list_commands(self, ctx: click.core.Context) -> list: 153 | """Mock Click subcommands.""" 154 | return [ 155 | "list", 156 | "create", 157 | "rollback", 158 | "remove" 159 | ] 160 | 161 | def get_command( 162 | self, 163 | ctx: click.core.Context, 164 | cmd_name: str 165 | ) -> click.core.Command: 166 | """Wrap Click subcommands.""" 167 | command: click.core.Command 168 | 169 | if cmd_name == "list": 170 | command = cli_list 171 | elif cmd_name == "create": 172 | command = cli_create 173 | elif cmd_name == "remove": 174 | command = cli_remove 175 | elif cmd_name == "rollback": 176 | command = cli_rollback 177 | else: 178 | command = cli_list_or_create 179 | 180 | return command 181 | 182 | 183 | @click.group( 184 | name="snapshot", 185 | cls=SnapshotCli, 186 | context_settings=dict( 187 | ignore_unknown_options=True, 188 | ) 189 | ) 190 | @click.pass_context 191 | def cli( 192 | ctx: IocClickContext 193 | ) -> None: 194 | """Take and manage resource snapshots.""" 195 | ctx.logger = ctx.parent.logger 196 | ctx.host = ctx.parent.host 197 | 198 | 199 | def _parse_identifier( 200 | ctx: IocClickContext, 201 | identifier: str, 202 | require_full_identifier: bool=False 203 | ) -> typing.Tuple[libioc.Jail.JailGenerator, typing.Optional[str]]: 204 | 205 | snapshot_name: typing.Optional[str] = None 206 | try: 207 | jail, snapshot_name = identifier.split("@") 208 | except ValueError: 209 | if require_full_identifier is True: 210 | raise libioc.errors.InvalidSnapshotIdentifier( 211 | identifier=identifier, 212 | logger=ctx.parent.logger 213 | ) 214 | jail = identifier 215 | 216 | return get_jail(jail, ctx, skip_invalid_config=True), snapshot_name 217 | -------------------------------------------------------------------------------- /ioc_cli/create.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """Create jails with the CLI.""" 26 | import click 27 | import typing 28 | 29 | import libioc.errors 30 | import libioc.Host 31 | import libioc.Jail 32 | import libioc.Logger 33 | import libioc.Release 34 | import libioc.ZFS 35 | 36 | from .shared.click import IocClickContext 37 | 38 | __rootcmd__ = True 39 | 40 | 41 | def validate_count( 42 | ctx: IocClickContext, 43 | param: typing.Union[ 44 | click.Option, 45 | typing.Union[click.Option, click.Parameter], 46 | typing.Union[bool, int, str] 47 | ], 48 | value: typing.Any 49 | ) -> int: 50 | """Take a string, removes the commas and returns an int.""" 51 | if isinstance(value, str): 52 | try: 53 | value = value.replace(",", "") 54 | 55 | return int(value) 56 | except ValueError: 57 | logger = libioc.Logger.Logger() 58 | logger.error(f"{value} is not a valid integer.") 59 | exit(1) 60 | else: 61 | return int(value) 62 | 63 | 64 | @click.command( 65 | name="create", 66 | help="Create a jail." 67 | ) 68 | @click.pass_context 69 | @click.option( 70 | "--count", "-c", 71 | callback=validate_count, 72 | default=1, 73 | help=( 74 | "Designate a number of jails to create." 75 | "Jails are numbered sequentially." 76 | ) 77 | ) 78 | @click.option( 79 | "--release", "-r", 80 | required=False, 81 | help="Specify the RELEASE to use for the new jail." 82 | ) 83 | @click.option( 84 | "--template", "-t", 85 | required=False, 86 | help="Specify the template to use for the new jail instead of a RELEASE." 87 | ) 88 | @click.option( 89 | "--basejail/--no-basejail", "-b/-nb", 90 | is_flag=True, 91 | default=True, 92 | help=( 93 | "Set the new jail type to a basejail (default). " 94 | "Basejails mount the specified RELEASE directories over the " 95 | "jail's directories." 96 | ) 97 | ) 98 | @click.option( 99 | "--empty", "-e", 100 | is_flag=True, 101 | default=False, 102 | help="Create an empty jail used for unsupported or custom jails.") 103 | @click.option( 104 | "--no-fetch", 105 | is_flag=True, 106 | default=False, 107 | help="Do not automatically fetch releases" 108 | ) 109 | @click.argument("name") 110 | @click.argument("props", nargs=-1) 111 | def cli( 112 | ctx: IocClickContext, 113 | release: typing.Optional[str], 114 | template: typing.Optional[str], 115 | count: int, 116 | props: typing.Tuple[str, ...], 117 | basejail: bool, 118 | empty: bool, 119 | name: str, 120 | no_fetch: bool 121 | ) -> None: 122 | """Create iocage jails.""" 123 | logger = ctx.parent.logger 124 | zfs: libioc.ZFS.ZFS = ctx.parent.zfs 125 | host: libioc.Host.Host = ctx.parent.host 126 | 127 | jail_data: typing.Dict[str, typing.Any] = {} 128 | 129 | if (release is None) and (template is None): 130 | logger.spam( 131 | "No release selected (-r, --release)." 132 | f" Selecting host release '{host.release_version}' as default." 133 | ) 134 | release = host.release_version 135 | 136 | try: 137 | resource_selector = libioc.ResourceSelector.ResourceSelector( 138 | name, 139 | logger=logger 140 | ) 141 | except libioc.errors.IocException: 142 | exit(1) 143 | 144 | jail_data["name"] = resource_selector.name 145 | root_datasets_name = resource_selector.source_name 146 | 147 | try: 148 | if release is not None: 149 | resource = libioc.Release.ReleaseGenerator( 150 | name=release, 151 | root_datasets_name=root_datasets_name, 152 | logger=logger, 153 | host=host, 154 | zfs=zfs 155 | ) 156 | if resource.fetched is False: 157 | if not resource.available: 158 | logger.error( 159 | f"The release '{resource.name}' does not exist" 160 | ) 161 | exit(1) 162 | 163 | msg = ( 164 | f"The release '{resource.name}' is available," 165 | " but not downloaded yet" 166 | ) 167 | if no_fetch: 168 | logger.error(msg) 169 | exit(1) 170 | else: 171 | logger.spam(msg) 172 | logger.log( 173 | f"Automatically fetching release '{resource.name}'" 174 | ) 175 | resource.fetch() 176 | elif template is not None: 177 | resource = libioc.Jail.JailGenerator( 178 | template, 179 | root_datasets_name=root_datasets_name, 180 | logger=logger, 181 | host=host, 182 | zfs=zfs 183 | ) 184 | else: 185 | logger.error("No release or jail selected") 186 | exit(1) 187 | 188 | if basejail: 189 | jail_data["basejail"] = True 190 | 191 | if props: 192 | for prop in props: 193 | try: 194 | key, value = prop.split("=", maxsplit=1) 195 | jail_data[key] = value 196 | except (ValueError, KeyError): 197 | logger.error(f"Invalid property {prop}") 198 | exit(1) 199 | except libioc.errors.IocException: 200 | exit(1) 201 | 202 | errors = False 203 | for i in range(count): 204 | 205 | jail = libioc.Jail.JailGenerator( 206 | jail_data, 207 | root_datasets_name=root_datasets_name, 208 | logger=logger, 209 | host=host, 210 | zfs=zfs, 211 | new=True 212 | ) 213 | suffix = f" ({i}/{count})" if count > 1 else "" 214 | try: 215 | jail.create(resource) 216 | msg_source = f" on {jail.source}" if len(host.datasets) > 1 else "" 217 | msg = ( 218 | f"{jail.humanreadable_name} successfully created" 219 | f" from {resource.name}" 220 | f"{msg_source}!{suffix}" 221 | ) 222 | logger.log(msg) 223 | except libioc.errors.IocException: 224 | exit(1) 225 | 226 | exit(int(errors)) 227 | -------------------------------------------------------------------------------- /ioc_cli/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | import typing 26 | import locale 27 | import os 28 | import re 29 | import signal 30 | import subprocess # nosec: B404 31 | import sys 32 | 33 | import click 34 | 35 | import libioc.Logger 36 | import libioc.events 37 | from libioc.errors import ( 38 | InvalidLogLevel, 39 | IocageNotActivated, 40 | ZFSSourceMountpoint 41 | ) 42 | from libioc.ZFS import get_zfs 43 | from libioc.Datasets import Datasets 44 | from libioc.Host import HostGenerator 45 | 46 | logger = libioc.Logger.Logger() 47 | 48 | click.core._verify_python3_env = lambda: None # type: ignore 49 | user_locale = os.environ.get("LANG", "en_US.UTF-8") 50 | locale.setlocale(locale.LC_ALL, user_locale) 51 | 52 | IOCAGE_CMD_FOLDER = os.path.abspath(os.path.dirname(__file__)) 53 | 54 | # @formatter:off 55 | # Sometimes SIGINT won't be installed. 56 | # http://stackoverflow.com/questions/40775054/capturing-sigint-using-keyboardinterrupt-exception-works-in-terminal-not-in-scr/40785230#40785230 57 | signal.signal(signal.SIGINT, signal.default_int_handler) 58 | # If a utility decides to cut off the pipe, we don't care (IE: head) 59 | signal.signal(signal.SIGPIPE, signal.SIG_DFL) 60 | # @formatter:on 61 | 62 | try: 63 | subprocess.check_call( # nosec 64 | ["/sbin/sysctl", "vfs.zfs.version.spa"], 65 | stdout=subprocess.PIPE, 66 | stderr=subprocess.PIPE 67 | ) 68 | except subprocess.CalledProcessError: 69 | logger.error( 70 | "ZFS is required to use libioc.\n" 71 | "Try calling 'kldload zfs' as root." 72 | ) 73 | exit(1) 74 | 75 | 76 | def set_to_dict(data: typing.Set[str]) -> typing.Dict[str, str]: 77 | """Convert a set of values to a dictionary.""" 78 | keys, values = zip(*[x.split("=", maxsplit=1) for x in data]) 79 | return dict(zip(keys, values)) 80 | 81 | 82 | def print_events( 83 | generator: typing.Generator[ 84 | typing.Union[libioc.events.IocEvent, bool], 85 | None, 86 | None 87 | ] 88 | ) -> typing.Optional[bool]: 89 | 90 | lines: typing.Dict[str, str] = {} 91 | for event in generator: 92 | 93 | if isinstance(event, bool): 94 | # a boolean terminates the event stream 95 | return event 96 | 97 | if event.identifier is None: 98 | identifier = "generic" 99 | else: 100 | identifier = event.identifier 101 | 102 | if event.type not in lines: 103 | lines[event.type] = {} 104 | 105 | # output fragments 106 | running_indicator = "+" if (event.done or event.skipped) else "-" 107 | name = event.type 108 | if event.identifier is not None: 109 | name += f"@{event.identifier}" 110 | 111 | output = f"[{running_indicator}] {name}: " 112 | 113 | if event.message is not None: 114 | output += event.message 115 | else: 116 | output += event.get_state_string( 117 | done="OK", 118 | error="FAILED", 119 | skipped="SKIPPED", 120 | pending="..." 121 | ) 122 | 123 | if event.duration is not None: 124 | output += " [" + str(round(event.duration, 3)) + "s]" 125 | 126 | # new line or update of previous 127 | if identifier not in lines[event.type]: 128 | # Indent if previous task is not finished 129 | lines[event.type][identifier] = logger.screen( 130 | output, 131 | indent=event.parent_count 132 | ) 133 | else: 134 | lines[event.type][identifier].edit( 135 | output, 136 | indent=event.parent_count 137 | ) 138 | 139 | 140 | class IOCageCLI(click.MultiCommand): 141 | """ 142 | Iterates in the 'cli' directory and will load any module's cli definition. 143 | """ 144 | 145 | def list_commands(self, ctx: click.core.Context): 146 | rv = [] 147 | 148 | for filename in os.listdir(IOCAGE_CMD_FOLDER): 149 | if filename.endswith('.py') and \ 150 | not filename.startswith('__init__'): 151 | rv.append(re.sub(r".py$", "", filename)) 152 | rv.sort() 153 | 154 | return rv 155 | 156 | def get_command(self, ctx, name): 157 | ctx.print_events = print_events 158 | mod = __import__(f"ioc_cli.{name}", None, None, ["ioc"]) 159 | 160 | is_root_cmd = ("__rootcmd__" in dir(mod)) and (mod.__rootcmd__ is True) 161 | if is_root_cmd and "--help" not in sys.argv[1:]: 162 | if len(sys.argv) != 1: 163 | if os.geteuid() != 0: 164 | app_name = mod.__name__.rsplit(".")[-1] 165 | logger.error( 166 | "You need to have root privileges" 167 | f" to run {app_name}" 168 | ) 169 | exit(1) 170 | return mod.cli 171 | 172 | 173 | @click.option( 174 | "--log-level", 175 | "-d", 176 | default=None, 177 | help=( 178 | f"Set the CLI log level {libioc.Logger.Logger.LOG_LEVELS}" 179 | ) 180 | ) 181 | @click.option( 182 | "--source", 183 | multiple=True, 184 | type=str, 185 | help="Globally override the activated iocage dataset(s)" 186 | ) 187 | @click.command(cls=IOCageCLI) 188 | @click.version_option( 189 | version="0.8.2 2019/08/10", 190 | prog_name="ioc", 191 | message="\n".join(( 192 | "%(prog)s, version %(version)s", 193 | f"libioc, version {libioc._get_version()}" 194 | )) 195 | ) 196 | @click.pass_context 197 | def cli(ctx, log_level: str, source: set) -> None: 198 | """A jail manager.""" 199 | if log_level is not None: 200 | try: 201 | logger.print_level = log_level 202 | except InvalidLogLevel: 203 | exit(1) 204 | ctx.logger = logger 205 | 206 | ctx.zfs = get_zfs(logger=ctx.logger) 207 | 208 | ctx.user_sources = None if (len(source) == 0) else set_to_dict(source) 209 | 210 | if ctx.invoked_subcommand in ["activate", "deactivate"]: 211 | return 212 | 213 | try: 214 | datasets = Datasets( 215 | sources=ctx.user_sources, 216 | zfs=ctx.zfs, 217 | logger=ctx.logger 218 | ) 219 | ctx.host = HostGenerator( 220 | datasets=datasets, 221 | logger=ctx.logger, 222 | zfs=ctx.zfs 223 | ) 224 | except (IocageNotActivated, ZFSSourceMountpoint): 225 | exit(1) 226 | 227 | -------------------------------------------------------------------------------- /ioc_cli/fstab.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """View and manipulate fstab files of a jail.""" 26 | import typing 27 | import errno 28 | import click 29 | import os 30 | 31 | import libioc.errors 32 | import libioc.Logger 33 | import libioc.Host 34 | import libioc.helpers 35 | import libioc.Jail 36 | import libioc.Config.Jail.File.Fstab 37 | 38 | from .shared.jail import get_jail 39 | from .shared.click import IocClickContext 40 | 41 | __rootcmd__ = True 42 | FstabLine = libioc.Config.Jail.File.Fstab.FstabLine 43 | 44 | 45 | def _get_relpath(path: str, jail: libioc.Jail.JailGenerator) -> str: 46 | if path.startswith(jail.root_path) is True: 47 | return path[len(jail.root_path.rstrip("/")):] 48 | else: 49 | return path 50 | 51 | 52 | def _get_abspath(path: str, jail: libioc.Jail.JailGenerator) -> str: 53 | result = str(os.path.realpath(os.path.join( 54 | jail.root_path, 55 | _get_relpath(path, jail).lstrip("/") 56 | ))) 57 | 58 | if result.startswith(jail.root_path): 59 | return result 60 | 61 | raise libioc.errors.InsecureJailPath( 62 | path=result, 63 | logger=jail.logger 64 | ) 65 | 66 | 67 | @click.command( 68 | name="add" 69 | ) 70 | @click.pass_context 71 | @click.argument( 72 | "source", 73 | nargs=1, 74 | required=True 75 | ) 76 | @click.argument( 77 | "destination", 78 | nargs=-1, 79 | required=False 80 | ) 81 | @click.argument("jail", nargs=1, required=True) 82 | @click.option( 83 | "--options", "-o", 84 | "mntops", 85 | default="ro", 86 | required=False, 87 | help="Mount options." 88 | ) 89 | @click.option( 90 | "--type", "-t", 91 | "vfstype", 92 | default="nullfs", 93 | required=False, 94 | help="Mount filesystem type." 95 | ) 96 | @click.option( 97 | "--freq", 98 | default=0, 99 | required=False, 100 | help="Number of days between dumps of the filesystem." 101 | ) 102 | @click.option( 103 | "--passno", 104 | default=0, 105 | required=False, 106 | help="Order of filesystem and quota checks on reboot." 107 | ) 108 | @click.option( 109 | "--comment", "-c", 110 | required=False, 111 | help="Add a comment to the fstab line." 112 | ) 113 | def cli_add( 114 | ctx: IocClickContext, 115 | source: str, 116 | destination: typing.Tuple[str], 117 | jail: str, 118 | mntops: str, 119 | vfstype: str, 120 | freq: int, 121 | passno: int, 122 | comment: typing.Optional[str] 123 | ) -> None: 124 | """Add lines to a jails fstab file.""" 125 | ioc_jail = get_jail(jail, ctx.parent, skip_invalid_config=True) 126 | 127 | if len(destination) == 0: 128 | desination_path = source 129 | else: 130 | desination_path = destination[0] 131 | 132 | try: 133 | source = libioc.Types.AbsolutePath(source) 134 | except: 135 | ctx.parent.logger.spam("mount source is not an absolute path") 136 | 137 | if os.path.exists(source) is False: 138 | ctx.parent.logger.error( 139 | f"The mount source {source} is does not exist" 140 | ) 141 | exit(1) 142 | return 143 | if os.path.isdir(source) is False: 144 | ctx.parent.logger.error( 145 | f"The mount source {source} is not a directory" 146 | ) 147 | exit(1) 148 | return 149 | ctx.parent.logger.spam( 150 | f"mount source {source} is an absolute path that exists" 151 | ) 152 | 153 | try: 154 | desination_path = _get_abspath(desination_path, ioc_jail) 155 | 156 | fstab = ioc_jail.fstab 157 | fstab.read_file() 158 | fstab.new_line( 159 | source=source, 160 | destination=desination_path, 161 | type=vfstype, 162 | options=mntops, 163 | freq=int(freq), 164 | passno=int(passno), 165 | comment=comment 166 | ) 167 | 168 | # ensure destination directory exists 169 | try: 170 | os.makedirs(desination_path) 171 | except OSError as exc: # Python >2.5 172 | if exc.errno == errno.EEXIST and os.path.isdir(desination_path): 173 | pass 174 | else: 175 | raise 176 | 177 | fstab.save() 178 | ctx.parent.logger.log( 179 | f"fstab mount added: {source} -> {desination_path} ({mntops})" 180 | ) 181 | exit(0) 182 | except libioc.errors.IocException: 183 | exit(1) 184 | 185 | 186 | @click.command( 187 | name="show" 188 | ) 189 | @click.pass_context 190 | @click.argument("jail", nargs=1, required=True) 191 | def cli_show(ctx: IocClickContext, jail: str) -> None: 192 | """Show a jails fstab file.""" 193 | ioc_jail = get_jail(jail, ctx.parent) 194 | if os.path.isfile(ioc_jail.fstab.path): 195 | with open(ioc_jail.fstab.path, "r") as f: 196 | print(f.read()) 197 | 198 | 199 | @click.command( 200 | name="rm" 201 | ) 202 | @click.argument( 203 | "source", 204 | nargs=1, 205 | required=False 206 | ) 207 | @click.argument("jail", nargs=1, required=True) 208 | @click.pass_context 209 | def cli_rm(ctx: IocClickContext, source: str, jail: str) -> None: 210 | """Remove a line from a jails fstab file.""" 211 | ioc_jail = get_jail(jail, ctx.parent) 212 | fstab = ioc_jail.fstab 213 | destination = None 214 | i = 0 215 | 216 | try: 217 | fstab.read_file() 218 | for existing_line in fstab: 219 | i += 1 220 | if isinstance(existing_line, FstabLine) is False: 221 | continue 222 | if existing_line["source"] == source: 223 | destination = fstab[i - 1]["destination"] 224 | del fstab[i - 1] 225 | fstab.save() 226 | break 227 | except libioc.errors.IocException: 228 | exit(1) 229 | 230 | if destination is None: 231 | ctx.parent.logger.error("no matching fstab line found") 232 | exit(1) 233 | 234 | ctx.parent.logger.log(f"fstab mount removed: {source} -> {destination}") 235 | 236 | 237 | class FstabCli(click.MultiCommand): 238 | """Python Click fstab subcommand boilerplate.""" 239 | 240 | def list_commands(self, ctx: click.core.Context) -> list: 241 | """Mock subcommands for Python Click.""" 242 | return [ 243 | "show", 244 | "add", 245 | "rm" 246 | ] 247 | 248 | def get_command( 249 | self, 250 | ctx: click.core.Context, 251 | cmd_name: str 252 | ) -> click.core.Command: 253 | """Wrap subcommand for Python Click.""" 254 | command: typing.Optional[click.core.Command] = None 255 | 256 | if cmd_name == "show": 257 | command = cli_show 258 | elif cmd_name == "add": 259 | command = cli_add 260 | elif cmd_name == "rm": 261 | command = cli_rm 262 | 263 | if command is None: 264 | raise NotImplementedError("action does not exist") 265 | 266 | return command 267 | 268 | 269 | @click.group( 270 | name="fstab", 271 | cls=FstabCli, 272 | context_settings=dict( 273 | ignore_unknown_options=True, 274 | ) 275 | ) 276 | @click.pass_context 277 | def cli( 278 | ctx: IocClickContext 279 | ) -> None: 280 | """View and manipulate a jails fstab file.""" 281 | ctx.logger = ctx.parent.logger 282 | ctx.host = ctx.parent.host 283 | -------------------------------------------------------------------------------- /ioc_cli/list.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2019, Stefan Grönke 2 | # Copyright (c) 2014-2018, iocage 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | """List jails, releases and templates with the CLI.""" 26 | import click 27 | import json 28 | import typing 29 | 30 | import libioc.errors 31 | import libioc.Logger 32 | import libioc.Host 33 | import libioc.Datasets 34 | import libioc.Resource 35 | import libioc.ListableResource 36 | import libioc.Jails 37 | import libioc.Releases 38 | 39 | from .shared.output import print_table 40 | from .shared.click import IocClickContext 41 | 42 | __rootcmd__ = True 43 | 44 | supported_output_formats = ['table', 'csv', 'list', 'json'] 45 | 46 | 47 | @click.command( 48 | name="list", 49 | help="List a specified dataset type, by default lists all jails." 50 | ) 51 | @click.pass_context 52 | @click.option("--release", "--base", "-r", "-b", "dataset_type", 53 | flag_value="base", help="List all bases.") 54 | @click.option("--template", "-t", "dataset_type", 55 | flag_value="template", help="List all templates.") 56 | @click.option("--pools", "-p", "dataset_type", 57 | flag_value="pool", help="List all iocage ZFS pools.") 58 | @click.option("--dataset", "-d", "dataset_type", 59 | flag_value="datasets", help="List all root data sources.") 60 | @click.option("--long", "-l", "_long", is_flag=True, default=False, 61 | help="Show the full uuid and ip4 address.") 62 | @click.option("--remote", "-R", 63 | is_flag=True, help="Show remote's available RELEASEs.") 64 | @click.option("--sort", "-s", "_sort", default=None, nargs=1, 65 | help="Sorts the list by the given type") 66 | @click.option("--output", "-o", default=None) 67 | @click.option("--output-format", "-f", default="table", 68 | type=click.Choice(supported_output_formats)) 69 | @click.option("--header/--no-header", "-H/-NH", is_flag=True, default=True, 70 | help="Show or hide column name heading.") 71 | @click.argument("filters", nargs=-1) 72 | def cli( 73 | ctx: IocClickContext, 74 | dataset_type: str, 75 | header: bool, 76 | _long: bool, 77 | remote: bool, 78 | _sort: typing.Optional[str], 79 | output: typing.Optional[str], 80 | output_format: str, 81 | filters: typing.Tuple[str, ...] 82 | ) -> None: 83 | """List jails in various formats.""" 84 | logger = ctx.parent.logger 85 | zfs = ctx.parent.zfs 86 | host: libioc.Host.HostGenerator = ctx.parent.host 87 | 88 | if output is not None and _long is True: 89 | logger.error("--output and --long can't be used together") 90 | exit(1) 91 | 92 | if output_format != "table" and _sort is not None: 93 | # Sorting destroys the ability to stream generators 94 | # ToDo: Figure out if we need to sort other output formats as well 95 | raise Exception("Sorting only allowed for tables") 96 | 97 | # empty filters will match all jails 98 | if len(filters) == 0: 99 | filters += ("*",) 100 | 101 | columns: typing.List[str] = [] 102 | 103 | try: 104 | 105 | if (dataset_type == "base") and (remote is True): 106 | columns = ["name", "eol"] 107 | resources = host.distribution.releases 108 | 109 | elif (dataset_type == "pool"): 110 | columns = ["name", "dataset"] 111 | datasets = libioc.Datasets.Datasets(zfs=zfs, logger=logger) 112 | resources = [ 113 | dict( 114 | name=name, 115 | dataset=x.root.name 116 | ) for name, x in datasets.items() 117 | ] 118 | 119 | else: 120 | 121 | resource_kwargs = dict() 122 | 123 | if (dataset_type == "base"): 124 | resources_class = libioc.Releases.ReleasesGenerator 125 | columns = ["full_name"] 126 | elif (dataset_type == "datasets"): 127 | resources_class = None 128 | columns = ["name", "dataset"] 129 | resources = [ 130 | dict(name=name, dataset=root_datasets.root.name) 131 | for name, root_datasets 132 | in host.datasets.items() 133 | ] 134 | else: 135 | resource_kwargs["skip_invalid_config"] = True 136 | resources_class = libioc.Jails.JailsGenerator 137 | columns = _list_output_comumns(output, _long) 138 | if dataset_type == "template": 139 | filters += ("template=yes",) 140 | else: 141 | filters += ("template=no,-",) 142 | 143 | if resources_class is not None: 144 | resources = resources_class( 145 | logger=logger, 146 | host=host, 147 | zfs=ctx.parent.zfs, 148 | # ToDo: allow quoted whitespaces from user inputs 149 | filters=filters, 150 | **resource_kwargs 151 | ) 152 | 153 | except libioc.errors.IocException: 154 | exit(1) 155 | 156 | try: 157 | if output_format == "list": 158 | _print_list(resources, columns, header, "\t") 159 | elif output_format == "csv": 160 | _print_list(resources, columns, header, ";") 161 | elif output_format == "json": 162 | _print_json(resources, columns) 163 | else: 164 | _print_table(resources, columns, header, _sort) 165 | except libioc.errors.IocException: 166 | exit(1) 167 | 168 | 169 | def _print_table( 170 | resources: typing.Generator[ 171 | typing.Union[ 172 | libioc.ListableResource.ListableResource, 173 | typing.List[typing.Dict[str, str]] 174 | ], 175 | None, 176 | None 177 | ], 178 | columns: list, 179 | show_header: bool, 180 | sort_key: typing.Optional[str]=None 181 | ) -> None: 182 | 183 | table_data = [] 184 | for resource in resources: 185 | table_data.append(_lookup_resource_values(resource, columns)) 186 | 187 | print_table(table_data, columns, show_header, sort_key) 188 | 189 | 190 | def _print_list( 191 | resources: typing.Generator[ 192 | typing.Union[ 193 | libioc.ListableResource.ListableResource, 194 | typing.List[typing.Dict[str, str]] 195 | ], 196 | None, 197 | None 198 | ], 199 | columns: list, 200 | show_header: bool, 201 | separator: str=";" 202 | ) -> None: 203 | 204 | if show_header is True: 205 | print(separator.join(columns).upper()) 206 | 207 | for resource in resources: 208 | print(separator.join(_lookup_resource_values(resource, columns))) 209 | 210 | 211 | def _print_json( 212 | resources: typing.Generator[ 213 | typing.Union[ 214 | libioc.ListableResource.ListableResource, 215 | typing.List[typing.Dict[str, str]] 216 | ], 217 | None, 218 | None 219 | ], 220 | columns: list, 221 | # json.dumps arguments 222 | indent: int=2, 223 | sort_keys: bool=True 224 | ) -> None: 225 | 226 | output = [] 227 | for resource in resources: 228 | output.append(dict(zip( 229 | columns, 230 | _lookup_resource_values(resource, columns) 231 | ))) 232 | 233 | print(json.dumps( 234 | output, 235 | indent=indent, 236 | sort_keys=sort_keys 237 | )) 238 | 239 | 240 | def _lookup_resource_values( 241 | resource: typing.Union[ 242 | 'libioc.Resource.Resource', 243 | typing.Dict[str, str] 244 | ], 245 | columns: typing.List[str] 246 | ) -> typing.List[str]: 247 | is_resorce = isinstance(resource, libioc.Resource.Resource) 248 | 249 | try: 250 | if is_resorce and ("getstring" in resource.__dir__()): 251 | _resource = resource # type: libioc.Resource.Resource 252 | return list(map( 253 | lambda column: str(_resource.getstring(column)), 254 | columns 255 | )) 256 | else: 257 | return list(map( 258 | lambda column: str(resource[column]), 259 | columns 260 | )) 261 | except libioc.errors.UnknownConfigProperty: 262 | exit(1) 263 | 264 | 265 | def _list_output_comumns( 266 | user_input: typing.Optional[str]="", 267 | long_mode: bool=False 268 | ) -> typing.List[str]: 269 | 270 | if user_input is not None: 271 | return user_input.strip().split(',') 272 | else: 273 | columns = ["jid", "full_name"] 274 | 275 | if long_mode is True: 276 | columns += [ 277 | "running", 278 | "release", 279 | "ip4_addr", 280 | "ip6_addr" 281 | ] 282 | else: 283 | columns += [ 284 | "running", 285 | "release", 286 | "ip4_addr" 287 | ] 288 | 289 | return columns 290 | -------------------------------------------------------------------------------- /.travis/mypy-stubs/libzfs.pyi: -------------------------------------------------------------------------------- 1 | # Stubs for libzfs (Python 3.6) 2 | # 3 | # NOTE: This dynamically typed stub was automatically generated by stubgen. 4 | 5 | import enum 6 | from datetime import date 7 | from typing import Any, Generator, List, Dict, Optional, Union, Set 8 | 9 | 10 | class SendFlag(enum.Enum): 11 | VERBOSE = 0 12 | REPLICATE = 1 13 | DOALL = 2 14 | FROMORIGIN = 3 15 | DEDUP = 3 16 | PROPS = 4 17 | DRYRUN = 5 18 | PARSABLE = 6 19 | PROGRESS = 7 20 | LARGEBLOCK = 8 21 | EMBED_DATA = 9 22 | 23 | 24 | DatasetType = ... # type: Any 25 | DiffFileType = ... # type: Any 26 | DiffRecordType = ... # type: Any 27 | Error = ... # type: Any 28 | FeatureState = ... # type: Any 29 | PoolState = ... # type: Any 30 | PoolStatus = ... # type: Any 31 | PropertySource = ... # type: Any 32 | ScanFunction = ... # type: Any 33 | ScanState = ... # type: Any 34 | SendFlags = ... # type: Set[SendFlag] 35 | VDevAuxState = ... # type: Any 36 | VDevState = ... # type: Any 37 | ZFS_PROPERTY_CONVERTERS = ... # type: Any 38 | ZIOType = ... # type: Any 39 | ZPOOL_PROPERTY_CONVERTERS = ... # type: Any 40 | collections = ... # type: Any 41 | errno = ... # type: Any 42 | numbers = ... # type: Any 43 | os = ... # type: Any 44 | stat = ... # type: Any 45 | threading = ... # type: Any 46 | 47 | # def __pyx_unpickle_ZFSProperty(*args, **kwargs): ... # noqa: T499 48 | # def __pyx_unpickle_ZFSPropertyDict(*args, **kwargs): ... # noqa: T499 49 | # def __pyx_unpickle_ZFSUserProperty(*args, **kwargs): ... # noqa: T499 50 | # def __pyx_unpickle_ZFSVdev(*args, **kwargs): ... # noqa: T499 51 | # def __pyx_unpickle_ZFSVdevStats(*args, **kwargs): ... # noqa: T499 52 | # def __pyx_unpickle_ZPoolProperty(*args, **kwargs): ... # noqa: T499 53 | # def __pyx_unpickle_ZPoolScrub(*args, **kwargs): ... # noqa: T499 54 | def clear_label(device: str) -> None: ... 55 | def nicestrtonum(zfs: ZFS, value: Union[int, str]) -> int: ... 56 | def parse_zfs_prop( 57 | prop: str, 58 | value: Any 59 | ) -> Optional[Union[str, int, bool]]: ... 60 | def parse_zpool_prop( 61 | prop: str, 62 | value: Any 63 | ) -> Optional[Union[str, int, bool]]: ... 64 | def read_label(device: str) -> Dict[str, str]: ... 65 | def serialize_zfs_prop(prop: str, value: Any) -> str: ... 66 | def serialize_zpool_prop(prop: str, value: Any) -> str: ... 67 | def vdev_label_offset(psize: int, l: int, offset: int) -> int: ... 68 | 69 | class DiffRecord: 70 | __init__ = ... # type: Any 71 | __getstate__ = ... # type: Any 72 | 73 | class ZFS: 74 | datasets = ... # type: Any 75 | errno = ... # type: Any 76 | errstr = ... # type: Any 77 | pools: Generator[ZFSPool, None, None] = ... 78 | snapshots: Generator[ZFSSnapshot, None, None] = ... 79 | __pyx_vtable__ = ... # type: Any 80 | def __init__(self, history: bool=True, history_prefix: str='') -> None: ... 81 | def create( 82 | self, 83 | name: str, 84 | topology: Any, 85 | opts: Dict[str, str], 86 | fsopts: Dict[str, str] 87 | ) -> str: ... 88 | def describe_resume_token(self, token: str) -> Dict[Any, Any]: ... 89 | def destroy(self, name: str) -> None: ... 90 | # def export_pool(self, *args, **kwargs): ... 91 | # def find_import(self, *args, **kwargs): ... 92 | # def generate_history_opts(self, *args, **kwargs): ... 93 | def get(self, name: str) -> ZFSPool: ... 94 | def get_dataset(self, name: str) -> ZFSDataset: ... 95 | def get_dataset_by_path(self, path: str) -> ZFSDataset: ... 96 | def get_object(self, name: str) -> ZFSDataset: ... 97 | def get_snapshot(self, name: str) -> ZFSSnapshot: ... 98 | # def history_vdevs_list(self, *args, **kwargs): ... 99 | def import_pool( 100 | self, 101 | pool: ZFSImportablePool, 102 | newname: str, 103 | opts: Dict[str, Any], 104 | missing_log: bool=False 105 | ) -> ZFSPool: ... 106 | def receive( 107 | self, 108 | name: str, 109 | fd: int, 110 | force: bool=False, 111 | nomount: bool=False, 112 | resumable: bool=False, 113 | props: Optional[Dict[Any, Any]]=None, 114 | limitds: Optional[Dict[Any, Any]]=None, 115 | ) -> None: ... 116 | # def send_resume( 117 | # self, 118 | # fd: int, 119 | # token, 120 | # flags=None 121 | # ): ... 122 | # def write_history(self, *args, **kwargs): ... 123 | # def __getstate__(self): ... 124 | # def __reduce_cython__(self, *args, **kwargs): ... 125 | # def __setstate_cython__(self, *args, **kwargs): ... 126 | 127 | # class ZFSBookmark(ZFSObject): 128 | # bookmark_name = ... # type: str 129 | # parent = ... # type: Any 130 | # def __init__(self, *args, **kwargs): ... 131 | # def __getstate__(self): ... 132 | # def __reduce_cython__(self, *args, **kwargs): ... 133 | # def __setstate_cython__(self, *args, **kwargs): ... 134 | 135 | class ZFSDataset(ZFSObject): 136 | bookmarks = ... # type: Any 137 | children: Generator[ZFSDataset, None, None] = ... 138 | children_recursive = ... # type: Any 139 | dependents = ... # type: Any 140 | mountpoint: str = ... 141 | snapshots: Generator[ZFSSnapshot, None, None]= ... 142 | snapshots_recursive = ... # type: Any 143 | __pyx_vtable__ = ... # type: Any 144 | def __init__(self, *args: Any, **kwargs: Any) -> None: ... 145 | def destroy_snapshot(self, name: str) -> None: ... 146 | def diff( 147 | self, 148 | fromsnap: str, 149 | tosnap: str 150 | ) -> Generator[DiffRecord, None, None]: ... 151 | # def get_send_progress(self, *args, **kwargs): ... 152 | def mount(self) -> None: ... 153 | def mount_recursive(self, ignore_errors: bool=False) -> None: ... 154 | def promote(self) -> None: ... 155 | def receive( 156 | self, 157 | fd: int, 158 | force: bool=False, 159 | nomount: bool=False, 160 | resumable: bool=False, 161 | props: Optional[Dict[Any, Any]]=None, 162 | limitds: Optional[Dict[Any, Any]]=None 163 | ) -> None: ... 164 | def send( 165 | self, 166 | fd: int, 167 | fromname: Optional[str]=None, 168 | toname: Optional[str]=None, 169 | flags: Set[str]=set() 170 | ) -> None: ... 171 | def snapshot( 172 | self, 173 | name: str, 174 | fsopts: Optional[Dict[str, str]]=None, 175 | recursive: bool=False 176 | ) -> None: ... 177 | def umount(self, force: bool=False) -> None: ... 178 | def umount_recursive(self, force: bool=False) -> None: ... 179 | def __getstate__(self) -> Any: ... 180 | 181 | class ZFSException(RuntimeError): 182 | __init__ = ... # type: Any 183 | 184 | class ZFSImportablePool(ZFSPool): 185 | config = ... # type: Any 186 | error_count = ... # type: Any 187 | features = ... # type: Any 188 | name: str = ... 189 | properties: Dict[str, ZFSProperty] = ... 190 | root_dataset: ZFSDataset = ... 191 | __pyx_vtable__ = ... # type: Any 192 | def __init__(self, *args: Any, **kwargs: Any) -> None: ... 193 | def attach_vdev(self, vdev: Any) -> None: ... 194 | def create(self, *args: Any, **kwargs: Any) -> None: ... 195 | def destroy(self, name: str) -> None: ... 196 | 197 | class ZFSObject: 198 | name: str = ... 199 | pool: ZFSPool = ... 200 | properties: Dict[str, ZFSProperty] = ... 201 | root = ... # type: Any 202 | type = ... # type: Any 203 | def __init__(self) -> None: ... 204 | def delete(self, defer: bool=False) -> None: ... 205 | def get_send_space(self, fromname: Optional[str]) -> Any: ... 206 | def rename( 207 | self, 208 | new_name: str, 209 | nounmount: bool=False, 210 | forceunmount: bool=False 211 | ) -> None: ... 212 | def __getstate__(self) -> Dict[str, str]: ... 213 | 214 | class ZFSPool: 215 | cache_vdevs = ... # type: Any 216 | config = ... # type: Any 217 | data_vdevs = ... # type: Any 218 | disks = ... # type: Any 219 | error_count = ... # type: Any 220 | features = ... # type: Any 221 | groups = ... # type: Any 222 | guid = ... # type: Any 223 | hostname = ... # type: str 224 | log_vdevs = ... # type: Any 225 | name: str = ... 226 | properties: Dict[str, ZFSProperty] = ... 227 | root = ... # type: Any 228 | root_dataset: ZFSDataset = ... 229 | root_vdev = ... # type: Any 230 | scrub = ... # type: Any 231 | spare_vdevs = ... # type: Any 232 | state = ... # type: Any 233 | status = ... # type: Any 234 | __pyx_vtable__ = ... # type: Any 235 | def __init__(self) -> None: ... 236 | # def attach_vdevs(self, *args, **kwargs): ... 237 | def clear(self) -> bool: ... 238 | def create( 239 | self, 240 | name: str, 241 | fsopts: Dict[str, Any], 242 | fstype: int=DatasetType.FILESYSTEM, 243 | sparse_vol: bool=False, 244 | create_ancestors: bool=False 245 | ) -> None: ... 246 | def delete(self) -> None: ... 247 | def start_scrub(self) -> None: ... 248 | def stop_scrub(self) -> None: ... 249 | def upgrade(self) -> None: ... 250 | # def vdev_by_guid(self, *args, **kwargs): ... 251 | # def __getstate__(self): ... 252 | 253 | class ZFSProperty: 254 | allowed_values = ... # type: Any 255 | dataset: ZFSDataset = ... 256 | name: str = ... 257 | parsed = ... # type: Any 258 | rawvalue = ... # type: Any 259 | source = ... # type: Any 260 | value: str = ... 261 | def __init__(self) -> None: ... 262 | def inherit(self, recursive: bool=False, received: bool=False) -> None: ... 263 | def refresh(self) -> None: ... 264 | def __getstate__(self) -> Dict[str, str]: ... 265 | 266 | # class ZFSPropertyDict(dict): 267 | # def __init__(self, *args, **kwargs) -> None: ... 268 | # def get(self, *args, **kwargs) -> ZFSProperty: ... 269 | # def has_key(self, *args, **kwargs): ... 270 | # def items(self, *args, **kwargs): ... 271 | # def iterkeys(self, *args, **kwargs): ... 272 | # def itervalues(self, *args, **kwargs): ... 273 | # def keys(self, *args, **kwargs): ... 274 | # def refresh(self) -> None: ... 275 | # def setdefault(self, *args, **kwargs): ... 276 | # def update(self, *args, **kwargs): ... 277 | # def values(self, *args, **kwargs): ... 278 | # def __contains__(self, *args, **kwargs): ... 279 | # def __delitem__(self, *args, **kwargs): ... 280 | # def __getitem__(self, index): ... 281 | # def __iter__(self): ... 282 | # def __reduce__(self): ... 283 | # def __setitem__(self, index, object): ... 284 | # def __setstate__(self, state): ... 285 | 286 | class ZFSSnapshot(ZFSObject): 287 | holds = ... # type: Any 288 | mountpoint: str = ... 289 | parent = ... # type: Any 290 | snapshot_name: str = ... 291 | def __init__(self, *args: Any, **kwargs: Any) -> None: ... 292 | def bookmark(self, name: str) -> None: ... 293 | def clone( 294 | self, 295 | name: str, 296 | opts: Optional[Dict[str, str]]=None 297 | ) -> None: ... 298 | def delete(self, recursive: bool=False) -> None: ... 299 | def hold(self, tag: str, recursive: bool=False) -> None: ... 300 | def release(self, tag: str, recursive: bool=False) -> None: ... 301 | def rollback(self, force: bool=False) -> None: ... 302 | def send( 303 | self, 304 | fd: int, 305 | fromname: Optional[str]=None, 306 | flags: Set[str]=set() 307 | ) -> None: ... 308 | def __getstate__(self) -> Dict[str, str]: ... 309 | 310 | class ZFSUserProperty(ZFSProperty): 311 | name: str = ... 312 | rawvalue = ... # type: Any 313 | source = ... # type: Any 314 | value: str = ... 315 | def __init__(self, value: Optional[Any]) -> None: ... 316 | 317 | # class ZFSVdev: 318 | # children = ... # type: Any 319 | # disks = ... # type: Any 320 | # group = ... # type: Any 321 | # guid = ... # type: Any 322 | # parent = ... # type: Any 323 | # path = ... # type: Any 324 | # root = ... # type: Any 325 | # size = ... # type: Any 326 | # stats = ... # type: Any 327 | # status = ... # type: Any 328 | # type = ... # type: Any 329 | # zpool = ... # type: Any 330 | # def __init__(self, *args, **kwargs): ... 331 | # def add_child_vdev(self, *args, **kwargs): ... 332 | # def attach(self, *args, **kwargs): ... 333 | # def degrade(self, *args, **kwargs): ... 334 | # def detach(self, *args, **kwargs): ... 335 | # def fault(self, *args, **kwargs): ... 336 | # def offline(self, *args, **kwargs): ... 337 | # def online(self, *args, **kwargs): ... 338 | # def remove(self, *args, **kwargs): ... 339 | # def replace(self, *args, **kwargs): ... 340 | # def __getstate__(self): ... 341 | # def __reduce_cython__(self, *args, **kwargs): ... 342 | # def __setstate_cython__(self, *args, **kwargs): ... 343 | 344 | # class ZFSVdevStats: 345 | # allocated = ... # type: Any 346 | # bytes = ... # type: Any 347 | # checksum_errors = ... # type: Any 348 | # configured_ashift = ... # type: Any 349 | # fragmentation = ... # type: Any 350 | # logical_ashift = ... # type: Any 351 | # ops = ... # type: Any 352 | # physical_ashift = ... # type: Any 353 | # read_errors = ... # type: Any 354 | # size = ... # type: Any 355 | # timestamp = ... # type: Any 356 | # vdev = ... # type: Any 357 | # write_errors = ... # type: Any 358 | # def __init__(self, *args, **kwargs): ... 359 | # def __getstate__(self): ... 360 | # def __reduce_cython__(self, *args, **kwargs): ... 361 | # def __setstate_cython__(self, *args, **kwargs): ... 362 | 363 | # class ZPoolFeature: 364 | # description = ... # type: Any 365 | # guid = ... # type: Any 366 | # name: str = ... 367 | # pool: ZFSPool = ... 368 | # state = ... # type: Any 369 | # def __init__(self, *args, **kwargs): ... 370 | # def enable(self, *args, **kwargs): ... 371 | # def __getstate__(self): ... 372 | # def __reduce_cython__(self, *args, **kwargs): ... 373 | # def __setstate_cython__(self, *args, **kwargs): ... 374 | 375 | # class ZPoolProperty: 376 | # allowed_values = ... # type: Any 377 | # name: str = ... 378 | # parsed = ... # type: Any 379 | # pool = ... # type: Any 380 | # rawvalue = ... # type: Any 381 | # source = ... # type: Any 382 | # value: str = ... 383 | # def __init__(self, *args, **kwargs): ... 384 | # def reset(self, *args, **kwargs): ... 385 | # def __getstate__(self): ... 386 | # def __reduce_cython__(self, *args, **kwargs): ... 387 | # def __setstate_cython__(self, *args, **kwargs): ... 388 | 389 | # class ZPoolScrub: 390 | # bytes_scanned = ... # type: Any 391 | # bytes_to_scan = ... # type: Any 392 | # end_time = ... # type: Any 393 | # errors = ... # type: Any 394 | # function = ... # type: Any 395 | # percentage = ... # type: Any 396 | # pool = ... # type: Any 397 | # root = ... # type: Any 398 | # start_time = ... # type: Any 399 | # stat = ... # type: Any 400 | # state = ... # type: Any 401 | # def __init__(self, *args, **kwargs): ... 402 | # def __getstate__(self): ... 403 | # def __reduce_cython__(self, *args, **kwargs): ... 404 | # def __setstate_cython__(self, *args, **kwargs): ... 405 | 406 | # class ZfsConverter: 407 | # __init__ = ... # type: Any 408 | # to_native = ... # type: Any 409 | # to_property = ... # type: Any 410 | --------------------------------------------------------------------------------