// 2020 Rackspace, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package network import ( "fmt" "strconv" "strings" "github.rackspace.com/DCM/discover/common" "github.rackspace.com/DCM/netparse" "github.rackspace.com/DCM/tools" "github.rackspace.com/gorax/clients/dcxSwitchapi" "github.rackspace.com/gorax/clients/unipede" "github.rackspace.com/gorax/passkey" ) type ( // Client struct contains a few clients that we use to interface with network infrastructure as well // as a context-like mapping of switches gathered per-initialization. Client struct { uni *unipede.Client fe *dcxSwitchapi.Client k *passkey.Client uniRef *int ctx map[string]Switch } // Opt type is a set of optional arguments that // could be included into the network package. Opt func(c *Client) // Switch structure here defines the network switch parameters we would use in // discovering devices on a network. This switch item is here to abstract data from // Unipede, FireEngine, or others if needed so we can make consistent logic base on // switch configurations. Switch struct { Name string `json:"switch_name"` IntConfig []netparse.IfcConfig `json:"interfaces_configuration"` Routes []netparse.IPRoute `json:"routes"` Vlans []netparse.Vlan `json:"vlans"` Macs map[string][]string `json:"macs"` Model string `json:"model"` Upstreams []string `json:"upstream_neighbors"` Updated string `json:"updated"` } ) // WithRefresh will set the unipede refresh interval. Default is nil func WithRefresh(i int) Opt { return func(c *Client) { c.uniRef = &i } } // WithRefresh will set the clients from an already initialized client func WithClients(dcx *dcxSwitchapi.Client, uni *unipede.Client) Opt { return func(c *Client) { c.fe = dcx c.uni = uni } } // WithPasskey will initialize clients with the passkey library func WithPasskey(k *passkey.Client) Opt { return func(c *Client) { c.k = k } } // New is the initialization function for this client. This will init all the // required items in the Client struct for the pkg methods. func New(opts ...Opt) (*Client, error) { c := &Client{ ctx: make(map[string]Switch), } for _, o := range opts { o(c) } // If the clients were set manually if c.fe != nil && c.uni != nil { return c, nil } // If the passkey client was passed in if c.k != nil { uKey := c.k.Service.Unipede.Credential ssoKey := c.k.Service.SSO.Credential var err error if c.uni, err = unipede.New("", "", uKey.Password); err != nil { return nil, err } if c.fe, err = dcxSwitchapi.NewClient(ssoKey.Username, ssoKey.Password); err != nil { return nil, err } return c, nil } return c, fmt.Errorf("no supported auths passed in") } // SwitchConfigs a helper method to return the switch config context with all the data gathered // from parsing the ports. // This will be empty until actual switch crawling is completed. func (c *Client) SwitchConfigs() map[string]Switch { return c.ctx } // ConfigsForPortsParsed a helper method for the Config call that will additionally populate the Config // element in the Assignment for each assignment on a Record. func (c *Client) ConfigsForPortsParsed(a []tools.PortParsed) []error { errs := make([]error, 0) for i := range a { config, err := c.Config(a[i]) if err != nil { errs = append(errs, fmt.Errorf("config:%s[%s]:%s", a[i].SwitchName, a[i].PortName, err)) } a[i].Config = &config } return errs } // Config is the main calling method for this package after initialization. This method will // reach out to the switch infrastructure and gather detailed information about a switch and // the port and return the port information back to the calling function. func (c *Client) Config(eth tools.PortParsed) (netparse.IfcConfig, error) { switchName := fmt.Sprintf("%s.%s", eth.SwitchName, eth.DataCenter) if _, ok := c.ctx[switchName]; !ok { if err := c.unipedeConfig(switchName); err != nil { return netparse.IfcConfig{}, err } } return netparse.IfcByName(eth.PortName, c.ctx[switchName].IntConfig) } // unipedeConfig method will pull the switch's config from unipede func (c *Client) unipedeConfig(switchName string) error { // Setting an empty switch here so that we have something in the context if // we error prior to populating all the switch info. This should prevent us from // attempting multiple calls to a switch that has already failed. c.ctx[switchName] = Switch{} opts := &unipede.UnipedeOptions{} configText, err := c.uni.GetSwitchConfig(switchName, nil, opts) if err != nil { return err } if configText == nil { return fmt.Errorf("no configuration found for %s", switchName) } info, err := c.uni.Device(switchName, nil, opts) if err != nil { return err } vlans, err := c.uni.UpDeviceVlans(switchName, nil, opts) if err != nil { return err } neiFilter := "direction:=north" neighbors, err := c.uni.UpNeighbors(switchName, nil, &unipede.UnipedeOptions{Filter: &neiFilter}) if err != nil { return err } newSwitch := Switch{ Name: switchName, IntConfig: netparse.IfcConfigParse(*configText), Routes: netparse.IPRouteParse(*configText), Vlans: func() []netparse.Vlan { retval := make([]netparse.Vlan, 0) for _, v := range vlans.Items { retval = append(retval, netparse.Vlan{ Vlan: v.Vlan, Name: v.Name, Shut: strings.ToLower(v.Status) != "active", SVIDevices: make([]string, 0), }) } return retval }(), Model: info.ChassisModel, Upstreams: func() []string { r := make([]string, 0) for _, upstream := range neighbors.Items { if strings.EqualFold(switchName, upstream.RemoteHostname) || common.IContains(upstream.RemoteHostname, "core") { continue } if common.StringInSlice(upstream.RemoteHostname, r) { continue } r = append(r, upstream.RemoteHostname) } return r }(), } macViews, err := c.uni.UpMacView(switchName, nil, nil, opts) if err != nil { return err } newSwitch.Macs = newSwitch.macAddressMapper(macViews.Items) var errs error for _, up := range newSwitch.Upstreams { if _, ok := c.ctx[up]; !ok { if err := c.unipedeConfig(up); err != nil { if errs == nil { errs = fmt.Errorf("[%s:%s]", up, err) } else { errs = fmt.Errorf("%s/[%s:%s]", errs, up, err) } } } } c.IfcsForVlans(&newSwitch) c.ctx[switchName] = newSwitch return errs } // MacAddressesForPort will crawl all the switch mac-address mappings for any mac that has // a port by the name passed in. func (s *Switch) MacAddressesForPort(ifcName string) []string { r := make([]string, 0) for mac, ifcs := range s.Macs { for _, ifc := range ifcs { if ifcName == ifc { _, macAddr := MacParse(mac) r = append(r, macAddr) break } } } return r } // MacVlansForPort will crawl all the switch mac-address mappings for any mac that has // a port by the name passed in. func (s *Switch) MacVlansForPort(ifcName string) []int { r := make([]int, 0) for mac, ifcs := range s.Macs { for _, ifc := range ifcs { if ifcName == ifc { vlan, _ := MacParse(mac) if !common.IntInSlice(vlan, r) { r = append(r, vlan) } break } } } return r } // macAddressMapper will search the mapping of mac addresses to determine the port that // the mac address lives on. func (s *Switch) macAddressMapper(macs []unipede.MacViewItems) map[string][]string { r := make(map[string][]string) for _, m := range macs { // These little guys, I wouldn't worry about these little guys. if m.Mac == "ffff.ffff.ffff" || m.Mac == "3333.0000.000d" { continue } // If the mac-address doesn't have an interface name associated with it, it's not // going to do us any good. if m.UpInterfaceName == "" { continue } // If the mac-address is from a root port or a vPC peer-link, then we don't want it if m.UpDeviceVlanPortRole == "root" || strings.Contains(m.Type, "peer-link") { continue } macStr := MacString(m) // Now we've got a good mac, lets create the slice if it doesn't exist. if _, ok := r[macStr]; !ok { r[macStr] = make([]string, 0) } if !common.StringInSlice(m.UpInterfaceName, r[macStr]) { r[macStr] = append(r[macStr], m.UpInterfaceName) } } // Once we have the mac to interface mapping, we need to go back through the list to get the // interfaces that are part of the port-channel for any mac-addresses that showed up as connected // to a port-channel. for mac, ifcs := range r { for _, ifc := range ifcs { if !strings.Contains(ifc, "Port-channel") { continue } if pc, err := strconv.Atoi(strings.Replace(ifc, "Port-channel", "", -1)); err == nil { for _, ifc := range s.IntConfig { if ifc.ChannelGroup == 0 { continue } if ifc.ChannelGroup != pc { continue } if !common.StringInSlice(ifc.Name, r[mac]) { r[mac] = append(r[mac], ifc.Name) } } } } } return r } // IfcsForVlans method will run on the switch and populate any layer 3 interfaces // for the vlans on the switch. func (c *Client) IfcsForVlans(s *Switch) { for idx := range s.Vlans { if s.Vlans[idx].SVIConfig != nil { continue } for _, swIfc := range s.IntConfig { if swIfc.Name == fmt.Sprintf("Vlan%d", s.Vlans[idx].Vlan) { s.Vlans[idx].SVIConfig = &swIfc s.Vlans[idx].SVIDevices = append(s.Vlans[idx].SVIDevices, s.Name) break } } if s.Vlans[idx].SVIConfig != nil { continue } for _, up := range s.Upstreams { if _, ok := c.ctx[up]; ok { for _, i := range c.ctx[up].IntConfig { if i.Name == fmt.Sprintf("Vlan%d", s.Vlans[idx].Vlan) { s.Vlans[idx].SVIConfig = &i s.Vlans[idx].SVIDevices = append(s.Vlans[idx].SVIDevices, up) break } } } } } } // MacString will string together the mac-address and vlan combination func MacString(mac unipede.MacViewItems) string { return fmt.Sprintf("%d::%s", mac.Vlan, mac.Mac) } // MacParse will parse the vlan::mac combination into a useable format func MacParse(mac string) (int, string) { s := strings.Split(mac, "::") if len(s) != 2 { return 0, mac } i, err := strconv.Atoi(s[0]) if err != nil { return 0, mac } return i, s[1] }