mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
improve cli formatting; add setup wizard
This commit is contained in:
@@ -1,10 +1,6 @@
|
|||||||
"""hyperglass cli module."""
|
"""hyperglass cli module."""
|
||||||
# Third Party
|
|
||||||
import stackprinter
|
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.cli import commands
|
from hyperglass.cli.commands import hg
|
||||||
|
|
||||||
stackprinter.set_excepthook(style="darkbg2")
|
CLI = hg
|
||||||
|
|
||||||
CLI = commands.hg
|
|
||||||
|
@@ -1,21 +1,16 @@
|
|||||||
"""CLI Command definitions."""
|
"""CLI Command definitions."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
import click
|
import inquirer
|
||||||
|
from click import group, option, confirm
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.cli.echo import error, value, cmd_help
|
from hyperglass.cli.echo import error, label, cmd_help
|
||||||
from hyperglass.cli.util import (
|
from hyperglass.cli.util import build_ui, start_web_server
|
||||||
build_ui,
|
|
||||||
fix_ownership,
|
|
||||||
migrate_config,
|
|
||||||
fix_permissions,
|
|
||||||
migrate_systemd,
|
|
||||||
start_web_server,
|
|
||||||
)
|
|
||||||
from hyperglass.cli.static import LABEL, CLI_HELP, E
|
from hyperglass.cli.static import LABEL, CLI_HELP, E
|
||||||
from hyperglass.cli.formatting import HelpColorsGroup, HelpColorsCommand, random_colors
|
from hyperglass.cli.formatting import HelpColorsGroup, HelpColorsCommand, random_colors
|
||||||
|
|
||||||
@@ -23,13 +18,11 @@ from hyperglass.cli.formatting import HelpColorsGroup, HelpColorsCommand, random
|
|||||||
WORKING_DIR = Path(__file__).parent
|
WORKING_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
@click.group(
|
@group(
|
||||||
cls=HelpColorsGroup,
|
cls=HelpColorsGroup,
|
||||||
help=CLI_HELP,
|
help=CLI_HELP,
|
||||||
help_headers_color=LABEL,
|
help_headers_color=LABEL,
|
||||||
help_options_custom_colors=random_colors(
|
help_options_custom_colors=random_colors("build-ui", "start", "secret", "setup"),
|
||||||
"build-ui", "start", "migrate-examples", "systemd", "permissions", "secret"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
def hg():
|
def hg():
|
||||||
"""Initialize Click Command Group."""
|
"""Initialize Click Command Group."""
|
||||||
@@ -52,15 +45,13 @@ def build_frontend():
|
|||||||
cls=HelpColorsCommand,
|
cls=HelpColorsCommand,
|
||||||
help_options_custom_colors=random_colors("-b"),
|
help_options_custom_colors=random_colors("-b"),
|
||||||
)
|
)
|
||||||
@click.option(
|
@option("-b", "--build", is_flag=True, help="Render theme & build frontend assets")
|
||||||
"-b", "--build", is_flag=True, help="Render theme & build frontend assets"
|
|
||||||
)
|
|
||||||
def start(build):
|
def start(build):
|
||||||
"""Start web server and optionally build frontend assets."""
|
"""Start web server and optionally build frontend assets."""
|
||||||
try:
|
try:
|
||||||
from hyperglass.api import start, ASGI_PARAMS
|
from hyperglass.api import start, ASGI_PARAMS
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
error("Error importing hyperglass", e)
|
error("Error importing hyperglass: {e}", e=e)
|
||||||
|
|
||||||
if build:
|
if build:
|
||||||
build_complete = build_ui()
|
build_complete = build_ui()
|
||||||
@@ -72,57 +63,13 @@ def start(build):
|
|||||||
start_web_server(start, ASGI_PARAMS)
|
start_web_server(start, ASGI_PARAMS)
|
||||||
|
|
||||||
|
|
||||||
@hg.command(
|
|
||||||
"migrate-examples",
|
|
||||||
short_help=cmd_help(E.PAPERCLIP, "Copy example configs to production config files"),
|
|
||||||
help=cmd_help(E.PAPERCLIP, "Copy example configs to production config files"),
|
|
||||||
cls=HelpColorsCommand,
|
|
||||||
help_options_custom_colors=random_colors(),
|
|
||||||
)
|
|
||||||
@click.option("-d", "--directory", required=True, help="Target directory")
|
|
||||||
def migrateconfig(directory):
|
|
||||||
"""Copy example configuration files to usable config files."""
|
|
||||||
migrate_config(Path(directory))
|
|
||||||
|
|
||||||
|
|
||||||
@hg.command(
|
|
||||||
"systemd",
|
|
||||||
help=cmd_help(E.CLAMP, " Copy systemd example to file system"),
|
|
||||||
cls=HelpColorsCommand,
|
|
||||||
help_options_custom_colors=random_colors("-d"),
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"-d",
|
|
||||||
"--directory",
|
|
||||||
default="/etc/systemd/system",
|
|
||||||
help="Destination Directory [default: 'etc/systemd/system']",
|
|
||||||
)
|
|
||||||
def migratesystemd(directory):
|
|
||||||
"""Copy example systemd service file to /etc/systemd/system/."""
|
|
||||||
migrate_systemd(WORKING_DIR / "hyperglass/hyperglass.service.example", directory)
|
|
||||||
|
|
||||||
|
|
||||||
@hg.command(
|
|
||||||
"permissions",
|
|
||||||
help=cmd_help(E.KEY, "Fix ownership & permissions of 'hyperglass/'"),
|
|
||||||
cls=HelpColorsCommand,
|
|
||||||
help_options_custom_colors=random_colors("--user", "--group"),
|
|
||||||
)
|
|
||||||
@click.option("--user", default="www-data")
|
|
||||||
@click.option("--group", default="www-data")
|
|
||||||
def permissions(user, group):
|
|
||||||
"""Run `chmod` and `chown` on the hyperglass/hyperglass directory."""
|
|
||||||
fix_permissions(user, group, WORKING_DIR)
|
|
||||||
fix_ownership(WORKING_DIR)
|
|
||||||
|
|
||||||
|
|
||||||
@hg.command(
|
@hg.command(
|
||||||
"secret",
|
"secret",
|
||||||
help=cmd_help(E.LOCK, "Generate agent secret"),
|
help=cmd_help(E.LOCK, "Generate agent secret"),
|
||||||
cls=HelpColorsCommand,
|
cls=HelpColorsCommand,
|
||||||
help_options_custom_colors=random_colors("-l"),
|
help_options_custom_colors=random_colors("-l"),
|
||||||
)
|
)
|
||||||
@click.option(
|
@option(
|
||||||
"-l", "--length", "length", default=32, help="Number of characters [default: 32]"
|
"-l", "--length", "length", default=32, help="Number of characters [default: 32]"
|
||||||
)
|
)
|
||||||
def generate_secret(length):
|
def generate_secret(length):
|
||||||
@@ -134,4 +81,49 @@ def generate_secret(length):
|
|||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
gen_secret = secrets.token_urlsafe(length)
|
gen_secret = secrets.token_urlsafe(length)
|
||||||
value("Secret", gen_secret)
|
label("Secret: {s}", s=gen_secret)
|
||||||
|
|
||||||
|
|
||||||
|
@hg.command(
|
||||||
|
"setup", help=cmd_help(E.TOOLBOX, "Run the setup wizard"), cls=HelpColorsCommand
|
||||||
|
)
|
||||||
|
def setup():
|
||||||
|
"""Define application directory, move example files, generate systemd service."""
|
||||||
|
from hyperglass.cli.util import create_dir, move_files, make_systemd, write_to_file
|
||||||
|
|
||||||
|
user_path = Path.home() / "hyperglass"
|
||||||
|
root_path = Path("/etc/hyperglass/")
|
||||||
|
|
||||||
|
install_paths = [
|
||||||
|
inquirer.List(
|
||||||
|
"install_path",
|
||||||
|
message="Choose a directory for hyperglass",
|
||||||
|
choices=[user_path, root_path],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
answer = inquirer.prompt(install_paths)
|
||||||
|
install_path = answer["install_path"]
|
||||||
|
ui_dir = install_path / "static" / "ui"
|
||||||
|
custom_dir = install_path / "static" / "custom"
|
||||||
|
|
||||||
|
create_dir(install_path)
|
||||||
|
create_dir(ui_dir, parents=True)
|
||||||
|
create_dir(custom_dir, parents=True)
|
||||||
|
|
||||||
|
example_dir = WORKING_DIR.parent / "examples"
|
||||||
|
files = example_dir.iterdir()
|
||||||
|
|
||||||
|
if confirm(
|
||||||
|
"Do you want to install example configuration files? (This is non-destructive)"
|
||||||
|
):
|
||||||
|
move_files(example_dir, install_path, files)
|
||||||
|
|
||||||
|
if install_path == user_path:
|
||||||
|
user = os.getlogin()
|
||||||
|
else:
|
||||||
|
user = "root"
|
||||||
|
|
||||||
|
if confirm("Do you want to generate a systemd service file?"):
|
||||||
|
systemd_file = install_path / "hyperglass.service"
|
||||||
|
systemd = make_systemd(user)
|
||||||
|
write_to_file(systemd_file, systemd)
|
||||||
|
@@ -1,73 +1,127 @@
|
|||||||
"""Helper functions for CLI message printing."""
|
"""Helper functions for CLI message printing."""
|
||||||
|
# Standard Library
|
||||||
|
import re
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
import click
|
from click import echo, style
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.cli.static import (
|
from hyperglass.cli.static import CMD_HELP, Message
|
||||||
CL,
|
from hyperglass.cli.exceptions import CliError
|
||||||
NL,
|
|
||||||
WS,
|
|
||||||
INFO,
|
|
||||||
ERROR,
|
|
||||||
LABEL,
|
|
||||||
VALUE,
|
|
||||||
STATUS,
|
|
||||||
SUCCESS,
|
|
||||||
CMD_HELP,
|
|
||||||
E,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_help(emoji="", help_text=""):
|
def cmd_help(emoji="", help_text=""):
|
||||||
"""Print formatted command help."""
|
"""Print formatted command help."""
|
||||||
return emoji + click.style(help_text, **CMD_HELP)
|
return emoji + style(help_text, **CMD_HELP)
|
||||||
|
|
||||||
|
|
||||||
def success(msg):
|
def _base_formatter(state, text, callback, **kwargs):
|
||||||
"""Print formatted success messages."""
|
"""Format text block, replace template strings with keyword arguments.
|
||||||
click.echo(E.CHECK + click.style(str(msg), **SUCCESS))
|
|
||||||
|
Arguments:
|
||||||
|
state {dict} -- Text format attributes
|
||||||
|
label {dict} -- Keyword format attributes
|
||||||
|
text {[type]} -- Text to format
|
||||||
|
callback {function} -- Callback function
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{str|ClickException} -- Formatted output
|
||||||
|
"""
|
||||||
|
fmt = Message(state)
|
||||||
|
|
||||||
|
if callback is None:
|
||||||
|
callback = style
|
||||||
|
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
if not isinstance(v, str):
|
||||||
|
v = str(v)
|
||||||
|
kwargs[k] = style(v, **fmt.kw)
|
||||||
|
|
||||||
|
text_all = re.split(r"(\{\w+\})", text)
|
||||||
|
text_all = [style(i, **fmt.msg) for i in text_all]
|
||||||
|
text_all = [i.format(**kwargs) for i in text_all]
|
||||||
|
|
||||||
|
if fmt.emoji:
|
||||||
|
text_all.insert(0, fmt.emoji)
|
||||||
|
|
||||||
|
text_fmt = "".join(text_all)
|
||||||
|
|
||||||
|
return callback(text_fmt)
|
||||||
|
|
||||||
|
|
||||||
def success_info(label, msg):
|
def info(text, callback=echo, **kwargs):
|
||||||
"""Print formatted labeled success messages."""
|
"""Generate formatted informational text.
|
||||||
click.echo(
|
|
||||||
E.CHECK
|
Arguments:
|
||||||
+ click.style(str(label), **SUCCESS)
|
text {str} -- Text to format
|
||||||
+ CL[1]
|
callback {callable} -- Callback function (default: {echo})
|
||||||
+ WS[1]
|
|
||||||
+ click.style(str(msg), **INFO)
|
Returns:
|
||||||
)
|
{str} -- Informational output
|
||||||
|
"""
|
||||||
|
return _base_formatter(state="info", text=text, callback=callback, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def info(msg):
|
def error(text, callback=CliError, **kwargs):
|
||||||
"""Print formatted informational messages."""
|
"""Generate formatted exception.
|
||||||
click.echo(E.INFO + click.style(str(msg), **INFO))
|
|
||||||
|
Arguments:
|
||||||
|
text {str} -- Text to format
|
||||||
|
callback {callable} -- Callback function (default: {echo})
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ClickException: Raised after formatting
|
||||||
|
"""
|
||||||
|
raise _base_formatter(state="error", text=text, callback=callback, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def status(msg):
|
def success(text, callback=echo, **kwargs):
|
||||||
"""Print formatted status messages."""
|
"""Generate formatted success text.
|
||||||
click.echo(click.style(str(msg), **STATUS))
|
|
||||||
|
Arguments:
|
||||||
|
text {str} -- Text to format
|
||||||
|
callback {callable} -- Callback function (default: {echo})
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{str} -- Success output
|
||||||
|
"""
|
||||||
|
return _base_formatter(state="success", text=text, callback=callback, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def error(msg, exc):
|
def warning(text, callback=echo, **kwargs):
|
||||||
"""Raise click exception with formatted output."""
|
"""Generate formatted warning text.
|
||||||
raise click.ClickException(
|
|
||||||
NL
|
Arguments:
|
||||||
+ E.ERROR
|
text {str} -- Text to format
|
||||||
+ click.style(str(msg), **LABEL)
|
callback {callable} -- Callback function (default: {echo})
|
||||||
+ CL[1]
|
|
||||||
+ WS[1]
|
Returns:
|
||||||
+ click.style(str(exc), **ERROR)
|
{str} -- Warning output
|
||||||
) from None
|
"""
|
||||||
|
return _base_formatter(state="warning", text=text, callback=callback, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def value(label, msg):
|
def label(text, callback=echo, **kwargs):
|
||||||
"""Print formatted label: value."""
|
"""Generate formatted info text with accented labels.
|
||||||
click.echo(
|
|
||||||
NL[1]
|
Arguments:
|
||||||
+ click.style(str(label), **LABEL)
|
text {str} -- Text to format
|
||||||
+ CL[1]
|
callback {callable} -- Callback function (default: {echo})
|
||||||
+ WS[1]
|
|
||||||
+ click.style(str(msg), **VALUE)
|
Returns:
|
||||||
+ NL[1]
|
{str} -- Label output
|
||||||
)
|
"""
|
||||||
|
return _base_formatter(state="label", text=text, callback=callback, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def status(text, callback=echo, **kwargs):
|
||||||
|
"""Generate formatted status text.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
text {str} -- Text to format
|
||||||
|
callback {callable} -- Callback function (default: {echo})
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{str} -- Status output
|
||||||
|
"""
|
||||||
|
return _base_formatter(state="status", text=text, callback=callback, **kwargs)
|
||||||
|
15
hyperglass/cli/exceptions.py
Normal file
15
hyperglass/cli/exceptions.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""hyperglass CLI custom exceptions."""
|
||||||
|
|
||||||
|
# Third Party
|
||||||
|
from click import ClickException, echo
|
||||||
|
from click._compat import get_text_stderr
|
||||||
|
|
||||||
|
|
||||||
|
class CliError(ClickException):
|
||||||
|
"""Custom exception to exclude the 'Error:' prefix from echos."""
|
||||||
|
|
||||||
|
def show(self, file=None):
|
||||||
|
"""Exclude 'Error:' prefix from raised exceptions."""
|
||||||
|
if file is None:
|
||||||
|
file = get_text_stderr()
|
||||||
|
echo(self.format_message())
|
@@ -34,6 +34,8 @@ class Emoji:
|
|||||||
CHECK = "\U00002705 "
|
CHECK = "\U00002705 "
|
||||||
INFO = "\U00002755 "
|
INFO = "\U00002755 "
|
||||||
ERROR = "\U0000274C "
|
ERROR = "\U0000274C "
|
||||||
|
WARNING = "\U000026A0\U0000FE0F "
|
||||||
|
TOOLBOX = "\U0001F9F0 "
|
||||||
ROCKET = "\U0001F680 "
|
ROCKET = "\U0001F680 "
|
||||||
SPARKLES = "\U00002728 "
|
SPARKLES = "\U00002728 "
|
||||||
PAPERCLIP = "\U0001F4CE "
|
PAPERCLIP = "\U0001F4CE "
|
||||||
@@ -51,14 +53,71 @@ E = Emoji()
|
|||||||
CLI_HELP = (
|
CLI_HELP = (
|
||||||
click.style("hyperglass", fg="magenta", bold=True)
|
click.style("hyperglass", fg="magenta", bold=True)
|
||||||
+ WS[1]
|
+ WS[1]
|
||||||
+ click.style("CLI Management Tool", fg="white")
|
+ click.style("Command Line Interface", fg="white")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Click Style Helpers
|
# Click Style Helpers
|
||||||
SUCCESS = {"fg": "green", "bold": True}
|
SUCCESS = {"fg": "green", "bold": True}
|
||||||
|
WARNING = {"fg": "yellow"}
|
||||||
ERROR = {"fg": "red", "bold": True}
|
ERROR = {"fg": "red", "bold": True}
|
||||||
LABEL = {"fg": "white"}
|
LABEL = {"fg": "white"}
|
||||||
INFO = {"fg": "blue", "bold": True}
|
INFO = {"fg": "blue", "bold": True}
|
||||||
STATUS = {"fg": "black"}
|
STATUS = {"fg": "black"}
|
||||||
VALUE = {"fg": "magenta", "bold": True}
|
VALUE = {"fg": "magenta", "bold": True}
|
||||||
CMD_HELP = {"fg": "white"}
|
CMD_HELP = {"fg": "white"}
|
||||||
|
|
||||||
|
|
||||||
|
class Message:
|
||||||
|
"""Helper class for single-character strings."""
|
||||||
|
|
||||||
|
colors = {
|
||||||
|
"warning": "yellow",
|
||||||
|
"success": "green",
|
||||||
|
"error": "red",
|
||||||
|
"info": "blue",
|
||||||
|
"status": "black",
|
||||||
|
"label": "white",
|
||||||
|
}
|
||||||
|
label_colors = {
|
||||||
|
"warning": "yellow",
|
||||||
|
"success": "green",
|
||||||
|
"error": "red",
|
||||||
|
"info": "blue",
|
||||||
|
"status": "black",
|
||||||
|
"label": "magenta",
|
||||||
|
}
|
||||||
|
emojis = {
|
||||||
|
"warning": E.WARNING,
|
||||||
|
"success": E.CHECK,
|
||||||
|
"error": E.ERROR,
|
||||||
|
"info": E.INFO,
|
||||||
|
"status": "",
|
||||||
|
"label": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, state):
|
||||||
|
"""Set instance character."""
|
||||||
|
self.state = state
|
||||||
|
self.color = self.colors[self.state]
|
||||||
|
self.label_color = self.label_colors[self.state]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def msg(self):
|
||||||
|
"""Click style attributes for message text."""
|
||||||
|
return {"fg": self.color}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kw(self):
|
||||||
|
"""Click style attributes for keywords."""
|
||||||
|
return {"fg": self.label_color, "bold": True, "underline": True}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def emoji(self):
|
||||||
|
"""Match emoji from state."""
|
||||||
|
return self.emojis[self.state]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""Stringify the instance character for representation."""
|
||||||
|
return "Message(msg={m}, kw={k}, emoji={e})".format(
|
||||||
|
m=self.msg, k=self.kw, e=self.emoji
|
||||||
|
)
|
||||||
|
Reference in New Issue
Block a user