import os import uuid import subprocess import requests import threading import json from urllib.parse import urlparse, urlunparse, urljoin from collections import namedtuple import shepherd.plugin # Check for shepherd.new file in edit conf dir. If there, # or if no shepherd.id file can be found, generate a new one. # For now, also attempt to delete /var/lib/zerotier-one/identity.public and identity.secret # Once generated, if it was due to shepherd.new file, delete it. #Start new thread, and push ID and core config to api.shepherd.distreon.net/client/update client_id = None control_url = None api_key = None def _update_job(core_config, plugin_config): payload = {"client_id":client_id, "core_config":core_config,"plugin_config":plugin_config} #json_string = json.dumps(payload) try: # Using the json arg rather than json.dumps ourselves automatically sets the Content-Type # header to application/json, which Flask expects to work correctly r = requests.post(control_url, json=payload, auth=(client_id, api_key)) except requests.exceptions.ConnectionError: raise def generate_new_zerotier_id(): print("Removing old Zerotier id files") try: os.remove("/var/lib/zerotier-one/identity.public") os.remove("/var/lib/zerotier-one/identity.secret") except: pass print("Restarting Zerotier systemd service to regenerate ID") subprocess.run(["systemctl", "restart", "zerotier-one.service"]) def generate_new_id(root_dir): global client_id with open(os.path.join(root_dir, "shepherd.id"), 'w+') as f: new_id = uuid.uuid1() client_id = str(new_id) f.write(client_id) generate_new_zerotier_id() def init_control(core_config, plugin_config): global client_id global control_url global api_key # On init, need to be able to quickly return the cached shepherd control layer if necessary. # Create the /update endpoint structure root_dir = os.path.expanduser(core_config["root_dir"]) editconf_dir = os.path.dirname(os.path.expanduser(core_config["conf_edit_path"])) #Some weirdness with URL parsing means that by default most urls (like www.google.com) # get treated as relative # https://stackoverflow.com/questions/53816559/python-3-netloc-value-in-urllib-parse-is-empty-if-url-doesnt-have control_url = core_config["control_server"] if "//" not in control_url: control_url = "//"+control_url control_url = urlunparse(urlparse(control_url)._replace(scheme="https")) control_url = urljoin(control_url, "/client/update") print(F"Control url is: {control_url}") api_key = core_config["api_key"] if os.path.isfile(os.path.join(editconf_dir, "shepherd.new")): generate_new_id(root_dir) os.remove(os.path.join(editconf_dir, "shepherd.new")) print(F"Config hostname: {core_config['hostname']}") if not (core_config["hostname"] == ""): print("Attempting to change hostname") subprocess.run(["raspi-config", "nonint", "do_hostname", core_config["hostname"]]) elif not os.path.isfile(os.path.join(root_dir, "shepherd.id")): generate_new_id(root_dir) else: with open(os.path.join(root_dir, "shepherd.id"), 'r') as id_file: client_id = id_file.readline().strip() print(F"Client ID is: {client_id}") control_thread = threading.Thread(target=_update_job, args=(core_config,plugin_config)) control_thread.start() def _post_logs_job(): logs = shepherd.plugin.plugin_functions["scout"].get_logs() measurements = shepherd.plugin.plugin_functions["scout"].get_measurements() payload = {"client_id":client_id, "logs":logs, "measurements":measurements} try: r = requests.post(control_url, json=payload, auth=(client_id, api_key)) except requests.exceptions.ConnectionError: pass def post_logs(): post_logs_thread = threading.Thread(target=_post_logs_job, args=()) post_logs_thread.start()