diff --git a/deployments/xapps/scaling-xapp/README.md b/deployments/xapps/scaling-xapp/README.md index c7e42d9df..7fffc36da 100644 --- a/deployments/xapps/scaling-xapp/README.md +++ b/deployments/xapps/scaling-xapp/README.md @@ -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) + diff --git a/deployments/xapps/scaling-xapp/main.go b/deployments/xapps/scaling-xapp/main.go index 71378bb69..03f3b1671 100644 --- a/deployments/xapps/scaling-xapp/main.go +++ b/deployments/xapps/scaling-xapp/main.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "strconv" + "strings" "time" "github.com/prometheus/client_golang/prometheus" @@ -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{ @@ -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 @@ -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 { @@ -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() {