Compare commits

...

26 Commits

Author SHA1 Message Date
1dce6fbfcd Bump: version 0.1.0 to 0.2.0 2022-04-07 11:54:23 -04:00
c8595f63cc Remove: lock file 2022-03-23 12:13:40 -04:00
0f31a14920 Update: Documentation 2022-03-23 12:11:56 -04:00
f6460b8db3 Fix: Wrong path in CI 2022-03-23 03:05:56 -04:00
4a0bc2e201 Add: gh action 2022-03-23 03:00:52 -04:00
90522b5b69 Fix: wrong plugin name 2022-03-23 02:57:25 -04:00
1f38ce7dba Fix: Minor bugs and log messages 2022-03-11 02:57:54 -05:00
07ecaa8085 Add: Option to support namespace prefixed keys 2022-03-10 15:40:50 -05:00
969d78a0ac Update: .gitignore and remove poetry.lock from repo 2022-03-10 12:10:41 -05:00
f12d9fd7be Update: Kheops core classes 2022-03-10 12:08:12 -05:00
5f80881c3c Update: File backend plugin 2022-03-10 12:07:35 -05:00
25c6bf36a2 Clean: common plugin classes 2022-03-10 12:07:12 -05:00
ff2f170685 Fix: missing name in plugins 2022-03-10 12:06:32 -05:00
0ce3417360 Fix: Broken jsonschema validator 2022-03-10 12:05:40 -05:00
4616b310d0 Change: Project license 2022-03-08 16:07:21 -05:00
c8d8f922d3 Add: pytest plugins 2022-03-08 16:07:18 -05:00
ee3df03829 Fix: namespace loading and error messages 2022-03-08 16:06:18 -05:00
a9e97d98c1 Change: exit error to exception 2022-02-15 10:41:00 -05:00
eeab4f8583 Fix: Loading errors when config file is missing 2022-02-14 15:41:08 -05:00
3eb67e584a Fix: missplaced gitignore 2022-02-14 15:37:34 -05:00
8060e285c0 Add: Kheops examples 2022-02-14 15:36:08 -05:00
98368ee53a Update: poetry.lock 2022-02-14 15:34:42 -05:00
00f87bb4f7 Update: Jupyter playbook 2022-02-14 15:34:28 -05:00
34587710b3 Add: .gitignore 2022-02-14 15:33:30 -05:00
4f28bdc66b Fix: Crash when kheops config file is missing 2022-02-14 15:33:16 -05:00
62c0d7352f Fix: Missing jinja2 library 2022-02-14 13:30:17 -05:00
29 changed files with 1599 additions and 832 deletions

107
.github/workflows/sphinx.yml vendored Normal file
View File

@ -0,0 +1,107 @@
# From: https://github.com/rkdarst/sphinx-actions-test/blob/master/.github/workflows/sphinx-build.yml
name: sphinx
on: [push, pull_request]
env:
DEFAULT_BRANCH: "main"
#SPHINXOPTS: "-W --keep-going -T"
# ^-- If these SPHINXOPTS are enabled, then be strict about the builds and fail on any warnings
jobs:
build-and-deploy:
name: Build and gh-pages
runs-on: ubuntu-latest
steps:
# https://github.com/marketplace/actions/checkout
- uses: actions/checkout@v2
with:
fetch-depth: 0
lfs: true
# https://github.com/marketplace/actions/setup-python
# ^-- This gives info on matrix testing.
- name: Install Python
uses: actions/setup-python@v2
with:
python-version: 3.8
# https://docs.github.com/en/actions/guides/building-and-testing-python#caching-dependencies
# ^-- How to set up caching for pip on Ubuntu
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache poetry
uses: actions/cache@v2
with:
#path: ~/.cache/pip
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
#key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-poetry-
${{ runner.os }}-
# https://docs.github.com/en/actions/guides/building-and-testing-python#installing-dependencies
# ^-- This gives info on installing dependencies with pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install poetry
poetry config virtualenvs.create false
poetry install
- name: Debugging information
run: |
echo "github.ref:" ${{github.ref}}
echo "github.event_name:" ${{github.event_name}}
echo "github.head_ref:" ${{github.head_ref}}
echo "github.base_ref:" ${{github.base_ref}}
set -x
git rev-parse --abbrev-ref HEAD
git branch
git branch -a
git remote -v
python -V
pip list --not-required
pip list
poetry debug
# Build
#- uses: ammaraskar/sphinx-problem-matcher@master
- name: Build Sphinx docs
working-directory: ./docs
run: |
./build_doc.sh
# make dirhtml
# This fixes broken copy button icons, as explained in
# https://github.com/coderefinery/sphinx-lesson/issues/50
# https://github.com/executablebooks/sphinx-copybutton/issues/110
# This can be removed once these PRs are accepted (but the
# fixes also need to propagate to other themes):
# https://github.com/sphinx-doc/sphinx/pull/8524
# https://github.com/readthedocs/sphinx_rtd_theme/pull/1025
#sed -i 's/url_root="#"/url_root=""/' _build/html/index.html || true
# Add the .nojekyll file
- name: nojekyll
working-directory: ./docs
#if: ${{ github.event_name == 'push' }}
run: |
touch _build/html/.nojekyll
# Deploy
# https://github.com/peaceiris/actions-gh-pages
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
#if: ${{ github.event_name == 'push' }}
#if: ${{ success() && github.event_name == 'push' && github.ref == 'refs/heads/$defaultBranch' }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/_build/html
publish_branch: gh-pages
force_orphan: true

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
kheops.egg-info/*
dist/*
setup.py
poetry.lock
**/__pycache__/**
**/.ipynb_checkpoints/**

View File

@ -9,26 +9,10 @@ kheops.plugin.backend package
Submodules Submodules
---------- ----------
kheops.plugin.backend.hier module kheops.plugin.backend.file module
--------------------------------- ---------------------------------
.. automodule:: kheops.plugin.backend.hier .. automodule:: kheops.plugin.backend.file
:members:
:undoc-members:
:show-inheritance:
kheops.plugin.backend.init module
---------------------------------
.. automodule:: kheops.plugin.backend.init
:members:
:undoc-members:
:show-inheritance:
kheops.plugin.backend.loop module
---------------------------------
.. automodule:: kheops.plugin.backend.loop
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -13,7 +13,7 @@ Subpackages
:maxdepth: 4 :maxdepth: 4
kheops.plugin.backend kheops.plugin.backend
kheops.plugin.engine kheops.plugin.scope
kheops.plugin.strategy kheops.plugin.strategy
Submodules Submodules

View File

@ -0,0 +1,26 @@
kheops.plugin.scope package
===========================
.. automodule:: kheops.plugin.scope
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
kheops.plugin.scope.hier module
-------------------------------
.. automodule:: kheops.plugin.scope.hier
:members:
:undoc-members:
:show-inheritance:
kheops.plugin.scope.loop module
-------------------------------
.. automodule:: kheops.plugin.scope.loop
:members:
:undoc-members:
:show-inheritance:

View File

@ -17,10 +17,18 @@ kheops.plugin.strategy.last module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
kheops.plugin.strategy.schema module kheops.plugin.strategy.merge\_deep module
------------------------------------ -----------------------------------------
.. automodule:: kheops.plugin.strategy.schema .. automodule:: kheops.plugin.strategy.merge_deep
:members:
:undoc-members:
:show-inheritance:
kheops.plugin.strategy.merge\_schema module
-------------------------------------------
.. automodule:: kheops.plugin.strategy.merge_schema
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -33,18 +33,10 @@ kheops.cli module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
kheops.managers module kheops.controllers module
---------------------- -------------------------
.. automodule:: kheops.managers .. automodule:: kheops.controllers
:members:
:undoc-members:
:show-inheritance:
kheops.query module
-------------------
.. automodule:: kheops.query
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -2,6 +2,6 @@ kheops
====== ======
.. toctree:: .. toctree::
:maxdepth: 8 :maxdepth: 4
kheops kheops

View File

@ -2,8 +2,8 @@
make clean make clean
#sphinx-apidoc -f -M -o ./api ../kheops/ sphinx-apidoc -f -M -o ./api ../kheops/
sphinx-apidoc -M -o ./api ../kheops/ #sphinx-apidoc -M -o ./api ../kheops/
mkdir -p learn mkdir -p learn
jupyter nbconvert --to markdown --output=../learn/learn101.md jupyter/learn101.ipynb jupyter nbconvert --to markdown --output=../learn/learn101.md jupyter/learn101.ipynb

View File

@ -53,7 +53,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
# #
html_theme = 'alabaster' html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,

View File

@ -10,7 +10,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 1, "execution_count": 3,
"id": "98d4907b", "id": "98d4907b",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -18,12 +18,12 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"/home/jez/volumes/data/prj/bell/training/tiger-ansible/ext/kheops\n" "/home/jez/prj/bell/dev/kheops\n"
] ]
} }
], ],
"source": [ "source": [
"cd /home/jez/volumes/data/prj/bell/training/tiger-ansible/ext/kheops\n", "cd ${KHEOPS_EXAMPLES_DIR:-/dev/null}\n",
"echo $PWD\n", "echo $PWD\n",
"export KHEOPS_NAMESPACE=ex1_enc \n", "export KHEOPS_NAMESPACE=ex1_enc \n",
"export KHEOPS_CONFIG=examples/kheops.yml\n", "export KHEOPS_CONFIG=examples/kheops.yml\n",
@ -48,7 +48,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 2, "execution_count": 4,
"id": "6ede46a3", "id": "6ede46a3",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -56,22 +56,24 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"usage: kheops [-h] [-v] [-c CONFIG]\n", "Usage: kheops [OPTIONS] COMMAND [ARGS]...\n",
" {schema,gen_doc,lookup,demo,subcommand2} ...\n",
"\n", "\n",
"Kheops, hierarchical data lookup tool\n", " Khéops, hierarchical key/value store\n",
"\n", "\n",
"options:\n", "Options:\n",
" -h, --help show this help message and exit\n", " -v, --verbose [default: 0]\n",
" -v, --verbose Increase verbosity (KHEOPS_VERBOSE) (default: 0)\n", " -c PATH Last name of person to greet. [env var:\n",
" -c CONFIG, --config CONFIG\n", " KHEOPS_CONFIG; default: kheops.yml]\n",
" Kheops configuration file (KHEOPS_CONFIG) (default:\n", " --install-completion [bash|zsh|fish|powershell|pwsh]\n",
" examples/kheops.yml)\n", " Install completion for the specified shell.\n",
" --show-completion [bash|zsh|fish|powershell|pwsh]\n",
" Show completion for the specified shell, to\n",
" copy it or customize the installation.\n",
" --help Show this message and exit.\n",
"\n", "\n",
"subcommands:\n", "Commands:\n",
" valid subcommands\n", " config\n",
"\n", " lookup Lookup database\n"
" {schema,gen_doc,lookup,demo,subcommand2}\n"
] ]
} }
], ],
@ -149,14 +151,6 @@
"A scope is completely optional while keys are required." "A scope is completely optional while keys are required."
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "9e6aae44",
"metadata": {},
"outputs": [],
"source": []
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "d509fa19", "id": "d509fa19",
@ -165,9 +159,35 @@
"## Basic hierarchy" "## Basic hierarchy"
] ]
}, },
{
"cell_type": "markdown",
"id": "8dd5b6b9",
"metadata": {},
"source": [
"Let's create a firest hierachy, we will define a first basic hierarchy. In `kheops.yml`, we can find:\n",
"\n",
"```\n",
"ex1_enc:\n",
" \n",
" config:\n",
" file_path_prefix: \"ex1_enc/\"\n",
" file_path_suffix: \"/ansible\"\n",
"\n",
" lookups:\n",
"\n",
" - path: default # Simplest form, just a path\n",
" - path: \"roles/{role}\" # If list, it's auto expanded like in bash\n",
" - path: \"env_{env}\" # If list, it's auto expanded like in bash\n",
" - path: \"nodes/{node}\"\n",
"\n",
"```\n",
"\n",
"Now we have our hierachy, let's create our files:"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 3, "execution_count": 9,
"id": "e510a46d", "id": "e510a46d",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -175,7 +195,7 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"\u001b[01;34mexamples/ex1_enc\u001b[00m\n", "\u001b[01;34mexamples/ex1_enc\u001b[0m\n",
"└── default.yml\n", "└── default.yml\n",
"\n", "\n",
"0 directories, 1 file\n", "0 directories, 1 file\n",
@ -217,7 +237,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 4, "execution_count": 8,
"id": "06d85b1c", "id": "06d85b1c",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -225,8 +245,9 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"env: NO_ENV\n", "profile:\n",
"product: NO_PRODUCT\n", " env: NO_ENV\n",
" product: NO_PRODUCT\n",
"\n" "\n"
] ]
} }
@ -245,7 +266,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 5, "execution_count": 10,
"id": "c5fad63b", "id": "c5fad63b",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -253,8 +274,9 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"env: NO_ENV\n", "profile:\n",
"product: NO_PRODUCT\n", " env: NO_ENV\n",
" product: NO_PRODUCT\n",
"\n" "\n"
] ]
} }
@ -263,6 +285,14 @@
"kheops lookup -e node=web.infra.net profile" "kheops lookup -e node=web.infra.net profile"
] ]
}, },
{
"cell_type": "markdown",
"id": "a0649351",
"metadata": {},
"source": [
"Same result, let's check how we can change this behavior."
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "53c43757", "id": "53c43757",
@ -281,7 +311,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 6, "execution_count": 11,
"id": "9f5aabaa", "id": "9f5aabaa",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -289,9 +319,9 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"\u001b[01;34mexamples/ex1_enc\u001b[00m\n", "\u001b[01;34mexamples/ex1_enc\u001b[0m\n",
"├── default.yml\n", "├── default.yml\n",
"└── \u001b[01;34mroles\u001b[00m\n", "└── \u001b[01;34mroles\u001b[0m\n",
" ├── mysql.yml\n", " ├── mysql.yml\n",
" └── web.yml\n", " └── web.yml\n",
"\n", "\n",
@ -336,7 +366,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 7, "execution_count": 12,
"id": "4762fa0c", "id": "4762fa0c",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -383,7 +413,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 8, "execution_count": 13,
"id": "0294ec50", "id": "0294ec50",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -391,21 +421,23 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"env: NO_ENV\n", "profile:\n",
"product: httpd_server\n", " env: NO_ENV\n",
"web_top_domain: ''\n", " product: httpd_server\n",
"web_app: NO_APP\n", " web_top_domain: ''\n",
"web_port: 80\n", " web_app: NO_APP\n",
"web_user_list:\n", " web_port: 80\n",
"- sysadmins\n", " web_user_list:\n",
" - sysadmins\n",
"\n", "\n",
"env: NO_ENV\n", "profile:\n",
"product: mysql_server\n", " env: NO_ENV\n",
"mysql_database: NO_DATABASE\n", " product: mysql_server\n",
"mysql_users:\n", " mysql_database: NO_DATABASE\n",
"- sysadmin@10.0.42%\n", " mysql_users:\n",
"mysql_port: 3306\n", " - sysadmin@10.0.42%\n",
"mysql_cluster: false\n", " mysql_port: 3306\n",
" mysql_cluster: false\n",
"\n" "\n"
] ]
} }
@ -436,7 +468,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 9, "execution_count": 14,
"id": "c1acb199", "id": "c1acb199",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -444,12 +476,12 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"\u001b[01;34mexamples/ex1_enc\u001b[00m\n", "\u001b[01;34mexamples/ex1_enc\u001b[0m\n",
"├── default.yml\n", "├── default.yml\n",
"├── \u001b[01;34mnodes\u001b[00m\n", "├── \u001b[01;34mnodes\u001b[0m\n",
"│   ├── mysql.infra.net.yml\n", "│   ├── mysql.infra.net.yml\n",
"│   └── web.infra.net.yml\n", "│   └── web.infra.net.yml\n",
"└── \u001b[01;34mroles\u001b[00m\n", "└── \u001b[01;34mroles\u001b[0m\n",
" ├── mysql.yml\n", " ├── mysql.yml\n",
" └── web.yml\n", " └── web.yml\n",
"\n", "\n",
@ -495,7 +527,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 10, "execution_count": 15,
"id": "b0d34416", "id": "b0d34416",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -503,24 +535,24 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"env: NO_ENV\n", "profile:\n",
"product: httpd_server\n", " env: NO_ENV\n",
"web_top_domain: ''\n", " product: httpd_server\n",
"web_app: myapp\n", " web_top_domain: ''\n",
"web_port: 80\n", " web_app: myapp\n",
"web_user_list:\n", " web_port: 80\n",
"- sysadmins\n", " web_user_list:\n",
"- domain_org\n", " - domain_org\n",
"- domain_org_external\n", " - domain_org_external\n",
"\n", "\n",
"env: NO_ENV\n", "profile:\n",
"product: mysql_server\n", " env: NO_ENV\n",
"mysql_database: app_domain_org\n", " product: mysql_server\n",
"mysql_users:\n", " mysql_database: app_domain_org\n",
"- sysadmin@10.0.42%\n", " mysql_users:\n",
"- app_domain_org@10.0.51%\n", " - app_domain_org@10.0.51%\n",
"mysql_port: 3306\n", " mysql_port: 3306\n",
"mysql_cluster: false\n", " mysql_cluster: false\n",
"\n" "\n"
] ]
} }
@ -548,7 +580,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 11, "execution_count": 16,
"id": "6c92b0cc", "id": "6c92b0cc",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -556,14 +588,14 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"\u001b[01;34mexamples/ex1_enc\u001b[00m\n", "\u001b[01;34mexamples/ex1_enc\u001b[0m\n",
"├── default.yml\n", "├── default.yml\n",
"├── env_dev.yml\n", "├── env_dev.yml\n",
"├── env_prod.yml\n", "├── env_prod.yml\n",
"├── \u001b[01;34mnodes\u001b[00m\n", "├── \u001b[01;34mnodes\u001b[0m\n",
"│   ├── mysql.infra.net.yml\n", "│   ├── mysql.infra.net.yml\n",
"│   └── web.infra.net.yml\n", "│   └── web.infra.net.yml\n",
"└── \u001b[01;34mroles\u001b[00m\n", "└── \u001b[01;34mroles\u001b[0m\n",
" ├── mysql.yml\n", " ├── mysql.yml\n",
" └── web.yml\n", " └── web.yml\n",
"\n", "\n",
@ -619,7 +651,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 13, "execution_count": 17,
"id": "9c3200b4", "id": "9c3200b4",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -627,31 +659,30 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"env: prod\n", "profile:\n",
"product: httpd_server\n", " env: prod\n",
"web_top_domain: infra.com\n", " product: httpd_server\n",
"web_app: myapp\n", " web_top_domain: infra.com\n",
"web_port: 80\n", " web_app: myapp\n",
"web_user_list:\n", " web_port: 80\n",
"- sysadmins\n", " web_user_list:\n",
"- domain_org\n", " - domain_org\n",
"- domain_org_external\n", " - domain_org_external\n",
"web_cache: 12h\n", " web_cache: 12h\n",
"\n", "\n",
"env: dev\n", "profile:\n",
"product: httpd_server\n", " env: dev\n",
"web_top_domain: dev.infra.net\n", " product: httpd_server\n",
"web_app: myapp\n", " web_top_domain: dev.infra.net\n",
"web_port: 80\n", " web_app: myapp\n",
"web_user_list:\n", " web_port: 80\n",
"- sysadmins\n", " web_user_list:\n",
"- debug_user\n", " - domain_org\n",
"- domain_org\n", " - domain_org_external\n",
"- domain_org_external\n", " web_cache: 1m\n",
"web_cache: 1m\n", " mysql_users:\n",
"mysql_users:\n", " - debug@10.0.%\n",
"- debug@10.0.%\n", " debug: true\n",
"debug: true\n",
"\n" "\n"
] ]
} }
@ -671,7 +702,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 14, "execution_count": 18,
"id": "7c5e86f3", "id": "7c5e86f3",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -679,31 +710,30 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"env: prod\n", "profile:\n",
"product: mysql_server\n", " env: prod\n",
"mysql_database: app_domain_org\n", " product: mysql_server\n",
"mysql_users:\n", " mysql_database: app_domain_org\n",
"- sysadmin@10.0.42%\n", " mysql_users:\n",
"- app_domain_org@10.0.51%\n", " - app_domain_org@10.0.51%\n",
"mysql_port: 3306\n", " mysql_port: 3306\n",
"mysql_cluster: false\n", " mysql_cluster: false\n",
"web_top_domain: infra.com\n", " web_top_domain: infra.com\n",
"web_cache: 12h\n", " web_cache: 12h\n",
"\n", "\n",
"env: dev\n", "profile:\n",
"product: mysql_server\n", " env: dev\n",
"mysql_database: app_domain_org\n", " product: mysql_server\n",
"mysql_users:\n", " mysql_database: app_domain_org\n",
"- sysadmin@10.0.42%\n", " mysql_users:\n",
"- debug@10.0.%\n", " - app_domain_org@10.0.51%\n",
"- app_domain_org@10.0.51%\n", " mysql_port: 3306\n",
"mysql_port: 3306\n", " mysql_cluster: false\n",
"mysql_cluster: false\n", " web_top_domain: dev.infra.net\n",
"web_top_domain: dev.infra.net\n", " web_cache: 1m\n",
"web_cache: 1m\n", " web_user_list:\n",
"web_user_list:\n", " - debug_user\n",
"- debug_user\n", " debug: true\n",
"debug: true\n",
"\n" "\n"
] ]
} }
@ -713,22 +743,6 @@
"kheops lookup -e node=mysql.infra.net -e role=mysql -e env=dev profile" "kheops lookup -e node=mysql.infra.net -e role=mysql -e env=dev profile"
] ]
}, },
{
"cell_type": "markdown",
"id": "db50e110",
"metadata": {},
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "c2c4658d",
"metadata": {},
"outputs": [],
"source": [
"\n"
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "e4bf6d8d", "id": "e4bf6d8d",
@ -739,7 +753,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 15, "execution_count": 19,
"id": "103cb37d", "id": "103cb37d",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -747,60 +761,64 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"env: NO_ENV\n", "profile:\n",
"product: NO_PRODUCT\n", " env: NO_ENV\n",
" product: NO_PRODUCT\n",
"\n", "\n",
"==> Per environment view\n", "==> Per environment view\n",
"env: prod\n", "profile:\n",
"product: NO_PRODUCT\n", " env: prod\n",
"web_top_domain: infra.com\n", " product: NO_PRODUCT\n",
"web_cache: 12h\n", " web_top_domain: infra.com\n",
" web_cache: 12h\n",
"\n", "\n",
"env: dev\n", "profile:\n",
"product: NO_PRODUCT\n", " env: dev\n",
"web_top_domain: dev.infra.net\n", " product: NO_PRODUCT\n",
"web_cache: 1m\n", " web_top_domain: dev.infra.net\n",
"web_user_list:\n", " web_cache: 1m\n",
"- debug_user\n", " web_user_list:\n",
"mysql_users:\n", " - debug_user\n",
"- debug@10.0.%\n", " mysql_users:\n",
"debug: true\n", " - debug@10.0.%\n",
" debug: true\n",
"\n", "\n",
"==> Per role and environment view\n", "==> Per role and environment view\n",
"env: prod\n", "profile:\n",
"product: mysql_server\n", " env: prod\n",
"mysql_database: NO_DATABASE\n", " product: mysql_server\n",
"mysql_users:\n", " mysql_database: NO_DATABASE\n",
"- sysadmin@10.0.42%\n", " mysql_users:\n",
"mysql_port: 3306\n", " - sysadmin@10.0.42%\n",
"mysql_cluster: false\n", " mysql_port: 3306\n",
"web_top_domain: infra.com\n", " mysql_cluster: false\n",
"web_cache: 12h\n", " web_top_domain: infra.com\n",
" web_cache: 12h\n",
"\n", "\n",
"env: prod\n", "profile:\n",
"product: httpd_server\n", " env: prod\n",
"web_top_domain: infra.com\n", " product: httpd_server\n",
"web_app: NO_APP\n", " web_top_domain: infra.com\n",
"web_port: 80\n", " web_app: NO_APP\n",
"web_user_list:\n", " web_port: 80\n",
"- sysadmins\n", " web_user_list:\n",
"web_cache: 12h\n", " - sysadmins\n",
" web_cache: 12h\n",
"\n", "\n",
"==> Per node view\n", "==> Per node view\n",
"env: dev\n", "profile:\n",
"product: httpd_server\n", " env: dev\n",
"web_top_domain: dev.infra.net\n", " product: httpd_server\n",
"web_app: myapp\n", " web_top_domain: dev.infra.net\n",
"web_port: 80\n", " web_app: myapp\n",
"web_user_list:\n", " web_port: 80\n",
"- sysadmins\n", " web_user_list:\n",
"- debug_user\n", " - domain_org\n",
"- domain_org\n", " - domain_org_external\n",
"- domain_org_external\n", " web_cache: 1m\n",
"web_cache: 1m\n", " mysql_users:\n",
"mysql_users:\n", " - debug@10.0.%\n",
"- debug@10.0.%\n", " debug: true\n",
"debug: true\n",
"\n" "\n"
] ]
} }
@ -902,7 +920,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 19, "execution_count": 21,
"id": "52f5033b", "id": "52f5033b",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -911,6 +929,7 @@
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"+ : Find where a key has been defined\n", "+ : Find where a key has been defined\n",
"+ : ==========================\n",
"+ grep --colour=auto -r '^profile:' examples/ex1_enc\n", "+ grep --colour=auto -r '^profile:' examples/ex1_enc\n",
"\u001b[35m\u001b[Kexamples/ex1_enc/env_prod.yml\u001b[m\u001b[K\u001b[36m\u001b[K:\u001b[m\u001b[K\u001b[01;31m\u001b[Kprofile:\u001b[m\u001b[K\n", "\u001b[35m\u001b[Kexamples/ex1_enc/env_prod.yml\u001b[m\u001b[K\u001b[36m\u001b[K:\u001b[m\u001b[K\u001b[01;31m\u001b[Kprofile:\u001b[m\u001b[K\n",
"\u001b[35m\u001b[Kexamples/ex1_enc/roles/mysql.yml\u001b[m\u001b[K\u001b[36m\u001b[K:\u001b[m\u001b[K\u001b[01;31m\u001b[Kprofile:\u001b[m\u001b[K\n", "\u001b[35m\u001b[Kexamples/ex1_enc/roles/mysql.yml\u001b[m\u001b[K\u001b[36m\u001b[K:\u001b[m\u001b[K\u001b[01;31m\u001b[Kprofile:\u001b[m\u001b[K\n",
@ -920,6 +939,7 @@
"\u001b[35m\u001b[Kexamples/ex1_enc/default.yml\u001b[m\u001b[K\u001b[36m\u001b[K:\u001b[m\u001b[K\u001b[01;31m\u001b[Kprofile:\u001b[m\u001b[K\n", "\u001b[35m\u001b[Kexamples/ex1_enc/default.yml\u001b[m\u001b[K\u001b[36m\u001b[K:\u001b[m\u001b[K\u001b[01;31m\u001b[Kprofile:\u001b[m\u001b[K\n",
"\u001b[35m\u001b[Kexamples/ex1_enc/env_dev.yml\u001b[m\u001b[K\u001b[36m\u001b[K:\u001b[m\u001b[K\u001b[01;31m\u001b[Kprofile:\u001b[m\u001b[K\n", "\u001b[35m\u001b[Kexamples/ex1_enc/env_dev.yml\u001b[m\u001b[K\u001b[36m\u001b[K:\u001b[m\u001b[K\u001b[01;31m\u001b[Kprofile:\u001b[m\u001b[K\n",
"+ : Find where a key has been defined and 5 first lines\n", "+ : Find where a key has been defined and 5 first lines\n",
"+ : ==========================\n",
"+ grep --colour=auto -r -A 5 web_user_list: examples/ex1_enc\n", "+ grep --colour=auto -r -A 5 web_user_list: examples/ex1_enc\n",
"\u001b[35m\u001b[Kexamples/ex1_enc/roles/web.yml\u001b[m\u001b[K\u001b[36m\u001b[K:\u001b[m\u001b[K \u001b[01;31m\u001b[Kweb_user_list:\u001b[m\u001b[K\n", "\u001b[35m\u001b[Kexamples/ex1_enc/roles/web.yml\u001b[m\u001b[K\u001b[36m\u001b[K:\u001b[m\u001b[K \u001b[01;31m\u001b[Kweb_user_list:\u001b[m\u001b[K\n",
"\u001b[35m\u001b[Kexamples/ex1_enc/roles/web.yml\u001b[m\u001b[K\u001b[36m\u001b[K-\u001b[m\u001b[K - sysadmins\n", "\u001b[35m\u001b[Kexamples/ex1_enc/roles/web.yml\u001b[m\u001b[K\u001b[36m\u001b[K-\u001b[m\u001b[K - sysadmins\n",
@ -937,6 +957,7 @@
"\u001b[35m\u001b[Kexamples/ex1_enc/env_dev.yml\u001b[m\u001b[K\u001b[36m\u001b[K-\u001b[m\u001b[K\n", "\u001b[35m\u001b[Kexamples/ex1_enc/env_dev.yml\u001b[m\u001b[K\u001b[36m\u001b[K-\u001b[m\u001b[K\n",
"\u001b[35m\u001b[Kexamples/ex1_enc/env_dev.yml\u001b[m\u001b[K\u001b[36m\u001b[K-\u001b[m\u001b[K debug: true\n", "\u001b[35m\u001b[Kexamples/ex1_enc/env_dev.yml\u001b[m\u001b[K\u001b[36m\u001b[K-\u001b[m\u001b[K debug: true\n",
"+ : Search from anything related to database\n", "+ : Search from anything related to database\n",
"+ : ==========================\n",
"+ grep --colour=auto -R -C 3 database examples/ex1_enc\n", "+ grep --colour=auto -R -C 3 database examples/ex1_enc\n",
"\u001b[35m\u001b[Kexamples/ex1_enc/roles/mysql.yml\u001b[m\u001b[K\u001b[36m\u001b[K-\u001b[m\u001b[Kprofile:\n", "\u001b[35m\u001b[Kexamples/ex1_enc/roles/mysql.yml\u001b[m\u001b[K\u001b[36m\u001b[K-\u001b[m\u001b[Kprofile:\n",
"\u001b[35m\u001b[Kexamples/ex1_enc/roles/mysql.yml\u001b[m\u001b[K\u001b[36m\u001b[K-\u001b[m\u001b[K product: \"mysql_server\"\n", "\u001b[35m\u001b[Kexamples/ex1_enc/roles/mysql.yml\u001b[m\u001b[K\u001b[36m\u001b[K-\u001b[m\u001b[K product: \"mysql_server\"\n",
@ -960,12 +981,17 @@
"set -x\n", "set -x\n",
"\n", "\n",
": Find where a key has been defined\n", ": Find where a key has been defined\n",
": ==========================\n",
"grep -r '^profile:' examples/$KHEOPS_NAMESPACE\n", "grep -r '^profile:' examples/$KHEOPS_NAMESPACE\n",
"\n", "\n",
"\n",
": Find where a key has been defined and 5 first lines\n", ": Find where a key has been defined and 5 first lines\n",
": ==========================\n",
"grep -r -A 5 'web_user_list:' examples/$KHEOPS_NAMESPACE\n", "grep -r -A 5 'web_user_list:' examples/$KHEOPS_NAMESPACE\n",
"\n", "\n",
"\n",
": Search from anything related to database\n", ": Search from anything related to database\n",
": ==========================\n",
"grep -R -C 3 'database' examples/$KHEOPS_NAMESPACE\n", "grep -R -C 3 'database' examples/$KHEOPS_NAMESPACE\n",
"\n", "\n",
"set +x" "set +x"
@ -989,7 +1015,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 18, "execution_count": 22,
"id": "49bc3fc3", "id": "49bc3fc3",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -1050,7 +1076,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 17, "execution_count": 23,
"id": "e4a1e8d1", "id": "e4a1e8d1",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -1058,26 +1084,25 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"--- /dev/fd/63\t2022-02-01 20:10:53.094525316 -0500\n", "--- /dev/fd/63\t2022-02-14 13:45:59.223619144 -0500\n",
"+++ /dev/fd/62\t2022-02-01 20:10:53.094525316 -0500\n", "+++ /dev/fd/62\t2022-02-14 13:45:59.223619144 -0500\n",
"@@ -1,11 +1,15 @@\n", "@@ -1,11 +1,14 @@\n",
"-env: prod\n", " profile:\n",
"+env: dev\n", "- env: prod\n",
"+ env: dev\n",
" product: httpd_server\n", " product: httpd_server\n",
"-web_top_domain: infra.com\n", "- web_top_domain: infra.com\n",
"+web_top_domain: dev.infra.net\n", "+ web_top_domain: dev.infra.net\n",
" web_app: myapp\n", " web_app: myapp\n",
" web_port: 80\n", " web_port: 80\n",
" web_user_list:\n", " web_user_list:\n",
" - sysadmins\n",
"+- debug_user\n",
" - domain_org\n", " - domain_org\n",
" - domain_org_external\n", " - domain_org_external\n",
"-web_cache: 12h\n", "- web_cache: 12h\n",
"+web_cache: 1m\n", "+ web_cache: 1m\n",
"+mysql_users:\n", "+ mysql_users:\n",
"+- debug@10.0.%\n", "+ - debug@10.0.%\n",
"+debug: true\n", "+ debug: true\n",
" \n" " \n"
] ]
}, },
@ -1094,14 +1119,6 @@
"<(kheops lookup -e node=web.infra.net -e role=web -e env=dev profile)" "<(kheops lookup -e node=web.infra.net -e role=web -e env=dev profile)"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "a6feb212",
"metadata": {},
"outputs": [],
"source": []
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "12b1730b", "id": "12b1730b",
@ -1112,7 +1129,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 21, "execution_count": 25,
"id": "3ac0cc53", "id": "3ac0cc53",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -1149,11 +1166,11 @@
"+------------------------+------------------------------+\n", "+------------------------+------------------------------+\n",
" INFO: Explain candidates:\n", " INFO: Explain candidates:\n",
"+----------------------------------------------------------------------------------+-------------------------------+------------------------------+\n", "+----------------------------------------------------------------------------------+-------------------------------+------------------------------+\n",
"| Status | Runtime | Data |\n", "| Status | Runtime | Key Value |\n",
"+----------------------------------------------------------------------------------+-------------------------------+------------------------------+\n", "+----------------------------------------------------------------------------------+-------------------------------+------------------------------+\n",
"| | | |\n", "| | | |\n",
"| Status:{ | Runtime:{ | Data:{ |\n", "| Status:{ | Runtime:{ | Key:{ |\n",
"| \"path\": \"/home/jez/volumes/data/prj/bell/training/tiger-ansible/ext/kheops ... | \"scope\": { | \"env\": \"NO_ENV\", |\n", "| \"path\": \"/home/jez/volumes/data/prj/bell/dev/kheops/examples/ex1_enc/defau ... | \"scope\": { | \"env\": \"NO_ENV\", |\n",
"| \"status\": \"found\", | \"role\": \"web\" | \"product\": \"NO_PRODUCT\" |\n", "| \"status\": \"found\", | \"role\": \"web\" | \"product\": \"NO_PRODUCT\" |\n",
"| \"rel_path\": \"examples/ex1_enc/default.yml\" | }, | } |\n", "| \"rel_path\": \"examples/ex1_enc/default.yml\" | }, | } |\n",
"| } | \"key\": \"profile\", | |\n", "| } | \"key\": \"profile\", | |\n",
@ -1164,8 +1181,8 @@
"| | \"backend_index\": 0 | |\n", "| | \"backend_index\": 0 | |\n",
"| | } | |\n", "| | } | |\n",
"| | | |\n", "| | | |\n",
"| Status:{ | Runtime:{ | Data:{ |\n", "| Status:{ | Runtime:{ | Key:{ |\n",
"| \"path\": \"/home/jez/volumes/data/prj/bell/training/tiger-ansible/ext/kheops ... | \"scope\": { | \"product\": \"httpd_server\", |\n", "| \"path\": \"/home/jez/volumes/data/prj/bell/dev/kheops/examples/ex1_enc/roles ... | \"scope\": { | \"product\": \"httpd_server\", |\n",
"| \"status\": \"found\", | \"role\": \"web\" | \"web_top_domain\": \"\", |\n", "| \"status\": \"found\", | \"role\": \"web\" | \"web_top_domain\": \"\", |\n",
"| \"rel_path\": \"examples/ex1_enc/roles/web.yml\" | }, | \"web_app\": \"NO_APP\", |\n", "| \"rel_path\": \"examples/ex1_enc/roles/web.yml\" | }, | \"web_app\": \"NO_APP\", |\n",
"| } | \"key\": \"profile\", | \"web_port\": 80, |\n", "| } | \"key\": \"profile\", | \"web_port\": 80, |\n",
@ -1176,25 +1193,26 @@
"| | \"backend_index\": 1 | |\n", "| | \"backend_index\": 1 | |\n",
"| | } | |\n", "| | } | |\n",
"+----------------------------------------------------------------------------------+-------------------------------+------------------------------+\n", "+----------------------------------------------------------------------------------+-------------------------------+------------------------------+\n",
"env: NO_ENV\n", "profile:\n",
"product: httpd_server\n", " env: NO_ENV\n",
"web_top_domain: ''\n", " product: httpd_server\n",
"web_app: NO_APP\n", " web_top_domain: ''\n",
"web_port: 80\n", " web_app: NO_APP\n",
"web_user_list:\n", " web_port: 80\n",
"- sysadmins\n", " web_user_list:\n",
" - sysadmins\n",
"\n" "\n"
] ]
} }
], ],
"source": [ "source": [
"kheops lookup -e role=web profile -x" "kheops lookup -e role=web profile -X"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "cb111da7", "id": "07eeed03",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [] "source": []

View File

@ -0,0 +1,5 @@
---
profile:
env: "NO_ENV"
product: "NO_PRODUCT"

View File

@ -0,0 +1,16 @@
---
profile:
env: dev
# We change the top domain for dev environment, and reduce the cache
web_top_domain: dev.infra.net
web_cache: 1m
# We want a debug users
web_user_list:
- debug_user
mysql_users:
- debug@10.0.%
debug: true

View File

@ -0,0 +1,8 @@
---
profile:
env: prod
# On production environment, we always want to use public faced domain and 12 hour cache.
web_top_domain: infra.com
web_cache: 12h

View File

@ -0,0 +1,6 @@
---
profile:
mysql_database: "app_domain_org"
mysql_users:
- "app_domain_org@10.0.51%"

View File

@ -0,0 +1,7 @@
---
profile:
web_app: 'myapp'
web_user_list:
- domain_org
- domain_org_external

View File

@ -0,0 +1,10 @@
---
profile:
product: "mysql_server"
mysql_database: "NO_DATABASE"
mysql_users:
- "sysadmin@10.0.42%"
mysql_port: 3306
mysql_cluster: False

View File

@ -0,0 +1,10 @@
---
profile:
product: "httpd_server"
web_top_domain: ""
web_app: "NO_APP"
web_port: 80
web_user_list:
- sysadmins

281
examples/kheops.yml Normal file
View File

@ -0,0 +1,281 @@
# Kheops can act as ENC
# This is a basic tree that demonstrate how to use Kheops as ENC:
# Examples:
# kheops -vvv -c examples/kheops.yml lookup2 -n ex1_enc -f examples/q_011.yml profile
#
ex1_enc:
#Query keys:
# profile,role,class:
# profiles,roles,classes:
config:
file_path_prefix: "ex1_enc/"
#file_path_suffix: "/ansible"
file_path_suffix: null
merge_deep_algo: additive
lookups:
- path: default # Simplest form, just a path
- path: "roles/{role}" # If list, it's auto expanded like in bash
- path: "env_{env}" # If list, it's auto expanded like in bash
- path: "nodes/{node}"
#loop:
# totot: loop2
#loop_1:
# tutu: blaaaa
### ADDD NEW EXAMPLES:
# Add a way to remove entries, so we need to talk about mergers... Not yet
exN_scope_plugins:
#Query keys:
# profile,role,class:
# profiles,roles,classes:
config:
file_path_prefix: "01_hello/"
#file_path_suffix: "/ansible"
file_path_suffix: null
merge_deep_algo: additive
lookups:
- path: "tree/{item_hier1}" # If list, it's auto expanded like in bash
scope:
- module: loop
data:
- toto1
- toto2
- toto3
- path: "tree/{item_loop}/{item_hier}" # If list, it's auto expanded like in bash
scope:
- module: loop
data:
- prod
- dev
- module: hier
data: node_fqdn
split: '.'
reversed: True
start: 2
- path: "tree/{item_hier1}" # If list, it's auto expanded like in bash
scope:
- module: hier
data: test_hg
split: '/'
reversed: False
# Kheops can store your configuration/data
# This is a basic tree that demonstrate how to use Kheops as ENC:
ex2_config:
#Query keys:
# profile,role,class:
# profiles,roles,classes:
lookups:
# Default config
- common/default
# Per distribution config
- "common/dist_{ansible_dist_name}"
- "common/dist_{ansible_dist_name}{ansible_dist_version}"
# Nested dir support
- path: site/{node_env}/{node_role}
engine: jerakia
# Per node and group config
- groups/{node_group}
- nodes/{node_fqdn}
# Override config
- common/override
#
# Kheops support many backends: file, http, consul, even script !!!
ex3_backends:
lookups:
- backend: file,glob,consul,http,script,vault,none
- path: node/{node_fqdn}
backend: file
file:
sufix: /ansible.yml
- path: node/{node_fqdn}
backend: glob
glob:
sufix: /ansible_*.yml
- backend: http
path: https://foreman.corp/host/{node_fqdn}/facts?fmt=json
http:
format: json
http_auth: SECRET_FILE.pass
headers:
# Keops support operations on scope variable
ex3_loops_profiles:
lookups:
# Group management, axpanded list
- path: groups/{item}
loop:
data: groups
# A more advanced example, hierarchical exampansion
- path: site/{foreman_hostgroups}/{ansible_fqdn} # If list, it's auto expanded like in bash
engine: jerakia
hier:
data: foreman_hostgroup
var: foreman_hostgroups
reversed: False
# DO an example of multiple loops
- nodes/{ansible_fqdn}
- common/override
# Kheops support nested hierarchy like in Foreman/Sattelite
# Example to use Kheops with Foreman
ex4_foreman_hostgroups:
#Query keys:
# profile,role,class:
# profiles,roles,classes:
lookups:
- path: site/loc_{foreman_location} # If list, it's auto expanded like in bash
engine: jerakia
- path: site/org_{foreman_organization} # If list, it's auto expanded like in bash
engine: jerakia
- path: site/{foreman_hostgroups}/{ansible_fqdn} # If list, it's auto expanded like in bash
engine: jerakia
hier:
data: foreman_hostgroup
var: foreman_hostgroups
reversed: False
- nodes/{ansible_fqdn}
# Kheops support namespace (For performance reasons) and include to make your code DRY
ex5_ns_include:
ns1:
lookups:
- include: common.yml
- path: ns1_only/{node_fqdn}
ns2:
lookups:
- include: common.yml
- include: ns2_only.yml
# Kheops allow to customize which and how keys are merged together
# Explain how to merge the keys
ex6_rules:
lookups:
...
rules:
- key: "my_key"
- key: ".*"
- key: ".*_db"
- key: "prefix_.*"
continue: True
merge:
last: No merge
basic: basic python merge, assemble list and combine dict 1 level.
schema: jsonmerge, heavy in resources
first: Why ?
smart_lists: support +-!~ key identifiers ...
smart_dict: support +-!~ key identifiers ...
# REal and complete example
ex7_data:
config:
app: {}
# prefix: ./ext/ansible-tree/tree
#totot: titi
lookups:
#prefix: ./ext/ansible-tree/tree
prefix: ext/tiger-jerakia/data/data
engine: file.auto
sufix: ansible.yaml
default_engine: jerakia
default_item:
engine: jerakia
# File lookup
lookups:
- common/deprecated
- common/all
- "common/dist_{ansible_dist_name}"
- "common/dist_{ansible_dist_name}{ansible_dist_version}"
# Default structure
# Profile management
- path: profiles/{item}
loop:
data: tiger_profiles
# THis is not dry, use include to make code DRY
- path: site/loc_{foreman_location} # If list, it's auto expanded like in bash
engine: jerakia
- path: site/org_{foreman_organization} # If list, it's auto expanded like in bash
engine: jerakia
- path: site/{foreman_hostgroups}/{ansible_fqdn} # If list, it's auto expanded like in bash
engine: jerakia
hier:
data: foreman_hostgroup
var: foreman_hostgroups
reversed: False
# loop:
# var: aa_env
# data:
# - prod
# - preprod
# - devel
# - path: site/{foreman_hostgroup}/{ansible_fqdn}
# engine: jerakia
- nodes/{ansible_fqdn}
- common/override
rules:
- rule: 'profilesss'
strategy: first
schema:
"$schema": http://json-schema.org/draft-04/schema#
type: array
mergeStrategy: append
- rule: '(.*)_item'
schema: generic item schema

View File

@ -11,89 +11,50 @@ from pathlib import Path
import anyconfig import anyconfig
from diskcache import Cache from diskcache import Cache
import kheops.plugin as KheopsPlugins
from kheops.controllers import QueryProcessor from kheops.controllers import QueryProcessor
from kheops.utils import schema_validate from kheops.utils import schema_validate
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
CONF_SCHEMA = { CONF_SCHEMA = {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"type": "object", "type": "object",
"additionalProperties": False, "additionalProperties": False,
"default": {}, "default": {},
"$def": { "required": ["config"],
"backends_items": {}, #"$def": {
"backends_config": {}, # "backends_items": {},
"rules_items": {}, # "backends_config": {},
"rules_config": {}, # "rules_items": {},
}, # "rules_config": {},
# "patternProperties": { #},
# ".*": {
# "type": "object",
# "optional": True,
# "additionalProperties": False,
"properties": { "properties": {
"lookups": {
"type": "array",
"default": [],
"items": {
"type": "object",
#"properties": {"$ref": "#/$defs/backends_items"},
},
},
"rules": {
"type": "array",
"default": [],
# "arrayItem": { "$ref": "#/$defs/rules_items" },
},
"config": { "config": {
"type": "object", "type": "object",
"default": {}, "default": {},
"additionalProperties": True, "additionalProperties": True,
#"required": ["app"],
"properties": { "properties": {
"app": { "app": {
"type": "object", "type": "object",
"default": {}, "default": {},
"additionalProperties": False, "additionalProperties": False,
"properties": {
"root": {
"default": None,
"oneOf": [
{
"type": "null",
"description": "Application current working directory is the `kheops.yml` directory",
},
{
"type": "string",
"description": "Application working directory. If a relative path is used, it will be depending on `kheops.yml` directory",
},
],
},
"cache": {
"default": "kheops_cache",
"oneOf": [
{
"type": "null",
"description": "Disable cache",
},
{
"type": "string",
"description": "Path of the cache directory",
},
],
},
},
},
# OLD
"tree": {
# "additionalProperties": False,
"type": "object",
"default": {},
"deprecated": True,
"properties": {
"prefix": {
"default": None,
"oneOf": [
{
"type": "null",
"description": "Disable prefix, all files are lookup up from the app root dir.",
},
{
"type": "string",
"description": """Add a path prefix before all paths. This is quite useful to store your YAML data in a dedicated tree.""",
},
],
},
},
}, },
"lookups": { "lookups": {
# "additionalProperties": False, # "additionalProperties": False,
@ -121,30 +82,7 @@ CONF_SCHEMA = {
}, },
}, },
}, },
"tree": {
"type": "array",
"default": [],
"items": {
"type": "object",
"properties": {"$ref": "#/$defs/backends_items"},
}, },
},
"lookups": {
"type": "array",
"default": [],
"items": {
"type": "object",
"properties": {"$ref": "#/$defs/backends_items"},
},
},
"rules": {
"type": "array",
"default": [],
# "arrayItem": { "$ref": "#/$defs/rules_items" },
},
},
# },
# },
} }
@ -184,18 +122,17 @@ class KheopsNamespace(GenericInstance, QueryProcessor):
:type config: Any :type config: Any
""" """
config = schema_validate(config, CONF_SCHEMA)
super().__init__(config)
self.name = name self.name = name
self.config = config or {}
self.app = app self.app = app
self.run = dict(app.run) self.run = dict(app.run)
# Validate configuration # Validate configuration
self.config = schema_validate(self.config, CONF_SCHEMA)
self.run["path_ns"] = str(Path(app.run["config_src"]).parent.resolve()) self.run["path_ns"] = str(Path(app.run["config_src"]).parent.resolve())
class Kheops(GenericInstance): class Kheops(GenericInstance):
""" """
Kheops Application Class Kheops Application Class
@ -228,6 +165,7 @@ class Kheops(GenericInstance):
raise Exception("Need a valid config") raise Exception("Need a valid config")
self.ns_name = namespace self.ns_name = namespace
self.namespaces = {}
self.raw_config = self.parse_conf(config) self.raw_config = self.parse_conf(config)
def parse_conf(self, config="kheops.yml"): def parse_conf(self, config="kheops.yml"):
@ -244,7 +182,10 @@ class Kheops(GenericInstance):
# Load config # Load config
if isinstance(config, str): if isinstance(config, str):
try:
dict_conf = anyconfig.load(config) dict_conf = anyconfig.load(config)
except Exception as err:
raise Exception ("Can't load kheops configuration, got: %s", err)
source = f"file:{config}" source = f"file:{config}"
elif isinstance(config, dict): elif isinstance(config, dict):
dict_conf = config dict_conf = config
@ -260,10 +201,14 @@ class Kheops(GenericInstance):
explain=False, explain=False,
validate_schema=False, validate_schema=False,
namespace=None, namespace=None,
namespace_prefix=False,
): ):
""" """
Lookup a key in hierarchy Lookup a key in hierarchy
For a given lookup:
* keys= [<namespace>:<key>]
:param keys: List of keys to query. :param keys: List of keys to query.
:type keys: list[str] :type keys: list[str]
@ -278,7 +223,7 @@ class Kheops(GenericInstance):
key_def = key_def or "" key_def = key_def or ""
# Identify namespace and key # Identify namespace and key
parts = key_def.split(":") parts = key_def.split("/")
ns_name = namespace or self.ns_name ns_name = namespace or self.ns_name
if len(parts) > 1: if len(parts) > 1:
ns_name = parts[0] ns_name = parts[0]
@ -287,17 +232,29 @@ class Kheops(GenericInstance):
key_name = parts[0] key_name = parts[0]
# Load namespace # Load namespace
if ns_name in self.namespaces:
ns_config = self.namespaces[ns_name]
else:
try:
ns_config = self.raw_config[ns_name] ns_config = self.raw_config[ns_name]
except KeyError as err:
raise Exception(f"Unknown kheops namespace: {ns_name}")
ns = KheopsNamespace(self, ns_name, ns_config) ns = KheopsNamespace(self, ns_name, ns_config)
# Get result # Get result
result = ns.query(key=key_name, scope=scope, explain=explain) result = ns.query(key=key_name, scope=scope, explain=explain)
# Prepare output
_key = key_name
if namespace_prefix == True:
_key = key_def
ret[_key] = result
# TODO: This may lead to inconsistant output format :/ # TODO: This may lead to inconsistant output format :/
# Return result # Return result
#if len(keys) > 1: #if len(keys) > 1:
# log.debug("Append '%s' to results", key_name) # log.debug("Append '%s' to results", key_name)
ret[key_name] = result
#else: #else:
# log.debug("Return '%s' result", key_name) # log.debug("Return '%s' result", key_name)
# return result # return result
@ -311,33 +268,7 @@ class Kheops(GenericInstance):
# To clean/implement
# def DEPRECATED_lookup(
# self,
# keys=None,
# policy=None,
# scope=None,
# trace=False,
# explain=False,
# validate_schema=False,
# ):
# """Lookup a key in hierarchy"""
# log.debug("Lookup key %s with scope: %s", keys, scope)
# assert isinstance(keys, list), f"Got {keys}"
# query = Query(app=self)
# ret = {}
# for key in keys:
# ret[key] = query.exec(
# key=key,
# scope=scope,
# policy=policy,
# trace=trace,
# explain=explain,
# validate_schema=validate_schema,
# )
# return ret
# def DEPRECATED_dump_schema(self): # def DEPRECATED_dump_schema(self):
# """Dump configuration schema""" # """Dump configuration schema"""

View File

@ -1,3 +1,9 @@
"""
Kheops controller
Main Kheops model classes
"""
import json import json
import logging import logging
@ -7,35 +13,45 @@ from pathlib import Path
from prettytable import PrettyTable from prettytable import PrettyTable
import kheops.plugin as KheopsPlugins import kheops.plugin as KheopsPlugins
from kheops.utils import render_template, render_template_python, str_ellipsis from kheops.utils import render_template_python, str_ellipsis
from pprint import pprint
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
tracer = logging.getLogger(f"{__name__}.explain") tracer = logging.getLogger(f"{__name__}.explain")
# Helper classes
# ------------------------
class LoadPlugin: class LoadPlugin:
"""Generic class to load plugins""" """Kheops plugins loader
This plugin loader is a helper to load a python module (Kheops Plugin) from
a plugin kind and name.
"""
def __init__(self, plugins): def __init__(self, plugins):
self.plugins = plugins self.plugins = plugins
def load(self, kind, name): def load(self, kind, name):
"""
Load a plugin
"""
assert isinstance(name, str), f"Got: {name}" assert isinstance(name, str), f"Got: {name}"
# Get plugin kind # Get plugin kind
try: try:
plugins = getattr(self.plugins, kind) plugins = getattr(self.plugins, kind)
except Exception as err: except AttributeError as err:
raise Exception(f"Unknown module kind '{kind}': {err}") raise Exception(f"Unknown module kind '{kind}': {err}") from err
# Get plugin class # Get plugin class
try: try:
plugin_cls = getattr(plugins, name) plugin_cls = getattr(plugins, name)
except Exception as err: except AttributeError as err:
raise Exception(f"Unknown module '{kind}.{name}': {err}") raise Exception(f"Unknown module '{kind}.{name}': {err}") from err
assert hasattr( assert hasattr(
plugin_cls, "Plugin" plugin_cls, "Plugin"
@ -44,11 +60,29 @@ class LoadPlugin:
# Return plugin Classe # Return plugin Classe
return plugin_cls.Plugin return plugin_cls.Plugin
class BackendCandidate():
"""Backend Candidate
plugin_loader = LoadPlugin(KheopsPlugins) This object represents a backend candidate. It holds the value of the
requested key, but also so source path, the status and some other metadata.
"""
def __init__(self, path=None, data=None, run=None, status=None):
assert isinstance(run, dict)
self.path = path
self.status = status or "unparsed"
self.run = run or {}
self.data = data or None
def __repr__(self):
return f"Status: {self.status}, Path: {self.path} => {self.data}"
class Query: class Query:
"""Query object
Object that hold key and scope.
"""
key = None key = None
scope = None scope = None
@ -60,17 +94,25 @@ class Query:
self.rule = None self.rule = None
# class QueryController(): # Query Processor class
# ------------------------
# def exec(self, key=None, scope=None):
# query = Query(key, scope)
# result = self.processor.exec(query)
# return result
class QueryProcessor: class QueryProcessor:
"""QueryProcessor class provides all the methods to be able to make queries""" """QueryProcessor
This class helps to do queries for a given key and scope. It provides a single
public method. It also implement an explain mechanism to help to troubleshoot query
lookup issues.
The query process consists in:
* Create a new query with the key and th scope
* Fetch and expand the lookup list (_exec_assemble_lookups)
* Fetch the rule that match the key (_exec_get_rule)
* Fetch the strategy that match the key
* Query all backends with lookup list (_exec_backend_plugins)
* Return result
"""
default_match_rule = { default_match_rule = {
"key": None, "key": None,
@ -84,19 +126,17 @@ class QueryProcessor:
"continue": True, "continue": True,
} }
# def __init__(self, app): def __init__(self, config):
# self.app = app self.plugin_loader = LoadPlugin(KheopsPlugins)
# self.config = app.conf2['config'] or {}
# self.lookups = app.conf2['lookups'] or []
# self.rules = app.conf2['rules'] or []
def CHILDREN_INIT(self, config):
self.config = config self.config = config
pass
# def exec(self, key=None, scope=None): # Query methods
# ------------------------
def query(self, key=None, scope=None, explain=False): def query(self, key=None, scope=None, explain=False):
"""Query key with scope
"""
if explain: if explain:
tracer.setLevel(logging.DEBUG) tracer.setLevel(logging.DEBUG)
@ -109,27 +149,23 @@ class QueryProcessor:
# Assemble if more than one and merge when continue. # Assemble if more than one and merge when continue.
# Got the Matched rule (RULE CACHE) # Got the Matched rule (RULE CACHE)
# We'll need strategy, and it's selector field: matched/first/last/all # We'll need strategy, and it's selector field: matched/first/last/all
# key_rule = self._get_key_rule(key) or {}
# key_strategy = key_rule.get('strategy', None)
key_rule = self._exec_get_rule(query) key_rule = self._exec_get_rule(query)
log.info("Matched rule for key '%s': %s", query.key, key_rule) log.info("Matched rule for key '%s': %s", query.key, key_rule)
# Build the lookups [] => [] # Build the lookups [] => []
# Fetch static config from app (for include and NS:includes ...) # Fetch static config from app (for include and NS:includes ...)
# Loop over lookups and process each lookup with ScopePlugins # Loop over lookups and process each lookup with ScopePlugins
lookups = self.config.get("lookups", {}).copy() lookups = self.config["lookups"].copy()
parsed_lookups = self._exec_assemble_lookups(lookups, query) parsed_lookups = self._exec_assemble_lookups(lookups, query)
# Generate explain report # Generate explain report
if explain: if explain:
self._explain_lookups(parsed_lookups) self._explain_lookups(parsed_lookups)
# FEtch the module # Fetch the module
# Retrieve the module instance # Retrieve the module instance
# Get it's match policy
# TODO
plugin_name = key_rule.get("strategy", None) plugin_name = key_rule.get("strategy", None)
strategy_plugin = plugin_loader.load("strategy", plugin_name)(self) strategy_plugin = self.plugin_loader.load("strategy", plugin_name)(self)
# Get the data (strategy.selector) # Get the data (strategy.selector)
# For each entry, ask the backend to return the data: file, http, consul ... # For each entry, ask the backend to return the data: file, http, consul ...
@ -151,94 +187,29 @@ class QueryProcessor:
return result return result
def _explain_lookups(self, parsed_lookups):
"""Explain list of lookups"""
table = PrettyTable() # Query parts methods
for item in parsed_lookups: # ------------------------
col1 = json.dumps(
{k: v for k, v in item.items() if k not in ["_run"]},
default=lambda o: "<not serializable>",
indent=2,
)
col2 = json.dumps(
item["_run"], default=lambda o: "<not serializable>", indent=2
)
table.add_row(
[
"\nConfig:" + str_ellipsis(col1, 60),
"\nRuntime:" + str_ellipsis(col2, 60),
]
)
table.field_names = ["Config", "Runtime"]
table.align = "l"
tracer.info("Explain lookups:\n" + str(table))
def _explain_candidates(self, candidates, query): def _exec_get_rule(self, query, mode="match"):
"""Explain list of candidates"""
# TOFIX: query is not needed here ! key = query.key
rules = self.config["rules"] or {}
table = PrettyTable() if mode == "match":
for item_obj in candidates: rule = dict(self.default_match_rule)
item = item_obj.__dict__ rules = [i for i in rules if i.get("key", None) == key]
item["rel_path"] = str(Path(item["path"]).relative_to(Path.cwd())) if len(rules) > 0:
match = rules[0]
col1 = json.dumps( rule.update(match)
{k: v for k, v in item.items() if k not in ["run", "data"]},
default=lambda o: "<not serializable>",
indent=2,
)
col2 = json.dumps(
item["run"]["_run"], default=lambda o: "<not serializable>", indent=2
)
col3 = (
item_obj.data.get(query.key, "NOT FOUND")
if query.key is not None
else item_obj.data
)
col3 = json.dumps(col3, default=lambda o: "<not serializable>", indent=2)
table.add_row(
[
"\nStatus:" + str_ellipsis(col1, 80),
"\nRuntime:" + str_ellipsis(col2, 60),
"\nKey:" + str_ellipsis(col3, 60),
]
)
table.field_names = ["Status", "Runtime", "Key Value"]
table.align = "l"
tracer.info("Explain candidates:\n" + str(table))
def _exec_backend_plugins(self, lookups, selector="matched"):
selector = "matched"
assert selector in ["last", "first", "all", "matched"]
assert isinstance(lookups, list)
# lookups = self.config.get("lookups", {}).copy()
plugins = {}
ret = []
for index, lookup_def in enumerate(lookups):
# Update object
lookup_def["_run"]["backend_index"] = index
# Load plugin
plugin_name = lookup_def["backend"]
if plugin_name in plugins:
plugin = plugins[plugin_name]
else: else:
plugin = plugin_loader.load("backend", plugin_name)(namespace=self) log.debug("Applying default rule for key '%s'", key)
rule = self.default_match_rule
else:
raise Exception(f"Mode '{mode}' is not implemented")
# Get candidates return rule
candidates = plugin.fetch_data(lookup_def)
# Apply selector
for candidate in candidates:
if candidate.status == "found" or selector == "all":
ret.append(candidate)
return ret
def _exec_assemble_lookups(self, lookups, query): def _exec_assemble_lookups(self, lookups, query):
@ -248,10 +219,10 @@ class QueryProcessor:
# Init the scope list # Init the scope list
new_lookups1 = [] new_lookups1 = []
for index, lookup_def in enumerate(lookups): for index, lookup_def in enumerate(lookups):
shortform = False #shortform = False
if isinstance(lookup_def, str): if isinstance(lookup_def, str):
shortform = True #shortform = True
lookup_def = { lookup_def = {
"path": lookup_def, "path": lookup_def,
} }
@ -279,7 +250,7 @@ class QueryProcessor:
plugin_name = plugin_def.get("module", None) plugin_name = plugin_def.get("module", None)
if plugin_name: if plugin_name:
plugin = plugin_loader.load("scope", plugin_name)(namespace=self) plugin = self.plugin_loader.load("scope", plugin_name)(namespace=self)
ret = plugin.process_items(ret, plugin_def) ret = plugin.process_items(ret, plugin_def)
new_lookups2.extend(ret) new_lookups2.extend(ret)
@ -295,25 +266,99 @@ class QueryProcessor:
lookup["path"] = new_path lookup["path"] = new_path
new_lookups3.append(lookup) new_lookups3.append(lookup)
else: else:
log.info("Ignore because of missing scope vars: '%s'", path) log.warning("Ignore lookup item because of missing scope vars: '%s'", path)
return new_lookups3 return new_lookups3
def _exec_get_rule(self, query, mode="match"):
key = query.key def _exec_backend_plugins(self, lookups, selector="matched"):
rules = self.config["rules"] or {} selector = "matched"
assert selector in ["last", "first", "all", "matched"]
assert isinstance(lookups, list)
# lookups = self.config.get("lookups", {}).copy()
if mode == "match": plugins = {}
rule = dict(self.default_match_rule) ret = []
rules = [i for i in rules if i.get("key", None) == key] for index, lookup_def in enumerate(lookups):
if len(rules) > 0:
match = rules[0] # Update object
rule.update(match) lookup_def["_run"]["backend_index"] = index
# Load plugin
plugin_name = lookup_def["backend"]
if plugin_name in plugins:
plugin = plugins[plugin_name]
else: else:
log.debug("Applying default rule for key '%s'", key) plugin = self.plugin_loader.load("backend", plugin_name)(namespace=self)
rule = self.default_match_rule
else:
raise Exception(f"Mode '{mode}' is not implemented")
return rule # Get candidates
candidates = plugin.fetch_data(lookup_def)
# Apply selector
for candidate in candidates:
if candidate.status == "found" or selector == "all":
ret.append(candidate)
return ret
# Explain methods
# ------------------------
def _explain_lookups(self, parsed_lookups):
"""Explain list of lookups"""
table = PrettyTable()
for item in parsed_lookups:
col1 = json.dumps(
{k: v for k, v in item.items() if k not in ["_run"]},
default=lambda o: "<not serializable>",
indent=2,
)
col2 = json.dumps(
item["_run"], default=lambda o: "<not serializable>", indent=2
)
table.add_row(
[
"\nConfig:" + str_ellipsis(col1, 60),
"\nRuntime:" + str_ellipsis(col2, 60),
]
)
table.field_names = ["Config", "Runtime"]
table.align = "l"
tracer.info("Explain lookups:\n%s", str(table))
def _explain_candidates(self, candidates, query):
"""Explain list of candidates"""
# TOFIX: query is not needed here !
table = PrettyTable()
for item_obj in candidates:
item = item_obj.__dict__
item["rel_path"] = str(Path(item["path"]).relative_to(Path.cwd()))
col1 = json.dumps(
{k: v for k, v in item.items() if k not in ["run", "data"]},
default=lambda o: "<not serializable>",
indent=2,
)
col2 = json.dumps(
item["run"]["_run"], default=lambda o: "<not serializable>", indent=2
)
col3 = (
item_obj.data.get(query.key, "NOT FOUND")
if query.key is not None and isinstance(item_obj.data, dict)
else item_obj.data
)
col3 = json.dumps(col3, default=lambda o: "<not serializable>", indent=2)
table.add_row(
[
"\nStatus:" + str_ellipsis(col1, 80),
"\nRuntime:" + str_ellipsis(col2, 60),
"\nKey:" + str_ellipsis(col3, 60),
]
)
table.field_names = ["Status", "Runtime", "Key Value"]
table.align = "l"
tracer.info("Explain candidates:\n%s", str(table))

View File

@ -2,40 +2,75 @@
import os import os
import logging import logging
from pathlib import Path # from pprint import pprint
import anyconfig import anyconfig
from anyconfig.common.errors import BaseError as AnyConfigBaseError
from kheops.utils import render_template, glob_files, render_template_python
from kheops.plugin.common import BackendPlugin, BackendCandidate from kheops.plugin.common import BackendPlugin, BackendCandidate
from pprint import pprint
log = logging.getLogger(__name__) 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):
class Plugin(BackendPlugin): class Plugin(BackendPlugin):
"""Generic Plugin Class""" """File Backend Plugin
This backend allows to lookup data into a file hierarchy. All files can be one of the
cupported by the anyconfig python library.
"""
plugin_name = "file"
extensions = {
".yml": "yaml",
".yaml": "yaml",
#".toml": "toml",
#".ini": "ini",
#".json": "json",
}
_schema_config = {
"backend_file": {
"title": "File Backend",
"description": "This backend will look for data inside a file hierarchy.",
"type": "object",
"properties": {
"extensions": {
"title": "File formats",
"description": """
This object describe which parser is assigned to which extension.
Adding more format will have a performance impact because it will try
to find all of the specified format. It is better to keep this list as small
as possible.
""",
"type": "object",
"default": extensions,
"additionalProperties": {
"title": "Name of the extension with parser",
"type": "string"
}
},
"path_prefix": {
"title": "Prefix string to append to final path",
"description": """
String to be added at the end of the resolved path. This is useful to change
the place of the root hierarchy.
""",
"type": "string"
},
"path_suffix": {
"title": "Suffix string to prepend to final path",
"description": """
String to be added at the end of the resolved path. This is useful to
provide Hiera or Jerakia support.""",
"type": "string",
"examples": [
{ "path_suffix": "/ansible" },
]
},
}
}
}
_plugin_name = "file"
_plugin_engine = "file"
# _schema_props_files = {
_schema_props_new = { _schema_props_new = {
"path": { "path": {
"anyOf": [ "anyOf": [
@ -66,19 +101,17 @@ class Plugin(BackendPlugin):
}, },
} }
extensions = {".yml": "yaml", ".yaml": "yaml"}
def _init(self): def _init(self):
# Guess top path # Build file prefix
top_path = self.ns.run["path_config"] top_path = self.ns.run["path_config"]
path_prefix = self.ns.config["config"].get("file_path_prefix", None) path_prefix = self.config.get("path_prefix", None)
if path_prefix: if path_prefix:
top_path = os.path.join(top_path, path_prefix) top_path = os.path.join(top_path, path_prefix)
self.top_path = top_path self.top_path = top_path
# Fetch module config # Build file sufix
path_suffix = self.ns.config["config"].get("file_path_suffix", "auto") path_suffix = self.config.get("path_suffix", "")
if path_suffix == "auto": if path_suffix == "auto":
path_suffix = f"/{self.ns.name}" path_suffix = f"/{self.ns.name}"
self.path_suffix = path_suffix self.path_suffix = path_suffix
@ -91,18 +124,24 @@ class Plugin(BackendPlugin):
raw_data = None raw_data = None
status = "not_found" status = "not_found"
for ext, parser in self.extensions.items(): extensions = self.config.get("extensions", self.extensions)
for ext, parser in extensions.items():
new_path = os.path.join(self.top_path, path + ext) new_path = os.path.join(self.top_path, path + ext)
log.debug("Looking into %s", new_path)
if os.path.isfile(new_path): if os.path.isfile(new_path):
status = "found" status = "found"
try: try:
log.info("Found file: %s", new_path)
raw_data = anyconfig.load(new_path, ac_parser=parser) raw_data = anyconfig.load(new_path, ac_parser=parser)
except Exception: except AnyConfigBaseError as err:
status = "broken" status = "broken"
raw_data = None raw_data = None
log.warning("Could not parse file %s: %s", new_path, err)
# Stop the loop extension if we found a result.
break break
log.debug("Skip absent file: %s", new_path)
ret = BackendCandidate( ret = BackendCandidate(
path=new_path, path=new_path,
status=status, status=status,

View File

@ -19,13 +19,24 @@ log = logging.getLogger(__name__)
# BackendPlugin[1] # BackendPlugin[1]
# Generic classes # Generic Plugin classes
# -------------------------
class KheopsPlugin: class KheopsPlugin:
plugin_name = None plugin_name = None
plugin_type = None plugin_type = None
plugin_kind = None plugin_kind = None
def __init__(self): def __init__(self):
assert isinstance(self.plugin_name, str), f"Missing name attribute in plugin: {self.__class__}"
assert isinstance(self.plugin_kind, str)
config_key = f"{self.plugin_kind}_{self.plugin_name}"
self.config = self.ns.config["config"].get(config_key, {})
self.config_key = config_key
#if self.config:
# log.debug("Load plugin configuration in config with key '%s', got: %s", config_key, self.config)
self._init() self._init()
def _init(self): def _init(self):
@ -47,20 +58,60 @@ class KheopsItemPlugin(KheopsPlugin):
pass pass
# Other classes # Plugin classes
class BackendCandidate: # -------------------------
def __init__(self, path=None, data=None, run=None, status=None):
assert isinstance(run, dict)
self.path = path
self.status = status or "unparsed"
self.run = run or {}
self.data = data or None
def __repr__(self): class BackendPlugin(KheopsItemPlugin):
return f"Status: {self.status}, Path: {self.path} => {self.data}" plugin_kind = "backend"
schema_prop = {
"backend": {}, # GENERIC, String
"file": {},
"glob": {},
"http": {},
"consul": {},
"vault": {},
}
def fetch_data(self, config) -> list:
raise Exception("Not implemented")
def __init__(self, namespace):
self.ns = namespace
super().__init__()
class StrategyPlugin(KheopsItemPlugin):
plugin_kind = "strategy"
schema_prop = {
"_strategy": {}, # GENERIC, String
"merge": {},
"first": {},
"last": {},
"smart": {},
"schema": {},
}
def merge_results(self, candidates, rule) -> list:
pass
def __init__(self, namespace):
self.ns = namespace
super().__init__()
class OutPlugin(KheopsItemPlugin):
plugin_kind = "out"
schema_prop = {
"_out": {}, # GENERIC, List of dict
"toml": {},
"validate": {},
}
def process_item(self, item) -> list:
pass
# Specific classes
class ConfPlugin(KheopsListPlugin): class ConfPlugin(KheopsListPlugin):
plugin_kind = "conf" plugin_kind = "conf"
schema_prop = { schema_prop = {
@ -89,6 +140,22 @@ class ScopePlugin(KheopsListPlugin):
self.ns = namespace self.ns = namespace
super().__init__() super().__init__()
# Helper classes
# -------------------------
class BackendCandidate():
"""Represent a backend candidate"""
def __init__(self, path=None, data=None, run=None, status=None):
assert isinstance(run, dict)
self.path = path
self.status = status or "unparsed"
self.run = run or {}
self.data = data or None
def __repr__(self):
return f"Status: {self.status}, Path: {self.path} => {self.data}"
class ScopeExtLoop: class ScopeExtLoop:
"""This Scope Extension allow to loop over a lookup""" """This Scope Extension allow to loop over a lookup"""
@ -165,55 +232,9 @@ class ScopeExtLoop:
return ret return ret
class BackendPlugin(KheopsItemPlugin):
plugin_kind = "backend"
schema_prop = {
"backend": {}, # GENERIC, String
"file": {},
"glob": {},
"http": {},
"consul": {},
"vault": {},
}
def fetch_data(self, lookups) -> list:
raise Exception("Not implemented")
def __init__(self, namespace):
self.ns = namespace
super().__init__()
class StrategyPlugin(KheopsItemPlugin): # To clean/implement
plugin_kind = "strategy"
schema_prop = {
"_strategy": {}, # GENERIC, String
"merge": {},
"first": {},
"last": {},
"smart": {},
"schema": {},
}
def merge_results(self, candidates, rule) -> list:
pass
def __init__(self, namespace):
self.ns = namespace
super().__init__()
class OutPlugin(KheopsItemPlugin):
plugin_kind = "out"
schema_prop = {
"_out": {}, # GENERIC, List of dict
"toml": {},
"validate": {},
}
def process_item(self, item) -> list:
pass
# # Candidate Classes # # Candidate Classes

View File

@ -1,20 +1,18 @@
"""Hierarchy backend plugin""" """Hierarchy backend plugin"""
import logging import logging
#from pprint import pprint
from kheops.plugin.common import ScopePlugin, ScopeExtLoop from kheops.plugin.common import ScopePlugin, ScopeExtLoop
from kheops.utils import path_assemble_hier from kheops.utils import path_assemble_hier
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from pprint import pprint
class Plugin(ScopePlugin, ScopeExtLoop): class Plugin(ScopePlugin, ScopeExtLoop):
"""Hierarchy plugin""" """Hierarchy plugin"""
_plugin_name = "hier" plugin_name = "hier"
_schema_props_new = { _schema_props_new = {
"hier": { "hier": {
"default": None, "default": None,

View File

@ -10,9 +10,9 @@ log = logging.getLogger(__name__)
class Plugin(ScopePlugin, ScopeExtLoop): class Plugin(ScopePlugin, ScopeExtLoop):
"""Hierarchy plugin""" """Loop plugin"""
_plugin_name = "hier" plugin_name = "loop"
_schema_props_new = { _schema_props_new = {
"hier": { "hier": {
"default": None, "default": None,

View File

@ -15,7 +15,7 @@ class Plugin(StrategyPlugin):
"""Last strategy plugin""" """Last strategy plugin"""
_plugin_name = "merge_schema" plugin_name = "merge_schema"
_schema_props_new = None _schema_props_new = None
selector = "matched" selector = "matched"

View File

@ -90,30 +90,21 @@ def render_template_python(text, params, ignore_missing=True):
# Schema Methods # Schema Methods
# ===================== # =====================
def _extend_with_default(validator_class): def _extend_with_default(validator_class):
validate_properties = validator_class.VALIDATORS["properties"] validate_properties = validator_class.VALIDATORS["properties"]
def set_defaults(validator, properties, instance, schema): def set_defaults(validator, properties, instance, schema):
for property, subschema in properties.items():
for prop, subschema in properties.items():
if "default" in subschema: if "default" in subschema:
instance.setdefault(prop, subschema["default"]) instance.setdefault(property, subschema["default"])
try:
for error in validate_properties( for error in validate_properties(
validator, validator, properties, instance, schema,
properties,
instance,
schema,
): ):
continue yield error
except Exception as err:
log.debug("Jsonschema validation error: %s", err)
return validators.extend( return validators.extend(
validator_class, validator_class, {"properties" : set_defaults},
{"properties": set_defaults},
) )

625
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
[tool.poetry] [tool.poetry]
name = "kheops" name = "kheops"
version = "0.1.0" version = "0.2.0"
description = "Data trees for Ansible" description = "Hierarchical key/value store"
authors = ["Robin Cordier"] authors = ["Robin Cordier"]
license = "GNU" license = "Apache License"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
@ -16,6 +16,7 @@ dpath = "^2.0.5"
diskcache = "^5.4.0" diskcache = "^5.4.0"
mergedeep = "^1.3.4" mergedeep = "^1.3.4"
typer = "^0.4.0" typer = "^0.4.0"
Jinja2 = "^3.0.3"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
json-schema-for-humans = "^0.40" json-schema-for-humans = "^0.40"
@ -29,6 +30,10 @@ bash_kernel = "^0.7.2"
jupyter = "^1.0.0" jupyter = "^1.0.0"
pandoc = "^2.0.1" pandoc = "^2.0.1"
pytest = "^6.2.5" pytest = "^6.2.5"
pytest-icdiff = "^0.5"
pytest-instafail = "^0.4.2"
pytest-cov = "^3.0.0"
sphinx-rtd-theme = "^1.0.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]