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
|
||||
---------------------------------------
|
||||
|
||||
.. _documentation:
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation coming soon TM.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"""Autonomic module."""
|
||||
"""autonomic module."""
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
|
@ -7,6 +7,6 @@ except ImportError:
|
|||
|
||||
|
||||
try:
|
||||
__version__ = pkg_resources.get_distribution("autonomic").version
|
||||
__version__ = pkg_resources.get_distribution('autonomic').version
|
||||
except Exception:
|
||||
__version__ = "unknown"
|
||||
__version__ = 'unknown'
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
|
||||
import click
|
||||
|
||||
from autonomic.command import actions, coophost, cooppaas, init
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
def autonomic(ctx):
|
||||
@click.command()
|
||||
def main():
|
||||
"""
|
||||
\b
|
||||
___ _ _
|
||||
|
@ -16,14 +13,9 @@ def autonomic(ctx):
|
|||
| _ | | | | __/ _ \| '_ \ / _ \| '_ ` _ \| |/ __|
|
||||
| | | | |_| | || (_) | | | | (_) | | | | | | | (__
|
||||
\_| |_/\__,_|\__\___/|_| |_|\___/|_| |_| |_|_|\___|
|
||||
|
||||
Hack the planet!
|
||||
|
||||
""" # noqa
|
||||
pass
|
||||
|
||||
|
||||
autonomic.add_command(init.init)
|
||||
autonomic.add_command(actions.actions)
|
||||
autonomic.add_command(coophost.coophost)
|
||||
autonomic.add_command(cooppaas.cooppaas)
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -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"
|
||||
copyright = "2019, decentral1se"
|
||||
html_static_path = ["_static"]
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
master_doc = "index"
|
||||
project = "autonomic utility belt"
|
||||
templates_path = ["_templates"]
|
||||
extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"]
|
||||
author = 'decentral1se'
|
||||
copyright = '2019, decentral1se'
|
||||
html_static_path = ['_static']
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
master_doc = 'index'
|
||||
project = 'autonomic'
|
||||
templates_path = ['_templates']
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx_autodoc_typehints']
|
||||
|
|
|
@ -20,9 +20,11 @@ black = "^19.10b0"
|
|||
isort = "^4.3.21"
|
||||
flake8 = "^3.7.9"
|
||||
|
||||
|
||||
[tool.black]
|
||||
line-length = 80
|
||||
target-version = ["py38"]
|
||||
skip-string-normalization = true
|
||||
include = '\.pyi?$'
|
||||
|
||||
[tool.towncrier]
|
||||
|
|
10
setup.cfg
10
setup.cfg
|
@ -6,7 +6,7 @@ max-line-length = 80
|
|||
|
||||
[isort]
|
||||
known_first_party = autonomic
|
||||
known_third_party = pytest, psutil
|
||||
known_third_party = pytest
|
||||
line_length = 80
|
||||
multi_line_output = 3
|
||||
skip = .venv, .tox
|
||||
|
@ -43,19 +43,13 @@ packages = find:
|
|||
zip_safe = False
|
||||
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
|
||||
|
||||
[options.packages.find]
|
||||
where = .
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
autonomic = autonomic.__main__:autonomic
|
||||
autonomic = autonomic.__main__:main
|
||||
|
||||
[build_sphinx]
|
||||
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):
|
||||
target = "pkg_resources.get_distribution"
|
||||
target = 'pkg_resources.get_distribution'
|
||||
mocker.patch(target, side_effect=Exception())
|
||||
|
||||
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
|
||||
type
|
||||
docs
|
||||
changelog
|
||||
metadata-release
|
||||
skip_missing_interpreters = True
|
||||
isolated_build = True
|
||||
|
@ -49,7 +50,6 @@ description = build the documentation
|
|||
skipdist = True
|
||||
deps =
|
||||
sphinx
|
||||
sphinx-autodoc-typehints
|
||||
sphinx_rtd_theme
|
||||
commands = python -m setup build_sphinx
|
||||
|
||||
|
|
Reference in New Issue
Block a user