commit df047ffd71a6d69823eaaaf34dec18f05097abe8 Author: Nathan Coad Date: Mon Mar 10 14:10:39 2025 +1100 initial diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..99effde --- /dev/null +++ b/go.mod @@ -0,0 +1,4 @@ +module nathan/go-ntp +go 1.24.1 + +require github.com/vmware/govmomi v0.43.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e429be2 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/vmware/govmomi v0.30.4 h1:BCKLoTmiBYRuplv3GxKEMBLtBaJm8PA56vo9bddIpYQ= +github.com/vmware/govmomi v0.30.4/go.mod h1:F7adsVewLNHsW/IIm7ziFURaXDaHEwcc+ym4r3INMdY= +github.com/vmware/govmomi v0.43.0 h1:7Kg3Bkdly+TrE67BYXzRq7ZrDnn7xqpKX95uEh2f9Go= +github.com/vmware/govmomi v0.43.0/go.mod h1:IOv5nTXCPqH9qVJAlRuAGffogaLsNs8aF+e7vLgsHJU= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1ef0aa7 --- /dev/null +++ b/main.go @@ -0,0 +1,311 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net/url" + "os" + "runtime" + "strings" + "time" + _ "time/tzdata" + + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/view" + "github.com/vmware/govmomi/vim25/mo" +) + +type HostTimeErrors struct { + HostName string + HostTime time.Time + 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 +) + +/* +type dateInfo struct { + types.HostDateTimeInfo + Service *types.HostService `json:"service"` + Current *time.Time `json:"current"` +} +*/ + +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") + + 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 { + 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) + } + } + + 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 { + fmt.Fprintf(os.Stderr, "Logging in error: %s\n", err) + os.Exit(1) + } + 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 { + fmt.Printf("Failed to retrieve host properties: %v", err) + continue + } + + // 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 properties: %v", err) + continue + } + + dts, err := host.ConfigManager().DateTimeSystem(ctx) + if err != nil { + fmt.Printf("error: %s\n", err) + os.Exit(1) + } + + hostTime, err := dts.Query(ctx) + //fmt.Printf(" - Host: %s; Time: %v\n", hs.Name, hostTime) + + // 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 { + fmt.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, + } + + hostTimeErrors = append(hostTimeErrors, thisResult) + } + + /* + 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) + } + */ + + // Output final results in JSON + if len(hostTimeErrors) > 0 { + j, _ := json.Marshal(hostTimeErrors) + fmt.Println(string(j)) + } else { + fmt.Println("{}") + } +}