Add coophost and coopaas experimental commands
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
cd3bd67783
commit
a178d0ed7c
@ -1,11 +1,27 @@
|
|||||||
Autonomic 0.0.2 (2020-04-11)
|
Autonomic 0.0.4 (2020-04-12)
|
||||||
============================
|
============================
|
||||||
|
|
||||||
Features
|
Features
|
||||||
--------
|
--------
|
||||||
|
|
||||||
- Init command. (#2)
|
- Add CoopHost / CoopPass (WIP) commands. (#4)
|
||||||
- Actions command. (#3)
|
- 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)
|
Autonomic 0.0.1 (2020-04-03)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from autonomic.command import actions, init
|
from autonomic.command import actions, coophost, cooppaas, init
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@ -25,3 +25,5 @@ def autonomic(ctx):
|
|||||||
|
|
||||||
autonomic.add_command(init.init)
|
autonomic.add_command(init.init)
|
||||||
autonomic.add_command(actions.actions)
|
autonomic.add_command(actions.actions)
|
||||||
|
autonomic.add_command(coophost.coophost)
|
||||||
|
autonomic.add_command(cooppaas.cooppaas)
|
||||||
|
@ -3,18 +3,19 @@
|
|||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from PyInquirer import prompt
|
|
||||||
|
|
||||||
from autonomic.config import ACTIONS_DIR, INFRA_DIR, PASS_STORE_DIR
|
from autonomic.config import ACTIONS_DIR, INFRA_DIR, PASS_STORE_DIR
|
||||||
from autonomic.infra import get_passwd, run_play
|
from autonomic.infra import get_passwd, run_play
|
||||||
from autonomic.settings import get
|
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.command()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def actions(ctx):
|
def actions(ctx):
|
||||||
"""Run an Ansible action."""
|
"""Run an Ansible action."""
|
||||||
|
ensure_config_dir()
|
||||||
|
|
||||||
env = environ.copy()
|
env = environ.copy()
|
||||||
|
|
||||||
env.update({"ANSIBLE_USER": get("username")})
|
env.update({"ANSIBLE_USER": get("username")})
|
||||||
@ -22,13 +23,11 @@ def actions(ctx):
|
|||||||
env.update({"PASSWORD_STORE_DIR": PASS_STORE_DIR})
|
env.update({"PASSWORD_STORE_DIR": PASS_STORE_DIR})
|
||||||
|
|
||||||
choices = ["addusers", "newhetzner", "rmhetzner", "newdokku", "pingall"]
|
choices = ["addusers", "newhetzner", "rmhetzner", "newdokku", "pingall"]
|
||||||
question = qlist("action", "Which Ansible action?", choices,)
|
action = question_ask("action", "Which Ansible action?", choices)
|
||||||
action = prompt(question)["action"]
|
|
||||||
|
|
||||||
if any(action in choice for choice in ["newhetzner", "rmhetzner"]):
|
if any(action in choice for choice in ["newhetzner", "rmhetzner"]):
|
||||||
choices = ["prod", "test", "cicd"]
|
choices = ["prod", "test", "cicd"]
|
||||||
question = qlist("key", "Which Hetzner API key?", choices)
|
key = question_ask("key", "Which Hetzner API key?", choices)
|
||||||
key = prompt(question)["key"]
|
|
||||||
|
|
||||||
path = "logins/hetzner/{}/api_key".format(key)
|
path = "logins/hetzner/{}/api_key".format(key)
|
||||||
secret = get_passwd(path)
|
secret = get_passwd(path)
|
||||||
|
88
autonomic/command/coophost.py
Normal file
88
autonomic/command/coophost.py
Normal 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
autonomic/command/cooppaas.py
Normal file
12
autonomic/command/cooppaas.py
Normal 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...")
|
@ -7,13 +7,12 @@ from subprocess import STDOUT
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
from emoji import emojize
|
from emoji import emojize
|
||||||
from PyInquirer import prompt
|
|
||||||
|
|
||||||
from autonomic.config import CONFIG_DIR, CONFIG_YAML, INFRA_DIR, INFRA_REPO
|
from autonomic.config import CONFIG_DIR, CONFIG_YAML, INFRA_DIR, INFRA_REPO
|
||||||
from autonomic.infra import members
|
from autonomic.infra import members
|
||||||
from autonomic.logger import log
|
from autonomic.logger import log
|
||||||
from autonomic.settings import add
|
from autonomic.settings import add
|
||||||
from autonomic.utils import is_proc, qlist, run
|
from autonomic.utils import is_proc, question_ask, run
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@ -53,11 +52,12 @@ def clone_infrastructure_repo():
|
|||||||
def store_username():
|
def store_username():
|
||||||
"""Store Autonomic username in the settings."""
|
"""Store Autonomic username in the settings."""
|
||||||
usernames = members(flatten="username")
|
usernames = members(flatten="username")
|
||||||
question = qlist("username", "What is you Autonomic username?", usernames)
|
username = question_ask(
|
||||||
answer = prompt(question)
|
"username", "What is you Autonomic username?", usernames,
|
||||||
add(answer)
|
)
|
||||||
|
add({"username": username})
|
||||||
|
|
||||||
msg = "Welcome comrade {} :kissing:".format(answer["username"])
|
msg = "Welcome comrade {} :kissing:".format(username)
|
||||||
log.success(emojize(msg, use_aliases=True))
|
log.success(emojize(msg, use_aliases=True))
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,11 +5,13 @@ is useful for storing information for future lookup when dealing with
|
|||||||
repetitive cooperative tasks.
|
repetitive cooperative tasks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from autonomic.config import CONFIG_YAML
|
from autonomic.config import CONFIG_YAML
|
||||||
from autonomic.utils import yaml_dump, yaml_load
|
from autonomic.utils import yaml_dump, yaml_load
|
||||||
|
|
||||||
|
|
||||||
def add(yaml):
|
def add(yaml: Dict[str, str]):
|
||||||
"""Add YAML to the settings file."""
|
"""Add YAML to the settings file."""
|
||||||
from autonomic.command.init import create_configuration
|
from autonomic.command.init import create_configuration
|
||||||
|
|
||||||
|
@ -8,16 +8,23 @@ things here.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from os import chdir
|
from os import chdir
|
||||||
|
from os.path import exists
|
||||||
|
from pathlib import Path
|
||||||
from subprocess import call, check_output
|
from subprocess import call, check_output
|
||||||
|
from sys import exit as sys_exit
|
||||||
|
|
||||||
|
from pexpect import spawn
|
||||||
from psutil import process_iter
|
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.logger import log
|
||||||
from autonomic.yaml import yaml
|
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.
|
"""Run a system command.
|
||||||
|
|
||||||
Please note, all **kwargs will be passed into the check_output command so
|
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:
|
if interactive:
|
||||||
return call(cmd, **kwargs)
|
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)
|
output = check_output(cmd, **kwargs)
|
||||||
return output.decode("utf-8")
|
return output.decode("utf-8")
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
@ -48,12 +66,12 @@ def exit(msg, code=1):
|
|||||||
else:
|
else:
|
||||||
log.info(msg)
|
log.info(msg)
|
||||||
|
|
||||||
exit(code)
|
sys_exit(code)
|
||||||
|
|
||||||
|
|
||||||
def qlist(name, message, choices):
|
def question_ask(name, message, choices):
|
||||||
"""A question in list format."""
|
"""Ask a question."""
|
||||||
return [
|
question = [
|
||||||
{
|
{
|
||||||
"type": "list",
|
"type": "list",
|
||||||
"name": name,
|
"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:
|
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())
|
return yaml.load(handle.read())
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
log.error(str(exception))
|
log.error(str(exception))
|
||||||
@ -105,3 +156,19 @@ def git_status(fpath):
|
|||||||
else:
|
else:
|
||||||
msg = "No unstaged changes found in {}".format(INFRA_DIR)
|
msg = "No unstaged changes found in {}".format(INFRA_DIR)
|
||||||
log.info(msg)
|
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)
|
||||||
|
@ -45,6 +45,7 @@ install_requires =
|
|||||||
click >= 7.1.1, <= 8.0
|
click >= 7.1.1, <= 8.0
|
||||||
colorama >= 0.4.3, <= 0.5
|
colorama >= 0.4.3, <= 0.5
|
||||||
emoji >= 0.5.4, <= 0.6
|
emoji >= 0.5.4, <= 0.6
|
||||||
|
pexpect >= 4.8.0, <= 4.9
|
||||||
psutil >= 5.7.0, <= 6.0
|
psutil >= 5.7.0, <= 6.0
|
||||||
pyinquirer >= 1.0.3, <= 1.1
|
pyinquirer >= 1.0.3, <= 1.1
|
||||||
ruamel.yaml >= 0.16.10, <= 0.17
|
ruamel.yaml >= 0.16.10, <= 0.17
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from subprocess import STDOUT
|
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():
|
def test_run_kwargs():
|
||||||
@ -18,20 +18,6 @@ def test_run_cwd(tmp_path):
|
|||||||
assert "testfile.txt" in output
|
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):
|
def test_yaml_load(tmp_path):
|
||||||
directory = tmp_path / "test"
|
directory = tmp_path / "test"
|
||||||
directory.mkdir()
|
directory.mkdir()
|
||||||
|
Reference in New Issue
Block a user