尝试在Gitlab CI中进行Ansible的自动化测试

这篇文章是2021年Ansible Advent日历的第24天。

首先

大家好,你们写了Ansible的测试吗?我觉得在我所见过的各种场合中,使用Ansible写测试的人可能不是很多。

因此,这次我要介绍一个为了试验圣诞节日历而创建的自动化测试的实践仓库。

通过完成这些任务,你也会成为Ansible自动化测试大师!

步骤

1. 项目准备

1.1. 登录GitLab.com

请使用我之前提供的账号,在https://gitlab.com/users/sign_in上登录到GitLab.com。

如果您尚未创建用户帐户,请从 https://gitlab.com/users/sign_up 进行用户创建。请注意,在创建帐户时需要进行电子邮件地址的确认。

1.2. 分叉项目的原始版本

为了能够在GitLab上执行CI流水线和编辑代码,请按照以下步骤在您的账户下创建该项目的派生项目(fork)。

    1. 在浏览器中访问 https://gitlab.com/konono/ansible_ci_demo,点击右上角的**Fork*按钮

 

    1. 在跳转至Fork项目页面上选择显示在您账户中的Select按钮

 

    确认已经切换到您fork的项目https://gitlab.com/{您的用户ID}/ansible_ci_demo

1.3. 克隆存储库

git clone [your repositoy]

安装1.4 pyenv。

# Clone repository
git clone https://github.com/pyenv/pyenv.git ~/.pyenv

# Configure environment
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(pyenv init -)"' >> ~/.bash_profile

# Instlal python

pyenv install 3.7.7
pyenv install 3.8.3

2. 静态测试

2.1.1. ansible-lint的意义。

我们来使用以下命令来安装ansible-lint。

pip3 ansible-lint

请尝试按以下方式编辑文件:
删除“name: Ensure proper Apache configuration”。

diff --git a/roles/apache/tasks/main.yml b/roles/apache/tasks/main.yml
index bd0aeac..af74986 100644
--- a/roles/apache/tasks/main.yml
+++ b/roles/apache/tasks/main.yml
@@ -5,9 +5,8 @@
     state: present
   notify: apache-restart

-- name: Ensure proper Apache configuration  <-- nameをtask optionから削除する
-  template:
-    src: httpd.conf.j2
+- template:
+    src: httpd.conf.j2
     dest: /etc/httpd/conf/httpd.conf
     mode: 0644
   notify: apache-restart

请执行ansible-lint命令,检查是否能够进行lint检查。

预测的执行结果

❯ ansible-lint
WARNING  Listing 1 violation(s) that are fatal
unnamed-task: All tasks should be named
roles/apache/tasks/main.yml:8 Task/Handler: template src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf mode=420

You can skip specific rules or tags by adding them to your configuration file:
# .ansible-lint
warn_list:  # or 'skip_list' to silence them completely
  - unnamed-task  # All tasks should be named

Finished with 1 failure(s), 0 warning(s) on 22 files.

2.1.2. 自定义 Ansible-lint 校验规则

通过 Ansible-lint,您可以使用Python编写规则来创建自定义的lint rule,这些规则可以用于编码约定等。
例如,如果有规则要求统一使用.yml扩展名的YAML文件,则可以如下所示表达。

猫咪规定/Yaml扩展规则.py

from ansiblelint.constants import odict
from ansiblelint.errors import MatchError
from ansiblelint.file_utils import Lintable
from ansiblelint.rules import AnsibleLintRule
from typing import Any
from typing import List


class YamlExtensionRule(AnsibleLintRule):
    id = 'yaml-extension'
    shortdesc = 'Playbooks should have the ".yml" extension'
    description = ''
    tags = ['yaml']
    done = []

    def matchplay(
        self, file: Lintable, data: "odict[str, Any]"
    ) -> List[MatchError]:
        if file.path.suffix in ('.yaml'):
            return [
                self.create_matcherror(
                    message="Playbook doesn't have '.yml' extension: " +
                    str(data['__file__']) + ". " + self.shortdesc,
                    linenumber=data['__line__'],
                    filename=file
                )
            ]
        return []

接下来,我们将创建测试文件。

vim site.yaml
---
- name: 'Lint test playbook'
  hosts: 'all'
  tasks:
    - name: 'Include apache'
      include_role:
        name: 'apache'

那么,让我们试用一下自己编写的定制检测规则吧。

git add site.yaml
❯ ansible-lint
WARNING  Listing 1 violation(s) that are fatal
yaml-extension: Playbook doesn't have '.yml' extension: site.yaml. Playbooks should have the ".yml" extension
site.yaml:2

You can skip specific rules or tags by adding them to your configuration file:
# .ansible-lint
warn_list:  # or 'skip_list' to silence them completely
  - yaml-extension  # Playbooks should have the ".yml" extension

Finished with 1 failure(s), 0 warning(s) on 23 files.

在檢測到yaml文件後,我們確認能夠返回錯誤。

最后,执行 git reset –hard 命令,将编辑过的文件恢复至原始状态。

2.2. YAML语法检查工具

让我们使用以下命令安装yamllint。

pip3 yamllint

请尝试按以下方式编辑文件:
在 “src:” 后面添加 [ ]。

❯ git diff
diff --git a/roles/apache/tasks/main.yml b/roles/apache/tasks/main.yml
index a54e21a..b7c8562 100644
--- a/roles/apache/tasks/main.yml
+++ b/roles/apache/tasks/main.yml
@@ -7,7 +7,7 @@

 - name: Ensure proper Apache configuration
   template:
-    src: httpd.conf.j2
+    src:  httpd.conf.j2
     dest: /etc/httpd/conf/httpd.conf
     mode: 0644
   notify: apache-restart

请执行yamllint. , 确认能够进行lint检查。

预测的执行结果。

❯ yamllint .
./roles/apache/tasks/main.yml
  10:10     error    too many spaces after colon  (colons)

我能够检测到违反默认规则的YAML。

最後,运行git reset –hard命令,将编辑过的文件恢复到原始状态。

3. 动态测试

3.1 分子

让我们使用以下命令来安装molecule。

pip3 molecule[docker]

将以下3个文件放置在roles/apache/molecule/default文件夹中。

❯ cat roles/apache/molecule/default/converge.yml
---
- name: Converge
  hosts: all
  tasks:
    - name: "Include apache"
      include_role:
        name: "apache"
❯ cat roles/apache/molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance
    image: registry.access.redhat.com/ubi8/ubi-init
    pre_build_image: true
    command: /sbin/init
    privileged: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
provisioner:
  name: ansible
verifier:
  name: ansible
❯ cat roles/apache/molecule/default/verify.yml
---
# This is an example playbook to execute Ansible tests.

- name: Verify
  hosts: all
  gather_facts: false
  tasks:
    - name: Check httpd server is running
      uri:
        url: http://localhost
        status_code: 200

接下来,请转到`roles/apache/`目录并执行`molecule test`命令。

使用该分子的单元测试中,设置容器并执行安装Apache的角色。然后执行HTTP GET请求并测试是否返回200。

预测的执行结果

❯ molecule test
--> Test matrix

└── default
    ├── dependency
    ├── lint
    ├── cleanup
    ├── destroy
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    ├── cleanup
    └── destroy

--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'lint'
--> Lint is disabled.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
--> Sanity checks: 'docker'
    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=instance)

    TASK [Wait for instance(s) deletion to complete] *******************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

--> Scenario: 'default'
--> Action: 'syntax'
    playbook: /Users/yyamashi/gitrepo/ansible_ci_demo/roles/apache/molecule/default/converge.yml
--> Scenario: 'default'
--> Action: 'create'
    PLAY [Create] ******************************************************************

    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item=None)

    TASK [Check presence of custom Dockerfiles] ************************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Create Dockerfiles from image names] *************************************
    skipping: [localhost] => (item=None)

    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Build an Ansible compatible image (new)] *********************************
    skipping: [localhost] => (item=molecule_local/registry.access.redhat.com/ubi8/ubi-init)

    TASK [Create docker network(s)] ************************************************

    TASK [Determine the CMD directives] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=instance)

    TASK [Wait for instance(s) creation to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost]

    PLAY RECAP *********************************************************************
    localhost                  : ok=5    changed=2    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0

--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
--> Scenario: 'default'
--> Action: 'converge'
    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [instance]

    TASK [Include apache] **********************************************************

    TASK [apache : Ensure apache is installed] *************************************
    changed: [instance]

    TASK [apache : Ensure proper Apache configuration] *****************************
    changed: [instance]

    TASK [apache : Deploy index.html] **********************************************
    changed: [instance]

    RUNNING HANDLER [apache : apache-restart] **************************************
    changed: [instance]

    PLAY RECAP *********************************************************************
    instance                   : ok=5    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Running Ansible Verifier
    PLAY [Verify] ******************************************************************

    TASK [Check httpd server is running] *******************************************
    ok: [instance]

    PLAY RECAP *********************************************************************
    instance                   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=instance)

    TASK [Wait for instance(s) deletion to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

--> Pruning extra files from scenario ephemeral directory

使用分子进行的动态测试到此结束。

4. 测试的自动化

4.1. 使用pre-commit来自动化静态测试

使用ansible-lint和yamllint,可以对ansible playbook和yaml进行静态测试。

然而,即使我们明确告知团队要使用这些工具,由于人类本性,有时候仍会忘记执行并提交。

因此,您可以使用标题中的pre-commit,在git commit时自动执行Lint测试。

让我们使用以下命令安装pre-commit插件。

pip3 install pre-commit

请按照以下方式来配置pre-commit。

---
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.4.0
    hooks:
      - id: check-case-conflict
      - id: check-merge-conflict
      - id: check-yaml
      - id: end-of-file-fixer
      - id: mixed-line-ending
  - repo: https://github.com/ansible/ansible-lint.git
    rev: v5.0.12
    hooks:
      - id: ansible-lint
        name: Ansible-lint
        description: This hook runs ansible-lint.
        entry: ansible-lint
        language: python
        # do not pass files to ansible-lint, see:
        # https://github.com/ansible/ansible-lint/issues/611
        always_run: true
        pass_filenames: false
  - repo: https://github.com/adrienverge/yamllint.git
    rev: v1.26.3
    hooks:
      - id: yamllint

首先将pre-commit应用到该仓库中。

pre-commit install

请尝试按照以下方式编辑文件。在”src:”之后添加方括号[ ]。

--- roles/apache/tasks/main.yml 2021-07-13 13:26:00.000000000 +0900
+++ roles/apache/tasks/main.yml_1       2021-07-14 23:01:58.000000000 +0900
@@ -7,7 +7,7 @@

 - name: Ensure proper Apache configuration
   template:
-    src: httpd.conf.j2
+    src:  httpd.conf.j2
     dest: /etc/httpd/conf/httpd.conf
     mode: 0644
   notify: apache-restart

让我们使用以下命令确认pre-commit是否正常运行。

git add .
❯ git commit -m 'TEST'
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
Check Yaml...............................................................Passed
Fix End of Files.........................................................Passed
Mixed line ending........................................................Passed
Ansible-lint.............................................................Failed
- hook id: ansible-lint
- exit code: 2

Loading custom .yamllint.yml config file, this extends our internal yamllint config.
WARNING  Listing 1 violation(s) that are fatal
yaml: too many spaces after colon (colons)
roles/apache/tasks/main.yml:10

You can skip specific rules or tags by adding them to your configuration file:
# .ansible-lint
warn_list:  # or 'skip_list' to silence them completely
  - yaml  # Violations reported by yamllint

Finished with 1 failure(s), 0 warning(s) on 22 files.

yamllint.................................................................Failed
- hook id: yamllint
- exit code: 1

roles/apache/tasks/main.yml
  10:10     error    too many spaces after colon  (colons)

flake8...............................................(no files to check)Skipped

通过在提交代码时运行ansible-lint和yamllint,您可以在提交之前检查并判断出不合规范的代码。

使用tox进行测试环境的自动化构建和多版本测试。

通过使用tox,我们可以在不破坏本地的Python环境的情况下进行测试。

此外,还可以实现Python和Library的多版本测试。

configuration是指在tox.ini文件中记录用于测试的命令、所需的测试包以及要测试的Python版本的信息。

这次的描述如下。

[tox]
envlist =
    py{37,38}-ansible29
    py{37,38}-pytest624
    py{37,38}-ansiblelint5012
    py{37,38}-flake8
skipsdist = True

[testenv]
passenv =
    TERM
[testenv:py{37,38}-ansible29]
passenv =
    DOCKER_HOST
    DOCKER_TLS_CERTDIR
    DOCKER_TLS_VERIFY
    DOCKER_CERT_PATH
setenv =
    MOLECULE_DEBUG = 1
deps =
    molecule[docker]
    ansible29: ansible>=2.9,<2.10
    yamllint
changedir = {toxinidir}/roles/apache
commands =
    molecule --version
    yamllint --version
    molecule test

[testenv:py{37,38}-pytest624]
deps =
    ansible>=2.9,<2.10
    ansible-lint==5.0.12
    -rtest-requirements.txt
changedir = {toxinidir}
commands =
    pytest --version
    pytest -v {toxinidir}/tests/Test_YamlExtensionRule.py

[testenv:py{37,38}-ansiblelint5012]
deps =
    ansible>=2.9,<2.10
    ansible-lint==5.0.12
    -rtest-requirements.txt
passenv =
changedir = {toxinidir}
commands =
    ansible-lint --version
    ansible-lint
[testenv:py{37,38}-flake8]
deps =
    hacking==4.1.0
passenv =
changedir = {toxinidir}
commands =
    flake8

只需运行tox命令,就可以自动创建venv环境、安装所需的包,并执行测试命令。

另外,可以通过使用-e选项只运行特定的测试。

在这个示例中,您可以尝试以下测试。

    • py{37,38}-ansible29: py37,38を使ったroles/apachのmoleculeテスト

 

    • py{37,38}-pytest624: py37,38を使ったrules/配下のpythonに対してのpytest

 

    • py{37,38}-ansiblelint5012: py37,38を使ったansible-lint

 

    py{37,38}-flake8: py37,38を使ったpythonのコーディングスタイルテスト

tox的执行示例

❯ tox -e py38-ansiblelint5012
py38-ansiblelint5012 installed: ansible==2.9.23,ansible-lint==5.0.12,apipkg==1.5,attrs==21.2.0,bracex==2.1.1,cffi==1.14.6,colorama==0.4.4,commonmark==0.9.1,coverage==5.5,cryptography==3.4.7,enrich==1.2.6,execnet==1.8.1,flaky==3.7.0,iniconfig==1.1.1,Jinja2==3.0.1,MarkupSafe==2.0.1,packaging==20.9,pluggy==0.13.1,psutil==5.8.0,py==1.10.0,pycparser==2.20,Pygments==2.9.0,pyparsing==2.4.7,pytest==6.2.4,pytest-cov==2.12.1,pytest-forked==1.3.0,pytest-xdist==2.2.1,PyYAML==5.4.1,rich==10.2.2,ruamel.yaml==0.17.7,ruamel.yaml.clib==0.2.2,six==1.16.0,tenacity==7.0.0,toml==0.10.2,wcmatch==8.2
py38-ansiblelint5012 run-test-pre: PYTHONHASHSEED='934219599'
py38-ansiblelint5012 run-test: commands[0] | ansible-lint --version
ansible-lint 5.0.12 using ansible 2.9.23
py38-ansiblelint5012 run-test: commands[1] | ansible-lint
______________________________________________ summary _______________________________________________
  py38-ansiblelint5012: commands succeeded
  congratulations :)

4.3. 使用CI来自动化测试。

通过使用tox,我们可以通过tox进行静态测试和动态测试的执行。

让我们利用安装了tox的容器,使得可以通过GitlabCI来执行这些测试。

通过CI的实施,每当在存储库中新增代码时,将自动进行静态/动态测试,始终能够确认代码的高质量。

为了测试持续集成,您需要在代码库的根目录下创建以下文件。

❯ cat .gitlab-ci.yml
---
stages:
  - flake8
  - pytest
  - lint
  - molecule

image: quay.io/kono/python_tox

variables:
  ANSIBLE_FORCE_COLOR: 1
  PYTHONUNBUFFERED: 1
  DOCKER_HOST: tcp://docker:2376
  DOCKER_TLS_CERTDIR: "/certs"
  DOCKER_TLS_VERIFY: 1
  DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"

services:
  - docker:dind

before_script:
  - docker --version

py37-flake8:
  stage: flake8
  script:
    - tox -e py37-flake8

py38-flake8:
  stage: flake8
  script:
    - tox -e py38-flake8

py37-pytest624:
  stage: pytest
  script:
    - tox -e py37-pytest624

py38-pytest624:
  stage: pytest
  script:
    - tox -e py38-pytest624

py37-ansiblelint5012:
  stage: lint
  script:
    - tox -e py37-ansiblelint5012

py38-ansiblelint5012:
  stage: lint
  script:
    - tox -e py38-ansiblelint5012

py37-ansible29:
  stage: molecule
  script:
    - tox -e py37-ansible29

py38-ansible29:
  stage: molecule
  script:
    - tox -e py38-ansible29

在此之后,您可以使用CI/CD->管道->运行管道来执行先前设置的CI。

image1

以上是手把手教学的结束。

最后/最终

这样,大家的Ansible开发存储库的提交无疑会变得更加清晰整洁!

感谢您的参与,谢谢您的陪伴。
请继续享受Ansible圣诞日历!

明天由Hideki Saito先生来为今年的Ansible做一个总结!

广告
将在 10 秒后关闭
bannerAds