Files
wiki/存储/Ceph/知识库/19-开发者贡献指南篇.md
T
2026-09-18 13:49:11 +08:00

194 KiB
Raw Blame History

基于 Ceph 官方文档整理 | 文档版本:Tentacle v20.2.4 | 抓取日期:2026-09-17 | 源文:ceph/ceph@v20.2.4 doc/dev/developer_guide/ 官网入口:https://docs.ceph.com/en/tentacle/dev/developer_guide/

第 19 册 · 开发者贡献指南篇


本册导读

本册面向想给 Ceph 提 PR、参与社区开发的人。内容覆盖从「第一次 fork 仓库」到「PR 被 merge、backport、跑 teuthology 集成测试、用 gdb 调试」的完整贡献链路,并包含两块体量很大的专题:Ceph Dashboard 前后端开发(本册最大块,约占源文一半)与集成测试框架 teuthology。

运维向读者可跳过本册。 本册讲的是「如何改 Ceph 的代码」,不是「如何部署/调优 Ceph 集群」。如果你只是要装集群、扩 OSD、排障,请看第 3 册(集群运维与配置)、第 4 册(RADOS 排障)、第 5 册(cephadm 部署运维)。唯一可能对你有用的章节是第七章(用 gdb attach 到 ceph-osd 进程)与第八章(Windows CI 的产物怎么看),但那也是排障时才用得上。

本册覆盖的源文件(doc/dev/developer_guide/,共 18 个 .rst,约 214KB)

分组 文件 大小 对应章节
入口 index.rst 949B 一
入门 intro.rst、essentials.rst 1.2KB / 11KB 一
工作流 basic-workflow.rst 23KB 二
任务管理 issue-tracker.rst 1.4KB 三
合并策略 merging.rst 3.9KB 四
测试 tests-unit-tests.rst、running-tests-locally.rst、tests-windows.rst 6.2KB / 7.5KB / 6KB 五、八
集成测试 testing_integration_tests/(index + intro + workflow + debugging-tips + kernel + sentry,共 6 个) 约 49KB 六
调试 debugging-gdb.rst 1.4KB 七
Dashboard dash-devel.rst 103KB 十
追踪 jaegertracing.rst 2.4KB 十一

阅读须知

  1. 命令、工具名、目录名、git 规范术语、环境变量一律保留英文原文,中文只做叙述。
  2. 文档的年代痕迹要如实标注。本册部分页面明显多年未更新:merging.rst 仍在用 hammer/infernalis/jewel/mimic 举例,teuthology-intro.rst 的平台列表写的是「as of September 2020」,Windows 章节的示例日志里有 ceph_test_dokan.DokanTests。这些内容不等于 Tentacle 的现状,遇到时本册会指出。
  3. 凡官方文档没写的,一律写「文档未明确」;本册作者补充的内容一律以 > 💡 补充: 标注。
  4. 每个章节末尾标注来源:> 引自:[<页面标题>](URL)。

一、入门:开发环境、社区与构建要点

1.1 索引页:本册的目录结构

doc/dev/developer_guide/index.rst 是整个开发者贡献指南的入口,作者署名 Loic Dachary 与 Nathan Cutler,许可为 CC-BY-SA-3.0。它的 toctree 如下(翻译保留原顺序):

英文条目 页面
Introduction intro
Essentials essentials
What is Merged and When merging
Issue tracker issue-tracker
Basic workflow basic-workflow
Tests: Unit Tests tests-unit-tests
Tests: Integration Tests (Teuthology) testing_integration_tests/index
Tests: Running Tests (Locally) running-tests-locally
Tests: Windows tests-windows
Ceph Dashboard Developer Documentation (formerly HACKING.rst) dash-devel
Tracing Developer Documentation jaegertracing
Cephadm Developer Documentation ../cephadm/index(不在本册范围)
Debugging with GDB debugging-gdb

页面顶部还有一条 note:你可能也对 /dev/internals 文档感兴趣。

引自:Contributing to Ceph: A Guide for Developers

1.2 Introduction:这份指南的两个目标

官方开篇写明本指南有两个目标:

  1. 降低软件开发者参与 Ceph 项目的门槛(lower the barrier to entry)。
  2. 作为 Ceph 开发者的参考手册(serve as a reference)。

它假设读者已经熟悉 Ceph 本身(分布式对象存储与文件系统,设计目标是提供出色的 performance、reliability 与 scalability)。如果不熟悉,官方给的三个入口是:

由于读者被假定为有网络连接的开发者,本指南对「在别处已讲过的主题」采取只给链接的处理方式。如果发现链接失效或有更好的链接,官方请你到 http://tracker.ceph.com/projects/ceph/issues/new 报 bug。

引自:Introduction

1.3 Essentials:每个 Ceph 开发者都必须知道的事

1.3.1 Leads(各组件负责人)

Ceph 由 Sage Weil 创建,现在由 Ceph Leadership Team(CLT) 领导。每个主要组件有自己的 lead。完整的 lead 列表及其 GitHub nick 见社区网站:https://ceph.io/en/community/team

列表里出现的 Ceph 专有缩写,官方说在 /architecture 文档里解释(不在本册范围)。

1.3.2 History

官方只给了一个外链:维基百科 Ceph 条目的 History 章节 https://en.wikipedia.org/wiki/Ceph_%28software%29#History

1.3.3 Licensing(许可证)

Ceph 是自由软件。除非另有说明,Ceph 源码按 LGPL2.1 或 LGPL3.0 分发。完整细节见源码树顶层目录的 COPYING 文件: https://github.com/ceph/ceph/blob/master/COPYING

1.3.4 Source code repositories(代码仓库)

代码托管在 GitHub 的 Ceph organization(https://github.com/ceph)下的多个仓库里,但本册只覆盖 https://github.com/ceph/ceph。

官方强调:想做出有意义的贡献,熟练掌握 git 是必需的(A working knowledge of git is essential)。git 文档入口:https://git-scm.com/doc

1.3.5 Redmine issue tracker

代码在 GitHub,但 issues(Bug、Feature、Backport、Documentation 等)在 Redmine:http://tracker.ceph.com

1.3.6 Slack、邮件列表与 IRC

渠道 说明
Slack https://join.slack.com/t/ceph-storage/shared_invite/zt-32hkefbs5-f6qZDZLd5U8CYj7drBTHFw
dev@ceph.io Ceph 本身的开发讨论、与其他技术的互操作性、项目运作。对所有人开放,订阅地址 https://lists.ceph.io/postorius/lists/dev.ceph.io
ceph-devel@vger.kernel.org Linux 内核 Ceph 客户端组件的讨论与 patch review。注意:2018 年中之前,这个列表曾经是通用的 devel-ceph 归档列表,搜索历史归档时留意这一点。订阅走 https://subspace.kernel.org/vger.kernel.org.html
IRC Ceph 社区在 OFTC 网络的 #ceph 频道实时交流。服务器 irc.oftc.net。建议注册账号,注册说明见 https://www.oftc.net/Services/#register-your-account

IRC 频道清单(官方给出):#ceph、#ceph-devel、#cephfs、#ceph-dashboard、#ceph-orchestrators、#sepia。#ceph 的讨论有日志和在线归档。

IRC 的礼仪提醒(官方原话大意):频道里可能有很多人,但不一定都在键盘前;没人回应就多等一会儿。

1.3.7 Submitting patches

官方明确说:提交 patch 的权威说明在源码树顶层目录的 CONTRIBUTING.rst 文件里,并建议所有新人仔细阅读: https://github.com/ceph/ceph/blob/main/CONTRIBUTING.rst

本册(developer_guide)与该文件之间可能有重叠。

1.3.8 Building from source(从源码构建)

官方只给了一个指向:/install/build-ceph(安装手册的构建章节,不在本册范围)。

1.3.9 用 ccache 加速本地构建

ccache(https://ccache.samba.org/)可以加快 Ceph 源码树的重构建。

前提:使用 ccache 之前,先确认源码树是干净的、不会产生构建失败。切分支时可能不知不觉引入旧的构建产物,导致构建失败。

清理源码树(两步):

ninja clean
git clean -fdx; git submodule foreach git clean -fdx

官方警告:上面第二条命令会删除源码树中所有不被 git 跟踪的文件。跑之前务必备份你的日志文件和配置选项。

开启 ccache 构建:

cmake -DWITH_CCACHE=ON ..

ccache 在大多数发行版里都有包。更多关于 run modes 见 https://ccache.samba.org/manual.html#_run_modes ;默认设置用 ccache -s 查看。

官方推荐:覆盖 max_size(默认 10G),用更大的值,比如 25G。配置说明见 https://ccache.samba.org/manual.html#_configuration

进一步提高缓存命中率:在开发环境里把版本信息和构建时间戳固定下来,这样包含这些信息的二进制就不必每次重建。做法是在 ccache.conf 里加:

sloppiness = time_macros
run_second_cpp = true

然后把环境变量 SOURCE_DATE_EPOCH 设成一个固定的 UNIX 时间戳,并在跑 cmake 时把 ENABLE_GIT_VERSION 设为 OFF:

export SOURCE_DATE_EPOCH=946684800
cmake -DWITH_CCACHE=ON -DENABLE_GIT_VERSION=OFF ..

官方警告:用这些选项编出来的二进制不适合生产或调试用途,因为它们不包含正确的构建时间和 git 版本信息。

1.3.10 开发模式集群与 Kubernetes/Rook

  • 开发模式集群:见 /dev/quick_guide(不在本册范围)。
  • Kubernetes/Rook 开发集群:见 kubernetes-dev 引用(不在本册范围)。

1.3.11 Backporting(回移)

所有 bugfix 都应先合到 main 分支,然后再 backport。

给 bugfix 打 backport 标记的方法:

  1. 确保它关联了一个 tracker issue;
  2. 把该 issue 的 Backport 字段设为你认为需要回移的先前版本的逗号分隔列表(例如 hammer,jewel);
  3. 你自己负责你提出的 PR 的 backport 工作(You are responsible for the backporting of pull requests that you raise)。

1.3.12 Dependabot

Dependabot 是 GitHub 的机器人,扫描仓库依赖里的安全漏洞(CVE)。如果发现可用修复,它会自动开 PR 升级依赖,并给出兼容度评分(基于该修复在其他 GitHub 仓库引发的 CI 失败数量)。经过配置后它也可以做非安全更新(升到最新的 minor 或 patch 版本)。

支持的语言和包管理器见 GitHub 官方文档。截至 2022 年 7 月,Ceph 项目只接收来自 pip(基于 requirements.txt)和 npm(package*.json)的告警;可以扩展到 git submodules、Golang、Java;C++ 包管理器(vcpkg、conan、C++20 modules)当时没有支持。

官方提醒:Dependabot 发现的很多依赖,其实在 Ceph GitHub 仓库之外更新更合适(比如发行版软件包)。但Dependabot 生成的新增/现存漏洞清单依然有用。

示例 PR:https://github.com/ceph/ceph/pull/46998

1.3.13 cluster log 使用指引

如果你的 patch 会向 Ceph cluster log 输出消息,官方要求你先读 /dev/logging(不在本册范围)。

引自:Essentials (tl;dr)

1.4 代码库结构:src/ 目录布局

⚠️ 文档未明确:doc/dev/developer_guide/ 下的 18 个页面没有提供 src/ 目录布局表。本节下方表格中的路径,全部是本册 18 个源文里实际出现过、可被确证的路径;不属于「官方给出的目录布局说明」。完整的 src/ 布局在官方文档中未明确(/dev/internals 可能有零散说明,但不在本册源文范围内)。

下表汇总本册源文里明确提到的路径及其内容:

路径 内容 出现于
src/vstart.sh vstart 脚本本体,在本机启动单节点 Ceph 集群。是 Ceph Dashboard 三种最常用开发环境的基础 dash-devel.rst
build/ 构建目录。vstart.sh 通常从这里调用;out/osd.0.pid 在这里 debugging-gdb.rst、dash-devel.rst
build/bin/ 编译产物二进制目录。vstart_runner.py 会在这里检查第一个参数对应的二进制是否存在 running-tests-locally.rst
build/out/ 日志输出目录(vstart 提示 see out/*);单测客户端日志 build/out/client.admin.<pid>.log running-tests-locally.rst、tests-unit-tests.rst
build/vstart_enviroment.sh 用于 source 设置环境变量,以便直接跑 workunit(注意官方文件名拼写是 enviroment,少一个 n) running-tests-locally.rst
build/src/pybind/mgr/dashboard/node-env/ Ceph 构建时创建的 nodeenv 虚拟环境,内含 node/npm,激活脚本在 bin/activate dash-devel.rst
build/src/pybind/mgr/dashboard/cypress/ Cypress 二进制缓存目录(Ceph 改写了默认位置,避免污染 ~/.cache) dash-devel.rst
src/CMakeLists.txt 单元测试在这里声明,使用 add_ceph_test / add_ceph_unittest tests-unit-tests.rst
cmake/modules/AddCephTest.cmake add_ceph_test / add_ceph_unittest 这两个 CMake 函数的定义处 tests-unit-tests.rst
src/test/cli/ CLI 工具的测试文件,扩展名 .t,通过 cram 运行 tests-unit-tests.rst
src/test/run-cli-tests 运行 CLI 测试的 shell 脚本 tests-unit-tests.rst
src/test/dokan/dokan.cc Windows 端 ceph-dokan 测试源码(Windows CI 示例日志引用) tests-windows.rst
src/pybind/ 大多数使用 tox 的 Python 模块所在地 tests-unit-tests.rst
src/pybind/mgr/ Ceph Manager Python API,tox.ini 在 ./src/pybind/mgr/tox.ini tests-unit-tests.rst
src/pybind/mgr/dashboard/ Ceph Dashboard 模块根目录(前端在 frontend/ 子目录),有 tox.ini dash-devel.rst
src/pybind/mgr/dashboard/frontend/ Dashboard 前端(Angular)。package.json、angular.json、proxy.conf.json.sample、cypress/ 都在这 dash-devel.rst
src/pybind/mgr/dashboard/frontend/src/app/shared/components 可在多个页面复用的前端组件声明处 dash-devel.rst
src/pybind/mgr/dashboard/frontend/src/app/core/error/ 通用前端错误处理组件(error.ts、DashboardError 类) dash-devel.rst
src/pybind/mgr/dashboard/frontend/src/testing/ 前端单测工具集,核心是 unit-test-helper.ts dash-devel.rst
src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf npm run i18n:extract 生成的翻译模板(不被 git 跟踪) dash-devel.rst
src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss Dashboard 所有 UI 颜色的变量定义处 dash-devel.rst
src/pybind/mgr/dashboard/frontend/src/styles/vendor/_variables.scss 厂商定制:覆盖 SCSS 变量默认值 dash-devel.rst
src/pybind/mgr/dashboard/frontend/src/styles/vendor/_style-overrides.scss 厂商定制:覆盖或扩展默认 CSS dash-devel.rst
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts 已使用图标清单(Fork Awesome 类名) dash-devel.rst
src/pybind/mgr/dashboard/frontend/cypress/integration/visualTests/ 视觉回归测试,命名约定 <component-name>.vrt-spec.ts dash-devel.rst
src/pybind/mgr/dashboard/frontend/cypress/integration/a11y/ 无障碍(accessibility)测试 dash-devel.rst
src/pybind/mgr/dashboard/frontend/cypress/screenshots/ E2E 失败用例截图输出目录 dash-devel.rst
src/pybind/mgr/dashboard/frontend/.tx/config transifex 配置,每个 Ceph release 一个 resource dash-devel.rst
src/pybind/mgr/dashboard/controllers/ Dashboard 后端控制器目录,OpenAPI 规范由这里动态生成 dash-devel.rst
src/pybind/mgr/dashboard/plugins/ Dashboard 插件目录 dash-devel.rst
src/pybind/mgr/dashboard/tests/ Dashboard 后端单测目录(如 test_controllers.py、test_rgw_client.py) dash-devel.rst、tests-unit-tests.rst
src/pybind/mgr/dashboard/openapi.yaml 版本化的 Ceph RESTful API OpenAPI 规范 dash-devel.rst
src/pybind/mgr/dashboard/settings.py Options 类所在文件,新增配置项在这里加 dash-devel.rst
src/pybind/mgr/dashboard/module.py Dashboard 模块入口,插件需要从这里 import(目前没有动态加载) dash-devel.rst
src/pybind/mgr/dashboard/run-backend-api-tests.sh 启动 vstart 集群并跑 teuthology API 测试 dash-devel.rst
src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh 启动 vstart 集群并跑全量前端 E2E dash-devel.rst
src/pybind/mgr/dashboard/ci/cephadm/run-cephadm-e2e-tests.sh 基于 KCLI 的 cephadm E2E 测试 dash-devel.rst
src/pybind/mgr/dashboard/ci/cephadm/start-cluster.sh cephadm 开发模式集群启动脚本,支持 --dev-mode、--expanded dash-devel.rst
src/pybind/mgr/dashboard/requirements.txt Dashboard 后端 Python 依赖清单 dash-devel.rst
src/pybind/mgr/dashboard/grafana_dashboards.jsonnet Grafana 面板的 jsonnet 源(官方写的路径是 monitoring/grafana/dashboards/jsonnet 目录下的 grafana_dashboards.jsonnet) dash-devel.rst
src/python-common/ Python common 模块,有 tox.ini tests-unit-tests.rst
src/cephadm/ cephadm,有 tox.ini tests-unit-tests.rst
src/ceph-volume/ ceph-volume,有多个 tox.ini(根目录、plugin/zfs、3 个 functional 测试目录) tests-unit-tests.rst
src/tools/cephfs/ CephFS 工具,有 tox.ini tests-unit-tests.rst
src/script/ 各类脚本:run_tox.sh、ptl-tool.py、ceph-debug-docker.sh 多个文件
src/script/ptl-tool.py Patrick Donnelly 的 ptl-tool,用于生成 merge commit basic-workflow.rst
src/script/ceph-debug-docker.sh 用于分析 Teuthology 失败的调试脚本 debugging-gdb.rst
qa/ 集成测试与 workunit 所在目录(qa/suites、qa/tasks、qa/workunits) 多个文件
qa/suites/ 集成测试的 yaml 定义(suite) teuthology-intro.rst
qa/tasks/ 集成测试的 Python 实现(task) teuthology-intro.rst
qa/workunits/ 可直接在 vstart 集群上跑的 shell 测试(如 mon/osd.sh) running-tests-locally.rst
qa/tasks/vstart_runner.py 在本机 vstart 集群上跑 qa/tasks/ 下 Python 测试的驱动器 running-tests-locally.rst
qa/tasks/mgr/dashboard/ Dashboard API 测试模块目录 dash-devel.rst
qa/suites/rados/mgr/tasks/dashboard.yaml 新增 Dashboard API 测试模块后,必须把模块路径加到这个文件的 modules 段 dash-devel.rst
qa/distros/supported/ teuthology 支持的发行版列表(截至 2020 年 9 月:RHEL/CentOS 8、Ubuntu 18.04) teuthology-intro.rst
.githubmap(仓库根) GitHub 用户名 → 邮箱映射,用于查 reviewer 邮箱 basic-workflow.rst
win32_build.sh / win32_deps_build.sh Windows 构建脚本与依赖构建脚本 tests-windows.rst

💡 补充:如果你需要一份完整的 src/ 子目录(如 src/osd/、src/mon/、src/mds/、src/common/、src/msg/、src/crimson/)逐条说明的布局表,官方 developer_guide 里没有;这类内容属于 doc/dev/internals.rst 与 doc/dev/osd_internals/ 的范畴,可参考本手册第 16、17、18 册。

引自:Essentials (tl;dr)

1.5 构建要点小结(本册范围内的)

要点 做法
从源码构建 参见 /install/build-ceph,本册未展开
加速重建 cmake -DWITH_CCACHE=ON ..,建议 max_size 调到 25G
最大化缓存命中 ccache.conf 加 sloppiness = time_macros + run_second_cpp = true;export SOURCE_DATE_EPOCH=946684800;cmake -DENABLE_GIT_VERSION=OFF
构建失败先清树 ninja clean,再 git clean -fdx; git submodule foreach git clean -fdx(会删掉未跟踪文件,先备份)
带 Jaeger 构建 WITH_JAEGER=true ./install-deps.sh 然后 cmake -DWITH_JAEGER=ON ..(见第十一章)
跑单测 顶层目录 ./run-make-check.sh(见第五章)

二、基本工作流(fork → 分支 → 提交 → PR → 评审 → merge)

这一章来自 basic-workflow.rst(23KB,本册第二大的页面)。

2.1 官方给出的工作流图

官方用 ditaa 画了一张图,文字化如下:

            Upstream Code                       Your Local Environment

           /----------\        git clone           /-------------\
           |   Ceph   | -------------------------> | ceph/main   |
           \----------/                            \-------------/
                ^                                    |
                |                                    | git branch fix_1
                | git merge                          |
                |                                    v
           /----------------\  git commit --amend   /-------------\
           | ninja check    |---------------------> | ceph/fix_1  |
           | ceph--qa--suite|                       \-------------/
           \----------------/                        |
                ^                                    | fix changes
                |                                    | test changes
                | review                             | git commit
                |                                    |
                |                                    v
           /--------------\                        /-------------\
           |   github     |<---------------------- | ceph/fix_1  |
           | pull request |         git push       \-------------/
           \--------------/

官方还推荐了一个视频:Getting Started with Ceph Development(https://www.youtube.com/watch?v=t5UIehZ1oLs,1 小时 15 分),说它是本工作流的实操总结。

2.2 第 0 步:更新 tracker

先找到你打算修的 bug 的 Redmine issue 号;如果没有,就创建一个。

唯一不需要建 tracker issue 的情况:minor documentation changes(文档的小改动)。

文档改动类型 是否需要 tracker issue
简单的文档清理(simple documentation cleanup) 不需要
新增文档章节或文件 需要(属 major)
对文档结构或内容做实质性改动 需要(属 major)

写 ticket 标题时的自检问题(官方原话):「如果两年后我要搜这个 ticket,我最可能搜哪些关键词?」——然后把这些关键词放进标题。描述里要有详细的信息。

如果权限够,把 Assignee 设成自己;权限不够就在评论里写一句「I am working on this issue」。

2.3 Ceph 工作流总览:三个仓库

# 仓库 说明
1 ceph/ceph upstream repository(上游仓库)
2 your_github_id/ceph 你的 fork
3 本地工作副本 你工作站上的 clone

完整步骤:

  1. 配置本地环境
    1. 创建 fork
    2. clone fork 到本地
  2. 修 bug
    1. 同步本地 main 与 upstream main
    2. 在本地工作副本创建 bugfix 分支
    3. 改代码
    4. push 到你的 fork
  3. 开 PR:请求把改动合入 upstream Ceph

2.4 准备本地工作副本(仅首次需要)

2.4.1 创建 fork

GitHub 文档:https://help.github.com/articles/fork-a-repo/#platform-linux

简言之:如果你的 GitHub 用户名是 mygithubaccount,你的 fork 就在 https://github.com/mygithubaccount/ceph。

2.4.2 Clone 你的 fork

git clone https://github.com/mygithubaccount/ceph

必须先 fork 再 clone。 不 fork 就没法开 GitHub pull request。

2.4.3 配置本地环境(生成 Signed-off-by 的必要步骤)

这一节的命令是为了让本地 git 生成 Signed-off-by: 标签,并让本地环境能与 upstream 同步。只在初次搭建时跑一次,但不跑就没法正常工作。

1. 配置用户名和邮箱(必须在 clone 出来的 ceph/ 目录里跑):

git config user.name "FIRST_NAME LAST_NAME"
git config user.email "MY_NAME@example.com"

2. 把 upstream 仓库加为 remote 并 fetch:

git remote add ceph https://github.com/ceph/ceph.git
git fetch ceph

这两条会把 ceph/ceph.git 的所有分支和提交拉到本地 git 仓库,存为 remotes/ceph/$BRANCH_NAME,在本地命令里可以引用为 ceph/$BRANCH_NAME。

2.5 修 bug

2.5.1 同步本地 main 与 upstream main

本地工作副本里 remotes/origin/main 是 main 分支的一个副本,叫「local main」。它在你 clone 的那一刻就「冻结」了,而 upstream 还在被别人持续更新,fork 会越漂越远。所以要经常同步:

git fetch ceph
git checkout main
git reset --hard ceph/main
git push -u origin main

如果 git status 出现「Untracked files」这一行,官方让你去看 the procedure on updating submodules(更新子模块的步骤,本册源文未展开,文档未明确)。

2.5.2 创建 bugfix 分支

git checkout main
git checkout -b fix_1
git push -u origin fix_1

三条命令的含义:

命令 作用
git checkout main 确保 bugfix 分支 fix_1 是从 upstream main 的最新状态创建的
git checkout -b fix_1 在本地工作副本创建名为 fix_1 的 bugfix 分支,修 bug 的改动都提交到这个分支
git push -u origin fix_1 把本地的 bugfix 分支推到你的 fork

2.5.3 在本地改代码

  1. 更新 tracker:把 issue 状态改成 In progress,告诉别人你在做了,避免重复劳动。没权限就评论一句。
  2. 修 bug 本身:官方坦白说「本指南没法告诉你怎么修你选中的 bug」。多数情况下要经历多轮测试,过程是迭代的,需要 trial、error、skill 和 patience。可用的验证工具见第五章、第六章。

2.5.4 提交并推送到你的 fork

提交(注意 -as,其中 s 就是 --signoff):

git commit -as

推送:

git push origin fix_1

关于 origin 的说明:

$ git remote -v
ceph	https://github.com/ceph/ceph.git (fetch)
ceph	https://github.com/ceph/ceph.git (push)
origin	git@github.com:username/ceph.git (fetch)
origin	git@github.com:username/ceph.git (push)

上面输出里 origin 那两行说明 origin 是你的 fork。origin 不一定是你的 fork 的名字——用 git remote -v 确认。

2.6 开 GitHub Pull Request

push 之后开 PR,让你的改动被 Ceph 贡献者社区看到、评审、可能追加测试、可能要求你改。

官方建议读:

资源 链接
GitHub PR 教程 https://help.github.com/articles/using-pull-requests/
Git Commit Good Practice(OpenStack Wiki) https://wiki.openstack.org/wiki/GitCommitMessages
Ceph 自己的 Submitting Patches https://github.com/ceph/ceph/blob/main/SubmittingPatches.rst

开完 PR 后,去 tracker issue 里加一条评论指向你的 PR,可以简单到只有一行:

*PR*: https://github.com/ceph/ceph/pull/$NUMBER_OF_YOUR_PULL_REQUEST

2.7 Commit message 规范(本册源文能确证的部分)

⚠️ 关于 Fixes: 等更细的 trailer 规范:文档未明确。本册源文(basic-workflow + essentials)只明确要求了下面表格里的这些;更完整的规范官方指向仓库里的 CONTRIBUTING.rst 与 SubmittingPatches.rst,本册未展开。

规范项 官方要求 出处
Signed-off-by: 必须有。通过 git commit -as(-s = --signoff)自动追加。这也是 CI 的一个检查项(a test to check that the commits are properly signed) basic-workflow.rst
提交前配置 git config user.name / git config user.email,否则生成不出 Signed-off-by basic-workflow.rst
前置 tracker issue 除 minor 文档改动外,先有 Redmine issue;PR 开出来后在 issue 里留 *PR*: <url> 评论 basic-workflow.rst
Backport 标记 在 tracker issue 的 Backport 字段填逗号分隔的先前版本列表(如 hammer,jewel),提交者自己负责 backport essentials.rst
Backport 前提 所有 bugfix 先合到 main,再 backport essentials.rst
Merge commit 标题 doc/component: title of the commit basic-workflow.rst
Merge commit 身体 Reviewed-by: Reviewer Name <rname@example.com>(邮箱用尖括号包起来) basic-workflow.rst
PR 只含一个 commit 最佳实践:一个 PR 只包含一个 commit,便于保持干净的 history、简化评审、便于 merge 与 revert basic-workflow.rst
Git commit 好实践 见 OpenStack Wiki 的 GitCommitMessages basic-workflow.rst

最基本的 merge commit 形式(官方原文):

doc/component: title of the commit

Reviewed-by: Reviewer Name <rname@example.com>

两部分:被 merge 的 commit 的标题 + 评审者的姓名和邮箱。

💡 补充:Ceph 社区实际使用的 Fixes: https://tracker.ceph.com/issues/NNNNN trailer 在 SubmittingPatches.rst 里有规定,但该文件不在本册源文范围内,因此本册不声称它是本册所讲内容。

2.8 Merge commit 的三条生成路径

2.8.1 用浏览器扩展自动填充

官方推荐 Ceph GitHub Helper Extension(https://github.com/tspmelo/ceph-github-helper):

启用后,打开 GitHub PR 页面,右上角会出现一条竖直的 helper;点用户剪影按钮,merge message 输入框会被自动填充。

2.8.2 查 reviewer 邮箱的两种方法

方法 做法
.githubmap 文件 仓库里 /ceph/.githubmap,查 GitHub 用户名对应的邮箱
git log 在待评审分支上跑 git log,评审者之前大概率提交过东西,日志里会有其邮箱

2.8.3 用 ptl-tool 生成

Patrick Donnelly 的 ptl-tool 位于 /ceph/src/script/ptl-tool.py。它生成的 merge commit 形如:

Merge PR #36257 into main
* refs/pull/36257/head:
        client: move client_lock to _unmount()
        client: add timer_lock support
Reviewed-by: Patrick Donnelly <pdonnell@redhat.com>

2.9 理解自动化的 PR 校验(CI)

创建或更新 PR 时,Ceph 的 CI 基础设施会自动测试。官方列出的自动化检查:

  1. 检查 commit 是否正确签名(signed)
  2. 检查文档能否构建(that the documentation builds)
  3. 检查 submodules 未被修改(that the submodules are unmodified)
  4. 检查 API 是否正常(that the API is in order)
  5. make check 测试

此外,根据 PR 修改了哪些文件,可能还会跑更多测试。

make check 会构建 PR 并跑一整套测试,跑在 Ceph CI 团队运维的服务器上,结果直接显示在 GitHub 的 PR 页面里。

官方明确要求:开 PR 之前先自己测。

make check 的注意事项

  • GitHub 的 make check 由 Jenkins 实例驱动。
  • Jenkins 会先把你的 PR 分支 merge 进 base 分支的最新版本再开始测试——所以你不需要为了拿到最新修复而 rebase。
  • 你可以随时通过在 PR 里发评论触发测试,评论里要包含字符串 test this please。由于订阅了 PR 的人可能误以为你在请求他/她去测,官方要求直接称呼 Jenkins,例如写 jenkins retest this please。
  • 只跑某一个测试可以用类似 jenkins test signed 的命令。这些命令列表会自动附加在每个新 PR 描述的末尾,去那里查你需要的那一条。
  • 出构建失败又不确定原因,就去看 make check 日志:点 PR 里 make check 旁边的 details 进 Jenkins web GUI,然后点左侧的 Console Output。
  • Jenkins 配了「已知与历史 make check 失败相关的字符串」的搜索,但不保证这些字符串与你的失败有关,还是得自己读日志。

2.10 集成测试(ceph-qa-suite)

有时候需要在物理或虚拟硬件上的真实 Ceph 集群上测试你的修复。这类测试在 ceph/qa 子目录(https://github.com/ceph/ceph/tree/main/qa/)里,通过 teuthology 框架(https://github.com/ceph/teuthology)运行。

Ceph 社区有 Sepia lab(https://wiki.sepia.ceph.com/doku.php)可以在物理硬件上跑集成测试。申请访问权限从 https://wiki.sepia.ceph.com/doku.php?id=vpnaccess 开始。

关于 needs-qa 与 needs-upgrade-testing 标签:

标签 含义
needs-qa 其他贡献者可能给你的 PR 加这个标签。这样可以把多个 PR 合到一条分支上一起测。teuthology suite 可能要跑几小时甚至几天,批量跑能减少资源争用、节省时间
needs-upgrade-testing 如果你的改动会影响升级,自己加上这个标签,表示应该安排一个升级测试 suite

2.11 Code review

bugfix 经过充分测试后(有时测试期间就会开始),会接受其他开发者的代码评审。形式通常是 PR 里的评论,也可能辅以 IRC、Slack 或邮件列表上的讨论。

2.12 修改(amend)你的 PR

测试与评审期间,你随时可以修改本地分支里的文件。改动在本地(fix_1 分支)提交后,必须 push 到 GitHub 才会出现在 PR 里。

修改 PR 的做法:往 fix_1 分支加 commit,通常再 rebase 来整理 git history(rebase 入门见 https://www.atlassian.com/git/tutorials/rewriting-history)。改完之后需要 force push:

git push --force origin fix_1

为什么要这么麻烦,而不是直接往 PR 里加 commit? 官方给了三条理由:

  1. 一个 PR 只含一个 commit 是最佳实践,能保持干净的 history;
  2. 简化 peer review;
  3. 让 merge 更容易;万一(in the unlikely event)PR 需要被 revert,只有一个 commit 的 PR 回退起来也更容易。

2.13 Merging

流程在项目 lead 合并你的 PR 时结束。

此时你(或合并 PR 的 lead)应该把 tracker 状态改成 Resolved。如果 issue 被标记了 backport,状态应改成 Pending Backport。

2.14 杂项 git 技巧

--set-upstream

忘记加 --set-upstream origin x 会看到:

fatal: The current branch {x} has no upstream branch.
To push the current branch and set the remote as upstream, use
   git push --set-upstream origin {x}

想让 git 自动创建对应的 upstream 分支(不用每次都加参数),在 ceph/ 目录里跑一次:

git config --global push.autoSetupRemote true

删除本地分支

git branch -d localBranchName

删除远程分支

git push origin --delete remoteBranchName

(这里的「远程」指你的 fork。)

纵向搜索某个文件里的某个字符串是被哪次 commit 引入的

git log -S 'foo' file.rst

-S <string> 会找出引入/移除该字符串的 commit。

引自:Basic Workflow


三、Issue Tracker 与任务管理

Ceph 开发者用 issue tracker 做两件事:

  1. 跟踪 issue——bug、fix request、feature request、backport request 等;
  2. 与其他开发者沟通,让大家知道 issue 的进展。

3.1 开始工作时的约定

当你开始处理某个已有 issue 时,最好让别人知道,避免重复劳动。典型做法:

  1. 把 Assignee 字段改成你自己;
  2. 把 Status 改成 In progress。

注意:Ceph 社区的新人通常没有足够权限修改这些字段。这时只需在 issue 里加一条简短说明即可。

3.2 常用状态的含义

Status Meaning
New 初始状态
In Progress 有人在做了
Need Review 修复的 pull request 已开
Pending Backport 修复已 merge,backport 待办
Resolved 修复以及(如果有)backport 都已 merge

引自:Issue Tracker


四、代码合并流程(merging.rst)

本章回答三个问题:什么(What)、合到哪(Where)、什么时候(When),以及分支之间的 merge 关系。

⚠️ 本章的举例版本(hammer / infernalis / jewel / luminous / mimic / nautilus / pacific / octopus)是文档原有内容,用于说明规则,不代表 Tentacle 当前正在用的分支名。

4.1 开发发布阶段(Development releases,即 x.0.z)

What(合什么):Features(新特性)+ Bug fixes(缺陷修复)。

Where(合到哪):

  • Features 合到 main 分支。
  • Bug fixes 应合到对应的 named branch,例如 14.0.z 对应 nautilus,16.0.z 对应 pacific。
  • 但这不是强制的——bug fix 和文档改进也可以合到 main,因为在开发发布阶段 main 本身也会偶尔被 merge 到 named branch。
  • 无论哪种情况,如果某个 bug fix 很重要,它也可以被标记 backport 到一个或多个先前的 stable release。

When(什么时候):每次 stable release 之后,先前 release 的候选分支进入 phase 2。举例:jewel named branch 是在 infernalis release candidates 进入 phase 2 时创建的。从那时起,main 就不再与 infernalis 关联。下一个 stable release 的 named branch 创建后,main 会偶尔被 merge 进去。

Branch merges(分支合并规则):

  • 最新的 stable release 分支被周期性地 merge 进 main。
  • main 分支被周期性地 merge 进 stable release 的分支。
  • 每次开发版(x.0.z)发布后,main 会立即 merge 进 stable release 分支。

4.2 Stable release candidates(x.1.z)阶段 1

What:只有 bug fixes。

Where:stable release 分支(如 10.0.z 对应 jewel,12.0.z 对应 luminous)或 main。

在此阶段,所有合到 main 的 commit 都会被 merge 到 named branch,反之亦然。换句话说,合到 named branch 还是 main 没有区别——反正都会进下一个 release candidate。

When:第一个 stable release candidate 发布之后,即在 release 分支上打了 x.1.0 tag 之后。

Branch merges:

  • stable release 分支被周期性地 merge 进 main;
  • main 被周期性地 merge 进 stable release 分支;
  • 每次 x.1.z release candidate 之后,main 立即 merge 进 stable release 分支。

4.3 Stable release candidates(x.1.z)阶段 2

What:只有 bug fixes。

Where:stable release 分支(如 13.0.z 对应 mimic,15.0.z 对应 octopus)。

在此阶段,所有合到 named branch 的 commit 都会被 merge 进 main。而往 named branch 的 cherry-pick 是手工做的,因为正式的 backport 流程要等 release 被宣布为 "stable" 之后才开始。

When:CLT 宣布进入 phase 2 之后。

Branch merges:stable release 分支偶尔 merge 进 main。

4.4 Stable releases(x.2.z)

What:

Where:stable release 分支(0.94.x 对应 hammer,9.2.x 对应 infernalis 等)。

When:stable release 发布之后,即在 release 分支上打了 "vx.2.0" tag 之后。

Branch merges:Never(从不)。

4.5 四个阶段对照表

阶段 版本形态 合什么 合到哪 何时开始 分支 merge 关系
Development releases x.0.z Features + Bug fixes Features → main;Bug fixes → named branch(也可 main) 每次 stable release 之后,上一批候选分支进入 phase 2 stable ↔ main 周期性互 merge;x.0.z 发布后 main 立即合入 stable
Stable RC phase 1 x.1.z 仅 Bug fixes stable release 分支 或 main(等价) 打了 x.1.0 tag 之后 stable ↔ main 周期性互 merge;每个 RC 后 main 立即合入 stable
Stable RC phase 2 x.1.z 仅 Bug fixes stable release 分支 CLT 宣布进入 phase 2 stable 偶尔 merge 进 main;反向靠手工 cherry-pick
Stable releases x.2.z Bug fixes;Features 有时接受;尽量 cherry-pick 自 main stable release 分支 打了 vx.2.0 tag 之后 Never

💡 补充:把四个阶段串起来的直觉是——越接近发布,收得越紧:从「什么都能合」到「只收 bugfix」到「只收 bugfix 且主方向只剩单向」,最后 stable 分支完全不再与其他分支互相 merge,只靠 backport(cherry-pick)单点进补丁。

引自:Commit merging: scope and cadence


五、本地运行测试(单元测试与 workunit)

5.1 Ceph 的两类测试

Ceph GitHub 仓库有两类测试:

类型 也称 特征
Unit tests make check tests 从源码编译完 Ceph 后,能在单台构建机器上轻松跑起来的测试
Integration tests teuthology tests 需要安装软件包、需要多机集群才能跑的测试

官方特意说明:严格来说 make check 测试并不是「单元测试」,只是因为它们能在单机构建环境下跑,才被归为一类。

5.2 「make check」是什么意思

编译完 Ceph 后,代码可以跑一整套测试。由于历史原因,这套测试常被称为 make check,尽管实际用来跑测试的命令现在是 ctest。

一个测试要被纳入这一类,必须满足:

  • 绑定的端口不与其他测试冲突;
  • 不需要 root 权限;
  • 不需要多于一机器就能跑;
  • 在几分钟内完成。

为简化表述,这类测试被称为 "make check tests" 或 "unit tests",用来和更复杂的、通过 teuthology 框架运行的 "integration tests" 区分开。

虽然可以直接跑 ctest,但正确设置环境比较麻烦。官方提供了一个脚本,从 Ceph 源码树顶层目录调用即可:

./run-make-check.sh

资源要求(官方给出):x86_64 架构上至少需要 8GB RAM 和 32GB 可用磁盘空间;其他架构要求可能不同。根据硬件不同,耗时从 20 分钟到 3 小时不等。

5.3 单元测试是怎么声明的

单元测试在 ./src 目录的 CMakeLists.txt 里声明,使用两个 CMake 函数:

函数 用途
add_ceph_test 声明脚本形式的单元测试
add_ceph_unittest 声明二进制形式的单元测试(编译期生成的可执行文件)

这两个函数本身定义在 ./cmake/modules/AddCephTest.cmake。

5.4 CLI 工具的单元测试

部分 CLI 工具用扩展名为 .t 的特殊文件测试,存放在 ./src/test/cli 下。这些测试通过 cram(https://bitheap.org/cram/)运行,驱动脚本是 ./src/test/run-cli-tests。

不适合放进 make check 的 cram 测试,也可以由 teuthology 通过 cram task(https://github.com/ceph/ceph/blob/master/qa/tasks/cram.py)来跑。

5.5 基于 Tox 的 Python 模块测试

Ceph 的部分 Python 模块使用 tox(https://tox.readthedocs.io/en/latest/)跑单元测试,这些模块大多在 ./src/pybind/ 下。

官方列出的(截至 2020 年 12 月)使用 tox 的模块清单:

模块 tox.ini 位置
Cephadm ./src/cephadm/tox.ini
Ceph Manager Python API ./src/pybind/mgr/tox.ini
Dashboard ./src/pybind/mgr/dashboard/tox.ini
(mgr 下另有) ./src/pybind/tox.ini
Python common ./src/python-common/tox.ini
CephFS ./src/tools/cephfs/tox.ini
ceph-volume ./src/ceph-volume/tox.ini
ceph-volume zfs plugin ./src/ceph-volume/plugin/zfs/tox.ini
ceph-volume batch functional ./src/ceph-volume/ceph_volume/tests/functional/batch/tox.ini
ceph-volume simple functional ./src/ceph-volume/ceph_volume/tests/functional/simple/tox.ini
ceph-volume lvm functional ./src/ceph-volume/ceph_volume/tests/functional/lvm/tox.ini

5.5.1 查看 tox 的环境与任务列表

大多数 tox 配置支持多个 environment 和 task,清单在 tox.ini 的 envlist 里。例如 ./src/cephadm/tox.ini 的前三行:

[tox]
envlist = py3, mypy
skipsdist=true

这个例子指定了 Python 3 和 mypy 两个环境。

列出环境:

tox --list

或

tox -l

5.5.2 运行 tox

在包含 tox.ini 的目录里直接执行 tox 即可。如果不指定环境(例如 -e $env1,$env2),tox 会跑所有环境。Jenkins 通过执行 ./src/script/run_tox.sh 来跑 tox。

Ceph Dashboard 的示例:

## Run Python 2+3 tests+lint commands:
tox -e py27,py3,lint,check

## Run Python 3 tests+lint commands:
tox -e py3,lint,check

## To run it as Jenkins would:
../../../script/run_tox.sh --tox-env py3,lint,check

5.5.3 Manager core 单元测试

目前只跑 mgr_util.py 里的 doctests(https://docs.python.org/3/library/doctest.html)。

要往 manager core 里加更多被测文件,打开 tox.ini,把文件加到包含 mgr_util.py 的那一行末尾。

5.6 单元测试的两个注意事项(caveats)

  1. 内存分配器:与各个 Ceph 守护进程和 ceph-fuse 不同,单元测试默认链接到系统默认的内存分配器(glibc),除非显式链接到别的。这样做的目的是让 valgrind 这类工具能在测试中使用。
  2. Google Test 会隐藏客户端输出。要调试客户端,先设好 debug level(例如 ceph config set client debug_rbd 20),然后去 build/out/client.admin.<pid>.log 找日志文件。这在查看 teuthology 里失败的单元测试 job 时也很有用——可以在相关的 yaml 文件里设置 job 的 debug level。

引自:Testing - unit tests

5.7 在本地跑 s3-tests(RGW)

RGW 代码可以这样测:本地从源码构建 Ceph → 起一个 vstart 集群 → 跑 "s3-tests" suite。以下步骤在 jewel 及以上应该可用。

Step 1 - 构建 Ceph:参见 /install/build-ceph。(Step 2 可以在构建期间并行做。)

Step 2 - vstart。构建完成后,仍在 git clone 的顶层目录里:

cd build/
RGW=1 ../src/vstart.sh -n

会输出很多内容,最后应看到类似:

started.  stop.sh to stop.  see out/* (e.g. 'tail -f out/????') for debug output.

表示集群已跑起来。

Step 3 - 跑 s3-tests:

../qa/workunits/rgw/run-s3tests.sh

5.8 用 vstart_runner.py 跑测试(CephFS / Mgr)

CephFS 和 Ceph Manager 的代码用 vstart_runner.py 测试。

5.8.1 安装 teuthology(前置)

Ceph 仓库里的 Python 测试要在本机执行,需要装 teuthology:

git clone https://github.com/ceph/teuthology
cd teuthology
./bootstrap install

这会在 teuthology 仓库根目录创建一个名为 virtualenv 的虚拟环境并安装 teuthology。

也可以用 pip 装到自定义虚拟环境(同时用 git clone 一份 teuthology 仓库):

virtualenv --python=python3 venv
source venv/bin/activate
pip install 'setuptools >= 12'
pip install teuthology[test]@git+https://github.com/ceph/teuthology
deactivate

如果上面两种方式都不行(比如 bootstrap 脚本有 bug 或暂时下载不到),可以从本机已有的 teuthology 仓库副本手动安装:

cd teuthology
virtualenv -p python3 venv
source venv/bin/activate
pip install -r requirements.txt
pip install .
deactivate

5.8.2 跑你的第一个测试

先成功构建 Ceph,然后:

cd build
../src/vstart.sh -n -d -l
source ~/path/to/teuthology/venv/bin/activate

跑一个具体的测试(例如 qa/tasks/cephfs/test_client_recovery 里 TestClientRecovery 类的 test_reconnect_timeout):

python ../qa/tasks/vstart_runner.py tasks.cephfs.test_client_recovery.TestClientRecovery.test_reconnect_timeout

也可以按组跑:

$ # run all tests in class TestClientRecovery
$ python ../qa/tasks/vstart_runner.py tasks.cephfs.test_client_recovery.TestClientRecovery
$ # run  all tests in test_client_recovery.py
$ python ../qa/tasks/vstart_runner.py tasks.cephfs.test_client_recovery

vstart_runner.py 根据传入的参数收集测试并像跑单个测试一样执行它们。

5.8.3 vstart_runner.py 的命令行选项

选项 说明
--clear-old-log 跑测试前删除旧日志文件
--create 跑测试前创建 Ceph 集群
--create-cluster-only 只创建集群然后退出,测试可以稍后再发
--interactive 测试失败时进入 Python shell
--log-ps-output 记录 ps 输出,调试时有用
--teardown 测试跑完后拆掉 Ceph 集群
--kclient 用内核 cephfs 客户端而不是 FUSE
--brxnet=<net/mask> 为 mount 客户端的网络命名空间容器指定新的 net/mask(默认 192.168.0.0/16)

三条官方 note:

  • 用 FUSE 客户端时,确保系统装了并启用了 fuse 包,且 /etc/fuse.conf 里加了 user_allow_other。
  • 用内核客户端时,用户必须能免密 sudo。
  • 内核客户端上的失败可能导致宿主机崩溃,建议在虚拟机里用这个功能。

5.8.4 vstart_runner.py 的内部工作机制

它主要做三件事:

  1. 收集并运行测试:setup/teardown 集群,收集并运行测试。由 scan_tests()、load_tests()、exec_test() 三个方法实现。所有命令行选项以及「记录日志」「把 traceback 复制到日志底部」等特性都在这里实现。
  2. 提供下发与测试 shell 命令的接口:测试是假设集群在远程机器上写的。vstart_runner.py 提供接口,让同样的测试能跑在本机的集群上。这靠 class LocalRemote 实现;LocalRemoteProcess 管理执行 LocalRemote 命令的进程,LocalDaemon 提供操作 Ceph 守护进程的接口,LocalFuseMount 创建并管理 FUSE mount。
  3. 提供操作 Ceph 集群的接口:LocalCephManager 提供带/不带 admin socket 执行 Ceph 集群命令的方法;LocalCephCluster 提供设置或清除 ceph.conf 的方法。

三条实现细节 note:

  • vstart_runner.py 无条件从命令参数里删除 adjust-ulimits 和 ceph-coverage,因为它们在开发者本机上不适用。
  • 对于 passwd 和 chown 命令,omit_sudo 会被无条件重置为 False。
  • 会检查 <ceph-repo-root>/build/bin/ 下是否存在与第一个参数同名的二进制文件;如果存在,第一个参数会被替换为该二进制文件的路径。

5.9 用 vstart_enviroment.sh 跑 workunit

和 s3-tests 类似,其他 workunit 也可以配置好环境后直接对着 vstart 集群跑。

设置环境:

. ./build/vstart_enviroment.sh

⚠️ 注意官方文件名是 vstart_enviroment.sh(enviroment,不是 environment),这是官方文档里就有的拼写。

跑一个 workunit(例如 mon/osd.sh):

./qa/workunits/mon/osd.sh

引自:Running Unit Tests

5.10 三类本地测试对比

测试方式 适用代码 前置条件 命令
make check / ctest 通用 C++ 单元测试 编译完 Ceph;8GB RAM / 32GB 磁盘 ./run-make-check.sh(或手工 ctest)
cram(.t 文件) CLI 工具 ./src/test/cli 下的 .t 文件 ./src/test/run-cli-tests
tox Python 模块(cephadm / mgr / dashboard / python-common / cephfs / ceph-volume) 装 tox、coverage tox -e py3,lint,check 或 ../../../script/run_tox.sh --tox-env py3,lint,check
s3-tests RGW 编译 + RGW=1 ../src/vstart.sh -n ../qa/workunits/rgw/run-s3tests.sh
vstart_runner.py CephFS、Mgr Python 测试 装 teuthology + ../src/vstart.sh -n -d -l python ../qa/tasks/vstart_runner.py tasks.cephfs.<module>.<Class>.<test>
workunit 通用 shell 测试 . ./build/vstart_enviroment.sh ./qa/workunits/<path>.sh

六、集成测试与 teuthology

testing_integration_tests/ 目录下共 6 个文件:index.rst、tests-integration-testing-teuthology-intro.rst(约 27KB,本册第三大)、-workflow.rst(18KB)、-debugging-tips.rst、-kernel.rst、tests-sentry-developers-guide.rst。

6.1 什么是集成测试

Ceph 有两类测试:make check 测试和集成测试。当一个测试需要多台机器、需要 root 权限、或者持续时间很长(例如要模拟真实的 Ceph 工作负载),它就被认定为集成测试。

集成测试被组织成 suites,定义在 ceph/qa 子目录(https://github.com/ceph/ceph/tree/master/qa),用 teuthology-suite 命令运行。该命令是 teuthology 框架(https://github.com/ceph/teuthology)的一部分。

6.1.1 关键认知:Teuthology 消费的是软件包

官方特别强调这一点「需要点时间才能理解其意义,但它极其重要」:这意味着自动化测试可以在多个平台上使用与任意运行这些平台的机器所能安装的相同软件包(RPM、DEB)来进行。

teuthology 有一份支持的平台清单(https://github.com/ceph/ceph/tree/master/qa/distros/supported),截至 2020 年 9 月,清单是「RHEL/CentOS 8」和「Ubuntu 18.04」。它期望拿到这些平台的预构建 Ceph 软件包,然后按测试要求把这些平台部署到机器上(裸金属或云供给的),装包,部署 Ceph 集群。

6.1.2 The Nightlies

大量集成测试会定期在 Sepia lab(https://wiki.sepia.ceph.com/doku.php)里针对 Ceph 官方仓库(master 开发分支和 stable 分支)跑。传统上叫 "the nightlies"(夜间测试),因为 Ceph 核心开发者过去住在同一时区,从他们的视角看测试是在 overnight 跑的。

结果发布在 http://pulpito.ceph.com/ 的 teuthology 用户下。开发者 nick 出现在测试结果 URL 里以及 Pulpito dashboard 的第一列。结果也会报到 ceph-qa 邮件列表(https://ceph.com/irc/)。

6.2 测试优先级(Testing Priority):务必设置 -p <N> 且 N < 500

teuthology-suite 有 -p <N> 选项,指定提交到队列的 job 优先级。N 越小,优先级越高。

N 的默认值是 1000。 由 teuthology@teuthology.front.sepia.ceph.com 自动调度的大多数 nightly 测试都以 N >= 500 跑。一些关键的 nightly 测试会给更高优先级,比如 smoke 测试或即将发布的重大版本的 QA 跑。

官方 note:nightly 期间要跑的测试量经常大到在 allotted 时间内跑不完全部 nightly 测试。

如果不把 N 设到 500 以下,你的测试优先级不会高于 nightly 测试——这意味着它们可能永远跑不起来。

优先级取值指南(官方表格):

优先级 适用场景
N < 10 天塌下来了,某组测试必须立刻跑
10 <= N < 50 你的测试很紧急,正阻塞其他重要开发
50 <= N < 75 你在测某个 feature/fix,且跑的 job 少于约 25 个;也用于紧急的 release 测试
75 <= N < 100 Tech Leads 常规用来针对 master 验证 PR 的优先级
100 <= N < 150 点发布(point release)的 QE 验证
150 <= N < 200 测某个 feature/fix,100 个 job 或更少;约 24 小时出结果
200 <= N < 500 大规模测试跑;约一周出结果

两个配套选项:

选项 用途
--dry-run 先看这条命令会触发多少 job。数量满意后,去掉 --dry-run 并加上 -p <合适的数字> 再跑一次
--force-priority 跳过优先级检查。官方要求:体谅其他开发者也要跑测试,只在紧急情况下用

6.3 Suites Inventory(suite 清单)

ceph/qa 子目录的 suites 目录包含所有 Ceph 组件的集成测试。

Suite 功能
ceph-deploy 用 ceph-deploy 安装 Ceph 集群
dummy 拿一台机器,什么都不做,返回成功。常用来验证集成测试基础设施本身是否正常
fs 测试用 kernel 和 FUSE 客户端挂载的 CephFS,也测多 MDS
krbd 测试 RBD 内核模块
powercycle 验证机器断电再上电时 Ceph 集群的行为
rados 在各种压力条件下运行含 OSD 和 MON 的 Ceph 集群
rbd 用真实 Ceph 集群跑 RBD 测试,带 qemu 和不带 qemu 都有
rgw 用真实 Ceph 集群跑 RGW 测试
smoke 用真实 Ceph 集群跑调用 Ceph API 的测试
teuthology 验证 teuthology 能跑集成测试,带 OpenStack 和不带都有
upgrade 对各个版本的 Ceph,验证升级能在不中断进行中工作负载的前提下完成

6.4 teuthology-describe

teuthology-describe 被加入 teuthology 框架,是为了便于文档化集成测试并增进理解。

做法是:在定义测试的 yaml 文件里嵌入 meta: 注解。查看效果:https://gist.github.com/jdurgin/09711d5923b583f60afc

由于这是个新特性,很多 yaml 文件还没被注解过。官方鼓励开发者去提高文档的覆盖率和质量。

6.5 怎么跑集成测试

  • 通常用 Sepia lab 跑。但作为 Ceph 新开发者,你多半没有 Sepia lab 访问权限;不过你可能能在 Sepia lab 之外的环境里跑一部分集成测试。去问相关团队的成员怎么弄。
  • 一种方式是在裸金属上搭 teuthology 集群,这是件复杂的活儿。入门笔记:https://docs.ceph.com/projects/teuthology/en/latest/LAB_SETUP.html
  • 每个 teuthology 集群(无论裸金属还是云供给)都有一台所谓的 teuthology machine,从那台机器上用 teuthology-suite 命令触发测试 suite。
  • 在该机器上跑下面的命令可拿到每个 teuthology-suite 选项的最新详尽说明:
teuthology-suite --help

teuthology-suite 在线文档:https://docs.ceph.com/projects/teuthology/en/latest/commands/teuthology-suite.html

为什么要跑并把结果发出去:在你贡献的代码上跑集成测试并发布结果,能让评审者验证改动没有引入回归,也能在测试失败时让评审者分析失败原因。

6.6 集成测试是怎么定义的

集成测试由 ceph/qa 子目录下 suites 子目录里的 yaml 文件定义,由 tasks 子目录里的 python 代码实现。

  • 有些测试("standalone tests")定义在单个 yaml 文件里;
  • 其他测试由一棵含若干 yaml 文件的目录树定义,运行时这些 yaml 会被组合成一个更大的 yaml 文件。

6.7 读一个 standalone test(单例)

以 rados/singleton/all/admin-socket.yaml 为例:

roles:
- - mon.a
  - osd.0
  - osd.1
tasks:
- install:
- ceph:
- admin_socket:
    osd.0:
      version:
      git_version:
      help:
      config show:
      config set filestore_dump_file /tmp/foo:
      perf dump:
      perf schema:

roles 数组:决定这个测试运行在什么样的集群构成上(几个 MON、几个 OSD 等),以及这些 role 如何分布到测试集群的机器上。

  • 顶层数组只有一个元素 → 只给这个测试分配一台机器;
  • 嵌套数组声明这台机器要跑一个 id 为 a 的 MON(mon.a)和两个 OSD(osd.0、osd.1)。

tasks 数组是测试主体:每个元素按顺序求值,导致 tasks 子目录(teuthology 仓库的,或 ceph/qa 子目录的)里对应的 python 文件被运行。"运行" 在这里指的是调用该文件里定义的 task() 函数。

本例的 task 作用
install 在每台机器上安装 Ceph 软件包(按 roles 数组定义)。完整描述见 https://github.com/ceph/teuthology/blob/master/teuthology/task/install/__init__.py(搜 "def task")
ceph 按 roles 数组要求启动 OSD 和 MON(可能还有 MDS),源码见 https://github.com/ceph/ceph/blob/master/qa/tasks/ceph.py(搜 "def task")。本例会启动一个 MON(mon.a)和两个 OSD(osd.0、osd.1),都在同一台机器上。当 Ceph 集群达到 HEALTH_OK 状态时,控制流才转到下一个 task
admin_socket 源码 https://github.com/ceph/ceph/blob/master/qa/tasks/admin_socket.py。它的参数(任何 task 的参数都一样)是一个结构,解释方式由该 task 文档化。本例的参数是发给 osd.0 的 admin socket 的一组命令;task 会验证每条命令都返回成功(exit code 为零)

运行这个测试:

teuthology-suite --machine-type smithi --suite rados/singleton/all/admin-socket.yaml fs/ext4.yaml

6.8 Test descriptions(测试描述)

每个测试有一个 "test description",类似目录路径,但不完全是。

  • standalone test 的描述:等同于定义它的 yaml 文件从 suites/ 目录开始的相对路径。
  • 更常见的情况:测试不是由单个 yaml 文件定义,而是由一棵 yaml 文件目录树定义。运行时遍历这棵树,所有 yaml 文件(facets)被组合成更大的 yaml "programs" 来定义测试。每个测试日志开头都会包含定义该测试的完整 yaml 清单。

这类测试的描述 = suites/ 下包含 yaml facets 的子目录 + 一个花括号 {} 表达式,里面是按拼接顺序排列的 yaml facet 列表。例如:

ceph-deploy/basic/{distros/centos_7.0.yaml tasks/ceph-deploy.yaml}

表示两个文件按顺序拼接:

  1. ceph-deploy/basic/distros/centos_7.0.yaml
  2. ceph-deploy/basic/tasks/ceph-deploy.yaml

6.9 测试是怎么从目录构建出来的:三个特殊文件操作符

一组给定 suites/ 子目录所定义的所有测试,称为一个 "integration test suite",或 "teuthology suite"。

yaml facets 的组合由放在目录树里的特殊文件控制,可以看作操作符:

特殊文件 名字 含义
% convolution(卷积) 操作符 从该目录下各子目录里的 yaml facets 构造测试矩阵
+ concatenation(拼接) 把一个目录内的文件拼接在一起
$ random selection(随机选择) 提示 teuthology 随机包含一个 YAML fragment

6.9.1 卷积操作符 %

% 是一个(通常为空的)文件,告诉 teuthology 从包含该操作符的目录下面的各子目录里的 YAML facets 构造测试矩阵。

以 ceph-deploy suite 为例,目录结构:

qa/suites/ceph-deploy
├── %
├── distros
│   ├── centos_latest.yaml
│   └── ubuntu_latest.yaml
└── tasks
    ├── ceph-admin-commands.yaml
    └── rbd_import_export.yaml

被解释为一个 2x1 矩阵,产生两个测试:

  1. ceph-deploy/basic/{distros/centos_7.0.yaml tasks/ceph-deploy.yaml}
  2. ceph-deploy/basic/{distros/ubuntu_16.04.yaml tasks/ceph-deploy.yaml}

人话:ceph-deploy.yaml 里的 task 打算在 CentOS 7.0 和 Ubuntu 16.04 上都跑一遍。

如果没有 % 文件,ceph-deploy 树会被解释成三个 standalone 测试:

  • ceph-deploy/basic/distros/centos_7.0.yaml
  • ceph-deploy/basic/distros/ubuntu_16.04.yaml
  • ceph-deploy/basic/tasks/ceph-deploy.yaml

(这当然是错的。)

symlink 技巧:suites/ceph-deploy/basic/distros/ 里的 centos_7.0.yaml 和 ubuntu_16.04.yaml 是符号链接。用 symlink 而不是复制,可以让一个文件出现在多个 suite 里,降低整个测试框架的维护成本。

跑整个 ceph-deploy suite:

teuthology-suite --machine-type smithi --suite ceph-deploy

跑其中一个测试(加 --filter):

teuthology-suite \
   --machine-type smithi \
   --suite ceph-deploy/basic \
   --filter 'ceph-deploy/basic/{distros/ubuntu_16.04.yaml tasks/ceph-deploy.yaml}'

官方 note:跑 standalone test 只用 --suite 就够了。但如果想跑一个「由目录树定义的 suite」里的单个测试,--suite 必须配合 --filter,因为 --suite 选项只认 POSIX 相对路径。

6.9.2 Nested Subsets(嵌套子集)

suite 会因为 yaml 配置的组合爆炸而变得非常大:写作时 rados suite 有超过 100,000 个 job。因此调度时常用 --subset 只跑一部分 job。但这只在被跑 suite 的顶层生效(例如 fs),这可能顺带放大了某些较大子 suite(如 fs:workload)相对某些较小但关键的子 suite(如 fs:volumes)的 job 占比。

因此,对那些永远不会被完整跑的子 suite 自动做子集化是很有吸引力的。做法:给 % 卷积操作符文件里写一个整数除数,而不是留空。该除数会自动对生成的矩阵做子集化。例如 % 文件里内容是 2,矩阵就会用与 --subset 相同的逻辑被一分为二。

注意:分子不像 --subset 选项那样指定,因为在可能有多层嵌套的情况下没法有意义地表达。取而代之的是随机选一个子集(上例就是 2 选 1)。选择基于调度时用的随机种子(--seed)。记住种子会被保存在结果里,这样对失败测试做 --rerun 时仍能保持正确的分子(subset of subsets)。

可以用 --no-nested-subset 参数禁用嵌套子集。

6.9.3 拼接操作符 +

为了在 suite 之间共享 yaml 文件时获得更大的灵活性,可以在一个目录里放特殊文件 +,把该目录内的文件拼接起来。

以 suites/rbd/thrash 为例:

qa/suites/rbd/thrash
├── %
├── clusters
│   ├── +
│   ├── fixed-2.yaml
│   └── openstack.yaml
└── workloads
    ├── rbd_api_tests_copy_on_read.yaml
    ├── rbd_api_tests.yaml
    └── rbd_fsx_rate_limit.yaml

这产生两个测试:

  • rbd/thrash/{clusters/fixed-2.yaml clusters/openstack.yaml workloads/rbd_api_tests_copy_on_read.yaml}
  • rbd/thrash/{clusters/fixed-2.yaml clusters/openstack.yaml workloads/rbd_api_tests.yaml}

因为 clusters/ 子目录里有特殊文件 +,该目录里其他所有文件(fixed-2.yaml 和 openstack.yaml)被拼接成一个文件对待。

如果没有 +,它们会和 workloads 目录里的文件做卷积,产生 2x2 矩阵四个测试:

  • rbd/thrash/{clusters/openstack.yaml workloads/rbd_api_tests_copy_on_read.yaml}
  • rbd/thrash/{clusters/openstack.yaml workloads/rbd_api_tests.yaml}
  • rbd/thrash/{clusters/fixed-2.yaml workloads/rbd_api_tests_copy_on_read.yaml}
  • rbd/thrash/{clusters/fixed-2.yaml workloads/rbd_api_tests.yaml}

clusters/fixed-2.yaml 被很多 suite 共享,用来定义如下 roles:

roles:
- [mon.a, mon.c, osd.0, osd.1, osd.2, client.0]
- [mon.b, osd.3, osd.4, osd.5, client.1]

跑整个 rbd/thrash suite:

teuthology-suite --machine-type smithi --suite rbd/thrash

跑其中一个测试:

teuthology-suite \
   --machine-type smithi \
   --suite rbd/thrash \
   --filter 'rbd/thrash/{clusters/fixed-2.yaml clusters/openstack.yaml workloads/rbd_api_tests_copy_on_read.yaml}'

6.9.4 Upgrade Testing(升级测试)

用 upgrade suite 可以验证:从较早版本升级能够成功完成,且不中断任何进行中的工作负载。

每个 release 分支的升级目录包含 2-x 升级测试,也就是说能测从当前版本往前 2 个版本升级到当前版本。升级过程与其他给定工作负载并行进行(用 https://github.com/ceph/teuthology/blob/main/teuthology/task/parallel.py)。

以 Quincy release 分支的升级测试目录为例:

.
├── octopus-x
└── pacific-x

可以测从 Octopus(2-x)或从 Pacific(1-x)升到 Quincy(x)。

一个简单的升级测试由以下部分组成:

├── 0-start.yaml
├── 1-tasks.yaml
├── upgrade-sequence.yaml
└── workload

先用旧版本启动集群,然后开始并行跑给定的 workload 和 upgrade-sequence:

- print: "**** done start parallel"
- parallel:
    - workload
    - upgrade-sequence
- print: "**** done end parallel"

workload 目录就是普通的 yaml 文件(和其他 suite 一样),而 upgrade-sequence 负责执行升级并等待其完成:

- print: "**** done start upgrade, wait"
...
  mon.a:
    - ceph orch upgrade start --image quay.ceph.io/ceph-ci/ceph:$sha1
    - while ceph orch upgrade status | jq '.in_progress' | grep true ; do ceph orch ps ; ceph versions ; sleep 30 ; done
...
- print: "**** done end upgrade, wait..."

也可以分阶段升级,中间穿插跑工作负载:

├── %
├── 0-cluster
├── 1-ceph-install
├── 2-partial-upgrade
├── 3-thrash
├── 4-workload
├── 5-finish-upgrade.yaml
├── 6-quincy.yaml
└── 8-final-workload

流程:启动集群 → 只升级集群的 2/3(2-partial-upgrade)→ 跑 thrash 测试和给定工作负载测试 → 继续升级剩下的部分(5-finish-upgrade.yaml)→ 最后要求升级后的 release(ceph require-osd-release quincy、ceph osd set-require-min-compat-client quincy)并跑 final-workload。

6.9.5 随机选择操作符 $

存在名为 $ 的文件,会提示 teuthology 随机包含其中一个 YAML fragment。这种场景典型地出现在:需要在若干 flavor 的 feature/option 里随机挑一个来测的时候。

6.10 Position Independent Linking(位置无关链接)

qa/suites 目录下每个目录里都有 .qa 符号链接,每个链接都递归地指向 ../.qa/,最终终止链接在 qa/ 目录本身:qa/.qa -> .。

这种符号链接布局让一个 suite 可以轻松被复制或移动而不破坏大量符号链接。例如:

qa/suites/fs/upgrade/nofs/centos_latest.yaml -> .qa/distros/supported/centos_latest.yaml

如果你把 nofs suite 复制到别处、在其上加一层父目录、或者把 centos_latest.yaml fragment 挪进一个子目录,这个链接都不会断。

对比传统写法:

qa/suites/fs/upgrade/nofs/centos_latest.yaml -> ../../../../distros/supported/centos_latest.yaml

一旦移动,很可能会断,因为到达 distros 目录所需的父目录层数可能变化。

官方建议:新增目录或 suite 时,记得一并加上 .qa 符号链接。 一条 find 命令就能搞定:

find qa/suites/ -type d -execdir ln -sfT ../.qa/ {}/.qa \;

6.11 按描述过滤测试

当少数 job 失败需要重跑时,可以用 --filter 选出描述匹配的测试。例如 rados suite 里的 all/peer.yaml 失败了,下面这条只跑包含这个文件的测试:

teuthology-suite --machine-type smithi --suite rados --filter all/peer.yaml
选项 作用
--filter 只跑描述包含给定字符串的测试
--filter-out 反过来,匹配不包含给定字符串的测试;可以与 --filter 组合使用

两者都接受逗号分隔的字符串列表(这意味着 ceph/qa 子目录里的文件名隐式禁止出现逗号字符)。例如:

teuthology-suite --machine-type smithi --suite rados --filter all/peer.yaml,all/rest-api.yaml

会跑包含 all/peer.yaml 或 all/rest-api.yaml 的测试。

重要:每个字符串是在 test description 的任意位置查找,且必须是精确匹配——它们不是正则表达式。

6.12 减少测试数量

rados suite 从几百个文件里生成数万甚至数十万个测试,这是因为 teuthology 在每遇到一个名为 % 的文件时就从子目录构造测试矩阵。例如 rados/basic suite 里所有测试都会用不同的 messenger 类型(simple、async、random)跑,因为它们通过特殊文件 % 与 msgr 目录组合。

所有集成测试在 Ceph 发布之前都必须跑一遍。 但如果只是要验证一个贡献能否合入而不冒引入低级回归的风险,跑一个子集就够了。

选项 作用 备注
--subset 减少被触发的测试数量。例如 --subset 0/4000 会跑尽可能少的测试 权衡是并非所有测试变体的组合都会被一起跑到;但无论比例给多小,teuthology 仍会保证 suite 里的每个文件至少出现在一个测试中。理解背后真正的逻辑需要读 teuthology 源码
--no-nested-subset 禁用嵌套子集(某些 suite 现在用了这个自动对一组精心挑选的 YAML 配置做子集化的特性) 想做自定义过滤时可用
--limit 只跑 suite 里前 N 个测试 很少有用,因为没法控制哪个测试排在第一个

引自:Testing - Integration Tests - Introduction

6.13 Teuthology 工作流:基础设施组件

一次 teuthology run 包含多个 job,每个 job 在相互隔离的环境里、在不同的 test node 集合上运行。

# 组件 说明
1 ceph-ci Ceph 主仓库的 clone,用于触发 Jenkins 开发构建
2 Ceph Jenkins 负责触发构建、把包上传到 Chacra、把构建更新推送到 Shaman
3 Shaman UI 界面,用来查看构建状态。后端是一个查询和存储构建信息的 REST API
4 Chacra 包上传目的地服务。上传到这里二进制任何人都能下载使用
5 Teuthology CLI 开发者用各种 Teuthology 命令调度和管理测试运行
6 Teuthology 负责把测试 job 推到 Beanstalk 队列和 Paddles;也负责从队列取 job 并跑测试
7 Beanstalk queue 包含所有排队 job 的优先队列。开发者通常不需要直接交互
8 Paddles 存储所有测试运行信息的后端服务。开发者通常不需要直接交互
9 Pulpito UI 界面(数据来自 Paddles),让开发者查看已调度测试的详细信息,包括状态和结果
10 Testnodes 用于跑测试的各种机器组成的集群。开发者通常把测试调度到 smithi 机器上——这是 Teuthology 集成测试的专用测试节点

工作流总览(文字版):

  1. 把带改动的分支推到 ceph-ci 仓库 → 自动触发 Jenkins 构建 Ceph 二进制,构建状态在 Shaman 上观察;
  2. 构建好的包上传到 Chacra;
  3. 要针对这个新构建调度 teuthology 集成测试,需要 Sepia lab 访问权限;
  4. 登录后做一次性的初始 teuthology 设置,之后用 teuthology-suite 调度,用 -c <ceph-ci branch name> 指定你的构建;
  5. 结果在 Pulpito 上看;
  6. 登录 developer playground machine 查看 teuthology run 的归档日志。

6.14 拿到二进制:构建 Ceph

  1. 把分支推到 ceph-ci 仓库,触发 Jenkins CI 上的二进制构建流程。
  2. 确认构建流程已启动:分支名出现在 Shaman 的 "Latest Builds Available" 列表里。不久之后,测试基础设施会往列表里加其他名字相近的构建——它们的名字里会包含各种 Linux 发行版的名字,用于在这些发行版上测你的构建。
  3. 等待包构建完成并上传到 Chacra、等待提供这些包的 repository 创建完成。Shaman 上 "Latest Builds Available" 列表里对应分支名的条目会变绿,表示包已上传到 Chacra 且 repository 已创建。等到每个条目都变绿。通常需要 2 到 3 小时,取决于机器可用性。

Chacra URL 可以从 https://shaman.ceph.com/api/search/?status=ready&project=ceph 查询。

两条 note:

  • 推到 ceph-ci 的分支可以是任意分支,不必是 PR 分支。
  • 如果你打算推 master 或其他标准分支,先查一下 Shaman,因为可能已经有构建好的了。

6.14.1 推到 ceph-ci 仓库

1. 把 ceph-ci 仓库加为本地 Ceph clone 的 remote:

git remote add ceph-ci git@github.com:ceph/ceph-ci.git

$ git remote -v
origin   git@github.com:ceph/ceph.git (fetch)
origin   git@github.com:ceph/ceph.git (push)
ceph-ci  git@github.com:ceph/ceph-ci.git (fetch)
ceph-ci  git@github.com:ceph/ceph-ci.git (push)

2. 推你的分支:

$ git push ceph-ci wip-yourname-feature-x

6.14.2 ceph-ci 分支命名规则

推到 ceph-ci 前,给分支名加上你自己的名字前缀。 例如分支 feature-x 应该命名为 wip-$yourname-feature-x。这样你的分支在 Shaman 和 Pulpito 上容易被找到。

如果你用的是某个 stable 分支(quincy、pacific 等),把该 stable 分支名也放进 ceph-ci 分支名里。 例如 feature-x PR 分支应命名为 wip-feature-x-nautilus。

官方强调:这不只是约定。这确保你的分支在正确的环境里被构建。

不用了就把分支从 ceph-ci 删掉。登录 GitHub 后,你在 ceph-ci 上的所有分支可以在这里看到:https://github.com/ceph/ceph-ci/branches

note:你可以只触发 CentOS 9.Stream 构建(排除 ubuntu 等其他发行版),方法是在 ceph-ci 分支名末尾加 "centos9-only",例如 wip-$yourname-feature-centos9-only。这能在你不需要其他发行版二进制时更快拿到构建并节省资源。

6.15 调度测试运行

6.15.1 触发 teuthology 测试

1. 登录 teuthology 机器:

ssh <username>@teuthology.front.sepia.ceph.com

(需要 Sepia lab 访问权限。申请:https://ceph.github.io/sepia/adding_users/)

2. 初次设置:按 teuthology 安装指南(https://docs.ceph.com/projects/teuthology/en/latest/INSTALL.html#installation-and-setup)在你的用户下配置 teuthology,之后你才能跑 teuthology 命令。

3. 跑 teuthology-suite 命令:

teuthology-suite -v \
-m smithi \
-c wip-devname-feature-x \
-s fs \
-p 110 \
--filter "cephfs-shell" \
-e foo@gmail.com
选项 含义
-v verbose
-m machine name(机器名)
-c 推到 ceph-ci 上的分支名
-s test-suite 名字
-p 数字越大,job 优先级越低
--filter 过滤给定 suite 里的测试,参数指定你想跑哪些测试
-e <email> 测试完成或超时时发邮件到指定地址。也可以在 ~/.teuthology.yaml 里用 results_email 指定

两条 note:

  • 上面命令里的优先级数字只是占位符,不要在你自己的测试里用。见「Testing Priority」的推荐取值。
  • 不要不带优先级数字就发命令。默认值是 1000,大到你的 job 很可能永远不跑。

跑 teuthology-suite --help 读这些和其他可用选项的说明。

4. 等测试跑。 teuthology-suite 会打印一个 Pulpito 链接,可以看结果。

6.15.2 其他常用选项

选项 用途
--dry-run 试跑,不真的调度。用来检查一条命令会调度多少 job、哪些 job
-d / --distro 发行版
--distro-version 发行版版本
--filter-out 反向过滤
--timeout 超时
flavor flavor
--rerun 重跑
--limit 限制 job 数量
-N job 跑多少次
--subset 减少被触发的测试数量

6.15.3 只改了 qa/ 目录:不用重新构建二进制

如果你只在 qa/ 目录里做改动,重跑测试前不必重新构建二进制,可以用为 ceph-ci 分支构建的二进制直接重跑,只需告诉 teuthology-suite 用一个单独的分支来跑测试。

具体做法:给 teuthology-suite 传两个额外参数——

  1. --suite-repo:指定你的 ceph 仓库;
  2. --suite-branch:指定你的分支名。

例如,你要在测完 branch-x(对应 ceph-ci 分支 wip-username-branch-x)之后改 qa/:

teuthology-suite -v \
 -m smithi \
 -c wip-username-branch-x \
 -s fs \
 -p 50 \
 --filter cephfs-shell

然后本地做修改、更新 PR 分支,再从你的 PR 分支触发测试:

teuthology-suite -v \
 -m smithi \
 -c wip-username-branch-x \
 -s fs -p 50 \
 --filter cephfs-shell \
 --suite-repo https://github.com/$username/ceph \
 --suite-branch branch-x

如何确认测试确实用了这个分支跑:看 teuthology job 开头打印的 job config 里的 suite_branch、suite_repo 和 suite_sha1 三个键的值。

note:如果改动不在 qa/ 目录里,就必须走标准流程:触发构建 → 等构建完成 → 触发测试 → 等结果。

6.15.4 过滤测试:关键词从哪找

Suite 里包含大量 yaml 文件的组合,可能调度出海量的 job,因此需要过滤。

过滤关键词的位置:qa/suites/<suite-name>/<subsuite-name>/tasks。该目录里每个 YAML 文件都能触发测试;把文件名去掉扩展名作为 --filter 的参数,就能触发那些测试。

例如上面用的 cephfs-shell,是因为 qa/suites/fs/basic_functional/tasks/ 下有一个 cephfs-shell.yaml。

如果文件名看不出它触发什么测试,就去搜文件内容里的 modules 属性。cephfs-shell.yaml 的 modules 属性是 tasks.cephfs.test_cephfs_shell,意味着它触发 qa/tasks/cephfs/test_cephfs_shell.py 里的所有测试。

6.16 查看测试结果

6.16.1 Pulpito Dashboard

https://pulpito.ceph.com/

6.16.2 Teuthology Archives(查日志)

测试跑完后,在 Pulpito 的 run 页面点 job ID 可以拿到日志。官方建议下载日志再看,而不是在浏览器里看,因为这些日志很容易达到 1GB。

也可以 ssh 到 developer playground machine(https://wiki.sepia.ceph.com/doku.php?id=devplayground)访问这个路径:

/teuthology/<run-name>/<job-id>/teuthology.log

例如:

/teuthology/teuthology-2019-12-10_05:00:03-smoke-master-testing-basic-smithi/4588482/teuthology.log

这比通过浏览器看快。

看 ceph 日志(cephadm、ceph monitors、ceph-mgr 等)或系统日志:在浏览器里把 job 的 teuthology log url 末尾的 teuthology.log 去掉,然后进入 remote/<machine>/log/;系统日志在 remote/<machine>/syslog/。在 developer playground 机器上对应路径是 /teuthology/<test-id>/<job-id>/remote/<machine>/。

其他为调试提供的文件:

文件 内容
unit_test_summary.yaml 所有单元测试失败的摘要。可选生成——只有当 job 的 YAML 文件里用了 unit_test_scan 配置项时才会生成
valgrind.yaml 汇总可能发生的 Valgrind 错误

便捷访问:/a/ 已经符号链接到 /teuthology/。例如上面的例子可以写成: /a/teuthology-2019-12-10_05:00:03-smoke-master-testing-basic-smithi/4588482/teuthology.log

6.17 杀掉测试

teuthology-kill 用来杀掉那些意外跑了几个小时的 job,或者开发者想提前终止测试时。

teuthology-kill -p  -r teuthology-2019-12-10_05:00:03-smoke-master-testing-basic-smithi -m smithi -o scheduled_teuthology@teuthology

传给 -r 的是 run name,可以在你触发的测试的 Pulpito 页面链接里轻松找到。例如: http://pulpito.front.sepia.ceph.com/teuthology-2019-12-10_05:00:03-smoke-master-testing-basic-smithi/

6.18 重跑测试

teuthology-suite 的 -r(--rerun)选项允许重跑测试,在测试失败或变成 dead 时很方便。--rerun 接受一个 teuthology run 的名字作为参数。 可以配合 -R(--rerun-statuses)选择从这次 run 里挑出哪些状态的测试重跑。例如只重跑上次变成 dead 的测试:

teuthology-suite -v \
 -m smithi \
 -c wip-rishabh-fs-test_cephfs_shell-fix \
 -p 50 \
 --r teuthology-2019-12-10_05:00:03-smoke-master-testing-basic-smithi \
 -R fail,dead,queued \
 -e $CEPH_QA_MAIL
选项 含义
-r, --rerun 尝试重新调度一次 run,只挑选状态被 --rerun-status 提及的那些 job
-R, --rerun-statuses 与 --rerun 配合使用的逗号分隔状态列表。支持的状态:dead、fail、pass、queued、running、waiting。默认值:fail,dead

引自:Integration Tests using Teuthology Workflow

6.19 分析与调试一个 Teuthology Job

6.19.1 查看结果

run 成功完成后,用 pulpito dashboard 看结果(https://pulpito.ceph.com):

http://pulpito.front.sepia.ceph.com/<job-name>/<job-id>/

或 ssh 进 teuthology server:

ssh <username>@teuthology.front.sepia.ceph.com

然后访问 teuthology archives,例如:

nano /a/teuthology-2021-01-06_07:01:02-rados-master-distro-basic-smithi/

(需要 Sepia lab 访问权限,申请见 https://ceph.github.io/sepia/adding_users/)

6.19.2 识别失败的 job

在 pulpito 上,红色表示一个 failed job 或一个 dead job。

一个 job 是 qa/suites 里 yaml fragments 定义的一组守护进程与配置的组合。Teuthology 使用这些配置并运行 qa/tasks 里列出的 tasks——这些 task 是搭建测试环境并测试 Ceph 各组件的命令。这些 task 覆盖了很大一部分用例,有助于暴露 make check 测试暴露不出的 bug。

一个 job 失败可能由以下一个或多个原因导致:

最终用于测试的 setup config yaml 文件可以在这里拿到:

/a/<job-name>/<job-id>/orig.config.yaml

更多关于 config.yaml 的细节见 https://docs.ceph.com/projects/teuthology/en/latest/detailed_test_config.html

6.19.3 定位失败原因(Triaging)

job 失败时,需要读它的 teuthology log 来定位原因。用 pulpito 上的 job name 和 id 找到失败 job 的日志:

http://qa-proxy.ceph.com/<job-name>/<job-id>/teuthology.log

打开文件:

/a/<job-name>/<job-id>/teuthology.log

例如:

nano /a/teuthology-2021-01-06_07:01:02-rados-master-distro-basic-smithi/5759282/teuthology.log

每一次 job 失败都会以 Traceback 的形式记录在 teuthology log 里,并被加进 job summary。

做法:找 Traceback 关键字,搜索调用栈和日志里导致失败的问题。通常 traceback 里会包含失败的那条命令。

note:teuthology 日志会被定期删除。如果上面示例的链接访问不了,就从 http://pulpito.front.sepia.ceph.com/ 随便找另一个 case 用。

6.19.4 上报 issue

原则:先确认你的 job 失败是不是已知 issue 导致的;如果不是,就提 tracker ticket。

流程:

  1. 定位完失败原因,确认不是你的代码改动导致的——这可能说明你遇到了上游分支的已知失败(本节例子里上游分支是 "octopus")。
  2. 去 https://tracker.ceph.com 用你在失败中看到的关键词搜索相关 issue。
  3. 找到相似 issue:在上面留言,按你的理解解释这次失败,务必附上你最近一次 test run 的链接。
  4. 没找到:为这个 issue 新建一个 tracker ticket,尽可能详尽地解释 job 失败的原因。
  5. 不确定失败原因:找团队成员求助。

6.19.5 用 interactive-on-error 调试

遇到 job 失败时应该尝试复现它。这就是 --interactive-on-error 的用武之地。

确认 job 失败后,用 teuthology 再跑一次同一个 job,但加上 interactive-on-error flag:

ideepika@teuthology:~/teuthology$ ./virtualenv/bin/teuthology -v --lock --block $<your-config-yaml> --interactive-on-error

可以用自定义 config.yaml,也可以用失败 job 的 yaml 文件。如果用失败 job 的,先把 orig.config.yaml 拷到本地目录:

ideepika@teuthology:~/teuthology$ cp /a/teuthology-2021-01-06_07:01:02-rados-master-distro-basic-smithi/5759282/orig.config.yaml test.yaml
ideepika@teuthology:~/teuthology$ ./virtualenv/bin/teuthology -v --lock --block test.yaml --interactive-on-error

它做了什么:如果用了 interactive-on-error 后 job 失败,teuthology 会锁住 config.yaml 需要的机器,停住这些测试机并把它们保持在失败发生那一刻的状态,然后把你放进一个交互式 Python session。从那里你可以 ssh 进系统排查失败原因。

排查完后直接终止 session 即可,teuthology 会清理 session 并解锁机器。

6.19.6 官方推荐的学习资源

资源 链接
Testing Ceph: Pains & Pleasures https://www.youtube.com/watch?v=gj1OXrKdSrs
Teuthology Training(播放列表) https://www.youtube.com/playlist?list=PLrBUGiINAakNsOwHaIM27OBGKezQbUdM-
Intro to Teuthology https://www.youtube.com/watch?v=WiEUzoS6Nc4

引自:Analyzing and Debugging A Teuthology Job

6.20 Kernel 开发的集成测试

6.20.1 CephFS

fs suite 会跑各种 kernel,由 kernel YAML fragments 描述。这些 fragments 被 fs suite 下的其他子 suite 符号链接引用。

这个 fragment 矩阵允许测试以下配置:

  • RHEL 8 上的 "stock" kernel(即随发行版自带的那个内核);
  • 内核开发团队的 testing 分支,代表正在积极测试中的补丁。这些补丁可能进入也可能不进入下一个上游内核发布,包含 CephFS 或 kRBD 改动的混合。对 testing kernel,我们用子 suite 指定的发行版来测。例如 fs:functional 子 suite 使用 supported random distros 的随机选择。

6.20.2 测试自定义内核

如果你在 ceph-client.git 上有一个 kernel 分支,并且已经用 shaman 构建过,那么可以通过给 teuthology-suite 传一个 YAML fragment 来覆盖 kernel,轻松测它:

$ cat custom-kernel.yaml
overrides:
  kernel:
    branch: for-linus

这为 suite 矩阵里指定的 kernel 分支提供了 override。你也可以把 override 指定为 kernel task 的一个 tag 或 SHA1。

重要提醒:覆盖 kernel 时,你应该减少 job 的选择范围,因为矩阵会包含很多你不关心的 kernel 配置(见上文 CephFS 一节);override YAML 会应用到 kernel 的所有配置,从而产生重复的测试。

运行命令形如:

teuthology-suite ... --suite fs --filter k-testing custom-kernel.yaml

其中 ... 是平时跑 teuthology-suite 时的其他常规选项。关键的过滤器 --filter k-testing 会把 job 选择范围限制在使用 kernel 的 testing 分支的那些(见 k-testing.yaml 文件),所以你只会选到用 testing 分支 kernel client 的 job。然后你的自定义 YAML 文件 custom-kernel.yaml 会进一步把 testing 分支覆盖成你指定的任何东西。

引自:Integration Tests for Kernel Development

6.21 Sentry

tests-sentry-developers-guide.rst 的完整内容只有两行:

Sentry Notes
============

To be updated. Feel free to contribute.

即:待补充,欢迎贡献。 官方文档本身没有写任何 Sentry 相关内容。

引自:Sentry Notes

6.22 子目录索引

testing_integration_tests/index.rst 的 toctree(用 :glob: + :titlesonly:):

条目 页面
Introduction tests-integration-testing-teuthology-intro
Workflow tests-integration-testing-teuthology-workflow
Debugging Tips tests-integration-testing-teuthology-debugging-tips
Kernel Development tests-integration-testing-teuthology-kernel
Sentry Notes tests-sentry-developers-guide

引自:Teuthology User Guide


七、用 GDB 调试 Ceph

GNU Project Debugger (GDB)(https://www.sourceware.org/gdb)是一个强大的工具,可以分析进程的执行流。GDB 能帮你找 bug、发现 crash 错误、或者在开发集群运行时跟踪源码。它也可以用来调试 Teuthology 测试运行。

7.1 起步

基础用法与示例:https://geeksforgeeks.org/gdb-command-in-linux-with-examples

GDB 可以 attach 到运行中的进程。 例如部署一个开发集群后,ceph-osd 守护进程的 PID 可以在这里找到:

$ cd build
$ cat out/osd.0.pid

attach gdb 到该进程:

$ gdb ./bin/ceph-osd -p <pid>

7.2 两条重要建议

1)编译时不要优化。 官方建议编译时不带任何优化(-O0 gcc flag),以避免中间值被消除。

2)调大的超时时间。 调试时因断点停下来可能导致超时,官方建议设置以下配置项:

[osd]
osd_op_thread_timeout = 1500
osd_op_thread_suicide_timeout = 1500

7.3 调试 Teuthology 测试

src/script/ceph-debug-docker.sh 可以用来分析 Teuthology 失败:

$ ./ceph-debug-docker.sh <branch-name>

更多信息参见该脚本的头部注释。

7.4 常用 GDB 命令速查

⚠️ 文档未明确:官方 debugging-gdb.rst 没有列出任何具体的 GDB 子命令(如 bt、info threads、thread apply all bt 等),只给了一个外部链接。上面的 gdb ./bin/ceph-osd -p <pid> 是官方给出的唯一一条完整命令。

💡 补充:本册源文能确证的 gdb 相关命令只有:

# 1) 找到 ceph-osd 的 PID
cd build
cat out/osd.0.pid

# 2) attach
gdb ./bin/ceph-osd -p <pid>

# 3) 分析 teuthology 失败
./ceph-debug-docker.sh <branch-name>

💡 补充:调试时若需要看 Ceph 自己的日志,注意 Google Test 与守护进程的客户端日志位置不同(见 5.6 节):单测客户端日志在 build/out/client.admin.<pid>.log;vstart 集群日志在 build/out/ 下,可用 tail -f out/???? 观察。

引自:GDB - The GNU Project Debugger


八、Windows 测试

8.1 背景

从 Pacific 起,Ceph 客户端工具和库可以在 Windows 上原生使用,这样 Windows 节点消费 Ceph 时不需要额外加 iSCSI gateway 或 SMB share 之类的层。

为了保证这些组件在 Windows 上持续正常工作,大量单元测试和集成测试被移植过来。

8.2 Windows CI Job 做了什么

Windows CI job(https://github.com/ceph/ceph-build/blob/main/ceph-windows-pull-requests/config/definitions/ceph-windows-pull-requests.yml)对每个 GitHub pull request 执行以下步骤:

  1. 起一台 Linux VM,在里面构建服务端(Linux)Ceph 二进制,并交叉编译 Windows(客户端)二进制;
  2. 重建 Linux VM 并启动一个 Ceph vstart 集群;
  3. 启动一台 Windows VM,在上面跑 Ceph 测试。

一个小型 PowerShell 框架(https://github.com/ceph/ceph-win32-tests/)负责并行化测试、聚合结果,并隔离或跳过已知的 flaky 测试。

控制台输出里可能包含编译错误和失败测试的名字。要拿到失败测试的控制台输出以及 Ceph 和操作系统日志,去看 Jenkins "Status" 页面的 build artifacts。

Windows CI artifacts 可以下载为 zip 归档,也可以在浏览器里直接看。点 "artifacts" 按钮查看内容。

8.3 Artifact 目录结构

路径 内容
client/ Ceph 客户端侧日志(Windows)
client/eventlog/ Windows 系统日志
client/logs/ Ceph 日志
client/*-windows.conf Ceph 配置文件
cluster/ Ceph 服务端日志(Linux)
cluster/ceph_logs/ Ceph 日志
cluster/journal journal
test_results/ 测试结果
test_results/out/ 按测试可执行文件分组的 raw 与 xml 测试输出
test_results/test_results.html 聚合测试报告(html)
test_results/test_results.txt 聚合测试报告(纯文本)

测试结果用 subunit 格式(https://github.com/testing-cabal/subunit)及相关工具聚合,在并行跑大量测试时特别好用。

8.4 怎么读聚合测试报告

聚合测试报告能很好地概览失败的测试。跳到文件末尾可以看到实际错误:

{0} unittest_mempool.mempool.bufferlist_reassign [0.000000s] ... ok
{0} unittest_mempool.mempool.bufferlist_c_str [0.006000s] ... ok
{0} unittest_mempool.mempool.btree_map_test [0.000000s] ... ok
{0} ceph_test_dokan.DokanTests.test_mount [9.203000s] ... FAILED

Captured details:
~~~~~~~~~~~~~~~~~
    b'/home/ubuntu/ceph/src/test/dokan/dokan.cc:136'
    b'Expected equality of these values:'
    b'  wait_for_mount(mountpoint)'
    b'    Which is: -138'
    b'  0'
    b''
    b'/home/ubuntu/ceph/src/test/dokan/dokan.cc:208'
    b'Expected equality of these values:'
    b'  ret'
    b'    Which is: "ceph-dokan: exit status: -22"'
    b'  ""'
    b'Failed unmapping: Y:\\'
{0} ceph_test_dokan.DokanTests.test_mount_read_only [9.140000s] ... FAILED

HTML 报告按测试 suite(测试二进制)分组,出于安全原因默认不渲染,但可以下载到本地看。

排查技巧:

  • 超时和缺失的测试结果往往是进程崩溃的信号。
  • 注意 ceph status 会在跑测试前后打印到控制台,这有助于识别崩溃的服务。
  • 也可以查服务日志(客户端和服务端都要看)。
  • 另外注意:Windows "application" 事件日志里会有 Windows 进程崩溃的条目。

8.5 FAQ

Q1:为什么我的 PR 只有 Windows CI job 失败?

Ceph 集成测试通常是通过 Teuthology 在 Ceph Lab 基础设施上做的。这些测试由 Ceph QA 团队按需触发,不会为每个提交的 PR 自动跑。

而 Windows CI job 只聚焦客户端侧的 Ceph 组件,因此能对每个 GitHub PR 及时跑各种集成测试。换句话说,它跑的是各种 librados、librbd 和 libcephfs 测试——这些是 make check 等其他检查所不跑的。

因此 Windows CI 经常能抓到其他检查漏掉的回归,而这些回归往往不是平台特有的,也影响 Linux。

Windows CI 失败时,强烈建议按上面的方法查测试结果。

另外要注意:Windows build script(https://github.com/ceph/ceph/blob/main/win32_build.sh)可能使用不同的编译 flag 和传给 CMake 的 -D 选项。例如它默认是 Release 模式而不是 Debug 模式;同时它用的是不同的 toolchain(mingw-llvm)和一套独立的 dependencies(https://github.com/ceph/ceph/blob/main/win32_deps_build.sh),需要时要记得升级版本。

Q2:为什么 Windows CI job 是强制的(mandatory)?

这个测试 job 最初是可选的,结果回归被引入得非常频繁。

后来 Windows 支持成熟到足以让这个 CI job 变成强制的。这显著减少了处理回归所需的工作量,也向 Ceph 用户保证了 Windows 支持的持续性。

另一个巨大好处(如上所述):它跑的集成测试能快速抓到往往同样影响 Linux 构建的回归,让开发者不必等完整的 Teuthology 结果。

引自:Testing - Windows


九、文档贡献

⚠️ 范围说明:documenting.rst 不在 doc/dev/developer_guide/ 目录下,它的实际位置是 doc/dev/documenting.rst,属于本手册第 18 册(内部实现篇·组件工具与 Crimson)的覆盖范围。按本册的取材范围,这里只讲与贡献流程直接相关的部分,完整内容见第 18 册。

9.1 从贡献流程角度看文档改动

场景 是否需要 Redmine tracker issue
Simple documentation cleanup(简单文档清理) 不需要
新增文档章节或文件 需要
对文档结构或内容做实质性改动 需要

引自:Basic Workflow

9.2 所有 PR 都会跑的一个 CI 检查:文档能否构建

官方列出的第 2 项自动化检查就是 a test to check that the documentation builds。也就是说,你的 rst 写坏了,PR 会红。

引自:Basic Workflow

9.3 doc/dev/documenting.rst 要点(摘要,详见第 18 册)

  • docs.ceph.com 上的文档由 Ceph git 仓库 /doc/ 目录里的 reStructuredText 源文件生成。
  • 除非你是在 /doc/dev/(开发者专区)里做增补,否则改动必须面向软件的最终用户来写。
  • 所有修改面向用户功能的 PR 都必须包含对应的文档更新,详见 SubmittingPatches。
  • 检查 rst 语法:在 GitHub 上看 diff 时用 "View" 按钮;或者用 admin/build-doc 脚本本地构建。
  • C/C++ 代码文档用 Doxygen(配合 Breathe 支持的 Doxygen 标记子集),注释写在声明函数的头文件里,函数应按逻辑分组。生成 HTML:
    cmake --build . --target doxygen
    
    输出在 build-doc/doxygen/html。
  • 画图工具:Graphviz(推荐把 DOT 源放单独文件,用 .. graphviz:: myfile.dot)、Ditaa、Blockdiag(如需要可集成)、Inkscape(应同时提交 SVG 和导出的 PNG,rst 里引用 PNG)。

💡 补充:本节 9.3 的内容来自 doc/dev/documenting.rst,不属于本册的源文范围(本册源文是 doc/dev/developer_guide/),此处仅作为与「贡献流程」相关的必要引述列出,完整讲解见第 18 册。


十、Ceph Dashboard 开发(dash-devel.rst,本册最大块)

这是本册最大的一章,源文 103KB、2747 行,官方标题是 Ceph Dashboard Developer Documentation (formerly HACKING.rst)——即它前身是 Dashboard 的 HACKING.rst。

10.1 Feature Design(特性设计)

为了促进在 Ceph Dashboard 新特性上的协作,第一步是定义一份设计文档。这些文档随后构成实现范围的基础,并让更多人能参与到 Ceph Dashboard UI 的演进中。

设计文档的 toctree 里目前挂着一个条目:

  • UI Design Goals → ../dashboard/ui_goals

10.2 Preliminary Steps:搭建开发环境

后续各章都假设你有一个运行中的 Ceph 集群,且至少跑着 dashboard manager module(少数例外)。本章讲如何为开发搭建这样一个系统,无需搭建完整的生产环境。

重要前提:每个 vstart 环境都需要 Ceph 从 GitHub 仓库编译(https://docs.ceph.com/docs/master/install/build-ceph/),不过 Docker 环境通过提供一个包含这些指令的 shell 脚本简化了这一步。

一个例外:ceph-dev 的 build-free 能力(见下文)——它通过用 RPM 系统包安装 Ceph 来做到免编译,同时你仍然能像往常一样使用本地 GitHub 仓库。

10.2.1 vstart 是什么

"vstart" 其实是 Ceph 仓库 src/ 目录里的一个 shell 脚本(src/vstart.sh),用来在执行它的机器上启动一个单节点 Ceph 集群。用它启动集群时,若干必需的以及一些可选的 Ceph 内部服务会自动启动。

vstart 是 Ceph Dashboard 三种最常用开发环境的基础。

更多见 Deploying a development cluster,开发者额外信息见 Developer Guide(https://docs.ceph.com/docs/master/dev/quick_guide/)。

10.2.2 基于宿主机 vs 基于 Docker 的开发环境

本册介绍三种都基于 vstart 的开发环境:

  1. vstart 跑在你的宿主机系统上
  2. vstart 跑在 Docker 环境里

两个 Docker 项目除了各自独立的开发分支和略有差异的做法,底层操作系统也不同:

Release ceph-dev-docker ceph-dev
Mimic openSUSE Leap 15 CentOS 7
Nautilus openSUSE Leap 15 CentOS 7
Octopus openSUSE Leap 15.2 CentOS 8
Master openSUSE Tumbleweed CentOS 8

重要 note:无论你选哪个环境,都需要在该环境里编译 Ceph。 如果你在宿主机上编译过 Ceph,切到 Docker 方案就得在 Docker 里重新编译;反之亦然(也可以另开一个独立仓库编译)。

ceph-dev 是唯一的例外:它提供 build-free 选项,通过 RPM 系统包安装 Ceph 实现免编译,同时你仍然可以用本地 GitHub 仓库工作。

基于宿主机的开发环境:

  • 不需要学 Docker、不需要 Docker 经验,可以直接上手;
  • 用于自动化(如 Ceph 编译)的脚本数量有限;
  • 没有预配置好、开箱即用的服务(Prometheus、Grafana 等);
  • 支持的宿主操作系统有限,取决于要用哪个 Ceph 版本;
  • 依赖需要装在你的宿主机上;
  • 你可能被迫升级宿主操作系统(例如因为编译 Ceph 用的 GCC 版本变了)。

基于 Docker 的开发环境:

  • 如果你还不熟悉 Docker,有一点学习开销;
  • 两个 Docker 项目都提供了帮你上手和自动化重复任务的脚本;
  • 都自带部分预配置的外部服务,可以用来对接或补充 Ceph Dashboard 特性,包括:
    • Prometheus
    • Grafana
    • Node-Exporter
    • Shibboleth
    • HAProxy
  • 与你宿主机用的操作系统无关。

10.2.3 在宿主机上跑 vstart

vstart 脚本通常从你的 build/ 目录这样调用:

../src/vstart.sh -n -d
  • -n:确保创建一个新的 vstart 集群,不会复用之前可能已创建的集群;
  • -d:在日志文件里开启 debug 消息。

还有更多选项,用 --help 参数可以列出。

vstart 输出末尾应该有 dashboard 及其 URL 的信息:

vstart cluster complete. Use stop.sh to stop. See out/* (e.g. 'tail -f out/????') for debug output.

dashboard urls: https://192.168.178.84:41259, https://192.168.178.84:43259, https://192.168.178.84:45259
  w/ user/pass: admin / admin

开发过程中(尤其是后端开发),你也会想偶尔检查 dashboard manager module 是否还在跑。 可以手工调用 ./bin/ceph mgr services,它会列出所有成功启用的服务的 URL。只有可通过 HTTP(S) 访问的服务才会列在那里,Ceph Dashboard 就是其中之一。应该类似这样:

$ ./bin/ceph mgr services
{
    "dashboard": "https://home:41931/"
}

默认这个环境给 Ceph Dashboard 用随机端口,你需要用上面这条命令查出来是哪个。

10.2.4 Docker

Docker 开发环境通常自带很多有用的脚本。例如 ceph-dev-docker 里有一个 start-ceph.sh,它会:清理日志文件、总是启动一个 Rados Gateway 服务、设置一些 Ceph Dashboard 配置选项、并自动运行一个 frontend proxy——全都在启动 vstart 集群之前或之后完成。

使用说明在各自仓库的 README 里(见 10.2.2 的两个链接)。

10.3 Frontend Development(前端开发)

在开发环境里启动 dashboard 之前,你需要先生成前端代码,然后要么使用一个已编译且运行的 Ceph 集群(例如由 vstart.sh 启动的),要么使用 standalone development web server。

构建过程基于 Node.js(https://nodejs.org/),需要安装 npm(https://www.npmjs.com/)。

10.3.1 前置条件与工具链

项 要求 / 说明
Node 20.13.1 或更高
NPM 10.5.2 或更高
nodeenv Ceph 构建时会创建一个装了 node 和 npm 的 virtualenv,可替代系统安装的 node/npm。用它之前先激活:. build/src/pybind/mgr/dashboard/node-env/bin/activate;用完 deactivate 退出
Angular CLI 如果你没有全局安装 Angular CLI(https://github.com/angular/angular-cli),那么执行 ng 命令时需要前面加一个 npm run

技术栈小结(官方明确提到的):Angular + Angular CLI + Node.js/npm + TypeScript + SCSS + HTML + Bootstrap(CSS 框架)+ Jest(单元测试)+ Cypress(E2E)+ Applitools Eyes(视觉回归)+ ngx-toastr(通知)+ Fork Awesome(图标)+ compodoc(代码文档)。

10.3.2 安装依赖

在 src/pybind/mgr/dashboard/frontend 目录里跑:

npm ci

10.3.3 增加或更新依赖包

npm install <PACKAGE_NAME>
npm ci

10.3.4 起开发服务器

  1. 基于 proxy.conf.json.sample 创建 proxy.conf.json 文件;
  2. 跑 npm start 起 dev server;
  3. 浏览器打开 http://localhost:4200/。改动任何源文件后应用会自动 reload。

10.3.5 代码脚手架

ng generate component component-name

也可以生成其他类型:

ng generate directive|pipe|service|class|guard|interface|enum|module

没装全局 Angular CLI 时,前面加 npm run。

10.3.6 构建项目

npm run build

构建产物存放在 dist/ 目录。用 --prod flag 做生产构建:

npm run build -- --prod

然后访问 https://localhost:8443。

10.3.7 构建代码文档

npm run doc-build

会在 documentation/ 目录生成代码文档。要在本地用浏览器访问:

npm run doc-serve

然后打开 http://localhost:8444。用 npm run compodoc -- <opts> 可以完全配置它(https://compodoc.app/guides/usage.html)。

10.3.8 代码 lint 与格式化

官方用于所有 TS、SCSS 和 HTML 文件的工具:

工具 链接
codelyzer http://codelyzer.com/
html-linter https://github.com/chinchiheather/html-linter
htmllint-cli https://github.com/htmllint/htmllint-cli
Prettier https://prettier.io/
ESLint https://eslint.org/
stylelint https://stylelint.io/

官方加了两个 npm script:

npm run lint   # 用所有 linter 检查前端文件
npm run fix    # 尝试修复所有检测到的 linting 错误

10.3.9 Ceph Dashboard 与 Bootstrap

目前 Ceph Dashboard 用 Bootstrap 作为 CSS 框架,这意味着大部分 SCSS 和 HTML 代码可以利用 Bootstrap 提供的所有 utility 和其他优势。

历史问题:过去经常使用自定义样式,导致越来越多的「只用过一次的变量」和「重复定义的变量」——有些忘了删,有些导致样式不一致(有人忘了改颜色或调整某个自定义 SCSS class)。

查当前 Bootstrap 版本:看 package.json,搜:

  • bootstrap:Bootstrap 版本;
  • @ng-bootstrap:使用的 Angular bindings 版本。

官方明确要求(今后访问组件时请照做):

  1. 这段 HTML/SCSS 代码用了自定义代码吗? 如果是:有必要吗? → 先清理干净再改你想修/想改的东西。
  2. 如果你在创建新组件:尽可能合理地使用 Bootstrap!别重复造轮子。
  3. 如果可能:查一下 Bootstrap 有没有「如何正确扩展它」的指南来实现你想要的效果。

官方给的理由:代码越像 Bootstrap,就越容易 theme、越容易维护、bug 越少;Bootstrap 本身考虑了可用性和用户体验,能同时提升这两点;最大好处是我们要维护的代码更少——对新手更好读,对已熟悉代码的人更容易。

10.4 编写前端单元测试

为高效编写单元测试,官方提供了一小套在 test suite 里使用的工具,位于:

src/pybind/mgr/dashboard/frontend/src/testing/

特别要看 unit-test-helper.ts。 你能找到:

工具 说明
configureTestBed 取代初始的 TestBed 方法。接受与 TestBed.configureTestingModule 相同的参数。 用它在开发时跑测试会快很多,因为它不会每次测试都从零重建一切。要用默认行为,传 true 作为第二个参数
PermissionHelper 帮助判断基于当前权限和列表中的选择,是否显示了正确的操作
FormHelper 用几个简单方法让测试表单容易得多。可以设置单个或多个 control、断言某个 control 是否有效或有错误,或者用一个方法同时做这两件事。此外还能断言某个 template element 或多个 elements 在渲染后的模板里可见

10.5 运行前端单元测试

npm run test

通过 Jest(https://facebook.github.io/jest/)执行单元测试。

如果所有测试都报错,可能是因为 Jest 或其他东西被更新了。可以尝试这样解决:

rm -rf dist node_modules
npm install

或清 Jest 缓存:

npx jest --clearCache

10.6 运行端到端(E2E)测试

Ceph Dashboard 用 Cypress(https://www.cypress.io/)跑前端 E2E 测试。

10.6.1 E2E 前置条件

  • 需要先构建前端。
  • 在某些环境下,取决于你的用户权限和 CYPRESS_CACHE_FOLDER,可能需要用 --unsafe-perm flag 跑 npm ci。
  • 可能需要安装额外的包才能跑 Cypress。跑 npx cypress verify 来验证。

10.6.2 run-frontend-e2e-tests.sh(全量 E2E 首选)

官方的 run-frontend-e2e-tests.sh 脚本是做全规模 e2e 跑时的首选方案。它会:验证所需组件是否都装了 → 起一个新的 vstart 集群 → 跑完整测试套件。

$ cd src/pybind/mgr/dashboard
$ ./run-frontend-e2e-tests.sh
方面 说明
报告 可以在终端跟踪 e2e 报告;失败用例的截图在 src/pybind/mgr/dashboard/frontend/cypress/screenshots/
Device 用 -d flag 强制指定设备:<chrome|chromium|electron|docker>,例如 ./run-frontend-e2e-tests.sh -d <chrome>
Remote 默认脚本会停掉并重启一个新的 vstart 集群。想在 ceph 环境外跑测试,需要用 -r 手动指定 dashboard url,可选地用 -u、-p 指定凭据:./run-frontend-e2e-tests.sh -r <DASHBOARD_URL> -u <E2E_LOGIN_USER> -p <E2E_LOGIN_PWD>
Note 用 docker 作为 device 时,可能需要 sudo 权限跑脚本

10.6.3 run-cephadm-e2e-tests.sh(cephadm 场景)

run-cephadm-e2e-tests.sh 跑 E2E 测试的一个子集,用来验证 Dashboard 与作为 Orchestrator 后端的 cephadm 行为是否正确。

前置条件:本机需要安装 KCLI(https://kcli.readthedocs.io/en/latest/)和 Node.js。

配置 KCLI plan:

$ sudo chown -R $(id -un) /var/lib/libvirt/images
$ mkdir -p /var/lib/libvirt/images/ceph-dashboard
$ kcli create pool -p /var/lib/libvirt/images/ceph-dashboard ceph-dashboard
$ kcli create network -c 192.168.100.0/24 ceph-dashboard

Note:这个脚本目标是作为 jenkins job 跑的,所以只在 jenkins 环境里触发清理。在本地,用户需要自己决定何时关掉集群(例如调试完之后)。

启动 E2E 测试:

$ cd <your/ceph/repo/dir>
$ sudo chown -R $(id -un) src/pybind/mgr/dashboard/frontend/{dist,node_modules,src/environments}
$ ./src/pybind/mgr/dashboard/ci/cephadm/run-cephadm-e2e-tests.sh

Note(Fedora 35):挂载 shared_folders 时可能出现权限错误,可以修:

$ sudo setfacl -R -m u:qemu:rwx <abs-path-to-your-user-home>

或者给你的 $HOME 目录设置合适的权限。

开发模式启动集群(前端构建进入 watch mode,你只需刷新页面就能看到改动):

$ ./src/pybind/mgr/dashboard/ci/cephadm/start-cluster.sh --dev-mode

Note:如果需要一个可直接部署服务的集群(monitor 守护进程分布在多台主机上且有足够 OSD),加 --expanded。

用改动测一遍:

$ ./src/pybind/mgr/dashboard/ci/cephadm/run-cephadm-e2e-tests.sh

关闭集群:

$ kcli delete plan -y ceph
$ # 在 development mode 下,还要杀掉 npm build watch 进程(例如 pkill -f "ng build")

10.6.4 其他运行方式(活跃开发期推荐)

在活跃开发期间,官方不建议跑上面那个脚本,因为它没有为「文件持续变动」做准备。改用下面这些命令:

命令 说明
npm run e2e 跑 ng serve 并打开 Cypress Test Runner
npm run e2e:ci 跑 ng serve 并把 Cypress Test Runner 跑一次
npx cypress run 直接调 cypress 跑 Cypress Test Runner。需要有一个正在运行的前端服务器
npx cypress open 直接调 cypress 打开 Cypress Test Runner。需要有一个正在运行的前端服务器

直接调 Cypress 的好处:可以使用任何可用的 flags(https://docs.cypress.io/guides/guides/command-line.html#cypress-run)来定制测试运行,而且不必每次都启动前端服务器。

用 open 类命令会打开一个 cypress 应用,里面能看到所有测试文件并逐个运行。这会以 watch mode 运行,所以改动测试文件会重新触发测试跑。这在 docker 里没法用,因为它需要 X11 环境才能打开。

默认 Cypress 会在 https://localhost:4200/ 找页面。 如果你用别的 URL 提供服务,需要通过导出环境变量 CYPRESS_BASE_URL 来配置,例如:

CYPRESS_BASE_URL=https://localhost:41076/ npx cypress open

10.6.5 CYPRESS_CACHE_FOLDER

通过 npm 安装 cypress 时,cypress app 的二进制也会被下载并存在一个缓存目录里,这样每次跑 npm ci 甚至在其他项目用 cypress 时都不必重新下载。

  • Cypress 默认用 ~/.cache 存二进制。
  • 为避免改动用户 home 目录,Ceph 把这个目录改成了 /ceph/build/src/pybind/mgr/dashboard/cypress——所以当你构建 ceph 或跑 run-frontend-e2e-tests.sh 时,Cypress 会用这个目录。
  • 当你用任何其他命令安装或运行 cypress 时,它会回到默认目录。
  • 官方建议:导出 CYPRESS_CACHE_FOLDER 环境变量指向一个固定目录,这样无论用哪个命令都用同一个目录。

10.7 编写 E2E 测试

10.7.1 PageHelper 类

PageHelper 类用于各种页面或 suite 都能用的通用代码。

示例方法:

  • navigateTo() —— 导航到特定页面并等待加载完成;
  • getFirstTableCell() —— 返回第一个表格单元格。也可以传一个期望内容的字符串,它会返回第一个包含该内容的单元格;
  • getTabsCount() —— 返回 tab 的数量。

凡是可能在多个页面上有用的方法都放这里。 另外,增强 PageHelper 派生类的方法也属于这里。一个好例子是 restrictTo() 装饰器:它确保 PageHelper 子类里实现的某个方法是在正确的页面上被调用的;如果不是,它会显示一条对开发者友好的警告。

10.7.2 PageHelper 的子类

Helper Methods:为了让针对某个特定 suite 的代码可复用,确保把它放在 PageHelper 的派生类里。例如对 pool suite,这样的方法就是 create()、exist() 和 delete()——它们对 pool 是特有的,但对其他 suite 也有用。

返回只能在特定页面上找到的 HTML 元素的方法,应该实现在 PageHelper 子类的 helper methods 里,或者作为 PageHelper 子类自己的方法。

Using PageHelpers:在任何 suite 里,应该实例化并直接调用具体 Helper 类的实例:

const pools = new PoolPageHelper();

it('should create a pool', () => {
  pools.exist(poolName, false);
  pools.navigateTo('create');
  pools.create(poolName, 8);
  pools.exist(poolName, true);
});

10.7.3 代码风格:describe() vs it()

官方请读者参考 Cypress Core Concepts(https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Cypress-Can-Be-Simple-Sometimes)来更好地理解如何编写和组织测试。

describe() vs it():

两者都是函数块,意味着测试需要的任何可执行代码都可以放在任一块里。但 Typescript 的作用域规则仍然适用,因此在 describe 里声明的任何变量对其中 it() 块都是可用的。

describe() 通常是测试的容器,让你把测试拆成多个部分。同样,测试运行前必须做的任何 setup 都可以在 describe() 块里初始化。示例:

describe('create, edit & delete image test', () => {
  const poolName = 'e2e_images_pool';

  before(() => {
    cy.login();
    pools.navigateTo('create');
    pools.create(poolName, 8, 'rbd');
    pools.exist(poolName, true);
  });

  beforeEach(() => {
    cy.login();
    images.navigateTo();
  });

  //...

});

如上所示,我们可以在 describe 里初始化变量 poolName,也可以在测试套件开始前跑命令(创建 pool)。describe() 块的消息应该说明这个测试套件是什么。

it() 块通常是** overarching test 的一部分**,包含测试套件的功能,各自扮演独立角色。示例:

describe('create, edit & delete image test', () => {
  //...

  it('should create image', () => {
    images.createImage(imageName, poolName, '1');
    images.getFirstTableCell(imageName).should('exist');
  });

  it('should edit image', () => {
    images.editImage(imageName, poolName, newImageName, '2');
    images.getFirstTableCell(newImageName).should('exist');
  });

  //...
});

it() 的命名规范(明确要求):每个 it() 块的消息都应该小写,并且写成能让 "it" 作为前缀的形式。

  • 好:it('edits the test image' () => ...) —— 用 it() 作前缀语法通顺;
  • 不好:it('image edit test' () => ...) —— 不通顺。

it() 应该描述这个单独的测试在做什么、期望发生什么。

10.8 视觉回归测试(Visual Regression Testing)

Ceph Dashboard 用 Applitools Eyes(https://applitools.com/products-eyes/)——一个 AI 驱动的自动化视觉回归测试工具。Applitools 与现有的 Cypress E2E 测试集成。

测试位置:ceph/src/pybind/mgr/dashboard/frontend/cypress/integration/visualTests,命名约定:<component-name>.vrt-spec.ts。

10.8.1 本地运行

需要 Applitools API key(没有可以注册免费账号)。拿到后导出为环境变量 APPLITOOLS_API_KEY。

然后就可以像普通 cypress E2E 测试一样跑:用 npx cypress open,或 headless 模式 npx cypress run。

10.8.2 截图(Capturing Screenshots)

Baseline screenshots(基线截图) 是 checkpoint screenshots(或者说你 feature 分支上的截图)用来比对的对象。

做法:先对 master 分支跑测试捕获 baseline,然后切到你的 feature 分支再跑一次捕获 checkpoint。

然后登录 applitools.com,落地页会看到 applitools eyes test runner,里面能看到你所有的截图。如果 baseline 和 checkpoint 之间有任何视觉回归或差异(diff),会用一层遮罩高亮出来。

10.8.3 写更多视觉回归测试

参考 Applitools 官方 cypress sdk 文档:https://www.npmjs.com/package/@applitools/eyes-cypress#usage

10.8.4 Jenkins 中的视觉回归测试

目前所有视觉回归测试在 ceph dashboard tests(https://jenkins.ceph.com/job/ceph-dashboard-pull-requests)这个 GitHub check 下的 Jenkins job 里跑。

10.8.5 接受或拒绝差异

目前只有 ceph dashboard 团队有 applitools test runner 的读写权限。如果测试报告了差异:

  • 想接受它们并更新 baseline 截图,或
  • 差异是真实的回归,想判为失败,

按这个指南操作:https://applitools.com/docs/topics/test-manager/pages/page-test-results/tm-accepting-and-rejecting-steps.html

10.8.6 调试回归

本地跑测试时如果报告了回归,可以利用 Applitools 的 Root Cause Analysis 特性(https://applitools.com/docs/topics/test-manager/viewers/root-cause-analysis.html)找回归原因。

10.9 前端单元测试 vs E2E 测试(FAQ)

10.9.1 它们各自是为什么设计的?

E2E 测试:需要一个功能完整的系统,测试应用所有组件的交互(Ceph、back-end、front-end)。E2E 测试被设计成模仿用户与应用交互时的行为——例如创建/编辑/删除一个条目这样的工作流。测试还应验证某些条目按用户点击 UI 时会看到的那样被显示(例如一个菜单项,或者一个在测试中被创建的 pool 及其属性应显示在表格里)。

Angular 单元测试:顾名思义,是更小代码单元的测试。这些测试用于测试各种 Angular 组件(如 services、pipes 等)。它们不需要连接后端,因此独立于后端。 后端预期的数据在前端被 mock,用这些数据就能测试前端功能而不需要来自后端的真实数据。简单情况下是「静态输入 + 函数调用 + 期望的静态输出」;更复杂的例子包括组件的 state(component class 的属性)如何根据给定输入改变输出。

10.9.2 什么样的测试算「有效」?

这不好回答,但用与已有 dashboard 测试相同方式写的新测试,一般应视为有效。

  • 单元测试应聚焦在被测试的组件上(Angular component、directive、service、pipe 等)。
  • E2E 测试应聚焦测试整个应用的功能。整体 E2E 测试中大约三分之一应该验证用户可见元素的正确性。

10.9.3 一个测试应该长什么样?

  • 单元测试应聚焦所述目的,不应试图在同一个 it 块里测其他东西。
  • E2E 测试应包含一段描述,要么验证用户可见元素的正确性,要么验证一个完整流程(例如 pool 的创建/验证/删除)。

10.9.4 应该覆盖什么?

  • E2E 测试应主要(但不排他)覆盖与后端的交互。这样与后端的交互就被用来写成集成测试。
  • 单元测试应主要覆盖组件的关键或复杂功能(Angular Components、Services、Pipes、Directives 等)。

10.9.5 不应该覆盖什么?

  • 避免重复测试:不要为已经被前端单元测试覆盖的东西写 E2E 测试,反之亦然。(可能无法完全避免重叠。)
  • 单元测试不应被用来大量点击组件;E2E 测试不应被用来大量测试单个 Angular 组件。

10.9.6 最佳实践 / 指导方针

作为通用指导,官方遵循 70/20/10 方法:

类型 占比
Unit tests 70%
Integration tests 20%
End-to-end tests 10%

进一步阅读:https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html 及其中提到的 "Testing Pyramid"。

10.10 更多帮助与 Generator 示例

用 ng help 获取更多 Angular CLI 帮助,或看 Angular CLI README(https://github.com/angular/angular-cli/blob/master/README.md)。

Generator 示例(官方原文):

# Create module 'Core'
src/app> ng generate module core -m=app --routing

# Create module 'Auth' under module 'Core'
src/app/core> ng generate module auth -m=core --routing
or, alternatively:
src/app> ng generate module core/auth -m=core --routing

# Create component 'Login' under module 'Auth'
src/app/core/auth> ng generate component login -m=core/auth
or, alternatively:
src/app> ng generate component core/auth/login -m=core/auth

10.11 前端 TypeScript 代码风格指南(推荐)

按来源对 import 分组,组之间用空行分隔。 来源分组可以是 Angular、external 或 internal。

import { Component } from '@angular/core';
import { Router } from '@angular/router';

import { ToastrManager } from 'ngx-toastr';

import { Credentials } from '../../../shared/models/credentials.model';
import { HostService } from './services/host.service';

10.12 可复用前端组件

有多个组件可以在不同页面上复用,这些组件声明在 components module 里:

src/pybind/mgr/dashboard/frontend/src/app/shared/components

cd-helper 组件

这个组件应该用来给用户提供补充信息。

<cd-helper>
  Some <strong>helper</strong> html text
</cd-helper>

10.13 术语与措辞(Terminology and wording)

不要使用 Ceph 组件名,官方建议用逻辑/通用名称:用 Block 而不是 RBD,用 Filesystem 而不是 CephFS,用 Object 而不是 RGW。不过由于 Ceph-Dashboard 无法完全隐藏 Ceph 内部,一些 Ceph 专有名称可能仍然可见。

动作标签和其他文本元素(表单标题、按钮等)的措辞,官方选择遵循这些指南:https://www.patternfly.org/styles/terminology-and-wording/#terminology-and-wording-for-action-labels

经验法则:大多数表单用 'Create' 和 'Delete',而不是 'Add' 和 'Remove'——除非某个已创建的条目被加入或移出一组条目(例如:给用户 'Add permission' vs 'Create (new) permission')。

为了强制使用这套措辞,创建了一个 ActionLabelsI18n service,为 UI 元素提供翻译好的标签。

10.14 前端品牌定制(Frontend branding)

每个厂商都可以按自己的需要定制 'Ceph dashboard'。无论是 logo、HTML 模板还是 TypeScript,frontend 文件夹里的每个文件都可以被替换。

替换方法:打开 ./frontend/angular.json,滚到 production 配置里的 fileReplacements 段。在这里添加你想定制的文件。

官方命名建议:把定制版文件放在与原文件同一个目录,并在文件名里、紧挨着扩展名之前加一个 .brand。一个 fileReplacement 例如:

{
  "replace": "src/app/core/auth/login/login.component.html",
  "with": "src/app/core/auth/login/login.component.brand.html"
}

serve 或 build 定制后的 UI:

$ npm run start -- --prod

或

$ npm run build -- --prod

两个已知限制:

  1. serve 或 build UI 时目前无法同时使用多个 configuration。也就是说,只为 branding 的 fileReplacements 单开一个 configuration 不可行,因为你反正要用 production configuration(https://github.com/angular/angular-cli/issues/10612)。
  2. fileReplacements 目前不支持 glob 表达式。在该特性实现之前,你必须手工把 file replacements 加到 angular.json(https://github.com/angular/angular-cli/issues/12354)。

但你仍应坚持建议的命名方案,因为一旦未来支持 glob,你就容易迁移。

改变量默认值或加自己的变量:覆盖 ./frontend/src/styles/vendor/_variables.scss,直接重新赋值你想改的变量,例如:

$color-primary: teal;

覆盖或扩展默认 CSS:把你的样式加到 ./frontend/src/styles/vendor/_style-overrides.scss。

10.15 UI Style Guide

样式指南的创建是为了记录 Ceph Dashboard 的标准,并在整个项目中保持一致性,让贡献者更容易设计 mockup 和 Dashboard 设计。

Ceph Dashboard 的开发环境启用了 live reloading,UI 里的改动会反映在打开的浏览器窗口中。Ceph Dashboard 使用 Bootstrap 作为主要的第三方 CSS 库。

避免重复代码。 尽量复用已有的 SCSS 声明,与现有 UI 保持一致。

总是先检查是否已有类似你想写的代码。 应尽量保持与现有代码相同的 look-and-feel。

10.15.1 Colors

Ceph Dashboard UI 中用到的所有颜色都列在 frontend/src/styles/defaults/_bootstrap-defaults.scss。

  • 使用新颜色时,总是在 _bootstrap-defaults.scss 里定义颜色变量,并使用变量而不是硬编码颜色值,这样颜色的改动会反映到相似的 UI 元素上。
  • Ceph Dashboard 的主色是 $primary,用于导航组件,以及作为表单 input 组件的 $border-color。
  • 次要色是 $secondary,是 Ceph Dashboard 的背景色。

10.15.2 Buttons

按钮用于执行动作,如 "Submit"、"Edit"、"Create"、"Update"。

场景 主按钮 次按钮
Forms 用 cd-submit-button 组件;文字应与表单标题相同且遵循 title case 用 cd-back-button 组件;文字应为 Cancel
Modals 用 cd-submit-button 组件;文字遵循 title case 且对应要执行的动作 用 cd-back-button 组件;文字应为 Close

其他约定:

  • 位置:Perform action 按钮始终在右,Cancel 按钮始终在左。
  • Disclosure Button:用于让用户显示/隐藏界面中的附加内容。
  • Action Button:用于执行编辑或更新组件等动作。所有 action button 都应有与其动作对应的图标,按钮文字遵循 title case。按钮颜色应与表单主按钮颜色一致。
  • Drop Down Buttons:用于显示预定义的动作列表。所有 dropdown 按钮都有与其动作对应的图标。

用文本超链接作为导航,引导用户到应用中的新页面,或锚定到页面内的某个 section。超链接的颜色应为 $primary。

10.15.4 Forms

  • 用红色描边标记无效表单字段并显示有意义的错误消息。消息用红色字体,尽可能具体。
  • 必填字段的错误消息必须 exactly 是:This field is required.
  • 用绿色描边和表单末尾的绿色对勾标记有效表单。
  • Section 的标题不应比父级更大。

10.15.5 Modals

  • 模糊化背景中的界面元素,让 modal 内容成为焦点。
  • Modal 的标题应反映它能执行的动作,并在 modal 顶部清楚写明。
  • 在 footer 用 cd-back-button 组件关闭 modal。

10.15.6 Icons

  • 使用 Fork Awesome(https://forkaweso.me/Fork-Awesome/)的 class 作为图标。
  • 已使用图标清单在 src/app/shared/enum/icons.enum.ts,HTML 里应引用这些,便于日后修改。
  • 图标与文字相邻时,水平居中对齐。图标堆叠时,也应垂直居中对齐。
  • 按钮配小图标。通知配大图标。

10.15.7 Navigation

  • 局部导航用 tabs。
  • 整体导航用可展开的垂直导航,按需折叠和展开条目。

10.15.8 Alerts and notifications

类型 颜色 class
默认通知 text-info
成功通知 text-success
失败通知 text-danger

10.16 前端错误处理

处理前端错误有一个通用 Error Component,位于:

./src/pybind/mgr/dashboard/frontend/src/app/core/error

上报一个新错误:只需扩展 error.ts 文件里的 DashboardError 类,并为新错误添加特定的 header 和 message。

已经有一些通用错误类就位,可以在不同场景调用和复用,例如 DashboardNotFoundError 和 DashboardForbiddenError。

用法示例:

throw new DashboardNotFoundError();

10.17 国际化(Internationalization, i18n)

10.17.1 从源码提取消息

在 src/pybind/mgr/dashboard/frontend 里跑:

$ npm run i18n:extract

这会先从 HTML 模板里提取所有被标记的消息,然后把 TypeScript 文件里所有被标记的字符串加到翻译模板中。

由于 Angular 本身仍不支持从 TypeScript 文件提取,官方使用 ngx-translator(https://github.com/ngx-translate/i18n-polyfill)的 extractor 来解析 TypeScript 文件。

命令成功跑完后,应该已经创建或更新了文件 src/locale/messages.xlf。

该文件不被 git 跟踪,你可以用它来离线开始翻译,或用于添加/更新 transifex 上的资源文件。

10.17.2 支持的语言

所有支持的语言都应注册在 supported-languages.enum.ts 的两个 export 里,并在 language-selector.component.spec.ts 里有对应的测试。

SupportedLanguages enum 提供默认语言选择的列表。

10.17.3 翻译流程

为了便利 Dashboard 的翻译流程,官方使用一个叫 transifex(https://www.transifex.com/)的 web 工具。

想帮忙翻译任何语言,只需去 transifex 项目页(https://www.transifex.com/ceph/ceph-dashboard/),加入项目,就可以立即开始翻译。

所有翻译会被 review,之后推送到 upstream。

10.17.4 更新已翻译的消息

  1. 下载 transifex CLI tool(https://github.com/transifex/cli)

  2. 创建 API token(https://www.transifex.com/user/settings/api/)

  3. 推送翻译:

    $ tx push -s
    

    这会把源文件推到 transifex。

  4. 拉取翻译:

    $ tx pull -r ceph-dashboard.<resource_slug> -f
    

    例如 tx pull -r ceph-dashboard.main。这会拉取该 resource 的所有翻译。

10.17.5 给 transifex 加一个新的 release resource

为了组织翻译,官方为每一个 Ceph release 创建一个 transifex resource。这意味着一旦发布了新版本,release 分支上的 src/pybind/mgr/dashboard/frontend/.tx/config 就需要更新。

把:

resource_name          = Main

替换成:

resource_name          = <Release-name>

例如 Tentacle release 的 resource 定义:

resource_name          = Tentacle

并把:

[o:ceph:p:ceph-dashboard:r:main]

替换成:

[o:ceph:p:ceph-dashboard:r:<release-name>]

例如 Tentacle release:

[o:ceph:p:ceph-dashboard:r:tentacle]

完成后推送翻译:

$ tx push -s

Note

:只有 <Release-name> 首字母大写。

10.17.6 i18n 书写建议(官方给了 4 组 avoid/recommended)

1)字符串应与元素在同一行开始和结束:

<!-- avoid -->
<span i18n>
  Foo
</span>

<!-- recommended -->
<span i18n>Foo</span>


<!-- avoid -->
<span i18n>
  Foo bar baz.
  Foo bar baz.
</span>

<!-- recommended -->
<span i18n>Foo bar baz.
  Foo bar baz.</span>

2)孤立的插值不应被翻译:

<!-- avoid -->
<span i18n>{{ foo }}</span>

<!-- recommended -->
<span>{{ foo }}</span>

3)用在句子里的插值应保留在翻译中:

<!-- recommended -->
<span i18n>There are {{ x }} OSDs.</span>

4)移除翻译上下文之外的元素:

<!-- avoid -->
<label i18n>
  Profile
  <span class="required"></span>
</label>

<!-- recommended -->
<label>
  <ng-container i18n>Profile<ng-container>
  <span class="required"></span>
</label>

5)保留影响句子的元素:

<!-- recommended -->
<span i18n>Profile <b>foo</b> will be removed.</span>

10.18 无障碍(Accessibility)

Ceph Dashboard 的很多部分以 WCAG 2.1(Web Content Accessibility Guidelines 2.1,https://www.w3.org/TR/WCAG21/)level A 无障碍一致性指南为模型。通过实施无障碍最佳实践,你在改善 Ceph Dashboard 对盲人和视障用户的可用性。

10.18.1 提交改动前的 5 项自检

  1. 给可操作的 HTML 元素加 ARIA labels 和 descriptions(https://www.w3.org/TR/wai-aria/)。
  2. 别忘了给 ARIA labels/descriptions 或任何用户可读文本打上翻译标记(i18n-title、i18n-aria-label...)。
  3. 给行为与其本意不同(例如 <a> 标签当 <button> 用)或提供扩展行为(roles)的 HTML 元素加 ARIA roles(https://www.w3.org/TR/wai-aria/#usage_intro)。
  4. 避免糟糕的色彩对比选择(前景-背景)。见 10.18.3 的工具。
  5. 测试菜单或下拉框时,确保在展开和收起两种状态下都用无障碍检查器扫一遍。 有时问题在菜单收起时被隐藏。

10.18.2 无障碍检查器

工具 链接
Accessibility insights plugin https://accessibilityinsights.io/downloads/
Site Improve plugin https://www.siteimprove.com/integrations/browser-extensions/
Axe devtools https://www.deque.com/axe/devtools/

用两个或更多工具测试可以大幅提升无障碍违规的检出率。

10.18.3 色彩对比检查器

工具 链接
Accessible web color-contrast checker https://accessibleweb.com/color-contrast-checker/
Colorsafe generator https://colorsafe.co/

10.18.4 无障碍 linter

如果你用 VSCode,可以装 axe accessibility linter(https://marketplace.visualstudio.com/items?itemName=deque-systems.vscode-axe-linter),能在开发时帮你发现并修复潜在问题。

10.18.5 无障碍测试

基于 Cypress 的 e2e 测试套件支持加入无障碍测试,使用 axe-core(https://github.com/dequelabs/axe-core)和 cypress-axe(https://github.com/component-driven/cypress-axe)。也可以直接使用自定义 Cypress 命令 cy.checkAccessibility。这是防止高影响组件出现无障碍回归的好办法。

测试位于 dashboard 的 a11y 目录:./src/pybind/mgr/dashboard/frontend/cypress/integration/a11y。示例:

describe('Navigation accessibility', { retries: 0 }, () => {
  const shared = new NavigationPageHelper();

  beforeEach(() => {
    cy.login();
    shared.navigateTo();
  });

  it('top-nav should have no accessibility violations', () => {
    cy.injectAxe();
    cy.checkAccessibility('.cd-navbar-top');
  });

  it('sidebar should have no accessibility violations', () => {
    cy.injectAxe();
    cy.checkAccessibility('nav[id=sidebar]');
  });

});

10.18.6 额外指南

如果你不确定该遵循哪种 UI 模式来实施无障碍修复,可以使用 patternfly 的指南:https://www.patternfly.org/v4/accessibility/accessibility-fundamentals

10.19 Backend Development(后端开发)

Dashboard 模块的 Python 后端代码需要若干 Python 模块,列在 requirements.txt 里。用 pip(https://pypi.python.org/pypi/pip)安装:

pip install -r requirements.txt

在 src/pybind/mgr/dashboard 目录里执行。

如果你用 ceph-dev-docker 开发环境,只需从顶层目录跑 ./install_deps.sh 即可安装它们。

10.20 后端单元测试

Dashboard 里有两种后端测试:

  1. 基于 tox 的单元测试
  2. 基于 Teuthology 的 API 测试

10.20.1 基于 tox 的单元测试

官方提供了一个 tox 配置文件,会在 Python 3 下跑单元测试,并运行 linting 工具来保证代码统一性。

前置:需要安装 tox 和 coverage。

dnf install python-tox python-coverage    # Fedora Linux

或:

$ pip install tox
$ pip install coverage

跑测试:在 dashboard 目录(即 tox.ini 所在目录)跑 src/script/run_tox.sh:

## Run Python 3 tests+lint commands:
$ ../../../script/run_tox.sh --tox-env py3,lint,check

## Run Python 3 arbitrary command (e.g. 1 single test):
$ ../../../script/run_tox.sh --tox-env py3 "" tests/test_rgw_client.py::RgwClientTest::test_ssl_verify

也可以直接跑 tox 而不是 run_tox.sh:

## Run Python 3 tests command:
$ tox -e py3

## Run Python 3 arbitrary command (e.g. 1 single test):
$ tox -e py3 tests/test_rgw_client.py::RgwClientTest::test_ssl_verify

自动按 PEP8 修复和格式化 Python 文件:

run_tox.sh --tox-env fix
# 或
tox -e fix

覆盖率:跑测试时会收集后端代码的 coverage 信息。可以看 tox 输出的 coverage 信息,或在 tox 成功跑完后执行:

$ coverage html

这会创建一个 htmlcov 目录,包含后端代码覆盖率的 HTML 表示。

10.20.2 基于 Teuthology 的 API 测试

怎么跑已有的 API 测试:为了在真实 Ceph 集群上跑 API 测试,官方借助 Teuthology 框架。好处是能抓到因 Ceph 内部代码变更而产生的 bug。

官方的 run-backend-api-tests.sh 脚本会在跑 Teuthology 测试之前启动一个 vstart Ceph 集群,跑完后再停掉集群。(当然这要求你已经构建/编译过 Ceph。)

$ ./run-backend-api-tests.sh

或者指定测试名跑一个或多个特定测试:

$ ./run-backend-api-tests.sh tasks.mgr.dashboard.test_pool.PoolTest

或者 source 该脚本手工跑:

$ source run-backend-api-tests.sh
$ run_teuthology_tests [tests]...
$ cleanup_teuthology

怎么写自己的测试:两种方式。

  1. 扩展 qa/tasks/mgr/dashboard 目录里已有的某个测试类。
  2. 添加你自己的 API 测试模块(例如你在创建一个新 controller 时)。做法:把包含新测试类的文件加到 qa/tasks/mgr/dashboard 目录,并在那里实现所有测试。

别忘了把新模块的路径加到 qa/suites/rados/mgr/tasks/dashboard.yaml 的 modules 段。

短示例:假设你创建了一个新 controller my_new_controller.py 和相关的测试模块 test_my_new_controller.py,你需要把 tasks.mgr.dashboard.test_my_new_controller 加到 dashboard.yaml 文件的 modules 段。

另外,如果你在删除测试模块,记住同时删掉相关的段,否则 Teuthology 测试运行会失败。

合入前的要求:

  • 提交 PR 前,请在你的开发环境里跑过 API 测试(如上所述)。
  • 还要确保一次完整的 Teuthology/sepia lab QA run(基于你的改动)已成功完成,然后才能被 merge。
  • 你不需要自己调度 QA run——只要你觉得可以合了(例如 make check 成功、PR 已 approved、所有评论都已处理),就给 PR 加上 'needs-qa' 标签。有 Teuthology/sepia lab 访问权限的开发者之一会处理并把结果反馈给你。

10.21 如何新增一个 controller?

一个 controller 是一个继承自 BaseController 类的 Python class,并用 @Controller、@ApiController 或 @UiApiController 之一装饰。这个 Python class 必须存放在 controllers 目录下的一个 Python 文件里。Dashboard 模块会在启动时自动加载你的新 controller。

三个装饰器的关系与用途:

装饰器 用途 URL 前缀
@Controller 其他任何类型的 controller 见下
@ApiController 提供 API 风格 REST 接口的 controller(是 @Controller 的特化) /api
@UiApiController 被 UI 消费但不属于 'public' API 的端点(也是 @Controller 的特化) /ui-api

一个 controller 有一个 URL prefix path,在 controller 装饰器里指定,该 controller 暴露的所有端点都共享这个前缀。

一个 controller 的端点通过在 controller class 上实现一个用 @Endpoint 装饰的方法来暴露。

示例:在 controllers 目录创建 ping.py:

from ..tools import Controller, ApiController, UiApiController, BaseController, Endpoint

@Controller('/ping')
class Ping(BaseController):
  @Endpoint()
  def hello(self):
    return {'msg': "Hello"}

@ApiController('/ping')
class ApiPing(BaseController):
  @Endpoint()
  def hello(self):
    return {'msg': "Hello"}

@UiApiController('/ping')
class UiApiPing(BaseController):
  @Endpoint()
  def hello(self):
    return {'msg': "Hello"}

URL 生成规则:

Controller hello 端点的 URL 规则
Ping https://mgr_hostname:8443/ping/hello(HTTP GET) controller URL path /ping 拼接方法名 hello
ApiPing https://mgr_hostname:8443/api/ping/hello(HTTP GET) API controller URL path /ping 被 /api 前缀修饰,再拼方法名
UiApiPing https://mgr_hostname:8443/ui-api/ping/hello 被 /ui-api 前缀修饰

内部实现上:

@ApiController('/ping')   <=>   @Controller('/ping', base_url="/api")
@UiApiController('/ping') <=>   @Controller('/ping', base_url="/ui-api")

10.21.1 @Endpoint 装饰器支持的参数

参数 默认值 说明
method="GET" GET 允许访问该端点的 HTTP 方法
path="/<method_name>" 方法名 端点的 URL path,不含 controller URL path 前缀
path_params=[] [] 对应 URL path 参数的方法参数名列表。只能在 method in ['POST', 'PUT'] 时使用
query_params=[] [] 对应 URL query 参数的方法参数名列表
json_response=True True 端点响应是否应序列化为 JSON 格式
proxy=False False 该端点是否应作为代理使用

10.21.2 方法参数如何映射为 path / query / body 参数

HTTP 方法 默认行为 覆盖方式
GET / DELETE 非可选参数默认视为 path 参数;可选参数视为 query 参数 在 endpoint 装饰器里指定 query_parameters,可以让一个非可选参数变成 query 参数
POST / PUT 所有方法参数默认视为 body 参数 用 path_params 和 query_params 指定哪些方法参数是 path、哪些是 query

Body 参数从请求 body 解码,可以是 form 格式,也可以是 JSON 格式的 dictionary。

10.21.3 自定义端点的完整示例

from ..tools import Controller, BaseController, Endpoint

@Controller('/ping')
class Ping(BaseController):

  # URL: /ping/{key}?opt1=...&opt2=...
  @Endpoint(path="/", query_params=['opt1'])
  def index(self, key, opt1, opt2=None):
    """..."""

  # URL: /ping/{key}?opt1=...&opt2=...
  @Endpoint(query_params=['opt1'])
  def __call__(self, key, opt1, opt2=None):
    """..."""

  # URL: /ping/post/{key1}/{key2}
  @Endpoint('POST', path_params=['key1', 'key2'])
  def post(self, key1, key2, data1, data2=None):
    """..."""

要点:

  • path 选项可以覆盖生成的端点 URL,从而不在 URL 里使用方法名。 在 index 方法里把 path 设为 "/",生成的端点就可以通过 controller 的根 URL 访问。
  • 另一种生成「只通过 controller 的 path URL 访问」的端点的方式是使用 __call__ 方法(如上例)。
  • 从第三个方法可以看到:path 参数是从 URL 里解析「URL path 之后由 / 分隔的值列表」得到的——对 index 方法是 /ping 之后,对 post 方法是 /ping/post 之后。

10.21.4 显式声明 URL 中的 path 参数

用 Python 方法参数来定义端点 URL 里的 path 参数很容易,但在 URL 结构中这些参数的位置上仍稍显死板。有时我们想显式定义一个把 path 参数与 URL 静态部分混合的 URL scheme。controller 基础设施支持在 controller 级别和方法级别都声明带显式 path 参数的 URL path。

from ..tools import Controller, BaseController, Endpoint

@Controller('/ping/{node}/stats')
class Ping(BaseController):

  # URL: /ping/{node}/stats/{date}/latency?unit=...
  @Endpoint(path="/{date}/latency")
  def latency(self, node, date, unit="ms"):
    """ ..."""

这里在 controller URL path 里显式声明了 path 参数 {node},在 latency 方法里声明了 path 参数 {date}。latency 方法的端点可通过 https://mgr_hostname:8443/ping/{node}/stats/{date}/latency 访问。

想看 @Endpoint 装饰器的完整示例集,请查看单元测试文件 tests/test_controllers.py。 那里有很多如何自定义 endpoint 方法的例子。

10.22 实现 Proxy Controller

有时你可能需要把一些请求从 Dashboard 前端直接转发给外部服务。为此官方提供了 @Proxy 装饰器。(具体例子可以看 controllers/rgw.py,官方在那里实现了一个 RGW Admin Ops proxy。)

@Proxy 装饰器是 @Endpoint 装饰器的包装,已经把端点定制成作为代理工作。

代理端点的工作方式:捕获跟在 controller URL 前缀路径之后的 URL path,并且不对请求体做任何解码。

from ..tools import Controller, BaseController, Proxy

@Controller('/foo/proxy')
class FooServiceProxy(BaseController):

  @Proxy()
  def proxy(self, path, **params):
    """
    if requested URL is "/foo/proxy/access/service?opt=1"
    then path is "access/service" and params is {'opt': '1'}
    """

10.23 RESTController 是怎么工作的

官方还提供了一个用 RESTController class 创建 REST 风格 controller 的简单机制。任何继承 RESTController 的 class 默认都会返回 JSON。

RESTController 基本上是一个额外的抽象层,用来简化并统一对 collection 的操作。一个 collection 就是一组特定类型的对象组成的数组。RESTController 启用了一些请求类型 + 给定参数 → 特定方法名的默认映射。

import cherrypy
from ..tools import ApiController, RESTController

@ApiController('ping')
class Ping(RESTController):
  def list(self):
    return {"msg": "Hello"}

  def get(self, id):
    return self.objects[id]

上例中,list 方法被自动用于所有不带额外参数、请求类型为 GET 的 api/ping 请求。如果请求带了一个额外参数(本例是 ID),它就不再映射到 list,而是映射到 get,返回给定 ID 的元素(假设 self.objects 之前已被填充)。

完整的默认映射表(官方表格):

Request type Arguments Method Status Code
GET No list 200
PUT No bulk_set 200
POST No create 201
DELETE No bulk_delete 204
GET Yes get 200
PUT Yes set 200
DELETE Yes delete 204

要为上述方法使用自定义端点,可以用 @RESTController.MethodMap:

import cherrypy
from ..tools import ApiController, RESTController

  @RESTController.MethodMap(version='0.1')
  def create(self):
    return {"msg": "Hello"}

MethodMap 装饰器支持三个自定义参数:

参数 说明
resource" resource id(官方原文如此,含一个多余引号)
status=200 设置 HTTP 状态响应码
version version

10.24 如何在 RESTController 里使用自定义 API 端点?

如果你没有任何访问限制,可以用 @Endpoint。

如果你给端点设置了 permission scope 来限制访问,@Endpoint 会失败,因为它不知道该用哪个 permission property。

在一个受限制的 RESTController 里使用自定义端点,要用 @RESTController.Collection。 如果你在 RESTController class 里设置了 RESOURCE_ID,也可以选择 @RESTController.Resource。

import cherrypy
from ..tools import ApiController, RESTController

@ApiController('ping', Scope.Ping)
class Ping(RESTController):
  RESOURCE_ID = 'ping'

  @RESTController.Resource('GET')
  def some_get_endpoint(self):
    return {"msg": "Hello"}

  @RESTController.Collection('POST')
  def some_post_endpoint(self, **data):
    return {"msg": data}

这两个装饰器都支持五个自定义参数:

参数 说明
method="GET" 允许访问该端点的 HTTP 方法
path="/<method_name>" 端点的 URL path,不含 controller URL path 前缀
status=200 设置 HTTP 状态响应码
query_params=[] 对应 URL query 参数的方法参数名列表
version version

10.25 如何限制 controller 的访问?

所有 controller 默认都需要认证。

如果你要求某个 controller 可以免认证访问,可以给 controller 装饰器加参数 secure=False:

import cherrypy
from . import ApiController, RESTController


@ApiController('ping', secure=False)
class Ping(RESTController):
  def list(self):
    return {"msg": "Hello"}

10.26 如何创建一个使用 'public' API 的专用 UI 端点?

有时我们想把多次调用合并成一次,以节省带宽或其他性能原因。

做法:首先创建一个 @UiApiController(用于被 UI 消费但不属于 'public' API 的端点),让这个 ui class 继承自那个 REST controller class,这样你就能使用 api controller 的所有方法。

import cherrypy
from . import UiApiController, ApiController, RESTController


@ApiController('ping', secure=False)  # /api/ping
class Ping(RESTController):
  def list(self):
    return self._list()

  def _list(self):  # To not get in conflict with the JSON wrapper
    return [1,2,3]


@UiApiController('ping', secure=False)  # /ui-api/ping
class PingUi(Ping):
  def list(self):
    return self._list() + [4, 5, 6]

注意官方注释:_list() 加下划线前缀是为了不与 JSON wrapper 冲突。

10.27 如何从 controller 里访问 manager module 实例?

官方把 manager module 实例作为一个全局变量提供,可以在任何模块里 import。

import logging
import cherrypy
from .. import mgr
from ..tools import ApiController, RESTController

logger = logging.getLogger(__name__)

@ApiController('servers')
class Servers(RESTController):
    def list(self):
        logger.debug('Listing available servers')
        return {'servers': mgr.list_servers()}

10.28 如何为 controller 写单元测试?

官方提供了名为 ControllerTestCase 的测试辅助类,方便为 controller 创建单元测试。

如果要给上面的 Ping controller 写单测,在 tests 目录创建 test_ping.py:

from .helper import ControllerTestCase
from .controllers.ping import Ping


class PingTest(ControllerTestCase):
    @classmethod
    def setup_test(cls):
        cp_config = {'tools.authenticate.on': True}
        cls.setup_controllers([Ping], cp_config=cp_config)

    def test_ping(self):
        self._get("/api/ping")
        self.assertStatus(200)
        self.assertJsonBody({'msg': 'Hello'})

ControllerTestCase class 的工作方式:先初始化一个 CherryPy webserver,然后调用 setup_test() 类方法,在那里我们可以显式加载想测试的 controller(上例只加载了 Ping)。我们也可以提供 cp_config 来更新 controller 的 cherrypy config(如示例中启用认证)。

10.29 如何更新或创建新的 Grafana dashboard?

官方用 jsonnet 和 grafonnet-lib 来写 grafana dashboard 的代码。所有 dashboard 都写在 monitoring/grafana/dashboards/jsonnet 目录下的 grafana_dashboards.jsonnet 文件里。

生成 dashboard json 文件:在 grafana/dashboards 目录跑:

jsonnet -m . jsonnet/grafana_dashboards.jsonnet

(要让上面命令成功,需要装 jsonnet 包并在本机 clone 了 grafonnet-lib 目录。有问题参考 https://grafana.github.io/grafonnet-lib/getting-started/)

要更新已有的 grafana dashboard 或创建新的:更新 grafana_dashboards.jsonnet 文件,然后用上面的命令生成新的/更新后的 json 文件。不熟悉 grafonnet 或 jsonnet 的可以看 https://grafana.github.io/grafonnet-lib/ 。

jsonnet 格式的 dashboard 示例:

指定 grafana dashboard 属性(title、uid 等)可以创建一个 local function:

local dashboardSchema(title, uid, time_from, refresh, schemaVersion, tags,timezone, timepicker)

加一个 graph panel 可以在 local function 里指定 graph schema:

local graphPanelSchema(title, nullPointMode, stack, formatY1, formatY2, labelY1, labelY2, min, fill, datasource)

然后在 dashboard 定义里用这些 function:

{
    radosgw-sync-overview.json: //json file name to be generated

    dashboardSchema(
      'RGW Sync Overview', 'rgw-sync-overview', 'now-1h', '15s', .., .., ..
    )

    .addPanels([
      graphPanelSchema(
        'Replication (throughput) from Source Zone', 'Bps', null, .., .., ..)
    ])
}

合法的 grafonnet-lib 属性见 https://grafana.github.io/grafonnet-lib/api-docs/

10.30 如何在 controller 里监听 manager 通知?

manager 会就若干类型的集群事件通知各模块,例如集群日志事件等。

每个模块有一个叫 notify 的 "global" handler 函数,manager 调用它来通知模块。但这个 handler 函数不得阻塞或花太多时间处理事件通知。

为此官方提供了一个 notification queue,controller 可以向它注册自己以接收集群通知。

下面是一个实现了一个非常简单的 live log viewer 页面的 controller:

import collections

import cherrypy

from ..tools import ApiController, BaseController, NotificationQueue


@ApiController('livelog')
class LiveLog(BaseController):
    log_buffer = collections.deque(maxlen=1000)

    def __init__(self):
        super(LiveLog, self).__init__()
        NotificationQueue.register(self.log, 'clog')

    def log(self, log_struct):
        self.log_buffer.appendleft(log_struct)

    @cherrypy.expose
    def default(self):
        ret = '<html><meta http-equiv="refresh" content="2" /><body>'
        for l in self.log_buffer:
            ret += "{}<br>".format(l)
        ret += "</body></html>"
        return ret

NotificationQueue class 提供 register 方法:

  • 第一个参数是函数;
  • 第二个参数是 "notification type"(通知类型);
  • 可以省略第二个参数,那样你就注册为监听所有类型的所有通知。

可用的通知类型清单(官方注明这些未来可能变化):

类型 含义
clog 集群日志通知
command 由 MgrModule.send_command 发出的命令完成时的通知
perf_schema_update perf counters schema 更新
mon_map monitor map 更新
fs_map cephfs map 更新
osd_map OSD map 更新
service_map services(RGW、RBD-Mirror 等)map 更新
mon_status monitor status 定期更新
health health status 定期更新
pg_summary PG 状态信息的定期更新

10.31 当 controller 访问某个 Ceph 模块时,单元测试怎么写?

考虑下面这个获取 rbd pool 里 RBD image 列表的 controller:

import rbd
from .. import mgr
from ..tools import ApiController, RESTController


@ApiController('rbdimages')
class RbdImages(RESTController):
    def __init__(self):
        self.ioctx = mgr.rados.open_ioctx('rbd')
        self.rbd = rbd.RBD()

    def list(self):
        return [{'name': n} for n in self.rbd.list(self.ioctx)]

上例中我们想 mock rbd.list 函数的返回值,以便测试 controller 的 JSON 响应。

单测代码:

import mock
from .helper import ControllerTestCase


class RbdImagesTest(ControllerTestCase):
    @mock.patch('rbd.RBD.list')
    def test_list(self, rbd_list_mock):
        rbd_list_mock.return_value = ['img1', 'img2']
        self._get('/api/rbdimages')
        self.assertJsonBody([{'name': 'img1'}, {'name': 'img2'}])

10.32 如何新增一个配置项?

如果你需要为新特性存储某个配置设置,官方已经提供了一个简单的机制来指定/使用新的 config setting。

例如,想加一个保存 dashboard admin 邮箱地址的配置项,只需在 settings.py 文件的 Options class 里加一个 setting 名作为 class attribute:

# ...
class Options(object):
  # ...

  ADMIN_EMAIL_ADDRESS = ('admin@admin.com', str)

class attribute 的值是一个 pair,由该 setting 的默认值和该值的 python type 组成。

声明了 ADMIN_EMAIL_ADDRESS class attribute 后,重启 dashboard 模块时你会自动多出两条 CLI 命令来 get 和 set 该 setting:

$ ceph dashboard get-admin-email-address
$ ceph dashboard set-admin-email-address <value>

要在 Python 代码里访问或修改该配置值(无论是在 controller 里还是别处),只需 import Settings class 并这样访问:

from settings import Settings

# ...
tmp_var = Settings.ADMIN_EMAIL_ADDRESS

# ....
Settings.ADMIN_EMAIL_ADDRESS = 'myemail@admin.com'

settings 管理实现会保证:如果你从 Python 代码里改了一个 setting 值,从 CLI 访问该 setting 时会看到这个改动,反之亦然。

10.33 如何异步执行 controller 的读写操作?

有些 controller 需要执行会改变 Ceph 集群状态的操作。这些操作可能需要一些时间执行,而为了在 Web UI 里保持良好的用户体验,我们需要异步执行这些操作,并立即返回一些信息给前端,说明操作正在后台运行。

为此官方加入了对**异步任务(asynchronous tasks)**的支持。触发一个异步任务必须用 TaskManager class 的下面这个类方法:

from ..tools import TaskManager
# ...
TaskManager.run(name, metadata, func, args, kwargs)
参数 说明
name 一个可用于给任务分组的字符串。例如 RBD image 创建任务可以指定 "rbd/create",RBD image 删除任务 "rbd/remove"
metadata 一个 dictionary,存放刻画该任务的 key-value 对。例如创建 RBD image 的任务可以传 {'pool_name': "rbd", image_name': "test-img"}
func 实现操作代码的 python 函数,将被异步执行
args / kwargs 任务管理器开始执行时传给 func 的位置参数和命名参数

TaskManager.run 方法触发 func 的异步执行并返回一个 Task 对象。

Task 提供公开方法 Task.wait(timeout),可以等待任务完成,最多等 timeout 秒(以参数形式提供)。如果不提供参数,wait 方法会一直阻塞到任务完成。

Task.wait 对「通常执行很快、但有时可能跑很久」的任务非常有用。

Task.wait 的返回值是一个 pair (state, value):

state 取值 含义
VALUE_DONE = "done" 已完成
VALUE_EXECUTING = "executing" 执行中
  • 如果 state == VALUE_DONE,value 存放函数 func 的执行结果;
  • 如果 state == VALUE_EXECUTING,则 value == None。

幂等性规则:(name, metadata) 这一对应能唯一标识正在运行的任务。这意味着如果你试图触发一个新任务,其 (name, metadata) 与当前正在运行的任务相同,那么新任务不会被创建,你会拿到当前正在运行任务的 task 对象。

task1 = TaskManager.run("dummy/task", {'attr': 2}, func)
task2 = TaskManager.run("dummy/task", {'attr': 2}, func)

如果第二次 TaskManager.run 调用时第一个任务仍在执行,它会返回同一个 task 对象:assert task1 == task2。

10.34 如何获取执行中和已完成的异步任务列表?

执行中和已完成任务的列表包含在 Summary controller 里,dashboard 前端每 5 秒轮询一次它。此外官方还提供了一个专门的 controller 来获取同样的列表。

Task controller 暴露 /api/task 端点,返回执行中和已完成的任务列表。该端点接受 name 参数,其值可以是一个 glob 表达式。 例如 HTTP GET 请求 /api/task?name=rbd/* 会返回所有名字以 rbd/ 开头的执行中和已完成任务。

防止已完成任务列表无限增长的措施:

  • 始终只保留最近 10 个已完成的任务;
  • 其余较旧的已完成任务在达到 1 分钟 TTL 时被删除。TTL 用任务完成执行时的时间戳计算;
  • 一分钟后,当已完成任务信息被检索时(无论是通过 summary controller 还是 task controller),它会自动从列表中删除,不会再包含在后续的任务查询中。

每个执行中的任务用如下 dictionary 表示:

{
  'name': "name",  # str
  'metadata': { },  # dict
  'begin_time': "2018-03-14T15:31:38.423605Z",  # str (ISO 8601 format)
  'progress': 0  # int (percentage)
}

每个已完成的任务用如下 dictionary 表示:

{
  'name': "name",  # str
  'metadata': { },  # dict
  'begin_time': "2018-03-14T15:31:38.423605Z",  # str (ISO 8601 format)
  'end_time': "2018-03-14T15:31:39.423605Z",  # str (ISO 8601 format)
  'duration': 0.0,  # float
  'progress': 0  # int (percentage)
  'success': True,  # bool
  'ret_value': None,  # object, populated only if 'success' == True
  'exception': None,  # str, populated only if 'success' == False
}

10.35 如何在异步任务里使用异步 API?

前面描述的 TaskManager.run 方法很适合调用阻塞型函数,因为它在一个新建的线程里跑该函数。但有时我们想调用某个本身就是异步的 API 的函数。

对于这些情况,我们不想为了跑一个非阻塞函数而新建一个线程,而是想利用该函数的异步特性。TaskManager.run 已经为此做好了准备:通过传一个 TaskExecutor 类型的对象作为额外参数 executor。

完整方法签名:

TaskManager.run(name, metadata, func, args=None, kwargs=None, executor=None)

TaskExecutor class 负责执行给定任务函数的代码,并定义了三个可被子类覆盖的方法:

def init(self, task)
def start(self)
def finish(self, ret_value, exception)
方法 调用时机 / 作用
init(self, task) 在跑任务函数之前调用,接收 task 对象(class Task)
start(self) 跑任务函数。默认实现是在当前线程上下文里跑任务函数
finish(self, ret_value, exception) 当任务函数结束时应被调用,参数要么是填充了执行结果的 ret_value,要么(在执行抛异常的情况下)是一个 exception 对象

要利用非阻塞函数的异步特性,开发者应通过创建 TaskExecutor class 的子类来实现一个自定义 executor,并把该自定义 executor class 的实例作为 TaskManager.run 的 executor 参数传入。

完整示例:用自定义 executor 执行 MgrModule.send_command 这个异步函数

import json
from mgr_module import CommandResult
from .. import mgr
from ..tools import ApiController, RESTController, NotificationQueue, \
                    TaskManager, TaskExecutor


class SendCommandExecutor(TaskExecutor):
    def __init__(self):
        super(SendCommandExecutor, self).__init__()
        self.tag = None
        self.result = None

    def init(self, task):
        super(SendCommandExecutor, self).init(task)

        # we need to listen for 'command' events to know when the command
        # finishes
        NotificationQueue.register(self._handler, 'command')

        # store the CommandResult object to retrieve the results
        self.result = self.task.fn_args[0]
        if len(self.task.fn_args) > 4:
            # the user specified a tag for the command, so let's use it
            self.tag = self.task.fn_args[4]
        else:
            # let's generate a unique tag for the command
            self.tag = 'send_command_{}'.format(id(self))
            self.task.fn_args.append(self.tag)

    def _handler(self, data):
        if data == self.tag:
            # the command has finished, notifying the task with the result
            self.finish(self.result.wait(), None)
            # deregister listener to avoid memory leaks
            NotificationQueue.deregister(self._handler, 'command')


@ApiController('test')
class Test(RESTController):

    def _run_task(self, osd_id):
        task = TaskManager.run("test/task", {}, mgr.send_command,
                               [CommandResult(''), 'osd', osd_id,
                                json.dumps({'prefix': 'perf histogram dump'})],
                               executor=SendCommandExecutor())
        return task.wait(1.0)

    def get(self, osd_id):
        status, value = self._run_task(osd_id)
        return {'status': status, 'value': value}

上面的 SendCommandExecutor executor class 可以用于任何对 MgrModule.send_command 的调用——这意味着对于我们在 controller 里用到的每一个非阻塞 API,只需要实现一个自定义 executor class。

默认的 executor(当没有给 TaskManager.run 传 executor 对象时使用)是 ThreadedExecutor。可以在 tools.py 文件里查看它的实现。

10.36 如何更新异步任务的执行进度?

异步任务基础设施支持更新正在执行任务的执行进度。进度可以从任务正在执行的代码内部更新——那通常正是我们有进度信息的地方。

要从任务代码内部更新进度,TaskManager class 提供了一个获取当前 task 对象的方法:

TaskManager.current_task()

上述方法仅在使用默认 executor ThreadedExecutor 执行任务时可用。

current_task() 方法返回当前的 Task 对象。Task 对象提供两个更新执行进度值的公开方法:

方法 参数含义
set_progress(percentage) 一个整数,表示我们想给任务设置的绝对百分比
inc_progress(delta) 一个整数,表示我们想在当前执行进度百分比上增加的增量

示例:一个触发新任务并更新其进度的 controller

import random
import time
import cherrypy
from ..tools import TaskManager, ApiController, BaseController


@ApiController('dummy_task')
class DummyTask(BaseController):
    def _dummy(self):
        top = random.randrange(100)
        for i in range(top):
            TaskManager.current_task().set_progress(i*100/top)
            # or TaskManager.current_task().inc_progress(100/top)
            time.sleep(1)
        return "finished"

    @cherrypy.expose
    @cherrypy.tools.json_out()
    def default(self):
        task = TaskManager.run("dummy/task", {}, self._dummy)
        return task.wait(5)  # wait for five seconds

10.37 前端如何处理异步任务?

所有执行中的以及最近完成的异步任务都会显示在菜单栏的 "Background-Tasks" 上,如果已完成则显示在 "Recent-Notifications" 上。

对于每个任务,必须提供:

  1. 三种状态(running、success、failure)各一个 operation name;
  2. 一个说明谁被牵涉其中的函数;
  3. 错误描述(如果有的话)。

做法:追加到 TaskManagerMessageService.messages。必须这样做才能在所有任务和状态之间保持一致性。

Operation Object

保证所有任务之间的一致性。它由三个动词组成,分别对应不同状态,例如:

{running: 'Creating', failure: 'create', success: 'Created'}

时态规则(明确要求):

  1. running 用现在分词(present participle),例如 'Updating';
  2. 失败消息总是以 'Failed to ' 开头,后面接动词原形(present tense),例如 'update';
  3. 成功用过去式(past tense),例如 'Updated'。

Involves Function

保证一个任务的所有消息之间的一致性,它描述操作牵涉到谁。它是一个接收 task 的 metadata 并返回字符串的函数,例如返回 "RBD 'somePool/someImage'"。

组合后的消息

状态 生成的消息
Failure Failed to create RBD 'somePool/someImage'
Running Creating RBD 'somePool/someImage'
Success Created RBD 'somePool/someImage'

自动 vs 手动

  • 自动处理任务用 TaskWrapperService.wrapTaskAroundCall。
  • 如果出于某种原因 wrapTaskAroundCall 对你不生效,你必须手动通过 TaskManagerService.subscribe 订阅你的异步任务,并提供一个 callback,在成功时通知用户。
  • 一个通知可以用 NotificationService.notifyTask 触发。它会使用 TaskManagerMessageService.messages 基于任务状态显示消息。
  • API 错误的通知由 ApiInterceptorService 处理。

用法示例

export class TaskManagerMessageService {
  // ...
  messages = {
    // Messages for task 'rbd/create'
    'rbd/create': new TaskManagerMessage(
      // Message prefixes
      ['create', 'Creating', 'Created'],
      // Message suffix
      (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}'`,
      (metadata) => ({
        // Error code and description
        '17': `Name is already used by RBD '${metadata.pool_name}/${
               metadata.image_name}'.`
      })
    ),
    // ...
  };
  // ...
}

export class RBDFormComponent {
  // ...
  createAction() {
    const request = this.createRequest();
    // Subscribes to 'call' with submitted 'task' and handles notifications
    return this.taskWrapper.wrapTaskAroundCall({
      task: new FinishedTask('rbd/create', {
        pool_name: request.pool_name,
        image_name: request.name
      }),
      call: this.rbdService.create(request)
    });
  }
  // ...
}

10.38 REST API 文档

Ceph-Dashboard 为 Ceph RESTful API 提供两种文档:

类型 位置
Static documentation(静态文档) 见 mgr ceph api。来自一个带版本的规范,位于 src/pybind/mgr/dashboard/openapi.yaml
Interactive documentation(交互式文档) 从一个运行中的 Ceph-Dashboard 实例访问(右上角的 ? 图标 > API Docs)

如果对 controllers/ 目录做了改动,很可能会导致生成的 OpenAPI 规范发生变化。 为此官方实现了一个 checker 来阻止非预期的变更。该检查由 Pull Request CI(make check)自动触发,也可以手工调用:

tox -e openapi-check

如果该 checker 失败,说明当前 PR 正在修改 Ceph API,因此:

  1. 应该显式更新带版本的 OpenAPI 规范:
    tox -e openapi-fix
    
  2. 会请求 @ceph/api 团队进行评审(通过 GitHub CODEOWNERS 自动完成),以评估变更影响。

此外,可以从 OpenAPI 规范生成 Sphinx 文档:

tox -e openapi-doc

10.38.1 两个用于补充文档的装饰器

Ceph RESTful OpenAPI 规范是从 controllers/ 目录里的 Controllers 动态生成的。但默认情况下它不够详细,因此有两个应当被使用的装饰器:

装饰器 用途 参数
@EndpointDoc() 端点的文档 四个可选参数:description、group、parameters、responses
@ControllerDoc() controller 或与端点关联的 group 的文档 只取前两个参数:description 和 group

各参数含义:

  • description:一个字符串,对象的简短(1-2 句)描述。
  • group:默认情况下,一个端点与同一 controller class 里的其他端点归为一组。group 是一个字符串,可以把一个端点或一个 class 里的所有端点指派给另一个 controller 或一个有意义的 group 名。
  • parameters:一个 dict,用于描述 path、query 或 request body 参数。
    • 默认情况下,一个端点的所有参数都会列在 Swagger UI 页面上,包括该参数是 optional/required 以及默认值的信息。
    • 但是:参数没有描述,参数类型只在某些情况下显示。
    • 添加信息时,每个参数应如下例那样描述。注意参数类型应表示为 python 内置类型,而不是字符串。允许的值:str、int、bool、float。
@EndpointDoc(parameters={'my_string': (str, 'Description of my_string')})
def method(my_string): pass

body 参数可以有更复杂的情况:

  • 如果参数是一个 dictionary,类型应被一个包含其嵌套参数的 dict 替换。描述嵌套参数时使用与其他参数相同的格式。但是,所有嵌套参数默认都被设为 required。 如果某个嵌套参数是可选的,必须像下面例子里的 item2 那样指定。如果嵌套参数被设为可选,也可以指定默认值(嵌套参数的默认值不会自动提供)。
@EndpointDoc(parameters={
  'my_dictionary': ({
    'item1': (str, 'Description of item1'),
    'item2': (str, 'Description of item2', True),  # item2 is optional
    'item3': (str, 'Description of item3', True, 'foo'),  # item3 is optional with 'foo' as default value
}, 'Description of my_dictionary')})
def method(my_dictionary): pass
  • 如果参数是基本类型的 list,类型应用方括号包起来。
@EndpointDoc(parameters={'my_list': ([int], 'Description of my_list')})
def method(my_list): pass
  • 如果参数是带嵌套参数的 list,嵌套参数应放在一个 dictionary 里并用方括号包起来。
@EndpointDoc(parameters={
  'my_list': ([{
    'list_item': (str, 'Description of list_item'),
    'list_item2': (str, 'Description of list_item2')
}], 'Description of my_list')})
def method(my_list): pass
  • responses:一个 dict,用于描述响应。描述响应的规则与 request body 参数相同,只有一点不同:响应还需要被指派给相关的响应码,如下例:
@EndpointDoc(responses={
  '400':{'my_response': (str, 'Description of my_response')}})
def method(): pass

10.39 Python 中的错误处理

良好的错误处理是创造良好用户体验、提供良好 API 的关键要求。

Dashboard 代码不应重复 C++ 代码。 因此,如果 C++ 里的错误处理已足以提供良好反馈,就不必再包一层去捕获这些错误。另一方面,输入校验是捕获错误并生成最佳错误消息的最佳位置。如果需要,尽早生成错误。

后端提供几种标准的返回错误方式。

1)通用的 Internal Server Error:

Status Code: 500
{
    "version": <cherrypy version, e.g. 13.1.0>,
    "detail": "The server encountered an unexpected condition which prevented it from fulfilling the request.",
}

2)后端生成的错误,标准错误格式:

Status Code: 400
{
    "detail": str(e),     # E.g. "[errno -42] <some error message>"
    "component": "rbd",   # this can be null to represent a global error code
    "code": "3",          # Or a error name, e.g. "code": "some_error_key"
}

3)如果 API 端点使用了 @ViewCache 临时缓存结果,错误长这样:

Status Code 400
{
    "detail": str(e),     # E.g. "[errno -42] <some error message>"
    "component": "rbd",   # this can be null to represent a global error code
    "code": "3",          # Or a error name, e.g. "code": "some_error_key"
    'status': 3,          # Indicating the @ViewCache error status
}

4)如果 API 端点使用了 task,错误长这样:

Status Code 400
{
    "detail": str(e),     # E.g. "[errno -42] <some error message>"
    "component": "rbd",   # this can be null to represent a global error code
    "code": "3",          # Or a error name, e.g. "code": "some_error_key"
    "task": {             # Information about the task itself
        "name": "taskname",
        "metadata": {...}
    }
}

原则:

  • 我们的 WebUI 应该把 API 生成的错误展示给用户,尤其是 wizard 和 dialog 里与字段相关的错误,或者显示非侵入式通知。
  • 在 Python 里处理异常应当是一种例外(exception)。 一般而言,项目里应该只有很少的 exception handler。
  • 默认情况下,把错误传播给 API,因为它无论如何会处理所有异常。
  • 一般而言,在 handler 里通过加 logger.exception() 并附一段描述来记录异常。
  • 我们需要区分用户错误、内部错误和编程错误。使用不同的异常类型会减轻 API 层和 UI 的工作。
  • 标准的 Python 错误(如 SystemError、ValueError、KeyError)最终会变成 API 里的 internal server errors。
  • 一般而言,不要在 REST API 里 return 错误响应。 它们会由 error handler 返回。相反,应该 raise 合适的异常。

10.40 插件(Plug-ins)

新功能可以通过插件架构提供。这种方式带来的好处中,最显著的是松耦合开发。随着 Ceph Dashboard 功能越来越丰富,其代码库也越来越复杂。插件架构基于 hook 的本质允许以受控的方式扩展功能,并隔离改动范围。

Ceph Dashboard 依赖 Pluggy(https://pluggy.readthedocs.io)提供插件支持。 在 pluggy 之上,官方实现了一个基于接口(interface-based)的方法,并带有一些安全检查(方法覆盖检查和抽象方法检查)。

10.40.1 创建新插件的步骤

  1. 在 src/pybind/mgr/dashboard/plugins 下新增一个文件。
  2. import PLUGIN_MANAGER 实例和 Interfaces。
  3. 创建一个继承所需 interfaces 的 class。 插件库会检查 interfaces 的所有方法是否都被正确覆盖。
  4. 在 PLUGIN_MANAGER 实例里注册该插件。
  5. 从 Ceph Dashboard 的 module.py 里 import 该插件(目前没有实现动态加载)。

10.40.2 可用的 Mixins(helpers)

Mixin 说明
CanMgr 让插件可以通过 self.mgr 访问 mgr 实例

10.40.3 可用的 Interfaces

Interface 要求与执行时机
Initializable 覆盖 init() hook。该方法在 dashboard 模块的最开始、所有 import 执行完之后运行
Setupable 覆盖 setup() hook。该方法在 Ceph Dashboard 的 serve() 方法里运行——在 CherryPy 配置好之后,但在它启动之前。是插件初始化逻辑的占位点
HasOptions 覆盖 get_options() hook,返回一个 Options() 列表。这里返回的 options 会被加到 MODULE_OPTIONS
HasCommands 覆盖 register_commands() hook,定义插件能处理的命令并用 @CLICommand 装饰它们。命令可以选择性返回,以便能被外部调用(这让单元测试更容易)
HasControllers 覆盖 get_controllers() hook,按常规方式定义并返回 controllers
FilterRequest.BeforeHandler 覆盖 filter_request_before_handler() hook。该方法接收一个 cherrypy.request 对象进行处理。通常的实现会根据 request 元数据和其他条件,让某些请求通过,或者 raise 一个 cherrypy.HTTPError

新 interfaces 和 hooks 应该在需要实现新功能时随之添加。 上面这份清单只包含现有插件所需的 hooks。

10.40.4 完整插件示例

# src/pybind/mgr/dashboard/plugins/mute.py

from . import PLUGIN_MANAGER as PM
from . import interfaces as I

from mgr_module import CLICommand, Option
import cherrypy

@PM.add_plugin
class Mute(I.CanMgr, I.Setupable, I.HasOptions, I.HasCommands,
                     I.FilterRequest.BeforeHandler, I.HasControllers):
  @PM.add_hook
  def get_options(self):
    return [Option('mute', default=False, type='bool')]

  @PM.add_hook
  def setup(self):
    self.mute = self.mgr.get_module_option('mute')

  @PM.add_hook
  def register_commands(self):
    @CLICommand("dashboard mute")
    def _(mgr):
      self.mute = True
      self.mgr.set_module_option('mute', True)
      return 0

  @PM.add_hook
  def filter_request_before_handler(self, request):
    if self.mute:
      raise cherrypy.HTTPError(500, "I'm muted :-x")

  @PM.add_hook
  def get_controllers(self):
    from ..controllers import ApiController, RESTController

    @ApiController('/mute')
    class MuteController(RESTController):
      def get(_):
        return self.mute

    return [MuteController]

10.40.5 用 SimplePlugin 简化

此外,官方提供了一个创建插件的 helper:SimplePlugin,它简化了基本任务(Options、Commands 和常见的 Mixins)。上面的插件可以改写成:

from . import PLUGIN_MANAGER as PM
from . import interfaces as I
from .plugin import SimplePlugin as SP

import cherrypy

@PM.add_plugin
class Mute(SP, I.Setupable, I.FilterRequest.BeforeHandler, I.HasControllers):
  OPTIONS = [
      SP.Option('mute', default=False, type='bool')
  ]

  def shut_up(self):
    self.set_option('mute', True)
    self.mute = True
    return 0

  COMMANDS = [
      SP.Command("dashboard mute", handler=shut_up)
  ]

  @PM.add_hook
  def setup(self):
    self.mute = self.get_option('mute')

  @PM.add_hook
  def filter_request_before_handler(self, request):
    if self.mute:
      raise cherrypy.HTTPError(500, "I'm muted :-x")

  @PM.add_hook
  def get_controllers(self):
    from ..controllers import ApiController, RESTController

    @ApiController('/mute')
    class MuteController(RESTController):
      def get(_):
        return self.mute

    return [MuteController]

10.41 Dashboard 章节速查表

想做的事 命令 / 位置
装前端依赖 cd src/pybind/mgr/dashboard/frontend && npm ci
起前端 dev server 先建 proxy.conf.json(照抄 proxy.conf.json.sample),再 npm start → http://localhost:4200/
构建前端 npm run build(-- --prod 生产构建 → https://localhost:8443)
生成代码文档 npm run doc-build → documentation/;npm run doc-serve → http://localhost:8444
lint / 自动修复 npm run lint / npm run fix
前端单测 npm run test(Jest)
全量 E2E cd src/pybind/mgr/dashboard && ./run-frontend-e2e-tests.sh(-d 指定设备,-r/-u/-p 远程)
cephadm E2E ./src/pybind/mgr/dashboard/ci/cephadm/run-cephadm-e2e-tests.sh(需 KCLI)
活跃开发期 E2E npm run e2e / npm run e2e:ci / npx cypress open / npx cypress run
后端依赖 cd src/pybind/mgr/dashboard && pip install -r requirements.txt
后端单测 ../../../script/run_tox.sh --tox-env py3,lint,check 或 tox -e py3
单个后端测试 tox -e py3 tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
PEP8 自动修复 run_tox.sh --tox-env fix 或 tox -e fix
覆盖率报告 coverage html → htmlcov/
后端 API 测试 ./run-backend-api-tests.sh [tasks.mgr.dashboard.xxx]
OpenAPI 检查/修复/文档 tox -e openapi-check / tox -e openapi-fix / tox -e openapi-doc
i18n 提取 npm run i18n:extract
查 dashboard URL ./bin/ceph mgr services

引自:Ceph Dashboard Developer Documentation (formerly HACKING.rst)


十一、Jaeger 分布式追踪开发

11.1 背景

Jaeger + Opentracing 为分布式系统提供开箱即用的追踪服务,并因其简单性和标准化而成为被广泛使用的标准。

Ceph 使用经过修改的 jaeger-cpp-client(https://github.com/ceph/jaeger-client-cpp)作为提供给 Opentracing API 的 backend,它负责收集 span。这些 span 是用智能指针构造的,携带时间戳、TraceID 以及其他元信息(如与 span 关联的特定 tag/log),以便在分布式系统中唯一标识它。

11.2 基础架构与术语

官方只给了一个内部链接:参见 Ceph Tracing 文档的「BASIC ARCHITECTURE AND TERMINOLOGY」一节(相对路径 ../../../jaegertracing/#basic-architecture-and-terminology)。

⚠️ 文档未明确:developer_guide/jaegertracing.rst 本身没有展开架构与术语,只指向 doc/jaegertracing.rst(不在本册源文范围内)。本册因此不在此复述架构细节。

11.3 如何开始使用 tracing?

在 Ceph 里启用 jaegertracing 需要:部署 Jaeger 守护进程 + 带 Jaeger 编译 Ceph,编排好以便在 vstart 集群中给开发者使用。这里用的是一个 jaeger all-in-one docker(https://www.jaegertracing.io/docs/1.22/getting-started/#all-in-one),不推荐用于生产,但用于测试可以。

全部步骤如下:

1. 用 install-deps 更新系统上的 Jaeger 依赖:

$ WITH_JAEGER=true ./install-deps.sh

2. 带 Jaeger 编译 Ceph:

  • 对已预编译的构建:

    $ cd build
    $ cmake -DWITH_JAEGER=ON ..
    
  • 对用 do_cmake.sh 的全新编译:

    $ ./do_cmake.sh -DWITH_JAEGER=ON && ninja vstart
    

3. 编译成功后,用 --jaeger 启动一个 vstart 集群,它会用容器部署服务(docker/podman)部署 jaeger all-in-one:

$ MON=1 MGR=0 OSD=1 ../src/vstart.sh --with-jaeger

如果部署不成功,你可以手工部署 all-in-one 服务,然后不带 jaeger 启动 vstart 集群也可以。

⚠️ 注意:官方第 2 步的命令是 --jaeger,第 3 步的命令是 --with-jaeger。这是源文里的写法,本册原样保留,不做「纠正」。

4. 用 rados-bench write 测试 trace:

$ bin/rados -p test bench 5 write --no-cleanup

另见:https://medium.com/@deepikaupadhyay/using-jaeger-cpp-client-for-distributed-tracing-in-ceph-8b1f4906ca2

引自:JAEGER- DISTRIBUTED TRACING


文档未明确项

以下事项在 doc/dev/developer_guide/ 的 18 个源文里没有说明,本册不做猜测:

# 事项 说明
1 完整的 src/ 目录布局表 官方 developer_guide 没有任何 src/ 逐目录布局表。本册 1.4 节只列出源文里可确证的路径
2 Fixes: / Resolves: 等 commit trailer 的完整规范 本册源文只提到 Signed-off-by:(由 git commit -as 生成)与 merge commit 的 Reviewed-by:;Fixes: 未被本册源文提及,官方指向 CONTRIBUTING.rst / SubmittingPatches.rst
3 GDB 的具体子命令清单 debugging-gdb.rst 只给了 gdb ./bin/ceph-osd -p <pid> 和 ./ceph-debug-docker.sh <branch-name>,没有 bt、thread apply all bt 等命令说明,仅给了一个外部链接
4 Sentry 的任何内容 tests-sentry-developers-guide.rst 全文只有 "To be updated. Feel free to contribute."
5 更新 git submodules 的具体步骤 basic-workflow.rst 提到「如果 git status 出现 Untracked files,见 updating submodules 步骤」,但该步骤不在本册源文内
6 Jaeger 的基础架构与术语 jaegertracing.rst 只链接到 doc/jaegertracing.rst,本册源文不含其内容
7 PR 里可用的完整 Jenkins 命令列表 官方说「这些命令列表会自动附加在每个新 PR 描述的末尾」,但本册源文只举了 jenkins retest this please 和 jenkins test signed 两个例子,未给出完整清单
8 teuthology-suite 的全部选项 官方多次让读者跑 teuthology-suite --help,本册源文只详述了 -v、-m、-c、-s、-p、--filter、-e、--dry-run、--subset、--limit、-N、--rerun/-R、--suite-repo、--suite-branch、--no-nested-subset、--force-priority、--seed、-d、--timeout、flavor 等
9 --subset 背后真正的筛选逻辑 官方明说「理解背后真正的逻辑需要读 teuthology 源码」
10 Dashboard 前端各功能模块(pool / RBD / RGW / CephFS 等页面)的业务实现 dash-devel.rst 是开发指南,不是功能文档;具体页面功能见本手册第 13 册(监控告警与 Dashboard 篇)
11 单元测试 add_ceph_test / add_ceph_unittest 的完整参数语义 官方只说明二者的用途区别,参数细节指向 cmake/modules/AddCephTest.cmake
12 documenting.rst 的完整内容 该文件位于 doc/dev/documenting.rst,不在本册源文范围;本册第 9 章只给出与贡献流程相关的摘要,完整内容见第 18 册
13 Tentacle 当期的实际分支名与 CI 平台清单 merging.rst 用 hammer/infernalis/jewel 等历史版本举例;teuthology-intro.rst 的平台清单是「as of September 2020」的 RHEL/CentOS 8 + Ubuntu 18.04。Tentacle 的当前状态文档未明确
14 Ceph Dashboard 前端 Angular 的具体版本号 官方只给了 Node(>= 20.13.1)和 NPM(>= 10.5.2)的下限;Angular 与 Bootstrap 版本让读者自己看 package.json