Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions deployments/xapps/scaling-xapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,81 @@ kubectl logs -n ricxapp deployment/ricxapp-scaling
| K8s API | ❌ 不直接調用 | ✅ 直接調用 |
| RMR Messaging | ✅ 使用 | ❌ 不使用 |


## Policy Status Reporting (O-RAN A1-P v2 標準)

### 概述

Scaling xApp 完全符合 O-RAN A1-P v2 規範,自動向 A1 Mediator 報告每個 policy 的執行狀態。

### 狀態類型

- **ENFORCED**: Policy 成功執行,Deployment 已按要求擴展
- **NOT_ENFORCED**: Policy 無法執行(Deployment 不存在或操作失敗)

### API 規格

```
POST /A1-P/v2/policytypes/100/policies/{policyId}/status
Content-Type: application/json

{
"enforceStatus": "ENFORCED" | "NOT_ENFORCED",
"enforceReason": "Human-readable reason string"
}
```

### 示例

#### 成功情況
```json
{
"enforceStatus": "ENFORCED",
"enforceReason": "Successfully scaled ran-a/nf-sim to 5 replicas"
}
```

#### 失敗情況
```json
{
"enforceStatus": "NOT_ENFORCED",
"enforceReason": "Failed to scale default/nonexistent: deployments.apps \"nonexistent\" not found"
}
```

### Metrics

Policy Status Reporting 暴露專用 metrics:

```promql
# 狀態報告總數(按狀態和結果分類)
scaling_xapp_policy_status_reports_total{enforce_status, result}

# Labels:
# - enforce_status: "ENFORCED", "NOT_ENFORCED"
# - result: "success", "network_error", "http_error", "marshal_error"
```

### 日誌

```
2026-02-24T10:00:30Z INFO 📊 Policy status reported: policy-test-scale-to-5 → ENFORCED (HTTP 200)
2026-02-24T10:00:31Z INFO 📊 Policy status reported: policy-invalid → NOT_ENFORCED (HTTP 200)
```

### 故障處理

狀態報告失敗**不會**影響 scaling 操作本身:

- Scaling 成功 → 嘗試報告 ENFORCED(即使報告失敗,deployment 已擴展)
- Scaling 失敗 → 嘗試報告 NOT_ENFORCED

所有狀態報告失敗都會記錄在日誌和 metrics 中。

### O-RAN 合規性

- ✅ 符合 O-RAN.WG2.A1AP-v03.01 規範
- ✅ 支持 A1-P v2 API
- ✅ 自動狀態報告(無需手動觸發)
- ✅ 提供詳細的執行原因 (enforceReason)

80 changes: 78 additions & 2 deletions deployments/xapps/scaling-xapp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"

"github.com/prometheus/client_golang/prometheus"
Expand Down Expand Up @@ -38,6 +39,14 @@ var (
[]string{"method", "status_code"},
)

policyStatusReports = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "scaling_xapp_policy_status_reports_total",
Help: "Total number of policy status reports sent to A1 Mediator",
},
[]string{"enforce_status", "result"},
)

// Gauges
activePolicies = promauto.NewGauge(
prometheus.GaugeOpts{
Expand Down Expand Up @@ -103,6 +112,12 @@ type ScalingSpec struct {
Source string `json:"source"`
}

// PolicyStatus represents the status report sent to A1 Mediator
type PolicyStatus struct {
EnforceStatus string `json:"enforceStatus"` // "ENFORCED" or "NOT_ENFORCED"
EnforceReason string `json:"enforceReason"` // Human-readable reason
}

type ScalingXApp struct {
k8sClient *kubernetes.Clientset
a1URL string
Expand Down Expand Up @@ -258,8 +273,22 @@ func (x *ScalingXApp) executePolicy(ctx context.Context, policyID string) error
log.Printf("Executing scaling policy: %s (target=%s, namespace=%s, replicas=%d)",
policyID, spec.Target, spec.Namespace, spec.Replicas)

// Execute the scaling action
return x.scaleDeployment(ctx, spec)
// Execute the scaling action and report status
err = x.scaleDeployment(ctx, spec)

// Report policy status to A1 Mediator
if err != nil {
// Failed - report NOT_ENFORCED
reason := fmt.Sprintf("Failed to scale %s/%s: %v", spec.Namespace, spec.Target, err)
x.reportPolicyStatus(policyID, false, reason)
return err
}

// Success - report ENFORCED
reason := fmt.Sprintf("Successfully scaled %s/%s to %d replicas", spec.Namespace, spec.Target, spec.Replicas)
x.reportPolicyStatus(policyID, true, reason)

return nil
}

func (x *ScalingXApp) scaleDeployment(ctx context.Context, spec ScalingSpec) error {
Expand Down Expand Up @@ -301,6 +330,53 @@ func (x *ScalingXApp) scaleDeployment(ctx context.Context, spec ScalingSpec) err
return nil
}

// reportPolicyStatus reports policy enforcement status to A1 Mediator
func (x *ScalingXApp) reportPolicyStatus(policyID string, enforced bool, reason string) {
start := time.Now()
defer func() {
a1RequestDuration.WithLabelValues("POST_STATUS").Observe(time.Since(start).Seconds())
}()

// Prepare status payload
status := PolicyStatus{
EnforceStatus: "NOT_ENFORCED",
EnforceReason: reason,
}
if enforced {
status.EnforceStatus = "ENFORCED"
}

statusJSON, err := json.Marshal(status)
if err != nil {
log.Printf("Failed to marshal policy status for %s: %v", policyID, err)
policyStatusReports.WithLabelValues(status.EnforceStatus, "marshal_error").Inc()
return
}

// Send status to A1 Mediator
url := fmt.Sprintf("%s/A1-P/v2/policytypes/100/policies/%s/status", x.a1URL, policyID)

resp, err := http.Post(url, "application/json", strings.NewReader(string(statusJSON)))
if err != nil {
log.Printf("Failed to report status for policy %s: %v", policyID, err)
a1Requests.WithLabelValues("POST", "error").Inc()
policyStatusReports.WithLabelValues(status.EnforceStatus, "network_error").Inc()
return
}
defer resp.Body.Close()

a1Requests.WithLabelValues("POST", strconv.Itoa(resp.StatusCode)).Inc()

if resp.StatusCode >= 200 && resp.StatusCode < 300 {
log.Printf("📊 Policy status reported: %s → %s (HTTP %d)",
policyID, status.EnforceStatus, resp.StatusCode)
policyStatusReports.WithLabelValues(status.EnforceStatus, "success").Inc()
} else {
log.Printf("Failed to report policy status for %s: HTTP %d", policyID, resp.StatusCode)
policyStatusReports.WithLabelValues(status.EnforceStatus, "http_error").Inc()
}
}

func main() {
// Start metrics server
go func() {
Expand Down
Loading