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()
}