martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 1 | ============================================================= |
| 2 | reclass — recursive external node classification |
| 3 | ============================================================= |
| 4 | reclass is © 2007–2013 martin f. krafft <madduck@madduck.net> |
| 5 | and available under the terms of the Artistic Licence 2.0 |
martin f. krafft | e39e890 | 2013-06-14 22:12:17 +0200 | [diff] [blame] | 6 | ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 7 | |
| 8 | reclass is an "external node classifier" (ENC) as can be used with automation |
martin f. krafft | ff1bae8 | 2013-07-04 07:52:30 +0200 | [diff] [blame] | 9 | tools, such as Puppet, Salt, and Ansible. It is also a stand-alone tool for |
| 10 | merging data sources recursively. |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 11 | |
| 12 | The purpose of an ENC is to allow a system administrator to maintain an |
| 13 | inventory of nodes to be managed, completely separately from the configuration |
| 14 | of the automation tool. Usually, the external node classifier completely |
martin f. krafft | 5ee69b3 | 2013-06-24 13:41:06 +0200 | [diff] [blame] | 15 | replaces the tool-specific inventory (such as site.pp for Puppet, |
martin f. krafft | 3924e89 | 2013-06-25 11:57:03 +0200 | [diff] [blame] | 16 | ext_pillar/master_tops for Salt, or /etc/ansible/hosts). |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 17 | |
martin f. krafft | 6223989 | 2013-06-14 20:03:59 +0200 | [diff] [blame] | 18 | reclass allows you to define your nodes through class inheritance, while |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 19 | always able to override details further up the tree (i.e. in more specific |
| 20 | nodes). Think of classes as feature sets, as commonalities between nodes, or |
| 21 | as tags. Add to that the ability to nest classes (multiple inheritance is |
| 22 | allowed, well-defined, and encouraged), and piece together your infrastructure |
| 23 | from smaller bits, eliminating redundancy and exposing all important |
| 24 | parameters to a single location, logically organised. |
martin f. krafft | 6223989 | 2013-06-14 20:03:59 +0200 | [diff] [blame] | 25 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 26 | In general, the ENC fulfills two jobs: |
| 27 | |
| 28 | - it provides information about groups of nodes and group memberships |
| 29 | - it gives access to node-specific information, such as variables |
| 30 | |
martin f. krafft | 5ee69b3 | 2013-06-24 13:41:06 +0200 | [diff] [blame] | 31 | In this document, you will find an overview of the concepts of reclass and the |
| 32 | way it works. Have a look at README.Salt and README.Ansible for information |
| 33 | about integration of reclass with these tools. |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 34 | |
martin f. krafft | d4833b3 | 2013-06-23 13:35:46 +0200 | [diff] [blame] | 35 | Installation |
| 36 | ~~~~~~~~~~~~ |
martin f. krafft | 012103e | 2013-07-03 20:02:02 +0200 | [diff] [blame] | 37 | Before you can use reclass, you need to install it into a place where Python |
| 38 | can find it. Unless you installed a package from your distribution, the |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 39 | following step should install the package to /usr/local: |
martin f. krafft | d4833b3 | 2013-06-23 13:35:46 +0200 | [diff] [blame] | 40 | |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 41 | $ python setup.py install |
martin f. krafft | 012103e | 2013-07-03 20:02:02 +0200 | [diff] [blame] | 42 | |
| 43 | If you want to install to a different location, use --prefix like so: |
| 44 | |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 45 | $ python setup.py install --prefix=/opt/local |
| 46 | |
| 47 | Just make sure that the destination is in the Python module search path, which |
| 48 | you can check like this: |
| 49 | |
| 50 | $ python -c 'import sys; print sys.path' |
martin f. krafft | 012103e | 2013-07-03 20:02:02 +0200 | [diff] [blame] | 51 | |
| 52 | More options can be found in the output of |
| 53 | |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 54 | $ python setup.py install --help |
| 55 | $ python setup.py --help |
| 56 | $ python setup.py --help-commands |
| 57 | $ python setup.py --help [cmd] |
martin f. krafft | 012103e | 2013-07-03 20:02:02 +0200 | [diff] [blame] | 58 | |
| 59 | If you just want to run reclass from source, e.g. because you are going to be |
| 60 | making and testing changes, install it in "development mode": |
| 61 | |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 62 | $ python setup.py develop |
martin f. krafft | 012103e | 2013-07-03 20:02:02 +0200 | [diff] [blame] | 63 | |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 64 | To uninstall (the rm call is necessary due to http://bugs.debian.org/714960): |
martin f. krafft | 012103e | 2013-07-03 20:02:02 +0200 | [diff] [blame] | 65 | |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 66 | $ python setup.py develop --uninstall |
| 67 | $ rm /usr/local/bin/reclass* |
martin f. krafft | 012103e | 2013-07-03 20:02:02 +0200 | [diff] [blame] | 68 | |
| 69 | Uninstallation currently isn't possible for packages installed to /usr/local |
| 70 | as per the above method, unfortunately: http://bugs.python.org/issue4673. |
| 71 | The following should do: |
| 72 | |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 73 | $ rm -r /usr/local/lib/python*/dist-packages/reclass* /usr/local/bin/reclass* |
martin f. krafft | d4833b3 | 2013-06-23 13:35:46 +0200 | [diff] [blame] | 74 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 75 | reclass concepts |
| 76 | ~~~~~~~~~~~~~~~~ |
| 77 | reclass assumes a node-centric perspective into your inventory. This is |
| 78 | obvious when you query reclass for node-specific information, but it might not |
| 79 | be clear when you ask reclass to provide you with a list of groups. In that |
| 80 | case, reclass loops over all nodes it can find in its database, reads all |
| 81 | information it can find about the nodes, and finally reorders the result to |
| 82 | provide a list of groups with the nodes they contain. |
| 83 | |
| 84 | Since the term 'groups' is somewhat ambiguous, it helps to start off with |
| 85 | a short glossary of reclass-specific terminology: |
| 86 | |
| 87 | node: A node, usually a computer in your infrastructure |
| 88 | class: A category, tag, feature, or role that applies to a node |
| 89 | Classes may be nested, i.e. there can be a class hierarchy |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 90 | application: A specific set of behaviour to apply |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 91 | parameter: Node-specific variables, with inheritance throughout the class |
| 92 | hierarchy. |
| 93 | |
| 94 | A class consists of zero or more parent classes, zero or more applications, |
| 95 | and any number of parameters. |
| 96 | |
| 97 | A node is almost equivalent to a class, except that it usually does not (but |
| 98 | can) specify applications. |
| 99 | |
| 100 | When reclass parses a node (or class) definition and encounters a parent |
| 101 | class, it recurses to this parent class first before reading any data of the |
| 102 | node (or class). When reclass returns from the recursive, depth first walk, it |
| 103 | then merges all information of the current node (or class) into the |
| 104 | information it obtained during the recursion. |
| 105 | |
martin f. krafft | ff1cb06 | 2013-06-20 17:23:00 +0200 | [diff] [blame] | 106 | Furthermore, a node (or class) may define a list of classes it derives from, |
| 107 | in which case classes defined further down the list will be able to override |
| 108 | classes further up the list. |
| 109 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 110 | Information in this context is essentially one of a list of applications or |
| 111 | a list of parameters. |
| 112 | |
| 113 | The interaction between the depth-first walk and the delayed merging of data |
| 114 | means that the node (and any class) may override any of the data defined by |
| 115 | any of the parent classes (ancestors). This is in line with the assumption |
| 116 | that more specific definitions ("this specific host") should have a higher |
| 117 | precedence than more general definitions ("all webservers", which includes all |
| 118 | webservers in Munich, which includes "this specific host", for example). |
| 119 | |
| 120 | Here's a quick example, showing how parameters accumulate and can get |
| 121 | replaced. |
| 122 | |
| 123 | All unixnodes (i.e. nodes who have the 'unixnodes' class in their ancestry) |
| 124 | have /etc/motd centrally-managed (through the 'motd' application), and the |
| 125 | unixnodes class definition provides a generic message-of-the-day to be put |
| 126 | into this file. |
| 127 | |
| 128 | All debiannodes, which are descendants of unixnodes, should include the |
| 129 | Debian codename in this message, so the message-of-the-day is overwritten in |
| 130 | the debiannodes class. |
| 131 | |
| 132 | The node 'quantum.example.org' will have a scheduled downtime this weekend, |
| 133 | so until Monday, an appropriate message-of-the-day is added to the node |
| 134 | definition. |
| 135 | |
martin f. krafft | ff1cb06 | 2013-06-20 17:23:00 +0200 | [diff] [blame] | 136 | When the 'motd' application runs, it receives the appropriate |
martin f. krafft | a0db070 | 2013-06-20 17:25:01 +0200 | [diff] [blame] | 137 | message-of-the-day (from 'quantum.example.org' when run on that node) and |
martin f. krafft | ff1cb06 | 2013-06-20 17:23:00 +0200 | [diff] [blame] | 138 | writes it into /etc/motd. |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 139 | |
| 140 | At this point it should be noted that parameters whose values are lists or |
| 141 | key-value pairs don't get overwritten by children classes or node definitions, |
| 142 | but the information gets merged (recursively) instead. |
| 143 | |
| 144 | Similarly to parameters, applications also accumulate during the recursive |
| 145 | walk through the class ancestry. It is possible for a node or child class to |
| 146 | _remove_ an application added by a parent class, by prefixing the application |
| 147 | with '~'. |
| 148 | |
| 149 | Finally, reclass happily lets you use multiple inheritance, and ensures that |
| 150 | the resolution of parameters is still well-defined. Here's another example |
| 151 | building upon the one about /etc/motd above: |
| 152 | |
| 153 | 'quantum.example.org' (which is back up and therefore its node definition no |
| 154 | longer contains a message-of-the-day) is at a site in Munich. Therefore, it |
| 155 | is a child of the class 'hosted@munich'. This class is independent of the |
| 156 | 'unixnode' hierarchy, 'quantum.example.org' derives from both. |
| 157 | |
| 158 | In this example infrastructure, 'hosted@munich' is more specific than |
| 159 | 'debiannodes' because there are plenty of Debian nodes at other sites (and |
| 160 | some non-Debian nodes in Munich). Therefore, 'quantum.example.org' derives |
| 161 | from 'hosted@munich' _after_ 'debiannodes'. |
| 162 | |
| 163 | When an electricity outage is expected over the weekend in Munich, the admin |
| 164 | can change the message-of-the-day in the 'hosted@munich' class, and it will |
| 165 | apply to all hosts in Munich. |
| 166 | |
| 167 | However, not all hosts in Munich have /etc/motd, because some of them are |
| 168 | 'windowsnodes'. Since the 'windowsnodes' ancestry does not specify the |
| 169 | 'motd' application, those hosts have access to the message-of-the-day in the |
| 170 | node variables, but the message won't get used… |
| 171 | |
| 172 | … unless, of course, 'windowsnodes' specified a Windows-specific application |
| 173 | to bring such notices to the attention of the user. |
| 174 | |
martin f. krafft | ff1cb06 | 2013-06-20 17:23:00 +0200 | [diff] [blame] | 175 | It's also trivial to ensure a certain order of class evaluation. Here's |
| 176 | another example: |
| 177 | |
| 178 | The 'ssh.server' class defines the 'permit_root_login' parameter to 'no'. |
| 179 | |
| 180 | The 'backuppc.client' class defines the parameter to 'without-password', |
| 181 | because the BackupPC server might need to log in to the host as root. |
| 182 | |
| 183 | Now, what happens if the admin accidentally provides the following two |
| 184 | classes? |
| 185 | |
| 186 | - backuppc.client |
| 187 | - ssh.server |
| 188 | |
| 189 | Theoretically, this would mean 'permit_root_login' gets set to 'no'. |
| 190 | |
martin f. krafft | a0db070 | 2013-06-20 17:25:01 +0200 | [diff] [blame] | 191 | However, since all 'backuppc.client' nodes need 'ssh.server' (at least in |
| 192 | most setups), the class 'backuppc.client' itself derives from 'ssh.server', |
martin f. krafft | ff1cb06 | 2013-06-20 17:23:00 +0200 | [diff] [blame] | 193 | ensuring that it gets parsed before 'backuppc.client'. |
| 194 | |
| 195 | When reclass returns to the node and encounters the 'ssh.server' class |
martin f. krafft | a0db070 | 2013-06-20 17:25:01 +0200 | [diff] [blame] | 196 | defined there, it simply skips it, as it's already been processed. |
martin f. krafft | ff1cb06 | 2013-06-20 17:23:00 +0200 | [diff] [blame] | 197 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 198 | reclass operations |
| 199 | ~~~~~~~~~~~~~~~~~~ |
| 200 | While reclass has been built to support different storage backends through |
| 201 | plugins, currently only the 'yaml_fs' storage backend exists. This is a very |
| 202 | simple, yet powerful, YAML-based backend, using flat files on the filesystem |
| 203 | (as suggested by the _fs postfix). |
| 204 | |
| 205 | yaml_fs works with two directories, one for node definitions, and another for |
| 206 | class definitions. It is possible to use a single directory for both, but that |
| 207 | could get messy and is therefore not recommended. |
| 208 | |
| 209 | Files in those directories are YAML-files, specifying key-value pairs. The |
| 210 | following three keys are read by reclass: |
| 211 | |
| 212 | classes: a list of parent classes |
| 213 | appliations: a list of applications to append to the applications defined by |
| 214 | ancestors. If an application name starts with '~', it would |
| 215 | remove this application from the list, if it had already been |
| 216 | added — but it does not prevent a future addition. |
| 217 | E.g. '~firewalled' |
| 218 | parameters: key-value pairs to set defaults in class definitions, override |
| 219 | existing data, or provide node-specific information in node |
| 220 | specifications. |
| 221 | By convention, parameters corresponding to an application |
| 222 | should be provided as subkey-value pairs, keyed by the name of |
| 223 | the application, e.g. |
| 224 | |
| 225 | applications: |
| 226 | - ssh.server |
| 227 | parameters: |
| 228 | ssh.server: |
| 229 | permit_root_login: no |
| 230 | |
| 231 | reclass starts out reading a node definition file, obtains the list of |
| 232 | classes, then reads the files corresponding to these classes, recursively |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 233 | reading parent classes, and finally merges the applications list and the |
| 234 | parameters. |
| 235 | |
| 236 | Merging of parameters is done recursively, meaning that lists and dictionaries |
martin f. krafft | c4f3b83 | 2013-08-07 16:31:51 +0200 | [diff] [blame^] | 237 | are extended (recursively), rather than replaced. However, a scalar value |
| 238 | *does* overwrite a dictionary or list value. While the scalar could be |
| 239 | appended to an existing list, there is sane default assumption in the context |
| 240 | of a dictionary, so this behaviour seems the most logical. |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 241 | |
martin f. krafft | fa27b9a | 2013-08-07 16:07:14 +0200 | [diff] [blame] | 242 | Finally, parameters may reference each other, including deep references, e.g.: |
| 243 | |
| 244 | parameters: |
| 245 | location: Munich, Germany |
| 246 | motd: |
| 247 | header: This node sits in ${location} |
| 248 | for_demonstration: ${motd:header} |
| 249 | dict_reference: ${motd} |
| 250 | |
| 251 | After merging and interpolation, which happens automatically inside the |
| 252 | storage modules, the 'for_demonstration' parameter will have a value of "This |
| 253 | node sits in Munich, Germany". |
| 254 | |
| 255 | Types are preserved if the value contains nothing but a reference. Hence, the |
| 256 | value of 'dict_reference' will actually be a dictionary. |
| 257 | |
martin f. krafft | 9b2049e | 2013-06-14 20:05:08 +0200 | [diff] [blame] | 258 | Version control |
| 259 | ~~~~~~~~~~~~~~~ |
| 260 | I recommend you maintain your reclass inventory database in Git, right from |
| 261 | the start. |
| 262 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 263 | Usage |
| 264 | ~~~~~ |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 265 | For information on how to use reclass directly, invoke reclass with --help and |
| 266 | study the output. |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 267 | |
martin f. krafft | 3924e89 | 2013-06-25 11:57:03 +0200 | [diff] [blame] | 268 | The three options --inventory-base-uri, --nodes-uri, and --classes-uri |
| 269 | together specify the location of the inventory. If the base URI is specified, |
| 270 | then it is prepended to the other two URIs, unless they are absolute URIs. If |
| 271 | these two URIs are not specified, they default to 'nodes' and 'classes'. |
| 272 | Therefore, if your inventory is in '/etc/reclass/nodes' and |
| 273 | '/etc/reclass/classes', all you need to specify is the base URI as |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 274 | '/etc/reclass' — which is actually the default (see reclass/defaults.py). |
martin f. krafft | 3924e89 | 2013-06-25 11:57:03 +0200 | [diff] [blame] | 275 | |
martin f. krafft | da52287 | 2013-07-03 20:31:55 +0200 | [diff] [blame] | 276 | If you've installed reclass as per the above instructions, try to run it from |
| 277 | the source directory like this: |
| 278 | |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 279 | $ reclass -b examples/ --inventory |
| 280 | $ reclass -b examples/ --node localhost |
martin f. krafft | da52287 | 2013-07-03 20:31:55 +0200 | [diff] [blame] | 281 | |
| 282 | Those data come from examples/nodes and examples/classes, and you can surely |
| 283 | make your own way from here. |
| 284 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 285 | More commonly, however, use of reclass will happen indirectly, and through |
martin f. krafft | 5ee69b3 | 2013-06-24 13:41:06 +0200 | [diff] [blame] | 286 | so-called adapters, e.g. /…/reclass/adapters/salt. The job of an adapter is to |
| 287 | translate between different invocation paradigms, provide a sane set of |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 288 | default options, and massage the data from reclass into the format expected by |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 289 | the automation tool in use. Please have a look at the respective README files |
| 290 | for these adapters. |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 291 | |
| 292 | Configuration file |
| 293 | ~~~~~~~~~~~~~~~~~~ |
| 294 | reclass can read some of its configuration from a file. The file is |
| 295 | a YAML-file and simply defines key-value pairs. |
| 296 | |
| 297 | The configuration file can be used to set defaults for all the options that |
| 298 | are otherwise configurable via the command-line interface, so please use the |
| 299 | --help output of reclass for reference. The command-line option '--nodes-uri' |
| 300 | corresponds to the key 'nodes_uri' in the configuration file. For example: |
| 301 | |
| 302 | storage_type: yaml_fs |
| 303 | pretty_print: True |
| 304 | output: json |
martin f. krafft | 3924e89 | 2013-06-25 11:57:03 +0200 | [diff] [blame] | 305 | inventory_base_uri: /etc/reclass |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 306 | nodes_uri: ../nodes |
| 307 | |
| 308 | reclass first looks in the current directory for the file called |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 309 | 'reclass-config.yml' (see reclass/defaults.py) and if no such file is found, |
| 310 | it looks in $HOME, then in /etc/reclass, and then "next to" the reclass script |
| 311 | itself, i.e. if the script is symlinked to /srv/provisioning/reclass, then the |
| 312 | the script will try to access /srv/provisioning/reclass-config.yml. |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 313 | |
martin f. krafft | 3094327 | 2013-07-04 08:32:00 +0200 | [diff] [blame] | 314 | Note that yaml_fs is currently the only supported storage_type, and it's the |
| 315 | default if you don't set it. |
| 316 | |
| 317 | Adapters may implement their own lookup logic, of course, so make sure to read |
| 318 | their READMEs. |
| 319 | |
martin f. krafft | fa27b9a | 2013-08-07 16:07:14 +0200 | [diff] [blame] | 320 | -- martin f. krafft <madduck@madduck.net> Wed, 07 Aug 2013 16:21:04 +0200 |