Skip to content

Instantly share code, notes, and snippets.

@ssato
Last active November 17, 2021 17:39
Show Gist options
  • Save ssato/e2f47c694dfa56bab82b9ce2ef61426f to your computer and use it in GitHub Desktop.
Save ssato/e2f47c694dfa56bab82b9ce2ef61426f to your computer and use it in GitHub Desktop.
Assertive Programming in Ansible

(This article is a 12/16 minute post from https://qiita.com/advent-calendar/2019/ansible2.)

This is an English translation of https://gist.github.com/ssato/a31298910fecb723350ad68f4877bb53 using Google Translation.

Overview

In this article, I'll briefly explain how Assertive Programming can help you implement a robust Ansible Playbook, with a few examples.

Variable problems in #Ansible

Ansible Playbook In general, * variables * (* variable *) will almost always be used when trying to flexibly respond to differences in targets and environments and standardize them, or to give them some degree of extensibility. Also, if you write an Ansible Playbook without using any variables, you will only be able to deal with very rigid, specific objects, environments, settings, etc. Therefore, when writing an Ansible Playbook, it is very rare that variables do not appear at all, and the handling of variables is very important.

But unfortunately, * variables * in the Ansible world feel very poor compared to * variables * or the like in other programming languages. For example, variables in Ansible do not have the features and properties that modern programming languages ​​take for granted, and have the following problems that hinder the development of robust Ansible Playbooks and Roles: I think so.

  • Global variables only
  • There is no such thing as a namespace
  • Can be declared anywhere and can be changed at any time later
  • Even if it is undefined, it cannot be detected before execution, and it will be recognized as an error only after execution.
  • No warning is given even if the value is empty, and no error occurs at runtime.
  • Even if the type of the variable (implicitly assumed) and the type of the actually specified value do not match, basically no warning is given and no error occurs at runtime.
  • There is an implicit cast to bool or string and cannot be disabled
  • etc

Not all of these "problems" can be solved, but some of them are likely to be "covered by operations", that is, with a little additional effort on the part of Ansible Playbook developers. is.

In this article, I will show some concrete examples of improvement plans by Assertive Programming, and briefly explain the effects of each.

Assertive Programming

Before we talk about improving Ansible Playbook with Assertive Programming, let's take a quick look at what Assertive Programming is in the first place.

You can find some explanations by searching for terms such as Assertion, assertive programming, and assertive programming.

  • [Packages for Assertive Programming --CRAN] 1
  • [Assertion (software development) --Wikipedia] 2
  • [Assertion-Wikipedia] 3

Although there are some differences in expressions, it can be said that Assertive Programming is roughly as follows.

  • One of the Defensive Programming techniques aimed at Fail first

  • Before doing anything, write code that asserts that the preconditions that you expect it to be are met as expected.

    • Example 0. Asserting that a variable has been declared
    • Example 1. Asserting that a variable has some meaningful value set instead of being declared and null (indicating some undefined value).
    • Example 2. Asserting that the variable type is as expected at the beginning of the function
    • Example 3. At the beginning of the function, state that the value of the variable is in the expected range.
  • If it is not as stated, the program will stop immediately and no further processing will be executed.

    • Example. Python raises an AssertionError exception

Assertive Programming is expected to be more robust code that can capture special and unusual situations (expected preconditions are not met) and proceed further to prevent worse situations. I can do it.

To help you get a more concrete image of Assertive Programming, I'll give you some code examples in python, which may not be practical at all and may be a little overwhelming.

# Example 0. Global variable FOO should be defined (globals (): A function that returns a dictionary whose key is the defined global variable name and whose value is the value of the variable)
assert'FOO' in globals ()

# Example 1. FOO is None (None is like NULL / Nothing in python, since it is a singleton, you can confirm that it is not None by is not)
assert FOO is not None

def do_something (foo = FOO):
    assert isinstance (foo, int) #Example 2. foo should be int (a kind of integer type)
    assert 0 <foo <4 # Example 3. The value of foo is 1, 2, or 3
    return _do_something (foo) # (1, 2, 3) Performs some possible processing and returns ...

Strictly speaking, assertions often deal only with truly abnormal cases. For example, it is better to deal with the problem by explicitly confirming the value by conditional branching such as if statement instead of assertion. It may be common. However, to keep things simple, we'll deliberately extend the scope of assertions to include those that are usually written without assertions in other programming languages.

Also, unlike python, which is the base of Ansible, in other programming languages ​​with a powerful static attachment mechanism (Haskell, Rust, Scala, etc.), the compiler does the type checking in the first place, and whether variables can be changed (immutable or mutable?). ) Can be determined at the time of declaration (Rust, Scala, etc.) or not (Haskell, etc.), and even variable range constraints can be created at the type level (depending on the language with [Dependent type] 12). You can define the type, for example AGDA) It may not be necessary to express it in the first place, but it will be a big difference, so I will not touch it here.

Bad example without Assertion

So far, I've explained on the premise that it's bad if you don't check the variables themselves and their values ​​in various ways, but here's just one example where very bad things can happen.

# Never run Ansible Playbook
- hosts: localhost
  gather_facts: false
  connection: local
  become: true
  var:
    workdir: "" #Danger!
  tasks: tasks:
    #Disaster !!
    - name: Balse!
      command:>-
       rm -rf / {{workdir}}
  • Never run the above playbook as a sudo NOPASSWORD user even if you make a mistake !!! *

Assertive Programming in Ansible

Ansible also has an assert module, which seems to be able to realize Assertive Programming, so I will try it.

Check variable definition

Regarding how to use the assert module, you can easily write it as follows. (See the description of ansible-doc assert for details.)

- name: <assert task description ...>
  assert: assert:
    that:
      -Expression # 1 (Write an expression that evaluates to a boolean value)
      - Statement 2
        ... # Subsequent abbreviations and abbreviations are indicated by'...'
    fail_msg: Evaluate the declaration expressions in order, stop processing immediately if false, and output the message
  • List the expressions in the Jinja2 template that will result in true / false as a result of the final evaluation in that clause.
  • If you specify a character string in the fail_msg clause, it will be output when assertion fails.

Let's actually use the assert module to see if the variables are defined as we did in the python code. It's a bit long, but it can be defined by [defined in Jinja 2's Builtin test] 4.

  • [Example of checking if a variable is defined] Excerpt from 5

    - name: Check variables may have primitive values ​​are defined
      assert: assert:
        that:
          - sape_do_more_advanced_checks is defined
            ...
  • [Default definition of variables referenced above] 6

Variable type check

Similarly, let's use the assert module to perform variable type checking as we did in python code.

  • [Variable type check example] 9

    - name: Check the types of the variables may have primitive values
      assert: assert:
        that:
          - sape_a_str_0 is string
            ...
        fail_msg: |
          - sape_a_str_0: {{sape_a_str_0 | d ()}}
            ...
    
    - name: Check the types of the variables may have non primitive values
      assert: assert:
        that:
          - sape_a_list_0 is sequence
            ...
        fail_msg: |
          - sape_a_list_0: {{sape_a_list_0 | d () | to_nice_json}}
            ...

Jinja 2's test to check the types that can actually be used for type checking is very limited, and type checking such as whether it is a list consisting of exactly string elements is not possible, but strings and numbers You can check the difference.

If you want to output to fail_msg etc. for debug purpose regardless of whether the variable is defined, you should use d filter ([Jinja2 default filter] 7 alias) etc. as in this example. And if there is a possibility that the variable can be a list or mapping type at that time, it can not be output as it is, so it is good to use [Ansible Jinja 2 extension to_nice_json filter] 8 so that it can be expressed as a character string. Probably.

Variable value check

Now let's use the assert module to check the value of the variable as in the python code example above.

  • [Variable value check example] 10 excerpt
- name: Check values ​​of the variables should have primitive values
  assert: assert:
    that:
      - sape_do_more_advanced_checks in [true, false]
        ...
      - sape_a_str_1 | length> 0
      - sape_a_int_0 == 0
      - sape_a_int_1! = 0
      - sape_a_int_1> 0
        ...

- name: Check values ​​of the variables may have non primitive values
  assert: assert:
    that:
      ...
      - sape_a_dict_1 | length> 0
      - sape_a_dict_1.a! = 0
      - sape_a_dict_1.a> 0
      - sape_a_dict_1.b | length> 0
        ...

In some cases, value checking can be used to replace things that cannot be done with type checking, such as boolean checking. Also, it seems that casting a list or dict with a value with bool does not result in true, unlike python. This restriction is a little disappointing, but I will replace it with lengh> 0 etc.

Other more complex "statement" checks, etc.

Not only the assert module, but it can no longer be called an assertion, but it is a good idea to prioritize Fail First for tasks that are checked by other methods such as the command module because it is a check of preconditions in a broad sense.

-[Examples of other more complex "statement" checks] 11

It's a good idea to combine the command module and other modules that immediately result in an error if they fail, and assert for those that don't.

summary

  • In Ansible, variable constraints and functions are weak, which affects the robustness of the code.
  • Even in Ansible, you can do Assertive Programming by using assert module etc.
  • Even with Ansible, if you do Assertive Programming, you can do Defensive Programming while doing Fail First.

supplement

Using the type_debug filter

I didn't mention it because I don't like it subjectively, but it seems that you can use [type_debug] 13 to find out the type name of a variable in Python representation. You can use this to handle some difficult cases with Jinja2 type testing.

About sample Ansible Role and its tests

The [Ansible Role example] 14 used in this article has been tested with Travis-CI and GitLab-CI.

  • Test driver: [Use tox] 15
  • Test tools: molecule (using delegated driver) and [bats] 16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment