What's new in Salt 3006 Sulfur LTS

17 minute read Updated:

Salt Sulfur

If you do not have time to dig into all the cool stuff that went in, just read the LTS, Onedir, and Security sections because they will affect you sooner or later. For a shorter summary, check out the official announcement.

What's new in Salt 3006 Sulfur: LTS releases, Onedir (Relenv) packages, Security, State system enhancements, salt-ssh, Grains, Pillar, Slack Bolt engine, Jinja, Windows.

LTS/STS release strategy

Salt 3006 is the first LTS release as defined in the new (Django-like) release strategy SEP. As with the previous release strategy change, the root cause is lack of development resources to provide security bug fixes and patches for all supported versions.

Instead of the previously expected release cadence of 3-4 months (declared since Salt Neon), we should see two releases per year - one LTS (long-term support) and one STS (short-term support). LTS will be supported for two years and is intended for those who want stability. STS will be supported for 6 months and is for users who want quicker access to the latest features.

For more details and support timeline graphs, please read the announcement blog post.

Dot zero version number

Salt Neon introduced the new versioning scheme that starts from 3000. That decision introduced a minor inconsistency - a major version does not have .0 while a minor one has the patch number. Then came SEP-33 that proposed to add the .0 back.

This SEP was implemented in PRs #62742 by Caleb Beard , #63256 by Megan Wilhite , #63638 by Pedro Algarvio , and #1879 by Gareth J. Greenaway

Relenv-based onedir packages

Starting in 3006, only onedir-based packages will be available. Furthermore, the 3006 onedir packages are built with the new Relenv tool (instead of PyInstaller, which apparently introduced too many compatibility problems). A relenv-based package is basically a relocatable virtualenv with bundled Python interpreter and other libraries (excluding system ones like glibc).

For more information, check out the Packaging and Relenv docs. Also note the following caveat.

Test suite improvements

The Salt Project made several improvements to the test suite that will speed up the testing process for new PRs, including changes to test decorators and the process for running local tests with Nox. These improvements will allow the core Salt team to selectively choose to run and re-run certain tests based on which tests are applicable to the pull request. These changes will speed up the contributing and pull request review process.

Other changes:

Native minion packages

With the release of Salt 3006, the Salt Project will no longer build the native minion packages (AIX, Arista, Juniper, Solaris 10, Solaris 11). Instead it is inviting co-maintainers for these GitHub repositories, and for the community to further develop the native minions. For more details see the announcement blog post.


Unprivileged Salt Master user

Salt Master for Linux now runs under the salt user and group (the packages will add the user: salt config option to the Salt Master config). For more details and some caveats related to permissions see the official release notes.

PRs #64037 and #64084 by Megan Wilhite

Restricted default salt-api capabilities

All netapi clients, which provide the functionality to salt-api, will now be disabled by default as a security precaution. If you use salt-api, you must add the new netapi_enable_clients option to your salt master config. This is a breaking change and the salt-api will not function without this new configuration option.

Configuration with all possible clients enabled:

  - local
  - local_async
  - local_batch
  - local_subset
  - runner
  - runner_async
  - ssh
  - wheel
  - wheel_async

Enabling all clients is not recommended - only enable the clients that provide the functionality required.

PR #63050 by Barney Sowood

State system

Global state conditions

If we want to check a standard condition in grains, we’d have to template around every state block in which we’d want to perform that check:

{% if grains.get("virtual_subtype") != "chroot" -%}
    - name: service_name
{% endif -%}

Using the new feature we only need to set a minion configuration option on the host:

  service: ["not G@virtual_subtype:chroot"]

…and then a standard state block such as:

    - name: service_name

…will not be run on a host which doesn’t meet the conditions for the state to be run:

          ID: manage_service
    Function: service.running
        Name: service_name
      Result: None
     Comment: Failed to meet global state conditions. State not called.
     Started: 13:30:47.823249
    Duration: 3.012 ms

PR #62717 by Nicholas Hughes


umask is now a global state argument, instead of only applying to cmd.* states.

    - umask: "077"
    - contents: |
        Hello there!

PR #57803 by Erik Johnson

State queue

The queue=true parameter to state module functions allows a new state run to be queued and wait for a run already in progress to complete before proceeding. However, there is currently no way to enable it by default and no limit to how many runs will wait in the queue. If state runs are being performed on an interval and the runs take longer than the interval time, the queue could stack up indefinitely until the machine “falls over”.

Now, it is possible to enable queuing for state runs by default via the state_queue minion configuration option. It can be set to true or to an integer, which will be checked against the current queue size. If the queue is greater than or equal to the maximum, then that specific run will be treated as if queue=false and return a conflict.

PR #63357 by Nicholas Hughes

State events

Add state_events option to state.highstate, state.apply, state.sls, state.sls_id to enable state events whilst applying states. This allows users to enable state_events on a per-use basis rather than having to enable them globally in the master config for all state runs.

PR #63316 by Barney Sowood

Event ID lists

Add ability for salt.wait_for_event orchestration state to handle lists of received event ID values (like the lost value in presence events):

    - name: salt/presence/change
    - id_list:
       - some_old_node
    - timeout: 3000
    - event_id: lost
   "data": {
      "new": [],
      "lost": [
      "_stamp': '2021-06-23T15:02:04.621354"
     "tag": "salt/presence/change"

PR #60443 by @jwyoungpm

Other state improvements

Salt SSH

SSH known_hosts roster

This feature grabs IPs and hostnames from a known_hosts file and generates a roster for salt-ssh command. To pass other parameters, use roster_defaults:

# /etc/salt/master.d/ssh.conf
roster: sshknownhosts
ssh_known_hosts_file: /home/joeblade/.ssh/known_hosts

   user: joeblade
   sudo: True

However, the roster targeting would fail horribly if the HashKnownHosts OpenSSH option is enabled:

% sudo salt-ssh '*' test.ping

    ssh: Could not resolve hostname |1|ppu19wy2tbgdtc6djhhbm3lssds=|yrekx8uqm6ia3kdcbcf+rgsps5s=: Name or service not known

PRs #51840 by Rémi Jouannet and #54679 by Gareth J. Greenaway

Other SSH improvements




Slack Bolt engine

A couple of changes in Salt Slack engine:

PRs #62957, #63095, and #63005 by Gareth J. Greenaway




ifelse function

The ifelse function is like a multi-level if-else statement. It was inspired by CFEngine’s ifelse function, which in turn was inspired by Oracle’s DECODE function. It must have an odd number of arguments (from 1 to N). The last argument is the default value, like the else clause in standard programming languages. Every pair of arguments before the last one is evaluated as a pair. If the first one evaluates true, then the second one is returned, as if you had used the first one in a compound match expression. Boolean values can also be used as the first item in a pair, as it will be translated to a match that will always match (”*“) or never match (“SALT_IFELSE_MATCH_NOTHING”) a target system.

This is essentially another way to express the match.filter_by functionality in a way that’s familiar to CFEngine or Oracle users. Consider using match.filter_by unless this function fits your workflow.

{{ ifelse('foo*', 'fooval', 'bar*', 'barval', 'defaultval', minion_id='bar03') }}

PR #62509 by Nicholas Hughes

ipwrap filter

From a string, list, or tuple, returns any IPv6 addresses wrapped in square brackets:


{{ ['', 'foo', 'bar', 'fe80::', '2001:db8::1/64'] | ipwrap }}


["", "foo", "bar", "[fe80::]", "[2001:db8::1]/64"]

PR #61933 by Gary T. Giesen


Other notable changes

You can find other changes and bugfixes in the official CHANGELOG.md and Release Notes