embEDUx buildserver is a compound of several components. Together, these components form a unit that is designed to build different products automatically as soon as the user provides new build specifications via the product repositories. All the components that run on the buildserver will be contained by Docker containers.
Setup Routine With Ansible
The evaluation chose Ansible for the implementation of the setup routine. The tasks are be grouped into roles and plays, with the target architectures being variable. This allows to reuse tasks and roles for the different target architectures, but requires a high flexibility in the tasks.
Playbooks
The implementation includes one Playbook for every major setup task, and another one that includes all of them.
buildserver.yml
This playbook is the one that contains all major setup tasks. The Plays that are executed in the default configuration are listed in the following table.
Play | Hosts | Actions |
---|---|---|
#1 | all | Dependency Installation |
#2 | buildmaster | Buildmaster Container Setup |
#3 | all | Define groups for target architectures |
#4 | buildslaves-amd64 | Setup and run amd64 buildslaves |
#5 | buildslaves-armv6j_hardfp | Setup and run arm6j_hardfp buildslave containers |
#6 | buildslaves-armv7a_hardfp | Setup armv7a_hardfp buildslave containers |
dependencies.yml
The dependencies Playbook takes care of installing and starting Docker on the target machine. As required by design and evaluation, it can do this on Ubuntu and Gentoo machines. It has been tested on Virtual Machines that were installed with the mentioned Linux derivatives.
buildmaster.yml
The buildmaster Playbook builds and runs the buildmaster container on the target machine. The architecture will be chosen according to the target machine architecture. In theory, this should work on any target architecture that is available as Gentoo stage3, but it has only been tested on x86_64 target machines so far.
buildslaves.yml
The buildslaves Playbook builds and runs the buildslaves containers on the target machine. By default, the target machine is the buildmaster itself, thus the buildslaves for all targets architectures will run on the buildmaster. It should be possible to distribute the buildslaves to several machines, including foreign architectures, but that scenario has not been tested yet.
Buildmaster Configuration Generation
The buildmaster configuration is highly dependent on the configuration that is provided by the setup variables. As designed and evaluated, a template engine is used to generate the master.cfg during the setup procedure. A detailed explanation of how the variables and template are connected will be given in this chapter.
Provided Information - group_vars/all
The following file is the default configuration file for the setup routine. It contains the setup that has been running at the HTWG, and it includes the supported platforms used at the HTWG.
---
# Variables listed here are applicable to all host groups
base_dir: /var/tmp/embedux
config_dir: "{{ base_dir }}/config"
embedux_tmp: /var/tmp/embedux
repos_url_base: "https://github.com/embEDUx"
arch_branchregex:
amd64:
- ".*-ctng-.*"
- ".*qemu-virt-amd64.*"
armv6j_hardfp:
- ".*raspberry-pi"
armv7a_hardfp:
- ".*beaglebone-black"
- ".*banana-pi"
- ".*irisboard"
- ".*utilite-pro"
- ".*qemu-virt-arm.*"
arch_map:
x86_64: amd64
armv6l: armv6j_hardfp
armv7l: armv7a_hardfp
arch_short_map:
amd64: amd64
armv6j_hardfp: arm
armv7a_hardfp: arm
native_arch: "{{ arch_map[ansible_architecture] }}"
native_arch_short: "{{ arch_short_map[native_arch] }}"
docker_image_prefix: "embedux"
Configuration Template - master.cfg.j2
This file will be rendered according to the information from the group_vars/all file which contains the default configuration and can be edited by the Administrator. Additionally, secrets that have been defined in the vault are assigned to variables that are available in the buildmaster configuration. If interested, the instructions how to create the vault file are demonstrated within the setup documentation
PSK (Pre-Shared-Key)
This variable is filled in by the buildsetup template renderer.
psk = "{{ buildbot_psk }}"
The buildbot_psk-variable is special to the buildserver setup routine, because it is stored in the password protected vault file.
Usernames and Passwords For Web-Interface
User permissions for the web-interface are also defined in the previously mentioned vault-file.
users = [
{% for user,pw in users.items() %}
("{{ user}}", "{{ pw }}"),
{% endfor %}
]
Branch <-> Platform <-> Architecture Mapping / Repository URLs
The buildmaster configuration template implements the mapping between repository branches and the corresponding platform or architecture.
These data structures are filled in by the buildsetup template renderer.
arch_branch_res_map = {
{% for arch, regex_list in arch_branchregex.items() %}
"{{ arch }}": {{ regex_list }},
{% endfor %}
}
git_repo_uris = {
"default": ["{{ repos_url_base }}/linux-specs.git",
"{{ repos_url_base }}/uboot-specs.git",
"{{ repos_url_base }}/toolchain-specs.git",
"{{ repos_url_base }}/misc-specs.git"],
"rootfs": ["{{ repos_url_base }}/rootfs-specs.git"],
"rootfs_buildroutine": "{{ repos_url_base }}/rootfs-buildroutine.git",
}
As seen, the repo_url_base variable that is provided by the setup routine defines the URLs that are later being configured for change detection.
Continuous Integration
While the above chapter gives an introduction of how the setup incorporates the variables into the buildmaster configuration, this part will demonstrate how the Continuous Integration-aspects have been implemented with Buildbot.
Buildslaves
Every buildslave container needs an equivalent buildslave configuration. Each architecture is configured with two buildslaves, one for default builds and one for rootfs builds. All buildslaves use the previously explained PSK as a password.
c["slaves"] = [BuildSlave(arch, psk) for arch in arch_branch_res_map.keys()]
c["slaves"].extend([BuildSlave("rootfs_"+arch, psk) for arch in
arch_branch_res_map.keys()])
from buildbot.changes.gitpoller import GitPoller
c['change_source'] = []
for git_repo_uri in git_repo_uris["default"]+git_repo_uris["rootfs"]:
c['change_source'].append(GitPoller(
repourl=git_repo_uri,
branches=True,
pollinterval=30))
Repository Poller
Buildbot offers a poller for most repository formats. The GitPoller allows polling the above repositories for changes.
from buildbot.changes.gitpoller import GitPoller
c['change_source'] = []
for git_repo_uri in git_repo_uris["default"]+git_repo_uris["rootfs"]:
c['change_source'].append(GitPoller(
repourl=git_repo_uri,
branches=True,
pollinterval=30))
The repositories are polled for changes every 30 seconds.
Schedule Builds on Repository Changes
Schedulers are notified by the Repository Pollers on any change. The scheduler can then decide if a build should be scheduled or not. Buildbot offers several schedulers. A suitable scheduler for our purpose is the AnyBranchScheduler. In the following snippet, they are used together with regex expressions to accomplish the mapping shown in the previous code.
def default_repos(repository):
return repository in git_repo_uris["default"]
def rootfs_repos(repository):
return repository in git_repo_uris["rootfs"]
c['schedulers'] = []
for arch,branch_res in arch_branch_res_map.items():
for branch_re in branch_res:
c['schedulers'].append(AnyBranchScheduler(
name="default / arch: %s / branch-filter: '%s'" % (arch, branch_re),
change_filter=filter.ChangeFilter(branch_re=branch_re,
repository_fn=default_repos),
treeStableTimer=10,
builderNames=[arch]))
for arch in arch_branch_res_map.keys():
branch_re="%s.*" % arch
c['schedulers'].append(AnyBranchScheduler(
name="rootfs / arch: %s / branch-filter: '%s'" % (arch, branch_re),
change_filter=filter.ChangeFilter(branch_re=branch_re,
repository_fn=rootfs_repos),
treeStableTimer=10,
builderNames=["rootfs_"+arch]))
Build Factories and Builders
The actual build jobs are defined as BuildFactories, and are then assigned to the respective Builders These builders will receive build jobs by the scheduler accordingly.
Build Factories
The build factories for default and rootfs builds implement the buildjobs. After checking out the changed repository branches
Default factory - runs the ./build executable
The default factory expects the repository to have an executable named build. It will be executed after the changed repository branch has been checked out to the filesystem.
# default factory
factory_default = BuildFactory()
...
factory_default.addStep(ShellCommand(command=["./build"], haltOnFailure=True, usePTY=True))
...
RootFS factory - runs the ansible-playbook from the RootFS-Buildroutine
The RootFS factory works completely different compared to the default factory. It retrieves the RootFS build routine from the previously defined repository URL. Afterwards it runs the Playbook named site.yml which processes the RootFS specifications from the changed RootFS repository branch.
# rootfs factory
factory_rootfs = BuildFactory()
...
factory_rootfs.addStep(ShellCommand(command="/usr/bin/git clone --single-branch --depth 1 %s .ansible" % git_repo_uris['rootfs_buildroutine']))
factory_rootfs.addStep(ShellCommand(command="ansible-playbook -i .ansible/hosts .ansible/site.yml -vvvvv",
timeout=None, usePTY=True, haltOnFailure=True,
env={ "TERM": "vt100",
"ANSIBLE_CONFIG": ".ansible/ansible.cfg",
"ANSIBLE_FORCE_COLOR": "1",
"BUILDMASTER_URL": buildaster_url,
}))
Upload The Output
Afterwards the factories successfully complete their build processes, the content of directory called output is uploaded to the buildmaster webserver.
factory_rootfs.addStep(
DirectoryUpload(
slavesrc="output",
masterdest=Interpolate("/var/lib/buildmaster/public_html/%(prop:product)s/%(prop:platform)s"),
url=Interpolate("/%(prop:product)s/%(prop:platform)s")
)
)
Many build steps have been skipped for this overview, please consult the master.cfg.j2 file directly for more details.
Builders
The builder assignment implements the arch <-> branch <-> platform mapping. Effectively, the builders are assigned per architecture, since the platform mappings are already mapped to their according architecture.
c['builders'] = []
for arch in arch_branch_res_map.iterkeys():
c['builders'].append(
BuilderConfig(
name=arch,
slavenames=arch,
factory=factory_default,
))
for arch in arch_branch_res_map.iterkeys():
c['builders'].append(
BuilderConfig(
name="rootfs_"+arch,
slavenames="rootfs_"+arch,
factory=factory_rootfs,
))
Authentication and Permissions
Last but not least, the permissions for the previously rendered userlist are configured. This configuration only authenticated users to scheduler and abort any builds manually.
authz_cfg=authz.Authz(
# change any of these to True to enable; see the manual for more
# options
auth=auth.BasicAuth(users),
gracefulShutdown = 'auth',
forceBuild = 'auth', # use this to test your slave once it is set up
stopBuild = 'auth',
stopAllBuilds = 'auth',
cancelPendingBuild = 'auth',
pingBuilder = True,
)
Please consult the official docs at Webstatus for more details on these options.