Create an Ansible module for integrating your Google Calendar

Learn how to write an Ansible module in Go to integrate Google Calendar into your automation workflow.
89 readers like this.

In a previous article, I explored how Ansible can integrate with Google Calendar for change management, but I didn't get into the details of the Ansible module that was built for this purpose. In this article, I will cover the nuts and bolts of it.

While most Ansible modules are written in Python (see this example), that's not the only option you have. You can use other programming languages if you prefer. And if you like Go, this post is for you!

If you are new to Go, check out these pointers to get started.

Ansible and Go

There are at least four different ways that you can run a Go program from Ansible:

  1. Install Go and run your Go code with the go run command from Ansible.
  2. Cross-compile your Go code for different platforms before execution. Then call the proper binary from Ansible, based on the facts you collect from the host.
  3. Run your Go code or compiled binary from a container with the containers.podman collection. Something along the lines of:
    - name: Run Go container
      podman_container:
        name: go_test_container
        image: golang
        command: go version
        rm: true
        log_options: "path={{ log_file }}"
  4. Create an RPM package of your Go code with something like NFPM, and install it in the system of the target host. You can then call it with the Ansible modules shell or command.

Running an RPM package or container is not Go-specific (cases 3 and 4), so I will focus on the first two options.

Google Calendar API

You will need to interact with the Google Calendar API, which provides code examples for different programming languages. The one for Go will be the base for your Ansible module.

The tricky part is enabling the Calendar API and downloading the credentials you generate in the Google API Console (Credentials > + CREATE CREDENTIALS > OAuth client ID > Desktop App).

The arrow shows where to download your OAuth 2.0 client credentials (JSON file) once you create them in API Credentials.

Calling the module from Ansible

The calendar module takes the time to validate as an argument. The example below provides the current time. You can typically get this from Ansible facts (ansible_date_time). The JSON output of the module is registered in a variable named output to be used in a subsequent task:

- name: Check if timeslot is taken
  calendar:
    time: "{{ ansible_date_time.iso8601 }}"
  register: output

You might wonder where the calendar module code lives and how Ansible executes it. Please bear with me for a moment; I'll get to this after I cover other pieces of the puzzle.

Employ the time logic

With the Calendar API nuances out of the way, you can proceed to interact with the API and build a Go function to capture the module logic. The time is taken from the input arguments—in the playbook above—as the initial time (min) of the time window to validate (I arbitrarily chose a one-hour duration):

func isItBusy(min string) (bool, error) {
	...
	// max -> min.Add(1 * time.Hour)
	max, err := maxTime(min)
        // ...
	srv, err := calendar.New(client)
	// ...
	freebusyRequest := calendar.FreeBusyRequest{
		TimeMin: min,
		TimeMax: max,
		Items:   []*calendar.FreeBusyRequestItem{&cal},
	}
	// ...
	freebusyRequestResponse, err := freebusyRequestCall.Do()
	// ...
	if len(freebusyRequestResponse.Calendars[name].Busy) == 0 {
		return false, nil
	}
	return true, nil
}

It sends a FreeBusyRequest to Google Calendar with the time window's initial and finish time (min and max). It also creates a calendar client (srv) to authenticate the account correctly using the JSON file with the OAuth 2.0 client credentials. In return, you get a list of events during this time window.

If you get zero events, the function returns busy=false. However, if there is at least one event during this time window, it means busy=true. You can check out the full code in my GitHub repository.

Process the input and creating a response

Now, how does the Go code read the inputs arguments from Ansible and, in turn, generate a response that Ansible can process? The answer to this depends on whether you are running the Go CLI (command-line interface) or just executing a pre-compiled Go program binary (i.e., options 1 and 2 above).

Both options have their pros and cons. If you use the Go CLI, you can pass the arguments the way you prefer. However, to make it consistent with how it works for binaries you run from Ansible, both alternatives will read a JSON file in the examples presented here.

Reading the arguments

As shown in the Go code snippet below, an input file is processed, and Ansible provides a path to it when it calls a binary.

The content of the file is unmarshaled into an instance (moduleArg) of a previously defined struct (ModuleArgs). This is how you tell the Go code which data you expect to receive. This method enables you to gain access to the time specified in the playbook via moduleArg.time, which is then passed to the time logic function (isItBusy) for processing:

// ModuleArgs are the module inputs
type ModuleArgs struct {
	Time string
}

func main() {
	...
	argsFile := os.Args[1]
	text, err := ioutil.ReadFile(argsFile)
	...
	var moduleArgs ModuleArgs
	err = json.Unmarshal(text, &moduleArgs)
	...
	busy, err := isItBusy(moduleArg.time)
	...
}

Generating a response

The values to return are assigned to an instance of a Response object. Ansible will need this response includes a couple of boolean flags (Changed and Failed). You can add any other field you need; in this case, a Busy boolean value is carried to signal the response of the time logic function.

The response is marshaled into a message that you print out and Ansible can read:

// Response are the values returned from the module
type Response struct {
	Msg     string `json:"msg"`
	Busy    bool   `json:"busy"`
	Changed bool   `json:"changed"`
	Failed  bool   `json:"failed"`
}

func returnResponse(r Response) {
  ...
	response, err = json.Marshal(r)
	...
	fmt.Println(string(response))
	...
}

You can check out the full code on GitHub.

Execute a binary or Go code on the fly?

One of the cool things about Go is that you can cross-compile a Go program to run on different target operating systems and architectures. The binary files you compile can be executed in the target host without installing Go or any dependency.

This flexibility plays nicely with Ansible, which provides the target host details (ansible_system and ansible_architecture) via Ansible facts. In this example, the target architecture is fixed when compiling (x86_64), but binaries for macOS, Linux, and Windows are generated (via make compile). This produces the three files that are included in the library folder of the go_role role with the form of: calendar_$system:

⇨  tree roles/go_role/
roles/go_role/
├── library
│   ├── calendar_darwin
│   ├── calendar_linux
│   ├── calendar_windows
│   └── go_run
└── tasks
    ├── Darwin.yml
    ├── Go.yml
    ├── Linux.yml
    ├── main.yml
    └── Win32NT.yml

The go_role role that packages the calendar module is structured to replace $system from the executing filename based on the ansible_system discovered during runtime; this way, this role can run on any of these target operating systems. How cool is that!? You can test this with make test-ansible.

Alternatively, if Go is installed in the target system, then you can run the go run command to execute the code. If you want to install Go first as part of your playbook, you can use this role (see this example).

How do you run the go run command from Ansible? One option is to use the shell or command modules. However, this case uses a bonus Bash module to extend this exercise to include another programing language.

Bonus module: Bash

The file go_run in the library folder go_role role is the actual Bash code used to run the Go code on systems with Go installed. When Ansible runs this Bash module, it will pass a file to it with the module arguments defined in the playbook, which you can import in Bash with source $1. This provides access to the variable time. Otherwise, you get it from the system with date --iso-8601=seconds.

ISO 8601 and RFC 3339 make timestamps interoperable between Ansible, Bash, and Go. There is no need to parse or transform data between them.

#!/bin/bash

source $1

# Fail if time is not set or add a sane default
if [ -z "$time" ]; then
     time=$(date --iso-8601=seconds)
fi

printf '{"Name": "%s", "Time": "%s"}' "$name" "$time" > $file

go run *.go $file

With the inputs at hand, a JSON file is generated with printf. This file is passed as an argument to the Go code via the go run command. You can test this with make test-manual-bash. Check out the full code on GitHub.

Using the module output as a conditional

The response from the calendar module (output) can now be used as a conditional to determine whether the next task should run or not:

tasks:
  - shell: echo "Run this only when not busy!"
    when: not output.busy

If you want to avoid running the previous task to get the response and instead use the module's output directly in your when statement, then an Action plugin might help.

Another alternative, especially if Go is not your thing, is something like this plugin that my good friend Paulo wrote after we discussed this specific use-case.

Conclusions

While Ansible and most of its modules are written in Python, it can seamlessly integrate with tools or scripts that use another programming language. This is key to improving efficiency and operational agility without the need to rip and replace.


This originally appeared on Medium as Get yourself GOing with Ansible under a CC BY-SA 4.0 license and is republished with permission.

What to read next
User profile image.
Technology professional with 15 years of experience helping customers design, deploy, and operate large-scale networks, with an emphasis on infrastructure automation. Cisco Certified Design Expert (CCDE) and Internetwork Expert (CCIE). Occasional speaker at international conferences and blogger. Enjoys writing open-source software in Go and he is a Cloud enthusiast.

Comments are closed.

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.