mirror of
https://github.com/iFargle/headscale-webui.git
synced 2025-12-12 01:49:46 +01:00
[deploy] Allow changing data directory in bare-metal setups
This commit is contained in:
@@ -4,6 +4,7 @@ COLOR="blue-grey"
|
||||
HS_SERVER=http://localhost:8080
|
||||
KEY="GenerateYourOwnRandomKey"
|
||||
SCRIPT_NAME=/admin
|
||||
DATA_DIRECTORY=/data
|
||||
DOMAIN_NAME=http://localhost:8080
|
||||
AUTH_TYPE="Basic"
|
||||
LOG_LEVEL="Debug"
|
||||
|
||||
1
SETUP.md
1
SETUP.md
@@ -37,6 +37,7 @@ That's it. Cheers.
|
||||
* `COLOR` Set this to your preferred color scheme. See the [MaterializeCSS docs](https://materializecss.github.io/materialize/color.html#palette) for examples. Only set the "base" color -- ie, instead of `blue-gray darken-1`, just use `blue-gray`.
|
||||
* `HS_SERVER` is the URL for your Headscale control server.
|
||||
* `SCRIPT_NAME` is your "Base Path" for hosting. For example, if you want to host on http://localhost/admin, set this to `/admin`, otherwise remove this variable entirely.
|
||||
* `DATA_DIRECTORY` is your "Data Path". This is there the application will create and store data. Only applicable for bare metal installations.
|
||||
* `KEY` is your encryption key. Set this to a random value generated from `openssl rand -base64 32`
|
||||
* `AUTH_TYPE` can be set to `Basic` or `OIDC`. See the [Authentication](#Authentication) section below for more information.
|
||||
* `LOG_LEVEL` can be one of `Debug`, `Info`, `Warning`, `Error`, or `Critical` for decreasing verbosity. Default is `Info` if removed from your Environment.
|
||||
|
||||
@@ -9,6 +9,7 @@ from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
LOG_LEVEL = os.environ["LOG_LEVEL"].replace('"', '').upper()
|
||||
DATA_DIRECTORY = os.environ["DATA_DIRECTORY"].replace('"', '') if os.environ["DATA_DIRECTORY"] else "/data"
|
||||
# Initiate the Flask application and logging:
|
||||
app = Flask(__name__, static_url_path="/static")
|
||||
match LOG_LEVEL:
|
||||
@@ -41,7 +42,7 @@ def set_api_key(api_key):
|
||||
# User-set encryption key
|
||||
encryption_key = os.environ['KEY']
|
||||
# Key file on the filesystem for persistent storage
|
||||
key_file = open("/data/key.txt", "wb+")
|
||||
key_file = open(os.path.join(DATA_DIRECTORY, "key.txt"), "wb+")
|
||||
# Preparing the Fernet class with the key
|
||||
fernet = Fernet(encryption_key)
|
||||
# Encrypting the key
|
||||
@@ -50,11 +51,11 @@ def set_api_key(api_key):
|
||||
return True if key_file.write(encrypted_key) else False
|
||||
|
||||
def get_api_key():
|
||||
if not os.path.exists("/data/key.txt"): return False
|
||||
if not os.path.exists(os.path.join(DATA_DIRECTORY, "key.txt")): return False
|
||||
# User-set encryption key
|
||||
encryption_key = os.environ['KEY']
|
||||
# Key file on the filesystem for persistent storage
|
||||
key_file = open("/data/key.txt", "rb+")
|
||||
key_file = open(os.path.join(DATA_DIRECTORY, "key.txt"), "rb+")
|
||||
# The encrypted key read from the file
|
||||
enc_api_key = key_file.read()
|
||||
if enc_api_key == b'': return "NULL"
|
||||
|
||||
101
helper.py
101
helper.py
@@ -4,6 +4,7 @@ import os, headscale, requests, logging
|
||||
from flask import Flask
|
||||
|
||||
LOG_LEVEL = os.environ["LOG_LEVEL"].replace('"', '').upper()
|
||||
DATA_DIRECTORY = os.environ["DATA_DIRECTORY"].replace('"', '') if os.environ["DATA_DIRECTORY"] else "/data"
|
||||
# Initiate the Flask application and logging:
|
||||
app = Flask(__name__, static_url_path="/static")
|
||||
match LOG_LEVEL:
|
||||
@@ -45,7 +46,7 @@ def text_color_duration(duration):
|
||||
if days > 5: return "deep-orange-text text-lighten-1"
|
||||
if days > 1: return "deep-orange-text text-lighten-1"
|
||||
if hours > 12: return "orange-text "
|
||||
if hours > 1: return "orange-text text-lighten-2"
|
||||
if hours > 1: return "orange-text text-lighten-2"
|
||||
if hours == 1: return "yellow-text "
|
||||
if mins > 15: return "yellow-text text-lighten-2"
|
||||
if mins > 5: return "green-text text-lighten-3"
|
||||
@@ -57,11 +58,11 @@ def key_check():
|
||||
api_key = headscale.get_api_key()
|
||||
url = headscale.get_url()
|
||||
|
||||
# Test the API key. If the test fails, return a failure.
|
||||
# Test the API key. If the test fails, return a failure.
|
||||
# AKA, if headscale returns Unauthorized, fail:
|
||||
app.logger.info("Testing API key validity.")
|
||||
status = headscale.test_api_key(url, api_key)
|
||||
if status != 200:
|
||||
if status != 200:
|
||||
app.logger.info("Got a non-200 response from Headscale. Test failed (Response: %i)", status)
|
||||
return False
|
||||
else:
|
||||
@@ -128,7 +129,7 @@ def format_message(error_type, title, message):
|
||||
<ul class="collection">
|
||||
<li class="collection-item avatar">
|
||||
"""
|
||||
|
||||
|
||||
match error_type.lower():
|
||||
case "warning":
|
||||
icon = """<i class="material-icons circle yellow">priority_high</i>"""
|
||||
@@ -143,7 +144,7 @@ def format_message(error_type, title, message):
|
||||
icon = """<i class="material-icons circle grey">help</i>"""
|
||||
title = """<span class="title">Information - """+title+"""</span>"""
|
||||
|
||||
content = content+icon+title+message
|
||||
content = content+icon+title+message
|
||||
content = content+"""
|
||||
</li>
|
||||
</ul>
|
||||
@@ -158,12 +159,12 @@ def access_checks():
|
||||
# Return an error message if things fail.
|
||||
# Return a formatted error message for EACH fail.
|
||||
checks_passed = True # Default to true. Set to false when any checks fail.
|
||||
data_readable = False # Checks R permissions of /data
|
||||
data_writable = False # Checks W permissions of /data
|
||||
data_readable = False # Checks R permissions of DATA_DIRECTORY
|
||||
data_writable = False # Checks W permissions of DATA_DIRECTORY
|
||||
data_executable = False # Execute on directories allows file access
|
||||
file_readable = False # Checks R permissions of /data/key.txt
|
||||
file_writable = False # Checks W permissions of /data/key.txt
|
||||
file_exists = False # Checks if /data/key.txt exists
|
||||
file_readable = False # Checks R permissions of DATA_DIRECTORY/key.txt
|
||||
file_writable = False # Checks W permissions of DATA_DIRECTORY/key.txt
|
||||
file_exists = False # Checks if DATA_DIRECTORY/key.txt exists
|
||||
config_readable = False # Checks if the headscale configuration file is readable
|
||||
|
||||
|
||||
@@ -176,32 +177,32 @@ def access_checks():
|
||||
checks_passed = False
|
||||
app.logger.critical("Headscale URL: Response 200: FAILED")
|
||||
|
||||
# Check: /data is rwx for 1000:1000:
|
||||
if os.access('/data/', os.R_OK): data_readable = True
|
||||
# Check: DATA_DIRECTORY is rwx for 1000:1000:
|
||||
if os.access(DATA_DIRECTORY, os.R_OK): data_readable = True
|
||||
else:
|
||||
app.logger.critical("/data READ: FAILED")
|
||||
app.logger.critical(f"{DATA_DIRECTORY} READ: FAILED")
|
||||
checks_passed = False
|
||||
if os.access('/data/', os.W_OK): data_writable = True
|
||||
if os.access(DATA_DIRECTORY, os.W_OK): data_writable = True
|
||||
else:
|
||||
app.logger.critical("/data WRITE: FAILED")
|
||||
app.logger.critical(f"{DATA_DIRECTORY} WRITE: FAILED")
|
||||
checks_passed = False
|
||||
if os.access('/data/', os.X_OK): data_executable = True
|
||||
if os.access(DATA_DIRECTORY, os.X_OK): data_executable = True
|
||||
else:
|
||||
app.logger.critical("/data EXEC: FAILED")
|
||||
app.logger.critical(f"{DATA_DIRECTORY} EXEC: FAILED")
|
||||
checks_passed = False
|
||||
|
||||
# Check: /data/key.txt exists and is rw:
|
||||
if os.access('/data/key.txt', os.F_OK):
|
||||
# Check: DATA_DIRECTORY/key.txt exists and is rw:
|
||||
if os.access(os.path.join(DATA_DIRECTORY, "key.txt"), os.F_OK):
|
||||
file_exists = True
|
||||
if os.access('/data/key.txt', os.R_OK): file_readable = True
|
||||
if os.access(os.path.join(DATA_DIRECTORY, "key.txt"), os.R_OK): file_readable = True
|
||||
else:
|
||||
app.logger.critical("/data/key.txt READ: FAILED")
|
||||
app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} READ: FAILED")
|
||||
checks_passed = False
|
||||
if os.access('/data/key.txt', os.W_OK): file_writable = True
|
||||
if os.access(os.path.join(DATA_DIRECTORY, "key.txt"), os.W_OK): file_writable = True
|
||||
else:
|
||||
app.logger.critical("/data/key.txt WRITE: FAILED")
|
||||
app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} WRITE: FAILED")
|
||||
checks_passed = False
|
||||
else: app.logger.error("/data/key.txt EXIST: FAILED - NO ERROR")
|
||||
else: app.logger.error(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} EXIST: FAILED - NO ERROR")
|
||||
|
||||
# Check: /etc/headscale/config.yaml is readable:
|
||||
if os.access('/etc/headscale/config.yaml', os.R_OK): config_readable = True
|
||||
@@ -219,7 +220,7 @@ def access_checks():
|
||||
if not server_reachable:
|
||||
app.logger.critical("Server is unreachable")
|
||||
message = """
|
||||
<p>Your headscale server is either unreachable or not properly configured.
|
||||
<p>Your headscale server is either unreachable or not properly configured.
|
||||
Please ensure your configuration is correct (Check for 200 status on
|
||||
"""+url+"""/api/v1 failed. Response: """+str(response.status_code)+""".)</p>
|
||||
"""
|
||||
@@ -237,58 +238,58 @@ def access_checks():
|
||||
message_html += format_message("Error", "/etc/headscale/config.yaml not readable", message)
|
||||
|
||||
if not data_writable:
|
||||
app.logger.critical("/data folder is not writable")
|
||||
message = """
|
||||
<p>/data is not writable. Please ensure your
|
||||
permissions are correct. /data mount should be writable
|
||||
app.logger.critical(f"{DATA_DIRECTORY} folder is not writable")
|
||||
message = f"""
|
||||
<p>{DATA_DIRECTORY} is not writable. Please ensure your
|
||||
permissions are correct. {DATA_DIRECTORY} mount should be writable
|
||||
by UID/GID 1000:1000.</p>
|
||||
"""
|
||||
|
||||
message_html += format_message("Error", "/data not writable", message)
|
||||
message_html += format_message("Error", f"{DATA_DIRECTORY} not writable", message)
|
||||
|
||||
if not data_readable:
|
||||
app.logger.critical("/data folder is not readable")
|
||||
message = """
|
||||
<p>/data is not readable. Please ensure your
|
||||
permissions are correct. /data mount should be readable
|
||||
app.logger.critical(f"{DATA_DIRECTORY} folder is not readable")
|
||||
message = f"""
|
||||
<p>{DATA_DIRECTORY} is not readable. Please ensure your
|
||||
permissions are correct. {DATA_DIRECTORY} mount should be readable
|
||||
by UID/GID 1000:1000.</p>
|
||||
"""
|
||||
|
||||
message_html += format_message("Error", "/data not readable", message)
|
||||
message_html += format_message("Error", f"{DATA_DIRECTORY} not readable", message)
|
||||
|
||||
if not data_executable:
|
||||
app.logger.critical("/data folder is not readable")
|
||||
message = """
|
||||
<p>/data is not executable. Please ensure your
|
||||
permissions are correct. /data mount should be readable
|
||||
app.logger.critical(f"{DATA_DIRECTORY} folder is not readable")
|
||||
message = f"""
|
||||
<p>{DATA_DIRECTORY} is not executable. Please ensure your
|
||||
permissions are correct. {DATA_DIRECTORY} mount should be readable
|
||||
by UID/GID 1000:1000. (chown 1000:1000 /path/to/data && chmod -R 755 /path/to/data)</p>
|
||||
"""
|
||||
|
||||
message_html += format_message("Error", "/data not executable", message)
|
||||
message_html += format_message("Error", f"{DATA_DIRECTORY} not executable", message)
|
||||
|
||||
|
||||
if file_exists:
|
||||
# If it doesn't exist, we assume the user hasn't created it yet.
|
||||
# Just redirect to the settings page to enter an API Key
|
||||
if not file_writable:
|
||||
app.logger.critical("/data/key.txt is not writable")
|
||||
message = """
|
||||
<p>/data/key.txt is not writable. Please ensure your
|
||||
permissions are correct. /data mount should be writable
|
||||
app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} is not writable")
|
||||
message = f"""
|
||||
<p>{os.path.join(DATA_DIRECTORY, 'key.txt')} is not writable. Please ensure your
|
||||
permissions are correct. {DATA_DIRECTORY} mount should be writable
|
||||
by UID/GID 1000:1000.</p>
|
||||
"""
|
||||
|
||||
message_html += format_message("Error", "/data/key.txt not writable", message)
|
||||
message_html += format_message("Error", f"{os.path.join(DATA_DIRECTORY, 'key.txt')} not writable", message)
|
||||
|
||||
if not file_readable:
|
||||
app.logger.critical("/data/key.txt is not readable")
|
||||
message = """
|
||||
<p>/data/key.txt is not readable. Please ensure your
|
||||
permissions are correct. /data mount should be readable
|
||||
app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} is not readable")
|
||||
message = f"""
|
||||
<p>{os.path.join(DATA_DIRECTORY, 'key.txt')} is not readable. Please ensure your
|
||||
permissions are correct. {DATA_DIRECTORY} mount should be readable
|
||||
by UID/GID 1000:1000.</p>
|
||||
"""
|
||||
|
||||
message_html += format_message("Error", "/data/key.txt not readable", message)
|
||||
message_html += format_message("Error", f"{os.path.join(DATA_DIRECTORY, 'key.txt')} not readable", message)
|
||||
|
||||
return message_html
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ from flask import Flask, escape, Markup, redirect, rende
|
||||
from dateutil import parser
|
||||
from flask_executor import Executor
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
# Global vars
|
||||
# Colors: https://materializecss.com/color.html
|
||||
COLOR = os.environ["COLOR"].replace('"', '').lower()
|
||||
|
||||
Reference in New Issue
Block a user