From da98e296e604dfcd68b3d619e8a40e3555cd65df Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Sun, 24 Sep 2023 22:20:40 +0200 Subject: [PATCH] =?UTF-8?q?=E2=AD=90=EF=B8=8F=20discovery=20for=20vsphere?= =?UTF-8?q?=20(#1881)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _motor/discovery/common/discovery.go | 9 - _motor/discovery/vsphere/list.go | 289 ------------------ _motor/discovery/vsphere/vsphere_resolver.go | 189 ------------ .../vsphere/vsphere_resolver_test.go | 47 --- providers/assets.go | 2 + providers/gcp/provider/provider.go | 2 - providers/vsphere/config/config.go | 17 +- providers/vsphere/connection/platform.go | 42 ++- providers/vsphere/connection/platform_test.go | 2 +- providers/vsphere/provider/provider.go | 47 ++- providers/vsphere/provider/provider_test.go | 45 +++ providers/vsphere/resources/datacenter.go | 39 +-- providers/vsphere/resources/discovery.go | 212 +++++++++++++ providers/vsphere/resources/host.go | 8 +- providers/vsphere/resources/vm.go | 63 ++++ providers/vsphere/resources/vsphere.go | 24 +- providers/vsphere/resources/vsphere.lr.go | 4 +- 17 files changed, 417 insertions(+), 624 deletions(-) delete mode 100644 _motor/discovery/common/discovery.go delete mode 100644 _motor/discovery/vsphere/list.go delete mode 100644 _motor/discovery/vsphere/vsphere_resolver.go delete mode 100644 _motor/discovery/vsphere/vsphere_resolver_test.go create mode 100644 providers/vsphere/resources/discovery.go create mode 100644 providers/vsphere/resources/vm.go diff --git a/_motor/discovery/common/discovery.go b/_motor/discovery/common/discovery.go deleted file mode 100644 index f688d01099..0000000000 --- a/_motor/discovery/common/discovery.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package common - -const ( - DiscoveryAuto = "auto" - DiscoveryAll = "all" -) diff --git a/_motor/discovery/vsphere/list.go b/_motor/discovery/vsphere/list.go deleted file mode 100644 index 646ee901e1..0000000000 --- a/_motor/discovery/vsphere/list.go +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package vsphere - -import ( - "context" - "errors" - "fmt" - - "github.com/rs/zerolog/log" - "github.com/vmware/govmomi" - "github.com/vmware/govmomi/find" - "github.com/vmware/govmomi/object" - "github.com/vmware/govmomi/vim25/mo" - "github.com/vmware/govmomi/vim25/types" - "go.mondoo.com/cnquery/motor/asset" - "go.mondoo.com/cnquery/motor/providers" - provider "go.mondoo.com/cnquery/motor/providers/vsphere" - "go.mondoo.com/cnquery/resources/packs/vsphere/resourceclient" -) - -func New(client *govmomi.Client) *VSphere { - return &VSphere{ - Client: client, - } -} - -type VSphere struct { - Client *govmomi.Client -} - -func (v *VSphere) InstanceUuid() (string, error) { - return provider.InstanceUUID(v.Client) -} - -func (v *VSphere) ListEsxiHosts() ([]*asset.Asset, error) { - instanceUuid, err := v.InstanceUuid() - if err != nil { - return nil, err - } - - dcs, err := v.listDatacenters() - if err != nil { - return nil, err - } - - res := []*asset.Asset{} - for i := range dcs { - dc := dcs[i] - hostList, err := v.listHosts(dc) - if err != nil { - return nil, err - } - hostsAsAssets, err := hostsToAssetList(instanceUuid, hostList) - if err != nil { - return nil, err - } - res = append(res, hostsAsAssets...) - } - return res, nil -} - -func hostsToAssetList(instanceUuid string, hosts []*object.HostSystem) ([]*asset.Asset, error) { - res := []*asset.Asset{} - for i := range hosts { - host := hosts[i] - props, err := hostProperties(host) - if err != nil { - return nil, err - } - - // NOTE: if a host is not running properly (returning not responding), the properties are nil - ha := &asset.Asset{ - Name: host.Name(), - State: asset.State_STATE_UNKNOWN, - Labels: map[string]string{ - "vsphere.vmware.com/name": host.Name(), - "vsphere.vmware.com/type": host.Reference().Type, - "vsphere.vmware.com/moid": host.Reference().Encode(), - "vsphere.vmware.com/inventorypath": host.InventoryPath, - }, - PlatformIds: []string{provider.VsphereResourceID(instanceUuid, host.Reference())}, - } - - // add more information if available - if props != nil && props.Config != nil { - ha.Labels["vsphere.vmware.com/product-name"] = props.Config.Product.Name - ha.Labels["vsphere.vmware.com/product-version"] = props.Config.Product.Version - ha.Labels["vsphere.vmware.com/os-type"] = props.Config.Product.OsType - ha.Labels["vsphere.vmware.com/produce-lineid"] = props.Config.Product.ProductLineId - ha.State = mapHostPowerstateToState(props.Runtime.PowerState) - } - - res = append(res, ha) - } - return res, nil -} - -func hostProperties(host *object.HostSystem) (*mo.HostSystem, error) { - ctx := context.Background() - var props mo.HostSystem - if err := host.Properties(ctx, host.Reference(), nil, &props); err != nil { - return nil, err - } - return &props, nil -} - -func mapHostPowerstateToState(hostPowerState types.HostSystemPowerState) asset.State { - switch hostPowerState { - case types.HostSystemPowerStatePoweredOn: - return asset.State_STATE_RUNNING - case types.HostSystemPowerStatePoweredOff: - return asset.State_STATE_STOPPED - case types.HostSystemPowerStateStandBy: - return asset.State_STATE_PENDING - case types.HostSystemPowerStateUnknown: - return asset.State_STATE_UNKNOWN - default: - return asset.State_STATE_UNKNOWN - } -} - -func (v *VSphere) ListVirtualMachines(parentTC *providers.Config) ([]*asset.Asset, error) { - instanceUuid, err := v.InstanceUuid() - if err != nil { - return nil, err - } - - dcs, err := v.listDatacenters() - if err != nil { - return nil, err - } - - res := []*asset.Asset{} - for i := range dcs { - dc := dcs[i] - vmList, err := v.listVirtualMachines(dc) - if err != nil { - return nil, err - } - vmsAsAssets, err := vmsToAssetList(instanceUuid, vmList, parentTC) - if err != nil { - return nil, err - } - res = append(res, vmsAsAssets...) - } - - return res, nil -} - -func vmsToAssetList(instanceUuid string, vms []*object.VirtualMachine, parentTC *providers.Config) ([]*asset.Asset, error) { - res := []*asset.Asset{} - for i := range vms { - vm := vms[i] - - platformId := provider.VsphereResourceID(instanceUuid, vm.Reference()) - log.Debug().Str("platform-id", platformId).Msg("found vsphere vm") - - vmInfo, err := resourceclient.VmInfo(vm) - if err != nil { - return nil, err - } - - guestState := mapVmGuestState(vmInfo.Guest.GuestState) - - ha := &asset.Asset{ - Name: vm.Name(), - // TODO: derive platform information guest id e.g. debian10_64Guest, be aware that this does not need to be - // the correct platform name - State: guestState, - Labels: map[string]string{ - "vsphere.vmware.com/name": vm.Name(), - "vsphere.vmware.com/type": vm.Reference().Type, - "vsphere.vmware.com/moid": vm.Reference().Encode(), - "vsphere.vmware.com/ip-address": vmInfo.Guest.IpAddress, - "vsphere.vmware.com/inventory-path": vm.InventoryPath, - "vsphere.vmware.com/guest-hostname": vmInfo.Guest.HostName, - "vsphere.vmware.com/guest-family": vmInfo.Guest.GuestFamily, - "vsphere.vmware.com/guest-id": vmInfo.Guest.GuestId, - "vsphere.vmware.com/guest-fullname": vmInfo.Guest.GuestFullName, - }, - PlatformIds: []string{platformId}, - } - - if guestState == asset.State_STATE_RUNNING { - ha.Connections = []*providers.Config{ - { - Backend: providers.ProviderType_VSPHERE_VM, - Host: parentTC.Host, - Insecure: parentTC.Insecure, - Credentials: parentTC.Credentials, - Options: map[string]string{ - "inventoryPath": vm.InventoryPath, - }, - }, - } - } - - res = append(res, ha) - } - return res, nil -} - -func mapVmGuestState(vsphereGuestState string) asset.State { - switch types.VirtualMachineGuestState(vsphereGuestState) { - case types.VirtualMachineGuestStateRunning: - return asset.State_STATE_RUNNING - case types.VirtualMachineGuestStateShuttingDown: - return asset.State_STATE_STOPPING - case types.VirtualMachineGuestStateResetting: - return asset.State_STATE_REBOOT - case types.VirtualMachineGuestStateStandby: - return asset.State_STATE_PENDING - case types.VirtualMachineGuestStateNotRunning: - return asset.State_STATE_STOPPED - case types.VirtualMachineGuestStateUnknown: - return asset.State_STATE_UNKNOWN - default: - return asset.State_STATE_UNKNOWN - } -} - -func (v *VSphere) listDatacenters() ([]*object.Datacenter, error) { - finder := find.NewFinder(v.Client.Client, true) - l, err := finder.ManagedObjectListChildren(context.Background(), "/") - if err != nil { - return nil, nil - } - var dcs []*object.Datacenter - for _, item := range l { - if item.Object.Reference().Type == "Datacenter" { - dc, err := v.getDatacenter(item.Path) - if err != nil { - return nil, err - } - dcs = append(dcs, dc) - } - } - return dcs, nil -} - -func (v *VSphere) getDatacenter(dc string) (*object.Datacenter, error) { - finder := find.NewFinder(v.Client.Client, true) - t := v.Client.ServiceContent.About.ApiType - switch t { - case "HostAgent": - return finder.DefaultDatacenter(context.Background()) - case "VirtualCenter": - if dc != "" { - return finder.Datacenter(context.Background(), dc) - } - return finder.DefaultDatacenter(context.Background()) - } - return nil, fmt.Errorf("unsupported ApiType: %s", t) -} - -func (c *VSphere) listHosts(dc *object.Datacenter) ([]*object.HostSystem, error) { - finder := find.NewFinder(c.Client.Client, true) - finder.SetDatacenter(dc) - res, err := finder.HostSystemList(context.Background(), "*") - if err != nil && IsNotFound(err) { - return []*object.HostSystem{}, nil - } else if err != nil { - return nil, err - } - return res, nil -} - -func (c *VSphere) listVirtualMachines(dc *object.Datacenter) ([]*object.VirtualMachine, error) { - finder := find.NewFinder(c.Client.Client, true) - finder.SetDatacenter(dc) - res, err := finder.VirtualMachineList(context.Background(), "*") - if err != nil && IsNotFound(err) { - return []*object.VirtualMachine{}, nil - } else if err != nil { - return nil, err - } - return res, nil -} - -// IsNotFound returns a boolean indicating whether the error is a not found error. -func IsNotFound(err error) bool { - if err == nil { - return false - } - var e *find.NotFoundError - return errors.As(err, &e) -} diff --git a/_motor/discovery/vsphere/vsphere_resolver.go b/_motor/discovery/vsphere/vsphere_resolver.go deleted file mode 100644 index bb95b2e12b..0000000000 --- a/_motor/discovery/vsphere/vsphere_resolver.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package vsphere - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/rs/zerolog/log" - "go.mondoo.com/cnquery/motor/asset" - "go.mondoo.com/cnquery/motor/discovery/common" - "go.mondoo.com/cnquery/motor/motorid" - "go.mondoo.com/cnquery/motor/platform/detector" - "go.mondoo.com/cnquery/motor/providers" - "go.mondoo.com/cnquery/motor/providers/resolver" - "go.mondoo.com/cnquery/motor/providers/vsphere" - "go.mondoo.com/cnquery/motor/vault" -) - -const ( - DiscoveryApi = "api" - DiscoveryInstances = "instances" - DiscoveryHostMachines = "host-machines" -) - -type Resolver struct{} - -func (r *Resolver) Name() string { - return "VMware vSphere Resolver" -} - -func (r *Resolver) AvailableDiscoveryTargets() []string { - return []string{common.DiscoveryAuto, common.DiscoveryAll, DiscoveryInstances, DiscoveryHostMachines} -} - -func (r *Resolver) Resolve(ctx context.Context, root *asset.Asset, pCfg *providers.Config, credsResolver vault.Resolver, sfn common.QuerySecretFn, userIdDetectors ...providers.PlatformIdDetector) ([]*asset.Asset, error) { - resolved := []*asset.Asset{} - - // we leverage the vsphere provider to establish a connection - m, err := resolver.NewMotorConnection(ctx, pCfg, credsResolver) - if err != nil { - return nil, err - } - defer m.Close() - - trans, ok := m.Provider.(*vsphere.Provider) - if !ok { - return nil, errors.New("could not initialize vsphere provider") - } - - // detect platform info for the asset - pf, err := m.Platform() - if err != nil { - return nil, err - } - - if pCfg.IncludesOneOfDiscoveryTarget(common.DiscoveryAll, common.DiscoveryAuto, DiscoveryApi) { - // add asset for the api itself - info := trans.Info() - assetObj := &asset.Asset{ - Name: fmt.Sprintf("%s (%s)", pCfg.Host, info.Name), - Platform: pf, - Connections: []*providers.Config{pCfg}, // pass-in the current config - Labels: map[string]string{ - "vsphere.vmware.com/name": info.Name, - "vsphere.vmware.com/uuid": info.InstanceUuid, - }, - } - fingerprint, err := motorid.IdentifyPlatform(m.Provider, pf, nil) - if err != nil { - return nil, err - } - assetObj.PlatformIds = fingerprint.PlatformIDs - if fingerprint.Name != "" { - assetObj.Name = fingerprint.Name - } - - log.Debug().Strs("identifier", assetObj.PlatformIds).Msg("motor connection") - - resolved = append(resolved, assetObj) - } - - client := trans.Client() - discoveryClient := New(client) - - if pCfg.IncludesOneOfDiscoveryTarget(common.DiscoveryAll, common.DiscoveryAuto, DiscoveryHostMachines) { - // resolve esxi hosts - hosts, err := discoveryClient.ListEsxiHosts() - if err != nil { - return nil, err - } - - // add provider config for each host - for i := range hosts { - host := hosts[i] - ht := pCfg.Clone() - // pass-through "vsphere.vmware.com/reference-type" and "vsphere.vmware.com/inventorypath" - ht.Options = host.Annotations - host.Connections = append(host.Connections, ht) - - pf, err := detector.VspherePlatform(trans, host.PlatformIds[0]) - if err == nil { - host.Platform = pf - } else { - log.Error().Err(err).Msg("could not determine platform information for ESXi host") - } - - resolved = append(resolved, host) - } - } - - if pCfg.IncludesOneOfDiscoveryTarget(common.DiscoveryAll, DiscoveryInstances) { - // resolve vms - vms, err := discoveryClient.ListVirtualMachines(pCfg) - if err != nil { - return nil, err - } - - // add provider config for each vm - for i := range vms { - vm := vms[i] - - pf, err := detector.VspherePlatform(trans, vm.PlatformIds[0]) - if err == nil { - vm.Platform = pf - } else { - log.Error().Err(err).Msg("could not determine platform information for ESXi vm") - } - - // find the secret reference for the asset - EnrichVsphereToolsConnWithSecrets(vm, credsResolver, sfn) - - resolved = append(resolved, vm) - } - } - - // filter assets - discoverFilter := map[string]string{} - if pCfg.Discover != nil { - discoverFilter = pCfg.Discover.Filter - } - - if namesFilter, ok := discoverFilter["names"]; ok { - names := strings.Split(namesFilter, ",") - resolved = filter(resolved, func(a *asset.Asset) bool { - return contains(names, a.Name) - }) - } - - if moidsFilter, ok := discoverFilter["moids"]; ok { - moids := strings.Split(moidsFilter, ",") - resolved = filter(resolved, func(a *asset.Asset) bool { - label, ok := a.Labels["vsphere.vmware.com/moid"] - log.Debug().Strs("moids", moids).Str("search", label).Msg("check if moid is included") - if !ok { - return false - } - return contains(moids, label) - }) - } - - return resolved, nil -} - -func filter(a []*asset.Asset, keep func(asset *asset.Asset) bool) []*asset.Asset { - n := 0 - for _, x := range a { - if keep(x) { - a[n] = x - n++ - } - } - a = a[:n] - return a -} - -func contains(slice []string, entry string) bool { - sanitizedEntry := strings.ToLower(strings.TrimSpace(entry)) - - for i := range slice { - if strings.ToLower(strings.TrimSpace(slice[i])) == sanitizedEntry { - return true - } - } - return false -} diff --git a/_motor/discovery/vsphere/vsphere_resolver_test.go b/_motor/discovery/vsphere/vsphere_resolver_test.go deleted file mode 100644 index 582c2e7921..0000000000 --- a/_motor/discovery/vsphere/vsphere_resolver_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package vsphere - -import ( - "context" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.mondoo.com/cnquery/motor/asset" - "go.mondoo.com/cnquery/motor/providers" - "go.mondoo.com/cnquery/motor/providers/vsphere/vsimulator" - "go.mondoo.com/cnquery/motor/vault" -) - -func TestVsphereResolver(t *testing.T) { - vs, err := vsimulator.New() - require.NoError(t, err) - defer vs.Close() - - port, err := strconv.Atoi(vs.Server.URL.Port()) - require.NoError(t, err) - - // start vsphere discover - r := Resolver{} - assets, err := r.Resolve(context.Background(), &asset.Asset{}, &providers.Config{ - Backend: providers.ProviderType_VSPHERE, - Host: vs.Server.URL.Hostname(), - Port: int32(port), - Insecure: true, // allows self-signed certificates - Credentials: []*vault.Credential{ - { - Type: vault.CredentialType_password, - User: vsimulator.Username, - Secret: []byte(vsimulator.Password), - }, - }, - Discover: &providers.Discovery{ - Targets: []string{"all"}, - }, - }, nil, nil) - require.NoError(t, err) - assert.Equal(t, 9, len(assets)) // api + esx + vm -} diff --git a/providers/assets.go b/providers/assets.go index 3f5efb798f..4312e121c5 100644 --- a/providers/assets.go +++ b/providers/assets.go @@ -7,6 +7,7 @@ import ( "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" "go.mondoo.com/cnquery/cli/config" + "go.mondoo.com/cnquery/logger" "go.mondoo.com/cnquery/providers-sdk/v1/inventory" pp "go.mondoo.com/cnquery/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/providers-sdk/v1/upstream" @@ -17,6 +18,7 @@ func ProcessAssetCandidates(runtime *Runtime, connectRes *pp.ConnectRes, upstrea if connectRes.Inventory == nil || connectRes.Inventory.Spec == nil { return []*inventory.Asset{connectRes.Asset}, nil } else { + logger.DebugDumpJSON("inventory-resolved", connectRes.Inventory) assetCandidates = connectRes.Inventory.Spec.Assets } log.Debug().Msgf("resolved %d assets", len(assetCandidates)) diff --git a/providers/gcp/provider/provider.go b/providers/gcp/provider/provider.go index d7b01a80e6..afbf4ee900 100644 --- a/providers/gcp/provider/provider.go +++ b/providers/gcp/provider/provider.go @@ -274,7 +274,6 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } func (s *Service) detect(asset *inventory.Asset, conn shared.GcpConnection) error { - // TODO: adjust asset detection asset.Id = conn.Config().Type asset.Name = conn.Config().Host @@ -286,7 +285,6 @@ func (s *Service) detect(asset *inventory.Asset, conn shared.GcpConnection) erro Kind: "api", Title: "GCP Cloud", } - // TODO: Add platform IDs asset.PlatformIds = []string{"//platformid.api.mondoo.app/runtime/gcp/"} } diff --git a/providers/vsphere/config/config.go b/providers/vsphere/config/config.go index 5fd8afccb0..a954fdea80 100644 --- a/providers/vsphere/config/config.go +++ b/providers/vsphere/config/config.go @@ -6,6 +6,7 @@ package config import ( "go.mondoo.com/cnquery/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/providers/vsphere/provider" + "go.mondoo.com/cnquery/providers/vsphere/resources" ) var Config = plugin.Provider{ @@ -15,12 +16,16 @@ var Config = plugin.Provider{ ConnectionTypes: []string{provider.ConnectionType}, Connectors: []plugin.Connector{ { - Name: "vsphere", - Use: "vsphere user@host", - Short: "VMware vSphere", - Discovery: []string{}, - MinArgs: 1, - MaxArgs: 1, + Name: "vsphere", + Use: "vsphere user@host", + Short: "VMware vSphere", + Discovery: []string{ + resources.DiscoveryApi, + resources.DiscoveryInstances, + resources.DiscoveryHostMachines, + }, + MinArgs: 1, + MaxArgs: 1, Flags: []plugin.Flag{ { Long: "ask-pass", diff --git a/providers/vsphere/connection/platform.go b/providers/vsphere/connection/platform.go index 86df3680e1..c73b692fbe 100644 --- a/providers/vsphere/connection/platform.go +++ b/providers/vsphere/connection/platform.go @@ -18,6 +18,12 @@ import ( "go.mondoo.com/cnquery/mrn" ) +const ( + VspherePlatform = "vmware-vsphere" + EsxiPlatform = "vmware-esxi" + Family = "vmware-vsphere" +) + func listDatacenters(c *govmomi.Client) ([]*object.Datacenter, error) { finder := find.NewFinder(c.Client, true) l, err := finder.ManagedObjectListChildren(context.Background(), "/") @@ -82,6 +88,20 @@ type EsxiSystemVersion struct { Moid string } +func (c *VsphereConnection) EsxiVersion(moid string) (*EsxiSystemVersion, error) { + hostRef, err := DecodeMoid(moid) + if err != nil { + return nil, err + } + + host, err := c.Host(hostRef) + if err != nil { + return nil, err + } + + return esxiVersion(host) +} + // $ESXCli.system.version.get() // Build : Releasebuild-8169922 // Patch : 0 @@ -89,7 +109,7 @@ type EsxiSystemVersion struct { // Update : 0 // Version : 6.7.0 // see https://kb.vmware.com/s/article/2143832 for version and build number mapping -func EsxiVersion(host *object.HostSystem) (*EsxiSystemVersion, error) { +func esxiVersion(host *object.HostSystem) (*EsxiSystemVersion, error) { e, err := esxcli.NewExecutor(host.Client(), host) if err != nil { return nil, err @@ -139,7 +159,7 @@ func EsxiVersion(host *object.HostSystem) (*EsxiSystemVersion, error) { return &version, nil } -func GetHost(client *govmomi.Client) (*object.HostSystem, error) { +func getHost(client *govmomi.Client) (*object.HostSystem, error) { dcs, err := listDatacenters(client) if err != nil { return nil, err @@ -162,10 +182,10 @@ func GetHost(client *govmomi.Client) (*object.HostSystem, error) { return host, nil } -func InstanceUUID(client *govmomi.Client) (string, error) { +func (c *VsphereConnection) InstanceUUID() (string, error) { // determine identifier since ESXI connections do not return an InstanceUuid - if !client.IsVC() { - host, err := GetHost(client) + if !c.client.IsVC() { + host, err := getHost(c.client) if err != nil { return "", err } @@ -174,7 +194,7 @@ func InstanceUUID(client *govmomi.Client) (string, error) { return host.Reference().Value, nil } - v := client.ServiceContent.About + v := c.client.ServiceContent.About return v.InstanceUuid, nil } @@ -190,7 +210,7 @@ func (c *VsphereConnection) Identifier() (string, error) { return c.selectedPlatformID, nil } - id, err := InstanceUUID(c.Client()) + id, err := c.InstanceUUID() if err != nil { log.Warn().Err(err).Msg("failed to get vsphere instance uuid") // This error is being ignored @@ -205,11 +225,11 @@ func (c *VsphereConnection) Info() types.AboutInfo { return c.Client().ServiceContent.About } -func VsphereResourceID(instance string, reference types.ManagedObjectReference) string { - return "//platformid.api.mondoo.app/runtime/vsphere/instance/" + instance + "/moid/" + reference.Encode() +func VsphereResourceID(instance string, moid string) string { + return "//platformid.api.mondoo.app/runtime/vsphere/instance/" + instance + "/moid/" + moid } -func decodeMoid(moid string) (types.ManagedObjectReference, error) { +func DecodeMoid(moid string) (types.ManagedObjectReference, error) { r := types.ManagedObjectReference{} s := strings.SplitN(moid, "-", 2) @@ -235,7 +255,7 @@ func ParseVsphereResourceID(id string) (types.ManagedObjectReference, error) { return reference, errors.New("vsphere platform id has invalid type") } - reference, err = decodeMoid(moid) + reference, err = DecodeMoid(moid) if err != nil { return reference, err } diff --git a/providers/vsphere/connection/platform_test.go b/providers/vsphere/connection/platform_test.go index d3000a7dc1..c7bf694881 100644 --- a/providers/vsphere/connection/platform_test.go +++ b/providers/vsphere/connection/platform_test.go @@ -35,7 +35,7 @@ func TestVsphereID(t *testing.T) { } func TestMrnParser(t *testing.T) { - id := VsphereResourceID("uuid", types.ManagedObjectReference{Type: "VirtualMachine", Value: "4"}) + id := VsphereResourceID("uuid", types.ManagedObjectReference{Type: "VirtualMachine", Value: "4"}.Encode()) assert.Equal(t, "//platformid.api.mondoo.app/runtime/vsphere/instance/uuid/moid/VirtualMachine-4", id) ok := IsVsphereResourceID(id) assert.True(t, ok) diff --git a/providers/vsphere/provider/provider.go b/providers/vsphere/provider/provider.go index e9d69692c1..90ff30c0d3 100644 --- a/providers/vsphere/provider/provider.go +++ b/providers/vsphere/provider/provider.go @@ -79,6 +79,19 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) conf.Credentials = append(conf.Credentials, vault.NewPasswordCredential(user, string(x.Value))) } + // parse discovery flags + conf.Discover = &inventory.Discovery{ + Targets: []string{}, + } + if x, ok := flags["discover"]; ok && len(x.Array) != 0 { + for i := range x.Array { + entry := string(x.Array[i].Value) + conf.Discover.Targets = append(conf.Discover.Targets, entry) + } + } else { + conf.Discover.Targets = []string{resources.DiscoveryAuto} + } + asset := inventory.Asset{ Connections: []*inventory.Config{conf}, } @@ -114,11 +127,16 @@ func (s *Service) Connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } } + in, err := s.discover(conn) + if err != nil { + return nil, err + } + return &plugin.ConnectRes{ Id: conn.ID(), Name: conn.Name(), Asset: req.Asset, - Inventory: nil, + Inventory: in, }, nil } @@ -163,15 +181,18 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } func (s *Service) detect(asset *inventory.Asset, conn *connection.VsphereConnection) error { - // TODO: adjust asset detection with full discovery asset.Id = conn.Conf.Type asset.Name = conn.Conf.Host + vSphereInfo := conn.Info() asset.Platform = &inventory.Platform{ - Name: "vsphere", - Family: []string{"vsphere"}, - Kind: "api", - Title: "VMware vSphere", + Name: connection.VspherePlatform, + Family: []string{connection.Family}, + Title: "VMware vSphere " + vSphereInfo.Version, + Version: vSphereInfo.Version, + Build: vSphereInfo.Build, + Kind: "api", + Runtime: "vsphere", } id, err := conn.Identifier() @@ -182,6 +203,20 @@ func (s *Service) detect(asset *inventory.Asset, conn *connection.VsphereConnect return nil } +func (s *Service) discover(conn *connection.VsphereConnection) (*inventory.Inventory, error) { + if conn.Conf.Discover == nil { + return nil, nil + } + + runtime, ok := s.runtimes[conn.ID()] + if !ok { + // no connection found, this should never happen + return nil, errors.New("connection " + strconv.FormatUint(uint64(conn.ID()), 10) + " not found") + } + + return resources.Discover(runtime) +} + func (s *Service) GetData(req *plugin.DataReq) (*plugin.DataRes, error) { runtime, ok := s.runtimes[req.Connection] if !ok { diff --git a/providers/vsphere/provider/provider_test.go b/providers/vsphere/provider/provider_test.go index 10423d092d..a50de394d5 100644 --- a/providers/vsphere/provider/provider_test.go +++ b/providers/vsphere/provider/provider_test.go @@ -4,6 +4,7 @@ package provider import ( + "github.com/stretchr/testify/require" "strconv" "testing" @@ -123,3 +124,47 @@ func TestResource_Vsphere(t *testing.T) { assert.Equal(t, "DC0_H0", string(dataResp.Data.Value)) }) } + +func TestVsphereDiscovery(t *testing.T) { + vs, err := vsimulator.New() + if err != nil { + panic(err) + } + + port, err := strconv.Atoi(vs.Server.URL.Port()) + if err != nil { + panic(err) + } + + srv := &Service{ + runtimes: map[uint32]*plugin.Runtime{}, + lastConnectionID: 0, + } + + resp, err := srv.Connect(&plugin.ConnectReq{ + Asset: &inventory.Asset{ + Connections: []*inventory.Config{ + { + Type: "vsphere", + Host: vs.Server.URL.Hostname(), + Port: int32(port), + Insecure: true, // allows self-signed certificates + Discover: &inventory.Discovery{ + Targets: []string{"auto"}, + }, + Credentials: []*vault.Credential{ + { + Type: vault.CredentialType_password, + User: vsimulator.Username, + Secret: []byte(vsimulator.Password), + }, + }, + }, + }, + }, + }, nil) + require.NoError(t, err) + assert.NotNil(t, resp.Asset) + assert.Equal(t, 8, len(resp.Inventory.Spec.Assets)) // api + esx + vm + +} diff --git a/providers/vsphere/resources/datacenter.go b/providers/vsphere/resources/datacenter.go index 6a8df3ecb7..ece56471c2 100644 --- a/providers/vsphere/resources/datacenter.go +++ b/providers/vsphere/resources/datacenter.go @@ -39,6 +39,7 @@ func newVsphereHostResources(vClient *resourceclient.Client, runtime *plugin.Run if err != nil { return nil, err } + mqlHost.(*mqlVsphereHost).host = hostInfo mqlHosts[i] = mqlHost } @@ -165,22 +166,7 @@ func (v *mqlVsphereDatacenter) vms() ([]interface{}, error) { return nil, err } - props, err := resourceclient.VmProperties(vmInfo) - if err != nil { - return nil, err - } - - var name string - if vmInfo != nil && vmInfo.Config != nil { - name = vmInfo.Config.Name - } - - mqlVm, err := CreateResource(v.MqlRuntime, "vsphere.vm", map[string]*llx.RawData{ - "moid": llx.StringData(vm.Reference().Encode()), - "name": llx.StringData(name), - "properties": llx.DictData(props), - "inventoryPath": llx.StringData(vm.InventoryPath), - }) + mqlVm, err := newMqlVm(v.MqlRuntime, vm, vmInfo) if err != nil { return nil, err } @@ -190,24 +176,3 @@ func (v *mqlVsphereDatacenter) vms() ([]interface{}, error) { return mqlVms, nil } - -func (v *mqlVsphereVm) id() (string, error) { - return v.Moid.Data, nil -} - -func (v *mqlVsphereVm) advancedSettings() (map[string]interface{}, error) { - conn := v.MqlRuntime.Connection.(*connection.VsphereConnection) - vClient := getClientInstance(conn) - - if v.InventoryPath.Error != nil { - return nil, v.InventoryPath.Error - } - path := v.InventoryPath.Data - - vm, err := vClient.VirtualMachineByInventoryPath(path) - if err != nil { - return nil, err - } - - return resourceclient.AdvancedSettings(vm) -} diff --git a/providers/vsphere/resources/discovery.go b/providers/vsphere/resources/discovery.go new file mode 100644 index 0000000000..cfcfc81e5d --- /dev/null +++ b/providers/vsphere/resources/discovery.go @@ -0,0 +1,212 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resources + +import ( + "github.com/rs/zerolog/log" + "github.com/vmware/govmomi/vim25/types" + "go.mondoo.com/cnquery/llx" + "go.mondoo.com/cnquery/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/providers/vsphere/connection" + "go.mondoo.com/cnquery/utils/stringx" +) + +// Discovery Flags +const ( + DiscoveryAll = "all" // api, hosts, instances + DiscoveryAuto = "auto" // api, hosts + + DiscoveryApi = "api" + DiscoveryInstances = "instances" + DiscoveryHostMachines = "host-machines" +) + +var All = []string{ + DiscoveryApi, + DiscoveryHostMachines, + DiscoveryInstances, +} + +var Auto = []string{ + DiscoveryApi, + DiscoveryHostMachines, +} + +func Discover(runtime *plugin.Runtime) (*inventory.Inventory, error) { + conn := runtime.Connection.(*connection.VsphereConnection) + + asset := conn.Asset() + if asset == nil { + return nil, nil + } + + // if the asset is not a vsphere asset, return nil + if asset.Platform == nil { + return nil, nil + } + + // we only run discovery on vSphere API assets + if asset.Platform.Name != connection.VspherePlatform { + return nil, nil + } + + in := &inventory.Inventory{Spec: &inventory.InventorySpec{ + Assets: []*inventory.Asset{}, + }} + + targets := handleTargets(conn.Conf.Discover.Targets) + + if stringx.Contains(targets, DiscoveryApi) { + in.Spec.Assets = append(in.Spec.Assets, conn.Asset()) + } + + res, err := NewResource(runtime, "vsphere", map[string]*llx.RawData{}) + if err != nil { + return nil, err + } + + vsphereResource := res.(*mqlVsphere) + + datacenterList := vsphereResource.GetDatacenters() + if datacenterList.Error != nil { + return nil, datacenterList.Error + } + + for i := range datacenterList.Data { + datacenterResource := datacenterList.Data[i].(*mqlVsphereDatacenter) + for i := range targets { + target := targets[i] + list, err := discoverDatacenter(conn, datacenterResource, target) + if err != nil { + log.Error().Err(err).Msg("error during discovery") + continue + } + in.Spec.Assets = append(in.Spec.Assets, list...) + } + } + + return in, nil +} + +func handleTargets(targets []string) []string { + if len(targets) == 0 || stringx.Contains(targets, DiscoveryAuto) { + // default to auto if none defined + return Auto + } + if stringx.Contains(targets, DiscoveryAll) { + return All + } + return targets +} + +func discoverDatacenter(conn *connection.VsphereConnection, datacenterResource *mqlVsphereDatacenter, target string) ([]*inventory.Asset, error) { + assetList := []*inventory.Asset{} + + instanceUuid, err := conn.InstanceUUID() + if err != nil { + return nil, err + } + + // resolve esxi hosts + switch target { + case DiscoveryHostMachines: + hostList := datacenterResource.GetHosts() + if hostList.Error != nil { + return nil, hostList.Error + } + for j := range hostList.Data { + mqlHost := hostList.Data[j].(*mqlVsphereHost) + + esxiVersion, err := conn.EsxiVersion(mqlHost.Moid.Data) + if err != nil { + return nil, err + } + + platformID := connection.VsphereResourceID(instanceUuid, mqlHost.Moid.Data) + clonedConfig := conn.Conf.Clone(inventory.WithoutDiscovery()) + clonedConfig.PlatformId = platformID + assetList = append(assetList, &inventory.Asset{ + Name: mqlHost.Name.Data, + Platform: &inventory.Platform{ + Title: "VMware ESXi", + Name: connection.EsxiPlatform, + Version: esxiVersion.Version, + Build: esxiVersion.Build, + Kind: "baremetal", + Runtime: "vsphere-host", + Family: []string{connection.Family}, + }, + Connections: []*inventory.Config{clonedConfig}, // pass-in the parent connection config + Labels: map[string]string{ + "vsphere.vmware.com/name": mqlHost.Name.Data, + "vsphere.vmware.com/moid": mqlHost.Moid.Data, + "vsphere.vmware.com/inventorypath": mqlHost.InventoryPath.Data, + }, + State: mapHostPowerstateToState(mqlHost.host.Runtime.PowerState), + PlatformIds: []string{platformID}, + }) + } + case DiscoveryInstances: + vmList := datacenterResource.GetVms() + if vmList.Error != nil { + return nil, vmList.Error + } + for j := range vmList.Data { + vm := vmList.Data[j].(*mqlVsphereVm) + + platformID := connection.VsphereResourceID(instanceUuid, vm.Moid.Data) + clonedConfig := conn.Conf.Clone(inventory.WithoutDiscovery()) + clonedConfig.PlatformId = platformID + assetList = append(assetList, &inventory.Asset{ + Name: vm.Name.Data, + Platform: &inventory.Platform{}, + Connections: []*inventory.Config{clonedConfig}, + Labels: map[string]string{ + "vsphere.vmware.com/name": vm.Name.Data, + "vsphere.vmware.com/moid": vm.Moid.Data, + "vsphere.vmware.com/inventory-path": vm.InventoryPath.Data, + }, + State: mapVmGuestState(vm.vm.Guest.GuestState), + PlatformIds: []string{platformID}, + }) + } + } + + return assetList, nil +} + +func mapHostPowerstateToState(hostPowerState types.HostSystemPowerState) inventory.State { + switch hostPowerState { + case types.HostSystemPowerStatePoweredOn: + return inventory.State_STATE_RUNNING + case types.HostSystemPowerStatePoweredOff: + return inventory.State_STATE_STOPPED + case types.HostSystemPowerStateStandBy: + return inventory.State_STATE_PENDING + case types.HostSystemPowerStateUnknown: + return inventory.State_STATE_UNKNOWN + default: + return inventory.State_STATE_UNKNOWN + } +} + +func mapVmGuestState(vsphereGuestState string) inventory.State { + switch types.VirtualMachineGuestState(vsphereGuestState) { + case types.VirtualMachineGuestStateRunning: + return inventory.State_STATE_RUNNING + case types.VirtualMachineGuestStateShuttingDown: + return inventory.State_STATE_STOPPING + case types.VirtualMachineGuestStateResetting: + return inventory.State_STATE_REBOOT + case types.VirtualMachineGuestStateStandby: + return inventory.State_STATE_PENDING + case types.VirtualMachineGuestStateNotRunning: + return inventory.State_STATE_STOPPED + case types.VirtualMachineGuestStateUnknown: + return inventory.State_STATE_UNKNOWN + default: + return inventory.State_STATE_UNKNOWN + } +} diff --git a/providers/vsphere/resources/host.go b/providers/vsphere/resources/host.go index 9036a89ea3..b04a9bcf49 100644 --- a/providers/vsphere/resources/host.go +++ b/providers/vsphere/resources/host.go @@ -5,17 +5,21 @@ package resources import ( "errors" + "github.com/vmware/govmomi/vim25/mo" "time" - "go.mondoo.com/cnquery/providers-sdk/v1/plugin" - "go.mondoo.com/cnquery/llx" + "go.mondoo.com/cnquery/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/providers-sdk/v1/util/convert" "go.mondoo.com/cnquery/providers/vsphere/connection" "go.mondoo.com/cnquery/providers/vsphere/resources/resourceclient" "go.mondoo.com/cnquery/types" ) +type mqlVsphereHostInternal struct { + host *mo.HostSystem +} + func (v *mqlVsphereHost) id() (string, error) { return v.Moid.Data, nil } diff --git a/providers/vsphere/resources/vm.go b/providers/vsphere/resources/vm.go new file mode 100644 index 0000000000..3dce52cc32 --- /dev/null +++ b/providers/vsphere/resources/vm.go @@ -0,0 +1,63 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resources + +import ( + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + "go.mondoo.com/cnquery/llx" + "go.mondoo.com/cnquery/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/providers/vsphere/connection" + "go.mondoo.com/cnquery/providers/vsphere/resources/resourceclient" +) + +func newMqlVm(runtime *plugin.Runtime, vm *object.VirtualMachine, vmInfo *mo.VirtualMachine) (*mqlVsphereVm, error) { + props, err := resourceclient.VmProperties(vmInfo) + if err != nil { + return nil, err + } + + var name string + if vmInfo != nil && vmInfo.Config != nil { + name = vmInfo.Config.Name + } + + mqlVm, err := CreateResource(runtime, "vsphere.vm", map[string]*llx.RawData{ + "moid": llx.StringData(vm.Reference().Encode()), + "name": llx.StringData(name), + "properties": llx.DictData(props), + "inventoryPath": llx.StringData(vm.InventoryPath), + }) + if err != nil { + return nil, err + } + + mqlVm.(*mqlVsphereVm).vm = vmInfo + return mqlVm.(*mqlVsphereVm), nil +} + +type mqlVsphereVmInternal struct { + vm *mo.VirtualMachine +} + +func (v *mqlVsphereVm) id() (string, error) { + return v.Moid.Data, nil +} + +func (v *mqlVsphereVm) advancedSettings() (map[string]interface{}, error) { + conn := v.MqlRuntime.Connection.(*connection.VsphereConnection) + vClient := getClientInstance(conn) + + if v.InventoryPath.Error != nil { + return nil, v.InventoryPath.Error + } + path := v.InventoryPath.Data + + vm, err := vClient.VirtualMachineByInventoryPath(path) + if err != nil { + return nil, err + } + + return resourceclient.AdvancedSettings(vm) +} diff --git a/providers/vsphere/resources/vsphere.go b/providers/vsphere/resources/vsphere.go index 26641530f6..485fd79a48 100644 --- a/providers/vsphere/resources/vsphere.go +++ b/providers/vsphere/resources/vsphere.go @@ -5,7 +5,6 @@ package resources import ( "errors" - "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/vim25/mo" "go.mondoo.com/cnquery/llx" @@ -129,7 +128,6 @@ func esxiHostProperties(conn *connection.VsphereConnection) (*object.HostSystem, h = hosts[0] } else { - // check if the connection was initialized with a specific host identifier, err := conn.Identifier() if err != nil || !connection.IsVsphereResourceID(identifier) { @@ -233,27 +231,7 @@ func (v *mqlEsxi) vm() (*mqlVsphereVm, error) { return nil, err } - props, err := resourceclient.VmProperties(vmInfo) - if err != nil { - return nil, err - } - - var name string - if vmInfo != nil && vmInfo.Config != nil { - name = vmInfo.Config.Name - } - - mqlVm, err := CreateResource(v.MqlRuntime, "vsphere.vm", map[string]*llx.RawData{ - "moid": llx.StringData(vm.Reference().Encode()), - "name": llx.StringData(name), - "properties": llx.DictData(props), - "inventoryPath": llx.StringData(vm.InventoryPath), - }) - if err != nil { - return nil, err - } - - return mqlVm.(*mqlVsphereVm), nil + return newMqlVm(v.MqlRuntime, vm, vmInfo) } func (v *mqlEsxiCommand) id() (string, error) { diff --git a/providers/vsphere/resources/vsphere.lr.go b/providers/vsphere/resources/vsphere.lr.go index 0b273ecc67..20c569dee6 100644 --- a/providers/vsphere/resources/vsphere.lr.go +++ b/providers/vsphere/resources/vsphere.lr.go @@ -1326,7 +1326,7 @@ func (c *mqlVsphereCluster) GetHosts() *plugin.TValue[[]interface{}] { type mqlVsphereHost struct { MqlRuntime *plugin.Runtime __id string - // optional: if you define mqlVsphereHostInternal it will be used here + mqlVsphereHostInternal Moid plugin.TValue[string] Name plugin.TValue[string] InventoryPath plugin.TValue[string] @@ -1564,7 +1564,7 @@ func (c *mqlVsphereHost) GetSnmp() *plugin.TValue[map[string]interface{}] { type mqlVsphereVm struct { MqlRuntime *plugin.Runtime __id string - // optional: if you define mqlVsphereVmInternal it will be used here + mqlVsphereVmInternal Moid plugin.TValue[string] Name plugin.TValue[string] InventoryPath plugin.TValue[string]