package netmap import ( "errors" "fmt" "slices" "strconv" "strings" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap" frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto" "git.frostfs.info/TrueCloudLab/hrw" ) // NodeInfo groups information about FrostFS storage node which is reflected // in the FrostFS network map. Storage nodes advertise this information when // registering with the FrostFS network. After successful registration, information // about the nodes is available to all network participants to work with the network // map (mainly to comply with container storage policies). // // NodeInfo is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap.NodeInfo // message. See ReadFromV2 / WriteToV2 methods. // // Instances can be created using built-in var declaration. type NodeInfo struct { m netmap.NodeInfo hash uint64 } // reads NodeInfo from netmap.NodeInfo message. If checkFieldPresence is set, // returns an error on absence of any protocol-required field. Verifies format of any // presented field according to FrostFS API V2 protocol. func (x *NodeInfo) readFromV2(m netmap.NodeInfo, checkFieldPresence bool) error { var err error binPublicKey := m.GetPublicKey() if checkFieldPresence && len(binPublicKey) == 0 { return errors.New("missing public key") } if checkFieldPresence && m.NumberOfAddresses() <= 0 { return errors.New("missing network endpoints") } attributes := m.GetAttributes() mAttr := make(map[string]struct{}, len(attributes)) for i := range attributes { key := attributes[i].GetKey() if key == "" { return fmt.Errorf("empty key of the attribute #%d", i) } else if _, ok := mAttr[key]; ok { return fmt.Errorf("duplicated attbiuted %s", key) } switch { case key == attrCapacity: _, err = strconv.ParseUint(attributes[i].GetValue(), 10, 64) if err != nil { return fmt.Errorf("invalid %s attribute: %w", attrCapacity, err) } case key == attrPrice: var err error _, err = strconv.ParseUint(attributes[i].GetValue(), 10, 64) if err != nil { return fmt.Errorf("invalid %s attribute: %w", attrPrice, err) } default: if attributes[i].GetValue() == "" { return fmt.Errorf("empty value of the attribute %s", key) } } } x.m = m x.hash = hrw.Hash(binPublicKey) return nil } // ReadFromV2 reads NodeInfo from the netmap.NodeInfo message. Checks if the // message conforms to FrostFS API V2 protocol. // // See also WriteToV2. func (x *NodeInfo) ReadFromV2(m netmap.NodeInfo) error { return x.readFromV2(m, true) } // WriteToV2 writes NodeInfo to the netmap.NodeInfo message. The message MUST NOT // be nil. // // See also ReadFromV2. func (x NodeInfo) WriteToV2(m *netmap.NodeInfo) { *m = x.m } // Marshal encodes NodeInfo into a binary format of the FrostFS API protocol // (Protocol Buffers with direct field order). // // See also Unmarshal. func (x NodeInfo) Marshal() []byte { var m netmap.NodeInfo x.WriteToV2(&m) return m.StableMarshal(nil) } // Unmarshal decodes FrostFS API protocol binary format into the NodeInfo // (Protocol Buffers with direct field order). Returns an error describing // a format violation. // // See also Marshal. func (x *NodeInfo) Unmarshal(data []byte) error { var m netmap.NodeInfo err := m.Unmarshal(data) if err != nil { return err } return x.readFromV2(m, false) } // MarshalJSON encodes NodeInfo into a JSON format of the FrostFS API protocol // (Protocol Buffers JSON). // // See also UnmarshalJSON. func (x NodeInfo) MarshalJSON() ([]byte, error) { var m netmap.NodeInfo x.WriteToV2(&m) return m.MarshalJSON() } // UnmarshalJSON decodes FrostFS API protocol JSON format into the NodeInfo // (Protocol Buffers JSON). Returns an error describing a format violation. // // See also MarshalJSON. func (x *NodeInfo) UnmarshalJSON(data []byte) error { var m netmap.NodeInfo err := m.UnmarshalJSON(data) if err != nil { return err } return x.readFromV2(m, false) } // SetPublicKey sets binary-encoded public key bound to the node. The key // authenticates the storage node, so it MUST be unique within the network. // // Argument MUST NOT be mutated, make a copy first. // // See also PublicKey. func (x *NodeInfo) SetPublicKey(key []byte) { x.m.SetPublicKey(key) x.hash = hrw.Hash(x.m.GetPublicKey()) } // PublicKey returns value set using SetPublicKey. // // Zero NodeInfo has no public key, which is incorrect according to // FrostFS system requirements. // // Return value MUST not be mutated, make a copy first. func (x NodeInfo) PublicKey() []byte { return x.m.GetPublicKey() } // StringifyPublicKey returns HEX representation of PublicKey. func StringifyPublicKey(node NodeInfo) string { return frostfscrypto.StringifyKeyBinary(node.PublicKey()) } // SetNetworkEndpoints sets list to the announced node's network endpoints. // Node MUSt have at least one announced endpoint. List MUST be unique. // Endpoints are used for communication with the storage node within FrostFS // network. It is expected that node serves storage node services on these // endpoints (it also adds a wait on their network availability). // // Argument MUST NOT be mutated, make a copy first. // // See also IterateNetworkEndpoints. func (x *NodeInfo) SetNetworkEndpoints(v ...string) { x.m.SetAddresses(v...) } // NumberOfNetworkEndpoints returns number of network endpoints announced by the node. // // See also SetNetworkEndpoints. func (x NodeInfo) NumberOfNetworkEndpoints() int { return x.m.NumberOfAddresses() } // IterateNetworkEndpoints iterates over network endpoints announced by the // node and pass them into f. Breaks iteration on f's true return. Handler // MUST NOT be nil. // // Zero NodeInfo contains no endpoints which is incorrect according to // FrostFS system requirements. // // See also SetNetworkEndpoints. func (x NodeInfo) IterateNetworkEndpoints(f func(string) bool) { x.m.IterateAddresses(f) } // IterateNetworkEndpoints is an extra-sugared function over IterateNetworkEndpoints // method which allows to unconditionally iterate over all node's network endpoints. func IterateNetworkEndpoints(node NodeInfo, f func(string)) { node.IterateNetworkEndpoints(func(addr string) bool { f(addr) return false }) } // assert NodeInfo type provides hrw.Hasher required for HRW sorting. var _ hrw.Hasher = NodeInfo{} // Hash implements hrw.Hasher interface. // // Hash is needed to support weighted HRW therefore sort function sorts nodes // based on their public key. Hash isn't expected to be used directly. func (x NodeInfo) Hash() uint64 { if x.hash != 0 { return x.hash } return hrw.Hash(x.m.GetPublicKey()) } func (x *NodeInfo) setNumericAttribute(key string, num uint64) { x.SetAttribute(key, strconv.FormatUint(num, 10)) } // SetPrice sets the storage cost declared by the node. By default, zero // price is announced. func (x *NodeInfo) SetPrice(price uint64) { x.setNumericAttribute(attrPrice, price) } // Price returns price set using SetPrice. // // Zero NodeInfo has zero price. func (x NodeInfo) Price() uint64 { val := x.Attribute(attrPrice) if val == "" { return 0 } price, err := strconv.ParseUint(val, 10, 64) if err != nil { panic(fmt.Sprintf("unexpected price parsing error %s: %v", val, err)) } return price } // SetCapacity sets the storage capacity declared by the node. By default, zero // capacity is announced. func (x *NodeInfo) SetCapacity(capacity uint64) { x.setNumericAttribute(attrCapacity, capacity) } // capacity returns capacity set using SetCapacity. // // Zero NodeInfo has zero capacity. func (x NodeInfo) capacity() uint64 { val := x.Attribute(attrCapacity) if val == "" { return 0 } capacity, err := strconv.ParseUint(val, 10, 64) if err != nil { panic(fmt.Sprintf("unexpected capacity parsing error %s: %v", val, err)) } return capacity } const attrUNLOCODE = "UN-LOCODE" // SetLOCODE specifies node's geographic location in UN/LOCODE format. Each // storage node MUST declare it for entrance to the FrostFS network. Node MAY // declare the code of the nearest location as needed, for example, when it is // impossible to unambiguously attribute the node to any location from UN/LOCODE // database. // // See also LOCODE. func (x *NodeInfo) SetLOCODE(locode string) { x.SetAttribute(attrUNLOCODE, locode) } // LOCODE returns node's location code set using SetLOCODE. // // Zero NodeInfo has empty location code which is invalid according to // FrostFS API system requirement. func (x NodeInfo) LOCODE() string { return x.Attribute(attrUNLOCODE) } // SetCountryCode sets code of the country in ISO 3166-1_alpha-2 to which // storage node belongs (or the closest one). // // SetCountryCode is intended only for processing the network registration // request by the Inner Ring. Other parties SHOULD NOT use it. func (x *NodeInfo) SetCountryCode(countryCode string) { x.SetAttribute("CountryCode", countryCode) } // SetCountryName sets short name of the country in ISO-3166 format to which // storage node belongs (or the closest one). // // SetCountryName is intended only for processing the network registration // request by the Inner Ring. Other parties SHOULD NOT use it. func (x *NodeInfo) SetCountryName(country string) { x.SetAttribute("Country", country) } // SetLocationName sets storage node's location name from "NameWoDiacritics" // column in the UN/LOCODE record corresponding to the specified LOCODE. // // SetLocationName is intended only for processing the network registration // request by the Inner Ring. Other parties SHOULD NOT use it. func (x *NodeInfo) SetLocationName(location string) { x.SetAttribute("Location", location) } // SetSubdivisionCode sets storage node's subdivision code from "SubDiv" column in // the UN/LOCODE record corresponding to the specified LOCODE. // // SetSubdivisionCode is intended only for processing the network registration // request by the Inner Ring. Other parties SHOULD NOT use it. func (x *NodeInfo) SetSubdivisionCode(subDiv string) { x.SetAttribute("SubDivCode", subDiv) } // SetSubdivisionName sets storage node's subdivision name in ISO 3166-2 format. // // SetSubdivisionName is intended only for processing the network registration // request by the Inner Ring. Other parties SHOULD NOT use it. func (x *NodeInfo) SetSubdivisionName(subDiv string) { x.SetAttribute("SubDiv", subDiv) } // SetContinentName sets name of the storage node's continent from // Seven-Continent model. // // SetContinentName is intended only for processing the network registration // request by the Inner Ring. Other parties SHOULD NOT use it. func (x *NodeInfo) SetContinentName(continent string) { x.SetAttribute("Continent", continent) } // Enumeration of well-known attributes. const ( // attrPrice is a key to the node attribute that indicates the // price in GAS tokens for storing one GB of data during one Epoch. attrPrice = "Price" // attrCapacity is a key to the node attribute that indicates the // total available disk space in Gigabytes. attrCapacity = "Capacity" // attrExternalAddr is a key for the attribute storing node external addresses. attrExternalAddr = "ExternalAddr" // sepExternalAddr is a separator for multi-value ExternalAddr attribute. sepExternalAddr = "," ) // SetExternalAddresses sets multi-addresses to use // to connect to this node from outside. // // Panics if addr is an empty list. func (x *NodeInfo) SetExternalAddresses(addr ...string) { x.SetAttribute(attrExternalAddr, strings.Join(addr, sepExternalAddr)) } // ExternalAddresses returns list of multi-addresses to use // to connect to this node from outside. func (x NodeInfo) ExternalAddresses() []string { a := x.Attribute(attrExternalAddr) if len(a) == 0 { return nil } return strings.Split(a, sepExternalAddr) } // NumberOfAttributes returns number of attributes announced by the node. // // See also SetAttribute. func (x NodeInfo) NumberOfAttributes() int { return len(x.m.GetAttributes()) } // IterateAttributes iterates over all node attributes and passes the into f. // Handler MUST NOT be nil. func (x NodeInfo) IterateAttributes(f func(key, value string)) { a := x.m.GetAttributes() for i := range a { f(a[i].GetKey(), a[i].GetValue()) } } // SetAttribute sets value of the node attribute value by the given key. // Both key and value MUST NOT be empty. func (x *NodeInfo) SetAttribute(key, value string) { if key == "" { panic("empty key in SetAttribute") } else if value == "" { panic("empty value in SetAttribute") } a := x.m.GetAttributes() for i := range a { if a[i].GetKey() == key { a[i].SetValue(value) return } } a = append(a, netmap.Attribute{}) a[len(a)-1].SetKey(key) a[len(a)-1].SetValue(value) x.m.SetAttributes(a) } // Attribute returns value of the node attribute set using SetAttribute by the // given key. Returns empty string if attribute is missing. func (x NodeInfo) Attribute(key string) string { a := x.m.GetAttributes() for i := range a { if a[i].GetKey() == key { return a[i].GetValue() } } return "" } // SortAttributes sorts node attributes set using SetAttribute lexicographically. // The method is only needed to make NodeInfo consistent, e.g. for signing. func (x *NodeInfo) SortAttributes() { as := x.m.GetAttributes() if len(as) == 0 { return } slices.SortFunc(as, func(ai, aj netmap.Attribute) int { if r := strings.Compare(ai.GetKey(), aj.GetKey()); r != 0 { return r } return strings.Compare(ai.GetValue(), aj.GetValue()) }) x.m.SetAttributes(as) } // SetOffline sets the state of the node to "offline". When a node updates // information about itself in the network map, this action is interpreted as // an intention to leave the network. func (x *NodeInfo) SetOffline() { x.m.SetState(netmap.Offline) } // IsOffline checks if the node is in the "offline" state. // // Zero NodeInfo has undefined state which is not offline (note that it does not // mean online). // // See also SetOffline. func (x NodeInfo) IsOffline() bool { return x.m.GetState() == netmap.Offline } // SetOnline sets the state of the node to "online". When a node updates // information about itself in the network map, this // action is interpreted as an intention to enter the network. // // See also IsOnline. func (x *NodeInfo) SetOnline() { x.m.SetState(netmap.Online) } // IsOnline checks if the node is in the "online" state. // // Zero NodeInfo has undefined state which is not online (note that it does not // mean offline). // // See also SetOnline. func (x NodeInfo) IsOnline() bool { return x.m.GetState() == netmap.Online } // SetMaintenance sets the state of the node to "maintenance". When a node updates // information about itself in the network map, this // state declares temporal unavailability for a node. // // See also IsMaintenance. func (x *NodeInfo) SetMaintenance() { x.m.SetState(netmap.Maintenance) } // IsMaintenance checks if the node is in the "maintenance" state. // // Zero NodeInfo has undefined state. // // See also SetMaintenance. func (x NodeInfo) IsMaintenance() bool { return x.m.GetState() == netmap.Maintenance }