Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/architecture/daemons.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,5 @@ The mechanics, in the order they are done:
config path.
6. Add the row to the instance table in {doc}`../operations/systemd` and to the daemon
inventory above.
7. Commit, then on the host `git pull` and `hispec deploy <instance>`, which
copies both files into `/etc/hispec`, enables and starts it.
251 changes: 163 additions & 88 deletions docs/operations/systemd.md

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions src/hispec/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
from .daemon import HispecDaemon
"""HISPEC instrument control software."""

__all__ = [
"HispecDaemon",
"HispecDaemon", # pylint: disable=undefined-all-variable
]


def __getattr__(name):
# Imported on first use rather than at package import, so that
# `import hispec.cli` does not pull in libby. The CLI has to keep working
# (`hispec doctor` especially) on a host whose venv is half broken.
if name == "HispecDaemon":
from .daemon import HispecDaemon # pylint: disable=import-outside-toplevel
return HispecDaemon
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
110 changes: 110 additions & 0 deletions src/hispec/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""``hispec``: deploy, run and inspect the HISPEC daemons on this host.

One command for operators and developers, replacing the separate
hispec-fei-start / hispec-fei-stop / hispec-doctor scripts. Host setup
(users, groups, unit file, polkit, venv) is still systemd/install.sh, run by
an admin; nothing here needs root.
"""
from __future__ import annotations

import argparse
from typing import List, Optional

from . import commands
from .doctor import doctor
from .instances import Paths

TARGETS_HELP = ("instance names (hsfei_adc), subsystems (hsfei, or just fei), "
"or 'all'")

EPILOG = """\
examples:
hispec status what is deployed here, and is it running?
hispec deploy hsfei_newthing copy its files from the repo, enable, start
hispec deploy --new the same for every instance not deployed yet
hispec start fei start every deployed hsfei_* daemon
hispec stop fei stop them, in reverse order
hispec restart hsfei_adc e.g. after editing /etc/hispec/hsfei_adc.yaml
hispec logs hsfei_adc -f follow its log
hispec doctor why isn't it working?

Every subcommand takes --help. Full guide:
https://caltechopticalobservatories.github.io/hispec/operations/systemd.html
"""


def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="hispec",
description="Deploy, run and inspect the HISPEC daemons on this host.",
epilog=EPILOG,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
sub = parser.add_subparsers(dest="command", metavar="<command>")

def add(name, func, help_text, description=None):
p = sub.add_parser(name, help=help_text, description=description or help_text,
formatter_class=argparse.RawDescriptionHelpFormatter)
p.set_defaults(func=func)
return p

p = add("deploy", commands.deploy,
"copy instances from the repo to this host, enable and start them",
"Copy each instance's .env and config from the repo into /etc/hispec, add\n"
"it to the boot set, and start it. No daemon-reload or install.sh needed.\n\n"
"The instance file always follows the repo. A deployed config that differs\n"
"from the repo is kept, since it holds this host's real hardware values;\n"
"pass --force to replace it.")
p.add_argument("names", nargs="*", metavar="name", help="instances to deploy")
p.add_argument("--new", action="store_true",
help="deploy every repo instance not yet deployed here")
p.add_argument("--force", action="store_true",
help="replace deployed configs that differ from the repo")
p.add_argument("--no-start", action="store_true", help="do not start them now")
p.add_argument("--no-enable", action="store_true", help="do not start them at boot")
p.add_argument("-n", "--dry-run", action="store_true", help="print what would happen")

for verb, text in (("start", "start daemons (already-running ones are left alone)"),
("stop", "stop daemons, in reverse order (they still start at boot)"),
("restart", "restart daemons, e.g. after a config edit or git pull")):
p = add(verb, commands.act, text)
p.set_defaults(verb=verb)
p.add_argument("targets", nargs="+", metavar="target", help=TARGETS_HELP)
p.add_argument("-n", "--dry-run", action="store_true", help="print what would happen")

for verb, text in (("enable", "start daemons at every boot (--now: and start now)"),
("disable", "stop starting daemons at boot (--now: and stop now)")):
p = add(verb, commands.enable, text)
p.set_defaults(verb=verb)
p.add_argument("targets", nargs="+", metavar="target", help=TARGETS_HELP)
now = "start" if verb == "enable" else "stop"
p.add_argument("--now", action="store_true", help=f"also {now} them now")
p.add_argument("-n", "--dry-run", action="store_true", help="print what would happen")

p = add("status", commands.status, "show each daemon: running? enabled at boot? since when?")
p.add_argument("targets", nargs="*", metavar="target", help=TARGETS_HELP)
p.add_argument("-a", "--all", action="store_true",
help="include instances defined in the repo but not deployed here")

p = add("logs", commands.logs, "show daemon logs (journalctl)")
p.add_argument("targets", nargs="+", metavar="target", help=TARGETS_HELP)
p.add_argument("-f", "--follow", action="store_true", help="keep printing new lines")
p.add_argument("-n", "--lines", type=int, help="how many lines (default 100)")
p.add_argument("--since", help='e.g. "1 hour ago", today, "2026-09-21 09:00"')
p.add_argument("-p", "--priority", help="e.g. err, warning")

add("doctor", doctor, "check this host and your account, and say what to fix")
return parser


def main(argv: Optional[List[str]] = None) -> int:
"""Entry point for the ``hispec`` console script."""
parser = _parser()
args = parser.parse_args(argv)
if not getattr(args, "func", None):
parser.print_help()
return 2
try:
return args.func(Paths.from_env(), args)
except KeyboardInterrupt:
return 130
6 changes: 6 additions & 0 deletions src/hispec/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""python -m hispec.cli"""
import sys

from . import main

sys.exit(main())
Loading
Loading