Esempio n. 1
0
def create(instance: str,
           source: str,
           memory: str,
           properties: list,
           literal_url: bool = False,
           start: bool = False) -> None:
    """Create a new Minecraft Server Instance.

    Downloads the correct jar-file, configures the server and asks the user to accept the EULA.

    Arguments:
        instance (str): The Instance ID.
        source (str): The Type ID of the Minecraft Server Binary.
        memory (str): The Memory-String. Can be appended by K, M or G, to signal Kilo- Mega- or Gigabytes.
        properties (list): A list with Strings in the format of "KEY=VALUE".
        literal_url (bool): Determines if the TypeID is a literal URL. Default: False
        start (bool): Starts the Server directly if set to True. Default: False
    """
    instance_path = storage.get_instance_path(instance)
    if instance_path.exists():
        raise FileExistsError("Instance already exists.")

    storage.create_dirs(instance_path)

    jar_path_src, version = web.pull(source, literal_url)
    jar_path_dest = instance_path / "server.jar"
    storage.symlink(jar_path_src, jar_path_dest)
    proc.pre_start(jar_path_dest)
    if config.accept_eula(instance_path):
        if properties:
            properties_dict = config.properties_to_dict(properties)
            config.set_properties(instance_path / "server.properties",
                                  properties_dict)
        if memory:
            env_path = instance_path / CFGVARS.get('system', 'env_file')
            config.set_properties(env_path, {"MEM": memory})
        if start:
            notified_set_status(instance, "start", persistent=True)

        started = "and started " if start else ""
        print(f"Configured {started}with Version '{version}'.")

    else:
        print("How can you not agree that tacos are tasty?!?")
        raise ValueError("EULA was not accepted.")
Esempio n. 2
0
def demote() -> Callable:
    """Demote a subprocess. for use in preexec_fn.

    Returns:
        Callable: Returns a function executed by Popen() before running the external command.
    """
    user_name = CFGVARS.get('system', 'server_user')
    user = getpwnam(user_name)

    def set_ids() -> None:
        if os.getgid() + os.getuid() == 0:
            # Set EGID and EUID so that GID and UID can be set correctly.
            os.setegid(0)
            os.seteuid(0)

            os.setgid(user.pw_gid)
            os.setuid(user.pw_uid)

    return set_ids
Esempio n. 3
0
def remove_jar(source: str, force: bool = False) -> None:
    """Remove an .jar-File from disk.

    Arguments:
        type_id (str): The type_id of the .jar-File to be deleted.
    Keyword Arguments:
        force (bool): Delete the Jar File without a prompt. (default: {False})
    """
    del_all = source == "all"
    if not del_all:
        del_path = get_jar_path(source)
        msg = f"Are you absolutely sure you want to remove the Server Jar '{source}'?"
    else:
        del_path = get_jar_path(bare=True)
        msg = "Are you sure you want to remove all unused cached Server Jars?"

    if not del_all:
        if not del_path.exists():
            raise FileNotFoundError(f"Type-ID not found in cache: {del_path}.")
        all_instances = get_instance_path(bare=True).iterdir()
        for instance_path in all_instances:
            env_path = instance_path / CFGVARS.get('system', 'env_file')
            try:
                env = config.get_properties(env_path)
            except OSError:
                env = {}
            server_jar = instance_path / env.get("JARFILE", "server.jar")
            if get_real_abspath(server_jar) == del_path:
                raise f"Type-ID is associated with Instance {instance_path.name}."
    else:
        if not del_path.exists():
            raise FileNotFoundError("Cache already cleared.")
    if force or visuals.bool_selector(msg):
        if not del_all:
            del_path.unlink()
        else:
            remove_all(del_path)
Esempio n. 4
0
def get_permlevel(args: ap.Namespace, elevation: dict) -> dict:
    """Determine the Permission Level by arguments. Returns the User with sufficient Permissions.

    Args:
        args (Namespace): Parsed Parameters.
        elevation (dict): a dict containing the following keys:
            - default (required): The default user for which the app runs (can be "login_user", "server_user", or "root").
            - change_to: Which User to change to (can be "login_user", "server_user", or "root"). Applied if on_cond is omitted or a Condition of it was met.
            - on_cond: a dict containing "name of a parameter: desired value". At least one must apply to trigger a change_to user change.
            - change_fully: Determines if the Process is demoted internally, only applies if change_to is "root".

    Returns:
        dict: The Name of the User with sufficient permissions for the Action,
              and if no further demotion is needed.
    """
    users = {
        "login_user": LOGIN_USER,
        "server_user": CFGVARS.get('system', 'server_user'),
        "root": "root"
    }

    permissions = {"usr": users.get(elevation.get("default"))}
    conditions = elevation.get("on_cond")
    cond_match = not bool(conditions)
    if conditions:
        kwargs = vars(args)
        for key, val in conditions.items():
            if key in kwargs and kwargs[key] == val:
                cond_match = True
                break

    change_to = elevation.get("change_to")
    if cond_match and change_to:
        permissions["usr"] = users.get(change_to)
        if not elevation.get("change_fully", False):
            permissions["eusr"] = users.get(elevation.get("default"))
    return permissions
Esempio n. 5
0
def collect_server_data(instance: str) -> dict:
    """Collect Data about the server and return it as a dict.

    Args:
        instance (str): The instance of which the data should be collected.

    Returns:
        dict: A dict containing various Information about the server.
    """
    instance_path = storage.get_instance_path(instance)
    if not instance_path.exists():
        raise FileNotFoundError(f"Instance not found: {instance_path}.")

    properties = config.get_properties(instance_path / "server.properties")
    try:
        envinfo = config.get_properties(instance_path /
                                        CFGVARS.get('system', 'env_file'))
    except FileNotFoundError:
        envinfo = None

    port = properties.get("server-port")
    server = MinecraftServer('localhost', int(port))
    status_info = status.get_simple_status(server)

    files = storage.get_child_paths(instance_path)
    total_size = sum(x.stat().st_size for x in files)

    unit = service.get_unit(instance)
    state = get_online_state(unit, status_info.get("proto"))

    cmdvars = {
        k: v
        for k, v in (x.decode().split("=") for x in unit.Service.Environment)
    }
    cmd = " ".join(x.decode() for x in unit.Service.ExecStart[0][1])
    resolved_cmd = cmd.replace("${", "{").format(**cmdvars)

    jar_path = instance_path / cmdvars.get("JARFILE", "server.jar")
    resolved_jar_path = storage.get_real_abspath(jar_path)
    type_id = None
    if resolved_jar_path != jar_path:
        type_id = storage.get_type_id(resolved_jar_path)

    try:
        with open(instance_path / "whitelist.json") as wlist_hnd:
            whitelist = json.load(wlist_hnd)
    except FileNotFoundError:
        whitelist = None

    try:
        plugins = plugin.get_plugins(instance)
    except FileNotFoundError:
        plugins = None

    data = {
        "instance_name": instance,
        "instance_path": str(instance_path),
        "total_file_size": total_size,
        "status": {
            "players_online": status_info.get('online'),
            "players_max": properties.get('max-players', '?'),
            "protocol_name": status_info.get('proto'),
            "protocol_version": status_info.get('version'),
        },
        "service": {
            "description": unit.Unit.Description.decode(),
            "unit_file_state": unit.UnitFileState.decode(),
            "state": state,
            "main_pid": unit.Service.MainPID,
            "start_command": resolved_cmd,
            "memory_usage": unit.Service.MemoryCurrent,
            "env": dict(cmdvars),
        },
        "type_id": type_id,
        "config": {
            "server.properties": properties,
            "whitelist": whitelist,
            "env_file": envinfo
        },
        "plugins": plugins,
    }

    return data
Esempio n. 6
0
def configure(instance: str,
              editor: str,
              properties: list = None,
              edit_paths: list = None,
              memory: str = None,
              restart: bool = False) -> None:
    """Edits configurations, restarts the server if forced, and swaps in the new configurations.

    Args:
        instance (str): The Instance ID.
        editor (str): A Path to an Editor Binary.
        properties (list): The Properties to be changed in the server.properties File.
        edit_paths (list): The Paths to be edited interactively with the specified Editor.
        memory (str): Update the Memory Allocation. Can be appended by K, M or G, to signal Kilo- Mega- or Gigabytes.
        restart (bool, optional): Stops the server, applies changes and starts it again when set to true.
        Defaults to False.
    """
    if not any((properties, edit_paths, memory)):
        raise ValueError("No properties or files to edit specified.")

    instance_path = storage.get_instance_path(instance)
    paths = {}

    if properties:
        properties_path = instance_path / "server.properties"
        tmp_path = storage.tmpcopy(properties_path)
        properties_dict = config.properties_to_dict(properties)
        config.set_properties(tmp_path, properties_dict)
        paths.update({properties_path: tmp_path})

    if memory:
        env_path = instance_path / CFGVARS.get('system', 'env_file')
        tmp_path = storage.tmpcopy(env_path)
        config.set_properties(tmp_path, {"MEM": memory})
        paths.update({env_path: tmp_path})

    if edit_paths:
        for file_path in edit_paths:
            # Check if a Temporary File of the Config already exists
            if file_path not in paths.keys():
                abspath = instance_path / file_path
                tmp_path = storage.tmpcopy(abspath)
                proc.edit(tmp_path, editor)
                if storage.get_file_hash(tmp_path) != storage.get_file_hash(
                        abspath):
                    paths.update({abspath: tmp_path})
                else:
                    tmp_path.unlink()
            else:
                proc.edit(paths[file_path], editor)

    unit = service.get_unit(instance)
    do_restart = service.is_active(unit) and len(paths) > 0 and restart
    if do_restart:
        notified_set_status(instance, "stop",
                            "Reconfiguring and restarting Server.")

    for dst, src in paths.items():
        storage.move(src, dst)

    if do_restart:
        notified_set_status(instance, "start")
Esempio n. 7
0
import os
import gzip
import shutil
import random
import string
import hashlib
import tempfile as tmpf
import zipfile as zf
from pathlib import Path
from datetime import datetime
from grp import getgrgid
from pwd import getpwnam
from mcctl import service, config, visuals, perms, CFGVARS

SERVER_USER = CFGVARS.get('system', 'server_user')


def get_home_path(user_name: str = SERVER_USER) -> Path:
    """Return the home directory of a user.

    Arguments:
        user_name (str): The username of which the home directory should be determined.

    Returns:
        Path: The home directory of the user.
    """
    user_data = getpwnam(user_name)
    return Path(user_data.pw_dir)

Esempio n. 8
0
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# mcctl is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with mcctl. If not, see <http:// www.gnu.org/licenses/>.

import time
from pystemd.systemd1 import Unit, Manager
from mcctl import CFGVARS, perms

UNIT_NAME = CFGVARS.get('system', 'systemd_service')


def get_unit(instance: str) -> Unit:
    """Return the systemd Unit name of the Instance.

    Args:
        instance (str): The name of the instance.

    Returns:
        Unit: The systemd service Unit.
    """
    unit = Unit(f"{UNIT_NAME}@{instance}.service")
    unit.load()
    return unit
Esempio n. 9
0
def get_parser() -> ap.ArgumentParser:
    """Parse Arguments from the Command Line input and returns the converted Values.

    Returns:
        ArgumentParser: A parser with all arguments.

    Raises:
        argparse.ArgumentTypeError: Raised when the parameters given cannot be parsed correctly.
    """
    def check_type_id(value: str) -> str:
        if not (re.search(r'^[a-z0-9\._-]+(:[a-z0-9\._-]+){1,2}$', value)
                or web.is_url(value)):
            raise ap.ArgumentTypeError(
                "must be in the form '<TYPE>:<VERSION>:<BUILD>' or URL.")
        return value

    def check_strict_type_id(value: str) -> str:
        if not re.search(r'^[a-z0-9\._-]+(:[a-z0-9\._-]+){1,2}$|^all$', value):
            raise ap.ArgumentTypeError(
                "must be in the form '<TYPE>:<VERSION>:<BUILD>' or 'all'.")
        return value

    def check_mem(value: str) -> str:
        if not re.search(r'^[0-9]*[1-9]+[0-9]*[KMG]$', value):
            raise ap.ArgumentTypeError("Must be in Format <NUMBER>{K,M,G}.")
        return value

    def autocomplete_instance(value: str) -> str:
        base = storage.get_instance_path(bare=True)
        try:
            matches = [
                x.name for x in base.iterdir() if x.name.startswith(value)
            ]
        except OSError:
            return value
        if len(matches) > 1:
            raise ap.ArgumentTypeError(
                f"Instance Name '{value}' is ambiguous.")
        elif len(matches) < 1:
            return value
        return matches[0]

    default_err_template = "{args.action} instance '{args.instance}'"
    no_elev = {"default": "server_user"}
    semi_elev = {"default": "server_user", "change_to": "root"}
    root_elev = {
        "default": "login_user",
        "change_to": "root",
        "change_fully": True
    }
    re_start_elev = {
        "default": "server_user",
        "change_to": "root",
        "on_cond": {
            'restart': True,
            'start': True
        }
    }

    parser = ap.ArgumentParser(
        "mcctl",
        description=
        "Manage, configure, and create multiple Minecraft servers easily with a command-line interface.\n"
        f"Version: {__version__}",
        formatter_class=ap.ArgumentDefaultsHelpFormatter)
    parser.add_argument("-v",
                        "--verbose",
                        action='store_true',
                        help="Enable verbose/debugging output.")
    parser.set_defaults(err_template=default_err_template, elevation=no_elev)

    subparsers = parser.add_subparsers(title="actions", dest="action")
    subparsers.required = True

    force_parser = ap.ArgumentParser(add_help=False)
    force_parser.add_argument("-f",
                              "--force",
                              action="store_true",
                              help="Proceed without a prompt.")

    type_id_parser = ap.ArgumentParser(add_help=False,
                                       formatter_class=ap.RawTextHelpFormatter)
    type_id_parser.add_argument("-u",
                                "--url",
                                dest="literal_url",
                                action='store_true',
                                help="Treat the TypeID Value as a URL.")
    type_id_parser.add_argument(
        "source",
        metavar="TYPEID_OR_URL",
        type=check_type_id,
        help=("Type ID in '<TYPE>:<VERSION>:<BUILD>' format.\n"
              "'<TYPE>:latest' or '<TYPE>:latest-snap' are also allowed.\n"
              "Types: 'paper', 'spigot', 'vanilla'\n"
              "Versions: e.g. '1.15.2', 'latest'\n"
              "Build (only for paper): e.g. '122', 'latest'\n"))

    # Auto-Completing Instance Name
    existing_instance_parser = ap.ArgumentParser(add_help=False)
    existing_instance_parser.add_argument(
        "instance",
        metavar="INSTANCE_ID",
        type=autocomplete_instance,
        help="Instance Name of the Minecraft Server.")
    # New Instance Name
    instance_parser = ap.ArgumentParser(add_help=False)
    instance_parser.add_argument("instance",
                                 metavar="INSTANCE_ID",
                                 help="Instance Name of the Minecraft Server.")
    # Optional Instance Name
    instance_subfolder_parser = ap.ArgumentParser(add_help=False)
    instance_subfolder_parser.add_argument(
        "instance_subfolder",
        metavar="INSTANCE/SUBFOLDER",
        nargs="?",
        type=autocomplete_instance,
        help="Instance Name or Subpath in Instance Files, e.g. INSTANCE/world."
    )

    message_parser = ap.ArgumentParser(add_help=False)
    message_parser.add_argument(
        "-m",
        "--message",
        help="Reason for the restart/stop. Informs the Players on the Server.")

    restart_parser = ap.ArgumentParser(add_help=False)
    restart_parser.add_argument(
        "-r",
        "--restart",
        action='store_true',
        help="Stop the Server, apply config changes, and start it again.")

    memory_parser = ap.ArgumentParser(add_help=False)
    memory_parser.add_argument(
        "-m",
        "--memory",
        type=check_mem,
        help="Memory Allocation for the Server in {K,M,G}Bytes, e.g. 2G, 1024M."
    )

    parser_attach = subparsers.add_parser(
        "attach",
        parents=[existing_instance_parser],
        help="Attach to the Console of the Minecraft Instance.")
    parser_attach.set_defaults(func=proc.attach,
                               err_template="attach to '{args.instance}'")

    parser_config = subparsers.add_parser(
        "config",
        parents=[existing_instance_parser, restart_parser, memory_parser],
        help="Configure/Change Files of a Minecraft Server Instance.")
    parser_config.add_argument(
        "-e",
        "--edit",
        nargs="+",
        dest="edit_paths",
        metavar="FILE",
        help="Edit a File in the Instance Folder interactively.")
    parser_config.add_argument(
        "-p",
        "--properties",
        nargs="+",
        help=
        "Change server.properties options, e.g. server-port=25567 'motd=My new and cool Server'."
    )
    parser_config.set_defaults(func=common.configure,
                               err_template="configure '{args.instance}'",
                               editor=CFGVARS.get('user', 'editor'),
                               elevation=re_start_elev)

    parser_create = subparsers.add_parser(
        "create",
        parents=[instance_parser, type_id_parser, memory_parser],
        help="Create a new Server Instance.")
    parser_create.add_argument(
        "-s",
        "--start",
        action='store_true',
        help="Start the Server after creation, persistent enabled.")
    parser_create.add_argument(
        "-p",
        "--properties",
        nargs="+",
        help="server.properties options in 'KEY1=VALUE1 KEY2=VALUE2' Format.")
    parser_create.set_defaults(func=common.create,
                               elevation={
                                   "default": "server_user",
                                   "change_to": "root",
                                   "on_cond": {
                                       'start': True
                                   }
                               })

    parser_exec = subparsers.add_parser(
        "exec",
        parents=[existing_instance_parser],
        help="Execute a command in the Console of the Instance.")
    parser_exec.add_argument("command",
                             nargs="+",
                             help="Command to execute in the Server Console.")
    parser_exec.set_defaults(func=proc.mc_exec,
                             err_template="execute command on {args.instance}")

    parser_export = subparsers.add_parser(
        "export",
        parents=[existing_instance_parser],
        help="Export an Instance to a zip File.")
    parser_export.add_argument("-c",
                               "--compress",
                               action='store_true',
                               help="Compress the Archive.")
    parser_export.add_argument("-w",
                               "--world-only",
                               action='store_true',
                               help="Only export World Data.")
    parser_export.set_defaults(func=storage.export, elevation=semi_elev)

    parser_import = subparsers.add_parser(
        "import", help="Import an Instance from a zip File.")
    parser_import.add_argument("zip_path",
                               metavar="ZIPFILE",
                               help="The Path of the archived Server.")
    parser_import.add_argument("-i",
                               "--instance",
                               help="Instance Name of the Minecraft Server.")
    parser_import.add_argument(
        "-w",
        "--world-only",
        action='store_true',
        help="Only import World Data (the instance must already exist).")
    parser_import.set_defaults(err_template="{args.action} '{args.zip_path}'",
                               func=storage.mc_import,
                               elevation=root_elev)

    parser_logs = subparsers.add_parser("logs",
                                        parents=[existing_instance_parser],
                                        help="Read the Logs of a Server.")
    parser_logs.add_argument("-n",
                             "--lines",
                             dest="limit",
                             type=int,
                             default=0,
                             help="Limit the line output count to n.")
    parser_logs.set_defaults(func=storage.logs,
                             err_template="read logs of '{args.instance}'")

    parser_inspect = subparsers.add_parser(
        "inspect",
        parents=[existing_instance_parser],
        help=
        "Get any information you can think of about the server in json format."
    )
    parser_inspect.set_defaults(func=common.inspect,
                                err_template="{args.action} {args.instance}")

    parser_install = subparsers.add_parser(
        "install",
        parents=[existing_instance_parser, restart_parser],
        formatter_class=ap.RawTextHelpFormatter,
        help=
        "Install or update a server Plugin from a local Path/URL/Zip File.\n"
        "Old plugins closely matching the new name can optionally be deleted.")
    parser_install.add_argument(
        "sources",
        metavar="LOCAL_PATH_OR_URL",
        nargs="+",
        help="Paths or URLs which point to Plugins or zip Files.")
    parser_install.add_argument(
        "-a",
        "--autoupgrade",
        action='store_true',
        help=
        "Upgrade plugin and remove previous versions by name (interactive).")
    parser_install.set_defaults(
        func=plugin.install,
        elevation=root_elev,
        err_template="{args.action} plugins on {args.instance}")

    parser_list = subparsers.add_parser(
        "ls", help="List Instances, installed Versions, Plugins, etc.")
    parser_list.add_argument("what",
                             metavar="WHAT",
                             nargs="?",
                             choices=("plugins", "instances", "jars"),
                             default="instances",
                             help="What Type (instnaces/jars) to return.")
    parser_list.add_argument("-f",
                             "--filter",
                             dest="filter_str",
                             default='',
                             help="Filter by Version or Instance Name, etc.")
    parser_list.set_defaults(func=common.mc_ls,
                             err_template="{args.action} {args.what}")

    parser_pull = subparsers.add_parser(
        "pull",
        parents=[type_id_parser],
        help="Pull a Server .jar-File from the Internet.")
    parser_pull.set_defaults(
        func=web.pull,
        err_template="{args.action} Server Type '{args.source}'")

    parser_rename = subparsers.add_parser("rename",
                                          parents=[existing_instance_parser],
                                          help="Rename a Server Instance.")
    parser_rename.add_argument("new_name",
                               metavar="NEW_NAME",
                               help="The new Name of the Server Instance.")
    parser_rename.set_defaults(func=common.rename)

    parser_restart = subparsers.add_parser(
        "restart",
        parents=[existing_instance_parser, message_parser],
        help="Restart a Server Instance.")
    parser_restart.set_defaults(func=common.notified_set_status,
                                elevation=semi_elev)

    parser_remove = subparsers.add_parser(
        "rm",
        parents=[existing_instance_parser, force_parser],
        help="Remove a Server Instance.")
    parser_remove.set_defaults(func=storage.remove,
                               err_template="remove '{args.instance}'")

    parser_remove_jar = subparsers.add_parser(
        "rmj", parents=[force_parser], help="Remove a cached Server Binary.")
    parser_remove_jar.add_argument(
        "source",
        metavar="TYPEID",
        type=check_strict_type_id,
        help=("Type ID in '<TYPE>:<VERSION>:<BUILD>' format.\n"
              "'<TYPE>:latest' or '<TYPE>:latest-snap' are NOT allowed.\n"
              "'all' removes all cached Files.\n"))
    parser_remove_jar.set_defaults(
        func=storage.remove_jar,
        err_template="remove .jar File '{args.source}'")

    parser_start = subparsers.add_parser("start",
                                         parents=[existing_instance_parser],
                                         help="Start a Server Instance.")
    parser_start.add_argument("-p",
                              "--persistent",
                              action='store_true',
                              help="Start even after Reboot.")
    parser_start.set_defaults(func=common.notified_set_status,
                              elevation=semi_elev)

    parser_status = subparsers.add_parser(
        "status",
        parents=[existing_instance_parser],
        help="Get extensive Information about the Server Instance.")
    parser_status.set_defaults(
        func=common.mc_status,
        err_template="retrieve {args.action} of '{args.instance}'",
        elevation=semi_elev)

    parser_stop = subparsers.add_parser(
        "stop",
        parents=[existing_instance_parser, message_parser],
        help="Stop a Server Instance.")
    parser_stop.add_argument("-p",
                             "--persistent",
                             action='store_true',
                             help="Do not start again after Reboot.")
    parser_stop.set_defaults(func=common.notified_set_status,
                             elevation=semi_elev)

    parser_uninstall = subparsers.add_parser(
        "uninstall",
        parents=[existing_instance_parser, restart_parser, force_parser],
        help="Uninstall a server Plugin by File Name.")
    parser_uninstall.add_argument(
        "plugins",
        metavar="PLUGIN_NAME",
        nargs="+",
        help="Plugin File names in the plugins folder of the instance.")
    parser_uninstall.set_defaults(
        func=plugin.uninstall,
        elevation=re_start_elev,
        err_template="{args.action} plugins on {args.instance}")

    parser_update = subparsers.add_parser(
        "update",
        parents=[existing_instance_parser, type_id_parser, restart_parser],
        help="Update a Server Instance.")
    parser_update.set_defaults(func=common.update, elevation=re_start_elev)

    parser_shell = subparsers.add_parser(
        "shell",
        parents=[instance_subfolder_parser],
        help="Use a Shell to interactively edit a Server Instance.")
    parser_shell.set_defaults(func=proc.shell,
                              err_template="invoke a Shell",
                              shell_path=CFGVARS.get('user', 'shell'))

    parser_wcfg = subparsers.add_parser(
        "write-cfg", help="Write mcctl configuration and exit.")
    parser_wcfg.add_argument(
        "-u",
        "--user",
        action="store_true",
        help=
        "Write the Configuration in the Home of the user logged in instead of /etc."
    )
    parser_wcfg.set_defaults(func=write_cfg,
                             err_template="write Configuration File",
                             elevation={
                                 "default": "login_user",
                                 "change_to": "root",
                                 "change_fully": True,
                                 "on_cond": {
                                     'user': False
                                 }
                             })

    return parser