Ansible中的单元测试是确保角色按预期运行的关键。通过允许您指定针对不同环境测试角色的方案,Molecule使此过程更容易。使用Ansible,Molecule将角色卸载到配置器,该配置器在配置的环境中部署角色并调用验证器(例如Testinfra)来检查配置偏差。这可确保您的角色在该特定方案中对环境进行了所有预期的更改。
在本指南中,您将构建一个Ansible角色,将Apache部署到主机并配置Firewalld。要测试此角色是否按预期工作,您将使用Docker作为驱动程序在Molecule中创建测试,并使用Testinfra(用于测试服务器状态的Python库)创建测试。Molecule将提供Docker容器来测试角色,Testinfra将验证服务器是否已按预期配置。完成后,您将能够跨环境创建多个测试用例,并使用Molecule运行这些测试。
准备
在开始本指南之前,您需要以下内容:
注意:Molecule可以使用Python 2.7或Python 3.6。由于Ubuntu 16.04默认包含Python 3.5和2.7,因此我们将在本教程中安装和使用Python 2.7以使用内置存储库。
让我们首先在我们的主机上创建一个虚拟环境,然后在该环境中安装我们的测试所需的软件包。
首先以非root用户身份登录并确保您的存储库是最新的:
$ sudo apt-get update -y
这将确保您的软件包存储库包含python-pip
将安装的软件包的最新版本pip和Python 2.7。我们将用于pip创建虚拟环境并安装其他软件包。要安装pip,请运行:
$ sudo apt-get install -y python-pip
使用pip
安装virtualenvPython
模块和任何更新:
$ pip install pip virtualenv -U
该-U
标志告诉pip
更新任何以前安装的包。
接下来,让我们创建并激活虚拟环境:
$ virtualenv my_env
激活它以确保您的操作仅限于该环境:
$ . my_env/bin/activate
使用pip
安装molecule
,ansible
和docker-py
:
(my_env) sammy@ubuntu:$ pip install molecule ansible docker-py
以下是每个包的功能:
接下来,让我们在Molecule中创建一个角色。
在我们的环境设置之后,让我们使用Molecule来创建一个基本角色,我们将用它来测试Apache的安装。此过程将创建目录结构和一些初始测试,并将Docker指定为驱动程序,以便Molecule使用Docker运行其测试。
创建一个名为httpd
的新角色:
(my_env) sammy@ubuntu:$ molecule init role -r httpd -d docker
该-r
标志指定角色的名称,同时-d
指定驱动程序,该驱动程序为Molecule
提供主机以供测试。切换到新创建的角色的目录:
(my_env) sammy@ubuntu:$ cd httpd
测试默认角色以检查Molecule是否已正确设置:
(my_env) sammy@ubuntu:$ molecule test
您将看到将每一个列出默认测试操作的输出:
--> Validating schema /home/sammy/httpd/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix
└── default
├── lint
├── destroy
├── dependency
├── syntax
├── create
├── prepare
├── converge
├── idempotence
├── side_effect
├── verify
└── destroy
...
在开始测试之前,Molecule验证配置文件molecule.yml
以确保一切正常。它还会打印此测试矩阵,该矩阵指定测试操作的顺序。
一旦我们创建了角色并定制了测试,我们将详细讨论每个测试操作。现在,请注意PLAY_RECAP
每个测试,并确保没有任何默认操作返回failed
状态。例如,PLAY_RECAP
默认'create'操作应如下所示:
...
PLAY RECAP *********************************************************************
localhost : ok=5 changed=4 unreachable=0 failed=0
让我们继续修改我们的角色来配置Apache和Firewalld。
要配置Apache,我们将为角色创建任务文件,指定要安装的软件包和要启用的服务。这些详细信息将从我们用于替换默认Apache索引页的变量文件和模板中提取。
使用nano或您喜欢的文本编辑器为角色创建任务文件:
(my_env) sammy@ubuntu:$ nano tasks/main.yml
您将看到该文件已存在。删除那里的内容并粘贴以下代码去安装所需的软件包并启用正确的服务,HTML默认值和防火墙设置:
~/httpd/tasks/main.yml
---
- name: "Ensure required packages are present"
yum:
name: "{{ pkg_list }}"
state: present
- name: "Ensure latest index.html is present"
template:
src: index.html.j2
dest: /var/www/html/index.html
- name: "Ensure httpd service is started and enabled"
service:
name: "{{ item }}"
state: started
enabled: True
with_items: "{{ svc_list }}"
- name: "Whitelist http in firewalld"
firewalld:
service: http
state: enabled
permanent: True
immediate: True
这剧本包括4个任务:
pkg_list
变量文件中列出的包。变量文件将位于~/httpd/vars/main.yml
您将在本节末尾创建它。index.html.j2
并将其粘贴到Apache生成的默认/var/www/html/index.html
文件上。svc_list
变量文件中列出的服务。http
服务列入白名单firewalld
。Firewalld是一个完整的防火墙解决方案,默认存在于CentOS服务器上。要使http服务正常工作,我们需要公开所需的端口。指示firewalld将服务列入白名单可确保将服务所需的所有端口列入白名单。完成后保存并关闭文件。
接下来,让我们为index.html.j2
模板页面创建一个templates目录:
(my_env) sammy@ubuntu:$ mkdir templates
创建页面本身:
(my_env) sammy@ubuntu:$ nano templates/index.html.j2
粘贴以下样板代码:
~/httpd/templates/index.html.j2
<div style="text-align: center">
<h2>Managed by Ansible</h2>
</div>
保存并关闭文件。
完成角色的最后一步是编写变量文件,该文件为我们的主角色playbook提供包和服务的名称:
(my_env) sammy@ubuntu:$ nano vars/main.yml
使用以下代码粘贴默认内容,该代码指定pkg_list
和svc_list
:
vars/main.yml
---
pkg_list:
- httpd
- firewalld
svc_list:
- httpd
- firewalld
这些列表包含以下信息:
pkg_list
:这包含角色将安装的软件包的名称:httpd和firewalld。svc_list
:这包含角色将启动和启用的服务的名称:httpd和firewalld。注意:确保您的变量文件没有任何空行,否则您的测试将在linting期间失败。
现在我们已经完成了我们的角色创建,让我们配置Molecule以测试它是否按预期工作。
配置Molecule涉及两个步骤:修改Molecule配置文件本身,以及创建自定义yamllint
文件。Yamllint是一个YAML代码linter,用于检查语法有效性,密钥重复以及行长度,尾随空格和缩进等外观问题。
我们的修改包括:
~/httpd/molecule/default/molecule.yml
添加选项使用自定义yamllint
配置文件并创建文件本身。此文件将启用两个例外:大于80个字符的行和truthy值。因为Ansible和Yamllint使用冲突的语法来表达truthy值,这将防止不必要的语法错误。milcom/centos7-systemd
映像。特权模式几乎允许容器运行其主机的所有功能。让我们编辑molecule.yml
以反映这些变化:
(my_env) sammy@ubuntu:$ nano molecule/default/molecule.yml
添加yamllint
选项和平台信息:
~/httpd/molecule/default/molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
options:
config-file: molecule/default/yamllint.yml
platforms:
- name: centos7
image: milcom/centos7-systemd
privileged: True
provisioner:
name: ansible
lint:
name: ansible-lint
scenario:
name: default
verifier:
name: testinfra
lint:
name: flake8
完成后保存并关闭文件。
请注意在molecule.yml
中的参考文献molecule/default/yamllint.yml
的yamllint
文件。我们来创建这个文件:
(my_env) sammy@ubuntu:$ nano molecule/default/yamllint.yml
通过粘贴以下规则输入测试环境的自定义配置,这些规则定义了行的长度规范和真值设置:
~/httpd/molecule/default/yamllint.yml
---
extends: default
rules:
line-length:
max: 120
level: warning
truthy: disable
我们添加了两条规则:
line_length
:此规则指定允许的最大行长度为120个字符(最多80个字符),并且如果违反规则,则linter应发出警告。truthy
:此规则禁用truthy值,因为Ansible和Yamllint使用冲突的语法来表达它们。这将防止不必要的语法错误。既然我们已经成功配置了测试环境,那么让我们继续编写Molecule在执行角色后对我们的容器运行的测试用例。
在此角色的测试中,我们将检查以下条件:
httpd
和firewalld
正在安装的软件包。httpd
和firewalld
服务正在运行并启用。http
服务。index.html
包含我们的模板文件中指定的相同数据。如果所有这些测试都通过,则角色按预期工作。
要编写这些条件的测试用例,让我们编辑默认测试~/httpd/molecule/default/tests/test_default.py
。使用Testinfra,我们将测试用例编写为使用Molecule类的Python函数。
打开test_default.py
:
(my_env) sammy@ubuntu:$ nano molecule/default/tests/test_default.py
删除文件的内容,以便您可以从头开始编写测试。
注意:在编写测试时,请确保它们由两个新行分隔,否则它们将失败。
首先导入所需的Python模块:
~/httpd/molecule/default/tests/test_default.py
import os
import pytest
import testinfra.utils.ansible_runner
这些模块包括:
os
:这个内置的Python模块支持与操作系统相关的功能,使Python可以与底层操作系统进行交互。pytest
:该pytest模块可以进行测试编写。testinfra.utils.ansible_runner
:此Testinfra模块使用Ansible作为命令执行的后端。在模块导入下,粘贴以下代码,该代码使用Ansible后端返回当前主机实例:
~/httpd/molecule/default/tests/test_default.py
...
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')
将我们的测试文件配置为使用Ansible后端,让我们编写单元测试来测试主机的状态。
第一个测试将确保httpd
和firewalld
安装:
~/httpd/molecule/default/tests/test_default.py
...
@pytest.mark.parametrize('pkg', [
'httpd',
'firewalld'
])
def test_pkg(host, pkg):
package = host.package(pkg)
assert package.is_installed
测试使用pytest.mark.parametrize
,允许我们参数化测试的参数。第一个测试将test_pkg
作为参数来测试httpd
和firewalld
包的存在。
接下来的测试,检查是否httpd
与firewalld
正在运行并启用。它需要test_svc
作为参数:
~/httpd/molecule/default/tests/test_default.py
...
@pytest.mark.parametrize('svc', [
'httpd',
'firewalld'
])
def test_svc(host, svc):
service = host.service(svc)
assert service.is_running
assert service.is_enabled
最后一个测试检查传递给parametrize()
的文件和内容是否存在。如果文件不是由我们的角色创建的且内容设置不正确,assert
则会返回False
:
~/httpd/molecule/default/tests/test_default.py
...
@pytest.mark.parametrize('file, content', [
("/etc/firewalld/zones/public.xml", "<service name=\"http\"/>"),
("/var/www/html/index.html", "Managed by Ansible")
])
def test_files(host, file, content):
file = host.file(file)
assert file.exists
assert file.contains(content)
在每次测试中,assert
将根据测试结果来返回True或False。
最终的文件如下所示:
/httpd/molecule/default/tests/test_default.py
import os
import pytest
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')
@pytest.mark.parametrize('pkg', [
'httpd',
'firewalld'
])
def test_pkg(host, pkg):
package = host.package(pkg)
assert package.is_installed
@pytest.mark.parametrize('svc', [
'httpd',
'firewalld'
])
def test_svc(host, svc):
service = host.service(svc)
assert service.is_running
assert service.is_enabled
@pytest.mark.parametrize('file, content', [
("/etc/firewalld/zones/public.xml", "<service name=\"http\"/>"),
("/var/www/html/index.html", "Managed by Ansible")
])
def test_files(host, file, content):
file = host.file(file)
assert file.exists
assert file.contains(content)
现在我们已经指定了测试用例,让我们测试一下这个角色。
一旦我们启动测试,Molecule将执行我们在场景中定义的操作。我们将再次运行默认molecule场景,在默认测试序列中执行操作,同时更仔细地查看每个场景。
再次运行默认方案的测试:
(my_env) sammy@ubuntu:$ molecule test
这将启动测试运行。初始输出打印默认测试矩阵:
--> Validating schema /home/sammy/httpd/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix
└── default
├── lint
├── destroy
├── dependency
├── syntax
├── create
├── prepare
├── converge
├── idempotence
├── side_effect
├── verify
└── destroy
让我们来看看每个测试动作和预期的输出,从linting开始。
linting操作执行yamllint
,flake8
以及ansible-lint
:
yamllint
:此linter被执行在角色目录中的所有YAML文件上。flake8
:这个Python代码linter检查为Testinfra创建的测试。ansible-lint
:Ansible playbooks的这个linter在所有场景中都会执行。...
--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /home/sammy/httpd/...
Lint completed successfully.
--> Executing Flake8 on files found in /home/sammy/httpd/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /home/sammy/httpd/molecule/default/playbook.yml...
Lint completed successfully.
使用destroy.yml
文件执行下一个操作destroy。这样做是为了测试我们在新创建的容器上的角色。
默认情况下,destroy被调用两次:在测试运行开始时,删除任何预先存在的容器,最后删除新创建的容器:
...
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************
TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=None)
TASK [Wait for instance(s) deletion to complete] *******************************
ok: [localhost] => (item=None)
TASK [Delete docker network(s)] ************************************************
PLAY RECAP *********************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0
在销毁操作完成后,测试将继续进行依赖。 如果您的角色需要,此操作允许您从ansible-galaxy
中提取依赖项。
...
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
下一个测试操作是语法检查,它在默认的playbook.yml
playbook上执行。它的工作方式--syntax-check
与命令中的标志`ansible-playbook --syntax-check playbook.yml类似:
...
--> Scenario: 'default'
--> Action: 'syntax'
playbook: /home/sammy/httpd/molecule/default/playbook.yml
接下来,测试继续进行创建操作。使用我们角色的Molecule目录中的create.yml
文件来创建具有我们规范的Docker容器:
...
--> Scenario: 'default'
--> Action: 'create'
PLAY [Create] ******************************************************************
TASK [Log into a Docker registry] **********************************************
skipping: [localhost] => (item=None)
TASK [Create Dockerfiles from image names] *************************************
changed: [localhost] => (item=None)
TASK [Discover local Docker images] ********************************************
ok: [localhost] => (item=None)
TASK [Build an Ansible compatible image] ***************************************
changed: [localhost] => (item=None)
TASK [Create docker network(s)] ************************************************
TASK [Create molecule instance(s)] *********************************************
changed: [localhost] => (item=None)
TASK [Wait for instance(s) creation to complete] *******************************
changed: [localhost] => (item=None)
PLAY RECAP *********************************************************************
localhost : ok=5 changed=4 unreachable=0 failed=0
创建后,测试继续进行准备操作。此操作执行prepare playbook,它在运行掩盖之前将主机置于特定状态。如果您的角色需要在执行角色之前预先配置系统,这将非常有用。同样,这不适用于我们的角色:
...
--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured
准备好后,converge 操作通过运行playbook.yml
playbook 在容器上执行您的角色。如果molecule.yml
文件中配置了多个平台,Molecule将执行完如下:
...
--> Scenario: 'default'
--> Action: 'converge'
PLAY [Converge] ****************************************************************
TASK [Gathering Facts] *********************************************************
ok: [centos7]
TASK [httpd : Ensure required packages are present] ****************************
changed: [centos7] => (item=[u'httpd', u'firewalld'])
TASK [httpd : Ensure latest index.html is present] *****************************
changed: [centos7]
TASK [httpd : Ensure httpd service is started and enabled] *********************
changed: [centos7] => (item=httpd)
changed: [centos7] => (item=firewalld)
TASK [httpd : Whitelist http in firewalld] *************************************
changed: [centos7]
PLAY RECAP *********************************************************************
centos7 : ok=5 changed=4 unreachable=0 failed=0
在converge操作之后,测试继续进行idempotence操作。此操作测试剧本的idempotence确保多次运行中没有发生意外更改:
...
--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
下一个测试动作是side-effect动作。这使您生成可以测试更多内容的情况,例如HA故障转移。默认情况下,Molecule不配置side-effect playbook并跳过任务:
...
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
然后,Molecule将使用默认验证程序Testinfra运行验证程序操作。此操作执行您之前编写的测试test_default.py
。如果所有测试成功通过,您将看到成功消息,Molecule将继续执行下一步:
...
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /home/sammy/httpd/molecule/default/tests/...
============================= test session starts ==============================
platform linux2 -- Python 2.7.12, pytest-3.5.1, py-1.5.3, pluggy-0.6.0
rootdir: /home/sammy/httpd/molecule/default, inifile:
plugins: testinfra-1.12.0
collected 6 items
tests/test_default.py ...... [100%]
========================== 6 passed in 37.88 seconds ===========================
Verifier completed successfully.
最后,Molecule 会破坏测试期间完成的实例,并删除分配给这些实例的网络:
...
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************
TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=None)
TASK [Wait for instance(s) deletion to complete] *******************************
changed: [localhost] => (item=None)
TASK [Delete docker network(s)] ************************************************
PLAY RECAP *********************************************************************
localhost : ok=2 changed=2 unreachable=0 failed=0
测试操作现已完成,验证我们的角色是否按预期工作。
在本文中,您创建了一个Ansible角色来安装和配置Apache和Firewalld。然后,您使用Testinfra编写了单元测试,Molecule用它来评估角色是否成功运行。
您可以使用相同的方法处理非常复杂的角色,并使用CI管道自动化测试。Molecule是一个高度可配置的工具,可用于测试Ansible支持的任何提供者的角色,而不仅仅是Docker。它还可以针对您自己的基础架构进行自动化测试,确保您的角色始终保持最新且功能正常。可以使用官方Molecule文档是学习如何使用Molecule的最佳资源。
参考文献:《How To Test Ansible Roles with Molecule》
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。