config_ninja.cli
Create config-ninja's CLI with typer.
config-ninja
Manage operating system configuration files based on data in the cloud.
Usage:
$ config-ninja [OPTIONS] COMMAND [ARGS]...
Options:
-c, --config PATH: Path toconfig-ninja's own configuration file.-v, --version: Print the version and exit.--install-completion: Install completion for the current shell.--show-completion: Show completion for the current shell, to copy it or customize the installation.--help: Show this message and exit.
Commands:
apply: Apply the specified configuration to the system.get: Print the value of the specified configuration object.monitor: Apply all configuration objects to the filesystem, and poll for changes.self: Operate on this installation ofconfig-ninja.version: Print the version and exit.
config-ninja apply
Apply the specified configuration to the system.
Usage:
$ config-ninja apply [OPTIONS] [KEY]
Arguments:
[KEY]: If specified, only apply the configuration object with this key.
Options:
-p, --poll: Enable polling; print the configuration on changes.--help: Show this message and exit.
config-ninja get
Print the value of the specified configuration object.
Usage:
$ config-ninja get [OPTIONS] KEY
Arguments:
KEY: The key of the configuration object to retrieve [required]
Options:
-p, --poll: Enable polling; print the configuration on changes.--help: Show this message and exit.
config-ninja monitor
Apply all configuration objects to the filesystem, and poll for changes.
Usage:
$ config-ninja monitor [OPTIONS]
Options:
--help: Show this message and exit.
config-ninja self
Operate on this installation of config-ninja.
Usage:
$ config-ninja self [OPTIONS] COMMAND [ARGS]...
Options:
--help: Show this message and exit.
Commands:
install: Installconfig-ninjaas asystemdservice.print: If specified, only apply the configuration object with this key.uninstall: Uninstall theconfig-ninjasystemdservice.
config-ninja self install
Install config-ninja as a systemd service.
Both --env and --var can be passed multiple times.
Example:
$ config-ninja self install --env FOO,BAR,BAZ --env SPAM --var EGGS=42
The environment variables FOO, BAR, BAZ, and SPAM will be read from the current shell and written to the service file, while EGGS will be set to 42.
Usage:
$ config-ninja self install [OPTIONS]
Options:
-e, --env NAME[,NAME...]: Embed these environment variables into the unit file. Can be used multiple times.-p, --print-only: Just print theconfig-ninja.servicefile; do not write.--run-as user[:group]: Configure the systemd unit to run the service as this user (and optionally group).-u, --user, --user-mode: User mode installation (does not requiresudo)--var VARIABLE=VALUE: Embed the specifiedVARIABLE=VALUEinto the unit file. Can be used multiple times.-w, --workdir PATH: Run the service from this directory.--help: Show this message and exit.
config-ninja self print
Print config-ninja's settings.
Usage:
$ config-ninja self print [OPTIONS]
Options:
--help: Show this message and exit.
config-ninja self uninstall
Uninstall the config-ninja systemd service.
Usage:
$ config-ninja self uninstall [OPTIONS]
Options:
-p, --print-only: Just print theconfig-ninja.servicefile; do not write.-u, --user, --user-mode: User mode installation (does not requiresudo)--help: Show this message and exit.
config-ninja version
Print the version and exit.
Usage:
$ config-ninja version [OPTIONS]
Options:
--help: Show this message and exit.
typer does not support from __future__ import annotations as of 2023-12-31
1"""Create `config-ninja`_'s CLI with `typer`_. 2 3.. include:: cli.md 4 5.. note:: `typer`_ does not support `from __future__ import annotations` as of 2023-12-31 6 7.. _config-ninja: https://bryant-finney.github.io/config-ninja/config_ninja.html 8.. _typer: https://typer.tiangolo.com/ 9""" 10 11import asyncio 12import contextlib 13import logging 14import os 15import sys 16import typing 17from pathlib import Path 18 19import pyspry 20import rich 21import typer 22import yaml 23from rich.logging import RichHandler 24from rich.markdown import Markdown 25 26import config_ninja 27from config_ninja import controller, systemd 28from config_ninja.controller import SYSTEMD_AVAILABLE 29 30try: 31 from typing import Annotated, TypeAlias # type: ignore[attr-defined,unused-ignore] 32except ImportError: # pragma: no cover 33 from typing_extensions import Annotated, TypeAlias # type: ignore[assignment,attr-defined,unused-ignore] 34 35 36# ruff: noqa: PLR0913 37# pylint: disable=redefined-outer-name,unused-argument,too-many-arguments 38 39__all__ = [ 40 'app', 41 'get', 42 'apply', 43 'monitor', 44 'self_print', 45 'install', 46 'uninstall', 47 'version', 48 'main', 49] 50 51LOG_MISSING_SETTINGS_MESSAGE = "Could not find [bold blue]config-ninja[/]'s settings file" 52LOG_VERBOSITY_MESSAGE = 'logging verbosity set to [green]%s[/green]' 53 54logger = logging.getLogger(__name__) 55 56app_kwargs: typing.Dict[str, typing.Any] = { 57 'context_settings': {'help_option_names': ['-h', '--help']}, 58 'no_args_is_help': True, 59 'rich_markup_mode': 'rich', 60} 61 62app = typer.Typer(**app_kwargs) 63"""The root `typer`_ application. 64 65.. _typer: https://typer.tiangolo.com/ 66""" 67 68self_app = typer.Typer(**app_kwargs) 69 70app.add_typer(self_app, name='self', help='Operate on this installation of [bold blue]config-ninja[/].') 71 72ActionType = typing.Callable[[str], typing.Any] 73 74 75def help_callback(ctx: typer.Context, value: typing.Optional[bool] = None) -> None: 76 """Print the help message for the command.""" 77 if ctx.resilient_parsing: # pragma: no cover 78 return 79 80 if value: 81 rich.print(ctx.get_help()) 82 raise typer.Exit() 83 84 85HelpAnnotation: TypeAlias = Annotated[ 86 typing.Optional[bool], 87 typer.Option( 88 '-h', 89 '--help', 90 callback=help_callback, 91 rich_help_panel='Global', 92 show_default=False, 93 is_eager=True, 94 help='Show this message and exit.', 95 ), 96] 97 98KeyAnnotation: TypeAlias = Annotated[ 99 str, 100 typer.Argument(help='The key of the configuration object to retrieve', show_default=False), 101] 102OptionalKeyAnnotation: TypeAlias = Annotated[ 103 typing.Optional[typing.List[str]], 104 typer.Argument( 105 help='Apply the configuration object(s) with matching key(s)' 106 ' (multiple values may be provided). If unspecified, all objects will be applied', 107 show_default=False, 108 metavar='[KEY...]', 109 ), 110] 111PollAnnotation: TypeAlias = Annotated[ 112 typing.Optional[bool], 113 typer.Option( 114 '-p', 115 '--poll', 116 help='Enable polling; print the configuration on changes.', 117 show_default=False, 118 ), 119] 120PrintAnnotation: TypeAlias = Annotated[ 121 typing.Optional[bool], 122 typer.Option( 123 '-p', 124 '--print-only', 125 help='Just print the [bold cyan]config-ninja.service[/] file; do not write.', 126 show_default=False, 127 ), 128] 129 130 131def load_config(ctx: typer.Context, value: typing.Optional[Path]) -> None: 132 """Load the settings file from the given path.""" 133 if ctx.resilient_parsing: # pragma: no cover 134 return 135 136 ctx.ensure_object(dict) 137 if not value and 'settings' in ctx.obj: 138 logger.debug('already loaded settings') 139 return 140 141 try: 142 settings_file = value or config_ninja.resolve_settings_path() 143 except FileNotFoundError as exc: 144 logger.warning( 145 '%s%s', 146 LOG_MISSING_SETTINGS_MESSAGE, 147 (' at any of the following locations:\n - ' + '\n - '.join(f'{p}' for p in exc.args[1])) 148 if len(exc.args) > 1 149 else '', 150 extra={'markup': True}, 151 ) 152 ctx.obj['settings'] = None 153 154 else: 155 ctx.obj['settings'] = config_ninja.load_settings(settings_file) 156 157 158ConfigAnnotation: TypeAlias = Annotated[ 159 typing.Optional[Path], 160 typer.Option( 161 '-c', 162 '--config', 163 callback=load_config, 164 help="Path to [bold blue]config-ninja[/]'s own configuration file.", 165 rich_help_panel='Global', 166 show_default=False, 167 ), 168] 169UserAnnotation: TypeAlias = Annotated[ 170 bool, 171 typer.Option( 172 '-u', 173 '--user', 174 '--user-mode', 175 help='User mode installation (does not require [bold orange3]sudo[/])', 176 show_default=False, 177 ), 178] 179WorkdirAnnotation: TypeAlias = Annotated[ 180 typing.Optional[Path], 181 typer.Option('-w', '--workdir', help='Run the service from this directory.', show_default=False), 182] 183 184 185def parse_env(ctx: typer.Context, value: typing.Optional[typing.List[str]]) -> typing.List[str]: 186 """Parse the environment variables from the command line.""" 187 if ctx.resilient_parsing or not value: 188 return [] 189 190 return [v for val in value for v in val.split(',')] 191 192 193EnvNamesAnnotation: TypeAlias = Annotated[ 194 typing.Optional[typing.List[str]], 195 typer.Option( 196 '-e', 197 '--env', 198 help='Embed these environment variables into the unit file. Can be used multiple times.', 199 show_default=False, 200 callback=parse_env, 201 metavar='NAME[,NAME...]', 202 ), 203] 204 205 206class UserGroup(typing.NamedTuple): 207 """Run the service using this user (and optionally group).""" 208 209 user: str 210 """The user to run the service as.""" 211 212 group: typing.Optional[str] = None 213 """The group to run the service as.""" 214 215 @classmethod 216 def parse(cls, value: str) -> 'UserGroup': 217 """Parse the `--run-as user[:group]` argument for the `systemd` service.""" 218 return cls(*value.split(':')) 219 220 221RunAsAnnotation: TypeAlias = Annotated[ 222 typing.Optional[UserGroup], 223 typer.Option( 224 '--run-as', 225 help='Configure the systemd unit to run the service as this user (and optionally group).', 226 metavar='user[:group]', 227 parser=UserGroup.parse, 228 ), 229] 230 231 232class Variable(typing.NamedTuple): 233 """Set this variable in the shell used to run the `systemd` service.""" 234 235 name: str 236 """The name of the variable.""" 237 238 value: str 239 """The value of the variable.""" 240 241 242def parse_var(value: str) -> Variable: 243 """Parse the `--var VARIABLE=VALUE` arguments for setting variables in the `systemd` service.""" 244 try: 245 parsed = Variable(*value.split('=')) 246 except TypeError as exc: 247 rich.print(f'[red]ERROR[/]: Invalid argument (expected [yellow]VARIABLE=VALUE[/] pair): [purple]{value}[/]') 248 raise typer.Exit(1) from exc 249 250 return parsed 251 252 253VariableAnnotation: TypeAlias = Annotated[ 254 typing.Optional[typing.List[Variable]], 255 typer.Option( 256 '--var', 257 help='Embed the specified [yellow]VARIABLE=VALUE[/] into the unit file. Can be used multiple times.', 258 metavar='VARIABLE=VALUE', 259 show_default=False, 260 parser=parse_var, 261 ), 262] 263 264 265def configure_logging(ctx: typer.Context, verbose: typing.Optional[bool] = None) -> None: 266 """Callback for the `--verbose` option to configure logging verbosity. 267 268 By default, log messages at the `logging.INFO` level: 269 270 >>> configure_logging(ctx) 271 >>> caplog.messages 272 ['logging verbosity set to [green]INFO[/green]'] 273 274 <!-- Clear the `caplog` fixture for the `doctest`, but exclude this from the docs 275 >>> caplog.clear() 276 277 --> 278 When `verbose` is `True`, log messages at the `logging.DEBUG` level: 279 280 >>> configure_logging(ctx, True) 281 >>> caplog.messages 282 ['logging verbosity set to [green]DEBUG[/green]'] 283 """ 284 if ctx.resilient_parsing: # pragma: no cover # this is for tab completions 285 return 286 287 verbosity = logging.DEBUG if verbose else logging.INFO 288 289 logging.basicConfig( 290 level=verbosity, 291 format='%(message)s', 292 force=True, 293 datefmt='[%X]', 294 handlers=[RichHandler(rich_tracebacks=True)], 295 ) 296 logger.debug(LOG_VERBOSITY_MESSAGE, logging.getLevelName(verbosity), extra={'markup': True}) 297 298 299VerbosityAnnotation = Annotated[ 300 typing.Optional[bool], 301 typer.Option( 302 '-v', 303 '--verbose', 304 callback=configure_logging, 305 rich_help_panel='Global', 306 help='Log messages at the [black]DEBUG[/] level.', 307 is_eager=True, 308 show_default=False, 309 ), 310] 311 312 313def version_callback(ctx: typer.Context, value: typing.Optional[bool] = None) -> None: 314 """Print the version of the package.""" 315 if ctx.resilient_parsing: # pragma: no cover # this is for tab completions 316 return 317 318 if value: 319 rich.print(config_ninja.__version__) 320 raise typer.Exit() 321 322 323VersionAnnotation = Annotated[ 324 typing.Optional[bool], 325 typer.Option( 326 '-V', 327 '--version', 328 callback=version_callback, 329 rich_help_panel='Global', 330 show_default=False, 331 is_eager=True, 332 help='Print the version and exit.', 333 ), 334] 335 336 337@contextlib.contextmanager 338def handle_key_errors(objects: typing.Dict[str, typing.Any]) -> typing.Iterator[None]: 339 """Handle KeyError exceptions within the managed context.""" 340 try: 341 yield 342 except KeyError as exc: # pragma: no cover 343 rich.print(f'[red]ERROR[/]: Missing key: [green]{exc.args[0]}[/]\n') 344 rich.print(yaml.dump(objects)) 345 raise typer.Exit(1) from exc 346 347 348async def poll_all( 349 controllers: typing.List[controller.BackendController], get_or_write: typing.Literal['get', 'write'] 350) -> None: 351 """Run the given controllers within an `asyncio` event loop to monitor and apply changes.""" 352 await asyncio.gather(*[ctrl.aget(rich.print) if get_or_write == 'get' else ctrl.awrite() for ctrl in controllers]) 353 354 355def _check_systemd() -> None: 356 if not SYSTEMD_AVAILABLE: 357 rich.print('[red]ERROR[/]: Missing [bold gray93]systemd[/]!') 358 rich.print('Currently, this command only works on linux.') 359 raise typer.Exit(1) 360 361 362def _new_controller(settings: pyspry.Settings, key: str) -> controller.BackendController: 363 ctrl = controller.BackendController(settings, key, handle_key_errors) 364 ctrl.dest.path.parent.mkdir(parents=True, exist_ok=True) 365 return ctrl 366 367 368# ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ 369# command definitions 370 371 372@app.command() 373def get( 374 ctx: typer.Context, 375 keys: OptionalKeyAnnotation = None, 376 poll: PollAnnotation = False, 377 get_help: HelpAnnotation = None, 378 config: ConfigAnnotation = None, 379 verbose: VerbosityAnnotation = None, 380 version: VersionAnnotation = None, 381) -> None: 382 """Print the value of the specified configuration object.""" 383 settings: pyspry.Settings = ctx.obj['settings'] 384 385 controllers = [_new_controller(settings, key) for key in keys or settings.OBJECTS] 386 387 if poll: 388 logger.debug( 389 'Begin monitoring (read-only): %s', 390 ', '.join(f'[yellow]{ctrl.key}[/yellow]' for ctrl in controllers), 391 extra={'markup': True}, 392 ) 393 asyncio.run(poll_all(controllers, 'get')) 394 return 395 396 for ctrl in controllers: 397 logger.debug('Get [yellow]%s[/yellow]: %s', ctrl.key, ctrl, extra={'markup': True}) 398 ctrl.get(rich.print) 399 400 401@app.command() 402def apply( 403 ctx: typer.Context, 404 keys: OptionalKeyAnnotation = None, 405 poll: PollAnnotation = False, 406 get_help: HelpAnnotation = None, 407 config: ConfigAnnotation = None, 408 verbose: VerbosityAnnotation = None, 409 version: VersionAnnotation = None, 410) -> None: 411 """Apply the specified configuration to the system.""" 412 settings: pyspry.Settings = ctx.obj['settings'] 413 414 controllers = [_new_controller(settings, key) for key in keys or settings.OBJECTS] 415 416 if poll: 417 rich.print('Begin monitoring: ' + ', '.join(f'[yellow]{ctrl.key}[/yellow]' for ctrl in controllers)) 418 asyncio.run(poll_all(controllers, 'write')) 419 return 420 421 for ctrl in controllers: 422 rich.print(f'Apply [yellow]{ctrl.key}[/yellow]: {ctrl}') 423 ctrl.write() 424 425 426@app.command( 427 deprecated=True, 428 short_help='[dim]Apply all configuration objects to the filesystem, and poll for changes.[/] [red](deprecated)[/]', 429 help='Use [bold blue]config-ninja apply --poll[/] instead.', 430) 431def monitor(ctx: typer.Context) -> None: 432 """Apply all configuration objects to the filesystem, and poll for changes.""" 433 settings: pyspry.Settings = ctx.obj['settings'] 434 controllers = [controller.BackendController(settings, key, handle_key_errors) for key in settings.OBJECTS] 435 for ctrl in controllers: 436 ctrl.dest.path.parent.mkdir(parents=True, exist_ok=True) 437 438 rich.print('Begin monitoring: ' + ', '.join(f'[yellow]{ctrl.key}[/yellow]' for ctrl in controllers)) 439 asyncio.run(poll_all(controllers, 'write')) 440 441 442@self_app.command(name='print') 443def self_print( 444 ctx: typer.Context, 445 get_help: HelpAnnotation = None, 446 config: ConfigAnnotation = None, 447 verbose: VerbosityAnnotation = None, 448 version: VersionAnnotation = None, 449) -> None: 450 """Print [bold blue]config-ninja[/]'s settings.""" 451 if not (settings := ctx.obj.get('settings')): 452 raise typer.Exit(1) 453 rich.print(yaml.dump(settings.OBJECTS)) 454 455 456@self_app.command() 457def install( 458 env_names: EnvNamesAnnotation = None, 459 print_only: PrintAnnotation = None, 460 run_as: RunAsAnnotation = None, 461 user_mode: UserAnnotation = False, 462 variables: VariableAnnotation = None, 463 workdir: WorkdirAnnotation = None, 464 get_help: HelpAnnotation = None, 465 config: ConfigAnnotation = None, 466 verbose: VerbosityAnnotation = None, 467 version: VersionAnnotation = None, 468) -> None: 469 """Install [bold blue]config-ninja[/] as a [bold gray93]systemd[/] service. 470 471 Both --env and --var can be passed multiple times. 472 473 Example: 474 config-ninja self install --env FOO,BAR,BAZ --env SPAM --var EGGS=42 475 476 The environment variables [purple]FOO[/], [purple]BAR[/], [purple]BAZ[/], and [purple]SPAM[/] will be read from the current shell and written to the service file, while [purple]EGGS[/] will be set to [yellow]42[/]. 477 """ 478 environ = {name: os.environ[name] for name in env_names or [] if name in os.environ} 479 environ.update(variables or []) 480 481 kwargs = { 482 # the command to use when invoking config-ninja from systemd 483 'config_ninja_cmd': sys.argv[0] if sys.argv[0].endswith('config-ninja') else f'{sys.executable} {sys.argv[0]}', 484 # write these environment variables into the systemd service file 485 'environ': environ, 486 # run `config-ninja` from this directory (if specified) 487 'workdir': workdir, 488 } 489 if run_as: 490 kwargs['user'] = run_as.user 491 if run_as.group: 492 kwargs['group'] = run_as.group 493 494 svc = systemd.Service('config_ninja', 'systemd.service.j2', user_mode) 495 if print_only: 496 rendered = svc.render(**kwargs) 497 rich.print(Markdown(f'# {svc.path}\n```systemd\n{rendered}\n```')) 498 raise typer.Exit(0) 499 500 _check_systemd() 501 502 rich.print(f'Installing {svc.path}') 503 rich.print(svc.install(**kwargs)) 504 505 rich.print('[green]SUCCESS[/] :white_check_mark:') 506 507 508@self_app.command() 509def uninstall( 510 print_only: PrintAnnotation = None, 511 user: UserAnnotation = False, 512 get_help: HelpAnnotation = None, 513 config: ConfigAnnotation = None, 514 verbose: VerbosityAnnotation = None, 515 version: VersionAnnotation = None, 516) -> None: 517 """Uninstall the [bold blue]config-ninja[/] [bold gray93]systemd[/] service.""" 518 svc = systemd.Service('config_ninja', 'systemd.service.j2', user or False) 519 if print_only: 520 rich.print(Markdown(f'# {svc.path}\n```systemd\n{svc.read()}\n```')) 521 raise typer.Exit(0) 522 523 _check_systemd() 524 525 rich.print(f'Uninstalling {svc.path}') 526 svc.uninstall() 527 rich.print('[green]SUCCESS[/] :white_check_mark:') 528 529 530@self_app.callback( 531 invoke_without_command=True, 532 name='self', 533) 534def self_main( 535 ctx: typer.Context, 536 get_help: HelpAnnotation = None, 537 config: ConfigAnnotation = None, 538 verbose: VerbosityAnnotation = None, 539 version: VersionAnnotation = None, 540) -> None: 541 """Print the help message for the `self` command.""" 542 if not ctx.invoked_subcommand: 543 rich.print(ctx.get_help()) 544 545 546@app.command() 547def version( 548 ctx: typer.Context, 549 get_help: HelpAnnotation = None, 550 config: ConfigAnnotation = None, 551 verbose: VerbosityAnnotation = None, 552 version: VersionAnnotation = None, 553) -> None: 554 """Print the version and exit.""" 555 version_callback(ctx, True) 556 557 558@app.callback(invoke_without_command=True) 559def main( 560 ctx: typer.Context, 561 get_help: HelpAnnotation = None, 562 config: ConfigAnnotation = None, 563 verbose: VerbosityAnnotation = None, 564 version: VersionAnnotation = None, 565) -> None: 566 """Manage operating system configuration files based on data in the cloud.""" 567 ctx.ensure_object(dict) 568 569 if not ctx.invoked_subcommand: # pragma: no cover 570 rich.print(ctx.get_help()) 571 572 573logger.debug('successfully imported %s', __name__)
The root typer application.
373@app.command() 374def get( 375 ctx: typer.Context, 376 keys: OptionalKeyAnnotation = None, 377 poll: PollAnnotation = False, 378 get_help: HelpAnnotation = None, 379 config: ConfigAnnotation = None, 380 verbose: VerbosityAnnotation = None, 381 version: VersionAnnotation = None, 382) -> None: 383 """Print the value of the specified configuration object.""" 384 settings: pyspry.Settings = ctx.obj['settings'] 385 386 controllers = [_new_controller(settings, key) for key in keys or settings.OBJECTS] 387 388 if poll: 389 logger.debug( 390 'Begin monitoring (read-only): %s', 391 ', '.join(f'[yellow]{ctrl.key}[/yellow]' for ctrl in controllers), 392 extra={'markup': True}, 393 ) 394 asyncio.run(poll_all(controllers, 'get')) 395 return 396 397 for ctrl in controllers: 398 logger.debug('Get [yellow]%s[/yellow]: %s', ctrl.key, ctrl, extra={'markup': True}) 399 ctrl.get(rich.print)
Print the value of the specified configuration object.
402@app.command() 403def apply( 404 ctx: typer.Context, 405 keys: OptionalKeyAnnotation = None, 406 poll: PollAnnotation = False, 407 get_help: HelpAnnotation = None, 408 config: ConfigAnnotation = None, 409 verbose: VerbosityAnnotation = None, 410 version: VersionAnnotation = None, 411) -> None: 412 """Apply the specified configuration to the system.""" 413 settings: pyspry.Settings = ctx.obj['settings'] 414 415 controllers = [_new_controller(settings, key) for key in keys or settings.OBJECTS] 416 417 if poll: 418 rich.print('Begin monitoring: ' + ', '.join(f'[yellow]{ctrl.key}[/yellow]' for ctrl in controllers)) 419 asyncio.run(poll_all(controllers, 'write')) 420 return 421 422 for ctrl in controllers: 423 rich.print(f'Apply [yellow]{ctrl.key}[/yellow]: {ctrl}') 424 ctrl.write()
Apply the specified configuration to the system.
427@app.command( 428 deprecated=True, 429 short_help='[dim]Apply all configuration objects to the filesystem, and poll for changes.[/] [red](deprecated)[/]', 430 help='Use [bold blue]config-ninja apply --poll[/] instead.', 431) 432def monitor(ctx: typer.Context) -> None: 433 """Apply all configuration objects to the filesystem, and poll for changes.""" 434 settings: pyspry.Settings = ctx.obj['settings'] 435 controllers = [controller.BackendController(settings, key, handle_key_errors) for key in settings.OBJECTS] 436 for ctrl in controllers: 437 ctrl.dest.path.parent.mkdir(parents=True, exist_ok=True) 438 439 rich.print('Begin monitoring: ' + ', '.join(f'[yellow]{ctrl.key}[/yellow]' for ctrl in controllers)) 440 asyncio.run(poll_all(controllers, 'write'))
Apply all configuration objects to the filesystem, and poll for changes.
443@self_app.command(name='print') 444def self_print( 445 ctx: typer.Context, 446 get_help: HelpAnnotation = None, 447 config: ConfigAnnotation = None, 448 verbose: VerbosityAnnotation = None, 449 version: VersionAnnotation = None, 450) -> None: 451 """Print [bold blue]config-ninja[/]'s settings.""" 452 if not (settings := ctx.obj.get('settings')): 453 raise typer.Exit(1) 454 rich.print(yaml.dump(settings.OBJECTS))
Print [bold blue]config-ninja[/]'s settings.
457@self_app.command() 458def install( 459 env_names: EnvNamesAnnotation = None, 460 print_only: PrintAnnotation = None, 461 run_as: RunAsAnnotation = None, 462 user_mode: UserAnnotation = False, 463 variables: VariableAnnotation = None, 464 workdir: WorkdirAnnotation = None, 465 get_help: HelpAnnotation = None, 466 config: ConfigAnnotation = None, 467 verbose: VerbosityAnnotation = None, 468 version: VersionAnnotation = None, 469) -> None: 470 """Install [bold blue]config-ninja[/] as a [bold gray93]systemd[/] service. 471 472 Both --env and --var can be passed multiple times. 473 474 Example: 475 config-ninja self install --env FOO,BAR,BAZ --env SPAM --var EGGS=42 476 477 The environment variables [purple]FOO[/], [purple]BAR[/], [purple]BAZ[/], and [purple]SPAM[/] will be read from the current shell and written to the service file, while [purple]EGGS[/] will be set to [yellow]42[/]. 478 """ 479 environ = {name: os.environ[name] for name in env_names or [] if name in os.environ} 480 environ.update(variables or []) 481 482 kwargs = { 483 # the command to use when invoking config-ninja from systemd 484 'config_ninja_cmd': sys.argv[0] if sys.argv[0].endswith('config-ninja') else f'{sys.executable} {sys.argv[0]}', 485 # write these environment variables into the systemd service file 486 'environ': environ, 487 # run `config-ninja` from this directory (if specified) 488 'workdir': workdir, 489 } 490 if run_as: 491 kwargs['user'] = run_as.user 492 if run_as.group: 493 kwargs['group'] = run_as.group 494 495 svc = systemd.Service('config_ninja', 'systemd.service.j2', user_mode) 496 if print_only: 497 rendered = svc.render(**kwargs) 498 rich.print(Markdown(f'# {svc.path}\n```systemd\n{rendered}\n```')) 499 raise typer.Exit(0) 500 501 _check_systemd() 502 503 rich.print(f'Installing {svc.path}') 504 rich.print(svc.install(**kwargs)) 505 506 rich.print('[green]SUCCESS[/] :white_check_mark:')
Install [bold blue]config-ninja[/] as a [bold gray93]systemd[/] service.
Both --env and --var can be passed multiple times.
Example:
config-ninja self install --env FOO,BAR,BAZ --env SPAM --var EGGS=42
The environment variables [purple]FOO[/], [purple]BAR[/], [purple]BAZ[/], and [purple]SPAM[/] will be read from the current shell and written to the service file, while [purple]EGGS[/] will be set to [yellow]42[/].
509@self_app.command() 510def uninstall( 511 print_only: PrintAnnotation = None, 512 user: UserAnnotation = False, 513 get_help: HelpAnnotation = None, 514 config: ConfigAnnotation = None, 515 verbose: VerbosityAnnotation = None, 516 version: VersionAnnotation = None, 517) -> None: 518 """Uninstall the [bold blue]config-ninja[/] [bold gray93]systemd[/] service.""" 519 svc = systemd.Service('config_ninja', 'systemd.service.j2', user or False) 520 if print_only: 521 rich.print(Markdown(f'# {svc.path}\n```systemd\n{svc.read()}\n```')) 522 raise typer.Exit(0) 523 524 _check_systemd() 525 526 rich.print(f'Uninstalling {svc.path}') 527 svc.uninstall() 528 rich.print('[green]SUCCESS[/] :white_check_mark:')
Uninstall the [bold blue]config-ninja[/] [bold gray93]systemd[/] service.
547@app.command() 548def version( 549 ctx: typer.Context, 550 get_help: HelpAnnotation = None, 551 config: ConfigAnnotation = None, 552 verbose: VerbosityAnnotation = None, 553 version: VersionAnnotation = None, 554) -> None: 555 """Print the version and exit.""" 556 version_callback(ctx, True)
Print the version and exit.
559@app.callback(invoke_without_command=True) 560def main( 561 ctx: typer.Context, 562 get_help: HelpAnnotation = None, 563 config: ConfigAnnotation = None, 564 verbose: VerbosityAnnotation = None, 565 version: VersionAnnotation = None, 566) -> None: 567 """Manage operating system configuration files based on data in the cloud.""" 568 ctx.ensure_object(dict) 569 570 if not ctx.invoked_subcommand: # pragma: no cover 571 rich.print(ctx.get_help())
Manage operating system configuration files based on data in the cloud.