266 lines
9.2 KiB
Go
266 lines
9.2 KiB
Go
package history
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func diffComponentStates(before, after ComponentStateV1) ([]PatchOp, error) {
|
|
var ops []PatchOp
|
|
ops = appendPatchStringPtr(ops, "/identity/vendor_serial", strPtr(before.Identity.VendorSerial), strPtr(after.Identity.VendorSerial), true)
|
|
ops = appendPatchStringPtr(ops, "/identity/vendor", before.Identity.Vendor, after.Identity.Vendor, false)
|
|
ops = appendPatchStringPtr(ops, "/identity/model", before.Identity.Model, after.Identity.Model, false)
|
|
ops = appendPatchStringPtr(ops, "/runtime/firmware_version", before.Runtime.FirmwareVersion, after.Runtime.FirmwareVersion, false)
|
|
ops = appendPatchStringPtr(ops, "/runtime/health_status", before.Runtime.HealthStatus, after.Runtime.HealthStatus, false)
|
|
ops = appendPatchStringPtr(ops, "/runtime/health_status_at", before.Runtime.HealthStatusAt, after.Runtime.HealthStatusAt, false)
|
|
ops = appendPatchStringPtr(ops, "/installation/current_machine_id", before.Installation.CurrentMachineID, after.Installation.CurrentMachineID, false)
|
|
ops = appendPatchStringPtr(ops, "/installation/installed_at", before.Installation.InstalledAt, after.Installation.InstalledAt, false)
|
|
ops = appendPatchStringPtr(ops, "/installation/slot_name", before.Installation.SlotName, after.Installation.SlotName, false)
|
|
ops = appendPatchStringPtr(ops, "/metadata/first_seen_at", before.Metadata.FirstSeenAt, after.Metadata.FirstSeenAt, false)
|
|
ops = appendPatchStringPtr(ops, "/metadata/component_type", before.Metadata.ComponentType, after.Metadata.ComponentType, false)
|
|
return ops, nil
|
|
}
|
|
|
|
func diffAssetStates(before, after AssetStateV1) ([]PatchOp, error) {
|
|
var ops []PatchOp
|
|
ops = appendPatchStringPtr(ops, "/identity/name", strPtr(before.Identity.Name), strPtr(after.Identity.Name), true)
|
|
ops = appendPatchStringPtr(ops, "/identity/vendor_serial", strPtr(before.Identity.VendorSerial), strPtr(after.Identity.VendorSerial), true)
|
|
ops = appendPatchStringPtr(ops, "/identity/vendor", before.Identity.Vendor, after.Identity.Vendor, false)
|
|
ops = appendPatchStringPtr(ops, "/identity/model", before.Identity.Model, after.Identity.Model, false)
|
|
ops = appendPatchStringPtr(ops, "/identity/machine_tag", before.Identity.MachineTag, after.Identity.MachineTag, false)
|
|
for key, beforeValue := range before.Firmware.Devices {
|
|
afterValue, ok := after.Firmware.Devices[key]
|
|
if !ok {
|
|
ops = append(ops, PatchOp{Op: "remove", Path: "/firmware/devices/" + escapeJSONPointer(key)})
|
|
continue
|
|
}
|
|
if beforeValue != afterValue {
|
|
ops = append(ops, PatchOp{Op: "replace", Path: "/firmware/devices/" + escapeJSONPointer(key), Value: afterValue})
|
|
}
|
|
}
|
|
for key, afterValue := range after.Firmware.Devices {
|
|
if _, ok := before.Firmware.Devices[key]; ok {
|
|
continue
|
|
}
|
|
ops = append(ops, PatchOp{Op: "add", Path: "/firmware/devices/" + escapeJSONPointer(key), Value: afterValue})
|
|
}
|
|
if !equalStringSlices(before.Inventory.InstalledComponentIDs, after.Inventory.InstalledComponentIDs) {
|
|
ops = append(ops, PatchOp{Op: "replace", Path: "/inventory/installed_component_ids", Value: after.Inventory.InstalledComponentIDs})
|
|
}
|
|
return ops, nil
|
|
}
|
|
|
|
func appendPatchStringPtr(ops []PatchOp, path string, before, after *string, required bool) []PatchOp {
|
|
b := ""
|
|
a := ""
|
|
bok := false
|
|
aok := false
|
|
if before != nil {
|
|
b = strings.TrimSpace(*before)
|
|
if b != "" || required {
|
|
bok = true
|
|
}
|
|
}
|
|
if after != nil {
|
|
a = strings.TrimSpace(*after)
|
|
if a != "" || required {
|
|
aok = true
|
|
}
|
|
}
|
|
switch {
|
|
case bok && aok && b == a:
|
|
return ops
|
|
case !bok && !aok:
|
|
return ops
|
|
case !aok:
|
|
return append(ops, PatchOp{Op: "remove", Path: path})
|
|
case !bok:
|
|
return append(ops, PatchOp{Op: "add", Path: path, Value: a})
|
|
default:
|
|
return append(ops, PatchOp{Op: "replace", Path: path, Value: a})
|
|
}
|
|
}
|
|
|
|
func strPtr(v string) *string {
|
|
s := strings.TrimSpace(v)
|
|
return &s
|
|
}
|
|
|
|
func escapeJSONPointer(value string) string {
|
|
value = strings.ReplaceAll(value, "~", "~0")
|
|
value = strings.ReplaceAll(value, "/", "~1")
|
|
return value
|
|
}
|
|
|
|
func (s *Service) insertComponentTimelineFromStates(ctx context.Context, tx *sql.Tx, componentID, changeType, sourceType string, eventTime time.Time, logicalEntityType, logicalEventID string, correlationID *string, before, after *ComponentStateV1) error {
|
|
eventType, firmware := componentTimelineProjection(changeType, before, after)
|
|
if eventType == "" {
|
|
return nil
|
|
}
|
|
slotForEvent := timelineSlotFromComponentStates(changeType, before, after)
|
|
var machineID *string
|
|
if after != nil && after.Installation.CurrentMachineID != nil {
|
|
machineID = after.Installation.CurrentMachineID
|
|
} else if before != nil && before.Installation.CurrentMachineID != nil {
|
|
machineID = before.Installation.CurrentMachineID
|
|
}
|
|
if isPairedComponentTimelineChange(changeType) && machineID != nil {
|
|
if err := s.insertTimelineProjectionRawWithSlot(ctx, tx, "asset", *machineID, eventType, sourceType, eventTime, logicalEntityType, logicalEventID, correlationID, machineID, &componentID, firmware, slotForEvent); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return s.insertTimelineProjectionRawWithSlot(ctx, tx, "component", componentID, eventType, sourceType, eventTime, logicalEntityType, logicalEventID, correlationID, machineID, &componentID, firmware, slotForEvent)
|
|
}
|
|
|
|
func (s *Service) insertAssetTimelineFromStates(ctx context.Context, tx *sql.Tx, assetID, changeType, sourceType string, eventTime time.Time, logicalEntityType, logicalEventID string, correlationID *string, before, after *AssetStateV1) error {
|
|
eventType, firmware := assetTimelineProjection(changeType, before, after)
|
|
if eventType == "" {
|
|
return nil
|
|
}
|
|
return s.insertTimelineProjectionRaw(ctx, tx, "asset", assetID, eventType, sourceType, eventTime, logicalEntityType, logicalEventID, correlationID, &assetID, nil, firmware)
|
|
}
|
|
|
|
func componentTimelineProjection(changeType string, before, after *ComponentStateV1) (string, *string) {
|
|
switch strings.TrimSpace(changeType) {
|
|
case "COMPONENT_STATUS_SET":
|
|
if after == nil || after.Runtime.HealthStatus == nil {
|
|
return "", nil
|
|
}
|
|
switch *after.Runtime.HealthStatus {
|
|
case "OK":
|
|
return "COMPONENT_OK", nil
|
|
case "UNKNOWN":
|
|
return "COMPONENT_UNKNOWN", nil
|
|
case "WARNING":
|
|
return "COMPONENT_WARNING", nil
|
|
case "FAILED":
|
|
return "COMPONENT_FAILED", nil
|
|
default:
|
|
return "", nil
|
|
}
|
|
case "COMPONENT_FIRMWARE_SET":
|
|
if after == nil || after.Runtime.FirmwareVersion == nil {
|
|
return "", nil
|
|
}
|
|
current := strings.TrimSpace(*after.Runtime.FirmwareVersion)
|
|
if current == "" {
|
|
return "", nil
|
|
}
|
|
if before == nil || before.Runtime.FirmwareVersion == nil || strings.TrimSpace(*before.Runtime.FirmwareVersion) == "" {
|
|
v := fmt.Sprintf("- -> %s", current)
|
|
return "FIRMWARE_INSTALLED", &v
|
|
}
|
|
if strings.TrimSpace(*before.Runtime.FirmwareVersion) == current {
|
|
return "", nil
|
|
}
|
|
v := current
|
|
return "FIRMWARE_CHANGED", &v
|
|
case "COMPONENT_INSTALLED":
|
|
return "INSTALLED", nil
|
|
case "COMPONENT_REMOVED":
|
|
return "REMOVED", nil
|
|
case "COMPONENT_REGISTRY_UPDATED":
|
|
return "COMPONENT_UPDATED", nil
|
|
case "COMPONENT_ROLLBACK_APPLIED":
|
|
return "ROLLBACK_APPLIED", nil
|
|
case "COMPONENT_FIRST_SEEN_CORRECTED":
|
|
return "", nil
|
|
default:
|
|
return "CHANGE_APPLIED", nil
|
|
}
|
|
}
|
|
|
|
func assetTimelineProjection(changeType string, before, after *AssetStateV1) (string, *string) {
|
|
switch strings.TrimSpace(changeType) {
|
|
case "ASSET_FIRMWARE_DEVICE_SET":
|
|
if after == nil {
|
|
return "", nil
|
|
}
|
|
// If exactly one device changed, capture its version for the timeline payload.
|
|
changed := 0
|
|
var last string
|
|
beforeDevices := map[string]string{}
|
|
if before != nil {
|
|
beforeDevices = before.Firmware.Devices
|
|
}
|
|
for name, version := range after.Firmware.Devices {
|
|
if beforeDevices[name] != version {
|
|
changed++
|
|
last = version
|
|
}
|
|
}
|
|
for name := range beforeDevices {
|
|
if _, ok := after.Firmware.Devices[name]; !ok {
|
|
changed++
|
|
}
|
|
}
|
|
if changed == 0 {
|
|
return "", nil
|
|
}
|
|
if strings.TrimSpace(last) != "" {
|
|
return "FIRMWARE_CHANGED", &last
|
|
}
|
|
return "FIRMWARE_CHANGED", nil
|
|
case "ASSET_REGISTRY_UPDATED":
|
|
return "ASSET_UPDATED", nil
|
|
case "ASSET_LOG_COLLECTED":
|
|
return "LOG_COLLECTED", nil
|
|
case "ASSET_COMPONENT_SET_CHANGED":
|
|
return "", nil
|
|
case "ASSET_ROLLBACK_APPLIED":
|
|
return "ROLLBACK_APPLIED", nil
|
|
default:
|
|
return "CHANGE_APPLIED", nil
|
|
}
|
|
}
|
|
|
|
func isPairedComponentTimelineChange(changeType string) bool {
|
|
switch strings.TrimSpace(changeType) {
|
|
case "COMPONENT_STATUS_SET", "COMPONENT_FIRMWARE_SET", "COMPONENT_INSTALLED", "COMPONENT_REMOVED":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func timelineSlotFromComponentStates(changeType string, before, after *ComponentStateV1) *string {
|
|
switch strings.TrimSpace(changeType) {
|
|
case "COMPONENT_REMOVED":
|
|
if before != nil {
|
|
return before.Installation.SlotName
|
|
}
|
|
return nil
|
|
case "COMPONENT_INSTALLED":
|
|
if after != nil && after.Installation.SlotName != nil {
|
|
return after.Installation.SlotName
|
|
}
|
|
if before != nil {
|
|
return before.Installation.SlotName
|
|
}
|
|
return nil
|
|
default:
|
|
if after != nil && after.Installation.SlotName != nil {
|
|
return after.Installation.SlotName
|
|
}
|
|
if before != nil {
|
|
return before.Installation.SlotName
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func equalStringSlices(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|