테스트

테스트 (Tests)

Jinja에서 테스트(test)는 템플릿 표현식을 평가해서 TrueFalse를 돌려주는 방법이에요. Jinja는 이미 많은 내장 테스트를 갖고 있고, 공식 Jinja 템플릿 문서의 기본 제공 테스트를 참고할 수 있어요.

테스트와 필터의 가장 큰 차이는 테스트는 비교에 쓰이고, 필터는 데이터 조작에 쓰인다는 점이에요. 또한 테스트는 map(), select() 같은 목록 처리 필터 안에서도 쓸 수 있어 목록의 항목을 골라내는 데 활용돼요. 다른 모든 템플릿 처리와 마찬가지로 테스트도 항상 Ansible 컨트롤 노드에서 실행되고, 작업의 대상 머신에서는 실행되지 않아요 — 로컬 데이터를 검사하기 때문이에요. Jinja2 테스트 외에도 Ansible은 몇 가지 테스트를 더 제공하고, 사용자가 직접 만들기도 쉬워요.

출처: 문서

본문

테스트 문법

테스트 문법은 필터 문법(variable | filter)과 달라요. 역사적으로 Ansible은 테스트를 Jinja 테스트와 Jinja 필터 양쪽으로 등록해서, 필터 문법으로도 참조할 수 있게 했어요. 하지만 Ansible 2.5부터는 필터로 Jinja 테스트를 쓰면 deprecation 경고가 나고, 2.9+ 부터는 테스트 문법을 쓰는 것이 필수가 됐어요.

Jinja 테스트를 쓰는 문법은 다음과 같아요.

variable is test_name

예를 들면,

result is failed

문자열 테스트

문자열을 부분 문자열(substring)이나 정규식에 매칭할 때는 match, search, regex 테스트를 써요.

vars:
  url: "https://example.com/users/foo/resources/bar"

tasks:
    - debug:
        msg: "matched pattern 1"
      when: url is match("https://example.com/users/.*/resources")

    - debug:
        msg: "matched pattern 2"
      when: url is search("users/.*/resources/.*")

    - debug:
        msg: "matched pattern 3"
      when: url is search("users")

    - debug:
        msg: "matched pattern 4"
      when: url is regex("example\.com/\w+/foo")

match는 문자열 시작 부분에 패턴이 있으면 성공하고, search는 문자열 어디에든 패턴이 있으면 성공해요. 기본적으로 regexsearch처럼 동작하지만, match_type 키워드 인자를 넘겨 다른 검사도 수행하도록 구성할 수 있어요. match_type은 검색에 쓰일 re 메서드를 결정해요. 전체 목록은 관련 파이썬 문서에서 확인할 수 있어요.

모든 문자열 테스트는 선택적인 ignorecasemultiline 인자도 받아요. 각각 파이썬 re 라이브러리의 re.Ire.M에 대응해요.

Vault

2.10 버전에 새로 추가됐어요.

변수가 인라인 단일 vault 암호화 값인지 확인하려면 vault_encrypted 테스트를 쓸 수 있어요.

vars:
  variable: !vault |
    $ANSIBLE_VAULT;1.2;AES256;dev
    61323931353866666336306139373937316366366138656131323863373866376666353364373761
    3539633234313836346435323766306164626134376564330a373530313635343535343133316133
    36643666306434616266376434363239346433643238336464643566386135356334303736353136
    6565633133366366360a326566323363363936613664616364623437336130623133343530333739
    3039

tasks:
  - debug:
      msg: '{{ (variable is vault_encrypted) | ternary("Vault encrypted", "Not vault encrypted") }}'

참·거짓 (truthiness) 테스트

2.10 버전에 새로 추가됐어요.

Ansible 2.10부터 파이썬처럼 truthy/falsy 검사를 할 수 있어요.

- debug:
    msg: "Truthy"
  when: value is truthy
  vars:
    value: "some string"

- debug:
    msg: "Falsy"
  when: value is falsy
  vars:
    value: ""

또한 truthyfalsy 테스트는 convert_bool이라는 선택 매개변수를 받아서 불리언 표시를 실제 불리언으로 변환하려 시도해요.

- debug:
    msg: "Truthy"
  when: value is truthy(convert_bool=True)
  vars:
    value: "yes"

- debug:
    msg: "Falsy"
  when: value is falsy(convert_bool=True)
  vars:
    value: "off"

버전 비교

1.6 버전에 새로 추가됐어요.

참고: 2.5에서 version_compareversion으로 이름이 바뀌었어요.

버전 번호를 비교할 때, 예를 들어 ansible_facts['distribution_version']이 '12.04'보다 크거나 같은지 확인하려면 version 테스트를 써요.

"{{ ansible_facts['distribution_version'] is version('12.04', '>=') }}"

ansible_facts['distribution_version']이 12.04보다 크거나 같으면 이 테스트는 True를, 그렇지 않으면 False를 돌려줘요.

version 테스트는 다음 연산자를 받아요.

<, lt, <=, le, >, gt, >=, ge, ==, =, eq, !=, <>, ne

이 테스트는 세 번째 매개변수 strict도 받아요. 이는 ansible.module_utils.compat.version.StrictVersion에 정의된 엄격한 버전 파싱을 쓸지 결정해요. 기본값은 False(LooseVersion 사용)이고, True면 엄격한 버전 파싱을 켜요.

{{ sample_version_var is version('1.0', operator='lt', strict=True) }}

Ansible 2.11부터 version 테스트는 strict와 상호 배타적인 version_type 매개변수를 받아요. 값은 다음과 같아요.

loose, strict, semver, semantic, pep440
  • loose — 파이썬 distutils.version.LooseVersion 클래스에 해당해요. 모든 버전 형식이 이 타입에 유효해요. 비교 규칙은 단순하고 예측 가능하지만, 항상 기대한 결과를 주지는 않을 수 있어요.
  • strict — 파이썬 distutils.version.StrictVersion 클래스에 해당해요. 버전 번호는 점으로 구분된 2~3개의 숫자 구성 요소로 이뤄지고, 끝에 선택적인 '프리릴리즈' 태그가 붙을 수 있어요. 프리릴리즈 태그는 'a'나 'b'라는 단일 글자 뒤에 숫자가 오는 형태예요. 두 버전 번호의 숫자 구성 요소가 같다면 프리릴리즈 태그가 있는 쪽이 항상 더 이른(작은) 버전으로 간주돼요.
  • semver / semantic — 버전 비교에 Semantic Version 체계를 구현해요.
  • pep440 — 버전 비교에 파이썬 PEP-440 버저닝 규칙을 구현해요. 2.14 버전에 추가됐어요.

version_type으로 semantic 버전을 비교하려면 다음과 같이 해요.

"{{ sample_semver_var is version('2.0.0-rc.1+build.123', 'lt', version_type='semver') }}"

Ansible 2.14에서 version_typepep440 옵션이 추가됐는데, 그 규칙은 PEP-440에 정의돼 있어요. 다음 예시는 이 타입이 프리릴리즈를 일반 릴리즈보다 작은 것으로 구분하는 방법을 보여줘요.

"{{ '2.14.0rc1' is version('2.14.0', 'lt', version_type='pep440') }}"

플레이북이나 역할에서 version을 쓸 때는 FAQ에 설명된 대로 {{ }}를 쓰지 마세요.

vars:
    my_version: 1.2.3

tasks:
    - debug:
        msg: "my_version is higher than 1.0.0"
      when: my_version is version('1.0.0', '>')

집합 이론 테스트

2.1 버전에 새로 추가됐어요.

참고: 2.5에서 issubset·issupersetsubset·superset으로 이름이 바뀌었어요.

어떤 목록이 다른 목록을 포함하는지, 또는 다른 목록에 포함되는지 보려면 'subset'과 'superset'을 써요.

vars:
    a: [1,2,3,4,5]
    b: [2,3]
tasks:
    - debug:
        msg: "A includes B"
      when: a is superset(b)

    - debug:
        msg: "B is included in A"
      when: b is subset(a)

목록이 값을 포함하는지 테스트

2.8 버전에 새로 추가됐어요.

Ansible에는 Jinja2가 제공하는 in 테스트와 비슷하지만 반대로 동작하는 contains 테스트가 있어요. contains 테스트는 select, reject, selectattr, rejectattr 필터와 함께 쓰도록 설계됐어요.

vars:
  lacp_groups:
    - master: lacp0
      network: 10.65.100.0/24
      gateway: 10.65.100.1
      dns4:
        - 10.65.100.10
        - 10.65.100.11
      interfaces:
        - em1
        - em2

    - master: lacp1
      network: 10.65.120.0/24
      gateway: 10.65.120.1
      dns4:
        - 10.65.100.10
        - 10.65.100.11
      interfaces:
          - em3
          - em4

tasks:
  - debug:
      msg: "{{ (lacp_groups|selectattr('interfaces', 'contains', 'em1')|first).master }}"

목록 값이 True인지 테스트

2.4 버전에 새로 추가됐어요.

anyall을 써서 목록의 요소 중 하나라도(any), 또는 전부(all) 참인지 확인할 수 있어요.

vars:
  mylist:
      - 1
      - "{{ 3 == 3 }}"
      - True
  myotherlist:
      - False
      - True
tasks:

  - debug:
      msg: "all are true!"
    when: mylist is all

  - debug:
      msg: "at least one is true"
    when: myotherlist is any

경로 테스트

참고: 2.5에서 다음 테스트들은 is_ 접두사를 제거하도록 이름이 바뀌었어요.

다음 테스트들은 컨트롤 노드의 경로에 대한 정보를 제공해요.

- debug:
    msg: "path is a directory"
  when: mypath is directory

- debug:
    msg: "path is a file"
  when: mypath is file

- debug:
    msg: "path is a symlink"
  when: mypath is link

- debug:
    msg: "path already exists"
  when: mypath is exists

- debug:
    msg: "path is {{ (mypath is abs)|ternary('absolute','relative')}}"

- debug:
    msg: "path is the same file as path2"
  when: mypath is same_file(path2)

- debug:
    msg: "path is a mount"
  when: mypath is mount

- debug:
    msg: "path is a directory"
  when: mypath is directory
  vars:
     mypath: /my/path

- debug:
    msg: "path is a file"
  when: "'/my/path' is file"

크기 형식 테스트

human_readablehuman_to_bytes 함수는 플레이북이 작업에서 올바른 크기 형식을 쓰는지 확인하게 해 줘요. 컴퓨터에는 Byte 형식을, 사람에게는 사람이 읽을 수 있는 형식을 제공하고 있는지 검증하는 거예요.

Human readable

주어진 문자열이 사람이 읽을 수 있는 형식인지 아닌지 단언(assert)해요.

예를 들면,

- name: "Human Readable"
  assert:
    that:
      - '"1.00 Bytes" == 1|human_readable'
      - '"1.00 bits" == 1|human_readable(isbits=True)'
      - '"10.00 KB" == 10240|human_readable'
      - '"97.66 MB" == 102400000|human_readable'
      - '"0.10 GB" == 102400000|human_readable(unit="G")'
      - '"0.10 Gb" == 102400000|human_readable(isbits=True, unit="G")'

이 결과는 다음과 같아요.

{ "changed": false, "msg": "All assertions passed" }

Human to bytes

주어진 문자열을 Byte 형식으로 돌려줘요.

예를 들면,

- name: "Human to Bytes"
  assert:
    that:
      - "{{'0'|human_to_bytes}}        == 0"
      - "{{'0.1'|human_to_bytes}}      == 0"
      - "{{'0.9'|human_to_bytes}}      == 1"
      - "{{'1'|human_to_bytes}}        == 1"
      - "{{'10.00 KB'|human_to_bytes}} == 10240"
      - "{{   '11 MB'|human_to_bytes}} == 11534336"
      - "{{  '1.1 GB'|human_to_bytes}} == 1181116006"
      - "{{'10.00 Kb'|human_to_bytes(isbits=True)}} == 10240"

이 결과는 다음과 같아요.

{ "changed": false, "msg": "All assertions passed" }

작업 결과 테스트

다음 작업들은 작업의 상태를 확인하기 위한 테스트들을 보여주는 예시예요.

tasks:

  - shell: /usr/bin/foo
    register: result
    ignore_errors: True

  - debug:
      msg: "it failed"
    when: result is failed

  # in most cases you'll want a handler, but if you want to do something right now, this is nice
  - debug:
      msg: "it changed"
    when: result is changed

  - debug:
      msg: "it succeeded in Ansible >= 2.1"
    when: result is succeeded

  - debug:
      msg: "it succeeded"
    when: result is success

  - debug:
      msg: "it was skipped"
    when: result is skipped

참고: 2.1부터 엄격하게 문법을 맞춰야 하는 사람들을 위해 success, failure, change, skip을 쓸 수도 있어요.

타입 테스트

타입을 확인할 때 type_debug 필터를 써서 그 타입의 문자열 이름과 비교하고 싶어질 수 있는데, 대신 타입 테스트 비교를 쓰는 게 좋아요. 예:

tasks:
  - name: "String interpretation"
    vars:
      a_string: "A string"
      a_dictionary: {"a": "dictionary"}
      a_list: ["a", "list"]
    assert:
      that:
      # Note that a string is classed as also being "iterable" and "sequence", but not "mapping"
      - a_string is string and a_string is iterable and a_string is sequence and a_string is not mapping

      # Note that a dictionary is classed as not being a "string", but is "iterable", "sequence" and "mapping"
      - a_dictionary is not string and a_dictionary is iterable and a_dictionary is mapping

      # Note that a list is classed as not being a "string" or "mapping" but is "iterable" and "sequence"
      - a_list is not string and a_list is not mapping and a_list is iterable

  - name: "Number interpretation"
    vars:
      a_float: 1.01
      a_float_as_string: "1.01"
      an_integer: 1
      an_integer_as_string: "1"
    assert:
      that:
      # Both a_float and an_integer are "number", but each has their own type as well
      - a_float is number and a_float is float
      - an_integer is number and an_integer is integer

      # Both a_float_as_string and an_integer_as_string are not numbers
      - a_float_as_string is not number and a_float_as_string is string
      - an_integer_as_string is not number and a_float_as_string is string

      # a_float or a_float_as_string when cast to a float and then to a string should match the same value cast only to a string
      - a_float | float | string == a_float | string
      - a_float_as_string | float | string == a_float_as_string | string

      # Likewise an_integer and an_integer_as_string when cast to an integer and then to a string should match the same value cast only to an integer
      - an_integer | int | string == an_integer | string
      - an_integer_as_string | int | string == an_integer_as_string | string

      # However, a_float or a_float_as_string cast as an integer and then a string does not match the same value cast to a string
      - a_float | int | string != a_float | string
      - a_float_as_string | int | string != a_float_as_string | string

      # Again, Likewise an_integer and an_integer_as_string cast as a float and then a string does not match the same value cast to a string
      - an_integer | float | string != an_integer | string
      - an_integer_as_string | float | string != an_integer_as_string | string

  - name: "Native Boolean interpretation"
    loop:
    - yes
    - true
    - True
    - TRUE
    - no
    - No
    - NO
    - false
    - False
    - FALSE
    assert:
      that:
      # Note that while other values may be cast to boolean values, these are the only ones that are natively considered boolean
      # Note also that `yes` is the only case-sensitive variant of these values.
      - item is boolean

더 알아보기 (Learn more)

  • Ansible playbooks — 플레이북 소개
  • Conditionals — 플레이북의 조건문
  • Using variables — 변수에 대한 모든 것
  • Loops — 플레이북에서 반복하기
  • Roles — 역할로 플레이북 구성하기
  • General tips — 플레이북 팁과 요령
  • Communication — 질문·도움이 필요하면 Ansible 커뮤니케이션 가이드 방문