Added comments
This commit is contained in:
parent
acda51a6b4
commit
b99fe97267
71
cli.py
71
cli.py
@ -20,24 +20,61 @@ TEMPLATE_DIR = "./templates"
|
|||||||
|
|
||||||
|
|
||||||
#region util
|
#region util
|
||||||
def gen_pass() -> str:
|
def gen_pass(pass_length: int = 20) -> str:
|
||||||
|
"""Generates a simple password with the provided length
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pass_length (int, optional): Length of password. Defaults to 20.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The generated password.
|
||||||
|
"""
|
||||||
alphabet = string.ascii_letters + string.digits
|
alphabet = string.ascii_letters + string.digits
|
||||||
password = "".join(secrets.choice(alphabet) for _ in range(20))
|
password = "".join(secrets.choice(alphabet) for _ in range(pass_length))
|
||||||
return password
|
return password
|
||||||
|
|
||||||
|
|
||||||
def check_positive(value: str):
|
def check_positive(value: str) -> int:
|
||||||
|
"""`argparse` type helper to check whether a given input is a positive integer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (str): Input to validate
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Input is neither a decimal or positive
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The parsed input if valid
|
||||||
|
"""
|
||||||
ivalue = int(value)
|
ivalue = int(value)
|
||||||
if ivalue <= 0:
|
if ivalue < 0:
|
||||||
raise Exception("Supplied number must be >= 0")
|
raise Exception(f"Supplied number must be >= 0")
|
||||||
return ivalue
|
return ivalue
|
||||||
|
|
||||||
|
|
||||||
def encode_member(member: str, mapping: Mapping[str, Any]) -> str:
|
def encode_member(member: str, mapping: Mapping[str, Any]) -> str:
|
||||||
|
"""Encodes the member-entry of an inventory file with additional mappings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
member (str)
|
||||||
|
mapping (Mapping[str, Any])
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: member with mapping encoded
|
||||||
|
"""
|
||||||
return member + " " + " ".join([f"{k}={v}" for k, v in mapping.items()])
|
return member + " " + " ".join([f"{k}={v}" for k, v in mapping.items()])
|
||||||
|
|
||||||
|
|
||||||
def iter_ips(ip_format: str, start_octet: int):
|
def iter_ips(ip_format: str, start_octet: int):
|
||||||
|
"""Simple iterator for generating ip's
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ip_format (str)
|
||||||
|
start_octet (int)
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Yeah idk too lazy too look up what the type annotation for a generator is
|
||||||
|
"""
|
||||||
ip_int = start_octet
|
ip_int = start_octet
|
||||||
ip_fmt = ip_format
|
ip_fmt = ip_format
|
||||||
while ip_int < 255:
|
while ip_int < 255:
|
||||||
@ -46,6 +83,13 @@ def iter_ips(ip_format: str, start_octet: int):
|
|||||||
|
|
||||||
|
|
||||||
def copy_template(src: str, dest: str, mapping: Mapping[str, Any] = {}):
|
def copy_template(src: str, dest: str, mapping: Mapping[str, Any] = {}):
|
||||||
|
"""Templates and writes a template file using Jinja as templating engine.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src (str): content of the file
|
||||||
|
dest (str): place to write templated file to
|
||||||
|
mapping (Mapping[str, Any], optional): datamapping. Defaults to {}.
|
||||||
|
"""
|
||||||
c = Path(src).read_text()
|
c = Path(src).read_text()
|
||||||
t: Template = Template(c)
|
t: Template = Template(c)
|
||||||
r = t.render(mapping)
|
r = t.render(mapping)
|
||||||
@ -63,6 +107,15 @@ class MachineResources:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_prompt() -> "MachineResources":
|
def from_prompt() -> "MachineResources":
|
||||||
|
"""Generate `MachineResources` from prompt.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception
|
||||||
|
Exception
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MachineResources
|
||||||
|
"""
|
||||||
cpus = input(
|
cpus = input(
|
||||||
"How many processors would you like to assign (default=1): ")
|
"How many processors would you like to assign (default=1): ")
|
||||||
if not cpus:
|
if not cpus:
|
||||||
@ -84,7 +137,9 @@ class MachineResources:
|
|||||||
|
|
||||||
|
|
||||||
class InventoryWriter:
|
class InventoryWriter:
|
||||||
|
"""
|
||||||
|
Helper class for generating Ansible inventory files.
|
||||||
|
"""
|
||||||
def __init__(self, location: str) -> None:
|
def __init__(self, location: str) -> None:
|
||||||
self._file_handle = Path(location)
|
self._file_handle = Path(location)
|
||||||
self._groups: dict[str, set[str]] = DefaultDict(set)
|
self._groups: dict[str, set[str]] = DefaultDict(set)
|
||||||
@ -112,7 +167,7 @@ def list_envs(args: argparse.Namespace):
|
|||||||
customer_path = path.join("customers", args.customer_name, "envs")
|
customer_path = path.join("customers", args.customer_name, "envs")
|
||||||
print(" ".join(os.listdir(customer_path)))
|
print(" ".join(os.listdir(customer_path)))
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise Exception(f"Customer `{args.customer_name}` does not exist.")
|
print(f"Customer `{args.customer_name}` does not exist.")
|
||||||
|
|
||||||
|
|
||||||
def delete_env(args: argparse.Namespace):
|
def delete_env(args: argparse.Namespace):
|
||||||
@ -282,7 +337,7 @@ def main() -> int:
|
|||||||
cenv_parser.add_argument("--ip-int",
|
cenv_parser.add_argument("--ip-int",
|
||||||
type=check_positive,
|
type=check_positive,
|
||||||
help="4th octet to start at",
|
help="4th octet to start at",
|
||||||
default=10)
|
default="10")
|
||||||
cenv_parser.set_defaults(func=create_env)
|
cenv_parser.set_defaults(func=create_env)
|
||||||
|
|
||||||
# CLI definition for positional arg "delete"
|
# CLI definition for positional arg "delete"
|
||||||
|
193
service.py
193
service.py
@ -1,59 +1,160 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from pathlib import Path
|
||||||
|
from os import path
|
||||||
import subprocess as sub
|
import subprocess as sub
|
||||||
import re
|
import re
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
P_BANNER = """
|
||||||
|
██╗ ██╗███████╗██╗ ██╗ ██╗ ██████╗ ███╗ ███╗
|
||||||
|
██║ ██║██╔════╝██║ ██║ ██╔╝██╔═══██╗████╗ ████║
|
||||||
|
██║ █╗ ██║█████╗ ██║ █████╔╝ ██║ ██║██╔████╔██║
|
||||||
|
██║███╗██║██╔══╝ ██║ ██╔═██╗ ██║ ██║██║╚██╔╝██║
|
||||||
|
╚███╔███╔╝███████╗███████╗██║ ██╗╚██████╔╝██║ ╚═╝ ██║
|
||||||
|
╚══╝╚══╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
|
||||||
|
"""
|
||||||
|
|
||||||
P_OPTIONS = """
|
P_OPTIONS = """
|
||||||
Options:
|
Opties:
|
||||||
0) Exit
|
0) Afsluiten
|
||||||
1) Maak/Update klantomgeving(en)
|
1) Maak/Wijzig omgeving(en)
|
||||||
2) Vernietig klantomgeving(en)
|
2) Vernietig omgeving(en)
|
||||||
3) Recover klantomgeving(en)
|
3) Recover omgeving(en)
|
||||||
4) List klantomgeving(en)
|
4) List omgeving(en)
|
||||||
|
|
||||||
h) Help
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RE_TEXT = re.compile(r"^\w+$")
|
RE_TEXT = re.compile(r"^\w+$")
|
||||||
RE_NUM = re.compile(r"^\d+$")
|
RE_NUM = re.compile(r"^\d+$")
|
||||||
|
RE_ANY = re.compile(r".+")
|
||||||
|
|
||||||
|
|
||||||
def _take_input(prompt: str, pattern: re.Pattern, default: str="") -> str:
|
class Prompter:
|
||||||
|
|
||||||
|
def __init__(self, ps1: str = "") -> None:
|
||||||
|
self.ps1 = ps1
|
||||||
|
|
||||||
|
def _build_prompt(self, prompt: str) -> str:
|
||||||
|
return self.ps1 + prompt
|
||||||
|
|
||||||
|
def take_input(self,
|
||||||
|
prompt: str,
|
||||||
|
pattern: re.Pattern[Any],
|
||||||
|
default: str = "") -> str:
|
||||||
|
prompt = self._build_prompt(prompt)
|
||||||
|
return Prompter.input(prompt, lambda x: bool(pattern.match(x)),
|
||||||
|
default)
|
||||||
|
|
||||||
|
def take_choice(self, prompt: str, options: list[str]) -> str:
|
||||||
|
prompt = self._build_prompt(prompt)
|
||||||
|
return Prompter.input(prompt, lambda x: x in options)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def input(prompt: str,
|
||||||
|
validate: Callable[[str], bool],
|
||||||
|
default: str = "") -> str:
|
||||||
while True:
|
while True:
|
||||||
i = input(prompt)
|
i = input(prompt)
|
||||||
if i == "":
|
if i == "":
|
||||||
i = default
|
i = default
|
||||||
if pattern.match(i):
|
if validate(i):
|
||||||
return i
|
return i
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_list(customer: str) -> list[str]:
|
||||||
|
"""Fetches and parses a list of a customer's environments
|
||||||
|
|
||||||
|
Args:
|
||||||
|
customer (str): The customer to fetch environments from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: The parsed environments
|
||||||
|
"""
|
||||||
|
return sub.check_output(["python3", "cli.py", "list",
|
||||||
|
customer]).decode().strip("\n").split(" ")
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
|
print(P_BANNER)
|
||||||
|
customer_name = Prompter.input("Wat is uw naam: ",
|
||||||
|
lambda x: bool(RE_TEXT.match(x)))
|
||||||
|
|
||||||
|
"""
|
||||||
|
Ensures the necessary customer setup.
|
||||||
|
"""
|
||||||
|
envs_dir = path.join("customers", customer_name, "envs")
|
||||||
|
Path(envs_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Initializes models for the event loop
|
||||||
|
"""
|
||||||
|
p = Prompter(ps1=f"{customer_name} > ")
|
||||||
|
|
||||||
|
"""
|
||||||
|
The main "event" loop
|
||||||
|
"""
|
||||||
while True:
|
while True:
|
||||||
print(P_OPTIONS)
|
print(P_OPTIONS)
|
||||||
c = input("Input: ")
|
c = p.take_input("Keuze: ", RE_ANY)
|
||||||
if c == "h":
|
print(end="\n")
|
||||||
continue
|
|
||||||
|
|
||||||
if c == "0":
|
if c == "0":
|
||||||
|
"""
|
||||||
|
Used to break out of the loop and exit the portal.
|
||||||
|
"""
|
||||||
break
|
break
|
||||||
|
|
||||||
if c == "1":
|
if c == "1":
|
||||||
customer_name = _take_input("Customer name (example=opg): ",
|
"""
|
||||||
RE_TEXT)
|
Used to either update or create an environment.
|
||||||
env_name = _take_input("Environment name (example=prod): ",
|
"""
|
||||||
RE_TEXT)
|
envs = get_env_list(customer_name)
|
||||||
amnt_nginx_web = _take_input(
|
print(f"NOTE: Kies een albestaande omgevingen {envs} of iets nieuws...")
|
||||||
"Number of nginx webservers (default=1): ", RE_NUM, default="1")
|
fmt = "Omgevingsnaam:"
|
||||||
amnt_nginx_lb = _take_input(
|
env_name = p.take_input(fmt, RE_TEXT)
|
||||||
"Number of nginx loadbalancers (default=1): ", RE_NUM, default="1")
|
|
||||||
amnt_psql = _take_input(
|
templates = ["production", "acceptance", "test", "custom"]
|
||||||
"Number of postgres instances (default=1): ", RE_NUM, default="1")
|
fmt = f"Het type omgeving {templates}: "
|
||||||
ip_format = _take_input(
|
template_name = p.take_choice(fmt, templates)
|
||||||
"Format of ip (default=192.168.56.{}, `{}` will be replaced with the correct octet at runtime): ",
|
|
||||||
re.compile(r".+"), default="192.168.56.{}")
|
if template_name == "custom":
|
||||||
ip_int = _take_input(
|
"""
|
||||||
"Number to start formatting the IP from (default=10): ",
|
Asks for all machine's individually
|
||||||
RE_NUM, default="10")
|
"""
|
||||||
|
fmt = "Aantal nginx webservers (default=1): "
|
||||||
|
amnt_nginx_web = p.take_input(fmt, RE_NUM, default="1")
|
||||||
|
|
||||||
|
fmt = "Aantal nginx loadbalancers (default=1): "
|
||||||
|
amnt_nginx_lb = p.take_input(fmt, RE_NUM, default="1")
|
||||||
|
|
||||||
|
fmt = "Aantal postgres instances (default=1): "
|
||||||
|
amnt_psql = p.take_input(fmt, RE_NUM, default="1")
|
||||||
|
elif template_name == "production":
|
||||||
|
amnt_nginx_web = "2"
|
||||||
|
amnt_nginx_lb = "1"
|
||||||
|
amnt_psql = "1"
|
||||||
|
elif template_name == "acceptance":
|
||||||
|
amnt_nginx_web = "1"
|
||||||
|
amnt_nginx_lb = "0"
|
||||||
|
amnt_psql = "1"
|
||||||
|
# elif template_name == "test":
|
||||||
|
else:
|
||||||
|
amnt_nginx_web = "1"
|
||||||
|
amnt_nginx_lb = "0"
|
||||||
|
amnt_psql = "0"
|
||||||
|
|
||||||
|
"""
|
||||||
|
Define the format for templating ip-addresses.`{}` is required
|
||||||
|
and will be subbed at runtime with the correct octet.
|
||||||
|
"""
|
||||||
|
print(end="\n")
|
||||||
|
fmt = "NOTE: `{}` will be replaced with the correct octet at runtime"
|
||||||
|
print(fmt)
|
||||||
|
fmt = "Format of ip (default=192.168.56.{}): "
|
||||||
|
ip_format = p.take_input(fmt, RE_ANY, default="192.168.56.{}")
|
||||||
|
|
||||||
|
fmt = "Number to start formatting the IP from (default=10): "
|
||||||
|
ip_int = p.take_input(fmt, RE_NUM, default="10")
|
||||||
|
|
||||||
sub.call([
|
sub.call([
|
||||||
"python3", "cli.py", "create", customer_name, env_name,
|
"python3", "cli.py", "create", customer_name, env_name,
|
||||||
@ -61,25 +162,37 @@ def main() -> int:
|
|||||||
"--num-nginx-lb", amnt_nginx_lb, "--ip-format", ip_format,
|
"--num-nginx-lb", amnt_nginx_lb, "--ip-format", ip_format,
|
||||||
"--ip-int", ip_int
|
"--ip-int", ip_int
|
||||||
])
|
])
|
||||||
|
print(f"Omgeving `{env_name}` successvol gemaakt.")
|
||||||
|
|
||||||
if c == "2":
|
if c == "2":
|
||||||
customer_name = _take_input("Customer name (example=opg): ",
|
"""
|
||||||
RE_TEXT)
|
Deletes all traces of the chosen environment.
|
||||||
env_name = _take_input("Environment name (example=prod): ",
|
"""
|
||||||
RE_TEXT)
|
envs = get_env_list(customer_name)
|
||||||
|
fmt = f"Omgevingsnaam {envs}: "
|
||||||
|
env_name = p.take_choice(fmt, envs)
|
||||||
sub.call(["python3", "cli.py", "delete", customer_name, env_name])
|
sub.call(["python3", "cli.py", "delete", customer_name, env_name])
|
||||||
|
|
||||||
if c == "3":
|
if c == "3":
|
||||||
customer_name = _take_input("Customer name (example=opg): ",
|
"""
|
||||||
RE_TEXT)
|
Allows the customer to "force" their environment into the desired `up-state`.
|
||||||
env_name = _take_input("Environment name (example=prod): ",
|
This `up-state` is a representation of the Vagrantfile, Ansible inventory and
|
||||||
RE_TEXT)
|
.ssh directory of that specific environment. The .ssh directory is only
|
||||||
|
necessary in case of an existing storage-volume. Otherwise a new one
|
||||||
|
keypair can be manually created or created using option 1.
|
||||||
|
"""
|
||||||
|
envs = get_env_list(customer_name)
|
||||||
|
fmt = f"Omgevingsnaam {envs}: "
|
||||||
|
env_name = p.take_choice(fmt, envs)
|
||||||
sub.call(["python3", "cli.py", "recover", customer_name, env_name])
|
sub.call(["python3", "cli.py", "recover", customer_name, env_name])
|
||||||
|
|
||||||
if c == "4":
|
if c == "4":
|
||||||
customer_name = _take_input("Customer name (example=opg): ",
|
"""
|
||||||
RE_TEXT)
|
This branch displays the customer's existing environments
|
||||||
sub.call(["python3", "cli.py", "list", customer_name])
|
"""
|
||||||
|
print("De volgende omgeving(en) zijn aanwezig: ")
|
||||||
|
for env in get_env_list(customer_name):
|
||||||
|
print(f" - {env}")
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user