Buildbot:一个故事,其中包含另一个持续集成系统的示例


(图片来自官方网站

顾名思义,Buildbot是一个持续集成系统(ci)。 在哈布雷上已经关于他的几篇 文章 ,但是,从我的角度来看,从他们那里并不清楚该工具的优势。 另外,它们几乎没有示例,这使得很难看到程序的全部功能。 在我的文章中,我将尝试弥补这些缺点,讨论内部设备Buildbot'a并提供一些非标准脚本的示例。

常用词


当前,有大量的连续集成系统,其中之一涉及到一个相当合乎逻辑的问题,其精神是“如果您已经有一个<program_name>并且每个人都使用它,为什么需要它?” 我将尝试回答有关Buildbot的问题。 一些信息将与现有文章重复,一些信息在官方文档中进行了描述,但这对于叙述的一致性是必需的。

与其他持续集成系统的主要区别在于,Buildbot是用于编写ci的Python框架,而不是开箱即用的解决方案。 这意味着,为了将项目连接到Buildbot,必须首先使用Buildbot框架编写一个单独的python程序,该程序可实现项目所需的持续集成功能。 这种方法提供了极大的灵活性,允许您实施棘手的测试方案,这些方案由于架构上的限制而对于开箱即用的解决方案是不可能的。

此外,Buildbot不是服务,因此您必须诚实地将其部署在基础架构上。 在这里,我注意到该框架非常忠实于系统的资源。 当然不是C或C ++,但是python胜过了Java竞争对手。 例如,在这里,将内存消耗与GoCD进行比较(是的,尽管名称如此,这是一个Java系统):

Buildbot:



GoCD:



您自己部署和编写单独的测试程序会使您对初始设置感到难过。 但是,大量的内置类大大简化了脚本编写。 这些类涵盖了许多标准操作,无论是从github存储库获取更改还是使用CMake构建项目。 结果,对于某些travis-ci,用于小型项目的标准脚本不会比YML文件复杂。 我不会写有关部署的内容,现有文章中对此进行了详细介绍,并且那里也没有什么复杂的内容。

我注意到Buildbot的下一个功能是,默认情况下,测试逻辑是在ci服务器一侧实现的。 这与现在流行的“流水线作为代码”方法背道而驰,在该方法中,测试逻辑与项目源代码一起在存储库中的文件(如.travis.yml)中描述,而ci服务器仅读取该文件并执行它说什么。 同样,这只是默认行为。 Buildbot框架的功能允许您通过将测试脚本存储在资源库中来实现所描述的方法。 甚至还有一个现成的解决方案-bb -travis ,它试图从Buildbot和travis-ci中获取最大收益。 另外,在本文后面,我将描述如何自己实现类似于此行为的操作。

默认情况下,Buildbot会在推送时收集所有提交。 这似乎是一些小的不必要功能,但对我而言,它已成为主要优点之一。 许多流行的开箱即用的解决方案(travis-ci,gitlab-ci)根本没有提供这样的机会,仅与分支上的最后一次提交一起使用。 想象一下,在开发过程中,您常常不得不选择提交。 进行不起作用的提交将是令人不快的,由于它是与上面的一堆提交一起启动的,因此构建系统未对其进行检查。 当然,在Buildbot中,您只能构建最后一个提交,这是通过仅设置一个参数来完成的。

该框架有一个很好的文档,详细描述了从通用体系结构到扩展内置类的准则的所有内容。 但是,即使有了这样的文档,您也可能必须查看源代码中的某些内容。 它已根据GPL v2许可证完全开放,并且易于阅读。 缺点-该文档仅以英语提供,而俄语仅提供很少的网络信息。 该工具昨天没有出现,它借助pythonWiresharkLLVM许多其他知名项目进行了组装。 更新即将发布,该项目得到了许多开发人员的支持,因此我们可以谈论可靠性和稳定性。


(Python Buildbot主页)

定理


这部分实质上是有关框架体系结构的官方文档这一章的免费翻译。 它显示了从ci系统接收更改到将结果通知发送给用户的完整操作链。 因此,您对项目的源代码进行了更改,并将其发送到远程存储库。 图片中示意性地显示了接下来发生的情况:


(图片来自官方文档

首先,Buildbot应该以某种方式发现存储库中发生了更改。 有两种主要方法-网络钩子和轮询,尽管没有人禁止提出更复杂的方法。 在第一种情况下,在Buildbot中,BaseHookHandler后代类对此负责。 有许多现成的解决方案,例如GitHubHandlerGitoriusHandler 。 这些类中的关键方法是getChanges() 。 它的逻辑非常简单:它必须将HTTP请求转换为更改对象列表。

对于第二种情况,您需要PollingChangeSource后代 。 同样,还有现成的解决方案,例如GitPollerHgPoller 。 关键方法是poll() 。 它以一定的频率被调用,并且必须以某种方式在存储库中创建更改列表。 在git的情况下,这可能是对git fetch的调用以及与先前保存状态的比较。 如果内置功能不够用,那么只需创建自己的继承者类并重载该方法即可。 使用轮询的示例:

c['change_source'] = [changes.GitPoller( repourl = 'git@git.example.com:project', project = 'My Project', branches = True, #      pollInterval = 60 )] 

Webhook更加易于使用,主要是不要忘记在git服务器端对其进行配置。 这只是配置文件中的一行:

 c['www']['change_hook_dialects'] = { 'github': {} } 

下一步,将更改对象输入到调度程序对象( scheduler )。 内置调度程序的示例: AnyBranchSchedulerNightlySchedulerForceScheduler等。 每个调度程序都将所有更改对象作为输入,但是仅选择那些通过过滤器的更改对象。 过滤器通过change_filter参数传递到构造函数中的调度程序。 在输出中,计划者创建构建请求。 调度程序根据builders参数选择构建器。

一些计划者有一个棘手的参数,称为treeStableTimer 。 它的工作方式如下:收到更改后,调度程序不会立即创建新的构建请求,而是会启动计时器。 如果新的更改到达并且计时器尚未到期,则将旧的更改替换为新的更改,并更新计时器。 计时器结束时,调度程序仅根据上一次保存的更改创建一个构建请求。

因此,实现了在推送时仅组装最后提交的逻辑。 调度程序配置示例:

 c['schedulers'] = [schedulers.AnyBranchScheduler( name = 'My Scheduler', treeStableTimer = None, change_filter = util.ChangeFilter(project = 'My Project'), builderNames = ['My Builder'] )] 

构建请求(无论听起来有多奇怪)都进入构建器的输入。 收集器的任务是在可访问的“工人”上运行装配。 Worker是一个构建环境,例如Stretch64或ubuntu1804x64。 工人列表通过worker参数传递。 列表中的所有工作程序都应该相同(即名称自然不同,但内部环境相同),因为收集器可以自由选择任何可用的工作程序。 在此处设置多个值可以平衡负载,而不是在不同的环境中构建。 使用factor y参数,收集器将接收到一系列步骤以构建项目。 我将在下面详细介绍。

配置收集器的示例:

 c['builders'] = [util.BuilderConfig( name = 'My Builder', workernames = ['stretch32'], factory = factory )] 

至此,项目准备就绪。 Buildbot的最后一步是通知构建。 记者班对此负责。 一个经典的例子是MailNotifier类,该类发送带有构建结果的电子邮件。 MailNotifier连接示例

 c['services'] = [reporters.MailNotifier( fromaddr = 'buildbot@example.com', relayhost = 'mail.example.com', smtpPort = 25, extraRecipients = ['devel@example.com'], sendToInterestedUsers = False )] 

好了,是时候继续介绍完整的示例了。 我注意到Buildbot本身是使用Twisted框架编写的,因此对它的熟悉将大大有助于Buildbot脚本的编写和理解。 我们将为一个名为Pet Project的项目打个鞭子。 让它用C ++编写,使用CMake组装,源代码位于git存储库中。 我们不太懒惰,为他编写了由ctest团队运行的测试。 最近,我们阅读了这篇文章,并意识到我们希望将新获得的知识应用于我们的项目。

示例一:使其正常工作


实际上,配置文件:

100行python代码
 from buildbot.plugins import * # shortcut c = BuildmasterConfig = {} # create workers c['workers'] = [worker.Worker('stretch32', 'example_password')] # general settings c['title'] = 'Buildbot: test' c['titleURL'] = 'https://buildbot.example.com/' c['buildbotURL'] = 'https://buildbot.example.com/' # setup database c['db'] = { 'db_url': 'sqlite:///state.sqlite' } # port to communicate with workers c['protocols'] = { 'pb': { 'port': 9989 } } # make buildbot developers a little bit happier c['buildbotNetUsageData'] = 'basic' # webserver setup c['www'] = dict(plugins = dict(waterfall_view={}, console_view={}, grid_view={})) c['www']['authz'] = util.Authz( allowRules = [util.AnyEndpointMatcher(role = 'admins')], roleMatchers = [util.RolesFromUsername(roles = ['admins'], usernames = ['root'])] ) c['www']['auth'] = util.UserPasswordAuth([('root', 'root_password')]) # mail notification c['services'] = [reporters.MailNotifier( fromaddr = 'buildbot@example.com', relayhost = 'mail.example.com', smtpPort = 25, extraRecipients = ['devel@example.com'], sendToInterestedUsers = False )] c['change_source'] = [changes.GitPoller( repourl = 'git@git.example.com:pet-project', project = 'Pet Project', branches = True, pollInterval = 60 )] c['schedulers'] = [schedulers.AnyBranchScheduler( name = 'Pet Project Scheduler', treeStableTimer = None, change_filter = util.ChangeFilter(project = 'Pet Project'), builderNames = ['Pet Project Builder'] )] factory = util.BuildFactory() factory.addStep(steps.Git( repourl = util.Property('repository'), workdir = 'sources', haltOnFailure = True, submodules = True, progress = True) ) factory.addStep(steps.ShellSequence( name = 'create builddir', haltOnFailure = True, hideStepIf = lambda results, s: results == util.SUCCESS, commands = [ util.ShellArg(command = ['rm', '-rf', 'build']), util.ShellArg(command = ['mkdir', 'build']) ]) ) factory.addStep(steps.CMake( workdir = 'build', path = '../sources', haltOnFailure = True) ) factory.addStep(steps.Compile( name = 'build project', workdir = 'build', haltOnFailure = True, warnOnWarnings = True, command = ['make']) ) factory.addStep(steps.ShellCommand( name = 'run tests', workdir = 'build', haltOnFailure = True, command = ['ctest']) ) c['builders'] = [util.BuilderConfig( name = 'Pet Project Builder', workernames = ['stretch32'], factory = factory )] 


通过编写这些行,我们可以在推送到存储库时自动组装,漂亮的网络外观,电子邮件通知以及任何自重ci的其他属性。 其中的大部分应该很清楚:调度程序,收集器和其他对象的设置与前面给出的示例相似,大多数参数的值都很直观。 详细地说,我将只专注于创建工厂,这是我之前承诺要做的。

工厂由Buildbot必须为项目完成的构建步骤组成。 与其他类一样,有许多现成的解决方案。 我们的工厂包括五个步骤。 通常,第一步是获取存储库的当前状态,在这里我们不会例外。 为此,我们使用标准的Git类:

第一步
 factory = util.BuildFactory() factory.addStep(steps.Git( repourl = util.Property('repository'), workdir = 'sources', haltOnFailure = True, submodules = True, progress = True) ) 


接下来,我们需要创建一个目录,将在其中组装项目-我们将完全利用源代码进行构建。 在此之前,您必须记住删除该目录(如果已经存在)。 因此,我们需要执行两个命令。 ShellSequence类将帮助我们:

第二步
 factory.addStep(steps.ShellSequence( name = 'create builddir', haltOnFailure = True, hideStepIf = lambda results, s: results == util.SUCCESS, commands = [ util.ShellArg(command = ['rm', '-rf', 'build']), util.ShellArg(command = ['mkdir', 'build']) ]) ) 


现在您需要启动CMake。 为此,使用两个类之一( ShellCommandCMake)是合乎逻辑的。 我们将使用后者,但区别很小:它是第一个类的简单包装 ,使传递特定于CMake的参数更加方便。

第三步
 factory.addStep(steps.CMake( workdir = 'build', path = '../sources', haltOnFailure = True) ) 


是时候编译项目了。 与前面的情况一样,您可以使用ShellCommand 。 同样,还有Compile类,它是ShellCommand的包装器。 但是,这是一个比较棘手的包装器: Compile类在编译期间监视警告,并将警告准确地显示在单独的日志中。 这就是为什么我们将使用Compile类的原因:

第四步
 factory.addStep(steps.Compile( name = 'build project', workdir = 'build', haltOnFailure = True, warnOnWarnings = True, command = ['make']) ) 


最后,运行我们的测试。 在这里,我们将使用前面提到的ShellCommand类:

第五步
 factory.addStep(steps.ShellCommand( name = 'run tests', workdir = 'build', haltOnFailure = True, command = ['ctest']) ) 


例子二:管道作为代码


在这里,我将展示如何实现预算选项以将测试逻辑与项目源代码一起存储,而不是在ci-server配置文件中。 为此,请将.buildbot文件与代码一起放入存储库中,其中每行包含单词,第一行解释为要执行的命令的目录,其余解释为带有参数的命令。 对于我们的宠物项目, .buildbot文件将如下所示:

.buildbot文件与命令
. rm -rf build
. mkdir build
build cmake ../sources
build make
build ctest


现在,我们需要修改Buildbot配置文件。 要分析.buildbot文件,我们必须编写自己的步骤的类。 此步骤将读取.buildbot文件,然后为每一行添加带有必要参数的ShellCommand步骤。 为了动态添加步骤,我们将使用build.addStepsAfterCurrentStep()方法。 看起来一点也不可怕:

类分析步骤
 class AnalyseStep(ShellMixin, BuildStep): def __init__(self, workdir, **kwargs): kwargs = self.setupShellMixin(kwargs, prohibitArgs = ['command', 'workdir', 'want_stdout']) BuildStep.__init__(self, **kwargs) self.workdir = workdir @defer.inlineCallbacks def run(self): self.stdio_log = yield self.addLog('stdio') cmd = RemoteShellCommand( command = ['cat', '.buildbot'], workdir = self.workdir, want_stdout = True, want_stderr = True, collectStdout = True ) cmd.useLog(self.stdio_log) yield self.runCommand(cmd) if cmd.didFail(): defer.returnValue(util.FAILURE) results = [] for row in cmd.stdout.splitlines(): lst = row.split() dirname = lst.pop(0) results.append(steps.ShellCommand( name = lst[0], command = lst, workdir = dirname ) ) self.build.addStepsAfterCurrentStep(results) defer.returnValue(util.SUCCESS) 


通过这种方法,收集器的工厂变得更加简单和通用:

工厂分析.buildbot文件
 factory = util.BuildFactory() factory.addStep(steps.Git( repourl = util.Property('repository'), workdir = 'sources', haltOnFailure = True, submodules = True, progress = True, mode = 'incremental') ) factory.addStep(AnalyseStep( name = 'Analyse .buildbot file', workdir = 'sources', haltOnFailure = True, hideStepIf = lambda results, s: results == util.SUCCESS) ) 


示例三:以工人为代码


现在想象一下,在项目代码旁边,我们不需要确定命令顺序,而是确定程序集的环境。 实际上,我们定义工人。 .buildbot文件可能看起来像这样:

.buildbot环境文件
{
"workers": ["stretch32", "wheezy32"]
}


在这种情况下,Buildbot配置文件将变得更加复杂,因为我们希望将不同环境上的程序集互连(如果至少一个环境失败,则整个提交都将视为无效)。 二级帮助我们解决问题。 我们将有一个本地工作程序,该程序分析.buildbot文件并在所需工作程序上运行构建。 首先,如前面的示例一样,我们将编写分析.buildbot文件的步骤。 要在特定工作程序上启动程序集,将使用“ 触发”步骤中的捆绑包和特殊类型的TriggerableScheduler调度程序。 我们的步骤变得有些复杂,但很容易理解:

类分析步骤
 class AnalyseStep(ShellMixin, BuildStep): def __init__(self, workdir, **kwargs): kwargs = self.setupShellMixin(kwargs, prohibitArgs = ['command', 'workdir', 'want_stdout']) BuildStep.__init__(self, **kwargs) self.workdir = workdir @defer.inlineCallbacks def _getWorkerList(self): cmd = RemoteShellCommand( command = ['cat', '.buildbot'], workdir = self.workdir, want_stdout = True, want_stderr = True, collectStdout = True ) cmd.useLog(self.stdio_log) yield self.runCommand(cmd) if cmd.didFail(): defer.returnValue([]) # parse JSON try: payload = json.loads(cmd.stdout) workers = payload.get('workers', []) except json.decoder.JSONDecodeError as e: raise ValueError('Error loading JSON from .buildbot file: {}' .format(str(e))) defer.returnValue(workers) @defer.inlineCallbacks def run(self): self.stdio_log = yield self.addLog('stdio') try: workers = yield self._getWorkerList() except ValueError as e: yield self.stdio_log.addStdout(str(e)) defer.returnValue(util.FAILURE) results = [] for worker in workers: results.append(steps.Trigger( name = 'check on worker "{}"'.format(worker), schedulerNames = ['Pet Project ({}) Scheduler'.format(worker)], waitForFinish = True, haltOnFailure = True, warnOnWarnings = True, updateSourceStamp = False, alwaysUseLatest = False ) ) self.build.addStepsAfterCurrentStep(results) defer.returnValue(util.SUCCESS) 


我们将对本地工人使用此步骤。 请注意,我们已将标签设置为我们的收集器“ Pet Project Builder”。 有了它,我们可以过滤MailNotifier ,告诉它只应将信件发送给某些收集者。 如果未执行此过滤,则在两个环境中构建提交时,我们将收到三个字母。

普通收藏家
 factory = util.BuildFactory() factory.addStep(steps.Git( repourl = util.Property('repository'), workdir = 'sources', haltOnFailure = True, submodules = True, progress = True, mode = 'incremental') ) factory.addStep(AnalyseStep( name = 'Analyse .buildbot file', workdir = 'sources', haltOnFailure = True, hideStepIf = lambda results, s: results == util.SUCCESS) ) c['builders'] = [util.BuilderConfig( name = 'Pet Project Builder', tags = ['generic_builder'], workernames = ['local'], factory = factory )] 


我们仍然需要为所有实际工作人员添加收集器和相同的Triggerable Scheduler:

合适环境中的收藏家
 for worker in allWorkers: c['schedulers'].append(schedulers.Triggerable( name = 'Pet Project ({}) Scheduler'.format(worker), builderNames = ['Pet Project ({}) Builder'.format(worker)]) ) c['builders'].append(util.BuilderConfig( name = 'Pet Project ({}) Builder'.format(worker), workernames = [worker], factory = specific_factory) ) 



(我们的项目在两种环境中的构建页面)

示例四:每几次提交一个字母


如果使用上述任何示例,您会注意到一个不愉快的功能。 由于每次提交都会创建一个字母,因此当我们向分支推送20个新的提交时,我们将收到20个字母。 避免出现这种情况,就像前面的示例一样,我们将提供两级帮助。 我们还需要修改类以获取更改。 我们将只创建一个这样的对象,而不是创建许多更改对象,这些对象的属性将传输所有提交的列表。 急着可以这样做:

类MultiGitHubHandler
 class MultiGitHubHandler(GitHubHandler): def getChanges(self, request): new_changes = GitHubHandler.getChanges(self, request) if not new_changes: return ([], 'git') change = new_changes[-1] change['revision'] = '{}..{}'.format( new_changes[0]['revision'], new_changes[-1]['revision']) commits = [c['revision'] for c in new_changes] change['properties']['commits'] = commits return ([change], 'git') c['www']['change_hook_dialects'] = { 'base': { 'custom_class': MultiGitHubHandler } } 


要使用这种异常的更改对象,我们需要自己的特殊步骤,该步骤动态创建收集特定提交的步骤:

类GenerateCommitSteps
 class GenerateCommitSteps(BuildStep): def run(self): commits = self.getProperty('commits') results = [] for commit in commits: results.append(steps.Trigger( name = 'Checking commit {}'.format(commit), schedulerNames = ['Pet Project Commits Scheduler'], waitForFinish = True, haltOnFailure = True, warnOnWarnings = True, sourceStamp = { 'branch': util.Property('branch'), 'revision': commit, 'codebase': util.Property('codebase'), 'repository': util.Property('repository'), 'project': util.Property('project') } ) ) self.build.addStepsAfterCurrentStep(results) return util.SUCCESS 


添加我们的公共收集器,该收集器仅涉及运行单个提交的程序集。 应该对它进行标记,以便通过此标记本身过滤发送信件的过程。

通用邮件提取程序
 c['schedulers'] = [schedulers.AnyBranchScheduler( name = 'Pet Project Branches Scheduler', treeStableTimer = None, change_filter = util.ChangeFilter(project = 'Pet Project'), builderNames = ['Pet Project Branches Builder'] )] branches_factory = util.BuildFactory() branches_factory.addStep(GenerateCommitSteps( name = 'Generate commit steps', haltOnFailure = True, hideStepIf = lambda results, s: results == util.SUCCESS) ) c['builders'] = [util.BuilderConfig( name = 'Pet Project Branches Builder', tags = ['branch_builder'], workernames = ['local'], factory = branches_factory )] 


仍然仅添加单个提交的收集器。 我们只是不使用标签来标记该收集器,因此不会为其创建字母。

通用邮件提取程序
 c['schedulers'].append(schedulers.Triggerable( name = 'Pet Project Commits Scheduler', builderNames = ['Pet Project Commits Builder']) ) c['builders'].append(util.BuilderConfig( name = 'Pet Project Commits Builder', workernames = ['stretch32'], factory = specific_factory) ) 


最后的话


本文绝不会取代阅读官方文档,因此,如果您对Buildbot感兴趣,那么下一步就是阅读它。 github上提供了所有示例的配置文件的完整版本。 相关链接,摘自该文章的大部分材料:

  1. 官方文件
  2. 项目源代码

Source: https://habr.com/ru/post/zh-CN439096/


All Articles