跳到主要内容

Ansible Playbooks: 详细指南

OmniCRM 产品通过 Ansible 进行配置,允许根据每个产品及其相关库存的特定要求进行自动化服务管理。

另请参见:SIM Card Provisioning <concepts_sim_provisioning> 以获取基于 Ansible 的移动服务配置的完整示例,包括物理 SIM 卡和 eSIM。

Playbooks 和产品如何协同工作

关键概念: Playbooks 实际上是在 OmniCRM 中创建服务的工具。当您将 playbook 分配给产品时,您正在定义 发生什么 当该产品被配置 - 但这对不同产品可能意味着不同的事情。

产品触发 Playbooks

当产品在 OmniCRM 中被配置时:

  1. 产品定义指定要运行的 playbook(通过 provisioning_play 字段)
  2. 产品将变量传递给 playbook(通过 provisioning_json_vars 和库存选择)
  3. playbook 执行并执行其编程的操作
  4. playbook 确定创建什么(如果有的话)

Playbooks 可以做什么

单个配置 playbook 可以:

创建多个服务
一个捆绑产品的 playbook 可能创建:

  • 一个主要的互联网服务记录
  • 一个 IPTV 附加服务记录
  • 一个 VoIP 服务记录
  • 所有这些都通过一个产品配置操作完成

不创建服务
有些 playbooks 根本不创建服务记录:

  • 一个仅配置 CPE 设备的 playbook
  • 一个将配置发送到网络设备的 playbook
  • 一个更新外部系统的 playbook

创建一个服务
最常见的模式:

  • 为客户创建一个服务记录
  • 将库存链接到该服务
  • 为该服务设置计费

修改现有服务
充值和附加 playbooks:

  • 不创建新服务
  • 更新现有服务记录(添加数据、延长到期等)
  • 向现有计费账户添加余额

执行没有服务记录的操作
一些 playbooks 纯粹是操作性的:

  • 重置账户余额
  • 在客户之间交换库存项目
  • 生成报告或配置

示例:不同的 Playbook 行为

# 产品 1:移动 SIM 服务(创建 1 个服务)
# provisioning_play: play_simple_service
- 在 CRM 中创建服务记录
- 在 OCS 中创建计费账户
- 分配 SIM 卡和电话号码库存
- 发送欢迎电子邮件

# 产品 2:互联网捆绑包(创建 3 个服务)
# provisioning_play: play_bundle_internet_tv_voice
- 创建互联网服务记录
- 创建 IPTV 服务记录
- 创建 VoIP 服务记录
- 将所有服务链接到同一客户
- 为捆绑包创建单一计费账户

# 产品 3:数据充值(创建 0 个服务)
# provisioning_play: play_topup_no_charge
- 根据 service_id 查找现有服务
- 向现有 OCS 账户添加数据余额
- 更新服务到期日期
- 不创建新服务

# 产品 4:CPE 配置(创建 0 个服务)
# provisioning_play: play_prov_cpe_mikrotik
- 生成路由器配置
- 用配置更新库存记录
- 将配置通过电子邮件发送给支持团队
- 不创建服务(仅设备设置)

关键点:playbook 定义行为,产品只是触发器。

Plays 与 Tasks

理解 Plays 和 Tasks 之间的区别对于使用 OmniCRM playbooks 至关重要。

Play(Playbook)
一个完整的配置工作流,协调多个任务以实现业务目标。 Plays 是存储在 OmniCRM-API/Provisioners/plays/ 中的顶级 playbooks,并在产品定义中引用。

示例:

  • play_simple_service.yaml - 配置基本服务
  • play_topup_no_charge.yaml - 对服务应用免费充值
  • play_prov_cpe_mikrotik.yaml - 配置客户场所设备

Task(可重用组件)
一组自包含的、可重用的操作,可以被多个 plays 包含。 Tasks 以 task_ 为前缀,并位于同一目录中。

示例:

  • task_welcome_email.yaml - 向客户发送欢迎电子邮件
  • task_activate_olt.yaml - 激活 OLT 设备
  • task_notify_ocs.yaml - 向计费系统发送通知

它们之间的关系:

# play_simple_service.yaml (A Play)
- name: Simple Provisioning Play
hosts: localhost
tasks:
- name: Main provisioning block
block:
- name: Create service
uri: ...

- name: Configure billing
uri: ...

# Include reusable task
- include_tasks: task_welcome_email.yaml

# Include post-provisioning tasks
- include_tasks: post_provisioning_tasks.yaml

Playbook 结构和组成

所有 OmniCRM playbooks 遵循一致的结构。理解这一结构对于创建和维护 playbooks 至关重要。

基本结构

每个 playbook 都以这些标准头开始:

- name: Descriptive Name of the Playbook
hosts: localhost # 始终为 localhost
gather_facts: no # 为了性能禁用
become: False # 不提升权限

tasks:
- name: Main block
block:
# 配置任务在这里

rescue:
# 回滚/清理任务在这里

头部解释

name
在配置日志和 UI 中显示的描述性名称。这在配置记录中显示为 playbook_description

hosts: localhost
所有 OmniCRM playbooks 都在 localhost 上运行,因为它们通过 API 与远程系统交互,而不是通过 SSH。

gather_facts: no
Ansible 的事实收集被禁用,因为:

  • 我们不需要系统信息
  • 它增加了不必要的开销
  • 如果在调试输出中显示,可能会导致浏览器崩溃

become: False
由于我们正在进行 API 调用,而不是修改系统文件,因此不需要提升权限。

配置加载

每个 playbook 必须加载中央配置文件:

tasks:
- name: Include vars of crm_config
ansible.builtin.include_vars:
file: "../../crm_config.yaml"
name: crm_config

这使得配置可用,如 crm_config.ocs.cgratescrm_config.crm.base_url 等。

crm_config.yaml 通常包含:

ocs:
cgrates: "10.0.1.100:2080"
ocsTenant: "default_tenant"
crm:
base_url: "https://crm.example.com"

变量访问模式

变量可以来自多个来源:

来自产品定义:

- name: Access product_id passed by OmniCRM
debug:
msg: "Provisioning product {{ product_id }}"

来自库存选择:

- name: Get inventory ID for SIM Card
set_fact:
sim_card_id: "{{ hostvars[inventory_hostname]['SIM Card'] | int }}"
when: "'SIM Card' in hostvars[inventory_hostname]"

来自 API 响应:

- 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: Use the product name
debug:
msg: "Product name is {{ api_response_product.json.product_name }}"

常见 Playbook 模式

服务配置模式

这是创建新服务的最常见模式。

- name: Service Provisioning Playbook
hosts: localhost
gather_facts: no
become: False

tasks:
- name: Main block
block:

# 1. 加载配置
- name: Include vars of crm_config
ansible.builtin.include_vars:
file: "../../crm_config.yaml"
name: crm_config

# 2. 获取产品信息
- 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

# 3. 获取客户信息
- name: Get Customer information from CRM API
uri:
url: "http://localhost:5000/crm/customer/customer_id/{{ customer_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: api_response_customer

# 4. 从检索的数据中设置事实
- name: Set package facts
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 }}"

# 5. 生成唯一标识符
- name: Generate UUID
set_fact:
uuid: "{{ 99999999 | random | to_uuid }}"

- name: Generate Service UUID
set_fact:
service_uuid: "Service_{{ uuid[0:8] }}"

# 6. 在计费系统中创建账户
- name: Create account in OCS/CGRateS
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
headers:
Content-Type: "application/json"
body:
{
"method": "ApierV2.SetAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ActionPlanIds": [],
"ActionPlansOverwrite": true,
"ExtraOptions": {
"AllowNegative": false,
"Disabled": false
},
"ReloadScheduler": true
}]
}
status_code: 200
register: ocs_response

- name: Verify OCS account creation
assert:
that:
- ocs_response.status == 200
- ocs_response.json.result == "OK"

# 7. 添加初始余额
- name: Add 0 Monetary Balance
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"BalanceType": "*monetary",
"Categories": "*any",
"Balance": {
"ID": "Initial Balance",
"Value": 0,
"ExpiryTime": "+4320h",
"Weight": 1,
"Blocker": true
}
}]
}
status_code: 200
register: balance_response

# 8. 在 CRM 中创建服务记录
- name: Get current date and time in ISO 8601 format
command: date --utc +%Y-%m-%dT%H:%M:%S%z
register: current_date_time

- name: Add Service via API
uri:
url: "http://localhost:5000/crm/service/"
method: PUT
body_format: json
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body:
{
"customer_id": "{{ customer_id }}",
"product_id": "{{ product_id }}",
"service_name": "{{ package_name }} - {{ service_uuid }}",
"service_type": "generic",
"service_uuid": "{{ service_uuid }}",
"service_billed": true,
"service_taxable": true,
"service_provisioned_date": "{{ current_date_time.stdout }}",
"service_status": "Active",
"wholesale_cost": "{{ api_response_product.json.wholesale_cost | float }}",
"retail_cost": "{{ monthly_cost | float }}"
}
status_code: 200
register: service_creation_response

# 9. 添加设置费用交易
- name: Add Setup Cost Transaction via API
uri:
url: "http://localhost:5000/crm/transaction/"
method: PUT
headers:
Content-Type: "application/json"
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",
"description": "Setup costs for {{ package_comment }}",
"invoice_id": null,
"retail_cost": "{{ setup_cost | float }}"
}
return_content: yes
register: transaction_response

# 10. 包含后配置任务
- include_tasks: post_provisioning_tasks.yaml

rescue:

# 回滚/清理部分
- name: Print all vars for debugging
debug:
var: hostvars[inventory_hostname]

- name: Remove account in OCS
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV2.RemoveAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ReloadScheduler": true
}]
}
status_code: 200
ignore_errors: True
when: service_uuid is defined

- name: Delete Service from CRM if it was created
uri:
url: "http://localhost:5000/crm/service/service_id/{{ service_creation_response.json.service_id }}"
method: DELETE
headers:
Authorization: "Bearer {{ access_token }}"
status_code: 200
ignore_errors: True
when: service_creation_response is defined

- name: Fail if not intentional deprovision
assert:
that:
- action == "deprovision"

Topup/Recharge 模式

用于向现有服务添加信用、数据或时间。

- name: Service Topup Playbook
hosts: localhost
gather_facts: no
become: False

tasks:
- name: Include vars of crm_config
ansible.builtin.include_vars:
file: "../../crm_config.yaml"
name: crm_config

# 1. 获取服务信息
- 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

# 2. 获取产品信息(要充值的内容)
- 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

# 3. 提取服务详细信息
- 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 }}"
topup_value: "{{ api_response_product.json.retail_cost }}"

# 4. 在计费系统中执行操作(免费充值)
- name: Execute Action to add credits
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "APIerSv1.ExecuteAction",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ActionsId": "Action_Topup_Standard"
}]
}
status_code: 200
register: action_response

- name: Verify action executed successfully
assert:
that:
- action_response.status == 200
- action_response.json.result == "OK"

# 5. 重置任何触发的限制
- name: Reset ActionTriggers
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "APIerSv1.ResetAccountActionTriggers",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"Executed": false
}]
}
status_code: 200

# 6. 更新服务日期
- name: Calculate new expiry date
command: "date --utc +%Y-%m-%dT%H:%M:%S%z -d '+30 days'"
register: new_expiry_date

- name: Update Service with new expiry
uri:
url: "http://localhost:5000/crm/service/{{ service_id }}"
method: PATCH
headers:
Authorization: "Bearer {{ access_token }}"
Content-Type: "application/json"
body_format: json
body:
{
"service_deactivate_date": "{{ new_expiry_date.stdout }}",
"service_status": "Active"
}

# 7. 可选:发送通知
- name: Send Notification SMS
uri:
url: "http://sms-gateway/api/send"
method: POST
body_format: json
body:
{
"source": "CompanyName",
"destination": "{{ customer_phone }}",
"message": "Your service has been topped up. New expiry: {{ new_expiry_date.stdout }}"
}
status_code: 201
ignore_errors: True

CPE 配置模式

用于配置客户场所设备(路由器、调制解调器、ONT)。

- name: CPE Provisioning Playbook
hosts: localhost
gather_facts: no
become: False

tasks:
- name: Include vars of crm_config
ansible.builtin.include_vars:
file: "../../crm_config.yaml"
name: crm_config

# 1. 获取 CPE 的库存项目
- name: Set CPE inventory ID from hostvars
set_fact:
cpe_inventory_id: "{{ hostvars[inventory_hostname]['WiFi Router CPE'] | int }}"
when: "'WiFi Router CPE' in hostvars[inventory_hostname]"

# 2. 从库存中获取 CPE 详细信息
- name: Get Inventory data for CPE
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ cpe_inventory_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: api_response_cpe

# 3. 获取客户站点信息
- name: Get Site info from API
uri:
url: "{{ crm_config.crm.base_url }}/crm/site/customer_id/{{ customer_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: api_response_site

# 4. 用位置更新 CPE 库存
- name: Patch CPE inventory item with location
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ cpe_inventory_id }}"
method: PATCH
body_format: json
headers:
Authorization: "Bearer {{ access_token }}"
body:
{
"address_line_1": "{{ api_response_site.json.0.address_line_1 }}",
"city": "{{ api_response_site.json.0.city }}",
"state": "{{ api_response_site.json.0.state }}",
"latitude": "{{ api_response_site.json.0.latitude }}",
"longitude": "{{ api_response_site.json.0.longitude }}"
}
status_code: 200

# 5. 生成凭据
- name: Set CPE hostname
set_fact:
cpe_hostname: "CPE_{{ cpe_inventory_id }}"
cpe_username: "admin_{{ cpe_inventory_id }}"

- name: Generate random password
set_fact:
cpe_password: "{{ lookup('pipe', 'cat /dev/urandom | tr -dc a-zA-Z0-9 | head -c 16') }}"

# 6. 生成 WiFi 凭据
- name: Set WiFi SSID
set_fact:
wifi_ssid: "Network_{{ cpe_inventory_id }}"

- name: Generate WiFi password
set_fact:
word_list:
- apple
- cloud
- river
- mountain
- ocean

- name: Create WiFi PSK
set_fact:
random_word: "{{ word_list | random }}"
random_number: "{{ 99999 | random(start=10000) }}"

- name: Combine WiFi PSK
set_fact:
wifi_psk: "{{ random_word }}{{ random_number }}"

# 7. 生成配置文件
- name: Set config filename
set_fact:
config_name: "{{ cpe_hostname }}_{{ lookup('pipe', 'date +%Y%m%d%H%M%S') }}.cfg"
config_dest: "/tmp/{{ cpe_hostname }}_{{ lookup('pipe', 'date +%Y%m%d%H%M%S') }}.cfg"

- name: Create config from template
template:
src: "templates/cpe_router_config.j2"
dest: "{{ config_dest }}"

# 8. 读取生成的配置
- name: Read config file
ansible.builtin.slurp:
src: "{{ config_dest }}"
register: config_content

# 9. 用配置更新库存
- name: Patch CPE inventory with config
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ cpe_inventory_id }}"
method: PATCH
body_format: json
headers:
Authorization: "Bearer {{ access_token }}"
body:
{
"itemtext3": "{{ wifi_ssid }}",
"itemtext4": "{{ wifi_psk }}",
"management_url": "{{ cpe_hostname }}",
"management_username": "{{ cpe_username }}",
"management_password": "{{ cpe_password }}",
"config_content": "{{ config_content.content | b64decode }}",
"inventory_notes": "Provisioned: {{ lookup('pipe', 'date +%Y-%m-%d') }}"
}
status_code: 200

# 10. 将配置发送给支持团队
- name: Email configuration to support
uri:
url: "https://api.mailjet.com/v3.1/send"
method: POST
body_format: json
headers:
Content-Type: "application/json"
body:
{
"Messages": [{
"From": {
"Email": "provisioning@example.com",
"Name": "Provisioning System"
},
"To": [{
"Email": "support@example.com",
"Name": "Support Team"
}],
"Subject": "CPE Config - {{ cpe_hostname }}",
"Attachments": [{
"ContentType": "text/plain",
"Filename": "{{ config_name }}",
"Base64Content": "{{ config_content.content }}"
}]
}]
}
user: "{{ mailjet_api_key }}"
password: "{{ mailjet_api_secret }}"
force_basic_auth: true
status_code: 200

自动续费模式

使用 CGRateS ActionPlans 配置自动定期收费或续费。

# 部分充值 playbook 设置自动续费

# 1. 规范化 auto_renew 参数
- name: Normalize auto_renew to boolean
set_fact:
auto_renew_bool: "{{ (auto_renew | string | lower) in ['true', '1', 'yes'] }}"

# 2. 创建自动续费的操作
- name: Create Action for AutoRenew
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.SetActions",
"params": [{
"ActionsId": "Action_AutoTopup_{{ service_uuid }}_{{ product_id }}",
"Overwrite": true,
"Actions": [
{
"Identifier": "*http_post",
"ExtraParameters": "{{ crm_config.crm.base_url }}/crm/provision/simple_provision_addon/service_id/{{ service_id }}/product_id/{{ product_id }}"
},
{
"Identifier": "*cdrlog",
"BalanceType": "*generic",
"ExtraParameters": "{\"Category\":\"^activation\",\"Destination\":\"Auto Renewal\"}"
}
]
}]
}
status_code: 200
register: action_response
when: auto_renew_bool

# 3. 创建每月 ActionPlan
- name: Create ActionPlan for Monthly Renewal
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.SetActionPlan",
"params": [{
"Id": "ActionPlan_Monthly_{{ service_uuid }}_{{ product_id }}",
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"ActionPlan": [{
"ActionsId": "Action_AutoTopup_{{ service_uuid }}_{{ product_id }}",
"Years": "*any",
"Months": "*any",
"MonthDays": "*any",
"WeekDays": "*any",
"Time": "*monthly",
"StartTime": "*now",
"Weight": 10
}],
"Overwrite": true,
"ReloadScheduler": true
}]
}
status_code: 200
when: auto_renew_bool

# 4. 将 ActionPlan 分配给账户
- name: Assign ActionPlan to account
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV2.SetAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ActionPlanIds": ["ActionPlan_Monthly_{{ service_uuid }}_{{ product_id }}"],
"ActionPlansOverwrite": true,
"ReloadScheduler": true
}]
}
status_code: 200
when: auto_renew_bool

# 5. 如果禁用自动续费,则移除 ActionPlan
- name: Remove ActionPlan from account
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.RemoveActionPlan",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Id": "ActionPlan_Monthly_{{ service_uuid }}_{{ product_id }}"
}]
}
status_code: 200
ignore_errors: true
when: not auto_renew_bool

可重用任务

可重用任务是小型、自包含的 playbooks,可以被多个 plays 包含。它们促进代码重用和一致性。

欢迎电子邮件任务

task_welcome_email.yaml - 向新客户发送欢迎电子邮件。

# 此任务期望这些变量由父 play 设置:
# - api_response_customer (客户详细信息)
# - package_name (产品名称)
# - monthly_cost (定期费用)
# - setup_cost (一次性费用)

- name: Set email configuration
set_fact:
mailjet_api_key: "{{ lookup('env', 'MAILJET_API_KEY') }}"
mailjet_api_secret: "{{ lookup('env', 'MAILJET_SECRET') }}"
email_from: "noreply@example.com"
recipients: []

- name: Set email subject and sender name
set_fact:
email_subject: "Welcome to our service!"
email_from_name: "Customer Service Team"

- name: Prepare list of recipients from customer contacts
loop: "{{ api_response_customer.json.contacts }}"
set_fact:
recipients: "{{ recipients + [{'Email': item.contact_email, 'Name': item.contact_firstname ~ ' ' ~ item.contact_lastname}] }}"

- name: Get first contact name
set_fact:
first_contact: "{{ api_response_customer.json.contacts[0].contact_firstname }}"

- name: Send welcome email
uri:
url: "https://api.mailjet.com/v3.1/send"
method: POST
body_format: json
headers:
Content-Type: "application/json"
body:
{
"Messages": [{
"From": {
"Email": "{{ email_from }}",
"Name": "{{ email_from_name }}"
},
"To": "{{ recipients }}",
"Subject": "{{ email_subject }}",
"TextPart": "Dear {{ first_contact }}, welcome! Your service is ready.",
"HTMLPart": "Dear {{ first_contact }},<br/><h3>Welcome!</h3><br/>Your {{ package_name }} service is now active.<br/>Monthly cost: ${{ monthly_cost }}<br/>Setup fee: ${{ setup_cost }}<br/>If you have any questions, contact support@example.com"
}]
}
user: "{{ mailjet_api_key }}"
password: "{{ mailjet_api_secret }}"
force_basic_auth: true
status_code: 200
register: email_response

后配置任务

post_provisioning_tasks.yaml - 在每次配置后运行的标准清理和通知。

# 此文件在大多数配置 playbooks 的末尾包含
# 它处理常见的后配置操作

- include_tasks: task_notify_ocs.yaml

task_notify_ocs.yaml 可能包含:

- name: Notify OCS of provisioning completion
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "APIerSv1.ReloadCache",
"params": [{
"ArgsCache": "*all"
}]
}
status_code: 200
ignore_errors: true

常见操作

与库存合作

检索库存详细信息:

- name: Get SIM Card inventory ID
set_fact:
sim_inventory_id: "{{ hostvars[inventory_hostname]['SIM Card'] | int }}"
when: "'SIM Card' in hostvars[inventory_hostname]"

- name: Get SIM Card details
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ sim_inventory_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: sim_response

- name: Extract SIM details
set_fact:
iccid: "{{ sim_response.json.iccid }}"
imsi: "{{ sim_response.json.imsi }}"
ki: "{{ sim_response.json.ki }}"

将库存分配给客户:

- name: Assign SIM to customer
uri:
url: "{{ crm_config.crm.base_url }}/crm/inventory/inventory_id/{{ sim_inventory_id }}"
method: PATCH
headers:
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": {{ customer_id }},
"service_id": {{ service_id }},
"item_state": "Assigned"
}
status_code: 200

日期和时间操作

获取当前日期/时间:

- name: Get current date and time in ISO 8601 format
command: date --utc +%Y-%m-%dT%H:%M:%S%z
register: current_date_time

- name: Get today's date only
set_fact:
today: "{{ lookup('pipe', 'date +%Y-%m-%d') }}"

计算未来日期:

- name: Calculate expiry date 30 days from now
command: "date --utc +%Y-%m-%dT%H:%M:%S%z -d '+30 days'"
register: expiry_date

- name: Calculate date 90 days in future
command: "date --utc +%Y-%m-%d -d '+{{ days }} days'"
register: future_date
vars:
days: 90

生成随机值

UUID 和标识符:

- name: Generate UUID
set_fact:
uuid: "{{ 99999999 | random | to_uuid }}"

- name: Generate service identifier
set_fact:
service_uuid: "SVC_{{ uuid[0:8] }}"

随机密码:

- name: Generate secure password
set_fact:
password: "{{ lookup('pipe', 'cat /dev/urandom | tr -dc a-zA-Z0-9 | head -c 16') }}"

可记忆的密码短语:

- name: Set word list
set_fact:
words:
- alpha
- bravo
- charlie
- delta
- echo

- name: Generate passphrase
set_fact:
word: "{{ words | random }}"
number: "{{ 99999 | random(start=10000) }}"

- name: Combine into passphrase
set_fact:
passphrase: "{{ word }}{{ number }}"

与 CGRateS/OCS 合作

创建账户:

- name: Create billing account
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV2.SetAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ActionPlanIds": [],
"ActionPlansOverwrite": true,
"ExtraOptions": {
"AllowNegative": false,
"Disabled": false
},
"ReloadScheduler": true
}]
}
status_code: 200
register: account_response

添加余额:

- name: Add data balance
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"BalanceType": "*data",
"Categories": "*any",
"Balance": {
"ID": "Data Package",
"Value": 10737418240,
"ExpiryTime": "+720h",
"Weight": 10
}
}]
}
status_code: 200

执行操作:

- name: Execute charging action
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "APIerSv1.ExecuteAction",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ActionsId": "Action_Standard_Charge"
}]
}
status_code: 200

获取账户信息:

- name: Get account details
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV2.GetAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}"
}]
}
status_code: 200
register: account_info

与属性配置文件合作:

- name: Get AttributeProfile
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "APIerSv1.GetAttributeProfile",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"ID": "ATTR_{{ service_uuid }}"
}]
}
return_content: yes
status_code: 200
register: attr_response
ignore_errors: true

- name: Extract attribute value
set_fact:
phone_number: "{{ attr_response.json.result.Attributes | json_query(\"[?Path=='*req.PhoneNumber'].Value[0].Rules\") | first }}"
when: attr_response is defined

条件逻辑

检查变量是否存在:

- name: Use custom value or default
set_fact:
monthly_cost: "{{ custom_cost | default(50.00) }}"

- name: Only run if variable is defined
debug:
msg: "Service UUID is {{ service_uuid }}"
when: service_uuid is defined

布尔条件:

- name: Provision equipment
include_tasks: configure_cpe.yaml
when: provision_cpe | default(false) | bool

- name: Skip if deprovision
assert:
that:
- action != "deprovision"
when: action is defined

多个条件:

- name: Complex conditional task
uri:
url: "{{ endpoint }}"
method: POST
when:
- service_uuid is defined
- customer_id is defined
- action != "deprovision"
- enable_feature | default(true) | bool

循环和迭代

简单循环:

- name: Create multiple balances
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.AddBalance",
"params": [{
"Account": "{{ service_uuid }}",
"BalanceType": "{{ item.type }}",
"Balance": {
"Value": "{{ item.value }}"
}
}]
}
loop:
- { type: "*voice", value: 3600 }
- { type: "*data", value: 10737418240 }
- { type: "*sms", value: 100 }

循环 API 响应:

- name: Get all customer sites
uri:
url: "{{ crm_config.crm.base_url }}/crm/site/customer_id/{{ customer_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
register: sites_response

- name: Configure equipment at each site
debug:
msg: "Configuring site at {{ item.address_line_1 }}"
loop: "{{ sites_response.json }}"

错误处理

使用 ignore_errors:

- name: Optional SMS notification
uri:
url: "http://sms-gateway/send"
method: POST
body: {...}
ignore_errors: true

用于验证的断言:

- name: Verify API response
assert:
that:
- response.status == 200
- response.json.result == "OK"
fail_msg: "API call failed: {{ response.json }}"

条件错误处理:

- name: Try to get existing service
uri:
url: "{{ crm_config.crm.base_url }}/crm/service/service_uuid/{{ service_uuid }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
register: service_lookup
failed_when: false

- name: Create service if it doesn't exist
uri:
url: "{{ crm_config.crm.base_url }}/crm/service/"
method: PUT
body: {...}
when: service_lookup.status == 404

最佳实践

变量命名

使用描述性、一致的名称:

# 好
service_uuid: "SVC_12345"
customer_name: "John Smith"
monthly_cost: 49.99

# 坏
svc: "SVC_12345"
name: "John Smith"
cost: 49.99

按来源前缀变量:

api_response_customer: {...}
api_response_product: {...}
cgr_account_info: {...}

调试

打印变量以进行故障排除:

- name: Print all variables
debug:
var: hostvars[inventory_hostname]

- name: Print specific variable
debug:
msg: "Service UUID: {{ service_uuid }}"

- name: Print API response
debug:
var: api_response_product.json

验证

始终验证关键 API 响应:

- name: Create account
uri:
url: "{{ billing_endpoint }}"
method: POST
body: {...}
register: response

- name: Verify account creation
assert:
that:
- response.status == 200
- response.json.result == "OK"
fail_msg: "Failed to create account: {{ response.json }}"

幂等性

设计任务以安全地重新运行:

# 首先检查资源是否存在
- name: Check if account exists
uri:
url: "{{ ocs_endpoint }}/get_account"
method: POST
body: {"Account": "{{ service_uuid }}"}
register: account_check
failed_when: false

# 仅在不存在时创建
- name: Create account
uri:
url: "{{ ocs_endpoint }}/create_account"
method: POST
body: {...}
when: account_check.status == 404

安全性

绝不要硬编码凭据:

# 坏
mailjet_api_key: "abc123def456"

# 好 - 使用环境变量
mailjet_api_key: "{{ lookup('env', 'MAILJET_API_KEY') }}"

# 好 - 使用配置文件
mailjet_api_key: "{{ crm_config.email.api_key }}"

始终使用 HTTPS 和身份验证:

- name: Call external API
uri:
url: "https://api.example.com/endpoint"
method: POST
headers:
Authorization: "Bearer {{ access_token }}"
validate_certs: yes

文档

记录复杂逻辑:

# 计算部分月份的按比例收费
# 如果客户在 15 号注册,而计费在 1 号,
# 则按剩余天数收取 50% 的月费
- name: Calculate days until end of month
command: "date -d 'last day of this month' +%d"
register: days_in_month

- name: Get current day
command: "date +%d"
register: current_day

- name: Calculate pro-rata amount
set_fact:
days_remaining: "{{ (days_in_month.stdout | int) - (current_day.stdout | int) }}"
pro_rata_cost: "{{ (monthly_cost | float) * (days_remaining | float) / (days_in_month.stdout | float) }}"

测试 Playbooks

测试方法

  1. 首先进行干运行:在非生产系统上测试
  2. 验证变量:使用调试任务确认所有必需变量均已存在
  3. 检查响应:在继续之前验证 API 响应
  4. 回滚测试:故意使任务失败以验证回收块工作
  5. 去配置测试:使用 action: "deprovision" 测试以验证清理

示例测试 playbook:

- name: Test Service Provisioning
hosts: localhost
gather_facts: no

tasks:
- name: Verify required variables
assert:
that:
- product_id is defined
- customer_id is defined
- access_token is defined
fail_msg: "Missing required variables"

- name: Test API connectivity
uri:
url: "http://localhost:5000/crm/health"
method: GET
register: health_check

- name: Verify health check
assert:
that:
- health_check.status == 200

常见陷阱

缺少类型转换:

# 错误 - 可能是字符串
customer_id: "{{ customer_id }}"

# 正确 - 确保为整数
customer_id: {{ customer_id | int }}

未处理未定义的变量:

# 错误 - 如果未定义则失败
service_uuid: "{{ service_uuid }}"

# 正确 - 提供默认值
service_uuid: "{{ service_uuid | default('') }}"

忘记验证:

# 错误 - 不检查响应
- name: Create account
uri: ...
register: response

# 正确 - 验证响应
- name: Create account
uri: ...
register: response

- name: Verify creation
assert:
that:
- response.json.result == "OK"

配置工作流

通常,Omnitouch 员工将与客户合作:

  1. 定义产品要求
  2. 开发必要的 Ansible playbooks 以自动化配置过程
  3. 在暂存环境中测试 playbooks
  4. 部署到生产

这确保每项服务都以一致和可靠的方式部署,减少错误的风险,并确保所有必要步骤以正确的顺序完成。

Ansible 变量

传递给 Ansible playbooks 的变量包括:

产品变量
来自 OmniCRM 产品配置,定义服务应如何设置。

库存变量
从库存中选择,包括调制解调器、SIM 卡、IP 地址块或配置所需的电话号码等项目。

系统变量
由 OmniCRM 自动添加:

  • product_id - 正在配置的产品
  • customer_id - 接收服务的客户
  • service_id - 正在修改的服务(用于充值/更改)
  • access_token - 用于 API 身份验证的 JWT

去配置

当服务不再需要时,Ansible Playbooks 也用于使用 rescue 块模式去配置服务。这:

  • 移除任何配置
  • 将库存释放回池中
  • 删除计费账户
  • 确保系统保持干净

去配置方法

去配置主要有两种触发方式:

1. 自动回滚(配置失败)
当主配置块中的任何任务失败时,回收部分自动执行以清理部分更改。

2. 手动去配置
在运行 playbook 时设置 action: "deprovision",故意触发回收块以移除服务。

去配置模式

所有 OmniCRM playbooks 遵循此结构以安全清理:

- name: Service Provisioning Playbook
hosts: localhost
gather_facts: no
become: False

tasks:
- name: Main block
block:
# 所有配置任务在这里
- name: Create OCS account
uri: ...

- name: Create service record
uri: ...
register: service_creation_response

- name: Add transaction
uri: ...

rescue:
# 去配置/回滚任务在这里
- name: Print all vars for debugging
debug:
var: hostvars[inventory_hostname]

# 清理任务按相反顺序执行
- name: Remove account in OCS
uri: ...

- name: Delete Service from CRM
uri: ...

- name: Fail if not intentional deprovision
assert:
that:
- action == "deprovision"

完整去配置示例

以下是 play_simple_service.yaml 的完整示例:

rescue:
# 1. 调试信息
- name: Print all vars for Deprovision/Rollback
debug:
var: hostvars[inventory_hostname]

- name: Get today's date
set_fact:
today: "{{ lookup('pipe', 'date +%Y-%m-%d') }}"

# 2. 如果可用,获取服务信息
- name: Try to get Service information from CRM API to Deprovision
uri:
url: "http://localhost:5000/crm/service/service_id/{{ service_creation_response.json.service_id }}"
method: GET
headers:
Authorization: "Bearer {{ access_token }}"
return_content: yes
register: api_response_service
ignore_errors: True
when: service_creation_response is defined and service_creation_response.json is defined and service_creation_response.json.service_id is defined

- name: Print api_response_service
debug:
var: api_response_service
when: api_response_service is defined

# 3. 为清理设置 service_uuid
- name: Set service_uuid facts for Deprovision
set_fact:
service_uuid: "{{ api_response_service.json.service_uuid }}"
when:
- service_uuid is not defined
- api_response_service is defined
- api_response_service.json is defined

# 4. 移除 OCS 账户
- name: Remove account in OCS
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ access_token }}"
body:
{
"method": "ApierV2.RemoveAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ReloadScheduler": true
}]
}
status_code: 200
register: response
ignore_errors: True
when: service_uuid is defined

- name: Print response from OCS account removal
debug:
var: response
when: response is defined

# 5. 从 CRM 删除服务记录
- name: Delete Service from CRM if it was created
uri:
url: "http://localhost:5000/crm/service/service_id/{{ service_creation_response.json.service_id }}"
method: DELETE
headers:
Authorization: "Bearer {{ access_token }}"
status_code: 200
register: delete_service_response
ignore_errors: True
when: service_creation_response is defined and service_creation_response.json is defined and service_creation_response.json.service_id is defined

- name: Print delete service response
debug:
var: delete_service_response
when: delete_service_response is defined

# 6. 确定这是故意的还是失败
- name: Set status to "Success" if Manual deprovision / Fail if failed provision
assert:
that:
- action == "deprovision"
fail_msg: "Provisioning failed and was rolled back"
success_msg: "Service deprovisioned successfully"

关键去配置概念

ignore_errors: True
大多数清理任务使用 ignore_errors: True,因为资源可能不存在(例如,如果配置在创建它们之前失败)。

条件执行
使用 when 子句仅清理已创建的资源:

when: service_creation_response is defined

反向顺序
清理按创建的反向顺序进行:

  1. 首先删除依赖资源(交易、库存分配)
  2. 删除服务记录
  3. 删除 OCS/计费账户
  4. 释放任何被占用的资源

最终断言
最终断言区分:

  • 故意去配置 (action == "deprovision") → 成功
  • 配置失败(未设置操作) → 失败

去配置不同资源类型

OCS/CGRateS 账户:

- name: Remove account in OCS
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV2.RemoveAccount",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Account": "{{ service_uuid }}",
"ReloadScheduler": true
}]
}
status_code: 200
ignore_errors: True
when: service_uuid is defined

CRM 服务记录:

- name: Delete Service from CRM
uri:
url: "http://localhost:5000/crm/service/service_id/{{ service_id }}"
method: DELETE
headers:
Authorization: "Bearer {{ access_token }}"
status_code: 200
ignore_errors: True
when: service_id is defined

库存项目(返回池):

- name: Release SIM card back to inventory pool
uri:
url: "http://localhost:5000/crm/inventory/inventory_id/{{ sim_inventory_id }}"
method: PATCH
headers:
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"customer_id": null,
"service_id": null,
"item_state": "Available"
}
status_code: 200
ignore_errors: True
when: sim_inventory_id is defined

ActionPlans(定期收费):

- name: Remove ActionPlan from account
uri:
url: "http://{{ crm_config.ocs.cgrates }}/jsonrpc"
method: POST
body_format: json
body:
{
"method": "ApierV1.RemoveActionPlan",
"params": [{
"Tenant": "{{ crm_config.ocs.ocsTenant }}",
"Id": "ActionPlan_Monthly_{{ service_uuid }}_{{ product_id }}"
}]
}
status_code: 200
ignore_errors: True

付款授权:

- name: Release payment authorization
uri:
url: "http://localhost:5000/crm/payments/release/{{ authorization_id }}"
method: POST
headers:
Authorization: "Bearer {{ access_token }}"
body_format: json
body:
{
"metadata": {
"release_reason": "provisioning_failed"
}
}
return_content: yes
ignore_errors: True
when: authorization_id is defined

手动去配置工作流

要手动去配置服务:

  1. 确定要去配置的服务
  2. 运行原始配置 playbook,设置 action: "deprovision"
  3. Playbook 立即进入回收块
  4. 所有清理任务执行
  5. 服务被干净地移除

示例 API 调用:

POST /crm/provision/run_playbook
{
"product_id": 123,
"customer_id": 456,
"service_id": 789,
"action": "deprovision"
}

部分清理场景

场景 1:OCS 账户已创建,服务创建失败

  • OCS 账户存在
  • 服务记录不存在
  • 回收块移除 OCS 账户
  • 没有服务可删除(安全跳过)

场景 2:服务已创建,交易失败

  • OCS 账户存在
  • 服务记录存在
  • 交易不存在
  • 回收块移除 OCS 账户和服务
  • 没有交易可作废(从未创建)

场景 3:完整配置,付款捕获失败

  • OCS 账户存在
  • 服务记录存在
  • 付款已授权但未捕获
  • 回收块:
    • 释放付款授权(客户未收费)
    • 移除服务记录
    • 移除 OCS 账户

去配置最佳实践

1. 广泛使用 ignore_errors
清理应宽容 - 如果资源不存在,则不失败。

2. 使用 when 子句检查资源是否存在
仅在资源已创建时尝试清理:

when: service_creation_response is defined

3. 打印调试信息
始终包括调试任务以帮助故障排除失败的配置:

- name: Print all vars for debugging
debug:
var: hostvars[inventory_hostname]

4. 以反向依赖顺序清理
在父项之前删除子项:

  • 交易在服务之前
  • 服务在 OCS 账户之前
  • 库存分配在库存释放之前

5. 处理付款授权
始终在回收块中释放付款���权,以避免对失败的配置向客户收费。

6. 清理后重新加载调度程序
在移除 OCS 资源时,包括 ReloadScheduler: true 以确保 CGRateS 立即更新。

去配置与删除

去配置(通过 playbook 回收):

  • 从所有系统中移除服务
  • 释放库存
  • 取消定期收费
  • 保留审计记录
  • 推荐的方法

直接删除(通过 API):

  • 仅删除 CRM 记录
  • 不清理 OCS 账户
  • 不释放库存
  • 可能留下孤立的资源
  • 不推荐用于生产

回滚和错误处理

Ansible 的 block/rescue 特性在配置和去配置过程中用于优雅地处理错误。如果在配置的任何点任务失败,回收部分将自动回滚更改,以返回到一致状态。这确保了可靠性并减少了部分或失败部署的风险。

错误处理示例

- name: Main block
block:
- name: Step 1: Create account
uri: ...
register: response

- name: Verify step 1
assert:
that:
- response.status == 200

- name: Step 2: Create service
uri: ...

rescue:
# 如果任何断言失败或任何任务出错:
# 1. 执行跳转到回收块
# 2. 清理任务执行
# 3. Playbook 失败并显示错误信息
- name: Cleanup partial changes
uri: ...

有关配置系统、工作流和身份验证的完整详细信息,请参见 concepts_provisioning