作者选择Mozilla基金会作为Write for DOnations计划的一部分接受捐赠。
介绍
Ansible中的单元测试是确保角色按预期运行的关键。 通过允许您指定针对不同环境测试角色的方案, Molecule使此过程更容易。 使用Ansible,Molecule将角色卸载到配置器,该配置器在配置的环境中部署角色并调用验证器(例如Testinfra )来检查配置偏差。 这可确保您的角色在该特定方案中对环境进行了所有预期的更改。
在本指南中,您将构建一个Ansible角色,将Apache部署到主机并在CentOS 7上配置firewalld 。要测试此角色是否按预期工作,您将使用Docker作为驱动程序和Testinfra(一个Python库)在Molecule中创建一个测试用于测试服务器的状态。 Molecule将提供Docker容器来测试角色,Testinfra将验证服务器是否已按预期配置。 完成后,您将能够跨环境为构建创建多个测试用例,并使用Molecule运行这些测试。
先决条件
在开始本指南之前,您需要以下内容:
- 一个Ubuntu 18.04服务器。 按照初始服务器设置中的步骤使用Ubuntu 18.04指南创建非root sudo用户,并确保无需密码即可连接到服务器。
- Docker安装在您的服务器上。 按照如何在Ubuntu 18.04上安装和使用Docker中的第1步和2,包括将非root用户添加到
docker
组。 - 在服务器上安装并配置了Python 3和
venv
。 请遵循如何在Ubuntu 18.04服务器上安装Python 3和设置编程环境以获得指导。 - 熟悉Ansible Playbook。 有关查看,请参阅配置管理101:编写Ansible Playbooks 。
第1步 - 准备环境
如果您已遵循先决条件,则应安装并正确配置Python 3, venv
和Docker。 让我们首先创建一个虚拟环境来测试Ansible with Molecule。
首先以非root用户身份登录并创建新的虚拟环境:
python3 -m venv my_env
激活它以确保您的操作仅限于该环境:
source my_env/bin/activate
接下来,在激活的环境中,安装wheel
包,它提供了pip
用于安装Ansible的bdist_wheel
setuptools
扩展:
python3 -m pip install wheel
你现在可以用pip
安装molecule
和docker
。 Ansible将自动安装为Molecule的依赖项:
python3 -m pip install molecule docker
以下是每个包的功能:
-
molecule
:这是您将用于测试角色的主要Molecule包。 安装molecule
自动安装Ansible以及其他依赖项,并允许使用Ansible playbooks来执行角色和测试。 -
docker
:Molecule使用此Python库与Docker进行交互。 您将需要这个,因为您使用Docker作为驱动程序。
接下来,让我们在Molecule中创建一个角色。
第2步 - 在分子中创建角色
设置好您的环境后,您可以使用Molecule创建一个基本角色,用于测试Apache的安装。 此角色将创建目录结构和一些初始测试,并将Docker指定为驱动程序,以便Molecule使用Docker运行其测试。
创建一个名为ansible-apache
的新角色:
molecule init role -r ansible-apache -d docker
-r
标志指定角色的名称,而-d
指定驱动程序,驱动程序为Molecule提供主机以供测试。
切换到新创建的角色的目录:
cd ansible-apache
测试默认角色以检查Molecule是否已正确设置:
molecule test
您将看到列出每个默认测试操作的输出。 在开始测试之前,Molecule验证配置文件molecule.yml
以确保一切正常。 它还会打印此测试矩阵,该矩阵指定测试操作的顺序:
Output--> Validating schema /home/sammy/ansible-apache/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix
└── default
├── lint
├── destroy
├── dependency
├── syntax
├── create
├── prepare
├── converge
├── idempotence
├── side_effect
├── verify
└── destroy
...
在您创建角色并自定义测试后,我们将详细讨论每个测试操作。 现在,请注意每个测试的PLAY_RECAP
,并确保没有任何默认操作返回failed
状态。 例如,默认'create'
操作的PLAY_RECAP
应如下所示:
Output...
PLAY RECAP *********************************************************************
localhost : ok=5 changed=4 unreachable=0 failed=0
让我们继续修改角色以配置Apache和firewalld。
第3步 - 配置Apache和Firewalld
要配置Apache和firewalld,您将为角色创建任务文件,指定要安装的软件包和要启用的服务。 这些详细信息将从您将用于替换默认Apache索引页的变量文件和模板中提取。
仍然在ansible-apache
目录中,使用nano
或您喜欢的文本编辑器为角色创建任务文件:
nano tasks/main.yml
您将看到该文件已存在。 删除那里的内容并用以下代码替换它以安装所需的包并启用正确的服务,HTML默认值和防火墙设置:
---
- 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个任务:
-
"Ensure required packages are present"
:此任务将安装pkg_list
下的变量文件中列出的包。 变量文件将位于~/ansible-apache/vars/main.yml
,您将在此步骤结束时创建它。 -
"Ensure latest index.html is present"
:此任务将复制模板页面index.html.j2
,并将其粘贴到Apache生成的默认索引文件/var/www/html/index.html
。 您还将在此步骤中创建新模板。 -
"Ensure httpd service is started and enabled"
:此任务将启动并启用变量文件中svc_list
中列出的服务。 -
"Whitelist http in firewalld"
:此任务将在firewalld
中将http
服务列入白名单。 Firewalld是一个完整的防火墙解决方案,默认存在于CentOS服务器上。 要使http
服务正常工作,您需要公开所需的端口。 指示firewalld
将服务列入白名单可确保将服务所需的所有端口列入白名单。
完成后保存并关闭文件。
接下来,让我们为index.html.j2
模板页面创建一个templates
目录:
mkdir templates
创建页面本身:
nano templates/index.html.j2
粘贴以下样板代码:
<div style="text-align: center">
<h2>Managed by Ansible</h2>
</div>
保存并关闭文件。
完成角色的最后一步是编写变量文件,该文件为我们的主角色playbook提供包和服务的名称:
nano vars/main.yml
使用以下代码粘贴默认内容,该代码指定pkg_list
和svc_list
:
---
pkg_list:
- httpd
- firewalld
svc_list:
- httpd
- firewalld
这些列表包含以下信息:
-
pkg_list
:包含角色将安装的软件包的名称:httpd
和firewalld
。 -
svc_list
:它包含角色将启动和启用的服务的名称:httpd
和firewalld
。
注意:确保您的变量文件没有任何空行,否则您的测试将在linting期间失败。
现在您已经完成了角色的创建,让我们配置Molecule以测试它是否按预期工作。
第4步 - 修改运行测试的角色
在我们的例子中,配置Molecule涉及修改Molecule配置文件molecule.yml
以添加平台规范。 因为您正在测试配置和启动httpd
systemd服务的角色,所以您需要使用启用了systemd配置和特权模式的映像。 在本教程中,您将使用Docker Hub上提供的milcom/centos7-systemd
映像。 特权模式允许容器运行其主机的几乎所有功能。
让我们编辑molecule.yml
来反映这些变化:
nano molecule/default/molecule.yml
添加突出显示的平台信息:
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
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将针对您的容器运行的测试用例。
第5步 - 编写测试用例
在此角色的测试中,您将检查以下条件:
- 已安装
httpd
和firewalld
软件包。 -
httpd
和firewalld
服务正在运行并启用。 - 在防火墙设置中启用了
http
服务。 -
index.html
包含模板文件中指定的相同数据。
如果所有这些测试都通过,则角色按预期工作。
要编写这些条件的测试用例,让我们编辑~/ansible-apache/molecule/default/tests/test_default.py
。 使用Testinfra,我们将测试用例编写为使用Molecule类的Python函数。
打开test_default.py
:
nano molecule/default/tests/test_default.py
删除文件的内容,以便您可以从头开始编写测试。
注意:在编写测试时,请确保它们由两个新行分隔,否则它们将失败。
首先导入所需的Python模块:
import os
import pytest
import testinfra.utils.ansible_runner
这些模块包括:
-
os
:这个内置的Python模块支持与操作系统相关的功能,使Python可以与底层操作系统进行交互。 -
pytest
:pytest
模块可以进行测试编写。 -
testinfra.utils.ansible_runner
:此Testinfra模块使用Ansible作为命令执行的后端 。
在模块导入下,添加以下代码,该代码使用Ansible后端返回当前主机实例:
...
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')
将测试文件配置为使用Ansible后端,让我们编写单元测试来测试主机的状态。
第一个测试将确保安装httpd
和firewalld
:
...
@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
作为参数:
...
@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
:
...
@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
。
完成的文件如下所示:
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)
现在您已经指定了测试用例,让我们测试一下这个角色。
第6步 - 使用分子测试角色
一旦启动测试,Molecule将执行您在场景中定义的操作。 现在让我们再次运行默认molecule
场景,执行默认测试序列中的操作,同时更仔细地查看每个场景。
再次运行默认方案的测试:
molecule test
这将启动测试运行。 初始输出打印默认测试矩阵:
Output--> Validating schema /home/sammy/ansible-apache/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix
└── default
├── lint
├── destroy
├── dependency
├── syntax
├── create
├── prepare
├── converge
├── idempotence
├── side_effect
├── verify
└── destroy
让我们来看看每个测试动作和预期的输出,从linting开始。
yamllint
动作执行yamllint
, flake8
和flake8
ansible-lint
:
-
yamllint
:此yamllint
在角色目录中存在的所有YAML文件上执行。 -
flake8
:这个Python代码linter检查为Testinfra创建的测试。 -
ansible-lint
:Ansible playbooks的这个linter在所有场景中都会执行。
Output...
--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /home/sammy/ansible-apache/...
Lint completed successfully.
--> Executing Flake8 on files found in /home/sammy/ansible-apache/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /home/sammy/ansible-apache/molecule/default/playbook.yml...
Lint completed successfully.
使用destroy.yml
文件执行下一个操作destroy 。 这样做是为了测试我们在新创建的容器上的角色。
默认情况下,destroy被调用两次:在测试运行开始时,删除任何预先存在的容器,最后删除新创建的容器:
Output...
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************
TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Wait for instance(s) deletion to complete] *******************************
ok: [localhost] => (item=None)
ok: [localhost]
TASK [Delete docker network(s)] ************************************************
skipping: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0
在销毁操作完成后,测试将继续进行依赖 。 如果您的角色需要,此操作允许您从ansible-galaxy
提取依赖项。 在这种情况下,我们的角色不会:
Output...
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
下一个测试操作是语法检查,它在默认的playbook.yml
playbook上执行。 它的工作方式类似于命令ansible-playbook --syntax-check playbook.yml
的--syntax-check
标志ansible-playbook --syntax-check playbook.yml
:
Output...
--> Scenario: 'default'
--> Action: 'syntax'
playbook: /home/sammy/ansible-apache/molecule/default/playbook.yml
接下来,测试继续进行创建操作。 这将使用角色的Molecule目录中的create.yml
文件来创建具有您的规范的Docker容器:
Output...
--> Scenario: 'default'
--> Action: 'create'
PLAY [Create] ******************************************************************
TASK [Log into a Docker registry] **********************************************
skipping: [localhost] => (item=None)
skipping: [localhost]
TASK [Create Dockerfiles from image names] *************************************
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Discover local Docker images] ********************************************
ok: [localhost] => (item=None)
ok: [localhost]
TASK [Build an Ansible compatible image] ***************************************
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Create docker network(s)] ************************************************
skipping: [localhost]
TASK [Create molecule instance(s)] *********************************************
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Wait for instance(s) creation to complete] *******************************
changed: [localhost] => (item=None)
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=5 changed=4 unreachable=0 failed=0
创建后,测试继续进行准备操作。 此操作执行prepare playbook,它在运行converge之前将主机置于特定状态。 如果您的角色需要在执行角色之前预先配置系统,这将非常有用。 同样,这不适用于我们的角色:
Output...
--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
在准备之后, 聚合操作通过运行playbook.yml
playbook在容器上执行您的角色。 如果在molecule.yml
文件中配置了多个平台,Molecule将会聚合所有这些:
Output...
--> Scenario: 'default'
--> Action: 'converge'
PLAY [Converge] ****************************************************************
TASK [Gathering Facts] *********************************************************
ok: [centos7]
TASK [ansible-apache : Ensure required packages are present] *******************
changed: [centos7]
TASK [ansible-apache : Ensure latest index.html is present] ********************
changed: [centos7]
TASK [ansible-apache : Ensure httpd service is started and enabled] ************
changed: [centos7] => (item=httpd)
changed: [centos7] => (item=firewalld)
TASK [ansible-apache : Whitelist http in firewalld] ****************************
changed: [centos7]
PLAY RECAP *********************************************************************
centos7 : ok=5 changed=4 unreachable=0 failed=0
在掩盖之后,测试继续进行幂等性 。 此操作测试playbook for idempotence以确保在多次运行中没有进行任何意外更改:
Output...
--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
下一个测试动作是副作用动作。 这使您可以生成可以测试更多内容的情况,例如HA故障转移。 默认情况下,Molecule不配置副作用playbook并跳过任务:
Output...
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
然后,Molecule将使用默认验证程序Testinfra运行验证程序操作。 此操作将执行您之前在test_default.py
编写的测试。 如果所有测试成功通过,您将看到成功消息,Molecule将继续执行下一步:
Output...
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /home/sammy/ansible-apache/molecule/default/tests/...
============================= test session starts ==============================
platform linux -- Python 3.6.5, pytest-3.7.3, py-1.5.4, pluggy-0.7.1
rootdir: /home/sammy/ansible-apache/molecule/default, inifile:
plugins: testinfra-1.14.1
collected 6 items
tests/test_default.py ...... [100%]
========================== 6 passed in 41.05 seconds ===========================
Verifier completed successfully.
最后,Molecule 会破坏测试期间完成的实例,并删除分配给这些实例的网络:
Output...
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************
TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Wait for instance(s) deletion to complete] *******************************
changed: [localhost] => (item=None)
changed: [localhost]
TASK [Delete docker network(s)] ************************************************
skipping: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=2 changed=2 unreachable=0 failed=0
测试操作现已完成,验证您的角色是否按预期工作。
结论
在本文中,您创建了一个Ansible角色来安装和配置Apache和firewalld。 然后,您使用Testinfra编写了单元测试,Molecule用它来断言角色成功运行。
您可以对高度复杂的角色使用相同的基本方法,并使用CI管道自动化测试。 Molecule是一个高度可配置的工具,可用于测试Ansible支持的任何提供者的角色,而不仅仅是Docker。 它还可以针对您自己的基础架构进行自动化测试,确保您的角色始终保持最新且功能正常。 官方Molecule文档是学习如何使用Molecule的最佳资源。