Ejemplo n.º 1
0
def update(instance: str,
           source: str,
           literal_url: bool = False,
           restart: bool = False) -> None:
    """Change the Jar File of a server.

    Stops the Server if necessary, deletes the old Jar File and copies the new one, starts the Server again.

    Arguments:
        instance (str): The Instance ID.
        source (str): The Type ID or URL of the new minecraft server Jar.
        literal_url (bool): Determines if the TypeID is a literal URL. Default: False
        allow_restart (bool): Allows a Server restart if the Server is running. Default: False
    """
    jar_src, version = web.pull(source, literal_url)
    jar_dest = storage.get_instance_path(instance) / "server.jar"
    storage.symlink(jar_src, jar_dest)

    additions = ''
    unit = service.get_unit(instance)
    if service.is_active(unit) and restart:
        notified_set_status(instance, "restart",
                            f"Updating to Version {version}")
    else:
        additions = " Manual restart required."
    print(f"Update successful.{additions}")
Ejemplo n.º 2
0
def list_instances(filter_str: str = '') -> None:
    """Print a list of all instances.

    Output a table of all instances with their respective Name, Server Version String, Status and persistence.

    Keyword Arguments:
        filter_str (str): Filter the list by instance name. (default: {''})
    """
    base_path = storage.get_instance_path(bare=True)
    server_paths = base_path.iterdir()
    servers = [x.name for x in server_paths]

    name_col_width = str(len(max(servers, key=len)) + 1)
    template = "{:" + name_col_width + "} {:<6} {:20} {:14} {:10} {:10}"
    title = template.format("Name", "Port", "Server Version", "Player Count",
                            "Status", "Persistent")

    print(title)
    for name in servers:
        if filter_str in name:
            cfg = config.get_properties(base_path / name / "server.properties")
            port = int(cfg.get("server-port"))
            server = MinecraftServer('localhost', port)
            status_info = status.get_simple_status(server)

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

            player_ratio = f"{status_info.get('online')}/{cfg.get('max-players')}"
            contents = template.format(name, port, status_info.get("version"),
                                       player_ratio, state.capitalize(),
                                       str(service.is_enabled(unit)))
            print(contents)
Ejemplo n.º 3
0
def install(instance: str, sources: list, restart: bool = False, autoupgrade: str = "ask") -> None:
    """Install a list of archived or bare plugins on a server.

    Args:
        instance (str): The name of the instance.
        sources (list): A list of zip and jar Files/URLs which contain or are Plugins.
        autoupgrade (bool): Uninstall similarly named Plugins but ask first.
        restart (bool, optional): Restart the Server after Installation. Defaults to False.

    Raises:
        FileNotFoundError: If a Plugin File or Archive is not found.
        ValueError: Unsupported File Format/No sources specified.
    """
    instance_path = storage.get_instance_path(instance)
    plugin_dest = storage.get_plugin_path(instance)

    if not instance_path.is_dir():
        raise FileNotFoundError(f"Instance not found: {instance_path}.")
    if not plugin_dest.is_dir():
        raise FileNotFoundError("This Instance does not support plugins.")
    if not sources:
        raise ValueError("No Plugins specified to install.")

    unique_files = set(sources)
    with tmpf.TemporaryDirectory() as tmp_dir:
        for source in unique_files:
            if web.is_url(source):
                print(f"Downloading '{source}'...")
                downloaded = web.download(source, tmp_dir)
                unique_files.add(downloaded)
                unique_files.discard(source)
        installed = set()
        plugin_sources = (Path(x) for x in unique_files)
        for plugin_source in plugin_sources:
            print(f"Installing '{plugin_source.name}'...")
            if plugin_source.suffix == ".zip":
                installed_files = storage.install_compressed_plugin(
                    plugin_source, plugin_dest)
                installed.update(installed_files)

            elif plugin_source.suffix == ".jar":
                installed_file = storage.install_bare_plugin(
                    plugin_source, plugin_dest)
                installed.add(installed_file)

            else:
                raise ValueError(f"'{plugin_source}' is not a .zip- or .jar-File.")
    state_note = "Manual restart/reload required."
    if restart:
        state_note = "Restarted Server."
        common.notified_set_status(instance, "restart", "Installing Plugins.")
    print(f"Installed {', '.join(installed)}. {state_note}")
    if autoupgrade:
        auto_uninstall(instance, installed, False)
Ejemplo n.º 4
0
def rename(instance: str, new_name: str) -> None:
    """Rename a server instance.

    A server instance is renamed. The server has to be stopped and disabled, so no invalid service links can occur.

    Arguments:
        instance (str): Current name of the Instance.
        new_name (str): New name of the Instance.
    """
    unit = service.get_unit(instance)
    if (service.is_enabled(unit) or service.is_active(unit)):
        raise OSError("The server is still persistent and/or running.")
    server_path = storage.get_instance_path(instance)
    server_path.rename(server_path.parent / new_name)
Ejemplo n.º 5
0
 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]
Ejemplo n.º 6
0
def mc_exec(instance: str,
            command: list,
            pollrate: float = 0.2,
            max_retries: int = 24,
            max_flush_retries: int = 4) -> None:
    """Execute a command on the console of a server.

    Uses the 'stuff' command of screen to pass the minecraft command to the server.
    Return Values are read from 'latest.log' shortly after the command is executed.
    The logfile is read every <timeout> seconds. If nothing is appended to the Log after the set amount of <retries>,
    the function exits. If there were already some lines received, the function tries <flush_retries> times before exiting.
    Like this, the function will more likely give an output, and will exit faster if an output was already returned.

    Arguments:
        instance (str): The name of the instance.
        command (list): A list of the individual parts of the command executed on the server console.

    Keyword Arguments:
        pollrate (float): The polling interval between log reads/checks. (default: {0.2})
        max_retries (int): The amount of retries when no lines have been pushed to console. (default: {25})
        max_flush_retries (int): The amount of retries when some lines have been pushed to console. (default: {10})
    """
    unit = service.get_unit(instance)
    if not service.is_active(unit):
        raise OSError("The Server is not running.")
    elif not status.is_ready(instance):
        raise ConnectionError("The Server is starting up.")

    log_path = storage.get_instance_path(instance) / "logs/latest.log"

    with open(log_path) as log_file:
        old_count = sum(1 for line in log_file) - 1

        jar_cmd = " ".join(command)
        # Use ^U^Y to cut and paste Text already in the Session
        cmd = shlex.split(
            f"screen -p 0 -S mc-{instance} -X stuff '^U{jar_cmd}^M^Y'")
        proc = sproc.Popen(cmd, preexec_fn=perms.demote())  # nopep8 pylint: disable=subprocess-popen-preexec-fn
        proc.wait()

        i = 0
        while i < max_retries:
            i += 1
            time.sleep(pollrate)
            log_file.seek(0)
            for j, line in enumerate(log_file):
                if j > old_count:
                    i = max_retries - max_flush_retries
                    print(line.rstrip())
                    old_count += 1
Ejemplo n.º 7
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.")
Ejemplo n.º 8
0
def uninstall(instance: str, plugins: list, restart: bool = False, force: bool = False) -> set:
    """Uninstall a Plugin from a Server Instance.

    Uninstall all plugins which contain an entry from {plugins} in their filename.

    Args:
        instance (str): The name of the instance.
        plugins (list): A list of plugin search terms, case insensitive.
        force (bool, optional): Don't prompt and proceed with deletion. Defaults to False.

    Raises:
        FileNotFoundError: If the instance does not exist or does not support plugins.

    Returns:
        set: A collection of all uninstalled plugins.
    """
    instance_path = storage.get_instance_path(instance)
    plugin_path = storage.get_plugin_path(instance)
    if not instance_path.is_dir():
        raise FileNotFoundError(f"Instance not found: {instance_path}.")
    if not plugin_path.is_dir():
        raise FileNotFoundError("This Instance does not support plugins.")

    installed_names = (x.name for x in plugin_path.iterdir()
                       if x.suffix == ".jar")
    resolved_names = set()
    for plugin_search in plugins:
        resolved_names.update(x for x in installed_names
                              if plugin_search.lower() in x.lower())
    if len(resolved_names) > 0:
        print("The following plugins will be removed:")
        print(f"  {', '.join(resolved_names)}")
        if force or visuals.bool_selector("Is this ok?"):
            if restart:
                common.notified_set_status(instance, "stop", "Removing Plugins.")
            for plugin_name in resolved_names:
                rm_path = plugin_path / plugin_name
                rm_path.unlink()
            if restart:
                common.notified_set_status(instance, "start")
            print(f"Removed {', '.join(resolved_names)}")
            return resolved_names
    else:
        print("No plugins found to uninstall.")
    return set()
Ejemplo n.º 9
0
def shell(instance_subfolder: str, shell_path: Path) -> None:
    """Create a shell process in the server directory.

    Launches a shell from the config file.

    Arguments:
        shell_path (Path): The Path to the Unix shell binary.
        instance_subfolder (str): The name of the instance or a subfolder in the Instance.
    """
    if instance_subfolder:
        sh_cwd = storage.get_instance_path(instance_subfolder)
        if not sh_cwd.is_dir():
            raise FileNotFoundError(
                f"Instance or subfolder not found: {sh_cwd}.")
    else:
        sh_cwd = storage.get_home_path()

    cmd = shlex.split(str(shell_path))
    proc = sproc.Popen(cmd, cwd=sh_cwd)
    proc.wait()
Ejemplo n.º 10
0
def is_ready(instance: str) -> bool:
    """Check if the Server is ready to serve connections/is fully started.

    Args:
        instance (str): The Instance ID.

    Returns:
        bool: True if the Server is ready to serve connections.
    """
    cfg = config.get_properties(
        storage.get_instance_path(instance) / "server.properties")
    port = int(cfg.get("server-port"))
    try:
        server = MinecraftServer('localhost', port)
        mc_status = server.status()
        proto = mc_status.version.protocol
    except (ConnectionError, sock_error):
        return False

    return proto > -1
Ejemplo n.º 11
0
def list_plugins(filter_str: str = '') -> None:
    """List all Servers which have plugins installed.

    Args:
        filter_str (str, optional): Simple line filter. Filter by Instance or plugin name. Defaults to ''.
    """
    base_path = storage.get_instance_path(bare=True)
    instance_paths = base_path.iterdir()

    template = "{:16} {:^14} {}"
    print(template.format("Instance", "Plugins", "Installed"))

    for instance_path in instance_paths:
        instance = instance_path.name
        try:
            plugins = get_plugins(instance)
        except FileNotFoundError:
            plugins = ()
        resolved = template.format(
            instance, ("supported" if plugins else "not supported"), ", ".join(plugins))
        if filter_str in resolved:
            print(resolved)
Ejemplo n.º 12
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
Ejemplo n.º 13
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")