Beispiel #1
0
class EssexPapertrail(ColorApp):
    """Print a sample Papertrail log_files.yml"""

    interactive = Flag(
        ['i', 'interactive'],
        help="interactively ask the user for host and port"
    )

    def main(self, host='{{ PAPERTRAIL_HOST }}', port='{{ PAPERTRAIL_PORT }}'):
        if self.interactive:
            host = input("Papertrail host: ")
            port = input("Papertrail port: ")
        entries = '\n'.join(
            f"  - tag: {svc.name}\n"
            f"    path: {self.parent.logs_dir / svc.name / 'current'}"
            for svc in self.parent.svcs
        )
        print(
            f"files:",
              f"{entries}",
            f"destination:",
            f"  host: {host}",
            f"  port: {port}",
            f"  protocol: tls", sep='\n'
        )
Beispiel #2
0
class EssexStatus(ColorApp):
    """View the current states of (all or specified) services"""

    enabled_only = Flag(
        ['e', 'enabled'],
        help="only list enabled services (configured to be running)"
    )

    def main(self, *svc_names):
        self.parent.fail_if_unsupervised()
        s6_svscanctl('-a', self.parent.svcs_dir)
        cols = (
            'up', 'wantedup', 'normallyup', 'ready', 'paused', 'pid',
            'exitcode', 'signal', 'signum', 'updownsince', 'readysince',
            'updownfor', 'readyfor'
        )
        errors = False
        for svc in self.parent.svc_map(svc_names or self.parent.svcs):
            if 'run' in svc:
                if self.enabled_only and 'down' in svc:
                    continue
                stats = {
                    col: False if val == 'false' else val
                    for col, val in zip(
                        cols, s6_svstat('-o', ','.join(cols), svc).split()
                    )
                }
                statline = f"{svc.name:<20} {'up' if stats['up'] else 'down':<5} {stats['updownfor'] + 's':<10} {stats['pid'] if stats['pid'] != '-1' else stats['exitcode']:<6} {'autorestarts' if stats['wantedup'] else '':<13} {'autostarts' if stats['normallyup'] else '':<11}"
                print(statline | (green if stats['up'] else red))
            else:
                warn(f"{svc} doesn't exist")
                errors = True
        if errors:
            fail(1)
Beispiel #3
0
class EssexTree(ColorApp):
    """View the process tree from the supervision root"""

    quiet = Flag(
        ['q', 'quiet'],
        help=(
            "don't print childless supervisors, s6-log processes, or s6-log supervisors; "
            "has no effect when pstree is provided by busybox"
        )
    )

    def main(self):
        self.parent.fail_if_unsupervised()
        try:
            readlink(pstree)
        except:  # real pstree
            tree = pstree['-apT', self.parent.root_pid]()
            if self.quiet:
                tl = tree.splitlines()
                whitelist = set(range(len(tl)))
                for i, line in enumerate(tl):
                    if re.match(r'^ +(\||`)-s6-supervise,', line):  # supervisor
                        if i + 1 == len(tl) or re.match(r'^ +(\||`)-s6-supervise,', tl[i + 1]):
                            whitelist.discard(i)
                    elif re.match(r'^ +\| +`-s6-log,', line):  # logger
                        whitelist.discard(i)
                        whitelist.discard(i - 1)
                tree = '\n'.join(tl[i] for i in sorted(whitelist))
        else:  # busybox pstree
            tree = pstree['-p', self.parent.root_pid]()
        print(tree)
Beispiel #4
0
class TemplateRenderer(Application):
    """
    Use json or yaml var_files to render template_file to an adjacent
    file with the same name but the extension stripped
    """

    VERSION = __version__

    overwrite = Flag(
        ['f', 'force'],
        help="Overwrite any existing destination file"
    )

    def main(self, template_file: ExistingFile, *var_files: ExistingFile):
        warn_shebangs(template_file)
        data = {}  # {var: value}
        var_mentions = defaultdict(list)  # {var: [files]}
        for vfile in var_files:
            loader = jloads if vfile.suffix.lower() == '.json' else yloads
            new_data = loader(vfile.read())
            for v in new_data:
                var_mentions[v].append(vfile)
            data.update(new_data)
        warn_overrides(var_mentions)
        rstr = Template(filename=str(template_file), data=data)()
        dest = template_file.with_suffix('')
        if not self.overwrite:
            try:
                NonexistentPath(dest)
            except ValueError as e:
                err(f"{type(e)}: {e}")
                err("Use -f/--force to overwrite")
                sys.exit(1)
        dest.write(rstr)
Beispiel #5
0
class EssexLog(ColorApp):
    """View (all or specified) services' current log files"""

    lines = SwitchAttr(
        ['n', 'lines'],
        argname='LINES',
        help=(
            "print only the last LINES lines from the service's current log file, "
            "or prepend a '+' to start at line LINES"
        ),
        default='+1'
    )

    follow = Flag(
        ['f', 'follow'],
        help="continue printing new lines as they are added to the log file"
    )

    debug = Flag(
        ['d', 'debug'],
        help="view the s6-svscan log file"
    )

    def main(self, *svc_names):
        logs = [
            self.parent.logs_dir / svc.name / 'current'
            for svc in self.parent.svc_map(svc_names or self.parent.svcs)
        ]
        if self.debug:
            logs.append(self.parent.logs_dir / '.s6-svscan' / 'current')
        if self.follow:
            with suppress(KeyboardInterrupt):
                try:
                    mtail = local.get('lnav', 'multitail')
                except CommandNotFound:
                    tail[['-n', self.lines, '-F'] + logs].run_fg()
                else:
                    mtail[logs].run_fg()
        else:
            for log in logs:
                if log.is_file():
                    tail['-vn', self.lines, log].run_fg()
                    print('\n')
Beispiel #6
0
class VWriter(Application):
    """
    Use json or yaml vars files to render each template file found recursively
    under the working folder, to an adjacent file with the same name but the
    extension stripped, overriding any vars in ./vars.json in the following
    order:

        <root_path>/vars.json,
        <root_path>/vars.yml,
        <root_path>/vars.yaml,
        <template.parent>/vars.json,
        <template.parent>/vars.yml,
        <template.parent>/vars.yaml

    Later entries override earlier ones.
    """

    VERSION = __version__

    overwrite = Flag(
        ['f', 'force'],
        help="Overwrite any existing destination file"
    )
    template_ext = SwitchAttr(
        ['t', 'template-ext'],
        help="Filename extension for templates",
        default='t'
    )
    vars_name = SwitchAttr(
        ['n', 'vars-name'],
        help="Filename (excluding extension) for each vars file",
        default='vars'
    )

    ext_precedence = ('json', 'yml', 'yaml')

    def get_vars_files(self, folder: ExistingDirectory):
        return [
            folder / f"{self.vars_name}.{ext}"
            for ext in self.ext_precedence
            if (folder / f"{self.vars_name}.{ext}").is_file()
        ]

    def main(self, root_path: ExistingDirectory=local.cwd):
        """root_path defaults to the process's working directory"""
        root_vfiles = self.get_vars_files(root_path)
        for tmplt in root_path.walk(lambda f: f.suffix == f".{self.template_ext}"):
            if tmplt.up() == root_path:
                vfiles = root_vfiles
            else:
                vfiles = (*root_vfiles, *self.get_vars_files(tmplt.up()))
            TemplateRenderer.invoke(tmplt, *vfiles, overwrite=self.overwrite)
Beispiel #7
0
class EssexList(ColorApp):
    """List all known services"""

    enabled_only = Flag(
        ['e', 'enabled'],
        help="only list enabled services (configured to be running)"
    )

    def main(self):
        if self.parent.svcs_dir.is_dir():
            if self.enabled_only:
                print(*(s for s in self.parent.svcs if 'down' not in s), sep='\n')
            else:
                print(*self.parent.svcs, sep='\n')
Beispiel #8
0
class Rawr(Application):

    VERSION = '0.0.1'
    adult = Flag(['a', 'adult'], help="Search (only) in the 'adult' category")

    def main(self, *search_terms):
        try:
            results = search(search_terms, adult=self.adult)
        except KeyboardInterrupt:
            results = None
        if not results:
            raise NoResults

        with suppress(CommandNotFound):
            print(
                f"{local['df']('-h', '-P', '.').splitlines()[-1].split()[3]} available"
                | yellow)
        uri = choose_result(results)
        if not uri:
            return
        try:
            clip(uri)
        except CommandNotFound:
            print(uri | blue)
        else:
            print("Magnet URI copied to clipboard" | green)

        show_connection()
        try:
            aria2c = local['aria2c']
        except CommandNotFound:
            print(
                "If I'd found the 'aria2c' command, I'd have offered to launch it for you."
                | yellow)
        else:
            if ask("Begin download with aria2" | magenta, True):
                # try:
                aria2c['--seed-time=0', uri] & FG
Beispiel #9
0
class EssexNew(ColorApp):
    """Create a new service"""

    working_dir = SwitchAttr(
        ['d', 'working-dir'],
        local.path,
        argname='WORKING_DIRECTORY',
        help=(
            "run the process from inside this folder; "
            "the default is SERVICES_DIRECTORY/svc_name"
        )
    )

    as_user = SwitchAttr(
        ['u', 'as-user'],
        argname='USERNAME',
        help="non-root user to run the new service as (only works for root)"
    )

    enabled = Flag(
        ['e', 'enable'],
        help="enable the new service after creation"
    )

    on_finish = SwitchAttr(
        ['f', 'finish'],
        argname='FINISH_CMD',
        help=(
            "command to run whenever the supervised process dies "
            "(must complete in under 5 seconds)"
        )
    )

    rotate_at = SwitchAttr(
        ['r', 'rotate-at'],
        Range(1, 256),
        argname='MEBIBYTES',
        help="archive each log file when it reaches MEBIBYTES mebibytes",
        default=4
    )

    prune_at = SwitchAttr(
        ['p', 'prune-at'],
        Range(0, 1024),
        argname='MEBIBYTES',
        help=(
            "keep up to MEBIBYTES mebibytes of logs before deleting the oldest; "
            "0 means never prune"
        ),
        default=40
    )

    on_rotate = SwitchAttr(
        ['o', 'on-rotate'],
        argname='PROCESSOR_CMD',
        help=(
            "processor command to run when rotating logs; "
            "receives log via stdin; "
            "its stdout is archived; "
            "PROCESSOR_CMD will be double-quoted"
        )
    )

    store = SwitchAttr(
        ['s', 'store'],
        argname='VARNAME=CMD',
        help=("run CMD and store its output in env var VARNAME before main cmd is run"),
        list=True
    )

    # TODO: use skabus-dyntee for socket-logging? maybe
    def main(self, svc_name, cmd):
        self.svc = self.parent.svcs_dir / svc_name
        if self.svc.exists():
            fail(1, f"{self.svc} already exists!")
        self.cmd = cmd
        if self.as_user and ':' in self.as_user:
            user, group = self.as_user.split(':', 1)
            if not user.isnumeric():
                user = uid('-u', user).strip()
            if not group.isnumeric():
                group = getent('group', group).split(':')[2]
            self.as_user = f"{user}:{group}"
        self.mk_runfile()
        self.mk_logger()
        if not self.enabled:
            (self.svc / 'down').touch()

    def mk_runfile(self):
        self.svc.mkdir()
        runfile = self.svc / 'run'
        shebang = ('#!/bin/execlineb -P', '')
        cmd = (self.cmd, "Do the thing")
        err_to_out = ('fdmove -c 2 1', "Send stderr to stdout")
        hash_run = (
            'foreground { redirfd -w 1 run.md5 md5sum run }',
            "Generate hashfile, to detect changes since launch"
        )
        set_user = (
            f's6-setuidgid {self.as_user}', "Run as this user"
        ) if self.as_user else None
        working_dir = (
            f'cd {self.working_dir}', "Enter working directory"
        ) if self.working_dir else None
        store_vars = []
        for store_var in self.store:
            var, store_cmd = store_var.split('=', 1)
            store_vars.append((f'backtick -n {var} {{ {store_cmd} }} importas -u {var} {var}', "Store command output"))
        runfile.write(columnize_comments(*filter(None, (
            shebang, err_to_out, hash_run, set_user, working_dir, *store_vars, cmd
        ))))
        runfile.chmod(0o755)
        if self.on_finish:
            runfile = self.svc / 'finish'
            shebang = ('#!/bin/execlineb', '')
            cmd = (self.on_finish, "Do the thing")
            runfile.write(columnize_comments(*filter(None, (
                shebang, err_to_out, set_user, cmd
            ))))
            runfile.chmod(0o755)

    def mk_logger(self):
        logger = self.svc / 'log'
        logger.mkdir()
        runfile = logger / 'run'
        shebang = ('#!/bin/execlineb -P', '')
        hash_run = (
            'foreground { redirfd -w 1 run.md5 md5sum run }',
            "Generate hashfile, to detect changes since launch"
        )
        receive = ('s6-log', "Receive process output")
        timestamp = ('  T', "Start each line with an ISO 8601 timestamp")
        rotate = (
            f'  s{self.rotate_at * 1024 ** 2}',
            "Archive log when it gets this big (bytes)"
        )
        prune = (
            f'  S{self.prune_at * 1024 ** 2}',
            "Purge oldest archived logs when the archive gets this big (bytes)"
        )
        process = (
            f'!"{self.on_rotate}"',
            "Processor (log --stdin--> processor --stdout--> archive)"
        ) if self.on_rotate else None
        logfile = (f'  {self.parent.logs_dir / self.svc.name}', "Store logs here")
        runfile.write(columnize_comments(*filter(None, (
            shebang, hash_run, receive, timestamp, rotate, prune, process, logfile
        ))))
        runfile.chmod(0o755)
Beispiel #10
0
class EssexPrint(ColorApp):
    """View (all or specified) services' run, finish, and log commands"""

    no_color = Flag(
        ['n', 'no-color'],
        help="do not colorize the output (for piping)"
    )

    run_only = Flag(
        ['r', 'run-only'],
        help="only print each service's runfile, ignoring any finish, crash, or logger scripts"
    )

    enabled_only = Flag(
        ['e', 'enabled'],
        help="only print contents of enabled services (configured to be running)"
    )

    def display(self, docpath):
        title_cat = tail['-vn', '+1', docpath]
        if self.no_color:
            title_cat.run_fg()
        else:
            try:
                (
                    title_cat |
                    local['highlight'][
                        '--stdout', '-O', 'truecolor', '-s', 'moria', '-S', 'sh'
                    ]
                ).run_fg()
            except CommandNotFound:
                try:
                    (
                        title_cat |
                        local['bat']['-p', '-l', 'sh']
                    ).run_fg()
                except CommandNotFound:
                    title_cat.run_fg()
        print('\n')

    def main(self, *svc_names):
        errors = False
        for svc in self.parent.svc_map(svc_names or self.parent.svcs):
            if self.enabled_only and 'down' in svc:
                continue
            found = False
            for file in ('run',) if self.run_only else ('run', 'finish', 'crash'):
                # if (runfile := svc / file).is_file():
                runfile = svc / file  #
                if runfile.is_file():  #
                    self.display(runfile)
                    found = True
            # if not self.run_only and (logger := svc / 'log' / 'run').is_file():
            logger = svc / 'log' / 'run'  #
            if not self.run_only and logger.is_file():  #
                self.display(logger)
                found = True
            if not found:
                warn(f"{svc} doesn't exist")
                errors = True
        if errors:
            fail(1)