package edgecliutils import ( "errors" "fmt" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/api/utils" "edge-infra.dev/pkg/lib/networkvalidator" ) func makeTermInput(currentTerminal *model.Terminal, lane, role, class, mac, ipaddr, gateway4addr, gateway6addr, discoverDisks, terminalDiskID, devicePath *string, prefixLen *int, dhcp4, dhcp6, includeDisk, expectEmpty *bool, family model.InetType) (*model.TerminalIDInput, error) { // Create term, iface and addr inputs separately, then update/merge termInput := &model.TerminalIDInput{ TerminalID: currentTerminal.TerminalID, TerminalValues: &model.TerminalUpdateInput{ Lane: lane, Disks: []*model.TerminalDiskIDInput{}, Interfaces: []*model.TerminalInterfaceIDInput{}, }, } if role != nil { rl := model.TerminalRoleType(*role) termInput.TerminalValues.Role = &rl } if class != nil { cl := model.TerminalClassType(*class) termInput.TerminalValues.Class = &cl } if discoverDisks != nil { dd := model.TerminalDiscoverDisksType(*discoverDisks) termInput.TerminalValues.DiscoverDisks = &dd } termInput = mergeIntoTermInput(termInput, currentTerminal) if terminalDiskID != nil { termInput.TerminalValues.Disks = []*model.TerminalDiskIDInput{ { TerminalDiskID: *terminalDiskID, TerminalDiskValues: &model.TerminalDiskUpdateInput{ DevicePath: devicePath, IncludeDisk: includeDisk, ExpectEmpty: expectEmpty, }, }, } } // Only attempt to update terminal interfaces if the user provides a MAC by which to identify // the interface to be modified. if mac != nil { ifaceInput := &model.TerminalInterfaceIDInput{ TerminalInterfaceID: "", // leave empty for now TerminalInterfaceValues: &model.TerminalInterfaceUpdateInput{ MacAddress: mac, Dhcp4: dhcp4, Dhcp6: dhcp6, Addresses: []*model.TerminalAddressIDInput{}, Gateway4: gateway4addr, Gateway6: gateway6addr, }, } addrInput := &model.TerminalAddressIDInput{ TerminalAddressID: "", // leave empty for now TerminalAddressValues: &model.TerminalAddressUpdateInput{ IP: ipaddr, PrefixLen: prefixLen, Family: &family, }, } err := updateIfaceOfTermInput(termInput.TerminalValues, ifaceInput, currentTerminal.Interfaces, addrInput) if err != nil { return nil, err } } return termInput, nil } // Our convention is to always mutate the _first_ argument func mergeIntoTermInput(newTerm *model.TerminalIDInput, currentTerminal *model.Terminal) *model.TerminalIDInput { newTerm.TerminalValues.Lane = utils.AssignNotNil(currentTerminal.Lane, newTerm.TerminalValues.Lane) newTerm.TerminalValues.Role = utils.AssignNotNil(¤tTerminal.Role, newTerm.TerminalValues.Role) newTerm.TerminalValues.Class = utils.AssignNotNil(currentTerminal.Class, newTerm.TerminalValues.Class) // Unlike the other helper functions, we don't invoke merging for ifaces/addresses // here since we'd need access to lots of CLI args which we don't want to pass around AFAP return newTerm } // Finds matching interface and updates the current values with new values func updateIfaceOfTermInput(newTerm *model.TerminalUpdateInput, newIface *model.TerminalInterfaceIDInput, currentTerminalInterfaces []*model.TerminalInterface, newAddr *model.TerminalAddressIDInput) error { idx, matchingIface := FindFirst(func(iface *model.TerminalInterface) bool { return iface.MacAddress == *newIface.TerminalInterfaceValues.MacAddress }, currentTerminalInterfaces) if matchingIface == nil { return fmt.Errorf("no matching interface found with mac address %v, cannot modify non-existent interface", *newIface.TerminalInterfaceValues.MacAddress) } mergedIface, err := mergeIntoTermIfaceInput(newIface, matchingIface, newAddr) if err != nil { return err } newTerm.Interfaces = SafeInsert(newTerm.Interfaces, idx, mergedIface) return nil } // newIface is a sparsely-populated value, containing _just_ the fields passed via CLI. // currentIface is a fully-populated value (of the wrong type: a GQL output type, not input type). // Both relate to the same entry in the list of interfaces. func mergeIntoTermIfaceInput(newIface *model.TerminalInterfaceIDInput, currentIface *model.TerminalInterface, newAddr *model.TerminalAddressIDInput) (*model.TerminalInterfaceIDInput, error) { // Add the ID omitted earlier newIface.TerminalInterfaceID = currentIface.TerminalInterfaceID newIface.TerminalInterfaceValues.Dhcp4 = utils.AssignNotNil(¤tIface.Dhcp4, newIface.TerminalInterfaceValues.Dhcp4) newIface.TerminalInterfaceValues.Dhcp6 = utils.AssignNotNil(¤tIface.Dhcp6, newIface.TerminalInterfaceValues.Dhcp6) newIface.TerminalInterfaceValues.Gateway4 = utils.AssignNotNil(currentIface.Gateway4, newIface.TerminalInterfaceValues.Gateway4) newIface.TerminalInterfaceValues.Gateway6 = utils.AssignNotNil(currentIface.Gateway6, newIface.TerminalInterfaceValues.Gateway6) newIface.TerminalInterfaceValues.MacAddress = utils.AssignNotNil(¤tIface.MacAddress, newIface.TerminalInterfaceValues.MacAddress) if newAddr != nil { err := updateAddressOfTermIfaceInput(newIface, newAddr, currentIface.Addresses) if err != nil { return nil, err } } err := validateTermIfaceInput(newIface) if err != nil { return nil, err } return newIface, nil } // Set the terminal address values that aren't being updated to the current values in the db. // Finds matching address by checking IDs. In _our_ case, we don't know the ID straight away - // need to use FindFirst with the predicate to look for the first ipv4/6 address attaching // to the original matching interface. func updateAddressOfTermIfaceInput(newIface *model.TerminalInterfaceIDInput, newAddr *model.TerminalAddressIDInput, currentTerminalAddresses []*model.TerminalAddress) error { idx, inetAddr := FindFirst(func(addr *model.TerminalAddress) bool { return addr.Family == *newAddr.TerminalAddressValues.Family }, currentTerminalAddresses) if inetAddr == nil { return fmt.Errorf("no %v address found for interface with ID %v, cannot modify non-existent address", *newAddr.TerminalAddressValues.Family, newIface.TerminalInterfaceID) } addr, err := mergeIntoTermAddrInput(newAddr, inetAddr) if err != nil { return err } newIface.TerminalInterfaceValues.Addresses = SafeInsert(newIface.TerminalInterfaceValues.Addresses, idx, addr) return nil } func mergeIntoTermAddrInput(newAddress *model.TerminalAddressIDInput, currentAddress *model.TerminalAddress) (*model.TerminalAddressIDInput, error) { // Add the ID omitted earlier newAddress.TerminalAddressID = currentAddress.TerminalAddressID newAddress.TerminalAddressValues.IP = utils.AssignNotNil(currentAddress.IP, newAddress.TerminalAddressValues.IP) newAddress.TerminalAddressValues.Family = utils.AssignNotNil(¤tAddress.Family, newAddress.TerminalAddressValues.Family) newAddress.TerminalAddressValues.PrefixLen = utils.AssignNotNil(¤tAddress.PrefixLen, newAddress.TerminalAddressValues.PrefixLen) err := validateTermAddrInput(newAddress) if err != nil { return nil, err } return newAddress, nil } func validateTermIfaceInput(iface *model.TerminalInterfaceIDInput) error { if iface.TerminalInterfaceValues.Dhcp4 != nil && !*iface.TerminalInterfaceValues.Dhcp4 { // DHCP4 false - IPv4 and gateway4 address must be provided if iface.TerminalInterfaceValues.Gateway4 == nil { return errors.New("DHCP4 is set to false - you must provide a value for gateway4") } else if !networkvalidator.ValidateIP(*iface.TerminalInterfaceValues.Gateway4) { return fmt.Errorf("invalid gateway4 address: %s", *iface.TerminalInterfaceValues.Gateway4) } } if iface.TerminalInterfaceValues.MacAddress != nil { _, err := networkvalidator.ValidateMacAddress(*iface.TerminalInterfaceValues.MacAddress) if err != nil { return err } } return nil } func validateTermAddrInput(addr *model.TerminalAddressIDInput) error { if addr.TerminalAddressID == "" { return fmt.Errorf("invalid/missing terminal address ID") } if addr.TerminalAddressValues.IP != nil { if !networkvalidator.ValidateIP(*addr.TerminalAddressValues.IP) { return fmt.Errorf("invalid/missing terminal address IP") } if addr.TerminalAddressValues.PrefixLen != nil && !networkvalidator.ValidateCIDR(*addr.TerminalAddressValues.IP, *addr.TerminalAddressValues.PrefixLen) { return fmt.Errorf("invalid/missing terminal address IP") } } return nil }