Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(map): use targeting like syntax for configuration #191

Conversation

baby-gnu
Copy link
Contributor

@baby-gnu baby-gnu commented Aug 25, 2020

PR progress checklist (to be filled in by reviewers)

  • Changes to documentation are appropriate (or tick if not required)
  • Changes to tests are appropriate (or tick if not required)
  • Reviews completed

What type of PR is this?

Primary type

  • [build] Changes related to the build system
  • [chore] Changes to the build process or auxiliary tools and libraries such as documentation generation
  • [ci] Changes to the continuous integration configuration
  • [feat] A new feature
  • [fix] A bug fix
  • [perf] A code change that improves performance
  • [refactor] A code change that neither fixes a bug nor adds a feature
  • [revert] A change used to revert a previous commit
  • [style] Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.)

Secondary type

  • [docs] Documentation changes
  • [test] Adding missing or correcting existing tests

Does this PR introduce a BREAKING CHANGE?

Yes.

BREAKING CHANGE: the configuration map_jinja:sources is only configurable with salt://parameters/map_jinja.yaml and salt://{{ tplroot }}/parameters/map_jinja.yaml

BREAKING CHANGE: the map_jinja:config_get_roots is replaced by compound like map_jinja:sources

BREAKING CHANGE: the two map_jinja:sources config_get_lookup and config_get are replaced by C@<tplroot>:lookup and C@<tplroot> sources

Related issues and/or pull requests

#186

Describe the changes you're proposing

The config_get_lookup and config_get sources lack flexibility.

It's not easy to query several pillars and/or grains keys with the actual system. And the query method is forced to config.get without being configurable by the user.

We define a mechanism to select map.jinja sources with similar notation as the salt targeting system.

Each source has a type:

  • Y to load values from YAML files, this is the default when no type is defined
  • C to lookup values with config.get
  • G to lookup values with grains.get
  • I to lookup values with pillar.get

The YAML type can define the query method to lookup the key value to build the file name:

  • C to query with config.get, this is the default when to query method is defined
  • G to query with grains.get
  • I to query with pillar.get

The C, G or I types can define the SUB attribute to merge values in the sub key mapdata.<key> instead of directly in mapdata.

BREAKING CHANGE: the configuration map_jinja:sources is only configurable with salt://parameters/map_jinja.yaml and salt://{{ tplroot }}/parameters/map_jinja.yaml

BREAKING CHANGE: the map_jinja:config_get_roots is replaced by compound like map_jinja:sources

BREAKING CHANGE: the two map_jinja:sources config_get_lookup and config_get are replaced by C@<tplroot>:lookup and C@<tplroot> sources

Users can now define map.jinja sources like:

values:
  map_jinja:
    sources:
      # By default, when no `@` sign is present, it's `Y:C@` for YAML files with key lookup using `config.get`
      - "osarch"
      - "os_family"
      - "os"
      - "osfinger"

      # Use `:SUB` to merge values from `config.get` under `mapdata.<key>` to keep
      # compatibility with user pillars.
      # The `<key>` and `<key>:lookup` are merged together
      - "C:SUB@openssh:lookup"
      - "C:SUB@openssh"
      - "C:SUB@sshd_config:lookup"
      - "C:SUB@sshd_config"
      - "C:SUB@ssh_config:lookup"
      - "C:SUB@ssh_config"

      # Lookup role based YAML files with role names from pillars only
      - "Y:I@roles"

      # Lookup DNS domain name based YAML files with DNS domain names from grains only
      - "Y:G@dns:domain"

      - "id"

The map.jinja configuration are looked-up from:

  • salt://parameters/map_jinja.yaml to define it globally for all formulas
  • salt://{{ tplroot }}/parameters/map_jinja.yaml to define it per formula

The map_jinja.yaml file can contains Jinja constructs like the use of {{ tplroot }}:

values:
  map_jinja:
    sources:
      # default values
      - "osarch"
      - "os_family"
      - "os"
      - "osfinger"
      - "C@{{ tplroot ~ ':lookup' }}"
      - "C@{{ tplroot }}"
  
      # role based configuration
      - "roles"

      # DNS domain configured (DHCP or resolv.conf)
      - "dns:domain"
  
      # Based on minion ID
      - "domain"
  
      # default values
      - "id"

Pillar / config required to test the proposed changes

Included in the commit.

Debug log showing how the proposed changes work

[DEBUG   ] In saltenv 'base', looking at rel_path 'openssh/map.jinja' to resolve 'salt://openssh/map.jinja'
[DEBUG   ] In saltenv 'base', ** considering ** path '/tmp/kitchen/var/cache/salt/minion/files/base/openssh/map.jinja' to resolve 'salt://openssh/map.jinja'
[DEBUG   ] In saltenv 'base', looking at rel_path 'openssh/libsaltcli.jinja' to resolve 'salt://openssh/libsaltcli.jinja'
[DEBUG   ] In saltenv 'base', ** considering ** path '/tmp/kitchen/var/cache/salt/minion/files/base/openssh/libsaltcli.jinja' to resolve 'salt://openssh/libsaltcli.jinja'
[DEBUG   ] [libsaltcli] the salt command type has been identified to be: local
[DEBUG   ] map.jinja: built-in configuration sources:
values:
  map_jinja:
    sources:
    - osarch
    - os_family
    - os
    - osfinger
    - C@openssh:lookup
    - C@openssh
    - id
[DEBUG   ] map.jinja: load global map.jinja values from parameters/map_jinja.yaml
[DEBUG   ] Could not find file 'salt://parameters/map_jinja.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: load per formula map.jinja values from openssh/parameters/map_jinja.yaml
[DEBUG   ] In saltenv 'base', looking at rel_path 'openssh/parameters/map_jinja.yaml' to resolve 'salt://openssh/parameters/map_jinja.yaml'
[DEBUG   ] In saltenv 'base', ** considering ** path '/tmp/kitchen/var/cache/salt/minion/files/base/openssh/parameters/map_jinja.yaml' to resolve 'salt://openssh/parameters/map_jinja.yaml'
[DEBUG   ] map.jinja: configure sources from formula map.jinja configuration openssh/parameters/map_jinja.yaml:
values:
  map_jinja:
    sources:
    - osarch
    - os_family
    - os
    - osfinger
    - C:SUB@openssh:lookup
    - C:SUB@openssh
    - C:SUB@sshd_config:lookup
    - C:SUB@sshd_config
    - C:SUB@ssh_config:lookup
    - C:SUB@ssh_config
    - id
[DEBUG   ] map.jinja: load parameters from sources:
- osarch
- os_family
- os
- osfinger
- C:SUB@openssh:lookup
- C:SUB@openssh
- C:SUB@sshd_config:lookup
- C:SUB@sshd_config
- C:SUB@ssh_config:lookup
- C:SUB@ssh_config
- id
[DEBUG   ] map.jinja: load per formula default values from openssh/parameters/defaults.yaml
[DEBUG   ] In saltenv 'base', looking at rel_path 'openssh/parameters/defaults.yaml' to resolve 'salt://openssh/parameters/defaults.yaml'
[DEBUG   ] In saltenv 'base', ** considering ** path '/tmp/kitchen/var/cache/salt/minion/files/base/openssh/parameters/defaults.yaml' to resolve 'salt://openssh/parameters/defaults.yaml'
[DEBUG   ] map.jinja: lookup 'osarch' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file openssh/parameters/osarch/amd64.yaml
[DEBUG   ] Could not find file 'salt://openssh/parameters/osarch/amd64.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: lookup 'os_family' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file openssh/parameters/os_family/Debian.yaml
[DEBUG   ] In saltenv 'base', looking at rel_path 'openssh/parameters/os_family/Debian.yaml' to resolve 'salt://openssh/parameters/os_family/Debian.yaml'
[DEBUG   ] In saltenv 'base', ** considering ** path '/tmp/kitchen/var/cache/salt/minion/files/base/openssh/parameters/os_family/Debian.yaml' to resolve 'salt://openssh/parameters/os_family/Debian.yaml'
[DEBUG   ] map.jinja: merge parameters from openssh/parameters/os_family/Debian.yaml
[DEBUG   ] map.jinja: lookup 'os' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file openssh/parameters/os/Debian.yaml
[DEBUG   ] Could not find file 'salt://openssh/parameters/os/Debian.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: lookup 'osfinger' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file openssh/parameters/osfinger/Debian-10.yaml
[DEBUG   ] Could not find file 'salt://openssh/parameters/osfinger/Debian-10.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: retrieve 'openssh:lookup' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'openssh:lookup' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: retrieve 'openssh' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'openssh' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: retrieve 'sshd_config:lookup' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'sshd_config:lookup' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: retrieve 'sshd_config' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'sshd_config' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: retrieve 'ssh_config:lookup' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'ssh_config:lookup' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: retrieve 'ssh_config' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'ssh_config' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: lookup 'id' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file openssh/parameters/id/example.net.yaml
[DEBUG   ] Could not find file 'salt://openssh/parameters/id/example.net.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: save parameters in variable 'mapdata'

Documentation checklist

  • Updated the README (e.g. Available states).
  • Updated pillar.example.

Testing checklist

  • Included in Kitchen (i.e. under state_top).
  • Covered by new/existing tests (e.g. InSpec, Serverspec, etc.).
  • Updated the relevant test pillar.

Additional context

@pull-assistant
Copy link

pull-assistant bot commented Aug 25, 2020

Score: 1.00

Best reviewed: commit by commit


Optimal code review plan

     feat(map): use targeting like syntax for configuration

     docs(map): document the new map.jinja with targeting like syntax

Powered by Pull Assistant. Last update f33774a ... 4878ead. Read the comment docs.

@baby-gnu baby-gnu requested a review from a team August 25, 2020 10:36
@baby-gnu
Copy link
Contributor Author

There is one corner case for which I don't know what to do.

Users can query any value from pillars (using I@<key>), grains (using G@<key>), config (using C@<key>) but it can result in the error TypeError: Cannot update using non-dict types in dictupdate.update() if the returned value is not a dict, for example:

map_jinja:
  sources:
    # By default, when no `@` sign is present, it's `Y:C@` for YAML files with key lookup using `config.get`
    - "osarch"
    - "os_family"
    - "os"
    - "osfinger"

    # This will cause the error `TypeError: Cannot update using non-dict types in dictupdate.update()`
    - "G@saltversion"

    # Use `:SUB` to merge values from `config.get` under `mapdata.<key>` to keep
    # compatibility with user pillars.
    # The `<key>` and `<key>:lookup` are merged together
    - "C:SUB@openssh:lookup"
    - "C:SUB@openssh"
    - "C:SUB@sshd_config:lookup"
    - "C:SUB@sshd_config"
    - "C:SUB@ssh_config:lookup"
    - "C:SUB@ssh_config"

    # Lookup role based YAML files with role names from pillars only
    - "Y:I@roles"

    # Lookup DNS domain name based YAML files with DNS domain names from grains only
    - "Y:G@dns:domain"

    - "id"

I see two possibilities:

  1. ignore the key
  2. create mapdata[<key>] = <value>

Do you have any preference?

Thanks.

@n-rodriguez
Copy link
Member

Do you have any preference?

create mapdata[<key>] = <value>

what is the value in this case?

@baby-gnu baby-gnu removed the request for review from a team August 25, 2020 13:42
@baby-gnu
Copy link
Contributor Author

Do you have any preference?

create mapdata[<key>] = <value>

what is the value in this case?

in the example mapdata.saltversion == '3001.1'

@n-rodriguez
Copy link
Member

and mapdata in mapdata[<key>] = <value> is https://github.com/saltstack-formulas/openssh-formula/blob/master/openssh/map.jinja#L207 ?

well... it would lead to have dangling keys in mapdata? no?

@baby-gnu
Copy link
Contributor Author

and mapdata in mapdata[<key>] = <value> is https://github.com/saltstack-formulas/openssh-formula/blob/master/openssh/map.jinja#L207 ?

well... it would lead to have dangling keys in mapdata? no?

Ho no, the idea is to do the same as the sub key.

So, with the map_jinja:sources from previous example, the formula authors/contributors could do something like:

{%- set tplroot = tpldir.split('/')[0] %}
{%- from tplroot ~ "/map.jinja" import mapdata with context %}

{%- if mapdata.saltversion.startswith('3001') %}
{#- […] #}

@n-rodriguez
Copy link
Member

well... it would lead to have dangling keys in mapdata? no?

i meant dangling keys towards the pillars that are actually loaded 👍

saltversion: 3000.1
sshd_config:
  foo: bar

I don't know if it's a good idea...

@baby-gnu
Copy link
Contributor Author

well... it would lead to have dangling keys in mapdata? no?

i meant dangling keys towards the pillars that are actually loaded +1

saltversion: 3000.1
sshd_config:
  foo: bar

I don't know if it's a good idea...

Well, the load of C:SUB@sshd_config looks the same to me as G@saltversion except that C:SUB@sshd_config is a dict ;-)

@n-rodriguez
Copy link
Member

Well, the load of C:SUB@sshd_config looks the same to me as G@saltversion except that C:SUB@sshd_config is a dict ;-)

Well, the load of C:SUB@sshd_config looks the same to me as G@saltversion except that G@saltversion loads something completely different. But it seems to be the purpose of this PR.

except that C:SUB@sshd_config is a dict

I don't make the distinction on the value. No matter what the value is (a dict, a string, a bool, a null) you will store it in mapdata with mapdata[<key>] = <value>

@n-rodriguez
Copy link
Member

what bothers me is

saltversion: 3000.1
sshd_config:
  foo: bar

IMO saltversion has nothing to do here... but as I said it seems to be the purpose of this PR so why not.

I understand you want to do this :

{%- if mapdata.saltversion.startswith('3001') %}

but you can do it like this

{%- if salt['grains']['saltversion'].startswith('3001') %}

or

{%- if salt['grains'].get('saltversion').startswith('3001') %}

@baby-gnu
Copy link
Contributor Author

IMO saltversion has nothing to do here... but as I said it seems to be the purpose of this PR so why not.

Actually, the purpose of this PR is to have a single configuration for map.jinja by merging map_jinja:config_get_roots with map_jinja:sources. I remember that someone ask for something like the targeting notation to avoid having the special config_get_lookup and config_get in map_jinja:sources (but I don't remember the nickname).

I understand you want to do this :

{%- if mapdata.saltversion.startswith('3001') %}

but you can do it like this

{%- if salt['grains']['saltversion'].startswith('3001') %}

or

{%- if salt['grains'].get('saltversion').startswith('3001') %}

That's not really something I really want to do but a new possibility opened by this PR. It's just that I did some tests to see if C@, G@ and I@ were doing the proper thing and encounter the TypeError: Cannot update using non-dict types in dictupdate.update() when querying a random grain.

There are no many formulas with multiple variables exported by map.jinja, so this PR will be a little use in fact but I found amusing to do it and now I'm concerned about how to handle users requesting non dict grains.

@myii
Copy link
Member

myii commented Aug 25, 2020

Just some notes for the time being.

https://docs.saltstack.com/en/latest/topics/targeting/compound.html

Letter Match Type Example Alt Delimiter?
G Grains glob G@os:Ubuntu Yes
E PCRE Minion ID E@web\d+.(dev|qa|prod).loc No
P Grains PCRE P@os:(RedHat|Fedora|CentOS) Yes
L List of minions L@minion1.example.com,minion3.domain.com or bl*.domain.com No
I Pillar glob I@pdata:foobar Yes
J Pillar PCRE J@pdata:^(foo|bar)$ Yes
S Subnet/IP address S@192.168.1.0/24 or S@192.168.1.100 No
R Range cluster R@%foo.bar No
N Nodegroups N@group1 No

https://docs.saltstack.com/en/latest/faq.html#faq-grain-security

Is Targeting using Grain Data Secure?

Because grains can be set by users that have access to the minion configuration files on the local system, grains are considered less secure than other identifiers in Salt. Use caution when targeting sensitive operations or setting pillar values based on grain data.

The only grain which can be safely used is grains['id'] which contains the Minion ID.

When possible, you should target sensitive operations and data using the Minion ID. If the Minion ID of a system changes, the Salt Minion's public key must be re-accepted by an administrator on the Salt Master, making it less vulnerable to impersonation attacks.

  • This point isn't directly related to this PR but we really should consider encouraging sensitive pillar matching using the id only.

@baby-gnu
Copy link
Contributor Author

baby-gnu commented Sep 8, 2020

I found one issue with this scheme: I used to set a single pillar for map_jinja:sources to add roles and dns:domain and this can't be done with this PR since the name of the formula is used in the list.

This is a no GO for me. I need to think at a way to have both features.

@baby-gnu
Copy link
Contributor Author

baby-gnu commented Sep 18, 2020

This is a no GO for me. I need to think at a way to have both features.

I manage to do it only by loading a global defaults.yaml which contains the following YAML:

values:
  map_jinja:
    sources:
      - "osarch"
      - "os_family"
      - "os"
      - "osfinger"
      - "C@{{ tplroot ~ ':lookup' }}"
      - "C@{{ tplroot }}"
      - "dns:domain"
  
      # Based on minion ID
      - "domain"
      - "id"

This require the following diff:

diff --git a/openssh/map.jinja b/openssh/map.jinja
index 3ad3947..ce1dfa6 100644
--- a/openssh/map.jinja
+++ b/openssh/map.jinja
@@ -8,13 +8,25 @@
 {#- Where to lookup parameters source files #}
 {%- set map_sources_dir = tplroot ~ "/parameters" %}
 
-{#- Load defaults first to allow per formula default map.jinja configuration #}
+{#- Load global defaults first to allow centralised default map.jinja configuration #}
+{%- set _global_defaults_filename = "parameters/defaults.yaml" %}
+{%- do salt["log.debug"](
+      "map.jinja: load default parameters from "
+      ~ _global_defaults_filename
+    ) %}
+{%- load_yaml as global_default_settings %}
+{%-   include  _global_defaults_filename ignore missing %}
+{%- endload %}
+
+{#- Load formula defaults secondly to allow per formula default map.jinja configuration #}
 {%- set _defaults_filename = map_sources_dir ~ "/defaults.yaml" %}
 {%- do salt["log.debug"](
-      "map.jinja: initialise parameters from "
+      "map.jinja: load default parameters from "
       ~ _defaults_filename
     ) %}
-{%- import_yaml _defaults_filename as default_settings %}
+{%- load_yaml as default_settings %}
+{%-   include  _defaults_filename ignore missing %}
+{%- endload %}
 
 {#- List of sources to lookup for parameters #}
 {%- do salt["log.debug"]("map.jinja: lookup 'map_jinja' configuration sources") %}
@@ -28,7 +40,22 @@
       "C@" ~ tplroot,
       "id",
     ] %}
-{#- Configure map.jinja from defaults.yaml #}
+{%- do salt["log.debug"](
+      "map.jinja: configure map.jinja from global defaults "
+      ~ _global_defaults_filename
+      ~ ": \n" ~ global_default_settings
+        | yaml(False)
+    ) %}
+{%- set map_sources = global_default_settings | traverse(
+      "values:map_jinja:sources",
+      map_sources,
+    ) %}
+
+{%- do salt["log.debug"](
+      "map.jinja: configure map.jinja from formula defaults "
+      ~ _defaults_filename
+      ~ ": \n" ~ default_settings | yaml(False)
+    ) %}
 {%- set map_sources = default_settings | traverse(
       "values:map_jinja:sources",
       map_sources,

This result in the following

[DEBUG   ] Fetching file from saltenv 'base', ** attempting ** 'salt://foo/map.jinja'
[DEBUG   ] No dest file found
[INFO    ] Fetching file from saltenv 'base', ** done ** 'foo/map.jinja'
[DEBUG   ] In saltenv 'base', looking at rel_path 'foo/libsaltcli.jinja' to resolve 'salt://foo/libsaltcli.jinja'
[DEBUG   ] In saltenv 'base', ** considering ** path '/var/cache/salt/minion/files/base/foo/libsaltcli.jinja' to resolve 'salt://foo/libsaltcli.jinja'
[DEBUG   ] LazyLoaded log.debug
[DEBUG   ] [libsaltcli] the salt command type has been identified to be: minion
[DEBUG   ] map.jinja: load default parameters from parameters/defaults.yaml
[DEBUG   ] In saltenv 'base', looking at rel_path 'parameters/defaults.yaml' to resolve 'salt://parameters/defaults.yaml'
[DEBUG   ] In saltenv 'base', ** considering ** path '/var/cache/salt/minion/files/base/parameters/defaults.yaml' to resolve 'salt://parameters/defaults.yaml'
[DEBUG   ] map.jinja: load default parameters from foo/parameters/defaults.yaml
[DEBUG   ] In saltenv 'base', looking at rel_path 'foo/parameters/defaults.yaml' to resolve 'salt://foo/parameters/defaults.yaml'
[DEBUG   ] In saltenv 'base', ** considering ** path '/var/cache/salt/minion/files/base/foo/parameters/defaults.yaml' to resolve 'salt://foo/parameters/defaults.yaml'
[DEBUG   ] map.jinja: lookup 'map_jinja' configuration sources
[DEBUG   ] map.jinja: configure map.jinja from global defaults parameters/defaults.yaml:
values:
  map_jinja:
    sources:
    - osarch
    - os_family
    - os
    - osfinger
    - C@foo:lookup
    - C@foo
    - dns:domain
    - domain
    - id
[DEBUG   ] map.jinja: configure map.jinja from formula defaults foo/parameters/defaults.yaml:
values: {}
[DEBUG   ] LazyLoaded config.get
[DEBUG   ] map.jinja: load parameters with sources from ['osarch', 'os_family', 'os', 'osfinger', 'C@foo:lookup', 'C@foo', 'dns:domain', 'domain', 'id']
[DEBUG   ] map.jinja: lookup 'osarch' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file foo/parameters/osarch/amd64.yaml
[DEBUG   ] Could not find file 'salt://foo/parameters/osarch/amd64.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: lookup 'os_family' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file foo/parameters/os_family/Debian.yaml
[DEBUG   ] Could not find file 'salt://foo/parameters/os_family/Debian.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: lookup 'os' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file foo/parameters/os/Debian.yaml
[DEBUG   ] Could not find file 'salt://foo/parameters/os/Debian.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: lookup 'osfinger' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file foo/parameters/osfinger/Debian-10.yaml
[DEBUG   ] Could not find file 'salt://foo/parameters/osfinger/Debian-10.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: retrieve 'foo:lookup' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge 'foo:lookup' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] LazyLoaded slsutil.merge
[DEBUG   ] map.jinja: retrieve 'foo' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge 'foo' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: lookup 'dns:domain' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file foo/parameters/dns:domain/example.net.yaml
[DEBUG   ] Could not find file 'salt://foo/parameters/dns:domain/example.net.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: lookup 'domain' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file foo/parameters/domain/example.net.yaml
[DEBUG   ] Could not find file 'salt://foo/parameters/domain/example.net.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: lookup 'id' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file foo/parameters/id/testmachine2.example.net.yaml
[DEBUG   ] Could not find file 'salt://foo/parameters/id/testmachine2.example.net.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: save parameters in variable 'mapdata'

EDIT: I modified the patch to have better log and fix the global parameter path (no leading /)

@baby-gnu
Copy link
Contributor Author

So, finally, I think the remove of config_get_lookup and config_get requires to not configure map.jinja by looking up map_jinja:sources with config.get.

The configuration of map.jinja became only possible with:

  1. a global defaults.yaml (not sure to put it under /parameters or directly at top level)
  2. a per formula defaults.yaml (required for openssh which requires more than C@{{ tplroot }})

If people agree, I'll rework that PR with the proposed patch.

Regards.

@myii
Copy link
Member

myii commented Sep 21, 2020

  1. a per formula defaults.yaml (required for openssh which requires more than C@{{ tplroot }})

@baby-gnu My general instinct is that this is the right option. Do we have an example of what this looks like?

@baby-gnu
Copy link
Contributor Author

baby-gnu commented Sep 21, 2020

Now, I'm wondering if using the file name defaults.yaml to configure map.jinja is a good idea. It's like mixing 2 things:

  • the map.jinja configuration
  • the formula configuration

We could load the map_jinja:sources from several places, either global or formula specific.

Global values

These files don't exists actually

  • /parameters/defaults.yaml: default global values (for all formula), it could include map_jinja:sources but others too so I'm not very confident with it for now
  • /parameters/map_jinja.yaml: unambiguous, dedicated file to configure map.jinja globally (for all formula), it's my preferred choice for global map.jinja configuration

Per formula values

  • {{ tplroot }}/parameters/defaults.yaml: default values for a specific formula, it could include map_jinja:sources but is mostly used for the formula itself. Modifying this file for a user requires to copy all the content of the original one (since the salt file server serves the first file found)
  • {{ tplroot }}/parameters/map_jinja.yaml: unambiguous, dedicated file to configure map.jinja for a specific formula, it does not exists actually but could be introduced by this PR, it's my preferred choice for per formula map.jinja configuration

@baby-gnu
Copy link
Contributor Author

@baby-gnu My general instinct is that this is the right option. Do we have an example of what this looks like?

I think I could update the PR #194 to show how to use the salt file server instead of pillars.

@myii
Copy link
Member

myii commented Sep 21, 2020

Global values

...

  • /parameters/map_jinja.yaml: unambiguous, dedicated file to configure map.jinja globally (for all formula), it's my preferred choice for global map.jinja configuration

Per formula values

...

  • {{ tplroot }}/parameters/map_jinja.yaml: unambiguous, dedicated file to configure map.jinja for a specific formula, it does not exists actually but could be introduced by this PR, it's my preferred choice for per formula map.jinja configuration

@baby-gnu Gut feeling is the above. Actually, I'd be really interested if we could have both -- use the global settings if not overridden by the user in {{ tplroot }}/parameters/map_jinja.yaml. Need a way to make it easy for users to provide these files without clashing with any files provided by the repo itself.

@baby-gnu
Copy link
Contributor Author

Now, I'm wondering if using the file name defaults.yaml to configure map.jinja is a good idea. It's like mixing 2 things:

* the `map.jinja` configuration

* the formula configuration

We could load the map_jinja:sources from several places, either global or formula specific.

I made a test:

  1. load map.jinja config from parameters/map_jinja.yaml if it exists (no error if it does not), this file can be provided by user.
  2. load map.jinja config from {{ tplroot }}/parameters/map_jinja.yaml if it exists (no error if it does not), this file is provided by the formula and can be easily overrode by the user

Note that the map.jinja values are removed from _mapdata as you can see in the failing tests, I can merge it in the default_settings if you prefer to follow the any changes to the map.jinja configuration.

And here is the DEBUG output I got:

[DEBUG   ] In saltenv 'base', looking at rel_path 'openssh/map.jinja' to resolve 'salt://openssh/map.jinja'
[DEBUG   ] In saltenv 'base', ** considering ** path '/tmp/kitchen/var/cache/salt/minion/files/base/openssh/map.jinja' to resolve 'salt://openssh/map.jinja'
[DEBUG   ] In saltenv 'base', looking at rel_path 'openssh/libsaltcli.jinja' to resolve 'salt://openssh/libsaltcli.jinja'
[DEBUG   ] In saltenv 'base', ** considering ** path '/tmp/kitchen/var/cache/salt/minion/files/base/openssh/libsaltcli.jinja' to resolve 'salt://openssh/libsaltcli.jinja'
[DEBUG   ] [libsaltcli] the salt command type has been identified to be: local
[DEBUG   ] map.jinja: load global map.jinja values from parameters/map_jinja.yaml
[DEBUG   ] Could not find file 'salt://parameters/map_jinja.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: load per formula map.jinja values from openssh/parameters/map_jinja.yaml
[DEBUG   ] In saltenv 'base', looking at rel_path 'openssh/parameters/map_jinja.yaml' to resolve 'salt://openssh/parameters/map_jinja.yaml'
[DEBUG   ] In saltenv 'base', ** considering ** path '/tmp/kitchen/var/cache/salt/minion/files/base/openssh/parameters/map_jinja.yaml' to resolve 'salt://openssh/parameters/map_jinja.yaml'
[DEBUG   ] map.jinja: built-in configuration sources: 
values:
  map_jinja:
    sources:
    - osarch
    - os_family
    - os
    - osfinger
    - C@openssh:lookup
    - C@openssh
    - id
[DEBUG   ] map.jinja: configuration sources from global map.jinja configuration parameters/map_jinja.yaml: 
values:
  map_jinja:
    sources: {}
[DEBUG   ] map.jinja: configuration sources from formula map.jinja configuration openssh/parameters/map_jinja.yaml: 
values:
  map_jinja:
    sources:
    - osarch
    - os_family
    - os
    - osfinger
    - C:SUB@openssh:lookup
    - C:SUB@openssh
    - C:SUB@sshd_config:lookup
    - C:SUB@sshd_config
    - C:SUB@ssh_config:lookup
    - C:SUB@ssh_config
    - id
[DEBUG   ] map.jinja: load parameters with sources from: 
- osarch
- os_family
- os
- osfinger
- C:SUB@openssh:lookup
- C:SUB@openssh
- C:SUB@sshd_config:lookup
- C:SUB@sshd_config
- C:SUB@ssh_config:lookup
- C:SUB@ssh_config
- id
[DEBUG   ] map.jinja: load per formula default values from openssh/parameters/defaults.yaml
[DEBUG   ] In saltenv 'base', looking at rel_path 'openssh/parameters/defaults.yaml' to resolve 'salt://openssh/parameters/defaults.yaml'
[DEBUG   ] In saltenv 'base', ** considering ** path '/tmp/kitchen/var/cache/salt/minion/files/base/openssh/parameters/defaults.yaml' to resolve 'salt://openssh/parameters/defaults.yaml'
[DEBUG   ] map.jinja: lookup 'osarch' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file openssh/parameters/osarch/amd64.yaml
[DEBUG   ] Could not find file 'salt://openssh/parameters/osarch/amd64.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: lookup 'os_family' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file openssh/parameters/os_family/Debian.yaml
[DEBUG   ] In saltenv 'base', looking at rel_path 'openssh/parameters/os_family/Debian.yaml' to resolve 'salt://openssh/parameters/os_family/Debian.yaml'
[DEBUG   ] In saltenv 'base', ** considering ** path '/tmp/kitchen/var/cache/salt/minion/files/base/openssh/parameters/os_family/Debian.yaml' to resolve 'salt://openssh/parameters/os_family/Debian.yaml'
[DEBUG   ] map.jinja: merge parameters from openssh/parameters/os_family/Debian.yaml
[DEBUG   ] map.jinja: lookup 'os' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file openssh/parameters/os/Debian.yaml
[DEBUG   ] Could not find file 'salt://openssh/parameters/os/Debian.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: lookup 'osfinger' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file openssh/parameters/osfinger/Debian-10.yaml
[DEBUG   ] Could not find file 'salt://openssh/parameters/osfinger/Debian-10.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: retrieve 'openssh:lookup' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'openssh:lookup' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: retrieve 'openssh' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'openssh' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: retrieve 'sshd_config:lookup' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'sshd_config:lookup' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: retrieve 'sshd_config' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'sshd_config' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: retrieve 'ssh_config:lookup' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'ssh_config:lookup' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: retrieve 'ssh_config' with 'config.get', merge: strategy='None'
[DEBUG   ] map.jinja: merge sub key 'ssh_config' retrieved with 'config.get', merge: strategy='smart', lists='False'
[DEBUG   ] map.jinja: lookup 'id' with 'config.get'
[DEBUG   ] map.jinja: load parameters from file openssh/parameters/id/example.net.yaml
[DEBUG   ] Could not find file 'salt://openssh/parameters/id/example.net.yaml' in saltenv 'base'
[DEBUG   ] map.jinja: save parameters in variable 'mapdata'

What do you think about this?

@myii
Copy link
Member

myii commented Sep 21, 2020

@baby-gnu Yes, this seems like the right way forward.

  1. load map.jinja config from {{ tplroot }}/parameters/map_jinja.yaml if it exists (no error if it does not), this file is provided by the formula and can be easily overrode by the user

How would the user overrride this file?

Note that the map.jinja values are removed from _mapdata as you can see in the failing tests, I can merge it in the default_settings if you prefer to follow the any changes to the map.jinja configuration.

Since these are both relatively new features (v4/5 map.jinja and _mapdata dump comparison), I'm not attached to either option. Should we be verifying that map_jinja:sources are working as expected? I suppose we should, if it's relatively simple to implement.

@baby-gnu
Copy link
Contributor Author

How would the user overrride this file?

As an example, I will:

  1. make sure roots is higher in priority than gitfs
  2. create /srv/salt/parameters and /srv/salt/{{ tplroot }}/parameters/ directories
  3. create the /srv/salt/parameters/map_jinja.yaml with the following content
    values:
      map_jinja:
        sources:
          # default values
          - "osarch"
          - "os_family"
          - "os"
          - "osfinger"
          - "C@{{ tplroot ~ ':lookup' }}"
          - "C@{{ tplroot }}"
      
          # role based configuration
          - "roles"
    
          # DNS domain configured (DHCP or resolv.conf)
          - "dns:domain"
      
          # Based on minion ID
          - "domain"
      
          # default values
          - "id"
  4. create the /srv/salt/openssh/parameters/map_jinja.yaml with the following content
    values:
      map_jinja:
        sources:
          # default values
          - "osarch"
          - "os_family"
          - "os"
          - "osfinger"
    
          # Merge values from `config.get` under `mapdata.<key>` to keep
          # compatibility with user pillars.
          # The `<key>` and `<key>:lookup` are merged together
          - "C:SUB@openssh:lookup"
          - "C:SUB@openssh"
          - "C:SUB@sshd_config:lookup"
          - "C:SUB@sshd_config"
          - "C:SUB@ssh_config:lookup"
          - "C:SUB@ssh_config"
    
          # role based configuration
          - "roles"
    
          # DNS domain configured (DHCP or resolv.conf)
          - "dns:domain"
      
          # Based on minion ID
          - "domain"
      
          # default values
          - "id"

This way:

  • I configure globally map.jinja to include parameters values per roles and DNS domain
  • I configure the map.jinja for the openssh-formula to include parameters values per roles and DNS domain, this one is required since the global configuration would broke this formula

Since these are both relatively new features (v4/5 map.jinja and _mapdata dump comparison), I'm not attached to either option. Should we be verifying that map_jinja:sources are working as expected? I suppose we should, if it's relatively simple to implement.

Yes, it is. And it's working.

I'll merge my personal branch tomorrow and clean the PR if you are OK.

Thanks for your comments.

@myii
Copy link
Member

myii commented Sep 21, 2020

I'll merge my personal branch tomorrow and clean the PR if you are OK.

@baby-gnu All good at this end. Please go ahead so that we can start testing it out for proper feedback.

@baby-gnu baby-gnu force-pushed the feature/map-config-targeting-like-syntax branch from fe98819 to bf9d76d Compare September 22, 2020 06:50
@baby-gnu
Copy link
Contributor Author

@myii Now that I finalized this branch, I now see that the map_jinja.yaml could contains quite complex things like:

values:
  map_jinja:
    sources:
      # default values
      - "parameters/osarch/{{ salt['grains.get']('osarch') }}.yaml"
      - "parameters/os_family/{{ salt['grains.get']('os_family') }}.yaml"
      - "parameters/os/{{ salt['grains.get']('os') }}.yaml"
      - "parameters/osfinger/{{ salt['grains.get']('osfinger') }}.yaml"
      - "C@{{ tplroot ~ ':lookup' }}"
      - "C@{{ tplroot }}"
  
      # role based configuration
{%- for role in salt['config.get']('roles') %}
{%-   if role %}
      - "parameters/roles/{{ role }}.yaml"
{%-   endif %}
{%- endfor %}

      # DNS domain configured (DHCP or resolv.conf)
      - "parameters/dns:domain/{{ salt['grains.get']('dns:domain') }}.yaml"
  
      # Based on minion ID
      - "parameters/domain/{{ salt['grains.get']('domain') }}.yaml"
  
      # default values
      - "parameters/id/{{ salt['grains.get']('id') }}.yaml"

Which:

  • can looks like PillarStack configuration
  • have a cleaner/shorter syntax than PillarStack
  • merge values from YAML files and config.get/pillar.get/grains.get

So, if someone found that map.jinja is too complex, we could simplify a lot map.jinja to the expense of forcing Jinja in map_jinja.yaml file. But I prefer the way it is now ;-)

Regards.

@n-rodriguez
Copy link
Member

@baby-gnu just a question :

      # role based configuration
{%- for role in salt['config.get']('roles') %}
{%-   if role %}
      - "parameters/roles/{{ role }}.yaml"
{%-   endif %}
{%- endfor %}

how do you make the distinction between client role and server role? one machine could have both roles, for instance salt-master and salt-minion role.

@baby-gnu
Copy link
Contributor Author

how do you make the distinction between client role and server role? one machine could have both roles, for instance salt-master and salt-minion role.

Actually, with this jinja code (and it's equivalent in map.jinja which will handle the - roles item), map.jinja will load values from:

  • {{ tplroot }}/parameters/roles/salt-master.yaml
  • {{ tplroot }}/parameters/roles/salt-minion.yaml

You can only have a problem if both roles define the same parameter with different values.

The writer of a formula should take care of that case and if her formula has several components, they must play well together on the same minion ;-)

But maybe you see any issue with this?

@n-rodriguez
Copy link
Member

n-rodriguez commented Sep 22, 2020

Actually, with this jinja code (and it's equivalent in map.jinja which will handle the - roles item), map.jinja will load values from:

* `{{ tplroot }}/parameters/roles/salt-master.yaml`
* `{{ tplroot }}/parameters/roles/salt-minion.yaml`

Sure. But what about syslog-ng? Of course you can do with a suffix :

  • {{ tplroot }}/parameters/roles/syslog-ng-client.yaml
  • {{ tplroot }}/parameters/roles/syslog-ng-server.yaml

or a prefix :

  • {{ tplroot }}/parameters/roles/client-syslog-ng.yaml
  • {{ tplroot }}/parameters/roles/server-syslog-ng.yaml

But on my side I ended up using this with stacker :

  • {{ tplroot }}/parameters/roles/client/syslog-ng.yaml
  • {{ tplroot }}/parameters/roles/server/syslog-ng.yaml

or

  • {{ tplroot }}/parameters/roles/client/salt.yaml
  • {{ tplroot }}/parameters/roles/server/salt.yaml

Edit :

I find it cleaner than the way I was using before.

I used prefix (client- or server-) but had to play with strings to get the role "type" (client or server)
Example : server-syslog-ng | split('-')[0] to get server and go in the right directory.

Now I do something like :

{%- for role in salt['config.get']('roles:server', []) %}
{{ tplroot }}/parameters/roles/server/{{ role }}.yaml
{% endfor %}

{%- for role in salt['config.get']('roles:client', []) %}
{{ tplroot }}/parameters/roles/client/{{ role }}.yaml
{% endfor %}

@baby-gnu
Copy link
Contributor Author

baby-gnu commented Nov 3, 2020

While I agreed with the idea, it's (again), far too much refactoring for little gain -- apologies.

OK, so let everything in tlproot. Thanks.

docs/map.jinja.rst Outdated Show resolved Hide resolved
docs/map.jinja.rst Show resolved Hide resolved
@baby-gnu
Copy link
Contributor Author

baby-gnu commented Nov 9, 2020

There is no delimiter for config.get over salt-ssh:

root@scribe:~# salt-ssh test-minion config.get 'foo!bar' delimiter='!'
[ERROR   ] TypeError encountered executing config.get: get() got an unexpected keyword argument 'delimiter'
test-minion:
    TypeError encountered executing config.get: get() got an unexpected keyword argument 'delimiter'

@baby-gnu baby-gnu force-pushed the feature/map-config-targeting-like-syntax branch from ce9f1b5 to 9129a23 Compare January 6, 2021 13:08
@baby-gnu
Copy link
Contributor Author

baby-gnu commented Jan 6, 2021

Now I'll refactor the commits to have a better implementation progression, for future history review ;-)

@baby-gnu baby-gnu force-pushed the feature/map-config-targeting-like-syntax branch 2 times, most recently from a035ab2 to a85ecb2 Compare January 8, 2021 15:00
@baby-gnu
Copy link
Contributor Author

baby-gnu commented Jan 8, 2021

So now, I think the commits are in good shape for review and view how things are changed to reach v5 map.jinja.

I even validate manually each of them separately:

for i in $(git log --pretty=format:"%h" master..HEAD)
do
    git checkout ${i}
	./bin/kitchen converge default-debian-10-master-py3 && ./bin/kitchen verify default-debian-10-master-py3
done

and all runs were successful.

Copy link
Member

@myii myii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baby-gnu Got an issue with the initial checks across unique platforms.

openssh/libmatchers.jinja Outdated Show resolved Hide resolved
@baby-gnu baby-gnu force-pushed the feature/map-config-targeting-like-syntax branch from a85ecb2 to 9bc8c8f Compare January 11, 2021 08:25
Copy link
Member

@myii myii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baby-gnu We're all good with the implementation, this is the eyeball-review, nearly all just formatting. Just one main issue identified with the positioning of to_bool.

openssh/libmapstack.jinja Outdated Show resolved Hide resolved
openssh/libmapstack.jinja Outdated Show resolved Hide resolved
openssh/libmapstack.jinja Outdated Show resolved Hide resolved
openssh/libmapstack.jinja Outdated Show resolved Hide resolved
openssh/libmapstack.jinja Outdated Show resolved Hide resolved
openssh/libmatchers.jinja Outdated Show resolved Hide resolved
openssh/map.jinja Outdated Show resolved Hide resolved
openssh/map.jinja Outdated Show resolved Hide resolved
openssh/map.jinja Outdated Show resolved Hide resolved
openssh/map.jinja Outdated Show resolved Hide resolved
The `config_get_lookup` and `config_get` sources lack flexibility.

It's not easy to query several pillars and/or grains keys with the
actual system. And the query method is forced to `config.get` without
being configurable by the user.

We define a mechanism to select `map.jinja` sources with similar
notation as the salt targeting system.

The `map.jinja` file uses several sources where to lookup parameter
values. The list of sources can be modified by two files:

1. a global salt://parameters/map_jinja.yaml
2. a per formula salt://{{ tplroot }}/parameters/map_jinja.yaml.

Each source definition has the form `<TYPE>:<OPTION>@<KEY>` where
`<TYPE>` can be one of:

- `Y` to load values from YAML files, this is the default when no type
  is defined
- `C` to lookup values with `config.get`
- `G` to lookup values with `grains.get`
- `I` to lookup values with `pillar.get`

The YAML type option can define the query method to lookup the key
value to build the file name:

- `C` to query with `config.get`, this is the default when to query
  method is defined
- `G` to query with `grains.get`
- `I` to query with `pillar.get`

The `C`, `G` or `I` types can define the `SUB` option to store values
in the sub key `mapdata.<key>` instead of directly in `mapdata`.

Finally, the `<KEY>` describe what to lookup to either build the YAML
filename or gather values using one of the query method.

BREAKING CHANGE: the configuration `map_jinja:sources` is only
                 configurable with `salt://parameters/map_jinja.yaml`
		 and `salt://{{ tplroot }}/parameters/map_jinja.yaml`

BREAKING CHANGE: the `map_jinja:config_get_roots` is replaced by
                 compound like `map_jinja:sources`

BREAKING CHANGE: the two `config_get_lookup` and `config_get` are
                 replaced by `C@<tplroot>:lookup` and `C@<tplroot>`
		 sources
No need to prefix some of them with underscore `_`.
@baby-gnu baby-gnu force-pushed the feature/map-config-targeting-like-syntax branch from 9bc8c8f to 7de2d6f Compare January 12, 2021 08:01
Copy link
Member

@myii myii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@myii myii merged commit 9955923 into saltstack-formulas:master Jan 12, 2021
@saltstack-formulas-travis

🎉 This PR is included in version 3.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@myii
Copy link
Member

myii commented Jan 12, 2021

@baby-gnu It took us only 6 months (nearly) but we got there in the end! Thanks for your patience and perseverance.

A big thanks also goes to @n-rodriguez @dafyddj and @vutny for their involvement with improving this implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants