Compare commits

...

38 Commits

Author SHA1 Message Date
c81588265b Small changes 2022-04-09 19:01:24 +02:00
55a487fed3 Confirmation of chosen preset self-service portal 2022-04-09 11:08:46 +02:00
777d0cfbd7 Added enum for self-service options 2022-04-09 10:54:13 +02:00
b546e03e37 Added more comments 2022-04-08 22:32:30 +02:00
6608e1216b Updated ansible.cfg template to attempt more retries 2022-04-08 22:32:13 +02:00
61428cf80a Formatted files using yapf 2022-04-08 21:22:16 +02:00
db13dfb04b Removed unnecessary comments 2022-04-08 21:21:34 +02:00
b99fe97267 Added comments 2022-04-08 21:19:18 +02:00
acda51a6b4 Added default values for taking input 2022-04-08 14:20:40 +02:00
cd736a2606 Fixed ansible collection requirement install 2022-04-08 14:20:22 +02:00
a1c7052e85 Added self-service-portal wrapper around CLI 2022-04-08 13:14:55 +02:00
efe5901a55 Refactored service.py and added recovery cmd 2022-04-07 14:02:04 +02:00
7ed3be574a Only shows section in vagrantfile when applicable 2022-04-07 14:01:41 +02:00
a330bb17d1 Added resource mngmnt option for machine types 2022-03-15 23:08:54 +01:00
4f3de27656 Removed README.md 2022-03-15 08:41:04 +01:00
7b00e6d893 Prevent accidental .ssh overwrite fuck up 2022-03-15 08:35:34 +01:00
63b959c8cd Better indication no postgres instance detected 2022-03-15 08:34:58 +01:00
04a7ff551c Fixed broken ansible package install_deps.sh 2022-03-15 08:30:07 +01:00
0b574e24bb Added annotations import to service.py 2022-03-14 22:53:20 +01:00
52b3dad70c Fixed install_deps.sh for ubuntu 20.04 2022-03-14 22:53:08 +01:00
b9d844d405 Updated install_deps.sh 2022-03-14 22:29:46 +01:00
e7f26fbf6f Replaced self_service.sh with service.py 2022-03-14 21:58:25 +01:00
cdca807aa0 Added php7.4-pgsql dep to nginx-webserver 2022-03-14 15:20:10 +01:00
ba93280fd6 Updated index.php 2022-03-14 15:19:44 +01:00
790e83b8fb Added copying of sample-data to db vm 2022-03-14 14:39:23 +01:00
b465413d42 Added update step to update pg_hba.conf postgresql/tasks 2022-03-14 11:47:55 +01:00
58f2a973d9 Moved sample.csv 2022-03-14 11:47:20 +01:00
c23898f2d0 WIP self-service.sh 2022-03-14 11:47:07 +01:00
400806fd29 WIP postgresql role 2022-03-14 11:33:22 +01:00
6e55c7ef47 Added postgres section to Vagrantfile.template 2022-03-14 11:32:58 +01:00
b71d98dcea install_deps.sh installs ansible requirements 2022-03-14 11:32:32 +01:00
e8d86d41ea Added postgres to site.yml 2022-03-10 21:27:31 +01:00
909ef2b5c8 Added README.md 2022-03-10 21:27:13 +01:00
4fb0c5c43b Added postgresql role 2022-03-10 21:26:46 +01:00
ec0a1adc01 Added php to nginx-webserver 2022-03-10 09:47:12 +01:00
5108efea23 Converted nginx-webserver index.html to php 2022-03-10 08:55:57 +01:00
b80bd3e913 Added shebang to install_deps.sh 2022-03-10 08:55:36 +01:00
fa939a9e90 Removed scripts (included in inv-alias branch) 2022-03-10 08:50:11 +01:00
22 changed files with 822 additions and 293 deletions

View File

@ -1 +0,0 @@
export PATH="$PATH:$PWD/scripts/bin"

View File

@ -1,2 +0,0 @@
cd ./scripts
go build -o ./bin/inv-alias ./inv-alias.go

378
cli.py Executable file
View File

@ -0,0 +1,378 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import dataclasses
import sys
from os import path
from pathlib import Path
import os
import time
from typing import Any, DefaultDict, Iterable, Mapping
from jinja2 import Template
import itertools
import subprocess as sub
import shutil
import string
import secrets
from dataclasses import dataclass
TEMPLATE_DIR = "./templates"
#region util
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
password = "".join(secrets.choice(alphabet) for _ in range(pass_length))
return password
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)
if ivalue < 0:
raise Exception(f"Supplied number must be >= 0")
return ivalue
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()])
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_fmt = ip_format
while ip_int < 255:
yield ip_fmt.format(ip_int)
ip_int += 1
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()
t: Template = Template(c)
r = t.render(mapping)
Path(dest).write_text(r)
#endregion
#region models
@dataclass
class MachineResources:
cpus: int
mem: int
@staticmethod
def from_prompt() -> "MachineResources":
"""Generate `MachineResources` from prompt.
Raises:
Exception
Exception
Returns:
MachineResources
"""
cpus = input(
"How many processors would you like to assign (default=1): ")
if not cpus:
cpus = "1"
if not cpus.isdigit() or int(cpus) < 0:
raise Exception("Expected a postive amount of processors")
mem = input(
"How many megabytes of RAM would you like to assign (default=1024): "
)
if not mem:
mem = "1024"
if not mem.isdigit() or int(mem) < 0:
raise Exception("Expected a postive amount of memory")
return MachineResources(cpus=int(cpus), mem=int(mem))
class InventoryWriter:
"""
Helper class for generating Ansible inventory files.
"""
def __init__(self, location: str) -> None:
self._file_handle = Path(location)
self._groups: dict[str, set[str]] = DefaultDict(set)
def add(self, name: str, members: Iterable[str]):
self._groups[name] |= set(members)
def _build_group(self, name: str, members: set[str]):
fmt = f"[{name}]\n" + "\n".join(members)
return fmt
def flush(self):
txt = ""
for name, members in self._groups.items():
txt += self._build_group(name, members) + "\n\n"
self._file_handle.write_text(txt, encoding="utf8")
#endregion
#region CLI positional flows
def list_envs(args: argparse.Namespace):
try:
customer_path = path.join("customers", args.customer_name, "envs")
print(" ".join(os.listdir(customer_path)))
except FileNotFoundError:
print(f"Customer `{args.customer_name}` does not exist.")
def delete_env(args: argparse.Namespace):
for env in args.env_names:
env_path = path.join("customers", args.customer_name, "envs", env)
sub.call(["vagrant", "destroy", "-f"], cwd=env_path)
shutil.rmtree(env_path)
print(f"Deleted `{env}` from customer `{ args.customer_name}`")
def recover_env(args: argparse.Namespace):
for env in args.env_names:
env_path = path.join("customers", args.customer_name, "envs", env)
sub.call(["vagrant", "up"], cwd=env_path)
# Artificial sleep waiting for sshd on VM to correctly start
print("Waiting on virtual machines...")
time.sleep(5)
sub.call(["ansible-playbook", "../../../../site.yml"], cwd=env_path)
print(f"Recovered `{env}` from customer `{ args.customer_name}`")
def create_env(args: argparse.Namespace):
"""
Managing environments works in a declarative way and thus it can
also be used for updating.
"""
if (args.num_nginx_web + args.num_nginx_lb + args.num_postgres) == 0:
raise Exception("At least one item should be deployed")
env_path = path.join("customers", args.customer_name, "envs",
args.env_name)
Path(env_path).mkdir(exist_ok=True, parents=True)
vagrant_mapping: dict[str, Any] = {
"webserver_specs": None,
"loadbalancers_specs": None,
"postgres_specs": None,
"num_webserver": args.num_nginx_web,
"num_loadbalancers": args.num_nginx_lb,
"num_postgres": args.num_postgres,
"env": args.env_name,
"customer_name": args.customer_name,
"ip_int": args.ip_int,
"ip_format": args.ip_format.replace("{}", "%d"),
}
if args.num_nginx_web > 0:
print("\nNginx webserver resources:")
web_specs = MachineResources.from_prompt()
vagrant_mapping["webserver_specs"] = dataclasses.asdict(web_specs)
if args.num_nginx_lb > 0:
print("\nNginx loadbalancer resources: ")
lb_specs = MachineResources.from_prompt()
vagrant_mapping["loadbalancers_specs"] = dataclasses.asdict(lb_specs)
if args.num_postgres > 0:
print("\nPostgresql machine resources: ")
psql_specs = MachineResources.from_prompt()
vagrant_mapping["postgres_specs"] = dataclasses.asdict(psql_specs)
# Template `ansible.cfg`
src = path.join(TEMPLATE_DIR, "ansible.cfg.template")
dest = path.join(env_path, "ansible.cfg")
copy_template(src=src, dest=dest)
# Create inventory file
inv_path = path.join(env_path, "inventory")
iw = InventoryWriter(inv_path)
ip_generator = iter_ips(args.ip_format, args.ip_int)
web_ips = itertools.islice(ip_generator, args.num_nginx_web)
iw.add("webserver", web_ips)
lb_ips = itertools.islice(ip_generator, args.num_nginx_lb)
iw.add("loadbalancer", lb_ips)
def psql_gen_pass(x: str) -> str:
return encode_member(x, {"psql_pass": gen_pass()})
psql_ips = list(itertools.islice(ip_generator, args.num_postgres))
psql_ips = map(psql_gen_pass, psql_ips)
iw.add("postgresql", psql_ips)
iw.flush()
# Template `Vagrantfile`
src = path.join(TEMPLATE_DIR, "Vagrantfile.template")
dest = path.join(env_path, "Vagrantfile")
should_reload = Path(dest).exists()
copy_template(src=src, dest=dest, mapping=vagrant_mapping)
# Generate .ssh if it doesn't exist already
ssh_dir = path.join(env_path, ".ssh")
Path(ssh_dir).mkdir(exist_ok=True)
rsa_path = path.join(ssh_dir, "id_rsa")
if not Path(rsa_path).exists():
print(end="\n")
sub.call([
"ssh-keygen",
"-t",
"rsa",
"-b",
"2048",
"-f",
rsa_path,
])
# Provision and configure machines
# Create VM's that do not exist yet
sub.call(["vagrant", "up"], cwd=env_path)
# Update VM's that already existed
if should_reload:
sub.call(["vagrant", "reload", "--provision"], cwd=env_path)
# Artificial sleep waiting for sshd on VM to correctly start
print("Waiting on virtual machines...")
time.sleep(5)
sub.call(["ansible-playbook", "../../../../site.yml"], cwd=env_path)
#endregion
def main() -> int:
parser = argparse.ArgumentParser()
sub_parser = parser.add_subparsers()
# CLI definition for positional arg "list"
list_parser = sub_parser.add_parser(
"list", help="list customer-owned environments")
list_parser.add_argument("customer_name",
type=str,
help="name of the customer")
list_parser.set_defaults(func=list_envs)
# CLI definition for positional arg "create"
cenv_parser = sub_parser.add_parser("create",
help="create/update an environment")
cenv_parser.add_argument("customer_name",
type=str,
help="name of the customer")
cenv_parser.add_argument("env_name",
type=str,
help="name of the environment")
cenv_parser.add_argument(
"--num-postgres",
type=check_positive,
help="number of postgres databases",
default=0,
)
cenv_parser.add_argument(
"--num-nginx-web",
type=check_positive,
help="number of nginx webservers",
default=0,
)
cenv_parser.add_argument(
"--num-nginx-lb",
type=check_positive,
help="number of nginx loadbalancers",
default=0,
)
cenv_parser.add_argument("--ip-format",
type=str,
help="format of ip",
default="192.168.56.{}")
cenv_parser.add_argument("--ip-int",
type=check_positive,
help="4th octet to start at",
default="10")
cenv_parser.set_defaults(func=create_env)
# CLI definition for positional arg "delete"
denv_parser = sub_parser.add_parser("delete", help="delete an environment")
denv_parser.add_argument("customer_name",
type=str,
help="name of the customer")
denv_parser.add_argument("env_names",
type=str,
nargs="+",
help="name of one or more environments")
denv_parser.set_defaults(func=delete_env)
# CLI definition for positional arg "recover"
denv_parser = sub_parser.add_parser("recover",
help="attempts to recover an env")
denv_parser.add_argument("customer_name",
type=str,
help="name of the customer")
denv_parser.add_argument("env_names",
type=str,
nargs="+",
help="name of one or more environments")
denv_parser.set_defaults(func=recover_env)
# Parse args
args = parser.parse_args(sys.argv[1:])
args.func(args)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -1 +1,4 @@
sudo apt-get -y install virtualbox vagrant ansible
#!/usr/bin/env bash
sudo apt update --yes && sudo apt upgrade --yes
sudo apt install --yes virtualbox vagrant ansible
ansible-galaxy collection install -r requirements.yml

4
requirements.yml Normal file
View File

@ -0,0 +1,4 @@
---
# ansible-galaxy requirements for this repository
collections:
- community.postgresql

View File

@ -1,4 +0,0 @@
#!/usr/bin/env bash
USER_DIR=./customers/$1
(cd $USER_DIR && vagrant destroy -f)
rm -rf $USER_DIR

View File

@ -1,6 +1,6 @@
---
# handlers file for nginx-webserver
- name: restart nginx
- name: reload nginx
ansible.builtin.service:
name: nginx
state: restarted

View File

@ -1,14 +1,36 @@
---
# tasks file for nginx-webserver
- name: Install nginx
- name: Install nginx and php
package:
name: nginx
name:
- nginx
- php7.4
- php7.4-fpm
- php7.4-cli
- php7.4-pgsql
state: present
update_cache: yes
become: true
notify: restart nginx
- name: Copy over index.html
- name: Copy over nginx.conf
ansible.builtin.template:
src: ./templates/index.html.j2
dest: /var/www/html/index.html
src: ./templates/nginx.cfg.j2
dest: /etc/nginx/sites-available/nginx.cfg
become: true
notify: reload nginx
- name: Enable nginx.conf
file:
src: /etc/nginx/sites-available/nginx.cfg
dest: /etc/nginx/sites-enabled/default
state: link
become: true
notify: reload nginx
- name: Remove nginx default crap
file:
state: absent
path: /var/www/html/*
become: true
- name: Copy over index.php
ansible.builtin.template:
src: ./templates/index.php.j2
dest: /var/www/html/index.php
become: true

View File

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello</title>
</head>
<body>
<p>Hostname: {{ ansible_facts.nodename }}</p>
<p>OS: {{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }}</p>
<p>Kernel: {{ ansible_facts.kernel }}</p>
<p>Memory usage: {{ ansible_facts.memfree_mb }}/{{ ansible_facts.memtotal_mb }}MB</p>
<p>Python version: {{ ansible_facts.python_version }}</p>
</body>
</html>

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello</title>
</head>
<body>
<h1><?php echo 'Hello, World!'; ?></h1>
<p>Hostname: {{ ansible_facts.nodename }}</p>
<p>OS: {{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }}</p>
<p>Kernel: {{ ansible_facts.kernel }}</p>
<p>Memory usage: {{ ansible_facts.memfree_mb }}/{{ ansible_facts.memtotal_mb }}MB</p>
<p>Python version: {{ ansible_facts.python_version }}</p>
{% if groups['postgresql'] is defined and groups['postgresql']|length > 0 %}
<p>
<?php
$db_handle = pg_connect("host={{ groups['postgresql'][0] }} dbname=test user=postgres password={{ hostvars[groups['postgresql'][0]]['psql_pass'] }}");
if ($db_handle) {
echo "Connection attempt succeeded.\n";
} else {
echo "Connection attempt failed.\n";
}
$query = "SELECT m.* FROM test.public.message m";
$result = pg_exec($db_handle, $query);
var_dump(pg_fetch_all($result));
?>
</p>
{% else %}
<p>No postgres instance detected.</p>
{% endif %}
</body>
</html>

View File

@ -0,0 +1,15 @@
server {
listen 80;
root /var/www/html;
index index.php;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
}
}

View File

@ -0,0 +1,2 @@
hello
world
1 hello
2 world

View File

@ -0,0 +1,2 @@
---
# handlers file for postgresql

View File

@ -0,0 +1,10 @@
galaxy_info:
author: strNophix
description: Postgresql Role
license: MIT
min_ansible_version: 2.1
galaxy_tags: []
dependencies: []

View File

@ -0,0 +1,72 @@
---
# tasks file for postgresql
- name: Install package dependencies
package:
name:
- postgresql
- postgresql-contrib
- python3-pip
- acl
state: present
update_cache: yes
become: true
- name: Install `psycopg2` driver for postgresql
pip:
name: psycopg2-binary
state: present
become: true
- name: Update `listen_address` in `/etc/postgresql/12/main/postgresql.conf`
lineinfile:
path: /etc/postgresql/12/main/postgresql.conf
regexp: ^#listen_addresses = 'localhost'
line: listen_addresses='*'
become: true
- name: Update `pg_hba.conf`
community.postgresql.postgresql_pg_hba:
dest: /etc/postgresql/12/main/pg_hba.conf
contype: host
users: postgres
source: 192.168.56.0/24
method: md5
create: true
become: true
- name: Create new test-database
become_user: postgres
become: yes
community.postgresql.postgresql_db:
name: test
- name: Create table `test`.`message`
become_user: postgres
become: yes
community.postgresql.postgresql_table:
db: test
name: message
columns:
- id bigserial primary key
- content text
- name: Copy over dummy-data for `test`.`message`
copy:
src: "{{ role_path }}/files/sample.csv"
dest: /tmp/sample.csv
- name: Insert sample data into `test`.`message`
become_user: postgres
become: yes
community.postgresql.postgresql_copy:
copy_from: /tmp/sample.csv
db: test
dst: message
columns: content
options:
format: csv
- name: Update password of postgres user
become_user: postgres
become: yes
community.postgresql.postgresql_user:
name: postgres
password: "{{ psql_pass }}"
- name: Restart postgresql service
service:
name: postgresql
state: restarted
enabled: yes
become: true

View File

@ -1,54 +0,0 @@
#!/usr/bin/env bash
VERSION="0.1.0"
function help() {
echo -e \
"Usage: $(basename $0) [OPTIONS] [COMMAND]\n\n" \
"Options:\n" \
" -i, --inv-file <path> Specify the Ansible inventory to add.\n" \
" -h, --help Show help.\n" \
" -v, --version Show version."
}
if [[ $# -eq 0 ]]; then
help
exit 1
fi
INVENTORY_FILE="$(pwd)/inventory"
while [[ $# -gt 0 ]]; do
case $1 in
-i|--inv-file)
INVENTORY_FILE="$2"
shift
shift
;;
-h|--help)
help
exit 1
;;
-v|--version)
echo $VERSION
exit 1
;;
-*|--*)
echo "hosto: unrecognized option '$1'"
help
exit 1
;;
*)
break
;;
esac
done
if [ -f $INVENTORY_FILE ]; then
sudo inv-alias add $INVENTORY_FILE
eval $@
sudo inv-alias rm $INVENTORY_FILE
else
echo "hosto: Could not find inventory file at $INVENTORY_FILE"
eval $@
fi

View File

@ -1,150 +0,0 @@
package main
import (
"bufio"
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strings"
)
type AliasMap map[string]string
const (
HostsFile string = "/etc/hosts"
)
func FixedSplit(s, sep string, parts int) []string {
n := make([]string, parts)
p := strings.SplitN(s, sep, parts)
copy(n, p)
return n
}
func IsLegalLine(line string) bool {
c := line[0]
return c != '[' && c != '#'
}
func BuildRegionString(regionName string, aliases AliasMap) string {
b := strings.Builder{}
b.WriteString("#region ")
b.WriteString(regionName)
b.WriteString("\n")
for ip, alias := range aliases {
b.WriteString(ip)
b.WriteString("\t")
b.WriteString(alias)
b.WriteString("\n")
}
b.WriteString("#endregion")
return b.String()
}
func BuildRegionRegexp(regionName string) *regexp.Regexp {
b := strings.Builder{}
b.WriteString("(?s)\n#region ")
b.WriteString(regexp.QuoteMeta(regionName))
b.WriteString(".*#endregion")
r := regexp.MustCompile(b.String())
return r
}
func ScanAliases(fileReader io.Reader) (AliasMap, error) {
aliasMap := AliasMap{}
scanner := bufio.NewScanner(fileReader)
for scanner.Scan() {
line := scanner.Text()
if IsLegalLine(line) {
s := FixedSplit(line, "#", 2)
ip, alias := strings.TrimSpace(s[0]), strings.TrimSpace(s[1])
if _, ok := aliasMap[ip]; !ok && alias != "" {
aliasMap[ip] = alias
}
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return aliasMap, nil
}
func AddAliases(fileName string) {
file, err := os.Open(fileName)
if err != nil {
log.Fatal(err)
}
h, err := ScanAliases(file)
if err != nil {
log.Fatal(err)
}
file.Close()
r := BuildRegionRegexp(fileName)
s := BuildRegionString(fileName, h)
content, err := ioutil.ReadFile(HostsFile)
if err != nil {
log.Fatal(err)
}
c := r.ReplaceAllString(string(content), s)
if !r.MatchString(c) {
c += ("\n" + s)
}
err = os.WriteFile(HostsFile, []byte(c[:]), fs.FileMode(os.O_WRONLY|os.O_TRUNC))
if err != nil {
log.Fatal(err)
}
}
func RemoveAliases(fileName string) {
regionReg := BuildRegionRegexp(fileName)
content, err := ioutil.ReadFile(HostsFile)
if err != nil {
log.Fatal(err)
}
c := regionReg.ReplaceAll(content, []byte(""))
err = os.WriteFile(HostsFile, c, fs.FileMode(os.O_WRONLY|os.O_TRUNC))
if err != nil {
log.Fatal(err)
}
}
func main() {
u := fmt.Sprintf("Please use: %s <add|rm> <file:path>\n", os.Args[0])
if len(os.Args) < 3 {
fmt.Println(u)
os.Exit(1)
}
p, err := filepath.Abs(os.Args[2])
if err != nil {
fmt.Println(err)
os.Exit(1)
}
switch os.Args[1] {
case "add":
AddAliases(p)
case "rm":
RemoveAliases(p)
default:
fmt.Println(u)
os.Exit(1)
}
}

View File

@ -1,47 +0,0 @@
#!/usr/bin/env bash
copy_template() {
cp -f ../../templates/$1 ./$2
}
write_inventory_group() {
local fmt="[$1]\n"
for ((i=$2; i<$2+$3; i++)) do
fmt+="192.168.56.$i\n"
done
echo -e $fmt >> ./inventory
}
# Take customer inputs
read -p "Klantnaam: " customerName
read -p "IpInt: " ipAddr
read -p "Number of webservers: " numWebserver
read -p "Number of loadbalancers: " numLoadbalancers
# Create customer directory and cd
mkdir -p ./customers/$customerName && cd $_
# Copy and fill-in necessary templates
copy_template ./Vagrantfile.template ./Vagrantfile
sed -i "s/#{customerName}/$customerName/" ./Vagrantfile
sed -i "s/#{ipAddr}/$ipAddr/" ./Vagrantfile
sed -i "s/#{numWebserver}/$numWebserver/" ./Vagrantfile
sed -i "s/#{numLoadbalancers}/$numLoadbalancers/" ./Vagrantfile
copy_template ./ansible.cfg.template ./ansible.cfg
# Generate ansible inventory file.
ipOffset=$ipAddr
write_inventory_group "webserver" $ipOffset $numWebserver
((ipOffset+=numWebserver))
write_inventory_group "loadbalancer" $ipOffset $numLoadbalancers
((ipOffset+=numLoadbalancers))
# Generate a new seperate ssh key for the customer
mkdir -p ./.ssh/
ssh-keygen -t rsa -b 2048 -f ./.ssh/id_rsa
# Provision and configure machines
vagrant up
ansible-playbook ../../site.yml

227
service.py Normal file
View File

@ -0,0 +1,227 @@
#!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path
from os import path
import subprocess as sub
import re
from typing import Any, Callable
from enum import Enum
P_BANNER = """
"""
P_OPTIONS = """
Opties:
0) Afsluiten
1) Maak/Wijzig omgeving(en)
2) Vernietig omgeving(en)
3) Recover omgeving(en)
4) List omgeving(en)
"""
class PortalOptions(Enum):
EXIT = "0"
CREATE = "1"
DESTROY = "2"
RECOVER = "3"
LIST = "4"
RE_TEXT = re.compile(r"^\w+$")
RE_NUM = re.compile(r"^\d+$")
RE_ANY = re.compile(r".+")
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)
def take_confirmation(self, prompt: str, default: str = "y") -> bool:
prompt = self._build_prompt(prompt)
i = Prompter.input(prompt, lambda x: x.lower() in ["y", "n"], default=default)
return i == "y"
@staticmethod
def input(prompt: str,
validate: Callable[[str], bool],
default: str = "") -> str:
while True:
i = input(prompt)
if i == "":
i = default
if validate(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:
print(P_BANNER)
customer_name = Prompter.input("Wat is uw naam: ",
lambda x: bool(RE_TEXT.match(x)))
"""
Ensures the necessary customer setup.
NOTE: The customer and the `envs` directory inside of it never get removed.
"""
envs_dir = path.join("customers", customer_name, "envs")
Path(envs_dir).mkdir(parents=True, exist_ok=True)
p = Prompter(ps1=f"{customer_name} > ")
while True:
print(P_OPTIONS)
choice = p.take_input("Keuze: ", RE_ANY)
print(end="\n")
if choice == PortalOptions.EXIT.value:
"""
Used to break out of the loop and exit the portal.
"""
break
if choice == PortalOptions.CREATE.value:
"""
Used to either update or create an environment.
"""
envs = get_env_list(customer_name)
print(
f"NOTE: Kies een albestaande omgevingen {envs} of iets nieuws..."
)
fmt = "Omgevingsnaam: "
env_name = p.take_input(fmt, RE_TEXT)
# Loop until customer has confirmed their desired combination of machines
while True:
templates = ["production", "acceptance", "test", "custom"]
fmt = f"Het type omgeving {templates}: "
template_name = p.take_choice(fmt, templates)
if template_name == "custom":
"""
Asks for all machine's individually
"""
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"
# TODO: migrate different machine types to a dict model
print(end="\n")
print("Deze omgeving bevat:")
if amnt_nginx_web > "0":
print(f" - {amnt_nginx_web} Nginx webserver(s)")
if amnt_nginx_lb > "0":
print(f" - {amnt_nginx_lb} Nginx loadbalancer(s)")
if amnt_psql > "0":
print(f" - {amnt_psql} Postgres instance(s)")
print(end="\n")
if p.take_confirmation("Bevestig (Y/n): "):
break
"""
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([
"python3", "cli.py", "create", customer_name, env_name,
"--num-nginx-web", amnt_nginx_web, "--num-postgres", amnt_psql,
"--num-nginx-lb", amnt_nginx_lb, "--ip-format", ip_format,
"--ip-int", ip_int
])
print(f"Omgeving `{env_name}` successvol gemaakt.")
if choice == PortalOptions.DESTROY.value:
"""
Deletes all traces of the chosen environment.
"""
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])
if choice == PortalOptions.RECOVER.value:
"""
Allows the customer to "force" their environment into the desired `up-state`.
This `up-state` is a representation of the Vagrantfile, Ansible inventory and
.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])
if choice == PortalOptions.LIST.value:
"""
This branch displays the customer's existing environments
"""
print("De volgende omgeving(en) zijn aanwezig: ")
for env in get_env_list(customer_name):
print(f" - {env}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -6,3 +6,7 @@
- hosts: loadbalancer
roles:
- nginx-loadbalancer
- hosts: postgresql
roles:
- postgresql

View File

@ -1,7 +1,7 @@
$ip_int = #{ipAddr}
$ip_int = {{ ip_int }}
def increment_ip()
ip = "192.168.56.%d" % [$ip_int]
ip = "{{ ip_format }}" % [$ip_int]
$ip_int += 1
return ip
end
@ -10,11 +10,11 @@ Vagrant.configure("2") do |config|
config.ssh.insert_key = false
config.ssh.private_key_path = ["./.ssh/id_rsa","~/.vagrant.d/insecure_private_key"]
num_webserver = #{numWebserver}
num_loadbalancer = #{numLoadbalancers}
num_postgresql = {{ num_postgres }}
(1..num_webserver).each do |nth|
machine_id = "#{customerName}-bloated-debian-web%d" % [nth]
{% if webserver_specs is not none %}
(1..{{ num_webserver }}).each do |nth|
machine_id = "{{ customer_name }}-{{ env }}-web%d" % [nth]
machine_ip = increment_ip()
config.vm.define machine_id do |web|
@ -25,15 +25,18 @@ Vagrant.configure("2") do |config|
web.vm.provision "file", source: "./.ssh/id_rsa.pub", destination: "~/.ssh/authorized_keys"
web.vm.provider "virtualbox" do |vb|
vb.memory = "1024"
vb.memory = {{ webserver_specs["mem"] }}
vb.cpus = {{ webserver_specs["cpus"] }}
vb.gui = false
vb.name = machine_id
end
end
end
{% endif %}
(1..num_loadbalancer).each do |nth|
machine_id = "#{customerName}-bloated-debian-lb%d" % [nth]
{% if loadbalancers_specs is not none %}
(1..{{ num_loadbalancers }}).each do |nth|
machine_id = "{{ customer_name }}-{{ env }}-lb%d" % [nth]
machine_ip = increment_ip()
config.vm.define machine_id do |web|
@ -44,10 +47,34 @@ Vagrant.configure("2") do |config|
web.vm.provision "file", source: "./.ssh/id_rsa.pub", destination: "~/.ssh/authorized_keys"
web.vm.provider "virtualbox" do |vb|
vb.memory = "1024"
vb.memory = {{ loadbalancers_specs["mem"] }}
vb.cpus = {{ loadbalancers_specs["cpus"] }}
vb.gui = false
vb.name = machine_id
end
end
end
{% endif %}
{% if postgres_specs is not none %}
(1..{{ num_postgres }}).each do |nth|
machine_id = "{{ customer_name }}-{{ env }}-db%d" % [nth]
machine_ip = increment_ip()
config.vm.define machine_id do |web|
web.vm.box = "ubuntu/focal64"
web.vm.hostname = machine_id
web.vm.network "private_network", ip: machine_ip
web.vm.provision "file", source: "./.ssh/id_rsa.pub", destination: "~/.ssh/authorized_keys"
web.vm.provider "virtualbox" do |vb|
vb.memory = {{ postgres_specs["mem"] }}
vb.cpus = {{ postgres_specs["cpus"] }}
vb.gui = false
vb.name = machine_id
end
end
end
{% endif %}
end

View File

@ -3,3 +3,6 @@ private_key_file = "./.ssh/id_rsa"
remote_user = "vagrant"
inventory = ./inventory
host_key_checking = False
[ssh_connection]
retries=2