agentendpoint/inventory.go (367 lines of code) (raw):

package agentendpoint import ( "bytes" "context" "crypto/sha256" "encoding/hex" "fmt" "io" "reflect" "sort" "strings" "time" "github.com/GoogleCloudPlatform/osconfig/agentconfig" "github.com/GoogleCloudPlatform/osconfig/attributes" "github.com/GoogleCloudPlatform/osconfig/clog" "github.com/GoogleCloudPlatform/osconfig/inventory" "github.com/GoogleCloudPlatform/osconfig/packages" "github.com/GoogleCloudPlatform/osconfig/retryutil" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" "cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb" datepb "google.golang.org/genproto/googleapis/type/date" ) const ( inventoryURL = agentconfig.ReportURL + "/guestInventory" ) // ReportInventory writes inventory to guest attributes and reports it to agent endpoint. func (c *Client) ReportInventory(ctx context.Context) { state := inventory.Get(ctx) if agentconfig.GuestAttributesEnabled() && !agentconfig.DisableInventoryWrite() { clog.Infof(ctx, "Writing inventory to guest attributes") write(ctx, state, inventoryURL) } c.report(ctx, state) } func write(ctx context.Context, state *inventory.InstanceInventory, url string) { clog.Debugf(ctx, "Writing instance inventory to guest attributes.") e := reflect.ValueOf(state).Elem() t := e.Type() for i := 0; i < e.NumField(); i++ { f := e.Field(i) u := fmt.Sprintf("%s/%s", url, t.Field(i).Name) switch f.Kind() { case reflect.String: clog.Debugf(ctx, "postAttribute %s: %+v", u, f) if err := attributes.PostAttribute(u, strings.NewReader(f.String())); err != nil { clog.Errorf(ctx, "postAttribute error: %v", err) } case reflect.Ptr: switch reflect.Indirect(f).Kind() { case reflect.Struct: clog.Debugf(ctx, "postAttributeCompressed %s", u) if err := attributes.PostAttributeCompressed(u, f.Interface()); err != nil { clog.Errorf(ctx, "postAttributeCompressed error: %v", err) } } } } } func (c *Client) report(ctx context.Context, state *inventory.InstanceInventory) { clog.Debugf(ctx, "Reporting instance inventory to agent endpoint.") inventory := formatInventory(ctx, state) reportFull := false var res *agentendpointpb.ReportInventoryResponse var err error f := func() error { res, err = c.reportInventory(ctx, inventory, reportFull) if err != nil { return err } return nil } if err = retryutil.RetryAPICall(ctx, apiRetrySec*time.Second, "ReportInventory", f); err != nil { clog.Errorf(ctx, "Error reporting inventory checksum: %v", err) return } if res.GetReportFullInventory() { reportFull = true if err = retryutil.RetryAPICall(ctx, apiRetrySec*time.Second, "ReportInventory", f); err != nil { clog.Errorf(ctx, "Error reporting full inventory: %v", err) return } } } func formatInventory(ctx context.Context, state *inventory.InstanceInventory) *agentendpointpb.Inventory { osInfo := &agentendpointpb.Inventory_OsInfo{ Hostname: state.Hostname, LongName: state.LongName, ShortName: state.ShortName, Version: state.Version, Architecture: state.Architecture, KernelVersion: state.KernelVersion, KernelRelease: state.KernelRelease, OsconfigAgentVersion: state.OSConfigAgentVersion, } installedPackages := formatPackages(ctx, state.InstalledPackages, state.ShortName) availablePackages := formatPackages(ctx, state.PackageUpdates, state.ShortName) return &agentendpointpb.Inventory{OsInfo: osInfo, InstalledPackages: installedPackages, AvailablePackages: availablePackages} } func formatPackages(ctx context.Context, pkgs *packages.Packages, shortName string) []*agentendpointpb.Inventory_SoftwarePackage { var softwarePackages []*agentendpointpb.Inventory_SoftwarePackage if pkgs == nil { return softwarePackages } if pkgs.Apt != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.Apt)) for i, pkg := range pkgs.Apt { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatAptPackage(pkg), } } softwarePackages = append(softwarePackages, temp...) } if pkgs.Deb != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.Deb)) for i, pkg := range pkgs.Deb { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatAptPackage(pkg), } } softwarePackages = append(softwarePackages, temp...) } if pkgs.GooGet != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.GooGet)) for i, pkg := range pkgs.GooGet { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatGooGetPackage(pkg), } } softwarePackages = append(softwarePackages, temp...) } if pkgs.Yum != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.Yum)) for i, pkg := range pkgs.Yum { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatYumPackage(pkg), } } softwarePackages = append(softwarePackages, temp...) } if pkgs.Zypper != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.Zypper)) for i, pkg := range pkgs.Zypper { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatZypperPackage(pkg), } } softwarePackages = append(softwarePackages, temp...) } if pkgs.Rpm != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.Rpm)) if packages.YumExists || !packages.ZypperExists { for i, pkg := range pkgs.Rpm { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatYumPackage(pkg), } } } else { for i, pkg := range pkgs.Rpm { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatZypperPackage(pkg), } } } softwarePackages = append(softwarePackages, temp...) } if pkgs.ZypperPatches != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.ZypperPatches)) for i, pkg := range pkgs.ZypperPatches { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatZypperPatch(pkg), } } softwarePackages = append(softwarePackages, temp...) } if pkgs.WUA != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.WUA)) for i, pkg := range pkgs.WUA { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatWUAPackage(pkg), } } softwarePackages = append(softwarePackages, temp...) } if pkgs.QFE != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.QFE)) for i, pkg := range pkgs.QFE { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatQFEPackage(ctx, pkg), } } softwarePackages = append(softwarePackages, temp...) } if pkgs.COS != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.COS)) for i, pkg := range pkgs.COS { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatCOSPackage(pkg), } } softwarePackages = append(softwarePackages, temp...) } if pkgs.WindowsApplication != nil { temp := make([]*agentendpointpb.Inventory_SoftwarePackage, len(pkgs.WindowsApplication)) for i, pkg := range pkgs.WindowsApplication { temp[i] = &agentendpointpb.Inventory_SoftwarePackage{ Details: formatWindowsApplication(pkg), } } softwarePackages = append(softwarePackages, temp...) } // Ignore Pip and Gem packages. return softwarePackages } func formatAptPackage(pkg *packages.PkgInfo) *agentendpointpb.Inventory_SoftwarePackage_AptPackage { fPkg := &agentendpointpb.Inventory_SoftwarePackage_AptPackage{ AptPackage: &agentendpointpb.Inventory_VersionedPackage{ PackageName: pkg.Name, Architecture: pkg.Arch, Version: pkg.Version, }, } // for some of the APT packages source package might be available. if pkg.Source.Name != "" { fPkg.AptPackage.Source = &agentendpointpb.Inventory_VersionedPackage_Source{ Name: pkg.Source.Name, Version: pkg.Source.Version, } } return fPkg } func formatCOSPackage(pkg *packages.PkgInfo) *agentendpointpb.Inventory_SoftwarePackage_CosPackage { return &agentendpointpb.Inventory_SoftwarePackage_CosPackage{ CosPackage: &agentendpointpb.Inventory_VersionedPackage{ PackageName: pkg.Name, Architecture: pkg.Arch, Version: pkg.Version, }} } func formatGooGetPackage(pkg *packages.PkgInfo) *agentendpointpb.Inventory_SoftwarePackage_GoogetPackage { return &agentendpointpb.Inventory_SoftwarePackage_GoogetPackage{ GoogetPackage: &agentendpointpb.Inventory_VersionedPackage{ PackageName: pkg.Name, Architecture: pkg.Arch, Version: pkg.Version, }} } func formatYumPackage(pkg *packages.PkgInfo) *agentendpointpb.Inventory_SoftwarePackage_YumPackage { fPkg := &agentendpointpb.Inventory_SoftwarePackage_YumPackage{ YumPackage: &agentendpointpb.Inventory_VersionedPackage{ PackageName: pkg.Name, Architecture: pkg.Arch, Version: pkg.Version, }, } // for some of the YUM packages source package might be available. if pkg.Source.Name != "" { fPkg.YumPackage.Source = &agentendpointpb.Inventory_VersionedPackage_Source{ Name: pkg.Source.Name, Version: pkg.Source.Version, } } return fPkg } func formatZypperPackage(pkg *packages.PkgInfo) *agentendpointpb.Inventory_SoftwarePackage_ZypperPackage { return &agentendpointpb.Inventory_SoftwarePackage_ZypperPackage{ ZypperPackage: &agentendpointpb.Inventory_VersionedPackage{ PackageName: pkg.Name, Architecture: pkg.Arch, Version: pkg.Version}} } func formatZypperPatch(pkg *packages.ZypperPatch) *agentendpointpb.Inventory_SoftwarePackage_ZypperPatch { return &agentendpointpb.Inventory_SoftwarePackage_ZypperPatch{ ZypperPatch: &agentendpointpb.Inventory_ZypperPatch{ PatchName: pkg.Name, Category: pkg.Category, Severity: pkg.Severity, Summary: pkg.Summary, }} } func formatWUAPackage(pkg *packages.WUAPackage) *agentendpointpb.Inventory_SoftwarePackage_WuaPackage { var categories []*agentendpointpb.Inventory_WindowsUpdatePackage_WindowsUpdateCategory for idx, category := range pkg.Categories { categories = append(categories, &agentendpointpb.Inventory_WindowsUpdatePackage_WindowsUpdateCategory{ Id: pkg.CategoryIDs[idx], Name: category, }) } return &agentendpointpb.Inventory_SoftwarePackage_WuaPackage{ WuaPackage: &agentendpointpb.Inventory_WindowsUpdatePackage{ Title: pkg.Title, Description: pkg.Description, Categories: categories, KbArticleIds: pkg.KBArticleIDs, SupportUrl: pkg.SupportURL, MoreInfoUrls: pkg.MoreInfoURLs, UpdateId: pkg.UpdateID, RevisionNumber: pkg.RevisionNumber, LastDeploymentChangeTime: timestamppb.New(pkg.LastDeploymentChangeTime), }} } func formatQFEPackage(ctx context.Context, pkg *packages.QFEPackage) *agentendpointpb.Inventory_SoftwarePackage_QfePackage { installedTime, err := time.Parse("1/2/2006", pkg.InstalledOn) if err != nil { clog.Warningf(ctx, "Error parsing QFE InstalledOn date: %v", err) } return &agentendpointpb.Inventory_SoftwarePackage_QfePackage{ QfePackage: &agentendpointpb.Inventory_WindowsQuickFixEngineeringPackage{ Caption: pkg.Caption, Description: pkg.Description, HotFixId: pkg.HotFixID, InstallTime: timestamppb.New(installedTime), }} } func formatWindowsApplication(pkg *packages.WindowsApplication) *agentendpointpb.Inventory_SoftwarePackage_WindowsApplication { d := datepb.Date{} // We have to check if date is zero. // Because zero value of time has Year, Month, Day equal to 1 if !pkg.InstallDate.IsZero() { d = datepb.Date{ Year: int32(pkg.InstallDate.Year()), Month: int32(pkg.InstallDate.Month()), Day: int32(pkg.InstallDate.Day()), } } return &agentendpointpb.Inventory_SoftwarePackage_WindowsApplication{ WindowsApplication: &agentendpointpb.Inventory_WindowsApplication{ DisplayName: pkg.DisplayName, DisplayVersion: pkg.DisplayVersion, Publisher: pkg.Publisher, InstallDate: &d, HelpLink: pkg.HelpLink, }} } func computeFingerprint(ctx context.Context, inventory *agentendpointpb.Inventory) (string, error) { fingerprint := sha256.New() b, err := proto.Marshal(inventory) if err != nil { return "", err } io.Copy(fingerprint, bytes.NewReader(b)) return hex.EncodeToString(fingerprint.Sum(nil)), nil } func computeStableFingerprint(ctx context.Context, inventory *agentendpointpb.Inventory) (string, error) { fingerprint := sha256.New() b, err := proto.Marshal(inventory.GetOsInfo()) if err != nil { return "", err } io.Copy(fingerprint, bytes.NewReader(b)) installedPackages := inventory.GetInstalledPackages() availablePackages := inventory.GetAvailablePackages() entries := make([]string, 0, len(installedPackages)+len(availablePackages)) for _, pkg := range installedPackages { entries = append(entries, fingerprintForPackage(pkg)) } for _, pkg := range availablePackages { entries = append(entries, fingerprintForPackage(pkg)) } sort.Strings(entries) for _, entry := range entries { if _, err := io.WriteString(fingerprint, entry); err != nil { return "", err } } return hex.EncodeToString(fingerprint.Sum(nil)), nil } func fingerprintForPackage(pkg *agentendpointpb.Inventory_SoftwarePackage) string { //Inventory_WindowsUpdatePackage struct contains repeated fields //we should rely on fields that are stable and sufficient enough to uniquely identify the package. if wua := pkg.GetWuaPackage(); wua != nil { return fmt.Sprintf("%s-%s-%d", wua.GetTitle(), wua.GetUpdateId(), wua.GetRevisionNumber()) } // For all packages other then wua we can just rely on proto String() method. return pkg.String() }