Add: Initial code base as POC
This commit is contained in:
commit
a3e6bbafd7
0
ansible_tree/__init__.py
Normal file
0
ansible_tree/__init__.py
Normal file
222
ansible_tree/app.py
Executable file
222
ansible_tree/app.py
Executable file
@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# import sys
|
||||
# sys.path.append("/home/jez/prj/bell/training/tiger-ansible/ext/ansible-tree")
|
||||
|
||||
|
||||
import sys
|
||||
import yaml
|
||||
import anyconfig
|
||||
from pprint import pprint
|
||||
|
||||
from ansible_tree.files import BackendsManager, RulesManager
|
||||
from ansible_tree.utils import schema_validate
|
||||
import anyconfig
|
||||
# from box import Box
|
||||
from pathlib import Path
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class Query():
|
||||
|
||||
matcher_merge_schema = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"mergeStrategy": "append",
|
||||
# "mergeStrategy": "arrayMergeById",
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"mergeStrategy": "objectMerge",
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, app):
|
||||
|
||||
self.app = app
|
||||
|
||||
self.key = None
|
||||
self.scope = None
|
||||
|
||||
self.paths = None
|
||||
self.data = None
|
||||
self.result = None
|
||||
|
||||
|
||||
self.matcher_schema = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"rule": {
|
||||
"type": "string",
|
||||
"default": ".*",
|
||||
"optional": True,
|
||||
},
|
||||
"strategy": {
|
||||
"type": "string",
|
||||
"default": "merge",
|
||||
"optional": True,
|
||||
"enum": ["first", "last", "merge"],
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"default": self.matcher_merge_schema,
|
||||
#"default": {},
|
||||
"optional": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
def exec(self, key=None, scope=None, policy=None, trace=False, explain=False):
|
||||
|
||||
bm = BackendsManager(app=self.app)
|
||||
mm = RulesManager(app=self.app)
|
||||
|
||||
log.debug(f"New query created")
|
||||
candidates = bm.query(key, scope, trace=trace)
|
||||
result = mm.get_result(candidates, key=key, trace=trace, explain=explain)
|
||||
return result
|
||||
|
||||
def dump(self):
|
||||
|
||||
ret = {}
|
||||
for i in dir(self):
|
||||
if not i.startswith('_'):
|
||||
ret[i] = getattr(self, i)
|
||||
|
||||
pprint (ret)
|
||||
|
||||
|
||||
|
||||
class App():
|
||||
|
||||
schema = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"default": {},
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "object",
|
||||
"optional": True,
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"app": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"root": {
|
||||
"type": "string",
|
||||
"default": None,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"tree": {
|
||||
#"additionalProperties": False,
|
||||
"type": "object",
|
||||
"default": {},
|
||||
},
|
||||
"rules": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
"tree": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, config="albero.yml", namespace='default'):
|
||||
conf2 = anyconfig.load(config)
|
||||
|
||||
# Validate configuration
|
||||
schema_validate(conf2, self.schema)
|
||||
try:
|
||||
conf2 = conf2[namespace]
|
||||
except KeyError:
|
||||
log.error (f"Can't find namespace '{namespace}' in config '{config}'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Init
|
||||
if not conf2['config']['app']['root']:
|
||||
conf2['config']['app']['root'] = Path(config).parent
|
||||
else:
|
||||
conf2['config']['app']['root'] = Path(conf2['config']['app']['root'])
|
||||
|
||||
# Finish
|
||||
self.conf2 = dict(conf2)
|
||||
|
||||
def lookup(self, key=None, policy=None, scope=None, trace=False, explain=False):
|
||||
log.debug(f"Lookup key {key} with scope: {scope}")
|
||||
q = Query(app = self)
|
||||
r = q.exec(key=key, scope=scope , policy=policy, trace=trace, explain=explain)
|
||||
|
||||
print ("=== Query Result ===")
|
||||
print(anyconfig.dumps(r, ac_parser='yaml'))
|
||||
print ("=== Query Result ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
CONFIG_FILE='/home/jez/prj/bell/training/tiger-ansible/tree.yml'
|
||||
app = App(CONFIG_FILE)
|
||||
|
||||
policy = None
|
||||
|
||||
#app.lookup(
|
||||
# "my_key",
|
||||
# policy=None,
|
||||
# hostname="myhost-lab.it.ms.bell.ca",
|
||||
# hostgroups=["Tiger", "Tiger/Test", "Tiger/Test/LastLvl"],
|
||||
# hostgroup="Tiger/Test/LastLvl"
|
||||
# )
|
||||
|
||||
|
||||
# app.lookup(
|
||||
# None,
|
||||
# hostname="myhost-lab.it.ms.bell.ca",
|
||||
# hostgroups=["Tiger", "Tiger/Test", "Tiger/Test/LastLvl"],
|
||||
# hostgroup="Tiger/Test/LastLvl"
|
||||
# )
|
||||
#
|
||||
|
||||
|
||||
print ("OKKKKK")
|
||||
180
ansible_tree/cli.py
Normal file
180
ansible_tree/cli.py
Normal file
@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Run like this:
|
||||
# python3 python_cli.py -vvvv demo
|
||||
# Author: mrjk
|
||||
|
||||
import os
|
||||
import anyconfig
|
||||
import sys
|
||||
import logging
|
||||
import argparse
|
||||
from pprint import pprint
|
||||
|
||||
import sys
|
||||
|
||||
# Devel tmp
|
||||
sys.path.append("/home/jez/prj/bell/training/tiger-ansible/ext/ansible-tree")
|
||||
|
||||
import ansible_tree.app as Albero
|
||||
|
||||
class CmdApp:
|
||||
"""Main CmdApp"""
|
||||
|
||||
def __init__(self):
|
||||
"""Start new App"""
|
||||
|
||||
self.get_args()
|
||||
self.get_logger(verbose=self.args.verbose, logger_name="ansible_tree")
|
||||
self.cli()
|
||||
|
||||
def get_logger(self, logger_name=None, create_file=False, verbose=0):
|
||||
"""Create CmdApp logger"""
|
||||
|
||||
# Take default app name
|
||||
if not logger_name:
|
||||
logger_name = __name__
|
||||
|
||||
# Manage logging level
|
||||
try:
|
||||
loglevel = {
|
||||
0: logging.ERROR,
|
||||
1: logging.WARN,
|
||||
2: logging.INFO,
|
||||
3: logging.DEBUG,
|
||||
}[verbose]
|
||||
except KeyError:
|
||||
loglevel = logging.DEBUG
|
||||
|
||||
# Create logger for prd_ci
|
||||
log = logging.getLogger(logger_name)
|
||||
log.setLevel(level=loglevel)
|
||||
|
||||
# Formatters
|
||||
format1 = "%(levelname)8s: %(message)s"
|
||||
format2 = "%(asctime)s.%(msecs)03d|%(name)-16s%(levelname)8s: %(message)s"
|
||||
format3 = (
|
||||
"%(asctime)s.%(msecs)03d"
|
||||
+ " (%(process)d/%(thread)d) "
|
||||
+ "%(pathname)s:%(lineno)d:%(funcName)s"
|
||||
+ ": "
|
||||
+ "%(levelname)s: %(message)s"
|
||||
)
|
||||
tformat1 = "%H:%M:%S"
|
||||
tformat2 = "%Y-%m-%d %H:%M:%S"
|
||||
formatter = logging.Formatter(format1, tformat1)
|
||||
|
||||
# Create console handler for logger.
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(level=logging.DEBUG)
|
||||
ch.setFormatter(formatter)
|
||||
log.addHandler(ch)
|
||||
|
||||
# Create file handler for logger.
|
||||
if isinstance(create_file, str):
|
||||
fh = logging.FileHandler(create_file)
|
||||
fh.setLevel(level=logging.DEBUG)
|
||||
fh.setFormatter(formatter)
|
||||
log.addHandler(fh)
|
||||
|
||||
# Return objects
|
||||
self.log = log
|
||||
self.loglevel = loglevel
|
||||
|
||||
def cli(self):
|
||||
"""Main cli command"""
|
||||
|
||||
# Dispatch sub commands
|
||||
if self.args.command:
|
||||
method = "cli_" + str(self.args.command)
|
||||
if hasattr(self, method):
|
||||
getattr(self, method)()
|
||||
else:
|
||||
self.log.error(f"Subcommand {self.args.command} does not exists.")
|
||||
else:
|
||||
self.log.error("Missing sub command")
|
||||
self.parser.print_help()
|
||||
|
||||
def get_args(self):
|
||||
"""Prepare command line"""
|
||||
|
||||
# Manage main parser
|
||||
parser = argparse.ArgumentParser(description="Albero, to lookup hierarchical data")
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="count", default=0, help="Increase verbosity"
|
||||
)
|
||||
parser.add_argument("help", action="count", default=0, help="Show usage")
|
||||
subparsers = parser.add_subparsers(
|
||||
title="subcommands", description="valid subcommands", dest="command"
|
||||
)
|
||||
|
||||
# Manage command: demo
|
||||
add_p = subparsers.add_parser("lookup")
|
||||
add_p.add_argument("-n", "--namespace", help="Namespace name", default='default')
|
||||
add_p.add_argument("-f", "--file", help="File with params as dict. Can be stdin - .")
|
||||
add_p.add_argument("-e", "--scope", dest="scope_param", action="append", default=[])
|
||||
add_p.add_argument("-p", "--policy")
|
||||
add_p.add_argument("-t", "--trace", action="store_true")
|
||||
add_p.add_argument("-x", "--explain", action="store_true")
|
||||
add_p.add_argument("key", default=None, nargs="*")
|
||||
|
||||
# Manage command: demo
|
||||
add_p = subparsers.add_parser("demo")
|
||||
add_p.add_argument("--env", default=os.environ.get("APP_SETTING", "Unset"))
|
||||
add_p.add_argument("--choice", choices=["choice1", "choice2"], type=str)
|
||||
add_p.add_argument("-s", "--store", action="store_true")
|
||||
add_p.add_argument("-a", "--append", dest="appended", action="append")
|
||||
# add_p.add_argument("--short", default=True, required=True)
|
||||
# add_p.add_argument("argument1")
|
||||
# add_p.add_argument("double_args", nargs=2)
|
||||
add_p.add_argument("nargs", nargs="*")
|
||||
|
||||
# Manage command: subcommand2
|
||||
upg_p = subparsers.add_parser("subcommand2")
|
||||
upg_p.add_argument("name")
|
||||
|
||||
# Register objects
|
||||
self.parser = parser
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def cli_demo(self):
|
||||
"""Display how to use logging"""
|
||||
|
||||
self.log.error("Test Critical message")
|
||||
self.log.warning("Test Warning message")
|
||||
self.log.info("Test Info message")
|
||||
self.log.debug(f"Command line vars: {vars(self.args)}")
|
||||
|
||||
def cli_lookup(self):
|
||||
"""Display how to use logging"""
|
||||
|
||||
config = '/home/jez/prj/bell/training/tiger-ansible/tree.yml'
|
||||
|
||||
# self.log.debug(f"Command line vars: {vars(self.args)}")
|
||||
keys = self.args.key or [None]
|
||||
|
||||
# Parse payload from enf file:
|
||||
new_params = {}
|
||||
if self.args.file:
|
||||
new_params = anyconfig.load(self.args.file, ac_parser="yaml")
|
||||
|
||||
# Parse cli params
|
||||
for i in self.args.scope_param:
|
||||
r = i.split('=')
|
||||
if len(r) != 2:
|
||||
raise Exception("Malformed params")
|
||||
new_params[r[0]] = r[1]
|
||||
|
||||
self.log.info(f"CLI: {keys} with env: {new_params}")
|
||||
|
||||
app = Albero.App(config=config, namespace=self.args.namespace)
|
||||
for key in keys:
|
||||
app.lookup(key=key,
|
||||
scope=new_params,
|
||||
trace=self.args.trace,
|
||||
explain=self.args.explain
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = CmdApp()
|
||||
437
ansible_tree/files.py
Normal file
437
ansible_tree/files.py
Normal file
@ -0,0 +1,437 @@
|
||||
|
||||
import copy
|
||||
import json
|
||||
import textwrap
|
||||
from prettytable import PrettyTable
|
||||
from pathlib import Path
|
||||
# from box import Box
|
||||
from jsonmerge import Merger
|
||||
import re
|
||||
import logging
|
||||
from pprint import pprint
|
||||
import collections
|
||||
|
||||
|
||||
from ansible_tree.utils import schema_validate, str_ellipsis
|
||||
import ansible_tree.plugin as TreePlugins
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
|
||||
# DEPRECATED class BackendEngineLoader():
|
||||
# DEPRECATED
|
||||
# DEPRECATED def get_class(self, item):
|
||||
# DEPRECATED engine_name = item.get('engine')
|
||||
# DEPRECATED assert (isinstance(engine_name, str)), f"Got: {engine_name} for {item}"
|
||||
# DEPRECATED
|
||||
# DEPRECATED # Check engine presence
|
||||
# DEPRECATED if not hasattr(TreePlugins.engine, engine_name):
|
||||
# DEPRECATED raise Exception(f"No plugin {engine_name} found for entry {item}!")
|
||||
# DEPRECATED
|
||||
# DEPRECATED cls = getattr(TreePlugins.engine, engine_name).Plugin
|
||||
# DEPRECATED return cls
|
||||
|
||||
|
||||
|
||||
# def BackendPluginInit(backends, ctx):
|
||||
#
|
||||
# for be in backends:
|
||||
# be.run = {}
|
||||
# be.scope = ctx['scope']
|
||||
# be.key = ctx['key']
|
||||
#
|
||||
# return backends
|
||||
|
||||
# def BackendPluginHier(backends, ctx):
|
||||
#
|
||||
# new_backends = []
|
||||
# for cand in backends:
|
||||
#
|
||||
# # Init
|
||||
# plugin_config = cand.config.get("hierarchy", None)
|
||||
# hier_data = plugin_config.get("data", None)
|
||||
# if not hier_data:
|
||||
# new_backends.append(cand)
|
||||
# continue
|
||||
#
|
||||
# # Retrieve config data
|
||||
# hier_var = plugin_config.get("var", "item")
|
||||
# hier_sep = plugin_config.get("separator", "/")
|
||||
# if isinstance(hier_data, str):
|
||||
# hier_data = cand.scope.get(hier_data, None)
|
||||
#
|
||||
# # Build a new list
|
||||
#
|
||||
# pprint (plugin_config)
|
||||
# pprint (hier_data)
|
||||
#
|
||||
# if isinstance(hier_data, str):
|
||||
# r = hier_data.split(hier_sep)
|
||||
# assert (isinstance(r, list)), f"Got: {r}"
|
||||
#
|
||||
# ret1 = []
|
||||
# for index, part in enumerate(r):
|
||||
#
|
||||
# try:
|
||||
# prefix = ret1[index - 1]
|
||||
# except IndexError:
|
||||
# prefix = f'{hier_sep}'
|
||||
# prefix = ""
|
||||
# item = f"{prefix}{part}{hier_sep}"
|
||||
# ret1.append(item)
|
||||
#
|
||||
# ret2 = []
|
||||
# for item in ret1:
|
||||
# _cand = copy.deepcopy(cand)
|
||||
# run = {
|
||||
# "index": index,
|
||||
# "hier_value": item,
|
||||
# "hier_var": hier_var,
|
||||
# }
|
||||
# _cand.run['hier'] = run
|
||||
# _cand.scope[hier_var] = item
|
||||
# ret2.append(_cand)
|
||||
# print ("RESULT")
|
||||
# pprint (ret2)
|
||||
#
|
||||
# new_backends.extend(ret2)
|
||||
# return new_backends
|
||||
#
|
||||
#
|
||||
|
||||
#def BackendPluginLoop(backends, ctx):
|
||||
#
|
||||
# new_backends = []
|
||||
# for cand in backends:
|
||||
#
|
||||
# # Init
|
||||
# loop_config = cand.config.get("loop", None)
|
||||
# loop_data = loop_config.get("data", None)
|
||||
# if not loop_data:
|
||||
# new_backends.append(cand)
|
||||
# continue
|
||||
#
|
||||
# # Retrieve config data
|
||||
# loop_var = loop_config.get("var", "item")
|
||||
# if isinstance(loop_data, str):
|
||||
# loop_data = cand.scope.get(loop_data, None)
|
||||
# assert (isinstance(loop_data, list)), f"Got: {loop_data}"
|
||||
#
|
||||
# # Build a new list
|
||||
# ret = []
|
||||
# for idx, item in enumerate(loop_data):
|
||||
# _cand = copy.deepcopy(cand)
|
||||
# run = {
|
||||
# "loop_index": idx,
|
||||
# "loop_value": item,
|
||||
# "loop_var": loop_var,
|
||||
# }
|
||||
# _cand.run['loop'] = run
|
||||
# _cand.scope[loop_var] = item
|
||||
# ret.append(_cand)
|
||||
#
|
||||
# new_backends.extend(ret)
|
||||
#
|
||||
# return new_backends
|
||||
|
||||
|
||||
class LoadPlugin():
|
||||
|
||||
def __init__(self, plugins):
|
||||
self.plugins = plugins
|
||||
|
||||
def load(self, kind, name):
|
||||
|
||||
assert (isinstance(name, str)), f"Got: {name}"
|
||||
|
||||
# Get plugin kind
|
||||
try:
|
||||
plugins = getattr(self.plugins, kind)
|
||||
except Exception as e:
|
||||
raise Exception(f"Unknown module kind '{kind}': {e}")
|
||||
|
||||
# Get plugin class
|
||||
try:
|
||||
plugin_cls = getattr(plugins, name)
|
||||
except Exception as e:
|
||||
raise Exception(f"Unknown module '{kind}.{name}': {e}")
|
||||
|
||||
assert (hasattr(plugin_cls, 'Plugin')), f'Plugin {kind}/{name} is not a valid plugin'
|
||||
|
||||
# Return plugin Classe
|
||||
return plugin_cls.Plugin
|
||||
|
||||
|
||||
|
||||
class BackendsManager():
|
||||
|
||||
_schema_props_default = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"default": "",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"default": "BLAAAAHHH"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": True,
|
||||
"default": {},
|
||||
"properties": {
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"default": "jerakia",
|
||||
"optional": False,
|
||||
},
|
||||
"value": {
|
||||
"default": 'UNSET',
|
||||
"optional": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
def _validate_item(self, item):
|
||||
if isinstance(item, str):
|
||||
item = {
|
||||
"engine": self.config_main.default_engine,
|
||||
"value": item,
|
||||
}
|
||||
item = schema_validate(item, self._schema_props_default)
|
||||
assert (isinstance(item, dict))
|
||||
return item
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
self.config_app = app.conf2['config']['app']
|
||||
self.config_main = app.conf2['config']['tree']
|
||||
self.config_items = list(app.conf2['tree'])
|
||||
# THIS MAKE A BUG !!!! self.plugin_loader = LoadPlugin(TreePlugins)
|
||||
|
||||
|
||||
self.plugins = [
|
||||
'init',
|
||||
'loop',
|
||||
'hier',
|
||||
]
|
||||
|
||||
# Auto init
|
||||
self.backends = self.config_items
|
||||
|
||||
|
||||
def query(self, key=None, scope=None, trace=False):
|
||||
backends = self.get_backends(key=key, scope=scope, trace=trace)
|
||||
ret = self.get_results(backends, trace=trace)
|
||||
return ret
|
||||
|
||||
def get_backends(self, key=None, scope=None, trace=False):
|
||||
log.debug(f"Look for candidates for key '{key}' in backend: {self.backends}")
|
||||
|
||||
# Prepare plugins
|
||||
plugin_loader = LoadPlugin(TreePlugins)
|
||||
_run = {
|
||||
"key": key,
|
||||
"scope": scope,
|
||||
}
|
||||
|
||||
# Preprocess backends plugins
|
||||
backends = self.config_items
|
||||
log.debug(f"Backend preprocessing of {len(backends)} elements")
|
||||
for plugin in self.plugins:
|
||||
#backend_cls = plugin_loader.load('backend', plugin)
|
||||
plugin = plugin_loader.load(
|
||||
'backend', plugin
|
||||
)()
|
||||
|
||||
log.debug(f"Run {plugin}")
|
||||
new_backend, _run = plugin.process(backends, _run)
|
||||
|
||||
assert(isinstance(new_backend, list)), f"Got: {new_backend}"
|
||||
assert(isinstance(_run, dict)), f"Got: {_run}"
|
||||
backends = new_backend
|
||||
|
||||
# pprint (backends)
|
||||
for i in backends:
|
||||
assert (i.get('engine')), f"Got: {i}"
|
||||
|
||||
log.debug(f"Backend preprocessing made {len(backends)} elements")
|
||||
return backends
|
||||
|
||||
|
||||
def get_results(self, backends, trace=False):
|
||||
|
||||
# Prepare plugins
|
||||
plugin_loader = LoadPlugin(TreePlugins)
|
||||
|
||||
new_results = []
|
||||
for backend in backends:
|
||||
#result_cls = result_loader.load('result', result)
|
||||
# print ("BACKKENNDNNDNDNDND")
|
||||
# pprint(backend)
|
||||
|
||||
engine = plugin_loader.load(
|
||||
'engine', backend['engine']
|
||||
)(
|
||||
backend,
|
||||
parent=self, app=self.app)
|
||||
|
||||
log.debug(f"Run engine: {engine}")
|
||||
new_result = engine.process()
|
||||
|
||||
assert(isinstance(new_result, list)), f"Got: {new_result}"
|
||||
new_results.extend(new_result)
|
||||
|
||||
# Filter out? Not here !new_results = [i for i in new_results if i['found'] ]
|
||||
# pprint (new_results)
|
||||
# print ("OKKKKKKKKKKKKKKKKKKKKKKKKK SO FAR")
|
||||
return new_results
|
||||
|
||||
|
||||
|
||||
class RulesManager():
|
||||
|
||||
default_merge_schema = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"mergeStrategy": "append",
|
||||
# "mergeStrategy": "arrayMergeById",
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"mergeStrategy": "objectMerge",
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
rule_schema = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"rule": {
|
||||
"default": ".*",
|
||||
"optional": True,
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
},
|
||||
],
|
||||
},
|
||||
"trace": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"explain": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"strategy": {
|
||||
"type": "string",
|
||||
"default": "schema",
|
||||
# "default": "last",
|
||||
"optional": True,
|
||||
# "enum": ["first", "last", "merge"],
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"default": None,
|
||||
"optional": True,
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
self.config_app = app.conf2['config']['app']
|
||||
self.config_main = app.conf2['config']['rules']
|
||||
self.config_items = list(app.conf2['rules'])
|
||||
|
||||
def get_result(self, candidates, key=None, scope=None, trace=False, explain=False):
|
||||
#trace=False
|
||||
|
||||
rules = self.config_items
|
||||
key = key or ''
|
||||
|
||||
# Filter out invalid candidates
|
||||
matched_candidates = [i for i in candidates if i['found'] == True]
|
||||
if len(matched_candidates) == 0:
|
||||
log.debug("No matched candidates")
|
||||
return None
|
||||
|
||||
|
||||
# Look for matching key in rules defiunitions
|
||||
regex_support = False
|
||||
matched_rule = {}
|
||||
if regex_support:
|
||||
raise Exception("Not Implemented")
|
||||
else:
|
||||
rule = [ i for i in rules if i.get('rule') == key ]
|
||||
if len(rule) == 0:
|
||||
log.debug(f"No matched rule for {key}, applying defaults")
|
||||
else:
|
||||
matched_rule = rule[0]
|
||||
log.debug(f"Matcher rule for {key}: {matched_rule}")
|
||||
|
||||
matched_rule['trace'] = trace
|
||||
matched_rule['explain'] = explain
|
||||
schema_validate(matched_rule, self.rule_schema)
|
||||
|
||||
|
||||
|
||||
# Prepare plugins
|
||||
assert(isinstance(matched_candidates, list)), f"Got: {matched_candidates}"
|
||||
assert(isinstance(matched_rule, dict)), f"Got: {matched_rule}"
|
||||
strategy = matched_rule.get('strategy', 'first')
|
||||
log.debug(f"Key '{key}' matched rule '{rule}' with '{strategy}' strategy")
|
||||
|
||||
# Load plugin
|
||||
log.debug(f"Run strategy: {strategy}")
|
||||
plugin_loader = LoadPlugin(TreePlugins)
|
||||
strategy = plugin_loader.load('strategy',
|
||||
strategy,
|
||||
)(parent=self, app=self.app)
|
||||
new_result = strategy.process(matched_candidates, matched_rule)
|
||||
|
||||
return new_result
|
||||
3
ansible_tree/plugin/__init__.py
Normal file
3
ansible_tree/plugin/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from . import engine
|
||||
from . import backend
|
||||
from . import strategy
|
||||
3
ansible_tree/plugin/backend/__init__.py
Normal file
3
ansible_tree/plugin/backend/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from . import init
|
||||
from . import loop
|
||||
from . import hier
|
||||
115
ansible_tree/plugin/backend/hier.py
Normal file
115
ansible_tree/plugin/backend/hier.py
Normal file
@ -0,0 +1,115 @@
|
||||
|
||||
|
||||
import copy
|
||||
# from pathlib import Path
|
||||
# from ansible_tree.utils import render_template
|
||||
# from ansible_tree.plugin.common import PluginBackendClass
|
||||
# from pprint import pprint
|
||||
#
|
||||
# import logging
|
||||
# import anyconfig
|
||||
# import textwrap
|
||||
|
||||
from ansible_tree.plugin.common import PluginBackendClass
|
||||
from pprint import pprint
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Plugin(PluginBackendClass):
|
||||
|
||||
_plugin_name = "hier"
|
||||
_schema_props_files = {
|
||||
"path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
sssss_schema_props_default = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"default": "",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"default": "BLAAAAHHH"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": True,
|
||||
"default": {},
|
||||
"properties": {
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"default": "jerakia",
|
||||
"optional": False,
|
||||
},
|
||||
"value": {
|
||||
"default": 'UNSET',
|
||||
"optional": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def process(self, backends: list, ctx: dict) -> (list, dict):
|
||||
|
||||
new_backends = []
|
||||
for cand in backends:
|
||||
|
||||
# Init
|
||||
plugin_config = cand.get("hier", {})
|
||||
hier_data = plugin_config.get("data", None)
|
||||
if not hier_data:
|
||||
new_backends.append(cand)
|
||||
continue
|
||||
|
||||
# Retrieve config data
|
||||
hier_var = plugin_config.get("var", "item")
|
||||
hier_sep = plugin_config.get("separator", "/")
|
||||
if isinstance(hier_data, str):
|
||||
hier_data = cand['_run']['scope'].get(hier_data, None)
|
||||
|
||||
# Build a new list
|
||||
|
||||
if isinstance(hier_data, str):
|
||||
r = hier_data.split(hier_sep)
|
||||
assert (isinstance(r, list)), f"Got: {r}"
|
||||
|
||||
ret1 = []
|
||||
for index, part in enumerate(r):
|
||||
|
||||
try:
|
||||
prefix = ret1[index - 1]
|
||||
except IndexError:
|
||||
prefix = f'{hier_sep}'
|
||||
prefix = ""
|
||||
item = f"{prefix}{part}{hier_sep}"
|
||||
ret1.append(item)
|
||||
|
||||
ret2 = []
|
||||
for item in ret1:
|
||||
_cand = copy.deepcopy(cand)
|
||||
run = {
|
||||
"index": index,
|
||||
"hier_value": item,
|
||||
"hier_var": hier_var,
|
||||
}
|
||||
_cand['_run']['hier'] = run
|
||||
_cand['_run']['scope'][hier_var] = item
|
||||
ret2.append(_cand)
|
||||
|
||||
new_backends.extend(ret2)
|
||||
return new_backends, ctx
|
||||
|
||||
|
||||
79
ansible_tree/plugin/backend/init.py
Normal file
79
ansible_tree/plugin/backend/init.py
Normal file
@ -0,0 +1,79 @@
|
||||
|
||||
|
||||
from ansible_tree.plugin.common import PluginBackendClass
|
||||
from pprint import pprint
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
import copy
|
||||
|
||||
class Plugin(PluginBackendClass):
|
||||
|
||||
_plugin_name = "init"
|
||||
_schema_props_files = {
|
||||
"path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
sssss_schema_props_default = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"default": "",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"default": "BLAAAAHHH"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": True,
|
||||
"default": {},
|
||||
"properties": {
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"default": "jerakia",
|
||||
"optional": False,
|
||||
},
|
||||
"value": {
|
||||
"default": 'UNSET',
|
||||
"optional": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
default_engine = 'jerakia'
|
||||
|
||||
|
||||
def process(self, backends: list, ctx: dict) -> (list, dict):
|
||||
|
||||
new_backends = []
|
||||
for index, item in enumerate(backends):
|
||||
default = {
|
||||
"value": item,
|
||||
}
|
||||
|
||||
if not isinstance(item, dict):
|
||||
item = default
|
||||
|
||||
item['engine'] = item.get('engine', self.default_engine )
|
||||
item['_run'] = copy.deepcopy(ctx)
|
||||
item['_run']['backend'] = {
|
||||
"index": index,
|
||||
}
|
||||
new_backends.append(item)
|
||||
|
||||
return new_backends, ctx
|
||||
|
||||
|
||||
100
ansible_tree/plugin/backend/loop.py
Normal file
100
ansible_tree/plugin/backend/loop.py
Normal file
@ -0,0 +1,100 @@
|
||||
|
||||
|
||||
import copy
|
||||
from pathlib import Path
|
||||
from ansible_tree.utils import render_template
|
||||
from ansible_tree.plugin.common import PluginBackendClass
|
||||
from pprint import pprint
|
||||
|
||||
import logging
|
||||
import anyconfig
|
||||
import textwrap
|
||||
|
||||
|
||||
class Plugin(PluginBackendClass):
|
||||
|
||||
_plugin_name = "loop"
|
||||
_schema_props_files = {
|
||||
"path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
sssss_schema_props_default = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"default": "",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"default": "BLAAAAHHH"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": True,
|
||||
"default": {},
|
||||
"properties": {
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"default": "jerakia",
|
||||
"optional": False,
|
||||
},
|
||||
"value": {
|
||||
"default": 'UNSET',
|
||||
"optional": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def process(self, backends: list, ctx: dict) -> (list, dict):
|
||||
|
||||
|
||||
new_backends = []
|
||||
for cand in backends:
|
||||
cand = dict(cand)
|
||||
|
||||
# Init
|
||||
loop_config = cand.get("loop", {})
|
||||
loop_data = loop_config.get("data", None)
|
||||
if not loop_data:
|
||||
new_backends.append(cand)
|
||||
continue
|
||||
|
||||
# Retrieve config data
|
||||
loop_var = loop_config.get("var", "item")
|
||||
if isinstance(loop_data, str):
|
||||
loop_data = cand['_run']['scope'].get(loop_data, None)
|
||||
assert (isinstance(loop_data, list)), f"Got: {loop_data}"
|
||||
|
||||
# Build a new list
|
||||
ret = []
|
||||
for idx, item in enumerate(loop_data):
|
||||
_cand = copy.deepcopy(cand)
|
||||
run = {
|
||||
"loop_index": idx,
|
||||
"loop_value": item,
|
||||
"loop_var": loop_var,
|
||||
}
|
||||
_cand['_run']['loop'] = run
|
||||
_cand['_run']['scope'][loop_var] = item
|
||||
#_cand.scope[loop_var] = item
|
||||
ret.append(_cand)
|
||||
|
||||
new_backends.extend(ret)
|
||||
|
||||
return new_backends, ctx
|
||||
|
||||
|
||||
|
||||
294
ansible_tree/plugin/common.py
Normal file
294
ansible_tree/plugin/common.py
Normal file
@ -0,0 +1,294 @@
|
||||
|
||||
# from box import Box
|
||||
import textwrap
|
||||
from pprint import pprint
|
||||
import glob
|
||||
from pathlib import Path
|
||||
from jinja2 import Template
|
||||
import yaml
|
||||
import json
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from ansible_tree.utils import schema_validate
|
||||
import copy
|
||||
|
||||
# Candidate Classes
|
||||
# =============================
|
||||
class Candidate():
|
||||
engine = None
|
||||
found = False
|
||||
data = None
|
||||
run = None
|
||||
scope = None
|
||||
key = None
|
||||
|
||||
def __init__(self, run):
|
||||
self.run = copy.deepcopy(run)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__dict__}"
|
||||
|
||||
def _report_data(self, data=None):
|
||||
default_data = {
|
||||
#"rule": self.config,
|
||||
"value": self.engine._plugin_value,
|
||||
"data": self.data,
|
||||
}
|
||||
data = data or default_data
|
||||
d = json.dumps(data, indent=2) #, sort_keys=True, )
|
||||
return d
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Generic Classes
|
||||
# =============================
|
||||
class PluginClass():
|
||||
_plugin_type = "none"
|
||||
_plugin_value = None
|
||||
_schema_props_plugin = {
|
||||
"engine": {
|
||||
"type": "string",
|
||||
# TODO: Fix this ug
|
||||
"default": "jerakia"
|
||||
},
|
||||
"value": {},
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
kind = self._plugin_type
|
||||
name = self._plugin_name
|
||||
value = getattr(self, 'value', 'NO VALUE')
|
||||
return f"{kind}.{name}:{value}"
|
||||
|
||||
def __init__(self, config=None, parent=None, app=None):
|
||||
# assert (isinstance(config, dict)), f"Got: {config}"
|
||||
self.parent = parent
|
||||
self.app = app
|
||||
self.config = config or {}
|
||||
|
||||
self._init()
|
||||
self._validate()
|
||||
|
||||
def _init(self):
|
||||
pass
|
||||
def _validate(self):
|
||||
pass
|
||||
|
||||
class PluginBackendClass(PluginClass):
|
||||
_plugin_type = "backend"
|
||||
|
||||
def _init(self):
|
||||
pass
|
||||
|
||||
class PluginStrategyClass(PluginClass):
|
||||
_plugin_type = "strategy"
|
||||
|
||||
def _init(self):
|
||||
pass
|
||||
|
||||
class PluginEngineClass(PluginClass):
|
||||
_plugin_type = "engine"
|
||||
|
||||
_schema_props_default = {
|
||||
"value": {
|
||||
"default": "UNSET",
|
||||
},
|
||||
|
||||
#### SHOULD NOT BE HERE
|
||||
"hier": {
|
||||
"additionalProperties": True,
|
||||
"optional": True,
|
||||
"properties": {
|
||||
"var": {
|
||||
"type": "string",
|
||||
"default": "item",
|
||||
"optional": True,
|
||||
},
|
||||
"data": {
|
||||
"default": None,
|
||||
"anyOf": [
|
||||
{ "type": "null" },
|
||||
{ "type": "string" },
|
||||
{ "type": "array" },
|
||||
]
|
||||
},
|
||||
"separator": {
|
||||
"type": "string",
|
||||
"default": "/",
|
||||
"optional": True,
|
||||
},
|
||||
"reversed": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"optional": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Default plugin API Methods
|
||||
# =====================
|
||||
def _init(self):
|
||||
assert isinstance(self.config, dict), f"Got: {self.config}"
|
||||
|
||||
def _validate(self):
|
||||
|
||||
# Build schema
|
||||
schema_keys = [a for a in dir(self) if a.startswith('_schema_props_')]
|
||||
props = {}
|
||||
for key in schema_keys:
|
||||
schema = getattr(self, key)
|
||||
props.update(schema)
|
||||
self.schema = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"type": "object",
|
||||
"additionalProperties": True,
|
||||
"properties": props,
|
||||
}
|
||||
|
||||
# log.debug (f"Validate {self.config} against {self.schema}")
|
||||
self.config = schema_validate(self.config, self.schema)
|
||||
return True
|
||||
|
||||
|
||||
# Public Methods
|
||||
# =====================
|
||||
def dump(self):
|
||||
ret = {
|
||||
"config": self.config,
|
||||
}
|
||||
return ret
|
||||
|
||||
def lookup_candidates(self, key=None, scope=None):
|
||||
raise Exception (f"Module does not implement this method :(")
|
||||
# It must always return a list of `Candidate` instances
|
||||
return []
|
||||
|
||||
def _example(self):
|
||||
print (f"Module does not implement this method :(")
|
||||
return None
|
||||
|
||||
|
||||
# File plugins Extensions
|
||||
# =============================
|
||||
|
||||
class PluginFileGlob():
|
||||
|
||||
_schema_props_glob = {
|
||||
"glob": {
|
||||
"additionalProperties": False,
|
||||
"default": {
|
||||
"file": "ansible.yaml",
|
||||
},
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"default": "ansible",
|
||||
"optional": True,
|
||||
},
|
||||
"ext": {
|
||||
"type": "array",
|
||||
"default": [ "yml", "yaml" ],
|
||||
"optional": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def _glob(self, item):
|
||||
|
||||
# DIRECT CALL TO APP< TOFIX
|
||||
app_config = self.app.conf2
|
||||
root = app_config.get("default", {}).get("config", {}).get("root", f"{Path.cwd()}/tree")
|
||||
#root = self.app.conf2.config.app.root
|
||||
# TOFIX print ("ITEM! %s" % type(root))
|
||||
# TOFIX print ("ITEM2 %s" % self.app.conf2.config.app.root)
|
||||
|
||||
glob_config = self.config.get("glob", {})
|
||||
glob_file = glob_config['file']
|
||||
#glob_ext = glob_config['ext']
|
||||
|
||||
item = Path(root) / Path(item) / Path(glob_file)
|
||||
item = f"{item}"
|
||||
#file = f"{glob_file}.{glob_ext}"
|
||||
|
||||
#print ("ITEM %s" % item)
|
||||
files = glob.glob(item)
|
||||
|
||||
log.debug (f"Matched file for glob '{item}': {files}")
|
||||
|
||||
return files
|
||||
|
||||
|
||||
## DEPRECATED !!!!
|
||||
#class PluginFileLoop():
|
||||
#
|
||||
# _schema_props_loop = {
|
||||
# "loop": {
|
||||
# "additionalProperties": False,
|
||||
# "default": {
|
||||
# "var": "item",
|
||||
# "data": None,
|
||||
# },
|
||||
# "properties": {
|
||||
# "var": {
|
||||
# "type": "string",
|
||||
# "default": "item",
|
||||
# "optional": True,
|
||||
# },
|
||||
# "data": {
|
||||
# "default": None,
|
||||
# "anyOf": [
|
||||
# { "type": "null" },
|
||||
# { "type": "string" },
|
||||
# { "type": "array" },
|
||||
# ]
|
||||
# },
|
||||
# },
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# def _loop(self, item, params):
|
||||
# print ("_loop IS DEPRECATED")
|
||||
# loop = self.config.get("loop", None)
|
||||
#
|
||||
#
|
||||
# # Check if loop is enabled
|
||||
# if loop['data'] is None:
|
||||
# return [ item ]
|
||||
#
|
||||
# log.debug(f"Loop enabled for: {item}")
|
||||
#
|
||||
# _var = loop['var']
|
||||
# ref = loop['data']
|
||||
#
|
||||
# # replace value:
|
||||
# data = params.get(ref, [])
|
||||
# if data is None or len(data) < 1:
|
||||
# return [item]
|
||||
# elif not isinstance(data, list):
|
||||
# raise Exception(f"Expected a list, got: {data}")
|
||||
#
|
||||
# # Loop over lists
|
||||
# ret = []
|
||||
# for line in data:
|
||||
# t = Template(item)
|
||||
# param_dict = { _var: line}
|
||||
# r = t.render(**param_dict)
|
||||
# ret.append(r)
|
||||
# #print (f"{item} ==> {params} => {r}")
|
||||
#
|
||||
# return ret
|
||||
#
|
||||
# # Loop over data
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1
ansible_tree/plugin/engine/__init__.py
Normal file
1
ansible_tree/plugin/engine/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import jerakia
|
||||
192
ansible_tree/plugin/engine/jerakia.py
Normal file
192
ansible_tree/plugin/engine/jerakia.py
Normal file
@ -0,0 +1,192 @@
|
||||
|
||||
from pathlib import Path
|
||||
from ansible_tree.utils import render_template
|
||||
from ansible_tree.plugin.common import PluginEngineClass, PluginFileGlob, Candidate
|
||||
from pprint import pprint
|
||||
|
||||
import logging
|
||||
import anyconfig
|
||||
import textwrap
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class FileCandidate(Candidate):
|
||||
path = None
|
||||
|
||||
def _report_data(self):
|
||||
data = {
|
||||
#"rule": self.config,
|
||||
"value": self.engine._plugin_value,
|
||||
"data": self.data,
|
||||
"path": str(self.path.relative_to(Path.cwd())),
|
||||
}
|
||||
data = dict(self.config)
|
||||
return super()._report_data(data)
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginEngineClass, PluginFileGlob):
|
||||
|
||||
_plugin_name = 'jerakia'
|
||||
|
||||
### OLD
|
||||
_plugin_engine = "jerakia"
|
||||
_schema_props_files = {
|
||||
"path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# def __repr__(self):
|
||||
# engine = self.config.get('engine')
|
||||
# value = self.
|
||||
# return f"Plugin instance {engine}: {value}"
|
||||
|
||||
|
||||
def _init(self):
|
||||
|
||||
paths = self.config.get('path', self.config.get('value'))
|
||||
if isinstance(paths, str):
|
||||
paths = [paths]
|
||||
elif isinstance(paths, list):
|
||||
pass
|
||||
else:
|
||||
raise Exception (f"Unsupported path value, expected str or dict, got: {paths} in {self.config}")
|
||||
|
||||
self.paths = paths
|
||||
self.value = paths
|
||||
|
||||
def _preprocess(self, scope):
|
||||
|
||||
# Manage loops
|
||||
paths = self.paths
|
||||
|
||||
# Manage var substr
|
||||
ret = []
|
||||
for p in paths:
|
||||
p = render_template(p, scope)
|
||||
ret.append(p)
|
||||
|
||||
log.debug(f"Render pattern: {ret}")
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def _show_paths(self, scope):
|
||||
|
||||
parsed = self._preprocess(scope)
|
||||
log.debug(f"Expanded paths to: {parsed}")
|
||||
|
||||
# Look for files (NOT BE HERE !!!)
|
||||
ret3 = []
|
||||
for p in parsed:
|
||||
globbed = self._glob(p)
|
||||
ret3.extend(globbed)
|
||||
log.debug(f"Matched globs: {ret3}")
|
||||
|
||||
return ret3
|
||||
|
||||
|
||||
def process(self):
|
||||
|
||||
|
||||
#scope = self.scope
|
||||
# pprint (self.config)
|
||||
scope = dict(self.config['_run']['scope'])
|
||||
key = self.config['_run']['key']
|
||||
assert isinstance(scope, dict), f"Got: {scope}"
|
||||
assert isinstance(key, (str, type(None))), f"Got: {key}"
|
||||
|
||||
t = self._show_paths(scope)
|
||||
|
||||
ret = []
|
||||
for index, path in enumerate(self._show_paths(scope)):
|
||||
log.debug(f"Reading file: {path}")
|
||||
# Fetch data
|
||||
found = False
|
||||
raw_data = anyconfig.load(path, ac_parser="yaml")
|
||||
data = None
|
||||
if key is None:
|
||||
data = raw_data
|
||||
found = True
|
||||
else:
|
||||
try:
|
||||
data = raw_data[key]
|
||||
found = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Build result object
|
||||
result = {}
|
||||
result['run'] = {
|
||||
'path': path,
|
||||
'rel_path': str(Path(path).relative_to(Path.cwd())),
|
||||
}
|
||||
result['parent'] = self.config
|
||||
result['data'] = data
|
||||
result['found'] = found
|
||||
|
||||
ret.append(result)
|
||||
|
||||
return ret
|
||||
|
||||
######## OLD
|
||||
|
||||
# # Read raw file content
|
||||
# data = anyconfig.load(path, ac_parser="yaml")
|
||||
#
|
||||
# ret_obj2 ={
|
||||
# "_run": _run,
|
||||
|
||||
# }
|
||||
|
||||
# #### OLD
|
||||
|
||||
# ret_obj = FileCandidate(self.config)
|
||||
# ret_obj.engine = self
|
||||
# ret_obj.data = None
|
||||
|
||||
# found = False
|
||||
# if key is None:
|
||||
# ret_obj.data = data
|
||||
# found = True
|
||||
# else:
|
||||
# try:
|
||||
# ret_obj.data = data[key]
|
||||
# found = True
|
||||
# except Exception:
|
||||
# pass
|
||||
|
||||
# # ret_obj.run['path'] = path
|
||||
# # ret_obj.run['found'] = found
|
||||
# # ret_obj.run['scope'] = scope
|
||||
# # ret_obj.run['key'] = key
|
||||
# be = {
|
||||
# "index": index,
|
||||
# "path": path,
|
||||
# "rel_path": str(Path(path).relative_to(Path.cwd())),
|
||||
# }
|
||||
# #qu = {
|
||||
# # "scope": scope,
|
||||
# # "key": key,
|
||||
# # }
|
||||
# ret_obj.run['backend'] = be
|
||||
# #ret_obj.run['query'] = qu
|
||||
|
||||
# #log.debug(f"Found value: {ret_obj}")
|
||||
# ret_obj.found = found
|
||||
# ret.append(ret_obj)
|
||||
|
||||
|
||||
|
||||
|
||||
2
ansible_tree/plugin/strategy/__init__.py
Normal file
2
ansible_tree/plugin/strategy/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import last
|
||||
from . import schema
|
||||
14
ansible_tree/plugin/strategy/last.py
Normal file
14
ansible_tree/plugin/strategy/last.py
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
import logging
|
||||
from ansible_tree.plugin.common import PluginStrategyClass
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Plugin(PluginStrategyClass):
|
||||
|
||||
_plugin_name = "last"
|
||||
|
||||
def process(self, candidates: list, rule=None) -> (list, dict):
|
||||
|
||||
return candidates[-1]
|
||||
|
||||
125
ansible_tree/plugin/strategy/schema.py
Normal file
125
ansible_tree/plugin/strategy/schema.py
Normal file
@ -0,0 +1,125 @@
|
||||
|
||||
|
||||
import logging
|
||||
from ansible_tree.plugin.common import PluginStrategyClass
|
||||
from ansible_tree.utils import schema_validate, str_ellipsis
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
import json
|
||||
from pprint import pprint
|
||||
from jsonmerge import Merger
|
||||
from prettytable import PrettyTable
|
||||
|
||||
class Plugin(PluginStrategyClass):
|
||||
|
||||
_plugin_name = "schema"
|
||||
|
||||
default_merge_schema = {
|
||||
"$schema": 'http://json-schema.org/draft-04/schema#',
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"mergeStrategy": "append",
|
||||
# "mergeStrategy": "arrayMergeById",
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"mergeStrategy": "objectMerge",
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
"mergeStrategy": "overwrite",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def process(self, candidates: list, rule=None) -> (list, dict):
|
||||
|
||||
trace = rule['trace']
|
||||
explain = rule['explain']
|
||||
schema = rule.get('schema', None) or self.default_merge_schema
|
||||
merger = Merger(schema)
|
||||
t = PrettyTable()
|
||||
t1 = PrettyTable()
|
||||
|
||||
new_candidate = None
|
||||
for index, item in enumerate(candidates):
|
||||
new_value = item['data']
|
||||
result = merger.merge(new_candidate, new_value)
|
||||
|
||||
backend_info = dict(item['parent'])
|
||||
backend_run = backend_info.pop("_run")
|
||||
if explain:
|
||||
t1.add_row([
|
||||
index,
|
||||
'\nBackendRun: ' + str_ellipsis(json.dumps(
|
||||
backend_run,
|
||||
default=lambda o: '<not serializable>', indent=2), 70),
|
||||
'\nRuleRun: ' + str_ellipsis(json.dumps(
|
||||
item['run'],
|
||||
default=lambda o: '<not serializable>', indent=2), 70),
|
||||
'---\nResult: ' + str_ellipsis(json.dumps(
|
||||
result,
|
||||
default=lambda o: '<not serializable>', indent=2), 70),
|
||||
])
|
||||
|
||||
if trace:
|
||||
t.add_row([
|
||||
index,
|
||||
'---\nBackendConfig: ' + str_ellipsis(json.dumps(
|
||||
backend_info,
|
||||
default=lambda o: '<not serializable>', indent=2), 70) +
|
||||
'\nBackendRun: ' + str_ellipsis(json.dumps(
|
||||
backend_run,
|
||||
default=lambda o: '<not serializable>', indent=2), 70),
|
||||
|
||||
'---\nRuleConfig: ' + str_ellipsis(json.dumps(
|
||||
rule,
|
||||
default=lambda o: '<not serializable>', indent=2), 70) +
|
||||
'\nRuleRun: ' + str_ellipsis(json.dumps(
|
||||
item['run'],
|
||||
default=lambda o: '<not serializable>', indent=2), 70) +
|
||||
#'\nSource: ' + str_ellipsis(json.dumps(
|
||||
# new_candidate,
|
||||
# default=lambda o: '<not serializable>', indent=2), 70) +
|
||||
'\nNew data: ' + str_ellipsis(json.dumps(
|
||||
new_value,
|
||||
default=lambda o: '<not serializable>', indent=2), 70),
|
||||
|
||||
'---\nResult: ' + str_ellipsis(json.dumps(
|
||||
result,
|
||||
default=lambda o: '<not serializable>', indent=2), 70),
|
||||
]
|
||||
)
|
||||
new_candidate = result
|
||||
|
||||
if trace:
|
||||
t.field_names = ["Index", "Backend", "Rule", "Data"]
|
||||
t.align = 'l'
|
||||
print (t)
|
||||
if explain:
|
||||
t1.field_names = ["Index", "Backend", "Rule", "Data"]
|
||||
t1.align = 'l'
|
||||
print('Explain:\n' + repr(t1))
|
||||
|
||||
return new_candidate
|
||||
|
||||
|
||||
16
ansible_tree/test.sh
Normal file
16
ansible_tree/test.sh
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
python ./ansible_tree/cli.py
|
||||
|
||||
$APP lookup profiles -e
|
||||
|
||||
lookup profiles -e "hostgroup=[ 'Tiger/ICN/Tiger/Infra/Prod' ]" -e "hostgroups=[ 'Tiger', 'Tiger/ICN', 'Tiger/ICN/Tiger', 'Tiger/ICN/Tiger/Infra', 'Tiger/ICN/Tiger/Infra/Prod' ]" -e "ansible_fqdn=tiger-ops.it.ms.bell.ca" -e "ansible_dist_name=Rhel" -e "ansible_dist_version=8" -e "tiger_org=ICN"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-e "hostgroup=[ 'Tiger/ICN/Tiger/Infra/Prod' ]"
|
||||
-e "hostgroup=[ 'Tiger/ICN/Tiger/Infra/Prod' ]"
|
||||
-e "hostgroup=[ 'Tiger/ICN/Tiger/Infra/Prod' ]"
|
||||
-e "hostgroups=[ 'Tiger', 'ICN', 'Tiger', 'Infra', 'Prod' ]"
|
||||
147
ansible_tree/utils.py
Normal file
147
ansible_tree/utils.py
Normal file
@ -0,0 +1,147 @@
|
||||
|
||||
from pathlib import Path
|
||||
from jinja2 import Template
|
||||
import yaml
|
||||
import json
|
||||
import glob
|
||||
|
||||
from jsonschema import validate, Draft7Validator, validators, exceptions
|
||||
import collections
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# # File parsers
|
||||
# # =====================
|
||||
#
|
||||
# class FileParserClass():
|
||||
#
|
||||
# def __init__(self, path):
|
||||
# self.path = path
|
||||
#
|
||||
# def from_file(self, file):
|
||||
# raise Exception ("Not implemented")
|
||||
#
|
||||
# def from_string(self, data):
|
||||
# raise Exception ("Not implemented")
|
||||
#
|
||||
# def from_dict(self, data):
|
||||
# raise Exception ("Not implemented")
|
||||
#
|
||||
# class FilesYAMLParser(FileParserClass):
|
||||
# def get_data(self):
|
||||
# with open(self.path, "r") as stream:
|
||||
# try:
|
||||
# return yaml.safe_load(stream)
|
||||
# except yaml.YAMLError as exc:
|
||||
# raise Exception(exc)
|
||||
# print(exc)
|
||||
#
|
||||
#
|
||||
# class FilesJSONParser(FileParserClass):
|
||||
# pass
|
||||
# class FilesRawParser(FileParserClass):
|
||||
# pass
|
||||
# class FilesTOMLParser(FileParserClass):
|
||||
# pass
|
||||
# class FilesCSVParser(FileParserClass):
|
||||
# pass
|
||||
# class FilesINIParser(FileParserClass):
|
||||
# pass
|
||||
#
|
||||
# format_db = {
|
||||
# ".raw": FilesRawParser,
|
||||
# ".yml": FilesYAMLParser,
|
||||
# ".yaml": FilesYAMLParser,
|
||||
# ".json": FilesJSONParser,
|
||||
# }
|
||||
|
||||
|
||||
# Utils Methods
|
||||
# =====================
|
||||
|
||||
def render_template(path, params):
|
||||
"""Render template for a given string"""
|
||||
assert (isinstance(params, dict)), f"Got: {params}"
|
||||
t = Template(path)
|
||||
return t.render(**params)
|
||||
|
||||
#def read_file(file):
|
||||
# with open(file, 'r') as f:
|
||||
# data = f.read().replace('\n', '')
|
||||
# return data
|
||||
#
|
||||
#
|
||||
#def parse_file(file, fmt='auto'):
|
||||
# print ("DEPRECATED")
|
||||
# raise Exception ("parse_file is deprecated")
|
||||
#
|
||||
# data = read_file(file)
|
||||
#
|
||||
# # Autodetect format from file name
|
||||
# if fmt == 'auto':
|
||||
# p = Path(file)
|
||||
# fmt = p.suffix
|
||||
# else:
|
||||
# fmt = f".{fmt}"
|
||||
#
|
||||
# # Retrieve parser
|
||||
# if fmt is None:
|
||||
# raise Exception ("No available driver to read file: %s" % p )
|
||||
# fmt_cls = format_db.get(fmt, None)
|
||||
#
|
||||
# # Parse content
|
||||
# o = fmt_cls(str(p))
|
||||
# return o.get_data()
|
||||
|
||||
# Schema Methods
|
||||
# =====================
|
||||
|
||||
def _extend_with_default(validator_class):
|
||||
validate_properties = validator_class.VALIDATORS["properties"]
|
||||
|
||||
def set_defaults(validator, properties, instance, schema):
|
||||
|
||||
for property, subschema in properties.items():
|
||||
if "default" in subschema:
|
||||
instance.setdefault(property, subschema["default"])
|
||||
|
||||
try:
|
||||
for error in validate_properties(
|
||||
validator, properties, instance, schema,
|
||||
):
|
||||
continue
|
||||
except Exception as e:
|
||||
print ("CATCHED2222 ", e)
|
||||
|
||||
return validators.extend(
|
||||
validator_class, {"properties" : set_defaults},
|
||||
)
|
||||
|
||||
|
||||
def schema_validate(config, schema):
|
||||
|
||||
# Validate the schema
|
||||
DefaultValidatingDraft7Validator = _extend_with_default(Draft7Validator)
|
||||
try:
|
||||
DefaultValidatingDraft7Validator(schema).validate(config)
|
||||
except Exception as e:
|
||||
print (e)
|
||||
p = list(collections.deque(e.schema_path))
|
||||
p = '/'.join([ str(i) for i in p ])
|
||||
p = f"schema/{p}"
|
||||
raise Exception(
|
||||
f"Failed validating {p} for resource with content: {config} with !!!!!! schema: {schema}"
|
||||
)
|
||||
return config
|
||||
|
||||
def str_ellipsis(txt, length=120):
|
||||
txt = str(txt)
|
||||
ret = []
|
||||
for string in txt.splitlines():
|
||||
string = (string[:length - 4 ] + ' ...') if len(string) > length else string
|
||||
ret.append(string)
|
||||
ret = '\n'.join(ret)
|
||||
return ret
|
||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[tool.poetry]
|
||||
name = "ansible-tree"
|
||||
version = "0.1.0"
|
||||
description = "Data trees for Ansible"
|
||||
authors = ["Robin Cordier"]
|
||||
license = "GNU"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
jsonschema = "^4.3.3"
|
||||
jsonmerge = "^1.8.0"
|
||||
anyconfig = "^0.12.0"
|
||||
python-box = "^5.4.1"
|
||||
prettytable = "^3.0.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
Loading…
x
Reference in New Issue
Block a user