Aller au contenu principal

Système de Provisionnement

OmniCRM utilise Ansible pour automatiser le provisionnement, la configuration et le déprovisionnement des services clients. Le système de provisionnement est conçu pour être flexible, permettant des flux de travail complexes tout en maintenant la cohérence et la fiabilité.

Événements de Provisionnement Récents

::: note ::: title Remarque :::

Pour un guide complet du parcours produit-service avec des exemples détaillés de playbooks Ansible, des stratégies de tarification et des scénarios du monde réel, voir Guide Complet du Cycle de Vie du Produit. :::

Aperçu

Lorsqu'un produit est commandé ou qu'un service doit être configuré, OmniCRM crée un Travail de Provisionnement qui exécute un ou plusieurs playbooks Ansible. Ces playbooks interagissent avec divers systèmes backend (OCS/CGRateS, équipements réseau, APIs, etc.) pour provisionner complètement le service.

Le système de provisionnement prend en charge deux flux de travail principaux :

  1. Provisionnement Standard - Déclenché par le personnel ou les clients via l'UI/API
  2. Provisionnement Simple - Déclenché par des systèmes externes comme OCS pour des opérations automatisées

Valeurs de Statut de Provisionnement

Les travaux de provisionnement et les tâches individuelles peuvent avoir les statuts suivants :

  • Statut 0 (Succès) - Le travail de provisionnement s'est terminé avec succès
  • Statut 1 (En cours) - Le travail de provisionnement ou la tâche est actuellement en cours d'exécution
  • Statut 2 (Échoué - Critique) - Un échec critique s'est produit, ce qui a entraîné l'échec du provisionnement
  • Statut 3 (Échoué - Ignoré) - Une tâche a échoué mais avait ignore_errors: true, donc le provisionnement a continué

Lorsqu'un travail de provisionnement échoue, OmniCRM envoie des notifications par e-mail à la liste de notification d'échec configurée avec des informations d'erreur détaillées.

Comment les Produits Pilotent le Provisionnement

La définition du Produit est le plan pour ce qui est provisionné et comment. Lorsqu'un utilisateur sélectionne un produit à provisionner, le système lit plusieurs champs clés de la définition du produit pour déterminer quoi faire.

Champs de Produit Utilisés dans le Provisionnement

Une définition de produit contient :

  • provisioning_play - Le nom du playbook Ansible à exécuter (sans l'extension .yaml)
  • provisioning_json_vars - Chaîne JSON contenant des variables par défaut à passer à Ansible
  • inventory_items_list - Liste des types d'inventaire qui doivent être assignés (par exemple, ['SIM Card', 'Mobile Number'])
  • product_id, product_name, champs de tarification - Passés automatiquement au playbook

Exemple de Définition de Produit

{
"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
}

Du Produit au Travail de Provisionnement

Lorsque le provisionnement est initié, le système :

  1. Charge le playbook spécifié dans provisioning_play

    Le système recherche OmniCRM-API/Provisioners/plays/play_psim_only.yaml

  2. Fusionne les variables de plusieurs sources dans extra_vars :

    a. Depuis provisioning_json_vars : {"iccid": "", "msisdn": ""} b. Depuis le corps de la requête : Toute variable supplémentaire fournie par l'utilisateur/API c. Depuis les champs de produit : product_id, customer_id, etc. d. Depuis l'authentification : access_token ou configuration pour refresh_token

  3. Assigne l'inventaire basé sur inventory_items_list

    Avant d'exécuter le playbook, l'UI/API demande la sélection de l'inventaire :

    • SIM Card - L'utilisateur sélectionne une SIM disponible dans l'inventaire
    • Mobile Number - L'utilisateur sélectionne un numéro de téléphone disponible

    Les IDs d'inventaire sélectionnés sont ajoutés à extra_vars avec le type d'inventaire comme clé :

    extra_vars = {
    "product_id": 1,
    "customer_id": 456,
    "SIM Card": 789, # inventory_id de la SIM sélectionnée
    "Mobile Number": 101, # inventory_id du numéro de téléphone sélectionné
    "iccid": "", # Depuis provisioning_json_vars
    "msisdn": "", # Depuis provisioning_json_vars
    "access_token": "eyJ..."
    }
  4. Passe tout à Ansible via hostvars[inventory_hostname]

    À l'intérieur du playbook, les variables sont accessibles comme :

    - 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]"

Comment les Playbooks Utilisent les Variables d'Inventaire

Une fois que le playbook a les IDs d'inventaire, il récupère les détails complets de l'inventaire depuis l'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 }}"

Le playbook peut ensuite utiliser ces valeurs pour :

  • Provisionner la carte SIM sur le HSS avec l'IMSI
  • Configurer le numéro de téléphone dans le système de facturation
  • Assigner les éléments d'inventaire au client
  • Créer l'enregistrement de service avec ces détails

Exemple du Monde Réel : Provisionnement de SIM Mobile

Depuis play_psim_only.yaml, voici comment il utilise les données de produit et d'inventaire :

- 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

Cela démontre le flux complet :

  1. La définition du produit spécifie provisioning_play: "play_psim_only"
  2. Le produit nécessite inventory_items_list: ['SIM Card', 'Mobile Number']
  3. L'utilisateur sélectionne des éléments d'inventaire lors du provisionnement
  4. Les IDs d'inventaire sont passés au playbook comme extra_vars
  5. Le playbook récupère les détails complets de l'inventaire depuis l'API
  6. Le playbook utilise les données d'inventaire pour configurer l'équipement réseau
  7. Le playbook marque l'inventaire comme assigné au client

Rollback et Nettoyage : Modèle de Meilleure Pratique

Meilleure Pratique Critique : Le même playbook doit gérer à la fois le rollback de provisionnement échoué et le déprovisionnement intentionnel en utilisant la structure block et rescue d'Ansible.

Structure du Playbook

Depuis play_psim_only.yaml :

- name: OmniCore Service Provisioning 2024
hosts: localhost
gather_facts: no
become: False

tasks:
- name: Main block
block:
# --- TÂCHES DE PROVISIONNEMENT ---
- name: Get Product information
uri: ...

- name: Create account in OCS
uri: ...

- name: Provision subscriber on HSS
uri: ...

- name: Create service record
uri: ...

# ... beaucoup d'autres tâches de provisionnement ...

rescue:
# --- TÂCHES DE NETTOYAGE ---
# Cette section s'exécute lorsque :
# 1. Toute tâche dans le bloc échoue (rollback)
# 2. action == "deprovision" (nettoyage intentionnel)

- 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:] }}", # Numéro fictif
"ue_ambr_dl": 9999999, # Inutilisable
"ue_ambr_ul": 9999999
}
loop: "{{ hss_subscriber_data | dict2items }}"
when:
- deprovision_subscriber | default(false) | bool == false

# L'assertion finale détermine le succès ou l'échec
- name: Set status to "Success" if Manual deprovision / Fail if failed provision
assert:
that:
- action == "deprovision"

Pourquoi Ce Modèle est une Meilleure Pratique

1. Pas de Duplication de Code

Les mêmes tâches de nettoyage gèrent les deux scénarios :

  • Échec de Provision (Rollback) : Si une tâche dans le block échoue, la section rescue s'exécute automatiquement
  • Déprovisionnement Intentionnel : Lorsqu'il est appelé avec action: "deprovision", le playbook passe immédiatement à rescue

2. Nettoyage Complet Garanti

Lorsqu'un provisionnement échoue en cours de route, la section de secours garantit :

  • Tous les comptes OCS créés sont supprimés
  • Toutes les entrées d'équipement réseau configurées sont supprimées
  • L'inventaire assigné est retourné au pool
  • Les abonnés HSS sont supprimés ou mis en état dormant
  • Aucun provisionnement partiel ne reste dans aucun système

Cela empêche les ressources "orphelines" qui :

  • Consomment de l'inventaire sans être suivies
  • Créent des comptes de facturation qui ne sont pas liés aux services
  • Provoquent de la confusion lors du dépannage
  • Gaspillent des ressources réseau

3. Gestion des Échecs Graceful avec ignore_errors

Remarquez que chaque tâche de nettoyage utilise ignore_errors: True. C'est intentionnel car :

  • Lors du rollback, certaines ressources peuvent ne pas avoir encore été créées
  • Nous voulons tenter toutes les tâches de nettoyage même si certaines échouent
  • L'assertion finale détermine le succès/échec global

Par exemple, si le provisionnement échoue à "Créer un compte dans OCS", le nettoyage essaiera de :

  • Supprimer le compte OCS (échouera, mais ignoré)
  • Supprimer les profils d'attributs (échouera, mais ignoré)
  • Retourner l'inventaire (réussit)
  • Supprimer l'abonné HSS (peut ne pas exister, ignoré)

4. Distinction entre Déprovisionnement et Rollback

L'assertion finale à la fin de rescue est astucieuse :

- name: Set status to "Success" if Manual deprovision / Fail if failed provision
assert:
that:
- action == "deprovision"

Cela signifie :

  • Si action == "deprovision" : L'assertion passe, le playbook réussit (statut 0)
  • Si action n'est pas défini ou != "deprovision" : L'assertion échoue, le playbook échoue (statut 2)

Ainsi, le même code de nettoyage entraîne différents statuts de travail de provisionnement en fonction de l'intention.

5. Nettoyage Conditionnel Basé sur le Type de Service

Certaines tâches de nettoyage utilisent des conditionnels pour gérer différents scénarios :

- 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

Cela permet un nettoyage flexible :

  • Suppression complète : Lorsque les SIM sont dédiées aux clients (deprovision_subscriber: true)
  • État dormant : Lorsque les SIM sont réutilisables et doivent rester dans le HSS (deprovision_subscriber: false)

Comment Utiliser Ce Modèle

Pour le Provisionnement :

{
"product_id": 1,
"customer_id": 456,
"provisioning_play": "play_psim_only"
}

Si le provisionnement échoue, un rollback automatique se produit via rescue.

Pour le Déprovisionnement :

{
"service_id": 123,
"service_uuid": "Service_abc123",
"action": "deprovision",
"provisioning_play": "play_psim_only"
}

Le playbook passe directement à la section rescue, exécute tout le nettoyage et réussit.

Résumé des Avantages

Source unique de vérité : Un playbook gère le provisionnement et le déprovisionnement ✅ Opérations atomiques : Soit entièrement provisionné, soit entièrement nettoyé ✅ Pas de ressources orphelines : Les provisions échouées ne laissent aucune trace ✅ Maintenance facilitée : Les modifications de la logique de provisionnement s'appliquent automatiquement au nettoyage ✅ Réduction des erreurs : Aucune chance que le code de provisionnement et de déprovisionnement soit désynchronisé ✅ Testable : Peut tester la logique de déprovisionnement en exécutant avec action: "deprovision"

Ce modèle doit être suivi dans tous les playbooks de provisionnement pour garantir la fiabilité et la cohérence.

Surcharge des Variables de Produit

Les provisioning_json_vars peuvent être surchargées au moment du provisionnement. Par exemple, un produit pourrait définir :

{
"provisioning_json_vars": "{\"monthly_cost\": 50, \"data_limit_gb\": 100}"
}

Mais lors du provisionnement, vous pouvez surcharger celles-ci :

{
"product_id": 1,
"customer_id": 456,
"monthly_cost": 45,
"data_limit_gb": 150
}

Les extra_vars fusionnés utiliseront les valeurs surchargées. Cela permet :

  • Une tarification personnalisée pour des clients spécifiques
  • Différents limites de données en fonction des promotions
  • Des tests avec différents paramètres sans modifier le produit

Produits Sans Inventaire

Tous les produits ne nécessitent pas d'inventaire. Par exemple, un addon de données ou un basculement de fonctionnalité pourrait avoir :

{
"product_id": 10,
"product_name": "Extra 10GB Data",
"provisioning_play": "play_local_data_addon",
"provisioning_json_vars": "{\"data_gb\": 10}",
"inventory_items_list": "[]"
}

Dans ce cas, le playbook reçoit :

extra_vars = {
"product_id": 10,
"customer_id": 456,
"service_id": 123, # Service auquel ajouter des données
"data_gb": 10,
"access_token": "eyJ..."
}

Le playbook ajoute simplement les données au service existant sans avoir besoin d'éléments d'inventaire.

Flux de Travail de Provisionnement Standard

Le provisionnement standard est initié lorsque :

  • Un membre du personnel ajoute un service à un client depuis l'UI
  • Un client commande un service via le portail d'auto-assistance
  • L'API est appelée directement avec PUT /crm/provision/

Lorsque Vous Cliquez sur "Provisionner"

Voici le flux complet qui se produit lorsque l'utilisateur clique sur le bouton "Provisionner" :

1. L'UI Affiche la Sélection de Produit

L'utilisateur sélectionne un produit dans le catalogue de produits. Le produit contient :

  • provisioning_play - Quel playbook Ansible exécuter
  • inventory_items_list - Inventaire requis (par exemple, ['SIM Card', 'Mobile Number'])
  • provisioning_json_vars - Variables par défaut

2. Sélecteur d'Inventaire (Si Nécessaire)

Si inventory_items_list n'est pas vide, une fenêtre modale apparaît montrant des listes déroulantes pour chaque type d'inventaire. L'utilisateur doit sélectionner des éléments d'inventaire disponibles avant de continuer.

3. Bouton de Provisionnement Cliqué

JavaScript envoie la requête 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. L'API Reçoit la Requête

Le point de terminaison de provisionnement (routes/provisioning.py) :

  • Valide l'authentification (jeton Bearer, clé API ou liste blanche IP)
  • Vérifie que l'utilisateur a la permission CREATE_PROVISION
  • Extrait initiating_user du jeton
  • Charge la définition du produit depuis la base de données
  • Récupère le chemin du playbook : OmniCRM-API/Provisioners/plays/play_psim_only.yaml

5. Variables Fusionnées

Le système combine les variables de plusieurs sources :

# Depuis le produit
product_vars = json.loads(product['provisioning_json_vars'])
# Depuis le corps de la requête
request_vars = request.json
# Ajouté par le système
system_vars = {
'product_id': 42,
'customer_id': 123,
'access_token': g.access_token, # Voir la section d'authentification ci-dessous
'initiating_user': 7
}
# Final fusionné
extra_vars = {**product_vars, **request_vars, **system_vars}

6. Enregistrement de Provision Créé

Enregistrement de base de données créé avec statut 1 (En cours) :

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, # En cours
'task_count': 85,
'initiating_user': 7,
'created': '2025-01-10T14:30:00Z'
}

7. Thread en Arrière-plan Lancé

run_playbook_in_background(
playbook='plays/play_psim_only.yaml',
extra_vars=extra_vars,
provision_id=456,
refresh_token=refresh_token # Pour le rafraîchissement du jeton pendant l'exécution
)

8. L'API Retourne Immédiatement

Réponse retournée à l'UI avec provision_id :

{
"provision_id": 456,
"provisioning_status": 1,
"message": "Travail de provisionnement créé"
}

9. L'UI Interroge pour des Mises à Jour

L'UI commence à interroger GET /crm/provision/provision_id/456 toutes les 3 secondes pour vérifier le statut. La réponse comprend :

{
"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 S'exécute en Arrière-plan

Le playbook exécute les tâches séquentiellement :

  • Chaque tâche terminée crée un enregistrement Provision_Event dans la base de données
  • L'événement comprend : nom de la tâche, statut (0=succès, 2=échec, 3=échec mais ignoré), résultat JSON
  • L'UI affiche la progression en temps réel montrant les tâches terminées et la tâche actuellement en cours d'exécution
  • Les tâches échouées affichent des messages d'erreur dans les détails de l'événement

Suivi dans l'UI :

Pendant que le provisionnement est en cours (statut 1), les utilisateurs peuvent voir :

  • Page de Détails du Service - Affiche le badge de statut de provisionnement (En cours/Succès/Échec)
  • Journal d'Activité - Liste tous les événements de provisionnement avec des horodatages
  • Vue des Détails de la Provision - Montre la progression tâche par tâche avec déploiement/repli pour les détails

Exemple d'affichage :

Statut de Provisionnement : En cours (8 sur 12 tâches complétées)

✓ Get Product information from CRM API ✓ Fetch Customer details ✓ Assign SIM Card from inventory (ICCID: 8991101200003204510) ✓ Assign Mobile Number (555-0123) ⟳ Create account in OCS/CGRateS (en cours...) ⏺ Configure network policies ⏺ Create service record ...

11. Le Provisionnement se Termine

Statut final défini :

  • provisioning_status: 0 - Succès
  • provisioning_status: 2 - Échec (erreur critique)

L'UI arrête l'interrogation et affiche le résultat :

  • Succès : Coche verte, service marqué Actif, l'utilisateur peut voir les détails du service
  • Échec : X rouge, message d'erreur affiché, option de réessayer ou de contacter le support
  • Notification par e-mail : En cas d'échec, un e-mail est envoyé à provisioning.failure_list dans la configuration

Authentification et Autorisation

Suivi des Utilisateurs

Chaque travail de provisionnement suit quel utilisateur l'a initié :

  • Initié par l'utilisateur : Le champ initiating_user est défini sur l'ID de l'utilisateur de son jeton JWT
  • Auth par clé API : Utilise le premier ID d'utilisateur admin
  • Auth par liste blanche IP : Utilise le premier ID d'utilisateur admin

Vérifications de Permission

Le système vérifie les permissions avant de permettre le provisionnement :

  • Le personnel a besoin de la permission CREATE_PROVISION
  • Les clients ne peuvent provisionner que des services pour leur propre compte (VIEW_OWN_PROVISION permission)

Comment Ansible S'authentifie avec l'API CRM

Les playbooks Ansible doivent effectuer des appels API authentifiés vers le CRM (pour récupérer les détails du produit, créer des services, mettre à jour l'inventaire, etc.). L'authentification est gérée via des jetons Bearer passés au playbook.

La source du access_token dépend de la méthode d'authentification utilisée pour appeler l'API de provisionnement :

Méthode 1 : Connexion Utilisateur (Jeton Bearer)

Lorsqu'un utilisateur se connecte via l'UI web :

  1. L'utilisateur s'authentifie : POST /crm/auth/login
  2. Reçoit un access_token JWT (à court terme, 15-30 min) et un refresh_token (à long terme)
  3. Fait une demande de provisionnement avec le jeton Bearer dans l'en-tête
  4. L'API de provisionnement extrait le jeton de l'en-tête Authorization: Bearer ...
  5. Stocke dans g.access_token (contexte de requête Flask)
  6. Passe à Ansible comme variable access_token

Code (permissions.py) :

# Extraire le jeton Bearer de l'en-tête
auth_header = request.headers.get('Authorization', '')
if auth_header.startswith('Bearer '):
bearer_token = auth_header[7:]
# Valider et décoder
decoded_token = jwt.decode(bearer_token, secret_key, algorithms=['HS256'])
# Stocker pour le provisionnement
g.access_token = bearer_token

Code (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)

Méthode 2 : Clé API (En-tête X-API-KEY)

Pour les systèmes automatisés utilisant des clés API :

  1. Le système fait une demande : PUT /crm/provision/ avec X-API-KEY: votre-clé-api... en-tête
  2. L'API de provisionnement valide la clé API contre crm_config.yaml
  3. Génère un nouveau jeton JWT à la volée pour le premier utilisateur admin
  4. Stocke dans g.access_token
  5. Passe à Ansible

Pourquoi Générer un Jeton ?

Les clés API sont des chaînes, pas des JWT. Les playbooks appellent des points de terminaison API s'attendant à une authentification JWT. Donc :

  • Valider la clé API
  • Si valide et a le rôle admin, générer un JWT temporaire
  • Utiliser l'ID du premier utilisateur admin comme sujet du JWT
  • Le jeton permet au playbook de faire des appels API authentifiés

Code (permissions.py) :

def handle_api_key_auth(f, api_key, *args, **kwargs):
if not secure_compare_api_key(api_key):
return {'message': 'Clé API invalide'}, 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

Méthode 3 : Liste Blanche IP

Pour les systèmes internes de confiance sur des réseaux privés :

  1. Le système fait une demande depuis une IP sur liste blanche (par exemple, 192.168.1.100)
  2. L'API de provisionnement vérifie l'IP du client contre ip_whitelist dans crm_config.yaml
  3. Si sur liste blanche, génère un nouveau jeton JWT pour le premier utilisateur admin
  4. Stocke dans g.access_token
  5. Passe à Ansible

Code (permissions.py) :

def handle_ip_auth(f, *args, **kwargs):
client_ip = get_real_client_ip()
if not is_ip_whitelisted(client_ip):
return {'message': 'Accès refusé'}, 403

admin_user_id = retrieve_first_admin_user_id()
access_token = create_access_token(identity=str(admin_user_id))
g.access_token = access_token

Utilisation du Jeton dans les Playbooks

Chaque appel API dans le playbook inclut le jeton :

- 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"

Expiration et Rafraîchissement du Jeton

Les playbooks de longue durée (5-10 minutes) peuvent dépasser la durée de vie du access_token (expiration de 15-30 min). Pour les provisions initiées par l'utilisateur, le système passe à la fois access_token et refresh_token :

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

Si le access_token expire, le coureur de playbook peut :

  1. Détecter la réponse 401 Non autorisée
  2. Appeler POST /crm/auth/refresh avec refresh_token
  3. Recevoir un nouveau access_token
  4. Réessayer la requête échouée

Pour l'authentification par clé API/liste blanche IP, les jetons g��nérés peuvent avoir une expiration plus longue (1-2 heures) car ce sont des systèmes automatisés de confiance.

Le Processus de Provisionnement

  1. Création de Travail

    Lorsqu'une demande de provisionnement est reçue, le système :

    • Valide la demande et vérifie les permissions
    • Charge le playbook Ansible spécifié dans la définition du produit
    • Crée un enregistrement Provision dans la base de données avec statut 1 (En cours)
    • Extrait les variables de la définition du produit et du corps de la requête
    • Capture les jetons d'authentification pour l'accès API
  2. Gestion des Jetons

    Les playbooks Ansible doivent s'authentifier avec l'API CRM pour récupérer des données et apporter des modifications. Le système de provisionnement gère cela de deux manières :

    • Jeton Bearer (JWT) : Pour le provisionnement initié par l'utilisateur, le refresh_token de la demande est utilisé pour générer des jetons d'accès frais pendant l'exécution du playbook
    • Auth par Clé API/IP : Pour les systèmes automatisés, un access_token est passé directement au playbook via g.access_token
  3. Exécution en Arrière-plan

    Le playbook s'exécute dans un thread en arrière-plan en utilisant playbook_runner_v2. Cela permet à l'API de retourner immédiatement pendant que le provisionnement continue de manière asynchrone.

    Pendant l'exécution :

    • Chaque tâche terminée/échouée crée un enregistrement Provision_Event
    • Le gestionnaire d'événements surveille les échecs critiques par rapport aux échecs ignorés
    • Des mises à jour de statut en temps réel sont écrites dans la base de données
    • L'UI peut interroger pour des mises à jour via GET /crm/provision/provision_id/<id>
  4. Exécution du Playbook

    Le playbook Ansible effectue généralement ces opérations :

    • Récupère les informations sur le produit depuis l'API
    • Récupère les informations sur le client depuis l'API
    • Assigne des éléments d'inventaire (cartes SIM, adresses IP, numéros de téléphone, etc.)
    • Crée des comptes dans OCS/OCS
    • Configure l'équipement réseau
    • Crée l'enregistrement de service dans l'API CRM
    • Ajoute des transactions de coût de configuration
    • Envoie des e-mails/SMS de bienvenue aux clients
  5. Gestion des Erreurs

    Les playbooks Ansible utilisent des sections block et rescue pour le rollback :

    • Si une tâche critique échoue, la section de secours supprime le provisionnement partiel
    • Les tâches avec ignore_errors: true sont marquées comme statut 3 et ne font pas échouer le travail
    • Les erreurs fatales (erreurs de syntaxe YAML, échecs de connexion) créent un événement d'erreur spécial avec des informations de débogage

Exemple : Playbook de Provisionnement Standard

Voici un exemple depuis 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

Ce playbook démontre le flux typique :

  1. Récupérer les détails du produit depuis l'API CRM
  2. Générer un UUID de service unique
  3. Créer le compte de facturation dans OCS
  4. Créer l'enregistrement de service via l'API CRM
  5. Ajouter des transactions de coût de configuration
  6. Si quelque chose échoue, la section rescue supprime le compte OCS

Flux de Travail de Provisionnement Simple

Le provisionnement simple est conçu pour les systèmes automatisés qui ont besoin de déclencher le provisionnement sans interaction utilisateur. Le cas d'utilisation le plus courant est le déclenchement de provisionnement d'addon par OCS via des ActionPlans.

Points de Terminaison de Provisionnement Simple

OmniCRM fournit deux points de terminaison de provisionnement simple :

  • POST /crm/provision/simple_provision_addon/service_id/<id>/product_id/<id>

    Pour le provisionnement automatisé d'addon (par exemple, frais récurrents, recharges automatiques)

  • POST /crm/provision/simple_provision_addon_recharge/service_id/<id>/product_id/<id>

    Pour des opérations de recharge rapides qui nécessitent un retour immédiat

Authentification pour le Provisionnement Simple

Les points de terminaison de provisionnement simple utilisent la liste blanche IP ou les clés API pour l'authentification :

  • L'IP source de la demande est vérifiée contre ip_whitelist dans crm_config.yaml
  • Ou une clé API de api_keys dans crm_config.yaml peut être fournie
  • Un jeton d'accès est généré et passé au playbook via g.access_token

Exemple : Callback d'ActionPlan OCS

OCS peut être configuré pour appeler le point de terminaison de provisionnement simple lors de l'exécution d'actions récurrentes :

{
"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"
}
]
}]
}

L'action effectue un POST HTTP à :

Cela déclenche le playbook associé (par exemple, play_topup_no_charge.yaml) qui ajoute des données/crédits au service.

Exemple : Playbook de Recharge Simple

Depuis 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": "Merci pour la recharge de {{ monthly_cost }}!"
}
status_code: 201
ignore_errors: True

Ce playbook :

  1. Récupère les détails du service et du produit depuis l'API
  2. Récupère la méthode de paiement du client
  3. Charge le client via l'API Stripe
  4. Ajoute un solde monétaire à OCS
  5. Enregistre la transaction dans le CRM
  6. Envoie un SMS de confirmation (avec ignore_errors: True pour que les échecs ne fassent pas échouer le travail)

Chaînes de Provisionnement

Pour des produits complexes nécessitant plusieurs étapes de provisionnement, OmniCRM prend en charge les chaînes de provisionnement. Une chaîne exécute plusieurs playbooks séquentiellement, passant le contexte entre eux.

Exemple d'utilisation : Un service groupé qui provisionne :

  1. Service Internet de base (crée l'enregistrement de service principal)
  2. Addon IPTV (utilise le service_id de l'étape 1)
  3. Addon IP statique (utilise le service_id de l'étape 1)

Le service de provisionnement :

  • Interroge automatiquement la base de données pour le service_id créé par le premier playbook
  • L'injecte dans les extra_vars pour les playbooks suivants
  • Suit chaque playbook comme un enregistrement Provision séparé

Raisons d'Échec et Débogage

Lorsque le provisionnement échoue, le système capture des informations détaillées pour aider à diagnostiquer le problème.

Scénarios d'Échec Courants

Échecs de Tâches Critiques (Statut 2)

Ceux-ci provoquent l'échec de l'ensemble du travail de provisionnement :

  • Appels API retournant des codes de statut inattendus
  • Échecs d'assertions (par exemple, assert: that: response.status == 200)
  • Éléments d'inventaire requis manquants
  • Équipements réseau inaccessibles
  • Identifiants invalides ou jetons expirés
  • Erreurs OCS/OCS

Échecs Ignorés (Statut 3)

Ceux-ci sont enregistrés mais ne font pas échouer le travail :

  • Notifications SMS/e-mail optionnelles échouant
  • Recherches de données non critiques (marquées avec ignore_errors: True)
  • Opérations de nettoyage lors du déprovisionnement

Erreurs Fatales

Celles-ci empêchent le playbook de s'exécuter :

  • Erreurs de syntaxe YAML dans le playbook
  • Variables non définies dans le playbook
  • Fichiers de playbook manquants
  • Échecs de connexion au contrôleur Ansible

Lorsqu'une erreur fatale se produit, le système crée un événement d'erreur spécial contenant :

  • Le code de sortie Ansible
  • L'intégralité de stdout (contient des détails sur les erreurs de syntaxe)
  • L'intégralité de stderr (contient des erreurs d'exécution)
  • Une liste des causes courantes pour ce type d'échec
  • Toutes les variables passées au playbook

E-mails de Notification d'Erreur

Lorsque le provisionnement échoue (statut 2), un e-mail est automatiquement envoyé à la liste de notification d'échec configurée (provisioning.failure_list dans crm_config.yaml).

L'e-mail comprend :

  • Informations sur le client
  • Détails sur le produit/service
  • Résultats des tâches codés par couleur :
    • Vert : Tâches réussies
    • Orange : Tâches échouées mais ignorées
    • Rouge : Échecs critiques
  • Pour les échecs critiques : Sortie de débogage complète incluant les corps de requête/réponse
  • Pour les erreurs fatales : Sortie Ansible, messages d'erreur et causes courantes

Suivi des Travaux de Provisionnement

API de Statut de Provisionnement

Pour vérifier le statut d'un travail de provisionnement :

GET /crm/provision/provision_id/<id>
Authorization: Bearer <token>

La réponse comprend :

{
"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": "{...}"
}
]
}

Liste des Travaux de Provisionnement

Pour obtenir une liste paginée de tous les travaux de provisionnement :

GET /crm/provision/?page=1&per_page=20&sort=provision_id&order=desc
Authorization: Bearer <token>

Prend en charge le filtrage :

GET /crm/provision/?filters={"provisioning_status":[2]}&search=Mobile
Authorization: Bearer <token>

Cela ne retourne que les travaux échoués (statut 2) où la description contient "Mobile".

Meilleures Pratiques

Conception de Playbook

  • Utilisez toujours block/rescue : Assurez-vous que le provisionnement partiel peut être annulé
  • Utilisez ignore_errors avec parcimonie : Seulement pour des opérations vraiment optionnelles
  • Journalisez les variables importantes : Utilisez des tâches debug pour enregistrer des valeurs clés pour le dépannage
  • Validez les réponses : Utilisez assert pour vérifier que les réponses API sont conformes aux attentes
  • Idempotence : Concevez des playbooks pour être sûrs d'être réexécutables

Authentification

  • Provisionnement initié par l'utilisateur : Utilisez toujours refresh_token pour les playbooks de longue durée
  • Provisionnement automatisé : Utilisez la liste blanche IP ou les clés API avec des jetons d'accès générés
  • Expiration du jeton : Le refresh_token garantit que les jetons d'accès sont régénérés au besoin

Gestion des Erreurs

  • Fournir un contexte : Inclure customer_id, service_id et détails de l'opération dans les messages d'erreur
  • Notifier de manière appropriée : Les échecs critiques déclenchent des e-mails, mais ne spammez pas pour des échecs attendus
  • Infos de débogage : Capturez les corps de requête/réponse complets dans les enregistrements Provision_Event

Sécurité

  • Validez les entrées : Vérifiez customer_id, product_id, service_id avant le provisionnement
  • Vérifications de permission : Vérifiez que les utilisateurs ne peuvent provisionner que pour des clients autorisés
  • Données sensibles : Utilisez le système de redaction pour supprimer les mots de passe/clés des journaux
  • Liste blanche IP : Restreindre les points de terminaison simple_provision aux systèmes de confiance uniquement

Performance

  • Exécution en arrière-plan : Ne bloquez jamais les réponses API en attendant le provisionnement
  • Intervalles de sondage : L'UI doit interroger pour des mises à jour de statut toutes les 2-5 secondes
  • Tâches parallèles : Utilisez le parallélisme natif d'Ansible pour des opérations indépendantes
  • Mises à jour de la base de données : Le gestionnaire d'événements met à jour la base de données en temps réel, pas besoin de requêter pendant l'exécution

Documentation Connexe

  • concepts_ansible - Concepts généraux de provisionnement Ansible
  • concepts_api - Authentification et utilisation de l'API CRM
  • concepts_products_and_services - Définitions de produits et de services
  • administration_inventory - Gestion de l'inventaire pour le provisionnement