// 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 ecm import ( "fmt" "net" "strconv" "strings" "github.com/labstack/gommon/color" "github.rackspace.com/DCM/discover/cmdb" "github.rackspace.com/DCM/discover/common" "github.rackspace.com/DCM/discover/log" "github.rackspace.com/DCM/netparse" "github.rackspace.com/DCM/tools" "github.rackspace.com/gorax/clients/dcxAutohost" ) type ( // Config is the parent type used as the response to the MakeENVs // function. Config struct { Environments []NetEnv `json:"environments"` PublicVlans []netparse.Vlan `json:"public_vlans"` Logs log.Collector `json:"logs"` } // NetEnv is a representation of a temporary environment we will // build out in the discovery process so we can send it into the // Update method NetEnv struct { Number string `json:"number" bson:"number"` Gateways []cmdb.Model `json:"gateways" bson:"gateways"` Devices []cmdb.Model `json:"devices" bson:"devices"` Vlans []netparse.Vlan `json:"vlans" bson:"vlans"` RoutedBlocks []netparse.IPRoute `json:"routed_blocks" bson:"routed_blocks"` PrivateBlocks []cmdb.Block `json:"private_blocks" bson:"private_blocks"` Translations []common.Translation `json:"vlan_translations" bson:"vlan_translations"` } ) // GatewayDevices is a helper method attached to the NetEnv struct to pull the gateway // devices out of the NetEnv struct func (e *NetEnv) GatewayDevices() []int { r := make([]int, 0) for _, d := range e.Gateways { r = append(r, d.ID) } return r } // EnvDevices is a helper method attached to the NetEnv struct to pull the environment // devices out of the NetEnv struct func (e *NetEnv) EnvDevices() []int { r := make([]int, 0) for _, d := range e.Devices { r = append(r, d.ID) } return r } // TranslationMap method attached to the Translation type will generate a mapping // of source->destination vlan ID mappings func (e *NetEnv) TranslationMap() map[int]int { vx := make(map[int]int) for _, vl := range e.Translations { if vl.MigrationVlan.Vlan != 0 { vx[vl.ConfiguredVlan.Vlan] = vl.MigrationVlan.Vlan } } return vx } // SetVlanTranslation is similar to the SetTranslationsFromDB method, except that it will use a // common.VlanX as its source type. func (e *NetEnv) SetVlanTranslation(vlan int, xlate common.VlanX) { // Add the newly created segment as a translation for i := range e.Translations { if e.Translations[i].ConfiguredVlan.Vlan != vlan { continue } if e.Translations[i].MigrationVlan.ZoneName != xlate.ZoneName { continue } e.Translations[i].MigrationVlan = xlate } } // SetTranslationsFromDB is a helper method attached to the NetEnv struct that will set any migration // vlans that have already been generated and set in the DB. func (e *NetEnv) SetTranslationsFromDB(xlates []dcxAutohost.EnvironmentMigrationVlan) { for i := range e.Translations { for _, x := range xlates { if x.ConfiguredVlan.VlanNumber != e.Translations[i].ConfiguredVlan.Vlan { continue } e.Translations[i].MigrationVlan.ZoneName = x.MigrationVlan.ZoneName e.Translations[i].MigrationVlan.Vlan = x.MigrationVlan.VlanNumber e.Translations[i].MigrationVlan.NetworkSegments = x.MigrationVlan.NetworkSegments break } } } // Alias function will use a common strategy to build a alias name for // a given network environment. func (e *NetEnv) Alias() string { for _, i := range e.Devices { name := strings.Replace(i.Account.Name, " ", "_", -1) if len(name) > 6 { name = name[:6] } //nolint return fmt.Sprintf("%s-%s", strings.ToUpper(name), strings.ToUpper(i.Region)) } return "" } // IPAddressAssigner will build the payload of assignments we need to send to DCX for this // particular network environment. func (e *NetEnv) IPAddressAssigner(envNATs map[string]cmdb.NAT) []dcxAutohost.IPAssign { r := make([]dcxAutohost.IPAssign, 0) for i := range e.Devices { usedPrimary := false primaryIP := e.Devices[i].Device.Device.PrimaryIP addresses := make([]dcxAutohost.Address, 0) for _, mac := range e.Devices[i].MACs { if val, ok := envNATs[mac]; ok { if val.Real == nil { continue } // Get the string value for each of these addresses mappedIP := "" realIP := val.Real.String() if val.Mapped != nil { mappedIP = val.Mapped.String() } // Add this address onto the list of device addresses e.Devices[i].IPs = append(e.Devices[i].IPs, cmdb.IPAssignment{Public: mappedIP, Private: realIP}) // Create the address object with the assumption that we are going to assign it with no role. // We will figure out if we should move this to the NAT later. address := dcxAutohost.Address{Role: "", Type: "exnet", Address: realIP} // If we've already set an address as the primary, then remove the role if mappedIP == primaryIP || realIP == primaryIP { address.Role = "primary" usedPrimary = true } // If the mapped address is set on the NAT, then we need to convert this type to a publicnet address // and set the IP to the mapped IP and the NAT to the private IP. if val.Mapped != nil { address.Type = "publicnet" address.Address = mappedIP address.NAT.Address = realIP } addresses = append(addresses, address) } } // If we never found the primary assignment from the macs and ARPs, then add it on here. if !usedPrimary && primaryIP != "" { gotNAT := false for i := range envNATs { if envNATs[i].Mapped.String() == primaryIP { addresses = append(addresses, dcxAutohost.Address{ Role: "primary", Type: "publicnet", Address: primaryIP, NAT: dcxAutohost.StaticNAT{Address: i}}) gotNAT = true break } if i == primaryIP { addresses = append(addresses, dcxAutohost.Address{ Role: "primary", Type: "publicnet", Address: envNATs[i].Mapped.String(), NAT: dcxAutohost.StaticNAT{Address: i}}) gotNAT = true break } } if !gotNAT { addresses = append(addresses, dcxAutohost.Address{Role: "primary", Type: "publicnet", Address: primaryIP}) } } // If we found some address mappings from the mac-addresses, then add them onto the request list. if len(addresses) != 0 { r = append(r, dcxAutohost.IPAssign{Addresses: addresses, DeviceID: e.Devices[i].ID}) } } return r } // PublicBlockAdder method on the NetEnv pointer receiver will add a static route // onto the route blocks for an environment func (e *NetEnv) PublicBlockAdder(routes []netparse.IPRoute) { rbIdx := -1 for _, route := range routes { // Find the environment RoutedBlocks with the same next-hop IP, or -1 // if not found. for rbIdx = range e.RoutedBlocks { if e.RoutedBlocks[rbIdx].NextHop.String() == route.NextHop.String() { break } } // If a next-hop address is not found, then we can add the entire route type // to the list and continue onto the next item in the routes list. if rbIdx == -1 { e.RoutedBlocks = append(e.RoutedBlocks, route) continue } // For each destination network in the route object, check to see if the // environment RouteBlock destinations already has the same CIDR defined. // If it doesn't then we need to add it onto the destinations. for _, rd := range route.Dest { hasDest := false for i := range e.RoutedBlocks[rbIdx].Dest { if e.RoutedBlocks[rbIdx].Dest[i].CIDR() == rd.CIDR() { hasDest = true break } } if !hasDest { e.RoutedBlocks[rbIdx].Dest = append(e.RoutedBlocks[rbIdx].Dest, rd) } } } } // PrivateBlockAdder method on the NetEnv pointer receiver will add a block to the environment // if it does not already exist. func (e *NetEnv) PrivateBlockAdder(d cmdb.Model) { for _, dBlk := range d.Blocks { exist := false for i := range e.PrivateBlocks { if e.PrivateBlocks[i].CIDR == dBlk.CIDR { exist = true break } } if !exist { e.PrivateBlocks = append(e.PrivateBlocks, dBlk) } } } // VirtualDeviceAdder method will go through a list of virtual devices, or otherwise not evaluated due to // no port assignments, and determine if we can assign a device to an environment based on its // primary IP. func (e *NetEnv) VirtualDeviceAdder(d cmdb.Model) error { ip := net.ParseIP(d.Device.Device.PrimaryIP) if ip == nil { return fmt.Errorf("[%d] No PrimaryIP found", d.ID) } added := false for _, block := range e.RoutedBlocks { for _, netBlk := range block.Dest { _, netObj, err := net.ParseCIDR(netBlk.CIDR()) if err != nil { break } if netObj.Contains(ip) { added = true e.Devices = append(e.Devices, d) } } } if !added { return fmt.Errorf("[%d] PrimaryIP %s not found in blocks", d.ID, ip.String()) } return nil } // PrivateBlockToVlanAssigner method will go through the list of private blocks added to the environment, and // determine if they can be associated with a vlan. If so, we add that block onto the vlan translation func (e *NetEnv) PrivateBlockToVlanAssigner() { for _, block := range e.PrivateBlocks { if block.Vlan == nil { continue } for i := range e.Translations { if e.Translations[i].ConfiguredVlan.Vlan == *block.Vlan { e.Translations[i].ConfiguredVlan.CIDR = block.CIDR break } } } } // DeviceVlanAssigner method will use a combination of the discovered vlan config, along with the vlan // translations mapping, to generate a list of vlans that we should make the device+vlan association with. func (e *NetEnv) DeviceVlanAssigner(d cmdb.Model) []int { r := make([]int, 0) vx := e.TranslationMap() for _, vl := range d.Vlans { if xvl, ok := vx[vl.Vlan]; ok { r = append(r, xvl) } } return r } // PortSegmentAssigner method will use a combination of the discovered vlan config, along with the port-maps // and vlan translations to create a listing of port+segment associations we'll need to make for this device. func (e *NetEnv) PortSegmentAssigner(d cmdb.Model) []dcxAutohost.SwitchVlanReq { r := make([]dcxAutohost.SwitchVlanReq, 0) // Anonymous function to get the list of vlan translations that need to be // mapped to this specific port. vlansForPort := func(p tools.PortParsed) []int { r := make([]int, 0) for _, vl := range p.Vlans { for _, vlt := range e.Translations { if vlt.ConfiguredVlan.Vlan == vl { r = append(r, vlt.MigrationVlan.Vlan) } } } return r } // Anonymous function to get the port map that matches this specific source port configuration. portMapForPort := func(portMaps []*dcxAutohost.PortMapPair, port tools.PortParsed) *dcxAutohost.PortMapPair { for _, pm := range portMaps { if pm.Source.Switch.Number == port.SwitchName && pm.Source.Name == port.PortName { return pm } } return nil } // Logic to generate a list of SwitchVlanReq for each vlan that needs to be associated to this // migration port. for _, port := range d.Environment.Current.PortsParsed { for _, vl := range vlansForPort(port) { portMap := portMapForPort(d.PortMap.PortMaps, port) if portMap == nil { continue } r = append(r, dcxAutohost.SwitchVlanReq{ Switch: portMap.Migration.Switch.Number, SwitchPort: portMap.Migration.PortNumber, VlanNumber: vl, }) } } return r } // IPBlockAssigner method will generate a request to send to DCX to create the IP block association // in environment manager. func (e *NetEnv) IPBlockAssigner(dt netparse.DestinationNetwork, env string) dcxAutohost.BlockSpec { accountNum, accountName := -1, "" for i := range e.Devices { if e.Devices[i].Account.Number > 1 { accountNum = e.Devices[i].Account.Number accountName = e.Devices[i].Account.Name break } } return dcxAutohost.BlockSpec{ AccountNumber: fmt.Sprintf("%d", accountNum), Description: fmt.Sprintf("%d - %s", accountNum, accountName), CIDR: dt.CIDR(), Environment: env, PolicyName: "host", } } // DeviceAdder is method attached to the NetEnv pointer receiver that is used to // perform add operations on the NetEnv for elements that exist within the device model. // The current operations are: // 1.) Add Device Model as a Gateway or Device // 2.) Add Public IP Blocks based on static routes if a Gateway model // 3.) Add Private IP Block based on blocks if a Gateway model. func DeviceAdder(ds []cmdb.Model, env NetEnv) NetEnv { for _, d := range ds { exist := false for _, e := range env.Devices { if e.Linked.ID == d.Linked.ID { exist = true break } } for _, e := range env.Gateways { if e.Linked.ID == d.Linked.ID { exist = true break } } if !exist { if d.IsGateway { env.Gateways = append(env.Gateways, d) env.PublicBlockAdder(d.Routes) env.PrivateBlockAdder(d) } else { env.Devices = append(env.Devices, d) } } for _, v2 := range d.Vlans { vlanExist := false for _, v := range env.Vlans { if v2.Vlan == v.Vlan { vlanExist = true break } } if !vlanExist { xl := common.Translation{ ConfiguredVlan: common.VlanX{Vlan: v2.Vlan, NetworkSegments: []string{v2.SegmentName()}, ZoneName: v2.Zone}, } if v2.MigrationZone != "" { xl.MigrationVlan.ZoneName = v2.MigrationZone } env.Translations = append(env.Translations, xl) env.Vlans = append(env.Vlans, v2) } } } return env } // SourceZone will pull a zone for each of the vlans mapped in the xlates. func (e *NetEnv) SourceZone() string { r := "" for _, device := range e.Devices { if device.PortMap.SourceDC != "" && device.PortMap.SourceDC != "" { r = device.PortMap.SourceDC + ":ZoneDefault" break } } for _, x := range e.Vlans { if len(x.SVIDevices) != 0 { r = strings.ToUpper(x.SVIDevices[0]) break } } if !strings.Contains(r, ".") { return r } split := strings.Split(r, ".") if len(split) > 1 { r = fmt.Sprintf("%s:Zone%s", split[1], split[0]) } return r } // EnvNATs will pull a consolidated list of unique NAT statements across all // edge devices in an environment func (e *NetEnv) EnvNATs() map[string]cmdb.NAT { r := make(map[string]cmdb.NAT) for _, d := range e.Gateways { for k := range d.NATs { if _, ok := r[k]; !ok { r[k] = d.NATs[k] } } } for _, d := range e.Devices { for k, v := range d.NATs { if _, ok := r[v.MAC]; !ok { r[v.MAC] = d.NATs[k] } } } return r } // PublicVlanAdder method on the Config pointer receiver will add a vlan onto // the public vlans (SVIs) in the builder response. func (r *Config) PublicVlanAdder(vl netparse.Vlan) { for i := range r.PublicVlans { if r.PublicVlans[i].SegmentName() == vl.SegmentName() { r.PublicVlans[i] = vl return } } r.PublicVlans = append(r.PublicVlans, vl) } // Print is a method attached to the Config type used to debug output from the // Config built in memory. func (r Config) Print() { for _, t := range r.Environments { fmt.Print(color.Magenta("[++NEW ENVIRONMENT++]\n")) fmt.Print(color.Magenta("-----------------------------\n")) for _, d := range t.Devices { fmt.Printf("\tDEVICE [%s] IsGateway=%v Model=%s Mac-Count=%d\n", color.Green(d.Linked.ID), d.IsGateway, d.Device.Device.MakeModel, len(d.MACs)) } for _, d := range t.Gateways { fmt.Printf("\tDEVICE [%s] IsGateway=%v Model=%s\n", color.Green(d.Linked.ID), d.IsGateway, d.Device.Device.MakeModel) } for _, d := range t.Translations { fmt.Printf("\tVLAN [Vlan%s] Segment=%v Block=%s\n", color.Cyan(d.ConfiguredVlan.Vlan), d.ConfiguredVlan.NetworkSegments, d.ConfiguredVlan.CIDR) } for _, r := range t.PrivateBlocks { vlan := "" if r.Vlan != nil { vlan = strconv.Itoa(*r.Vlan) } fmt.Printf("\tPRIV BLOCK [%s] Vlan=%s\n", color.Yellow(r.CIDR), vlan) } for _, r := range t.RoutedBlocks { for _, dest := range r.Dest { fmt.Printf("\tPUBL BLOCK [%s] NextHop=%s\n", color.Yellow(dest.CIDR()), r.NextHop.String()) } } } for _, vl := range r.PublicVlans { fmt.Printf(color.Green("[NA SEGMENT] Name=%s Vlan=%s\n"), vl.SegmentName(), vl.Name) } }