Browse Source

Add coophost and coopaas experimental commands

master 0.0.4
Luke Murphy 10 months ago
parent
commit
a178d0ed7c
Signed by: decentral1se GPG Key ID: 5E2EF5A63E3718CC
10 changed files with 214 additions and 41 deletions
  1. +19
    -3
      CHANGELOG.rst
  2. +3
    -1
      autonomic/__main__.py
  3. +5
    -6
      autonomic/command/actions.py
  4. +88
    -0
      autonomic/command/coophost.py
  5. +12
    -0
      autonomic/command/cooppaas.py
  6. +6
    -6
      autonomic/command/init.py
  7. +3
    -1
      autonomic/settings.py
  8. +76
    -9
      autonomic/utils.py
  9. +1
    -0
      setup.cfg
  10. +1
    -15
      test/test_utils.py

+ 19
- 3
CHANGELOG.rst View File

@ -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)


+ 3
- 1
autonomic/__main__.py View File

@ -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)

+ 5
- 6
autonomic/command/actions.py View File

@ -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)


+ 88
- 0
autonomic/command/coophost.py View File

@ -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))

+ 12
- 0
autonomic/command/cooppaas.py View File

@ -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...")

+ 6
- 6
autonomic/command/init.py View File

@ -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))


+ 3
- 1
autonomic/settings.py View File

@ -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


+ 76
- 9
autonomic/utils.py View File

@ -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")
try:
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))
def yaml_load(fpath):
"""Load a YAML file."""
try:
with open(fpath, "r") as handle:
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)

+ 1
- 0
setup.cfg View File

@ -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


+ 1
- 15
test/test_utils.py View File

@ -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()