// 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 cmdb import ( "fmt" "net" "strconv" "strings" "sync" "github.com/gofrs/uuid" "github.com/sirupsen/logrus" "github.rackspace.com/DCM/discover/common" "github.rackspace.com/DCM/netparse" "github.rackspace.com/gorax/asaapi/pkg/asa" "github.rackspace.com/gorax/asaapi/pkg/asaModels" "github.rackspace.com/gorax/clients/coreAPI" "github.rackspace.com/gorax/clients/dcxAutohost" "github.rackspace.com/gorax/clients/goctk" "github.rackspace.com/gorax/clients/mongodbAPI" "github.rackspace.com/gorax/clients/mydp" "github.rackspace.com/gorax/clients/sshAPI" "github.rackspace.com/gorax/passkey" ) // DP type represents client references to third-party resources required // for the methods within this package. type DP struct { sync.RWMutex MyDP *mydp.Client CTK *goctk.Client MGO *mongodbAPI.Client DCX *dcxAutohost.Client DeviceMap map[string]DeviceInfo CustomerMap map[string]mydp.Customer key *passkey.Client } // DeviceInfo struct is an internal struct (but used by exported functions) that contains // a consolidated list of mydp structs type DeviceInfo struct { mydp.Device Eths []mydp.EtherPort } // DPDatacenter is mapping of the datacenters as they are named in admin.mydatapipe with // a key value of the Rackspace region we would consider that DC to be in. var DPDatacenter = map[string]string{ "alicloud hkg": "hkg", "amazon ap-northeast": "jpn", "amazon ap-southeast": "sin", "amazon cn-north-1": "bjs", "amazon eu-west": "irl", "amazon sa-east-1": "sao", "amazon us-east": "cloud", "amazon us-west-1": "cloud", "amazon us-west-2": "cloud", "ams01 - layered tech amsterdam": "ams", "ams02 - gogrid amsterdam(ams2)": "ams", "ams03 - amsterdam": "ams", "clt - claremont, bbb": "clt", "customer's location/asia": "cdc", "customer's location/east na": "cdc", "customer's location/europe": "cdc", "customer's location/west na": "cdc", "cwl - adapt cardiff - st. mellons(cwl1)": "cwl", "cwl- newport bt data centre(cwl2)": "cwl", "de1 - equinix de1(den80)": "den", "den1 - centurylink dn1 (highlands ranch)": "den", "dexin bidg, beijing sinnet technology co., ltd.": "bjs", "dfw01 - databank dallas(dfw4)": "dfw", "equinix hk5(hkg80)": "hkg", "equinix hong kong": "hkg", "equinix rio de janeiro": "rio", "equinix sao paulo sp2": "sao", "ewr - 165 halsey": "ewr", "fra - frankfurt, equinix (fra2)": "fra", "gs2_woking_virtualdc": "lon", "hkg - wanchai - hong kong office": "hkg", "hkg1 - mega-iadvantage(hkg31)": "hkg", "hkg2 - asia-tone i-center (equinix.hk3)": "hkg", "hkg3 -globalgateway (eqnxhk1)(hkg32)": "hkg", "hkg4 - telehouse (hkg3)": "hkg", "iad - ashburn, equinix dc10(iad4)": "iad", "iad - ashburn, equinix dc11": "iad", "iad - ashburn, equinix dc2(iad31)": "iad", "iad - ashburn, equinix dc4": "iad", "iad - ashburn, equinix dc5(iad5)": "iad", "iad - ragingwire": "iad", "iad01 - layered tech reston": "iad", "iad2 - centurylink dc10": "iad", "ixcellerate moscow one": "svo", "jersey city office": "jrc", "kef - iceland": "kef", "lax01 - layered tech los angeles": "lax", "lba - leeds angel house(lba1)": "lba", "lga01 - layered tech new york": "nyc", "lhr01 - layered tech london": "lon", "lon - level 3 braham street": "lon", "lon - level 3 goswell rd.(lon8)": "lon", "lon1 - london office": "lon", "lon2 - global switch east(lon7)": "lon", "lon3 -telehouseeast(lon34)": "lon", "lon4 - digital reality - sovereign house(lon33)": "lon", "lt import: unknown": "unknown", "mci - airworld kansas city (mci1)": "mci", "ny2 equinix secaucus, 275 hartz way": "nyc", "ny6 equinix secaucus, 105 enterprise avenue s.": "nyc", "nyc - equinix new york (111 8th street)": "nyc", "old austin office": "aus", "ord1 - rackspace chicago": "ord", "ord3 - coresite ch1": "ord", "rackspace hong kong": "hkg", "rackspace london": "lon", "seattle, equinix se3": "sea", "sfo - gogrid san francisco": "sfo", "sg1-equinix": "sin", "sg2-equinix": "sin", "sg3-equinix": "sin", "sha - equinix data center(yangpu)": "sha", "sha1 - jingqiao internet data center (closed)": "sha", "sha2 - sds1 data center (closed)": "sha", "sha3 - sds2 data center": "sha", "sin office - regus one fullerton": "sin", "sj2 - datapipe san jose": "sjc", "sj3 - coresite - 55 market st": "sjc", "sjc - equinix san jose, 11 great oaks": "sjc", "sje01 - layered tech santa clara": "sjc", "slo - equinix slough ld4": "slo", "slo - equinix slough ld5(lon6)": "slo", "smq - somerset - 125 belmont (nyc2)": "smq", "smq - somerset - campus (nyc1)": "smq", "wok - woking digital reality(lon9)": "wok", "yyc01 - layered tech calgary": "yyc", "yyz01 - layered tech toronto": "yyz", } // dpClient (Client) function should exist in every implementation of the Client interface. // This function will create an initialized type that will satisfy the requirements of the Client interface. func dpClient(key *passkey.Client) (*DP, error) { logrus.Debug("Initializing Datapipe Discovery Client.") client := &DP{key: key} mydpKey := key.DCM.MyAdmin.Credential dpClient, err := mydp.New(mydpKey.Username, mydpKey.Password, false) if err != nil { return nil, fmt.Errorf("dpClient:mydp.New:%s", err) } ssoKey := key.Service.SSO.Credential dcx, err := dcxAutohost.New(dcxAutohost.WithCredential(ssoKey.Username, ssoKey.Password), false) if err != nil { return nil, fmt.Errorf("dpClient:dcx.New:%s", err) } ctkKey := key.Service.Netsec01.Credential ctk, err := goctk.NewCTK(goctk.SSO(ctkKey.Username, ctkKey.Password)) if err != nil { return nil, fmt.Errorf("dpClient:goctk.NewCTK:%s", err) } client.MyDP = dpClient client.CTK = ctk client.DCX = dcx client.MGO = mongodbAPI.New(mongodbAPI.WithToken(ctk.Token), "migration", false) client.DeviceMap = make(map[string]DeviceInfo) client.CustomerMap = make(map[string]mydp.Customer) logrus.Debug("Initialization of Datapipe Discovery Client done.") return client, nil } // Get method takes a cmdb. pointer receiver and serves as the main // entry point into the gather implementation of this data type. // // Discover will contain all the custom logic to gather information about a device from the network as // well as any CMDB system records that we may need to make decisions or update environment manager. func (c *DP) Get(uniqueID string) (Model, error) { r := Model{ Routes: make([]netparse.IPRoute, 0), NATs: make(map[string]NAT), MACs: make([]string, 0), Blocks: make([]Block, 0), IPs: make([]IPAssignment, 0), } intID, err := strconv.Atoi(uniqueID) if err != nil { return r, fmt.Errorf("Get:ID:strconv.Atoi:%s", err) } r.Device.Linked.ID = intID // Get basic information about the device d, err := c.DeviceInfo(strconv.Itoa(r.Device.Linked.ID)) if err != nil { return r, fmt.Errorf("Get:mydp.DeviceByDeviceID:%d:%s", r.Device.Linked.ID, err) } c.Lock() if _, ok := c.CustomerMap[d.Device.CustomerID]; !ok { // Get account information on this customer. acct, err := c.MyDP.GetCustomer(common.Atoi(d.CustomerID)) if err != nil { c.Unlock() return r, err } c.CustomerMap[d.CustomerID] = *acct } c.Unlock() account := c.CustomerMap[d.CustomerID] r.ID = common.Atoi(d.RSCoreDeviceID) // Normalize the Datapipe DCs r.Region = DPDatacenter[strings.ToLower(d.DatacenterTitle)] r.Device.Device.PrimaryIP = d.PrimaryIP r.Device.Device.DataCenter = d.DatacenterTitle r.Device.Device.Type = d.SpecificDeviceType r.Device.Device.HostName = d.Name r.Account.Number = common.Atoi(account.RSCoreCustomerID) r.Account.Name = account.CustomerName r.Account.AM = account.ProjectManager r.Account.BDC = account.SalesEngineer r.Linked.SpecificID = common.Atoi(d.SpecificDeviceID) r.Linked.AccountNumber = common.Atoi(d.CustomerID) r.Linked.AccountName = d.CustomerName // getSpecificDevice is going to reach out to admin.mydatapipe and get details for the // specific device ID/specific device type. Given the more specific platform, this // should give us more details like, os name, os version, failover mate, etc.. if err := c.getSpecificDevice(&r); err != nil { logrus.Errorf("Get:range:getDevice:%s", err) } // portAssignments is going to reach out to myadmin once more and get the ether connections and format // them into a structure that we can work with similar to Rackspace locations. // This will also reach out to the DCX API to gather the migration ports as they are assigned in // the Rackspace DC for the RS device. if err := c.portAssignments(&r); err != nil { logrus.Errorf("Get:range:portAssignments:%s", err) } // getNATs is going to reach out to the actual device and attempt to pull NAT statements for the device. This // will only take action on supported network devices that have NAT statements, unsupported devices will return // nil. if err := c.getNATs(&r, &d.Device); err != nil { logrus.Errorf("Get:range:getNATs:%s", err) } if err := c.Map(&r); err != nil { logrus.Errorf("Get:range:portMap:%s", err) } return r, nil } // getNATs will assess the make_model attribute in the Device sub-type to determine if the device // passed in is eligible to have NATs collected on, if so, we will use the proper client to // gather NAT statements. func (c *DP) getNATs(d *Model, dp *mydp.Device) error { acsKey := c.key.Service.TacacsLogin.Credential if strings.Contains(strings.ToLower(d.Device.Device.MakeModel), "cisco asa") { core := &coreAPI.Client{SessionInfo: coreAPI.SessionInfo{Username: acsKey.Username}} uid, _ := uuid.NewV4() sshClient, err := sshAPI.NewDPSSHAPIClient(*dp, core, "asa", true, uid) if err != nil { return fmt.Errorf("NewOptASA:RS:NewSSHAPIClient:%s", err) } a, err := asa.NewOptASA(d.Linked.ID, acsKey.Username, acsKey.Password, asa.WithMyDPInfo(dp), asa.WithSSHAPI(sshClient)) if err != nil { return fmt.Errorf("NewOptASA:%s", err) } if err := a.GetNat(); err != nil { // Ok, so... this is our first attempt to gather data from the ASA. If we get an unable // to authenticate error, then it's probably because our Datapipe credentials didn't work and its // possibly on Rackspace TACACS. Let's make an attempt to reinitialize with RS creds and see if that // works. // // WHATS THE WORST THAT COULD HAPPEN, AMIRITE?! if strings.Contains(strings.ToLower(err.Error()), "unable to authenticate") { sshClient.UseAuth("rackspace") if a, err = asa.NewOptASA(d.Linked.ID, acsKey.Username, acsKey.Password, asa.WithMyDPInfo(dp), asa.WithSSHAPI(sshClient)); err != nil { return fmt.Errorf("NewOptASA:RS:ASA:%s", err) } if err := a.GetNat(); err != nil { return fmt.Errorf("GetNat:%s", err) } } } if err := a.GetArp(); err != nil { return fmt.Errorf("GetArp:%s", err) } if err := a.GetObjects(); err != nil { return fmt.Errorf("GetObjects:%s", err) } if err := a.GetInterfaces(); err != nil { return fmt.Errorf("GetInterfaces:%s", err) } for _, ifc := range a.Config.Interfaces { if ifc.IPAddress != nil || ifc.Netmask != nil { ip := net.ParseIP(*ifc.IPAddress) if !common.IsPublicIP(ip) { nm := net.ParseIP(*ifc.Netmask).To4() ones, bits := net.IPv4Mask(nm[0], nm[1], nm[2], nm[3]).Size() d.Blocks = append(d.Blocks, Block{ CIDR: fmt.Sprintf("%s/%d", ip.Mask(net.CIDRMask(ones, bits)), ones), }) } } for _, subIfc := range ifc.SubInt { if subIfc.IPAddress == nil || subIfc.Netmask == nil { continue } ip := net.ParseIP(*subIfc.IPAddress) if !common.IsPublicIP(ip) { nm := net.ParseIP(*subIfc.Netmask).To4() ones, bits := net.IPv4Mask(nm[0], nm[1], nm[2], nm[3]).Size() d.Blocks = append(d.Blocks, Block{ CIDR: fmt.Sprintf("%s/%d", ip.Mask(net.CIDRMask(ones, bits)), ones), Vlan: subIfc.Vlan, }) } } } for _, arp := range a.Status.ARP { d.NATs[arp.Address] = NAT{Real: net.ParseIP(arp.Address), MAC: arp.Mac} } objToIP := make(map[string]asaModels.NetworkObject) for _, o := range a.Config.Objects.NetworkObjects { if o == nil { continue } objToIP[o.Name] = *o } for _, n := range a.Config.Nat.Static { if natObj, ok := d.NATs[n.SourceIP.IP.String()]; ok { natObj.Mapped = n.DestinationIP d.NATs[n.SourceIP.IP.String()] = natObj } } for _, n := range a.Config.Nat.Auto { if val, ok := objToIP[n.Name]; ok { if val.Network == nil { continue } if val.Subnet == nil { if natObj, ok := d.NATs[*val.Network]; ok { natObj.Mapped = net.ParseIP(n.Xlate) d.NATs[*val.Network] = natObj } continue } // If we get here, that means the NAT is a subnet NAT, the rest of this // code is a bunch of junk to expand the subnets to get ALL THEM NATZ realCIDR, err := common.IPNetToCIDR(*val.Network, *val.Subnet) if err != nil { logrus.Errorf("[%d] NAT:%s %s:%s", d.ID, *val.Network, *val.Subnet, err) continue } mappedCIDR, err := common.IPNetToCIDR(n.Xlate, *val.Subnet) if err != nil { logrus.Errorf("[%d] NAT:%s %s:%s", d.ID, n.Xlate, *val.Subnet, err) continue } realIPs, err := common.ExpandBlock(realCIDR) if err != nil { logrus.Errorf("[%d] NAT:%s:%s", d.ID, realCIDR, err) continue } mappedIPs, err := common.ExpandBlock(mappedCIDR) if err != nil { logrus.Errorf("[%d] NAT:%s:%s", d.ID, mappedCIDR, err) continue } for i, r := range realIPs { if len(mappedIPs) >= i { if natObj, ok := d.NATs[r]; ok { natObj.Mapped = net.ParseIP(mappedIPs[i]) d.NATs[r] = natObj } else { d.NATs[r] = NAT{Real: net.ParseIP(r), Mapped: net.ParseIP(mappedIPs[i])} } } } } } } return nil }