From 07b1a6d4fef2457955eadebd2ac4cd11aa3da09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= <michael.loipfuehrer@tum.de> Date: Sun, 19 May 2019 18:04:50 +0200 Subject: [PATCH] management script --- .gitignore | 3 + README.md | 29 ++++++ Readme.md | 0 container/bootstrap.sh | 5 + container/nginx | 9 ++ container/nspawn | 8 ++ container/sshd_config | 121 +++++++++++++++++++++++ lustmolch.py | 211 ++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + sites-enabled/lustmolch | 10 -- sites-enabled/pot | 13 --- www/index.html | 10 -- 12 files changed, 388 insertions(+), 33 deletions(-) create mode 100644 .gitignore create mode 100644 README.md delete mode 100644 Readme.md create mode 100755 container/bootstrap.sh create mode 100644 container/nginx create mode 100644 container/nspawn create mode 100644 container/sshd_config create mode 100755 lustmolch.py create mode 100644 requirements.txt delete mode 100644 sites-enabled/lustmolch delete mode 100644 sites-enabled/pot delete mode 100644 www/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8cedc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.env +__pycache__ +containers.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ff7208 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Der Lustmolch +Allgemeiner container host für (semi-offizielle) Stusta websites. + +## How-To +Alle management Befehle laufen entweder über `lustmolch.py` oder über `machinectl`. + +### Container erstellen +```bash +python3 lustmolch.py create-container <container-name> +``` +Der container wird als basic debian image in `/var/lib/machines` angelegt und `bootstrap.sh` wird ausgeführt. +Dabei werden nötige Pakete installiert, alle Konfigurationsfiles sowohl auf dem host als auch im Container abgelegt. +Außerdem wird im container **openssh-server** installiert und gestartet. Der Port wird dynamisch auf den ersten +freien Port ab **10022** in Inkrementen von **1000** gesetzt. + +Die templates für Konfigurationsfiles liegen im directory **container**. + +### Container VERNICHTEN +```bash +python3 lustmolch.py remove-container <container-name> +``` +Der Container und alle Konfigurationsfiles auf dem Host werden gelöscht. + +### SSH Key installieren +```bash +python3 lustmolch.py install-ssh-key <container-name> <path-to-ssh-key> +``` +Der angegeben SSH key wird in `/root/.ssh/authorized_keys` kopiert. Es ist möglich den key als String-Parameter +zu übergeben, dabei muss das Flag `--key-string` gesetzt sein. \ No newline at end of file diff --git a/Readme.md b/Readme.md deleted file mode 100644 index e69de29..0000000 diff --git a/container/bootstrap.sh b/container/bootstrap.sh new file mode 100755 index 0000000..7d85d9e --- /dev/null +++ b/container/bootstrap.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +apt-get update +apt-get -y upgrade +apt-get -y install dbus openssh-server vim \ No newline at end of file diff --git a/container/nginx b/container/nginx new file mode 100644 index 0000000..8272ec3 --- /dev/null +++ b/container/nginx @@ -0,0 +1,9 @@ +server { + listen 80; + server_name {{name}}.stusta.de; + charset utf-8; + + location /static { + alias /var/www/{{name}}; + } +} \ No newline at end of file diff --git a/container/nspawn b/container/nspawn new file mode 100644 index 0000000..bc26999 --- /dev/null +++ b/container/nspawn @@ -0,0 +1,8 @@ +[Network] +VirtualEthernet=no + +[Files] +Bind=/var/www/{{name}}:/var/www + +[Exec] +PrivateUsers=off \ No newline at end of file diff --git a/container/sshd_config b/container/sshd_config new file mode 100644 index 0000000..6c252b8 --- /dev/null +++ b/container/sshd_config @@ -0,0 +1,121 @@ +# $OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $ + +# This is the sshd server system-wide configuration file. See +# sshd_config(5) for more information. + +# This sshd was compiled with PATH=/usr/bin:/bin:/usr/sbin:/sbin + +# The strategy used for options in the default sshd_config shipped with +# OpenSSH is to specify options with their default value where +# possible, but leave them commented. Uncommented options override the +# default value. + +Port {{ssh_port}} +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#HostKey /etc/ssh/ssh_host_rsa_key +#HostKey /etc/ssh/ssh_host_ecdsa_key +#HostKey /etc/ssh/ssh_host_ed25519_key + +# Ciphers and keying +#RekeyLimit default none + +# Logging +#SyslogFacility AUTH +#LogLevel INFO + +# Authentication: + +#LoginGraceTime 2m +PermitRootLogin prohibit-password +#StrictModes yes +#MaxAuthTries 6 +#MaxSessions 10 + +PubkeyAuthentication yes + +# Expect .ssh/authorized_keys2 to be disregarded by default in future. +#AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2 + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + +# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts +#HostbasedAuthentication no +# Change to yes if you don't trust ~/.ssh/known_hosts for +# HostbasedAuthentication +#IgnoreUserKnownHosts no +# Don't read the user's ~/.rhosts and ~/.shosts files +#IgnoreRhosts yes + +# To disable tunneled clear text passwords, change to no here! +#PasswordAuthentication yes +#PermitEmptyPasswords no + +# Change to yes to enable challenge-response passwords (beware issues with +# some PAM modules and threads) +ChallengeResponseAuthentication no + +# Kerberos options +#KerberosAuthentication no +#KerberosOrLocalPasswd yes +#KerberosTicketCleanup yes +#KerberosGetAFSToken no + +# GSSAPI options +#GSSAPIAuthentication no +#GSSAPICleanupCredentials yes +#GSSAPIStrictAcceptorCheck yes +#GSSAPIKeyExchange no + +# Set this to 'yes' to enable PAM authentication, account processing, +# and session processing. If this is enabled, PAM authentication will +# be allowed through the ChallengeResponseAuthentication and +# PasswordAuthentication. Depending on your PAM configuration, +# PAM authentication via ChallengeResponseAuthentication may bypass +# the setting of "PermitRootLogin without-password". +# If you just want the PAM account and session checks to run without +# PAM authentication, then enable this but set PasswordAuthentication +# and ChallengeResponseAuthentication to 'no'. +UsePAM yes + +#AllowAgentForwarding yes +#AllowTcpForwarding yes +#GatewayPorts no +X11Forwarding yes +#X11DisplayOffset 10 +#X11UseLocalhost yes +#PermitTTY yes +PrintMotd no +#PrintLastLog yes +#TCPKeepAlive yes +#PermitUserEnvironment no +#Compression delayed +#ClientAliveInterval 0 +#ClientAliveCountMax 3 +#UseDNS no +#PidFile /var/run/sshd.pid +#MaxStartups 10:30:100 +#PermitTunnel no +#ChrootDirectory none +#VersionAddendum none + +# no default banner path +#Banner none + +# Allow client to pass locale environment variables +AcceptEnv LANG LC_* + +# override default of no subsystems +Subsystem sftp /usr/lib/openssh/sftp-server + +# Example of overriding settings on a per-user basis +#Match User anoncvs +# X11Forwarding no +# AllowTcpForwarding no +# PermitTTY no +# ForceCommand cvs server diff --git a/lustmolch.py b/lustmolch.py new file mode 100755 index 0000000..28ad460 --- /dev/null +++ b/lustmolch.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +import json +import shutil +from collections import namedtuple +from subprocess import run +from pathlib import Path +from jinja2 import Environment, PackageLoader +import click + + +env = Environment(loader=PackageLoader('lustmolch', 'container')) +cfg_template = namedtuple('cfg_template', ['source', 'path', 'filename']) + +template_files_host = [ + cfg_template('nginx', Path('/etc/nginx/sites-available'), '{name}'), + cfg_template('nspawn', Path('/etc/systemd/nspawn'), '{name}.nspawn') +] +template_files_container = [ + cfg_template('sshd_config', Path('/etc/ssh'), 'sshd_config') +] + +FLAVOUR = 'buster' +DEBIAN_MIRROR = 'http://mirror.stusta.de/debian' + +www_root = Path('/var/www') + +SSH_START_PORT = 10022 +SSH_PORT_INCREMENT = 1000 + + +def next_ssh_port(config_file, name): + """ + Return the next available port for the containers ssh server to run on. + If the container is already present in the list of installed containers + returns the configured port. + Args: + config_file: Path to container configuration file (containers.json) + name: Container name + + Returns: SSH port + + """ + if not Path(config_file).exists(): + cfg = {} + else: + with open(config_file, 'r') as f: + cfg = json.load(f) + + if name in cfg: + return cfg.get(name).get('ssh_port') + port = SSH_START_PORT + for container in cfg.items(): + if container.get('ssh_port') >= port: + port = container.get('ssh_port') + SSH_PORT_INCREMENT + + return port + + +def update_config(config_file, name, container): + if not Path(config_file).exists(): + with open(config_file, 'w+') as f: + cfg = {name: container} + json.dump(cfg, f, indent=4) + else: + with open(config_file, 'r') as f: + cfg = json.load(f) + cfg[name] = container + with open(config_file, 'w') as f: + json.dump(cfg, f, indent=4) + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option('--dry-run', is_flag=True, default=False) +@click.option('--config-file', default='containers.json', help='Container configuration file') +@click.argument('name') +def create_container(dry_run, config_file, name): + if dry_run: + click.echo(f'Doing a dry run') + + # create shared folder for html static files + www_dir = www_root / name + click.echo(f'Creating shared www directory "{www_dir}"') + if not dry_run: + www_dir.mkdir(parents=True, exist_ok=True) + + # place configuration files + context = { + 'name': name, + 'ssh_port': next_ssh_port(config_file, name) + } + for cfg in template_files_host: + template = env.get_template(cfg.source) + file_name = cfg.path / (cfg.filename.format(**context)) + click.echo(f'Placing config file {file_name}') + if not dry_run: + with open(file_name, 'w+') as cfg_file: + cfg_file.write(template.render(context)) + + # create machine + machine_path = Path('/var/lib/machines', name) + click.echo(f'Running debootstrap') + if not dry_run: + run(['debootstrap', FLAVOUR, machine_path, DEBIAN_MIRROR], capture_output=True, check=True) + + # start container for the first time + # click.echo(f'Starting container for the first time') + # if not dry_run: + # run(['systemd-nspawn', '-D', machine_path], check=True) + + click.echo(f'Bootstrapping container') + if not dry_run: + # copy and run bootstrap shell script + script_location = '/opt/bootstrap.sh' + script_location_host = str(machine_path) + script_location + + shutil.copy('container/bootstrap.sh', script_location_host) + Path(script_location_host).chmod(0o755) + run(['systemd-nspawn', '-D', str(machine_path), script_location], check=True) + + click.echo(f'Copying config files into container') + if not dry_run: + for cfg in template_files_container: + template = env.get_template(cfg.source) + file_name = cfg.filename.format(**context) + click.echo(f'Placing config file {file_name}') + if not dry_run: + with open(Path(f'{machine_path}{cfg.path}/{file_name}'), 'w+') as f: + f.write(template.render(context)) + + click.echo(f'Starting container') + if not dry_run: + run(['machinectl', 'start', name], capture_output=True, check=True) + + click.echo(f'Updating container configuration file') + if not dry_run: + update_config(config_file, name, container=context) + + click.echo(f'All done, ssh server running on port {context["ssh_port"]}') + + +@cli.command() +@click.option('--config-file', default='containers.json', help='Container configuration file') +@click.option('--key-string', is_flag=True, default=False) +@click.argument('name') +@click.argument('key') +def install_ssh_key(config_file, key_string, name, key): + ssh_dir = Path('/var/lib/machines', name, 'root/.ssh') + authorized_keys = ssh_dir / 'authorized_keys' + if key_string: + key_string = key + else: + with open(key, 'r') as f: + key_string = f.read() + + click.echo(f'Appending ssh key\n{key_string} to {authorized_keys}') + ssh_dir.mkdir(mode=0o700, parents=True, exist_ok=True) + with open(authorized_keys, 'a+') as f: + f.write(key_string) + authorized_keys.chmod(0o600) + + +@cli.command() +@click.option('--config-file', default='containers.json', help='Container configuration file') +@click.argument('name') +def remove_container(config_file, name): + machine_path = Path('/var/lib/machines', name) + + # removing shared folder + www_dir = www_root / name + click.echo(f'Removing shared www folder') + try: + shutil.rmtree(www_dir, ignore_errors=True) + except OSError as e: + click.echo(f'{e} ignored when removing container') + + # deleting placed config files + for cfg in template_files_host: + file_name = cfg.path / cfg.filename.format(name=name) + click.echo(f'Removing config file {file_name}') + try: + file_name.unlink() + except OSError as e: + click.echo(f'{e} ignored when removing file {file_name}') + + # delete container itself + click.echo(f'Removing container') + try: + shutil.rmtree(machine_path, ignore_errors=True) + except OSError as e: + click.echo(f'{e} ignored when removing container') + + # remove container from configuration file + click.echo(f'Updating configuration file') + try: + with open(config_file, 'r') as f: + cfg = json.load(f) + if name in cfg: + del cfg[name] + with open(config_file, 'w') as f: + json.dump(cfg, f, indent=4) + except OSError as e: + click.echo(f'{e} ignored when updating config file') + + +if __name__ == '__main__': + cli() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f1fa21e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +click >= 7.0 +jinja2 >= 2.10 \ No newline at end of file diff --git a/sites-enabled/lustmolch b/sites-enabled/lustmolch deleted file mode 100644 index 95e7338..0000000 --- a/sites-enabled/lustmolch +++ /dev/null @@ -1,10 +0,0 @@ -server { - - listen 80; - server_name lustmolch.stusta.de; - charset utf-8; - - location / { - alias /var/www/html; - } -} diff --git a/sites-enabled/pot b/sites-enabled/pot deleted file mode 100644 index ed7b332..0000000 --- a/sites-enabled/pot +++ /dev/null @@ -1,13 +0,0 @@ -server { - - listen 80; - server_name pot.stusta.de; - charset utf-8; - - location / { - proxy_pass http://localhost:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } -} diff --git a/www/index.html b/www/index.html deleted file mode 100644 index 466da21..0000000 --- a/www/index.html +++ /dev/null @@ -1,10 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <title>Lustmolch</title> -</head> -<body> - <h1>THIS IS LUSTMOLCH</h1> -</body> -</html> \ No newline at end of file -- GitLab