diff --git a/diode-server/docker/sample.env b/diode-server/docker/sample.env index 6fa140b1..d954a4df 100644 --- a/diode-server/docker/sample.env +++ b/diode-server/docker/sample.env @@ -5,7 +5,7 @@ REDIS_HOST=diode-redis REDIS_PORT=6378 RECONCILER_GRPC_HOST=diode-reconciler RECONCILER_GRPC_PORT=8081 -NETBOX_DIODE_PLUGIN_API_BASE_URL=http://192.168.1.3:8000/netbox/api/plugins/diode +NETBOX_DIODE_PLUGIN_API_BASE_URL=http://NETBOX_HOST/api/plugins/diode DIODE_TO_NETBOX_API_KEY=1368dbad13e418d5a443d93cf255edde03a2a754 NETBOX_TO_DIODE_API_KEY=1e99338b8cab5fc637bc55f390bda1446f619c42 DIODE_API_KEY=5a52c45ee8231156cb620d193b0291912dd15433 diff --git a/diode-server/netbox/dcim.go b/diode-server/netbox/dcim.go index b5c2ba09..7be5298a 100644 --- a/diode-server/netbox/dcim.go +++ b/diode-server/netbox/dcim.go @@ -1,5 +1,147 @@ package netbox +import ( + "errors" + "fmt" + + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" +) + +var ( + // ErrInvalidInterfaceType is returned when the interface type is invalid + ErrInvalidInterfaceType = errors.New("invalid interface type") + + // ErrInvalidInterfaceMode is returned when the interface mode is invalid + ErrInvalidInterfaceMode = errors.New("invalid interface mode") + + // DefaultInterfaceType is the default interface type + DefaultInterfaceType = "other" + + interfaceTypesMap = map[string]struct{}{ + "virtual": {}, + "bridge": {}, + "lag": {}, + "100base-fx": {}, + "100base-lfx": {}, + "100base-tx": {}, + "100base-t1": {}, + "1000base-t": {}, + "1000base-x-gbic": {}, + "1000base-x-sfp": {}, + "2.5gbase-t": {}, + "5gbase-t": {}, + "10gbase-t": {}, + "10gbase-cx4": {}, + "10gbase-x-sfpp": {}, + "10gbase-x-xfp": {}, + "10gbase-x-xenpak": {}, + "10gbase-x-x2": {}, + "25gbase-x-sfp28": {}, + "50gbase-x-sfp56": {}, + "40gbase-x-qsfpp": {}, + "50gbase-x-sfp28": {}, + "100gbase-x-cfp": {}, + "100gbase-x-cfp2": {}, + "100gbase-x-cfp4": {}, + "100gbase-x-cxp": {}, + "100gbase-x-cpak": {}, + "100gbase-x-dsfp": {}, + "100gbase-x-sfpdd": {}, + "100gbase-x-qsfp28": {}, + "100gbase-x-qsfpdd": {}, + "200gbase-x-cfp2": {}, + "200gbase-x-qsfp56": {}, + "200gbase-x-qsfpdd": {}, + "400gbase-x-cfp2": {}, + "400gbase-x-qsfp112": {}, + "400gbase-x-qsfpdd": {}, + "400gbase-x-osfp": {}, + "400gbase-x-osfp-rhs": {}, + "400gbase-x-cdfp": {}, + "400gbase-x-cfp8": {}, + "800gbase-x-qsfpdd": {}, + "800gbase-x-osfp": {}, + "1000base-kx": {}, + "10gbase-kr": {}, + "10gbase-kx4": {}, + "25gbase-kr": {}, + "40gbase-kr4": {}, + "50gbase-kr": {}, + "100gbase-kp4": {}, + "100gbase-kr2": {}, + "100gbase-kr4": {}, + "ieee802.11a": {}, + "ieee802.11g": {}, + "ieee802.11n": {}, + "ieee802.11ac": {}, + "ieee802.11ad": {}, + "ieee802.11ax": {}, + "ieee802.11ay": {}, + "ieee802.15.1": {}, + "other-wireless": {}, + "gsm": {}, + "cdma": {}, + "lte": {}, + "sonet-oc3": {}, + "sonet-oc12": {}, + "sonet-oc48": {}, + "sonet-oc192": {}, + "sonet-oc768": {}, + "sonet-oc1920": {}, + "sonet-oc3840": {}, + "1gfc-sfp": {}, + "2gfc-sfp": {}, + "4gfc-sfp": {}, + "8gfc-sfpp": {}, + "16gfc-sfpp": {}, + "32gfc-sfp28": {}, + "64gfc-qsfpp": {}, + "128gfc-qsfp28": {}, + "infiniband-sdr": {}, + "infiniband-ddr": {}, + "infiniband-qdr": {}, + "infiniband-fdr10": {}, + "infiniband-fdr": {}, + "infiniband-edr": {}, + "infiniband-hdr": {}, + "infiniband-ndr": {}, + "infiniband-xdr": {}, + "t1": {}, + "e1": {}, + "t3": {}, + "e3": {}, + "xdsl": {}, + "docsis": {}, + "gpon": {}, + "xg-pon": {}, + "xgs-pon": {}, + "ng-pon2": {}, + "epon": {}, + "10g-epon": {}, + "cisco-stackwise": {}, + "cisco-stackwise-plus": {}, + "cisco-flexstack": {}, + "cisco-flexstack-plus": {}, + "cisco-stackwise-80": {}, + "cisco-stackwise-160": {}, + "cisco-stackwise-320": {}, + "cisco-stackwise-480": {}, + "cisco-stackwise-1t": {}, + "juniper-vcp": {}, + "extreme-summitstack": {}, + "extreme-summitstack-128": {}, + "extreme-summitstack-256": {}, + "extreme-summitstack-512": {}, + "other": {}, + } + + interfaceModesMap = map[string]struct{}{ + "access": {}, + "tagged": {}, + "tagged-all": {}, + } +) + const ( // DcimDeviceObjectType represents the DCIM device object type DcimDeviceObjectType = "dcim.device" @@ -198,3 +340,258 @@ func NewDcimDevice() *DcimDevice { Status: &status, } } + +// FromProtoDeviceEntity converts a diode device entity to a DCIM device +func FromProtoDeviceEntity(entity *diodepb.Entity) (*DcimDevice, error) { + if entity == nil || entity.GetDevice() == nil { + return nil, fmt.Errorf("entity is nil or not a device") + } + + return FromProtoDevice(entity.GetDevice()), nil +} + +// FromProtoDevice converts a diode device to a DCIM device +func FromProtoDevice(devicePb *diodepb.Device) *DcimDevice { + if devicePb == nil { + return nil + } + + return &DcimDevice{ + Name: devicePb.Name, + Site: FromProtoSite(devicePb.Site), + Role: FromProtoRole(devicePb.Role), + DeviceType: FromProtoDeviceType(devicePb.DeviceType), + Platform: FromProtoPlatform(devicePb.Platform), + Serial: devicePb.Serial, + Description: devicePb.Description, + Status: (*DcimDeviceStatus)(&devicePb.Status), + AssetTag: devicePb.AssetTag, + PrimaryIPv4: nil, + PrimaryIPv6: nil, + Comments: devicePb.Comments, + Tags: FromProtoTags(devicePb.Tags), + } +} + +// FromProtoDeviceRoleEntity converts a diode device role entity to a DCIM device role +func FromProtoDeviceRoleEntity(entity *diodepb.Entity) (*DcimDeviceRole, error) { + if entity == nil || entity.GetDeviceRole() == nil { + return nil, fmt.Errorf("entity is nil or not a device role") + } + + return FromProtoRole(entity.GetDeviceRole()), nil +} + +// FromProtoDeviceTypeEntity converts a diode device type entity to a DCIM device type +func FromProtoDeviceTypeEntity(entity *diodepb.Entity) (*DcimDeviceType, error) { + if entity == nil || entity.GetDeviceType() == nil { + return nil, fmt.Errorf("entity is nil or not a device type") + } + + return FromProtoDeviceType(entity.GetDeviceType()), nil +} + +// FromProtoManufacturerEntity converts a diode manufacturer entity to a DCIM manufacturer +func FromProtoManufacturerEntity(entity *diodepb.Entity) (*DcimManufacturer, error) { + if entity == nil || entity.GetManufacturer() == nil { + return nil, fmt.Errorf("entity is nil or not a manufacturer") + } + + return FromProtoManufacturer(entity.GetManufacturer()), nil +} + +// FromProtoPlatformEntity converts a diode platform entity to a DCIM platform +func FromProtoPlatformEntity(entity *diodepb.Entity) (*DcimPlatform, error) { + if entity == nil || entity.GetPlatform() == nil { + return nil, fmt.Errorf("entity is nil or not a platform") + } + + return FromProtoPlatform(entity.GetPlatform()), nil +} + +// FromProtoSiteEntity converts a diode site entity to a DCIM site +func FromProtoSiteEntity(entity *diodepb.Entity) (*DcimSite, error) { + if entity == nil || entity.GetSite() == nil { + return nil, fmt.Errorf("entity is nil or not a site") + } + + return FromProtoSite(entity.GetSite()), nil +} + +// FromProtoSite converts a diode site to a DCIM site +func FromProtoSite(sitePb *diodepb.Site) *DcimSite { + if sitePb == nil { + return nil + } + + return &DcimSite{ + Name: sitePb.Name, + Slug: sitePb.Slug, + Status: (*DcimSiteStatus)(&sitePb.Status), + Facility: sitePb.Facility, + TimeZone: sitePb.TimeZone, + Description: sitePb.Description, + Comments: sitePb.Comments, + Tags: FromProtoTags(sitePb.Tags), + } +} + +// FromProtoRole converts a diode role to a DCIM device role +func FromProtoRole(rolePb *diodepb.Role) *DcimDeviceRole { + if rolePb == nil { + return nil + } + + var color *string + if rolePb.Color != "" { + color = &rolePb.Color + } + + return &DcimDeviceRole{ + Name: rolePb.Name, + Slug: rolePb.Slug, + Color: color, + Description: rolePb.Description, + Tags: FromProtoTags(rolePb.Tags), + } +} + +// FromProtoDeviceType converts a diode device type to a DCIM device type +func FromProtoDeviceType(deviceTypePb *diodepb.DeviceType) *DcimDeviceType { + if deviceTypePb == nil { + return nil + } + + return &DcimDeviceType{ + Model: deviceTypePb.Model, + Slug: deviceTypePb.Slug, + Manufacturer: FromProtoManufacturer(deviceTypePb.Manufacturer), + Description: deviceTypePb.Description, + Comments: deviceTypePb.Comments, + PartNumber: deviceTypePb.PartNumber, + Tags: FromProtoTags(deviceTypePb.Tags), + } +} + +// FromProtoManufacturer converts a diode manufacturer to a DCIM manufacturer +func FromProtoManufacturer(manufacturerPb *diodepb.Manufacturer) *DcimManufacturer { + if manufacturerPb == nil { + return nil + } + + return &DcimManufacturer{ + Name: manufacturerPb.Name, + Slug: manufacturerPb.Slug, + Description: manufacturerPb.Description, + Tags: FromProtoTags(manufacturerPb.Tags), + } +} + +// FromProtoPlatform converts a diode platform to a DCIM platform +func FromProtoPlatform(platformPb *diodepb.Platform) *DcimPlatform { + if platformPb == nil { + return nil + } + + return &DcimPlatform{ + Name: platformPb.Name, + Slug: platformPb.Slug, + Manufacturer: FromProtoManufacturer(platformPb.Manufacturer), + Description: platformPb.Description, + Tags: FromProtoTags(platformPb.Tags), + } +} + +// DcimInterface represents a DCIM interface +type DcimInterface struct { + ID int `json:"id,omitempty"` + Device *DcimDevice `json:"device,omitempty"` + Name string `json:"name,omitempty"` + Label *string `json:"label,omitempty"` + Type *string `json:"type,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + MTU *int `json:"mtu,omitempty"` + MACAddress *string `json:"mac_address,omitempty" mapstructure:"mac_address,omitempty"` + Speed *int `json:"speed,omitempty"` + WWN *string `json:"wwn,omitempty"` + MgmtOnly *bool `json:"mgmt_only,omitempty" mapstructure:"mgmt_only,omitempty"` + Description *string `json:"description,omitempty"` + MarkConnected *bool `json:"mark_connected,omitempty" mapstructure:"mark_connected,omitempty"` + Mode *string `json:"mode,omitempty"` + Tags []*Tag `json:"tags,omitempty"` +} + +func validateInterfaceType(t string) bool { + _, ok := interfaceTypesMap[t] + return ok +} + +func validateInterfaceMode(m string) bool { + if m == "" { + return true + } + _, ok := interfaceModesMap[m] + return ok +} + +// Validate checks if the DCIM interface is valid +func (i *DcimInterface) Validate() error { + if i.Type != nil && !validateInterfaceType(*i.Type) { + return ErrInvalidInterfaceType + } + if i.Mode != nil && !validateInterfaceMode(*i.Mode) { + return ErrInvalidInterfaceMode + } + return nil +} + +// NewDcimInterface creates a new DCIM interface placeholder +func NewDcimInterface() *DcimInterface { + return &DcimInterface{ + Device: NewDcimDevice(), + Name: "undefined", + } +} + +// FromProtoInterfaceEntity converts a diode interface entity to a DCIM interface +func FromProtoInterfaceEntity(entity *diodepb.Entity) (*DcimInterface, error) { + if entity == nil || entity.GetInterface() == nil { + return nil, fmt.Errorf("entity is nil or not an interface") + } + + return FromProtoInterface(entity.GetInterface()), nil +} + +// FromProtoInterface converts a diode interface to a DCIM interface +func FromProtoInterface(interfacePb *diodepb.Interface) *DcimInterface { + if interfacePb == nil { + return nil + } + + var interfaceType *string + if interfacePb.Type != "" { + interfaceType = &interfacePb.Type + } + + var mode *string + if interfacePb.Mode != "" { + mode = &interfacePb.Mode + } + + return &DcimInterface{ + Name: interfacePb.Name, + Device: FromProtoDevice(interfacePb.Device), + Label: interfacePb.Label, + Type: interfaceType, + Enabled: interfacePb.Enabled, + MTU: int32PtrToIntPtr(interfacePb.Mtu), + MACAddress: interfacePb.MacAddress, + Speed: int32PtrToIntPtr(interfacePb.Speed), + WWN: interfacePb.Wwn, + MgmtOnly: interfacePb.MgmtOnly, + Description: interfacePb.Description, + MarkConnected: interfacePb.MarkConnected, + Mode: mode, + Tags: FromProtoTags(interfacePb.Tags), + } +} diff --git a/diode-server/netbox/dcim_interface.go b/diode-server/netbox/dcim_interface.go deleted file mode 100644 index f0322531..00000000 --- a/diode-server/netbox/dcim_interface.go +++ /dev/null @@ -1,189 +0,0 @@ -package netbox - -import "errors" - -var ( - // ErrInvalidInterfaceType is returned when the interface type is invalid - ErrInvalidInterfaceType = errors.New("invalid interface type") - - // ErrInvalidInterfaceMode is returned when the interface mode is invalid - ErrInvalidInterfaceMode = errors.New("invalid interface mode") - - // DefaultInterfaceType is the default interface type - DefaultInterfaceType = "other" -) - -var interfaceTypesMap = map[string]struct{}{ - "virtual": {}, - "bridge": {}, - "lag": {}, - "100base-fx": {}, - "100base-lfx": {}, - "100base-tx": {}, - "100base-t1": {}, - "1000base-t": {}, - "1000base-x-gbic": {}, - "1000base-x-sfp": {}, - "2.5gbase-t": {}, - "5gbase-t": {}, - "10gbase-t": {}, - "10gbase-cx4": {}, - "10gbase-x-sfpp": {}, - "10gbase-x-xfp": {}, - "10gbase-x-xenpak": {}, - "10gbase-x-x2": {}, - "25gbase-x-sfp28": {}, - "50gbase-x-sfp56": {}, - "40gbase-x-qsfpp": {}, - "50gbase-x-sfp28": {}, - "100gbase-x-cfp": {}, - "100gbase-x-cfp2": {}, - "100gbase-x-cfp4": {}, - "100gbase-x-cxp": {}, - "100gbase-x-cpak": {}, - "100gbase-x-dsfp": {}, - "100gbase-x-sfpdd": {}, - "100gbase-x-qsfp28": {}, - "100gbase-x-qsfpdd": {}, - "200gbase-x-cfp2": {}, - "200gbase-x-qsfp56": {}, - "200gbase-x-qsfpdd": {}, - "400gbase-x-cfp2": {}, - "400gbase-x-qsfp112": {}, - "400gbase-x-qsfpdd": {}, - "400gbase-x-osfp": {}, - "400gbase-x-osfp-rhs": {}, - "400gbase-x-cdfp": {}, - "400gbase-x-cfp8": {}, - "800gbase-x-qsfpdd": {}, - "800gbase-x-osfp": {}, - "1000base-kx": {}, - "10gbase-kr": {}, - "10gbase-kx4": {}, - "25gbase-kr": {}, - "40gbase-kr4": {}, - "50gbase-kr": {}, - "100gbase-kp4": {}, - "100gbase-kr2": {}, - "100gbase-kr4": {}, - "ieee802.11a": {}, - "ieee802.11g": {}, - "ieee802.11n": {}, - "ieee802.11ac": {}, - "ieee802.11ad": {}, - "ieee802.11ax": {}, - "ieee802.11ay": {}, - "ieee802.15.1": {}, - "other-wireless": {}, - "gsm": {}, - "cdma": {}, - "lte": {}, - "sonet-oc3": {}, - "sonet-oc12": {}, - "sonet-oc48": {}, - "sonet-oc192": {}, - "sonet-oc768": {}, - "sonet-oc1920": {}, - "sonet-oc3840": {}, - "1gfc-sfp": {}, - "2gfc-sfp": {}, - "4gfc-sfp": {}, - "8gfc-sfpp": {}, - "16gfc-sfpp": {}, - "32gfc-sfp28": {}, - "64gfc-qsfpp": {}, - "128gfc-qsfp28": {}, - "infiniband-sdr": {}, - "infiniband-ddr": {}, - "infiniband-qdr": {}, - "infiniband-fdr10": {}, - "infiniband-fdr": {}, - "infiniband-edr": {}, - "infiniband-hdr": {}, - "infiniband-ndr": {}, - "infiniband-xdr": {}, - "t1": {}, - "e1": {}, - "t3": {}, - "e3": {}, - "xdsl": {}, - "docsis": {}, - "gpon": {}, - "xg-pon": {}, - "xgs-pon": {}, - "ng-pon2": {}, - "epon": {}, - "10g-epon": {}, - "cisco-stackwise": {}, - "cisco-stackwise-plus": {}, - "cisco-flexstack": {}, - "cisco-flexstack-plus": {}, - "cisco-stackwise-80": {}, - "cisco-stackwise-160": {}, - "cisco-stackwise-320": {}, - "cisco-stackwise-480": {}, - "cisco-stackwise-1t": {}, - "juniper-vcp": {}, - "extreme-summitstack": {}, - "extreme-summitstack-128": {}, - "extreme-summitstack-256": {}, - "extreme-summitstack-512": {}, - "other": {}, -} - -var interfaceModesMap = map[string]struct{}{ - "access": {}, - "tagged": {}, - "tagged-all": {}, -} - -// DcimInterface represents a DCIM interface -type DcimInterface struct { - ID int `json:"id,omitempty"` - Device *DcimDevice `json:"device,omitempty"` - Name string `json:"name,omitempty"` - Label *string `json:"label,omitempty"` - Type *string `json:"type,omitempty"` - Enabled *bool `json:"enabled,omitempty"` - MTU *int `json:"mtu,omitempty"` - MACAddress *string `json:"mac_address,omitempty" mapstructure:"mac_address,omitempty"` - Speed *int `json:"speed,omitempty"` - WWN *string `json:"wwn,omitempty"` - MgmtOnly *bool `json:"mgmt_only,omitempty" mapstructure:"mgmt_only,omitempty"` - Description *string `json:"description,omitempty"` - MarkConnected *bool `json:"mark_connected,omitempty" mapstructure:"mark_connected,omitempty"` - Mode *string `json:"mode,omitempty"` - Tags []*Tag `json:"tags,omitempty"` -} - -func validateInterfaceType(t string) bool { - _, ok := interfaceTypesMap[t] - return ok -} - -func validateInterfaceMode(m string) bool { - if m == "" { - return true - } - _, ok := interfaceModesMap[m] - return ok -} - -// Validate checks if the DCIM interface is valid -func (i *DcimInterface) Validate() error { - if i.Type != nil && !validateInterfaceType(*i.Type) { - return ErrInvalidInterfaceType - } - if i.Mode != nil && !validateInterfaceMode(*i.Mode) { - return ErrInvalidInterfaceMode - } - return nil -} - -// NewDcimInterface creates a new DCIM interface placeholder -func NewDcimInterface() *DcimInterface { - return &DcimInterface{ - Device: NewDcimDevice(), - Name: "undefined", - } -} diff --git a/diode-server/netbox/dcim_wrappers.go b/diode-server/netbox/dcim_wrappers.go new file mode 100644 index 00000000..f161db22 --- /dev/null +++ b/diode-server/netbox/dcim_wrappers.go @@ -0,0 +1,1864 @@ +package netbox + +import ( + "errors" + "fmt" + "slices" + + "github.com/gosimple/slug" + "github.com/mitchellh/hashstructure/v2" + + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" +) + +// DcimDeviceDataWrapper represents a DCIM device data wrapper +type DcimDeviceDataWrapper struct { + BaseDataWrapper + Device *DcimDevice +} + +func (*DcimDeviceDataWrapper) comparableData() {} + +// FromProtoEntity sets the data from a proto entity +func (dw *DcimDeviceDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + device, err := FromProtoDeviceEntity(entity) + if err != nil { + return err + } + dw.Device = device + return nil +} + +// Data returns the Device +func (dw *DcimDeviceDataWrapper) Data() any { + return dw.Device +} + +// IsValid returns true if the Device is not nil +func (dw *DcimDeviceDataWrapper) IsValid() bool { + if dw.Device != nil && !dw.hasParent && dw.Device.Name == "" { + dw.Device = nil + } + return dw.Device != nil +} + +// Normalise normalises the data +func (dw *DcimDeviceDataWrapper) Normalise() { + if dw.IsValid() && dw.Device.Tags != nil && len(dw.Device.Tags) == 0 { + dw.Device.Tags = nil + } + dw.intended = true +} + +// NestedObjects returns all nested objects +func (dw *DcimDeviceDataWrapper) NestedObjects() ([]ComparableData, error) { + if len(dw.nestedObjects) > 0 { + return dw.nestedObjects, nil + } + + if dw.Device != nil && dw.hasParent && dw.Device.Name == "" { + dw.Device = nil + } + + objects := make([]ComparableData, 0) + + if dw.Device == nil && dw.intended { + return objects, nil + } + + if dw.Device == nil && dw.hasParent { + dw.Device = NewDcimDevice() + dw.placeholder = true + } + + // Ignore primary IP addresses for time being + dw.Device.PrimaryIPv4 = nil + dw.Device.PrimaryIPv6 = nil + + site := DcimSiteDataWrapper{Site: dw.Device.Site, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} + + so, err := site.NestedObjects() + if err != nil { + return nil, err + } + + objects = append(objects, so...) + + dw.Device.Site = site.Site + + if dw.Device.Platform != nil { + platform := DcimPlatformDataWrapper{Platform: dw.Device.Platform, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} + + po, err := platform.NestedObjects() + if err != nil { + return nil, err + } + + objects = append(objects, po...) + + dw.Device.Platform = platform.Platform + } + + deviceType := DcimDeviceTypeDataWrapper{DeviceType: dw.Device.DeviceType, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} + + dto, err := deviceType.NestedObjects() + if err != nil { + return nil, err + } + + objects = append(objects, dto...) + + dw.Device.DeviceType = deviceType.DeviceType + + deviceRole := DcimDeviceRoleDataWrapper{DeviceRole: dw.Device.Role, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} + + dro, err := deviceRole.NestedObjects() + if err != nil { + return nil, err + } + + objects = append(objects, dro...) + + dw.Device.Role = deviceRole.DeviceRole + + if dw.Device.Tags != nil { + for _, t := range dw.Device.Tags { + if t.Slug == "" { + t.Slug = slug.Make(t.Name) + } + objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + dw.nestedObjects = objects + + objects = append(objects, dw) + + return objects, nil +} + +// DataType returns the data type +func (dw *DcimDeviceDataWrapper) DataType() string { + return DcimDeviceObjectType +} + +// ObjectStateQueryParams returns the query parameters needed to retrieve its object state +func (dw *DcimDeviceDataWrapper) ObjectStateQueryParams() map[string]string { + params := map[string]string{ + "q": dw.Device.Name, + } + if dw.Device.Site != nil { + params["site__name"] = dw.Device.Site.Name + } + return params +} + +// ID returns the ID of the data +func (dw *DcimDeviceDataWrapper) ID() int { + return dw.Device.ID +} + +// Patch creates patches between the actual, intended and current data +func (dw *DcimDeviceDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { + intended, ok := cmp.(*DcimDeviceDataWrapper) + if !ok && intended != nil { + return nil, errors.New("invalid data type") + } + + actualNestedObjectsMap := make(map[string]ComparableData) + for _, obj := range dw.nestedObjects { + actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj + } + + actualSite := extractFromObjectsMap(actualNestedObjectsMap, fmt.Sprintf("%p", dw.Device.Site)) + intendedSite := extractFromObjectsMap(intendedNestedObjects, fmt.Sprintf("%p", dw.Device.Site)) + + actualPlatform := extractFromObjectsMap(actualNestedObjectsMap, fmt.Sprintf("%p", dw.Device.Platform)) + intendedPlatform := extractFromObjectsMap(intendedNestedObjects, fmt.Sprintf("%p", dw.Device.Platform)) + + actualDeviceType := extractFromObjectsMap(actualNestedObjectsMap, fmt.Sprintf("%p", dw.Device.DeviceType)) + intendedDeviceType := extractFromObjectsMap(intendedNestedObjects, fmt.Sprintf("%p", dw.Device.DeviceType)) + + actualRole := extractFromObjectsMap(actualNestedObjectsMap, fmt.Sprintf("%p", dw.Device.Role)) + intendedRole := extractFromObjectsMap(intendedNestedObjects, fmt.Sprintf("%p", dw.Device.Role)) + + reconciliationRequired := true + + if intended != nil { + currentNestedObjectsMap := make(map[string]ComparableData) + currentNestedObjects, err := intended.NestedObjects() + if err != nil { + return nil, err + } + for _, obj := range currentNestedObjects { + currentNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj + } + + dw.Device.ID = intended.Device.ID + dw.Device.Name = intended.Device.Name + + if dw.Device.Status == nil || *dw.Device.Status == "" { + dw.Device.Status = intended.Device.Status + } + + if dw.Device.Description == nil { + dw.Device.Description = intended.Device.Description + } + + if dw.Device.Comments == nil { + dw.Device.Comments = intended.Device.Comments + } + + if dw.Device.AssetTag == nil { + dw.Device.AssetTag = intended.Device.AssetTag + } + + if dw.Device.Serial == nil { + dw.Device.Serial = intended.Device.Serial + } + + if actualSite.IsPlaceholder() && intended.Device.Site != nil { + intendedSite = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Device.Site)) + } + + siteObjectsToReconcile, siteErr := actualSite.Patch(intendedSite, intendedNestedObjects) + if siteErr != nil { + return nil, siteErr + } + + site, err := copyData(actualSite.Data().(*DcimSite)) + if err != nil { + return nil, err + } + site.Tags = nil + + if !actualSite.HasChanged() { + site = &DcimSite{ + ID: actualSite.ID(), + } + + intendedSiteID := intendedSite.ID() + if intended.Device.Site != nil { + intendedSiteID = intended.Device.Site.ID + } + + intended.Device.Site = &DcimSite{ + ID: intendedSiteID, + } + } + + dw.Device.Site = site + + dw.objectsToReconcile = append(dw.objectsToReconcile, siteObjectsToReconcile...) + + if actualPlatform != nil { + if actualPlatform.IsPlaceholder() && intended.Device.Platform != nil { + intendedPlatform = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Device.Platform)) + } + + platformObjectsToReconcile, platformErr := actualPlatform.Patch(intendedPlatform, intendedNestedObjects) + if platformErr != nil { + return nil, platformErr + } + + platform, err := copyData(actualPlatform.Data().(*DcimPlatform)) + if err != nil { + return nil, err + } + platform.Tags = nil + + if !actualPlatform.HasChanged() { + platform = &DcimPlatform{ + ID: actualPlatform.ID(), + } + + intendedPlatformID := intendedPlatform.ID() + if intended.Device.Platform != nil { + intendedPlatformID = intended.Device.Platform.ID + } + + intended.Device.Platform = &DcimPlatform{ + ID: intendedPlatformID, + } + } + + dw.Device.Platform = platform + + dw.objectsToReconcile = append(dw.objectsToReconcile, platformObjectsToReconcile...) + } else { + if intended.Device.Platform != nil { + platformID := intended.Device.Platform.ID + dw.Device.Platform = &DcimPlatform{ + ID: platformID, + } + intended.Device.Platform = &DcimPlatform{ + ID: platformID, + } + } + } + + if actualDeviceType.IsPlaceholder() && intended.Device.DeviceType != nil { + intendedDeviceType = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Device.DeviceType)) + } + + deviceTypeObjectsToReconcile, deviceTypeErr := actualDeviceType.Patch(intendedDeviceType, intendedNestedObjects) + if deviceTypeErr != nil { + return nil, deviceTypeErr + } + + deviceType, err := copyData(actualDeviceType.Data().(*DcimDeviceType)) + if err != nil { + return nil, err + } + deviceType.Tags = nil + + if !actualDeviceType.HasChanged() { + deviceType = &DcimDeviceType{ + ID: actualDeviceType.ID(), + } + + intendedDeviceTypeID := intendedDeviceType.ID() + if intended.Device.DeviceType != nil { + intendedDeviceTypeID = intended.Device.DeviceType.ID + } + + intended.Device.DeviceType = &DcimDeviceType{ + ID: intendedDeviceTypeID, + } + } + + dw.Device.DeviceType = deviceType + + dw.objectsToReconcile = append(dw.objectsToReconcile, deviceTypeObjectsToReconcile...) + + if actualRole.IsPlaceholder() && intended.Device.Role != nil { + intendedRole = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Device.Role)) + } + + roleObjectsToReconcile, roleErr := actualRole.Patch(intendedRole, intendedNestedObjects) + if roleErr != nil { + return nil, roleErr + } + + role, err := copyData(actualRole.Data().(*DcimDeviceRole)) + if err != nil { + return nil, err + } + role.Tags = nil + + if !actualRole.HasChanged() { + role = &DcimDeviceRole{ + ID: actualRole.ID(), + } + + intendedRoleID := intendedRole.ID() + if intended.Device.Role != nil { + intendedRoleID = intended.Device.Role.ID + } + + intended.Device.Role = &DcimDeviceRole{ + ID: intendedRoleID, + } + } + + dw.Device.Role = role + + dw.objectsToReconcile = append(dw.objectsToReconcile, roleObjectsToReconcile...) + + tagsToMerge := mergeTags(dw.Device.Tags, intended.Device.Tags, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.Device.Tags = tagsToMerge + } + + for _, t := range dw.Device.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) + intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) + + reconciliationRequired = actualHash != intendedHash + } else { + dw.SetDefaults() + + siteObjectsToReconcile, siteErr := actualSite.Patch(intendedSite, intendedNestedObjects) + if siteErr != nil { + return nil, siteErr + } + + site, err := copyData(actualSite.Data().(*DcimSite)) + if err != nil { + return nil, err + } + site.Tags = nil + + if !actualSite.HasChanged() { + site = &DcimSite{ + ID: actualSite.ID(), + } + } + dw.Device.Site = site + + dw.objectsToReconcile = append(dw.objectsToReconcile, siteObjectsToReconcile...) + + if actualPlatform != nil { + platformObjectsToReconcile, platformErr := actualPlatform.Patch(intendedPlatform, intendedNestedObjects) + if platformErr != nil { + return nil, platformErr + } + + platform, err := copyData(actualPlatform.Data().(*DcimPlatform)) + if err != nil { + return nil, err + } + platform.Tags = nil + + if !actualPlatform.HasChanged() { + platform = &DcimPlatform{ + ID: actualPlatform.ID(), + } + } + dw.Device.Platform = platform + + dw.objectsToReconcile = append(dw.objectsToReconcile, platformObjectsToReconcile...) + } + + deviceTypeObjectsToReconcile, deviceTypeErr := actualDeviceType.Patch(intendedDeviceType, intendedNestedObjects) + if deviceTypeErr != nil { + return nil, deviceTypeErr + } + + deviceType, err := copyData(actualDeviceType.Data().(*DcimDeviceType)) + if err != nil { + return nil, err + } + deviceType.Tags = nil + + if !actualDeviceType.HasChanged() { + deviceType = &DcimDeviceType{ + ID: actualDeviceType.ID(), + } + } + dw.Device.DeviceType = deviceType + + dw.objectsToReconcile = append(dw.objectsToReconcile, deviceTypeObjectsToReconcile...) + + roleObjectsToReconcile, roleErr := actualRole.Patch(intendedRole, intendedNestedObjects) + if roleErr != nil { + return nil, roleErr + } + + role, err := copyData(actualRole.Data().(*DcimDeviceRole)) + if err != nil { + return nil, err + } + role.Tags = nil + + if !actualRole.HasChanged() { + role = &DcimDeviceRole{ + ID: actualRole.ID(), + } + } + dw.Device.Role = role + + dw.objectsToReconcile = append(dw.objectsToReconcile, roleObjectsToReconcile...) + + tagsToMerge := mergeTags(dw.Device.Tags, nil, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.Device.Tags = tagsToMerge + } + + for _, t := range dw.Device.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + } + + if reconciliationRequired { + dw.hasChanged = true + dw.objectsToReconcile = append(dw.objectsToReconcile, dw) + } + + dedupObjectsToReconcile, err := dedupObjectsToReconcile(dw.objectsToReconcile) + if err != nil { + return nil, err + } + dw.objectsToReconcile = dedupObjectsToReconcile + + return dw.objectsToReconcile, nil +} + +// SetDefaults sets the default values for the device +func (dw *DcimDeviceDataWrapper) SetDefaults() { + if dw.Device.Status == nil || *dw.Device.Status == "" { + status := DcimDeviceStatusActive + dw.Device.Status = &status + } +} + +// DcimDeviceRoleDataWrapper represents a DCIM device role data wrapper +type DcimDeviceRoleDataWrapper struct { + BaseDataWrapper + DeviceRole *DcimDeviceRole +} + +func (*DcimDeviceRoleDataWrapper) comparableData() {} + +// FromProtoEntity sets the data from a proto entity +func (dw *DcimDeviceRoleDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + role, err := FromProtoDeviceRoleEntity(entity) + if err != nil { + return err + } + dw.DeviceRole = role + return nil +} + +// Data returns the DeviceRole +func (dw *DcimDeviceRoleDataWrapper) Data() any { + return dw.DeviceRole +} + +// IsValid returns true if the DeviceRole is not nil +func (dw *DcimDeviceRoleDataWrapper) IsValid() bool { + if dw.DeviceRole != nil && !dw.hasParent && dw.DeviceRole.Name == "" { + dw.DeviceRole = nil + } + return dw.DeviceRole != nil +} + +// Normalise normalises the data +func (dw *DcimDeviceRoleDataWrapper) Normalise() { + if dw.IsValid() && dw.DeviceRole.Tags != nil && len(dw.DeviceRole.Tags) == 0 { + dw.DeviceRole.Tags = nil + } + dw.intended = true +} + +// NestedObjects returns all nested objects +func (dw *DcimDeviceRoleDataWrapper) NestedObjects() ([]ComparableData, error) { + if len(dw.nestedObjects) > 0 { + return dw.nestedObjects, nil + } + + if dw.DeviceRole != nil && dw.hasParent && dw.DeviceRole.Name == "" { + dw.DeviceRole = nil + } + + objects := make([]ComparableData, 0) + + if dw.DeviceRole == nil && dw.intended { + return objects, nil + } + + if dw.DeviceRole == nil && dw.hasParent { + dw.DeviceRole = NewDcimDeviceRole() + dw.placeholder = true + } + + if dw.DeviceRole.Slug == "" { + dw.DeviceRole.Slug = slug.Make(dw.DeviceRole.Name) + } + + if dw.DeviceRole.Tags != nil { + for _, t := range dw.DeviceRole.Tags { + if t.Slug == "" { + t.Slug = slug.Make(t.Name) + } + objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + dw.nestedObjects = objects + + objects = append(objects, dw) + + return objects, nil +} + +// DataType returns the data type +func (dw *DcimDeviceRoleDataWrapper) DataType() string { + return DcimDeviceRoleObjectType +} + +// ObjectStateQueryParams returns the query parameters needed to retrieve its object state +func (dw *DcimDeviceRoleDataWrapper) ObjectStateQueryParams() map[string]string { + return map[string]string{ + "q": dw.DeviceRole.Name, + } +} + +// ID returns the ID of the data +func (dw *DcimDeviceRoleDataWrapper) ID() int { + return dw.DeviceRole.ID +} + +// Patch creates patches between the actual, intended and current data +func (dw *DcimDeviceRoleDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { + intended, ok := cmp.(*DcimDeviceRoleDataWrapper) + if !ok && intended != nil { + return nil, errors.New("invalid data type") + } + + actualNestedObjectsMap := make(map[string]ComparableData) + for _, obj := range dw.nestedObjects { + actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj + } + + reconciliationRequired := true + + if intended != nil { + dw.DeviceRole.ID = intended.DeviceRole.ID + dw.DeviceRole.Name = intended.DeviceRole.Name + dw.DeviceRole.Slug = intended.DeviceRole.Slug + + if dw.IsPlaceholder() || dw.DeviceRole.Color == nil || *dw.DeviceRole.Color == "" { + dw.DeviceRole.Color = intended.DeviceRole.Color + } + + if dw.DeviceRole.Description == nil { + dw.DeviceRole.Description = intended.DeviceRole.Description + } + + tagsToMerge := mergeTags(dw.DeviceRole.Tags, intended.DeviceRole.Tags, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.DeviceRole.Tags = tagsToMerge + } + + for _, t := range dw.DeviceRole.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) + intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) + + reconciliationRequired = actualHash != intendedHash + } else { + dw.SetDefaults() + + tagsToMerge := mergeTags(dw.DeviceRole.Tags, nil, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.DeviceRole.Tags = tagsToMerge + } + + for _, t := range dw.DeviceRole.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + } + + if reconciliationRequired { + dw.hasChanged = true + dw.objectsToReconcile = append(dw.objectsToReconcile, dw) + } + + return dw.objectsToReconcile, nil +} + +// SetDefaults sets the default values for the device role +func (dw *DcimDeviceRoleDataWrapper) SetDefaults() { + if dw.DeviceRole.Color == nil || *dw.DeviceRole.Color == "" { + color := "000000" + dw.DeviceRole.Color = &color + } +} + +// DcimDeviceTypeDataWrapper represents a DCIM device type data wrapper +type DcimDeviceTypeDataWrapper struct { + BaseDataWrapper + DeviceType *DcimDeviceType +} + +func (*DcimDeviceTypeDataWrapper) comparableData() {} + +// FromProtoEntity sets the data from a proto entity +func (dw *DcimDeviceTypeDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + deviceType, err := FromProtoDeviceTypeEntity(entity) + if err != nil { + return err + } + dw.DeviceType = deviceType + return nil +} + +// Data returns the DeviceType +func (dw *DcimDeviceTypeDataWrapper) Data() any { + return dw.DeviceType +} + +// IsValid returns true if the DeviceType is not nil +func (dw *DcimDeviceTypeDataWrapper) IsValid() bool { + if dw.DeviceType != nil && !dw.hasParent && dw.DeviceType.Model == "" { + dw.DeviceType = nil + } + return dw.DeviceType != nil +} + +// Normalise normalises the data +func (dw *DcimDeviceTypeDataWrapper) Normalise() { + if dw.IsValid() && dw.DeviceType.Tags != nil && len(dw.DeviceType.Tags) == 0 { + dw.DeviceType.Tags = nil + } + dw.intended = true +} + +// DataType returns the data type +func (dw *DcimDeviceTypeDataWrapper) DataType() string { + return DcimDeviceTypeObjectType +} + +// ObjectStateQueryParams returns the query parameters needed to retrieve its object state +func (dw *DcimDeviceTypeDataWrapper) ObjectStateQueryParams() map[string]string { + params := map[string]string{ + "q": dw.DeviceType.Model, + } + if dw.DeviceType.Manufacturer != nil { + params["manufacturer__name"] = dw.DeviceType.Manufacturer.Name + } + return params +} + +// ID returns the ID of the data +func (dw *DcimDeviceTypeDataWrapper) ID() int { + return dw.DeviceType.ID +} + +// NestedObjects returns all nested objects +func (dw *DcimDeviceTypeDataWrapper) NestedObjects() ([]ComparableData, error) { + if len(dw.nestedObjects) > 0 { + return dw.nestedObjects, nil + } + + if dw.DeviceType != nil && dw.hasParent && dw.DeviceType.Model == "" { + dw.DeviceType = nil + } + + objects := make([]ComparableData, 0) + + if dw.DeviceType == nil && dw.intended { + return objects, nil + } + + if dw.DeviceType == nil && dw.hasParent { + dw.DeviceType = NewDcimDeviceType() + dw.placeholder = true + } + + if dw.DeviceType.Slug == "" { + dw.DeviceType.Slug = slug.Make(dw.DeviceType.Model) + } + + manufacturer := DcimManufacturerDataWrapper{Manufacturer: dw.DeviceType.Manufacturer, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} + + mo, err := manufacturer.NestedObjects() + if err != nil { + return nil, err + } + + objects = append(objects, mo...) + + dw.DeviceType.Manufacturer = manufacturer.Manufacturer + + if dw.DeviceType.Tags != nil && len(dw.DeviceType.Tags) == 0 { + dw.DeviceType.Tags = nil + } + + if dw.DeviceType.Tags != nil { + for _, t := range dw.DeviceType.Tags { + if t.Slug == "" { + t.Slug = slug.Make(t.Name) + } + objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + dw.nestedObjects = objects + + objects = append(objects, dw) + + return objects, nil +} + +// Patch creates patches between the actual, intended and current data +func (dw *DcimDeviceTypeDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { + intended, ok := cmp.(*DcimDeviceTypeDataWrapper) + if !ok && intended != nil { + return nil, errors.New("invalid data type") + } + + actualNestedObjectsMap := make(map[string]ComparableData) + for _, obj := range dw.nestedObjects { + actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj + } + + actualManufacturerKey := fmt.Sprintf("%p", dw.DeviceType.Manufacturer) + actualManufacturer := extractFromObjectsMap(actualNestedObjectsMap, actualManufacturerKey) + intendedManufacturer := extractFromObjectsMap(intendedNestedObjects, actualManufacturerKey) + + reconciliationRequired := true + + if intended != nil { + currentNestedObjectsMap := make(map[string]ComparableData) + currentNestedObjects, err := intended.NestedObjects() + if err != nil { + return nil, err + } + for _, obj := range currentNestedObjects { + currentNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj + } + + dw.DeviceType.ID = intended.DeviceType.ID + dw.DeviceType.Model = intended.DeviceType.Model + dw.DeviceType.Slug = intended.DeviceType.Slug + + if dw.DeviceType.Description == nil { + dw.DeviceType.Description = intended.DeviceType.Description + } + + if dw.DeviceType.Comments == nil { + dw.DeviceType.Comments = intended.DeviceType.Comments + } + + if dw.DeviceType.PartNumber == nil { + dw.DeviceType.PartNumber = intended.DeviceType.PartNumber + } + + if actualManufacturer.IsPlaceholder() && intended.DeviceType.Manufacturer != nil { + intendedManufacturer = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.DeviceType.Manufacturer)) + } + + manufacturerObjectsToReconcile, manufacturerErr := actualManufacturer.Patch(intendedManufacturer, intendedNestedObjects) + if manufacturerErr != nil { + return nil, manufacturerErr + } + + manufacturer, err := copyData(actualManufacturer.Data().(*DcimManufacturer)) + if err != nil { + return nil, err + } + manufacturer.Tags = nil + + if !actualManufacturer.HasChanged() { + manufacturer = &DcimManufacturer{ + ID: actualManufacturer.ID(), + } + + intendedManufacturerID := intendedManufacturer.ID() + if intended.DeviceType.Manufacturer != nil { + intendedManufacturerID = intended.DeviceType.Manufacturer.ID + } + + intended.DeviceType.Manufacturer = &DcimManufacturer{ + ID: intendedManufacturerID, + } + } + + dw.DeviceType.Manufacturer = manufacturer + + tagsToMerge := mergeTags(dw.DeviceType.Tags, intended.DeviceType.Tags, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.DeviceType.Tags = tagsToMerge + } + + for _, t := range dw.DeviceType.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + dw.objectsToReconcile = append(dw.objectsToReconcile, manufacturerObjectsToReconcile...) + + actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) + intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) + + reconciliationRequired = actualHash != intendedHash + } else { + manufacturerObjectsToReconcile, manufacturerErr := actualManufacturer.Patch(intendedManufacturer, intendedNestedObjects) + if manufacturerErr != nil { + return nil, manufacturerErr + } + + manufacturer, err := copyData(actualManufacturer.Data().(*DcimManufacturer)) + if err != nil { + return nil, err + } + manufacturer.Tags = nil + + if !actualManufacturer.HasChanged() { + manufacturer = &DcimManufacturer{ + ID: actualManufacturer.ID(), + } + } + dw.DeviceType.Manufacturer = manufacturer + + tagsToMerge := mergeTags(dw.DeviceType.Tags, nil, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.DeviceType.Tags = tagsToMerge + } + + for _, t := range dw.DeviceType.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + dw.objectsToReconcile = append(dw.objectsToReconcile, manufacturerObjectsToReconcile...) + } + + if reconciliationRequired { + dw.hasChanged = true + dw.objectsToReconcile = append(dw.objectsToReconcile, dw) + } + + return dw.objectsToReconcile, nil +} + +// SetDefaults sets the default values for the device type +func (dw *DcimDeviceTypeDataWrapper) SetDefaults() {} + +// DcimInterfaceDataWrapper represents a DCIM interface data wrapper +type DcimInterfaceDataWrapper struct { + BaseDataWrapper + Interface *DcimInterface +} + +func (*DcimInterfaceDataWrapper) comparableData() {} + +// FromProtoEntity sets the data from a proto entity +func (dw *DcimInterfaceDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + interf, err := FromProtoInterfaceEntity(entity) + if err != nil { + return err + } + dw.Interface = interf + return nil +} + +// Data returns the Interface +func (dw *DcimInterfaceDataWrapper) Data() any { + return dw.Interface +} + +// IsValid returns true if the Interface is not nil +func (dw *DcimInterfaceDataWrapper) IsValid() bool { + if dw.Interface != nil && !dw.hasParent && dw.Interface.Name == "" { + dw.Interface = nil + } + + if dw.Interface != nil { + if err := dw.Interface.Validate(); err != nil { + return false + } + } + + return dw.Interface != nil +} + +// Normalise normalises the data +func (dw *DcimInterfaceDataWrapper) Normalise() { + if dw.IsValid() && dw.Interface.Tags != nil && len(dw.Interface.Tags) == 0 { + dw.Interface.Tags = nil + } + dw.intended = true +} + +// NestedObjects returns all nested objects +func (dw *DcimInterfaceDataWrapper) NestedObjects() ([]ComparableData, error) { + if len(dw.nestedObjects) > 0 { + return dw.nestedObjects, nil + } + + if dw.Interface != nil && dw.hasParent && dw.Interface.Name == "" { + dw.Interface = nil + } + + objects := make([]ComparableData, 0) + + if dw.Interface == nil && dw.intended { + return objects, nil + } + + if dw.Interface == nil && dw.hasParent { + dw.Interface = NewDcimInterface() + dw.placeholder = true + } + + device := DcimDeviceDataWrapper{Device: dw.Interface.Device, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} + + do, err := device.NestedObjects() + if err != nil { + return nil, err + } + + objects = append(objects, do...) + + dw.Interface.Device = device.Device + + if dw.Interface.Tags != nil { + for _, t := range dw.Interface.Tags { + if t.Slug == "" { + t.Slug = slug.Make(t.Name) + } + objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + dw.nestedObjects = objects + + objects = append(objects, dw) + + return objects, nil +} + +// DataType returns the data type +func (dw *DcimInterfaceDataWrapper) DataType() string { + return DcimInterfaceObjectType +} + +// ObjectStateQueryParams returns the query parameters needed to retrieve its object state +func (dw *DcimInterfaceDataWrapper) ObjectStateQueryParams() map[string]string { + params := map[string]string{ + "q": dw.Interface.Name, + } + if dw.Interface.Device != nil { + params["device__name"] = dw.Interface.Device.Name + + if dw.Interface.Device.Site != nil { + params["device__site__name"] = dw.Interface.Device.Site.Name + } + } + return params +} + +// ID returns the ID of the data +func (dw *DcimInterfaceDataWrapper) ID() int { + return dw.Interface.ID +} + +func (dw *DcimInterfaceDataWrapper) hash() string { + var deviceName, siteName string + if dw.Interface.Device != nil { + deviceName = dw.Interface.Device.Name + if dw.Interface.Device.Site != nil { + siteName = dw.Interface.Device.Site.Name + } + } + return slug.Make(fmt.Sprintf("%s-%s-%s", dw.Interface.Name, deviceName, siteName)) +} + +// Patch creates patches between the actual, intended and current data +func (dw *DcimInterfaceDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { + intended, ok := cmp.(*DcimInterfaceDataWrapper) + if !ok && intended != nil { + return nil, errors.New("invalid data type") + } + + actualNestedObjectsMap := make(map[string]ComparableData) + for _, obj := range dw.nestedObjects { + actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj + } + + actualDevice := extractFromObjectsMap(actualNestedObjectsMap, fmt.Sprintf("%p", dw.Interface.Device)) + intendedDevice := extractFromObjectsMap(intendedNestedObjects, fmt.Sprintf("%p", dw.Interface.Device)) + + reconciliationRequired := true + + if intended != nil && dw.hash() == intended.hash() { + currentNestedObjectsMap := make(map[string]ComparableData) + currentNestedObjects, err := intended.NestedObjects() + if err != nil { + return nil, err + } + for _, obj := range currentNestedObjects { + currentNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj + } + + dw.Interface.ID = intended.Interface.ID + dw.Interface.Name = intended.Interface.Name + + if actualDevice.IsPlaceholder() && intended.Interface.Device != nil { + intendedDevice = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Interface.Device)) + } + + deviceObjectsToReconcile, deviceErr := actualDevice.Patch(intendedDevice, intendedNestedObjects) + if deviceErr != nil { + return nil, deviceErr + } + + device, err := copyData(actualDevice.Data().(*DcimDevice)) + if err != nil { + return nil, err + } + device.Tags = nil + + if !actualDevice.HasChanged() { + device = &DcimDevice{ + ID: actualDevice.ID(), + } + + intendedDeviceID := intendedDevice.ID() + if intended.Interface.Device != nil { + intendedDeviceID = intended.Interface.Device.ID + } + + intended.Interface.Device = &DcimDevice{ + ID: intendedDeviceID, + } + } + + dw.Interface.Device = device + + dw.objectsToReconcile = append(dw.objectsToReconcile, deviceObjectsToReconcile...) + + if dw.Interface.Label == nil { + dw.Interface.Label = intended.Interface.Label + } + + if dw.Interface.Type == nil { + dw.Interface.Type = intended.Interface.Type + } + + if dw.Interface.Enabled == nil { + dw.Interface.Enabled = intended.Interface.Enabled + } + + if dw.Interface.MTU == nil { + dw.Interface.MTU = intended.Interface.MTU + } + + if dw.Interface.MACAddress == nil { + dw.Interface.MACAddress = intended.Interface.MACAddress + } + + if dw.Interface.Speed == nil { + dw.Interface.Speed = intended.Interface.Speed + } + + if dw.Interface.WWN == nil { + dw.Interface.WWN = intended.Interface.WWN + } + + if dw.Interface.MgmtOnly == nil { + dw.Interface.MgmtOnly = intended.Interface.MgmtOnly + } + + if dw.Interface.Description == nil { + dw.Interface.Description = intended.Interface.Description + } + + if dw.Interface.MarkConnected == nil { + dw.Interface.MarkConnected = intended.Interface.MarkConnected + } + + if dw.Interface.Mode == nil { + dw.Interface.Mode = intended.Interface.Mode + } + + tagsToMerge := mergeTags(dw.Interface.Tags, intended.Interface.Tags, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.Interface.Tags = tagsToMerge + } + + for _, t := range dw.Interface.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) + intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) + + reconciliationRequired = actualHash != intendedHash + } else { + dw.SetDefaults() + + deviceObjectsToReconcile, deviceErr := actualDevice.Patch(intendedDevice, intendedNestedObjects) + if deviceErr != nil { + return nil, deviceErr + } + + device, err := copyData(actualDevice.Data().(*DcimDevice)) + if err != nil { + return nil, err + } + device.Tags = nil + + if !actualDevice.HasChanged() { + device = &DcimDevice{ + ID: actualDevice.ID(), + } + } + dw.Interface.Device = device + + tagsToMerge := mergeTags(dw.Interface.Tags, nil, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.Interface.Tags = tagsToMerge + } + + for _, t := range dw.Interface.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + dw.objectsToReconcile = append(dw.objectsToReconcile, deviceObjectsToReconcile...) + } + + if reconciliationRequired { + dw.hasChanged = true + dw.objectsToReconcile = append(dw.objectsToReconcile, dw) + } + + return dw.objectsToReconcile, nil +} + +// SetDefaults sets the default values for the interface +func (dw *DcimInterfaceDataWrapper) SetDefaults() { + if dw.Interface.Type == nil { + dw.Interface.Type = &DefaultInterfaceType + } +} + +// DcimManufacturerDataWrapper represents a DCIM manufacturer data wrapper +type DcimManufacturerDataWrapper struct { + BaseDataWrapper + Manufacturer *DcimManufacturer +} + +func (*DcimManufacturerDataWrapper) comparableData() {} + +// FromProtoEntity sets the data from a proto entity +func (dw *DcimManufacturerDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + manufacturer, err := FromProtoManufacturerEntity(entity) + if err != nil { + return err + } + dw.Manufacturer = manufacturer + return nil +} + +// Data returns the Manufacturer +func (dw *DcimManufacturerDataWrapper) Data() any { + return dw.Manufacturer +} + +// IsValid returns true if the Manufacturer is not nil +func (dw *DcimManufacturerDataWrapper) IsValid() bool { + if dw.Manufacturer != nil && !dw.hasParent && dw.Manufacturer.Name == "" { + dw.Manufacturer = nil + } + return dw.Manufacturer != nil +} + +// Normalise normalises the data +func (dw *DcimManufacturerDataWrapper) Normalise() { + if dw.IsValid() && dw.Manufacturer.Tags != nil && len(dw.Manufacturer.Tags) == 0 { + dw.Manufacturer.Tags = nil + } + dw.intended = true +} + +// NestedObjects returns all nested objects +func (dw *DcimManufacturerDataWrapper) NestedObjects() ([]ComparableData, error) { + if len(dw.nestedObjects) > 0 { + return dw.nestedObjects, nil + } + + if dw.Manufacturer != nil && dw.hasParent && dw.Manufacturer.Name == "" { + dw.Manufacturer = nil + } + + objects := make([]ComparableData, 0) + + if dw.Manufacturer == nil && dw.intended { + return objects, nil + } + + if dw.Manufacturer == nil && dw.hasParent { + dw.Manufacturer = NewDcimManufacturer() + dw.placeholder = true + } + + if dw.Manufacturer.Slug == "" { + dw.Manufacturer.Slug = slug.Make(dw.Manufacturer.Name) + } + + if dw.Manufacturer.Tags != nil { + for _, t := range dw.Manufacturer.Tags { + if t.Slug == "" { + t.Slug = slug.Make(t.Name) + } + objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + dw.nestedObjects = objects + + objects = append(objects, dw) + + return objects, nil +} + +// DataType returns the data type +func (dw *DcimManufacturerDataWrapper) DataType() string { + return DcimManufacturerObjectType +} + +// ObjectStateQueryParams returns the query parameters needed to retrieve its object state +func (dw *DcimManufacturerDataWrapper) ObjectStateQueryParams() map[string]string { + return map[string]string{ + "q": dw.Manufacturer.Name, + } +} + +// ID returns the ID of the data +func (dw *DcimManufacturerDataWrapper) ID() int { + return dw.Manufacturer.ID +} + +// Patch creates patches between the actual, intended and current data +func (dw *DcimManufacturerDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { + intended, ok := cmp.(*DcimManufacturerDataWrapper) + + if !ok && intended != nil { + return nil, errors.New("invalid data type") + } + + reconciliationRequired := true + + if intended != nil { + dw.Manufacturer.ID = intended.Manufacturer.ID + dw.Manufacturer.Name = intended.Manufacturer.Name + dw.Manufacturer.Slug = intended.Manufacturer.Slug + + if dw.Manufacturer.Description == nil { + dw.Manufacturer.Description = intended.Manufacturer.Description + } + + tagsToMerge := mergeTags(dw.Manufacturer.Tags, intended.Manufacturer.Tags, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.Manufacturer.Tags = tagsToMerge + } + + actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) + intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) + + reconciliationRequired = actualHash != intendedHash + } else { + tagsToMerge := mergeTags(dw.Manufacturer.Tags, nil, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.Manufacturer.Tags = tagsToMerge + } + } + + for _, t := range dw.Manufacturer.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + if reconciliationRequired { + dw.hasChanged = true + dw.objectsToReconcile = append(dw.objectsToReconcile, dw) + } + + return dw.objectsToReconcile, nil +} + +func mergeTags(actualTags []*Tag, intendedTags []*Tag, intendedNestedObjects map[string]ComparableData) []*Tag { + tagsToMerge := make([]*Tag, 0) + tagsToCreate := make([]*Tag, 0) + + tagNamesToMerge := make([]string, 0) + tagNamesToCreate := make([]string, 0) + + for _, t := range intendedTags { + if !slices.Contains(tagNamesToMerge, t.Name) { + tagNamesToMerge = append(tagNamesToMerge, t.Name) + tagsToMerge = append(tagsToMerge, t) + } + } + + for _, t := range actualTags { + tagKey := fmt.Sprintf("%p", t) + tagWrapper := extractFromObjectsMap(intendedNestedObjects, tagKey) + + if !slices.Contains(tagNamesToMerge, t.Name) && tagWrapper != nil { + tagNamesToMerge = append(tagNamesToMerge, t.Name) + tagsToMerge = append(tagsToMerge, tagWrapper.Data().(*Tag)) + continue + } + + if tagWrapper == nil { + if !slices.Contains(tagNamesToCreate, t.Name) { + tagNamesToCreate = append(tagNamesToCreate, t.Name) + tagsToCreate = append(tagsToCreate, t) + } + } + } + + return append(tagsToMerge, tagsToCreate...) +} + +// SetDefaults sets the default values for the manufacturer +func (dw *DcimManufacturerDataWrapper) SetDefaults() {} + +// DcimPlatformDataWrapper represents a DCIM platform data wrapper +type DcimPlatformDataWrapper struct { + BaseDataWrapper + Platform *DcimPlatform +} + +func (*DcimPlatformDataWrapper) comparableData() {} + +// FromProtoEntity sets the data from a proto entity +func (dw *DcimPlatformDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + platform, err := FromProtoPlatformEntity(entity) + if err != nil { + return err + } + dw.Platform = platform + return nil +} + +// Data returns the Platform +func (dw *DcimPlatformDataWrapper) Data() any { + return dw.Platform +} + +// IsValid returns true if the Platform is not nil +func (dw *DcimPlatformDataWrapper) IsValid() bool { + if dw.Platform != nil && !dw.hasParent && dw.Platform.Name == "" { + dw.Platform = nil + } + return dw.Platform != nil +} + +// Normalise normalises the data +func (dw *DcimPlatformDataWrapper) Normalise() { + if dw.IsValid() && dw.Platform.Tags != nil && len(dw.Platform.Tags) == 0 { + dw.Platform.Tags = nil + } + dw.intended = true +} + +// NestedObjects returns all nested objects +func (dw *DcimPlatformDataWrapper) NestedObjects() ([]ComparableData, error) { + if len(dw.nestedObjects) > 0 { + return dw.nestedObjects, nil + } + + if dw.Platform != nil && dw.hasParent && dw.Platform.Name == "" { + dw.Platform = nil + } + + objects := make([]ComparableData, 0) + + if dw.Platform == nil && dw.intended { + return objects, nil + } + + if dw.Platform == nil && dw.hasParent { + dw.Platform = NewDcimPlatform() + dw.placeholder = true + } + + if dw.Platform.Slug == "" { + dw.Platform.Slug = slug.Make(dw.Platform.Name) + } + + if dw.Platform.Manufacturer != nil { + manufacturer := DcimManufacturerDataWrapper{Manufacturer: dw.Platform.Manufacturer, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} + + mo, err := manufacturer.NestedObjects() + if err != nil { + return nil, err + } + + objects = append(objects, mo...) + + dw.Platform.Manufacturer = manufacturer.Manufacturer + } + + if dw.Platform.Tags != nil { + for _, t := range dw.Platform.Tags { + if t.Slug == "" { + t.Slug = slug.Make(t.Name) + } + objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + dw.nestedObjects = objects + + objects = append(objects, dw) + + return objects, nil +} + +// DataType returns the data type +func (dw *DcimPlatformDataWrapper) DataType() string { + return DcimPlatformObjectType +} + +// ObjectStateQueryParams returns the query parameters needed to retrieve its object state +func (dw *DcimPlatformDataWrapper) ObjectStateQueryParams() map[string]string { + params := map[string]string{ + "q": dw.Platform.Name, + } + if dw.Platform.Manufacturer != nil { + params["manufacturer__name"] = dw.Platform.Manufacturer.Name + } + return params +} + +// ID returns the ID of the data +func (dw *DcimPlatformDataWrapper) ID() int { + return dw.Platform.ID +} + +// Patch creates patches between the actual, intended and current data +func (dw *DcimPlatformDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { + intended, ok := cmp.(*DcimPlatformDataWrapper) + if !ok && intended != nil { + return nil, errors.New("invalid data type") + } + + actualNestedObjectsMap := make(map[string]ComparableData) + for _, obj := range dw.nestedObjects { + actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj + } + + actualManufacturerKey := fmt.Sprintf("%p", dw.Platform.Manufacturer) + actualManufacturer := extractFromObjectsMap(actualNestedObjectsMap, actualManufacturerKey) + intendedManufacturer := extractFromObjectsMap(intendedNestedObjects, actualManufacturerKey) + + reconciliationRequired := true + + if intended != nil { + currentNestedObjectsMap := make(map[string]ComparableData) + currentNestedObjects, err := intended.NestedObjects() + if err != nil { + return nil, err + } + for _, obj := range currentNestedObjects { + currentNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj + } + + dw.Platform.ID = intended.Platform.ID + dw.Platform.Name = intended.Platform.Name + dw.Platform.Slug = intended.Platform.Slug + + if actualManufacturer != nil { + if actualManufacturer.IsPlaceholder() && intended.Platform.Manufacturer != nil { + intendedManufacturer = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Platform.Manufacturer)) + } + + manufacturerObjectsToReconcile, manufacturerErr := actualManufacturer.Patch(intendedManufacturer, intendedNestedObjects) + if manufacturerErr != nil { + return nil, manufacturerErr + } + + manufacturer, err := copyData(actualManufacturer.Data().(*DcimManufacturer)) + if err != nil { + return nil, err + } + manufacturer.Tags = nil + + if !actualManufacturer.HasChanged() { + manufacturer = &DcimManufacturer{ + ID: actualManufacturer.ID(), + } + + intendedManufacturerID := intendedManufacturer.ID() + if intended.Platform.Manufacturer != nil { + intendedManufacturerID = intended.Platform.Manufacturer.ID + } + + intended.Platform.Manufacturer = &DcimManufacturer{ + ID: intendedManufacturerID, + } + } + + dw.Platform.Manufacturer = manufacturer + + dw.objectsToReconcile = append(dw.objectsToReconcile, manufacturerObjectsToReconcile...) + } else { + if intended.Platform.Manufacturer != nil { + manufacturerID := intended.Platform.Manufacturer.ID + + dw.Platform.Manufacturer = &DcimManufacturer{ + ID: manufacturerID, + } + intended.Platform.Manufacturer = &DcimManufacturer{ + ID: manufacturerID, + } + } + } + + if dw.Platform.Description == nil { + dw.Platform.Description = intended.Platform.Description + } + + tagsToMerge := mergeTags(dw.Platform.Tags, intended.Platform.Tags, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.Platform.Tags = tagsToMerge + } + + for _, t := range dw.Platform.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) + intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) + + reconciliationRequired = actualHash != intendedHash + } else { + if actualManufacturer != nil { + manufacturerObjectsToReconcile, manufacturerErr := actualManufacturer.Patch(intendedManufacturer, intendedNestedObjects) + if manufacturerErr != nil { + return nil, manufacturerErr + } + + manufacturer, err := copyData(actualManufacturer.Data().(*DcimManufacturer)) + if err != nil { + return nil, err + } + manufacturer.Tags = nil + + if !actualManufacturer.HasChanged() { + manufacturer = &DcimManufacturer{ + ID: actualManufacturer.ID(), + } + } + dw.Platform.Manufacturer = manufacturer + + dw.objectsToReconcile = append(dw.objectsToReconcile, manufacturerObjectsToReconcile...) + } + + tagsToMerge := mergeTags(dw.Platform.Tags, nil, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.Platform.Tags = tagsToMerge + } + + for _, t := range dw.Platform.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + } + + if reconciliationRequired { + dw.hasChanged = true + dw.objectsToReconcile = append(dw.objectsToReconcile, dw) + } + + return dw.objectsToReconcile, nil +} + +// SetDefaults sets the default values for the platform +func (dw *DcimPlatformDataWrapper) SetDefaults() {} + +// DcimSiteDataWrapper represents a DCIM site data wrapper +type DcimSiteDataWrapper struct { + BaseDataWrapper + Site *DcimSite +} + +func (*DcimSiteDataWrapper) comparableData() {} + +// FromProtoEntity sets the data from a proto entity +func (dw *DcimSiteDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + site, err := FromProtoSiteEntity(entity) + if err != nil { + return err + } + dw.Site = site + return nil +} + +// Data returns the Site +func (dw *DcimSiteDataWrapper) Data() any { + return dw.Site +} + +// IsValid returns true if the Site is not nil +func (dw *DcimSiteDataWrapper) IsValid() bool { + if dw.Site != nil && !dw.hasParent && dw.Site.Name == "" { + dw.Site = nil + } + return dw.Site != nil +} + +// Normalise normalises the data +func (dw *DcimSiteDataWrapper) Normalise() { + if dw.IsValid() && dw.Site.Tags != nil && len(dw.Site.Tags) == 0 { + dw.Site.Tags = nil + } + dw.intended = true +} + +// NestedObjects returns all nested objects +func (dw *DcimSiteDataWrapper) NestedObjects() ([]ComparableData, error) { + if len(dw.nestedObjects) > 0 { + return dw.nestedObjects, nil + } + + if dw.Site != nil && dw.hasParent && dw.Site.Name == "" { + dw.Site = nil + } + + objects := make([]ComparableData, 0) + + if dw.Site == nil && dw.intended { + return objects, nil + } + + if dw.Site == nil && dw.hasParent { + dw.Site = NewDcimSite() + dw.placeholder = true + } + + if dw.Site.Slug == "" { + dw.Site.Slug = slug.Make(dw.Site.Name) + } + + if dw.Site.Tags != nil { + for _, t := range dw.Site.Tags { + if t.Slug == "" { + t.Slug = slug.Make(t.Name) + } + objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + dw.nestedObjects = objects + + objects = append(objects, dw) + + return objects, nil +} + +// DataType returns the data type +func (dw *DcimSiteDataWrapper) DataType() string { + return DcimSiteObjectType +} + +// ObjectStateQueryParams returns the query parameters needed to retrieve its object state +func (dw *DcimSiteDataWrapper) ObjectStateQueryParams() map[string]string { + return map[string]string{ + "q": dw.Site.Name, + } +} + +// ID returns the ID of the data +func (dw *DcimSiteDataWrapper) ID() int { + return dw.Site.ID +} + +// Patch creates patches between the actual, intended and current data +func (dw *DcimSiteDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { + intended, ok := cmp.(*DcimSiteDataWrapper) + if !ok && intended != nil { + return nil, errors.New("invalid data type") + } + + actualNestedObjectsMap := make(map[string]ComparableData) + for _, obj := range dw.nestedObjects { + actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj + } + + reconciliationRequired := true + + if intended != nil { + dw.Site.ID = intended.Site.ID + dw.Site.Name = intended.Site.Name + dw.Site.Slug = intended.Site.Slug + + if dw.Site.Status == nil || *dw.Site.Status == "" { + dw.Site.Status = intended.Site.Status + } + + if dw.Site.Facility == nil { + dw.Site.Facility = intended.Site.Facility + } + + if dw.Site.TimeZone == nil { + dw.Site.TimeZone = intended.Site.TimeZone + } + + if dw.Site.Description == nil { + dw.Site.Description = intended.Site.Description + } + + if dw.Site.Comments == nil { + dw.Site.Comments = intended.Site.Comments + } + + tagsToMerge := mergeTags(dw.Site.Tags, intended.Site.Tags, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.Site.Tags = tagsToMerge + } + + for _, t := range dw.Site.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + + actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) + intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) + + reconciliationRequired = actualHash != intendedHash + } else { + dw.SetDefaults() + + tagsToMerge := mergeTags(dw.Site.Tags, nil, intendedNestedObjects) + + if len(tagsToMerge) > 0 { + dw.Site.Tags = tagsToMerge + } + + for _, t := range dw.Site.Tags { + if t.ID == 0 { + dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) + } + } + } + + if reconciliationRequired { + dw.hasChanged = true + dw.objectsToReconcile = append(dw.objectsToReconcile, dw) + } + + return dw.objectsToReconcile, nil +} + +// SetDefaults sets the default values for the site +func (dw *DcimSiteDataWrapper) SetDefaults() { + if dw.Site.Status == nil || *dw.Site.Status == "" { + status := DcimSiteStatusActive + dw.Site.Status = &status + } +} diff --git a/diode-server/netbox/extras.go b/diode-server/netbox/extras.go deleted file mode 100644 index dea849ed..00000000 --- a/diode-server/netbox/extras.go +++ /dev/null @@ -1,14 +0,0 @@ -package netbox - -const ( - // ExtrasTagObjectType represents the tag object type - ExtrasTagObjectType = "extras.tag" -) - -// Tag represents a tag -type Tag struct { - ID int `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Slug string `json:"slug,omitempty"` - Color string `json:"color,omitempty"` -} diff --git a/diode-server/netbox/ipam.go b/diode-server/netbox/ipam.go index 67db19ee..f13c3247 100644 --- a/diode-server/netbox/ipam.go +++ b/diode-server/netbox/ipam.go @@ -8,6 +8,8 @@ import ( "github.com/iancoleman/strcase" "github.com/mitchellh/mapstructure" + + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" ) const ( @@ -129,6 +131,68 @@ func IpamIPAddressAssignedObjectHookFunc() mapstructure.DecodeHookFunc { } } +// FromProtoIPAddressEntity converts a diode IP address entity to a IPAM IP address +func FromProtoIPAddressEntity(entity *diodepb.Entity) (*IpamIPAddress, error) { + if entity == nil || entity.GetIpAddress() == nil { + return nil, fmt.Errorf("entity is nil or not an IP address") + } + + return FromProtoIPAddress(entity.GetIpAddress()), nil +} + +// FromProtoIPAddress converts a diode IP address to a IPAM IP address +func FromProtoIPAddress(ipaddressPb *diodepb.IPAddress) *IpamIPAddress { + if ipaddressPb == nil { + return nil + } + + var status *string + if ipaddressPb.Status != "" { + status = &ipaddressPb.Status + } + + var role *string + if ipaddressPb.Role != "" { + role = &ipaddressPb.Role + } + + return &IpamIPAddress{ + Address: ipaddressPb.Address, + AssignedObject: FromProtoIPAddressAssignedObject(ipaddressPb.AssignedObject), + Status: status, + Role: role, + DNSName: ipaddressPb.DnsName, + Description: ipaddressPb.Description, + Comments: ipaddressPb.Comments, + Tags: FromProtoTags(ipaddressPb.Tags), + } +} + +// FromProtoIPAddressAssignedObject converts a diode IP address assigned object to an IPAM IP address assigned object +func FromProtoIPAddressAssignedObject(assignedObjectPb any) IPAddressAssignedObject { + if assignedObjectPb == nil { + return nil + } + + switch assignedObjectPb := assignedObjectPb.(type) { + case *diodepb.IPAddress_Interface: + return FromProtoIPAddressInterface(assignedObjectPb) + default: + return nil + } +} + +// FromProtoIPAddressInterface converts a diode IP address interface to an IPAM IP address interface +func FromProtoIPAddressInterface(interfacePb *diodepb.IPAddress_Interface) *IPAddressInterface { + if interfacePb == nil { + return nil + } + + return &IPAddressInterface{ + Interface: FromProtoInterface(interfacePb.Interface), + } +} + // IpamPrefix represents an IPAM Prefix type IpamPrefix struct { ID int `json:"id,omitempty"` @@ -161,3 +225,35 @@ func (p *IpamPrefix) Validate() error { } return nil } + +// FromProtoPrefixEntity converts a diode prefix entity to a IPAM prefix +func FromProtoPrefixEntity(entity *diodepb.Entity) (*IpamPrefix, error) { + if entity == nil || entity.GetPrefix() == nil { + return nil, fmt.Errorf("entity is nil or not a prefix") + } + + return FromProtoPrefix(entity.GetPrefix()), nil +} + +// FromProtoPrefix converts a diode prefix to a IPAM prefix +func FromProtoPrefix(prefixPb *diodepb.Prefix) *IpamPrefix { + if prefixPb == nil { + return nil + } + + var status *string + if prefixPb.Status != "" { + status = &prefixPb.Status + } + + return &IpamPrefix{ + Prefix: prefixPb.Prefix, + Site: FromProtoSite(prefixPb.Site), + Status: status, + IsPool: prefixPb.IsPool, + MarkUtilized: prefixPb.MarkUtilized, + Description: prefixPb.Description, + Comments: prefixPb.Comments, + Tags: FromProtoTags(prefixPb.Tags), + } +} diff --git a/diode-server/netbox/ipam_wrappers.go b/diode-server/netbox/ipam_wrappers.go index 0266cab7..3e50b568 100644 --- a/diode-server/netbox/ipam_wrappers.go +++ b/diode-server/netbox/ipam_wrappers.go @@ -6,6 +6,8 @@ import ( "github.com/gosimple/slug" "github.com/mitchellh/hashstructure/v2" + + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" ) // IpamIPAddressDataWrapper represents the IPAM IP address data wrapper @@ -16,6 +18,16 @@ type IpamIPAddressDataWrapper struct { func (*IpamIPAddressDataWrapper) comparableData() {} +// FromProtoEntity sets the data from a proto entity +func (dw *IpamIPAddressDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + ipAddress, err := FromProtoIPAddressEntity(entity) + if err != nil { + return err + } + dw.IPAddress = ipAddress + return nil +} + // Data returns the IP address func (dw *IpamIPAddressDataWrapper) Data() any { return dw.IPAddress @@ -337,7 +349,7 @@ func (dw *IpamIPAddressDataWrapper) Patch(cmp ComparableData, intendedNestedObje // SetDefaults sets the default values for the IP address func (dw *IpamIPAddressDataWrapper) SetDefaults() { - if dw.IPAddress.Status == nil { + if dw.IPAddress.Status == nil || *dw.IPAddress.Status == "" { dw.IPAddress.Status = &DefaultIPAddressStatus } } @@ -350,6 +362,16 @@ type IpamPrefixDataWrapper struct { func (*IpamPrefixDataWrapper) comparableData() {} +// FromProtoEntity sets the data from a proto entity +func (dw *IpamPrefixDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + prefix, err := FromProtoPrefixEntity(entity) + if err != nil { + return err + } + dw.Prefix = prefix + return nil +} + // Data returns the Prefix func (dw *IpamPrefixDataWrapper) Data() any { return dw.Prefix diff --git a/diode-server/netbox/virtualization.go b/diode-server/netbox/virtualization.go index c28ec578..6ddb315d 100644 --- a/diode-server/netbox/virtualization.go +++ b/diode-server/netbox/virtualization.go @@ -1,6 +1,11 @@ package netbox -import "errors" +import ( + "errors" + "fmt" + + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" +) const ( // VirtualizationClusterObjectType represents the Virtualization Cluster object type @@ -187,3 +192,169 @@ func NewVirtualizationVirtualDisk() *VirtualizationVirtualDisk { Size: 0, } } + +// FromProtoClusterEntity converts a diode cluster entity to a cluster +func FromProtoClusterEntity(entity *diodepb.Entity) (*VirtualizationCluster, error) { + if entity == nil || entity.GetCluster() == nil { + return nil, fmt.Errorf("entity is nil or not a cluster") + } + + return FromProtoCluster(entity.GetCluster()), nil +} + +// FromProtoCluster converts a diode cluster to a virtualization cluster +func FromProtoCluster(clusterPb *diodepb.Cluster) *VirtualizationCluster { + if clusterPb == nil { + return nil + } + + var status *string + if clusterPb.Status != "" { + status = &clusterPb.Status + } + + return &VirtualizationCluster{ + Name: clusterPb.Name, + Type: FromProtoClusterType(clusterPb.Type), + Group: FromProtoClusterGroup(clusterPb.Group), + Site: FromProtoSite(clusterPb.Site), + Status: status, + Description: clusterPb.Description, + Tags: FromProtoTags(clusterPb.Tags), + } +} + +// FromProtoClusterTypeEntity converts a diode cluster type entity to a cluster type +func FromProtoClusterTypeEntity(entity *diodepb.Entity) (*VirtualizationClusterType, error) { + if entity == nil || entity.GetClusterType() == nil { + return nil, fmt.Errorf("entity is nil or not a cluster type") + } + + return FromProtoClusterType(entity.GetClusterType()), nil +} + +// FromProtoClusterType converts a diode cluster type to a cluster type +func FromProtoClusterType(clusterTypePb *diodepb.ClusterType) *VirtualizationClusterType { + if clusterTypePb == nil { + return nil + } + + return &VirtualizationClusterType{ + Name: clusterTypePb.Name, + Slug: clusterTypePb.Slug, + Description: clusterTypePb.Description, + Tags: FromProtoTags(clusterTypePb.Tags), + } +} + +// FromProtoClusterGroupEntity converts a diode cluster group entity to a cluster group +func FromProtoClusterGroupEntity(entity *diodepb.Entity) (*VirtualizationClusterGroup, error) { + if entity == nil || entity.GetClusterGroup() == nil { + return nil, fmt.Errorf("entity is nil or not a cluster group") + } + + return FromProtoClusterGroup(entity.GetClusterGroup()), nil +} + +// FromProtoClusterGroup converts a diode cluster group to a cluster group +func FromProtoClusterGroup(clusterGroupPb *diodepb.ClusterGroup) *VirtualizationClusterGroup { + if clusterGroupPb == nil { + return nil + } + + return &VirtualizationClusterGroup{ + Name: clusterGroupPb.Name, + Slug: clusterGroupPb.Slug, + Description: clusterGroupPb.Description, + Tags: FromProtoTags(clusterGroupPb.Tags), + } +} + +// FromProtoVirtualMachineEntity converts a diode virtual machine entity to a virtual machine +func FromProtoVirtualMachineEntity(entity *diodepb.Entity) (*VirtualizationVirtualMachine, error) { + if entity == nil || entity.GetVirtualMachine() == nil { + return nil, fmt.Errorf("entity is nil or not a virtual machine") + } + + return FromProtoVirtualMachine(entity.GetVirtualMachine()), nil +} + +// FromProtoVirtualMachine converts a diode virtual machine to a virtual machine +func FromProtoVirtualMachine(virtualMachinePb *diodepb.VirtualMachine) *VirtualizationVirtualMachine { + if virtualMachinePb == nil { + return nil + } + + var status *string + if virtualMachinePb.Status != "" { + status = &virtualMachinePb.Status + } + + return &VirtualizationVirtualMachine{ + Name: virtualMachinePb.Name, + Status: status, + Site: FromProtoSite(virtualMachinePb.Site), + Cluster: FromProtoCluster(virtualMachinePb.Cluster), + Role: FromProtoRole(virtualMachinePb.Role), + Device: FromProtoDevice(virtualMachinePb.Device), + Platform: FromProtoPlatform(virtualMachinePb.Platform), + PrimaryIPv4: FromProtoIPAddress(virtualMachinePb.PrimaryIp4), + PrimaryIPv6: FromProtoIPAddress(virtualMachinePb.PrimaryIp6), + Vcpus: int32PtrToIntPtr(virtualMachinePb.Vcpus), + Memory: int32PtrToIntPtr(virtualMachinePb.Memory), + Disk: int32PtrToIntPtr(virtualMachinePb.Disk), + Description: virtualMachinePb.Description, + Comments: virtualMachinePb.Comments, + Tags: FromProtoTags(virtualMachinePb.Tags), + } +} + +// FromProtoVMInterfaceEntity converts a diode virtual machine interface entity to a virtual machine interface +func FromProtoVMInterfaceEntity(entity *diodepb.Entity) (*VirtualizationVMInterface, error) { + if entity == nil || entity.GetVminterface() == nil { + return nil, fmt.Errorf("entity is nil or not a virtual machine interface") + } + + return FromProtoVMInterface(entity.GetVminterface()), nil +} + +// FromProtoVMInterface converts a diode virtual machine interface to a virtual machine interface +func FromProtoVMInterface(vmInterfacePb *diodepb.VMInterface) *VirtualizationVMInterface { + if vmInterfacePb == nil { + return nil + } + + return &VirtualizationVMInterface{ + VirtualMachine: FromProtoVirtualMachine(vmInterfacePb.VirtualMachine), + Name: vmInterfacePb.Name, + Enabled: vmInterfacePb.Enabled, + MTU: int32PtrToIntPtr(vmInterfacePb.Mtu), + MACAddress: vmInterfacePb.MacAddress, + Description: vmInterfacePb.Description, + Tags: FromProtoTags(vmInterfacePb.Tags), + } +} + +// FromProtoVirtualDiskEntity converts a diode virtual disk entity to a virtual disk +func FromProtoVirtualDiskEntity(entity *diodepb.Entity) (*VirtualizationVirtualDisk, error) { + if entity == nil || entity.GetVirtualDisk() == nil { + return nil, fmt.Errorf("entity is nil or not a virtual disk") + } + + return FromProtoVirtualDisk(entity.GetVirtualDisk()), nil +} + +// FromProtoVirtualDisk converts a diode virtual disk to a virtual disk +func FromProtoVirtualDisk(virtualDiskPb *diodepb.VirtualDisk) *VirtualizationVirtualDisk { + if virtualDiskPb == nil { + return nil + } + + return &VirtualizationVirtualDisk{ + VirtualMachine: FromProtoVirtualMachine(virtualDiskPb.VirtualMachine), + Name: virtualDiskPb.Name, + Size: int(virtualDiskPb.Size), + Description: virtualDiskPb.Description, + Tags: FromProtoTags(virtualDiskPb.Tags), + } +} diff --git a/diode-server/netbox/virtualization_wrappers.go b/diode-server/netbox/virtualization_wrappers.go index 7f411cc2..e272f89c 100644 --- a/diode-server/netbox/virtualization_wrappers.go +++ b/diode-server/netbox/virtualization_wrappers.go @@ -6,6 +6,8 @@ import ( "github.com/gosimple/slug" "github.com/mitchellh/hashstructure/v2" + + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" ) // VirtualizationClusterGroupDataWrapper represents a virtualization cluster group data wrapper @@ -16,6 +18,16 @@ type VirtualizationClusterGroupDataWrapper struct { func (*VirtualizationClusterGroupDataWrapper) comparableData() {} +// FromProtoEntity sets the data from a proto entity +func (vw *VirtualizationClusterGroupDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + clusterGroup, err := FromProtoClusterGroupEntity(entity) + if err != nil { + return err + } + vw.ClusterGroup = clusterGroup + return nil +} + // Data returns the DeviceRole func (vw *VirtualizationClusterGroupDataWrapper) Data() any { return vw.ClusterGroup @@ -159,6 +171,16 @@ type VirtualizationClusterTypeDataWrapper struct { func (*VirtualizationClusterTypeDataWrapper) comparableData() {} +// FromProtoEntity sets the data from a proto entity +func (vw *VirtualizationClusterTypeDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + clusterType, err := FromProtoClusterTypeEntity(entity) + if err != nil { + return err + } + vw.ClusterType = clusterType + return nil +} + // Data returns the DeviceRole func (vw *VirtualizationClusterTypeDataWrapper) Data() any { return vw.ClusterType @@ -302,6 +324,16 @@ type VirtualizationClusterDataWrapper struct { func (*VirtualizationClusterDataWrapper) comparableData() {} +// FromProtoEntity sets the data from a proto entity +func (vw *VirtualizationClusterDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + cluster, err := FromProtoClusterEntity(entity) + if err != nil { + return err + } + vw.Cluster = cluster + return nil +} + // Data returns the DeviceRole func (vw *VirtualizationClusterDataWrapper) Data() any { return vw.Cluster @@ -664,7 +696,12 @@ func (vw *VirtualizationClusterDataWrapper) Patch(cmp ComparableData, intendedNe } // SetDefaults sets the default values for the device type -func (vw *VirtualizationClusterDataWrapper) SetDefaults() {} +func (vw *VirtualizationClusterDataWrapper) SetDefaults() { + if vw.Cluster.Status == nil || *vw.Cluster.Status == "" { + status := "active" + vw.Cluster.Status = &status + } +} // VirtualizationVirtualMachineDataWrapper represents a virtualization virtual machine data wrapper type VirtualizationVirtualMachineDataWrapper struct { @@ -674,6 +711,16 @@ type VirtualizationVirtualMachineDataWrapper struct { func (*VirtualizationVirtualMachineDataWrapper) comparableData() {} +// FromProtoEntity sets the data from a proto entity +func (vw *VirtualizationVirtualMachineDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + virtualMachine, err := FromProtoVirtualMachineEntity(entity) + if err != nil { + return err + } + vw.VirtualMachine = virtualMachine + return nil +} + // Data returns the DeviceRole func (vw *VirtualizationVirtualMachineDataWrapper) Data() any { return vw.VirtualMachine @@ -1221,7 +1268,12 @@ func (vw *VirtualizationVirtualMachineDataWrapper) Patch(cmp ComparableData, int } // SetDefaults sets the default values for the device type -func (vw *VirtualizationVirtualMachineDataWrapper) SetDefaults() {} +func (vw *VirtualizationVirtualMachineDataWrapper) SetDefaults() { + if vw.VirtualMachine.Status == nil || *vw.VirtualMachine.Status == "" { + status := "active" + vw.VirtualMachine.Status = &status + } +} // VirtualizationVMInterfaceDataWrapper represents a virtualization VM interface data wrapper type VirtualizationVMInterfaceDataWrapper struct { @@ -1231,6 +1283,16 @@ type VirtualizationVMInterfaceDataWrapper struct { func (*VirtualizationVMInterfaceDataWrapper) comparableData() {} +// FromProtoEntity sets the data from a proto entity +func (vw *VirtualizationVMInterfaceDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + vmInterface, err := FromProtoVMInterfaceEntity(entity) + if err != nil { + return err + } + vw.VMInterface = vmInterface + return nil +} + // Data returns the DeviceRole func (vw *VirtualizationVMInterfaceDataWrapper) Data() any { return vw.VMInterface @@ -1477,6 +1539,16 @@ type VirtualizationVirtualDiskDataWrapper struct { func (*VirtualizationVirtualDiskDataWrapper) comparableData() {} +// FromProtoEntity sets the data from a proto entity +func (vw *VirtualizationVirtualDiskDataWrapper) FromProtoEntity(entity *diodepb.Entity) error { + virtualDisk, err := FromProtoVirtualDiskEntity(entity) + if err != nil { + return err + } + vw.VirtualDisk = virtualDisk + return nil +} + // Data returns the DeviceRole func (vw *VirtualizationVirtualDiskDataWrapper) Data() any { return vw.VirtualDisk diff --git a/diode-server/netbox/wrappers.go b/diode-server/netbox/wrappers.go index 05e5c2d2..33318cd1 100644 --- a/diode-server/netbox/wrappers.go +++ b/diode-server/netbox/wrappers.go @@ -3,1859 +3,93 @@ package netbox import ( "errors" "fmt" - "slices" - "github.com/gosimple/slug" "github.com/jinzhu/copier" "github.com/mitchellh/hashstructure/v2" -) - -// ComparableData is an interface for NetBox comparable data -type ComparableData interface { - comparableData() - - // Data returns the data - Data() any - - // IsValid checks if the data is not nil - IsValid() bool - - // Normalise normalises the data - Normalise() - - // NestedObjects returns all nested objects - NestedObjects() ([]ComparableData, error) - - // DataType returns the data type - DataType() string - - // ObjectStateQueryParams returns the query parameters needed to retrieve its object state - ObjectStateQueryParams() map[string]string - - // ID returns the ID of the data - ID() int - - // IsPlaceholder returns true if the data is a placeholder - IsPlaceholder() bool - - // SetDefaults sets the default values for the data - SetDefaults() - - // Patch creates patches between the actual, intended and current data - Patch(ComparableData, map[string]ComparableData) ([]ComparableData, error) - - // HasChanged returns true if the data has changed - HasChanged() bool -} - -// BaseDataWrapper is the base struct for all data wrappers -type BaseDataWrapper struct { - placeholder bool - hasParent bool - intended bool - hasChanged bool - nestedObjects []ComparableData - objectsToReconcile []ComparableData -} - -// IsPlaceholder returns true if the data is a placeholder -func (bw *BaseDataWrapper) IsPlaceholder() bool { - return bw.placeholder -} - -// HasChanged returns true if the data has changed -func (bw *BaseDataWrapper) HasChanged() bool { - return bw.hasChanged -} - -func copyData[T any](srcData *T) (*T, error) { - var dstData T - if err := copier.Copy(&dstData, srcData); err != nil { - return nil, err - } - return &dstData, nil -} - -// DcimDeviceDataWrapper represents a DCIM device data wrapper -type DcimDeviceDataWrapper struct { - BaseDataWrapper - Device *DcimDevice -} - -func (*DcimDeviceDataWrapper) comparableData() {} - -// Data returns the Device -func (dw *DcimDeviceDataWrapper) Data() any { - return dw.Device -} - -// IsValid returns true if the Device is not nil -func (dw *DcimDeviceDataWrapper) IsValid() bool { - if dw.Device != nil && !dw.hasParent && dw.Device.Name == "" { - dw.Device = nil - } - return dw.Device != nil -} - -// Normalise normalises the data -func (dw *DcimDeviceDataWrapper) Normalise() { - if dw.IsValid() && dw.Device.Tags != nil && len(dw.Device.Tags) == 0 { - dw.Device.Tags = nil - } - dw.intended = true -} - -// NestedObjects returns all nested objects -func (dw *DcimDeviceDataWrapper) NestedObjects() ([]ComparableData, error) { - if len(dw.nestedObjects) > 0 { - return dw.nestedObjects, nil - } - - if dw.Device != nil && dw.hasParent && dw.Device.Name == "" { - dw.Device = nil - } - - objects := make([]ComparableData, 0) - - if dw.Device == nil && dw.intended { - return objects, nil - } - - if dw.Device == nil && dw.hasParent { - dw.Device = NewDcimDevice() - dw.placeholder = true - } - - // Ignore primary IP addresses for time being - dw.Device.PrimaryIPv4 = nil - dw.Device.PrimaryIPv6 = nil - - site := DcimSiteDataWrapper{Site: dw.Device.Site, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} - - so, err := site.NestedObjects() - if err != nil { - return nil, err - } - - objects = append(objects, so...) - - dw.Device.Site = site.Site - - if dw.Device.Platform != nil { - platform := DcimPlatformDataWrapper{Platform: dw.Device.Platform, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} - - po, err := platform.NestedObjects() - if err != nil { - return nil, err - } - - objects = append(objects, po...) - - dw.Device.Platform = platform.Platform - } - - deviceType := DcimDeviceTypeDataWrapper{DeviceType: dw.Device.DeviceType, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} - - dto, err := deviceType.NestedObjects() - if err != nil { - return nil, err - } - - objects = append(objects, dto...) - - dw.Device.DeviceType = deviceType.DeviceType - - deviceRole := DcimDeviceRoleDataWrapper{DeviceRole: dw.Device.Role, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} - - dro, err := deviceRole.NestedObjects() - if err != nil { - return nil, err - } - - objects = append(objects, dro...) - - dw.Device.Role = deviceRole.DeviceRole - - if dw.Device.Tags != nil { - for _, t := range dw.Device.Tags { - if t.Slug == "" { - t.Slug = slug.Make(t.Name) - } - objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - dw.nestedObjects = objects - - objects = append(objects, dw) - - return objects, nil -} - -// DataType returns the data type -func (dw *DcimDeviceDataWrapper) DataType() string { - return DcimDeviceObjectType -} - -// ObjectStateQueryParams returns the query parameters needed to retrieve its object state -func (dw *DcimDeviceDataWrapper) ObjectStateQueryParams() map[string]string { - params := map[string]string{ - "q": dw.Device.Name, - } - if dw.Device.Site != nil { - params["site__name"] = dw.Device.Site.Name - } - return params -} - -// ID returns the ID of the data -func (dw *DcimDeviceDataWrapper) ID() int { - return dw.Device.ID -} - -// Patch creates patches between the actual, intended and current data -func (dw *DcimDeviceDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { - intended, ok := cmp.(*DcimDeviceDataWrapper) - if !ok && intended != nil { - return nil, errors.New("invalid data type") - } - - actualNestedObjectsMap := make(map[string]ComparableData) - for _, obj := range dw.nestedObjects { - actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj - } - - actualSite := extractFromObjectsMap(actualNestedObjectsMap, fmt.Sprintf("%p", dw.Device.Site)) - intendedSite := extractFromObjectsMap(intendedNestedObjects, fmt.Sprintf("%p", dw.Device.Site)) - - actualPlatform := extractFromObjectsMap(actualNestedObjectsMap, fmt.Sprintf("%p", dw.Device.Platform)) - intendedPlatform := extractFromObjectsMap(intendedNestedObjects, fmt.Sprintf("%p", dw.Device.Platform)) - - actualDeviceType := extractFromObjectsMap(actualNestedObjectsMap, fmt.Sprintf("%p", dw.Device.DeviceType)) - intendedDeviceType := extractFromObjectsMap(intendedNestedObjects, fmt.Sprintf("%p", dw.Device.DeviceType)) - - actualRole := extractFromObjectsMap(actualNestedObjectsMap, fmt.Sprintf("%p", dw.Device.Role)) - intendedRole := extractFromObjectsMap(intendedNestedObjects, fmt.Sprintf("%p", dw.Device.Role)) - - reconciliationRequired := true - - if intended != nil { - currentNestedObjectsMap := make(map[string]ComparableData) - currentNestedObjects, err := intended.NestedObjects() - if err != nil { - return nil, err - } - for _, obj := range currentNestedObjects { - currentNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj - } - - dw.Device.ID = intended.Device.ID - dw.Device.Name = intended.Device.Name - - if dw.Device.Status == nil || *dw.Device.Status == "" { - dw.Device.Status = intended.Device.Status - } - - if dw.Device.Description == nil { - dw.Device.Description = intended.Device.Description - } - - if dw.Device.Comments == nil { - dw.Device.Comments = intended.Device.Comments - } - - if dw.Device.AssetTag == nil { - dw.Device.AssetTag = intended.Device.AssetTag - } - - if dw.Device.Serial == nil { - dw.Device.Serial = intended.Device.Serial - } - - if actualSite.IsPlaceholder() && intended.Device.Site != nil { - intendedSite = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Device.Site)) - } - - siteObjectsToReconcile, siteErr := actualSite.Patch(intendedSite, intendedNestedObjects) - if siteErr != nil { - return nil, siteErr - } - - site, err := copyData(actualSite.Data().(*DcimSite)) - if err != nil { - return nil, err - } - site.Tags = nil - - if !actualSite.HasChanged() { - site = &DcimSite{ - ID: actualSite.ID(), - } - - intendedSiteID := intendedSite.ID() - if intended.Device.Site != nil { - intendedSiteID = intended.Device.Site.ID - } - - intended.Device.Site = &DcimSite{ - ID: intendedSiteID, - } - } - - dw.Device.Site = site - - dw.objectsToReconcile = append(dw.objectsToReconcile, siteObjectsToReconcile...) - - if actualPlatform != nil { - if actualPlatform.IsPlaceholder() && intended.Device.Platform != nil { - intendedPlatform = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Device.Platform)) - } - - platformObjectsToReconcile, platformErr := actualPlatform.Patch(intendedPlatform, intendedNestedObjects) - if platformErr != nil { - return nil, platformErr - } - - platform, err := copyData(actualPlatform.Data().(*DcimPlatform)) - if err != nil { - return nil, err - } - platform.Tags = nil - - if !actualPlatform.HasChanged() { - platform = &DcimPlatform{ - ID: actualPlatform.ID(), - } - - intendedPlatformID := intendedPlatform.ID() - if intended.Device.Platform != nil { - intendedPlatformID = intended.Device.Platform.ID - } - - intended.Device.Platform = &DcimPlatform{ - ID: intendedPlatformID, - } - } - - dw.Device.Platform = platform - - dw.objectsToReconcile = append(dw.objectsToReconcile, platformObjectsToReconcile...) - } else { - if intended.Device.Platform != nil { - platformID := intended.Device.Platform.ID - dw.Device.Platform = &DcimPlatform{ - ID: platformID, - } - intended.Device.Platform = &DcimPlatform{ - ID: platformID, - } - } - } - - if actualDeviceType.IsPlaceholder() && intended.Device.DeviceType != nil { - intendedDeviceType = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Device.DeviceType)) - } - - deviceTypeObjectsToReconcile, deviceTypeErr := actualDeviceType.Patch(intendedDeviceType, intendedNestedObjects) - if deviceTypeErr != nil { - return nil, deviceTypeErr - } - - deviceType, err := copyData(actualDeviceType.Data().(*DcimDeviceType)) - if err != nil { - return nil, err - } - deviceType.Tags = nil - - if !actualDeviceType.HasChanged() { - deviceType = &DcimDeviceType{ - ID: actualDeviceType.ID(), - } - - intendedDeviceTypeID := intendedDeviceType.ID() - if intended.Device.DeviceType != nil { - intendedDeviceTypeID = intended.Device.DeviceType.ID - } - - intended.Device.DeviceType = &DcimDeviceType{ - ID: intendedDeviceTypeID, - } - } - - dw.Device.DeviceType = deviceType - - dw.objectsToReconcile = append(dw.objectsToReconcile, deviceTypeObjectsToReconcile...) - - if actualRole.IsPlaceholder() && intended.Device.Role != nil { - intendedRole = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Device.Role)) - } - - roleObjectsToReconcile, roleErr := actualRole.Patch(intendedRole, intendedNestedObjects) - if roleErr != nil { - return nil, roleErr - } - - role, err := copyData(actualRole.Data().(*DcimDeviceRole)) - if err != nil { - return nil, err - } - role.Tags = nil - - if !actualRole.HasChanged() { - role = &DcimDeviceRole{ - ID: actualRole.ID(), - } - - intendedRoleID := intendedRole.ID() - if intended.Device.Role != nil { - intendedRoleID = intended.Device.Role.ID - } - - intended.Device.Role = &DcimDeviceRole{ - ID: intendedRoleID, - } - } - - dw.Device.Role = role - - dw.objectsToReconcile = append(dw.objectsToReconcile, roleObjectsToReconcile...) - - tagsToMerge := mergeTags(dw.Device.Tags, intended.Device.Tags, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.Device.Tags = tagsToMerge - } - - for _, t := range dw.Device.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) - intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) - - reconciliationRequired = actualHash != intendedHash - } else { - dw.SetDefaults() - - siteObjectsToReconcile, siteErr := actualSite.Patch(intendedSite, intendedNestedObjects) - if siteErr != nil { - return nil, siteErr - } - - site, err := copyData(actualSite.Data().(*DcimSite)) - if err != nil { - return nil, err - } - site.Tags = nil - - if !actualSite.HasChanged() { - site = &DcimSite{ - ID: actualSite.ID(), - } - } - dw.Device.Site = site - - dw.objectsToReconcile = append(dw.objectsToReconcile, siteObjectsToReconcile...) - - if actualPlatform != nil { - platformObjectsToReconcile, platformErr := actualPlatform.Patch(intendedPlatform, intendedNestedObjects) - if platformErr != nil { - return nil, platformErr - } - - platform, err := copyData(actualPlatform.Data().(*DcimPlatform)) - if err != nil { - return nil, err - } - platform.Tags = nil - - if !actualPlatform.HasChanged() { - platform = &DcimPlatform{ - ID: actualPlatform.ID(), - } - } - dw.Device.Platform = platform - - dw.objectsToReconcile = append(dw.objectsToReconcile, platformObjectsToReconcile...) - } - - deviceTypeObjectsToReconcile, deviceTypeErr := actualDeviceType.Patch(intendedDeviceType, intendedNestedObjects) - if deviceTypeErr != nil { - return nil, deviceTypeErr - } - - deviceType, err := copyData(actualDeviceType.Data().(*DcimDeviceType)) - if err != nil { - return nil, err - } - deviceType.Tags = nil - - if !actualDeviceType.HasChanged() { - deviceType = &DcimDeviceType{ - ID: actualDeviceType.ID(), - } - } - dw.Device.DeviceType = deviceType - - dw.objectsToReconcile = append(dw.objectsToReconcile, deviceTypeObjectsToReconcile...) - - roleObjectsToReconcile, roleErr := actualRole.Patch(intendedRole, intendedNestedObjects) - if roleErr != nil { - return nil, roleErr - } - - role, err := copyData(actualRole.Data().(*DcimDeviceRole)) - if err != nil { - return nil, err - } - role.Tags = nil - - if !actualRole.HasChanged() { - role = &DcimDeviceRole{ - ID: actualRole.ID(), - } - } - dw.Device.Role = role - - dw.objectsToReconcile = append(dw.objectsToReconcile, roleObjectsToReconcile...) - - tagsToMerge := mergeTags(dw.Device.Tags, nil, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.Device.Tags = tagsToMerge - } - - for _, t := range dw.Device.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - } - - if reconciliationRequired { - dw.hasChanged = true - dw.objectsToReconcile = append(dw.objectsToReconcile, dw) - } - - dedupObjectsToReconcile, err := dedupObjectsToReconcile(dw.objectsToReconcile) - if err != nil { - return nil, err - } - dw.objectsToReconcile = dedupObjectsToReconcile - - return dw.objectsToReconcile, nil -} - -// SetDefaults sets the default values for the device -func (dw *DcimDeviceDataWrapper) SetDefaults() { - if dw.Device.Status == nil || *dw.Device.Status == "" { - status := DcimDeviceStatusActive - dw.Device.Status = &status - } -} - -// DcimDeviceRoleDataWrapper represents a DCIM device role data wrapper -type DcimDeviceRoleDataWrapper struct { - BaseDataWrapper - DeviceRole *DcimDeviceRole -} - -func (*DcimDeviceRoleDataWrapper) comparableData() {} - -// Data returns the DeviceRole -func (dw *DcimDeviceRoleDataWrapper) Data() any { - return dw.DeviceRole -} - -// IsValid returns true if the DeviceRole is not nil -func (dw *DcimDeviceRoleDataWrapper) IsValid() bool { - if dw.DeviceRole != nil && !dw.hasParent && dw.DeviceRole.Name == "" { - dw.DeviceRole = nil - } - return dw.DeviceRole != nil -} - -// Normalise normalises the data -func (dw *DcimDeviceRoleDataWrapper) Normalise() { - if dw.IsValid() && dw.DeviceRole.Tags != nil && len(dw.DeviceRole.Tags) == 0 { - dw.DeviceRole.Tags = nil - } - dw.intended = true -} - -// NestedObjects returns all nested objects -func (dw *DcimDeviceRoleDataWrapper) NestedObjects() ([]ComparableData, error) { - if len(dw.nestedObjects) > 0 { - return dw.nestedObjects, nil - } - - if dw.DeviceRole != nil && dw.hasParent && dw.DeviceRole.Name == "" { - dw.DeviceRole = nil - } - - objects := make([]ComparableData, 0) - - if dw.DeviceRole == nil && dw.intended { - return objects, nil - } - - if dw.DeviceRole == nil && dw.hasParent { - dw.DeviceRole = NewDcimDeviceRole() - dw.placeholder = true - } - - if dw.DeviceRole.Slug == "" { - dw.DeviceRole.Slug = slug.Make(dw.DeviceRole.Name) - } - - if dw.DeviceRole.Tags != nil { - for _, t := range dw.DeviceRole.Tags { - if t.Slug == "" { - t.Slug = slug.Make(t.Name) - } - objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - dw.nestedObjects = objects - - objects = append(objects, dw) - - return objects, nil -} - -// DataType returns the data type -func (dw *DcimDeviceRoleDataWrapper) DataType() string { - return DcimDeviceRoleObjectType -} - -// ObjectStateQueryParams returns the query parameters needed to retrieve its object state -func (dw *DcimDeviceRoleDataWrapper) ObjectStateQueryParams() map[string]string { - return map[string]string{ - "q": dw.DeviceRole.Name, - } -} - -// ID returns the ID of the data -func (dw *DcimDeviceRoleDataWrapper) ID() int { - return dw.DeviceRole.ID -} - -// Patch creates patches between the actual, intended and current data -func (dw *DcimDeviceRoleDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { - intended, ok := cmp.(*DcimDeviceRoleDataWrapper) - if !ok && intended != nil { - return nil, errors.New("invalid data type") - } - - actualNestedObjectsMap := make(map[string]ComparableData) - for _, obj := range dw.nestedObjects { - actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj - } - - reconciliationRequired := true - - if intended != nil { - dw.DeviceRole.ID = intended.DeviceRole.ID - dw.DeviceRole.Name = intended.DeviceRole.Name - dw.DeviceRole.Slug = intended.DeviceRole.Slug - - if dw.IsPlaceholder() || dw.DeviceRole.Color == nil { - dw.DeviceRole.Color = intended.DeviceRole.Color - } - - if dw.DeviceRole.Description == nil { - dw.DeviceRole.Description = intended.DeviceRole.Description - } - - tagsToMerge := mergeTags(dw.DeviceRole.Tags, intended.DeviceRole.Tags, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.DeviceRole.Tags = tagsToMerge - } - - for _, t := range dw.DeviceRole.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) - intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) - - reconciliationRequired = actualHash != intendedHash - } else { - dw.SetDefaults() - - tagsToMerge := mergeTags(dw.DeviceRole.Tags, nil, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.DeviceRole.Tags = tagsToMerge - } - - for _, t := range dw.DeviceRole.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - } - - if reconciliationRequired { - dw.hasChanged = true - dw.objectsToReconcile = append(dw.objectsToReconcile, dw) - } - - return dw.objectsToReconcile, nil -} - -// SetDefaults sets the default values for the device role -func (dw *DcimDeviceRoleDataWrapper) SetDefaults() { - if dw.DeviceRole.Color == nil { - color := "000000" - dw.DeviceRole.Color = &color - } -} - -// DcimDeviceTypeDataWrapper represents a DCIM device type data wrapper -type DcimDeviceTypeDataWrapper struct { - BaseDataWrapper - DeviceType *DcimDeviceType -} - -func (*DcimDeviceTypeDataWrapper) comparableData() {} - -// Data returns the DeviceType -func (dw *DcimDeviceTypeDataWrapper) Data() any { - return dw.DeviceType -} - -// IsValid returns true if the DeviceType is not nil -func (dw *DcimDeviceTypeDataWrapper) IsValid() bool { - if dw.DeviceType != nil && !dw.hasParent && dw.DeviceType.Model == "" { - dw.DeviceType = nil - } - return dw.DeviceType != nil -} - -// Normalise normalises the data -func (dw *DcimDeviceTypeDataWrapper) Normalise() { - if dw.IsValid() && dw.DeviceType.Tags != nil && len(dw.DeviceType.Tags) == 0 { - dw.DeviceType.Tags = nil - } - dw.intended = true -} - -// DataType returns the data type -func (dw *DcimDeviceTypeDataWrapper) DataType() string { - return DcimDeviceTypeObjectType -} - -// ObjectStateQueryParams returns the query parameters needed to retrieve its object state -func (dw *DcimDeviceTypeDataWrapper) ObjectStateQueryParams() map[string]string { - params := map[string]string{ - "q": dw.DeviceType.Model, - } - if dw.DeviceType.Manufacturer != nil { - params["manufacturer__name"] = dw.DeviceType.Manufacturer.Name - } - return params -} - -// ID returns the ID of the data -func (dw *DcimDeviceTypeDataWrapper) ID() int { - return dw.DeviceType.ID -} - -// NestedObjects returns all nested objects -func (dw *DcimDeviceTypeDataWrapper) NestedObjects() ([]ComparableData, error) { - if len(dw.nestedObjects) > 0 { - return dw.nestedObjects, nil - } - - if dw.DeviceType != nil && dw.hasParent && dw.DeviceType.Model == "" { - dw.DeviceType = nil - } - - objects := make([]ComparableData, 0) - - if dw.DeviceType == nil && dw.intended { - return objects, nil - } - - if dw.DeviceType == nil && dw.hasParent { - dw.DeviceType = NewDcimDeviceType() - dw.placeholder = true - } - - if dw.DeviceType.Slug == "" { - dw.DeviceType.Slug = slug.Make(dw.DeviceType.Model) - } - - manufacturer := DcimManufacturerDataWrapper{Manufacturer: dw.DeviceType.Manufacturer, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} - - mo, err := manufacturer.NestedObjects() - if err != nil { - return nil, err - } - - objects = append(objects, mo...) - - dw.DeviceType.Manufacturer = manufacturer.Manufacturer - - if dw.DeviceType.Tags != nil && len(dw.DeviceType.Tags) == 0 { - dw.DeviceType.Tags = nil - } - - if dw.DeviceType.Tags != nil { - for _, t := range dw.DeviceType.Tags { - if t.Slug == "" { - t.Slug = slug.Make(t.Name) - } - objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - dw.nestedObjects = objects - - objects = append(objects, dw) - - return objects, nil -} - -// Patch creates patches between the actual, intended and current data -func (dw *DcimDeviceTypeDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { - intended, ok := cmp.(*DcimDeviceTypeDataWrapper) - if !ok && intended != nil { - return nil, errors.New("invalid data type") - } - - actualNestedObjectsMap := make(map[string]ComparableData) - for _, obj := range dw.nestedObjects { - actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj - } - - actualManufacturerKey := fmt.Sprintf("%p", dw.DeviceType.Manufacturer) - actualManufacturer := extractFromObjectsMap(actualNestedObjectsMap, actualManufacturerKey) - intendedManufacturer := extractFromObjectsMap(intendedNestedObjects, actualManufacturerKey) - - reconciliationRequired := true - - if intended != nil { - currentNestedObjectsMap := make(map[string]ComparableData) - currentNestedObjects, err := intended.NestedObjects() - if err != nil { - return nil, err - } - for _, obj := range currentNestedObjects { - currentNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj - } - - dw.DeviceType.ID = intended.DeviceType.ID - dw.DeviceType.Model = intended.DeviceType.Model - dw.DeviceType.Slug = intended.DeviceType.Slug - - if dw.DeviceType.Description == nil { - dw.DeviceType.Description = intended.DeviceType.Description - } - - if dw.DeviceType.Comments == nil { - dw.DeviceType.Comments = intended.DeviceType.Comments - } - - if dw.DeviceType.PartNumber == nil { - dw.DeviceType.PartNumber = intended.DeviceType.PartNumber - } - - if actualManufacturer.IsPlaceholder() && intended.DeviceType.Manufacturer != nil { - intendedManufacturer = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.DeviceType.Manufacturer)) - } - - manufacturerObjectsToReconcile, manufacturerErr := actualManufacturer.Patch(intendedManufacturer, intendedNestedObjects) - if manufacturerErr != nil { - return nil, manufacturerErr - } - - manufacturer, err := copyData(actualManufacturer.Data().(*DcimManufacturer)) - if err != nil { - return nil, err - } - manufacturer.Tags = nil - - if !actualManufacturer.HasChanged() { - manufacturer = &DcimManufacturer{ - ID: actualManufacturer.ID(), - } - - intendedManufacturerID := intendedManufacturer.ID() - if intended.DeviceType.Manufacturer != nil { - intendedManufacturerID = intended.DeviceType.Manufacturer.ID - } - - intended.DeviceType.Manufacturer = &DcimManufacturer{ - ID: intendedManufacturerID, - } - } - - dw.DeviceType.Manufacturer = manufacturer - - tagsToMerge := mergeTags(dw.DeviceType.Tags, intended.DeviceType.Tags, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.DeviceType.Tags = tagsToMerge - } - - for _, t := range dw.DeviceType.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - dw.objectsToReconcile = append(dw.objectsToReconcile, manufacturerObjectsToReconcile...) - - actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) - intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) - - reconciliationRequired = actualHash != intendedHash - } else { - manufacturerObjectsToReconcile, manufacturerErr := actualManufacturer.Patch(intendedManufacturer, intendedNestedObjects) - if manufacturerErr != nil { - return nil, manufacturerErr - } - - manufacturer, err := copyData(actualManufacturer.Data().(*DcimManufacturer)) - if err != nil { - return nil, err - } - manufacturer.Tags = nil - - if !actualManufacturer.HasChanged() { - manufacturer = &DcimManufacturer{ - ID: actualManufacturer.ID(), - } - } - dw.DeviceType.Manufacturer = manufacturer - - tagsToMerge := mergeTags(dw.DeviceType.Tags, nil, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.DeviceType.Tags = tagsToMerge - } - - for _, t := range dw.DeviceType.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - dw.objectsToReconcile = append(dw.objectsToReconcile, manufacturerObjectsToReconcile...) - } - - if reconciliationRequired { - dw.hasChanged = true - dw.objectsToReconcile = append(dw.objectsToReconcile, dw) - } - - return dw.objectsToReconcile, nil -} - -// SetDefaults sets the default values for the device type -func (dw *DcimDeviceTypeDataWrapper) SetDefaults() {} - -// DcimInterfaceDataWrapper represents a DCIM interface data wrapper -type DcimInterfaceDataWrapper struct { - BaseDataWrapper - Interface *DcimInterface -} - -func (*DcimInterfaceDataWrapper) comparableData() {} - -// Data returns the Interface -func (dw *DcimInterfaceDataWrapper) Data() any { - return dw.Interface -} - -// IsValid returns true if the Interface is not nil -func (dw *DcimInterfaceDataWrapper) IsValid() bool { - if dw.Interface != nil && !dw.hasParent && dw.Interface.Name == "" { - dw.Interface = nil - } - - if dw.Interface != nil { - if err := dw.Interface.Validate(); err != nil { - return false - } - } - - return dw.Interface != nil -} - -// Normalise normalises the data -func (dw *DcimInterfaceDataWrapper) Normalise() { - if dw.IsValid() && dw.Interface.Tags != nil && len(dw.Interface.Tags) == 0 { - dw.Interface.Tags = nil - } - dw.intended = true -} - -// NestedObjects returns all nested objects -func (dw *DcimInterfaceDataWrapper) NestedObjects() ([]ComparableData, error) { - if len(dw.nestedObjects) > 0 { - return dw.nestedObjects, nil - } - - if dw.Interface != nil && dw.hasParent && dw.Interface.Name == "" { - dw.Interface = nil - } - - objects := make([]ComparableData, 0) - - if dw.Interface == nil && dw.intended { - return objects, nil - } - - if dw.Interface == nil && dw.hasParent { - dw.Interface = NewDcimInterface() - dw.placeholder = true - } - - device := DcimDeviceDataWrapper{Device: dw.Interface.Device, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} - - do, err := device.NestedObjects() - if err != nil { - return nil, err - } - - objects = append(objects, do...) - - dw.Interface.Device = device.Device - - if dw.Interface.Tags != nil { - for _, t := range dw.Interface.Tags { - if t.Slug == "" { - t.Slug = slug.Make(t.Name) - } - objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - dw.nestedObjects = objects - - objects = append(objects, dw) - - return objects, nil -} - -// DataType returns the data type -func (dw *DcimInterfaceDataWrapper) DataType() string { - return DcimInterfaceObjectType -} - -// ObjectStateQueryParams returns the query parameters needed to retrieve its object state -func (dw *DcimInterfaceDataWrapper) ObjectStateQueryParams() map[string]string { - params := map[string]string{ - "q": dw.Interface.Name, - } - if dw.Interface.Device != nil { - params["device__name"] = dw.Interface.Device.Name - - if dw.Interface.Device.Site != nil { - params["device__site__name"] = dw.Interface.Device.Site.Name - } - } - return params -} - -// ID returns the ID of the data -func (dw *DcimInterfaceDataWrapper) ID() int { - return dw.Interface.ID -} - -func (dw *DcimInterfaceDataWrapper) hash() string { - var deviceName, siteName string - if dw.Interface.Device != nil { - deviceName = dw.Interface.Device.Name - if dw.Interface.Device.Site != nil { - siteName = dw.Interface.Device.Site.Name - } - } - return slug.Make(fmt.Sprintf("%s-%s-%s", dw.Interface.Name, deviceName, siteName)) -} - -// Patch creates patches between the actual, intended and current data -func (dw *DcimInterfaceDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { - intended, ok := cmp.(*DcimInterfaceDataWrapper) - if !ok && intended != nil { - return nil, errors.New("invalid data type") - } - - actualNestedObjectsMap := make(map[string]ComparableData) - for _, obj := range dw.nestedObjects { - actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj - } - - actualDevice := extractFromObjectsMap(actualNestedObjectsMap, fmt.Sprintf("%p", dw.Interface.Device)) - intendedDevice := extractFromObjectsMap(intendedNestedObjects, fmt.Sprintf("%p", dw.Interface.Device)) - - reconciliationRequired := true - - if intended != nil && dw.hash() == intended.hash() { - currentNestedObjectsMap := make(map[string]ComparableData) - currentNestedObjects, err := intended.NestedObjects() - if err != nil { - return nil, err - } - for _, obj := range currentNestedObjects { - currentNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj - } - - dw.Interface.ID = intended.Interface.ID - dw.Interface.Name = intended.Interface.Name - - if actualDevice.IsPlaceholder() && intended.Interface.Device != nil { - intendedDevice = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Interface.Device)) - } - - deviceObjectsToReconcile, deviceErr := actualDevice.Patch(intendedDevice, intendedNestedObjects) - if deviceErr != nil { - return nil, deviceErr - } - - device, err := copyData(actualDevice.Data().(*DcimDevice)) - if err != nil { - return nil, err - } - device.Tags = nil - - if !actualDevice.HasChanged() { - device = &DcimDevice{ - ID: actualDevice.ID(), - } - - intendedDeviceID := intendedDevice.ID() - if intended.Interface.Device != nil { - intendedDeviceID = intended.Interface.Device.ID - } - - intended.Interface.Device = &DcimDevice{ - ID: intendedDeviceID, - } - } - - dw.Interface.Device = device - - dw.objectsToReconcile = append(dw.objectsToReconcile, deviceObjectsToReconcile...) - - if dw.Interface.Label == nil { - dw.Interface.Label = intended.Interface.Label - } - - if dw.Interface.Type == nil { - dw.Interface.Type = intended.Interface.Type - } - - if dw.Interface.Enabled == nil { - dw.Interface.Enabled = intended.Interface.Enabled - } - - if dw.Interface.MTU == nil { - dw.Interface.MTU = intended.Interface.MTU - } - - if dw.Interface.MACAddress == nil { - dw.Interface.MACAddress = intended.Interface.MACAddress - } - - if dw.Interface.Speed == nil { - dw.Interface.Speed = intended.Interface.Speed - } - - if dw.Interface.WWN == nil { - dw.Interface.WWN = intended.Interface.WWN - } - - if dw.Interface.MgmtOnly == nil { - dw.Interface.MgmtOnly = intended.Interface.MgmtOnly - } - - if dw.Interface.Description == nil { - dw.Interface.Description = intended.Interface.Description - } - - if dw.Interface.MarkConnected == nil { - dw.Interface.MarkConnected = intended.Interface.MarkConnected - } - - if dw.Interface.Mode == nil { - dw.Interface.Mode = intended.Interface.Mode - } - - tagsToMerge := mergeTags(dw.Interface.Tags, intended.Interface.Tags, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.Interface.Tags = tagsToMerge - } - - for _, t := range dw.Interface.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) - intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) - - reconciliationRequired = actualHash != intendedHash - } else { - dw.SetDefaults() - - deviceObjectsToReconcile, deviceErr := actualDevice.Patch(intendedDevice, intendedNestedObjects) - if deviceErr != nil { - return nil, deviceErr - } - - device, err := copyData(actualDevice.Data().(*DcimDevice)) - if err != nil { - return nil, err - } - device.Tags = nil - - if !actualDevice.HasChanged() { - device = &DcimDevice{ - ID: actualDevice.ID(), - } - } - dw.Interface.Device = device - - tagsToMerge := mergeTags(dw.Interface.Tags, nil, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.Interface.Tags = tagsToMerge - } - - for _, t := range dw.Interface.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - dw.objectsToReconcile = append(dw.objectsToReconcile, deviceObjectsToReconcile...) - } - - if reconciliationRequired { - dw.hasChanged = true - dw.objectsToReconcile = append(dw.objectsToReconcile, dw) - } - - return dw.objectsToReconcile, nil -} - -// SetDefaults sets the default values for the interface -func (dw *DcimInterfaceDataWrapper) SetDefaults() { - if dw.Interface.Type == nil { - dw.Interface.Type = &DefaultInterfaceType - } -} - -// DcimManufacturerDataWrapper represents a DCIM manufacturer data wrapper -type DcimManufacturerDataWrapper struct { - BaseDataWrapper - Manufacturer *DcimManufacturer -} - -func (*DcimManufacturerDataWrapper) comparableData() {} - -// Data returns the Manufacturer -func (dw *DcimManufacturerDataWrapper) Data() any { - return dw.Manufacturer -} - -// IsValid returns true if the Manufacturer is not nil -func (dw *DcimManufacturerDataWrapper) IsValid() bool { - if dw.Manufacturer != nil && !dw.hasParent && dw.Manufacturer.Name == "" { - dw.Manufacturer = nil - } - return dw.Manufacturer != nil -} - -// Normalise normalises the data -func (dw *DcimManufacturerDataWrapper) Normalise() { - if dw.IsValid() && dw.Manufacturer.Tags != nil && len(dw.Manufacturer.Tags) == 0 { - dw.Manufacturer.Tags = nil - } - dw.intended = true -} - -// NestedObjects returns all nested objects -func (dw *DcimManufacturerDataWrapper) NestedObjects() ([]ComparableData, error) { - if len(dw.nestedObjects) > 0 { - return dw.nestedObjects, nil - } - - if dw.Manufacturer != nil && dw.hasParent && dw.Manufacturer.Name == "" { - dw.Manufacturer = nil - } - - objects := make([]ComparableData, 0) - - if dw.Manufacturer == nil && dw.intended { - return objects, nil - } - - if dw.Manufacturer == nil && dw.hasParent { - dw.Manufacturer = NewDcimManufacturer() - dw.placeholder = true - } - - if dw.Manufacturer.Slug == "" { - dw.Manufacturer.Slug = slug.Make(dw.Manufacturer.Name) - } - - if dw.Manufacturer.Tags != nil { - for _, t := range dw.Manufacturer.Tags { - if t.Slug == "" { - t.Slug = slug.Make(t.Name) - } - objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - dw.nestedObjects = objects - - objects = append(objects, dw) - - return objects, nil -} - -// DataType returns the data type -func (dw *DcimManufacturerDataWrapper) DataType() string { - return DcimManufacturerObjectType -} - -// ObjectStateQueryParams returns the query parameters needed to retrieve its object state -func (dw *DcimManufacturerDataWrapper) ObjectStateQueryParams() map[string]string { - return map[string]string{ - "q": dw.Manufacturer.Name, - } -} - -// ID returns the ID of the data -func (dw *DcimManufacturerDataWrapper) ID() int { - return dw.Manufacturer.ID -} - -// Patch creates patches between the actual, intended and current data -func (dw *DcimManufacturerDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { - intended, ok := cmp.(*DcimManufacturerDataWrapper) - - if !ok && intended != nil { - return nil, errors.New("invalid data type") - } - - reconciliationRequired := true - - if intended != nil { - dw.Manufacturer.ID = intended.Manufacturer.ID - dw.Manufacturer.Name = intended.Manufacturer.Name - dw.Manufacturer.Slug = intended.Manufacturer.Slug - - if dw.Manufacturer.Description == nil { - dw.Manufacturer.Description = intended.Manufacturer.Description - } - - tagsToMerge := mergeTags(dw.Manufacturer.Tags, intended.Manufacturer.Tags, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.Manufacturer.Tags = tagsToMerge - } - - actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) - intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) - - reconciliationRequired = actualHash != intendedHash - } else { - tagsToMerge := mergeTags(dw.Manufacturer.Tags, nil, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.Manufacturer.Tags = tagsToMerge - } - } - - for _, t := range dw.Manufacturer.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - if reconciliationRequired { - dw.hasChanged = true - dw.objectsToReconcile = append(dw.objectsToReconcile, dw) - } - - return dw.objectsToReconcile, nil -} - -func mergeTags(actualTags []*Tag, intendedTags []*Tag, intendedNestedObjects map[string]ComparableData) []*Tag { - tagsToMerge := make([]*Tag, 0) - tagsToCreate := make([]*Tag, 0) - - tagNamesToMerge := make([]string, 0) - tagNamesToCreate := make([]string, 0) - - for _, t := range intendedTags { - if !slices.Contains(tagNamesToMerge, t.Name) { - tagNamesToMerge = append(tagNamesToMerge, t.Name) - tagsToMerge = append(tagsToMerge, t) - } - } - - for _, t := range actualTags { - tagKey := fmt.Sprintf("%p", t) - tagWrapper := extractFromObjectsMap(intendedNestedObjects, tagKey) - - if !slices.Contains(tagNamesToMerge, t.Name) && tagWrapper != nil { - tagNamesToMerge = append(tagNamesToMerge, t.Name) - tagsToMerge = append(tagsToMerge, tagWrapper.Data().(*Tag)) - continue - } - - if tagWrapper == nil { - if !slices.Contains(tagNamesToCreate, t.Name) { - tagNamesToCreate = append(tagNamesToCreate, t.Name) - tagsToCreate = append(tagsToCreate, t) - } - } - } - - return append(tagsToMerge, tagsToCreate...) -} - -// SetDefaults sets the default values for the manufacturer -func (dw *DcimManufacturerDataWrapper) SetDefaults() {} - -// DcimPlatformDataWrapper represents a DCIM platform data wrapper -type DcimPlatformDataWrapper struct { - BaseDataWrapper - Platform *DcimPlatform -} - -func (*DcimPlatformDataWrapper) comparableData() {} - -// Data returns the Platform -func (dw *DcimPlatformDataWrapper) Data() any { - return dw.Platform -} - -// IsValid returns true if the Platform is not nil -func (dw *DcimPlatformDataWrapper) IsValid() bool { - if dw.Platform != nil && !dw.hasParent && dw.Platform.Name == "" { - dw.Platform = nil - } - return dw.Platform != nil -} - -// Normalise normalises the data -func (dw *DcimPlatformDataWrapper) Normalise() { - if dw.IsValid() && dw.Platform.Tags != nil && len(dw.Platform.Tags) == 0 { - dw.Platform.Tags = nil - } - dw.intended = true -} - -// NestedObjects returns all nested objects -func (dw *DcimPlatformDataWrapper) NestedObjects() ([]ComparableData, error) { - if len(dw.nestedObjects) > 0 { - return dw.nestedObjects, nil - } - - if dw.Platform != nil && dw.hasParent && dw.Platform.Name == "" { - dw.Platform = nil - } - - objects := make([]ComparableData, 0) - - if dw.Platform == nil && dw.intended { - return objects, nil - } - - if dw.Platform == nil && dw.hasParent { - dw.Platform = NewDcimPlatform() - dw.placeholder = true - } - - if dw.Platform.Slug == "" { - dw.Platform.Slug = slug.Make(dw.Platform.Name) - } - - if dw.Platform.Manufacturer != nil { - manufacturer := DcimManufacturerDataWrapper{Manufacturer: dw.Platform.Manufacturer, BaseDataWrapper: BaseDataWrapper{placeholder: dw.placeholder, hasParent: true, intended: dw.intended}} - - mo, err := manufacturer.NestedObjects() - if err != nil { - return nil, err - } - - objects = append(objects, mo...) - - dw.Platform.Manufacturer = manufacturer.Manufacturer - } - - if dw.Platform.Tags != nil { - for _, t := range dw.Platform.Tags { - if t.Slug == "" { - t.Slug = slug.Make(t.Name) - } - objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - dw.nestedObjects = objects - - objects = append(objects, dw) - - return objects, nil -} - -// DataType returns the data type -func (dw *DcimPlatformDataWrapper) DataType() string { - return DcimPlatformObjectType -} - -// ObjectStateQueryParams returns the query parameters needed to retrieve its object state -func (dw *DcimPlatformDataWrapper) ObjectStateQueryParams() map[string]string { - params := map[string]string{ - "q": dw.Platform.Name, - } - if dw.Platform.Manufacturer != nil { - params["manufacturer__name"] = dw.Platform.Manufacturer.Name - } - return params -} - -// ID returns the ID of the data -func (dw *DcimPlatformDataWrapper) ID() int { - return dw.Platform.ID -} - -// Patch creates patches between the actual, intended and current data -func (dw *DcimPlatformDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { - intended, ok := cmp.(*DcimPlatformDataWrapper) - if !ok && intended != nil { - return nil, errors.New("invalid data type") - } - - actualNestedObjectsMap := make(map[string]ComparableData) - for _, obj := range dw.nestedObjects { - actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj - } - - actualManufacturerKey := fmt.Sprintf("%p", dw.Platform.Manufacturer) - actualManufacturer := extractFromObjectsMap(actualNestedObjectsMap, actualManufacturerKey) - intendedManufacturer := extractFromObjectsMap(intendedNestedObjects, actualManufacturerKey) - - reconciliationRequired := true - - if intended != nil { - currentNestedObjectsMap := make(map[string]ComparableData) - currentNestedObjects, err := intended.NestedObjects() - if err != nil { - return nil, err - } - for _, obj := range currentNestedObjects { - currentNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj - } - - dw.Platform.ID = intended.Platform.ID - dw.Platform.Name = intended.Platform.Name - dw.Platform.Slug = intended.Platform.Slug - - if actualManufacturer != nil { - if actualManufacturer.IsPlaceholder() && intended.Platform.Manufacturer != nil { - intendedManufacturer = extractFromObjectsMap(currentNestedObjectsMap, fmt.Sprintf("%p", intended.Platform.Manufacturer)) - } - - manufacturerObjectsToReconcile, manufacturerErr := actualManufacturer.Patch(intendedManufacturer, intendedNestedObjects) - if manufacturerErr != nil { - return nil, manufacturerErr - } - - manufacturer, err := copyData(actualManufacturer.Data().(*DcimManufacturer)) - if err != nil { - return nil, err - } - manufacturer.Tags = nil - - if !actualManufacturer.HasChanged() { - manufacturer = &DcimManufacturer{ - ID: actualManufacturer.ID(), - } - - intendedManufacturerID := intendedManufacturer.ID() - if intended.Platform.Manufacturer != nil { - intendedManufacturerID = intended.Platform.Manufacturer.ID - } - - intended.Platform.Manufacturer = &DcimManufacturer{ - ID: intendedManufacturerID, - } - } - - dw.Platform.Manufacturer = manufacturer - - dw.objectsToReconcile = append(dw.objectsToReconcile, manufacturerObjectsToReconcile...) - } else { - if intended.Platform.Manufacturer != nil { - manufacturerID := intended.Platform.Manufacturer.ID - - dw.Platform.Manufacturer = &DcimManufacturer{ - ID: manufacturerID, - } - intended.Platform.Manufacturer = &DcimManufacturer{ - ID: manufacturerID, - } - } - } - - if dw.Platform.Description == nil { - dw.Platform.Description = intended.Platform.Description - } - - tagsToMerge := mergeTags(dw.Platform.Tags, intended.Platform.Tags, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.Platform.Tags = tagsToMerge - } - - for _, t := range dw.Platform.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) - intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) - reconciliationRequired = actualHash != intendedHash - } else { - if actualManufacturer != nil { - manufacturerObjectsToReconcile, manufacturerErr := actualManufacturer.Patch(intendedManufacturer, intendedNestedObjects) - if manufacturerErr != nil { - return nil, manufacturerErr - } - - manufacturer, err := copyData(actualManufacturer.Data().(*DcimManufacturer)) - if err != nil { - return nil, err - } - manufacturer.Tags = nil - - if !actualManufacturer.HasChanged() { - manufacturer = &DcimManufacturer{ - ID: actualManufacturer.ID(), - } - } - dw.Platform.Manufacturer = manufacturer - - dw.objectsToReconcile = append(dw.objectsToReconcile, manufacturerObjectsToReconcile...) - } - - tagsToMerge := mergeTags(dw.Platform.Tags, nil, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.Platform.Tags = tagsToMerge - } - - for _, t := range dw.Platform.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - } - - if reconciliationRequired { - dw.hasChanged = true - dw.objectsToReconcile = append(dw.objectsToReconcile, dw) - } - - return dw.objectsToReconcile, nil -} - -// SetDefaults sets the default values for the platform -func (dw *DcimPlatformDataWrapper) SetDefaults() {} - -// DcimSiteDataWrapper represents a DCIM site data wrapper -type DcimSiteDataWrapper struct { - BaseDataWrapper - Site *DcimSite -} + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" +) -func (*DcimSiteDataWrapper) comparableData() {} +const ( + // ExtrasTagObjectType represents the tag object type + ExtrasTagObjectType = "extras.tag" +) -// Data returns the Site -func (dw *DcimSiteDataWrapper) Data() any { - return dw.Site -} +// ComparableData is an interface for NetBox comparable data +type ComparableData interface { + comparableData() -// IsValid returns true if the Site is not nil -func (dw *DcimSiteDataWrapper) IsValid() bool { - if dw.Site != nil && !dw.hasParent && dw.Site.Name == "" { - dw.Site = nil - } - return dw.Site != nil -} + // FromProtoEntity sets the data from a proto entity + FromProtoEntity(protoData *diodepb.Entity) error -// Normalise normalises the data -func (dw *DcimSiteDataWrapper) Normalise() { - if dw.IsValid() && dw.Site.Tags != nil && len(dw.Site.Tags) == 0 { - dw.Site.Tags = nil - } - dw.intended = true -} + // Data returns the data + Data() any -// NestedObjects returns all nested objects -func (dw *DcimSiteDataWrapper) NestedObjects() ([]ComparableData, error) { - if len(dw.nestedObjects) > 0 { - return dw.nestedObjects, nil - } + // IsValid checks if the data is not nil + IsValid() bool - if dw.Site != nil && dw.hasParent && dw.Site.Name == "" { - dw.Site = nil - } + // Normalise normalises the data + Normalise() - objects := make([]ComparableData, 0) + // NestedObjects returns all nested objects + NestedObjects() ([]ComparableData, error) - if dw.Site == nil && dw.intended { - return objects, nil - } + // DataType returns the data type + DataType() string - if dw.Site == nil && dw.hasParent { - dw.Site = NewDcimSite() - dw.placeholder = true - } + // ObjectStateQueryParams returns the query parameters needed to retrieve its object state + ObjectStateQueryParams() map[string]string - if dw.Site.Slug == "" { - dw.Site.Slug = slug.Make(dw.Site.Name) - } + // ID returns the ID of the data + ID() int - if dw.Site.Tags != nil { - for _, t := range dw.Site.Tags { - if t.Slug == "" { - t.Slug = slug.Make(t.Name) - } - objects = append(objects, &TagDataWrapper{Tag: t, hasParent: true}) - } - } + // IsPlaceholder returns true if the data is a placeholder + IsPlaceholder() bool - dw.nestedObjects = objects + // SetDefaults sets the default values for the data + SetDefaults() - objects = append(objects, dw) + // Patch creates patches between the actual, intended and current data + Patch(ComparableData, map[string]ComparableData) ([]ComparableData, error) - return objects, nil + // HasChanged returns true if the data has changed + HasChanged() bool } -// DataType returns the data type -func (dw *DcimSiteDataWrapper) DataType() string { - return DcimSiteObjectType +// BaseDataWrapper is the base struct for all data wrappers +type BaseDataWrapper struct { + placeholder bool + hasParent bool + intended bool + hasChanged bool + nestedObjects []ComparableData + objectsToReconcile []ComparableData } -// ObjectStateQueryParams returns the query parameters needed to retrieve its object state -func (dw *DcimSiteDataWrapper) ObjectStateQueryParams() map[string]string { - return map[string]string{ - "q": dw.Site.Name, - } +// IsPlaceholder returns true if the data is a placeholder +func (bw *BaseDataWrapper) IsPlaceholder() bool { + return bw.placeholder } -// ID returns the ID of the data -func (dw *DcimSiteDataWrapper) ID() int { - return dw.Site.ID +// HasChanged returns true if the data has changed +func (bw *BaseDataWrapper) HasChanged() bool { + return bw.hasChanged } -// Patch creates patches between the actual, intended and current data -func (dw *DcimSiteDataWrapper) Patch(cmp ComparableData, intendedNestedObjects map[string]ComparableData) ([]ComparableData, error) { - intended, ok := cmp.(*DcimSiteDataWrapper) - if !ok && intended != nil { - return nil, errors.New("invalid data type") - } - - actualNestedObjectsMap := make(map[string]ComparableData) - for _, obj := range dw.nestedObjects { - actualNestedObjectsMap[fmt.Sprintf("%p", obj.Data())] = obj - } - - reconciliationRequired := true - - if intended != nil { - dw.Site.ID = intended.Site.ID - dw.Site.Name = intended.Site.Name - dw.Site.Slug = intended.Site.Slug - - if dw.Site.Status == nil || *dw.Site.Status == "" { - dw.Site.Status = intended.Site.Status - } - - if dw.Site.Facility == nil { - dw.Site.Facility = intended.Site.Facility - } - - if dw.Site.TimeZone == nil { - dw.Site.TimeZone = intended.Site.TimeZone - } - - if dw.Site.Description == nil { - dw.Site.Description = intended.Site.Description - } - - if dw.Site.Comments == nil { - dw.Site.Comments = intended.Site.Comments - } - - tagsToMerge := mergeTags(dw.Site.Tags, intended.Site.Tags, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.Site.Tags = tagsToMerge - } - - for _, t := range dw.Site.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - - actualHash, _ := hashstructure.Hash(dw.Data(), hashstructure.FormatV2, nil) - intendedHash, _ := hashstructure.Hash(intended.Data(), hashstructure.FormatV2, nil) - - reconciliationRequired = actualHash != intendedHash - } else { - dw.SetDefaults() - - tagsToMerge := mergeTags(dw.Site.Tags, nil, intendedNestedObjects) - - if len(tagsToMerge) > 0 { - dw.Site.Tags = tagsToMerge - } - - for _, t := range dw.Site.Tags { - if t.ID == 0 { - dw.objectsToReconcile = append(dw.objectsToReconcile, &TagDataWrapper{Tag: t, hasParent: true}) - } - } - } - - if reconciliationRequired { - dw.hasChanged = true - dw.objectsToReconcile = append(dw.objectsToReconcile, dw) +func copyData[T any](srcData *T) (*T, error) { + var dstData T + if err := copier.Copy(&dstData, srcData); err != nil { + return nil, err } - - return dw.objectsToReconcile, nil + return &dstData, nil } -// SetDefaults sets the default values for the site -func (dw *DcimSiteDataWrapper) SetDefaults() { - if dw.Site.Status == nil || *dw.Site.Status == "" { - status := DcimSiteStatusActive - dw.Site.Status = &status - } +// Tag represents a tag +type Tag struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Color string `json:"color,omitempty"` } // TagDataWrapper represents a tag data wrapper @@ -1868,6 +102,11 @@ type TagDataWrapper struct { func (*TagDataWrapper) comparableData() {} +// FromProtoEntity sets the data from a proto entity +func (dw *TagDataWrapper) FromProtoEntity(*diodepb.Entity) error { + return nil +} + // Data returns the Tag func (dw *TagDataWrapper) Data() any { return dw.Tag @@ -1926,6 +165,24 @@ func (dw *TagDataWrapper) Patch(cmp ComparableData, _ map[string]ComparableData) // SetDefaults sets the default values for the platform func (dw *TagDataWrapper) SetDefaults() {} +// FromProtoTags converts a slice of diode tags to a slice of NetBox tags +func FromProtoTags(tagsPb []*diodepb.Tag) []*Tag { + if tagsPb == nil { + return nil + } + + var tags []*Tag + for _, tagPb := range tagsPb { + tags = append(tags, &Tag{ + Name: tagPb.Name, + Slug: tagPb.Slug, + Color: tagPb.Color, + }) + } + + return tags +} + // NewDataWrapper creates a new data wrapper for the given data type func NewDataWrapper(dataType string) (ComparableData, error) { switch dataType { @@ -1990,3 +247,13 @@ func dedupObjectsToReconcile(objects []ComparableData) ([]ComparableData, error) return dedupedObjectsToReconcile, nil } + +// int32PtrToIntPtr converts int32 pointer to int pointer +func int32PtrToIntPtr(v *int32) *int { + var i *int + if v != nil { + i = new(int) + *i = int(*v) + } + return i +} diff --git a/diode-server/reconciler/changeset/changeset.go b/diode-server/reconciler/changeset/changeset.go index 16a51d71..0addff20 100644 --- a/diode-server/reconciler/changeset/changeset.go +++ b/diode-server/reconciler/changeset/changeset.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "github.com/mitchellh/mapstructure" + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" "github.com/netboxlabs/diode/diode-server/netbox" "github.com/netboxlabs/diode/diode-server/netboxdiodeplugin" ) @@ -156,18 +157,13 @@ func extractIngestEntityData(ingestEntity IngestEntity) (netbox.ComparableData, return nil, err } - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - Result: &dw, - DecodeHook: mapstructure.ComposeDecodeHookFunc( - netbox.IpamIPAddressAssignedObjectHookFunc(), - ), - }) - if err != nil { - return nil, err + protoEntity, ok := ingestEntity.Entity.(*diodepb.Entity) + if !ok { + return nil, fmt.Errorf("ingest entity is not a proto entity") } - if err := decoder.Decode(ingestEntity.Entity); err != nil { - return nil, fmt.Errorf("failed to decode ingest entity %w", err) + if err = dw.FromProtoEntity(protoEntity); err != nil { + return nil, err } if !dw.IsValid() { diff --git a/diode-server/reconciler/changeset/changeset_dcim_test.go b/diode-server/reconciler/changeset/changeset_dcim_test.go index 0992feaf..731ca5f3 100644 --- a/diode-server/reconciler/changeset/changeset_dcim_test.go +++ b/diode-server/reconciler/changeset/changeset_dcim_test.go @@ -2,12 +2,12 @@ package changeset_test import ( "context" - "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" "github.com/netboxlabs/diode/diode-server/netbox" "github.com/netboxlabs/diode/diode-server/netboxdiodeplugin" "github.com/netboxlabs/diode/diode-server/netboxdiodeplugin/mocks" @@ -24,23 +24,24 @@ func TestDcimPrepare(t *testing.T) { } tests := []struct { name string - rawIngestEntity []byte + ingestEntity changeset.IngestEntity retrieveObjectStates []mockRetrieveObjectState wantChangeSet changeset.ChangeSet wantErr bool }{ { name: "[P1] ingest dcim.site with name only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.site", - "entity": { - "Site": { - "name": "Site A" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.site", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Site{ + Site: &diodepb.Site{ + Name: "Site A", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -73,16 +74,17 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P1] ingest dcim.site with name only - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.site", - "entity": { - "Site": { - "name": "Site A" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.site", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Site{ + Site: &diodepb.Site{ + Name: "Site A", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -107,24 +109,25 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P1] ingest dcim.site with tags - existing object found - update with new tags", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.site", - "entity": { - "Site": { - "name": "Site A", - "tags": [ - { - "name": "tag 1" - }, - { - "name": "tag 2" - } - ] - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.site", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Site{ + Site: &diodepb.Site{ + Name: "Site A", + Tags: []*diodepb.Tag{ + { + Name: "tag 1", + }, + { + Name: "tag 2", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -224,14 +227,15 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P1] ingest empty dcim.site - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.site", - "entity": { - "Site": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.site", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Site{ + Site: &diodepb.Site{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -241,16 +245,17 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P2] ingest dcim.devicerole with name only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicerole", - "entity": { - "DeviceRole": { - "name": "WAN Router" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicerole", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceRole{ + DeviceRole: &diodepb.Role{ + Name: "WAN Router", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.devicerole", @@ -283,16 +288,17 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P2] ingest dcim.devicerole with name only - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicerole", - "entity": { - "DeviceRole": { - "name": "WAN Router" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicerole", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceRole{ + DeviceRole: &diodepb.Role{ + Name: "WAN Router", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.devicerole", @@ -317,17 +323,18 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P2] ingest dcim.devicerole with name and new description - existing object found - update", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicerole", - "entity": { - "DeviceRole": { - "name": "WAN Router", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicerole", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceRole{ + DeviceRole: &diodepb.Role{ + Name: "WAN Router", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.devicerole", @@ -368,18 +375,19 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P2] ingest dcim.devicerole with same color - existing object found - nothing to update", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicerole", - "entity": { - "DeviceRole": { - "name": "WAN Router", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "color": "111222" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicerole", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceRole{ + DeviceRole: &diodepb.Role{ + Name: "WAN Router", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + Color: "111222", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.devicerole", @@ -405,14 +413,15 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P2] ingest empty dcim.devicerole - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicerole", - "entity": { - "DeviceRole": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicerole", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceRole{ + DeviceRole: &diodepb.Role{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -422,16 +431,17 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P3] ingest dcim.manufacturer with name only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.manufacturer", - "entity": { - "Manufacturer": { - "name": "Cisco" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.manufacturer", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Manufacturer{ + Manufacturer: &diodepb.Manufacturer{ + Name: "Cisco", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.manufacturer", @@ -463,16 +473,17 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P3] ingest dcim.manufacturer with name only - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.manufacturer", - "entity": { - "Manufacturer": { - "name": "Cisco" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.manufacturer", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Manufacturer{ + Manufacturer: &diodepb.Manufacturer{ + Name: "Cisco", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.manufacturer", @@ -496,14 +507,15 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P3] ingest empty dcim.manufacturer - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.manufacturer", - "entity": { - "Manufacturer": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.manufacturer", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Manufacturer{ + Manufacturer: &diodepb.Manufacturer{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -513,16 +525,17 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P4] ingest dcim.devicetype with model only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicetype", - "entity": { - "DeviceType": { - "model": "ISR4321" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicetype", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceType{ + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.manufacturer", @@ -578,16 +591,17 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P4] ingest dcim.devicetype with model only - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicetype", - "entity": { - "DeviceType": { - "model": "ISR4321" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicetype", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceType{ + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.manufacturer", @@ -630,14 +644,15 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P4] ingest empty dcim.devicetype - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicetype", - "entity": { - "DeviceType": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicetype", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceType{ + DeviceType: &diodepb.DeviceType{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -647,21 +662,22 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P5] ingest dcim.devicetype with manufacturer - existing object not found - create manufacturer and devicetype", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicetype", - "entity": { - "DeviceType": { - "model": "ISR4321", - "manufacturer": { - "name": "Cisco" - }, - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "part_number": "xyz123" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicetype", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceType{ + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + Manufacturer: &diodepb.Manufacturer{ + Name: "Cisco", + }, + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + PartNumber: strPtr("xyz123"), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.manufacturer", @@ -719,37 +735,38 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P5] ingest dcim.devicetype with new manufacturer - existing object found - create manufacturer and update devicetype", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicetype", - "entity": { - "DeviceType": { - "model": "ISR4321", - "manufacturer": { - "name": "Cisco", - "tags": [ - { - "name": "tag 1" + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicetype", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceType{ + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + Manufacturer: &diodepb.Manufacturer{ + Name: "Cisco", + Tags: []*diodepb.Tag{ + { + Name: "tag 1", + }, + { + Name: "tag 10", + }, + { + Name: "tag 11", + }, }, + }, + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + PartNumber: strPtr("xyz123"), + Tags: []*diodepb.Tag{ { - "name": "tag 10" + Name: "tag 3", }, - { - "name": "tag 11" - } - ] - }, - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "part_number": "xyz123", - "tags": [ - { - "name": "tag 3" - } - ] - } - }, - "state": 0 - }`), + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.manufacturer", @@ -949,23 +966,24 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P5.2] ingest dcim.devicetype with new manufacturer - existing object found - create manufacturer and update devicetype", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicetype", - "entity": { - "DeviceType": { - "model": "ISR4321", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "part_number": "xyz123", - "tags": [ - { - "name": "tag 3" - } - ] - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicetype", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceType{ + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + PartNumber: strPtr("xyz123"), + Tags: []*diodepb.Tag{ + { + Name: "tag 3", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.manufacturer", @@ -1079,26 +1097,27 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P5.3] ingest dcim.devicetype with new manufacturer - existing object found - update devicetype with new existing manufacturer", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicetype", - "entity": { - "DeviceType": { - "model": "ISR4321", - "manufacturer": { - "name": "Cisco" - }, - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "part_number": "xyz123", - "tags": [ - { - "name": "tag 3" - } - ] - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicetype", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceType{ + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + Manufacturer: &diodepb.Manufacturer{ + Name: "Cisco", + }, + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + PartNumber: strPtr("xyz123"), + Tags: []*diodepb.Tag{ + { + Name: "tag 3", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.manufacturer", @@ -1212,16 +1231,17 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P6] ingest dcim.device with name only - existing object not found - create device and all related objects (using placeholders)", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1357,16 +1377,17 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P6] ingest dcim.device with name only - existing object and its related objects found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1471,17 +1492,18 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P6] ingest dcim.device with empty site", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01", - "site": {} - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + Site: &diodepb.Site{}, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1586,33 +1608,34 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P7] ingest dcim.device - existing object not found - create device and all related objects", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01", - "device_type": { - "model": "ISR4321" - }, - "role": { - "name": "WAN Router" - }, - "site": { - "name": "Site A" - }, - "status": "active", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "serial": "123456", - "tags": [ - { - "name": "tag 1" - } - ] - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + }, + Role: &diodepb.Role{ + Name: "WAN Router", + }, + Site: &diodepb.Site{ + Name: "Site A", + }, + Status: "active", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + Serial: strPtr("123456"), + Tags: []*diodepb.Tag{ + { + Name: "tag 1", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1770,36 +1793,37 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P7] ingest dcim.device with device type having manufacturer defined - existing object not found - create device and all related objects", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01", - "device_type": { - "model": "ISR4321", - "manufacturer": { - "name": "Cisco" - } - }, - "role": { - "name": "WAN Router" - }, - "site": { - "name": "Site A" - }, - "status": "active", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "serial": "123456", - "tags": [ - { - "name": "tag 1" - } - ] - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + Manufacturer: &diodepb.Manufacturer{ + Name: "Cisco", + }, + }, + Role: &diodepb.Role{ + Name: "WAN Router", + }, + Site: &diodepb.Site{ + Name: "Site A", + }, + Status: "active", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + Serial: strPtr("123456"), + Tags: []*diodepb.Tag{ + { + Name: "tag 1", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1957,14 +1981,15 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P6] ingest empty dcim.device - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -1974,33 +1999,34 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P7] ingest dcim.device - existing object found - create missing related objects and update device", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01", - "device_type": { - "model": "ISR4321" - }, - "role": { - "name": "WAN Router" - }, - "site": { - "name": "Site A" - }, - "status": "active", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "serial": "123456", - "tags": [ - { - "name": "tag 1" - } - ] - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + }, + Role: &diodepb.Role{ + Name: "WAN Router", + }, + Site: &diodepb.Site{ + Name: "Site A", + }, + Status: "active", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + Serial: strPtr("123456"), + Tags: []*diodepb.Tag{ + { + Name: "tag 1", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -2191,31 +2217,32 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P8] ingest dcim.device - existing object not found - create device and all related objects", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01", - "device_type": { - "model": "ISR4321" - }, - "role": { - "name": "WAN Router" - }, - "site": { - "name": "Site A" - }, - "platform": { - "name": "Cisco IOS 15.6" - }, - "status": "active", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "serial": "123456" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + }, + Role: &diodepb.Role{ + Name: "WAN Router", + }, + Site: &diodepb.Site{ + Name: "Site A", + }, + Platform: &diodepb.Platform{ + Name: "Cisco IOS 15.6", + }, + Status: "active", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + Serial: strPtr("123456"), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -2377,31 +2404,32 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P8] ingest dcim.device - existing object found - create missing related objects and update device", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01", - "device_type": { - "model": "ISR4321" - }, - "role": { - "name": "WAN Router" - }, - "site": { - "name": "Site A" - }, - "platform": { - "name": "Cisco IOS 15.6" - }, - "status": "active", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "serial": "123456" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + }, + Role: &diodepb.Role{ + Name: "WAN Router", + }, + Site: &diodepb.Site{ + Name: "Site A", + }, + Platform: &diodepb.Platform{ + Name: "Cisco IOS 15.6", + }, + Status: "active", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + Serial: strPtr("123456"), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -2578,31 +2606,32 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P8] ingest dcim.device - existing object found - create some missing related objects, use other existing one and update device", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01", - "device_type": { - "model": "ISR4321" - }, - "role": { - "name": "WAN Router" - }, - "site": { - "name": "Site A" - }, - "platform": { - "name": "Cisco IOS 15.6" - }, - "status": "active", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "serial": "123456-2" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + }, + Role: &diodepb.Role{ + Name: "WAN Router", + }, + Site: &diodepb.Site{ + Name: "Site A", + }, + Platform: &diodepb.Platform{ + Name: "Cisco IOS 15.6", + }, + Status: "active", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + Serial: strPtr("123456-2"), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -2764,28 +2793,29 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P8.1] ingest dcim.device with partial data - existing object found - create missing related objects and update device", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01", - "device_type": { - "model": "ISR4321" - }, - "role": { - "name": "WAN Router" - }, - "platform": { - "name": "Cisco IOS 15.6" - }, - "status": "active", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "serial": "123456" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + }, + Role: &diodepb.Role{ + Name: "WAN Router", + }, + Platform: &diodepb.Platform{ + Name: "Cisco IOS 15.6", + }, + Status: "active", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + Serial: strPtr("123456"), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -2965,31 +2995,32 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P8.2] ingest dcim.device - existing object found - no changes to apply", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "router01", - "device_type": { - "model": "ISR4321" - }, - "role": { - "name": "WAN Router" - }, - "site": { - "name": "Site B" - }, - "platform": { - "name": "Cisco IOS 15.6" - }, - "status": "active", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "serial": "123456" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "router01", + DeviceType: &diodepb.DeviceType{ + Model: "ISR4321", + }, + Role: &diodepb.Role{ + Name: "WAN Router", + }, + Site: &diodepb.Site{ + Name: "Site B", + }, + Platform: &diodepb.Platform{ + Name: "Cisco IOS 15.6", + }, + Status: "active", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + Serial: strPtr("123456"), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -3124,18 +3155,19 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P9] ingest dcim.site with name, status and description - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.site", - "entity": { - "Site": { - "name": "Site A", - "status": "active", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.site", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Site{ + Site: &diodepb.Site{ + Name: "Site A", + Status: "active", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -3169,18 +3201,19 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P9] ingest dcim.site with name, status and new description - existing object found - update", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.site", - "entity": { - "Site": { - "name": "Site A", - "status": "active", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed molestie felis." - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.site", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Site{ + Site: &diodepb.Site{ + Name: "Site A", + Status: "active", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed molestie felis."), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -3221,17 +3254,18 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P10] ingest dcim.manufacturer with name and description - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.manufacturer", - "entity": { - "Manufacturer": { - "name": "Cisco", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.manufacturer", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Manufacturer{ + Manufacturer: &diodepb.Manufacturer{ + Name: "Cisco", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.manufacturer", @@ -3264,17 +3298,18 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P10] ingest dcim.manufacturer with name and new description - existing object found - update", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.manufacturer", - "entity": { - "Manufacturer": { - "name": "Cisco", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed molestie felis." - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.manufacturer", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Manufacturer{ + Manufacturer: &diodepb.Manufacturer{ + Name: "Cisco", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed molestie felis."), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.manufacturer", @@ -3313,18 +3348,19 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P11] ingest dcim.devicerole with name and additional attributes - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicerole", - "entity": { - "DeviceRole": { - "name": "WAN Router", - "color": "509415", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicerole", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceRole{ + DeviceRole: &diodepb.Role{ + Name: "WAN Router", + Color: "509415", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.devicerole", @@ -3358,18 +3394,19 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P11] ingest dcim.devicerole with name and new additional attributes - existing object found - update", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.devicerole", - "entity": { - "DeviceRole": { - "name": "WAN Router", - "color": "ffffff", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed molestie felis." - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.devicerole", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_DeviceRole{ + DeviceRole: &diodepb.Role{ + Name: "WAN Router", + Color: "ffffff", + Description: strPtr("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed molestie felis."), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.devicerole", @@ -3410,14 +3447,15 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P12] ingest empty dcim.platform - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.platform", - "entity": { - "Platform": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.platform", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Platform{ + Platform: &diodepb.Platform{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -3427,16 +3465,17 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P13] ingest dcim.interface with name only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.interface", - "entity": { - "Interface": { - "name": "GigabitEthernet0/0/0" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.interface", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Interface{ + Interface: &diodepb.Interface{ + Name: "GigabitEthernet0/0/0", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.interface", @@ -3593,19 +3632,20 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P13] ingest dcim.interface with name and device - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.interface", - "entity": { - "Interface": { - "name": "GigabitEthernet0/0/0", - "device": { - "name": "router01" - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.interface", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Interface{ + Interface: &diodepb.Interface{ + Name: "GigabitEthernet0/0/0", + Device: &diodepb.Device{ + Name: "router01", + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.interface", @@ -3770,20 +3810,21 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P13] ingest dcim.interface with name, device and new label - existing object found - update with new label", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.interface", - "entity": { - "Interface": { - "name": "GigabitEthernet0/0/0", - "device": { - "name": "router01" - }, - "label": "WAN" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.interface", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Interface{ + Interface: &diodepb.Interface{ + Name: "GigabitEthernet0/0/0", + Device: &diodepb.Device{ + Name: "router01", + }, + Label: strPtr("WAN"), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.interface", @@ -3965,14 +4006,15 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P13] ingest empty dcim.interface - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.interface", - "entity": { - "Interface": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.interface", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Interface{ + Interface: &diodepb.Interface{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -3982,35 +4024,34 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P14] ingest dcim.device with device type and manufacturer - device type and manufacturer objects found - create device with existing device type and manufacturer", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.device", - "entity": { - "Device": { - "name": "Device A", - "device_type": { - "model": "Device Type A", - "manufacturer": { - "name": "Manufacturer A" - } - }, - "role": { - "name": "Role ABC" - }, - "platform": { - "name": "Platform A", - "manufacturer": { - "name": "Manufacturer A" - } - }, - "serial": "123456", - "site": { - "name": "Site ABC" - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.device", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Device{ + Device: &diodepb.Device{ + Name: "Device A", + DeviceType: &diodepb.DeviceType{ + Model: "Device Type A", + Manufacturer: &diodepb.Manufacturer{ + Name: "Manufacturer A", + }, + }, + Role: &diodepb.Role{ + Name: "Role ABC", + }, + Platform: &diodepb.Platform{ + Name: "Platform A", + Manufacturer: &diodepb.Manufacturer{ + Name: "Manufacturer A", + }, + }, + Serial: strPtr("123456"), + Site: &diodepb.Site{Name: "Site ABC"}, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -4144,23 +4185,24 @@ func TestDcimPrepare(t *testing.T) { }, { name: "[P15] ingest dcim.interface with name, mtu, device with site - device exists for platform Arista - create interface with existing device and platform", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "dcim.interface", - "entity": { - "Interface": { - "name": "Ethernet2", - "device": { - "name": "CEOS1", - "site": { - "name": "default_namespace" - } - }, - "mtu": 1500 - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "dcim.interface", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Interface{ + Interface: &diodepb.Interface{ + Name: "Ethernet2", + Device: &diodepb.Device{ + Name: "CEOS1", + Site: &diodepb.Site{ + Name: "default_namespace", + }, + }, + Mtu: int32Ptr(1500), + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.interface", @@ -4289,10 +4331,6 @@ func TestDcimPrepare(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var ingestEntity changeset.IngestEntity - err := json.Unmarshal(tt.rawIngestEntity, &ingestEntity) - require.NoError(t, err) - mockClient := mocks.NewNetBoxAPI(t) for _, m := range tt.retrieveObjectStates { @@ -4308,7 +4346,7 @@ func TestDcimPrepare(t *testing.T) { }, nil) } - cs, err := changeset.Prepare(ingestEntity, mockClient) + cs, err := changeset.Prepare(tt.ingestEntity, mockClient) if tt.wantErr { require.Error(t, err) return @@ -4329,3 +4367,4 @@ func TestDcimPrepare(t *testing.T) { func strPtr(s string) *string { return &s } func intPtr(d int) *int { return &d } +func int32Ptr(d int32) *int32 { return &d } diff --git a/diode-server/reconciler/changeset/changeset_ipam_test.go b/diode-server/reconciler/changeset/changeset_ipam_test.go index a83db503..69c1c6a7 100644 --- a/diode-server/reconciler/changeset/changeset_ipam_test.go +++ b/diode-server/reconciler/changeset/changeset_ipam_test.go @@ -2,12 +2,12 @@ package changeset_test import ( "context" - "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" "github.com/netboxlabs/diode/diode-server/netbox" "github.com/netboxlabs/diode/diode-server/netboxdiodeplugin" "github.com/netboxlabs/diode/diode-server/netboxdiodeplugin/mocks" @@ -24,28 +24,29 @@ func TestIpamPrepare(t *testing.T) { } tests := []struct { name string - rawIngestEntity []byte + ingestEntity changeset.IngestEntity retrieveObjectStates []mockRetrieveObjectState wantChangeSet changeset.ChangeSet wantErr bool }{ { name: "[P1] ingest ipam.ipaddress with address and interface - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.ipaddress", - "entity": { - "IpAddress": { - "address": "192.168.0.1/22", - "AssignedObject": { - "Interface": { - "name": "GigabitEthernet0/0/0" - } - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.ipaddress", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_IpAddress{ + IpAddress: &diodepb.IPAddress{ + Address: "192.168.0.1/22", + AssignedObject: &diodepb.IPAddress_Interface{ + Interface: &diodepb.Interface{ + Name: "GigabitEthernet0/0/0", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -247,21 +248,22 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P1] ingest ipam.ipaddress with address and a new interface - existing IP address and interface not found - create an interface and IP address", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.ipaddress", - "entity": { - "IpAddress": { - "address": "192.168.0.1/22", - "AssignedObject": { - "Interface": { - "name": "GigabitEthernet0/0/0" - } - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.ipaddress", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_IpAddress{ + IpAddress: &diodepb.IPAddress{ + Address: "192.168.0.1/22", + AssignedObject: &diodepb.IPAddress_Interface{ + Interface: &diodepb.Interface{ + Name: "GigabitEthernet0/0/0", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -447,21 +449,22 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P1] ingest ipam.ipaddress with address and a new interface - IP address found assigned to a different interface - create the interface and the IP address", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.ipaddress", - "entity": { - "IpAddress": { - "address": "192.168.0.1/22", - "AssignedObject": { - "Interface": { - "name": "GigabitEthernet1/0/1" - } - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.ipaddress", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_IpAddress{ + IpAddress: &diodepb.IPAddress{ + Address: "192.168.0.1/22", + AssignedObject: &diodepb.IPAddress_Interface{ + Interface: &diodepb.Interface{ + Name: "GigabitEthernet1/0/1", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -695,21 +698,22 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P1] ingest ipam.ipaddress with assigned interface - existing IP address found assigned a different device - create IP address with a new assigned object (interface)", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.ipaddress", - "entity": { - "IpAddress": { - "address": "192.168.0.1/22", - "AssignedObject": { - "Interface": { - "name": "GigabitEthernet1/0/1" - } - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.ipaddress", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_IpAddress{ + IpAddress: &diodepb.IPAddress{ + Address: "192.168.0.1/22", + AssignedObject: &diodepb.IPAddress_Interface{ + Interface: &diodepb.Interface{ + Name: "GigabitEthernet1/0/1", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -969,21 +973,22 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P1] ingest ipam.ipaddress with address and interface - existing IP address found with same interface assigned - no update needed", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.ipaddress", - "entity": { - "IpAddress": { - "address": "192.168.0.1/22", - "AssignedObject": { - "Interface": { - "name": "GigabitEthernet0/0/0" - } - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.ipaddress", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_IpAddress{ + IpAddress: &diodepb.IPAddress{ + Address: "192.168.0.1/22", + AssignedObject: &diodepb.IPAddress_Interface{ + Interface: &diodepb.Interface{ + Name: "GigabitEthernet0/0/0", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1223,16 +1228,17 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P1] ingest ipam.ipaddress with address only - existing IP address found without interface assigned - no update needed", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.ipaddress", - "entity": { - "IpAddress": { - "address": "192.168.0.1/22" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.ipaddress", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_IpAddress{ + IpAddress: &diodepb.IPAddress{ + Address: "192.168.0.1/22", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "ipam.ipaddress", @@ -1256,22 +1262,23 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P1] ingest ipam.ipaddress with address and new description - existing IP address found - update IP address with new description", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.ipaddress", - "entity": { - "IpAddress": { - "address": "192.168.0.1/22", - "description": "new description", - "AssignedObject": { - "Interface": { - "name": "GigabitEthernet0/0/0" - } - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.ipaddress", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_IpAddress{ + IpAddress: &diodepb.IPAddress{ + Address: "192.168.0.1/22", + Description: strPtr("new description"), + AssignedObject: &diodepb.IPAddress_Interface{ + Interface: &diodepb.Interface{ + Name: "GigabitEthernet0/0/0", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1533,14 +1540,15 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P1] ingest empty ipam.ipaddress - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.ipaddress", - "entity": { - "IPAddress": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.ipaddress", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_IpAddress{ + IpAddress: &diodepb.IPAddress{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -1550,16 +1558,17 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P2] ingest ipam.prefix with prefix only - existing object not found - create prefix and site (placeholder)", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.prefix", - "entity": { - "Prefix": { - "prefix": "192.168.0.0/32" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.prefix", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Prefix{ + Prefix: &diodepb.Prefix{ + Prefix: "192.168.0.0/32", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1617,19 +1626,20 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P2] ingest ipam.prefix with prefix only - existing object and its related objects found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.prefix", - "entity": { - "Prefix": { - "prefix": "192.168.0.0/32", - "site": { - "name": "undefined" - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.prefix", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Prefix{ + Prefix: &diodepb.Prefix{ + Prefix: "192.168.0.0/32", + Site: &diodepb.Site{ + Name: "undefined", + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1673,17 +1683,18 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P2] ingest ipam.prefix with empty site", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.prefix", - "entity": { - "Prefix": { - "prefix": "192.168.0.0/32", - "site": {} - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.prefix", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Prefix{ + Prefix: &diodepb.Prefix{ + Prefix: "192.168.0.0/32", + Site: &diodepb.Site{}, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1727,21 +1738,22 @@ func TestIpamPrepare(t *testing.T) { }, { name: "[P2] ingest ipam.prefix with prefix and a tag - existing object found - create tag and update prefix", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "ipam.prefix", - "entity": { - "Prefix": { - "prefix": "192.168.0.0/32", - "tags": [ - { - "name": "tag 100" - } - ] - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "ipam.prefix", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Prefix{ + Prefix: &diodepb.Prefix{ + Prefix: "192.168.0.0/32", + Tags: []*diodepb.Tag{ + { + Name: "tag 100", + }, + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "dcim.site", @@ -1829,10 +1841,6 @@ func TestIpamPrepare(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var ingestEntity changeset.IngestEntity - err := json.Unmarshal(tt.rawIngestEntity, &ingestEntity) - require.NoError(t, err) - mockClient := mocks.NewNetBoxAPI(t) for _, m := range tt.retrieveObjectStates { @@ -1848,7 +1856,7 @@ func TestIpamPrepare(t *testing.T) { }, nil) } - cs, err := changeset.Prepare(ingestEntity, mockClient) + cs, err := changeset.Prepare(tt.ingestEntity, mockClient) if tt.wantErr { require.Error(t, err) return diff --git a/diode-server/reconciler/changeset/changeset_virt_test.go b/diode-server/reconciler/changeset/changeset_virt_test.go index 22fe3702..faea712f 100644 --- a/diode-server/reconciler/changeset/changeset_virt_test.go +++ b/diode-server/reconciler/changeset/changeset_virt_test.go @@ -2,12 +2,12 @@ package changeset_test import ( "context" - "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netboxlabs/diode/diode-server/gen/diode/v1/diodepb" "github.com/netboxlabs/diode/diode-server/netbox" "github.com/netboxlabs/diode/diode-server/netboxdiodeplugin" "github.com/netboxlabs/diode/diode-server/netboxdiodeplugin/mocks" @@ -24,23 +24,24 @@ func TestVirtualizationPrepare(t *testing.T) { } tests := []struct { name string - rawIngestEntity []byte + ingestEntity changeset.IngestEntity retrieveObjectStates []mockRetrieveObjectState wantChangeSet changeset.ChangeSet wantErr bool }{ { name: "[P1] ingest virtualization.clustergroup with name only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.clustergroup", - "entity": { - "ClusterGroup": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.clustergroup", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_ClusterGroup{ + ClusterGroup: &diodepb.ClusterGroup{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.clustergroup", @@ -72,16 +73,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P1] ingest virtualization.clustergroup with name only - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.clustergroup", - "entity": { - "ClusterGroup": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.clustergroup", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_ClusterGroup{ + ClusterGroup: &diodepb.ClusterGroup{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.clustergroup", @@ -105,14 +107,15 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P1] ingest empty virtualization.clustergroup - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.clustergroup", - "entity": { - "ClusterGroup": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.clustergroup", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_ClusterGroup{ + ClusterGroup: &diodepb.ClusterGroup{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -122,16 +125,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P2] ingest virtualization.clustertype with name only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.clustertype", - "entity": { - "ClusterType": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.clustertype", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_ClusterType{ + ClusterType: &diodepb.ClusterType{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.clustertype", @@ -163,16 +167,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P2] ingest virtualization.clustertype with name only - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.clustertype", - "entity": { - "ClusterType": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.clustertype", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_ClusterType{ + ClusterType: &diodepb.ClusterType{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.clustertype", @@ -196,14 +201,23 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P2] ingest empty virtualization.clustertype - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.clustertype", - "entity": { - "ClusterType": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.clustertype", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_ClusterType{ + ClusterType: &diodepb.ClusterType{}, + }, + }, + }, + //rawIngestEntity: []byte(`{ + // "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", + // "data_type": "virtualization.clustertype", + // "entity": { + // "ClusterType": {} + // }, + // "state": 0 + //}`), retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -213,16 +227,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P3] ingest virtualization.cluster with name only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.cluster", - "entity": { - "Cluster": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.cluster", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Cluster{ + Cluster: &diodepb.Cluster{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.cluster", @@ -284,7 +299,8 @@ func TestVirtualizationPrepare(t *testing.T) { ObjectID: nil, ObjectVersion: nil, Data: &netbox.VirtualizationCluster{ - Name: "Test", + Name: "Test", + Status: strPtr(netbox.DefaultVirtualizationStatus), Group: &netbox.VirtualizationClusterGroup{ ID: 1, }, @@ -302,16 +318,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P3] ingest virtualization.cluster with name only - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.cluster", - "entity": { - "Cluster": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.cluster", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Cluster{ + Cluster: &diodepb.Cluster{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.cluster", @@ -374,14 +391,15 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P3] ingest empty virtualization.cluster - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.cluster", - "entity": { - "Cluster": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.cluster", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Cluster{ + Cluster: &diodepb.Cluster{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -391,16 +409,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P4] ingest virtualization.virtualmachine with name only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.virtualmachine", - "entity": { - "VirtualMachine": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.virtualmachine", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_VirtualMachine{ + VirtualMachine: &diodepb.VirtualMachine{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.virtualmachine", @@ -450,7 +469,8 @@ func TestVirtualizationPrepare(t *testing.T) { ObjectID: nil, ObjectVersion: nil, Data: &netbox.VirtualizationVirtualMachine{ - Name: "Test", + Name: "Test", + Status: strPtr(netbox.DefaultVirtualizationStatus), Role: &netbox.DcimDeviceRole{ ID: 1, }, @@ -465,19 +485,20 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P4] ingest virtualization.virtualmachine with name and cluster - existing objects not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.virtualmachine", - "entity": { - "VirtualMachine": { - "name": "Test", - "cluster": { - "name": "Cluster-1" - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.virtualmachine", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_VirtualMachine{ + VirtualMachine: &diodepb.VirtualMachine{ + Name: "Test", + Cluster: &diodepb.Cluster{ + Name: "Cluster-1", + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.virtualmachine", @@ -562,7 +583,8 @@ func TestVirtualizationPrepare(t *testing.T) { ObjectID: nil, ObjectVersion: nil, Data: &netbox.VirtualizationCluster{ - Name: "Cluster-1", + Name: "Cluster-1", + Status: strPtr(netbox.DefaultVirtualizationStatus), Group: &netbox.VirtualizationClusterGroup{ ID: 1, }, @@ -581,9 +603,11 @@ func TestVirtualizationPrepare(t *testing.T) { ObjectID: nil, ObjectVersion: nil, Data: &netbox.VirtualizationVirtualMachine{ - Name: "Test", + Name: "Test", + Status: strPtr(netbox.DefaultVirtualizationStatus), Cluster: &netbox.VirtualizationCluster{ - Name: "Cluster-1", + Name: "Cluster-1", + Status: strPtr(netbox.DefaultVirtualizationStatus), Group: &netbox.VirtualizationClusterGroup{ ID: 1, }, @@ -608,19 +632,20 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P4] ingest virtualization.virtualmachine with name and existing cluster - existing vm not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.virtualmachine", - "entity": { - "VirtualMachine": { - "name": "Test", - "cluster": { - "name": "Cluster-2" - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.virtualmachine", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_VirtualMachine{ + VirtualMachine: &diodepb.VirtualMachine{ + Name: "Test", + Cluster: &diodepb.Cluster{ + Name: "Cluster-2", + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.virtualmachine", @@ -725,7 +750,8 @@ func TestVirtualizationPrepare(t *testing.T) { ObjectID: nil, ObjectVersion: nil, Data: &netbox.VirtualizationVirtualMachine{ - Name: "Test", + Name: "Test", + Status: strPtr(netbox.DefaultVirtualizationStatus), Cluster: &netbox.VirtualizationCluster{ ID: 1, }, @@ -743,19 +769,20 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P4] ingest virtualization.virtualmachine with name and cluster - existing vm found - create cluster", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.virtualmachine", - "entity": { - "VirtualMachine": { - "name": "Test", - "cluster": { - "name": "Cluster-3" - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.virtualmachine", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_VirtualMachine{ + VirtualMachine: &diodepb.VirtualMachine{ + Name: "Test", + Cluster: &diodepb.Cluster{ + Name: "Cluster-3", + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.virtualmachine", @@ -850,7 +877,8 @@ func TestVirtualizationPrepare(t *testing.T) { ObjectID: nil, ObjectVersion: nil, Data: &netbox.VirtualizationCluster{ - Name: "Cluster-3", + Name: "Cluster-3", + Status: strPtr(netbox.DefaultVirtualizationStatus), Group: &netbox.VirtualizationClusterGroup{ ID: 1, }, @@ -872,7 +900,8 @@ func TestVirtualizationPrepare(t *testing.T) { ID: 1, Name: "Test", Cluster: &netbox.VirtualizationCluster{ - Name: "Cluster-3", + Name: "Cluster-3", + Status: strPtr(netbox.DefaultVirtualizationStatus), Group: &netbox.VirtualizationClusterGroup{ ID: 1, }, @@ -898,19 +927,20 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P4] ingest virtualization.virtualmachine with name only - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.virtualmachine", - "entity": { - "VirtualMachine": { - "name": "Test", - "cluster": { - "name": "cluster1" - } - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.virtualmachine", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_VirtualMachine{ + VirtualMachine: &diodepb.VirtualMachine{ + Name: "Test", + Cluster: &diodepb.Cluster{ + Name: "cluster1", + }, + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.virtualmachine", @@ -1044,14 +1074,15 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P4] ingest empty virtualization.virtualmachine - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.virtualmachine", - "entity": { - "VirtualMachine": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.virtualmachine", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_VirtualMachine{ + VirtualMachine: &diodepb.VirtualMachine{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -1061,16 +1092,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P5] ingest virtualization.vminterface with name only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.vminterface", - "entity": { - "VMInterface": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.vminterface", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Vminterface{ + Vminterface: &diodepb.VMInterface{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.vminterface", @@ -1165,16 +1197,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P5] ingest virtualization.vminterface with name only - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.vminterface", - "entity": { - "VMInterface": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.vminterface", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Vminterface{ + Vminterface: &diodepb.VMInterface{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.vminterface", @@ -1256,14 +1289,15 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P5] ingest empty virtualization.vminterface - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.vminterface", - "entity": { - "VMInterface": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.vminterface", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Vminterface{ + Vminterface: &diodepb.VMInterface{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -1273,16 +1307,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P6] ingest virtualization.virtualdisk with name only - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.virtualdisk", - "entity": { - "VirtualDisk": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.virtualdisk", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_VirtualDisk{ + VirtualDisk: &diodepb.VirtualDisk{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.virtualdisk", @@ -1377,16 +1412,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P6] ingest virtualization.virtualdisk with name only and no existing site - existing object not found - create", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.virtualdisk", - "entity": { - "VirtualDisk": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.virtualdisk", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_VirtualDisk{ + VirtualDisk: &diodepb.VirtualDisk{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.virtualdisk", @@ -1492,16 +1528,17 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P6] ingest virtualization.virtualdisk with name only - existing object found - do nothing", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.virtualdisk", - "entity": { - "VirtualDisk": { - "name": "Test" - } - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.virtualdisk", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_VirtualDisk{ + VirtualDisk: &diodepb.VirtualDisk{ + Name: "Test", + }, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{ { objectType: "virtualization.virtualdisk", @@ -1570,14 +1607,15 @@ func TestVirtualizationPrepare(t *testing.T) { }, { name: "[P6] ingest empty virtualization.virtualdisk - error", - rawIngestEntity: []byte(`{ - "request_id": "cfa0f129-125c-440d-9e41-e87583cd7d89", - "data_type": "virtualization.virtualdisk", - "entity": { - "VirtualDisk": {} - }, - "state": 0 - }`), + ingestEntity: changeset.IngestEntity{ + RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", + DataType: "virtualization.virtualdisk", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_VirtualDisk{ + VirtualDisk: &diodepb.VirtualDisk{}, + }, + }, + }, retrieveObjectStates: []mockRetrieveObjectState{}, wantChangeSet: changeset.ChangeSet{ ChangeSetID: "5663a77e-9bad-4981-afe9-77d8a9f2b8b5", @@ -1589,10 +1627,6 @@ func TestVirtualizationPrepare(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var ingestEntity changeset.IngestEntity - err := json.Unmarshal(tt.rawIngestEntity, &ingestEntity) - require.NoError(t, err) - mockClient := mocks.NewNetBoxAPI(t) for _, m := range tt.retrieveObjectStates { @@ -1608,7 +1642,7 @@ func TestVirtualizationPrepare(t *testing.T) { }, nil) } - cs, err := changeset.Prepare(ingestEntity, mockClient) + cs, err := changeset.Prepare(tt.ingestEntity, mockClient) if tt.wantErr { require.Error(t, err) return diff --git a/diode-server/reconciler/ingestion_processor.go b/diode-server/reconciler/ingestion_processor.go index 82574cc7..2ec1620d 100644 --- a/diode-server/reconciler/ingestion_processor.go +++ b/diode-server/reconciler/ingestion_processor.go @@ -221,7 +221,7 @@ func (p *IngestionProcessor) handleStreamMessage(ctx context.Context, msg redis. ingestEntity := changeset.IngestEntity{ RequestID: ingestReq.GetId(), DataType: objectType, - Entity: v.GetEntity(), + Entity: v, State: int(reconcilerpb.State_NEW), } diff --git a/diode-server/reconciler/ingestion_processor_internal_test.go b/diode-server/reconciler/ingestion_processor_internal_test.go index 35dc5266..1daf899d 100644 --- a/diode-server/reconciler/ingestion_processor_internal_test.go +++ b/diode-server/reconciler/ingestion_processor_internal_test.go @@ -182,17 +182,15 @@ func TestReconcileEntity(t *testing.T) { ingestEntity := changeset.IngestEntity{ RequestID: "cfa0f129-125c-440d-9e41-e87583cd7d89", DataType: "dcim.site", - Entity: &diodepb.Entity_Site{ - Site: &diodepb.Site{ - Name: "Site A", + Entity: &diodepb.Entity{ + Entity: &diodepb.Entity_Site{ + Site: &diodepb.Site{ + Name: "Site A", + }, }, }, } - //ingestEntityJSON, _ := json.Marshal(ingestEntity) - //var ingestEntity2 changeset.IngestEntity - //_ = json.Unmarshal(ingestEntityJSON, &ingestEntity2) - cs, err := p.reconcileEntity(ctx, ingestEntity) if tt.expectedError { diff --git a/diode-server/reconciler/ingestion_processor_test.go b/diode-server/reconciler/ingestion_processor_test.go index ce25c41c..d54f6c48 100644 --- a/diode-server/reconciler/ingestion_processor_test.go +++ b/diode-server/reconciler/ingestion_processor_test.go @@ -160,6 +160,48 @@ func TestIngestionProcessorStart(t *testing.T) { }, }, }, + { + Entity: &diodepb.Entity_ClusterGroup{ + ClusterGroup: &diodepb.ClusterGroup{ + Name: "test-cluster-group", + }, + }, + }, + { + Entity: &diodepb.Entity_ClusterType{ + ClusterType: &diodepb.ClusterType{ + Name: "test-cluster-type", + }, + }, + }, + { + Entity: &diodepb.Entity_Cluster{ + Cluster: &diodepb.Cluster{ + Name: "test-cluster", + }, + }, + }, + { + Entity: &diodepb.Entity_VirtualMachine{ + VirtualMachine: &diodepb.VirtualMachine{ + Name: "test-vm", + }, + }, + }, + { + Entity: &diodepb.Entity_Vminterface{ + Vminterface: &diodepb.VMInterface{ + Name: "test-vm-interface", + }, + }, + }, + { + Entity: &diodepb.Entity_VirtualDisk{ + VirtualDisk: &diodepb.VirtualDisk{ + Name: "test-virtual-disk", + }, + }, + }, }, } reqBytes, err := proto.Marshal(ingestReq) diff --git a/diode-server/reconciler/logs_retriever.go b/diode-server/reconciler/logs_retriever.go index 5eda1a5d..60941c15 100644 --- a/diode-server/reconciler/logs_retriever.go +++ b/diode-server/reconciler/logs_retriever.go @@ -237,7 +237,7 @@ func buildQueryFilter(req *reconcilerpb.RetrieveIngestionLogsRequest) string { // apply optional filters for ingestion state if req.State != nil { - stateFilter := fmt.Sprintf("@state:[%d %d]", req.GetState(), req.GetState()) + stateFilter := fmt.Sprintf("@state:%s", req.GetState().String()) if queryFilter == "*" { queryFilter = stateFilter } else { diff --git a/diode-server/reconciler/server_internal_test.go b/diode-server/reconciler/server_internal_test.go index ccc5c15f..60fbe0ad 100644 --- a/diode-server/reconciler/server_internal_test.go +++ b/diode-server/reconciler/server_internal_test.go @@ -337,7 +337,7 @@ func TestRetrieveLogs(t *testing.T) { }, NextPageToken: "AAAFlw==", }, - queryFilter: "@state:[1 1]", + queryFilter: "@state:NEW", queryLimitOffset: 0, failCmd: false, hasError: false, @@ -391,7 +391,7 @@ func TestRetrieveLogs(t *testing.T) { }, NextPageToken: "AAAFlw==", }, - queryFilter: "@state:[2 2]", + queryFilter: "@state:RECONCILED", queryLimitOffset: 0, failCmd: false, hasError: false, @@ -445,7 +445,7 @@ func TestRetrieveLogs(t *testing.T) { }, NextPageToken: "AAAFlw==", }, - queryFilter: "@state:[3 3]", + queryFilter: "@state:FAILED", queryLimitOffset: 0, failCmd: false, hasError: false, @@ -499,7 +499,7 @@ func TestRetrieveLogs(t *testing.T) { }, NextPageToken: "AAAFlw==", }, - queryFilter: "@state:[4 4]", + queryFilter: "@state:NO_CHANGES", queryLimitOffset: 0, failCmd: false, hasError: false,