First steps in Salt, and Salt vs. Chef

August 12, 2018
salt chef config management

Credits

First off, I want to thank my friend Wes Whetstone for giving me feedback on this post before I published it.

He told me I was holding it (Salt) wrong and to stop trying to make Salt act like Chef. He was very respectful in this criticism and he encouraged me to rewrite the code using custom state modules and custom execution modules to make it more proper Salt code.

We both thought that it would great for me to publish this original post before updating the code and then later make a part 2 after updating the code to show the journey/iterations. So look out for a part 2 coming soon!


Background

Over the past few months, I have been experimenting with Salt on and off with the eventual goal of using it to configure Windows workstations and servers. And recently I got a new laptop at work, so I figured I would start writing Salt states to configure my own settings.

Normally I would be biased towards Chef, as it was the first CM (configuration management) tool I used and I just generally prefer it. However, I’m going to give Salt a try. Wes swears by it.

Starting down the Salt path

Picking something to manage: APM

Even though managing APM (Atom Package Manager) might not seem like an intuitive place to start with config management on workstations, I believe it will be a helpful for talking about Salt in general. So even if you don’t want/need to manage APM, you hopefully will learn stuff about Salt & Chef. So continue reading! :)

Salt State Modules: cmd.run

Atom provides a command line utility for managing APM packages for folks who prefer command line utilities over GUIs. As far as I know, there is no “API”-ish way to access APM. So, we must shell out to the APM cli tool. In Salt, you can interact with arbitrary binaries and cli tools via a state module called cmd.run.

So managing APM in Salt could look something like my code below.

My APM Salt State

This is my first, incomplete stab (don’t rub salt in it) at managing APM packages with Salt. Thus far my state can install APM packages but it can’t do things like uninstall APM packages. Check out the code here.

#!py

def run():
    config = {}

    role = __salt__['grains.get']('role', None)
    apm_packages = __salt__['pillar.get']("apm_packages:" + str(role), None)

    for package in apm_packages:

        unless_value = 'if ([boolean](apm list | select-string %s)) { exit 0 } else { exit 1 }' % package

        config['install apm package: {0}'.format(package)] = {
            'cmd.run': [
                { 'name': 'apm install {0}'.format(package) },
                { 'shell': "powershell" },
                { 'unless': unless_value }
            ]
        }

    return config

What I’ve learned

Here are some things I learned about Salt through this process which I think are worth sharing. Hopefully these tips will help you as you start writing Salt. Disclaimer: These are opinionated.

(1) Salt Renderers: Python for the win!

I find it much easier to figure out what’s going with my Salt code when it’s written with the Py renderer instead of the default Jinja renderer.

Consider the following example Python vs. Jinja example:

'apm install {0}'.format(package) vs. {{ apm install %d | format(package) }}

It’s much more clear to me where the Powershell ends and the Python begins in the first example vs. where the Powershell ends and the Jinja begins in the second example. This point becomes more clear when you compare the documentation for each renderer.

(2) Salt Requisites: unless is inflexible

The Salt unless requisite is less flexible than the equivalent Chef, not_if, because you are required to pass it a string.

The string you pass will interpreted by the shell you specify, such as {'shell':"powershell"}, and Salt will get an exit code from the shell. This exit code determines whether not the state continues. Unfortunately, you cannot pass a Python expression to unless which evaluates to True or False: you must pass a string. From the Salt code above:

'if ([boolean](apm list | select-string %s)) { exit 0 } else { exit 1 }'

I found this frustrating compared to Chef, because in Chef you can simply specify a boolean value in Ruby:

stuff = powershell_out("command").stdout.include?("thing")
not_if { stuff }

The not_if is being passed stuff: a real, Ruby boolean value. This eliminates the need to write potentially complex exit code logic in Powershell.

For more context on this, consider this full Chef example in the next section.

What’s the lesson here? Test your Powershell thoroughly in the normal shell or with .\salt-call.bat –local cmd.run_all “expression” shell=powershell before deploying so that you know your Salt requisite does what you expect.

APM Chef Resource

This is the Chef equivalent to my APM Salt state above. While it may be a bit harder to read (+1 for Salt), due in part to the busyness of the syntax highlighting, I prefer this because it’s more condensed and requires less Powershell. Also, to reiterate a previous point, the boundaries between Ruby and Powershell are easier to see than the boundaries between Jinja and Powershell.


resource_name :bk_apm
default_action :run

action :run do

  apm_cmd = node['bk_apm']['cmd']
  node['bk_apm']['packages'].each do |package|

    apm_list = "& \"#{apm_cmd}\" list | select-string #{package}"
    execute "install apm package: #{package}" do
      command "\"#{apm_cmd}\" install #{package}"
      not_if { powershell_out(apm_list).stdout.include?(package) }
      action :run
    end

  end
end

Future adventures in Salt and Chef

Hopefully I’ll blog about more Salt and Chef stuff in the near future.

Please feel free to pull request any of my code, give me feedback, or let me know if I got something wrong. You can contact me via email or Slack, but Slack is better :)

Here are direct links to message me on MacAdmins Slack and the SaltStack Community Slack.