tools/gcp-ips/main.go (215 lines of code) (raw):

// Copyright 2018 Google 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. // gcp-ips retrieves a list of IP addresses used by each subnet in a shared VPC // and writes the resulting information to files in Markdown format. // // See https://godoc.org/google.golang.org/api/compute/v1 and // https://github.com/googleapis/google-api-go-client/tree/master/compute/v1/compute-gen.go // for details on the Compute Engine API package main import ( "bytes" "fmt" "log" "net" "os" "path/filepath" "sort" "strings" "github.com/olekukonko/tablewriter" "golang.org/x/net/context" "golang.org/x/oauth2/google" "golang.org/x/sync/semaphore" "google.golang.org/api/compute/v1" ) const ( // outputDir is the name of the directory where output files will be written. outputDir = "output" // maxWorkers is the max number of goroutines allowed to run in parallel maxWorkers = 32 ) // projectResources stores the slices of addresses and instances for one project. // References: // https://godoc.org/google.golang.org/api/compute/v1#AddressAggregatedList // https://godoc.org/google.golang.org/api/compute/v1#InstanceAggregatedList // // Here we make the assumption that addresses and instances together will give // us all of the internal IPs used in a network. If this is not true, // projectResources should be expanded to include the missing resources, and // then make the appropriate API call in getResources to get the aggregated list type projectResources struct { Project string AddressAggregatedList *compute.AddressAggregatedList InstanceAggregatedList *compute.InstanceAggregatedList } // addressInfo holds the fields that we care about in our output table. type addressInfo struct { Project string IP string Status string Subnet string User string } // initClient initialize the Compute API client. func initClient() *compute.Service { ctx := context.Background() client, err := google.DefaultClient(ctx, compute.ComputeScope) if err != nil { log.Fatal(err) } computeService, err := compute.New(client) if err != nil { log.Fatal(err) } return computeService } // getServiceProjects returns a list of service projects for a given host project. func getServiceProjects(hostProject string, service *compute.Service) (*compute.ProjectsGetXpnResources, error) { log.Printf("Looking for service projects in %s", hostProject) res, err := service.Projects.GetXpnResources(hostProject).Do() if err != nil { log.Printf("Error getting service projects for %s: %v", hostProject, err) } return res, err } // getResources returns the addresses and instances for a project. func getResources(project string, service *compute.Service) *projectResources { log.Printf("Looking for addresses and instances in %s", project) addressAggregatedList, err := service.Addresses.AggregatedList(project).Do() if err != nil { log.Printf("Error getting addresses for %s: %v", project, err) } instanceAggregatedList, err := service.Instances.AggregatedList(project).Do() if err != nil { log.Printf("Error getting instances for %s: %v", project, err) } return &projectResources{ Project: project, AddressAggregatedList: addressAggregatedList, InstanceAggregatedList: instanceAggregatedList, } } // getAllResources returns addresses and instances for all service projects // attached to a host project. func getAllResources(hostProject string, service *compute.Service) []*projectResources { res, err := getServiceProjects(hostProject, service) if err != nil { log.Fatal(err) } ctx := context.TODO() sem := semaphore.NewWeighted(maxWorkers) output := make([]*projectResources, len(res.Resources)) // For each project, use a goroutine to get the resources for that project. for i := range res.Resources { if err := sem.Acquire(ctx, 1); err != nil { log.Printf("Failed to acquire semaphore: %v", err) break } go func(i int) { defer sem.Release(1) output[i] = getResources(res.Resources[i].Id, service) }(i) } if err := sem.Acquire(ctx, maxWorkers); err != nil { log.Printf("Failed to acquire semaphore: %v", err) } return output } // insertAddressInfo appends information from an addressInfo struct into a map // (addressInfoMap) keyed by IP address. // // If an IP already exists in the map, merge the information together. // Existing entries has precedence. This means that if, for some reason, the // addressInfo struct has different values than the existing entry, it will be // ignored. // // This should work fine assuming the address and instance resources // don't have contradicting information, which is pretty unlikely. A scenario // where this might happen is if the address resource represents its subnet one way, // and the instance using that same address represents its subnet a different way func insertAddressInfo(addressInfoMap map[string]*addressInfo, addressInfo *addressInfo) { i, ok := addressInfoMap[addressInfo.IP] if !ok { addressInfoMap[addressInfo.IP] = addressInfo return } if i.Status == "" { i.Status = addressInfo.Status } if i.Subnet == "" { i.Subnet = addressInfo.Subnet } if i.User == "" { i.User = addressInfo.User } } // getName parses self-links to get just the resource name at the end func getName(selfLink string) string { split := strings.Split(selfLink, "/") return split[len(split)-1] } // flatten processes a slice of projectResources. It pulls out the IPs and // information about those IPs that we are interested in, and returns a map of // addressInfo objects, where its keys are IP addresses func flatten(projectResourceList []*projectResources) map[string]*addressInfo { addressInfoMap := make(map[string]*addressInfo) for _, p := range projectResourceList { if p.AddressAggregatedList == nil { log.Printf("%s has no reserved addresses", p.Project) continue } for _, addressScopedList := range p.AddressAggregatedList.Items { if addressScopedList.Addresses == nil { continue } for _, address := range addressScopedList.Addresses { // make sure user is not nil, which happens when reserved IP // is RESERVED but not IN_USE var user string if address.Users != nil { user = getName(address.Users[0]) } insertAddressInfo(addressInfoMap, &addressInfo{ Project: p.Project, IP: address.Address, Status: address.Status, Subnet: getName(address.Subnetwork), User: user, }) } } if p.InstanceAggregatedList == nil { log.Printf("%s has no instances", p.Project) continue } for _, instanceScopedList := range p.InstanceAggregatedList.Items { if instanceScopedList.Instances == nil { continue } for _, instance := range instanceScopedList.Instances { insertAddressInfo(addressInfoMap, &addressInfo{ Project: p.Project, IP: instance.NetworkInterfaces[0].NetworkIP, Subnet: getName(instance.NetworkInterfaces[0].Subnetwork), User: instance.Name, }) } } } return addressInfoMap } // extractFields takes a list of projectResources and re-organize them by subnet. func extractFields(projectResourceList []*projectResources) map[string][]*addressInfo { addressInfoBySubnet := make(map[string][]*addressInfo) // Re-organize by subnet for _, addressInfo := range flatten(projectResourceList) { addressInfoBySubnet[addressInfo.Subnet] = append(addressInfoBySubnet[addressInfo.Subnet], addressInfo) } return addressInfoBySubnet } // writeToFile takes a subnet and its list of addressInfo objects, sorts list by // IP address, and then writes the result to a file in Markdown format. func writeToFile(subnet string, addressInfoList []*addressInfo) { if _, err := os.Stat(outputDir); os.IsNotExist(err) { os.Mkdir(outputDir, 0755) } f, err := os.Create(filepath.Join(outputDir, subnet+".md")) if err != nil { log.Fatal(err) } defer f.Close() _, err = fmt.Fprintf(f, "# Reserved IPs for %s\n", subnet) if err != nil { log.Fatal(err) } sort.Slice(addressInfoList, func(i, j int) bool { a := net.ParseIP(addressInfoList[i].IP) b := net.ParseIP(addressInfoList[j].IP) return bytes.Compare(a, b) < 0 }) var data [][]string for _, addressInfo := range addressInfoList { data = append(data, []string{ addressInfo.IP, addressInfo.Project, addressInfo.Status, addressInfo.User, }) } table := tablewriter.NewWriter(f) table.SetHeader([]string{"IP", "GCP Project", "Status", "User"}) table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) table.SetCenterSeparator("|") table.AppendBulk(data) table.Render() log.Printf("Writing to %s.md", subnet) } // writeAllToFile loops through addressBySubnet map and writes each subnet to a // different file func writeAllToFile(addressesBySubnet map[string][]*addressInfo) { for subnet, addressInfoList := range addressesBySubnet { if subnet != "" { writeToFile(subnet, addressInfoList) } } } func main() { // Host project (shared VPC project) is a required parameter if len(os.Args) < 2 { log.Fatalln("Missing required parameter: host-project") } hostProject := os.Args[1] computeService := initClient() resources := getAllResources(hostProject, computeService) addressInfoBySubnet := extractFields(resources) writeAllToFile(addressInfoBySubnet) }