Skip to content

Commit efe793d

Browse files
tas50claude
andcommitted
✨ add Hetzner Cloud platform detection
Detect Hetzner Cloud instances via DMI sys_vendor ("Hetzner") and resolve instance identity from the metadata service at 169.254.169.254/hetzner/v1/metadata. Wires into clouddetect and the os cloud resource resolver. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 66fba2b commit efe793d

File tree

5 files changed

+245
-5
lines changed

5 files changed

+245
-5
lines changed

providers/os/id/clouddetect/clouddetect.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"go.mondoo.com/mql/v13/providers/os/id/aws"
1313
"go.mondoo.com/mql/v13/providers/os/id/azure"
1414
"go.mondoo.com/mql/v13/providers/os/id/gcp"
15+
"go.mondoo.com/mql/v13/providers/os/id/hetzner"
1516
"go.mondoo.com/mql/v13/providers/os/id/ibm"
1617
"go.mondoo.com/mql/v13/providers/os/id/vmware"
1718
)
@@ -34,14 +35,16 @@ var (
3435
AZURE CloudProviderType = "AZURE"
3536
VMWARE CloudProviderType = "VMWARE"
3637
IBM CloudProviderType = "IBM"
38+
HETZNER CloudProviderType = "HETZNER"
3739
)
3840

3941
var detectors = map[CloudProviderType]detectorFunc{
40-
AWS: aws.Detect,
41-
GCP: gcp.Detect,
42-
AZURE: azure.Detect,
43-
VMWARE: vmware.Detect,
44-
IBM: ibm.Detect,
42+
AWS: aws.Detect,
43+
GCP: gcp.Detect,
44+
AZURE: azure.Detect,
45+
VMWARE: vmware.Detect,
46+
IBM: ibm.Detect,
47+
HETZNER: hetzner.Detect,
4548
}
4649

4750
// PlatformInfo contains platform information gathered from one of our cloud detectors.

providers/os/id/hetzner/hetzner.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package hetzner
5+
6+
import (
7+
"strings"
8+
9+
"github.com/rs/zerolog/log"
10+
"github.com/spf13/afero"
11+
"go.mondoo.com/mql/v13/providers-sdk/v1/inventory"
12+
"go.mondoo.com/mql/v13/providers/os/connection/shared"
13+
"go.mondoo.com/mql/v13/providers/os/id/hetznercloud"
14+
"go.mondoo.com/mql/v13/providers/os/resources/smbios"
15+
)
16+
17+
const (
18+
hetznerIdentifierFileLinux = "/sys/class/dmi/id/sys_vendor"
19+
)
20+
21+
func Detect(conn shared.Connection, pf *inventory.Platform) (string, string, []string) {
22+
sysVendor := ""
23+
if pf.IsFamily(inventory.FAMILY_LINUX) {
24+
content, err := afero.ReadFile(conn.FileSystem(), hetznerIdentifierFileLinux)
25+
if err != nil {
26+
log.Debug().Err(err).Msgf("unable to read %s", hetznerIdentifierFileLinux)
27+
return "", "", nil
28+
}
29+
sysVendor = strings.TrimSpace(string(content))
30+
} else {
31+
smbiosMgr, err := smbios.ResolveManager(conn, pf)
32+
if err != nil {
33+
log.Debug().Err(err).Msg("failed to resolve smbios manager")
34+
return "", "", nil
35+
}
36+
37+
info, err := smbiosMgr.Info()
38+
if err != nil {
39+
log.Debug().Err(err).Msg("failed to query smbios")
40+
return "", "", nil
41+
}
42+
sysVendor = info.SysInfo.Vendor
43+
}
44+
45+
if strings.Contains(sysVendor, "Hetzner") {
46+
mdsvc, err := hetznercloud.Resolve(conn, pf)
47+
if err != nil {
48+
log.Debug().Err(err).Msg("failed to get hetzner metadata resolver")
49+
return "", "", nil
50+
}
51+
id, err := mdsvc.Identify()
52+
if err == nil {
53+
return id.InstanceID, id.Hostname, nil
54+
}
55+
log.Debug().Err(err).
56+
Strs("platform", pf.GetFamily()).
57+
Msg("failed to get Hetzner platform id")
58+
}
59+
60+
return "", "", nil
61+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package hetznercloud
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"io"
10+
"strings"
11+
12+
"go.mondoo.com/mql/v13/providers-sdk/v1/inventory"
13+
"go.mondoo.com/mql/v13/providers/os/connection/shared"
14+
"gopkg.in/yaml.v3"
15+
)
16+
17+
const (
18+
metadataSvcURL = "http://169.254.169.254/hetzner/v1/metadata"
19+
)
20+
21+
func MondooHetznerInstanceID(instanceID string) string {
22+
return "//platformid.api.mondoo.app/runtime/hetzner/instances/" + instanceID
23+
}
24+
25+
type Identity struct {
26+
InstanceID string
27+
Hostname string
28+
Region string
29+
}
30+
31+
type InstanceIdentifier interface {
32+
Identify() (Identity, error)
33+
RawMetadata() (any, error)
34+
}
35+
36+
func Resolve(conn shared.Connection, pf *inventory.Platform) (InstanceIdentifier, error) {
37+
if pf.IsFamily(inventory.FAMILY_UNIX) {
38+
return &commandInstanceMetadata{conn, pf}, nil
39+
}
40+
return nil, fmt.Errorf(
41+
"hetzner cloud id detector is not supported for your asset: %s %s",
42+
pf.Name, pf.Version,
43+
)
44+
}
45+
46+
// hetznerMetadata represents the YAML structure returned by the Hetzner metadata service
47+
type hetznerMetadata struct {
48+
InstanceID int `yaml:"instance-id"`
49+
Hostname string `yaml:"hostname"`
50+
Region string `yaml:"region"`
51+
AvailabilityZone string `yaml:"availability-zone"`
52+
LocalIPv4 string `yaml:"local-ipv4"`
53+
}
54+
55+
type commandInstanceMetadata struct {
56+
conn shared.Connection
57+
platform *inventory.Platform
58+
}
59+
60+
func (m *commandInstanceMetadata) RawMetadata() (any, error) {
61+
data, err := m.fetchMetadata()
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
var rawMap map[string]any
67+
if err := yaml.Unmarshal(data, &rawMap); err != nil {
68+
return nil, err
69+
}
70+
return rawMap, nil
71+
}
72+
73+
func (m *commandInstanceMetadata) Identify() (Identity, error) {
74+
data, err := m.fetchMetadata()
75+
if err != nil {
76+
return Identity{}, err
77+
}
78+
79+
md := hetznerMetadata{}
80+
if err := yaml.Unmarshal(data, &md); err != nil {
81+
return Identity{}, fmt.Errorf("failed to decode Hetzner metadata: %w", err)
82+
}
83+
84+
if md.InstanceID == 0 {
85+
return Identity{}, errors.New("hetzner metadata did not contain an instance-id")
86+
}
87+
88+
return Identity{
89+
InstanceID: MondooHetznerInstanceID(fmt.Sprintf("%d", md.InstanceID)),
90+
Hostname: md.Hostname,
91+
Region: md.Region,
92+
}, nil
93+
}
94+
95+
func (m *commandInstanceMetadata) fetchMetadata() ([]byte, error) {
96+
cmdStr := fmt.Sprintf("curl --retry 3 --retry-delay 1 --connect-timeout 1 --retry-max-time 5 --max-time 10 --noproxy '*' %s", metadataSvcURL)
97+
cmd, err := m.conn.RunCommand(cmdStr)
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
data, err := io.ReadAll(cmd.Stdout)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
content := strings.TrimSpace(string(data))
108+
if content == "" {
109+
return nil, errors.New("empty response from Hetzner metadata service")
110+
}
111+
112+
return []byte(content), nil
113+
}

providers/os/resources/cloud/cloud.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ func Resolve(conn shared.Connection) (OSCloud, error) {
4343
return &vmware{conn}, nil
4444
case clouddetect.IBM:
4545
return &ibm{conn}, nil
46+
case clouddetect.HETZNER:
47+
return &hetznerCloud{conn}, nil
4648
default:
4749
return &none{}, nil
4850
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package cloud
5+
6+
import (
7+
"errors"
8+
9+
"github.com/rs/zerolog/log"
10+
"go.mondoo.com/mql/v13/providers/os/connection/shared"
11+
"go.mondoo.com/mql/v13/providers/os/id/hetznercloud"
12+
)
13+
14+
const HETZNER Provider = "Hetzner"
15+
16+
// hetznerCloud implements the OSCloud interface for Hetzner Cloud
17+
type hetznerCloud struct {
18+
conn shared.Connection
19+
}
20+
21+
func (h *hetznerCloud) Provider() Provider {
22+
return HETZNER
23+
}
24+
25+
func (h *hetznerCloud) Instance() (*InstanceMetadata, error) {
26+
mdsvc, err := hetznercloud.Resolve(h.conn, h.conn.Asset().GetPlatform())
27+
if err != nil {
28+
log.Debug().Err(err).Msg("os.cloud.hetzner> failed to get metadata resolver")
29+
return nil, err
30+
}
31+
metadata, err := mdsvc.RawMetadata()
32+
if err != nil {
33+
log.Debug().Err(err).Msg("os.cloud.hetzner> failed to get raw metadata")
34+
return nil, err
35+
}
36+
if metadata == nil {
37+
log.Debug().Msg("os.cloud.hetzner> no metadata found")
38+
return nil, errors.New("no metadata")
39+
}
40+
41+
instanceMd := InstanceMetadata{Metadata: metadata}
42+
43+
m, ok := metadata.(map[string]any)
44+
if !ok {
45+
return &instanceMd, errors.New("unexpected raw metadata")
46+
}
47+
48+
if value, ok := m["hostname"]; ok {
49+
if hostname, ok := value.(string); ok {
50+
instanceMd.PrivateHostname = hostname
51+
}
52+
}
53+
54+
if value, ok := m["local-ipv4"]; ok {
55+
if localIP, ok := value.(string); ok && localIP != "" {
56+
instanceMd.PrivateIpv4 = []Ipv4Address{{IP: localIP}}
57+
}
58+
}
59+
60+
return &instanceMd, nil
61+
}

0 commit comments

Comments
 (0)