aboutsummaryrefslogtreecommitdiff
path: root/tool/modules
diff options
context:
space:
mode:
Diffstat (limited to 'tool/modules')
-rw-r--r--tool/modules/backup.py41
-rw-r--r--tool/modules/check.py20
-rw-r--r--tool/modules/config.py44
-rw-r--r--tool/modules/configfile.py41
-rw-r--r--tool/modules/download_tools.py47
-rw-r--r--tool/modules/helper.py18
-rw-r--r--tool/modules/install_docker.py16
-rwxr-xr-xtool/modules/nginx.py134
8 files changed, 280 insertions, 81 deletions
diff --git a/tool/modules/backup.py b/tool/modules/backup.py
new file mode 100644
index 0000000..7921d0d
--- /dev/null
+++ b/tool/modules/backup.py
@@ -0,0 +1,41 @@
+from .path import *
+from rich.prompt import Prompt, Confirm
+from urllib.request import urlretrieve
+import subprocess
+from datetime import datetime
+
+
+def backup_restore(http_url_or_path, /, console):
+ url = http_url_or_path
+ if len(url) == 0:
+ raise Exception("You specify an empty url. Abort.")
+ if url.startswith("http://") or url.startswith("https://"):
+ download_path = os.path.join(tmp_dir, "data.tar.xz")
+ if os.path.exists(download_path):
+ to_remove = Confirm.ask(
+ f"I want to download to [cyan]{download_path}[/]. However, there is a file already there. Do you want to remove it first", default=False, console=console)
+ if to_remove:
+ os.remove(download_path)
+ else:
+ raise Exception(
+ "Aborted! Please check the file and try again.")
+ urlretrieve(url, download_path)
+ url = download_path
+ subprocess.run(["sudo", "tar", "-xJf", url, "-C", project_dir], check=True)
+
+
+def backup_backup(path, /, console):
+ ensure_backup_dir()
+ now = datetime.utcnow().isoformat(timespec="seconds") + "Z"
+ if path is None:
+ path = Prompt.ask(
+ "You don't specify the path to backup to. Please specify one. http and https are NOT supported", console=console, default=os.path.join(backup_dir, now + ".tar.xz"))
+ if len(path) == 0:
+ raise Exception("You specify an empty path. Abort!")
+ if os.path.exists(path):
+ raise Exception(
+ "A file is already there. Please remove it first. Abort!")
+ subprocess.run(
+ ["sudo", "tar", "-cJf", path, "data", "-C", project_dir],
+ check=True
+ )
diff --git a/tool/modules/check.py b/tool/modules/check.py
new file mode 100644
index 0000000..2a082f6
--- /dev/null
+++ b/tool/modules/check.py
@@ -0,0 +1,20 @@
+import sys
+import re
+from os.path import *
+
+
+def check_python_version(required_version=(3, 10)):
+ return sys.version_info < required_version
+
+
+def check_ubuntu():
+ if not exists("/etc/os-release"):
+ return False
+ else:
+ with open("/etc/os-release", "r") as f:
+ content = f.read()
+ if re.search(r"NAME=\"?Ubuntu\"?", content, re.IGNORECASE) is None:
+ return False
+ if re.search(r"VERSION_ID=\"?22.04\"?", content, re.IGNORECASE) is None:
+ return False
+ return True
diff --git a/tool/modules/config.py b/tool/modules/config.py
index 37ad996..28b09a3 100644
--- a/tool/modules/config.py
+++ b/tool/modules/config.py
@@ -1,7 +1,8 @@
-from rich.prompt import Prompt
import pwd
import grp
import os
+from rich.prompt import Prompt
+from .path import config_file_path
class ConfigVar:
@@ -73,3 +74,44 @@ def check_config_var_set(needed_config_var_set: set):
if var_name not in needed_config_var_set:
less.append(var_name)
return (True if len(more) == 0 else False, more, less)
+
+
+def config_file_exists():
+ return os.path.isfile(config_file_path)
+
+
+def parse_config(str: str) -> dict:
+ config = {}
+ for line_number, line in enumerate(str.splitlines()):
+ # check if it's a comment
+ if line.startswith("#"):
+ continue
+ # check if there is a '='
+ if line.find("=") == -1:
+ raise ValueError(
+ f"Invalid config string. Please check line {line_number + 1}. There is even no '='!")
+ # split at first '='
+ key, value = line.split("=", 1)
+ key = key.strip()
+ value = value.strip()
+ config[key] = value
+ return config
+
+
+def get_domain() -> str:
+ if not config_file_exists():
+ raise ValueError("Config file not found!")
+ with open(config_file_path) as f:
+ config = parse_config(f.read())
+ if "CRUPEST_DOMAIN" not in config:
+ raise ValueError("Domain not found in config file!")
+ return config["CRUPEST_DOMAIN"]
+
+
+def config_to_str(config: dict) -> str:
+ return "\n".join([f"{key}={value}" for key, value in config.items()])
+
+
+def print_config(console, config: dict) -> None:
+ for key, value in config.items():
+ console.print(f"[magenta]{key}[/] = [cyan]{value}")
diff --git a/tool/modules/configfile.py b/tool/modules/configfile.py
deleted file mode 100644
index 6752e58..0000000
--- a/tool/modules/configfile.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import os.path
-from .path import config_file_path
-
-config_file_exist = os.path.isfile(config_file_path)
-
-
-def parse_config(str: str) -> dict:
- config = {}
- for line_number, line in enumerate(str.splitlines()):
- # check if it's a comment
- if line.startswith("#"):
- continue
- # check if there is a '='
- if line.find("=") == -1:
- raise ValueError(
- f"Invalid config string. Please check line {line_number + 1}. There is even no '='!")
- # split at first '='
- key, value = line.split("=", 1)
- key = key.strip()
- value = value.strip()
- config[key] = value
- return config
-
-
-def get_domain() -> str:
- if not config_file_exist:
- raise ValueError("Config file not found!")
- with open(config_file_path) as f:
- config = parse_config(f.read())
- if "CRUPEST_DOMAIN" not in config:
- raise ValueError("Domain not found in config file!")
- return config["CRUPEST_DOMAIN"]
-
-
-def config_to_str(config: dict) -> str:
- return "\n".join([f"{key}={value}" for key, value in config.items()])
-
-
-def print_config(console, config: dict) -> None:
- for key, value in config.items():
- console.print(f"[magenta]{key}[/] = [cyan]{value}")
diff --git a/tool/modules/download_tools.py b/tool/modules/download_tools.py
new file mode 100644
index 0000000..beb06d4
--- /dev/null
+++ b/tool/modules/download_tools.py
@@ -0,0 +1,47 @@
+import sys
+from os.path import *
+from urllib.request import *
+from rich.prompt import Confirm
+from .path import *
+from .helper import print_order
+
+
+TOOLS = [("docker-mailserver setup script", "docker-mailserver-setup.sh",
+ "https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh")]
+
+
+def download_tools(console):
+ # if we are not linux, we prompt the user
+ if sys.platform != "linux":
+ console.print(
+ "You are not running this script on linux. The tools will not work.", style="yellow")
+ if not Confirm.ask("Do you want to continue?", default=False, console=console):
+ return
+
+ for index, script in enumerate(TOOLS):
+ number = index + 1
+ total = len(TOOLS)
+ print_order(number, total, console)
+ name, filename, url = script
+ # if url is callable, call it
+ if callable(url):
+ url = url()
+ path = join(tool_dir, filename)
+ skip = False
+ if exists(path):
+ overwrite = Confirm.ask(
+ f"[cyan]{name}[/] already exists, download and overwrite?", default=False, console=console)
+ if not overwrite:
+ skip = True
+ else:
+ download = Confirm.ask(
+ f"Download [cyan]{name}[/] to [magenta]{path}[/]?", default=True, console=console)
+ if not download:
+ skip = True
+ if not skip:
+ console.print(f"Downloading {name}...")
+ urlretrieve(url, path)
+ os.chmod(path, 0o755)
+ console.print(f"Downloaded {name} to {path}.", style="green")
+ else:
+ console.print(f"Skipped {name}.", style="yellow")
diff --git a/tool/modules/helper.py b/tool/modules/helper.py
new file mode 100644
index 0000000..f8fe34a
--- /dev/null
+++ b/tool/modules/helper.py
@@ -0,0 +1,18 @@
+import os
+import os.path
+from .path import *
+
+
+def run_in_dir(dir: str, func: callable):
+ old_dir = os.path.abspath(os.getcwd())
+ os.chdir(dir)
+ func()
+ os.chdir(old_dir)
+
+
+def run_in_project_dir(func: callable):
+ run_in_dir(project_dir, func)
+
+
+def print_order(number: int, total: int, /, console) -> None:
+ console.print(f"\[{number}/{total}]", end=" ", style="green")
diff --git a/tool/modules/install_docker.py b/tool/modules/install_docker.py
new file mode 100644
index 0000000..ac50290
--- /dev/null
+++ b/tool/modules/install_docker.py
@@ -0,0 +1,16 @@
+from os.path import *
+from .path import *
+import urllib
+import subprocess
+
+
+def install_docker():
+ ensure_tmp_dir()
+ get_docker_path = join(tmp_dir, "get-docker.sh")
+ urllib.request.urlretrieve("https://get.docker.com", get_docker_path)
+ os.chmod(get_docker_path, 0o755)
+ subprocess.run(["sudo", "sh", get_docker_path], check=True)
+ subprocess.run(["sudo", "systemctl", "enable",
+ "--now", "docker"], check=True)
+ subprocess.run(["sudo", "usermod", "-aG", "docker",
+ os.getlogin()], check=True)
diff --git a/tool/modules/nginx.py b/tool/modules/nginx.py
index 9c51d66..087422b 100755
--- a/tool/modules/nginx.py
+++ b/tool/modules/nginx.py
@@ -1,56 +1,65 @@
#!/usr/bin/env python3
-from .template import Template
-from .path import *
import json
import jsonschema
import os
-import os.path
+from os.path import *
import shutil
+import subprocess
+from rich.prompt import Confirm
from cryptography.x509 import *
from cryptography.x509.oid import ExtensionOID
+from .template import Template
+from .path import *
-
-with open(os.path.join(nginx_template_dir, 'server.json')) as f:
+with open(join(nginx_template_dir, 'server.json')) as f:
server = json.load(f)
-with open(os.path.join(nginx_template_dir, 'server.schema.json')) as f:
+with open(join(nginx_template_dir, 'server.schema.json')) as f:
schema = json.load(f)
jsonschema.validate(server, schema)
non_template_files = ['forbid_unknown_domain.conf', "websocket.conf"]
-ssl_template = Template(os.path.join(nginx_template_dir, 'ssl.conf.template'))
-root_template = Template(os.path.join(
+ssl_template = Template(join(nginx_template_dir, 'ssl.conf.template'))
+root_template = Template(join(
nginx_template_dir, 'root.conf.template'))
-static_file_template = Template(os.path.join(
+static_file_template = Template(join(
nginx_template_dir, 'static-file.conf.template'))
-reverse_proxy_template = Template(os.path.join(
+reverse_proxy_template = Template(join(
nginx_template_dir, 'reverse-proxy.conf.template'))
-redirect_template = Template(os.path.join(
+redirect_template = Template(join(
nginx_template_dir, 'redirect.conf.template'))
-cert_only_template = Template(os.path.join(
+cert_only_template = Template(join(
nginx_template_dir, 'cert-only.conf.template'))
nginx_var_set = set.union(root_template.var_set,
static_file_template.var_set, reverse_proxy_template.var_set)
-def nginx_config_gen(domain: str, dest: str) -> None:
- if not os.path.isdir(dest):
+def list_subdomains(domain: str) -> list:
+ return [f"{s['subdomain']}.{domain}" for s in server["sites"]]
+
+
+def list_domains(domain: str) -> list:
+ return [domain, *list_subdomains(domain)]
+
+
+def generate_nginx_config(domain: str, dest: str) -> None:
+ if not isdir(dest):
raise ValueError('dest must be a directory')
# copy ssl.conf and https-redirect.conf which need no variable substitution
for filename in non_template_files:
- src = os.path.join(nginx_template_dir, filename)
- dst = os.path.join(dest, filename)
+ src = join(nginx_template_dir, filename)
+ dst = join(dest, filename)
shutil.copyfile(src, dst)
config = {"CRUPEST_DOMAIN": domain}
# generate ssl.conf
- with open(os.path.join(dest, 'ssl.conf'), 'w') as f:
+ with open(join(dest, 'ssl.conf'), 'w') as f:
f.write(ssl_template.generate(config))
# generate root.conf
- with open(os.path.join(dest, f'{domain}.conf'), 'w') as f:
+ with open(join(dest, f'{domain}.conf'), 'w') as f:
f.write(root_template.generate(config))
# generate nginx config for each site
sites: list = server["sites"]
@@ -72,16 +81,45 @@ def nginx_config_gen(domain: str, dest: str) -> None:
template = cert_only_template
else:
raise Exception('Invalid site type')
- with open(os.path.join(dest, f'{subdomain}.{domain}.conf'), 'w') as f:
+ with open(join(dest, f'{subdomain}.{domain}.conf'), 'w') as f:
f.write(template.generate(local_config))
-def list_subdomains(domain: str) -> list:
- return [f"{s['subdomain']}.{domain}" for s in server["sites"]]
+def check_nginx_config_dir(dir_path: str, domain: str) -> list:
+ if not exists(dir_path):
+ return []
+ good_files = [*non_template_files, "ssl.conf", *
+ [f"{full_domain}.conf" for full_domain in list_domains(domain)]]
+ bad_files = []
+ for path in os.listdir(dir_path):
+ file_name = basename(path)
+ if file_name not in good_files:
+ bad_files.append(file_name)
+ return bad_files
-def list_domains(domain: str) -> list:
- return [domain, *list_subdomains(domain)]
+def nginx(domain: str, /, console) -> None:
+ bad_files = check_nginx_config_dir(nginx_config_dir, domain)
+ if len(bad_files) > 0:
+ console.print(
+ "WARNING: It seems there are some bad conf files in the nginx config directory:", style="yellow")
+ for bad_file in bad_files:
+ console.print(bad_file, style="cyan")
+ to_delete = Confirm.ask(
+ "They will affect nginx in a [red]bad[/] way. Do you want to delete them?", default=True, console=console)
+ if to_delete:
+ for file in bad_files:
+ os.remove(join(nginx_config_dir, file))
+ console.print(
+ "I have found following var in nginx templates:", style="green")
+ for var in nginx_var_set:
+ console.print(var, style="magenta")
+ if not exists(nginx_config_dir):
+ os.mkdir(nginx_config_dir)
+ console.print(
+ f"Nginx config directory created at [magenta]{nginx_config_dir}[/]", style="green")
+ generate_nginx_config(domain, dest=nginx_config_dir)
+ console.print("Nginx config generated.", style="green")
def certbot_command_gen(domain: str, action, /, test=False, no_docker=False, *, standalone=None, email=None, agree_tos=False) -> str:
@@ -133,29 +171,16 @@ def certbot_command_gen(domain: str, action, /, test=False, no_docker=False, *,
return command
-def nginx_config_dir_check(dir_path: str, domain: str) -> list:
- if not os.path.exists(dir_path):
- return []
- good_files = [*non_template_files, "ssl.conf", *
- [f"{full_domain}.conf" for full_domain in list_domains(domain)]]
- bad_files = []
- for path in os.listdir(dir_path):
- basename = os.path.basename(path)
- if basename not in good_files:
- bad_files.append(basename)
- return bad_files
-
-
def get_cert_path(root_domain):
- return os.path.join(data_dir, "certbot", "certs", "live", root_domain, "fullchain.pem")
+ return join(data_dir, "certbot", "certs", "live", root_domain, "fullchain.pem")
def get_cert_domains(cert_path, root_domain):
- if not os.path.exists(cert_path):
+ if not exists(cert_path):
return None
- if not os.path.isfile(cert_path):
+ if not isfile(cert_path):
return None
with open(cert_path, 'rb') as f:
@@ -166,3 +191,34 @@ def get_cert_domains(cert_path, root_domain):
domains.remove(root_domain)
domains = [root_domain, *domains]
return domains
+
+
+def print_create_cert_message(domain, console):
+ console.print(
+ "Looks like you haven't run certbot to get the init ssl certificates. You may want to run following code to get one:", style="cyan")
+ console.print(certbot_command_gen(domain, "create"),
+ soft_wrap=True, highlight=False)
+
+
+def check_ssl_cert(domain, console):
+ cert_path = get_cert_path(domain)
+ tmp_cert_path = join(tmp_dir, "fullchain.pem")
+ console.print("Temporarily copy cert to tmp...", style="yellow")
+ ensure_tmp_dir()
+ subprocess.run(
+ ["sudo", "cp", cert_path, tmp_cert_path], check=True)
+ subprocess.run(["sudo", "chown", str(os.geteuid()),
+ tmp_cert_path], check=True)
+ cert_domains = get_cert_domains(tmp_cert_path, domain)
+ if cert_domains is None:
+ print_create_cert_message(domain, console)
+ else:
+ cert_domain_set = set(cert_domains)
+ domains = set(list_domains(domain))
+ if not cert_domain_set == domains:
+ console.print(
+ "Cert domains are not equal to host domains. Run following command to recreate it with nginx stopped.", style="red")
+ console.print(certbot_command_gen(
+ domain, "create", standalone=True), soft_wrap=True, highlight=False)
+ console.print("Remove tmp cert...", style="yellow")
+ os.remove(tmp_cert_path)