Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
cd3bd67783 | |||
b6187f4b1a | |||
082892101e | |||
fd75435a9c | |||
af2a228310 | |||
dcf23b8c35 | |||
3ff41797c2 | |||
2cd0c430cc | |||
3d76c710cc | |||
3e03c91cb3 | |||
fc33fd7a78 | |||
cb5d6fc77d | |||
b3c64f0403 | |||
49be06d41f | |||
1adab407e8 | |||
58b09694df | |||
9c692f2335 | |||
324d58690d | |||
3eade299a0 | |||
b69bfa28b4 | |||
c69bdec4c9 | |||
0806b4e134 | |||
7ee473c9b6 | |||
b58b9c3296 | |||
c6608c5568 | |||
bba4ec271d | |||
e5127602f0 | |||
2c894b5d1d | |||
e5df45650d | |||
b250604df5 |
28
.drone.yml
Normal file
28
.drone.yml
Normal 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
2
.envrc.sample
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# The path to our pass credentials store
|
||||||
|
export PASSWORD_STORE_DIR=$(pwd)/../infrastructure/credentials/password-store
|
@ -0,0 +1,17 @@
|
|||||||
|
Autonomic 0.0.2 (2020-04-11)
|
||||||
|
============================
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- Init command. (#2)
|
||||||
|
- Actions command. (#3)
|
||||||
|
|
||||||
|
|
||||||
|
Autonomic 0.0.1 (2020-04-03)
|
||||||
|
============================
|
||||||
|
|
||||||
|
Project announcements
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
- Initial project release! (#1)
|
||||||
|
@ -20,3 +20,10 @@ 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,9 +2,12 @@
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from autonomic.command import actions, init
|
||||||
|
|
||||||
@click.command()
|
|
||||||
def main():
|
@click.group()
|
||||||
|
@click.pass_context
|
||||||
|
def autonomic(ctx):
|
||||||
"""
|
"""
|
||||||
\b
|
\b
|
||||||
___ _ _
|
___ _ _
|
||||||
@ -13,9 +16,12 @@ def main():
|
|||||||
| _ | | | | __/ _ \| '_ \ / _ \| '_ ` _ \| |/ __|
|
| _ | | | | __/ _ \| '_ \ / _ \| '_ ` _ \| |/ __|
|
||||||
| | | | |_| | || (_) | | | | (_) | | | | | | | (__
|
| | | | |_| | || (_) | | | | (_) | | | | | | | (__
|
||||||
\_| |_/\__,_|\__\___/|_| |_|\___/|_| |_| |_|_|\___|
|
\_| |_/\__,_|\__\___/|_| |_|\___/|_| |_| |_|_|\___|
|
||||||
|
|
||||||
|
Hack the planet!
|
||||||
|
|
||||||
""" # noqa
|
""" # noqa
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
autonomic.add_command(init.init)
|
||||||
main()
|
autonomic.add_command(actions.actions)
|
||||||
|
39
autonomic/command/actions.py
Normal file
39
autonomic/command/actions.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""Actions module."""
|
||||||
|
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
import click
|
||||||
|
from PyInquirer import prompt
|
||||||
|
|
||||||
|
from autonomic.config import ACTIONS_DIR, INFRA_DIR, PASS_STORE_DIR
|
||||||
|
from autonomic.infra import get_passwd, run_play
|
||||||
|
from autonomic.settings import get
|
||||||
|
from autonomic.utils import git_status, qlist
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.pass_context
|
||||||
|
def actions(ctx):
|
||||||
|
"""Run an Ansible action."""
|
||||||
|
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"]
|
||||||
|
question = qlist("action", "Which Ansible action?", choices,)
|
||||||
|
action = prompt(question)["action"]
|
||||||
|
|
||||||
|
if any(action in choice for choice in ["newhetzner", "rmhetzner"]):
|
||||||
|
choices = ["prod", "test", "cicd"]
|
||||||
|
question = qlist("key", "Which Hetzner API key?", choices)
|
||||||
|
key = prompt(question)["key"]
|
||||||
|
|
||||||
|
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)
|
89
autonomic/command/init.py
Normal file
89
autonomic/command/init.py
Normal 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 PyInquirer import prompt
|
||||||
|
|
||||||
|
from autonomic.config import CONFIG_DIR, CONFIG_YAML, INFRA_DIR, INFRA_REPO
|
||||||
|
from autonomic.infra import members
|
||||||
|
from autonomic.logger import log
|
||||||
|
from autonomic.settings import add
|
||||||
|
from autonomic.utils import is_proc, qlist, run
|
||||||
|
|
||||||
|
|
||||||
|
@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")
|
||||||
|
question = qlist("username", "What is you Autonomic username?", usernames)
|
||||||
|
answer = prompt(question)
|
||||||
|
add(answer)
|
||||||
|
|
||||||
|
msg = "Welcome comrade {} :kissing:".format(answer["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
28
autonomic/config.py
Normal 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
43
autonomic/infra.py
Normal 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
150
autonomic/logger.py
Normal 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__)
|
47
autonomic/settings.py
Normal file
47
autonomic/settings.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""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 autonomic.config import CONFIG_YAML
|
||||||
|
from autonomic.utils import yaml_dump, yaml_load
|
||||||
|
|
||||||
|
|
||||||
|
def add(yaml):
|
||||||
|
"""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
|
107
autonomic/utils.py
Normal file
107
autonomic/utils.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
"""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 subprocess import call, check_output
|
||||||
|
|
||||||
|
from psutil import process_iter
|
||||||
|
|
||||||
|
from autonomic.config import INFRA_DIR
|
||||||
|
from autonomic.logger import log
|
||||||
|
from autonomic.yaml import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, cwd=None, interactive=False, **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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
exit(code)
|
||||||
|
|
||||||
|
|
||||||
|
def qlist(name, message, choices):
|
||||||
|
"""A question in list format."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"type": "list",
|
||||||
|
"name": name,
|
||||||
|
"message": message,
|
||||||
|
"choices": choices,
|
||||||
|
"filter": lambda answer: answer.lower(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def yaml_load(fpath):
|
||||||
|
"""Load a YAML file."""
|
||||||
|
try:
|
||||||
|
with open(fpath, "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)
|
13
autonomic/yaml.py
Normal file
13
autonomic/yaml.py
Normal 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
|
@ -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'
|
project = "autonomic utility belt"
|
||||||
templates_path = ['_templates']
|
templates_path = ["_templates"]
|
||||||
extensions = ['sphinx.ext.autodoc', 'sphinx_autodoc_typehints']
|
extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"]
|
||||||
|
@ -20,11 +20,9 @@ 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]
|
||||||
|
@ -6,7 +6,7 @@ max-line-length = 80
|
|||||||
|
|
||||||
[isort]
|
[isort]
|
||||||
known_first_party = autonomic
|
known_first_party = autonomic
|
||||||
known_third_party = pytest
|
known_third_party = pytest, psutil
|
||||||
line_length = 80
|
line_length = 80
|
||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
skip = .venv, .tox
|
skip = .venv, .tox
|
||||||
@ -43,13 +43,18 @@ 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
|
||||||
|
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__:main
|
autonomic = autonomic.__main__:autonomic
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
all_files = 1
|
all_files = 1
|
||||||
|
20
test/test_settings.py
Normal file
20
test/test_settings.py
Normal 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
|
58
test/test_utils.py
Normal file
58
test/test_utils.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from subprocess import STDOUT
|
||||||
|
|
||||||
|
from autonomic.utils import is_proc, qlist, 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_make_qlist():
|
||||||
|
output = qlist("foo", "bar", ["bang"])
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"type": "list",
|
||||||
|
"name": "foo",
|
||||||
|
"message": "bar",
|
||||||
|
"choices": ["bang"],
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val in expected.items():
|
||||||
|
assert expected[key] == output[0][key]
|
||||||
|
|
||||||
|
|
||||||
|
def test_yaml_load(tmp_path):
|
||||||
|
directory = tmp_path / "test"
|
||||||
|
directory.mkdir()
|
||||||
|
|
||||||
|
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"
|
||||||
|
5
test/test_yaml.py
Normal file
5
test/test_yaml.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from autonomic.yaml import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def test_yaml_config():
|
||||||
|
assert yaml.explicit_start is True
|
2
tox.ini
2
tox.ini
@ -6,7 +6,6 @@ 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
|
||||||
@ -50,6 +49,7 @@ 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