diff --git a/CHANGELOG.md b/CHANGELOG.md index dcaa91071..1856df694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -238,6 +238,8 @@ All notable changes to this project will be documented in this file. - Client - Cache network interface index/name lookups in liveness UDP service to fix high CPU usage caused by per-packet RTM_GETLINK netlink dumps - Add observability to BGP handleUpdate: log withdrawal/NLRI counts per batch and track processing duration via `doublezero_bgp_handle_update_duration_seconds` histogram +- Device Health Oracle + - Add device-health-oracle to provisioning workflow, matching current behavior by promoting link and device health to ready without actually performing any health checks - E2E tests - The QA alldevices test now skips devices that are not calling the controller - e2e: Expand RFC11 end-to-end testing ([#2801](https://github.com/malbeclabs/doublezero/pull/2801)) @@ -281,12 +283,11 @@ All notable changes to this project will be documented in this file. - feat(smartcontract): add use_onchain_deallocation flag to MulticastGroup ([#2748](https://github.com/malbeclabs/doublezero/pull/2748)) - CLI - Remove restriction for a single tunnel per user; now a user can have a unicast and multicast tunnel concurrently (but can only be a publisher _or_ a subscriber) ([2728](https://github.com/malbeclabs/doublezero/pull/2728)) - -## [v0.8.3](https://github.com/malbeclabs/doublezero/compare/client/v0.8.2...client/v0.8.3) – 2026-01-22 - - Data - Add indexer that syncs serviceability and telemetry data to ClickHouse and Neo4J +## [v0.8.3](https://github.com/malbeclabs/doublezero/compare/client/v0.8.2...client/v0.8.3) – 2026-01-22 + ### Breaking - None for this release diff --git a/controlplane/device-health-oracle/cmd/device-health-oracle/main.go b/controlplane/device-health-oracle/cmd/device-health-oracle/main.go index 7326f45b7..f93efc626 100644 --- a/controlplane/device-health-oracle/cmd/device-health-oracle/main.go +++ b/controlplane/device-health-oracle/cmd/device-health-oracle/main.go @@ -40,6 +40,7 @@ var ( ledgerRPCURL = flag.String("ledger-rpc-url", "", "the url of the ledger rpc") serviceabilityProgramID = flag.String("serviceability-program-id", "", "the id of the serviceability program") telemetryProgramID = flag.String("telemetry-program-id", "", "the id of the telemetry program") + signerKeypairPath = flag.String("signer-keypair", "", "path to the signer keypair file (JSON array format)") slackWebhookURL = flag.String("slack-webhook-url", "", "The Slack webhook URL to send alerts") provisioningSlotCount = flag.Uint64("provisioning-slot-count", defaultProvisioningSlotCount, "Burn-in slot count for new devices/links (~20 hours at 200000)") drainedSlotCount = flag.Uint64("drained-slot-count", defaultDrainedSlotCount, "Burn-in slot count for reactivated devices/links (~30 min at 5000)") @@ -109,9 +110,23 @@ func main() { } } + // Parse and validate signer keypair. + if *signerKeypairPath == "" { + log.Error("Missing required flag", "flag", "signer-keypair") + flag.Usage() + os.Exit(1) + } + signer, err := solana.PrivateKeyFromSolanaKeygenFile(*signerKeypairPath) + if err != nil { + log.Error("Failed to load signer keypair", "path", *signerKeypairPath, "error", err) + os.Exit(1) + } + log.Info("Signer public key", "pubkey", signer.PublicKey().String()) + // Initialize ledger clients. rpcClient := solanarpc.New(networkConfig.LedgerPublicRPCURL) serviceabilityClient := serviceability.New(rpcClient, networkConfig.ServiceabilityProgramID) + serviceabilityExecutor := serviceability.NewExecutor(log, rpcClient, &signer, networkConfig.ServiceabilityProgramID) telemetryClient := telemetry.New(log, rpcClient, nil, networkConfig.TelemetryProgramID) worker.MetricBuildInfo.WithLabelValues(version, commit, date).Set(1) @@ -129,15 +144,17 @@ func main() { }() w, err := worker.New(&worker.Config{ - Logger: log, - LedgerRPCClient: rpcClient, - Serviceability: serviceabilityClient, - Telemetry: telemetryClient, - Interval: *interval, - SlackWebhookURL: *slackWebhookURL, - Env: *env, - ProvisioningSlotCount: *provisioningSlotCount, - DrainedSlotCount: *drainedSlotCount, + Logger: log, + LedgerRPCClient: rpcClient, + Serviceability: serviceabilityClient, + ServiceabilityExecutor: serviceabilityExecutor, + ServiceabilityProgramID: networkConfig.ServiceabilityProgramID, + Telemetry: telemetryClient, + Interval: *interval, + SlackWebhookURL: *slackWebhookURL, + Env: *env, + ProvisioningSlotCount: *provisioningSlotCount, + DrainedSlotCount: *drainedSlotCount, }) if err != nil { log.Error("Failed to create worker", "error", err) diff --git a/controlplane/device-health-oracle/internal/worker/config.go b/controlplane/device-health-oracle/internal/worker/config.go index 40a493f8e..cdc336aa1 100644 --- a/controlplane/device-health-oracle/internal/worker/config.go +++ b/controlplane/device-health-oracle/internal/worker/config.go @@ -22,18 +22,25 @@ type ServiceabilityClient interface { GetProgramData(context.Context) (*serviceability.ProgramData, error) } +type ServiceabilityExecutor interface { + SetDeviceHealthBatch(ctx context.Context, updates []serviceability.DeviceHealthUpdate, globalStatePubkey solana.PublicKey) (solana.Signature, error) + SetLinkHealthBatch(ctx context.Context, updates []serviceability.LinkHealthUpdate, globalStatePubkey solana.PublicKey) (solana.Signature, error) +} + type TelemetryProgramClient interface { GetDeviceLatencySamples(ctx context.Context, originDevicePubKey, targetDevicePubKey, linkPubKey solana.PublicKey, epoch uint64) (*telemetry.DeviceLatencySamples, error) } type Config struct { - Logger *slog.Logger - LedgerRPCClient LedgerRPCClient - Serviceability ServiceabilityClient - Telemetry TelemetryProgramClient - Interval time.Duration - SlackWebhookURL string - Env string + Logger *slog.Logger + LedgerRPCClient LedgerRPCClient + Serviceability ServiceabilityClient + ServiceabilityExecutor ServiceabilityExecutor + ServiceabilityProgramID solana.PublicKey + Telemetry TelemetryProgramClient + Interval time.Duration + SlackWebhookURL string + Env string // Burn-in slot counts for devices/links. // ProvisioningSlotCount is used for new devices/links (status = Provisioning, DeviceProvisioning, LinkProvisioning). @@ -52,6 +59,12 @@ func (c *Config) Validate() error { if c.Serviceability == nil { return errors.New("serviceability client is required") } + if c.ServiceabilityExecutor == nil { + return errors.New("serviceability executor is required") + } + if c.ServiceabilityProgramID.IsZero() { + return errors.New("serviceability program ID is required") + } if c.Telemetry == nil { return errors.New("telemetry client is required") } diff --git a/controlplane/device-health-oracle/internal/worker/worker.go b/controlplane/device-health-oracle/internal/worker/worker.go index 701b2f29e..977ab955b 100644 --- a/controlplane/device-health-oracle/internal/worker/worker.go +++ b/controlplane/device-health-oracle/internal/worker/worker.go @@ -5,9 +5,24 @@ import ( "log/slog" "time" + "github.com/gagliardetto/solana-go" solanarpc "github.com/gagliardetto/solana-go/rpc" + "github.com/malbeclabs/doublezero/smartcontract/sdk/go/serviceability" ) +// Processing in batches greatly speeds up e2e tests since large numbers of link and devices records are created at the same time. +// We set maxBatchSize to 8 because Solana transactions are limited to 1232 bytes: +// Transaction overhead: +// - Blockhash: 32 bytes +// - Signatures: 64 bytes per signer (we have 1) +// - Message header: ~3 bytes +// - Account keys array (deduplicated) +// - Compact-u16 length prefixes +// +// Calculation: With the accounts being partially deduplicated (globalState, signer, and systemProgram are shared across all instructions), each additional instruction adds roughly ~100-120 bytes. +// With transaction overhead of ~200-300 bytes, we can fit approximately 8-10 instructions before hitting Solana's 1232-byte limit. +const maxBatchSize = 8 + type Worker struct { log *slog.Logger cfg *Config @@ -48,27 +63,142 @@ func (w *Worker) tick(ctx context.Context) { return } - provisioningSlot := currentSlot - w.cfg.ProvisioningSlotCount - drainedSlot := currentSlot - w.cfg.DrainedSlotCount + // Calculate burn-in slots, handling underflow for recently created environments + var provisioningSlot, drainedSlot uint64 + if currentSlot > w.cfg.ProvisioningSlotCount { + provisioningSlot = currentSlot - w.cfg.ProvisioningSlotCount + } + if currentSlot > w.cfg.DrainedSlotCount { + drainedSlot = currentSlot - w.cfg.DrainedSlotCount + } - provisioningTime, err := w.cfg.LedgerRPCClient.GetBlockTime(ctx, provisioningSlot) + w.log.Info("Device health oracle tick", + "currentSlot", currentSlot, + "provisioningSlotCount", w.cfg.ProvisioningSlotCount, + "provisioningSlot", provisioningSlot, + "drainedSlotCount", w.cfg.DrainedSlotCount, + "drainedSlot", drainedSlot) + + programData, err := w.cfg.Serviceability.GetProgramData(ctx) if err != nil { - w.log.Error("Failed to get block time for provisioning slot", "slot", provisioningSlot, "error", err) + w.log.Error("Failed to get program data", "error", err) return } - drainedTime, err := w.cfg.LedgerRPCClient.GetBlockTime(ctx, drainedSlot) + globalStatePubkey, _, err := serviceability.GetGlobalStatePDA(w.cfg.ServiceabilityProgramID) if err != nil { - w.log.Error("Failed to get block time for drained slot", "slot", drainedSlot, "error", err) + w.log.Error("Failed to get globalstate PDA", "error", err) return } - w.log.Info("Device health oracle tick", - "currentSlot", currentSlot, - "provisioningSlotCount", w.cfg.ProvisioningSlotCount, - "provisioningSlot", provisioningSlot, - "provisioningTime", provisioningTime.Time(), - "drainedSlotCount", w.cfg.DrainedSlotCount, - "drainedSlot", drainedSlot, - "drainedTime", drainedTime.Time()) + w.updatePendingDeviceHealth(ctx, programData.Devices, globalStatePubkey) + w.updatePendingLinkHealth(ctx, programData.Links, globalStatePubkey) +} + +func (w *Worker) updatePendingDeviceHealth(ctx context.Context, devices []serviceability.Device, globalStatePubkey solana.PublicKey) { + w.log.Debug("Processing devices", "count", len(devices)) + + // Collect devices that need health updates + var updates []serviceability.DeviceHealthUpdate + for _, device := range devices { + devicePubkey := solana.PublicKeyFromBytes(device.PubKey[:]) + w.log.Debug("Device state", + "device", devicePubkey.String(), + "code", device.Code, + "status", device.Status, + "statusValue", int(device.Status), + "health", device.DeviceHealth, + "healthValue", int(device.DeviceHealth)) + + // Only update health for devices in a provisioning state + if device.Status != serviceability.DeviceStatusDeviceProvisioning && + device.Status != serviceability.DeviceStatusLinkProvisioning { + continue + } + + if device.DeviceHealth == serviceability.DeviceHealthReadyForUsers || + device.DeviceHealth == serviceability.DeviceHealthReadyForLinks { + continue + } + + updates = append(updates, serviceability.DeviceHealthUpdate{ + DevicePubkey: devicePubkey, + Health: serviceability.DeviceHealthReadyForUsers, + }) + w.log.Info("Queuing device health update", + "device", devicePubkey.String(), + "code", device.Code, + "status", device.Status.String()) + } + + if len(updates) == 0 { + return + } + + for i := 0; i < len(updates); i += maxBatchSize { + end := i + maxBatchSize + if end > len(updates) { + end = len(updates) + } + batch := updates[i:end] + + w.log.Info("Sending batched device health update", "batchSize", len(batch), "batchNum", i/maxBatchSize+1) + + sig, err := w.cfg.ServiceabilityExecutor.SetDeviceHealthBatch(ctx, batch, globalStatePubkey) + if err != nil { + w.log.Error("Failed to set device health batch", "error", err) + continue + } + + w.log.Info("Device health batch updated", "count", len(batch), "signature", sig.String()) + } +} + +func (w *Worker) updatePendingLinkHealth(ctx context.Context, links []serviceability.Link, globalStatePubkey solana.PublicKey) { + // Collect links that need health updates + var updates []serviceability.LinkHealthUpdate + for _, link := range links { + // Update health for links in provisioning or drained states + if link.Status != serviceability.LinkStatusProvisioning && + link.Status != serviceability.LinkStatusSoftDrained && + link.Status != serviceability.LinkStatusHardDrained { + continue + } + + if link.LinkHealth == serviceability.LinkHealthReadyForService { + continue + } + + linkPubkey := solana.PublicKeyFromBytes(link.PubKey[:]) + updates = append(updates, serviceability.LinkHealthUpdate{ + LinkPubkey: linkPubkey, + Health: serviceability.LinkHealthReadyForService, + }) + w.log.Info("Queuing link health update", + "link", linkPubkey.String(), + "code", link.Code, + "status", link.Status.String()) + } + + if len(updates) == 0 { + return + } + + for i := 0; i < len(updates); i += maxBatchSize { + end := i + maxBatchSize + if end > len(updates) { + end = len(updates) + } + batch := updates[i:end] + + w.log.Info("Sending batched link health update", "batchSize", len(batch), "batchNum", i/maxBatchSize+1) + + sig, err := w.cfg.ServiceabilityExecutor.SetLinkHealthBatch(ctx, batch, globalStatePubkey) + if err != nil { + w.log.Error("Failed to set link health batch", "error", err) + continue + } + + w.log.Info("Link health batch updated", "count", len(batch), "signature", sig.String()) + } } diff --git a/controlplane/monitor/internal/serviceability/events_test.go b/controlplane/monitor/internal/serviceability/events_test.go index b90ad2547..a37c1dc6f 100644 --- a/controlplane/monitor/internal/serviceability/events_test.go +++ b/controlplane/monitor/internal/serviceability/events_test.go @@ -68,7 +68,7 @@ func TestCompare(t *testing.T) { { name: "multiple event types", before: []serviceability.Device{newTestDevice("1", serviceability.DeviceStatusActivated), newTestDevice("2", serviceability.DeviceStatusPending)}, // 2 is removed - after: []serviceability.Device{newTestDevice("1", serviceability.DeviceStatusSuspended), newTestDevice("3", serviceability.DeviceStatusPending)}, // 1 is modified, 3 is added + after: []serviceability.Device{newTestDevice("1", serviceability.DeviceStatusDrained), newTestDevice("3", serviceability.DeviceStatusPending)}, // 1 is modified, 3 is added expectedAdded: 1, expectedRemoved: 1, expectedModified: 1, diff --git a/controlplane/telemetry/internal/telemetry/peers_test.go b/controlplane/telemetry/internal/telemetry/peers_test.go index bbbea24f8..7552d7081 100644 --- a/controlplane/telemetry/internal/telemetry/peers_test.go +++ b/controlplane/telemetry/internal/telemetry/peers_test.go @@ -362,7 +362,7 @@ func TestAgentTelemetry_PeerDiscovery_Ledger(t *testing.T) { requireUnorderedEqual(t, expected, peerDiscovery.GetPeers()) }) - t.Run("skips suspended and rejected links", func(t *testing.T) { + t.Run("skips deleting and rejected links", func(t *testing.T) { t.Parallel() log := log.With("test", t.Name()) @@ -378,8 +378,8 @@ func TestAgentTelemetry_PeerDiscovery_Ledger(t *testing.T) { }, Links: []serviceability.Link{ { - PubKey: stringToPubkey("suspended_link"), - Status: serviceability.LinkStatusSuspended, + PubKey: stringToPubkey("deleting_link"), + Status: serviceability.LinkStatusDeleting, SideAPubKey: localDevicePK, SideZPubKey: stringToPubkey("device2"), TunnelNet: [5]uint8{10, 1, 5, 0, 31}, diff --git a/e2e/docker/device-health-oracle/entrypoint.sh b/e2e/docker/device-health-oracle/entrypoint.sh index 060baf3ae..5238e72e3 100755 --- a/e2e/docker/device-health-oracle/entrypoint.sh +++ b/e2e/docker/device-health-oracle/entrypoint.sh @@ -14,6 +14,10 @@ if [ -z "${DZ_TELEMETRY_PROGRAM_ID}" ]; then echo "DZ_TELEMETRY_PROGRAM_ID is not set" exit 1 fi +if [ -z "${DZ_SIGNER_KEYPAIR}" ]; then + echo "DZ_SIGNER_KEYPAIR is not set" + exit 1 +fi # Start Alloy in background if ALLOY_PROMETHEUS_URL is set if [ -n "${ALLOY_PROMETHEUS_URL:-}" ]; then @@ -22,4 +26,4 @@ if [ -n "${ALLOY_PROMETHEUS_URL:-}" ]; then fi # start device-health-oracle -device-health-oracle -ledger-rpc-url ${DZ_LEDGER_URL} -serviceability-program-id ${DZ_SERVICEABILITY_PROGRAM_ID} -telemetry-program-id ${DZ_TELEMETRY_PROGRAM_ID} -metrics-addr ":2112" -interval ${DZ_INTERVAL:-1m} +device-health-oracle -ledger-rpc-url ${DZ_LEDGER_URL} -serviceability-program-id ${DZ_SERVICEABILITY_PROGRAM_ID} -telemetry-program-id ${DZ_TELEMETRY_PROGRAM_ID} -signer-keypair ${DZ_SIGNER_KEYPAIR} -metrics-addr ":2112" -interval ${DZ_INTERVAL:-1m} diff --git a/e2e/internal/devnet/device_health_oracle.go b/e2e/internal/devnet/device_health_oracle.go index 8244ece15..f8eed2f16 100644 --- a/e2e/internal/devnet/device_health_oracle.go +++ b/e2e/internal/devnet/device_health_oracle.go @@ -108,11 +108,19 @@ func (d *DeviceHealthOracle) Start(ctx context.Context) error { "DZ_SERVICEABILITY_PROGRAM_ID": d.dn.Manager.ServiceabilityProgramID, "DZ_TELEMETRY_PROGRAM_ID": d.dn.Manager.TelemetryProgramID, "DZ_INTERVAL": d.dn.Spec.DeviceHealthOracle.Interval.String(), + "DZ_SIGNER_KEYPAIR": containerSolanaKeypairPath, } if d.dn.Prometheus != nil && d.dn.Prometheus.InternalURL != "" { env["ALLOY_PROMETHEUS_URL"] = d.dn.Prometheus.InternalRemoteWriteURL() } + containerFiles := []testcontainers.ContainerFile{ + { + HostFilePath: d.dn.Spec.Manager.ManagerKeypairPath, + ContainerFilePath: containerSolanaKeypairPath, + }, + } + req := testcontainers.ContainerRequest{ Image: d.dn.Spec.DeviceHealthOracle.ContainerImage, Name: d.dockerContainerName(), @@ -129,6 +137,7 @@ func (d *DeviceHealthOracle) Start(ctx context.Context) error { Memory: defaultContainerMemory, }, Labels: d.dn.labels, + Files: containerFiles, } container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, diff --git a/e2e/link_onchain_allocation_test.go b/e2e/link_onchain_allocation_test.go index 5b763fe86..925e8ad3b 100644 --- a/e2e/link_onchain_allocation_test.go +++ b/e2e/link_onchain_allocation_test.go @@ -275,8 +275,7 @@ func TestE2E_Link_OnchainAllocation(t *testing.T) { require.NoError(t, err) // Wait for link to transition to Deleting status - // Note: Go SDK uses LinkStatusDeleting (value 3) which corresponds to Rust's Deleting status - log.Debug("==> Waiting for link to transition to Deleting") + log.Info("==> Waiting for link to transition to Deleting") require.Eventually(t, func() bool { client, err := dn.Ledger.GetServiceabilityClient() if err != nil { @@ -288,7 +287,6 @@ func TestE2E_Link_OnchainAllocation(t *testing.T) { } for _, link := range data.Links { if link.Code == "test-dz01:test-dz02" { - // LinkStatusDeleting in Go SDK = Deleting in Rust (value 3) if link.Status == serviceability.LinkStatusDeleting { return true } diff --git a/rfcs/rfc12-network-provisioning.md b/rfcs/rfc12-network-provisioning.md index e61844953..ff25fc51e 100644 --- a/rfcs/rfc12-network-provisioning.md +++ b/rfcs/rfc12-network-provisioning.md @@ -445,10 +445,9 @@ pub enum DeviceHealth { #### - serviceability: add device.desired_status ``` pub enum DeviceDesiredStatus { - Unknown = 0, + Pending = 0, Activated = 1, - Drained = 2, - Deleted = 3, + Drained = 6, } ``` diff --git a/smartcontract/cli/src/globalconfig/authority/get.rs b/smartcontract/cli/src/globalconfig/authority/get.rs index 9577a6c0b..2d47afe9d 100644 --- a/smartcontract/cli/src/globalconfig/authority/get.rs +++ b/smartcontract/cli/src/globalconfig/authority/get.rs @@ -15,7 +15,9 @@ pub struct AuthorityDisplay { #[serde(serialize_with = "serializer::serialize_pubkey_as_string")] pub activator_authority: Pubkey, #[serde(serialize_with = "serializer::serialize_pubkey_as_string")] - pub access_authority: Pubkey, + pub sentinel_authority: Pubkey, + #[serde(serialize_with = "serializer::serialize_pubkey_as_string")] + pub health_oracle: Pubkey, } impl GetAuthorityCliCommand { @@ -24,7 +26,8 @@ impl GetAuthorityCliCommand { let config_display = AuthorityDisplay { activator_authority: gstate.activator_authority_pk, - access_authority: gstate.sentinel_authority_pk, + sentinel_authority: gstate.sentinel_authority_pk, + health_oracle: gstate.health_oracle_pk, }; let config_displays = vec![config_display]; let table = Table::new(config_displays) diff --git a/smartcontract/sdk/go/serviceability/influx_test.go b/smartcontract/sdk/go/serviceability/influx_test.go index 3948c6571..24994b916 100644 --- a/smartcontract/sdk/go/serviceability/influx_test.go +++ b/smartcontract/sdk/go/serviceability/influx_test.go @@ -147,6 +147,7 @@ func TestToLineProtocol(t *testing.T) { SideAIfaceName: "xe-0/0/0", SideZIfaceName: "xe-0/0/1", DelayOverrideNs: 2000000, + LinkHealth: LinkHealthPending, PubKey: pubKey1, }, ts: ts, @@ -178,6 +179,7 @@ func TestToLineProtocol(t *testing.T) { Code: "link-empty", ContributorPubKey: pubKey1Uint8, DelayOverrideNs: 0, + LinkHealth: LinkHealthPending, PubKey: pubKey1, }, ts: ts, diff --git a/smartcontract/sdk/go/serviceability/state.go b/smartcontract/sdk/go/serviceability/state.go index e6155ef89..e4819c4da 100644 --- a/smartcontract/sdk/go/serviceability/state.go +++ b/smartcontract/sdk/go/serviceability/state.go @@ -121,27 +121,34 @@ func (d DeviceDeviceType) String() string { type DeviceStatus uint8 const ( - DeviceStatusPending DeviceStatus = iota - DeviceStatusActivated - DeviceStatusSuspended - DeviceStatusDeleted - DeviceStatusRejected - DeviceStatusDrained - DeviceStatusDeviceProvisioning - DeviceStatusLinkProvisioning + DeviceStatusPending DeviceStatus = 0 + DeviceStatusActivated DeviceStatus = 1 + DeviceStatusDeleting DeviceStatus = 3 + DeviceStatusRejected DeviceStatus = 4 + DeviceStatusDrained DeviceStatus = 5 + DeviceStatusDeviceProvisioning DeviceStatus = 6 + DeviceStatusLinkProvisioning DeviceStatus = 7 ) func (d DeviceStatus) String() string { - return [...]string{ - "pending", - "activated", - "suspended", - "deleted", - "rejected", - "drained", - "device-provisioning", - "link-provisioning", - }[d] + switch d { + case DeviceStatusPending: + return "pending" + case DeviceStatusActivated: + return "activated" + case DeviceStatusDeleting: + return "deleting" + case DeviceStatusRejected: + return "rejected" + case DeviceStatusDrained: + return "drained" + case DeviceStatusDeviceProvisioning: + return "device-provisioning" + case DeviceStatusLinkProvisioning: + return "link-provisioning" + default: + return "unknown" + } } func (d DeviceStatus) IsDrained() bool { @@ -155,21 +162,28 @@ func (d DeviceStatus) MarshalJSON() ([]byte, error) { type DeviceHealth uint8 const ( - DeviceHealthUnknown DeviceHealth = iota - DeviceHealthPending // 1 - DeviceHealthReadyForLinks // 2 - DeviceHealthReadyForUsers // 3 - DeviceHealthImpaired // 4 + DeviceHealthUnknown DeviceHealth = 0 + DeviceHealthPending DeviceHealth = 1 + DeviceHealthReadyForLinks DeviceHealth = 2 + DeviceHealthReadyForUsers DeviceHealth = 3 + DeviceHealthImpaired DeviceHealth = 4 ) func (d DeviceHealth) String() string { - return [...]string{ - "unknown", - "pending", - "ready_for_links", - "ready_for_users", - "impaired", - }[d] + switch d { + case DeviceHealthUnknown: + return "unknown" + case DeviceHealthPending: + return "pending" + case DeviceHealthReadyForLinks: + return "ready_for_links" + case DeviceHealthReadyForUsers: + return "ready_for_users" + case DeviceHealthImpaired: + return "impaired" + default: + return fmt.Sprintf("DeviceHealth(%d)", d) + } } func (d DeviceHealth) MarshalJSON() ([]byte, error) { @@ -471,29 +485,37 @@ func (l LinkLinkType) MarshalJSON() ([]byte, error) { type LinkStatus uint8 const ( - LinkStatusPending LinkStatus = iota - LinkStatusActivated - LinkStatusSuspended - LinkStatusDeleted - LinkStatusRejected - LinkStatusRequested - LinkStatusHardDrained - LinkStatusSoftDrained - LinkStatusProvisioning + LinkStatusPending LinkStatus = 0 + LinkStatusActivated LinkStatus = 1 + LinkStatusDeleting LinkStatus = 3 + LinkStatusRejected LinkStatus = 4 + LinkStatusRequested LinkStatus = 5 + LinkStatusHardDrained LinkStatus = 6 + LinkStatusSoftDrained LinkStatus = 7 + LinkStatusProvisioning LinkStatus = 8 ) func (l LinkStatus) String() string { - return [...]string{ - "pending", - "activated", - "suspended", - "deleted", - "rejected", - "requested", - "hard-drained", - "soft-drained", - "provisioning", - }[l] + switch l { + case LinkStatusPending: + return "pending" + case LinkStatusActivated: + return "activated" + case LinkStatusDeleting: + return "deleting" + case LinkStatusRejected: + return "rejected" + case LinkStatusRequested: + return "requested" + case LinkStatusHardDrained: + return "hard-drained" + case LinkStatusSoftDrained: + return "soft-drained" + case LinkStatusProvisioning: + return "provisioning" + default: + return "unknown" + } } // IsHardDrained returns true if the link status is hard-drained @@ -508,17 +530,25 @@ func (l LinkStatus) MarshalJSON() ([]byte, error) { type LinkHealth uint8 const ( - LinkHealthPending LinkHealth = iota - LinkHealthReadyForService - LinkHealthImpaired + LinkHealthUnknown LinkHealth = 0 + LinkHealthPending LinkHealth = 1 + LinkHealthReadyForService LinkHealth = 2 + LinkHealthImpaired LinkHealth = 3 ) func (l LinkHealth) String() string { - return [...]string{ - "pending", - "ready_for_service", - "impaired", - }[l] + switch l { + case LinkHealthUnknown: + return "unknown" + case LinkHealthPending: + return "pending" + case LinkHealthReadyForService: + return "ready_for_service" + case LinkHealthImpaired: + return "impaired" + default: + return fmt.Sprintf("LinkHealth(%d)", l) + } } func (l LinkHealth) MarshalJSON() ([]byte, error) { diff --git a/smartcontract/sdk/go/serviceability/state_test.go b/smartcontract/sdk/go/serviceability/state_test.go index d319fd189..530bffd39 100644 --- a/smartcontract/sdk/go/serviceability/state_test.go +++ b/smartcontract/sdk/go/serviceability/state_test.go @@ -46,6 +46,7 @@ func TestCustomJSONMarshal(t *testing.T) { SideAIfaceName: "Switch1/1/1", SideZIfaceName: "Switch1/1/1", DelayOverrideNs: 10, + LinkHealth: serviceability.LinkHealthPending, PubKey: dummyPubKey, }, expected: `{ @@ -96,6 +97,7 @@ func TestCustomJSONMarshal(t *testing.T) { SideAIfaceName: "Edge1/0/0", SideZIfaceName: "Edge2/0/0", DelayOverrideNs: 0, + LinkHealth: serviceability.LinkHealthPending, PubKey: dummyPubKey, }, expected: `{ @@ -137,7 +139,7 @@ func TestCustomJSONMarshal(t *testing.T) { "SideAPubKey": "11111111111111111111111111111111", "SideZPubKey": "11111111111111111111111111111111", "LinkDesiredStatus":"pending", - "LinkHealth":"pending", + "LinkHealth":"unknown", "LinkType": "", "Bandwidth": 0, "Mtu": 0,