> For the complete documentation index, see [llms.txt](https://docs.oktopus.app.br/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.oktopus.app.br/getting-started/enterprise-edition/services.md).

# Services

The services are used in conjunction with TR-x69 discovery mechanisms to achieve Z**ero Touch Provisioning** flows. The CPE is added to the system beforehand, and the proper service is attached to it. When the field technician plugs the CPE into the network infrastructure, the CPE is provisioned with the parameters defined in the ACS specific to that customer.

{% embed url="<https://youtu.be/AnQlSJmgIGo>" %}

## Service Definition

A service definition is a set of parameters, variables, and operations that dictate the communication structure with the CPE when applying the service to its configuration.

## Scripts

Scripts in the zero-touch-provisioning system are written in **Lua** and provide the provisioning logic for CPE (Customer Premises Equipment) devices. They define how services are applied to devices, including configuration changes, credential management, and parameter updates.

### Execution Flow

1. **Script Initialization**: A Lua script is loaded with the device context (device ID, tenant ID, and variables)
2. **Lua Environment Setup**: The script runs in a secure Lua sandbox with access to predefined global variables and functions
3. **Function Calls**: The script can call Go functions to interact with devices and services
4. **Status Return**: The script must set a `provisioning_status` global variable to indicate success or failure

#### Global Variables

These variables are automatically available to scripts:

| Variable              | Type   | Description                                                              |
| --------------------- | ------ | ------------------------------------------------------------------------ |
| `device_id`           | string | The serial number or unique identifier of the target device              |
| `variables`           | table  | A table containing service-specific variables passed during provisioning |
| `provisioning_status` | string | Must be set to indicate the final status (see below)                     |

**Provisioning Status Values**

| Value           | Meaning                      |
| --------------- | ---------------------------- |
| `"provisioned"` | Service successfully applied |
| `"failed"`      | Service provisioning failed  |

**Important**: Scripts must explicitly set `provisioning_status` before completion. If not set, the provisioning will fail.

#### Example Structure

```lua
-- Access device information
local device_sn = get_device_id()
print("Processing device: " .. device_sn)

-- Access variables
local username = get_variable_value("pppoe_username")
local password = get_variable_value("pppoe_password")

-- Perform operations
local result = send_cwmp_message_and_forget(device_sn, message_xml, timeout)

-- Check result and set status
if result == false or type(result) == "table" then
    set_provisioning_status("failed")
else
    set_provisioning_status("provisioned")
end
```

### Available Functions

#### Provisioning Functions

**`set_provisioning_status(status)`**

Sets the final provisioning status for the script execution.

**Parameters:**

* `status` (string): Either `"provisioned"` or `"failed"`

**Returns:** Nothing

**Example:**

```lua
if success then
    set_provisioning_status("provisioned")
else
    set_provisioning_status("failed")
end
```

***

**`get_device_id()`**

Retrieves the device serial number or identifier currently being provisioned.

**Parameters:** None

**Returns:**

* (string) The device ID

**Example:**

```lua
local sn = get_device_id()
print("Device SN: " .. sn)
```

***

**`get_variable_value(variable_name)`**

Retrieves a variable value from the service instance configuration.

**Parameters:**

* `variable_name` (string): The name of the variable to retrieve

**Returns:**

* (string) The variable value, or nil if not found

**Example:**

```lua
local pppoe_username = get_variable_value("pppoe_username")
local pppoe_password = get_variable_value("pppoe_password")
```

***

#### CPE Interaction Functions

**`send_cwmp_message(device_id, message_body, method, block_queue)`**

Sends a CWMP message to the device and **blocks until the CPE answers** (or the request times out), unlike `send_cwmp_message_and_forget` below, which returns as soon as the message is handed off to the ACS adapter without waiting for — or confirming — the CPE actually applied it. Use this function when the script needs to read back a value or confirm a `Set` actually took effect.

**Parameters:**

* `device_id` (string): The serial number of the target device
* `message_body` (string): XML-formatted CWMP message
* `method` (number): CWMP message type, used to decode the response:
  * `0` = GetParameterValues
  * `1` = SetParameterValues
  * `2` = AddObject
  * `3` = DeleteObject
  * `4` = GetParameterNames
* `block_queue` (boolean, optional): whether to block the device's message queue until this request is answered

**Returns:**

* If the device isn't found, isn't online, or the request fails/times out: a *table* with `error_message` and `error_code`.
* If the CPE answers with a SOAP/CWMP `Fault` (regardless of `method`): a *table* with:
  * `error_message` (string): the top-level `FaultString`
  * `error_code` (string): the top-level `FaultCode` (e.g. `"9003"`)
  * `raw_response` (string): the full raw XML response received from the CPE
  * `parameter_faults` (table, optional): present for `SetParameterValues` faults — an array where each entry has `parameter_name`, `fault_code`, and `fault_string` for the specific parameter that was rejected
* Otherwise, the return shape depends on `method`:
  * `0` (Get): a *table* mapping each requested parameter path to its value.
  * `1` (Set): a *boolean* indicating success.
  * `2` (Add): a *table* with `instance_number` and `status`.
  * `3` (Delete): a *boolean* indicating success.
  * `4` (GetParameterNames): a *table* of parameter names.

**Example:**

```lua
-- Set a parameter, then verify it was actually applied on the CPE
local set_msg = [[
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soap:Header/>
  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <cwmp:SetParameterValues>
      <ParameterList soapenc:arrayType="cwmp:ParameterValueStruct[1]">
        <ParameterValueStruct>
          <Name>InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username</Name>
          <Value>]] .. get_variable_value("pppoe_username") .. [[</Value>
        </ParameterValueStruct>
      </ParameterList>
      <ParameterKey></ParameterKey>
    </cwmp:SetParameterValues>
  </soap:Body>
</soap:Envelope>
]]

local set_result = send_cwmp_message(get_device_id(), set_msg, 1)
if type(set_result) == "table" or set_result == false then
    print("Failed to set PPPoE username")
    set_provisioning_status("failed")
    return
end

-- Give the CPE time to apply it, then read it back to confirm
sleep(10)

local get_msg = [[
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soap:Header/>
  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <cwmp:GetParameterValues>
      <ParameterNames soapenc:arrayType="xsd:string[1]">
        <string>InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username</string>
      </ParameterNames>
    </cwmp:GetParameterValues>
  </soap:Body>
</soap:Envelope>
]]

local verification = send_cwmp_message(get_device_id(), get_msg, 0)
if verification["error_message"] ~= nil then
    print("Failed to verify PPPoE username: " .. verification["error_message"])
    set_provisioning_status("failed")
    return
end

local param = "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username"
if verification[param] == get_variable_value("pppoe_username") then
    print("PPPoE username verified successfully")
    set_provisioning_status("provisioned")
else
    print("PPPoE username verification failed, got: " .. tostring(verification[param]))
    set_provisioning_status("failed")
end
```

***

**`send_cwmp_message_and_forget(device_id, message_body, timeout)`**

Enqueue a CWMP message to the device and does not wait for a response.

**Parameters:**

* `device_id` (string): The serial number of the target device
* `message_body` (string): XML-formatted CWMP message
* `timeout` (number): Timeout in seconds (for the operation)

**Returns:**

* (boolean) `true` if the message was successfully sent
* (table) Error table with `error_message` and `error_code` fields if failed

**Example:**

```lua
local cwmp_msg = [[
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <cwmp:SetParameterValues>
      <!-- parameters -->
    </cwmp:SetParameterValues>
  </soap:Body>
</soap:Envelope>
]]

local result = send_cwmp_message_and_forget(get_device_id(), cwmp_msg, 2)

if result == false or type(result) == "table" then
    print("Error: " .. result["error_message"] .. " (" .. result["error_code"] .. ")")
    set_provisioning_status("failed")
else
    print("Message sent successfully")
end
```

***

#### Utility Functions

**`sleep(seconds)`**

Pauses script execution for the specified number of seconds.

**Parameters:**

* `seconds` (number): Number of seconds to sleep

**Returns:** Nothing

**Example:**

```lua
print("Waiting for device to process...")
sleep(5)
print("Continuing...")
```

***

**`generate_random_string(length)`**

Generates a random alphanumeric string of the specified length.

**Parameters:**

* `length` (number): Length of the random string to generate (must be > 0)

**Returns:**

* (string) Random string of the specified length, or empty string if length is invalid

**Example:**

```lua
local random_password = generate_random_string(12)
print("Generated password: " .. random_password)
```

***

**`create_or_update_device_credential(username, password)`**

Creates or updates device credentials for authentication.

**Parameters:**

* `username` (string): The username (cannot contain dots, cannot be empty)
* `password` (string): The password (cannot contain dots, cannot be empty)

**Returns:**

* (table) Result table with the following fields:
  * `ok` (boolean): Success flag
  * `username` (string): The full username (prefixed with organization ID)
  * `password` (string): The password
  * `error_msg` (string): Error message if failed, empty if successful

**Validation:**

* Username and password cannot be empty
* Username and password cannot contain dots
* Requires valid organization context

**Example:**

```lua
local result = create_or_update_device_credential("admin_user", "secure_pass_123")

if result["ok"] then
    print("Credential created: " .. result["username"])
    -- Now you can use result["username"] and result["password"] for device login
else
    print("Failed to create credential: " .. result["error_msg"])
    set_provisioning_status("failed")
end
```

***

**`refresh_device_parameters(device_id)`**

Triggers a parameter refresh on the device, forcing it to send an updated parameter list to the ACS.

**Parameters:**

* `device_id` (string): The serial number of the device

**Returns:**

* (boolean) `true` if refresh was successfully triggered, `false` otherwise

**Example:**

```lua
if refresh_device_parameters(get_device_id()) then
    print("Device parameters refresh initiated")
else
    print("Failed to refresh device parameters")
    set_provisioning_status("failed")
end
```

#### Best Practices

1. **Always Check Return Values**: Never assume a function succeeded

   ```lua
   local result = send_cwmp_message_and_forget(device_id, msg, 2)
   if result == false or type(result) == "table" then
       -- Handle error
   end
   ```
2. **Use Descriptive Logging**: Leverage the `print()` function

   ```lua
   print("Step 1: Creating PPPoE WAN connection")
   local result = send_cwmp_message_and_forget(device_id, msg, 2)
   if result == false then
       print("Failed to create PPPoE WAN")
       set_provisioning_status("failed")
       return
   end
   ```
3. **Set Status Explicitly**: Always set provisioning status before script ends

   ```lua
   -- Good
   if success then
       set_provisioning_status("provisioned")
   else
       set_provisioning_status("failed")
   end

   -- Avoid - status not set
   if success then
       print("Success")
   end
   ```
4. **Handle Sequential Operations**: Use proper error handling for multi-step processes

   ```lua
   -- Step 1
   local step1 = send_cwmp_message_and_forget(device_id, msg1, 2)
   if step1 == false then
       set_provisioning_status("failed")
       return
   end

   -- Step 2
   sleep(1)
   local step2 = send_cwmp_message_and_forget(device_id, msg2, 2)
   if step2 == false then
       set_provisioning_status("failed")
       return
   end

   set_provisioning_status("provisioned")
   ```

### Limitations

* Scripts run in a restricted Lua sandbox for security
* File I/O is not available
* Network operations are only available through the provided functions
* Scripts must complete within reasonable timeout limits

### Real-World Example: PPPoE Service Configuration

1. Creates a PPPoE WAN connection on the device
2. Sets PPPoE credentials using variables
3. Changes the WAN type to PPPoE
4. Refreshes device parameters
5. Properly handles errors at each step

The example demonstrates:

* Using variables from the service instance
* Building XML CWMP messages
* Sequential provisioning steps
* Error handling
* Device parameter refresh

```lua
local function createCWMPAddPPPoEWANMsg(wanInstanceNumber)
    return [[
        <?xml version="1.0" encoding="UTF-8"?>
        <soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..schemaswt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <soap:Header/>
        <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
            <cwmp:AddObject>
            <ObjectName>InternetGatewayDevice.WANDevice.1.WANConnectionDevice.]] .. wanInstanceNumber ..[[.WANPPPConnection.</ObjectName>
            <ParameterKey></ParameterKey>
            </cwmp:AddObject>
        </soap:Body>
        </soap:Envelope>
    ]]
end

local function createCWMPSetPPPoECredentialsMsg(wanInstanceNumber, pppoeInstanceNumber)
    return [[
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..schemaswt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soap:Header/>
  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <cwmp:SetParameterValues>
      <ParameterList soapenc:arrayType="cwmp:ParameterValueStruct[4]">
        <ParameterValueStruct>
            <Name>InternetGatewayDevice.WANDevice.1.WANConnectionDevice.]] .. wanInstanceNumber .. [[.WANPPPConnection.]].. pppoeInstanceNumber ..[[.Enable</Name>
            <Value xsi:type="xsd:boolean">1</Value>
        </ParameterValueStruct>
        <ParameterValueStruct>
            <Name>InternetGatewayDevice.WANDevice.1.WANConnectionDevice.]] .. wanInstanceNumber .. [[.WANPPPConnection.]].. pppoeInstanceNumber ..[[.Username</Name>
            <Value xsi:type="xsd:string">]] .. get_variable_value("pppoe_username") .. [[</Value>
        </ParameterValueStruct>
        <ParameterValueStruct>
            <Name>InternetGatewayDevice.WANDevice.1.WANConnectionDevice.]] .. wanInstanceNumber .. [[.WANPPPConnection.]].. pppoeInstanceNumber ..[[.Password</Name>
            <Value xsi:type="xsd:string">]] .. get_variable_value("pppoe_password") .. [[</Value>
        </ParameterValueStruct>
        <ParameterValueStruct>
            <Name>InternetGatewayDevice.WANDevice.1.WANConnectionDevice.]] .. wanInstanceNumber .. [[.WANPPPConnection.]].. pppoeInstanceNumber ..[[.Name</Name>
            <Value xsi:type="xsd:string">internet-pppoe</Value>
        </ParameterValueStruct>
      </ParameterList>
      <ParameterKey>LC1309946174</ParameterKey>
    </cwmp:SetParameterValues>
  </soap:Body>
</soap:Envelope>
    ]]
end

local function createCWMPChangeWanToPPPoE()
    return [[
        <?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..schemaswt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soap:Header/>
  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <cwmp:SetParameterValues>
      <ParameterList soapenc:arrayType="cwmp:ParameterValueStruct[1]">
        <ParameterValueStruct>
            <Name>InternetGatewayDevice.WANDevice.1.WANCommonInterfaceConfig.X_AVM-DE_InternetConnectionLinkType</Name>
            <Value xsi:type="xsd:string">PPPoE</Value>
        </ParameterValueStruct>
      </ParameterList>
      <ParameterKey>LC1309986174</ParameterKey>
    </cwmp:SetParameterValues>
  </soap:Body>
</soap:Envelope>
    ]]
end 

print("Processing device SN: " .. get_device_id() .. ", adding PPPoE WAN connection")
--add PPPoE WAN Connection
local addPPPoEMsgResult = send_cwmp_message_and_forget(get_device_id(), createCWMPAddPPPoEWANMsg(1), 2)
if type(addPPPoEMsgResult) == "table" or addPPPoEMsgResult == false then
    print("Failed to add PPPoE WAN, SN: " ..
    get_device_id() ..
    " | Error Message: " ..
    addPPPoEMsgResult["error_message"] .. " | Code: " .. addPPPoEMsgResult["error_code"])
    set_provisioning_status("failed")
else

    print("Added PPPoE WAN successfully done, SN: " .. get_device_id() .. ", now setting credentials and changing WAN to PPPoE")
    -- Set PPPoE credentials
    local setPppoeCredentialsMsgResult = send_cwmp_message_and_forget(get_device_id(), createCWMPSetPPPoECredentialsMsg(1, 1), 1)
    
    if type(setPppoeCredentialsMsgResult) == "table" or setPppoeCredentialsMsgResult == false then
        print("Failed to set PPPoE credentials, SN: " .. get_device_id() .. " | Error Message: " .. setPppoeCredentialsMsgResult["error_message"] .. " | Code: " .. setPppoeCredentialsMsgResult["error_code"])
        set_provisioning_status("failed")
    else
        
        print("Set PPPoE credentials successfully done, SN: " .. get_device_id() .. ", now changing WAN to PPPoE")
        -- Change WAN from IPoE to PPPoE
        local changeWanToPPPoEMsgResult = send_cwmp_message_and_forget(get_device_id(), createCWMPChangeWanToPPPoE(), 1)
        
        if type(changeWanToPPPoEMsgResult) == "table" or changeWanToPPPoEMsgResult == false then
            print("Failed to change WAN to PPPoE, SN: " .. get_device_id() .. " | Error Message: " .. changeWanToPPPoEMsgResult["error_message"] .. " | Code: " .. changeWanToPPPoEMsgResult["error_code"])
            set_provisioning_status("failed")
        else
            print("Changed WAN to PPPoE successfully done")
            if refresh_device_parameters(get_device_id()) == false then
                print("Failed to refresh device parameters")
                set_provisioning_status("failed")
            else
                print("Finished processing device SN: " .. get_device_id() .. ", messages queued")
                set_provisioning_status("provisioned")
            end
        end
    end
end
```

## Service Instances

The service instance is the attachment of a service to a CPE, defining the specific variable values for that equipment. It's applied automatically whenever the CPE sends one of the CWMP events configured on its service definition — for example, on first connection or after a factory reset — or on demand at any time, see [Manual Execution](#manual-execution) below.

### Trigger Events

Each service definition declares, via its `events` field, which CWMP events cause Oktopus to automatically apply it to the CPEs attached to it. A definition can list one or more of the standard TR-069 event codes:

| Code | Event                        |
| ---- | ---------------------------- |
| 0    | BOOTSTRAP                    |
| 1    | BOOT                         |
| 2    | PERIODIC                     |
| 3    | SCHEDULED                    |
| 4    | VALUE CHANGE                 |
| 5    | KICKED                       |
| 6    | CONNECTION REQUEST           |
| 7    | TRANSFER COMPLETE            |
| 8    | DIAGNOSTICS COMPLETE         |
| 9    | REQUEST DOWNLOAD             |
| 10   | AUTONOMOUS TRANSFER COMPLETE |

The most common choice is `0 - BOOTSTRAP`, which fires on the CPE's first connection or after a factory reset — this is what makes zero-touch provisioning "zero touch." A definition isn't limited to one event: for example, listing both `0` and `2` re-applies the service both on first contact and whenever the CPE issues a Periodic Inform.

A service definition can also declare **no events at all**. In that case it never runs automatically — it only executes when explicitly triggered, see [Manual Execution](#manual-execution) below.

### Manual Execution

Independent of its configured trigger events (if any), a service instance can always be executed on demand, immediately, without waiting for its trigger event to fire. This is the **only** way a manual-only service definition (one with no `events` configured) is ever executed.

### Status

The service attached to the CPE has a few statuses:

* Pending = It's waiting for a trigger event (or a manual execution) to start running and being applied to the equipment.
* Running = The instance was triggered — by an event or manually — and the script is being executed.
* Provisioned = Execution was a success, the CPE received all the messages.
* Failed = An issue happened during the execution of the script.

## In Practice

After learning the concepts, let's understand how it works in a real use case.

### Configure the CPE to connect to the ACS

The CPE must have a way to connect to the Controller system. You can use one of these options:

* Firmware with ACS pre-configured
* DHCP with option 43
* PPPoE reply with ACS server URL

{% stepper %}
{% step %}

### Add a CPE

Manually insert a new device with its unique identifier (e.g, serial number, agent endpoint id).
{% endstep %}

{% step %}

### Create a service definition

Write the Lua scripts and necessary variables to apply that service to the CPE.
{% endstep %}

{% step %}

### Attach the service to the CPE

Associate the CPE with a service, with its specific variable values for that device.
{% endstep %}

{% step %}

### Plug the CPE into the network

As the CPE already has the ACS configured and a connection to it, all services will be provisioned when it establishes communication with the controller system, and the device will come online with the customer's specific configuration.
{% endstep %}
{% endstepper %}
