diff --git a/autonomic/__main__.py b/autonomic/__main__.py index 06bfbf3..ea31c3a 100644 --- a/autonomic/__main__.py +++ b/autonomic/__main__.py @@ -4,7 +4,7 @@ import os import click -from autonomic.command import init +from autonomic.command import actions, init @click.group() @@ -31,3 +31,4 @@ def autonomic(ctx, debug): autonomic.add_command(init.init) +autonomic.add_command(actions.actions) diff --git a/autonomic/ansible.py b/autonomic/ansible.py new file mode 100644 index 0000000..2b86ed5 --- /dev/null +++ b/autonomic/ansible.py @@ -0,0 +1,12 @@ +"""Ansible management module.""" + +from autonomic.settings import INFRASTRUCTURE_PATH +from autonomic.system import run_command + + +def run_ansible_playbook(play, extra_env=None): + """Run ansible-playbook against a play.""" + command = [".venv/bin/ansible-playbook", play] + run_command( + command, cwd=INFRASTRUCTURE_PATH, output=True, extra_env=extra_env + ) diff --git a/autonomic/command/actions.py b/autonomic/command/actions.py new file mode 100644 index 0000000..0563751 --- /dev/null +++ b/autonomic/command/actions.py @@ -0,0 +1,65 @@ +"""Ansible Actions module.""" + +import click +from PyInquirer import prompt + +from autonomic import logger +from autonomic.ansible import run_ansible_playbook +from autonomic.passstore import get_from_pass +from autonomic.settings import ACTIONS_PATH + +log = logger.get_logger(__name__) + + +@click.command() +@click.pass_context +def actions(ctx): + """Run an Ansible action.""" + question = [ + { + "type": "list", + "name": "action", + "message": "Which Ansible action do you want to run?", + "choices": [ + {"name": "addusers"}, + {"name": "newhetzner"}, + {"name": "rmhetzner"}, + {"name": "newdokku"}, + ], + "filter": lambda val: val.lower(), + } + ] + action_answer = prompt(question) + + env = {} + if ( + action_answer["action"] == "newhetzner" + or action_answer["action"] == "rmhetzner" + ): + question = [ + { + "type": "list", + "name": "key", + "message": "Which Hetzner API key do you need?", + "choices": [ + {"name": "prod"}, + {"name": "test"}, + {"name": "cicd"}, + ], + "filter": lambda val: val.lower(), + } + ] + + key_answer = prompt(question) + path = "logins/hetzner/{}/api_key".format(key_answer["key"]) + secret = get_from_pass(path) + env["HCLOUD_TOKEN"] = secret + + play = "{}/{}.yml".format(ACTIONS_PATH, action_answer["action"]) + + run_ansible_playbook(play, extra_env=env) + + # TODO(decentral1se): + # git commit and push on infrastructure if we: + # 1. ran an action that adds something new to the pass store + # 2. added another machine to the inventory diff --git a/autonomic/command/init.py b/autonomic/command/init.py index 5e6649a..f83f6d2 100644 --- a/autonomic/command/init.py +++ b/autonomic/command/init.py @@ -15,7 +15,7 @@ from autonomic.settings import ( INFRASTRUCTURE_PATH, INFRASTRUCTURE_REPOSITORY, ) -from autonomic.system import ensure_installed, run_command +from autonomic.system import run_command log = logger.get_logger(__name__) @@ -25,9 +25,10 @@ log = logger.get_logger(__name__) def init(ctx): """Initialise the toolbelt.""" create_configuration_directory() - clone_infrastructure_repo() create_configuration_file() + clone_infrastructure_repo() ask_to_login() + install_dependencies() def create_configuration_directory(): @@ -39,13 +40,13 @@ def create_configuration_directory(): def clone_infrastructure_repo(): """Clone the infrastructure repository.""" if not os.path.exists(INFRASTRUCTURE_PATH): - ensure_installed("git") run_command( ["git", "clone", INFRASTRUCTURE_REPOSITORY, INFRASTRUCTURE_PATH] ) else: - os.chdir(INFRASTRUCTURE_PATH) - run_command(["git", "pull", "origin", "master"]) + run_command( + ["git", "pull", "origin", "master"], cwd=INFRASTRUCTURE_PATH + ) def ask_to_login(): @@ -75,3 +76,20 @@ def create_configuration_file(): if not os.path.exists(AUTONOMIC_YAML): with open(AUTONOMIC_YAML, "w") as handle: handle.write("---") + + +def install_dependencies(): + """Install infrastructure dependencies.""" + run_command( + ["/usr/bin/python3", "-m", "venv", ".venv"], cwd=INFRASTRUCTURE_PATH, + ) + + run_command( + [".venv/bin/pip", "install", "-r", "requirements.txt"], + cwd=INFRASTRUCTURE_PATH, + ) + + run_command( + [".venv/bin/ansible-galaxy", "install", "-r", "requirements.yml"], + cwd=INFRASTRUCTURE_PATH, + ) diff --git a/autonomic/config.py b/autonomic/config.py index 49aad16..ba37f7c 100644 --- a/autonomic/config.py +++ b/autonomic/config.py @@ -32,3 +32,17 @@ def add_to_config(data): with open(AUTONOMIC_YAML, "w") as handle: yaml.dump(config, handle) + + +def get_from_config(key): + """Get values from the autonomic.yml file.""" + ensure_config() + + with open(AUTONOMIC_YAML, "r") as handle: + config = yaml.load(handle.read()) + + try: + return config[key] + except KeyError: + msg = "Unable to retrieve {} from {}".format(key, AUTONOMIC_YAML) + exit_with_msg(msg) diff --git a/autonomic/passstore.py b/autonomic/passstore.py new file mode 100644 index 0000000..1b6a3a1 --- /dev/null +++ b/autonomic/passstore.py @@ -0,0 +1,22 @@ +"""Password store management module.""" + +import os +import subprocess + +from autonomic.settings import INFRASTRUCTURE_PATH, PASSWORD_STORE_PATH +from autonomic.system import exit_with_msg + + +def get_from_pass(path): + """Retrieve a password from the password store.""" + env = os.environ.copy() + env.update({"PASSWORD_STORE_DIR": PASSWORD_STORE_PATH}) + + try: + os.chdir(INFRASTRUCTURE_PATH) + command = ["pass", "show", path] + output = subprocess.check_output(command, env=env) + return output.decode("utf-8").strip() + except subprocess.CalledProcessError as exception: + msg = "{} failed! Saw {}".format(" ".join(command), str(exception)) + exit_with_msg(msg) diff --git a/autonomic/settings.py b/autonomic/settings.py index 409f8a9..f0acf7d 100644 --- a/autonomic/settings.py +++ b/autonomic/settings.py @@ -10,4 +10,8 @@ INFRASTRUCTURE_REPOSITORY = ( ) INFRASTRUCTURE_PATH = "{}/infrastructure".format(CONFIG_PATH) MEMBERS_YAML = "{}/resources/members.yml".format(INFRASTRUCTURE_PATH) +ACTIONS_PATH = "{}/actions".format(INFRASTRUCTURE_PATH) AUTONOMIC_YAML = "{}/autonomic.yml".format(CONFIG_PATH) +PASSWORD_STORE_PATH = "{}/credentials/password-store".format( + INFRASTRUCTURE_PATH +) diff --git a/autonomic/system.py b/autonomic/system.py index 941aa18..82f9dd3 100644 --- a/autonomic/system.py +++ b/autonomic/system.py @@ -1,26 +1,43 @@ """System related functions.""" -import shutil +import os import subprocess import sys from autonomic import logger +from autonomic.settings import PASSWORD_STORE_PATH log = logger.get_logger(__name__) -def ensure_installed(package): - """Ensure a system dependency is installed""" - if shutil.which(package) is None: - msg = "{} is not installed?".format(package) - exit_with_msg(msg) - - -def run_command(command): +def run_command(command, cwd=None, output=False, extra_env=None): """Run a command.""" + from autonomic.config import get_from_config + + username = get_from_config("username") + + env = os.environ.copy() + env.update({"PASSWORD_STORE_DIR": PASSWORD_STORE_PATH}) + env.update({"REMOTE_USER": username}) + env.update({"ANSIBLE_USER": username}) + + for key, val in extra_env.items(): + env.update({key: val}) + try: - log.info("Running '{}'".format(" ".join(command))) - subprocess.check_output(command) + if cwd: + os.chdir(cwd) + + log.info( + "Running {}{}".format( + " ".join(command), " in {}".format(cwd) if cwd else "" + ) + ) + + if output: + subprocess.call(command, env=env) + else: + subprocess.check_output(command, env=env, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as exception: msg = "{} failed! Saw {}".format(" ".join(command), str(exception)) exit_with_msg(msg)