Skip to content

Commit 501924e

Browse files
mcanevetsmira
authored andcommitted
fix(machined): use ParseFQDN for hostname parsing in OpenNebula
Two bugs are fixed: 1. DNS_HOSTNAME was wrongly used as Domainname. DNS_HOSTNAME is a boolean flag (YES/NO) that tells the OpenNebula daemon to perform a reverse DNS lookup; it is not a domain name string. Using it as Domainname produced invalid FQDNs like "myhost.YES". 2. No FQDN splitting: if the hostname source contained a dot (e.g. NAME="myhost.example.com"), the full string was used as Hostname instead of splitting on the first dot. Both bugs are fixed by switching to ParseFQDN(), consistent with how all other Talos platform implementations handle hostname parsing. Signed-off-by: Mickaël Canévet <mickael.canevet@proton.ch> Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com> (cherry picked from commit ae61f5a)
1 parent e9331b2 commit 501924e

File tree

2 files changed

+200
-11
lines changed

2 files changed

+200
-11
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package opennebula_test
6+
7+
import (
8+
"testing"
9+
10+
"github.com/cosi-project/runtime/pkg/state"
11+
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
12+
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula"
17+
)
18+
19+
// minimalContext returns the minimum context bytes needed to exercise hostname
20+
// parsing without triggering ETH* processing.
21+
func minimalContext(vars string) []byte {
22+
return []byte("ETH0_MAC = \"02:00:c0:a8:01:5c\"\nETH0_IP = \"10.0.0.1\"\nETH0_MASK = \"255.255.255.0\"\n" + vars)
23+
}
24+
25+
func TestSanitizeHostname(t *testing.T) {
26+
t.Parallel()
27+
28+
o := &opennebula.OpenNebula{}
29+
st := state.WrapCore(namespaced.NewState(inmem.Build))
30+
31+
for _, tc := range []struct {
32+
name string
33+
nameVar string
34+
wantHostname string
35+
wantDomainname string
36+
}{
37+
{
38+
name: "clean hostname passes through unchanged",
39+
nameVar: "myhost",
40+
wantHostname: "myhost",
41+
wantDomainname: "",
42+
},
43+
{
44+
name: "FQDN is split on first dot",
45+
nameVar: "myhost.example.com",
46+
wantHostname: "myhost",
47+
wantDomainname: "example.com",
48+
},
49+
{
50+
name: "invalid chars replaced with hyphen",
51+
nameVar: "my_host",
52+
wantHostname: "my-host",
53+
wantDomainname: "",
54+
},
55+
{
56+
name: "leading and trailing hyphens stripped",
57+
nameVar: "-myhost-",
58+
wantHostname: "myhost",
59+
wantDomainname: "",
60+
},
61+
{
62+
name: "per-label hyphen trimming",
63+
nameVar: "my-.host",
64+
wantHostname: "my",
65+
wantDomainname: "host",
66+
},
67+
} {
68+
t.Run(tc.name, func(t *testing.T) {
69+
t.Parallel()
70+
71+
ctx := minimalContext("NAME = \"" + tc.nameVar + "\"")
72+
73+
networkConfig, err := o.ParseMetadata(st, ctx)
74+
require.NoError(t, err)
75+
require.Len(t, networkConfig.Hostnames, 1)
76+
77+
assert.Equal(t, tc.wantHostname, networkConfig.Hostnames[0].Hostname)
78+
assert.Equal(t, tc.wantDomainname, networkConfig.Hostnames[0].Domainname)
79+
})
80+
}
81+
82+
t.Run("empty string produces no hostname entry", func(t *testing.T) {
83+
t.Parallel()
84+
85+
ctx := minimalContext("NAME = \"\"")
86+
87+
networkConfig, err := o.ParseMetadata(st, ctx)
88+
require.NoError(t, err)
89+
assert.Empty(t, networkConfig.Hostnames)
90+
})
91+
}
92+
93+
func TestParseMetadataHostname(t *testing.T) {
94+
t.Parallel()
95+
96+
o := &opennebula.OpenNebula{}
97+
st := state.WrapCore(namespaced.NewState(inmem.Build))
98+
99+
for _, tc := range []struct {
100+
name string
101+
vars string
102+
wantHostname string
103+
wantDomainname string
104+
}{
105+
{
106+
name: "HOSTNAME takes priority",
107+
vars: "HOSTNAME = \"fromhostname\"\nSET_HOSTNAME = \"fromsethostname\"\nNAME = \"fromname\"",
108+
wantHostname: "fromhostname",
109+
wantDomainname: "",
110+
},
111+
{
112+
name: "falls back to SET_HOSTNAME when HOSTNAME is empty",
113+
vars: "SET_HOSTNAME = \"fromsethostname\"\nNAME = \"fromname\"",
114+
wantHostname: "fromsethostname",
115+
wantDomainname: "",
116+
},
117+
{
118+
name: "falls back to NAME when both HOSTNAME and SET_HOSTNAME are empty",
119+
vars: "NAME = \"fromname\"",
120+
wantHostname: "fromname",
121+
wantDomainname: "",
122+
},
123+
{
124+
name: "DNS_HOSTNAME=YES is not used as Domainname",
125+
vars: "NAME = \"myhost\"\nDNS_HOSTNAME = \"YES\"",
126+
wantHostname: "myhost",
127+
wantDomainname: "",
128+
},
129+
{
130+
name: "FQDN in NAME is split into Hostname and Domainname",
131+
vars: "NAME = \"myhost.example.com\"",
132+
wantHostname: "myhost",
133+
wantDomainname: "example.com",
134+
},
135+
} {
136+
t.Run(tc.name, func(t *testing.T) {
137+
t.Parallel()
138+
139+
ctx := minimalContext(tc.vars)
140+
141+
networkConfig, err := o.ParseMetadata(st, ctx)
142+
require.NoError(t, err)
143+
require.Len(t, networkConfig.Hostnames, 1)
144+
145+
assert.Equal(t, tc.wantHostname, networkConfig.Hostnames[0].Hostname)
146+
assert.Equal(t, tc.wantDomainname, networkConfig.Hostnames[0].Domainname)
147+
})
148+
}
149+
}

internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula/opennebula.go

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,39 @@ func parseAliases(oneContext map[string]string, ifaceName, ifaceNameLower string
117117
return addrs, nil
118118
}
119119

120+
// sanitizeHostname replaces characters invalid in DNS labels with hyphens,
121+
// strips leading/trailing hyphens from the whole string and from each label.
122+
// This mirrors the reference sanitization in one-apps/context-linux:
123+
//
124+
// sed -e 's/[^-a-zA-Z0-9\.]/-/g' -e 's/^-*//g' -e 's/-*$//g'
125+
//
126+
// Talos is intentionally stricter: it also trims hyphens per-label so every
127+
// label is RFC-1123-valid (no label may start or end with a hyphen).
128+
func sanitizeHostname(raw string) string {
129+
var b strings.Builder
130+
131+
for _, r := range raw {
132+
switch {
133+
case r >= 'a' && r <= 'z',
134+
r >= 'A' && r <= 'Z',
135+
r >= '0' && r <= '9',
136+
r == '-', r == '.':
137+
b.WriteRune(r)
138+
default:
139+
b.WriteRune('-')
140+
}
141+
}
142+
143+
s := strings.Trim(b.String(), "-")
144+
145+
labels := strings.Split(s, ".")
146+
for i, l := range labels {
147+
labels[i] = strings.Trim(l, "-")
148+
}
149+
150+
return strings.Join(labels, ".")
151+
}
152+
120153
// parseRouteFields extracts the destination prefix, gateway string, and optional
121154
// metric string from the fields of a single route entry.
122155
//
@@ -247,6 +280,9 @@ func (o *OpenNebula) ParseMetadata(st state.State, oneContextPlain []byte) (*run
247280
}
248281

249282
// Create HostnameSpecSpec entry
283+
// HOSTNAME is checked first (deviation from the reference which tries
284+
// SET_HOSTNAME before HOSTNAME) to preserve backward compatibility with
285+
// existing Talos deployments that rely on the OpenNebula-injected FQDN.
250286
hostnameValue := oneContext["HOSTNAME"]
251287
if hostnameValue == "" {
252288
hostnameValue = oneContext["SET_HOSTNAME"]
@@ -255,6 +291,8 @@ func (o *OpenNebula) ParseMetadata(st state.State, oneContextPlain []byte) (*run
255291
}
256292
}
257293

294+
hostnameValue = sanitizeHostname(hostnameValue)
295+
258296
// Seed the merged DNS/search-domain slices with global variables (DNS,
259297
// SEARCH_DOMAIN). These are applied regardless of interface, matching the
260298
// reference get_nameservers()/get_searchdomains() which processes global
@@ -434,19 +472,21 @@ func (o *OpenNebula) ParseMetadata(st state.State, oneContextPlain []byte) (*run
434472
})
435473
}
436474

437-
// Create HostnameSpecSpec entry
438-
networkConfig.Hostnames = append(networkConfig.Hostnames,
439-
network.HostnameSpecSpec{
440-
Hostname: hostnameValue,
441-
Domainname: oneContext["DNS_HOSTNAME"],
442-
ConfigLayer: network.ConfigPlatform,
443-
},
444-
)
445-
446-
// Create Metadata entry
475+
hostnameSpec := network.HostnameSpecSpec{
476+
ConfigLayer: network.ConfigPlatform,
477+
}
478+
479+
if hostnameValue != "" {
480+
if err := hostnameSpec.ParseFQDN(hostnameValue); err != nil {
481+
return nil, fmt.Errorf("failed to parse hostname: %w", err)
482+
}
483+
484+
networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec)
485+
}
486+
447487
networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{
448488
Platform: o.Name(),
449-
Hostname: hostnameValue,
489+
Hostname: hostnameSpec.Hostname,
450490
InstanceID: oneContext["VMID"],
451491
}
452492

0 commit comments

Comments
 (0)