1 line
26 KiB
JSON
1 line
26 KiB
JSON
{"repo": "anjomro/headscale-webui", "files": {"renderer.py": {"7ab508f071fe9040ea9133020601197ffb8e4d5d": {"t": 1785595757.0604842, "n": 25065, "h": "9b4a89290f8434d9", "c": "import headscale, helper, json, logging, sys, pytz, os, time, yaml\nfrom flask import Markup, render_template, Flask\nfrom datetime import datetime, timedelta, date\nfrom dateutil import parser\n\n# Threading to speed things up\nfrom concurrent.futures import wait, ALL_COMPLETED\nfrom flask_executor import Executor\n\napp = Flask(__name__)\nexecutor = Executor(app)\n\nlog = logging.getLogger('server.renderer')\n\ndef render_overview():\n url = headscale.get_url()\n api_key = headscale.get_api_key()\n\n timezone = pytz.timezone(os.environ[\"TZ\"] if os.environ[\"TZ\"] else \"UTC\")\n local_time = timezone.localize(datetime.now())\n \n # Overview page will just read static information from the config file and display it\n # Open the config.yaml and parse it.\n config_file = open(\"/etc/headscale/config.yaml\", \"r\")\n config_yaml = yaml.safe_load(config_file)\n\n # Get and display the following information:\n # Overview of the server's machines, users, preauth keys, API key expiration, server version\n \n # Get all machines:\n machines_count = 0\n machines = headscale.get_machines(url, api_key)\n for machine in machines[\"machines\"]:\n machines_count += 1\n\n # Get all routes:\n routes = headscale.get_routes(url,api_key)\n total_routes = len(routes[\"routes\"])\n enabled_routes = 0\n for route in routes[\"routes\"]:\n if route[\"enabled\"] and route['advertised']: \n enabled_routes += 1\n\n # Get a count of all enabled exit routes\n exits_count = 0\n exits_enabled_count = 0\n for route in routes[\"routes\"]:\n if route['advertised']:\n if route[\"prefix\"] == \"0.0.0.0/0\" or route[\"prefix\"] == \"::/0\":\n exits_count +=1\n if route[\"enabled\"]:\n exits_enabled_count += 1\n\n # Get User and PreAuth Key counts\n user_count = 0\n usable_keys_count = 0\n users = headscale.get_users(url, api_key)\n for user in users[\"users\"]:\n user_count +=1\n preauth_keys = headscale.get_preauth_keys(url, api_key, user[\"name\"])\n for key in preauth_keys[\"preAuthKeys\"]:\n expiration_parse = parser.parse(key[\"expiration\"])\n key_expired = True if expiration_parse < local_time else False\n if key[\"reusable\"] and not key_expired: usable_keys_count += 1\n if not key[\"reusable\"] and not key[\"used\"] and not key_expired: usable_keys_count += 1\n\n overview_content = \"\"\"\n <div class=\"col s12 m6\">\n <div class=\"card hoverable\">\n <div class=\"card-content\">\n <span class=\"card-title\">Stats</span>\n <p>\n <table>\n <tr><td> Machines Added </td><td> \"\"\"+ str(machines_count) +\"\"\" </td></tr>\n <tr><td> Users </td><td> \"\"\"+ str(user_count) +\"\"\" </td></tr>\n <tr><td> Usable PreAuth Keys </td><td> \"\"\"+ str(usable_keys_count) +\"\"\" </td></tr>\n <tr><td> Enabled/Total Routes</td><td> \"\"\"+ str(enabled_routes) +\"\"\"/\"\"\"+str(total_routes)+\"\"\" </td></tr>\n <tr><td> Enabled/Total Exits</td><td> \"\"\"+ str(exits_enabled_count) +\"\"\"/\"\"\"+str(exits_count)+\"\"\" </td></tr>\n </table>\n </p>\n </div>\n </div>\n </div>\n \"\"\"\n # Overview of general configs from the YAML\n general_content = \"\"\"\n <div class=\"col s12 m6\">\n <div class=\"card hoverable\">\n <div class=\"card-content\">\n <span class=\"card-title\">General</span>\n <p>\n <table>\n <tr><td> IP Prefixes </td><td> \"\"\"+str(config_yaml[\"ip_prefixes\"]) +\"\"\" </td></tr>\n <tr><td> Server URL </td><td> \"\"\"+str(config_yaml[\"server_url\"]) +\"\"\" </td></tr>\n <tr><td> Updates Disabled? </td><td> \"\"\"+str(config_yaml[\"disable_check_updates\"]) +\"\"\" </td></tr>\n <tr><td> Ephemeral Node Timeout </td><td> \"\"\"+str(config_yaml[\"ephemeral_node_inactivity_timeout\"]) +\"\"\" </td></tr>\n <tr><td> Node Update Check Interval </td><td> \"\"\"+str(config_yaml[\"node_update_check_interval\"]) +\"\"\" </td></tr>\n </table>\n </p>\n </div>\n </div>\n </div>\n \"\"\"\n\n # Whether OIDC is configured\n if config_yaml[\"oidc\"]:\n oidc_content = \"\"\"\n <div class=\"col s12 m6\">\n <div class=\"card hoverable\">\n <div class=\"card-content\">\n <span class=\"card-title\">OIDC</span>\n <p>\n <table> \n <tr><td> Issuer </td><td> \"\"\"+str(config_yaml[\"oidc\"][\"issuer\"]) +\"\"\" </td></tr>\n <tr><td> Client ID </td><td> \"\"\"+str(config_yaml[\"oidc\"][\"client_id\"]) +\"\"\" </td></tr>\n <tr><td> Scope </td><td> \"\"\"+str(config_yaml[\"oidc\"][\"scope\"]) +\"\"\" </td></tr>\n <tr><td> Allowed Domains </td><td> \"\"\"+str(config_yaml[\"oidc\"][\"allowed_domains\"]) +\"\"\" </td></tr>\n <tr><td> Strip Email Domain </td><td> \"\"\"+str(config_yaml[\"oidc\"][\"strip_email_domain\"])+\"\"\" </td></tr>\n </table>\n </p>\n </div>\n </div>\n </div>\n \"\"\"\n\n if config_yaml[\"derp\"][\"server\"]:\n derp_content = \"\"\"\n <div class=\"col s12 m6\">\n <div class=\"card hoverable\">\n <div class=\"card-content\">\n <span class=\"card-title\">Built-in DERP</span>\n <p>\n <table>\n <tr><td> Enabled </td><td> \"\"\"+str(config_yaml[\"derp\"][\"server\"][\"enabled\"]) +\"\"\" </td></tr>\n <tr><td> Region ID </td><td> \"\"\"+str(config_yaml[\"derp\"][\"server\"][\"region_id\"]) +\"\"\" </td></tr>\n <tr><td> Region Code </td><td> \"\"\"+str(config_yaml[\"derp\"][\"server\"][\"region_code\"]) +\"\"\" </td></tr>\n <tr><td> Region Name </td><td> \"\"\"+str(config_yaml[\"derp\"][\"server\"][\"region_name\"]) +\"\"\" </td></tr>\n <tr><td> STUN Address </td><td> \"\"\"+str(config_yaml[\"derp\"][\"server\"][\"stun_listen_addr\"]) +\"\"\" </td></tr>\n </table>\n </p>\n </div>\n </div>\n </div>\n \"\"\"\n\n # Whether there are custom DERP servers\n # If there are custom DERP servers, get the file location from the config file. Assume mapping is the same.\n # Whether the built-in DERP server is enabled \n # The IP prefixes\n # The DNS config\n if config_yaml[\"dns_config\"]:\n dns_content = \"\"\"\n <div class=\"col s12 m6\">\n <div class=\"card hoverable\">\n <div class=\"card-content\">\n <span class=\"card-title\">DNS</span>\n <p>\n <table>\n <tr><td> Nameservers </td><td> \"\"\"+str(config_yaml[\"dns_config\"][\"nameservers\"])+\"\"\" </td></tr>\n <tr><td> MagicDNS </td><td> \"\"\"+str(config_yaml[\"dns_config\"][\"magic_dns\"]) +\"\"\" </td></tr>\n <tr><td> Domains </td><td> \"\"\"+str(config_yaml[\"dns_config\"][\"domains\"]) +\"\"\" </td></tr>\n <tr><td> Base Domain </td><td> \"\"\"+str(config_yaml[\"dns_config\"][\"base_domain\"])+\"\"\" </td></tr>\n <tr><td> </td><td><br></td></tr>\n </table>\n </p>\n </div>\n </div>\n </div>\n \"\"\"\n if config_yaml[\"derp\"][\"paths\"]: pass\n # # open the path:\n # derp_file = \n # config_file = open(\"/etc/headscale/config.yaml\", \"r\")\n # config_yaml = yaml.safe_load(config_file)\n # The ACME config, if not empty\n # Whether updates are running\n # Whether metrics are enabled (and their listen addr)\n # The log level\n # What kind of Database is being used to drive headscale\n\n content = \"<br><div class='row'>\" + overview_content + general_content + derp_content + oidc_content + dns_content + \"</div>\"\n return Markup(content)\n\ndef thread_machine_content(machine, machine_content, idx):\n # machine = passed in machine information\n # content = place to write the content\n\n url = headscale.get_url()\n api_key = headscale.get_api_key()\n\n # Set the current timezone and local time\n timezone = pytz.timezone(os.environ[\"TZ\"] if os.environ[\"TZ\"] else \"UTC\")\n local_time = timezone.localize(datetime.now())\n\n # Get the machines routes\n pulled_routes = headscale.get_machine_routes(url, api_key, machine[\"id\"])\n routes = \"\"\n\n# New format to parse:\n# New JSON endpoint requires a machine_id and a route_id to toggle.\n# Pass the following info:\n# 1. Machine ID\n# 2. Route ID (NEW REQ)\n# 3. Route State (NEW REQ) - This would be the JSON key of pulled_routes[\"routes\"][index][enabled]\n# {\n# \"routes\": [\n# { <INDEX>\n# \"id\": \"1\",\n# \"prefix\": \"0.0.0.0/0\",\n# \"advertised\": true,\n# \"enabled\": true,\n# },\n\n # Test if the machine is an exit node:\n exit_node = False\n # If the LENGTH of \"routes\" is NULL/0, there are no routes, enabled or disabled:\n if len(pulled_routes[\"routes\"]) > 0:\n advertised_and_enabled = False\n advertised_route = False\n # First, check if there are any routes that are both enabled and advertised\n for route in pulled_routes[\"routes\"]:\n if route [\"advertised\"] and route[\"enabled\"]: \n advertised_and_enabled = True\n if route[\"advertised\"]:\n advertised_route = True\n if advertised_and_enabled or advertised_route:\n routes = \"\"\"\n <li class=\"collection-item avatar\">\n <i class=\"material-icons circle\">directions</i>\n <span class=\"title\">Routes</span>\n <p><div>\n \"\"\"\n for route in pulled_routes[\"routes\"]:\n # log.info(\"Route: [\"+str(route['machine']['name'])+\"] id: \"+str(route['id'])+\" / prefix: \"+str(route['prefix'])+\" enabled?: \"+str(route['enabled']))\n # Check if the route is enabled:\n route_enabled = \"red\"\n route_tooltip = 'enable'\n if route[\"enabled\"]:\n route_enabled = \"green\"\n route_tooltip = 'disable'\n if route[\"prefix\"] == \"0.0.0.0/0\" or route[\"prefix\"] == \"::/0\" and str(route[\"enabled\"]) == \"True\":\n exit_node = True\n routes = routes+\"\"\"\n <p \n class='waves-effect waves-light btn-small \"\"\"+route_enabled+\"\"\" lighten-2 tooltipped'\n data-position='top' data-tooltip='Click to \"\"\"+route_tooltip+\"\"\"'\n id='\"\"\"+route['id']+\"\"\"'\n onclick=\"toggle_route(\"\"\"+route['id']+\"\"\", '\"\"\"+str(route['enabled'])+\"\"\"')\">\n \"\"\"+route['prefix']+\"\"\"\n </p>\n \"\"\"\n routes = routes+\"</div></p></li>\"\n# This entire thing will probably need to change after the new API endpoint change.\n# Old code for v1.17.0\n# # Test if the machine is an exit node:\n# exit_node = False\n# # If there are ANY advertised routes, print them:\n# if len(pulled_routes[\"routes\"][\"advertisedRoutes\"]) > 0:\n# routes = \"\"\"\n# <li class=\"collection-item avatar\">\n# <i class=\"material-icons circle\">directions</i>\n# <span class=\"title\">Routes</span>\n# <p><div>\n# \"\"\"\n# for advertised_route in pulled_routes[\"routes\"][\"advertisedRoutes\"]:\n# # Check if the route has been enabled. Red for False, Green for True \n# route_enabled = \"red\"\n# route_tooltip = 'enable'\n# for enabled_route in pulled_routes[\"routes\"][\"enabledRoutes\"]:\n# if advertised_route == enabled_route: \n# route_enabled = \"green\"\n# route_tooltip = 'disable'\n# if (advertised_route == \"0.0.0.0/0\" or advertised_route == \"::/0\") and route_enabled == \"green\": \n# exit_node = True\n# routes = routes+\"\"\"\n# <p \n# class='waves-effect waves-light btn-small \"\"\"+route_enabled+\"\"\" lighten-2 tooltipped'\n# data-position='top' data-tooltip='Click to \"\"\"+route_tooltip+\"\"\"'\n# id='\"\"\"+machine['id']+\"\"\"-\"\"\"+advertised_route+\"\"\"'\n# onclick=\"toggle_route(\"\"\"+machine['id']+\"\"\", '\"\"\"+advertised_route+\"\"\"')\">\n# \"\"\"+advertised_route+\"\"\"\n# </p>\n# \"\"\"\n# routes = routes+\"</div></p></li>\"\n\n # Get machine tags\n tag_array = \"\"\n for tag in machine[\"forcedTags\"]: tag_array = tag_array+\"{tag: '\"+tag[4:]+\"'}, \"\n tags = \"\"\"\n <li class=\"collection-item avatar\">\n <i class=\"material-icons circle tooltipped\" data-position=\"right\" data-tooltip=\"Spaces will be replaced with a dash (-) upon page refresh\">label</i>\n <span class=\"title\">Tags</span>\n <p><div style='margin: 0px' class='chips' id='\"\"\"+machine[\"id\"]+\"\"\"-tags'></div></p>\n </li>\n <script>\n window.addEventListener('load', \n function() { \n var instances = M.Chips.init ( \n document.getElementById('\"\"\"+machine['id']+\"\"\"-tags'), ({\n data:[\"\"\"+tag_array+\"\"\"], \n onChipDelete() { delete_chip(\"\"\"+machine[\"id\"]+\"\"\", this.chipsData) }, \n onChipAdd() { add_chip(\"\"\"+machine[\"id\"]+\"\"\", this.chipsData) }\n }) \n );\n }, false\n ); \n </script>\n \"\"\"\n\n # Get the machine IP's\n machine_ips = \"<ul>\"\n for ip in machine[\"ipAddresses\"]:\n machine_ips = machine_ips+\"<li>\"+ip+\"</li>\"\n machine_ips = machine_ips+\"</ul>\"\n\n # Format the dates for easy readability\n last_seen_parse = parser.parse(machine[\"lastSeen\"])\n last_seen_local = last_seen_parse.astimezone(timezone)\n last_seen_delta = local_time - last_seen_local\n last_seen_print = helper.pretty_print_duration(last_seen_delta)\n last_seen_time = str(last_seen_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+\" \"+str(timezone)+\" (\"+str(last_seen_print)+\")\"\n \n last_update_parse = local_time if machine[\"lastSuccessfulUpdate\"] is None else parser.parse(machine[\"lastSuccessfulUpdate\"])\n last_update_local = last_update_parse.astimezone(timezone)\n last_update_delta = local_time - last_update_local\n last_update_print = helper.pretty_print_duration(last_update_delta)\n last_update_time = str(last_update_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+\" \"+str(timezone)+\" (\"+str(last_update_print)+\")\"\n\n created_parse = parser.parse(machine[\"createdAt\"])\n created_local = created_parse.astimezone(timezone)\n created_delta = local_time - created_local\n created_print = helper.pretty_print_duration(created_delta)\n created_time = str(created_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+\" \"+str(timezone)+\" (\"+str(created_print)+\")\"\n\n # Get the first 10 characters of the PreAuth Key:\n if machine[\"preAuthKey\"]:\n preauth_key = str(machine[\"preAuthKey\"][\"key\"])[0:10]\n else: preauth_key = \"None\"\n\n # Set the status badge color:\n text_color = helper.text_color_duration(last_seen_delta)\n # Set the user badge color:\n user_color = helper.get_color(int(machine[\"user\"][\"id\"]))\n\n # Generate the various badges:\n status_badge = \"<i class='material-icons left tooltipped \"+text_color+\"' data-position='top' data-tooltip='Last Seen: \"+last_seen_print+\"' id='\"+machine[\"id\"]+\"-status'>fiber_manual_record</i>\"\n user_badge = \"<span class='badge ipinfo \" + user_color + \" white-text hide-on-small-only' id='\"+machine[\"id\"]+\"-ns-badge'>\"+machine[\"user\"][\"name\"]+\"</span>\"\n exit_node_badge = \"\" if not exit_node else \"<span class='badge grey white-text text-lighten-4 tooltipped' data-position='left' data-tooltip='This machine has an enabled exit route.'>Exit Node</span>\"\n\n\n machine_content[idx] = (str(render_template(\n 'machines_card.html', \n given_name = machine[\"givenName\"],\n machine_id = machine[\"id\"],\n hostname = machine[\"name\"],\n ns_name = machine[\"user\"][\"name\"],\n ns_id = machine[\"user\"][\"id\"],\n ns_created = machine[\"user\"][\"createdAt\"],\n last_seen = str(last_seen_print),\n last_update = str(last_update_print),\n machine_ips = Markup(machine_ips),\n advertised_routes = Markup(routes),\n exit_node_badge = Markup(exit_node_badge),\n status_badge = Markup(status_badge),\n user_badge = Markup(user_badge),\n last_update_time = str(last_update_time),\n last_seen_time = str(last_seen_time),\n created_time = str(created_time),\n preauth_key = str(preauth_key),\n machine_tags = Markup(tags),\n )))\n log.info(\"Finished thread for machine \"+machine[\"givenName\"]+\" index \"+str(idx))\n\n# Render the cards for the machines page:\ndef render_machines_cards():\n url = headscale.get_url()\n api_key = headscale.get_api_key()\n machines_list = headscale.get_machines(url, api_key)\n\n #########################################\n # Thread this entire thing. \n numThreads = len(machines_list[\"machines\"])\n iterable = []\n machine_content = {}\n for i in range (0, numThreads):\n log.info(\"Appending iterable: \"+str(i))\n iterable.append(i)\n # Flask-Executor Method:\n log.info(\"Starting futures\")\n futures = [executor.submit(thread_machine_content, machines_list[\"machines\"][idx], machine_content, idx) for idx in iterable]\n # Wait for the executor to finish all jobs:\n wait(futures, return_when=ALL_COMPLETED)\n log.info(\"Finished futures\")\n\n # DEBUG: Do in a forloop:\n # for idx in iterable: thread_machine_content(machines_list[\"machines\"][idx], machine_content, idx)\n\n # Sort the content by machine_id:\n sorted_machines = {key: val for key, val in sorted(machine_content.items(), key = lambda ele: ele[0])}\n\n content = \"<div class='u-flex u-justify-space-evenly u-flex-wrap u-gap-1'>\"\n # Print the content\n\n for index in range(0, numThreads):\n content = content+str(sorted_machines[index])\n # content = content+str(sorted_machines[index])\n\n content = content+\"</div>\"\n\n return Markup(content)\n\n# Render the cards for the Users page:\ndef render_users_cards():\n url = headscale.get_url()\n api_key = headscale.get_api_key()\n user_list = headscale.get_users(url, api_key)\n\n # Set the current timezone and local time\n timezone = pytz.timezone(os.environ[\"TZ\"] if os.environ[\"TZ\"] else \"UTC\")\n local_time = timezone.localize(datetime.now())\n\n content = \"<div class='u-flex u-justify-space-evenly u-flex-wrap u-gap-1'>\"\n for user in user_list[\"users\"]:\n # Get all preAuth Keys in the user, only display if one exists:\n preauth_keys_collection = build_preauth_key_table(user[\"name\"])\n\n # Set the user badge color:\n user_color = helper.get_color(int(user[\"id\"]), \"text\")\n\n # Generate the various badges:\n status_badge = \"<i class='material-icons left \"+user_color+\"' id='\"+user[\"id\"]+\"-status'>fiber_manual_record</i>\"\n\n content = content + render_template(\n 'users_card.html', \n status_badge = Markup(status_badge),\n user_name = user[\"name\"],\n user_id = user[\"id\"],\n preauth_keys_collection = Markup(preauth_keys_collection)\n ) \n content = content+\"</div>\"\n return Markup(content)\n\n# Builds the preauth key table for the User page\ndef build_preauth_key_table(user_name):\n url = headscale.get_url()\n api_key = headscale.get_api_key()\n\n preauth_keys = headscale.get_preauth_keys(url, api_key, user_name)\n preauth_keys_collection = \"\"\"<li class=\"collection-item avatar\">\n <span\n class='badge grey lighten-2 btn-small' \n onclick='toggle_expired()'\n >Toggle Expired</span>\n <span \n href=\"#card_modal\" \n class='badge grey lighten-2 btn-small modal-trigger' \n onclick=\"load_modal_add_preauth_key('\"\"\"+user_name+\"\"\"')\"\n >Add PreAuth Key</span>\n <i class=\"material-icons circle\">vpn_key</i>\n <span class=\"title\">PreAuth Keys</span>\n \"\"\"\n if len(preauth_keys[\"preAuthKeys\"]) == 0: preauth_keys_collection += \"<p>No keys defined for this user</p>\"\n if len(preauth_keys[\"preAuthKeys\"]) > 0:\n preauth_keys_collection += \"\"\"\n <table class=\"responsive-table striped\" id='\"\"\"+user_name+\"\"\"-preauthkey-table'>\n <thead>\n <tr>\n <td>ID</td>\n <td>Key</td>\n <td><center>Reusable</center></td>\n <td><center>Used</center></td>\n <td><center>Ephemeral</center></td>\n <td><center>Usable</center></td>\n <td><center>Actions</center></td>\n </tr>\n </thead>\n \"\"\"\n for key in preauth_keys[\"preAuthKeys\"]:\n # Get the key expiration date and compare it to now to check if it's expired:\n # Set the current timezone and local time\n timezone = pytz.timezone(os.environ[\"TZ\"] if os.environ[\"TZ\"] else \"UTC\")\n local_time = timezone.localize(datetime.now())\n expiration_parse = parser.parse(key[\"expiration\"])\n key_expired = True if expiration_parse < local_time else False\n expiration_time = str(expiration_parse.strftime('%A %m/%d/%Y, %H:%M:%S'))+\" \"+str(timezone)\n\n # Class for the javascript function to look for to toggle the hide function\n hide_expired = \"expired-row\" if key_expired else \"\"\n\n key_usable = False\n if key[\"reusable\"] and not key_expired: key_usable = True\n if not key[\"reusable\"] and not key[\"used\"] and not key_expired: key_usable = True\n\n tooltip_expired = \"Expiration: \"+expiration_time\n\n btn_reusable = \"<i class='pulse material-icons tiny blue-text text-darken-1'>fiber_manual_record</i>\" if key[\"reusable\"] else \"\"\n btn_ephemeral = \"<i class='pulse material-icons tiny red-text text-darken-1'>fiber_manual_record</i>\" if key[\"ephemeral\"] else \"\"\n btn_used = \"<i class='pulse material-icons tiny yellow-text text-darken-1'>fiber_manual_record</i>\" if key[\"used\"] else \"\"\n btn_usable = \"<i class='pulse material-icons tiny green-text text-darken-1'>fiber_manual_record</i>\" if key_usable else \"\"\n\n # Other buttons:\n btn_delete = \"<span href='#card_modal' data-tooltip='Expire this PreAuth Key' class='btn-small modal-trigger badge tooltipped white-text red' onclick='load_modal_expire_preauth_key(\\\"\"+user_name+\"\\\", \\\"\"+str(key[\"key\"])+\"\\\")'>Expire</span>\" if not key_expired else \"\"\n tooltip_data = \"Expiration: \"+expiration_time\n\n # TR ID will look like \"1-albert-tr\"\n preauth_keys_collection = preauth_keys_collection+\"\"\"\n <tr id='\"\"\"+key[\"id\"]+\"\"\"-\"\"\"+user_name+\"\"\"-tr' class='\"\"\"+hide_expired+\"\"\"'>\n <td>\"\"\"+str(key[\"id\"])+\"\"\"</td>\n <td class='tooltipped' data-tooltip='\"\"\"+tooltip_data+\"\"\"'>\"\"\"+str(key[\"key\"])+\"\"\"</td>\n <td><center>\"\"\"+btn_reusable+\"\"\"</center></td>\n <td><center>\"\"\"+btn_used+\"\"\"</center></td>\n <td><center>\"\"\"+btn_ephemeral+\"\"\"</center></td>\n <td><center>\"\"\"+btn_usable+\"\"\"</center></td>\n <td><center>\"\"\"+btn_delete+\"\"\"</center></td>\n </tr>\n \"\"\"\n\n preauth_keys_collection = preauth_keys_collection+\"\"\"</table>\n </li>\n \"\"\"\n return preauth_keys_collection"}}}} |