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 |
| 9 | tools, such as Puppet, Salt, and Ansible. |
| 10 | |
| 11 | The purpose of an ENC is to allow a system administrator to maintain an |
| 12 | inventory of nodes to be managed, completely separately from the configuration |
| 13 | of the automation tool. Usually, the external node classifier completely |
| 14 | replaces the tool-specific inventory (such as site.pp for Puppet, or |
| 15 | /etc/ansible/hosts). |
| 16 | |
martin f. krafft | 6223989 | 2013-06-14 20:03:59 +0200 | [diff] [blame] | 17 | reclass allows you to define your nodes through class inheritance, while |
| 18 | always able to override details of classes further up the tree. Think of |
| 19 | classes as feature sets, as commonalities between nodes, or as tags. Add to |
| 20 | that the ability to nest classes (multiple inheritance is allowed, |
| 21 | well-defined, and encouraged), and piece together your infrastructure from |
| 22 | smaller bits, eliminating redundancy and exposing all important parameters to |
| 23 | a single location, logically organised. |
| 24 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 25 | In general, the ENC fulfills two jobs: |
| 26 | |
| 27 | - it provides information about groups of nodes and group memberships |
| 28 | - it gives access to node-specific information, such as variables |
| 29 | |
| 30 | While reclass was born into a Puppet environment and has also been used with |
| 31 | Salt, the version you have in front of you is a rewrite from scratch, which |
| 32 | was targetted at Ansible. However, care was taken to make the code flexible |
| 33 | enough to allow it to be used from Salt, Puppet, and maybe even other tools as |
| 34 | well. |
| 35 | |
| 36 | In this document, you will find an overview of the concepts of reclass, the |
| 37 | way it works, and how it can be tied in with Ansible. |
| 38 | |
martin f. krafft | d4833b3 | 2013-06-23 13:35:46 +0200 | [diff] [blame] | 39 | Installation |
| 40 | ~~~~~~~~~~~~ |
| 41 | Before you can use reclass, you need to run make to configure the scripts to |
| 42 | your system. Right now, this only involves setting the full path to the |
| 43 | Python interpreter. |
| 44 | |
| 45 | make |
| 46 | |
| 47 | If your Python interpreter is not /usr/bin/python and is also not in your |
| 48 | $PATH, then you need to pass that to make, e.g. |
| 49 | |
| 50 | make PYTHON=/opt/local/bin/python |
| 51 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 52 | Quick start — Ansible |
| 53 | ~~~~~~~~~~~~~~~~~~~~~ |
| 54 | The following steps should get you up and running quickly. Generally, we will |
| 55 | be working in /etc/ansible. However, if you are using a source-code checkout |
| 56 | of Ansible, you might also want to work inside the ./hacking directory |
| 57 | instead. |
| 58 | |
| 59 | Or you can also just look into ./examples/ansible of your reclass checkout, |
| 60 | where the following steps have already been prepared. |
| 61 | |
| 62 | /…/reclass refers to the location of your reclass checkout. |
| 63 | |
| 64 | 1. Symlink /…/reclass/adapters/ansible to /etc/ansible/hosts (or |
| 65 | ./hacking/hosts) |
| 66 | |
| 67 | 2. Copy the two directories 'nodes' and 'classes' from the example |
| 68 | subdirectory in the reclass checkout to /etc/ansible |
| 69 | |
| 70 | If you prefer to put those directories elsewhere, you can create |
| 71 | /etc/ansible/reclass-config.yml with contents such as |
| 72 | |
| 73 | storage_type: yaml_fs |
| 74 | nodes_uri: /srv/reclass/nodes |
| 75 | classes_uri: /srv/reclass/classes |
| 76 | |
| 77 | Note that yaml_fs is currently the only supported storage_type, and it's |
| 78 | the default if you don't set it. |
| 79 | |
| 80 | 3. Check out your inventory by invoking |
| 81 | |
| 82 | ./hosts --list |
| 83 | |
| 84 | which should return 5 groups in JSON-format, and each group has exactly |
| 85 | one member 'localhost'. |
| 86 | |
| 87 | 4. See the node information for 'localhost': |
| 88 | |
| 89 | ./hosts --host localhost |
| 90 | |
| 91 | This should print a set of keys and values, including a greeting, |
| 92 | a colour, and a sub-class called 'RECLASS'. |
| 93 | |
| 94 | 5. Execute some ansible commands, e.g. |
| 95 | |
| 96 | ansible -i hosts \* --list-hosts |
| 97 | ansible -i hosts \* -m ping |
| 98 | ansible -i hosts \* -m debug -a 'msg="${greeting}"' |
| 99 | ansible -i hosts \* -m setup |
| 100 | ansible-playbook -i hosts test.yml |
| 101 | |
| 102 | 6. You can also invoke reclass directly, which gives a slightly different |
| 103 | view onto the same data, i.e. before it has been adapted for Ansible: |
| 104 | |
| 105 | /…/reclass.py --pretty-print --inventory |
| 106 | /…/reclass.py --pretty-print --nodeinfo localhost |
| 107 | |
| 108 | reclass concepts |
| 109 | ~~~~~~~~~~~~~~~~ |
| 110 | reclass assumes a node-centric perspective into your inventory. This is |
| 111 | obvious when you query reclass for node-specific information, but it might not |
| 112 | be clear when you ask reclass to provide you with a list of groups. In that |
| 113 | case, reclass loops over all nodes it can find in its database, reads all |
| 114 | information it can find about the nodes, and finally reorders the result to |
| 115 | provide a list of groups with the nodes they contain. |
| 116 | |
| 117 | Since the term 'groups' is somewhat ambiguous, it helps to start off with |
| 118 | a short glossary of reclass-specific terminology: |
| 119 | |
| 120 | node: A node, usually a computer in your infrastructure |
| 121 | class: A category, tag, feature, or role that applies to a node |
| 122 | Classes may be nested, i.e. there can be a class hierarchy |
| 123 | application: A specific set of behaviour to apply to members of a class |
| 124 | parameter: Node-specific variables, with inheritance throughout the class |
| 125 | hierarchy. |
| 126 | |
| 127 | A class consists of zero or more parent classes, zero or more applications, |
| 128 | and any number of parameters. |
| 129 | |
| 130 | A node is almost equivalent to a class, except that it usually does not (but |
| 131 | can) specify applications. |
| 132 | |
| 133 | When reclass parses a node (or class) definition and encounters a parent |
| 134 | class, it recurses to this parent class first before reading any data of the |
| 135 | node (or class). When reclass returns from the recursive, depth first walk, it |
| 136 | then merges all information of the current node (or class) into the |
| 137 | information it obtained during the recursion. |
| 138 | |
martin f. krafft | ff1cb06 | 2013-06-20 17:23:00 +0200 | [diff] [blame] | 139 | Furthermore, a node (or class) may define a list of classes it derives from, |
| 140 | in which case classes defined further down the list will be able to override |
| 141 | classes further up the list. |
| 142 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 143 | Information in this context is essentially one of a list of applications or |
| 144 | a list of parameters. |
| 145 | |
| 146 | The interaction between the depth-first walk and the delayed merging of data |
| 147 | means that the node (and any class) may override any of the data defined by |
| 148 | any of the parent classes (ancestors). This is in line with the assumption |
| 149 | that more specific definitions ("this specific host") should have a higher |
| 150 | precedence than more general definitions ("all webservers", which includes all |
| 151 | webservers in Munich, which includes "this specific host", for example). |
| 152 | |
| 153 | Here's a quick example, showing how parameters accumulate and can get |
| 154 | replaced. |
| 155 | |
| 156 | All unixnodes (i.e. nodes who have the 'unixnodes' class in their ancestry) |
| 157 | have /etc/motd centrally-managed (through the 'motd' application), and the |
| 158 | unixnodes class definition provides a generic message-of-the-day to be put |
| 159 | into this file. |
| 160 | |
| 161 | All debiannodes, which are descendants of unixnodes, should include the |
| 162 | Debian codename in this message, so the message-of-the-day is overwritten in |
| 163 | the debiannodes class. |
| 164 | |
| 165 | The node 'quantum.example.org' will have a scheduled downtime this weekend, |
| 166 | so until Monday, an appropriate message-of-the-day is added to the node |
| 167 | definition. |
| 168 | |
martin f. krafft | ff1cb06 | 2013-06-20 17:23:00 +0200 | [diff] [blame] | 169 | When the 'motd' application runs, it receives the appropriate |
martin f. krafft | a0db070 | 2013-06-20 17:25:01 +0200 | [diff] [blame] | 170 | 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] | 171 | writes it into /etc/motd. |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 172 | |
| 173 | At this point it should be noted that parameters whose values are lists or |
| 174 | key-value pairs don't get overwritten by children classes or node definitions, |
| 175 | but the information gets merged (recursively) instead. |
| 176 | |
| 177 | Similarly to parameters, applications also accumulate during the recursive |
| 178 | walk through the class ancestry. It is possible for a node or child class to |
| 179 | _remove_ an application added by a parent class, by prefixing the application |
| 180 | with '~'. |
| 181 | |
| 182 | Finally, reclass happily lets you use multiple inheritance, and ensures that |
| 183 | the resolution of parameters is still well-defined. Here's another example |
| 184 | building upon the one about /etc/motd above: |
| 185 | |
| 186 | 'quantum.example.org' (which is back up and therefore its node definition no |
| 187 | longer contains a message-of-the-day) is at a site in Munich. Therefore, it |
| 188 | is a child of the class 'hosted@munich'. This class is independent of the |
| 189 | 'unixnode' hierarchy, 'quantum.example.org' derives from both. |
| 190 | |
| 191 | In this example infrastructure, 'hosted@munich' is more specific than |
| 192 | 'debiannodes' because there are plenty of Debian nodes at other sites (and |
| 193 | some non-Debian nodes in Munich). Therefore, 'quantum.example.org' derives |
| 194 | from 'hosted@munich' _after_ 'debiannodes'. |
| 195 | |
| 196 | When an electricity outage is expected over the weekend in Munich, the admin |
| 197 | can change the message-of-the-day in the 'hosted@munich' class, and it will |
| 198 | apply to all hosts in Munich. |
| 199 | |
| 200 | However, not all hosts in Munich have /etc/motd, because some of them are |
| 201 | 'windowsnodes'. Since the 'windowsnodes' ancestry does not specify the |
| 202 | 'motd' application, those hosts have access to the message-of-the-day in the |
| 203 | node variables, but the message won't get used… |
| 204 | |
| 205 | … unless, of course, 'windowsnodes' specified a Windows-specific application |
| 206 | to bring such notices to the attention of the user. |
| 207 | |
martin f. krafft | ff1cb06 | 2013-06-20 17:23:00 +0200 | [diff] [blame] | 208 | It's also trivial to ensure a certain order of class evaluation. Here's |
| 209 | another example: |
| 210 | |
| 211 | The 'ssh.server' class defines the 'permit_root_login' parameter to 'no'. |
| 212 | |
| 213 | The 'backuppc.client' class defines the parameter to 'without-password', |
| 214 | because the BackupPC server might need to log in to the host as root. |
| 215 | |
| 216 | Now, what happens if the admin accidentally provides the following two |
| 217 | classes? |
| 218 | |
| 219 | - backuppc.client |
| 220 | - ssh.server |
| 221 | |
| 222 | Theoretically, this would mean 'permit_root_login' gets set to 'no'. |
| 223 | |
martin f. krafft | a0db070 | 2013-06-20 17:25:01 +0200 | [diff] [blame] | 224 | However, since all 'backuppc.client' nodes need 'ssh.server' (at least in |
| 225 | most setups), the class 'backuppc.client' itself derives from 'ssh.server', |
martin f. krafft | ff1cb06 | 2013-06-20 17:23:00 +0200 | [diff] [blame] | 226 | ensuring that it gets parsed before 'backuppc.client'. |
| 227 | |
| 228 | 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] | 229 | 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] | 230 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 231 | reclass operations |
| 232 | ~~~~~~~~~~~~~~~~~~ |
| 233 | While reclass has been built to support different storage backends through |
| 234 | plugins, currently only the 'yaml_fs' storage backend exists. This is a very |
| 235 | simple, yet powerful, YAML-based backend, using flat files on the filesystem |
| 236 | (as suggested by the _fs postfix). |
| 237 | |
| 238 | yaml_fs works with two directories, one for node definitions, and another for |
| 239 | class definitions. It is possible to use a single directory for both, but that |
| 240 | could get messy and is therefore not recommended. |
| 241 | |
| 242 | Files in those directories are YAML-files, specifying key-value pairs. The |
| 243 | following three keys are read by reclass: |
| 244 | |
| 245 | classes: a list of parent classes |
| 246 | appliations: a list of applications to append to the applications defined by |
| 247 | ancestors. If an application name starts with '~', it would |
| 248 | remove this application from the list, if it had already been |
| 249 | added — but it does not prevent a future addition. |
| 250 | E.g. '~firewalled' |
| 251 | parameters: key-value pairs to set defaults in class definitions, override |
| 252 | existing data, or provide node-specific information in node |
| 253 | specifications. |
| 254 | By convention, parameters corresponding to an application |
| 255 | should be provided as subkey-value pairs, keyed by the name of |
| 256 | the application, e.g. |
| 257 | |
| 258 | applications: |
| 259 | - ssh.server |
| 260 | parameters: |
| 261 | ssh.server: |
| 262 | permit_root_login: no |
| 263 | |
| 264 | reclass starts out reading a node definition file, obtains the list of |
| 265 | classes, then reads the files corresponding to these classes, recursively |
| 266 | reading parent classes, and finally merges the applications list (append |
| 267 | unless |
| 268 | |
martin f. krafft | 9b2049e | 2013-06-14 20:05:08 +0200 | [diff] [blame] | 269 | Version control |
| 270 | ~~~~~~~~~~~~~~~ |
| 271 | I recommend you maintain your reclass inventory database in Git, right from |
| 272 | the start. |
| 273 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 274 | Usage |
| 275 | ~~~~~ |
| 276 | For information on how to use reclass directly, invoke reclass.py with --help |
| 277 | and study the output. |
| 278 | |
| 279 | More commonly, however, use of reclass will happen indirectly, and through |
| 280 | so-called adapters, e.g. /…/reclass/adapters/ansible. The job of an adapter is |
| 281 | to translate between different invocation paradigms, provide a sane set of |
| 282 | default options, and massage the data from reclass into the format expected by |
| 283 | the automation tool in use. |
| 284 | |
| 285 | Configuration file |
| 286 | ~~~~~~~~~~~~~~~~~~ |
| 287 | reclass can read some of its configuration from a file. The file is |
| 288 | a YAML-file and simply defines key-value pairs. |
| 289 | |
| 290 | The configuration file can be used to set defaults for all the options that |
| 291 | are otherwise configurable via the command-line interface, so please use the |
| 292 | --help output of reclass for reference. The command-line option '--nodes-uri' |
| 293 | corresponds to the key 'nodes_uri' in the configuration file. For example: |
| 294 | |
| 295 | storage_type: yaml_fs |
| 296 | pretty_print: True |
| 297 | output: json |
| 298 | nodes_uri: ../nodes |
| 299 | |
| 300 | reclass first looks in the current directory for the file called |
| 301 | 'reclass-config.yml' and if no such file is found, it looks "next to" the |
| 302 | reclass script itself. Adapters implement their own lookup logic. |
| 303 | |
| 304 | Integration with Ansible |
| 305 | ~~~~~~~~~~~~~~~~~~~~~~~~ |
| 306 | The integration between reclass and Ansible is performed through an adapter, |
| 307 | and needs not be of our concern too much. |
| 308 | |
| 309 | However, Ansible has no concept of "nodes", "applications", "parameters", and |
| 310 | "classes". Therefore it is necessary to explain how those correspond to |
| 311 | Ansible. Crudely, the following mapping exists: |
| 312 | |
| 313 | nodes hosts |
| 314 | classes groups |
| 315 | applications playbooks |
| 316 | parameters host_vars |
| 317 | |
| 318 | reclass does not provide any group_vars because of its node-centric |
| 319 | perspective. While class definitions include parameters, those are inherited |
| 320 | by the node definitions and hence become node_vars. |
| 321 | |
| 322 | reclass also does not provide playbooks, nor does it deal with any of the |
| 323 | related Ansible concepts, i.e. vars_files, vars, tasks, handlers, roles, etc.. |
| 324 | |
| 325 | Let it be said at this point that you'll probably want to stop using |
| 326 | host_vars, group_vars and vars_files altogether, and if only because you |
| 327 | should no longer need them, but also because the variable precedence rules |
| 328 | of Ansible are full of surprises, at least to me. |
| 329 | |
| 330 | reclass' Ansible adapter massage the reclass output into Ansible-usable data, |
| 331 | namely: |
| 332 | |
| 333 | - Every class in the ancestry of a node becomes a group to Ansible. This is |
| 334 | mainly useful to be able to target nodes during interactive use of |
| 335 | Ansible, e.g. |
| 336 | |
| 337 | ansible debiannode@wheezy -m command -a 'apt-get upgrade' |
| 338 | → upgrade all Debian nodes running wheezy |
| 339 | |
| 340 | ansible ssh.server -m command -a 'invoke-rc.d ssh restart' |
| 341 | → restart all SSH server processes |
| 342 | |
| 343 | ansible mailserver -m command -a 'tail -n1000 /var/log/mail.err' |
| 344 | → obtain the last 1,000 lines of all mailserver error log files |
| 345 | |
| 346 | The attentive reader might stumble over the use of singular words, whereas |
| 347 | it might make more sense to address all 'mailserver*s*' with this tool. |
| 348 | This is convention and up to you. I prefer to think of my node as |
| 349 | a (singular) mailserver when I add 'mailserver' to its parent classes. |
| 350 | |
| 351 | - Every entry in the list of a host's applications might well correspond to |
| 352 | an Ansible playbook. Therefore, reclass creates a (Ansible-)group for |
martin f. krafft | 9a9b0ac | 2013-06-21 21:24:18 +0200 | [diff] [blame] | 353 | every application, and adds '_hosts' to the name. This postfix can be |
| 354 | configured with a CLI option (--applications-postfix) or in the |
| 355 | configuration file (applications_postfix). |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 356 | |
| 357 | For instance, the ssh.server class adds the ssh.server application to |
| 358 | a node's application list. Now the admin might create an Ansible playbook |
| 359 | like so: |
| 360 | |
| 361 | - name: SSH server management |
| 362 | hosts: ssh.server_hosts ← SEE HERE |
| 363 | tasks: |
| 364 | - name: install SSH package |
| 365 | action: … |
| 366 | … |
| 367 | |
| 368 | There's a bit of redundancy in this, but unfortunately Ansible playbooks |
| 369 | hardcode the nodes to which a playbook applies. |
| 370 | |
martin f. krafft | b608e6d | 2013-06-14 22:10:43 +0200 | [diff] [blame] | 371 | It's now trivial to apply this playbook across your infrastructure: |
| 372 | |
| 373 | ansible-playbook ssh.server.yml |
| 374 | |
| 375 | My suggested way to use Ansible site-wide is then to create a 'site' |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 376 | playbook that includes all the other playbooks (which shall hopefully be |
| 377 | based on Ansible roles), and then to invoke Ansible like this: |
| 378 | |
| 379 | ansible-playbook site.yml |
| 380 | |
| 381 | or, if you prefer only to reconfigure a subset of nodes, e.g. all |
| 382 | webservers: |
| 383 | |
| 384 | ansible-playbook site.yml --limit webserver |
| 385 | |
| 386 | Again, if the singular word 'webserver' puts you off, change the |
| 387 | convention as you wish. |
| 388 | |
martin f. krafft | b608e6d | 2013-06-14 22:10:43 +0200 | [diff] [blame] | 389 | And if anyone comes up with a way to directly connect groups in the |
| 390 | inventory with roles, thereby making it unnecessary to write playbook |
| 391 | files (containing redundant information), please tell me! |
| 392 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 393 | - Parameters corresponding to a node become host_vars for that host. |
| 394 | |
martin f. krafft | 6e9dcba | 2013-06-16 15:21:09 +0200 | [diff] [blame] | 395 | It is possible to include Jinja2-style variables like you would in Ansible, |
| 396 | in parameter values. This is especially powerful in combination with the |
| 397 | recursive merging, e.g. |
| 398 | |
| 399 | parameters: |
| 400 | motd: |
| 401 | greeting: Welcome to {{ ansible_fqdn }}! |
| 402 | closing: This system is part of {{ realm }} |
| 403 | |
| 404 | Now you just need to specify realm somewhere. The reference can reside in |
| 405 | a parent class, while the variable is defined e.g. in the node. |
| 406 | |
martin f. krafft | 3c33322 | 2013-06-14 19:27:57 +0200 | [diff] [blame] | 407 | Contributing to reclass |
| 408 | ~~~~~~~~~~~~~~~~~~~~~~~ |
| 409 | Conttributions to reclass are very welcome. Since I prefer to keep a somewhat |
| 410 | clean history, I will not merge pull requests. Please send your patches using |
| 411 | git-format-patch and git-send-e-mail to reclass@pobox.madduck.net. |
| 412 | |
| 413 | I have added rudimentary unit tests, and it would be nice if you could submit |
| 414 | your changes with appropriate changes to the tests. To run tests, invoke |
| 415 | ./run_tests.py in the top-level checkout directory. |
| 416 | |
| 417 | If you have larger ideas, I'll be looking forward to discuss them with you. |
| 418 | |
martin f. krafft | e39e890 | 2013-06-14 22:12:17 +0200 | [diff] [blame] | 419 | -- martin f. krafft <madduck@madduck.net> Fri, 14 Jun 2013 22:12:05 +0200 |