123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353 |
- .. _conventions-formula:
- =============
- Salt Formulas
- =============
- Formulas are pre-written Salt States. They are as open-ended as Salt States
- themselves and can be used for tasks such as installing a package, configuring,
- and starting a service, setting up users or permissions, and many other common
- tasks.
- All official Salt Formulas are found as separate Git repositories in the
- "saltstack-formulas" organization on GitHub:
- https://github.com/saltstack-formulas
- As a simple example, to install the popular Apache web server (using the normal
- defaults for the underlying distro) simply include the
- :formula_url:`apache-formula` from a top file:
- .. code-block:: yaml
- base:
- 'web*':
- - apache
- Installation
- ============
- Each Salt Formula is an individual Git repository designed as a drop-in
- addition to an existing Salt State tree. Formulas can be installed in the
- following ways.
- Adding a Formula as a GitFS remote
- ----------------------------------
- One design goal of Salt's GitFS fileserver backend was to facilitate reusable
- States. GitFS is a quick and natural way to use Formulas.
- 1. :ref:`Install any necessary dependencies and configure GitFS
- <tutorial-gitfs>`.
- 2. Add one or more Formula repository URLs as remotes in the
- :conf_master:`gitfs_remotes` list in the Salt Master configuration file:
- .. code-block:: yaml
- gitfs_remotes:
- - https://github.com/saltstack-formulas/apache-formula
- - https://github.com/saltstack-formulas/memcached-formula
- **We strongly recommend forking a formula repository** into your own GitHub
- account to avoid unexpected changes to your infrastructure.
- Many Salt Formulas are highly active repositories so pull new changes with
- care. Plus any additions you make to your fork can be easily sent back
- upstream with a quick pull request!
- 3. Restart the Salt master.
- Beginning with the 2018.3.0 release, using formulas with GitFS is now much more
- convenient for deployments which use many different fileserver environments
- (i.e. saltenvs). Using the :ref:`all_saltenvs <gitfs-global-remotes>`
- parameter, files from a single git branch/tag will appear in all environments.
- See :ref:`here <gitfs-global-remotes>` for more information on this feature.
- Adding a Formula directory manually
- -----------------------------------
- Formulas are simply directories that can be copied onto the local file system
- by using Git to clone the repository or by downloading and expanding a tarball
- or zip file of the repository. The directory structure is designed to work with
- :conf_master:`file_roots` in the Salt master configuration.
- 1. Clone or download the repository into a directory:
- .. code-block:: bash
- mkdir -p /srv/formulas
- cd /srv/formulas
- git clone https://github.com/saltstack-formulas/apache-formula.git
- # or
- mkdir -p /srv/formulas
- cd /srv/formulas
- wget https://github.com/saltstack-formulas/apache-formula/archive/master.tar.gz
- tar xf apache-formula-master.tar.gz
- 2. Add the new directory to :conf_master:`file_roots`:
- .. code-block:: yaml
- file_roots:
- base:
- - /srv/salt
- - /srv/formulas/apache-formula
- 3. Restart the Salt Master.
- Usage
- =====
- Each Formula is intended to be immediately usable with sane defaults without
- any additional configuration. Many formulas are also configurable by including
- data in Pillar; see the :file:`pillar.example` file in each Formula repository
- for available options.
- Including a Formula in an existing State tree
- ---------------------------------------------
- Formula may be included in an existing ``sls`` file. This is often useful when
- a state you are writing needs to ``require`` or ``extend`` a state defined in
- the formula.
- Here is an example of a state that uses the :formula_url:`epel-formula` in a
- ``require`` declaration which directs Salt to not install the ``python26``
- package until after the EPEL repository has also been installed:
- .. code-block:: yaml
- include:
- - epel
- python26:
- pkg.installed:
- - require:
- - pkg: epel
- Including a Formula from a Top File
- -----------------------------------
- Some Formula perform completely standalone installations that are not
- referenced from other state files. It is usually cleanest to include these
- Formula directly from a Top File.
- For example the easiest way to set up an OpenStack deployment on a single
- machine is to include the :formula_url:`openstack-standalone-formula` directly from
- a :file:`top.sls` file:
- .. code-block:: yaml
- base:
- 'myopenstackmaster':
- - openstack
- Quickly deploying OpenStack across several dedicated machines could also be
- done directly from a Top File and may look something like this:
- .. code-block:: yaml
- base:
- 'controller':
- - openstack.horizon
- - openstack.keystone
- 'hyper-*':
- - openstack.nova
- - openstack.glance
- 'storage-*':
- - openstack.swift
- Configuring Formula using Pillar
- --------------------------------
- Salt Formulas are designed to work out of the box with no additional
- configuration. However, many Formula support additional configuration and
- customization through :ref:`Pillar <pillar>`. Examples of available options can
- be found in a file named :file:`pillar.example` in the root directory of each
- Formula repository.
- .. _extending-formulas:
- Using Formula with your own states
- ----------------------------------
- Remember that Formula are regular Salt States and can be used with all Salt's
- normal state mechanisms. Formula can be required from other States with
- :ref:`requisites-require` declarations, they can be modified using ``extend``,
- they can made to watch other states with :ref:`requisites-watch-in`.
- The following example uses the stock :formula_url:`apache-formula` alongside a
- custom state to create a vhost on a Debian/Ubuntu system and to reload the
- Apache service whenever the vhost is changed.
- .. code-block:: yaml
- # Include the stock, upstream apache formula.
- include:
- - apache
- # Use the watch_in requisite to cause the apache service state to reload
- # apache whenever the my-example-com-vhost state changes.
- my-example-com-vhost:
- file:
- - managed
- - name: /etc/apache2/sites-available/my-example-com
- - watch_in:
- - service: apache
- Don't be shy to read through the source for each Formula!
- Reporting problems & making additions
- -------------------------------------
- Each Formula is a separate repository on GitHub. If you encounter a bug with a
- Formula please file an issue in the respective repository! Send fixes and
- additions as a pull request. Add tips and tricks to the repository wiki.
- Writing Formulas
- ================
- Each Formula is a separate repository in the `saltstack-formulas`_ organization
- on GitHub.
- Get involved creating new Formulas
- ----------------------------------
- The best way to create new Formula repositories for now is to create a
- repository in your own account on GitHub and notify a SaltStack employee when
- it is ready. We will add you to the Contributors team on the
- `saltstack-formulas`_ organization and help you transfer the repository over.
- Ping a SaltStack employee on IRC (``#salt`` on Freenode), join the
- ``#formulas`` channel on the `salt-slack`_ (bridged to ``#saltstack-formulas``
- on Freenode) or send an email to the `salt-users`_ mailing list. Note that the
- IRC logs are available at https://freenode.logbot.info/salt and
- https://freenode.logbot.info/saltstack-formulas respectively.
- There are a lot of repositories in that organization! Team members can manage
- which repositories they are subscribed to on GitHub's watching page:
- https://github.com/watching.
- Members of the Contributors team are welcome to participate in reviewing pull
- requests across the Organization. Some repositories will have regular
- contributors and some repositories will not. As you get involved in a
- repository be sure to communicate with any other contributors there on pull
- requests that are large or have breaking changes.
- In general it is best to have another Contributor review and merge any pull
- requests that you open. Feel free to `at-mention`_ other regular contributors
- to a repository and request a review. However, there are a lot of formula
- repositories so if a repository does not yet have regular contributors or if
- your pull request has stayed open for more than a couple days feel free to
- "selfie-merge" your own pull request.
- .. _`at-mention`: https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax#mentioning-people-and-teams
- Style
- -----
- Maintainability, readability, and reusability are all marks of a good Salt sls
- file. This section contains several suggestions and examples.
- .. code-block:: jinja
- # Deploy the stable master branch unless version overridden by passing
- # Pillar at the CLI or via the Reactor.
- deploy_myapp:
- git.latest:
- - name: git@github.com/myco/myapp.git
- - version: {{ salt.pillar.get('myapp:version', 'master') }}
- Use a descriptive State ID
- ``````````````````````````
- The ID of a state is used as a unique identifier that may be referenced via
- other states in :ref:`requisites <requisites>`. It must be unique across the
- whole state tree (:ref:`it is a key in a dictionary <id-declaration>`, after
- all).
- In addition a state ID should be descriptive and serve as a high-level hint of
- what it will do, or manage, or change. For example, ``deploy_webapp``, or
- ``apache``, or ``reload_firewall``.
- Use ``module.function`` notation
- ````````````````````````````````
- So-called "short-declaration" notation is preferred for referencing state
- modules and state functions. It provides a consistent pattern of
- ``module.function`` shared between Salt States, the Reactor, Salt
- Mine, the Scheduler, as well as with the CLI.
- .. code-block:: yaml
- # Do
- apache:
- pkg.installed:
- - name: httpd
- # Don't
- apache:
- pkg:
- - installed
- - name: httpd
- Salt's state compiler will transform "short-decs" into the longer format
- :ref:`when compiling the human-friendly highstate structure into the
- machine-friendly lowstate structure <state-layers>`.
- Specify the ``name`` parameter
- ``````````````````````````````
- Use a unique and permanent identifier for the state ID and reserve ``name`` for
- data with variability.
- The :ref:`name declaration <name-declaration>` is a required parameter for all
- state functions. The state ID will implicitly be used as ``name`` if it is not
- explicitly set in the state.
- In many state functions the ``name`` parameter is used for data that varies
- such as OS-specific package names, OS-specific file system paths, repository
- addresses, etc. Any time the ID of a state changes all references to that ID
- must also be changed. Use a permanent ID when writing a state the first time to
- future-proof that state and allow for easier refactors down the road.
- Comment state files
- ```````````````````
- YAML allows comments at varying indentation levels. It is a good practice to
- comment state files. Use vertical whitespace to visually separate different
- concepts or actions.
- .. code-block:: yaml
- # Start with a high-level description of the current sls file.
- # Explain the scope of what it will do or manage.
- # Comment individual states as necessary.
- update_a_config_file:
- # Provide details on why an unusual choice was made. For example:
- #
- # This template is fetched from a third-party and does not fit our
- # company norm of using Jinja. This must be processed using Mako.
- file.managed:
- - name: /path/to/file.cfg
- - source: salt://path/to/file.cfg.template
- - template: mako
- # Provide a description or explanation that did not fit within the state
- # ID. For example:
- #
- # Update the application's last-deployed timestamp.
- # This is a workaround until Bob configures Jenkins to automate RPM
- # builds of the app.
- cmd.run:
- # FIXME: Joe needs this to run on Windows by next quarter. Switch these
- # from shell commands to Salt's file.managed and file.replace state
- # modules.
- - name: |
- touch /path/to/file_last_updated
- sed -e 's/foo/bar/g' /path/to/file_environment
- - onchanges:
- - file: a_config_file
- Be careful to use Jinja comments for commenting Jinja code and YAML comments
- for commenting YAML code.
- .. code-block:: jinja
- # BAD EXAMPLE
- # The Jinja in this YAML comment is still executed!
- # {% set apache_is_installed = 'apache' in salt.pkg.list_pkgs() %}
- # GOOD EXAMPLE
- # The Jinja in this Jinja comment will not be executed.
- {# {% set apache_is_installed = 'apache' in salt.pkg.list_pkgs() %} #}
- Easy on the Jinja!
- ------------------
- Jinja templating provides vast flexibility and power when building Salt sls
- files. It can also create an unmaintainable tangle of logic and data. Speaking
- broadly, Jinja is best used when kept apart from the states (as much as is
- possible).
- Below are guidelines and examples of how Jinja can be used effectively.
- Know the evaluation and execution order
- ```````````````````````````````````````
- High-level knowledge of how Salt states are compiled and run is useful when
- writing states.
- The default :conf_minion:`renderer` setting in Salt is Jinja piped to YAML.
- Each is a separate step. Each step is not aware of the previous or following
- step. Jinja is not YAML aware, YAML is not Jinja aware; they cannot share
- variables or interact.
- * Whatever the Jinja step produces must be valid YAML.
- * Whatever the YAML step produces must be a valid :ref:`highstate data
- structure <states-highstate-example>`. (This is also true of the final step
- for :ref:`any of the alternate renderers <all-salt.renderers>` in Salt.)
- * Highstate can be thought of as a human-friendly data structure; easy to write
- and easy to read.
- * Salt's state compiler validates the :ref:`highstate <running-highstate>` and
- compiles it to low state.
- * Low state can be thought of as a machine-friendly data structure. It is a
- list of dictionaries that each map directly to a function call.
- * Salt's state system finally starts and executes on each "chunk" in the low
- state. Remember that requisites are evaluated at runtime.
- * The return for each function call is added to the "running" dictionary which
- is the final output at the end of the state run.
- The full evaluation and execution order::
- Jinja -> YAML -> Highstate -> low state -> execution
- Avoid changing the underlying system with Jinja
- ```````````````````````````````````````````````
- Avoid calling commands from Jinja that change the underlying system. Commands
- run via Jinja do not respect Salt's dry-run mode (``test=True``)! This is
- usually in conflict with the idempotent nature of Salt states unless the
- command being run is also idempotent.
- Inspect the local system
- ````````````````````````
- A common use for Jinja in Salt states is to gather information about the
- underlying system. The ``grains`` dictionary available in the Jinja context is
- a great example of common data points that Salt itself has already gathered.
- Less common values are often found by running commands. For example:
- .. code-block:: jinja
- {% set is_selinux_enabled = salt.cmd.run('sestatus') == '1' %}
- This is usually best done with a variable assignment in order to separate the
- data from the state that will make use of the data.
- Gather external data
- ````````````````````
- One of the most common uses for Jinja is to pull external data into the state
- file. External data can come from anywhere like API calls or database queries,
- but it most commonly comes from flat files on the file system or Pillar data
- from the Salt Master. For example:
- .. code-block:: jinja
- {% set some_data = salt.pillar.get('some_data', {'sane default': True}) %}
- {# or #}
- {% import_yaml 'path/to/file.yaml' as some_data %}
- {# or #}
- {% import_json 'path/to/file.json' as some_data %}
- {# or #}
- {% import_text 'path/to/ssh_key.pub' as ssh_pub_key %}
- {# or #}
- {% from 'path/to/other_file.jinja' import some_data with context %}
- This is usually best done with a variable assignment in order to separate the
- data from the state that will make use of the data.
- Light conditionals and looping
- ``````````````````````````````
- Jinja is extremely powerful for programmatically generating Salt states. It is
- also easy to overuse. As a rule of thumb, if it is hard to read it will be hard
- to maintain!
- Separate Jinja control-flow statements from the states as much as is possible
- to create readable states. Limit Jinja within states to simple variable
- lookups.
- Below is a simple example of a readable loop:
- .. code-block:: jinja
- {% for user in salt.pillar.get('list_of_users', []) %}
- {# Ensure unique state IDs when looping. #}
- {{ user.name }}-{{ loop.index }}:
- user.present:
- - name: {{ user.name }}
- - shell: {{ user.shell }}
- {% endfor %}
- Avoid putting a Jinja conditionals within Salt states where possible.
- Readability suffers and the correct YAML indentation is difficult to see in the
- surrounding visual noise. Parametrization (discussed below) and variables are
- both useful techniques to avoid this. For example:
- .. code-block:: jinja
- {# ---- Bad example ---- #}
- apache:
- pkg.installed:
- {% if grains.os_family == 'RedHat' %}
- - name: httpd
- {% elif grains.os_family == 'Debian' %}
- - name: apache2
- {% endif %}
- {# ---- Better example ---- #}
- {% if grains.os_family == 'RedHat' %}
- {% set name = 'httpd' %}
- {% elif grains.os_family == 'Debian' %}
- {% set name = 'apache2' %}
- {% endif %}
- apache:
- pkg.installed:
- - name: {{ name }}
- {# ---- Good example ---- #}
- {% set name = {
- 'RedHat': 'httpd',
- 'Debian': 'apache2',
- }.get(grains.os_family) %}
- apache:
- pkg.installed:
- - name: {{ name }}
- Dictionaries are useful to effectively "namespace" a collection of variables.
- This is useful with parametrization (discussed below). Dictionaries are also
- easily combined and merged. And they can be directly serialized into YAML which
- is often easier than trying to create valid YAML through templating. For
- example:
- .. code-block:: jinja
- {# ---- Bad example ---- #}
- haproxy_conf:
- file.managed:
- - name: /etc/haproxy/haproxy.cfg
- - template: jinja
- {% if 'external_loadbalancer' in grains.roles %}
- - source: salt://haproxy/external_haproxy.cfg
- {% elif 'internal_loadbalancer' in grains.roles %}
- - source: salt://haproxy/internal_haproxy.cfg
- {% endif %}
- - context:
- {% if 'external_loadbalancer' in grains.roles %}
- ssl_termination: True
- {% elif 'internal_loadbalancer' in grains.roles %}
- ssl_termination: False
- {% endif %}
- {# ---- Better example ---- #}
- {% load_yaml as haproxy_defaults %}
- common_settings:
- bind_port: 80
- internal_loadbalancer:
- source: salt://haproxy/internal_haproxy.cfg
- settings:
- bind_port: 8080
- ssl_termination: False
- external_loadbalancer:
- source: salt://haproxy/external_haproxy.cfg
- settings:
- ssl_termination: True
- {% endload %}
- {% if 'external_loadbalancer' in grains.roles %}
- {% set haproxy = haproxy_defaults['external_loadbalancer'] %}
- {% elif 'internal_loadbalancer' in grains.roles %}
- {% set haproxy = haproxy_defaults['internal_loadbalancer'] %}
- {% endif %}
- {% do haproxy.settings.update(haproxy_defaults.common_settings) %}
- haproxy_conf:
- file.managed:
- - name: /etc/haproxy/haproxy.cfg
- - template: jinja
- - source: {{ haproxy.source }}
- - context: {{ haproxy.settings | yaml() }}
- There is still room for improvement in the above example. For example,
- extracting into an external file or replacing the if-elif conditional with a
- function call to filter the correct data more succinctly. However, the state
- itself is simple and legible, the data is separate and also simple and legible.
- And those suggested improvements can be made at some future date without
- altering the state at all!
- Avoid heavy logic and programming
- `````````````````````````````````
- Jinja is not Python. It was made by Python programmers and shares many
- semantics and some syntax but it does not allow for abitrary Python function
- calls or Python imports. Jinja is a fast and efficient templating language but
- the syntax can be verbose and visually noisy.
- Once Jinja use within an sls file becomes slightly complicated -- long chains
- of if-elif-elif-else statements, nested conditionals, complicated dictionary
- merges, wanting to use sets -- instead consider using a different Salt
- renderer, such as the Python renderer. As a rule of thumb, if it is hard to
- read it will be hard to maintain -- switch to a format that is easier to read.
- Using alternate renderers is very simple to do using Salt's "she-bang" syntax
- at the top of the file. The Python renderer must simply return the correct
- :ref:`highstate data structure <states-highstate-example>`. The following
- example is a state tree of two sls files, one simple and one complicated.
- ``/srv/salt/top.sls``:
- .. code-block:: yaml
- base:
- '*':
- - common_configuration
- - roles_configuration
- ``/srv/salt/common_configuration.sls``:
- .. code-block:: yaml
- common_users:
- user.present:
- - names:
- - larry
- - curly
- - moe
- ``/srv/salt/roles_configuration``:
- .. code-block:: python
- #!py
- def run():
- list_of_roles = set()
- # This example has the minion id in the form 'web-03-dev'.
- # Easily access the grains dictionary:
- try:
- app, instance_number, environment = __grains__["id"].split("-")
- instance_number = int(instance_number)
- except ValueError:
- app, instance_number, environment = ["Unknown", 0, "dev"]
- list_of_roles.add(app)
- if app == "web" and environment == "dev":
- list_of_roles.add("primary")
- list_of_roles.add("secondary")
- elif app == "web" and environment == "staging":
- if instance_number == 0:
- list_of_roles.add("primary")
- else:
- list_of_roles.add("secondary")
- # Easily cross-call Salt execution modules:
- if __salt__["myutils.query_valid_ec2_instance"]():
- list_of_roles.add("is_ec2_instance")
- return {
- "set_roles_grains": {
- "grains.present": [{"name": "roles"}, {"value": list(list_of_roles)},],
- },
- }
- Jinja Macros
- ````````````
- In Salt sls files Jinja macros are useful for one thing and one thing only:
- creating mini templates that can be reused and rendered on demand. Do not fall
- into the trap of thinking of macros as functions; Jinja is not Python (see
- above).
- Macros are useful for creating reusable, parameterized states. For example:
- .. code-block:: jinja
- {% macro user_state(state_id, user_name, shell='/bin/bash', groups=[]) %}
- {{ state_id }}:
- user.present:
- - name: {{ user_name }}
- - shell: {{ shell }}
- - groups: {{ groups | json() }}
- {% endmacro %}
- {% for user_info in salt.pillar.get('my_users', []) %}
- {{ user_state('user_number_' ~ loop.index, **user_info) }}
- {% endfor %}
- Macros are also useful for creating one-off "serializers" that can accept a
- data structure and write that out as a domain-specific configuration file. For
- example, the following macro could be used to write a php.ini config file:
- ``/srv/salt/php.sls``:
- .. code-block:: jinja
- php_ini:
- file.managed:
- - name: /etc/php.ini
- - source: salt://php.ini.tmpl
- - template: jinja
- - context:
- php_ini_settings: {{ salt.pillar.get('php_ini', {}) | json() }}
- ``/srv/pillar/php.sls``:
- .. code-block:: yaml
- php_ini:
- PHP:
- engine: 'On'
- short_open_tag: 'Off'
- error_reporting: 'E_ALL & ~E_DEPRECATED & ~E_STRICT'
- ``/srv/salt/php.ini.tmpl``:
- .. code-block:: jinja
- {% macro php_ini_serializer(data) %}
- {% for section_name, name_val_pairs in data.items() %}
- [{{ section_name }}]
- {% for name, val in name_val_pairs.items() -%}
- {{ name }} = "{{ val }}"
- {% endfor %}
- {% endfor %}
- {% endmacro %}
- ; File managed by Salt at <{{ source }}>.
- ; Your changes will be overwritten.
- {{ php_ini_serializer(php_ini_settings) }}
- Abstracting static defaults into a lookup table
- -----------------------------------------------
- Separate data that a state uses from the state itself to increases the
- flexibility and reusability of a state.
- An obvious and common example of this is platform-specific package names and
- file system paths. Another example is sane defaults for an application, or
- common settings within a company or organization. Organizing such data as a
- dictionary (aka hash map, lookup table, associative array) often provides a
- lightweight namespacing and allows for quick and easy lookups. In addition,
- using a dictionary allows for easily merging and overriding static values
- within a lookup table with dynamic values fetched from Pillar.
- A strong convention in Salt Formulas is to place platform-specific data, such
- as package names and file system paths, into a file named :file:`map.jinja`
- that is placed alongside the state files.
- The following is an example from the MySQL Formula.
- The :py:func:`grains.filter_by <salt.modules.grains.filter_by>` function
- performs a lookup on that table using the ``os_family`` grain (by default).
- The result is that the ``mysql`` variable is assigned to a *subset* of
- the lookup table for the current platform. This allows states to reference, for
- example, the name of a package without worrying about the underlying OS. The
- syntax for referencing a value is a normal dictionary lookup in Jinja, such as
- ``{{ mysql['service'] }}`` or the shorthand ``{{ mysql.service }}``.
- :file:`map.jinja`:
- .. code-block:: jinja
- {% set mysql = salt['grains.filter_by']({
- 'Debian': {
- 'server': 'mysql-server',
- 'client': 'mysql-client',
- 'service': 'mysql',
- 'config': '/etc/mysql/my.cnf',
- 'python': 'python-mysqldb',
- },
- 'RedHat': {
- 'server': 'mysql-server',
- 'client': 'mysql',
- 'service': 'mysqld',
- 'config': '/etc/my.cnf',
- 'python': 'MySQL-python',
- },
- 'Gentoo': {
- 'server': 'dev-db/mysql',
- 'client': 'dev-db/mysql',
- 'service': 'mysql',
- 'config': '/etc/mysql/my.cnf',
- 'python': 'dev-python/mysql-python',
- },
- }, merge=salt['pillar.get']('mysql:lookup')) %}
- Values defined in the map file can be fetched for the current platform in any
- state file using the following syntax:
- .. code-block:: jinja
- {% from "mysql/map.jinja" import mysql with context %}
- mysql-server:
- pkg.installed:
- - name: {{ mysql.server }}
- service.running:
- - name: {{ mysql.service }}
- Organizing Pillar data
- ``````````````````````
- It is considered a best practice to make formulas expect **all**
- formula-related parameters to be placed under second-level ``lookup`` key,
- within a main namespace designated for holding data for particular
- service/software/etc, managed by the formula:
- .. code-block:: yaml
- mysql:
- lookup:
- version: 5.7.11
- Collecting common values
- ````````````````````````
- Common values can be collected into a *base* dictionary. This
- minimizes repetition of identical values in each of the
- ``lookup_dict`` sub-dictionaries. Now only the values that are
- different from the base must be specified by the alternates:
- :file:`map.jinja`:
- .. code-block:: jinja
- {% set mysql = salt['grains.filter_by']({
- 'default': {
- 'server': 'mysql-server',
- 'client': 'mysql-client',
- 'service': 'mysql',
- 'config': '/etc/mysql/my.cnf',
- 'python': 'python-mysqldb',
- },
- 'Debian': {
- },
- 'RedHat': {
- 'client': 'mysql',
- 'service': 'mysqld',
- 'config': '/etc/my.cnf',
- 'python': 'MySQL-python',
- },
- 'Gentoo': {
- 'server': 'dev-db/mysql',
- 'client': 'dev-db/mysql',
- 'python': 'dev-python/mysql-python',
- },
- },
- merge=salt['pillar.get']('mysql:lookup'), base='default') %}
- Overriding values in the lookup table
- `````````````````````````````````````
- Allow static values within lookup tables to be overridden. This is a simple
- pattern which once again increases flexibility and reusability for state files.
- The ``merge`` argument in :py:func:`filter_by <salt.modules.grains.filter_by>`
- specifies the location of a dictionary in Pillar that can be used to override
- values returned from the lookup table. If the value exists in Pillar it will
- take precedence.
- This is useful when software or configuration files is installed to
- non-standard locations or on unsupported platforms. For example, the following
- Pillar would replace the ``config`` value from the call above.
- .. code-block:: yaml
- mysql:
- lookup:
- config: /usr/local/etc/mysql/my.cnf
- .. note:: Protecting Expansion of Content with Special Characters
- When templating keep in mind that YAML does have special characters for
- quoting, flows, and other special structure and content. When a Jinja
- substitution may have special characters that will be incorrectly parsed by
- YAML care must be taken. It is a good policy to use the ``yaml_encode`` or
- the ``yaml_dquote`` Jinja filters:
- .. code-block:: jinja
- {%- set foo = 7.7 %}
- {%- set bar = none %}
- {%- set baz = true %}
- {%- set zap = 'The word of the day is "salty".' %}
- {%- set zip = '"The quick brown fox . . ."' %}
- foo: {{ foo|yaml_encode }}
- bar: {{ bar|yaml_encode }}
- baz: {{ baz|yaml_encode }}
- zap: {{ zap|yaml_encode }}
- zip: {{ zip|yaml_dquote }}
- The above will be rendered as below:
- .. code-block:: yaml
- foo: 7.7
- bar: null
- baz: true
- zap: "The word of the day is \"salty\"."
- zip: "\"The quick brown fox . . .\""
- The :py:func:`filter_by <salt.modules.grains.filter_by>` function performs a
- simple dictionary lookup but also allows for fetching data from Pillar and
- overriding data stored in the lookup table. That same workflow can be easily
- performed without using ``filter_by``; other dictionaries besides data from
- Pillar can also be used.
- .. code-block:: jinja
- {% set lookup_table = {...} %}
- {% do lookup_table.update(salt.pillar.get('my:custom:data')) %}
- When to use lookup tables
- `````````````````````````
- The ``map.jinja`` file is only a convention within Salt Formulas. This greater
- pattern is useful for a wide variety of data in a wide variety of workflows.
- This pattern is not limited to pulling data from a single file or data source.
- This pattern is useful in States, Pillar and the Reactor, for example.
- Working with a data structure instead of, say, a config file allows the data to
- be cobbled together from multiple sources (local files, remote Pillar, database
- queries, etc), combined, overridden, and searched.
- Below are a few examples of what lookup tables may be useful for and how they
- may be used and represented.
- Platform-specific information
- .............................
- An obvious pattern and one used heavily in Salt Formulas is extracting
- platform-specific information such as package names and file system paths in
- a file named ``map.jinja``. The pattern is explained in detail above.
- Sane defaults
- .............
- Application settings can be a good fit for this pattern. Store default
- settings along with the states themselves and keep overrides and sensitive
- settings in Pillar. Combine both into a single dictionary and then write the
- application config or settings file.
- The example below stores most of the Apache Tomcat ``server.xml`` file
- alongside the Tomcat states and then allows values to be updated or augmented
- via Pillar. (This example uses the BadgerFish format for transforming JSON to
- XML.)
- ``/srv/salt/tomcat/defaults.yaml``:
- .. code-block:: yaml
- Server:
- '@port': '8005'
- '@shutdown': SHUTDOWN
- GlobalNamingResources:
- Resource:
- '@auth': Container
- '@description': User database that can be updated and saved
- '@factory': org.apache.catalina.users.MemoryUserDatabaseFactory
- '@name': UserDatabase
- '@pathname': conf/tomcat-users.xml
- '@type': org.apache.catalina.UserDatabase
- # <...snip...>
- ``/srv/pillar/tomcat.sls``:
- .. code-block:: yaml
- appX:
- server_xml_overrides:
- Server:
- Service:
- '@name': Catalina
- Connector:
- '@port': '8009'
- '@protocol': AJP/1.3
- '@redirectPort': '8443'
- # <...snip...>
- ``/srv/salt/tomcat/server_xml.sls``:
- .. code-block:: jinja
- {% import_yaml 'tomcat/defaults.yaml' as server_xml_defaults %}
- {% set server_xml_final_values = salt.pillar.get(
- 'appX:server_xml_overrides',
- default=server_xml_defaults,
- merge=True)
- %}
- appX_server_xml:
- file.serialize:
- - name: /etc/tomcat/server.xml
- - dataset: {{ server_xml_final_values | json() }}
- - formatter: xml_badgerfish
- The :py:func:`file.serialize <salt.states.file.serialize>` state can provide a
- shorthand for creating some files from data structures. There are also many
- examples within Salt Formulas of creating one-off "serializers" (often as Jinja
- macros) that reformat a data structure to a specific config file format. For
- example, look at the`Nginx vhosts`_ states or the `php.ini`_ file template.
- .. _`Nginx vhosts`: https://github.com/saltstack-formulas/nginx-formula/blob/5cad4512/nginx/ng/vhosts_config.sls
- .. _`php.ini`: https://github.com/saltstack-formulas/php-formula/blob/82e2cd3a/php/ng/files/php.ini
- Environment specific information
- ................................
- A single state can be reused when it is parameterized as described in the
- section below, by separating the data the state will use from the state that
- performs the work. This can be the difference between deploying *Application X*
- and *Application Y*, or the difference between production and development. For
- example:
- ``/srv/salt/app/deploy.sls``:
- .. code-block:: jinja
- {# Load the map file. #}
- {% import_yaml 'app/defaults.yaml' as app_defaults %}
- {# Extract the relevant subset for the app configured on the current
- machine (configured via a grain in this example). #}
- {% app = app_defaults.get(salt.grains.get('role')) %}
- {# Allow values from Pillar to (optionally) update values from the lookup
- table. #}
- {% do app_defaults.update(salt.pillar.get('myapp', {})) %}
- deploy_application:
- git.latest:
- - name: {{ app.repo_url }}
- - version: {{ app.version }}
- - target: {{ app.deploy_dir }}
- myco/myapp/deployed:
- event.send:
- - data:
- version: {{ app.version }}
- - onchanges:
- - git: deploy_application
- ``/srv/salt/app/defaults.yaml``:
- .. code-block:: yaml
- appX:
- repo_url: git@github.com/myco/appX.git
- target: /var/www/appX
- version: master
- appY:
- repo_url: git@github.com/myco/appY.git
- target: /var/www/appY
- version: v1.2.3.4
- Single-purpose SLS files
- ------------------------
- Each sls file in a Formula should strive to do a single thing. This increases
- the reusability of this file by keeping unrelated tasks from getting coupled
- together.
- As an example, the base Apache formula should only install the Apache httpd
- server and start the httpd service. This is the basic, expected behavior when
- installing Apache. It should not perform additional changes such as set the
- Apache configuration file or create vhosts.
- If a formula is single-purpose as in the example above, other formulas, and
- also other states can ``include`` and use that formula with :ref:`requisites`
- without also including undesirable or unintended side-effects.
- The following is a best-practice example for a reusable Apache formula. (This
- skips platform-specific options for brevity. See the full
- :formula_url:`apache-formula` for more.)
- .. code-block:: text
- # apache/init.sls
- apache:
- pkg.installed:
- [...]
- service.running:
- [...]
- # apache/mod_wsgi.sls
- include:
- - apache
- mod_wsgi:
- pkg.installed:
- [...]
- - require:
- - pkg: apache
- # apache/conf.sls
- include:
- - apache
- apache_conf:
- file.managed:
- [...]
- - watch_in:
- - service: apache
- To illustrate a bad example, say the above Apache formula installed Apache and
- also created a default vhost. The mod_wsgi state would not be able to include
- the Apache formula to create that dependency tree without also installing the
- unneeded default vhost.
- :ref:`Formulas should be reusable <extending-formulas>`. Avoid coupling
- unrelated actions together.
- .. _conventions-formula-parameterization:
- Parameterization
- ----------------
- *Parameterization is a key feature of Salt Formulas* and also for Salt
- States. Parameterization allows a single Formula to be reused across many
- operating systems; to be reused across production, development, or staging
- environments; and to be reused by many people all with varying goals.
- Writing states, specifying ordering and dependencies is the part that takes the
- longest to write and to test. Filling those states out with data such as users
- or package names or file locations is the easy part. How many users, what those
- users are named, or where the files live are all implementation details that
- **should be parameterized**. This separation between a state and the data that
- populates a state creates a reusable formula.
- In the example below the data that populates the state can come from anywhere
- -- it can be hard-coded at the top of the state, it can come from an external
- file, it can come from Pillar, it can come from an execution function call, or
- it can come from a database query. The state itself doesn't change regardless
- of where the data comes from. Production data will vary from development data
- will vary from data from one company to another, however the state itself stays
- the same.
- .. code-block:: jinja
- {% set user_list = [
- {'name': 'larry', 'shell': 'bash'},
- {'name': 'curly', 'shell': 'bash'},
- {'name': 'moe', 'shell': 'zsh'},
- ] %}
- {# or #}
- {% set user_list = salt['pillar.get']('user_list') %}
- {# or #}
- {% load_json "default_users.json" as user_list %}
- {# or #}
- {% set user_list = salt['acme_utils.get_user_list']() %}
- {% for user in list_list %}
- {{ user.name }}:
- user.present:
- - name: {{ user.name }}
- - shell: {{ user.shell }}
- {% endfor %}
- Configuration
- -------------
- Formulas should strive to use the defaults of the underlying platform, followed
- by defaults from the upstream project, followed by sane defaults for the
- formula itself.
- As an example, a formula to install Apache **should not** change the default
- Apache configuration file installed by the OS package. However, the Apache
- formula **should** include a state to change or override the default
- configuration file.
- Pillar overrides
- ----------------
- Pillar lookups must use the safe :py:func:`~salt.modules.pillar.get`
- and must provide a default value. Create local variables using the Jinja
- ``set`` construct to increase readability and to avoid potentially hundreds or
- thousands of function calls across a large state tree.
- .. code-block:: jinja
- {% from "apache/map.jinja" import apache with context %}
- {% set settings = salt['pillar.get']('apache', {}) %}
- mod_status:
- file.managed:
- - name: {{ apache.conf_dir }}
- - source: {{ settings.get('mod_status_conf', 'salt://apache/mod_status.conf') }}
- - template: {{ settings.get('template_engine', 'jinja') }}
- Any default values used in the Formula must also be documented in the
- :file:`pillar.example` file in the root of the repository. Comments should be
- used liberally to explain the intent of each configuration value. In addition,
- users should be able copy-and-paste the contents of this file into their own
- Pillar to make any desired changes.
- Scripting
- ---------
- Remember that both State files and Pillar files can easily call out to Salt
- :ref:`execution modules <all-salt.modules>` and have access to all the system
- grains as well.
- .. code-block:: jinja
- {% if '/storage' in salt['mount.active']() %}
- /usr/local/etc/myfile.conf:
- file:
- - symlink
- - target: /storage/myfile.conf
- {% endif %}
- Jinja macros to encapsulate logic or conditionals are discouraged in favor of
- :ref:`writing custom execution modules <writing-execution-modules>` in Python.
- Repository structure
- ====================
- A basic Formula repository should have the following layout:
- .. code-block:: text
- foo-formula
- |-- foo/
- | |-- map.jinja
- | |-- init.sls
- | `-- bar.sls
- |-- CHANGELOG.rst
- |-- LICENSE
- |-- pillar.example
- |-- README.rst
- `-- VERSION
- .. seealso:: :formula_url:`template-formula`
- The :formula_url:`template-formula` repository has a pre-built layout that
- serves as the basic structure for a new formula repository. Just copy the
- files from there and edit them.
- ``README.rst``
- --------------
- The README should detail each available ``.sls`` file by explaining what it
- does, whether it has any dependencies on other formulas, whether it has a
- target platform, and any other installation or usage instructions or tips.
- A sample skeleton for the ``README.rst`` file:
- .. code-block:: restructuredtext
- ===
- foo
- ===
- Install and configure the FOO service.
- **NOTE**
- See the full `Salt Formulas installation and usage instructions
- <https://docs.saltstack.com/en/latest/topics/development/conventions/formulas.html>`_.
- Available states
- ================
- .. contents::
- :local:
- ``foo``
- -------
- Install the ``foo`` package and enable the service.
- ``foo.bar``
- -----------
- Install the ``bar`` package.
- ``CHANGELOG.rst``
- -----------------
- The ``CHANGELOG.rst`` file should detail the individual versions, their
- release date and a set of bullet points for each version highlighting the
- overall changes in a given version of the formula.
- A sample skeleton for the `CHANGELOG.rst` file:
- :file:`CHANGELOG.rst`:
- .. code-block:: restructuredtext
- foo formula
- ===========
- 0.0.2 (2013-01-01)
- - Re-organized formula file layout
- - Fixed filename used for upstart logger template
- - Allow for pillar message to have default if none specified
- Versioning
- ----------
- Formula are versioned according to Semantic Versioning, https://semver.org/.
- .. note::
- Given a version number MAJOR.MINOR.PATCH, increment the:
- #. MAJOR version when you make incompatible API changes,
- #. MINOR version when you add functionality in a backwards-compatible manner, and
- #. PATCH version when you make backwards-compatible bug fixes.
- Additional labels for pre-release and build metadata are available as extensions
- to the MAJOR.MINOR.PATCH format.
- Formula versions are tracked using Git tags as well as the ``VERSION`` file
- in the formula repository. The ``VERSION`` file should contain the currently
- released version of the particular formula.
- Testing Formulas
- ================
- A smoke-test for invalid Jinja, invalid YAML, or an invalid Salt state
- structure can be performed by with the :py:func:`state.show_sls
- <salt.modules.state.show_sls>` function:
- .. code-block:: bash
- salt '*' state.show_sls apache
- Salt Formulas can then be tested by running each ``.sls`` file via
- :py:func:`state.apply <salt.modules.state.apply_>` and checking the output for
- the success or failure of each state in the Formula. This should be done for
- each supported platform.
- .. ............................................................................
- .. _`saltstack-formulas`: https://github.com/saltstack-formulas
|