Compare commits

...

33 Commits

Author SHA1 Message Date
Luke Murphy 9520868cc3
Add first stab at decrypt command
continuous-integration/drone/push Build is passing Details
2020-04-13 17:40:07 +02:00
Luke Murphy 3aebb211cf
Fix typo
continuous-integration/drone/push Build is passing Details
2020-04-12 15:52:05 +02:00
Luke Murphy a178d0ed7c
Add coophost and coopaas experimental commands
continuous-integration/drone/push Build is passing Details
2020-04-12 15:41:30 +02:00
Luke Murphy cd3bd67783
That made no difference, revert
continuous-integration/drone/push Build is passing Details
2020-04-12 00:14:17 +02:00
Luke Murphy b6187f4b1a
Try to name the builds
continuous-integration/drone/push Build is passing Details
2020-04-12 00:12:07 +02:00
Luke Murphy 082892101e
Remove caching for now
continuous-integration/drone/push Build is passing Details
2020-04-12 00:10:15 +02:00
Luke Murphy fd75435a9c
Another more portable user/home finder 2020-04-12 00:05:32 +02:00
Luke Murphy af2a228310
Add cache 2020-04-12 00:02:20 +02:00
Luke Murphy dcf23b8c35
Shore up test with a fixture for now
continuous-integration/drone/push Build was killed Details
2020-04-11 23:59:47 +02:00
Luke Murphy 3ff41797c2
Get user in a more portable way
continuous-integration/drone/push Build is failing Details
2020-04-11 23:56:38 +02:00
Luke Murphy 2cd0c430cc
Use pipelines syntax
continuous-integration/drone/push Build is failing Details
2020-04-11 23:51:25 +02:00
Luke Murphy 3d76c710cc
Don't quote
continuous-integration/drone/push Build encountered an error Details
2020-04-11 23:49:39 +02:00
Luke Murphy 3e03c91cb3
Unleash more matrix attempts
continuous-integration/drone/push Build encountered an error Details
2020-04-11 23:48:52 +02:00
Luke Murphy fc33fd7a78
Take a stab at the drone CI
continuous-integration/drone/push Build is failing Details
2020-04-11 23:42:09 +02:00
Luke Murphy cb5d6fc77d
Add 0.0.2 change log 2020-04-11 23:26:46 +02:00
Luke Murphy b3c64f0403
Fix output check on git status 2020-04-11 23:19:00 +02:00
Luke Murphy 49be06d41f
Fix output decoding and stripping for passwords 2020-04-11 23:18:50 +02:00
Luke Murphy 1adab407e8
Add pingall and fix env vars 2020-04-11 23:18:31 +02:00
Luke Murphy 58b09694df
Allow interactive calls 2020-04-11 23:07:20 +02:00
Luke Murphy 9c692f2335
Fix bad logic 2020-04-11 23:07:09 +02:00
Luke Murphy 324d58690d
Fix docs build 2020-04-11 22:56:28 +02:00
Luke Murphy 3eade299a0
Fix docs build and format docs conf 2020-04-11 22:54:28 +02:00
Luke Murphy b69bfa28b4
Appease tox (and don't run changelog) 2020-04-11 22:52:27 +02:00
Luke Murphy c69bdec4c9
Git status checks and more hacking 2020-04-11 22:47:16 +02:00
Luke Murphy 0806b4e134
Definitely hack the planet and emoji fixes 2020-04-11 21:28:22 +02:00
Luke Murphy 7ee473c9b6
Check ssh-agent 2020-04-11 21:25:49 +02:00
Luke Murphy b58b9c3296
Re-work hacking from last night 2020-04-11 21:10:29 +02:00
Luke Murphy c6608c5568
Add change log 2020-04-11 17:29:51 +02:00
Luke Murphy bba4ec271d
Add conditional check 2020-04-11 01:46:42 +02:00
Luke Murphy e5127602f0
Add some actions 2020-04-11 01:45:53 +02:00
Luke Murphy 2c894b5d1d
Let there be emoji 2020-04-11 00:26:13 +02:00
Luke Murphy e5df45650d
Login is possible
Closes #1.
2020-04-11 00:17:36 +02:00
Luke Murphy b250604df5
ADd envrc sample 2020-04-08 13:25:24 +02:00
25 changed files with 914 additions and 22 deletions

28
.drone.yml Normal file
View File

@ -0,0 +1,28 @@
---
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}

2
.envrc.sample Normal file
View File

@ -0,0 +1,2 @@
# The path to our pass credentials store
export PASSWORD_STORE_DIR=$(pwd)/../infrastructure/credentials/password-store

View File

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

View File

@ -20,3 +20,10 @@ autonomic
Command line utility belt for Autonomic
---------------------------------------
.. _documentation:
Documentation
-------------
Documentation coming soon TM.

View File

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

View File

@ -2,9 +2,12 @@
import click
from autonomic.command import actions, coophost, cooppaas, init
@click.command()
def main():
@click.group()
@click.pass_context
def autonomic(ctx):
"""
\b
___ _ _
@ -13,9 +16,14 @@ def main():
| _ | | | | __/ _ \| '_ \ / _ \| '_ ` _ \| |/ __|
| | | | |_| | || (_) | | | | (_) | | | | | | | (__
\_| |_/\__,_|\__\___/|_| |_|\___/|_| |_| |_|_|\___|
Hack the planet!
""" # noqa
pass
if __name__ == '__main__':
main()
autonomic.add_command(init.init)
autonomic.add_command(actions.actions)
autonomic.add_command(coophost.coophost)
autonomic.add_command(cooppaas.cooppaas)

View File

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

View File

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

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

89
autonomic/command/init.py Normal file
View File

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

28
autonomic/config.py Normal file
View File

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

43
autonomic/infra.py Normal file
View File

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

150
autonomic/logger.py Normal file
View File

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

49
autonomic/settings.py Normal file
View File

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

174
autonomic/utils.py Normal file
View File

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

13
autonomic/yaml.py Normal file
View File

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

View File

@ -1,8 +1,8 @@
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']
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"]

View File

@ -20,11 +20,9 @@ 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]

View File

@ -6,7 +6,7 @@ max-line-length = 80
[isort]
known_first_party = autonomic
known_third_party = pytest
known_third_party = pytest, psutil
line_length = 80
multi_line_output = 3
skip = .venv, .tox
@ -43,13 +43,19 @@ 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__:main
autonomic = autonomic.__main__:autonomic
[build_sphinx]
all_files = 1

20
test/test_settings.py Normal file
View File

@ -0,0 +1,20 @@
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

44
test/test_utils.py Normal file
View File

@ -0,0 +1,44 @@
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

View File

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

5
test/test_yaml.py Normal file
View File

@ -0,0 +1,5 @@
from autonomic.yaml import yaml
def test_yaml_config():
assert yaml.explicit_start is True

View File

@ -6,7 +6,6 @@ envlist =
format
type
docs
changelog
metadata-release
skip_missing_interpreters = True
isolated_build = True
@ -50,6 +49,7 @@ description = build the documentation
skipdist = True
deps =
sphinx
sphinx-autodoc-typehints
sphinx_rtd_theme
commands = python -m setup build_sphinx