// 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 discover contains the business logic to pull switchport data for a device and // interface with Rackspace systems to add to an environment. package ecm import ( "fmt" "strings" "github.com/sirupsen/logrus" "github.rackspace.com/DCM/discover/cmdb" "github.rackspace.com/DCM/discover/common" "github.rackspace.com/DCM/discover/log" "github.rackspace.com/DCM/discover/network" "github.rackspace.com/DCM/netparse" ) // Build will build the environment(s) locally in memory before checking/sending to // Update methods of the EM interface. func (c *DCX) Build(r []cmdb.Model, switchCtx map[string]network.Switch) (Config, error) { exNetVlanMap := make(map[int][]cmdb.Model) resp := Config{PublicVlans: make([]netparse.Vlan, 0)} notEvaluated := make([]cmdb.Model, 0) c.Debug("BuildEnv", "", fmt.Sprintf("Running on %d devices...", len(r))) // 1.) We need to find the edge devices and add the vlan details onto the devices. We can do this by going // through all the devices for the account and finding devices in a Vlan that have an SVI in // public IP space. Typically, we would determine the edge device by where the IP address routes to, // but the IP assignments in admin.mydatapipe are extremely unreliable. for i := range r { if len(r[i].Environment.Current.PortsParsed) == 0 { notEvaluated = append(notEvaluated, r[i]) continue } for idx, v := range r[i].Environment.Current.PortsParsed { // If there is no switch name, then we can't get the details about // the switch config, for obvious reasons. if v.SwitchName == "" { continue } if v.Config.Shutdown { c.Warn("Shutdown", r[i].ID, fmt.Sprintf("%s.%s[%s]", v.SwitchName, v.DataCenter, v.PortName)) continue } // Grab the switch information from the switchCtx. sw := switchByName(v.SwitchName, switchCtx) if sw == nil { continue } // We need the port-config to compare with the switch config // or we cannot continue if v.Config == nil { continue } // Check for open trunks, if it is an open trunk, append the mac address vlans // found to the vlans allowed on the trunk if v.Config.Mode == "trunk" && len(v.Config.VlanAllowed) == 0 { v.Config.VlanAllowed = sw.MacVlansForPort(v.PortName) c.Error("OpenTrunk", r[i].ID, fmt.Errorf("%s.%s[%s] MacVLANs:%v", v.SwitchName, v.DataCenter, v.PortName, v.Config.VlanAllowed)) } // Catch ports that are specifically configured for private vlan associations if v.Config.Mode == "private-vlan host" && len(v.Config.PrivVlanAssoc) != 0 { v.Config.AccessVlan = v.Config.PrivVlanAssoc[0].Primary } // Associate all mac addresses from this port r[i].MACs = append(r[i].MACs, sw.MacAddressesForPort(v.PortName)...) // VLANs that we'll need to check vlansToAudit := make([]int, 0) c.Debug("BuildEnv:Port", r[i].Linked.ID, fmt.Sprintf("%s[%s] (%d,%d,%v)", v.SwitchName, v.PortName, v.Config.AccessVlan, v.Config.NativeVlan, v.Config.VlanAllowed)) if v.Config.AccessVlan > 1 { vlansToAudit = append(vlansToAudit, v.Config.AccessVlan) } // Native VLAN: Perform similar checks as the Access VLAN. if v.Config.NativeVlan > 1 { vlansToAudit = append(vlansToAudit, v.Config.NativeVlan) } // Allowed VLANs: Perform similar checks as the Access and Native VLANs, for each iteration // of the Allowed VLANs group. for _, allowedVlan := range v.Config.VlanAllowed { if allowedVlan > 1 { vlansToAudit = append(vlansToAudit, allowedVlan) } } // Set the unique vlans found on this port to the Vlans type in the PortParsed parent. r[i].Environment.Current.PortsParsed[idx].Vlans = common.UniqueInts(vlansToAudit) // For each unique VLAN: Check to see if we find the vlan in the list of switch vlans // then determine if an SVI exist that is a public IP. If that's the case, // we know we have a gateway device. for _, vl := range r[i].Environment.Current.PortsParsed[idx].Vlans { if vlan := c.vlanByID(vl, sw); vlan != nil { c.Debug("BuildEnv:Vlan", r[i].Linked.ID, fmt.Sprintf("Found Vlan%d (%s)", vlan.Vlan, vlan.Name)) // If it's not a gateway vlan, then we can add it to the list of // vlans to associate devices to. This will prevent grouping devices together // by a gateway or public vlan, since those might be shared. if isGw := c.isGatewayVLAN(vlan); isGw { r[i].IsGateway = true for _, d := range vlan.SVIDevices { sw := switchByName(d, switchCtx) if sw == nil { continue } for _, route := range sw.Routes { if route.NextHop.String() == r[i].Device.Device.PrimaryIP { r[i].Routes = append(r[i].Routes, route) } } } // Since this device is public, lets look for the migration ports assigned to find out what zone // this device is going to land in, so we know where to create the SVI/Network Segments for _, port := range r[i].Environment.Migration.PortsParsed { if strings.EqualFold(port.PortType, "Public") { sw := fmt.Sprintf("%s.%s", port.SwitchName, r[i].Environment.Migration.DataCenter) infraDev, err := c.dcx.InfrastructureDevice(sw) if err != nil { c.Error("BuildEnv:VlanZone", r[i].ID, fmt.Errorf("unable to get zone for %s", sw)) continue } vlan.MigrationZone = infraDev.ZoneNumber } } resp.PublicVlanAdder(*vlan) } r[i].Vlans = append(r[i].Vlans, *vlan) c.addToVlanMap(vl, r[i], exNetVlanMap) } } } } // 2.) Now, we should have set the isGateway flag for any devices that are associated with // a vlan that has a public SVI associated with it. We should also have a mapping of vlan ids // to devices that are associated to them. We need to go through this list now to group all the // devices that are parts of the same vlans. groupedVlans := groupVlans(exNetVlanMap) resp.Environments = make([]NetEnv, len(groupedVlans)) for i, vlanGroup := range groupedVlans { resp.Environments[i] = NetEnv{ Gateways: make([]cmdb.Model, 0), Devices: make([]cmdb.Model, 0), Vlans: make([]netparse.Vlan, 0), RoutedBlocks: make([]netparse.IPRoute, 0), PrivateBlocks: make([]cmdb.Block, 0), Translations: make([]common.Translation, 0), } for _, vlan := range vlanGroup { resp.Environments[i] = DeviceAdder(exNetVlanMap[vlan], resp.Environments[i]) } // 2.a.) By this time, we should have the gateway devices in the environment, if we don't // then we need to fallback to another method as EM requires a gateway device. if len(resp.Environments[i].Gateways) == 0 { newDevices := make([]cmdb.Model, 0) for _, d := range resp.Environments[i].Devices { // So if it's a firewall, then add it as a gateway device if strings.Contains(strings.ToLower(d.Device.Device.Type), "firewall") { d.IsGateway = true resp.Environments[i].Gateways = append(resp.Environments[i].Gateways, d) } else { newDevices = append(newDevices, d) } } resp.Environments[i].Devices = newDevices } zone := resp.Environments[i].SourceZone() for x := range resp.Environments[i].Translations { resp.Environments[i].Translations[x].ConfiguredVlan.ZoneName = zone } } // 3.) Once we have all the environments created, and the source vlans assigned, we need to go back // through them and assign private IP blocks to vlans if we are able to. for i := range resp.Environments { resp.Environments[i].PrivateBlockToVlanAssigner() } // 4.) AND THEN, we go back through the environment and see if we can add not-evaluated device to the // environments based on IP assignments. After this, we will have a trimmed list of the notEvaluated // that couldn't be added based on IP assignments either. We'll find a way to handle those later. orphaned := make([]cmdb.Model, 0) for _, d := range notEvaluated { for i := range resp.Environments { if err := resp.Environments[i].VirtualDeviceAdder(d); err != nil { orphaned = append(orphaned, d) } } } // If there are no orphaned devices, we can stop processing here. if len(orphaned) == 0 { return resp, nil } // 5.) Finally, we need to handle the last few devices that do not have an IP address or port identifier in the // CMDB, so it's gets pretty sketch here. // 5.a.) First we find the environment with the most amount of devices. Since devices are divided up by // account, we are making an assumption here that the environment with the most amount of devices // is the environment where these orphaned devices will live. dCnt := 0 iter := -1 for i := range resp.Environments { if dCnt < len(resp.Environments[i].Devices) { dCnt = len(resp.Environments[i].Devices) iter = i } } if iter == -1 { logrus.Errorf("Unable to find environment to add orphaned devices to.") return resp, nil } // 5.b.) Now that we have the assumed environment, add those devices to the environment. logrus.Infof("ENV with %d devices, adding %d more orphaned devices.", len(resp.Environments[iter].Devices), len(orphaned)) resp.Environments[iter].Devices = append(resp.Environments[iter].Devices, orphaned...) // DEBUG Purposes //for _, l := range c.Logs { // l.Print(log.DebugLvl) //} resp.Logs = log.Collector{ Logs: c.Logs, Errors: c.Errors, } return resp, nil } func groupVlans(vMap map[int][]cmdb.Model) [][]int { retVal := make([][]int, 0) for _, r2 := range vMap { vlans := vlansForDevices(r2) if idx := hasVlanGroup(vlans, retVal); idx != -1 { retVal[idx] = append(retVal[idx], vlans...) } else { retVal = append(retVal, vlans) } } for i, vlans := range retVal { retVal[i] = common.UniqueInts(vlans) } return retVal } func vlansForDevices(devices []cmdb.Model) []int { vlans := make([]int, 0) for i := range devices { for _, v := range devices[i].Vlans { if !common.IntInSlice(v.Vlan, vlans) { vlans = append(vlans, v.Vlan) } } } return vlans } func hasVlanGroup(vlans []int, vg [][]int) int { for idx, v1 := range vg { for _, vlan := range v1 { for _, v := range vlans { if v == vlan { return idx } } } } return -1 } func switchByName(name string, switchCtx map[string]network.Switch) *network.Switch { for k, s := range switchCtx { if strings.HasPrefix(strings.ToLower(k), strings.ToLower(name)) { return &s } } return nil } func (c *DCX) isGatewayVLAN(vlan *netparse.Vlan) bool { if vlan.SVIConfig != nil { for i := range vlan.SVIConfig.IPAddress { if common.IsPublicIP(vlan.SVIConfig.IPAddress[i].Address) { return true } } } return false } func (c *DCX) vlanByID(vlanID int, sw *network.Switch) *netparse.Vlan { // If we've already mapped the vlans for this switch and we've found // something, return the vlan that matches. if val, ok := c.vlansByID[fmt.Sprintf("%s_%d", sw.Name, vlanID)]; ok { return &val } // Now let's check to see if we've already updated the map for this switch. // If so, then the vlan doesn't exist in the map. if strings.Contains(sw.Updated, "vlanByID") { return nil } // Then, mark this switch as ran, then go through and map the vlans. sw.Updated = fmt.Sprintf("%s:vlanByID", sw.Updated) for _, i := range sw.Vlans { c.vlansByID[fmt.Sprintf("%s_%d", sw.Name, i.Vlan)] = i } // Finally check to see if this vlan is in the ones we just mapped. if val, ok := c.vlansByID[fmt.Sprintf("%s_%d", sw.Name, vlanID)]; ok { return &val } return nil } // addToVlanMap method on the vlanMap will determine if a device should be added // to a vlan. func (c *DCX) addToVlanMap(vlan int, d cmdb.Model, vlanMap map[int][]cmdb.Model) { if _, ok := vlanMap[vlan]; !ok { vlanMap[vlan] = []cmdb.Model{d} return } // If the Datapipe DID matches one of the devices in the // map for this vlan, we do not need to add. for i, device := range vlanMap[vlan] { if device.Linked.ID == d.Linked.ID { // If the vlan passed in updated the IsGateway flag, // update the device record if d.IsGateway && !device.IsGateway { vlanMap[vlan][i] = d } return } } vlanMap[vlan] = append(vlanMap[vlan], d) }