自分用にLinuxカーネルのBuildbot + LAVAでCI環境を作るめも

ちょっとCI環境でも作るか〜ということで。

CI

だいたいこんな構成です。

f:id:masami256:20190714155852p:plain
構成

いまのところすべてローカル環境で閉じてます。うちはマンションタイプのBフレッツでそんなに速いわけでもないからローカルで閉じてるほうがテスト時間が短くて済むというとこです。AWSとかVPSを使っても良いんですけどね。

とりあえず動くようにはなったので、ブラッシュアップするところはたくさんあります😨

CIのシステムで使用するソフトウェア

次の2つを使います。

Buildbotは有名ですよね。LAVAはkernelci.orgで使われています。実ボードとかqemuなどの環境でLinuxをテストするのに向いているツールです。

インストール

buildbot

pythonのvirtualenvを使って専用のpython環境作ってインストールして使ってます。

LAVA

LAVAは一回スクラッチから設定したことあるんですが面倒なのでdockerを使います。githubkernelciのプロジェクトのgitリポジトリがあるのでcloneして使いました。

Webサーバ

Webサーバも使うんですがこれはLAVAからカーネルなどのファイルをダウンロードするのに使うだけなのでpythonワンライナーで済ませてます。

流れ

ファイルを編集してコミットしたらbuildbotがリポジトリをポーリングしているので、変更を検知したらビルド開始します。make bzImageとmake modules、make modules_installまで成功したらdracutでinitramfsを作り、bzImageとinitramfsをwebサーバのディレクトリに置きます。そしたらbuildbotのジョブからLAVAのAPIを叩いてboot testを実行します。buildbotはLAVAのテストが終わるのを待って、LAVAのテスト結果を最終的なビルドの結果とします。

画面

画面はこんな感じですね。

buildbot

f:id:masami256:20190714154349p:plain
buildbot

lavaテスト実行画面

f:id:masami256:20190714154731p:plain
lava

lavaテスト実行画面の最下部

f:id:masami256:20190714154753p:plain
lava

設定

buildbot

buildbotでのテストステップはmaster.cfgで完結してますが、テストステップの記述はファイルを分けたほうが綺麗でしょうね。

pollingの設定はこんな感じでローカルのディレクトリを直に設定。このディレクトリはgit --bare initしたリモートリポジトリではなく、単純にcloneしてきたディレクトリです。ポーリング間隔はもっと短くて良いかな。

c['change_source'].append(changes.GitPoller(
        '/home/masami/projects/linux-kernel',
        workdir='linux-workdir', branch='ktest',
        pollInterval=300))

スケジューリングの設定はこんな感じ。forceビルドもしますよねということで。

c['schedulers'].append(schedulers.SingleBranchScheduler(
                            name="ktest branch test",
                            change_filter=util.ChangeFilter(branch='ktest'),
                            treeStableTimer=None,
                            builderNames=["linux-master-builder"]))
c['schedulers'].append(schedulers.ForceScheduler(
                            name="force",
                            builderNames=["linux-master-builder"]))

テストステップはこうです。

 check out the source
factory.addStep(steps.Git(repourl='/home/masami/projects/linux-kernel', mode='incremental'))

# start build

factory.addStep(steps.ShellCommand(command=["make", "mrproper"]))
factory.addStep(steps.ShellCommand(command=["cp", "/home/masami/projects/build-test/vm-config", ".config"]))

config_path = "%s/.config" % tmpdir_path
factory.addStep(steps.ShellCommand(command=["./scripts/config", "--set-str", "CONFIG_LOCALVERSION", "-build-test"]))

import multiprocessing
parallel_opt = '-j%s' % multiprocessing.cpu_count()


# build kernel and modules
factory.addStep(steps.ShellCommand(command=["make", parallel_opt, "bzImage"]))
factory.addStep(steps.ShellCommand(command=["make", parallel_opt, "modules"]))

# install modules
factory.addStep(steps.ShellCommand(command=["make", "INSTALL_MOD_PATH=/tmp/linux-build-test", "modules_install"]))

factory.addStep(steps.ShellCommand(command=["cp", "-f", "/home/masami/projects/builder/worker/linux-master-builder/build/arch/x86/boot/bzImage", "/home/masami/projects/web/bzImage"]))
                
factory.addStep(steps.ShellCommand(command=["dracut", "--force", "-k", "/tmp/linux-build-test/lib/modules/5.2.0-build-test+", "--kernel-image", "/home/masami/projects/web/bzImage", "--kver", "5.2.0-build-test+", "/home/masami/projects/web/initramfs-build-test.img"]))

factory.addStep(steps.ShellCommand(command=["/home/masami/projects/build-test/boot-test.py", "/home/masami/projects/build-test/boot-test.yaml"]))

factory.addStep(steps.ShellCommand(command=["rm", "-fr", "/tmp/linux-build-test"]))

やってることはこんな感じです。

  1. git cloneする
  2. make mrproper
  3. 別のディレクトリからカーネルのコンフィグをコピーする
  4. CONFIG_LOCALVERSIONをセット
  5. 使えるcpu数を調べる
  6. make bzImage
  7. make modules
  8. make modules_install 9 bzImageをwebサーバのディレクトリにコピー
  9. dracutでinitramfsを作る
  10. 作ったinitramfsをwebサーバのディレクトリにコピー
  11. LAVAのAPIを実行するスクリプトを実行してLAVAでテスト
  12. 後始末

カーネルのバージョンを直書きしてるのは直さないとねとか色々。

LAVA

LAVAはdockerイメージをそのまま使っていて特に設定の変更はしてません。テストはAPIから実行するのでAPI用のトークンを作ったくらいですね。APIを実行するスクリプトは↓です。テストはjobという形で登録します。jobを記述するときのフォーマットはyaml形式で、APIから登録するときはyaml形式の文字列を送ります。

#!/usr/bin/env python3

import xmlrpc.client
import sys
import time
import yaml

def create_server_instance():
    username = 'user name'
    token = 'access token'
    hostname = 'localhost:10080'
    return xmlrpc.client.ServerProxy('http://%s:%s@%s/RPC2' % (username, token, hostname), allow_none=True)

def submit_job(server, job):

    return server.scheduler.submit_job(job)
    
def read_job_yaml(filepath):
    with open(filepath, 'r') as f:
        return f.read()

def wait_until_job_finish(server, job_id):

    while True:
        ret = server.scheduler.job_health(job_id)
        health = ret['job_health']

        if not health == 'Unknown':
            return
        time.sleep(5)

def check_job_result(server, job_id):
    results_raw = server.results.get_testjob_results_yaml(job_id)
    try:
        from yaml import CLoader as Loader
    except ImportError:
        from yaml import Loader
        
    results = yaml.load(results_raw, Loader=Loader)
    
    for ret in results:
        r = ret['result']
        if r == 'fail':
            return False

    return True

if __name__ == '__main__':
    if not len(sys.argv) == 2:
        print('[-]usage: %s <path to job definition yaml file>' % sys.argv[0])
        exit(1)

    server = create_server_instance()        
    job = read_job_yaml(sys.argv[1])
    job_id = submit_job(server, job)

    print('job: %d created' % job_id)

    wait_until_job_finish(server, job_id)

    ret = check_job_result(server, job_id)

    if ret:
        print('Test: success')
        exit(0)

    print('Test: fail')
    exit(1)

上記のスクリプトはscheduler.submit_job()でjobを登録し、scheduler.job_health()で終わるのを待っています。その後、get_testjob_results_yaml()でテスト結果をステップごとに見ていってるんですが、これは実際不要ですね。scheduler.job_health()の結果としては以下の4つで、Unkownはテスト実行中に返ってくるので結果のステータスは3個。なのでComplete以外ならエラーとしても大丈夫だと思います。

  • UNKONW(実行中はこれが返ってくる)
  • Complete(成功時)
  • Incomplete(失敗時)
  • Cancel
LAVA test case

APIでjob登録するとして、どんなyamlを書いてるのかというとこんなのです。

# simple boot test
device_type: qemu
job_name: kernel build test x86-64 , boot test

timeouts:
  job:
    minutes: 15
  action:
    minutes: 10
  connection:
    minutes: 10
priority: medium
visibility: public

context:
  arch: amd64
  memory: 4096
  extra_options:
    - --append "root=/dev/sda1 audit=0 console=tty0 console=ttyS0,115200"
    - -serial stdio
actions:
- deploy:
    timeout:
      minutes: 3
    to: tmpfs

    images:
      kernel:
        image_arg: -kernel {kernel}
        url: http://192.168.11.18:12080/bzImage
      initrd: 
        url: http://192.168.11.18:12080/initramfs-build-test.img
        image_arg: -initrd {initrd}        
      rootfs:
        image_arg: -drive file={rootfs} 
        url: http://192.168.11.18:12080/ktest.qcow2
- boot:
    timeout:
      minutes: 2
    method: qemu
    media: tmpfs
    prompts: 
      - "Last login:"
      - "root@ktest:"
    auto_login:
      login_prompt: "ktest login:"
      username: "root"

contextのところでqemuに渡すパラーメータを設定しています。actionsはテスト対象のカーネル、initrdなどの取得先とqemuに渡すパラメータの設定です。rootfsはその名の通りroot filesystemです。これはfedora30のserver版を使って作りました。ディスクに3GB割り当てたけど1GBでも充分だったな。 bootのところがbootテストの内容です。auto_loginのところで指定したログインプロンプトが表示されたらusernameを送ってます。rootfsのほうでrootのパスワードは無しに設定しているのでパスワード無しでログインしてます。もちろんパスワードプロンプトとパスワードを設定することできます。promptsは期待値になる部分で、ログインに成功したときに表示されるものをリストしてます。

lavaの出力のこの辺です。

f:id:masami256:20190714162213p:plain
lava

まとめ

BuildbotとLAVAを使い、ここまでの設定でカーネルを弄ってコミットするとビルド・テストが自動で実行できるようになりました🎉