OpenStack设计与实现(第3版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.5 OpenStack代码质量保证体系

虽然OpenStack诞生不久,但其红火的程度已经使它吸引了越来越多的眼球。对于这样一个迅速聚集了众多使用者和开发者的开源项目,我们相信,它一定有自己的一套成熟、高效的体系来保证代码的质量与项目的稳步推进。

因此,在探讨如何入手分析源码之后,本节将会对OpenStack如何保证自己的代码质量进行一些探究。

比尔·盖茨说过:用代码行数来衡量编程的进度,就如同用航空器零件的重量来衡量航空飞机的制造进度一样。所以,相对于代码的数量,我们通常更乐意去关注代码本身的质量。也因此,在开源社区里,除某些特殊的目的以外,我们也更愿意去关注一个人被接受patch的数目,而不是这些patch里代码的行数。

对于“代码质量”的定义,我们每个人应该都能说出不尽相同的看法,但是更多的感觉可能是“只可意会,不可言传”,很难真正地使用统一的标准去定义清楚。当然,已经有一些研究和工具在通过各种指标来对代码的质量进行量化,如图2-4所示。

总之,将“代码质量”定义清楚是一件非常复杂的事情。幸好,笛卡儿很有预见性地在17世纪的某一天写了这么一本书——《方法论》,在这本绝大部分人可能都不知道的书里将方法上升到了理论的高度。笛卡儿在这本书里将研究问题的方法归纳为简单的一句话,就是“复杂问题要简单化”。

遵循这个方法论,我们在此尝试解释一下代码质量。

代码一方面是给计算机读的,另一方面是给人读的,也就是给维护这份代码的人读的。给计算机读比较简单,只要遵守计算机语言的规则,计算机就能将它编译成最后的结果。给人读比较麻烦,在读别人的代码时,我们都希望这个代码写得比较简单,函数很短,命名能够让人望文生义,读起来就像读小说、故事会一样,等等。所以我们所希望的就是我们在自己编码时应该实现的目标,这就是站在通俗的角度简单化了的代码质量。

img

图2-4 以Code Review过程中每分钟出现“脏话”的个数来衡量代码的质量

这个简单化了的代码质量定义强调更多的是代码的可读性,即“代码应该是写给其他人来读的,而能让机器运行仅仅是其附带功能”。可读性是其他代码质量指标,包括可维护性、可靠性、可扩展性、性能等的基石,一般来说,干净、整洁的代码,往往运行速度更快。而且即使它们的运行速度不快,也可以很容易地让它们的运行速度变快。正如人们所说的,“优化正确的代码比改正优化过的代码容易多了”。

但是对于一个蓬勃发展、前景无限可期的开源项目来说,它的代码质量却不能被这么简单地定义,而是必须有一套行之有效的体系与工具来保证。

站在软件工程的高度,通常来说,代码质量保证步骤如图2-5所示。

img

图2-5 代码质量保证步骤

· 统一编码规范。可读性与可维护性的前提就是具有统一的编码规范。

· 代码静态检查。在代码的开发完成后,接着要进行的工作就是测试。而从计算机理论的角度来说,测试被划分为静态测试与动态测试。

其中,动态测试指的就是通常意义上我们所说的测试,它通过运行测试代码或直接运行被测试的软件来发现存在的问题。静态测试则是指应用其他手段实现测试目的的测试,比如属于人工范畴的代码评审(Code Review)与计算机辅助进行的代码静态检查。

代码静态检查主要指利用静态分析工具对代码进行特性分析,以便检查程序逻辑的各种缺陷和可疑的程序构造,如不符合编码规范、潜在的死循环等编译器发现不了的错误。之所以被称为代码静态检查,是因为它只是分析源码或生成的目标文件,并不实际运行源码生成的文件。它的目的是帮助我们尽可能早地发现代码中存在的问题并及时修复,将其消灭在萌芽状态,就能为后续工作节省大量的耗费在测试与调试上面的时间。

· 单元测试。

· 持续集成。持续集成(CI,Continuous Integration)会利用一系列的工具、方法和规则,通过自动化的构建(包括编译、发布、自动化测试等)尽快发现问题和错误,来提高开发代码的效率和质量。

· 代码评审与重构。代码评审可以帮助我们发现代码静态检查过程中无法发现的一些问题,比如,代码的编写是否符合编码规范,代码在逻辑上或功能上是否存在错误,代码在执行效率和性能上是否有需要改进的地方,代码的注释是否完整、正确,代码是否存在冗余和重复。通过代码评审所发现的问题要通过代码重构及时解决。

本节接下来的内容会按照上述的代码质量保证步骤,总结OpenStack使用的工具与采取的措施。

2.5.1 编码规范

程序员最讨厌的4件事应该是:写注释,写文档,别人不写注释,别人不写文档。对于这样一个貌似很不好相处的群体,有人说,如果莎士比亚生活在当下,他会是一名科技作家,而且他的座右铭会变成“消灭世界上所有的程序员”。

“消灭”当然是做不到的,于是一种所谓的编码规范被推上了“前台”,用来预防程序员的各种个性与创造力。

对于达到百万行代码这个量级的OpenStack来说,它当然也必须有一套自己的编码规范来约束及预防自己的众多开发者,确保他们把自己的创造力作用在构建一个蓬勃发展的开源云项目上,而不是作用在一个其他的什么“怪胎”上。

本节的内容将着重放在OpenStack编码规范检查工具及其相关的一个子项目Hacking上。

1.代码静态检查

如前文所述,代码静态检查是代码质量保证体系里很关键的一环,编码规范的检查又是代码静态检查的一种。为了更好地理解后面的内容,我们有必要先对代码静态检查进行一番了解。

对于C、C++等编译型语言来说,因为可以与编译器进行比对,所以在理解代码静态检查时会更加容易。

编译器与代码静态检查工具都能检查代码中的潜在问题。一般来说,编译器最重要的作用是生成可执行文件,所以对于词法、语法的分析相对局限,即在检测错误时,前后查看的代码较少。这也是基于编译器的性能来考虑的,因为还有很多的优化工作要做,所以编译器在词法、语法分析上不能耗费太多时间。尤其是对于Android这种代码量很大的项目,在编译都要耗费半天甚至更多的时间时,5%的性能差距就比较大了。

所以,尽管编译器擅长发现一些错误,但通常会因考虑速度而放弃对一些较难发现的条件的检查。这样一来,一些原本可以发现的错误,经常会被遗漏而成为应用中的Bug,比如,未成功释放已分配的内存、死循环等。

但是代码静态检查工具并没有在性能及时间方面比较苛刻的需求,因为它们并不需要像编译器那样频繁运行,所以它们可以牺牲运行的速度来换取更为彻底的词法、语法分析,更为准确地找到更多的问题。

在使用代码静态检查工具时,付出性能与时间的代价,其收获就是把更多原本只有在测试阶段甚至应用阶段才能发现的Bug在编码阶段暴露出来。从Bug的成本来分析,有这样一个公认的结论:Bug发现得越晚,修正的成本就越高,测试阶段修正Bug的成本大约是编码阶段的4倍。

在编码阶段,静态地分析代码就能找到代码的Bug,是很多程序员简单又美好的梦想。这个梦想在21世纪初,随着以PCLint、Klocwork、Coverity为代表的开源或商业静态分析软件的出现而变成了现实。这些代码静态检查工具逐渐成为很多商业公司或开发者的必备工具,如图2-6所示。

img

图2-6 代码静态检查工具

2006年,美国国土安全部发起并与斯坦福大学合办了一个关注开源代码完整性的研究项目Coverity Scan,这个项目每年都会使用Coverity公司的代码静态检查工具评估和帮助改进包括Linux内核、Android、Python在内的主要开源项目的代码质量,比如,在2006年就帮助修复了6000多个Bug。同时,这个项目每年都会发布一份开源软件质量报告,如图2-7所示为2011年报告中有关各种缺陷比例的一部分。

img

图2-7 开源软件质量报告(部分)

2.Python代码静态检查工具Flake8

对于与OpenStack息息相关的Python代码静态检查来说,目前的工具主要有Pylint、Pep8、Pyflakes、Flake8等。

Pylint违背了Python开发者Happy Coding的倡导,或许这也是未被OpenStack社区所采纳的缘由。

Pep8备受Python社区推崇,负责Python代码风格的检查,据路边社报道,在某些公司招聘Python工程师的要求中,有一条就是代码符合Pep8标准。

Pyflakes可以检查Python代码的逻辑错误。

Flake8是Pyflakes、Pep8及Ned Batchelder's McCabe script(关注Python代码复杂度的静态分析)三个工具的集大成者,综合封装了三者的功能,在简化操作的同时,还提供了扩展开发接口。

3.Hacking

经过上文的铺垫,我们可以很容易地知道,OpenStack使用的代码静态检查工具是Flake8,并实现了一组扩展的Flake8插件来满足OpenStack的特殊需要,这组插件单独作为一个子项目而存在,就是Hacking。部分Hacking源码如下:

img
img

从上面Hacking源码中的setup.cfg文件内容可以看出,到目前为止,Hacking主要在注释、异常、文档、兼容性等编码规范方面实现了将近30个Flake8插件的配置。

2.5.2 代码评审Gerrit

首先,我们来了解一个程序调试方法——橡皮鸭程序调试法,下面的内容主要来自酷壳网的一篇文章。

这个方法实施起来相当方便和简易,几乎不需要任何软件和硬件的支持,可以随时随地进行试验,你甚至可以把你的程序打印出来,在纸面上进行调试。

那么,为什么这个方法叫作橡皮鸭程序调试法呢?一是因为橡皮鸭貌似是西方人在泡澡时最喜欢玩的一个小玩具,所以,这个东西应该家家户户必备;二是因为这个方法由西方人发明,所以,就被命名为“橡皮鸭”了。下面是整个橡皮鸭程序调试法的流程,如图2-8所示。

img

图2-8 橡皮鸭程序调试法的流程

(1)找一只橡皮鸭。你可以去借、去买、自己制作……反正你要找到一只橡皮鸭。

(2)把这只橡皮鸭放在你面前。标准做法是放在你的桌子上的显示器边或键盘边,反正是你的面前,面朝你。

(3)打开你的代码。不管是计算机里的还是打印出来的。

(4)对着橡皮鸭,把你的所有代码一行一行地向它解释清楚。记住,这是解释,你需要解释你的想法、思路、观点。不然,那只能算是表述,而不是解释。

(5)在这个解释的过程中,你会发现自己的想法或思路与实际的代码偏离了,于是你也就找到了Bug。

(6)感谢橡皮鸭。找到了Bug,一定要记得感谢一下那只橡皮鸭。

这个方法是否让你感觉太“愚蠢”,太“弱智”了?不过,这个方法的确有效。因为,这就是Code Review的雏形!它的核心思想可以概括为:一旦一个问题被充分地描述了它的细节,那么解决方法也是显而易见的。

相信各位都有过这样的经历,当你无论如何都找不到问题的原因,转而寻求他人的帮助时,你要对别人解释你的整个想法和意图,或者问题背景,可能你自己都没有解释完,就已经找到问题的原因了。这就是这个方法的意义所在。

所以,“橡皮鸭”只是一个形式,其主要目的是要你把自己写的代码进行“自查”,也就是自己解释给自己听。当然,为了不让你像个“精神分裂”的程序员,引入“橡皮鸭”是很有必要的。所以,这种做法的本质是Code Review。

那么,对于OpenStack来说,为了保证代码评审有效进行,首先需要做的是为我们寻找合适的道具“橡皮鸭”,然后提供一个将这些道具和我们有效连接起来的平台。

这些道具自然就是分散在全球各地的OpenStack开发者们,与橡皮鸭不同的是,他们会发表自己的意见和看法。而这个连接的平台就是Gerrit。

1.Gerrit工作流程

Android在Git的使用上有两个重要的创新:一个是为多版本库协同而引入的repo(对Git使用的封装),另一个就是Gerrit——代码审核服务器。Gerrit为Git引入的代码审核是强制性的,也就是说除非特别授权设置,否则向Git版本库的推送(push)必须经过Gerrit服务器,并且修订必须经过一套代码审核的工作流程之后,才可能经过批准并纳入正式代码库中。

OpenStack也将Gerrit引入自己的代码管理里,工作流程大体和Android对Gerrit的使用相同,区别是过程更为简洁,而且使用了Jenkins来完成自动化测试,如图2-9所示。

img

图2-9 Gerrit工作流程

首先我们在本地代码仓库中做出自己的修改,然后我们就能很容易地通过git命令(或git-review封装)将自己的代码push到Gerrit管理下的Git版本库。Jenkins将对我们提交的代码进行自动测试并给出反馈,其他开发者也能够使用Gerrit对我们的代码给出他们的注释与反馈,其中,项目的maintainer(在OpenStack中称为Core Developer)的反馈权重更高(+2),如果你的patch能够得到两个“+2”,那么恭喜你,你的patch将被merge到OpenStack的源码树里。

所有这些注释、质疑、反馈、变更等代码评审的工作都通过Web界面来完成,因此Web服务器是Gerrit的重要组件,Gerrit通过Web服务器来实现对整个评审工作流的控制。针对某一个提交的Gerrit评审页面如图2-10所示。

img

图2-10 Gerrit评审页面

2.Gerrit账号

如果我们希望参与到上面的过程中,找到无数的“橡皮鸭”或者让自己成为“橡皮鸭”,那么我们必须有一个Gerrit账号,这个账号使用的是Launchpad账号。

也就是说,我们首先需要访问Launchpad的登录页面,使用自己的电子邮件地址注册Launchpad账号,并为自己选择一个Launchpad ID,之后登录自己的Launchpad主页。

在使用Launchpad账号登录之后,我们还需要上传自己的SSH公钥(SSH public key),公钥设置的页面有相应的HowTo告诉我们如何生成公钥并上传。

3.Gerrit实现原理

Gerrit基于SSH协议实现了一套自己的Git服务器,这样就可以基于自己的需求对Git数据传递进行更为精确的控制,为上述工作流程的实现建立了基础。访问Gerrit页面可以查看这个Git服务器的域名和端口“review.opendev.org 29418”,可以发现它使用了29418端口,并非是标准的22端口。

Gerrit的Git服务器,只允许用户向特殊的引用 refs/for/<branch-name>下执行推送(push),其中<branch-name>即为开发者的工作分支。Gerrit会为新的提交分配一个task-id,并为该task-id的访问建立引用refs/changes/nn/<task-id>/m,比如图2-10中的refs/changes/37/367737/2,其中:

· task-id为Gerrit按顺序分配给该评审任务的全局唯一的号码。

· nn为task-id的后两位数,位数不足的用零补齐,即nn为task-id除以100的余数。

· m为修订号,该task-id的首次提交修订号为1,如果该修订被拒绝,则需要更新代码后重新提交,修订号会依次增大。

为了保证在代码修改后重新提交时,不会产生新的重复的评审任务,Gerrit要求每个提交包含唯一的Change-Id,Gerrit一旦发现新的提交包含了已经处理过的Change-Id,就不再为该修订创建新的评审任务和task-id,而是仅仅把它作为已有task-id进行修订。

例如,图2-10所示评审任务的Change-Id为Icb21eeed0e004450556176d01520784acd98002e,在它被merge到正式的OpenStack源码树前共有两次修订,即Patch Set 2/2。

对于开发者来说,为了实现针对同一份代码的前后修订中包含唯一的、相同的Change-Id,需要在执行提交命令时使用--amend选项,以避免Gerrit创建新的评审任务。

4.git-review

如上所述,Gerrit做了大量的工作来保证代码从提交、评审、修订到再提交这个流程作业的顺利、有序进行,同时开发者需要在其中小心翼翼地进行配合。而这种谨慎与琐碎当然不太符合程序员们的日常行为,于是一个名称为git-review的工具出现了,它封装了与Gerrit交互的所有细节,我们需要做的只是开心地执行commit与review这两个git子命令,然后在Web图形界面上进行“看图说话”,根本不用去琢磨有关Gerrit的种种细节。

为了对一个项目使用git-review,我们需要先对该项目进行设置,比如,对Nova项目使用git-review:

img

git-review会检查我们是否能够登录Gerrit,如果不能,它会向我们索要Gerrit账号。如果我们看到“We don't know where your gerrit is.”这样的错误,就需要执行下面的命令:

img

然后我们经常做的事情,除修改代码之外,就是按照一个“熟练工”的标准执行下面的命令:

img

至于如何安装git-review在此不再赘述,因为大多数Linux发行版已经将其集成到了自己的包管理器中。

2.5.3 单元测试Tox

在StackOverflow上,一个有16.7k分的人问了一个有关单元测试的问题:“How deep are your unit tests?”意思就是:“单元测试需要做多细?”或者换句话说:“单元测试的这个单元的粒度是怎样的?”

针对这个问题,下面的回答获得了大部分的票数,被评为最佳回答。

I get paid for code that works,not for tests,so my philosophy is to test as little as possible to reach a given level of confidence (I suspect this level of confidence is high compared to industry standards,but that could just be hubris).If I don't typically make a kind of mistake (like setting the wrong variables in a constructor),I don't test for it.I do tend to make sense of test errors,so I'm extra careful when I have logic with complicated conditionals.When coding on a team,I modify my strategy to carefully test code that we,collectively,tend to get wrong.

老板为我的代码付报酬,而不是测试,所以,我对此的价值观是——测试越少越好,少到你对你的代码质量达到了某种自信(我觉得这种自信标准应该要高于业内的标准,当然,这种自信也可能是一种自大)。如果在我的编码生涯中不犯这种典型的错误(如:在构造函数中设置了一个错误的值),我就不会测试它。我倾向于对那些有意义的错误做测试,所以,我对一些比较复杂的条件逻辑会异常小心。当在一个团队中,我会非常小心地测试那些容易让团队出错的代码。

翻译来自酷壳网的文章,这不重要,重要的是这个回答来自Kent Beck(敏捷开发XP与测试驱动开发TDD的奠基者)。下面是有人针对Kent这个回答的回应。

The world does not think that Kent Beck would say this! There are legions of developers dutifully pursuing 100% coverage because they think it is what Kent Beck would do! I have told many that you said,in your XP book,that you don't always adhere to Test First religiously.But I'm surprised too.

只是要地球人都不会觉得Kent Beck会这么说!我们有大量程序员在忠实地追求着100%的代码测试覆盖率,因为这些程序员觉得Kent Beck也会这么做!我告诉过很多人,你在你的XP书里说过,你并不总是支持“宗教信仰式”的Test First,但是今天Kent这么说,我还是很惊讶!

回到OpenStack,单元测试又被称为Small Tests,粒度并不以开发者的个人意愿及其对自身水平的自信而转移,起码从形式上追求着将近100%的代码覆盖率。也因此,我们在提交一些新的代码时,必须做的事情往往是提交更多的测试代码,而且常常花在单元测试上的时间要更多。

1.OpenStack单元测试

概括来说,OpenStack单元测试追求的是隔离、速度及可移植。对于隔离,需要测试代码不和数据库、文件系统交互,也不能进行网络通信。另外,单元测试的粒度要足够小,确保一旦测试失败,就可以迅速地找到问题的根源。可移植是指测试代码不依赖于特定的硬件资源,允许任何开发者运行。

单元测试的代码位于每个项目源码树的<project>/tests/目录中,遵循oslo.test库提供的基础框架。通常单元测试的代码需要专注于对核心实现逻辑的测试上,如果需要测试的代码引入了其他的依赖,如依赖于某个特定的环境,则我们在编写单元测试代码的过程中,花费时间最多的可能就是如何隔离这些依赖,否则,即使测试失败,也很难定位出问题所在。

对SUT(the system under test,被测试系统)完成隔离的基本原则是引入Test Double(类似特技替身演员),并使用Test Double替代测试中的每一个依赖。

Test Double有多种类型,如Mock对象、Fake对象等,它们可以作为数据库、I/O,以及网络等对象的替身,并将相应的操作隔离。在测试运行过程中,当执行到这些操作时,不会深入方法内部去执行,而是会直接返回我们假设的一个值,例如:

img

上述代码示例使用Mock对象替换了XenServer环境下的XenAPI网络连接对象,如此一来,我们就可以在KVM等任何环境下执行单元测试,而不局限于XenServer环境。

2.Tox

执行单元测试的途径有两种:一种是Tox;另一种是项目源码树根目录下的run_tests.sh脚本。通常我们使用的是Tox。

Tox是一个标准的virtualenv(Virtual Python Environment Builder)管理器和命令行测试工具。可以用于:检查软件包能否在不同的Python版本或解释器下正常安装;在不同的环境中运行测试代码;作为持续集成服务器的前端,大大减少测试工作所需时间。

每个项目源码树的根目录下都有一个Tox配置文件tox.ini,如Ceilometer项目的tox.ini片段:

img

对于开发而言,通常只需要运行下面两个tox命令:

img

在第一次执行时,会自动安装一些依赖的软件包,如果自动安装失败,我们就需要根据提示信息手动进行安装。

如果我们只希望执行特定的单元测试代码,不喜欢浪费时间去等待所有单元测试的执行,则可以使用参数指定,比如,执行ceilometer/tests/compute/virt/xenapi/test_inspector.py:

img

2.5.4 持续集成Jenkins

《重构 改善代码既有的设计》作者Martin Fowler在《持续集成》一书中对持续集成的定义:持续集成是一种软件开发实践。在持续集成中,团队成员频繁地集成他们的工作成果,一般每人每天至少集成一次,也可以多次。每次集成会经过自动构建(包括自动测试)的检验,以尽快发现集成错误。许多团队发现这种方法可以显著减少集成引起的问题,并可以加快团队合作进行软件开发的速度。

通俗来说,持续集成(CI)需要对每次代码提交都进行一次从代码集成到打包发布的完整流程,以判断提交的代码对整个流程带来的影响程度。而这个过程中所使用的方法严重依赖于团队成员的多少、目标平台和配置的不同等因素。比如,只有一个人,同时只面向一个平台,那么在每次出现一个commit时,手动进行一下测试基本就能知道结果,也就完全不需要其他更为复杂的工具与方法。

但是对于OpenStack这样的项目来说,显然没有这么简单,通常会涉及一个使用版本控制软件来维护的代码仓库,自动的构建过程,包括自动编译、测试、部署等,以及一个持续集成服务器。

1.Jenkins

OpenStack使用Jenkins搭建自己的持续集成服务器。对于一般的小团队来说,通常会先commit再执行CI,而OpenStack则不同,它会先通过Gerrit对每个commit进行review,这时Jenkins会执行整个CI的过程,若通过,则会“+1”,否则会“-1”,如图2-11所示。

img

图2-11 Jenkins结果

如图2-11所示的Jenkins列标明了Jenkins针对这个commit进行了哪些测试及其结果,在这一列中,以“gate-ceilometer-”为前缀的是Tox的测试结果,如gate-ceilometer-pep8对应的就是运行tox-e pep8命令的测试结果。

2.Tempest

根据上文的描述,Jenkins需要依托大量的单元测试及集成测试代码,单元测试的代码位于各个项目自身的源码树里,而OpenStack的集成测试则使用Tempest作为框架。

Tempest是OpenStack项目中一个独立的项目,它的源码位于/opt/stack/tempest/目录下(使用Devstack部署开发环境),包含了大量的测试用例。例如:

img

etc/目录包含Tempest的配置文件,tools/目录包含一些辅助脚本,所有的测试用例都存放在tempest/目录下。

tempest.api主要测试OpenStack API部分的功能,tempest.cli测试OpenStack CLI接口,tempest.scenario主要根据一些复杂场景进行测试,包括启动VM、挂载Volume和网络配置等,tempest.stress主要进行压力测试,tempest.services则测试自己实现的API客户端,是对各个项目API的封装,目的是不让一些Bug隐藏在官方实现的客户端里面。

以tempest.api为例,它里面的所有测试用例都是基于tempest.test.BestTestCase的,这个基类声明了setUpClass方法,可以在类初始化时调用。tempest.api.<project>.base对这个基类进行了继承,如tempest.api.compute.base.BaseV2ComputeTest,还实现了很多工具函数,供各项目API相关的测试用例调用,有了这些工具函数,就可以很方便地编写具体的测试用例。

比如,tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON继承自BaseV2ComputeTest,所以在初始化时,就会把Tempest自己实现的API Client赋值给类的属性。在具体的测试函数里,FlavorsV2TestJSON就会利用这个Client的函数来对OpenStack进行查询:

img

在上述测试用例中,test_list_flavors_with_detail就是先利用flavor client来获取所有的flavor列表,再获取某个具体的flavor,然后验证这个flavor是否在所有的flavor里面的。

而这里使用的就是Tempest自己实现的RESTful API Client,具体实现位于tempest.services。

在我们提交代码到Gerrit上后,Jenkins会执行包括集成测试在内的各项测试,但有时仍然需要我们在本地执行集成测试,比如,针对新功能的patch引发Tempest某些测试用例执行失败,需要我们修改Tempest代码(通常的做法是注释掉这个失败的测试用例,并将修改提交给Tempest,待Tempest接受后,会成功通过原来的patch集成测试,等到它们被相应的项目接受后再修改Tempest代码并提交)。

本地执行Tempest测试可以使用nose或testr工具。

执行所有Tempest测试用例:

img

执行某个测试用例:

img

3.第三方CI

如前文所述,Jenkins是OpenStack的官方CI系统,每个patch在最终合并前都必须通过Jenkins的测试。

此外,还有许多第三方提供的自动化测试系统用于帮助验证、测试特定的patch,通常被称为第三方CI,如图2-12所示,Reviewers栏的Intel NFV CI、Intel PCI CI、Mellanox CI等均为第三方CI。

第三方CI也是通过Gerrit系统接入OpenStack的开发流程的。每提交一个patch,Gerrit会发布一个事件,Jenkins会监听Gerrit事件,启动patch测试或Gate(将代码合并入主干)流程。每个第三方CI一般都只关注某个官方项目,专门测试一部分代码,比如,Intel PCI Test只接受Nova Patch Set Create事件,启动针对PCI子系统的测试,并且测试结果会通过Gerrit反馈给OpenStack社区。最终,我们在该patch相应的Gerrit评审页面的Reviewers栏就能看到Intel PCI Test的测试结果。

img

图2-12 第三方CI

第三方CI基本上都会基于成熟的Jenkins测试系统,基本架构如图2-13所示,最基本的配置包括一个Jenkins Master,几个测试端,一个用于发布测试日志的开放的Web/FTP服务器,测试日志至少要保留几个月时间。OpenStack Infra对Jenkins和Web Server的设置都有具体的规定和指导。

img

图2-13 第三方CI基本架构

Jenkins官方提供了Gerrit trigger插件,可以连接到review.openstack.org:29418并接收官方Gerrit事件。在trigger插件内,可以过滤出感兴趣的变化,触发Jenkins具体的测试,比如Intel PCI Test会在Nova patch提交时触发针对PCI的测试。

当测试完成后,需要将测试日志发布到公开的Log Server内,并根据测试将结果反馈给Gerrit,同时将日志连接一并发回给Gerrit。之后,开发者(Stackers)就能看到测试结果,访问测试日志。

根据OpenStack 官方要求,每个第三方CI都需要申请一个专用OpenStack账号,用于接入官方CI系统。第三方CI的申请人需要确保第三方CI反馈给社区有意义的结果,并保证7×24小时运行,对于出现的问题要及时处理,确保OpenStack Infra团队可以联系到维护人员。

重要的要求如下所述。

(1)及时处理问题,更新CI状态。

(2)积极参与Infra team IRC会议。

(3)及时处理CI出现的各种问题。

第三方CI容易出现如下所述的一些问题。

(1)网络问题。

OpenStack测试环境一般是基于Devstack的,整个测试环境从开始搭建到结束的耗时从20分钟到50分钟不等,与测试的复杂程度相关。其中需要大量访问国际Internet,包括发行版、Python库、大量Git仓库。任意时刻出现网络不稳定、断网,都会导致测试失败或者用时增加。

为了提高可靠性,许多第三方CI会搭建本地发行版镜像、Pypi镜像及Git镜像,这无疑又增加了CI系统的复杂度,带来更多稳定性问题。

(2)机器问题。

作为Jenkins Slave的机器需要有很健壮的系统和硬件支撑,包括SSD硬盘、RAID系统等,一旦基础硬件出现问题,就会影响一批测试结果,需要及时处理。

(3)软件环境问题。

很多测试在VM/LXC内运行,这对于软件环境清理是一件好事,但是也存在需要直接运行在物理机器上的测试系统,此时的软件环境清理就面临非常严重的挑战。因为Devstack并不负责清理测试后所安装的软件及可能出现的问题,比如,MySQL容易配置/测试不当导致下一次部署失败。

(4)测试代码问题。

代码测试有两大类:一类是官方测试,此类测试与官方同步;另一类是私有测试,也是第三方CI存在的意义。如果私有测试的代码不能耦合到官方的测试库(Tempest),就会导致一系列问题,最常见的问题是官方Tempest代码变动导致私有测试失败,此时应该将测试代码从Tempest中解耦。

(5)自动化维护和恢复问题。

为了保证迅速处理所发生的问题,自动化的维护部署工具必不可少,使用流行的Ansible或Puppet都是不错的选择。

(6)监控和告警系统。

Zabbix可以提供基本的系统告警功能,但是还有更详细的告警需求,比如,测试持续失败、某台机器测试连续失败、本地镜像不能访问等。