forked from mirrors/thatmattlove-hyperglass
clean up installer, remove systemd from steps
This commit is contained in:
parent
bdb0c74a8c
commit
31a3ac00c9
4 changed files with 131 additions and 352 deletions
|
|
@ -2,12 +2,10 @@
|
|||
|
||||
# Standard Library
|
||||
import sys
|
||||
from getpass import getuser
|
||||
from pathlib import Path
|
||||
|
||||
# Third Party
|
||||
import inquirer
|
||||
from click import group, option, confirm, help_option
|
||||
from click import group, option, help_option
|
||||
|
||||
# Project
|
||||
from hyperglass.util import cpu_count
|
||||
|
|
@ -16,6 +14,7 @@ from hyperglass.util import cpu_count
|
|||
from .echo import error, label, success, cmd_help
|
||||
from .util import build_ui
|
||||
from .static import LABEL, CLI_HELP, E
|
||||
from .installer import Installer
|
||||
from .formatting import HelpColorsGroup, HelpColorsCommand, random_colors
|
||||
|
||||
# Define working directory
|
||||
|
|
@ -98,12 +97,9 @@ def build_frontend():
|
|||
)
|
||||
def start(build, direct, workers): # noqa: C901
|
||||
"""Start web server and optionally build frontend assets."""
|
||||
try:
|
||||
# Project
|
||||
from hyperglass.api import start as uvicorn_start
|
||||
from hyperglass.main import start
|
||||
except ImportError as e:
|
||||
error("Error importing hyperglass: {}", str(e))
|
||||
# Project
|
||||
from hyperglass.api import start as uvicorn_start
|
||||
from hyperglass.main import start
|
||||
|
||||
kwargs = {}
|
||||
if workers != 0:
|
||||
|
|
@ -121,10 +117,13 @@ def start(build, direct, workers): # noqa: C901
|
|||
|
||||
if not build and not direct:
|
||||
start(**kwargs)
|
||||
|
||||
elif not build and direct:
|
||||
uvicorn_start(**kwargs)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
error("Stopping hyperglass due to keyboard interrupt.")
|
||||
|
||||
except BaseException as err:
|
||||
error(str(err))
|
||||
|
||||
|
|
@ -167,65 +166,15 @@ def generate_secret(length):
|
|||
)
|
||||
def setup(unattended):
|
||||
"""Define application directory, move example files, generate systemd service."""
|
||||
# Project
|
||||
from hyperglass.cli.util import (
|
||||
create_dir,
|
||||
make_systemd,
|
||||
write_to_file,
|
||||
install_systemd,
|
||||
migrate_static_assets,
|
||||
|
||||
installer = Installer(unattended=unattended)
|
||||
installer.install()
|
||||
|
||||
success(
|
||||
"""Completed hyperglass installation.
|
||||
After adding your hyperglass.yaml file, you should run the `hyperglass build-ui` command.""" # noqa: E501
|
||||
)
|
||||
|
||||
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],
|
||||
)
|
||||
]
|
||||
if not unattended:
|
||||
answer = inquirer.prompt(install_paths)
|
||||
if answer is None:
|
||||
error("A directory for hyperglass is required")
|
||||
install_path = answer["install_path"]
|
||||
|
||||
elif unattended:
|
||||
install_path = user_path
|
||||
|
||||
ui_dir = install_path / "static" / "ui"
|
||||
images_dir = install_path / "static" / "images"
|
||||
favicon_dir = images_dir / "favicons"
|
||||
custom_dir = install_path / "static" / "custom"
|
||||
|
||||
create_dir(install_path)
|
||||
|
||||
for path in (ui_dir, images_dir, custom_dir, favicon_dir):
|
||||
create_dir(path, parents=True)
|
||||
|
||||
migrate_static_assets(install_path)
|
||||
|
||||
if install_path == user_path:
|
||||
user = getuser()
|
||||
else:
|
||||
user = "root"
|
||||
|
||||
do_systemd = True
|
||||
if not unattended and not confirm(
|
||||
"Do you want to generate a systemd service file?"
|
||||
):
|
||||
do_systemd = False
|
||||
|
||||
if do_systemd:
|
||||
systemd_file = install_path / "hyperglass.service"
|
||||
systemd = make_systemd(user)
|
||||
write_to_file(systemd_file, systemd)
|
||||
install_systemd(install_path)
|
||||
|
||||
build_ui()
|
||||
|
||||
|
||||
@hg.command(
|
||||
"system-info",
|
||||
|
|
|
|||
114
hyperglass/cli/installer.py
Normal file
114
hyperglass/cli/installer.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
"""Install hyperglass."""
|
||||
|
||||
# Standard Library
|
||||
import os
|
||||
import shutil
|
||||
from filecmp import dircmp
|
||||
from pathlib import Path
|
||||
|
||||
# Third Party
|
||||
import inquirer
|
||||
|
||||
# Local
|
||||
from .echo import error, success, warning
|
||||
from .util import create_dir
|
||||
|
||||
USER_PATH = Path.home() / "hyperglass"
|
||||
ROOT_PATH = Path("/etc/hyperglass/")
|
||||
ASSET_DIR = Path(__file__).parent.parent / "images"
|
||||
IGNORED_FILES = [".DS_Store"]
|
||||
|
||||
INSTALL_PATHS = [
|
||||
inquirer.List(
|
||||
"install_path",
|
||||
message="Choose a directory for hyperglass",
|
||||
choices=[USER_PATH, ROOT_PATH],
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def prompt_for_path() -> str:
|
||||
"""Recursively prompt the user for an app path until one is provided."""
|
||||
|
||||
answer = inquirer.prompt(INSTALL_PATHS)
|
||||
|
||||
if answer is None:
|
||||
warning("A directory for hyperglass is required")
|
||||
answer = prompt_for_path()
|
||||
|
||||
return answer["install_path"]
|
||||
|
||||
|
||||
class Installer:
|
||||
"""Install hyperglass."""
|
||||
|
||||
def __init__(self, unattended: bool):
|
||||
"""Initialize installer."""
|
||||
|
||||
self.unattended = unattended
|
||||
|
||||
def install(self) -> None:
|
||||
"""Complete the installation."""
|
||||
|
||||
self.app_path = self._get_app_path()
|
||||
self._scaffold()
|
||||
self._migrate_static_assets()
|
||||
|
||||
def _get_app_path(self) -> Path:
|
||||
"""Find the app path from env variables or a prompt."""
|
||||
|
||||
if self.unattended:
|
||||
return USER_PATH
|
||||
|
||||
app_path = os.environ.get("HYPERGLASS_PATH", None)
|
||||
|
||||
if app_path is None:
|
||||
app_path = prompt_for_path()
|
||||
|
||||
return app_path
|
||||
|
||||
def _scaffold(self) -> None:
|
||||
"""Create the file structure necessary for hyperglass to run."""
|
||||
|
||||
ui_dir = self.app_path / "static" / "ui"
|
||||
images_dir = self.app_path / "static" / "images"
|
||||
favicon_dir = images_dir / "favicons"
|
||||
custom_dir = self.app_path / "static" / "custom"
|
||||
|
||||
create_dir(self.app_path)
|
||||
|
||||
for path in (ui_dir, images_dir, favicon_dir, custom_dir):
|
||||
create_dir(path, parents=True)
|
||||
|
||||
def _migrate_static_assets(self) -> bool:
|
||||
"""Synchronize the project assets with the installation assets."""
|
||||
|
||||
target_dir = self.app_path / "static" / "images"
|
||||
|
||||
if not target_dir.exists():
|
||||
shutil.copytree(ASSET_DIR, target_dir)
|
||||
|
||||
# Compare the contents of the project's asset directory (considered
|
||||
# the source of truth) with the installation directory. If they do
|
||||
# not match, delete the installation directory's asset directory and
|
||||
# re-copy it.
|
||||
compare_initial = dircmp(ASSET_DIR, target_dir, ignore=IGNORED_FILES)
|
||||
|
||||
if not compare_initial.left_list == compare_initial.right_list:
|
||||
shutil.rmtree(target_dir)
|
||||
shutil.copytree(ASSET_DIR, target_dir)
|
||||
|
||||
# Re-compare the source and destination directory contents to
|
||||
# ensure they match.
|
||||
compare_post = dircmp(ASSET_DIR, target_dir, ignore=IGNORED_FILES)
|
||||
|
||||
if not compare_post.left_list == compare_post.right_list:
|
||||
error(
|
||||
"Files in {a} do not match files in {b}",
|
||||
a=str(ASSET_DIR),
|
||||
b=str(target_dir),
|
||||
)
|
||||
return False
|
||||
|
||||
success("Migrated assets from {a} to {b}", a=str(ASSET_DIR), b=str(target_dir))
|
||||
return True
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
"""CLI utility functions."""
|
||||
|
||||
# Standard Library
|
||||
import os
|
||||
import shutil
|
||||
from typing import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
# Third Party
|
||||
from click import echo, style
|
||||
|
||||
# Project
|
||||
from hyperglass.cli.echo import info, error, status, success, warning
|
||||
from hyperglass.cli.echo import info, error, status, success
|
||||
from hyperglass.cli.static import CL, NL, WS, E
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
|
|
@ -30,50 +29,6 @@ def async_command(func):
|
|||
return update_wrapper(wrapper, func)
|
||||
|
||||
|
||||
def fix_ownership(user, group, directory):
|
||||
"""Make user & group the owner of the directory."""
|
||||
# Standard Library
|
||||
import os
|
||||
import grp
|
||||
import pwd
|
||||
|
||||
uid = pwd.getpwnam(user).pw_uid
|
||||
gid = grp.getgrnam(group).gr_gid
|
||||
try:
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for d in dirs:
|
||||
full_path = os.path.join(root, d)
|
||||
os.chown(full_path, uid, gid)
|
||||
for f in files:
|
||||
full_path = os.path.join(root, f)
|
||||
os.chown(full_path, uid, gid)
|
||||
os.chown(root, uid, gid)
|
||||
except Exception as e:
|
||||
error("Failed to change '{d}' ownership: {e}", d="hyperglass/", e=e)
|
||||
|
||||
success("Successfully changed '{d}' ownership", d="hyperglass/")
|
||||
|
||||
|
||||
def fix_permissions(directory):
|
||||
"""Make directory readable by public."""
|
||||
# Standard Library
|
||||
import os
|
||||
|
||||
try:
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for d in dirs:
|
||||
full_path = os.path.join(root, d)
|
||||
os.chmod(full_path, 0o744)
|
||||
for f in files:
|
||||
full_path = os.path.join(root, f)
|
||||
os.chmod(full_path, 0o744)
|
||||
os.chmod(root, 0o744)
|
||||
except Exception as e:
|
||||
error("Failed to change '{d}' ownership: {e}", d="hyperglass/", e=e)
|
||||
|
||||
success("Successfully changed '{d}' ownership", d="hyperglass/")
|
||||
|
||||
|
||||
def start_web_server(start, params):
|
||||
"""Start web server."""
|
||||
msg_start = "Starting hyperglass web server on"
|
||||
|
|
@ -106,63 +61,6 @@ def start_web_server(start, params):
|
|||
error("Failed to start web server: {e}", e=e)
|
||||
|
||||
|
||||
def migrate_config(config_dir):
|
||||
"""Copy example config files and remove .example extensions."""
|
||||
status("Migrating example config files...")
|
||||
|
||||
# Standard Library
|
||||
import shutil
|
||||
|
||||
examples = Path(PROJECT_ROOT / "examples").glob("*.yaml.example")
|
||||
|
||||
if not isinstance(config_dir, Path):
|
||||
config_dir = Path(config_dir)
|
||||
|
||||
if not config_dir.exists():
|
||||
error("'{d}' does not exist", d=str(config_dir))
|
||||
|
||||
migrated = 0
|
||||
for file in examples:
|
||||
target_file = config_dir / file.with_suffix("").name
|
||||
try:
|
||||
if target_file.exists():
|
||||
info("{f} already exists", f=str(target_file))
|
||||
else:
|
||||
shutil.copyfile(file, target_file)
|
||||
migrated += 1
|
||||
info("Migrated {f}", f=str(target_file))
|
||||
except Exception as e:
|
||||
error("Failed to migrate '{f}': {e}", f=str(target_file), e=e)
|
||||
|
||||
if migrated == 0:
|
||||
info("Migrated {n} example config files", n=migrated)
|
||||
elif migrated > 0:
|
||||
success("Successfully migrated {n} example config files", n=migrated)
|
||||
|
||||
|
||||
def migrate_systemd(source, destination):
|
||||
"""Copy example systemd service file to /etc/systemd/system/."""
|
||||
# Standard Library
|
||||
import os
|
||||
import shutil
|
||||
|
||||
basefile, extension = os.path.splitext(source)
|
||||
newfile = os.path.join(destination, basefile)
|
||||
|
||||
try:
|
||||
status("Migrating example systemd service...")
|
||||
|
||||
if os.path.exists(newfile):
|
||||
info("'{f}' already exists", f=str(newfile))
|
||||
else:
|
||||
shutil.copyfile(source, newfile)
|
||||
|
||||
except Exception as e:
|
||||
error("Error migrating example systemd service: {e}", e=e)
|
||||
|
||||
success("Successfully migrated systemd service to: {f}", f=str(newfile))
|
||||
|
||||
|
||||
def build_ui():
|
||||
"""Create a new UI build.
|
||||
|
||||
|
|
@ -236,109 +134,6 @@ def create_dir(path, **kwargs):
|
|||
return True
|
||||
|
||||
|
||||
def move_files(src, dst, files): # noqa: C901
|
||||
"""Move iterable of files from source to destination.
|
||||
|
||||
Arguments:
|
||||
src {Path} -- Current directory of files
|
||||
dst {Path} -- Target destination directory
|
||||
files {Iterable} -- Iterable of files
|
||||
"""
|
||||
|
||||
if not isinstance(src, Path):
|
||||
try:
|
||||
src = Path(src)
|
||||
except TypeError:
|
||||
error("{p} is not a valid path", p=src)
|
||||
if not isinstance(dst, Path):
|
||||
try:
|
||||
dst = Path(dst)
|
||||
except TypeError:
|
||||
error("{p} is not a valid path", p=dst)
|
||||
|
||||
if not isinstance(files, Iterable):
|
||||
error(
|
||||
"{fa} must be an iterable (list, tuple, or generator). Received {f}",
|
||||
fa="Files argument",
|
||||
f=files,
|
||||
)
|
||||
|
||||
for path in (src, dst):
|
||||
if not path.exists():
|
||||
error("{p} does not exist", p=str(path))
|
||||
|
||||
migrated = 0
|
||||
|
||||
for file in files:
|
||||
dst_file = dst / file.name
|
||||
|
||||
if not file.exists():
|
||||
error("{f} does not exist", f=file)
|
||||
|
||||
try:
|
||||
if dst_file.exists():
|
||||
warning("{f} already exists", f=dst_file)
|
||||
else:
|
||||
shutil.copyfile(file, dst_file)
|
||||
migrated += 1
|
||||
info("Migrated {f}", f=dst_file)
|
||||
except Exception as e:
|
||||
error("Failed to migrate {f}: {e}", f=dst_file, e=e)
|
||||
|
||||
if migrated == 0:
|
||||
warning("Migrated {n} files", n=migrated)
|
||||
elif migrated > 0:
|
||||
success("Successfully migrated {n} files", n=migrated)
|
||||
return True
|
||||
|
||||
|
||||
def make_systemd(user):
|
||||
"""Generate a systemd file based on the local system.
|
||||
|
||||
Arguments:
|
||||
user {str} -- User hyperglass needs to be run as
|
||||
|
||||
Returns:
|
||||
{str} -- Generated systemd template
|
||||
"""
|
||||
|
||||
# Third Party
|
||||
import distro
|
||||
|
||||
template = """
|
||||
[Unit]
|
||||
Description=hyperglass
|
||||
After=network.target
|
||||
Requires={redis_name}
|
||||
|
||||
[Service]
|
||||
User={user}
|
||||
Group={group}
|
||||
ExecStart={hyperglass_path} start
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"""
|
||||
known_rhel = ("rhel", "centos")
|
||||
distro = distro.linux_distribution(full_distribution_name=False)
|
||||
if distro[0] in known_rhel:
|
||||
redis_name = "redis"
|
||||
else:
|
||||
redis_name = "redis-server"
|
||||
|
||||
hyperglass_path = shutil.which("hyperglass")
|
||||
|
||||
if not hyperglass_path:
|
||||
hyperglass_path = "python3 -m hyperglass.console"
|
||||
warning("hyperglass executable not found, using {h}", h=hyperglass_path)
|
||||
|
||||
systemd = template.format(
|
||||
redis_name=redis_name, user=user, group=user, hyperglass_path=hyperglass_path
|
||||
)
|
||||
info(f"Generated systemd service:\n{systemd}")
|
||||
return systemd
|
||||
|
||||
|
||||
def write_to_file(file, data):
|
||||
"""Write string data to a file.
|
||||
|
||||
|
|
@ -365,47 +160,6 @@ def write_to_file(file, data):
|
|||
return True
|
||||
|
||||
|
||||
def migrate_static_assets(app_path):
|
||||
"""Migrate app's static assets to app_path.
|
||||
|
||||
Arguments:
|
||||
app_path {Path} -- hyperglass runtime path
|
||||
"""
|
||||
# Project
|
||||
from hyperglass.util import migrate_static_assets as _migrate
|
||||
|
||||
migrated, msg, a, b = _migrate(app_path)
|
||||
if not migrated:
|
||||
callback = error
|
||||
elif migrated:
|
||||
callback = success
|
||||
|
||||
callback(msg, a=a, b=b)
|
||||
|
||||
|
||||
def install_systemd(app_path: Path) -> bool:
|
||||
"""Installs generated systemd file to system's systemd directory."""
|
||||
|
||||
service = app_path / "hyperglass.service"
|
||||
systemd = Path("/etc/systemd/system")
|
||||
installed = systemd / "hyperglass.service"
|
||||
|
||||
if not systemd.exists():
|
||||
error("{e} does not exist. Unable to install systemd service.", e=systemd)
|
||||
|
||||
try:
|
||||
installed.symlink_to(service)
|
||||
if not installed.exists():
|
||||
warning("Unable to symlink {s} to {d}", s=service, d=installed)
|
||||
else:
|
||||
success("Symlinked {s} to {d}", s=service, d=installed)
|
||||
|
||||
except PermissionError:
|
||||
warning("Permission denied to {f}. Systemd service not installed.", f=installed)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def system_info():
|
||||
"""Create a markdown table of various system information.
|
||||
|
||||
|
|
|
|||
|
|
@ -258,44 +258,6 @@ async def move_files(src, dst, files): # noqa: C901
|
|||
return migrated
|
||||
|
||||
|
||||
def migrate_static_assets(app_path):
|
||||
"""Synchronize the project assets with the installation assets."""
|
||||
|
||||
# Standard Library
|
||||
from filecmp import dircmp
|
||||
|
||||
asset_dir = Path(__file__).parent.parent / "images"
|
||||
target_dir = app_path / "static" / "images"
|
||||
|
||||
target_exists = target_dir.exists()
|
||||
|
||||
if not target_exists:
|
||||
shutil.copytree(asset_dir, target_dir)
|
||||
|
||||
# Compare the contents of the project's asset directory (considered
|
||||
# the source of truth) with the installation directory. If they do
|
||||
# not match, delete the installation directory's asset directory and
|
||||
# re-copy it.
|
||||
compare_initial = dircmp(asset_dir, target_dir, ignore=[".DS_Store"])
|
||||
|
||||
if not compare_initial.left_list == compare_initial.right_list:
|
||||
shutil.rmtree(target_dir)
|
||||
shutil.copytree(asset_dir, target_dir)
|
||||
|
||||
# Re-compare the source and destination directory contents to
|
||||
# ensure they match.
|
||||
compare_post = dircmp(asset_dir, target_dir, ignore=[".DS_Store"])
|
||||
|
||||
if not compare_post.left_list == compare_post.right_list:
|
||||
return (
|
||||
False,
|
||||
"Files in {a} do not match files in {b}",
|
||||
str(asset_dir),
|
||||
str(target_dir),
|
||||
)
|
||||
return (True, "Migrated assets from {a} to {b}", str(asset_dir), str(target_dir))
|
||||
|
||||
|
||||
async def check_node_modules():
|
||||
"""Check if node_modules exists and has contents.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue