1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00
2020-02-14 22:34:38 -07:00

384 lines
11 KiB
Python

"""Utility functions."""
def _logger():
from loguru import logger as _loguru_logger
from hyperglass.constants import LOG_HANDLER
from hyperglass.constants import LOG_LEVELS
_loguru_logger.remove()
_loguru_logger.configure(handlers=[LOG_HANDLER], levels=LOG_LEVELS)
return _loguru_logger
log = _logger()
def cpu_count():
"""Get server's CPU core count.
Used for number of web server workers.
Returns:
{int} -- CPU Cores
"""
import multiprocessing
return multiprocessing.cpu_count()
async def check_path(path, mode="r"):
"""Verify if a path exists and is accessible.
Arguments:
path {Path|str} -- Path object or string of path
mode {str} -- File mode, r or w
Raises:
RuntimeError: Raised if file does not exist or is not accessible
Returns:
{Path|None} -- Path object if checks pass, None if not.
"""
from pathlib import Path
from aiofile import AIOFile
try:
if not isinstance(path, Path):
path = Path(path)
if not path.exists():
raise FileNotFoundError(f"{str(path)} does not exist.")
async with AIOFile(path, mode):
result = path
except Exception:
result = None
return result
def check_python():
"""Verify Python Version.
Raises:
RuntimeError: Raised if running Python version is invalid.
Returns:
{str} -- Python version
"""
import sys
from hyperglass.constants import MIN_PYTHON_VERSION
pretty_version = ".".join(tuple(str(v) for v in MIN_PYTHON_VERSION))
if sys.version_info < MIN_PYTHON_VERSION:
raise RuntimeError(f"Python {pretty_version}+ is required.")
return pretty_version
async def build_ui(app_path):
"""Execute `next build` & `next export` from UI directory.
Raises:
RuntimeError: Raised if exit code is not 0.
RuntimeError: Raised when any other error occurs.
"""
import asyncio
from pathlib import Path
ui_dir = Path(__file__).parent.parent / "ui"
build_dir = app_path / "static" / "ui"
build_command = "node_modules/.bin/next build"
export_command = "node_modules/.bin/next export -o {f}".format(f=build_dir)
all_messages = []
for command in (build_command, export_command):
try:
proc = await asyncio.create_subprocess_shell(
cmd=command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=ui_dir,
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=60)
messages = stdout.decode("utf-8").strip()
errors = stderr.decode("utf-8").strip()
if proc.returncode != 0:
raise RuntimeError(f"\nMessages:\n{messages}\nErrors:\n{errors}")
await proc.wait()
all_messages.append(messages)
except Exception as e:
raise RuntimeError(str(e))
return "\n".join(all_messages)
async def write_env(variables):
"""Write environment variables to temporary JSON file.
Arguments:
variables {dict} -- Environment variables to write.
Raises:
RuntimeError: Raised on any errors.
"""
from aiofile import AIOFile
import ujson as json
from pathlib import Path
env_file = Path("/tmp/hyperglass.env.json") # noqa: S108
env_vars = json.dumps(variables)
try:
async with AIOFile(env_file, "w+") as ef:
await ef.write(env_vars)
await ef.fsync()
except Exception as e:
raise RuntimeError(str(e))
return f"Wrote {env_vars} to {str(env_file)}"
async def check_redis(db, config):
"""Ensure Redis is running before starting server.
Arguments:
db {int} -- Redis database ID
config {dict} -- Redis configuration parameters
Raises:
RuntimeError: Raised if Redis is not running.
Returns:
{bool} -- True if redis is running.
"""
import aredis
redis_instance = aredis.StrictRedis(db=db, **config)
redis_host = config["host"]
redis_port = config["port"]
try:
await redis_instance.echo("hyperglass test")
except Exception:
raise RuntimeError(
f"Redis isn't running at: {redis_host}:{redis_port}"
) from None
return True
async def clear_redis_cache(db, config):
"""Clear the Redis cache.
Arguments:
db {int} -- Redis database ID
config {dict} -- Redis configuration parameters
Raises:
RuntimeError: Raised if clearing the cache produces an error.
Returns:
{bool} -- True if cache was cleared.
"""
import aredis
try:
redis_instance = aredis.StrictRedis(db=db, **config)
await redis_instance.flushdb()
except Exception as e:
raise RuntimeError(f"Error clearing cache: {str(e)}") from None
return True
async 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
"""
import shutil
from pathlib import Path
from typing import Iterable
def error(*args, **kwargs):
msg = ", ".join(args)
kwargs = {k: str(v) for k, v in kwargs.items()}
error_msg = msg.format(**kwargs)
log.error(error_msg)
return RuntimeError(error_msg)
if not isinstance(src, Path):
try:
src = Path(src)
except TypeError:
raise error("{p} is not a valid path", p=src)
if not isinstance(dst, Path):
try:
dst = Path(dst)
except TypeError:
raise error("{p} is not a valid path", p=dst)
if not isinstance(files, Iterable):
raise 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():
raise error("{p} does not exist", p=path)
migrated = ()
for file in files:
dst_file = dst / file.name
if not file.exists():
raise error("{f} does not exist", f=file)
try:
if not dst_file.exists():
shutil.copyfile(file, dst_file)
migrated += (str(dst_file),)
except Exception as e:
raise error("Failed to migrate {f}: {e}", f=dst_file, e=e)
return migrated
async def build_frontend( # noqa: C901
dev_mode, dev_url, prod_url, params, app_path, force=False
):
"""Perform full frontend UI build process.
Securely creates temporary file, writes frontend configuration
parameters to file as JSON. Then writes the name of the temporary
file to /tmp/hyperglass.env.json as {"configFile": <file_name> }.
Webpack reads /tmp/hyperglass.env.json, loads the temporary file,
and sets its contents to Node environment variables during the build
process.
After the build is successful, the temporary file is automatically
closed during garbage collection.
Arguments:
dev_mode {bool} -- Development Mode
dev_url {str} -- Development Mode URL
prod_url {str} -- Production Mode URL
params {dict} -- Frontend Config paramters
Raises:
RuntimeError: Raised if errors occur during build process.
Returns:
{bool} -- True if successful
"""
import hashlib
import tempfile
import shutil
from filecmp import dircmp
from pathlib import Path
from aiofile import AIOFile
import ujson as json
env_file = Path("/tmp/hyperglass.env.json") # noqa: S108
env_vars = {"_HYPERGLASS_CONFIG_": params}
"""
Set NextJS production/development mode and base URL based on
developer_mode setting.
"""
if dev_mode:
env_vars.update({"NODE_ENV": "development", "_HYPERGLASS_URL_": dev_url})
else:
env_vars.update({"NODE_ENV": "production", "_HYPERGLASS_URL_": prod_url})
try:
env_json = json.dumps(env_vars)
"""
Create SHA256 hash from all parameters passed to UI, use as
build identifier.
"""
build_id = hashlib.sha256(env_json.encode()).hexdigest()
if env_file.exists() and not force:
"""
Read hard-coded environment file from last build. If build
ID matches this build's ID, don't run a new build.
"""
async with AIOFile(env_file, "r") as ef:
ef_json = await ef.read()
ef_id = json.loads(ef_json).get("buildId", "empty")
log.debug("Previous Build ID: {id}", id=ef_id)
if ef_id == build_id:
log.debug(
"UI parameters unchanged since last build, skipping UI build..."
)
return True
"""
Create temporary file. json file extension is added for easy
webpack JSON parsing.
"""
temp_file = tempfile.NamedTemporaryFile(
mode="w+", prefix="hyperglass_", suffix=".json", delete=not dev_mode
)
log.debug(
f"Created temporary UI config file: '{temp_file.name}' for build {build_id}"
)
async with AIOFile(temp_file.name, "w+") as temp:
await temp.write(env_json)
await temp.fsync()
# Write "permanent" file (hard-coded named) for Node to read.
async with AIOFile(env_file, "w+") as ef:
await ef.write(
json.dumps({"configFile": temp_file.name, "buildId": build_id})
)
await ef.fsync()
# While temporary file is still open, initiate UI build process.
if not dev_mode or force:
build_result = await build_ui(app_path=app_path)
if build_result:
log.debug("Completed UI build")
elif dev_mode and not force:
log.debug("Running in developer mode, did not build new UI files")
"""
Compare repository's static assets with build directory's
assets. If the contents don't match, re-copy the files.
"""
asset_dir = Path(__file__).parent.parent / "assets"
target_dir = app_path / "static" / "images"
comparison = dircmp(asset_dir, target_dir, ignore=[".DS_Store"])
if not comparison.left_list == comparison.right_list:
shutil.copytree(asset_dir, target_dir)
if not comparison.left_list == comparison.right_list:
raise Exception(
"Files in '{a}' do not match files in '{b}'".format(
a=str(asset_dir), b=str(target_dir)
)
)
except Exception as e:
raise RuntimeError(str(e))
return True