Compare commits
No commits in common. "master" and "0.0.1" have entirely different histories.
28
.drone.yml
28
.drone.yml
|
@ -1,28 +0,0 @@
|
||||||
---
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- IMAGE: 3.6-stretch
|
|
||||||
TOXENV: py36
|
|
||||||
- IMAGE: 3.7-stretch
|
|
||||||
TOXENV: py37
|
|
||||||
- IMAGE: 3.8-buster
|
|
||||||
TOXENV: py38
|
|
||||||
- IMAGE: 3.8-buster
|
|
||||||
TOXENV: lint
|
|
||||||
- IMAGE: 3.8-buster
|
|
||||||
TOXENV: sort
|
|
||||||
- IMAGE: 3.8-buster
|
|
||||||
TOXENV: format
|
|
||||||
- IMAGE: 3.8-buster
|
|
||||||
TOXENV: type
|
|
||||||
- IMAGE: 3.8-buster
|
|
||||||
TOXENV: docs
|
|
||||||
- IMAGE: 3.8-buster
|
|
||||||
TOXENV: metadata-release
|
|
||||||
|
|
||||||
pipeline:
|
|
||||||
build:
|
|
||||||
image: python:${IMAGE}
|
|
||||||
commands:
|
|
||||||
- pip install tox==3.14.6
|
|
||||||
- tox -e ${TOXENV}
|
|
|
@ -1,2 +0,0 @@
|
||||||
# The path to our pass credentials store
|
|
||||||
export PASSWORD_STORE_DIR=$(pwd)/../infrastructure/credentials/password-store
|
|
|
@ -1,42 +0,0 @@
|
||||||
Autonomic 0.0.5 (2020-04-13)
|
|
||||||
============================
|
|
||||||
|
|
||||||
Features
|
|
||||||
--------
|
|
||||||
|
|
||||||
- Add CoopHost decrypt command. (#5)
|
|
||||||
|
|
||||||
|
|
||||||
Autonomic 0.0.4 (2020-04-12)
|
|
||||||
============================
|
|
||||||
|
|
||||||
Features
|
|
||||||
--------
|
|
||||||
|
|
||||||
- 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)
|
|
||||||
============================
|
|
||||||
|
|
||||||
Project announcements
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
- Initial project release! (#1)
|
|
|
@ -20,10 +20,3 @@ autonomic
|
||||||
|
|
||||||
Command line utility belt for Autonomic
|
Command line utility belt for Autonomic
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
.. _documentation:
|
|
||||||
|
|
||||||
Documentation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Documentation coming soon TM.
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Autonomic module."""
|
"""autonomic module."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
@ -7,6 +7,6 @@ except ImportError:
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
__version__ = pkg_resources.get_distribution("autonomic").version
|
__version__ = pkg_resources.get_distribution('autonomic').version
|
||||||
except Exception:
|
except Exception:
|
||||||
__version__ = "unknown"
|
__version__ = 'unknown'
|
||||||
|
|
|
@ -2,12 +2,9 @@
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from autonomic.command import actions, coophost, cooppaas, init
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
@click.group()
|
def main():
|
||||||
@click.pass_context
|
|
||||||
def autonomic(ctx):
|
|
||||||
"""
|
"""
|
||||||
\b
|
\b
|
||||||
___ _ _
|
___ _ _
|
||||||
|
@ -16,14 +13,9 @@ def autonomic(ctx):
|
||||||
| _ | | | | __/ _ \| '_ \ / _ \| '_ ` _ \| |/ __|
|
| _ | | | | __/ _ \| '_ \ / _ \| '_ ` _ \| |/ __|
|
||||||
| | | | |_| | || (_) | | | | (_) | | | | | | | (__
|
| | | | |_| | || (_) | | | | (_) | | | | | | | (__
|
||||||
\_| |_/\__,_|\__\___/|_| |_|\___/|_| |_| |_|_|\___|
|
\_| |_/\__,_|\__\___/|_| |_|\___/|_| |_| |_|_|\___|
|
||||||
|
|
||||||
Hack the planet!
|
|
||||||
|
|
||||||
""" # noqa
|
""" # noqa
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
autonomic.add_command(init.init)
|
if __name__ == '__main__':
|
||||||
autonomic.add_command(actions.actions)
|
main()
|
||||||
autonomic.add_command(coophost.coophost)
|
|
||||||
autonomic.add_command(cooppaas.cooppaas)
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
"""Actions module."""
|
|
||||||
|
|
||||||
from os import environ
|
|
||||||
|
|
||||||
import click
|
|
||||||
|
|
||||||
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 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")})
|
|
||||||
env.update({"REMOTE_USER": get("username")})
|
|
||||||
env.update({"PASSWORD_STORE_DIR": PASS_STORE_DIR})
|
|
||||||
|
|
||||||
choices = ["addusers", "newhetzner", "rmhetzner", "newdokku", "pingall"]
|
|
||||||
action = question_ask("action", "Which Ansible action?", choices)
|
|
||||||
|
|
||||||
if any(action in choice for choice in ["newhetzner", "rmhetzner"]):
|
|
||||||
choices = ["prod", "test", "cicd"]
|
|
||||||
key = question_ask("key", "Which Hetzner API key?", choices)
|
|
||||||
|
|
||||||
path = "logins/hetzner/{}/api_key".format(key)
|
|
||||||
secret = get_passwd(path)
|
|
||||||
env["HCLOUD_TOKEN"] = secret
|
|
||||||
|
|
||||||
play = "{}/{}.yml".format(ACTIONS_DIR, action)
|
|
||||||
run_play(play, env=env)
|
|
||||||
git_status(INFRA_DIR)
|
|
|
@ -1,136 +0,0 @@
|
||||||
"""CoopHost module."""
|
|
||||||
|
|
||||||
from os import chdir, mkdir
|
|
||||||
from os.path import basename, exists
|
|
||||||
from pathlib import Path
|
|
||||||
from socket import gethostname
|
|
||||||
|
|
||||||
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,
|
|
||||||
exit,
|
|
||||||
input_ask,
|
|
||||||
pass_ask,
|
|
||||||
question_ask,
|
|
||||||
run,
|
|
||||||
yaml_dump,
|
|
||||||
yaml_load,
|
|
||||||
)
|
|
||||||
|
|
||||||
hostname = gethostname()
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.pass_context
|
|
||||||
def coophost(ctx):
|
|
||||||
"""Manage CoopHost resources."""
|
|
||||||
ensure_config_dir()
|
|
||||||
ensure_deploy_d_dir()
|
|
||||||
|
|
||||||
app_dir = Path(".").absolute()
|
|
||||||
app = basename(app_dir)
|
|
||||||
log.info("Auto-detected the {} application".format(app))
|
|
||||||
|
|
||||||
choices = ["encrypt", "decrypt"]
|
|
||||||
operation = question_ask("operation", "Which operation?", choices)
|
|
||||||
|
|
||||||
if operation == "encrypt":
|
|
||||||
encrypt(app, app_dir)
|
|
||||||
elif operation == "decrypt":
|
|
||||||
decrypt(app, app_dir)
|
|
||||||
|
|
||||||
|
|
||||||
def get_vault_pass(app):
|
|
||||||
"""Retrieve or set the app vault password."""
|
|
||||||
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))
|
|
||||||
return app_settings["vault-password"]
|
|
||||||
|
|
||||||
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}})
|
|
||||||
|
|
||||||
return vault_password
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt(app, app_dir):
|
|
||||||
"""Decrypt a secret."""
|
|
||||||
vault_password = get_vault_pass(app)
|
|
||||||
name = input_ask("Which variable do you want to decrypt?")
|
|
||||||
|
|
||||||
vault_path = (Path(".") / "deploy.d" / "vault").absolute()
|
|
||||||
var_path = (vault_path / "{}.yml".format(name)).absolute()
|
|
||||||
|
|
||||||
if not exists(var_path):
|
|
||||||
exit("{}.yml is missing?".format(name))
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
".venv/bin/ansible",
|
|
||||||
hostname,
|
|
||||||
"--inventory",
|
|
||||||
"{},".format(hostname),
|
|
||||||
"-m",
|
|
||||||
"debug",
|
|
||||||
"-a",
|
|
||||||
"var='{}'".format(name),
|
|
||||||
"-e @{}".format(var_path),
|
|
||||||
"--ask-vault-pass",
|
|
||||||
"-e",
|
|
||||||
"ansible_user={}".format(get("username")),
|
|
||||||
]
|
|
||||||
|
|
||||||
decrypted = run(
|
|
||||||
cmd,
|
|
||||||
cwd=INFRA_DIR,
|
|
||||||
output=True,
|
|
||||||
pexpect=True,
|
|
||||||
pexpected={"(?i)vault password:": vault_password},
|
|
||||||
)
|
|
||||||
|
|
||||||
log.info(decrypted)
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt(app, app_dir):
|
|
||||||
"""Encrypt a secret for a CoopHost package."""
|
|
||||||
vault_password = get_vault_pass(app)
|
|
||||||
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 {}".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))
|
|
|
@ -1,12 +0,0 @@
|
||||||
"""CoopPaas module."""
|
|
||||||
|
|
||||||
import click
|
|
||||||
|
|
||||||
from autonomic.logger import log
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.pass_context
|
|
||||||
def cooppaas(ctx):
|
|
||||||
"""Manage CoopPaas resources."""
|
|
||||||
log.info("Still experimenting...")
|
|
|
@ -1,89 +0,0 @@
|
||||||
"""Initialise the toolbelt."""
|
|
||||||
|
|
||||||
|
|
||||||
from os import mkdir
|
|
||||||
from os.path import exists
|
|
||||||
from subprocess import STDOUT
|
|
||||||
|
|
||||||
import click
|
|
||||||
from emoji import emojize
|
|
||||||
|
|
||||||
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, question_ask, run
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.pass_context
|
|
||||||
def init(ctx):
|
|
||||||
"""Initialise the toolbelt."""
|
|
||||||
create_configuration()
|
|
||||||
clone_infrastructure_repo()
|
|
||||||
store_username()
|
|
||||||
install_dependencies()
|
|
||||||
ssh_sanity_check()
|
|
||||||
|
|
||||||
msg = "Hack the planet! :earth_africa:"
|
|
||||||
log.success(emojize(msg, use_aliases=True))
|
|
||||||
|
|
||||||
|
|
||||||
def create_configuration():
|
|
||||||
"""Create toolbelt config directory."""
|
|
||||||
if not exists(CONFIG_DIR):
|
|
||||||
mkdir(CONFIG_DIR)
|
|
||||||
|
|
||||||
if not exists(CONFIG_YAML):
|
|
||||||
with open(CONFIG_YAML, "w") as handle:
|
|
||||||
handle.write("---")
|
|
||||||
|
|
||||||
|
|
||||||
def clone_infrastructure_repo():
|
|
||||||
"""Clone or update the infrastructure repository."""
|
|
||||||
if not exists(INFRA_DIR):
|
|
||||||
cmd = ["git", "clone", INFRA_REPO, INFRA_DIR]
|
|
||||||
run(cmd, stderr=STDOUT)
|
|
||||||
else:
|
|
||||||
cmd = ["git", "pull", "origin", "master"]
|
|
||||||
run(cmd, cwd=INFRA_DIR, stderr=STDOUT)
|
|
||||||
|
|
||||||
|
|
||||||
def store_username():
|
|
||||||
"""Store Autonomic username in the settings."""
|
|
||||||
usernames = members(flatten="username")
|
|
||||||
username = question_ask(
|
|
||||||
"username", "What is you Autonomic username?", usernames,
|
|
||||||
)
|
|
||||||
add({"username": username})
|
|
||||||
|
|
||||||
msg = "Welcome comrade {} :kissing:".format(username)
|
|
||||||
log.success(emojize(msg, use_aliases=True))
|
|
||||||
|
|
||||||
|
|
||||||
def install_dependencies():
|
|
||||||
"""Install infrastructure dependencies."""
|
|
||||||
cmd = ["/usr/bin/python3", "-m", "venv", ".venv"]
|
|
||||||
run(cmd, cwd=INFRA_DIR, stderr=STDOUT)
|
|
||||||
|
|
||||||
cmd = [".venv/bin/pip", "install", "-r", "requirements.txt"]
|
|
||||||
run(cmd, stderr=STDOUT)
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
".venv/bin/ansible-galaxy",
|
|
||||||
"install",
|
|
||||||
"-r",
|
|
||||||
"requirements.yml",
|
|
||||||
"--force",
|
|
||||||
]
|
|
||||||
run(cmd, stderr=STDOUT)
|
|
||||||
|
|
||||||
|
|
||||||
def ssh_sanity_check():
|
|
||||||
"""Try to recommend some SSH tips."""
|
|
||||||
if not is_proc("ssh-agent"):
|
|
||||||
msg = "warning: ssh-agent is not running :sob:"
|
|
||||||
log.warning(emojize(msg, use_aliases=True))
|
|
||||||
else:
|
|
||||||
msg = "ssh-agent is running :rocket:"
|
|
||||||
log.success(emojize(msg, use_aliases=True))
|
|
|
@ -1,28 +0,0 @@
|
||||||
"""Tool configuration."""
|
|
||||||
|
|
||||||
from os.path import expanduser
|
|
||||||
|
|
||||||
HOME_DIR = expanduser("~")
|
|
||||||
|
|
||||||
# configuration directory for the toolbelt
|
|
||||||
CONFIG_DIR = "{}/.autonomic".format(HOME_DIR)
|
|
||||||
|
|
||||||
# SSH url for our infrastructure repository
|
|
||||||
INFRA_REPO = (
|
|
||||||
"ssh://git@git.autonomic.zone:222/autonomic-cooperative/infrastructure.git"
|
|
||||||
)
|
|
||||||
|
|
||||||
# infrastructure directory
|
|
||||||
INFRA_DIR = "{}/infrastructure".format(CONFIG_DIR)
|
|
||||||
|
|
||||||
# list of autonomic members yaml file
|
|
||||||
MEMBERS_YAML = "{}/resources/members.yml".format(INFRA_DIR)
|
|
||||||
|
|
||||||
# directory where we store our ansible action plays
|
|
||||||
ACTIONS_DIR = "{}/actions".format(INFRA_DIR)
|
|
||||||
|
|
||||||
# toolbelt configuration file
|
|
||||||
CONFIG_YAML = "{}/autonomic.yml".format(CONFIG_DIR)
|
|
||||||
|
|
||||||
# password store directory
|
|
||||||
PASS_STORE_DIR = "{}/credentials/password-store".format(INFRA_DIR)
|
|
|
@ -1,43 +0,0 @@
|
||||||
"""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."""
|
|
||||||
cmd = [".venv/bin/ansible-playbook", play]
|
|
||||||
run(cmd, cwd=INFRA_DIR, interactive=True, 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.strip()
|
|
|
@ -1,150 +0,0 @@
|
||||||
"""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 os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import colorama
|
|
||||||
|
|
||||||
|
|
||||||
def should_do_markup():
|
|
||||||
return sys.stdout.isatty() and os.environ.get("TERM") != "dumb"
|
|
||||||
|
|
||||||
|
|
||||||
SUCCESS = 100
|
|
||||||
OUT = 101
|
|
||||||
|
|
||||||
|
|
||||||
class LogFilter(object):
|
|
||||||
def __init__(self, level):
|
|
||||||
self.__level = level
|
|
||||||
|
|
||||||
def filter(self, logRecord):
|
|
||||||
return logRecord.levelno <= self.__level
|
|
||||||
|
|
||||||
|
|
||||||
class CustomLogger(logging.getLoggerClass()): # type: ignore
|
|
||||||
def __init__(self, name, level=logging.NOTSET):
|
|
||||||
super(logging.getLoggerClass(), self).__init__(name, level)
|
|
||||||
logging.addLevelName(SUCCESS, "SUCCESS")
|
|
||||||
logging.addLevelName(OUT, "OUT")
|
|
||||||
|
|
||||||
def success(self, msg, *args, **kwargs):
|
|
||||||
if self.isEnabledFor(SUCCESS):
|
|
||||||
self._log(SUCCESS, msg, args, **kwargs)
|
|
||||||
|
|
||||||
def out(self, msg, *args, **kwargs):
|
|
||||||
if self.isEnabledFor(OUT):
|
|
||||||
self._log(OUT, msg, args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TrailingNewlineFormatter(logging.Formatter):
|
|
||||||
def format(self, record):
|
|
||||||
if record.msg:
|
|
||||||
record.msg = record.msg.rstrip()
|
|
||||||
return super(TrailingNewlineFormatter, self).format(record)
|
|
||||||
|
|
||||||
|
|
||||||
def get_logger(name=None):
|
|
||||||
logging.setLoggerClass(CustomLogger)
|
|
||||||
|
|
||||||
logger = logging.getLogger(name)
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
logger.addHandler(_get_info_handler())
|
|
||||||
logger.addHandler(_get_out_handler())
|
|
||||||
logger.addHandler(_get_warn_handler())
|
|
||||||
logger.addHandler(_get_error_handler())
|
|
||||||
logger.addHandler(_get_critical_handler())
|
|
||||||
logger.addHandler(_get_success_handler())
|
|
||||||
logger.propagate = False
|
|
||||||
|
|
||||||
return logger
|
|
||||||
|
|
||||||
|
|
||||||
def _get_info_handler():
|
|
||||||
handler = logging.StreamHandler(sys.stdout)
|
|
||||||
handler.setLevel(logging.INFO)
|
|
||||||
handler.addFilter(LogFilter(logging.INFO))
|
|
||||||
handler.setFormatter(
|
|
||||||
TrailingNewlineFormatter("--> {}".format(cyan_text("%(message)s")))
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
|
|
||||||
|
|
||||||
def _get_out_handler():
|
|
||||||
handler = logging.StreamHandler(sys.stdout)
|
|
||||||
handler.setLevel(OUT)
|
|
||||||
handler.addFilter(LogFilter(OUT))
|
|
||||||
handler.setFormatter(TrailingNewlineFormatter(" %(message)s"))
|
|
||||||
|
|
||||||
return handler
|
|
||||||
|
|
||||||
|
|
||||||
def _get_warn_handler():
|
|
||||||
handler = logging.StreamHandler(sys.stdout)
|
|
||||||
handler.setLevel(logging.WARN)
|
|
||||||
handler.addFilter(LogFilter(logging.WARN))
|
|
||||||
handler.setFormatter(TrailingNewlineFormatter(yellow_text("%(message)s")))
|
|
||||||
|
|
||||||
return handler
|
|
||||||
|
|
||||||
|
|
||||||
def _get_error_handler():
|
|
||||||
handler = logging.StreamHandler(sys.stderr)
|
|
||||||
handler.setLevel(logging.ERROR)
|
|
||||||
handler.addFilter(LogFilter(logging.ERROR))
|
|
||||||
handler.setFormatter(TrailingNewlineFormatter(red_text("%(message)s")))
|
|
||||||
|
|
||||||
return handler
|
|
||||||
|
|
||||||
|
|
||||||
def _get_critical_handler():
|
|
||||||
handler = logging.StreamHandler(sys.stderr)
|
|
||||||
handler.setLevel(logging.CRITICAL)
|
|
||||||
handler.addFilter(LogFilter(logging.CRITICAL))
|
|
||||||
handler.setFormatter(
|
|
||||||
TrailingNewlineFormatter(red_text("ERROR: %(message)s"))
|
|
||||||
)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
|
|
||||||
|
|
||||||
def _get_success_handler():
|
|
||||||
handler = logging.StreamHandler(sys.stdout)
|
|
||||||
handler.setLevel(SUCCESS)
|
|
||||||
handler.addFilter(LogFilter(SUCCESS))
|
|
||||||
handler.setFormatter(TrailingNewlineFormatter(green_text("%(message)s")))
|
|
||||||
|
|
||||||
return handler
|
|
||||||
|
|
||||||
|
|
||||||
def red_text(msg):
|
|
||||||
return color_text(colorama.Fore.RED, msg)
|
|
||||||
|
|
||||||
|
|
||||||
def yellow_text(msg):
|
|
||||||
return color_text(colorama.Fore.YELLOW, msg)
|
|
||||||
|
|
||||||
|
|
||||||
def green_text(msg):
|
|
||||||
return color_text(colorama.Fore.GREEN, msg)
|
|
||||||
|
|
||||||
|
|
||||||
def cyan_text(msg):
|
|
||||||
return color_text(colorama.Fore.CYAN, msg)
|
|
||||||
|
|
||||||
|
|
||||||
def color_text(color, msg):
|
|
||||||
if should_do_markup():
|
|
||||||
return "{}{}{}".format(color, msg, colorama.Style.RESET_ALL)
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
|
|
@ -1,49 +0,0 @@
|
||||||
"""Toolbelt settings module.
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from autonomic.config import CONFIG_YAML
|
|
||||||
from autonomic.utils import yaml_dump, yaml_load
|
|
||||||
|
|
||||||
|
|
||||||
def add(yaml: Dict[str, str]):
|
|
||||||
"""Add YAML to the settings file."""
|
|
||||||
from autonomic.command.init import create_configuration
|
|
||||||
|
|
||||||
create_configuration()
|
|
||||||
settings = yaml_load(CONFIG_YAML)
|
|
||||||
|
|
||||||
if settings is None:
|
|
||||||
settings = {}
|
|
||||||
|
|
||||||
for key in yaml:
|
|
||||||
settings[key] = yaml[key]
|
|
||||||
|
|
||||||
yaml_dump(CONFIG_YAML, settings)
|
|
||||||
|
|
||||||
|
|
||||||
def remove(key):
|
|
||||||
"""Remove key from the settings file."""
|
|
||||||
settings = yaml_load(CONFIG_YAML)
|
|
||||||
|
|
||||||
if settings is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
settings.pop(key, None)
|
|
||||||
|
|
||||||
yaml_dump(CONFIG_YAML, settings)
|
|
||||||
|
|
||||||
|
|
||||||
def get(key):
|
|
||||||
"""Retrieve settings key."""
|
|
||||||
settings = yaml_load(CONFIG_YAML)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return settings[key]
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
|
@ -1,174 +0,0 @@
|
||||||
"""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 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 CONFIG_DIR, CONFIG_YAML, INFRA_DIR
|
|
||||||
from autonomic.logger import log
|
|
||||||
from autonomic.yaml import yaml
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
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)))
|
|
||||||
|
|
||||||
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:
|
|
||||||
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)
|
|
||||||
|
|
||||||
sys_exit(code)
|
|
||||||
|
|
||||||
|
|
||||||
def question_ask(name, message, choices):
|
|
||||||
"""Ask a question."""
|
|
||||||
question = [
|
|
||||||
{
|
|
||||||
"type": "list",
|
|
||||||
"name": name,
|
|
||||||
"message": message,
|
|
||||||
"choices": choices,
|
|
||||||
"filter": lambda answer: answer.lower(),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(target, "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))
|
|
||||||
|
|
||||||
|
|
||||||
def is_proc(name):
|
|
||||||
"""Determine if a process is running or not."""
|
|
||||||
for process in process_iter():
|
|
||||||
try:
|
|
||||||
if name.lower() in process.name().lower():
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def git_status(fpath):
|
|
||||||
"""Check if Git reports changes to be committed."""
|
|
||||||
cmd = ["git", "status", "--porcelain"]
|
|
||||||
output = run(cmd, cwd=fpath)
|
|
||||||
|
|
||||||
if output:
|
|
||||||
msg = "warning: git reports uncommitted changes in {}".format(fpath)
|
|
||||||
log.warning(msg)
|
|
||||||
log.warning(output)
|
|
||||||
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,13 +0,0 @@
|
||||||
"""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
|
|
||||||
|
|
||||||
yaml = YAML()
|
|
||||||
|
|
||||||
# ensure that "---" is always at the top of the YAML file
|
|
||||||
yaml.explicit_start = True # type: ignore
|
|
|
@ -1,8 +1,8 @@
|
||||||
author = "decentral1se"
|
author = 'decentral1se'
|
||||||
copyright = "2019, decentral1se"
|
copyright = '2019, decentral1se'
|
||||||
html_static_path = ["_static"]
|
html_static_path = ['_static']
|
||||||
html_theme = "sphinx_rtd_theme"
|
html_theme = 'sphinx_rtd_theme'
|
||||||
master_doc = "index"
|
master_doc = 'index'
|
||||||
project = "autonomic utility belt"
|
project = 'autonomic'
|
||||||
templates_path = ["_templates"]
|
templates_path = ['_templates']
|
||||||
extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"]
|
extensions = ['sphinx.ext.autodoc', 'sphinx_autodoc_typehints']
|
||||||
|
|
|
@ -20,9 +20,11 @@ black = "^19.10b0"
|
||||||
isort = "^4.3.21"
|
isort = "^4.3.21"
|
||||||
flake8 = "^3.7.9"
|
flake8 = "^3.7.9"
|
||||||
|
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 80
|
line-length = 80
|
||||||
target-version = ["py38"]
|
target-version = ["py38"]
|
||||||
|
skip-string-normalization = true
|
||||||
include = '\.pyi?$'
|
include = '\.pyi?$'
|
||||||
|
|
||||||
[tool.towncrier]
|
[tool.towncrier]
|
||||||
|
|
10
setup.cfg
10
setup.cfg
|
@ -6,7 +6,7 @@ max-line-length = 80
|
||||||
|
|
||||||
[isort]
|
[isort]
|
||||||
known_first_party = autonomic
|
known_first_party = autonomic
|
||||||
known_third_party = pytest, psutil
|
known_third_party = pytest
|
||||||
line_length = 80
|
line_length = 80
|
||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
skip = .venv, .tox
|
skip = .venv, .tox
|
||||||
|
@ -43,19 +43,13 @@ packages = find:
|
||||||
zip_safe = False
|
zip_safe = False
|
||||||
install_requires =
|
install_requires =
|
||||||
click >= 7.1.1, <= 8.0
|
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
|
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
where = .
|
where = .
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
autonomic = autonomic.__main__:autonomic
|
autonomic = autonomic.__main__:main
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
all_files = 1
|
all_files = 1
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from autonomic.settings import add, get, remove
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def config_yml():
|
|
||||||
from autonomic.command.init import create_configuration
|
|
||||||
|
|
||||||
create_configuration()
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_get_remove(config_yml):
|
|
||||||
add({"foo": "bar"})
|
|
||||||
|
|
||||||
assert get("doesnt-exist") is None
|
|
||||||
assert get("foo") == "bar"
|
|
||||||
|
|
||||||
remove("foo")
|
|
||||||
assert get("foo") is None
|
|
|
@ -1,44 +0,0 @@
|
||||||
from subprocess import STDOUT
|
|
||||||
|
|
||||||
from autonomic.utils import is_proc, run, yaml_dump, yaml_load
|
|
||||||
|
|
||||||
|
|
||||||
def test_run_kwargs():
|
|
||||||
assert run(["whoami"], stderr=STDOUT) is not None
|
|
||||||
|
|
||||||
|
|
||||||
def test_run_cwd(tmp_path):
|
|
||||||
directory = tmp_path / "test"
|
|
||||||
directory.mkdir()
|
|
||||||
|
|
||||||
testfile = directory / "testfile.txt"
|
|
||||||
testfile.write_text("hello, world")
|
|
||||||
|
|
||||||
output = run(["ls"], cwd=directory.absolute())
|
|
||||||
assert "testfile.txt" in output
|
|
||||||
|
|
||||||
|
|
||||||
def test_yaml_load(tmp_path):
|
|
||||||
directory = tmp_path / "test"
|
|
||||||
directory.mkdir()
|
|
||||||
|
|
||||||
testfile = directory / "testfile.yml"
|
|
||||||
testfile.write_text("---\nusername: foobar")
|
|
||||||
|
|
||||||
loaded = yaml_load(testfile.absolute())
|
|
||||||
assert loaded["username"] == "foobar"
|
|
||||||
|
|
||||||
|
|
||||||
def test_yaml_dump(tmp_path):
|
|
||||||
directory = tmp_path / "test"
|
|
||||||
directory.mkdir()
|
|
||||||
|
|
||||||
testfile = directory / "testfile.yml"
|
|
||||||
yaml_dump(testfile.absolute(), {"username": "foobar"})
|
|
||||||
|
|
||||||
loaded = yaml_load(testfile.absolute())
|
|
||||||
assert loaded["username"] == "foobar"
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_proc():
|
|
||||||
assert is_proc("pytest") is True
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
|
|
||||||
def test_version_fails_gracefully(mocker):
|
def test_version_fails_gracefully(mocker):
|
||||||
target = "pkg_resources.get_distribution"
|
target = 'pkg_resources.get_distribution'
|
||||||
mocker.patch(target, side_effect=Exception())
|
mocker.patch(target, side_effect=Exception())
|
||||||
|
|
||||||
from autonomic.__init__ import __version__
|
from autonomic.__init__ import __version__
|
||||||
|
|
||||||
assert __version__ == "unknown"
|
assert __version__ == 'unknown'
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
from autonomic.yaml import yaml
|
|
||||||
|
|
||||||
|
|
||||||
def test_yaml_config():
|
|
||||||
assert yaml.explicit_start is True
|
|
2
tox.ini
2
tox.ini
|
@ -6,6 +6,7 @@ envlist =
|
||||||
format
|
format
|
||||||
type
|
type
|
||||||
docs
|
docs
|
||||||
|
changelog
|
||||||
metadata-release
|
metadata-release
|
||||||
skip_missing_interpreters = True
|
skip_missing_interpreters = True
|
||||||
isolated_build = True
|
isolated_build = True
|
||||||
|
@ -49,7 +50,6 @@ description = build the documentation
|
||||||
skipdist = True
|
skipdist = True
|
||||||
deps =
|
deps =
|
||||||
sphinx
|
sphinx
|
||||||
sphinx-autodoc-typehints
|
|
||||||
sphinx_rtd_theme
|
sphinx_rtd_theme
|
||||||
commands = python -m setup build_sphinx
|
commands = python -m setup build_sphinx
|
||||||
|
|
||||||
|
|
Reference in New Issue
Block a user