作者选择Mozilla基金会作为Write for DOnations计划的一部分接受捐赠。
介绍
Ansible中的单元测试是确保角色按预期运行的关键。 分子通过允许您指定针对不同环境测试角色的方案,使此过程更加简单。 在底层使用Ansible,分子将角色卸载到在配置的环境中部署角色的调配程序,并调用验证程序(如Testinfra )来检查配置偏移。 这可确保您的角色在特定情况下对环境进行了所有预期的更改。
在本指南中,您将构建一个将Apache部署到主机并配置Firewalld的Ansible角色。 为了测试这个角色是否按预期工作,您将使用Docker作为驱动程序创建一个Molecule测试,并使用Testinfra(一个用于测试服务器状态的Python库)创建一个测试。 分子将提供Docker容器来测试角色,Testinfra将验证服务器是否按照预期进行了配置。 完成后,您将能够为跨环境构建创建多个测试用例,并使用分子运行这些测试。
先决条件
在开始本指南之前,您需要以下内容:
- 一个Ubuntu 16.04服务器。 按照使用Ubuntu 16.04初始服务器设置指南中的步骤创建非root用户的sudo用户,并确保可以无需密码即可连接到服务器。 注意:分子可以使用Python 2.7或Python 3.6。 由于Ubuntu 16.04默认包含Python 3.5和2.7,我们将在本教程中安装和使用Python 2.7,以便使用内置的存储库。
- Docker安装在你的服务器上。 按照如何在Ubuntu 16.04上安装和使用Docker中概述的步骤操作,并确保将您的非root用户添加到
docker
组。 - 熟悉Ansible的手册。 如需查看,请查看配置管理101:编写Ansible Playbooks 。
第1步 - 准备环境
我们首先在主机上创建一个虚拟环境,然后在该环境中安装测试所需的包。
首先以非root用户身份登录,并确保您的存储库是最新的:
sudo apt-get update -y
这将确保您的软件包存储库包含最新版本的python-pip
软件包,它将安装pip
和Python 2.7。 我们将使用pip
创建一个虚拟环境并安装额外的软件包。 要安装pip
,请运行:
sudo apt-get install -y python-pip
使用pip
来安装virtualenv
Python模块和任何更新:
pip install pip virtualenv -U
-U
标志告诉pip
更新任何以前安装的软件包。
接下来,让我们创建并激活虚拟环境:
virtualenv my_env
激活它以确保您的操作仅限于该环境:
. my_env/bin/activate
使用pip
安装molecule
, ansible
和docker-py
:
pip install molecule ansible docker-py
以下是每个软件包将执行的操作:
-
molecule
:这是主要的分子包,用于测试角色。 -
ansible
:这个软件包允许使用Ansible Playbook,执行角色和相关的测试。 -
docker-py
:这个Python库被Molecule用来与Docker进行交互。 我们需要这个,因为我们使用Docker作为驱动程序。
接下来,让我们在分子中创建一个角色。
第2步 - 在分子中创建角色
在设置好环境后,让我们使用Molecule创建一个基本角色,我们将使用它来测试Apache的安装。 此过程将创建目录结构和一些初始测试,并将Docker指定为驱动程序,以便Molecule使用Docker来运行其测试。
创建一个名为httpd
的新角色:
molecule init role -r httpd -d docker
-r
标志指定角色的名称,而-d
指定驱动程序,该驱动程序为Molecule在测试中使用的主机提供配置。
转到新创建的角色的目录中:
cd httpd
测试默认角色以检查Molecule是否已正确设置:
molecule test
您将看到将列出每个默认测试操作的输出:
Output--> 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
状态。 例如,默认'create'
操作的PLAY_RECAP
应该如下所示:
Output...
PLAY RECAP *********************************************************************
localhost : ok=5 changed=4 unreachable=0 failed=0
让我们继续修改我们的角色来配置Apache和Firewalld。
第3步 - 配置Apache
要配置Apache,我们将为该角色创建一个任务文件,指定要安装的软件包和要启用的服务。 这些细节将从变量文件和模板中提取,我们将用它来替换默认的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
下变量文件中列出的软件包。 变量文件将位于~/httpd/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
Whitelist http in firewalld
:此任务将Whitelist http in 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>
保存并关闭文件。
完成角色的最后一步是编写变量文件,它提供了包和服务的名称给我们的主角色剧本:
nano vars/main.yml
使用以下代码粘贴默认内容,该代码指定pkg_list
和svc_list
:
---
pkg_list:
- httpd
- firewalld
svc_list:
- httpd
- firewalld
这些列表包含以下信息:
-
pkg_list
:这包含角色将要安装的软件包的名称:httpd
和firewalld
。 -
svc_list
:包含角色将启动并启用的服务的名称:httpd
和firewalld
。
注意:确保变量文件没有任何空行,否则在测试过程中测试将失败。
现在我们完成了创建角色,让我们配置Molecule来测试它是否按预期工作。
第4步 - 修改运行测试的角色
配置分子涉及两个步骤:修改分子配置文件本身,并创建自定义yamllint
文件。 Yamllint是一个YAML代码linter,用于检查语法有效性,关键重复以及线条长度,尾随空格和缩进等外观问题。
我们的修改将包括:
- 为
~/httpd/molecule/default/molecule.yml
添加一个选项以使用自定义yamllint
配置文件并创建文件本身。 该文件将启用两个例外:大于80个字符的行和真值。 因为Ansible和Yamllint使用冲突的语法来表达真值,所以这将防止不必要的语法错误。 - 添加平台规格。 由于我们正在测试配置和启动
httpd
systemd服务的角色,因此我们需要使用启用了systemd配置和特权模式的映像。 在本教程中,我们将使用Docker Hub上提供的milcom/centos7-systemd
映像。 特权模式允许容器运行几乎所有的主机功能。
让我们编辑molecule.yml
来反映这些变化:
nano molecule/default/molecule.yml
添加突出显示的yamllint
选项和平台信息:
---
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.yml
molecule/default/yamllint.yml
的yamllint
文件中。 我们来创建这个文件:
nano molecule/default/yamllint.yml
通过粘贴以下规则来输入测试环境的自定义配置,这些规则定义了行长度规格和真实值设置:
---
extends: default
rules:
line-length:
max: 120
level: warning
truthy: disable
我们添加了两条规则:
-
line_length
:此规则指定允许的最大行长度为120个字符(最多80个字符),并且如果违反规则,linter应该发出警告。 -
truthy
:由于Ansible和Yamllint使用冲突的语法来表达它们,因此此规则会禁用真值。 这将防止不必要的语法错误。
现在我们已经成功配置了测试环境,接下来让我们继续编写Molecule在执行角色后对我们的容器运行的测试用例。
第5步 - 编写测试用例
在测试这个角色时,我们会检查以下条件:
-
httpd
和firewalld
软件包已安装。 -
httpd
和firewalld
服务正在运行并启用。 -
http
服务在我们的防火墙设置中启用。 - 该
index.html
包含我们的模板文件中指定的相同数据。
如果所有这些测试都通过了,那么角色按预期工作。
要编写这些条件的测试用例,我们编辑~/httpd/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 test
这将启动测试运行。 初始输出打印默认测试矩阵:
Output--> 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开始,我们来看看每个测试操作和预期输出。
yamllint
动作执行yamllint
, flake8
和flake8
ansible-lint
:
-
yamllint
:该yamllint
在角色目录中的所有YAML文件上执行。 -
flake8
:这个Python代码linter检查为Testinfra创建的测试。 -
ansible-lint
:用于Ansible Playbook的linter在所有情况下都会执行。
Output...
--> 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使用destroy.yml
文件执行。 这是为了测试我们在新创建的容器上的角色。
默认情况下,destroy被调用两次:在测试运行开始时,删除所有预先存在的容器,最后删除新创建的容器:
Output...
--> 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
出来。 在这种情况下,我们的角色不会:
Output...
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
下一个测试操作是语法检查,它在默认的playbook.yml
剧本中执行。 它的工作方式类似于命令中的--syntax-check
标志ansible-playbook --syntax-check playbook.yml
:
Output...
--> Scenario: 'default'
--> Action: 'syntax'
playbook: /home/sammy/httpd/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)
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
创建完成后,测试转到准备操作。 此操作执行准备剧本,在运行收敛之前将主机置于特定状态。 如果您的角色需要在角色执行前预先配置系统,这非常有用。 再次,这不适用于我们的角色:
Output...
--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
准备好之后, 融合操作通过运行playbook.yml
手册在容器上执行您的角色。 如果在molecule.yml
文件中配置了多个平台,分子将集中在所有这些平台上:
Output...
--> 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
测试结束后,测试进入幂等性 。 这个动作测试幂等的剧本,以确保在多次运行中不会发生意外的变化:
Output...
--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
下一个测试操作是副作用操作。 这使您可以生成可以测试更多内容的情况,如HA故障切换。 默认情况下,Molecule不配置副作用剧本并跳过任务:
Output...
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
然后分子将使用默认验证程序Testinfra运行验证程序操作。 此操作将执行您之前在test_default.py
编写的测试。 如果所有测试成功通过,您将看到成功消息,分子将进入下一步:
Output...
--> 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.
最后,分子销毁测试期间完成的实例并删除分配给这些实例的网络:
Output...
--> 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编写了单元测试。
您可以对高度复杂的角色使用相同的基本方法,也可以使用CI管道自动执行测试。 Molecule是一个高度可配置的工具,可用于测试Ansible支持的任何提供者的角色,而不仅仅是Docker。 也可以根据自己的基础设施自动进行测试,确保您的角色始终保持最新并且功能正常。 官方Molecule文档是学习如何使用Molecule的最佳资源。