Open edX 用户退休(User Retirement)脚本实战:scripts/user_retirement 的部署、配置与运行指南

Open edX 用户退休(User Retirement)脚本实战:scripts/user_retirement 的部署、配置与运行指南 Open edX 用户退休User Retirement脚本实战scripts/user_retirement 的部署、配置与运行指南【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform本文基于 openedx-platform 仓库中 scripts/user_retirement/README.rst 展开讲解 Open edX 用户退休User Retirement驱动脚本的完整使用流程如何以稀疏克隆方式获取脚本、用 uv 搭建运行环境、编写 YAML 驱动配置、执行get_learners_to_retire.py与retire_one_learner.py完成 PII 数据擦除流水线并配合仓库内 docs 目录下的设计文档与源码理解其状态机模型、LMS 侧 Django 设置与失败恢复机制。一、用户退休功能与脚本目录定位随着 GDPR 等隐私法规的演进Open edX 平台需要提供遗忘用户个人身份信息PII的能力。学习者可以在 LMS 账户页面点击Delete My Account确认删除请求其账户被停用并进入PENDING状态随后由集中的driver驱动脚本编排对 LMS、论坛、ecommerce、credentials 等内部系统以及第三方营销服务的删除/解绑请求。这一设计目标在 docs/index.rst 与 docs/implementation_overview.rst 中有完整阐述。README.rst 说明scripts/user_retirement目录中的 Python 脚本是从 edX 早期的tubular仓库迁移而来的用于驱动用户退休工作流处理账户的停用与移除。这些脚本设计为可被任意自动化/CI 框架调用。从源码结构看该目录的组织方式如下顶层驱动脚本retire_one_learner.py、get_learners_to_retire.py、replace_usernames.py、retirement_archive_and_cleanup.py、retirement_bulk_status_update.py、retirement_partner_report.py共享工具库utils/LMS/IDA API 客户端 utils/edx_api.py、Jenkins 属性文件导出 utils/jenkins.py以及第三方营销 API 封装utils/thirdparty_apis/ 下的 amplitude、braze、hubspot、salesforce、segment 等依赖与配置pyproject.toml、requirements/base.txt、requirements/testing.txt、uv.lock、pytest.ini测试tests/ 目录包含对每个顶层脚本及第三方 API 客户端的测试用例二、退休工作流与状态机模型在理解脚本行为之前需要先理解 docs/implementation_overview.rst 描述的工作流模型工作流是一条由积木式 API组成的可配置流水线用于遗忘用户 PII、阻止用户重新登录、防止用户名/邮箱被复用流水线是线性的、可重跑的rerunnable某一阶段失败后可以恢复继续多个用户可以同时处于不同状态LMS 是状态的权威来源UserRetirementStatus模型记录每个用户当前所处的状态RetirementState模型则存储状态列表本身而非硬编码因为 Open edX 社区需要的状态无法预先穷举。典型的示例状态流转路径为PENDING - RETIRING_ENROLLMENTS - ENROLLMENTS_COMPLETE - RETIRING_FORUMS - FORUMS_COMPLETE - ... - COMPLETE其中任意RETIRING_*状态在 API 调用失败4xx/5xx时都会进入终端状态ERROREDPENDING也可进入ABORTED。除工具内部错误外用户最终必然落在COMPLETE/ERRORED/ABORTED三个终端状态之一。从 retire_one_learner.py 的源码可以印证这一模型——脚本用常量定义了这些魔法状态# Magic states with special meaning, these are required to be in LMS START_STATE PENDING ERROR_STATE ERRORED COMPLETE_STATE COMPLETE ABORTED_STATE ABORTED END_STATES (ERROR_STATE, ABORTED_STATE, COMPLETE_STATE)学习者视角的体验根据 docs/index.rst学习者点击Delete My Account按钮并输入密码确认后其所有浏览器会话会被登出并锁定账户系统随即发送确认邮件学习者在此后有限时间内由驱动脚本的--cool_off_days参数定义可联系管理员撤销请求。此时账户被停用deactivated但尚未退休retiredUserRetirementStatus表中新增一条记录并置为PENDING。LMS 设置中的FEATURES[ENABLE_ACCOUNT_DELETION]默认True控制Delete My Account区块的可见性。另外通过第三方认证社交登录注册的学习者必须先解除 LMS 账户与第三方账户的绑定否则删除按钮保持禁用状态。三、获取脚本稀疏克隆Sparse CheckoutREADME.rst 给出了通过部分克隆partial clone只获取脚本目录的方法避免下载整个 edx-platform 仓库。原文档给出的完整流程如下repo_url替换为 Open edX 平台仓库的克隆地址repo_urlOpen edX 平台仓库地址 branchmaster directoryscripts/user_retirement git clone --branch $branch --single-branch --depth1 --filtertree:0 $repo_url cd edx-platform git sparse-checkout init --cone git sparse-checkout set $directory要点说明--single-branch --depth1只拉取目标分支的最新一次提交减少下载量--filtertree:0是不带 tree 对象的克隆配合sparse-checkout实现按目录过滤git sparse-checkout set $directory之后工作区中只会出现scripts/user_retirement下的文件。README 同时指出也可以选用其他工具或库来完成部分克隆上述步骤只是演示。四、安装依赖uv推荐与传统 venv 两种方式方式一使用 uvREADME.rst 推荐用 uv 安装依赖uv sync --project scripts/user_retirement --frozen--frozen表示直接使用锁定的依赖集合对应 uv.lock保证环境可复现。随后所有命令都加上uv run --project scripts/user_retirement前缀在该虚拟环境中执行也可以直接激活环境source scripts/user_retirement/.venv/bin/activate方式二不使用 uvpyproject.toml 声明了requires-python 3.12因此不借助 uv 时需要用 Python 3.12 创建虚拟环境并从兼容性导出文件requirements/base.txt安装python3.12 -m venv ../venv source ../venv/bin/activate pip install -r scripts/user_retirement/requirements/base.txt从源码看该脚本集的核心依赖pyproject.toml包括click命令行解析、pyyaml驱动配置解析、requests/backoffAPI 调用与重试、edx-rest-api-clientIDP API 客户端、jenkinsapi、unicodecsv、simplejson、simple-salesforce、google-api-python-client、boto3等与其编排 LMS 第三方服务的定位一致。五、建立入口点Entry PointsREADME.rst 中说明先给脚本目录中的入口点脚本加执行权限并 source 它chmod x scripts/user_retirement/entry_points.sh source scripts/user_retirement/entry_points.shentry_points.sh 的实际内容是一组 shell alias把顶层脚本映射为短命令alias get_learners_to_retire.pypython scripts/user_retirement/get_learners_to_retire.py alias replace_usernames.pypython scripts/user_retirement/replace_usernames.py alias retire_one_learner.pypython scripts/user_retirement/retire_one_learner.py alias retirement_archive_and_cleanup.pypython scripts/user_retirement/retirement_archive_and_cleanup.py alias retirement_bulk_status_update.pypython scripts/user_retirement/retirement_bulk_status_update.py alias retirement_partner_report.pypython scripts/user_retirement/retirement_partner_report.py因此retire_one_learner.py这样的命令实际上是走 alias 再调用python 文件路径的执行方式。当然也可以不依赖 alias直接用文件路径执行 Python 脚本。六、驱动配置与两个核心驱动脚本docs/driver_setup.rst 指出scripts/user_retirement中包含两个用于驱动退休工作流的脚本它们共享一个必需的--config_file参数指向环境的驱动配置文件get_learners_to_retire.py生成已准备好立即退休的用户列表——即处于PENDING状态且已满足--cool_off_days冷却天数的用户。输出格式为 Jenkins 消费而设计为每个用户生成一个下游构建。retire_one_learner.py退休--username参数指定的用户。6.1 驱动配置文件YAML该配置是一个 YAML 文件包含 LMS 认证凭据auth secrets、API URL 映射和该环境特有的退休流水线阶段。docs/driver_setup.rst 给出的示例client_id: client ID for the retirement service user client_secret: client secret for the retirement service user base_urls: lms: https://courses.example.com/ ecommerce: https://ecommerce.example.com/ credentials: https://credentials.example.com/ retirement_pipeline: - [RETIRING_EMAIL_LISTS, EMAIL_LISTS_COMPLETE, LMS, retirement_retire_mailings] - [RETIRING_ENROLLMENTS, ENROLLMENTS_COMPLETE, LMS, retirement_unenroll] - [RETIRING_LMS_MISC, LMS_MISC_COMPLETE, LMS, retirement_lms_retire_misc] - [RETIRING_LMS, LMS_COMPLETE, LMS, retirement_lms_retire]各字段含义client_id/client_secretOAuth 凭据直接复制自create_dot_application管理命令的输出见下文退休服务用户一节base_urlsIDA 名称到基础 URL 的映射脚本据此拼接 API 地址。LMS 是必需的若流水线中还有对其它服务的 API 调用ecommerce、credentials 等这些服务也必须出现在base_urls中retirement_pipeline定义每个环境的执行步骤、状态名与顺序。每一项是一个四元素列表起始状态名start state结束状态名end state要调用的服务 key如LMS、LICENSE_MANAGER在 utils/edx_api.py 中调用的方法名例如[RETIRING_ENROLLMENTS, ENROLLMENTS_COMPLETE, LMS, retirement_unenroll]的含义是先把用户状态置为RETIRING_ENROLLMENTS调用已实例化的LmsApi.retirement_unenroll方法成功后把状态置为ENROLLMENTS_COMPLETE。从源码看这一解释与 retire_one_learner.py 的主循环完全吻合for start_state, end_state, service, method in config[retirement_pipeline]: # Skip anything that has already been done if config[all_states].index(start_state) learner_state_index: LOG(State {} completed in previous run, skipping.format(start_state)) continue ... config[LMS].update_learner_retirement_state(username, start_state, Starting: {}.format(start_state)) # This does the actual API call response getattr(config[service], method)(learner) ... config[LMS].update_learner_retirement_state(username, end_state, Ending: {} with response:\n{}.format(end_state, response))这里能看到可重跑设计的关键实现每次执行都先向 LMS 查询用户当前状态并映射到流水线中的索引凡是索引早于当前状态的阶段直接跳过每个阶段开始/结束时都会调用update_learner_retirement_state回写 LMS把响应内容写入状态记录。若中途抛异常脚本会把用户置为ERRORED并带上异常信息见下文从 ERRORED 恢复。retire_one_learner.py 顶部还定义了各失败场景的返回码便于 CI 框架区分失败原因ERR_SETUP_FAILED -1 ERR_USER_AT_END_STATE -2 ERR_USER_IN_WORKING_STATE -3 ERR_WHILE_RETIRING -4 ERR_BAD_LEARNER -5 ERR_UNKNOWN_STATE -6 ERR_BAD_CONFIG -7其中ERR_USER_AT_END_STATE用户已在终端状态与ERR_USER_IN_WORKING_STATE用户正处于RETIRING_*工作态可能有另一进程在处理都来自 retire_one_learner.py 的状态校验逻辑——它要求学习者不在END_STATES中、也不在config[working_states]即流水线各项的第一个状态中否则会直接退出防止并发重复执行同一用户。6.2 获取待退休学习者列表docs/driver_setup.rst 给出的示例mkdir learners_to_retire get_learners_to_retire.py \ --config_filepath/to/config.yml \ --output_dirlearners_to_retire \ --cool_off_days5从 get_learners_to_retire.py 源码可以补全参数与默认值的完整说明脚本用click解析参数且支持RETIREMENT_前缀的环境变量传参参数默认值说明--config_file无必填YAML 配置文件路径--cool_off_days7学习者在退休队列PENDING中需停留的天数达到后才会被实际退休--output_dir./jenkins_props输出 Jenkins 属性文件的目录--user_count_error_threshold300安全阀若返回的用户数超过该值直接报错退出而不退休防止攻击者向退休队列批量注入用户--max_user_batch_size200单次最多获取 X 个用户若低于user_count_error_threshold则只截断不报错脚本的执行逻辑get_learners_to_retire.py读取 YAML 配置后构造states_to_request [PENDING] end_states即PENDING加上流水线各阶段的结束状态调用LmsApi.learners_to_retire(...)拉取列表按max_user_batch_size截断、按user_count_error_threshold检查最后通过 utils/jenkins.py 的export_learner_job_properties把结果写成每用户一个的 INI 属性文件。6.3 对单个学习者执行退休README.rst 与 docs/driver_setup.rst 都给出了对单个用户执行退休的命令。运行完get_learners_to_retire.py后输出目录中会有若干 INI 文件每个文件包含一行USERNAMEusername-of-learner遍历这些文件对每个用户执行retire_one_learner.py \ --config_filepath/to/config.yml \ --usernameusername-of-learner-to-retire也可以直接用文件路径执行 Python 脚本python scripts/user_retirement/retire_one_learner.py \ --config_filesrc/config.yml \ --usernameuser1记得把src/config.yml替换为你的实际配置文件路径、user1替换为实际用户名。retire_one_learner.py 文件头部的 docstring 中还给出了本地环境devstack 风格的端口的配置示例其中流水线示例使用了LICENSE_MANAGER服务client_id: client id from LMS DOT client_secret: client secret from LMS DOT base_urls: lms: http://localhost:18000/ ecommerce: http://localhost:18130/ credentials: http://localhost:18150/ retirement_pipeline: - [RETIRING_LICENSE_MANAGER, LICENSE_MANAGER_COMPLETE, LICENSE_MANAGER, retire_learner] - [RETIRING_FORUMS, FORUMS_COMPLETE, LMS, retirement_retire_forum] - [RETIRING_EMAIL_LISTS, EMAIL_LISTS_COMPLETE, LMS, retirement_retire_mailings] - [RETIRING_ENROLLMENTS, ENROLLMENTS_COMPLETE, LMS, retirement_unenroll] - [RETIRING_LMS, LMS_COMPLETE, LMS, retirement_lms_retire]另外从 entry_points.sh 与目录结构看该目录还提供了一批配套运维脚本用于流水线的收尾与报告replace_usernames.py退休后的用户名处理、retirement_archive_and_cleanup.py归档与清理退休队列数据、retirement_bulk_status_update.py批量更新状态、retirement_partner_report.py面向合作方/第三方的退休报告。这些同样通过 alias 暴露为短命令测试覆盖见 tests/test_retirement_archive_and_cleanup.py、tests/test_retirement_bulk_status_update.py 等文件。七、LMS 侧设置Django 设置、状态表与服务用户docs/service_setup.rst 描述了 LMS 侧需要完成的设置这是驱动脚本能够工作的另一半前提。7.1 控制用户退休行为的 Django 设置设置名默认值说明RETIRED_USERNAME_PREFIXretired__user_哈希用户名所用的前缀供RETIRED_USERNAME_FMT使用RETIRED_EMAIL_PREFIXretired__user_哈希邮箱所用的前缀供RETIRED_EMAIL_FMT使用RETIRED_EMAIL_DOMAINretired.invalid哈希邮箱的域名部分供RETIRED_EMAIL_FMT使用RETIRED_USERNAME_FMTlambda settings: settings.RETIRED_USERNAME_PREFIX {}退休后用户名转换成的格式{}被用户名的哈希值替换RETIRED_EMAIL_FMTlambda settings: settings.RETIRED_EMAIL_PREFIX {} settings.RETIRED_EMAIL_DOMAIN退休后邮箱转换成的格式{}被邮箱哈希值替换RETIRED_USER_SALTSNone用户名/邮箱哈希使用的盐值列表。只有列表最后一项用于所有新的退休历史盐值保留以保证所有历史哈希值仍可校验。默认值必须覆盖RETIREMENT_SERVICE_WORKER_USERNAMERETIREMENT_SERVICE_USER退休服务 worker 的用户名RETIREMENT_STATES见lms/envs/common.py中的RETIREMENT_STATES设置定义退休工作流状态名与顺序的列表FEATURES[ENABLE_ACCOUNT_DELETION]True是否在账户设置页面显示 Delete My Account 区块文档特别指出部分设置的值是 lambda 函数而非普通字符串字面量这是 Open edX 特有的派生设置derived settings模式。7.2 退休状态表与 populate_retirement_states每个用户的退休状态存储在 LMS 数据库中状态列表本身也单独存储在数据库中RetirementState模型。由于状态列表会随时间、随不同安装而变化填充状态列表是管理员的责任。文档对状态列表提出了硬性约束至少要有开头的PENDING状态以及结尾的COMPLETED、ERRORED、ABORTED状态每一个RETIRING_foo状态都必须有对应的foo_COMPLETE状态。如需自定义状态通常在lms.yml中覆盖RETIREMENT_STATES然后用管理命令把状态表与设置同步$ ./manage.py lms --settingsyour-settings populate_retirement_states All states removed and new states added. Differences: Added: set([uRETIRING_ENROLLMENTS, uRETIRING_LMS, uLMS_MISC_COMPLETE, uRETIRING_LMS_MISC, uENROLLMENTS_COMPLETE, uLMS_COMPLETE]) Removed: set([]) Remaining: set([uERRORED, uPENDING, uABORTED, uCOMPLETE]) States updated successfully. Current states: PENDING (step 1) RETIRING_ENROLLMENTS (step 11) ENROLLMENTS_COMPLETE (step 21) ... COMPLETE (step 91)该命令是幂等的始终让状态表与设置中的RETIREMENT_STATES列表保持一致已存在的状态列在Remaining下不会重复添加。7.3 创建退休服务用户驱动脚本以退休服务用户的身份、通过 OAuth 客户端凭据与 LMS/IDA 认证因此必须创建该用户并生成 DOT 应用与客户端凭据app_nameretirement user_nameretirement_service_worker ./manage.py lms --settingsyour-settings manage_user $user_name $user_nameexample.com --staff --superuser ./manage.py lms --settingsyour-settings create_dot_application $app_name $user_name注意客户端凭据client ID 与 client secret只会打印到终端一次需要及时复制保存之后填入驱动脚本的 YAML 配置。退休服务用户还需要具备执行退休任务的权限通过在 Django 设置中指定RETIREMENT_SERVICE_WORKER_USERNAME完成RETIREMENT_SERVICE_WORKER_USERNAME retirement_service_worker7.4 Django 管理后台入口Django Admin 的USER_API分组下有三个与用户退休相关的模型名称URI说明Retirement States/admin/user_api/retirementstate/状态表由RETIREMENT_STATES定义、populate_retirement_states填充User Retirement Requests/admin/user_api/userretirementrequest/记录所有曾请求删除账户的用户 ID主要用于内部审计记账User Retirement Statuses/admin/user_api/userretirementstatus/管理每个学习者退休状态的核心模型特殊情况下可在此手动改状态八、处理特殊情况失败恢复、批量重跑与取消退休docs/special_cases.rst 覆盖了运维中最重要的三类场景。8.1 从 ERRORED 状态恢复当某个退休 API 返回失败4xx 或 5xx驱动脚本会立即把用户状态置为ERRORED。排查方法查看user_api_userretirementstatus表中该用户行的responses字段脚本每次状态切换都会把 API 响应写入该字段见 retire_one_learner.py 中的update_learner_retirement_state调用。问题修复后手动把current_state重置为应重试状态的前一个状态例如在 forums 退休阶段出错则从ERRORED改回ENROLLMENTS_COMPLETE。改完之后驱动脚本下次执行时会自动从断点继续——这正是 retire_one_learner.py 中跳过已完成阶段逻辑带来的可重跑能力。8.2 重跑部分或全部状态如果想从最初开始重跑所有退休把所有current_state COMPLETE的记录改回PENDING即可。典型场景全体退休跑完之后但清理退休队列之前新加了一个流水线阶段或某个阶段/API 开发时实际未生效却返回了成功。退休 API 被设计为幂等的对已经跑完的阶段重跑应该是无副作用的no-op。8.3 取消退休请求刚提交删除请求、仍处于PENDING状态的用户可以通过联系管理员撤销请求。平台提供管理命令给定用户邮箱即可恢复其登录能力并使其退出所有退休队列$ ./manage.py lms --settingsyour-settings cancel_user_retirement_request email-of-user-to-cancel-retirement限制该命令只对状态尚未越过PENDING的用户有效且用户恢复访问前需要重置密码。九、扩展退休流水线docs/extending_retirement.rst 说明当前退休代码只支持有限的一组服务LMS、Ecommerce、Course Discovery 以及部分历史第三方提供方要接入新系统例如学校的邮件服务商有两种主要方式LMS 内监听 Django Signal用户退休时 LMS 会发射名为USER_RETIRE_LMS_MISC的信号定义于openedx.core.djangoapps.user_api.accounts.signals可以编写 edx-platform 插件监听该信号在 LMS 内直接执行清理动作带外out of band进程利用退休 API 查询近期退休的用户在用户退休与退休数据被完全清除之间的时间窗口内把用户传播到外部系统并删除。具体可参照 utils/edx_api.py 的LmsApi例如用get_learners_by_date_and_status方法查找COMPLETE状态且最近 24 小时内更新的用户。该方式没有第一种的状态保证但能执行更灵活的动作。十、运行测试用例README.rst 最后给出了测试的两种运行方式。使用 uvuv run --project scripts/user_retirement --group test pytest scripts/user_retirement--group test对应 pyproject.toml 中的test依赖组pytest、requests_mock、responses、moto、mock、ddt等。不使用 uv先安装测试依赖再直接运行 pytestpip install -r scripts/user_retirement/requirements/testing.txt pytest scripts/user_retirement测试用例分布在 tests/ 目录按脚本与 API 客户端组织例如 tests/test_get_learners_to_retire.py、tests/test_retire_one_learner.py、tests/utils/test_edx_api.py以及 tests/utils/thirdparty_apis/ 下对 amplitude、braze、hubspot、salesforce、segment 客户端的测试。十一、小结一条可复现的退休流水线把 README 与 docs 串起来一个完整的部署闭环是稀疏克隆仓库只取scripts/user_retirementuv sync --project scripts/user_retirement --frozen建好环境LMS 侧配置RETIREMENT_STATES、RETIRED_USER_SALTS等 Django 设置执行populate_retirement_states同步状态表创建退休服务用户并记录 DOT 客户端凭据编写含client_id/client_secret/base_urls/retirement_pipeline四段 YAML 的驱动配置source scripts/user_retirement/entry_points.sh后按拉名单 → 逐人退休的节奏运行get_learners_to_retire.py注意cool_off_days、user_count_error_threshold安全阀与retire_one_learner.py失败时借助responses字段定位在 Django Admin 中把ERRORED重置回断点前的状态重新执行即可自动续跑需要接第三方系统时优先监听USER_RETIRE_LMS_MISC信号或用LmsApi.get_learners_by_date_and_status做带外同步。整套工具的可重跑性、幂等 API 设计、LMS 作为状态权威源这三点是它与一次性数据擦除脚本的本质区别也是运维时最重要的心智模型。【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考