Re-work hacking from last night

This commit is contained in:
Luke Murphy 2020-04-11 21:10:29 +02:00
parent c6608c5568
commit b58b9c3296
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
14 changed files with 253 additions and 282 deletions

View File

@ -1,21 +1,13 @@
"""Command-line entrypoint.""" """Command-line entrypoint."""
import os
import click import click
from autonomic.command import actions, init from autonomic.command import actions, init
@click.group() @click.group()
@click.option(
"--debug/--no-debug",
default=False,
help="Enable or disable debug mode",
show_default=True,
)
@click.pass_context @click.pass_context
def autonomic(ctx, debug): def autonomic(ctx):
""" """
\b \b
___ _ _ ___ _ _
@ -25,9 +17,7 @@ def autonomic(ctx, debug):
| | | | |_| | || (_) | | | | (_) | | | | | | | (__ | | | | |_| | || (_) | | | | (_) | | | | | | | (__
\_| |_/\__,_|\__\___/|_| |_|\___/|_| |_| |_|_|\___| \_| |_/\__,_|\__\___/|_| |_|\___/|_| |_| |_|_|\___|
""" # noqa """ # noqa
ctx.obj = {} pass
ctx.obj["DEBUG"] = debug
ctx.obj["HOME"] = "/home/{user}".format(user=os.getlogin())
autonomic.add_command(init.init) autonomic.add_command(init.init)

View File

@ -1,12 +0,0 @@
"""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
)

View File

@ -1,63 +1,35 @@
"""Ansible Actions module.""" """Actions module."""
from os import environ
import click import click
from PyInquirer import prompt from PyInquirer import prompt
from autonomic import logger from autonomic.config import ACTIONS_DIR
from autonomic.ansible import run_ansible_playbook from autonomic.infra import get_passwd, run_play
from autonomic.passstore import get_from_pass from autonomic.utils import qlist
from autonomic.settings import ACTIONS_PATH
log = logger.get_logger(__name__)
@click.command() @click.command()
@click.pass_context @click.pass_context
def actions(ctx): def actions(ctx):
"""Run an Ansible action.""" """Run an Ansible action."""
question = [ choices = ["addusers", "newhetzner", "rmhetzner", "newdokku"]
{ question = qlist("action", "Which Ansible action?", choices,)
"type": "list", action = prompt(question)["action"]
"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 in any(("newhetzner", "rmhetzner")):
if ( choices = ["prod", "test", "cicd"]
action_answer["action"] == "newhetzner" question = qlist("key", "Which Hetzner API key?", choices)
or action_answer["action"] == "rmhetzner" key = prompt(question)["key"]
):
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)
path = "logins/hetzner/{}/api_key".format(key_answer["key"]) secret = get_passwd(path)
secret = get_from_pass(path) env = environ.copy()
env["HCLOUD_TOKEN"] = secret env["HCLOUD_TOKEN"] = secret
play = "{}/{}.yml".format(ACTIONS_PATH, action_answer["action"]) play = "{}/{}.yml".format(ACTIONS_DIR, action)
run_play(play, env=env)
run_ansible_playbook(play, extra_env=env)
# TODO(decentral1se): # TODO(decentral1se):
# git commit and push on infrastructure if we: # git commit and push on infrastructure if we:

View File

@ -1,23 +1,18 @@
"""Initialise the toolbelt.""" """Initialise the toolbelt."""
import os
from os import mkdir
from os.path import exists
import click import click
import emoji from emoji import emojize
from PyInquirer import prompt from PyInquirer import prompt
from autonomic import logger from autonomic.config import CONFIG_DIR, CONFIG_YAML, INFRA_DIR, INFRA_REPO
from autonomic.config import add_to_config from autonomic.infra import members
from autonomic.infrastructure import get_members from autonomic.logger import log
from autonomic.settings import ( from autonomic.settings import add, get
AUTONOMIC_YAML, from autonomic.utils import qlist, run
CONFIG_PATH,
INFRASTRUCTURE_PATH,
INFRASTRUCTURE_REPOSITORY,
)
from autonomic.system import run_command
log = logger.get_logger(__name__)
@click.command() @click.command()
@ -25,71 +20,67 @@ log = logger.get_logger(__name__)
def init(ctx): def init(ctx):
"""Initialise the toolbelt.""" """Initialise the toolbelt."""
create_configuration_directory() create_configuration_directory()
create_configuration_file() create_settings_file()
clone_infrastructure_repo() clone_infrastructure_repo()
ask_to_login() store_username()
install_dependencies() install_dependencies()
def create_configuration_directory(): def create_configuration_directory():
"""Create toolbelt config directory.""" """Create toolbelt config directory."""
if not os.path.exists(CONFIG_PATH): if not exists(CONFIG_DIR):
os.mkdir(CONFIG_PATH) mkdir(CONFIG_DIR)
def clone_infrastructure_repo(): def clone_infrastructure_repo():
"""Clone the infrastructure repository.""" """Clone or update the infrastructure repository."""
if not os.path.exists(INFRASTRUCTURE_PATH): if not exists(INFRA_DIR):
run_command( cmd = ["git", "clone", INFRA_REPO, INFRA_DIR]
["git", "clone", INFRASTRUCTURE_REPOSITORY, INFRASTRUCTURE_PATH] run(cmd)
)
else: else:
run_command( cmd = ["git", "pull", "origin", "master"]
["git", "pull", "origin", "master"], cwd=INFRASTRUCTURE_PATH run(cmd, cwd=INFRA_DIR)
)
def ask_to_login(): def store_username():
"""Log in as your autonomic member username.""" """Store Autonomic username in the settings."""
members = get_members() if exists(CONFIG_YAML):
choices = [info["username"] for info in members["autonomic_members"]] username = get("username")
question = [ if username is not None:
{ msg = "Username already configured as {}".format(username)
"type": "list", log.info(msg)
"name": "username", return
"message": "What is your Autonomic username?",
"choices": choices,
"filter": lambda val: val.lower(),
}
]
usernames = members(flatten="username")
question = qlist("username", "What is you Autonomic username?", usernames)
answer = prompt(question) answer = prompt(question)
add_to_config(answer) add(answer)
msg = "Welcome comrade {} :kissing:".format(answer["username"]) msg = "Welcome comrade {} :kissing:".format(answer["username"])
emojized = emoji.emojize(msg, use_aliases=True) emojized = emojize(msg, use_aliases=True)
log.info(emojized) log.info(emojized)
def create_configuration_file(): def create_settings_file():
"""Create toolbelt config file.""" """Create settings file."""
if not os.path.exists(AUTONOMIC_YAML): if not exists(CONFIG_YAML):
with open(AUTONOMIC_YAML, "w") as handle: with open(CONFIG_YAML, "w") as handle:
handle.write("---") handle.write("---")
def install_dependencies(): def install_dependencies():
"""Install infrastructure dependencies.""" """Install infrastructure dependencies."""
run_command( cmd = ["/usr/bin/python3", "-m", "venv", ".venv"]
["/usr/bin/python3", "-m", "venv", ".venv"], cwd=INFRASTRUCTURE_PATH, run(cmd, cwd=INFRA_DIR)
)
run_command( cmd = [".venv/bin/pip", "install", "-r", "requirements.txt"]
[".venv/bin/pip", "install", "-r", "requirements.txt"], run(cmd, cwd=INFRA_DIR)
cwd=INFRASTRUCTURE_PATH,
)
run_command( cmd = [
[".venv/bin/ansible-galaxy", "install", "-r", "requirements.yml"], ".venv/bin/ansible-galaxy",
cwd=INFRASTRUCTURE_PATH, "install",
) "-r",
"requirements.yml",
"--force",
]
run(cmd, cwd=INFRA_DIR)

View File

@ -1,48 +1,32 @@
"""Configuration handling module.""" """Tool configuration."""
import os import os
from autonomic import logger # current user
from autonomic.settings import AUTONOMIC_YAML WHOAMI = os.getlogin()
from autonomic.system import exit_with_msg
from autonomic.yaml import yaml
log = logger.get_logger(__name__) # home directory of the current user
HOME_DIR = "/home/{}".format(WHOAMI)
# configuration directory for the toolbelt
CONFIG_DIR = "{}/.autonomic".format(HOME_DIR)
def ensure_config(): # SSH url for our infrastructure repository
"""Ensure the configuration exists.""" INFRA_REPO = (
if not os.path.exists(AUTONOMIC_YAML): "ssh://git@git.autonomic.zone:222/autonomic-cooperative/infrastructure.git"
msg = "{} is missing, run: autonomic init".format(AUTONOMIC_YAML) )
exit_with_msg(msg)
# infrastructure directory
INFRA_DIR = "{}/infrastructure".format(CONFIG_DIR)
def add_to_config(data): # list of autonomic members yaml file
"""Add values to the autonomic.yml file.""" MEMBERS_YAML = "{}/resources/members.yml".format(INFRA_DIR)
ensure_config()
with open(AUTONOMIC_YAML, "r") as handle: # directory where we store our ansible action plays
config = yaml.load(handle.read()) ACTIONS_DIR = "{}/actions".format(INFRA_DIR)
if config is None: # toolbelt configuration file
config = {} CONFIG_YAML = "{}/autonomic.yml".format(CONFIG_DIR)
for key in data: # password store directory
config[key] = data[key] PASS_STORE_DIR = "{}/credentials/password-store".format(INFRA_DIR)
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)

45
autonomic/infra.py Normal file
View File

@ -0,0 +1,45 @@
"""Infrastructure module.
All functionality that requires interacting with the infrastructure repository
should live in here. This module relies on the file structure and conventions
of the infrastructure repository directory layout. That can change without the
code knowing, so the code must act more defensively than normal. This is an
experiment.
"""
from os import environ
from autonomic.config import INFRA_DIR, MEMBERS_YAML, PASS_STORE_DIR
from autonomic.utils import run, yaml_load
def members(flatten=None):
"""The list of Autonomic members."""
members = yaml_load(MEMBERS_YAML)
if members is None:
return
if flatten:
return [info[flatten] for info in members["autonomic_members"]]
return members
def run_play(play, env=None):
"""Run an Ansible playbook."""
if env is None:
env = environ.copy()
cmd = [".venv/bin/ansible-playbook", play]
run(cmd, cwd=INFRA_DIR, env=env)
def get_passwd(path):
"""Lookup a password from the password store."""
env = environ.copy()
env.update({"PASSWORD_STORE_DIR": PASS_STORE_DIR})
cmd = ["pass", "show", path]
output = run(cmd, cwd=INFRA_DIR, env=env)
return output.decode("utf-8").strip()

View File

@ -1,20 +0,0 @@
"""Infrastructure handling module."""
import os
from autonomic.settings import MEMBERS_YAML
from autonomic.system import exit_with_msg
from autonomic.yaml import yaml
def get_members():
"""Get list of Autonomic members."""
if not os.path.exists(MEMBERS_YAML):
msg = "{} is missing, run: autonomic init".format(MEMBERS_YAML)
exit_with_msg(msg)
with open(MEMBERS_YAML, "r") as handle:
try:
return yaml.load(handle.read())
except Exception as exception:
exit_with_msg(str(exception))

View File

@ -1,4 +1,9 @@
"""Logging Module.""" """Logging Module.
This module is largely borrowed from Ansible Molecule and that is why it is
largely uncommented since we didn't write it and luckily, didn't have to! This
code just works TM. The same upstream license applies.
"""
import logging import logging
import os import os
@ -140,3 +145,6 @@ def color_text(color, msg):
if should_do_markup(): if should_do_markup():
return "{}{}{}".format(color, msg, colorama.Style.RESET_ALL) return "{}{}{}".format(color, msg, colorama.Style.RESET_ALL)
return msg return msg
log = get_logger(__name__)

View File

@ -1,22 +0,0 @@
"""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)

View File

@ -1,17 +1,32 @@
"""Static settings.""" """Toolbelt settings module.
import os All code related to working with the ~/.autonomic/settings.yml file. This file
is useful for storing information for future lookup when dealing with
repetitive cooperative tasks.
"""
USER = os.getlogin() from autonomic.config import CONFIG_YAML
HOME = "/home/{}".format(USER) from autonomic.utils import yaml_dump, yaml_load
CONFIG_PATH = "{}/.autonomic".format(HOME)
INFRASTRUCTURE_REPOSITORY = (
"ssh://git@git.autonomic.zone:222/autonomic-cooperative/infrastructure.git" def add(data):
) """Add values to the settings file."""
INFRASTRUCTURE_PATH = "{}/infrastructure".format(CONFIG_PATH) settings = yaml_load(CONFIG_YAML)
MEMBERS_YAML = "{}/resources/members.yml".format(INFRASTRUCTURE_PATH)
ACTIONS_PATH = "{}/actions".format(INFRASTRUCTURE_PATH) if settings is None:
AUTONOMIC_YAML = "{}/autonomic.yml".format(CONFIG_PATH) settings = {}
PASSWORD_STORE_PATH = "{}/credentials/password-store".format(
INFRASTRUCTURE_PATH for key in data:
) settings[key] = data[key]
yaml_dump(CONFIG_YAML, settings)
def get(key):
"""Retrieve setting values."""
settings = yaml_load(CONFIG_YAML)
try:
return settings[key]
except Exception:
return None

View File

@ -1,53 +0,0 @@
"""System related functions."""
import os
import subprocess
import sys
from autonomic import logger
from autonomic.settings import PASSWORD_STORE_PATH
log = logger.get_logger(__name__)
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})
if extra_env:
for key, val in extra_env.items():
env.update({key: val})
try:
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)
def exit(code=1):
sys.exit(code)
def exit_with_msg(msg, code=1):
log.critical(msg)
exit(code)

73
autonomic/utils.py Normal file
View File

@ -0,0 +1,73 @@
"""Utility functions.
While mostly a vague concept and sometimes a bad idea, the utilities module is
somewhere where reusable system related, data processing and other general
functions can live. This codebase seems to have a few of those, so it seems
like a necessary module. Let us be critical of our requirements when adding
things here.
"""
from os import chdir
from subprocess import CalledProcessError, check_output
from autonomic.logger import log
from autonomic.yaml import yaml
def run(cmd, cwd=None, **kwargs):
"""Run a system command.
Please note, all **kwargs will be passed into the check_output command so
that system call customisation can happen. Please name your keyword
arguments (like `cwd`) if you intend that they are used for other logic.
"""
try:
if cwd:
chdir(cwd)
log.info("Changed directory to {}".format(cwd))
log.info("Running {}".format(" ".join(cmd)))
return check_output(cmd, **kwargs)
except CalledProcessError as exception:
msg = "{} failed! Saw {}".format(" ".join(cmd), str(exception))
exit(msg)
def exit(msg, code=1):
"""Exit and log appropriate level."""
if code != 0:
log.critical(msg)
else:
log.info(msg)
exit(code)
def qlist(name, message, choices):
"""A question in list format."""
return [
{
"type": "list",
"name": name,
"message": message,
"choices": choices,
"filter": lambda answer: answer.lower(),
}
]
def yaml_load(fpath):
"""Load a YAML file."""
try:
with open(fpath, "r") as handle:
return yaml.load(handle.read())
except Exception as exception:
log.error(str(exception))
def yaml_dump(fpath, data):
"""Dump a YAML file."""
try:
with open(fpath, "w") as handle:
yaml.dump(data, handle)
except Exception as exception:
log.error(str(exception))

View File

@ -1,6 +1,13 @@
"""YAML initialisation module.""" """YAML initialisation module.
Since we would like a consistent approach to how we load, manipulate and dump
YAML, we centralise the approach into a single module to avoid duplicating all
the customisation code across the code base.
"""
from ruamel.yaml import YAML from ruamel.yaml import YAML
yaml = YAML() yaml = YAML()
# ensure that "---" is always at the top of the YAML file
yaml.explicit_start = True yaml.explicit_start = True

View File

@ -1,7 +0,0 @@
"""YAML initialisation module."""
from ruamel.yaml import YAML
yaml = YAML()
yaml.explicit_start = True