diff --git a/headscale.py b/headscale.py index 88cc136..c6e63ea 100644 --- a/headscale.py +++ b/headscale.py @@ -1,13 +1,12 @@ # pylint: disable=wrong-import-order -import requests, json, os, logger +import requests, json, os from cryptography.fernet import Fernet from datetime import timedelta, date from dateutil import parser -from flask import Flask +from flask import Flask, logging app = Flask(__name__, static_url_path="/static") -LOG = app.logger() ################################################################## # Functions related to HEADSCALE and API KEYS @@ -58,7 +57,7 @@ def test_api_key(url, api_key): def expire_key(url, api_key): payload = {'prefix':str(api_key[0:10])} json_payload=json.dumps(payload) - LOG.debug("Sending the payload '"+str(json_payload)+"' to the headscale server") + app.logger.debug("Sending the payload '"+str(json_payload)+"' to the headscale server") response = requests.post( str(url)+"/api/v1/apikey/expire", @@ -89,10 +88,10 @@ def renew_api_key(url, api_key): # If the delta is less than 5 days, renew the key: if delta < timedelta(days=5): - LOG.warning("Key is about to expire. Delta is "+str(delta)) + app.logger.warning("Key is about to expire. Delta is "+str(delta)) payload = {'expiration':str(new_expiration_date)} json_payload=json.dumps(payload) - LOG.debug("Sending the payload '"+str(json_payload)+"' to the headscale server") + app.logger.debug("Sending the payload '"+str(json_payload)+"' to the headscale server") response = requests.post( str(url)+"/api/v1/apikey", @@ -104,27 +103,27 @@ def renew_api_key(url, api_key): } ) new_key = response.json() - LOG.debug("JSON: "+json.dumps(new_key)) - LOG.debug("New Key is: "+new_key["apiKey"]) + app.logger.debug("JSON: "+json.dumps(new_key)) + app.logger.debug("New Key is: "+new_key["apiKey"]) api_key_test = test_api_key(url, new_key["apiKey"]) - LOG.debug("Testing the key: "+str(api_key_test)) + app.logger.debug("Testing the key: "+str(api_key_test)) # Test if the new key works: if api_key_test == 200: - LOG.info("The new key is valid and we are writing it to the file") + app.logger.info("The new key is valid and we are writing it to the file") if not set_api_key(new_key["apiKey"]): - LOG.error("We failed writing the new key!") + app.logger.error("We failed writing the new key!") return False # Key write failed - LOG.info("Key validated and written. Moving to expire the key.") + app.logger.info("Key validated and written. Moving to expire the key.") expire_key(url, api_key) return True # Key updated and validated else: - LOG.error("Testing the API key failed.") + app.logger.error("Testing the API key failed.") return False # The API Key test failed else: return True # No work is required # Gets information about the current API key def get_api_key_info(url, api_key): - LOG.info("Getting API key information") + app.logger.info("Getting API key information") response = requests.get( str(url)+"/api/v1/apikey", headers={ @@ -135,12 +134,12 @@ def get_api_key_info(url, api_key): json_response = response.json() # Find the current key in the array: key_prefix = str(api_key[0:10]) - LOG.info("Looking for valid API Key...") + app.logger.info("Looking for valid API Key...") for key in json_response["apiKeys"]: if key_prefix == key["prefix"]: - LOG.info("Key found.") + app.logger.info("Key found.") return key - LOG.error("Could not find a valid key in Headscale. Need a new API key.") + app.logger.error("Could not find a valid key in Headscale. Need a new API key.") return "Key not found" ################################################################## @@ -149,7 +148,7 @@ def get_api_key_info(url, api_key): # register a new machine def register_machine(url, api_key, machine_key, user): - LOG.info("Registering machine %s to user %s", str(machine_key), str(user)) + app.logger.info("Registering machine %s to user %s", str(machine_key), str(user)) response = requests.post( str(url)+"/api/v1/machine/register?user="+str(user)+"&key="+str(machine_key), headers={ @@ -162,7 +161,7 @@ def register_machine(url, api_key, machine_key, user): # Sets the machines tags def set_machine_tags(url, api_key, machine_id, tags_list): - LOG.info("Setting machine_id %s tag %s", str(machine_id), str(tags_list)) + app.logger.info("Setting machine_id %s tag %s", str(machine_id), str(tags_list)) response = requests.post( str(url)+"/api/v1/machine/"+str(machine_id)+"/tags", data=tags_list, @@ -176,7 +175,7 @@ def set_machine_tags(url, api_key, machine_id, tags_list): # Moves machine_id to user "new_user" def move_user(url, api_key, machine_id, new_user): - LOG.info("Moving machine_id %s to user %s", str(machine_id), str(new_user)) + app.logger.info("Moving machine_id %s to user %s", str(machine_id), str(new_user)) response = requests.post( str(url)+"/api/v1/machine/"+str(machine_id)+"/user?user="+str(new_user), headers={ @@ -190,13 +189,13 @@ def update_route(url, api_key, route_id, current_state): action = "" if current_state == "True": action = "disable" if current_state == "False": action = "enable" - LOG.info("Updating Route %s: Action: %s", str(route_id), str(action)) + app.logger.info("Updating Route %s: Action: %s", str(route_id), str(action)) # Debug - LOG.debug("URL: "+str(url)) - LOG.debug("Route ID: "+str(route_id)) - LOG.debug("Current State: "+str(current_state)) - LOG.debug("Action to take: "+str(action)) + app.logger.debug("URL: "+str(url)) + app.logger.debug("Route ID: "+str(route_id)) + app.logger.debug("Current State: "+str(current_state)) + app.logger.debug("Action to take: "+str(action)) response = requests.post( str(url)+"/api/v1/routes/"+str(route_id)+"/"+str(action), @@ -209,7 +208,7 @@ def update_route(url, api_key, route_id, current_state): # Get all machines on the Headscale network def get_machines(url, api_key): - LOG.info("Getting machine information") + app.logger.info("Getting machine information") response = requests.get( str(url)+"/api/v1/machine", headers={ @@ -221,7 +220,7 @@ def get_machines(url, api_key): # Get machine with "machine_id" on the Headscale network def get_machine_info(url, api_key, machine_id): - LOG.info("Getting information for machine ID %s", str(machine_id)) + app.logger.info("Getting information for machine ID %s", str(machine_id)) response = requests.get( str(url)+"/api/v1/machine/"+str(machine_id), headers={ @@ -233,7 +232,7 @@ def get_machine_info(url, api_key, machine_id): # Delete a machine from Headscale def delete_machine(url, api_key, machine_id): - LOG.info("Deleting machine %s", str(machine_id)) + app.logger.info("Deleting machine %s", str(machine_id)) response = requests.delete( str(url)+"/api/v1/machine/"+str(machine_id), headers={ @@ -243,14 +242,14 @@ def delete_machine(url, api_key, machine_id): ) status = "True" if response.status_code == 200 else "False" if response.status_code == 200: - LOG.info("Machine deleted.") + app.logger.info("Machine deleted.") else: - LOG.error("Deleting machine failed! %s", str(response.json())) + app.logger.error("Deleting machine failed! %s", str(response.json())) return {"status": status, "body": response.json()} # Rename "machine_id" with name "new_name" def rename_machine(url, api_key, machine_id, new_name): - LOG.info("Renaming machine %s", str(machine_id)) + app.logger.info("Renaming machine %s", str(machine_id)) response = requests.post( str(url)+"/api/v1/machine/"+str(machine_id)+"/rename/"+str(new_name), headers={ @@ -260,14 +259,14 @@ def rename_machine(url, api_key, machine_id, new_name): ) status = "True" if response.status_code == 200 else "False" if response.status_code == 200: - LOG.info("Machine renamed") + app.logger.info("Machine renamed") else: - LOG.error("Machine rename failed! %s", str(response.json())) + app.logger.error("Machine rename failed! %s", str(response.json())) return {"status": status, "body": response.json()} # Gets routes for the passed machine_id def get_machine_routes(url, api_key, machine_id): - LOG.info("Renaming machine %s", str(machine_id)) + app.logger.info("Renaming machine %s", str(machine_id)) response = requests.get( str(url)+"/api/v1/machine/"+str(machine_id)+"/routes", headers={ @@ -276,14 +275,14 @@ def get_machine_routes(url, api_key, machine_id): } ) if response.status_code == 200: - LOG.info("Routes obtained") + app.logger.info("Routes obtained") else: - LOG.error("Failed to get routes: %s", str(response.json())) + app.logger.error("Failed to get routes: %s", str(response.json())) return response.json() # Gets routes for the entire tailnet def get_routes(url, api_key): - LOG.info("Getting routes") + app.logger.info("Getting routes") response = requests.get( str(url)+"/api/v1/routes", headers={ @@ -299,7 +298,7 @@ def get_routes(url, api_key): # Get all users in use def get_users(url, api_key): - LOG.info("Getting Users") + app.logger.info("Getting Users") response = requests.get( str(url)+"/api/v1/user", headers={ @@ -311,7 +310,7 @@ def get_users(url, api_key): # Rename "old_name" with name "new_name" def rename_user(url, api_key, old_name, new_name): - LOG.info("Renaming user %s to %s.", str(old_name), str(new_name)) + app.logger.info("Renaming user %s to %s.", str(old_name), str(new_name)) response = requests.post( str(url)+"/api/v1/user/"+str(old_name)+"/rename/"+str(new_name), headers={ @@ -321,14 +320,14 @@ def rename_user(url, api_key, old_name, new_name): ) status = "True" if response.status_code == 200 else "False" if response.status_code == 200: - LOG.info("User renamed.") + app.logger.info("User renamed.") else: - LOG.error("Renaming User failed!") + app.logger.error("Renaming User failed!") return {"status": status, "body": response.json()} # Delete a user from Headscale def delete_user(url, api_key, user_name): - LOG.info("Deleting a User: %s", str(user_name)) + app.logger.info("Deleting a User: %s", str(user_name)) response = requests.delete( str(url)+"/api/v1/user/"+str(user_name), headers={ @@ -338,14 +337,14 @@ def delete_user(url, api_key, user_name): ) status = "True" if response.status_code == 200 else "False" if response.status_code == 200: - LOG.info("User deleted.") + app.logger.info("User deleted.") else: - LOG.error("Deleting User failed!") + app.logger.error("Deleting User failed!") return {"status": status, "body": response.json()} # Add a user from Headscale def add_user(url, api_key, data): - LOG.info("Adding user: %s", str(data)) + app.logger.info("Adding user: %s", str(data)) response = requests.post( str(url)+"/api/v1/user", data=data, @@ -357,9 +356,9 @@ def add_user(url, api_key, data): ) status = "True" if response.status_code == 200 else "False" if response.status_code == 200: - LOG.info("User added.") + app.logger.info("User added.") else: - LOG.error("Adding User failed!") + app.logger.error("Adding User failed!") return {"status": status, "body": response.json()} ################################################################## @@ -368,7 +367,7 @@ def add_user(url, api_key, data): # Get all PreAuth keys associated with a user "user_name" def get_preauth_keys(url, api_key, user_name): - LOG.info("Getting PreAuth Keys in User %s", str(user_name)) + app.logger.info("Getting PreAuth Keys in User %s", str(user_name)) response = requests.get( str(url)+"/api/v1/preauthkey?user="+str(user_name), headers={ @@ -381,7 +380,7 @@ def get_preauth_keys(url, api_key, user_name): # Add a preauth key to the user "user_name" given the booleans "ephemeral" # and "reusable" with the expiration date "date" contained in the JSON payload "data" def add_preauth_key(url, api_key, data): - LOG.info("Adding PreAuth Key: %s", str(data)) + app.logger.info("Adding PreAuth Key: %s", str(data)) response = requests.post( str(url)+"/api/v1/preauthkey", data=data, @@ -393,14 +392,14 @@ def add_preauth_key(url, api_key, data): ) status = "True" if response.status_code == 200 else "False" if response.status_code == 200: - LOG.info("PreAuth Key added.") + app.logger.info("PreAuth Key added.") else: - LOG.error("Adding PreAuth Key failed!") + app.logger.error("Adding PreAuth Key failed!") return {"status": status, "body": response.json()} # Expire a pre-auth key. data is {"user": "string", "key": "string"} def expire_preauth_key(url, api_key, data): - LOG.info("Expiring PreAuth Key...") + app.logger.info("Expiring PreAuth Key...") response = requests.post( str(url)+"/api/v1/preauthkey/expire", data=data, @@ -411,6 +410,6 @@ def expire_preauth_key(url, api_key, data): } ) status = "True" if response.status_code == 200 else "False" - LOG.debug("expire_preauth_key - Return: "+str(response.json())) - LOG.debug("expire_preauth_key - Status: "+str(status)) + app.logger.debug("expire_preauth_key - Return: "+str(response.json())) + app.logger.debug("expire_preauth_key - Status: "+str(status)) return {"status": status, "body": response.json()} diff --git a/helper.py b/helper.py index c5a4765..be660e9 100644 --- a/helper.py +++ b/helper.py @@ -1,10 +1,9 @@ # pylint: disable=wrong-import-order -import os, headscale, requests, logger -from flask import Flask +import os, headscale, requests +from flask import Flask, logging app = Flask(__name__, static_url_path="/static") -LOG = app.logger() def pretty_print_duration(duration, delta_type=""): """ Prints a duration in human-readable formats """ @@ -52,13 +51,13 @@ def key_check(): # Test the API key. If the test fails, return a failure. # AKA, if headscale returns Unauthorized, fail: - LOG.info("Testing API key validity.") + app.logger.info("Testing API key validity.") status = headscale.test_api_key(url, api_key) if status != 200: - LOG.info("Got a non-200 response from Headscale. Test failed (Response: %i)", status) + app.logger.info("Got a non-200 response from Headscale. Test failed (Response: %i)", status) return False else: - LOG.info("Key check passed.") + app.logger.info("Key check passed.") # Check if the key needs to be renewed headscale.renew_api_key(url, api_key) return True @@ -152,20 +151,20 @@ def access_checks(): server_reachable = True else: checks_passed = False - LOG.error("Headscale URL: Response 200: FAILED") + app.logger.error("Headscale URL: Response 200: FAILED") # Check: /data is rwx for 1000:1000: if os.access('/data/', os.R_OK): data_readable = True else: - LOG.error("/data READ: FAILED") + app.logger.error("/data READ: FAILED") checks_passed = False if os.access('/data/', os.W_OK): data_writable = True else: - LOG.error("/data WRITE: FAILED") + app.logger.error("/data WRITE: FAILED") checks_passed = False if os.access('/data/', os.X_OK): data_executable = True else: - LOG.error("/data EXEC: FAILED") + app.logger.error("/data EXEC: FAILED") checks_passed = False # Check: /data/key.txt exists and is rw: @@ -173,29 +172,29 @@ def access_checks(): file_exists = True if os.access('/data/key.txt', os.R_OK): file_readable = True else: - LOG.error("/data/key.txt READ: FAILED") + app.logger.error("/data/key.txt READ: FAILED") checks_passed = False if os.access('/data/key.txt', os.W_OK): file_writable = True else: - LOG.error("/data/key.txt WRITE: FAILED") + app.logger.error("/data/key.txt WRITE: FAILED") checks_passed = False - else: LOG.error("/data/key.txt EXIST: FAILED - NO ERROR") + else: app.logger.error("/data/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 elif os.access('/etc/headscale/config.yml', os.R_OK): config_readable = True else: - LOG.error("/etc/headscale/config.y(a)ml: READ: FAILED") + app.logger.error("/etc/headscale/config.y(a)ml: READ: FAILED") checks_passed = False if checks_passed: - LOG.error("All startup checks passed.") + app.logger.error("All startup checks passed.") return "Pass" message_html = "" # Generate the message: if not server_reachable: - LOG.error("Server is unreachable") + app.logger.error("Server is unreachable") message = """

Your headscale server is either unreachable or not properly configured. Please ensure your configuration is correct (Check for 200 status on @@ -205,7 +204,7 @@ def access_checks(): message_html += format_message("Error", "Headscale unreachable", message) if not config_readable: - LOG.error("Headscale configuration is not readable") + app.logger.error("Headscale configuration is not readable") message = """

/etc/headscale/config.yaml not readable. Please ensure your headscale configuration file resides in /etc/headscale and @@ -215,7 +214,7 @@ def access_checks(): message_html += format_message("Error", "/etc/headscale/config.yaml not readable", message) if not data_writable: - LOG.error("/data folder is not writable") + app.logger.error("/data folder is not writable") message = """

/data is not writable. Please ensure your permissions are correct. /data mount should be writable @@ -225,7 +224,7 @@ def access_checks(): message_html += format_message("Error", "/data not writable", message) if not data_readable: - LOG.error("/data folder is not readable") + app.logger.error("/data folder is not readable") message = """

/data is not readable. Please ensure your permissions are correct. /data mount should be readable @@ -235,7 +234,7 @@ def access_checks(): message_html += format_message("Error", "/data not readable", message) if not data_executable: - LOG.error("/data folder is not readable") + app.logger.error("/data folder is not readable") message = """

/data is not executable. Please ensure your permissions are correct. /data mount should be readable @@ -249,7 +248,7 @@ def access_checks(): # 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: - LOG.error("/data/key.txt is not writable") + app.logger.error("/data/key.txt is not writable") message = """

/data/key.txt is not writable. Please ensure your permissions are correct. /data mount should be writable @@ -259,7 +258,7 @@ def access_checks(): message_html += format_message("Error", "/data/key.txt not writable", message) if not file_readable: - LOG.error("/data/key.txt is not readable") + app.logger.error("/data/key.txt is not readable") message = """

/data/key.txt is not readable. Please ensure your permissions are correct. /data mount should be readable diff --git a/renderer.py b/renderer.py index 5b9b79a..92ce1fd 100644 --- a/renderer.py +++ b/renderer.py @@ -1,14 +1,13 @@ # pylint: disable=line-too-long, wrong-import-order -import headscale, helper, pytz, os, yaml, logger -from flask import Flask, Markup, render_template +import headscale, helper, pytz, os, yaml +from flask import Flask, Markup, render_template, logging from datetime import datetime from dateutil import parser from concurrent.futures import ALL_COMPLETED, wait from flask_executor import Executor app = Flask(__name__, static_url_path="/static") -LOG = app.logger() executor = Executor(app) def render_overview(): @@ -245,7 +244,7 @@ def thread_machine_content(machine, machine_content, idx):

""" for route in pulled_routes["routes"]: - # LOG.warning("Route: ["+str(route['machine']['name'])+"] id: "+str(route['id'])+" / prefix: "+str(route['prefix'])+" enabled?: "+str(route['enabled'])) + app.logger.debug("Route: ["+str(route['machine']['name'])+"] id: "+str(route['id'])+" / prefix: "+str(route['prefix'])+" enabled?: "+str(route['enabled'])) # Check if the route is enabled: route_enabled = "red" route_tooltip = 'enable' @@ -325,7 +324,7 @@ def thread_machine_content(machine, machine_content, idx): expiry_time = str(expiry_local.strftime('%m/%Y'))+" "+str(timezone)+" ("+str(expiry_print)+")" else: expiry_time = str(expiry_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone)+" ("+str(expiry_print)+")" - LOG.error("Machine: "+machine["name"]+" expires: "+str(expiry_local.strftime('%Y'))+" / "+str(expiry_delta.days)) + app.logger.debug("Machine: "+machine["name"]+" expires: "+str(expiry_local.strftime('%Y'))+" / "+str(expiry_delta.days)) expiring_soon = True if int(expiry_delta.days) < 14 and int(expiry_delta.days) > 0 else False # Get the first 10 characters of the PreAuth Key: @@ -367,7 +366,7 @@ def thread_machine_content(machine, machine_content, idx): expiration_badge = Markup(expiration_badge), machine_tags = Markup(tags), ))) - LOG.warning("Finished thread for machine "+machine["givenName"]+" index "+str(idx)) + app.logger.warning("Finished thread for machine "+machine["givenName"]+" index "+str(idx)) # Render the cards for the machines page: def render_machines_cards(): @@ -381,14 +380,14 @@ def render_machines_cards(): iterable = [] machine_content = {} for i in range (0, num_threads): - LOG.error("Appending iterable: "+str(i)) + app.logger.error("Appending iterable: "+str(i)) iterable.append(i) # Flask-Executor Method: - LOG.warning("Starting futures") + app.logger.warning("Starting futures") futures = [executor.submit(thread_machine_content, machines_list["machines"][idx], machine_content, idx) for idx in iterable] # Wait for the executor to finish all jobs: wait(futures, return_when=ALL_COMPLETED) - LOG.warning("Finished futures") + app.logger.warning("Finished futures") # DEBUG: Do in a forloop: # for idx in iterable: thread_machine_content(machines_list["machines"][idx], machine_content, idx) diff --git a/server.py b/server.py index f571aa7..ad8582b 100644 --- a/server.py +++ b/server.py @@ -1,9 +1,9 @@ # pylint: disable=wrong-import-order -import headscale, helper, json, os, pytz, renderer, secrets, requests, logger +import headscale, helper, json, os, pytz, renderer, secrets, requests from functools import wraps from datetime import datetime -from flask import Flask, Markup, redirect, render_template, request, url_for +from flask import Flask, Markup, redirect, render_template, request, url_for, logging from dateutil import parser from flask_executor import Executor from werkzeug.middleware.proxy_fix import ProxyFix @@ -36,7 +36,6 @@ dictConfig({ }) app = Flask(__name__, static_url_path="/static") -LOG = app.logger() executor = Executor(app) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) @@ -49,7 +48,7 @@ if AUTH_TYPE == "oidc": # https://gist.github.com/thomasdarimont/145dc9aa857b831ff2eff221b79d179a/ # https://www.authelia.com/integration/openid-connect/introduction/ # https://github.com/steinarvk/flask_oidc_demo - LOG.info("Loading OIDC libraries and configuring app...") + app.loggerinfo("Loading OIDC libraries and configuring app...") DOMAIN_NAME = os.environ["DOMAIN_NAME"] BASE_PATH = os.environ["SCRIPT_NAME"] if os.environ["SCRIPT_NAME"] != "/" else "" @@ -60,7 +59,7 @@ if AUTH_TYPE == "oidc": # Construct client_secrets.json: response = requests.get(str(OIDC_AUTH_URL)) oidc_info = response.json() - LOG.debug("JSON Dumps for OIDC_INFO: "+json.dumps(oidc_info)) + app.logger.debug("JSON Dumps for OIDC_INFO: "+json.dumps(oidc_info)) client_secrets = """{ "web": { @@ -80,10 +79,10 @@ if AUTH_TYPE == "oidc": with open("/app/instance/secrets.json", "w+") as secrets_json: secrets_json.write(client_secrets) - LOG.debug("Client Secrets: ") + app.logger.debug("Client Secrets: ") with open("/app/instance/secrets.json", "r+") as secrets_json: - LOG.debug("/app/instances/secrets.json:") - LOG.debug(secrets_json.read()) + app.logger.debug("/app/instances/secrets.json:") + app.logger.debug(secrets_json.read()) app.config.update({ 'SECRET_KEY': secrets.token_urlsafe(32), @@ -102,7 +101,7 @@ if AUTH_TYPE == "oidc": elif AUTH_TYPE == "basic": # https://flask-basicauth.readthedocs.io/en/latest/ - LOG.info("Loading basic auth libraries and configuring app...") + app.loggerinfo("Loading basic auth libraries and configuring app...") from flask_basicauth import BasicAuth app.config['BASIC_AUTH_USERNAME'] = os.environ["BASIC_AUTH_USER"].replace('"', '') @@ -289,9 +288,9 @@ def test_key_page(): if status != 200: return "Unauthenticated" renewed = headscale.renew_api_key(url, api_key) - LOG.warning("The below statement will be TRUE if the key has been renewed, ") - LOG.warning("or DOES NOT need renewal. False in all other cases") - LOG.warning("Renewed: "+str(renewed)) + app.loggerwarning("The below statement will be TRUE if the key has been renewed, ") + app.loggerwarning("or DOES NOT need renewal. False in all other cases") + app.loggerwarning("Renewed: "+str(renewed)) # The key works, let's renew it if it needs it. If it does, re-read the api_key from the file: if renewed: api_key = headscale.get_api_key()