简介:这是一份面向OpenStack运维工程师与云平台开发者的Python工具脚本,专为解决Cinder卷备份服务在多租户场景下的功能局限而设计,如跨租户备份可见性控制、自动备份轮换、运行中卷的无中断备份等实际运维痛点。资源包共4个文件,含核心脚本cinderback.py(实现全量/单卷备份还原、元数据导出导入、快照临时卷机制)、README.md(使用说明与权限配置)、requirements.txt(依赖声明)及.gitignore(开发规范),整体仅11KB,轻量易部署。目前已有313人学习下载,适合中高级云平台从业者快速集成、二次开发或作为Cinder Backup Service增强方案参考。读者可直接复用其多租户凭据管理逻辑、备份生命周期控制策略及兼容旧版cinderclient的降级处理机制,显著提升OpenStack块存储灾备流程的自动化与健壮性。
1. Cinder备份不是“一键导出”,而是要绕过OpenStack里那些藏在API背后的权限陷阱和状态盲区
某高校实验室跑着一套OpenStack Yoga环境,存储后端用的是Ceph RBD,日常靠Cinder管理块设备。某天核心数据库VM的卷突然损坏,他们翻遍Horizon控制台和openstack volume backup命令手册,发现备份列表里空空如也——不是没做备份,是备份任务全卡在creating状态超过2小时,日志里只有一行No valid host was found,连失败原因都懒得报。这时候才意识到:OpenStack原生备份流程根本没暴露底层调度器是否就绪、RBD镜像快照是否已同步、备份服务worker是否真的在监听。cinderback就是为这种场景写的:它不依赖Horizon或CLI封装层,直接穿透到Cinder API v3 + 备份服务(cinder-backup)的gRPC通信边界,用Python把“发起备份→轮询状态→校验快照一致性→导出元数据”这整条链路拧成一个可中断、可重试、带详细错误上下文的脚本流。适合正在维护生产级OpenStack集群的SRE、需要做灾备演练的运维工程师,以及被Cinder备份超时问题反复折磨却找不到日志入口的开发者——它不解决Ceph配额不足这类根因,但能让你在5分钟内看清到底是网络不通、token过期、还是备份服务根本没注册进Placement。
2. 架构设计:三层解耦,让备份逻辑脱离OpenStack版本升级的“玄学依赖”
cinderback不是对openstack volume backup create的简单封装,它的结构刻意避开了OpenStack CLI工具链的黑匣子。整个项目按职责切成三个物理隔离层:认证与会话管理层、Cinder API协议适配层、备份生命周期控制器。这种分法不是为了炫技,而是因为我们在某次Ussuri升Yoga过程中发现:CLI工具升级后--backup-type参数语义变了,但底层API/v3/{project_id}/backups的请求体结构没变——只要稳住API层,上层策略就能热插拔。
2.1 认证与会话管理层:用requests.Session接管所有HTTP连接生命周期
这个模块的核心是auth/session.py里的CinderSession类。它不调用keystoneauth1,而是手动构造Bearer Token请求,并把X-Auth-Token、X-OpenStack-Region-Name等头字段注入session对象。关键在于它重写了request()方法:
# auth/session.py def request(self, method, url, **kwargs): # 强制刷新token:当距离上次获取超过28分钟,或响应返回401时 if self._should_refresh_token(): self._refresh_auth_token() # 注入region和project_id到URL路径(OpenStack多region部署必需) final_url = self._inject_region_and_project(url) return super().request(method, final_url, **kwargs)提示:
_should_refresh_token()判断逻辑是time.time() - self._token_acquired_at > 28 * 60,而不是依赖keystone返回的expires_at字段——后者在某些定制化Keystone里格式不统一,曾导致某次凌晨批量备份全部因token解析失败而静默退出。
2.2 Cinder API协议适配层:把RESTful接口变成Python对象方法
api/v3.py定义了CinderV3Client类,它把每个API端点映射为一个方法。重点不在CRUD,而在状态机建模。比如create_backup()方法返回的不是原始JSON,而是一个BackupOperation对象:
# api/v3.py class BackupOperation: def __init__(self, backup_id: str, volume_id: str, status: str): self.backup_id = backup_id self.volume_id = volume_id self.status = status # 'creating', 'available', 'error', etc. self._poll_interval = 5 # 可被外部修改 self._max_retries = 120 # 对应10分钟超时 def poll_until_complete(self, client: "CinderV3Client") -> "BackupOperation": for _ in range(self._max_retries): resp = client.get_backup(self.backup_id) self.status = resp["backup"]["status"] if self.status in ["available", "error", "restoring"]: return self time.sleep(self._poll_interval) raise TimeoutError(f"Backup {self.backup_id} stuck in {self.status} after {self._max_retries} polls")这个设计让上层代码完全不用操心HTTP重试、JSON解析、状态码映射——poll_until_complete()内部已处理404(备份ID不存在)、401(token失效)、503(服务不可用)三类错误,并自动触发token刷新和重试。
2.3 备份生命周期控制器:把“备份”拆解为7个可审计步骤
core/backup_flow.py里的BackupFlow类才是真正的业务中枢。它不接受“给我备份这个卷”这种模糊指令,而是强制用户声明每个环节的行为:
| 步骤 | 方法名 | 是否可跳过 | 典型用途 |
|---|---|---|---|
| 1. 卷状态检查 | check_volume_status() | 否 | 拒绝备份in-use且未冻结的卷 |
| 2. 快照预检 | check_rbd_snapshot_consistency() | 是 | Ceph场景下验证rbd snap ls输出是否含volume ID |
| 3. 发起备份 | trigger_backup() | 否 | 调用CinderV3Client.create_backup() |
| 4. 状态轮询 | wait_for_completion() | 否 | 调用BackupOperation.poll_until_complete() |
| 5. 元数据导出 | export_metadata() | 是 | 生成JSON含backup_id,volume_id,created_at,size_gb |
| 6. 本地归档 | archive_to_local() | 是 | tar -czf backup_{id}.tar.gz /var/lib/cinder/backups/{id} |
| 7. 清理临时资源 | cleanup_temp_resources() | 是 | 删除临时快照、释放内存缓存 |
注意:第2步
check_rbd_snapshot_consistency()默认关闭,需显式传入--ceph-mode参数才会启用。这是因为非Ceph后端(如LVM、NFS)根本没有RBD快照概念,硬检查会导致误报。
3. 运行实操:从零配置到首次成功备份的完整链路
cinderback不提供pip install cinderback,它被设计为“下载即用”的脚本集合。整个流程围绕config.yaml展开——这不是可选配置,而是启动前必须填满的契约文件。
3.1 配置文件详解:6个必填字段与2个条件字段
config.yaml模板长这样(删减了注释):
auth: auth_url: "https://keystone.internal:5000/v3" username: "cinder-backup-user" password: "s3cr3t_p@ss" project_name: "admin" user_domain_name: "Default" project_domain_name: "Default" region_name: "RegionOne" cinder: endpoint_url: "https://cinder.internal:8776/v3" timeout: 30 backup: max_concurrent: 3 retry_delay: 10 ceph_mode: false # 设为true时启用RBD快照检查 logging: level: "INFO" file: "/var/log/cinderback/backup.log" # 仅当ceph_mode: true时必需 ceph: rbd_pool: "volumes" rbd_client: "cinder" keyring_path: "/etc/ceph/ceph.client.cinder.keyring"提示:
endpoint_url必须指向Cinder API v3,不能写成/v2/或省略版本号。我们曾在线上环境因填了https://cinder.internal:8776/(无版本)导致所有请求返回301重定向,而脚本没处理重定向逻辑,最终在get_backup()里卡死。
3.2 执行备份:一条命令背后的真实动作序列
假设你要备份卷ID为a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8的块设备,执行:
python3 cinderback.py --config config.yaml --volume-id a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8 --backup-name "pre-migration-20240520"这条命令实际触发以下动作(按时间顺序):
- 加载配置:解析
config.yaml,验证auth和cinder区块必填项是否存在,缺失则报错退出; - 建立会话:调用
CinderSession.authenticate(),向Keystone发送POST/v3/auth/tokens,提取X-Subject-Token并缓存; - 卷状态检查:GET
/v3/{project_id}/volumes/a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8,确认status为available或in-use(后者需额外检查是否挂载到running VM); - 发起备份请求:POST
/v3/{project_id}/backups,请求体包含:{ "backup": { "volume_id": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8", "name": "pre-migration-20240520", "description": "Auto-created by cinderback v1.2.0", "incremental": false, "force": false } } - 轮询状态:每5秒GET
/v3/{project_id}/backups/{backup_id},直到status变为available或error; - 导出元数据:将响应中
backup对象的id、volume_id、created_at、size、object_count字段写入backup_meta_a1b2c3d4.json; - 记录日志:在
/var/log/cinderback/backup.log中写入结构化日志行:2024-05-20 14:23:45,123 INFO backup_flow.py:187 Backup a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8 completed in 42.8s, size=100GB, status=available
3.3 日志与调试:如何快速定位“卡在creating”的真凶
当备份长时间停留在creating状态,别急着重启cinder-backup服务。先看三处日志:
- cinderback自身日志(
/var/log/cinderback/backup.log):搜索BackupOperation.poll_until_complete,看是否出现TimeoutError或ConnectionError; - Cinder API日志(
/var/log/cinder/api.log):搜索backup_id,找Creating backup和后续的Failed to create backup行; - cinder-backup服务日志(
/var/log/cinder/backup.log):搜索Starting backup,确认是否有NoValidHost或RBDImageNotFound报错。
我们曾遇到一次典型故障:backup.log里有ERROR cinder.backup.drivers.ceph [req-...] RBDImageNotFound: Image volumes/volume-a1b2c3d4... not found。排查发现是Ceph poolvolumes里该镜像已被rbd rm手动删除,但Cinder DB里volumes表的status仍是available。此时cinderback的check_volume_status()只能查DB状态,无法感知底层RBD变化——这就是为什么第2章强调它不解决根因,只暴露问题。
4. 常见问题排查:5条血泪经验总结的“踩坑清单”
cinderback在多个OpenStack版本(Rocky到Yoga)和存储后端(Ceph、LVM、NetApp)上跑过,以下是高频翻车点,按现象→原因→解决结构整理:
4.1 现象:Authentication failed: Invalid token,但keystone命令行能正常登录
原因:config.yaml中project_domain_name填成了project_domain_id(ID是UUID字符串,而name是Default)。OpenStack Keystone对这两个字段校验极严,填错一个字符就返回401,且错误信息不提示具体哪个字段错。
解决:用openstack project show admin -f value -c domain_id确认domain ID,再用openstack domain list查domain name,确保project_domain_name字段值与openstack domain list输出的第一列完全一致(注意大小写和空格)。
4.2 现象:备份任务始终卡在creating,backup.log里有NoValidHost
原因:cinder-backup服务在Placement中注册的resource provider(RP)缺少CUSTOM_BACKUP_DISK_GBtrait,或RP的total_capacity_gb为0。OpenStack Yoga+版本要求备份服务必须声明自己能提供多少备份存储空间。
解决:登录controller节点,执行openstack resource provider list --name cinder-backup找到RP UUID,再运行:
openstack resource provider trait set --trait CUSTOM_BACKUP_DISK_GB <rp_uuid> openstack resource provider inventory set --resource-class DISK_GB --total 10000 <rp_uuid>4.3 现象:ceph_mode: true时备份失败,报错rbd: command not found
原因:脚本默认调用系统PATH里的rbd命令,但某些Ceph部署(如容器化cinder-backup)把rbd放在/usr/bin/rbd之外,或未安装ceph-common包。
解决:在config.yaml中显式指定ceph.rbd_binary_path,例如:
ceph: rbd_binary_path: "/usr/local/bin/rbd"并在执行前用python3 -c "import shutil; print(shutil.which('rbd'))"验证路径是否可达。
4.4 现象:并发备份(max_concurrent: 3)时部分任务超时,日志显示Connection pool is full
原因:requests.Session默认连接池大小为10,3个并发任务各占1个连接,但每个任务在轮询阶段会持续占用连接(因keep-alive),导致第4个任务排队等待。
解决:在auth/session.py的CinderSession.__init__()中增加:
self.mount("http://", requests.adapters.HTTPAdapter( pool_connections=20, pool_maxsize=20, max_retries=urllib3.Retry(total=3, backoff_factor=0.3) ))4.5 现象:备份成功后export_metadata()生成的JSON里object_count为0
原因:Cinder备份元数据中的object_count字段由cinder-backup服务计算,它依赖swift或ceph后端的list_objects操作。若备份服务配置了backup_swift_url但Swift集群不可达,该字段就会为0,但不影响备份文件本身。
解决:这不是cinderback的bug,而是备份服务配置问题。检查/etc/cinder/cinder.conf中[backup]区块的backup_driver和对应后端配置,确保backup_ceph_conf或backup_swift_url指向可用服务。
5. 二次开发指南:给备份流程加“后悔药”和“进度条”
cinderback的扩展性设计体现在两个接口:BackupFlow的钩子方法(hook)和CinderV3Client的中间件机制。我们不需要改核心逻辑,就能插入自定义行为。
5.1 给备份加“后悔药”:实现备份前快照+备份后自动回滚
某次误操作导致备份覆盖了正确数据,团队决定在备份前强制创建Ceph快照,并在备份失败时自动回滚。这不是加个--rollback-on-fail参数就能搞定的——因为回滚操作必须在trigger_backup()之后、wait_for_completion()之前执行,且要捕获所有可能异常。
解决方案:继承BackupFlow,重写_execute_step()方法,在步骤3(触发备份)后插入快照清理逻辑:
# extensions/rollback_flow.py class RollbackBackupFlow(BackupFlow): def _execute_step(self, step_name: str, *args, **kwargs): if step_name == "trigger_backup": # 在触发备份前,先创建RBD快照 vol_id = kwargs["volume_id"] snapshot_name = f"cinderback_prebackup_{int(time.time())}" self._create_rbd_snapshot(vol_id, snapshot_name) # 把快照名存入实例变量,供后续回滚用 self._prebackup_snapshot = (vol_id, snapshot_name) result = super()._execute_step(step_name, *args, **kwargs) if step_name == "wait_for_completion" and result.status == "error": # 备份失败,触发回滚 self._rollback_to_snapshot() return result def _create_rbd_snapshot(self, vol_id: str, snap_name: str): cmd = [self.config.ceph.rbd_binary_path, "snap", "create", f"{self.config.ceph.rbd_pool}/volume-{vol_id}", "--snap", snap_name] subprocess.run(cmd, check=True) def _rollback_to_snapshot(self): vol_id, snap_name = self._prebackup_snapshot cmd = [self.config.ceph.rbd_binary_path, "snap", "rollback", f"{self.config.ceph.rbd_pool}/volume-{vol_id}", "--snap", snap_name] subprocess.run(cmd, check=True)然后在主脚本中替换初始化:
# cinderback.py if args.rollback_on_fail: flow = RollbackBackupFlow(config) else: flow = BackupFlow(config)5.2 给轮询加“进度条”:用rich库渲染实时状态
原生轮询只有日志输出,运维人员无法直观感知进度。我们用rich.progress实现动态进度条,关键是要把BackupOperation.poll_until_complete()的轮询循环暴露出来:
# core/backup_flow.py from rich.progress import Progress, SpinnerColumn, TextColumn def wait_for_completion_with_progress(self, client: "CinderV3Client") -> "BackupOperation": with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True ) as progress: task = progress.add_task("Waiting for backup...", total=self._max_retries) for i in range(self._max_retries): resp = client.get_backup(self.backup_id) self.status = resp["backup"]["status"] progress.update(task, advance=1, description=f"Backup {self.backup_id[:8]}: {self.status}") if self.status in ["available", "error", "restoring"]: return self time.sleep(self._poll_interval) raise TimeoutError(...)注意:
rich不兼容Windows终端,生产环境部署前需加平台判断:
if os.name == "nt": from rich.console import Console console = Console(force_terminal=False) # 禁用Windows ANSI else: console = Console()5.3 改造验证:如何证明你的扩展没破坏原有逻辑
每次改完代码,必须跑通三类验证:
| 验证类型 | 命令 | 预期结果 | 失败含义 |
|---|---|---|---|
| 单元测试 | pytest tests/test_backup_flow.py -v | 所有test_*方法通过 | 核心流程逻辑断裂 |
| 集成测试 | python3 cinderback.py --config test_config.yaml --volume-id fake-id --dry-run | 输出DRY RUN: would create backup for volume fake-id,无HTTP请求发出 | Session或Client初始化异常 |
| 真机冒烟 | python3 cinderback.py --config prod_config.yaml --volume-id <real-id> --backup-name smoke-test | 生成backup_meta_*.json且status=available | 认证、网络、权限任一环节失败 |
从那以后我每次提交extensions/目录下的代码,都强制走一遍这三步验证——哪怕只是改一行日志格式。因为cinderback不是玩具项目,它跑在别人生产环境的备份流水线上,少一个try/except就可能让整个灾备窗口失效。希望帮到你。
本文还有配套的精品资源,点击获取