The coolest features in Salt 2019.2 Fluorine 25 minute read Updated: Apr 27, 2019

It started as a series of tweets about a few exciting features I’ve found in Salt Fluorine git branch. Then the release was postponed from 2018.11 to 2019.2, and that gave me enough time to consolidate these tweets (and lots of other features) into a full-blown blog post. It is not a replacement for the official release notes, but I hope you will enjoy it!

UPDATE (2020.05.20): the 2019.2.5 bugfix release is available and is strongly recommended.

A list of the most exciting features in Salt 2019.2 Fluorine

Network automation

My network management skills are rusty, but I used to work in this field around 2004-2009. I had to deal with Cisco and Huawei gear, and the CLI experience was awful. However, I still remember my excitement when I got the chance to work with Juniper hardware and Junos in particular. It felt like proper Unix, had version control (with diffs!), atomic commit/rollback support and tons of other well-thought-out features.

Many configuration management tools are evolving in this direction, unifying change management processes across different networking equipment from different vendors. With Salt, it is possible to have version control, code reviews, automatic rollouts, and other nice features, even for network devices that do not support this out of the box. Things have improved a lot, so I ALMOST want to work in this field again 😀

There are lots of network-related features in Salt Fluorine. Below is a brief list, please refer to the docs for a complete description.

Details

Multiple SaltSSH versions

This is a clever hack to allow SaltSSH work across different major Python versions and use multiple versions of Salt. The only necessary condition is that your states should be compatible with all the versions you want to use.

To use SaltSSH across Python 2.7 - Python 3.x, you need to install Salt packages for both versions of the interpreter on a master machine. Using multiple Salt versions is trickier - to do that you need to install them into isolated folders, including all necessary dependencies:

ssh_ext_alternatives : 2016.3 : # Namespace, can be actually anything. py-version : [ 2 , 6 ] # Constraint to specific interpreter version path : /opt/ 2016.3 /salt # Main Salt installation dependencies : # List of dependencies and their installation paths jinja2 : /opt/jinja2 yaml : /opt/yaml tornado : /opt/tornado msgpack : /opt/msgpack certifi : /opt/certifi singledispatch : /opt/singledispatch.py singledispatch_helpers : /opt/singledispatch_helpers.py markupsafe : /opt/markupsafe backports_abc : /opt/backports_abc.py

Details

Terraform roster for SaltSSH

It looks like Salt has lost the battle for multi-cloud infrastructure provisioning (especially for non-VM resource types). It is hard to beat the vast amount of different providers in Terraform, its speed, excellent vendor support program and even things like fast GCP feature development using Magic Modules. So, the only sensible strategy left is to provide good interoperability with Terraform, in order to use both tools in tandem.

With the terraform-provider-salt you can keep your infrastructure-as-code self-contained in a single directory and use Terraform + salt-ssh combo to provision and configure your nodes:

$ terraform apply $ salt-ssh '*' state.highstate

This is done by creating a mapping between the Terraform resources and salt-ssh roster entries:

resource "libvirt_domain" "domain" { name = "domain-${count.index}" memory = 1024 disk { volume_id = "${element(libvirt_volume.volume.*.id, count.index)}" } network_interface { network_name = "default" hostname = "minion${count.index}" wait_for_lease = 1 } cloudinit = "${libvirt_cloudinit.init.id}" count = 2 } resource "salt_host" "example" { host = "$ { libvirt_domain . domain . network_interface . 0 . addresses . 0 } }

Also, you can use local_file Terraform resource to render simple pillar files:

resource "local_file" "pillar_database_cluster" { filename = "${path.module}/srv/pillar/terraform_database_cluster.sls" content = <<EOF terraform: database_master_ip: $ { salt_host . master . host } EOF }

NOTE: please do not confuse the terraform-provider-salt with the built-in salt-masterless Terraform provisioner. The latter one connects to a node via ssh, syncs pillar/state trees, runs the salt-bootstrap.sh script and then uses the salt-call --local command to apply a state.

Details

Ansible playbooks and inventory

Ansible is definitely more popular, but people still switch to Salt. The reasons can be very different, such as infrastructure growth, unacceptable playbook running time, a need to react to infrastructure events automatically, and even the existential fear of IBM. As with Terraform, the right strategy is to simplify interoperability and increase the ease of switching.

Salt.modules.ansiblegate was first introduced in Salt 2018.3. In Salt Fluorine it gained the ability to run Ansible playbooks (both from states and orchestration jobs).

Also from now on Salt uses ansible-inventory command internally to retrieve a roster.

Details

CLI exit codes

This feature is not very visible from a user perspective but is quite essential. It streamlines many edge cases in CLI exit codes and can simplify your scripts or CI workflows.

In addition to the table above:

A non-zero exit code will be set when minions fail to return

For a multi-module salt call, the highest return value will be used to determine if it failed

Salt-call exit code is no longer ignored by salt-ssh

As a bonus, it is possible to override a retcode from cmd.run and other cmd functions, by specifying the success_retcodes parameter

% salt-call --local cmd.run cmd = 'return 42' ; echo $? [ ERROR ] Command 'return 42' failed with return code: 42 [ ERROR ] retcode: 42 [ ERROR ] Command 'return 42' failed with return code: 42 [ ERROR ] output: local: 1 % salt-call --local cmd.run cmd = 'return 42' success_retcodes = '[42]' ; echo $? local: 0

Details

Loadable matchers

When you read the Salt targeting documentation, it is not immediately obvious that minions are responsible for matching themselves against a target specification. Every minion looks at the targeting data broadcasted by the salt-master via ZeroMQ pub/sub messaging channel and decides if they match it (NOTE: this process is more nuanced if you have zmq_filtering enabled).

In Salt Fluorine all matchers were moved to the separate module namespace and can be dynamically loaded. It is not possible to create a new matcher, because the required CLI plumbing does not exist yet. For now, you can only override any existing matcher by placing a file into the _matchers directory in your file_roots and propagating it with saltutil.sync_matchers :

# list_matcher.py from __future__ import absolute_import , print_function , unicode_literals from salt.ext import six def match ( self , tgt ): ''' Determines if this host is on the list ''' if isinstance ( tgt , six . string_types ): # The stock matcher splits on `,`. Change to `/` below. tgt = tgt . split ( '/' ) return bool ( self . opts [ 'id' ] in tgt )

Details

Loadable serializers

Add your own custom serializers (XML, anyone?) via Salt dynamic module distribution mechanism. You can use them to manage domain-specific file formats, such as configuration files.

Example serializer (put this into salt://_serializers/hcl.py ):

"""HCL deserializer.""" try : import hcl # https://github.com/virtuald/pyhcl available = True except ImportError : available = False from salt.serializers import DeserializationError from salt.ext import six __all__ = [ 'deserialize' , 'available' ] def deserialize ( stream_or_string , ** options ): try : if not isinstance ( stream_or_string , ( bytes , six . string_types )): return hcl . load ( stream_or_string ) if isinstance ( stream_or_string , bytes ): stream_or_string = stream_or_string . decode ( 'utf-8' ) return hcl . loads ( stream_or_string ) except Exception as e : raise DeserializationError ( e ) def serialize ( obj , ** options ): raise NotImplementedError ( "pyhcl can only serialize to json" )

# salt/hcl.sls { % load_text as hcl_example % } variable "ami" { description = "the AMI to use" } resource "aws_instance" "web" { ami = "${var.ami}" count = 2 source_dest_check = false connection { user = "root" } } { % endload % } { % do salt . log . warning ( salt [ 'slsutil . deserialize' ]( 'hcl' , hcl_example )) % }

To sync serializers from salt://_serializers run the salt '*' saltutil.sync_serializers command:

% salt-call --local saltutil.sync_serializers local: - serializers.hcl % salt-call --local state.apply hcl [ WARNING ] { "resource" : { "aws_instance" : { "web" : { "ami" : " ${ var .ami } " , "connection" : { "user" : "root" } , "count" : 2 , "source_dest_check" : false }}} , "variable" : { "ami" : { "description" : "the AMI to use" }}}

Details

Docker proxy minion

This adds the new minion type called docker , which uses docker.call to run Salt modules in Docker containers without installing Salt in the container. Also, it can run state.sls , state.apply and state.highstate in the Docker container.

Details

Other Docker improvements

Using proxy minion from the CLI

This is a huge time saver for the real men who write their own device drivers people who write and troubleshoot their own proxy minions. It allows you to use proxy minions from the minion box without a master:

% salt-call --local --proxyid PROXY_ID test.ping

This salt-call console command does basically the same thing as the separate salt-proxy --proxyid PROXY_ID process, but just for a single call. It also nicely complements the above-mentioned Docker proxy minion feature.

Details

Grains in grains

When writing custom grains, sometimes it is useful to reuse the data from already existing grain. It is hard to do because the grain evaluation order is nondeterministic, but this restriction has been partially lifted. For custom grains, if the function accepts an argument named grains , then the previously rendered core grains will be passed in as a dictionary.

import re def if_count ( grains ): """Return number of interfaces excluding the loopback.""" return { 'if_count' : len ([ i for i in grains [ 'ip_interfaces' ] if i != 'lo' ]) } def role ( grains ): """Extract a server role from FQDN like env-role-NN.domain.tld.""" match = re . match ( '^[a-z0-9]+-([a-z0-9]+)-[0-9]+\.[a-z0-9-]+\.[a-z]+$' , grains [ 'fqdn' ] ) return { 'role' : match . group ( 1 )} if match else {}

Details

SaltSSH key password

Previously, using ssh-agent was the only way to use password-protected SSH keys with salt-ssh . Now, the private key passphrase can be set in 3 different places:

--priv-passwd argument for salt-ssh command

argument for command ssh_priv_passwd master config option

master config option priv_passwd in a salt-ssh roster entry

Details

Configurable module environment

On the surface, this feature allows you to specify arbitrary environment variables for aptpkg , yumpkg and zypper execution modules. Under the hood, it is much more interesting, because it allows you to specify custom environment variables for shell commands executed by Salt modules (states, execution modules, returners, etc.). To do that, you need to add something like this to either grains or pillars:

system-environment : <type> : # Type of the module (states, modules, etc.). <module> : # Module name _ : # Namespace for all functions in the module <VAR> : "value" <function> : # Namespace only for particular function in the module <VAR> : "value"

The configuration is quite granular: it allows you to define variables for any module type, module name and even for a specific function. The only caveat is that the environment won’t be applied to all existing modules automatically. It requires explicit module-level support. To do that, you need to call salt.utils.environment.get_module_environment(globals(), function=None) in your module and pass the resulting environment to __salt__['cmd.run'] or __salt__['cmd.run_all'] .

Details

Wtmp/btmp beacon improvements

action key has been added to the events fired by wtmp beacon, which will contain either the string login or logout

key has been added to the events fired by beacon, which will contain either the string or Added configurable ut_type to distinguish between login and logout events

to distinguish between login and logout events Better support of posting failed logins to Slack via reactor

Group support in the btmp and wtmp beacons

Details

Cross platform file monitoring

It is similar to the inotify beacon, with the main difference that it uses the cross-platform watchdog module and works on Linux 2.6+, Mac OS X, BSD Unix variants, Windows Vista and later and also has an OS-independent polling mechanism.

beacons : watchdog : - directories : /path/to/dir : mask : - create - modify - delete - move

Details

Redis SDB module

Initially, SDB modules in Salt were designed to store secrets, as an alternative to pillar files. Redis does not look like a good option to store sensitive data (as compared to Vault and some of the other SDB modules). However, if you want to store more dynamic data across different masters and minions and then use it in your states, then Redis looks like a good solution. Also, there is the Redis cache backend and also Redis returner, which you can query using the SDB module.

Details

Google Chat module

{% if pillar.get( 'sadserver_scheduled_run' , False) %} sadserver_quote : # NOTE: Put use_superseded: ['module.run'] into the minion config module.run : - google_chat.send_message : # https://developers.google.com/hangouts/chat/how-tos/webhooks - url : "https://chat.googleapis.com/v1/spaces/XXX/messages?key=YYY&token=ZZZ" - message : {{ salt [ 'cmd.run' ] ( 'python3 -c \'import json, random, sys, urllib.request; sys.stdout.buffer.write(("```{}```".format(random.choice(json.loads(urllib.request.urlopen("https://brokenco.de/files/sadserver.json").read().decode("utf-8")))["full_text"].strip())).encode("utf-8"))\'' ) | yaml_encode }} # Also try https://brokenco.de/files/sadoperator.json {% else %} sadserver_morning_schedule : schedule.present : - function : state.sls_id - job_args : - sadserver_quote - {{ sls }} - job_kwargs : pillar : sadserver_scheduled_run : True - when : - Monday 9 : 00am - Tuesday 9 : 00am - Wednesday 9 : 00am - Thursday 9 : 00am - Friday 9 : 00am {% endif %}

Details

SMTP attachments

With the addition of the new parameter named attachments , you can send local files over email by using smtp state/module. One obvious example is sending out generated client certificates:

{% set email= 'user@example.com' %} {% set filename = email.replace( '@' , '-' ).replace( '.' , '-' ) %} email-openvpn-config-for-{{ filename }} : smtp.send_msg : - name : | Your personal OpenVPN configuration file is attached below: - subject : 'Your OpenVPN profile' - recipient : {{ email }} - sender : admin@example.com - profile : smtp-credentials - attachments : - /etc/pki/openvpn/{{ filename }}.ovpn - onchanges : - file : /etc/pki/openvpn/{{ filename }}.ovpn /etc/pki/openvpn/{{ filename }}.ovpn : file.managed : - mode : 0600 - template : jinja - defaults : ca : __slot__ : salt : file.read(/etc/pki/ca.crt) tls_crypt : __slot__ : salt : file.read(/etc/pki/static.key) cert : __slot__ : salt : file.read(/etc/pki/issued/{{ filename }}.crt) key : __slot__ : salt : file.read(/etc/pki/private/{{ filename }}.key) - source : salt : //openvpn/files/config.ovpn.j2

# salt/openvpn/files/config.ovpn.j2 client dev tun remote openvpn.example.com 1194 udp connect-retry-max 3 resolv-retry 5 tls-timeout 10 nobind persist-key persist-tun explicit-exit-notify verb 3 remote-cert-tls server <ca> {{ ca }} </ca> <cert> {{ cert }} </cert> <key> {{ key }} </key> <tls-crypt> {{ tls_crypt }} </tls-crypt>

Details

Jira integration

Writing a terraform provider to create the JIRA tickets needed to provision a new machine in the datacenter.



Am I devopsing right? — DevSadOps (@sadoperator) July 1, 2016

This is an execution module to manipulate Jira tickets using the Jira python client library. It could be used to automatically raise Jira tickets when something happens and supports the following operations:

create_issue

assign_issue

add_comment

check the issue_closed status

There is no corresponding state module, so if you want to use it in a state, you need to use the module.run function.

Details

KMS decryption renderer

The new renderer that utilizes AWS Key Management Service to decrypt pillar values using Fernet symmetric encryption. To use it you need to specify KMS credentials and then include your ciphertexts in a pillar file:

#!yaml|aws_kms a-secret : gAAAAABaj5uzShPI3PEz6nL5Vhk2eEHxGXSZj8g71B84CZsVjAAt

Please note, that the aws_kms renderer will attempt to decrypt each pillar value recursively (only string types). You probably want to use it sparingly and for small pillar files.

Details

Enhancements in package modules

Various enhancements for the FreeBSD pkgng execution module

New package locking functions: pkg.lock , pkg.unlock , pkg.locked , pkg.list_locked

, , , Add pkg.list_upgrades for compatibility with pkg.uptodate state

for compatibility with state Support pkgs keyword in pkg.upgrade for compatibility with pkg.uptodate state

keyword in for compatibility with state Add pkg.hold and pkg.unhold for compatibility with pkg.installed state

and for compatibility with state Add clean_all and dryrun options

Details

Other enhancements

New Azure ARM modules

Huge improvements in Azure ARM execution and state modules:

Azure GovCloud support

Cloud module can now hit endpoints for any Microsoft hosted endpoint or custom Azure Stack endpoint

General improvements

Custom scripts can now be passed via VM extensions

Fixed functions to work from the command line in order to list various resources accessible to the cloud provider configuration

Start/stop actions

Compute module/state

Manage availability sets

Capture a VM to create a template

Convert virtual machine disks from blob-based to managed disks

Manage virtual machines (deallocate, generalize, power off, start, restart, redeploy)

Network module/state

Check domain name availability

Manage public IP addresses

Manage security rules

Manage network security groups

Manage virtual networks and subnets

Manage load balancers

List subscription network usage

Manage network interfaces

Manage route filter rules, routes, and route tables

Resource module/state

Manage resource groups

Manage deployments and deployment operations

List subscriptions and locations

List tenants

manage policy assignments and definitions

Details

Cloud map improvements

The first feature allows you to override providers in map nodes (previously it was only possible to specify a provider in the cloud profile):

webapp : - node1 : provider : ny : openstack - node2 : provider : nj : openstack - node3 : provider : toronto : openstack

The second feature enables use of cloud map data sourced from the pillar ( salt-call cloud.map_run map_pillar=my-prd-map ):

cloud : maps : my-prd-map : named-profile- 01 : - named-instance- 01 - named-instance- 02

Details

Other cloud improvements

Windows runas improvements

Runas can launch processes on behalf of users without a password

Support for LOCAL SERVICE and NETWORK SERVICE system accounts

Runas can now use system accounts from salt-call (SYSTEM, LOCAL SERVICE, and NETWORK SERVICE)

Fix runas when running under winrm

Details

Windows Advanced Audit policies

This module allows you to view and modify the audit settings as they are applied on the machine (either set directly or via local or domain group policy). The audit settings are broken down into nine categories (and the default one named All ):

Account Logon

Account Management

Detailed Tracking

DS Access

Logon/Logoff

Object Access

Policy Change

Privilege Use

System

You can use the auditpol.get_settings , auditpol.get_setting and auditpol.set_setting functions in the following way:

# Get current state of all audit settings salt * auditpol.get_settings # Get the current state of all audit settings in the "Account Logon" # category salt * auditpol.get_settings category = "Account Logon" # Get current state of the "Credential Validation" setting salt * auditpol.get_setting name = "Credential Validation" # Set the state of the "Credential Validation" setting to Success and # Failure salt * auditpol.set_setting name = "Credential Validation" value = "Success and Failure" # Set the state of the "Credential Validation" setting to No Auditing salt * auditpol.set_setting name = "Credential Validation" value = "No Auditing"

Details

LXD module/state

This feature was extracted from the lxd-formula and requires the pylxd module to work.

lxd state to initialize the LXD daemon, manage config settings, authenticate to a remote peer

state to initialize the LXD daemon, manage config settings, authenticate to a remote peer lxd_container state to manage containers ( present , absent , running , stopped , frozen , migrated )

state to manage containers ( , , , , , ) lxd_image state to manage images ( present , absent )

state to manage images ( , ) lxd_profile state to manage profiles ( present , absent )

Details

Libvirt improvements

NI Linux RT improvements

SmartOS improvements

Docker image support in smartos.imgadm module/state

module/state Docker support for smartos.vm_present

Beacons for imgadm and vmadm

Details

GlusterFS improvements

Methods to get and set the GlusterFS op-version. PR #45091 by Robert Penberthy

Fix GlusterFS module for version 4.0 and above. PR #48222 by Martin Polreich

Ability to create “replica 3 arbiter 1” volumes. PR #48774 by @jdito

Vault improvements

Runner function to unseal Vault server

salt-run vault.unseal uses the keys from the fault configuration to unseal Vault server.

PR #46996 by Daniel Wallace

wrapped_token authentication method

A new wrapped_token authentication method that allows automatic unwrapping of vault wrapped tokens in the local vault configuration.

PR #47072 by Beorn Facchini

vault.write_raw function

This function allows you to write raw data to a vault:

salt '*' vault.write_raw "secret/my/secret" '{"user":"foo","password": "bar"}'

PR #47712 by @slaws

Named roles

An option to define named role for creation of vault tokens. User can define specific token named role for minion created tokens and explicitly define its behavior and access policies. Example: https://www.nomadproject.io/docs/vault-integration/index.html#vault-token-role-configuration.

PR #48586 by @astorath

Zabbix improvements

Zabbix inventory support. PR #45458 by Christian McHugh

State modules for Zabbix templates, actions and valuemaps. PR #47398 by @slivik

New state zabbix_user.admin_password_present . PR #47970 by @slivik

Nifty tricks

JID in the logs

This feature is super useful for debugging purposes because it allows you to grep the logs for a specific job ID. It is off by default, but I think you should enable it right away! Add the following lines to your master/minion config file (the relevant part is %(jid)s ):

log_fmt_logfile : '%(asctime)s,%(msecs)03d [%(name)-17s][%(levelname)-8s] %(jid)s %(message)s' log_level : info

Then you can use the JID to filter the log:

% sudo salt -- show - jid minion1 state . apply teststate jid : 20181128221721109828 minion1 : ---------- ID : test_notification Function : test . show_notification Result : True Comment : Hello there ! Started : 22 : 17 : 21.283097 Duration : 2.02 ms Changes : Summary for minion1 ------------ Succeeded : 1 Failed : 0 ------------ Total states run : 1 Total run time : 2.020 ms % sudo salt minion1 cmd . run 'grep "\[JID: 20181128221721109828\]" /var/log/salt/minion' minion1 : 2018-11-28 22 : 17 : 21 , 133 [ salt . minion ][ INFO ] [ JID : 20181128221721109828 ] Starting a new job 20181128221721109828 with PID 1955 2018-11-28 22 : 17 : 21 , 237 [ salt . state ][ INFO ] [ JID : 20181128221721109828 ] Loading fresh modules for state activity 2018-11-28 22 : 17 : 21 , 283 [ salt . state ][ INFO ] [ JID : 20181128221721109828 ] Running state [ test_notification ] at time 22 : 17 : 21.283098 2018-11-28 22 : 17 : 21 , 283 [ salt . state ][ INFO ] [ JID : 20181128221721109828 ] Executing state test . show_notification for [ test_notification ] 2018-11-28 22 : 17 : 21 , 284 [ salt . state ][ INFO ] [ JID : 20181128221721109828 ] Hello there ! 2018-11-28 22 : 17 : 21 , 285 [ salt . state ][ INFO ] [ JID : 20181128221721109828 ] Completed state [ test_notification ] at time 22 : 17 : 21.285117 ( duration_in_ms = 2.02 ) 2018-11-28 22 : 17 : 21 , 287 [ salt . minion ][ INFO ] [ JID : 20181128221721109828 ] Returning information for job : 20181128221721109828

Docs: Logging changes. PR #48660 by Gareth J. Greenaway

Per-state failhard

If you want to abort the state execution process when any single state fails, you can use global or per-state failhard option. Starting with Salt Fluorine, you can override the global failhard: True option by adding failhard: False to any state. This is also helpful if you want to use onfail , onfail_in or onfail_any state requisites because otherwise they are ignored.

PR #46448 by Herbert

Wildcard pillar includes

Previously it was only possible to use wildcards to include states, and now you can do the same for pillar (both in top.sls and individual pillar files via include ):

base : '*' : - users.*

PR #45269 by Richard W.

Nodegroup support for compound matcher

Use salt -C 'N@nodegroup' on the command line and compound nodegroup matchers in your top.sls (apparently, this requires putting nodegroups into both master and minion configuration files). PR #47421 by Matt Phillips

List the states that will be applied on highstate

% salt '*' state.show_states minion1: - base - timezone - ntp

Unlike the state.show_top , this function also shows included states.

PR #44475 by Petr Michalec

slsutil.merge_all function

The same as slsutil.merge , but can merge more than two dictionaries at once:

# pillar file {% set layer1 = { 'a' : 'a' , 'b' : 'b' , 'c' : 'c' } %} {% set layer2 = { 'a' : 'z' } %} {% set layer3 = { 'c' : 'x' } %} value: {{ salt [ 'slsutil.merge_all' ]([ layer1 , layer2 , layer3 ]) }}

% salt - call -- local pillar . get value local : ---------- a : z b : b c : x

PR #47679 by Tyler Couto

New functions in the defaults module

New defaults.deepcopy function

function New defaults.update function

function defaults.merge got the merge_lists and in_place arguments

PRs #44850, #44851 and #45051 by Ahmed M. AbouZaid

state.sls_exists function

The main use-case for this function is to conditionally include a state only if the corresponding file exists (for example, to apply per-role or per-host states automatically). Unlike the file.exists , it should work with gitfs too.

base: '*': - base {% set minion_state = 'minion.' ~ ( grains.id | replace ( '.' , '_' )) - %} {% if salt [ 'state.sls_exists' ]( minion_state ) - %} - {{ minion_state }} {% - endif %}

PR #45730 by Christian McHugh

file.tidied state

This is a port of Puppet tidy resource. It allows you to recursively remove unwanted files based on specific criteria. For example:

prepared-for-distribution : file.tidied : - name : /tmp/source - matches : - . *\.pyc - . *\.orig - . *\.rej - .DS_Store

PR #47718 by Dirk Heinrichs

Use unless and onlyif together

Allow the unless and onlyif requisites to both work when specified in a state file. Previously, if you tried to specify them simultaneously, onlyif would take precedence and the unless statement was ignored.

PR #45229 by Gareth J. Greenaway

Relative Jinja imports

Allow you to include Jinja templates using relative paths: {% from './foo' import bar %} . Quite useful for writing formulas or if you have a Jinja macro that is used across different states.

PR #47490 by @plastikos

config.items helper

This helper returns a complete config from the currently running minion process, including the default values.

# salt minion1 config.items minion1 : ---------- __cli : salt - minion __role : minion acceptance_wait_time : 10 acceptance_wait_time_max : 0 always_verify_signature : False append_minionid_config_dirs : auth_safemode : False auth_timeout : 5 auth_tries : 7 auto_accept : True autoload_dynamic_modules : True autosign_timeout : 120 ...

You can use it to find the difference between two minions (with the help of yamldiff ):

# yamldiff --file1 <(salt minion1 config.items --out yaml) \ --file2 <(salt minion2 config.items --out yaml) - minion1: { + minion2: { - uuid: "c6c51464-65cc-b044-94dd-927488c90cfc", + uuid: "538dce5d-228c-3d42-83ca-98ab6b999f83", - id: "minion1", + id: "minion2", - ipv6: true, + ipv6: false,

PR #48681 by @Arabus

Other notable features

Also, you can follow me on Twitter where I periodically post things like this: