Archives 2025

Return values from Ansible roles

Ansible roles aren’t functions, so they don’t actually return values. But what if they could?

I recently wrote a role to update a software package, which involved asking some questions:

  1. What’s the latest version?
  2. Which version, if any, is currently installed?

I wanted my role to be modular(ish), so it made sense to outsource those questions to other roles. But how would they return their results? I tried adding register: latest_version to the role invocation, but that didn’t work.

But you can set facts inside a role:

# ... Figure out installed version of Foo package...
- name: Return a value
  set_fact:
    installed_version: "1.2.3"

and if you know that this role sets installed_version, you can treat that as a return value, just as if it were a task with register: installed_version. Also, the fact that it sets, installed_version, is local to the client, so each host will have a copy in hostvars[ansible_host]. This makes sense: after all, you might want to know which of your machines are out of date.

The problem, though, is that installed_version is “global” to each client. So if you’re checking the installed version of multiple packages

- name: Upgrade if necessary
  roles:
    - role: check_version
      package: Foo
    - role: check_version
      package: Bar
    - role: check_version
      package: Baz
  tasks:
    - debug: var=installed_version

how do you know which package’s installed_version you’re dealing with?

The way to fix this is to realize that you can use string interpolation on the left side of the colon. When we invoke the role, let’s also give it the name of a fact to put the return value in:

- name: Upgrade if necessary
  roles:
    - role: check_version
      package: Foo
      return: foo_version
    - role: check_version
      package: Bar
      return: bar_version
    - role: check_version
      package: Baz
      return: baz_version

The check_version role then sees return as a role variable, and can use that in a play:

# ... Look up installed version of the given package...
- name: Return version number
  set_fact:
    "{{ return }}": "1.2.3"

And then just check the appropriate fact for each role invocation.

- debug: var=foo_version
- debug: var=bar_version
- debug: var=baz_version

Bear in mind that these variables have the same scope as other facts, so if two roles set installed_version, you’re back to the earlier problem of them stepping on each other’s feet. And yes, the whole thing feels like a hack. But it does mean you can return values from roles.

Location-Based Themes in Emacs

There are lots of Emacs themes out there. People mostly use them to change colors. That’s great, but you can do more. As the documentation says:

Custom themes are collections of settings that can be enabled or disabled as a unit.

That can be any setting, not just colors.

I use Emacs in multiple situations. I use it at home, I use it in the office, I use it when I’m working from home, and so on. And there are settings that change depending on how and where I’m using Emacs, the most obvious of which being user-mail-address, which I want to set to user@home.org when I’m sending personal mail from home, and user@work.com when I’m sending professional mail from work. I have other settings that change between locations, like the directory where org files go, because reasons. Your location-dependent settings will be different.

So it seems the natural way to deal with this is to put all of the location-dependent settings in a theme, and load whichever theme is appropriate for what you’re doing. Since themes are groups of settings that can be turned on or off as a group, you can even switch themes in the middle of an Emacs session. For instance, if you’re a consultant working with two clients, you can enable theme client1 in the morning; and then, in the afternoon, disable theme client1 and enable theme client2 to update your settings.

Setting Up Location-Based Themes

To start with, let’s set up a couple of themes called personal and work, for personal and professional stuff, respectively. Check the value of custom-theme-directory. By default, Emacs looks for themes in the same directory as init.el, but I like to put them in a separate subdirectory, ~/.emacs.d/themes. Customize this to your taste. For the rest of this post, I’ll assume that you’re using ~/.emacs.d/themes.

Theme files should go in a file named <theme-name>-theme.el. So create the file ~/.emacs.d/themes/personal-theme.el to hold the personal theme:

(deftheme personal
  "Settings when working on personal projects."
  :family "location")

(custom-theme-set-variables 'personal
 '(user-mail-address "bob@home.org")
 ;; Add additional variables here.
 )

(provide-theme 'loc-personal)

You can now run M-x load-theme personal to load the theme. Theme files contain Emacs Lisp code, which can be a security risk, so the first time you load it, or any time you make any changes, Emacs will ask you to confirm whether to load the theme, and whether to mark it as safe in the future. If you say yes, the next time it’ll just load the theme asking for confirmation.

Follow the same steps to create a work theme.

Loading A Theme at Startup

Once you have something that works reasonably well, you’ll probably want something that loads the right theme when Emacs starts. For this example, we’ll assume that if the hostname is my-laptop, then you’re working on personal things and want to load the personal theme, while if the hostname is office-workstation, then you’re at work and want to load the work theme.

Add something like the following to your ~/.emacs.d/init.el:

(add-hook 'emacs-startup-hook
  (lambda nil
    ;; Figure out which theme to load, depending on which machine this
    ;; is running on, and which options were specified.
    (let ((loc-theme 'unknon)
          )
      (cond
       ;; Running on personal laptop.
       ((string= system-name "my-laptop")
        (setq loc-theme 'personal)
        )

       ;; Running on work machine.
       ((string= system-name "office-workstation")
        (setq loc-theme 'work))

       ;; Add any other locations/work modes here.
       ))

      ;; Check whether the theme exists. If it does, load it.
      (if (member loc-theme (custom-available-themes))
          (progn
            (load-theme loc-theme)
            )
        (warn "Can't find location theme \"%s\"" loc-theme)
        ))))

Here, we’re just looking at the hostname, but obviously the condition can be arbitrarily complex. You might pick different themes depending on the day of the week, or whether you’re ssh-ed in from another host, the phase of the moon, or whatever makes sense in your situation. Add or remove themes as necessary.

Further Thoughts

One advantage of doing things this way is separation of information. Maybe you want to make your Emacs setup publicly visible on github, but your work setup includes hostnames or other information that shouldn’t leave company premises. In this case, you can store your work-theme.el file on your company machine, perhaps in a separate directory. You can add an entry to custom-theme-load-path, a directory outside of ~/.emacs.d, so that your work theme doesn’t accidentally get added to the git repo that has your init.el and your publicly-visible themes.

There’s one problem that I haven’t found a good solution to: child themes. Let’s say you have your work theme, with a hundred work-related settings. But of those 100 settings, there are three that change depending on whether you’re logged in to host1 or host2.It would be nice to have a host1 theme that automatically includes everything from the work theme, plus the three settings that are specific to host1. I don’t see a good way of doing this. It might make sense to use literate programming to generate multiple *-theme.el files from one source .org file.