1
0
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:
checktheroads
2020-02-14 16:28:45 -07:00
parent dbf6628e8b
commit 3d46ff4380
5 changed files with 240 additions and 124 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View 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())

View File

@@ -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
)