1
0
mirror of https://github.com/inofix/maestro.py.git synced 2026-02-05 09:45:24 +01:00
Files
maestro.py/main.py
2019-04-23 10:21:42 +02:00

266 lines
8.8 KiB
Python

import imp
import os
import pathlib
import re
import shutil
import subprocess
import sys
from git.repo.base import Repo
import click
import yaml
from maestro import settings
# source the local python config file
if os.path.isfile('.maestro.yml'):
with open('.maestro.yml') as f:
settings.__dict__.update(yaml.load(f, Loader=yaml.Loader))
else:
click.secho('Warning: No .maestro.yml found!', fg='red')
#
# CLI commands
#
class MaestroCatch(click.Group):
"""This class will catch all errors and print them nicely"""
def invoke(self, ctx):
try:
return super().invoke(ctx)
except (KeyError, AttributeError) as exc:
click.secho('fatal: {} — did you provide a .maestro.yml file?'.format(exc), fg='red')
except Exception as exc:
click.secho('fatal: {}'.format(exc), fg='red')
@click.group(cls=MaestroCatch)
def main():
pass
@main.command()
def init():
initialize_workdir()
do_init()
do_reinit()
@main.command()
def reinit():
initialize_workdir()
do_reinit()
@main.command()
def list():
nodes = get_nodes()
process_nodes(list_node, nodes)
@main.command()
def shortlist():
nodes = get_nodes()
process_nodes(list_node_short, nodes)
@main.command()
@click.option('-n', '--node', 'nodeFilter', help='filter by node')
@click.option('-p', '--project', 'projectFilter', help='filter by project')
@click.option('-c', '--class', 'classFilter', help='filter by class')
def reclass(nodeFilter, classFilter, projectFilter):
print_plain_reclass(nodeFilter, classFilter, projectFilter)
#
# Functions
#
def print_usage():
print('Usage: {} [option] action'.format(sys.argv[0]))
def error(*args):
print('Error: {}'.format(''.join(str(x) for x in args)))
sys.exit(1)
def do_init():
for repository_name, repository_remote in settings.toclone.items():
git_dest = ''
if repository_name in settings.inventorydirs:
git_dest = settings.inventorydirs[repository_name]
elif repository_name in settings.playbookdirs:
git_dest = settings.playbookdirs[repository_name]
elif repository_name in settings.localdirs:
git_dest = settings.localdirs[repository_name]
else:
print('there is no corresponding directory defined in your config for {}'.format(repository))
if not git_dest:
print('could not find git_dest')
continue
if os.path.isdir('{}/.git'.format(git_dest)):
print('update repository {}'.format(git_dest))
git = Repo(git_dest).git
git.pull()
else:
print('clone repository {}'.format(git_dest))
os.makedirs(git_dest, exist_ok=True)
Repo.clone_from(repository_remote, git_dest)
def do_reinit():
print('Re-create the inventory. Note: there will be warnings for duplicates, etc.')
to_reinit = ['nodes', 'classes']
for _dir in to_reinit:
to_refresh = '{}/{}'.format(settings.INVENTORYDIR, _dir)
shutil.rmtree(to_refresh, ignore_errors=True)
os.makedirs(to_refresh)
for idir in settings.inventorydirs.values():
for _dir in to_reinit:
copy_directories_and_yaml(idir, _dir)
print('Re-connect ansible to our reclass inventory')
if os.path.isfile('{}/hosts'.format(settings.INVENTORYDIR)):
os.remove('{}/hosts'.format(settings.INVENTORYDIR))
if os.path.isfile('{}/reclass-config.yml'.format(settings.INVENTORYDIR)):
os.remove('{}/reclass-config.yml'.format(settings.INVENTORYDIR))
if not os.path.isfile(settings.ANSIBLE_CONNECT):
error('reclass is not installed (looked in {})'.format(settings.ANSIBLE_CONNECT))
os.symlink(settings.ANSIBLE_CONNECT, '{}/hosts'.format(settings.INVENTORYDIR))
# TODO: checkout $_pre
if True:
with open('{}/reclass-config.yml'.format(settings.INVENTORYDIR), 'w+') as f:
f.write(settings.RECLASS_CONFIG_INITIAL)
print('Installed reclass config')
with open('./ansible.cfg', 'w+') as f:
f.write(settings.ANSIBLE_CONFIG_INITIAL)
print('Installed ansible config')
# TODO: check how/why to assign $ANSIBLE_CONFIG
print('Installing all necessary ansible-galaxy roles')
for _dir in settings.playbookdirs.values():
f = '{}/{}'.format(_dir, settings.GALAXYROLES)
if not os.path.isfile(f):
continue
print('Found {}'.format(f))
with open(f) as fi:
lines = fi.readlines()
if not any(re.match(r'^- src:', line) for line in lines):
print(' .. but it was empty, ignoring..')
continue
if subprocess.call([settings.ANSIBLE_GALAXY, 'install', '-r', f]):
error(
"ansible-galaxy failed to perform the " \
"installation. Please make sure all the " \
"roles exist and that you have " \
"write access to the ansible 'roles_path', " \
"it can be controled in ansible.cfg in " \
"the [defaults] section."
)
print('done')
def initialize_workdir():
pathlib.Path(settings.WORKDIR).mkdir(parents=True, exist_ok=True)
def copy_directories_and_yaml(frm, subdir):
frm = os.path.abspath(frm)
for (dirpath, _, filenames) in os.walk('{}/{}'.format(frm, subdir)):
os.makedirs(dirpath.replace(frm, settings.INVENTORYDIR), exist_ok=True)
for fname in filenames:
if fname.endswith('.yml'):
path = '{}/{}'.format(dirpath, fname)
os.symlink(path, path.replace(frm, settings.INVENTORYDIR))
# First call to reclass to get an overview of the hosts available
def get_nodes():
nodes_path = '{}/nodes'.format(settings.INVENTORYDIR)
if not os.path.isdir(nodes_path):
error('reclass environment not found at {}'.format(nodes_path))
reclass_filter = ''
if len(settings.PROJECTFILTER):
if os.path.isdir('{}/{}'.format(nodes_path, settings.PROJECTFILTER)):
reclass_filter = '-u nodes/{}'.format(settings.PROJECTFILTER)
else:
error('This project does not exist in {}'.format(settings.INVENTORYDIR))
res = subprocess.check_output(['reclass', '-b', settings.INVENTORYDIR, '-i'])
yamlresult = yaml.load(res, Loader=yaml.Loader)
# TODO: process_nodes process_classes etc.
return yamlresult['nodes']
def list_node(name, properties):
project = properties['__reclass__']['node'].split('/')[0]
parameters = properties['parameters']
output = name
output += click.style(' ({}:{})'.format(properties.get('environment'),
project),
fg='cyan')
role = properties['parameters'].get('role', '')
role_colors = {
'development': 'red',
'fallback': 'green',
'productive': 'yellow',
}
output += click.style(role, fg=role_colors.get(role, 'reset'))
codename = parameters.get('os__codename', '')
release = parameters.get('os__release', '')
output += click.style(' ({}{}{})'.format(
parameters.get('os__distro', ''),
'-' + codename if codename else '',
' ' + release if release else ''),
fg='blue'
)
print(output)
def list_node_short(name, _):
print(name)
def process_nodes(command, nodes):
for key, value in sorted(nodes.items(), reverse=True):
command(key, value)
def print_plain_reclass(nodeFilter, classFilter, projectFilter):
dirStructure = os.walk('{}/nodes/'.format(settings.INVENTORYDIR), followlinks=True)
nodes_uri = ''
reclassmode = ''
filterednodes = []
if nodeFilter is not None:
for (dirpath, dirnames, filenames) in dirStructure:
for filename in filenames:
node = os.path.basename(filename).replace('.yml', '')
if nodeFilter in node:
reclassmode = node
pass
if len(reclassmode):
reclassmode = '-n {}'.format(reclassmode)
else:
error('The node does not seem to exist: {}'.format(nodeFilter))
else:
reclassmode = '-i'
if projectFilter is not None:
import pdb; pdb.set_trace()
nodes_uri = '{}/nodes/{}'.format(settings.INVENTORYDIR, projectFilter)
if not os.path.isdir(nodes_uri):
error('No such project dir: {}'.format(nodes_uri))
elif classFilter is not None:
error("Classes are not supported here, use project filter instead")
args = ['reclass', '-b', settings.INVENTORYDIR, nodes_uri, reclassmode]
if len(nodes_uri):
args = ['reclass', '-b', settings.INVENTORYDIR, '-u', nodes_uri, reclassmode]
res = subprocess.check_output(args)
print('reclass_result', reclass_result)
# def print_warning(param):
# if verbose > 1:
# click.secho('Warning', fg='yellow')
# print('Warning')