Python renderer that includes a Pythonic Object based interface
Evan Borgstrom <evan@borgstrom.ca>
Let's take a look at how you use pyobjects in a state file. Here's a quick
example that ensures the /tmp
directory is in the correct state.
1 #!pyobjects
2
3 File.managed("/tmp", user='root', group='root', mode='1777')
Nice and Pythonic!
By using the "shebang" syntax to switch to the pyobjects renderer we can now write our state data using an object based interface that should feel at home to python developers. You can import any module and do anything that you'd like (with caution, importing sqlalchemy, django or other large frameworks has not been tested yet). Using the pyobjects renderer is exactly the same as using the built-in Python renderer with the exception that pyobjects provides you with an object based interface for generating state data.
Pyobjects takes care of creating an object for each of the available states on
the minion. Each state is represented by an object that is the CamelCase
version of its name (i.e. File
, Service
, User
, etc), and these
objects expose all of their available state functions (i.e. File.managed
,
Service.running
, etc).
The name of the state is split based upon underscores (_
), then each part
is capitalized and finally the parts are joined back together.
Some examples:
postgres_user
becomes PostgresUser
ssh_known_hosts
becomes SshKnownHosts
How about something a little more complex. Here we're going to get into the core of how to use pyobjects to write states.
1 #!pyobjects
2
3 with Pkg.installed("nginx"):
4 Service.running("nginx", enable=True)
5
6 with Service("nginx", "watch_in"):
7 File.managed("/etc/nginx/conf.d/mysite.conf",
8 owner='root', group='root', mode='0444',
9 source='salt://nginx/mysite.conf')
The objects that are returned from each of the magic method calls are setup to
be used a Python context managers (with
) and when you use them as such all
declarations made within the scope will automatically use the enclosing
state as a requisite!
The above could have also been written use direct requisite statements as.
1 #!pyobjects
2
3 Pkg.installed("nginx")
4 Service.running("nginx", enable=True, require=Pkg("nginx"))
5 File.managed("/etc/nginx/conf.d/mysite.conf",
6 owner='root', group='root', mode='0444',
7 source='salt://nginx/mysite.conf',
8 watch_in=Service("nginx"))
You can use the direct requisite statement for referencing states that are generated outside of the current file.
1 #!pyobjects
2
3 # some-other-package is defined in some other state file
4 Pkg.installed("nginx", require=Pkg("some-other-package"))
The last thing that direct requisites provide is the ability to select which of the SaltStack requisites you want to use (require, require_in, watch, watch_in, use & use_in) when using the requisite as a context manager.
1 #!pyobjects
2
3 with Service("my-service", "watch_in"):
4 ...
The above example would cause all declarations inside the scope of the context
manager to automatically have their watch_in
set to
Service("my-service")
.
To include other states use the include()
function. It takes one name per
state to include.
To extend another state use the extend()
function on the name when creating
a state.
1 #!pyobjects
2
3 include('http', 'ssh')
4
5 Service.running(extend('apache'),
6 watch=[File('/etc/httpd/extra/httpd-vhosts.conf')])
Like any Python project that grows you will likely reach a point where you want to create reusability in your state tree and share objects between state files, Map Data (described below) is a perfect example of this.
To facilitate this Python's import
statement has been augmented to allow
for a special case when working with a Salt state tree. If you specify a Salt
url (salt://...
) as the target for importing from then the pyobjects
renderer will take care of fetching the file for you, parsing it with all of
the pyobjects features available and then place the requested objects in the
global scope of the template being rendered.
This works for all types of import statements; import X
,
from X import Y
, and from X import Y as Z
.
1 #!pyobjects
2
3 import salt://myfile.sls
4 from salt://something/data.sls import Object
5 from salt://something/data.sls import Object as Other
See the Map Data section for a more practical use.
Caveats:
Imported objects are ALWAYS put into the global scope of your template, regardless of where your import statement is.
In the spirit of the object interface for creating state data pyobjects also
provides a simple object interface to the __salt__
object.
A function named salt
exists in scope for your sls files and will dispatch
its attributes to the __salt__
dictionary.
The following lines are functionally equivalent:
1 #!pyobjects
2
3 ret = salt.cmd.run(bar)
4 ret = __salt__['cmd.run'](bar)
Pyobjects provides shortcut functions for calling pillar.get
,
grains.get
, mine.get
& config.get
on the __salt__
object. This
helps maintain the readability of your state files.
Each type of data can be access by a function of the same name: pillar()
,
grains()
, mine()
and config()
.
The following pairs of lines are functionally equivalent:
1 #!pyobjects
2
3 value = pillar('foo:bar:baz', 'qux')
4 value = __salt__['pillar.get']('foo:bar:baz', 'qux')
5
6 value = grains('pkg:apache')
7 value = __salt__['grains.get']('pkg:apache')
8
9 value = mine('os:Fedora', 'network.interfaces', 'grain')
10 value = __salt__['mine.get']('os:Fedora', 'network.interfaces', 'grain')
11
12 value = config('foo:bar:baz', 'qux')
13 value = __salt__['config.get']('foo:bar:baz', 'qux')
Pyobjects provides variable access to the minion options dictionary and the SLS name that the code resides in. These variables are the same as the opts and sls variables available in the Jinja renderer.
The following lines show how to access that information.
1 #!pyobjects
2
3 test_mode = __opts__["test"]
4 sls_name = __sls__
When building complex states or formulas you often need a way of building up a map of data based on grain data. The most common use of this is tracking the package and service name differences between distributions.
To build map data using pyobjects we provide a class named Map that you use to build your own classes with inner classes for each set of values for the different grain matches.
1 #!pyobjects
2
3 class Samba(Map):
4 merge = 'samba:lookup'
5 # NOTE: priority is new to 2017.7.0
6 priority = ('os_family', 'os')
7
8 class Ubuntu:
9 __grain__ = 'os'
10 service = 'smbd'
11
12 class Debian:
13 server = 'samba'
14 client = 'samba-client'
15 service = 'samba'
16
17 class RHEL:
18 __match__ = 'RedHat'
19 server = 'samba'
20 client = 'samba'
21 service = 'smb'
Note
By default, the os_family
grain will be used as the target for
matching. This can be overridden by specifying a __grain__
attribute.
If a __match__
attribute is defined for a given class, then that value
will be matched against the targeted grain, otherwise the class name's
value will be be matched.
Given the above example, the following is true:
Minions with an os_family
of Debian will be assigned the
attributes defined in the Debian class.
Minions with an os
grain of Ubuntu will be assigned the
attributes defined in the Ubuntu class.
Minions with an os_family
grain of RedHat will be assigned the
attributes defined in the RHEL class.
That said, sometimes a minion may match more than one class. For instance,
in the above example, Ubuntu minions will match both the Debian and
Ubuntu classes, since Ubuntu has an os_family
grain of Debian
and an os
grain of Ubuntu. As of the 2017.7.0 release, the order is
dictated by the order of declaration, with classes defined later overriding
earlier ones. Additionally, 2017.7.0 adds support for explicitly defining
the ordering using an optional attribute called priority
.
Given the above example, os_family
matches will be processed first,
with os
matches processed after. This would have the effect of
assigning smbd
as the service
attribute on Ubuntu minions. If the
priority
item was not defined, or if the order of the items in the
priority
tuple were reversed, Ubuntu minions would have a service
attribute of samba
, since os_family
matches would have been
processed second.
To use this new data you can import it into your state file and then access
your attributes. To access the data in the map you simply access the attribute
name on the base class that is extending Map. Assuming the above Map was in the
file samba/map.sls
, you could do the following.
1 #!pyobjects
2
3 from salt://samba/map.sls import Samba
4
5 with Pkg.installed("samba", names=[Samba.server, Samba.client]):
6 Service.running("samba", name=Samba.service)
This provides a wrapper for bare imports.
This loads our states into the salt __context__