From a178d0ed7cc77bae6eb82f6a980e8808c36e483b Mon Sep 17 00:00:00 2001 From: Luke Murphy Date: Sun, 12 Apr 2020 15:41:30 +0200 Subject: [PATCH] Add coophost and coopaas experimental commands --- CHANGELOG.rst | 22 +++++++-- autonomic/__main__.py | 4 +- autonomic/command/actions.py | 11 ++--- autonomic/command/coophost.py | 88 +++++++++++++++++++++++++++++++++++ autonomic/command/cooppaas.py | 12 +++++ autonomic/command/init.py | 12 ++--- autonomic/settings.py | 4 +- autonomic/utils.py | 85 +++++++++++++++++++++++++++++---- setup.cfg | 1 + test/test_utils.py | 16 +------ 10 files changed, 214 insertions(+), 41 deletions(-) create mode 100644 autonomic/command/coophost.py create mode 100644 autonomic/command/cooppaas.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6544af4..6b76bfd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,11 +1,27 @@ -Autonomic 0.0.2 (2020-04-11) +Autonomic 0.0.4 (2020-04-12) ============================ Features -------- -- Init command. (#2) -- Actions command. (#3) +- Add CoopHost / CoopPass (WIP) commands. (#4) +- Further sanity checks. (#5) + + +Autonomic 0.0.3 (2020-04-11) +============================ + +Features +-------- + +- Add init command. (#2) +- Add Ansible actions command. (#3) + + +Autonomic 0.0.2 (2020-04-11) +============================ + +- Woops, missed a tag! Autonomic 0.0.1 (2020-04-03) diff --git a/autonomic/__main__.py b/autonomic/__main__.py index 7d1911e..b966355 100644 --- a/autonomic/__main__.py +++ b/autonomic/__main__.py @@ -2,7 +2,7 @@ import click -from autonomic.command import actions, init +from autonomic.command import actions, coophost, cooppaas, init @click.group() @@ -25,3 +25,5 @@ def autonomic(ctx): autonomic.add_command(init.init) autonomic.add_command(actions.actions) +autonomic.add_command(coophost.coophost) +autonomic.add_command(cooppaas.cooppaas) diff --git a/autonomic/command/actions.py b/autonomic/command/actions.py index 56358da..195a2b2 100644 --- a/autonomic/command/actions.py +++ b/autonomic/command/actions.py @@ -3,18 +3,19 @@ from os import environ import click -from PyInquirer import prompt from autonomic.config import ACTIONS_DIR, INFRA_DIR, PASS_STORE_DIR from autonomic.infra import get_passwd, run_play from autonomic.settings import get -from autonomic.utils import git_status, qlist +from autonomic.utils import ensure_config_dir, git_status, question_ask @click.command() @click.pass_context def actions(ctx): """Run an Ansible action.""" + ensure_config_dir() + env = environ.copy() env.update({"ANSIBLE_USER": get("username")}) @@ -22,13 +23,11 @@ def actions(ctx): env.update({"PASSWORD_STORE_DIR": PASS_STORE_DIR}) choices = ["addusers", "newhetzner", "rmhetzner", "newdokku", "pingall"] - question = qlist("action", "Which Ansible action?", choices,) - action = prompt(question)["action"] + action = question_ask("action", "Which Ansible action?", choices) if any(action in choice for choice in ["newhetzner", "rmhetzner"]): choices = ["prod", "test", "cicd"] - question = qlist("key", "Which Hetzner API key?", choices) - key = prompt(question)["key"] + key = question_ask("key", "Which Hetzner API key?", choices) path = "logins/hetzner/{}/api_key".format(key) secret = get_passwd(path) diff --git a/autonomic/command/coophost.py b/autonomic/command/coophost.py new file mode 100644 index 0000000..cf51168 --- /dev/null +++ b/autonomic/command/coophost.py @@ -0,0 +1,88 @@ +"""CoopHost module.""" + +from os import chdir, mkdir +from os.path import basename, exists +from pathlib import Path + +import click + +from autonomic.config import CONFIG_YAML, INFRA_DIR +from autonomic.logger import log +from autonomic.settings import add, get +from autonomic.utils import ( + ensure_config_dir, + ensure_deploy_d_dir, + input_ask, + pass_ask, + question_ask, + run, + yaml_dump, + yaml_load, +) + + +@click.command() +@click.pass_context +def coophost(ctx): + """Manage CoopHost resources.""" + ensure_config_dir() + + choices = ["encrypt"] + operation = question_ask("operation", "Which operation?", choices) + + if operation == "encrypt": + encrypt() + + +def encrypt(): + """Encrypt a secret for a CoopHost package.""" + ensure_deploy_d_dir() + + app_dir = Path(".").absolute() + + app = basename(Path(".").absolute()) + log.info("Auto-detected the {} application".format(app)) + + app_settings = get(app) + if app_settings is not None and "vault-password" in app_settings: + log.info("Using app vault password stored in {}".format(CONFIG_YAML)) + vault_password = app_settings["vault-password"] + else: + log.info("No app vault password configured") + vault_password = pass_ask("Vault password?") + + log.info("App vault password stored in {}".format(CONFIG_YAML)) + add({app: {"vault-password": vault_password}}) + + name = input_ask("Which variable do you want to encrypt?") + value = pass_ask("Variable value to encrypt?") + + cmd = [".venv/bin/ansible-vault", "encrypt_string", "--name", name, value] + encrypted = run( + cmd, + cwd=INFRA_DIR, + pexpect=True, + pexpected={ + "(?i)new vault password:": vault_password, + "(?i)confirm new vault password:": vault_password, + }, + ) + encrypted = ( + encrypted.strip() + .replace("\r", "") + .replace("\nEncryption successful", "") + ) + + chdir(app_dir) + log.info("Changed directory back to to {}".format(app_dir)) + + vault_path = (Path(".") / "deploy.d" / "vault").absolute() + if not exists(vault_path): + log.info("Creating {}".format(vault_path)) + mkdir(vault_path) + + var_path = (vault_path / "{}.yml".format(name)).absolute() + with open(var_path, "w"): + loaded = yaml_load(encrypted, text=True) + yaml_dump(var_path, loaded) + log.success("Encrypted and saved {} in {}".format(name, var_path)) diff --git a/autonomic/command/cooppaas.py b/autonomic/command/cooppaas.py new file mode 100644 index 0000000..bef9bd0 --- /dev/null +++ b/autonomic/command/cooppaas.py @@ -0,0 +1,12 @@ +"""CoopPaas module.""" + +import click + +from autonomic.logger import log + + +@click.command() +@click.pass_context +def cooppaas(ctx): + """Manage CoopPaas resources.""" + log.info("Still experimenting...") diff --git a/autonomic/command/init.py b/autonomic/command/init.py index 73a1777..9c3e0d8 100644 --- a/autonomic/command/init.py +++ b/autonomic/command/init.py @@ -7,13 +7,12 @@ from subprocess import STDOUT import click from emoji import emojize -from PyInquirer import prompt from autonomic.config import CONFIG_DIR, CONFIG_YAML, INFRA_DIR, INFRA_REPO from autonomic.infra import members from autonomic.logger import log from autonomic.settings import add -from autonomic.utils import is_proc, qlist, run +from autonomic.utils import is_proc, question_ask, run @click.command() @@ -53,11 +52,12 @@ def clone_infrastructure_repo(): def store_username(): """Store Autonomic username in the settings.""" usernames = members(flatten="username") - question = qlist("username", "What is you Autonomic username?", usernames) - answer = prompt(question) - add(answer) + username = question_ask( + "username", "What is you Autonomic username?", usernames, + ) + add({"username": username}) - msg = "Welcome comrade {} :kissing:".format(answer["username"]) + msg = "Welcome comrade {} :kissing:".format(username) log.success(emojize(msg, use_aliases=True)) diff --git a/autonomic/settings.py b/autonomic/settings.py index 5ec00be..bc94627 100644 --- a/autonomic/settings.py +++ b/autonomic/settings.py @@ -5,11 +5,13 @@ is useful for storing information for future lookup when dealing with repetitive cooperative tasks. """ +from typing import Dict + from autonomic.config import CONFIG_YAML from autonomic.utils import yaml_dump, yaml_load -def add(yaml): +def add(yaml: Dict[str, str]): """Add YAML to the settings file.""" from autonomic.command.init import create_configuration diff --git a/autonomic/utils.py b/autonomic/utils.py index e69d5e3..76e0d22 100644 --- a/autonomic/utils.py +++ b/autonomic/utils.py @@ -8,16 +8,23 @@ things here. """ from os import chdir +from os.path import exists +from pathlib import Path from subprocess import call, check_output +from sys import exit as sys_exit +from pexpect import spawn from psutil import process_iter +from PyInquirer import prompt -from autonomic.config import INFRA_DIR +from autonomic.config import CONFIG_DIR, CONFIG_YAML, INFRA_DIR from autonomic.logger import log from autonomic.yaml import yaml -def run(cmd, cwd=None, interactive=False, **kwargs): +def run( + cmd, cwd=None, interactive=False, pexpect=False, pexpected=None, **kwargs +): """Run a system command. Please note, all **kwargs will be passed into the check_output command so @@ -34,6 +41,17 @@ def run(cmd, cwd=None, interactive=False, **kwargs): if interactive: return call(cmd, **kwargs) + if pexpect: + child = spawn(" ".join(cmd)) + for expected, response in pexpected.items(): + try: + child.expect(expected, timeout=5) + child.sendline(response) + except Exception as exception: + msg = "Pexpecting failed, saw {}".format(str(exception)) + exit(msg) + return child.read().decode("utf-8") + output = check_output(cmd, **kwargs) return output.decode("utf-8") except Exception as exception: @@ -48,12 +66,12 @@ def exit(msg, code=1): else: log.info(msg) - exit(code) + sys_exit(code) -def qlist(name, message, choices): - """A question in list format.""" - return [ +def question_ask(name, message, choices): + """Ask a question.""" + question = [ { "type": "list", "name": name, @@ -63,11 +81,44 @@ def qlist(name, message, choices): } ] + return answer(question, name) + + +def pass_ask(message): + """Ask for a password.""" + question = [{"type": "password", "message": message, "name": "password"}] + return answer(question, "password") + + +def input_ask(message): + """Ask for input.""" + question = [{"type": "input", "message": message, "name": "input"}] + return answer(question, "input") + + +def answer(question, key): + """Retrieve an answer from a prompt""" + result = prompt(question) + + if not result: + exit("Prompt cancelled") -def yaml_load(fpath): - """Load a YAML file.""" try: - with open(fpath, "r") as handle: + return result[key] + except KeyError: + exit("{} key is missing".format(key)) + + +def yaml_load(target, text=False): + """Load a YAML file or text.""" + if text is True: + try: + return yaml.load(target) + except Exception as exception: + log.error(str(exception)) + + try: + with open(target, "r") as handle: return yaml.load(handle.read()) except Exception as exception: log.error(str(exception)) @@ -105,3 +156,19 @@ def git_status(fpath): else: msg = "No unstaged changes found in {}".format(INFRA_DIR) log.info(msg) + + +def ensure_config_dir(): + """Ensure configuration directory is in place.""" + if not exists(CONFIG_DIR): + msg = "{} is missing, did you run 'autonomic init'?".format(CONFIG_YAML) + exit(msg) + + +def ensure_deploy_d_dir(): + """Ensure deploy.d directory is in place.""" + deploy_d_dir = (Path(".") / "deploy.d").absolute() + + if not exists(deploy_d_dir): + msg = "No deploy.d folder found, are you in the right place?" + exit(msg) diff --git a/setup.cfg b/setup.cfg index 39e41ca..90fe228 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,7 @@ install_requires = click >= 7.1.1, <= 8.0 colorama >= 0.4.3, <= 0.5 emoji >= 0.5.4, <= 0.6 + pexpect >= 4.8.0, <= 4.9 psutil >= 5.7.0, <= 6.0 pyinquirer >= 1.0.3, <= 1.1 ruamel.yaml >= 0.16.10, <= 0.17 diff --git a/test/test_utils.py b/test/test_utils.py index 9edf2ac..0a7b164 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,6 +1,6 @@ from subprocess import STDOUT -from autonomic.utils import is_proc, qlist, run, yaml_dump, yaml_load +from autonomic.utils import is_proc, run, yaml_dump, yaml_load def test_run_kwargs(): @@ -18,20 +18,6 @@ def test_run_cwd(tmp_path): assert "testfile.txt" in output -def test_make_qlist(): - output = qlist("foo", "bar", ["bang"]) - - expected = { - "type": "list", - "name": "foo", - "message": "bar", - "choices": ["bang"], - } - - for key, val in expected.items(): - assert expected[key] == output[0][key] - - def test_yaml_load(tmp_path): directory = tmp_path / "test" directory.mkdir()