package main import ( "bytes" "context" "flag" "fmt" "html/template" "log" "net/url" "os" "strings" "time" _ "time/tzdata" "github.com/vmware/govmomi" "github.com/vmware/govmomi/property" "github.com/vmware/govmomi/view" "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/types" "github.com/PuerkitoBio/goquery" goconfluence "github.com/virtomize/confluence-go-api" ) var ( c *govmomi.Client ctx context.Context cancel context.CancelFunc sha1ver string // sha1 revision used to build the program buildTime string // when the executable was built busSharingResults []BusSharingResults multiWriterResults []MultiWriterResults location *time.Location ) type BusSharingResults struct { VmName string ClusterName string ControllerName string SharingType string } type MultiWriterResults struct { VmName string ClusterName string DiskLabel string SharingType string } // Thanks chatgpt // Refer also to https://github.com/vmware/govmomi/blob/v0.32.0/vim25/types/enum.go#L9704“ func sharedBusToString(sharedBus types.VirtualSCSISharing) string { switch sharedBus { case types.VirtualSCSISharingNoSharing: return "No Sharing" case types.VirtualSCSISharingPhysicalSharing: return "Physical" case types.VirtualSCSISharingVirtualSharing: return "Virtual" default: return "Unknown" } } func processVMs(client *govmomi.Client) error { var clusterName string ctx := context.Background() m := view.NewManager(client.Client) //f := find.NewFinder(client.Client, true) pc := property.DefaultCollector(client.Client) fmt.Printf("processVMs : Preparing views\n") // Get a view of all the VMs vms, err := m.CreateContainerView(ctx, client.ServiceContent.RootFolder, []string{"VirtualMachine"}, true) if err != nil { return err } defer vms.Destroy(ctx) // Get a view of all the hosts hs, err := m.CreateContainerView(ctx, client.ServiceContent.RootFolder, []string{"HostSystem"}, true) if err != nil { return err } defer hs.Destroy(ctx) // Retrieve all the VMs fmt.Printf("Getting VM listing\n") var vmList []mo.VirtualMachine err = vms.Retrieve(ctx, []string{"VirtualMachine"}, []string{"summary", "config", "name"}, &vmList) if err != nil { log.Printf("Error retrieving vm list : '%s'\n", err) return err } fmt.Printf("Found %d VMs\n", len(vmList)) // Retrieve all the hosts fmt.Printf("Getting host listing\n") var hsList []mo.HostSystem err = hs.Retrieve(ctx, []string{"HostSystem"}, []string{"name", "parent"}, &hsList) if err != nil { log.Printf("Error retrieving hostsystem list : '%s'\n", err) return err } fmt.Printf("Found %d hosts\n", len(hsList)) // Iterate through VMs and check for SCSI bus sharing for _, vm := range vmList { //fmt.Printf("vm : %s [%s]\n", vm.Name, vm.Summary.Runtime.Host) //fmt.Printf("vm parent: %v\n", vm.Parent) /* // TODO : check for err // Get the object for this VM ref, _ := f.ObjectReference(ctx, vm.Entity().Self) ovm, _ := ref.(*object.VirtualMachine) // Get the resource pool and the owner of it pool, _ := ovm.ResourcePool(ctx) owner, _ := pool.Owner(ctx) fmt.Printf("owner: %v\n", owner) */ // Determine cluster based on runtime host of VM based on https://github.com/vmware/govmomi/issues/1242#issuecomment-427671990 for _, host := range hsList { if host.Reference() == *vm.Summary.Runtime.Host { //fmt.Printf("host %s matches, host parent %s\n", host.Name, host.Parent) var cluster mo.ManagedEntity err = pc.RetrieveOne(ctx, *host.Parent, []string{"name"}, &cluster) if err != nil { log.Printf("Error retrieving cluster object : '%s'\n", err) clusterName = "" break } //fmt.Println(cluster.Name) clusterName = cluster.Name break } } if vm.Config != nil && len(vm.Config.Hardware.Device) > 0 { for _, device := range vm.Config.Hardware.Device { //fmt.Printf("device: %v\n", device) //fmt.Println("Type of variable1:", reflect.TypeOf(device)) if scsi, ok := device.(types.BaseVirtualSCSIController); ok { //fmt.Printf("scsi: %v\n", scsi) controller := scsi.GetVirtualSCSIController() //fmt.Printf("controller: %s\n", device.GetVirtualDevice().DeviceInfo.GetDescription().Label) //fmt.Printf("VM %s is using SCSI bus sharing mode: %s\n", vm.Name, string(controller.SharedBus)) if controller.SharedBus != "noSharing" { fmt.Printf("VM %s is using SCSI bus sharing mode: %s\n", vm.Name, string(controller.SharedBus)) result := BusSharingResults{ VmName: vm.Name, ClusterName: clusterName, ControllerName: device.GetVirtualDevice().DeviceInfo.GetDescription().Label, SharingType: sharedBusToString(controller.SharedBus), } busSharingResults = append(busSharingResults, result) } } else if vdisk, ok := device.(*types.VirtualDisk); ok { //fmt.Printf("vdisk: %v\n", vdisk) // See https://github.com/vmware/govmomi/blob/main/object/virtual_device_list_test.go for info diskLabel := vdisk.VirtualDevice.DeviceInfo.GetDescription().Label // See https://github.com/vmware/govmomi/blob/main/vim25/types/enum.go#L7538 // Sharing can be sharingNone or sharingMultiWriter backing := vdisk.VirtualDevice.Backing //fmt.Println("Type of backing:", reflect.TypeOf(backing)) // make sure we have a regular disk, not an RDM which has type VirtualDiskRawDiskMappingVer1BackingInfo if info, ok := backing.(*types.VirtualDiskFlatVer2BackingInfo); ok { sharingType := info.Sharing if sharingType == "sharingMultiWriter" { fmt.Printf("VM %s is using MultiWriter on disk %s\n", vm.Name, diskLabel) result := MultiWriterResults{ VmName: vm.Name, ClusterName: clusterName, DiskLabel: diskLabel, SharingType: sharingType, } multiWriterResults = append(multiWriterResults, result) } } } } } else if vm.Config == nil { fmt.Printf("vm %s has no config\n", vm.Name) } else { fmt.Printf("vm %s is something strange\n", vm.Name) } } return nil } func generateBusSharingTable() string { // Define the HTML template htmlTemplate := `{{range .}}{{end}}
VM NameCluster NameController NameSharing Type
{{.VmName}}{{.ClusterName}}{{.ControllerName}}{{.SharingType}}
` // Create a new template and parse the HTML template tmpl := template.Must(template.New("table").Parse(htmlTemplate)) // Create a buffer to store the HTML output var buf bytes.Buffer // Execute the template with the results and write to the buffer err := tmpl.Execute(&buf, busSharingResults) if err != nil { panic(err) } // Convert the buffer to a string htmlString := buf.String() fmt.Printf("generateBusSharingTable : %s\n", htmlString) return htmlString } func generateMultiWriterTable() string { // Define the HTML template htmlTemplate := `{{range .}}{{end}}
VM NameCluster NameDisk LabelSharing Type
{{.VmName}}{{.ClusterName}}{{.DiskLabel}}{{.SharingType}}
` // Create a new template and parse the HTML template tmpl := template.Must(template.New("table").Parse(htmlTemplate)) // Create a buffer to store the HTML output var buf bytes.Buffer // Execute the template with the results and write to the buffer err := tmpl.Execute(&buf, multiWriterResults) if err != nil { panic(err) } // Convert the buffer to a string htmlString := buf.String() fmt.Printf("generateMultiWriterTable : %s\n", htmlString) return htmlString } func updateHtml(htmlContent string, heading string, newTable string) string { // Load the HTML content into a goquery document doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent)) if err != nil { fmt.Println(err) return htmlContent } // Find the h2 heading that matches our heading passed to the function doc.Find("h2").Each(func(i int, s *goquery.Selection) { if s.Text() == heading { fmt.Printf("Found h2 element matching '%s'\n", heading) // Find the table that follows this h2 heading table := s.NextFiltered("table") // Replace the table with a new table newTable, _ := goquery.NewDocumentFromReader(strings.NewReader(newTable)) table.ReplaceWithSelection(newTable.Selection) } }) // Render the html h, err := doc.Html() if err != nil { fmt.Println(err) return htmlContent } // goquery produces implicit nodes when parsing the input html, remove them now //fmt.Printf("updateHtml whole doc : %s\n", h) h = strings.Replace(h, "", "", -1) h = strings.Replace(h, "", "", -1) return h } func updateConfluenceBusSharing(api *goconfluence.API, vCenterHostname string, pageId string, spaceKey string) { // get content by content id c, err := api.GetContentByID(pageId, goconfluence.ContentQuery{ SpaceKey: spaceKey, Expand: []string{"body.storage", "version"}, }) if err != nil { fmt.Println(err) return } //fmt.Printf("%+v\n", c) //fmt.Printf("Current version number : '%d'\n", c.Version.Number) newVersion := c.Version.Number + 1 // Generate new content for confluence //fmt.Printf("Current content: %s\n", c.Body.Storage.Value) newTable := generateBusSharingTable() newContent := updateHtml(c.Body.Storage.Value, vCenterHostname, newTable) //fmt.Printf("New Content: %v\n", newContent) data := &goconfluence.Content{ ID: pageId, Type: "page", Title: c.Title, Body: goconfluence.Body{ Storage: goconfluence.Storage{ Value: newContent, Representation: "storage", }, }, Version: &goconfluence.Version{ Number: newVersion, }, Space: &goconfluence.Space{ Key: spaceKey, }, } //fmt.Printf("confluence object : '%v'\n", data) result, err := api.UpdateContent(data) if err != nil { fmt.Printf("Error updating content: %s\n", err) return } fmt.Printf("result: %v\n", result) } func updateConfluenceMultiWriter(api *goconfluence.API, vCenterHostname string, pageId string, spaceKey string) { // get content by content id c, err := api.GetContentByID(pageId, goconfluence.ContentQuery{ SpaceKey: spaceKey, Expand: []string{"body.storage", "version"}, }) if err != nil { fmt.Println(err) return } //fmt.Printf("%+v\n", c) //fmt.Printf("Current version number : '%d'\n", c.Version.Number) newVersion := c.Version.Number + 1 // Generate new content for confluence //fmt.Printf("Current content: %s\n", c.Body.Storage.Value) newTable := generateMultiWriterTable() newContent := updateHtml(c.Body.Storage.Value, vCenterHostname, newTable) //fmt.Printf("New Content: %v\n", newContent) data := &goconfluence.Content{ ID: pageId, Type: "page", Title: c.Title, Body: goconfluence.Body{ Storage: goconfluence.Storage{ Value: newContent, Representation: "storage", }, }, Version: &goconfluence.Version{ Number: newVersion, }, Space: &goconfluence.Space{ Key: spaceKey, }, } //fmt.Printf("confluence object : '%v'\n", data) result, err := api.UpdateContent(data) if err != nil { fmt.Printf("Error updating content: %s\n", err) return } fmt.Printf("result: %v\n", result) } /* func updateConfluencePage(api *goconfluence.API, vCenterHostname string, pageId string, spaceKey string, newContent string, newVersion int) { } */ func main() { var err error // Command line flags vURL := flag.String("url", "", "The URL of a vCenter server, eg https://server.domain.example/sdk") vUser := flag.String("user", "", "The username to use when connecting to vCenter") vPass := flag.String("password", "", "The password to use when connecting to vCenter") vInsecure := flag.Bool("insecure", true, "Allow insecure connections to vCenter") vTZ := flag.String("tz", "Australia/Sydney", "The timezone to use when converting vCenter UTC times") cURL := flag.String("confluence-url", "https://confluence.yourdomain.com/wiki/rest/api", "The URL to your confluence rest API endpoint") cToken := flag.String("confluence-token", "", "Your Confluence Personal Access Token") cBusSharingId := flag.String("confluence-bussharing-pageid", "", "The page ID for the VMs with Bus Sharing report") cMultiWriterId := flag.String("confluence-multiwriter-pageid", "", "The page ID for the VMs with MultiWriter report") cSpaceKey := flag.String("confluence-spacekey", "HCS", "The confluence space key to use") flag.Parse() // Print logs to file /* f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatalf("error opening file: %v", err) } defer f.Close() log.SetOutput(f) */ fmt.Printf("Starting execution. Built on %s from sha1 %s\n", buildTime, sha1ver) if len(*vURL) == 0 { panic("Unable to connect to empty vCenter URL") } // Look for credentials from environment variables val, present := os.LookupEnv("VCENTER_USER") if present { fmt.Println("Loaded vcenter user from environment variable") *vUser = val } val, present = os.LookupEnv("VCENTER_PASS") if present { fmt.Println("Loaded vcenter password from environment variable") *vPass = val } val, present = os.LookupEnv("CONFLUENCE_TOKEN") if present { fmt.Println("Loaded confluence Personal Access Token from environment variable") *cToken = val } // So we can convert vCenter UTC to our local timezone fmt.Printf("Setting timezone to '%s'\n", *vTZ) location, err = time.LoadLocation(*vTZ) if err != nil { fmt.Fprintf(os.Stderr, "Error setting timezone to %s : %s\n", *vTZ, err) os.Exit(1) } u, err := url.Parse(*vURL) if err != nil { fmt.Fprintf(os.Stderr, "Error parsing url %s : %s\n", *vURL, err) os.Exit(1) } else { if !strings.HasSuffix(u.Path, "/sdk") { u.Path, _ = url.JoinPath(u.Path, "/sdk") log.Printf("Updated vCenter URL to '%v'\n", u) } } vCenterHostname := u.Host fmt.Printf("Connecting to vCenter %s\n", u) u.User = url.UserPassword(*vUser, *vPass) ctx, cancel = context.WithCancel(context.Background()) defer cancel() // Login to vcenter c, err = govmomi.NewClient(ctx, u, *vInsecure) if err != nil { fmt.Fprintf(os.Stderr, "Logging in error: %s\n", err) os.Exit(1) } defer c.Logout(ctx) // Find scsi bus sharing and multi writer VMs err = processVMs(c) if err != nil { fmt.Printf("Error processing list of VMs : %s\n", err) os.Exit(1) } // Connect to confluence if len(*cURL) > 0 && len(*cToken) > 0 { fmt.Printf("Connecting to confluence %s\n", *cURL) api, err := goconfluence.NewAPI(*cURL, "", *cToken) if err != nil { fmt.Println(err) return } updateConfluenceBusSharing(api, vCenterHostname, *cBusSharingId, *cSpaceKey) updateConfluenceMultiWriter(api, vCenterHostname, *cMultiWriterId, *cSpaceKey) } else { fmt.Println("Not updating confluence, no details provided") } }