跳到主要内容

供应系统

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

最近的供应事件

::: note ::: title 注意 :::

有关产品到服务旅程的完整演练,包括详细的 Ansible 剧本示例、定价策略和实际场景,请参见 完整产品生命周期指南。 :::

概述

当产品被订购或服务需要配置时,OmniCRM 创建一个 供应作业,执行一个或多个 Ansible 剧本。这些剧本与各种后端系统(OCS/CGRateS、网络设备、API 等)进行交互,以完全供应服务。

供应系统支持两种主要工作流程:

  1. 标准供应 - 通过 UI/API 由员工或客户触发
  2. 简单供应 - 由 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_idproduct_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
}

从产品到供应作业

当供应被启动时,系统:

  1. 加载 provisioning_play 中指定的剧本

    系统查找 OmniCRM-API/Provisioners/plays/play_psim_only.yaml

  2. 将多个来源的变量合并到 extra_vars

    a. 来自 provisioning_json_vars: {"iccid": "", "msisdn": ""} b. 来自请求体: 用户/API 提供的任何附加变量 c. 来自产品字段: product_idcustomer_id 等 d. 来自身份验证: access_token 或用于 refresh_token 的设置

  3. 根据 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..."
    }
  4. 通过 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

这演示了完整的流程:

  1. 产品定义指定 provisioning_play: "play_psim_only"
  2. 产品需要 inventory_items_list: ['SIM Card', 'Mobile Number']
  3. 用户在供应过程中选择库存项目
  4. 库存 ID 作为 extra_vars 传递给剧本
  5. 剧本从 API 获取完整的库存详细信息
  6. 剧本使用库存数据配置网络设备
  7. 剧本将库存标记为分配给客户

回滚和清理:最佳实践模式

关键最佳实践:同一剧本应处理失败的供应回滚和使用 Ansible 的有意取消供应,使用 blockrescue 结构。

剧本结构

来自 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 登录时:

  1. 用户进行身份验证:POST /crm/auth/login
  2. 收到 JWT access_token(短期有效,15-30 分钟)和 refresh_token(长期有效)
  3. 使用 Bearer 令牌在头中进行供应请求
  4. 供应 API 从 Authorization: Bearer ... 头中提取令牌
  5. 存储在 g.access_token 中(Flask 请求上下文)
  6. 作为 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 密钥的自动化系统:

  1. 系统发出请求:PUT /crm/provision/,带有 X-API-KEY: your-api-key...
  2. 供应 API 根据 crm_config.yaml 验证 API 密钥
  3. 为第一个管理员用户动态生成新的 JWT 令牌
  4. 存储在 g.access_token
  5. 传递给 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 白名单

对于在私有网络上受信任的内部系统:

  1. 系统从白名单 IP 发出请求(例如,192.168.1.100)
  2. 供应 API 检查客户端 IP 是否在 crm_config.yaml 中的 ip_whitelist
  3. 如果在白名单中,为第一个管理员用户生成新的 JWT 令牌
  4. 存储在 g.access_token
  5. 传递给 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_tokenrefresh_token

refresh_token = request.cookies.get('refresh_token')
run_playbook(playbook_path, extra_vars, provision_id, refresh_token=refresh_token)

如果 access_token 过期,剧本运行程序可以:

  1. 检测 401 未授权响应
  2. 使用 refresh_token 调用 POST /crm/auth/refresh
  3. 接收新的 access_token
  4. 重试失败的请求

对于 API 密钥/IP 白名单身份验证,生成的令牌可以有更长的过期时间(1-2 小时),因为这些是受信任的自动化系统。

供应过程

  1. 作业创建

    当收到供应请求时,系统:

    • 验证请求并检查权限
    • 加载产品定义中指定的 Ansible 剧本
    • 在数据库中创建状态为 1(运行中)的 Provision 记录
    • 从产品定义和请求体中提取变量
    • 捕获用于 API 访问的身份验证令牌
  2. 令牌处理

    Ansible 剧本需要通过 CRM API 进行身份验证,以检索数据并进行更改。供应系统以两种方式处理此问题:

    • Bearer 令牌(JWT):对于用户发起的供应,来自请求的 refresh_token 用于在剧本执行期间生成新访问令牌
    • API 密钥/IP 身份验证:对于自动化系统,access_token 直接通过 g.access_token 传递给剧本
  3. 后台执行

    剧本在后台线程中运行,使用 playbook_runner_v2。这允许 API 立即返回,同时供应异步继续。

    在执行期间:

    • 每个任务的完成/失败都会创建 Provision_Event 记录
    • 事件处理程序监控严重与被忽略的失败
    • 实时状态更新写入数据库
    • UI 可以通过 GET /crm/provision/provision_id/<id> 轮询更新
  4. 剧本执行

    Ansible 剧本通常执行以下操作:

    • 从 API 检索产品信息
    • 从 API 检索客户信息
    • 分配库存项目(SIM 卡、IP 地址、电话号码等)
    • 在 OCS/OCS 中创建账户
    • 配置网络设备
    • 在 CRM API 中创建服务记录
    • 添加设置成本交易
    • 向客户发送欢迎电子邮件/SMS
  5. 错误处理

    Ansible 剧本使用 blockrescue 部分进行回滚:

    • 如果关键任务失败,清理部分会移除部分供应
    • 带有 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

此剧本演示了典型流程:

  1. 从 CRM API 检索产品详细信息
  2. 生成唯一的服务 UUID
  3. 在 OCS 中创建计费账户
  4. 通过 CRM API 创建服务记录
  5. 添加设置成本交易
  6. 如果任何步骤失败,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.yamlapi_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

此剧本:

  1. 从 API 获取服务和产品详细信息
  2. 检索客户的支付方式
  3. 通过 Stripe API 向客户收费
  4. 向 OCS 添加货币余额
  5. 在 CRM 中记录交易
  6. 发送确认 SMS(使用 ignore_errors: True,以便失败不会导致作业失败)

供应链

对于需要多个供应步骤的复杂产品,OmniCRM 支持 供应链。链依次执行多个剧本,并在它们之间传递上下文。

示例用例:一个捆绑服务,供应:

  1. 基础互联网服务(创建主要服务记录)
  2. IPTV 附加(使用步骤 1 的 service_id)
  3. 静态 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 - 供应的库存管理