package main import ( "context" "encoding/json" "flag" "fmt" "log" "net/url" "os" "runtime" "strings" "time" _ "time/tzdata" "github.com/beevik/ntp" "github.com/vmware/govmomi" "github.com/vmware/govmomi/find" "github.com/vmware/govmomi/view" "github.com/vmware/govmomi/vim25/methods" "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/types" ) type HostTimeErrors struct { HostName string HostTime time.Time HostTimeDrift string Cluster string Vcenter string } var ( c *govmomi.Client ctx context.Context cancel context.CancelFunc location *time.Location sha1ver string // sha1 revision used to build the program buildTime string // when the executable was built hostTimeErrors []HostTimeErrors ) // maxAllowedDrift is the maximum allowed time difference (1 second). const maxAllowedDrift = 2 * time.Second /* type dateInfo struct { types.HostDateTimeInfo Service *types.HostService `json:"service"` Current *time.Time `json:"current"` } */ func checkClockDrift(ntpServers []string) bool { localTime := time.Now() inSync := false for _, server := range ntpServers { ntpTime, err := ntp.Time(server) if err != nil { log.Printf("Failed to get time from %s: %v\n", server, err) continue } diff := localTime.Sub(ntpTime) if diff < 0 { diff = -diff } log.Printf("NTP Server: %s\n", server) log.Printf("Local Time: %s\n", localTime) log.Printf("NTP Time : %s\n", ntpTime) log.Printf("Time Difference: %v\n", diff) if diff <= maxAllowedDrift { inSync = true } log.Println("----------") } return inSync } func prettyPrint(args ...interface{}) { var caller string timeNow := time.Now().Format("01-02-2006 15:04:05") prefix := fmt.Sprintf("[%s] %s -- ", "PrettyPrint", timeNow) _, fileName, fileLine, ok := runtime.Caller(1) if ok { caller = fmt.Sprintf("%s:%d", fileName, fileLine) } else { caller = "" } fmt.Printf("\n%s%s\n", prefix, caller) if len(args) == 2 { label := args[0] value := args[1] s, _ := json.MarshalIndent(value, "", "\t") fmt.Printf("%s%s: %s\n", prefix, label, string(s)) } else { s, _ := json.MarshalIndent(args, "", "\t") fmt.Printf("%s%s\n", prefix, string(s)) } } func main() { // Command line flags for the vCenter connection 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") vTZ := flag.String("tz", "Australia/Sydney", "The timezone to use when converting vCenter UTC times") vInsecure := flag.Bool("insecure", true, "Allow insecure connections to vCenter") vAllowedDiff := flag.Int("diff", 300, "Permitted time difference in seconds") vNtpServers := flag.String("ntp-servers", "pool.ntp.org", "A comma separated list of NTP sources to validate the local system clock") 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) log.Printf("Starting execution. Built on %s from sha1 %s\n", buildTime, sha1ver) // So we can convert vCenter UTC to our local timezone log.Printf("Setting timezone to '%s'\n", *vTZ) location, err = time.LoadLocation(*vTZ) if err != nil { log.Printf("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) } } // Validate system clock ntpList := strings.FieldsFunc(*vNtpServers, func(r rune) bool { return r == ',' }) if !checkClockDrift(ntpList) { log.Fatalf("Local system clock is not in sync, unable to proceed") os.Exit(1) } log.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 { log.Fatalf("Logging in error: %s\n", err) } defer c.Logout(ctx) // Create a view manager m := view.NewManager(c.Client) // Create a container view for all Datacenters dcView, err := m.CreateContainerView(ctx, c.Client.ServiceContent.RootFolder, []string{"Datacenter"}, true) if err != nil { log.Fatalf("Failed to create container view: %v", err) } defer dcView.Destroy(ctx) // Retrieve all Datacenters var dcs []mo.Datacenter err = dcView.Retrieve(ctx, []string{"Datacenter"}, []string{"name", "hostFolder"}, &dcs) if err != nil { log.Fatalf("Failed to retrieve datacenters: %v", err) } // Iterate through each datacenter and find hosts for _, dc := range dcs { //fmt.Printf("Datacenter: %s\n", dc.Name) // Create a finder for the datacenter finder := find.NewFinder(c.Client, false) datacenter, err := finder.Datacenter(ctx, dc.Name) if err != nil { log.Printf("Failed to get datacenter object: %v", err) continue } finder.SetDatacenter(datacenter) // Find all ESXi hosts in this datacenter hosts, err := finder.HostSystemList(ctx, "*") if err != nil { log.Printf("Failed to get hosts in datacenter %s: %v", dc.Name, err) continue } // Retrieve host properties /* var hostProperties []mo.HostSystem err = dcView.Retrieve(ctx, []string{"HostSystem"}, []string{"name", "hardware.systemInfo", "configManager"}, &hostProperties) if err != nil { log.Printf("Failed to retrieve host properties in datacenter %s: %v", dc.Name, err) continue } */ log.Printf("DC %s contains %d hosts\n", dc.Name, len(hosts)) // Print hosts for _, host := range hosts { clusterName := "" var hs mo.HostSystem err = host.Properties(ctx, host.Reference(), []string{"name", "parent"}, &hs) if err != nil { log.Printf("Failed to retrieve host %s parent properties: %v", hs.Name, err) continue } dts, err := host.ConfigManager().DateTimeSystem(ctx) if err != nil { log.Printf("Failed to get datetime system for host %s: %v", hs.Name, err) continue } // test triggering update // Call RefreshDateTimeSystem method using methods.RefreshDateTimeSystem refreshReq := &types.RefreshDateTimeSystem{ This: dts.Reference(), } _, err = methods.RefreshDateTimeSystem(ctx, c.Client, refreshReq) if err != nil { log.Printf("Failed to refresh datetime system for host %s: %v", hs.Name, err) } else { //prettyPrint(refreshResp) } /* // Call UpdateDateTime method using methods.UpdateDateTime - this will manually set the ESXi clock updateReq := &types.UpdateDateTime{ This: dts.Reference(), DateTime: time.Now().UTC(), } _, err = methods.UpdateDateTime(ctx, c.Client, updateReq) if err != nil { log.Fatal(err) } */ // Get the current ESXi time which is UTC hostTime, err := dts.Query(ctx) // Convert ESXi UTC time to local time esxiTimeLocal := hostTime.Local() localTime := time.Now() diff := localTime.Sub(esxiTimeLocal) maxDiff := time.Duration(*vAllowedDiff) * time.Second if diff > maxDiff || diff < -maxDiff { log.Printf("ESXi %s time differs from local time by more than %d seconds (ESXi: %v, Local: %v)", hs.Name, *vAllowedDiff, esxiTimeLocal, localTime) // Get the cluster name if hs.Parent.Type == "ClusterComputeResource" { // Retrieve properties of the compute resource var moCompute mo.ComputeResource err = c.RetrieveOne(ctx, *hs.Parent, nil, &moCompute) if err == nil { clusterName = moCompute.Name } } thisResult := HostTimeErrors{ HostName: hs.Name, Cluster: clusterName, Vcenter: u.Host, HostTime: esxiTimeLocal, HostTimeDrift: diff.String(), } hostTimeErrors = append(hostTimeErrors, thisResult) } else { log.Printf("[%s] Host: %s; Time: %v; Drift: %s\n", dc.Name, hs.Name, hostTime, diff) } /* var hostDts mo.HostDateTimeSystem if err = dts.Properties(ctx, dts.Reference(), nil, &hostDts); err != nil { fmt.Printf("error: %s\n", err) os.Exit(1) } ss, err := host.ConfigManager().ServiceSystem(ctx) if err != nil { fmt.Printf("error: %s\n", err) os.Exit(1) } services, err := ss.Service(ctx) if err != nil { fmt.Printf("error: %s\n", err) os.Exit(1) } res := &dateInfo{HostDateTimeInfo: hostDts.DateTimeInfo} for i, service := range services { if service.Key == "ntpd" { res.Service = &services[i] break } } res.Current, err = dts.Query(ctx) if err != nil { fmt.Printf("error: %s\n", err) os.Exit(1) } prettyPrint(res) */ } } /* // Create a new result result := OutageResults{ VM: event.Vm.Name, OutageDuration: out.Format("15:04:05"), OutageStart: outageStart, RestartTime: restartTime, Cluster: event.ComputeResource.Name, FailedHost: failedHost, NewHost: event.Host.Name, GuestOS: vmOS, CurrentPowerState: vmPowerState, Description: event.FullFormattedMessage, } // Append to list of all results results = append(results, result) } for _, hostEvent := range hostFailures { hostResults = append(hostResults, HostFailureResults{ HostName: hostEvent.Host.Name, FailureTime: hostEvent.CreatedTime.In(location), Cluster: hostEvent.ComputeResource.Name, Vcenter: u.Host, }) } } else { log.Printf("Found %d hostfailure messages in last %.1f hour(s)", len(hostFailures), begin.Abs().Hours()) } */ // Combine details of host outages and VM outages into one interface /* var combined []interface{} for _, h := range hostResults { combined = append(combined, h) } for _, v := range results { combined = append(combined, v) } */ log.Printf("Finished checking vCenter %s\n", u.Host) // Output final results in JSON if len(hostTimeErrors) > 0 { j, _ := json.Marshal(hostTimeErrors) fmt.Println(string(j)) } else { fmt.Println("{}") } }