Skip to content

AI-operated service with Pack

The service owner story where the AI does the rendering: consumers declare what they want, NetOrca Pack's processors turn each declaration into deployable configuration and verify it, and your automation only executes and reports back. When a deployment fails, you feed the error back and the AI fixes its own rendering. Concepts and the module map live in the Pack and AI guide; this page is the workflow.

The running example is a real one: a VIRTUAL_SERVER service whose consumers declare {name, vip, backend, waf} and whose config processor renders F5 AS3 (the modern successor of the WAF pack demo, which drove these same endpoints with a hand-rolled REST client - every raw requests call there is now a module task).

Personas: platform admin (once), service owner (you), consumer (unchanged - they just declare).

Stage 0 - provision the AI stack (once per service)

One prerequisite lives outside Ansible by design: an admin configures the LLM models in the GUI's Platform Settings (the platform accepts model writes only from superuser sessions - see the guide). Everything else is declarative:

- name: Provision the AI stack for VIRTUAL_SERVER
  hosts: localhost
  gather_facts: false
  vars:
    service_id: 49
  tasks:
    - name: Which LLM models does the platform offer?
      netautomate.netorca.netorca_llm_model_info:
      register: models

    - name: Pick one by name
      ansible.builtin.set_fact:
        llm_id: "{{ (models.llm_models | selectattr('name', 'eq', 'Claude 4.6') | first).id }}"

    - name: Knowledge the AI should render against
      netautomate.netorca.netorca_ai_document:
        service_id: "{{ service_id }}"
        filename: "{{ item | basename }}"
        raw_content: "{{ lookup('ansible.builtin.file', item) }}"
      loop: "{{ lookup('ansible.builtin.fileglob', 'knowledge/*.md', wantlist=true) }}"

    - name: The config processor - declaration in, AS3 out
      netautomate.netorca.netorca_ai_processor:
        service_id: "{{ service_id }}"
        action_type: config
        name: vip_config
        llm_model: "{{ llm_id }}"
        prompt: "{{ lookup('ansible.builtin.file', 'prompts/config_prompt.md') }}"
        response_schema: "{{ lookup('ansible.builtin.file', 'prompts/config_schema.json') | from_json }}"
        extra_data:
          enable_pack_context: true
        active: true

    - name: The verify processor - checks the rendering before anyone deploys it
      netautomate.netorca.netorca_ai_processor:
        service_id: "{{ service_id }}"
        action_type: verify
        name: vip_verify
        llm_model: "{{ llm_id }}"
        prompt: "Verify the AS3 against the declaration; approve only exact intent matches."
        active: true

    - name: Turn Pack on
      netautomate.netorca.netorca_pack_profile:
        service_id: "{{ service_id }}"
        pack_enabled: true

Every task above is an idempotent upsert - re-running the play converges, and changing a prompt file in Git changes exactly that processor. The runnable version (with an optional auto-approval validator) is examples/scenarios/pack_ai/provision_ai_stack.yml.

Stage 1 - runs happen

From here, pipelines appear without you: a consumer's merge submits a declaration, the change instance is approved (by your validation playbook, a human, or a validator processor with allow_auto_approval), and the platform triggers config -> verify. Each run is a pipeline with a version, a state (WAITING_FOR_RESPONSE -> OK / FAILED) and the accumulated LLM cost. Manual runs and re-renders use netorca_pack_trigger - see the trigger-then-poll pattern.

Stage 2 - the executor loop

Successful, not-yet-applied pipelines are your work queue. Deploy what the AI rendered, report the result into the run, mark it applied:

- name: Pack executor loop
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Runs waiting for an executor
      netautomate.netorca.netorca_pack_pipeline_info:
        state: [OK]
        applied: false
      register: queue

    - name: Execute each run
      ansible.builtin.include_tasks: tasks/execute_one.yml
      loop: "{{ queue.pack_pipelines }}"
      loop_control:
        loop_var: pipeline
        label: "pipeline #{{ pipeline.id }} v{{ pipeline.version }}"

with tasks/execute_one.yml as the block/rescue unit - the same honesty rule as the classic fulfilment loop: success and failure both get reported, just into the pipeline instead of the change instance:

- block:
    - name: Deploy the rendered config (your infrastructure step)
      ansible.builtin.uri:
        url: "https://{{ bigip }}/mgmt/shared/appsvcs/declare"
        method: POST
        body: "{{ pipeline.config.data.as3_json }}"
        body_format: json
        force_basic_auth: true
        url_username: "{{ bigip_user }}"
        url_password: "{{ bigip_password }}"

    - name: Report success into the run
      netautomate.netorca.netorca_pack_data:
        object_id: "{{ pipeline.config.object_id }}"
        stage: execution
        data:
          success: true
          deployed_at: "{{ now(utc=true).isoformat() }}"

    - name: Off the queue
      netautomate.netorca.netorca_pack_pipeline:
        id: "{{ pipeline.id }}"
        applied: true

  rescue:
    - name: Report the failure into the run
      netautomate.netorca.netorca_pack_data:
        object_id: "{{ pipeline.config.object_id }}"
        stage: execution
        data:
          success: false
          error: "{{ ansible_failed_result.msg | default('unknown error') }}"

Runnable version: examples/scenarios/pack_ai/executor_loop.yml (writes gated behind -e apply_changes=true, so it is safe to point at a shared instance).

Stage 3 - self-healing: retrigger with feedback

When execution failed because the rendering was wrong, don't fix the config by hand - tell the AI and let it re-render:

- name: Ask the AI to fix its own output
  netautomate.netorca.netorca_pack_trigger:
    object_id: "{{ pipeline.config.object_id }}"
    retrigger: true
    comment: "deployment failed: {{ ansible_failed_result.msg }} - render against vlan 210"

The retriggered run restarts at config with your comment in the prompts, producing a new pipeline version; your executor loop picks it up on the next pass. This trigger-execute-feedback cycle is the pack equivalent of the WAF demo's tuning loop, where Splunk events and BIG-IP suggestions flowed back into every re-render.

Operating notes

  • Watch the spend: every pipeline carries its cost. A scheduled report is one task - netorca_pack_pipeline_info with start_date, then sum cost over the results.
  • State machine: WAITING_FOR_RESPONSE is normal both mid-run and at an external execution stage; only FAILED needs attention. SCHEDULED shows up for crontab-scheduled processors.
  • Consumers see none of this - their experience stays the GitOps flow; Pack only changes who (or what) does the owner-side work.
  • What the modules replaced: the original pack repos vendored a raw REST client (netorca_pack_client.py) for stage reads, pushes and retriggers. The executor loop above is that client's whole job, in four declarative tasks, with check mode and structured errors.