供应系统
OmniCRM 使用 Ansible 来自动化客户服务的供应、配置和取消供应。供应系统旨在灵活,能够支持复杂的工作流程,同时保持一致性和可靠性。

::: note ::: title 注意 :::
有关产品到服务旅程的完整演练,包括详细的 Ansible 剧本示例、定价策略和实际场景,请参见 完整产品生命周期指南。 :::
概述
当产品被订购或服务需要配置时,OmniCRM 创建一个 供应作业,执行一个或多个 Ansible 剧本。这些剧本与各种后端系统(OCS/CGRateS、网络设备、API 等)进行交互,以完全供应服务。
供应系统支持两种主要工作流程:
- 标准供应 - 通过 UI/API 由员工或客户触发
- 简单供应 - 由 OCS 等外部系统触发,用于自动化操作
供应状态值
供应作业和单个任务可以具有以下状态:
- 状态 0(成功) - 供应作业成功完成
- 状态 1(运行中) - 供应作业或任务当前正在执行
- 状态 2(失败 - 严重) - 发生了严重故障,导致供应失败
- 状态 3(失败 - 被忽略) - 任务失败但具有
ignore_errors: true,因此供应继续
当供应作业失败时,OmniCRM 会向配置的失败通知列表发送电子邮件通知,并附上详细的错误信息。
产品如何驱动供应
产品 定义是供应内容及其方式的蓝图。当用户选择要供应的产品时,系统会从产品定义中读取几个关键字段以确定要执行的操作。
供应中使用的产品字段
产品定义包含:
provisioning_play- 要执行的 Ansible 剧本的名称(不带 .yaml 扩展名)provisioning_json_vars- 包含要传递给 Ansible 的默认变量的 JSON 字符串inventory_items_list- 必须分配的库存类型列表(例如,['SIM Card', 'Mobile Number'])product_id、product_name、定价字段 - 自动传递给剧本
示例产品定义
{
"product_id": 1,
"product_slug": "Mobile-SIM",
"product_name": "Mobile SIM Only",
"provisioning_play": "play_psim_only",
"provisioning_json_vars": "{\"iccid\": \"\", \"msisdn\": \"\"}",
"inventory_items_list": "['SIM Card', 'Mobile Number']",
"retail_cost": 0,
"retail_setup_cost": 0,
"wholesale_cost": 3,
"wholesale_setup_cost": 1
}
从产品到供应作业
当供应被启动时,系统:
-
加载
provisioning_play中指定的剧本系统查找
OmniCRM-API/Provisioners/plays/play_psim_only.yaml -
将多个来源的变量合并到
extra_vars中:a. 来自 provisioning_json_vars:
{"iccid": "", "msisdn": ""}b. 来自请求体: 用户/API 提供的任何附加变量 c. 来自产品字段:product_id、customer_id等 d. 来自身份验证:access_token或用于refresh_token的设置 -
根据
inventory_items_list分配库存在运行剧本之前,UI/API 提示进行库存选择:
- SIM 卡 - 用户从库存中选择一个可用的 SIM
- 手机号码 - 用户选择一个可用的电话号码
选定的库存 ID 被添加到
extra_vars中,库存类型作为键:extra_vars = {
"product_id": 1,
"customer_id": 456,
"SIM Card": 789, # 选定 SIM 的 inventory_id
"Mobile Number": 101, # 选定电话号码的 inventory_id
"iccid": "", # 来自 provisioning_json_vars
"msisdn": "", # 来自 provisioning_json_vars
"access_token": "eyJ..."
} -
通过
hostvars[inventory_hostname]将所有内容传递给 Ansible在剧本内部,变量可以访问如下:
- name: Get inventory_id for SIM Card
set_fact:
inventory_id_sim_card: "{{ hostvars[inventory_hostname]['SIM Card'] | int }}"
when: "'SIM Card' in hostvars[inventory_hostname]"
剧本如何使用库存变量
一旦剧本获得库存 ID,它将从 API 获取完整的库存详细信息:
- name: Get SIM Card Details from Inventory
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ inventory_id_sim_card }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: sim_card_response
- name: Extract ICCID and IMSI from inventory
set_fact:
iccid: "{{ sim_card_response.json.iccid }}"
imsi: "{{ sim_card_response.json.imsi }}"
- name: Get Phone Number Details from Inventory
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ inventory_id_phone_number }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: phone_number_response
- name: Extract MSISDN
set_fact:
msisdn: "{{ phone_number_response.json.msisdn }}"
然后剧本可以使用这些值来:
- 在 HSS 上供应 SIM 卡和 IMSI
- 在计费系统中配置电话号码
- 将库存项目分配给客户
- 创建包含这些详细信息的服务记录
实际示例:移动 SIM 供应
从 play_psim_only.yaml,它如何使用产品和库存数据:
- name: Get Product information from CRM API
uri:
url: "{{ crm_config.crm.base_url }}/crm/product/product_id/{{ product_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: api_response_product
- name: Set package facts from product
set_fact:
package_name: "{{ api_response_product.json.product_name }}"
package_comment: "{{ api_response_product.json.comment }}"
setup_cost: "{{ api_response_product.json.retail_setup_cost }}"
monthly_cost: "{{ api_response_product.json.retail_cost }}"
- name: Set inventory_id_sim_card if SIM Card was selected
set_fact:
inventory_id_sim_card: "{{ hostvars[inventory_hostname]['SIM Card'] | int }}"
when: "'SIM Card' in hostvars[inventory_hostname]"
- name: Set inventory_id_phone_number if Mobile Number was selected
set_fact:
inventory_id_phone_number: "{{ hostvars[inventory_hostname]['Mobile Number'] | int }}"
when: "'Mobile Number' in hostvars[inventory_hostname]"
- name: Get SIM Card details from inventory
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ inventory_id_sim_card }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: sim_inventory_response
- name: Get Phone Number details from inventory
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ inventory_id_phone_number }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: phone_inventory_response
- name: Extract values from inventory
set_fact:
iccid: "{{ sim_inventory_response.json.iccid }}"
imsi: "{{ sim_inventory_response.json.imsi }}"
msisdn: "{{ phone_inventory_response.json.msisdn }}"
ki: "{{ sim_inventory_response.json.ki }}"
opc: "{{ sim_inventory_response.json.opc }}"
- name: Provision subscriber on HSS
uri:
url: "http://{{ hss_server }}/subscriber/{{ imsi }}"
method: PUT
body_format: json
body:
{
"imsi": "{{ imsi }}",
"msisdn": "{{ msisdn }}",
"ki": "{{ ki }}",
"opc": "{{ opc }}",
"enabled": true
}
status_code: 200
- name: Assign inventory to customer
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ inventory_id_sim_card }}"
method: PATCH
headers:
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": {{ customer_id }},
"item_state": "Assigned"
}
status_code: 200
这演示了完整的流程:
- 产品定义指定
provisioning_play: "play_psim_only" - 产品需要
inventory_items_list: ['SIM Card', 'Mobile Number'] - 用户在供应过程中选择库存项目
- 库存 ID 作为
extra_vars传递给剧本 - 剧本从 API 获取完整的库存详细信息
- 剧本使用库存数据配置网络设备
- 剧本将库存标记为分配给客户
回滚和清理:最佳实践模式
关键最佳实践:同一剧本应处理失败的供应回滚和使用 Ansible 的有意取消供应,使用 block 和 rescue 结构。
剧本结构
来自 play_psim_only.yaml:
- name: OmniCore Service Provisioning 2024
hosts: localhost
gather_facts: no
become: False
tasks:
- name: Main block
block:
# --- 供应任务 ---
- name: Get Product information
uri: ...
- name: Create account in OCS
uri: ...
- name: Provision subscriber on HSS
uri: ...
- name: Create service record
uri: ...
# ... 许多其他供应任务 ...
rescue:
# --- 清理任务 ---
# 此部分在以下情况下运行:
# 1. 块中的任何任务失败(回滚)
# 2. action == "deprovision"(有意清理)
- name: Get Inventory items linked to this service
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/customer_id/{{ customer_id }}"
method: GET
register: inventory_api_response
ignore_errors: True
- name: Return inventory to pool
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ item.inventory_id }}"
method: PATCH
body_format: json
body:
service_id: null
customer_id: null
item_state: "Used"
with_items: "{{ inventory_api_response.json.data }}"
ignore_errors: True
- name: Delete Account from Charging
uri:
url: "http://{{ crm_config.ocs.OCS }}/jsonrpc"
method: POST
body:
{
"method": "ApierV1.RemoveAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}"
}]
}
ignore_errors: True
- name: Delete Attribute Profile
uri:
url: "http://{{ crm_config.ocs.OCS }}/jsonrpc"
method: POST
body:
{
"method": "APIerSv1.RemoveAttributeProfile",
"params": [{
"ID": "ATTR_ACCOUNT_{{ service_uuid }}"
}]
}
ignore_errors: True
- name: Remove Resource Profile
uri: ...
ignore_errors: True
- name: Remove Filters
uri: ...
ignore_errors: True
- name: Deprovision Subscriber from HSS
uri:
url: "{{ item.key }}/subscriber/{{ item.value.subscriber_id }}"
method: DELETE
loop: "{{ hss_subscriber_data | dict2items }}"
ignore_errors: True
when:
- deprovision_subscriber | bool == true
- name: Patch Subscriber to Dormant State
uri:
url: "{{ item.key }}/subscriber/{{ item.value.subscriber_id }}"
method: PATCH
body:
{
"enabled": true,
"msisdn": "9999{{ imsi[-10:] }}", # 虚拟号码
"ue_ambr_dl": 9999999, # 不可用的高值
"ue_ambr_ul": 9999999
}
loop: "{{ hss_subscriber_data | dict2items }}"
when:
- deprovision_subscriber | default(false) | bool == false
# 最终断言决定成功或失败
- name: Set status to "Success" if Manual deprovision / Fail if failed provision
assert:
that:
- action == "deprovision"
为什么这个模式是最佳实践
1. 无代码重复
相同的清理任务处理两种情况:
- 失败的供应(回滚):如果
block中的任何任务失败,rescue部分会自动执行 - 有意取消供应:当以
action: "deprovision"调用时,剧本会立即跳转到rescue
2. 完整清理保证
当供应在中途失败时,清理部分确保:
- 所有创建的 OCS 账户被删除
- 所有配置的网络设备条目被移除
- 分配的库存被返回到池中
- HSS 订阅者被删除或设置为休眠
- 没有部分供应留在任何系统中
这防止了“孤立”资源,这些资源:
- 消耗库存而未被跟踪
- 创建未��接到服务的计费账户
- 在故障排除期间造成困惑
- 浪费网络资源
3. 使用 ignore_errors 进行优雅的失败处理
注意每个清理任务都使用 ignore_errors: True。这是故意的,因为:
- 在回滚期间,某些资源可能尚未创建
- 我们希望尝试所有清理任务,即使某些任务失败
- 最终的断言决定整体成功/失败
例如,如果在“创建 OCS 账户”时供应失败,清理将尝试:
- 删除 OCS 账户(将失败,但被忽略)
- 移除属性配置文件(将失败,但被忽略)
- 返回库存(成功)
- 删除 HSS 订阅者(可能不存在,被忽略)
4. 区分取消供应和回滚
rescue 末尾的最终断言很巧妙:
- name: Set status to "Success" if Manual deprovision / Fail if failed provision
assert:
that:
- action == "deprovision"
这意味着:
- 如果
action == "deprovision":断言通过,剧本成功(状态 0) - 如果
action未设置或 != "deprovision":断言失败,剧本失败(状态 2)
因此,相同的清理代码根据意图导致不同的供应作业状态。
5. 基于服务类型的条件清理
某些清理任务使用条件来处理不同的场景:
- name: Deprovision Subscriber from HSS
uri: ...
when:
- deprovision_subscriber | bool == true
- name: Patch Subscriber to Dormant State
uri: ...
when:
- deprovision_subscriber | default(false) | bool == false
这允许灵活的清理:
- 完全删除:当 SIM 专用于客户时(
deprovision_subscriber: true) - 休眠状态:当 SIM 可重复使用并应保留在 HSS 中时(
deprovision_subscriber: false)
如何使用此模式
用于供应:
{
"product_id": 1,
"customer_id": 456,
"provisioning_play": "play_psim_only"
}
如果供应失败,将通过 rescue 自动回滚。
用于取消供应:
{
"service_id": 123,
"service_uuid": "Service_abc123",
"action": "deprovision",
"provisioning_play": "play_psim_only"
}
剧本直接跳过到 rescue 部分,运行所有清理,并成功。
优势总结
✅ 单一真实来源:一个剧本处理供应和取消供应 ✅ 原子操作:要么完全供应,要么完全清理 ✅ 没有孤立资源:失败的供应不留痕迹 ✅ 更容易维护:对供应逻辑的更改自动适用于清理 ✅ 减少错误:没有供应和取消供应代码不同步的机会 ✅ 可测试:可以通过运行 action: "deprovision" 测试取消供应逻辑
此模式应在所有供应剧本中遵循,以确保可靠性和一致性。
重写产品变量
provisioning_json_vars 可以在供应时被重写。例如,产品可能定义:
{
"provisioning_json_vars": "{\"monthly_cost\": 50, \"data_limit_gb\": 100}"
}
但在供应时,您可以重写这些:
{
"product_id": 1,
"customer_id": 456,
"monthly_cost": 45,
"data_limit_gb": 150
}
合并的 extra_vars 将使用重写的值。这允许:
- 为特定客户提供定制定价
- 根据促销提供不同的数据限制
- 在不修改产品的情况下测试不同的参数
没有库存的产品
并非所有产品都需要库存。例如,数据附加或功能切换可能具有:
{
"product_id": 10,
"product_name": "Extra 10GB Data",
"provisioning_play": "play_local_data_addon",
"provisioning_json_vars": "{\"data_gb\": 10}",
"inventory_items_list": "[]"
}
在这种情况下,剧本接收:
extra_vars = {
"product_id": 10,
"customer_id": 456,
"service_id": 123, # 要添加数据的服务
"data_gb": 10,
"access_token": "eyJ..."
}
剧本只需将数据添加到现有服务中,而无需��何库存项目。
标准供应工作流程
标准供应在以下情况下启动:
- 员工通过 UI 向客户添加服务
- 客户通过自助服务门户订购服务
- API 直接调用
PUT /crm/provision/
当您点击“供应”时
当用户点击“供应”按钮时,会发生完整的流程:
1. UI 显示产品选择
用户从产品目录中选择一个产品。产品包含:
provisioning_play- 要运行的 Ansible 剧本inventory_items_list- 所需库存(例如,['SIM Card', 'Mobile Number'])provisioning_json_vars- 默认变量
2. 库存选择器(如果需要)
如果 inventory_items_list 不为空,将出现一个模态窗口,显示每种库存类型的下拉菜单。用户必须在继续之前选择可用的库存项目。
3. 点击供应按钮
JavaScript 发送 PUT /crm/provision/ 请求:
PUT /crm/provision/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"product_id": 42,
"customer_id": 123,
"SIM Card": 5001,
"Mobile Number": 5002
}
4. API 接收请求
供应端点(routes/provisioning.py):
- 验证身份验证(Bearer 令牌、API 密钥或 IP 白名单)
- 检查用户是否具有
CREATE_PROVISION权限 - 从令牌中提取
initiating_user - 从数据库加载产品定义
- 检索剧本路径:
OmniCRM-API/Provisioners/plays/play_psim_only.yaml
5. 变量合并
系统将来自多个来源的变量组合:
# 来自产品
product_vars = json.loads(product['provisioning_json_vars'])
# 来自请求体
request_vars = request.json
# 系统添加
system_vars = {
'product_id': 42,
'customer_id': 123,
'access_token': g.access_token, # 见下面的身份验证部分
'initiating_user': 7
}
# 最终合并
extra_vars = {**product_vars, **request_vars, **system_vars}
6. 创建供应记录
数据库记录创建,状态为 1(运行中):
provision = {
'provision_id': 456,
'customer_id': 123,
'product_id': 42,
'provisioning_play': 'play_psim_only',
'provisioning_json_vars': json.dumps(extra_vars),
'provisioning_status': 1, # 运行中
'task_count': 85,
'initiating_user': 7,
'created': '2025-01-10T14:30:00Z'
}
7. 生成后台线程
run_playbook_in_background(
playbook='plays/play_psim_only.yaml',
extra_vars=extra_vars,
provision_id=456,
refresh_token=refresh_token # 用于执行期间的令牌刷新
)
8. API 立即返回
响应返回到 UI,包含 provision_id:
{
"provision_id": 456,
"provisioning_status": 1,
"message": "Provisioning job created"
}
9. UI 轮询更新
UI 开始每 3 秒轮询一次 GET /crm/provision/provision_id/456 以检查状态。响应包括:
{
"provision_id": 456,
"provisioning_status": 1,
"task_count": 12,
"provisioning_result_json": [
{
"event_number": 1,
"event_name": "Get Product information from CRM API",
"provisioning_status": 0,
"timestamp": "2024-01-15T10:30:05"
},
{
"event_number": 2,
"event_name": "Assign SIM Card from inventory",
"provisioning_status": 1,
"timestamp": "2024-01-15T10:30:07"
}
]
}
10. Ansible 在后台执行
剧本按顺序运行任务:
- 每个任务完成都会在数据库中创建
Provision_Event记录 - 事件包括:任务名称、状态(0=成功,2=失败,3=失败但被忽略)、结果 JSON
- UI 显示实时进度,显示已完成的任务和当前正在运行的任务
- 失败的任务在事件详细信息中显示错误消息
在 UI 中跟踪:
在供应运行时(状态 1),用户可以查看:
- 服务详细信息页面 - 显示供应状态徽章(运行中/成功/失败)
- 活动日志 - 列出所有供应事件及其时间戳
- 供应详细视图 - 显示逐任务进度,带有展开/折叠详细信息
示例显示:
供应状态:运行中(12 个任务中的 8 个已完成)
✓ 从 CRM API 获取产品信息 ✓ 获取客户详细信息 ✓ 从库存中分配 SIM 卡(ICCID: 8991101200003204510) ✓ 分配手机号码(555-0123) ⟳ 在 OCS/CGRateS 中创建账户(进行中...) ⏺ 配置网络策略 ⏺ 创建服务记录 ...
11. 供应完成
最终状态设置:
provisioning_status: 0- 成功provisioning_status: 2- 失败(严重错误)
UI 停止轮询并显示结果:
- 成功:绿色勾号,服务标记为活动,用户可以查看服务详细信息
- 失败:红色 X,显示错误消息,选项为重试或联系支持
- 电子邮件通知:如果失败,电子邮件发送到配置中的
provisioning.failure_list
身份验证和授权
用户跟踪
每个供应作业跟踪哪个用户发起了它:
- 用户发起:
initiating_user字段设置为来自其 JWT 令牌的用户 ID - API 密钥身份验证:使用第一个管理员用户 ID
- IP 白名单身份验证:使用第一个管理员用户 ID
权限检查
系统在允许供应之前检查权限:
- 员工需要
CREATE_PROVISION权限 - 客户只能为自己的账户供应服务(
VIEW_OWN_PROVISION权限)
Ansible 如何通过 CRM API 进行身份验证
Ansible 剧本需要进行身份验证的 API 调用回 CRM(以获取产品详细信息、创建服务、更新库存等)。身份验证通过传递给剧本的 Bearer 令牌 处理。
access_token 的来源取决于用于调用供应 API 的身份验证方法:
方法 1:用户登录(Bearer 令牌)
当用户通过 Web UI 登录时:
- 用户进行身份验证:
POST /crm/auth/login - 收到 JWT
access_token(短期有效,15-30 分钟)和refresh_token(长期有效) - 使用 Bearer 令牌在头中进行供应请求
- 供应 API 从
Authorization: Bearer ...头中提取令牌 - 存储在
g.access_token中(Flask 请求上下文) - 作为
access_token变量传递给 Ansible
代码(permissions.py):
# 从头中提取 Bearer 令牌
auth_header = request.headers.get('Authorization', '')
if auth_header.startswith('Bearer '):
bearer_token = auth_header[7:]
# 验证和解码
decoded_token = jwt.decode(bearer_token, secret_key, algorithms=['HS256'])
# 存储用于供应
g.access_token = bearer_token
代码(provisioning.py):
if "access_token" in g:
json_data['access_token'] = g.access_token
run_playbook(playbook_path, extra_vars=json_data, provision_id=provision_id)
方法 2:API 密钥(X-API-KEY 头)
对于使用 API 密钥的自动化系统:
- 系统发出请求:
PUT /crm/provision/,带有X-API-KEY: your-api-key...头 - 供应 API 根据
crm_config.yaml验证 API 密钥 - 为第一个管理员用户动态生成新的 JWT 令牌
- 存储在
g.access_token中 - 传递给 Ansible
为什么生成令牌?
API 密钥是字符串,而不是 JWT。剧本调用 API 端点时需要 JWT 身份验证。因此:
- 验证 API 密钥
- 如果有效且具有
admin角色,则生成临时 JWT - 使用第一个管理员用户的 ID 作为 JWT 主题
- 令牌允许剧本进行身份验证的 API 调用
代码(permissions.py):
def handle_api_key_auth(f, api_key, *args, **kwargs):
if not secure_compare_api_key(api_key):
return {'message': 'Invalid API key'}, 401
API_KEYS = yaml_config.get('api_keys', {})
if api_key in API_KEYS:
if 'admin' in API_KEYS[api_key].get('roles', []):
admin_user_id = retrieve_first_admin_user_id()
access_token = create_access_token(identity=str(admin_user_id))
g.access_token = access_token
方法 3:IP 白名单
对于在私有网络上受信任的内部系统:
- 系统从白名单 IP 发出请求(例如,192.168.1.100)
- 供应 API 检查客户端 IP 是否在
crm_config.yaml中的ip_whitelist中 - 如果在白名单中,为第一个管理员用户生成新的 JWT 令牌
- 存储在
g.access_token中 - 传递给 Ansible
代码(permissions.py):
def handle_ip_auth(f, *args, **kwargs):
client_ip = get_real_client_ip()
if not is_ip_whitelisted(client_ip):
return {'message': 'Access denied'}, 403
admin_user_id = retrieve_first_admin_user_id()
access_token = create_access_token(identity=str(admin_user_id))
g.access_token = access_token
在剧本中使用令牌
剧本中的每个 API 调用都包括令牌:
- name: Get Product Details
uri:
url: "http://localhost:5000/crm/product/product_id/{{ product_id }}"
headers:
Authorization: "Bearer {{ access_token }}"
- name: Create Service Record
uri:
url: "http://localhost:5000/crm/service/"
method: PUT
headers:
Authorization: "Bearer {{ access_token }}"
body:
customer_id: "{{ customer_id }}"
service_name: "Mobile Service"
令牌过期和刷新
长时间运行的剧本(5-10 分钟)可能会超出 access_token(15-30 分钟过期)。对于用户发起的供应,系统传递 access_token 和 refresh_token:
refresh_token = request.cookies.get('refresh_token')
run_playbook(playbook_path, extra_vars, provision_id, refresh_token=refresh_token)
如果 access_token 过期,剧本运行程序可以:
- 检测 401 未授权响应
- 使用
refresh_token调用POST /crm/auth/refresh - 接收新的
access_token - 重试失败的请求
对于 API 密钥/IP 白名单身份验证,生成的令牌可以有更长的过期时间(1-2 小时),因为这些是受信任的自动化系统。
供应过程
-
作业创建
当收到供应请求时,系统:
- 验证请求并检查权限
- 加载产品定义中指定的 Ansible 剧本
- 在数据库中创建状态为 1(运行中)的
Provision记录 - 从产品定义和请求体中提取变量
- 捕获用于 API 访问的身份验证令牌
-
令牌处理
Ansible 剧本需要通过 CRM API 进行身份验证,以检索数据并进行更改。供应系统以两种方式处理此问题:
- Bearer 令牌(JWT):对于用户发起的供应,来自请求的
refresh_token用于在剧本执行期间生成新访问令牌 - API 密钥/IP 身份验证:对于自动化系统,
access_token直接通过g.access_token传递给剧本
- Bearer 令牌(JWT):对于用户发起的供应,来自请求的
-
后台执行
剧本在后台线程中运行,使用
playbook_runner_v2。这允许 API 立即返回,同时供应异步继续。在执行期间:
- 每个任务的完成/失败都会创建
Provision_Event记录 - 事件处理程序监控严重与被忽略的失败
- 实时状态更新写入数据库
- UI 可以通过
GET /crm/provision/provision_id/<id>轮询更新
- 每个任务的完成/失败都会创建
-
剧本执行
Ansible 剧本通常执行以下操作:
- 从 API 检索产品信息
- 从 API 检索客户信息
- 分配库存项目(SIM 卡、IP 地址、电话号码等)
- 在 OCS/OCS 中创建账户
- 配置网络设备
- 在 CRM API 中创建服务记录
- 添加设置成本交易
- 向客户发送欢迎电子邮件/SMS
-
错误处理
Ansible 剧本使用
block和rescue部分进行回滚:- 如果关键任务失败,清理部分会移除部分供应
- 带有
ignore_errors: true的任务标记为状态 3,并且不会使作业失败 - 致命错误(YAML 语法、连接���败)会创建一个特殊的错误事件,包含调试信息
示例:标准供应剧本
以下是来自 play_simple_service.yaml 的示例:
- name: Simple Provisioning Play
hosts: localhost
gather_facts: no
become: False
tasks:
- name: Main block
block:
- name: Get Product information from CRM API
uri:
url: "http://localhost:5000/crm/product/product_id/{{ product_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
validate_certs: no
register: api_response_product
- name: Set package facts
set_fact:
package_name: "{{ api_response_product.json.product_name }}"
setup_cost: "{{ api_response_product.json.retail_setup_cost }}"
monthly_cost: "{{ api_response_product.json.retail_cost }}"
- name: Generate Service UUID
set_fact:
service_uuid: "Service_{{ 99999999 | random | to_uuid }}"
- name: Create account in OCS
uri:
url: "http://{{ crm_config.ocs.OCS }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV2.SetAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ActionPlanIds": [],
"ExtraOptions": { "AllowNegative": false, "Disabled": false }
}]
}
status_code: 200
register: response
- name: Add Service via API
uri:
url: "http://localhost:5000/crm/service/"
method: PUT
body_format: json
headers:
Authorization: "Bearer {{ access_token }}"
body:
{
"customer_id": "{{ customer_id }}",
"product_id": "{{ product_id }}",
"service_name": "Service: {{ service_uuid }}",
"service_uuid": "{{ service_uuid }}",
"service_status": "Active",
"retail_cost": "{{ monthly_cost | float }}"
}
status_code: 200
register: service_creation_response
- name: Add Setup Cost Transaction
uri:
url: "http://localhost:5000/crm/transaction/"
method: PUT
headers:
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": {{ customer_id | int }},
"service_id": {{ service_creation_response.json.service_id | int }},
"title": "{{ package_name }} - Setup Costs",
"retail_cost": "{{ setup_cost | float }}"
}
register: api_response_transaction
rescue:
- name: Remove account in OCS
uri:
url: "http://{{ crm_config.ocs.OCS }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV2.RemoveAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}"
}]
}
status_code: 200
- name: Fail the provision
assert:
that:
- false
此剧本演示了典型流程:
- 从 CRM API 检索产品详细信息
- 生成唯一的服务 UUID
- 在 OCS 中创建计费账户
- 通过 CRM API 创建服务记录
- 添加设置成本交易
- 如果任何步骤失败,
rescue部分将移除 OCS 账户
简单供应工作流程
简单供应旨在用于需要触发供应而无需用户交互的自动化系统。最常见的用例是 OCS 通过 ActionPlans 触发附加供应。
简单供应端点
OmniCRM 提供两个简单供应端点:
-
POST /crm/provision/simple_provision_addon/service_id/<id>/product_id/<id>用于自动化附加供应(例如,定期收费、自动充值)
-
POST /crm/provision/simple_provision_addon_recharge/service_id/<id>/product_id/<id>用于需要立即反馈的快速充值操作
简单供应的身份验证
简单供应端点使用 IP 白名单 或 API 密钥 进行身份验证:
- 请求的源 IP 会检查是否在
crm_config.yaml中的ip_whitelist中 - 或者可以提供来自
crm_config.yaml中api_keys的 API 密钥 - 生成访问令牌并通过
g.access_token传递给剧本
示例:OCS ActionPlan 回调
OCS 可以配置在执行定期操作时调用简单供应端点:
{
"method": "ApierV1.SetActionPlan",
"params": [{
"Id": "ActionPlan_Service123_Monthly_Charge",
"ActionsId": "Action_Service123_Add_Monthly_Data",
"Timing": {
"Years": [],
"Months": [],
"MonthDays": [1],
"Time": "00:00:00Z"
},
"Weight": 10,
"ActionTriggers": [
{
"ThresholdType": "*min_event_counter",
"ThresholdValue": 1,
"ActionsID": "Action_Service123_HTTP_Callback"
}
]
}]
}
该操作会进行 HTTP POST:
这会触发相关的剧本(例如,play_topup_no_charge.yaml),该剧本会向服务添加数据/积分。
示例:简单充值剧本
来自 play_topup_monetary.yaml:
- name: Mobile Topup Monetary - 2024
hosts: localhost
gather_facts: no
become: False
tasks:
- name: Get Product information from CRM API
uri:
url: "http://localhost:5000/crm/product/product_id/{{ product_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: api_response_product
- name: Get Service information from CRM API
uri:
url: "http://localhost:5000/crm/service/service_id/{{ service_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: api_response_service
- name: Set service facts
set_fact:
service_uuid: "{{ api_response_service.json.service_uuid }}"
customer_id: "{{ api_response_service.json.customer_id }}"
package_name: "{{ api_response_product.json.product_name }}"
monthly_cost: "{{ api_response_product.json.retail_cost }}"
- name: Get Customer Payment Method
uri:
url: "http://localhost:5000/crm/stripe/customer_id/{{ customer_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: api_response_stripe
- name: Charge customer
uri:
url: "http://localhost:5000/crm/stripe/charge_card/{{ customer_stripe_id }}"
method: POST
headers:
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"retail_cost": "{{ monthly_cost }}",
"description": "{{ package_name }} topup",
"customer_id": "{{ customer_id | int }}",
"service_id": "{{ service_id | int }}"
}
register: api_response_stripe
- name: Add monetary balance to OCS
uri:
url: "http://{{ crm_config.ocs.OCS }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"BalanceType": "*monetary",
"Balance": {
"Value": "{{ monthly_cost | float * 100 }}",
"ExpiryTime": "+4320h"
}
}]
}
status_code: 200
- name: Add Transaction to CRM
uri:
url: "http://localhost:5000/crm/transaction/"
method: PUT
headers:
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": {{ customer_id | int }},
"service_id": {{ service_id | int }},
"title": "{{ package_name }}",
"retail_cost": "{{ monthly_cost | float }}"
}
- name: Send Notification SMS
uri:
url: "http://sms-gateway/SMS/plaintext/{{ api_key }}"
method: POST
body_format: json
body:
{
"source_msisdn": "YourCompany",
"destinatination_msisdn": "{{ customer_phone }}",
"message_body": "Thanks for topping up {{ monthly_cost }}!"
}
status_code: 201
ignore_errors: True
此剧本:
- 从 API 获取服务和产品详细信息
- 检索客户的支付方式
- 通过 Stripe API 向客户收费
- 向 OCS 添加货币余额
- 在 CRM 中记录交易
- 发送确认 SMS(使用
ignore_errors: True,以便失败不会导致作业失败)
供应链
对于需要多个供应步骤的复杂产品,OmniCRM 支持 供应链。链依次执行多个剧本,并在它们之间传递上下文。
示例用例:一个捆绑服务,供应:
- 基础互联网服务(创建主要服务记录)
- IPTV 附加(使用步骤 1 的 service_id)
- 静态 IP 附加(使用步骤 1 的 service_id)
供应服务会自动:
- 查询数据库以获取第一个剧本创建的
service_id - 将其注入到后续剧本的
extra_vars中 - 将每个剧本跟踪为单独的
Provision记录
失败原因和调试
当供应失败时,系统捕获详细信息以帮助诊断问题。
常见失败场景
关键任务失败(状态 2)
这些会导致整个供应作业失败:
- API 调用返回意外状态代码
- 断言失败(例如,
assert: that: response.status == 200) - 缺少必需的库存项目
- 网络设备无法访问
- 无效的凭据或过期的令牌
- OCS/OCS 错误
被忽略的失败(状态 3)
这些被记录但不会导致作业失败:
- 可选的 SMS/电子邮件通知失败
- 非关键数据查找(标记为
ignore_errors: True) - 在取消供应期间的清理操作
致命错误
这些会阻止剧本运行:
- 剧本中的 YAML 语法错误
- 剧本中的未定义变量
- 缺少剧本文件
- 与 Ansible 控制器的连接失败
当发生致命错误时,系统会创建一个特殊的错误事件,包含:
- Ansible 退出代码
- 完整的 stdout(包含语法错误详细信息)
- 完整的 stderr(包含运行时错误)
- 该类型失败的常见原因列表
- 传递给剧本的所有变量
错误通知电子邮件
当供应失败(状态 2)时,电子邮件会自动发送到配置的失败通知列表(crm_config.yaml 中的 provisioning.failure_list)。
电子邮件包括:
- 客户信息
- 产品/服务详细信息
- 颜色编码的任务结果:
- 绿色:成功的任务
- 橙色:失败但被忽略的任务
- 红色:严重失败
- 对于严重失败:完整的调试输出,包括请求/响应主体
- 对于致命错误:Ansible 输出、错误消息和常见原因
监控供应作业
供应状态 API
要检查供应作业的状态:
GET /crm/provision/provision_id/<id>
Authorization: Bearer <token>
响应包括:
{
"provision_id": 123,
"customer_id": 456,
"customer_name": "John Smith",
"product_id": 10,
"provisioning_status": 0,
"provisioning_play": "play_psim_only",
"playbook_description": "OmniCore Service Provisioning 2024",
"task_count": 85,
"provisioning_result_json": [
{
"event_number": 1,
"event_name": "Get Product information from CRM API",
"provisioning_status": 1,
"provisioning_result_json": "{...}"
},
{
"event_number": 2,
"event_name": "Create account in OCS",
"provisioning_status": 1,
"provisioning_result_json": "{...}"
}
]
}
列出供应作业
要获取所有供应作业的分页列表:
GET /crm/provision/?page=1&per_page=20&sort=provision_id&order=desc
Authorization: Bearer <token>
支持过滤:
GET /crm/provision/?filters={"provisioning_status":[2]}&search=Mobile
Authorization: Bearer <token>
这仅返回失败的作业(状态 2),其中描述包含“Mobile”。
最佳实践
剧本设计
- 始终使用 block/rescue:确保可以回滚部分供应
- 明智地使用 ignore_errors:仅用于真正可选的操作
- 记录重要变量:使用
debug任务记录关键值以便于故障排除 - 验证响应:使用
assert检查 API 响应是否符合预期 - 幂等性:设计剧本以安全地重新运行
身份验证
- 用户发起的供应:对于长时间运行的剧本,始终使用 refresh_token
- 自��化供应:使用 IP 白名单或 API 密钥与生成的访问令牌
- 令牌过期:refresh_token 确保访问令牌根据需要重新生成
错误处理
- 提供上下文:在错误消息中包含 customer_id、service_id 和操作详细信息
- 适当通知:严重失败触发电子邮件,但对于预期的失败不发送垃圾邮件
- 调试信息:在 Provision_Event 记录中捕获完整的请求/响应主体
安全
- 验证输入:在供应之前检查 customer_id、product_id、service_id
- 权限检查:验证用户只能为授权客户供应
- 敏感数据:使用删除系统从日志中删除密码/密钥
- IP 白名单:仅将 simple_provision 端点限制为受信任的系统
性能
- 后台执行:永远不要阻塞 API 响应以等待供应
- 轮询间隔:UI 应每 2-5 秒轮询一次状态更新
- 并行任务:使用 Ansible 的原生并行性进行独立操作
- 数据库更新:事件处理程序实时更新数据库,无需在执行期间查询
相关文档
concepts_ansible- 一般 Ansible 供应概念concepts_api- CRM API 身份验证和使用concepts_products_and_services- 产品和服务定义administration_inventory- 供应的库存管理