Code Monkey home page Code Monkey logo

salt-tower's Introduction

Salt Tower (Logo)


Salt Tower โ€” A Flexible External Pillar Module

GitHub Workflow Status

Salt Tower is an advanced and flexible ext_pillar that gives access to pillar values while processing and merging them, can render all usual salt file formats and include private and binary files for a minion.

Salt Tower is inspired by pillarstack for merging pillar files and giving access to them. It also has a top file like salt itself and utilizes salt renderers to supports all formats such as YAML, Jinja, Python and any combination. Supercharged renderers for plain text and YAML are included too.

Each tower data file is passed the current processed pillars. They can therefore access previously defined values. Data files can include other files that are all merged together.

Salt Tower is designed to completely replace the usual pillar repository or can be utilized beside salts original pillar that e.g. can bootstrap a salt master with Salt Tower.

Questions or Need Help?

See examples. They each have their own README further explaining the given example.

There is a group and mailing list. You can join the group here or by sending a subscribe-email.

Feel free to ask for help, discuss solutions or ideas there. Otherwise, you can open an issue.

Installation

GitFS

You can include this repository as a gitfs root and synchronize the extensions on the master:

gitfs_remotes:
- https://github.com/jgraichen/salt-tower.git:
  - base: v1.12.0

Sync all modules:

$ salt-run saltutil.sync_all
pillar:
    - pillar.tower
renderers:
    - renderers.filter
    - renderers.text
    - renderers.yamlet

Please note that everything in this repository would be merged with your other roots.

pip

pip install salt-tower

Manual installation

Install the extension files from the salt_tower/{pillar,renderers} directories into the extension_modules directory configured in salt.

Configuration

Salt Tower is configured as an ext_pillar:

ext_pillar:
  - tower: /path/to/tower.sls

Top File

The tower.sls file is similar to the usual top.sls with some important differences.

Ordered matchers

Pillar top items are ordered and processed in order of appearance. You can therefore define identical matchers multiple times.

# tower.sls
base:
  - '*':
      - first

  - '*':
      - second

Common includes

You do not need to define a matcher at all, the files will be included for all minions. Furthermore, you also can use globs to match multiple files, e.g. include all files from common/.

base:
  - common/*

Grains

The top file itself is rendered using the default renderer (yaml|jinja). Therefore, you can use e.g. grains to include specific files.

base:
  - common/*
  - dist/{{ grains['oscodename'] }}

Embedded data

You can directly include pillar data into the top file simply be defining a dict item.

base:
  - '*.a.example.org':
      - site:
          id: a
          name: A Site

Iterative pillar processing

All matchers are compound matchers by default. As items are processes in order of appearance, later items can patch on previously defined pillar values. The above example includes application.sls for any minion matching *.a.example.org simply because it defines a site pillar value.

base:
  - '*.a.example.org':
      - site: {id: a, name: A Site}

  - 'I@site:*':
      - applications

Late-bound variable replacement

File includes are preprocessed by a string formatter to late-bind pillar values.

base:
  - '*.a.example.org':
      - site: {id: a, env: production}

  - '*.a-staging.example.org':
      - site: {id: a, env: staging}

  - 'I@site:*':
      - site/default
      - site/{site.id}
      - site/{site.id}/{site.env}/*

In the above example a minion node0.a-staging.example.org will include the following files:

site/default
site/a
site/a/staging/*

File lookup

File names will be matches to files and directories, e.g. when including path/to/file the first existing match will be used:

path/to/file
path/to/file.sls
path/to/file/init.sls

Tower Data File

A data file is processed like a usual pillar file. Rendering uses salts template engines therefore all usual features should be available.

The injected pillar objects can be used to access previously defined values. The additional .get method allows to traverse the pillar tree.

application:
  title: Site of {{ pillar.get('tenant:name') }}

Note: Using salt['pillar.get']() will not work.

Tower data files can be any supported template format including python files:

#!py

def run():
    ret = {'databases': []}

    for app in __pillar__['application']:
        ret['databases'].append({
            'name': '{0}-{1}'.format(app['name'], app['env'])
        })

    return ret

Includes

Pillar data files can include other pillar files similar to how states can be included:

include:
  - another/pillar

data: more

Included files cannot be used in the pillar data file template itself but are merged in the pillar before the new pillar data. Includes can be relative to the current file by prefixing a dot:

include:
  - file/from/pillar/root.sls
  - ./adjacent_file.sls
  - ../parent_file.sls

Yamlet renderer

The Yamlet renderer is an improved YAML renderer that supports loading other files and rendering templates:

ssh_private_key: !read id_rsa
ssh_public_key: !read id_rsa.pub

This reads a file from the pillar directory in plain text or binary and embeds it into the pillar to e.g. ease shipping private file blobs to minions.

Using the !include tag files can be pushed through salts rendering pipeline on the server:

nginx:
  sites:
    my-app: !include ../files/site.conf
#!jinja | text strip
server {
  listen {{ pillar.get('my-app:ip') }}:80;
  root /var/www/my-app;
}

The pillar will return the following:

nginx:
  sites:
    my-app: |
      server {
        listen 127.0.0.1:80;
        root /var/www/my-app;
      }

This can greatly simplify states as they only need to drop pillar values into config files and restart services:

nginx:
  pkg.installed: []
  service.running: []

{% for name in pillar.get('nginx:sites', {}) %}
/etc/nginx/sites-enabled/{{ name }}:
  file.managed:
    - contents_pillar: nginx:sites:{{ name }}
    - makedirs: True
    - watch_in:
      - service: nginx
{% endfor %}

The yamlet renderer !include macro does accept context variables too:

nginx:
  sites:
    my-app: !include
      source: ../files/site.conf
      context:
        listen_ip: 127.0.0.1
#!jinja | text strip
server {
  listen {{ listen_ip }}:80;
  root /var/www/my-app;
}

Text renderer

The text renderer (used above) renders a file as plain text. It stripes the shebang and can optionally strip whitespace from the beginning and end.

#!text strip

Hello World

This will return:

Hello World

The text renderer is mostly used for embedding rendered configuration files into a Yamlet file.

Filter renderer

The filter renderer returns only a subset of data that matches a given grain or pillar key value:

#!yamlet | filter grain=os_family default='Unknown OS'

Debian:
  package_source: apt

RedHat:
  package_source: rpm

Unknown OS:
  package_source: unknown

When this file is rendered, only the data from the matching top level key is returned. The renderer supports glob matches and uses the minion ID by default:

#!yamlet | filter

minion-1:
  monitoring:
    type: ping
    address: 10.0.0.1

webserver-*:
  monitoring:
    type: http
    address: http://example.org

Advanced usage (very dangerous)

The pillar object passed to the python template engine is the actual mutable dict reference used to process and merge the data. It is possible to modify this dict e.g. in a python template without returning anything:

#!py

import copy

def run():
    databases = __pillar__['databases']
    default = databases.pop('default') # Deletes from actual pillar

    for name, config in databases.items():
        databases[name] = dict(default, **config)

    return {}

Note 1: Do not return None. Otherwise, Salt will render the template twice and all side effects will be applied twice.

Note 2: The __pillar__ object in Python templates is different to other template engines. It is a dict and does not allow traversing using get.

#!py

def run():
    return {
        'wrong': __pilar__.get('tenant:name'),
        'python': __pillar__['tenant']['name'],
        'alternative': tower.get('tenant:name')
    }

The above example demonstrates different usages. The first example will only work if the pillar contains an actual tenant:name top-level key. The second example is idiomatic-python but will raise an error if the keys do not exist. The third example uses the additional tower helper module to traverse the pillar data.

The tower pillar object is available in all rendering engines and can be used for low-level interaction with the ext_pillar engine. Some available functions are:

tower.get(key, default=None, require=False)

Get a pillar value by given traverse path:

tower.get('my:pillar:key')

If require=True is set, default will be ignored and a KeyError will be raised if the pillar key is not found.

tower.update(dict)

Merges given dictionary into the pillar data.

tower.update({'my': {'pillar': 'data'}})

assert tower.get('my:pillar') == 'data'

tower.merge(tgt, *objects)

Merges given dictionaries or lists into the first one.

Note: The first given dictionary or list is mutated and returned.

tgt = {}

ret = tower.merge(tgt, {'a': 1})

assert ret is tgt
assert tgt['a'] == 1

tower.format(obj, *args, **kwargs)

Performs recursive late-bind string formatting using tower pillar and given arguments ad keywords for resolving. Uses string.Formatter internally.

tower.update({
    'database': {
        'password': 'secret'
    }
})

ret = tower.format('postgres://user@{database.password}/db')

assert ret == 'postgres://user@secret/db'

Format accept dictionaries and list as well and can therefore be used to format full or partial pillar data, this can be used to e.g. format defaults with extra variables:

#!py

def run():
    returns = {}
    defaults = __pillar__['default_app_config']
    # e.g. {
    #        'database': 'sqlite:///opt/{name}.sqlite'
    #        'listen': '0.0.0.0:{app.port}'
    # }

    for name, conf in __pillar__['applications'].items():
        # Merge defaults with conf into new dictionary
        conf = tower.merge({}, defaults, conf)

        # Format late-bind defaults with application config
        conf = tower.format(conf, name=name, app=conf)

        returns[name] = conf

    return {'applications': returns}

salt-tower's People

Contributors

ixs avatar jgraichen avatar raddessi avatar renovate-bot avatar renovate[bot] avatar zixo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

salt-tower's Issues

how to debug errors

how to deal with errors like:

Error encountered while rendering pillar top file

i known that i somewhere have error, but how to find it?

Late-bind variable interpolation via YAML tag

Late-bind variable interpolation should be marked in the YAML file and finally handled by the tower e.g.:

# jinja | yamlet

data:
  var: 5
  result: !format "some late bound key with {data.val}"

This is much safer than late bound formatting the whole pillar but requires extensive interaction between the yamlet renderer and the salt tower.

Configure salt environments directly in ext_pillar configuration

The tower pillar should be configurable to load a specific top file based on the salt environment, e.g.:

ext_pillar:
- tower: 
    salt:env:
      base: /srv/salt/pillar/tower.sls
      staging: /src/salt/pillar-staging/tower.sls

This way one can have different pillars directories/checkouts for different environments and easily test changes on actual minions by passing the e.g. staging environment to a highstate.

feature plans

thanks for such project, i'm fan of pillarstack (i'm slightly rewrite it for my needs and also fixup top.sls file to have equal directory structure (i have /projects /systems (debian, fedora and so), environment, nodes and inside it some dirs for roles like dns-server mail-server and so).
Main drawback that this can't work in default salt setup, most of the people that see my use-case says, that i'm need to use reclass.
What you can say about reclass? Does it possible to replicate with it something like pillarstack and salt-tower?
Also do you plan maintain this project and how long? (this is long term question, because if you drop it other may not pick up it...)

Imports happen relative to state dir rather than pillar?

Hi, first off I want to say thank you for this project! It adds functionality to salt that is sorely needed. I'm starting to migrate my projects over right now.

Describe the bug
I have a few states that import gpg file contents from files outside the pillars just to keep the pillars clean using syntax like {%- import_yaml './bar.sls' as bar_pillar %}, but it seems that when using tower salt ends up looking for the files under the state directory rather than the pillar directory. Is this intended?

To Reproduce
Create a pillar foo.sls with:

{%- import_yaml './bar.sls' as bar_pillar %}
foo: {{ bar_pillar.bar }}

And the corresponding pillar file bar.sls in the same directory:

bar: baz

then add foo to the list of included pillars in the tower top file.
The master log should show messages about the file bar.sls not being found. If you move that file to the state dir it should be found however.

Expected behavior
Standard salt imports in pillars do happen relative to the directory of the pillar file being rendered, it would be ideal if this followed the same behavior.

Environment information:

  • Salt version: 3002.5
  • Python version 3.8
  • Operating system Fedora 32

Additional context
NA

Late-bound variable replacement

Hi,

Just found this project and it looks very promising.
Could you please clarify the "Late-bound variable replacement" feature? So it's basically the same idea as the stack in pillarstack so you can re-use pillar data from the previously included pillar?

What I'm really looking for is a lazy variable interpolation, so I could do something like this:

var1: test
var2: ${var1}

In a single pillar file.
It's implemented in saltclass and reclass but they are too complex and require a really weird approach like defining a class for each minion.

Missing tower data file should be error or option

salt-tower logs a warning for every missing tower data file. This is noisy due to missing data file based on generic matches like arch/{{ pillar['arch'].sls or minion/{{ grains['id'] }}.sls.

It should be possible to mark includes as optional (so no warning is logged). Missing required data files should result in a noticeable error.

tower.sls rendering ignores env

tower.sls env ignored

I am not able to use salt-tower with multiple environments.

To Reproduce

salt-master configuration

/etc/salt/master.d/f_defaults.conf:

...
gitfs_remotes:
  - https://github.com/jgraichen/salt-tower.git:
    - all_saltenvs: v1.10.0
ext_pillar:
  - tower: /srv/salt/towers/tower.sls
...

salt-minion configuration

$ salt salt-master config.get salt:minion
salt-master:
    ----------
    http_connect_timeout:
        60
    lock_saltenv:
        True
    pillarenv:
        saltmaster
    saltenv:
        saltmaster
    state_top_saltenv:
        saltmaster

Tower .sls

/srv/salt/towers/tower.sls

saltmaster:
  - '*':
    - magic: saltmaster_8ed104d8-dced-4d73-8816-0da95e74ab21

Output

$ salt salt-master pillar.get magic -ldebug
...
[DEBUG   ] Rendered data from file: /srv/salt/towers/tower.sls:
...
saltmaster:
  - '*':
    - magic: saltmaster_8ed104d8-dced-4d73-8816-0da95e74ab21
...
salt-master:

I get pillar corectly render only if I use base.

Expected behavior
Access pillar data per env.

Environment information:

  • Salt version: [3004.2]
  • Python version [3.7.15]
  • Operating system [Amazon Linux 2]

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Awaiting Schedule

These updates are awaiting their schedule. Click on a checkbox to get an update now.

  • chore(deps): update dependency dev/ruff to v0.3.5
  • chore(deps): update dependency docs/mkdocs-material to v9.5.17
  • chore(deps): lock file maintenance

Detected dependencies

github-actions
.github/workflows/docs.yml
  • actions/checkout v4
  • ubuntu 22.04
.github/workflows/test.yml
  • actions/checkout v4
  • pdm-project/setup-pdm v4
  • actions/checkout v4
  • pdm-project/setup-pdm v4
  • actions/checkout v4
  • pdm-project/setup-pdm v4
  • ubuntu 22.04
  • ubuntu 22.04
  • ubuntu 22.04
pep621
pyproject.toml
  • test/pylint ==3.1.0
  • test/pytest ==8.1.1
  • docs/mike ==2.0.0
  • docs/mkdocs-awesome-pages-plugin ==2.9.2
  • docs/mkdocs-git-revision-date-plugin ==0.3.2
  • docs/mkdocs-material ==9.5.16
  • pdm-pep517 >=1.0
  • dev/mypy ==1.9.0
  • dev/pylint ==3.1.0
  • dev/pyright >=1.1.356
  • dev/pytest ==8.1.1
  • dev/ruff ==0.3.4
  • dev/tox ==4.14.2

  • Check this box to trigger a request for Renovate to run again on this repository

when using with salt-ssh, tower runs twice and final pillar data gets wrong

Describe the bug
When used with salt-ssh to run a state, I end up with wrong data - tower is run twice, and due to merge strategies for arrays (I think), I get doubled data. If running just salt-ssh pillar.items I get the correct data.

To Reproduce
Steps to reproduce the behavior:

  1. Salt master / ext pillar configuration
    nothing out of the ordinary
  2. Broken component e.g. tower ext_pillar or yamlet renderer
    salt-ssh and/or tower (salt-ssh is known for behaving wrong sometimes)
  3. Demonstrating pillar data files, tower.sls, etc.
  4. Minion configuration (grains, os, ...)
  5. Pillar output
    can't give the output.

Expected behavior
Expecting the pillar data to be correct (tower running only once) when running an actual state via salt-ssh

Environment information:

  • Salt version: 3002.2
  • Python version: 3.9
  • Operating system Manjaro (Arch based)

Additional context
I added a print stack trace in tower/pillar/...: ext_pillar() - before doing the computation, and it looks like tower is run twice:

  1. probably before doing the actual ssh - to create pillar cache:
  File "/usr/lib/python3.9/site-packages/salt/client/ssh/__init__.py", line 1079, in run
    stdout, retcode = self.run_wfunc()
  File "/usr/lib/python3.9/site-packages/salt/client/ssh/__init__.py", line 1171, in run_wfunc
    pillar_data = pillar.compile_pillar()
  File "/usr/lib/python3.9/site-packages/salt/pillar/__init__.py", line 1187, in compile_pillar
  1. at start of state run, to ... build the pillar:
  File "/usr/lib/python3.9/site-packages/salt/client/ssh/__init__.py", line 1079, in run
    stdout, retcode = self.run_wfunc()
  File "/usr/lib/python3.9/site-packages/salt/client/ssh/__init__.py", line 1248, in run_wfunc
    result = self.wfuncs[self.fun](*self.args, **self.kwargs)
  File "/usr/lib/python3.9/site-packages/salt/client/ssh/wrapper/state.py", line 174, in sls
    st_ = salt.client.ssh.state.SSHHighState(
  File "/usr/lib/python3.9/site-packages/salt/client/ssh/state.py", line 82, in __init__
    self.state = SSHState(opts, pillar, wrapper)
  File "/usr/lib/python3.9/site-packages/salt/client/ssh/state.py", line 44, in __init__
    super(SSHState, self).__init__(opts, pillar)
  File "/usr/lib/python3.9/site-packages/salt/state.py", line 760, in __init__
    self.opts["pillar"] = self._gather_pillar()
  File "/usr/lib/python3.9/site-packages/salt/state.py", line 825, in _gather_pillar
    return pillar.compile_pillar()
  File "/usr/lib/python3.9/site-packages/salt/pillar/__init__.py", line 1187, in compile_pillar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.