def register_job(self, job): """register job by `job.name` and construct their instances""" if job.name in self.jobs: log.warning(f"overwriting job (during register) with name: {job.name}") self.jobs[job.name] = job() log.info(f"registered job: {job.name}")
def start(self): if self.started: log.warning(f"trying to start already started: {self.cmd}") return False self.cmd = self.cmd if not self.shell else " ".join(self.cmd) self.proc = subprocess.Popen(self.cmd, shell=self.shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) self.started = time.time() log.debug(f"started {self.cmd} ({'non-' if not self.block else ''}blocking)") # return if non-blocking, else wait + pass-on output if not self.block: return True log.debug("blocking call, waiting now...") while self.proc.returncode is None: try: self.proc.wait(timeout=10) except subprocess.TimeoutExpired: log.debug("blocking-cmd timeout (10secs)") self._handle_stdout_stream(poll_block_ms=1000) log.debug("blocking call finished") return True
def _run(self, cfg, board, kwargs): self.interval = 900 # only set trusted domains, if nextcloud is installed if not self.nc.is_installed: log.debug("cannot set trusted_domains - uninitialized nextcloud") self.interval = 15 return False try: trusted_domains = self.nc.get_config("trusted_domains") except NextcloudError: log.warning("cannot get trusted_domains from nextcloud") self.interval = 15 return False default_entry = trusted_domains[0] entries = [default_entry] + self.static_entries[:] if cfg["config"].get("domain"): entries.append(cfg["config"]["domain"]) if cfg["config"].get("proxy_active") and cfg["config"].get("proxy_domain"): entries.append(cfg["config"]["proxy_domain"]) if any(entry not in trusted_domains for entry in entries): try: self.nc.set_config("trusted_domains", entries) except NextcloudError: log.warning("failed to write all trusted_domains") self.interval = 15
def delete_cert(self, domain): certs = self.get_local_certs() for cert in certs: if domain in cert["domains"]: cmd = self.delete_cmd.format(config_dir=self.config_dir, domain=domain) cr = CommandRunner(cmd, block=True) return cr.returncode == 0 log.warning(f"{domain} not in certbot certificates for deletion found") return False
def rsync_dir(self, key, src_dir, tar_dir, block_main=False): """ rsync is used to copy files for export & import. * a --dry-run is parsed 1st to determine the number of files (paths) to be created on the target side * -av is used to get one line of output for each "created" file (path), which determines the percentage """ cmd = self.rsync_stats_cmd.format(src=src_dir, tar=tar_dir) cr = CommandRunner(cmd, block=True) stats = {"key": key} parsing = { "Number of files:": "all", "Number of created files:": "create", "Number of deleted files:": "delete", "Number of regular files transferred:": "transfer", "Total file size:": "size" } for line in cr.output: for needle, tag in parsing.items(): if needle in line: try: num_tok = line.split(":")[1].strip().split( " ")[0].strip() stats[tag] = int(num_tok.replace(",", "")) except ValueError: log.warning(f"failed parsing: {tag} - line: {line}") break # all what we need is parsed if len(stats) >= 6: break self.rsync_stats = stats def parse(line, data_dct): data_dct.setdefault("line", 0) data_dct.setdefault("ratio", 0) data_dct["num_files"] = stats data_dct["line"] += 1 cnt = stats.get("create", 0) if cnt == 0: data_dct["ratio"] = 100 else: data_dct["ratio"] = int( round(min(1, max(0, data_dct["line"] / cnt)), 2) * 100) cmd = self.rsync_base_cmd.format(src=src_dir, tar=tar_dir) self.rsync_proc = CommandRunner(cmd, cb_parse=parse, block=block_main) return False
def retries(cls, count, *vargs, **kwargs): # cannot retry, if non-blocking if not kwargs.get("block"): log.warning("trying CommandRunner-retries w/o block: no retries are done!") return cls(*vargs, **kwargs) # if 'block' is set, retry until returncode == 0 for idx in range(count): res = cls(*vargs, **kwargs) if res.returncode == 0: return res log.debug(f"retry #{idx+1}, returncode: {res.returncode}") res.log_output() # failed retries log.error("retried hard for {count} times, failed anyways") return res
def _run(self, cfg, board, kwargs): token = cfg["config"]["desec_token"] dns_mode = cfg["config"]["dns_mode"] domain = cfg["config"]["domain"] if dns_mode not in ["desec_done", "off"]: if not services.is_active("ddclient"): services.restart("ddclient") services.enable("ddclient") return if dns_mode == "off": if services.is_active("ddclient"): services.stop("ddclient") services.disable("ddclient") return if dns_mode == "desec_done": if services.is_active("ddclient"): services.stop("ddclient") services.disable("ddclient") dns = DNSManager() ipv4 = dns.get_ipv4() ipv6 = dns.get_ipv6() headers = {"Authorization": f"Token {token}"} params = {"hostname": domain} if ipv4: params["myipv4"] = ipv4 if ipv6: params["myipv6"] = ipv6 res = requests.get("https://update.dedyn.io", params=params, headers=headers) if res.ok: log.info(f"updated deSEC IPv4 ({ipv4}) and IPv6 ({ipv6}) address for '{domain}'") else: log.warning(f"failed updating IPs ({ipv4} & {ipv6}) for domain: '{domain}'") log.debug(f"result: {res.text} status_code: {res.status_code} url: {res.url}")
def test_resolve4(): ip_type = request.path.split("/")[-1] domain = cfg["config"]["domain"] resolve_ip = None ext_ip = None if not domain: return error("no domain is set") dns = DNSManager() dns.clear_dns_caches() if ip_type == "ipv4": resolve_ip = dns.resolve_ipv4(domain) ext_ip = dns.get_ipv4() else: resolve_ip = dns.resolve_ipv6(domain) ext_ip = dns.get_ipv6() log.info( f"resolving '{domain}' to IP: {resolve_ip}, external IP: {ext_ip}") data = {"ip": ext_ip, "resolve_ip": resolve_ip, "domain": domain} # if not both "resolve" and "getip" are successful, we have failed if resolve_ip is None or ext_ip is None: log.error(f"failed resolving and/or getting external {ip_type}") return error("Resolve test: Not OK", data=data) # resolving to wrong ip if resolve_ip != ext_ip: log.warning(f"Resolved {ip_type} does not match external {ip_type}") log.warning("This might indicate a bad DynDNS configuration") return error("Resolve test: Not OK", data=data) # all good! return success("Resolve test: OK", data=data)
def handle_config(): if request.method == "GET": data = dict(cfg["config"]) try: data["conf"] = Path(DDCLIENT_CONFIG_PATH).read_text("utf-8") except FileNotFoundError: data["conf"] = "" return success(data=data) # save dyndns related values to configuration elif request.method == "POST": run_jobs = [] for key in request.form: val = request.form.get(key) # special config-value 'conf' represents ddclient-config-contents if key == "conf": old_conf = Path(DDCLIENT_CONFIG_PATH).read_text("utf-8") if old_conf != val: log.info("writing ddclient config and restarting service") Path(DDCLIENT_CONFIG_PATH).write_text(val, "utf-8") elif len(val.strip()) == 0: log.info("writing empty ddclient config") Path(DDCLIENT_CONFIG_PATH).write_text(val, "utf-8") services.stop("ddclient") services.disable("ddclient") if len(val.strip()) > 0: services.enable("ddclient") services.restart("ddclient") run_jobs.append("DynDNSUpdate") elif key in AVAIL_CONFIGS and val is not None: # only allow valid DYNDNS_MODES if key == "dns_mode" and val not in DYNDNS_MODES: log.warning( f"key: 'dns_mode' has invalid value: {val} - skipping") continue # start DynDNS update on "desec_done" elif key == "dns_mode" and val == "desec_done": run_jobs.append("DynDNSUpdate") # start TrustedDomains update on new domain elif "domain" in key: run_jobs.append("TrustedDomains") # deactivate proxy on request elif key == "proxy_active" and val.lower() == "false": proxy_tunnel = ProxyTunnel() proxy_tunnel.stop() # skip if 'val' is empty elif val is None: log.debug(f"skipping key: '{key}' -> no value provided") continue # convert to bool, ugly? if val.lower() in ["true", "false"]: val = val.lower() == "true" # put key-value into cfg and save (yes, saving each value) cfg["config"][key] = val log.debug(f"saving key: '{key}' with value: '{val}'") cfg.save() # run jobs collected during configuration update if len(run_jobs) > 0: for job in run_jobs: job_queue.put(job) return success("DynDNS configuration saved")
def _run(self, cfg=None, board=None, kwargs=None): # factory-reset is executed by systemd log.warning("Starting factory-reset operation") shield.set_led_state("factory-reset") services.start("nextbox-factory-reset")
def _run(self, cfg, board, kwargs): board.set("tls", { "what": "enable", "state": "running", }) certs = Certificates() domain = cfg.get("config", {}).get("domain") email = cfg.get("config", {}).get("email") if not domain or not email: log.error(f"cannot enable tls, domain: '{domain}' email: '{email}'") board.set("tls", { "what": "domain-or-email", "state": "fail", }) return False my_cert = certs.get_cert(domain) if not my_cert: log.warning(f"could not get local certificate for: {domain}, acquiring...") if not certs.acquire_cert(domain, email): msg = f"Failed to acquire {domain} with {email}" log.error(msg) board.set("tls", { "what": "acquire", "state": "fail", }) return False log.info(f"acquired certificate for: {domain} with {email}") my_cert = certs.get_cert(domain) certs.write_apache_ssl_conf( my_cert["domains"][0], my_cert["fullchain_path"], my_cert["privkey_path"] ) if not certs.set_apache_config(ssl=True): board.set("tls", { "what": "apache-config", "state": "fail", }) log.error("failed enabling ssl configuration for apache") return False log.info(f"activated https for apache using: {domain}") cfg["config"]["https_port"] = 443 cfg["config"]["email"] = email cfg.save() board.set("tls", { "what": "enable", "state": "success", }) # we need to "re-wire" the proxy to port 443 on activated TLS add_msg = "" if cfg["config"]["proxy_active"]: subdomain = cfg["config"]["proxy_domain"].split(".")[0] scheme = "https" token = cfg["config"]["nk_token"] proxy_tunnel = ProxyTunnel() try: proxy_port = proxy_tunnel.setup(token, subdomain, scheme) except ProxySetupError as e: log.error("register at proxy-server error", exc_info=e) except Exception as e: log.error("unexpected error during proxy setup", exc_info=e) cfg["config"]["proxy_port"] = proxy_port cfg.save()