Skip to content

Commit 334db54

Browse files
committed
add CloudInit settings source
- add cloudinit 'userData' settings as a possible source for agent settings information. This is intended as an alternative to CDRom settings for vSphere stemcells, and should fall back to the CDRom method in case the CPI is older and doesn't set the userData.
1 parent b4c66f4 commit 334db54

File tree

3 files changed

+236
-0
lines changed

3 files changed

+236
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package infrastructure
2+
3+
import (
4+
"bytes"
5+
"compress/gzip"
6+
"encoding/base64"
7+
"encoding/json"
8+
"io"
9+
10+
bosherr "github.com/cloudfoundry/bosh-utils/errors"
11+
boshlog "github.com/cloudfoundry/bosh-utils/logger"
12+
boshsys "github.com/cloudfoundry/bosh-utils/system"
13+
14+
boshplatform "github.com/cloudfoundry/bosh-agent/v2/platform"
15+
boshsettings "github.com/cloudfoundry/bosh-agent/v2/settings"
16+
)
17+
18+
type CloudInitSettingsSource struct {
19+
platform boshplatform.Platform
20+
cmdRunner boshsys.CmdRunner
21+
22+
logTag string
23+
logger boshlog.Logger
24+
}
25+
26+
func NewCloudInitSettingsSource(
27+
platform boshplatform.Platform,
28+
logger boshlog.Logger,
29+
) *CloudInitSettingsSource {
30+
return &CloudInitSettingsSource{
31+
platform: platform,
32+
cmdRunner: platform.GetRunner(),
33+
logTag: "CloudInitSettingsSource",
34+
logger: logger,
35+
}
36+
}
37+
38+
func (s CloudInitSettingsSource) PublicSSHKeyForUsername(string) (string, error) {
39+
return "", nil
40+
}
41+
42+
func (s *CloudInitSettingsSource) Settings() (boshsettings.Settings, error) {
43+
var settings boshsettings.Settings
44+
45+
// Try to get settings data from vmwware-rpctool first, then fallback to vmtoolsd
46+
stdout, _, exitStatus, err := s.cmdRunner.RunCommand("vmware-rpctool", "info-get guestinfo.userdata")
47+
if err != nil || exitStatus != 0 {
48+
stdout, _, exitStatus, err = s.cmdRunner.RunCommand("vmtoolsd", "--cmd", "info-get guestinfo.userdata")
49+
if err != nil || exitStatus != 0 {
50+
return boshsettings.Settings{}, bosherr.WrapError(err, "getting user data from vmware tools")
51+
}
52+
}
53+
54+
decodedBytes, err := base64.StdEncoding.DecodeString(stdout)
55+
if err != nil {
56+
return boshsettings.Settings{}, bosherr.WrapError(err, "decoding user data")
57+
}
58+
59+
// unzip the data, if it is gzipped
60+
if bytes.HasPrefix(decodedBytes, []byte{0x1f, 0x8b}) {
61+
gzReader, err := gzip.NewReader(bytes.NewReader(decodedBytes))
62+
if err != nil {
63+
return boshsettings.Settings{}, bosherr.WrapError(err, "unzipping user data")
64+
}
65+
//nolint:errcheck
66+
defer gzReader.Close()
67+
68+
decodedBytes, err = io.ReadAll(gzReader)
69+
if err != nil {
70+
return boshsettings.Settings{}, bosherr.WrapError(err, "unzipping user data")
71+
}
72+
}
73+
74+
err = json.Unmarshal(decodedBytes, &settings)
75+
if err != nil {
76+
return settings, bosherr.WrapErrorf(
77+
err, "Parsing settings from vmware tools")
78+
}
79+
80+
_, _, _, err = s.cmdRunner.RunCommand("vmware-rpctool", "info-set guestinfo.userdata ---")
81+
if err != nil {
82+
//nolint:errcheck
83+
s.cmdRunner.RunCommand("vmtoolsd", "--cmd", "info-set guestinfo.userdata ---")
84+
}
85+
_, _, _, err = s.cmdRunner.RunCommand("vmware-rpctool", "info-set guestinfo.userdata.encoding ")
86+
if err != nil {
87+
//nolint:errcheck
88+
s.cmdRunner.RunCommand("vmtoolsd", "--cmd", "info-set guestinfo.userdata.encoding ")
89+
}
90+
91+
return settings, nil
92+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package infrastructure_test
2+
3+
import (
4+
"bytes"
5+
"compress/gzip"
6+
"encoding/base64"
7+
"encoding/json"
8+
"errors"
9+
10+
. "github.com/onsi/ginkgo/v2"
11+
. "github.com/onsi/gomega"
12+
13+
"github.com/cloudfoundry/bosh-utils/logger"
14+
"github.com/cloudfoundry/bosh-utils/system/fakes"
15+
16+
. "github.com/cloudfoundry/bosh-agent/v2/infrastructure"
17+
"github.com/cloudfoundry/bosh-agent/v2/platform/platformfakes"
18+
boshsettings "github.com/cloudfoundry/bosh-agent/v2/settings"
19+
)
20+
21+
var _ = Describe("CloudInitSettingsSource", func() {
22+
var (
23+
platform *platformfakes.FakePlatform
24+
cmdRunner *fakes.FakeCmdRunner
25+
source *CloudInitSettingsSource
26+
settings boshsettings.Settings
27+
encodedSettings string
28+
)
29+
30+
BeforeEach(func() {
31+
platform = &platformfakes.FakePlatform{}
32+
cmdRunner = fakes.NewFakeCmdRunner()
33+
platform.GetRunnerReturns(cmdRunner)
34+
logger := logger.NewLogger(logger.LevelNone)
35+
source = NewCloudInitSettingsSource(platform, logger)
36+
settings = boshsettings.Settings{AgentID: "123"}
37+
settingsBytes, err := json.Marshal(settings)
38+
Expect(err).ToNot(HaveOccurred())
39+
encodedSettings = base64.StdEncoding.EncodeToString(settingsBytes)
40+
})
41+
42+
Describe("Settings", func() {
43+
It("returns settings from vmware-rpctool", func() {
44+
cmdRunner.AddCmdResult("vmware-rpctool info-get guestinfo.userdata", fakes.FakeCmdResult{Stdout: encodedSettings})
45+
46+
settings, err := source.Settings()
47+
Expect(err).ToNot(HaveOccurred())
48+
Expect(settings.AgentID).To(Equal("123"))
49+
})
50+
51+
It("returns gzipped settings from vmware-rpctool", func() {
52+
var b bytes.Buffer
53+
w := gzip.NewWriter(&b)
54+
//nolint:errcheck
55+
defer w.Close()
56+
57+
settingsBytes, err := json.Marshal(settings)
58+
Expect(err).ToNot(HaveOccurred())
59+
_, err = w.Write(settingsBytes)
60+
Expect(err).ToNot(HaveOccurred())
61+
err = w.Close()
62+
Expect(err).ToNot(HaveOccurred())
63+
64+
gzippedEncodedSettings := base64.StdEncoding.EncodeToString(b.Bytes())
65+
66+
cmdRunner.AddCmdResult("vmware-rpctool info-get guestinfo.userdata", fakes.FakeCmdResult{Stdout: gzippedEncodedSettings})
67+
68+
settings, err := source.Settings()
69+
Expect(err).ToNot(HaveOccurred())
70+
Expect(settings.AgentID).To(Equal("123"))
71+
})
72+
73+
It("returns settings from vmtoolsd when vmware-rpctool fails", func() {
74+
cmdRunner.AddCmdResult("vmware-rpctool info-get guestinfo.userdata", fakes.FakeCmdResult{Error: errors.New("fail"), ExitStatus: 1})
75+
cmdRunner.AddCmdResult("vmtoolsd --cmd info-get guestinfo.userdata", fakes.FakeCmdResult{Stdout: encodedSettings})
76+
cmdRunner.AddCmdResult("vmware-rpctool info-set guestinfo.userdata ---", fakes.FakeCmdResult{Error: errors.New("fail"), ExitStatus: 1})
77+
cmdRunner.AddCmdResult("vmware-rpctool info-set guestinfo.userdata.encoding ", fakes.FakeCmdResult{Error: errors.New("fail"), ExitStatus: 1})
78+
79+
settings, err := source.Settings()
80+
Expect(err).ToNot(HaveOccurred())
81+
Expect(settings.AgentID).To(Equal("123"))
82+
Expect(cmdRunner.RunCommands).To(HaveLen(6))
83+
Expect(cmdRunner.RunCommands[0]).To(Equal([]string{"vmware-rpctool", "info-get guestinfo.userdata"}))
84+
Expect(cmdRunner.RunCommands[1]).To(Equal([]string{"vmtoolsd", "--cmd", "info-get guestinfo.userdata"}))
85+
Expect(cmdRunner.RunCommands[2]).To(Equal([]string{"vmware-rpctool", "info-set guestinfo.userdata ---"}))
86+
Expect(cmdRunner.RunCommands[3]).To(Equal([]string{"vmtoolsd", "--cmd", "info-set guestinfo.userdata ---"}))
87+
Expect(cmdRunner.RunCommands[4]).To(Equal([]string{"vmware-rpctool", "info-set guestinfo.userdata.encoding "}))
88+
Expect(cmdRunner.RunCommands[5]).To(Equal([]string{"vmtoolsd", "--cmd", "info-set guestinfo.userdata.encoding "}))
89+
})
90+
91+
It("returns an error if both tools fail", func() {
92+
cmdRunner.AddCmdResult("vmware-rpctool info-get guestinfo.userdata", fakes.FakeCmdResult{Error: errors.New("fail"), ExitStatus: 1})
93+
cmdRunner.AddCmdResult("vmtoolsd --cmd info-get guestinfo.userdata", fakes.FakeCmdResult{Error: errors.New("fail"), ExitStatus: 1})
94+
95+
_, err := source.Settings()
96+
Expect(err).To(HaveOccurred())
97+
Expect(err.Error()).To(ContainSubstring("getting user data from vmware tools"))
98+
})
99+
100+
It("returns an error if decoding fails", func() {
101+
cmdRunner.AddCmdResult("vmware-rpctool info-get guestinfo.userdata", fakes.FakeCmdResult{Stdout: "not-base64"})
102+
103+
_, err := source.Settings()
104+
Expect(err).To(HaveOccurred())
105+
Expect(err.Error()).To(ContainSubstring("decoding user data"))
106+
})
107+
108+
It("returns an error if unmarshalling fails", func() {
109+
encoded := base64.StdEncoding.EncodeToString([]byte("not-json"))
110+
cmdRunner.AddCmdResult("vmware-rpctool info-get guestinfo.userdata", fakes.FakeCmdResult{Stdout: encoded})
111+
112+
_, err := source.Settings()
113+
Expect(err).To(HaveOccurred())
114+
Expect(err.Error()).To(ContainSubstring("Parsing settings from vmware tools"))
115+
})
116+
117+
It("clears the guestinfo.userdata after reading it", func() {
118+
cmdRunner.AddCmdResult("vmware-rpctool info-get guestinfo.userdata", fakes.FakeCmdResult{Stdout: encodedSettings})
119+
cmdRunner.AddCmdResult("vmware-rpctool info-set guestinfo.userdata ---", fakes.FakeCmdResult{Stdout: ""})
120+
cmdRunner.AddCmdResult("vmware-rpctool info-set guestinfo.userdata.encoding ", fakes.FakeCmdResult{Stdout: ""})
121+
122+
_, err := source.Settings()
123+
Expect(err).ToNot(HaveOccurred())
124+
125+
Expect(cmdRunner.RunCommands).To(HaveLen(3))
126+
Expect(cmdRunner.RunCommands[1]).To(Equal([]string{"vmware-rpctool", "info-set guestinfo.userdata ---"}))
127+
Expect(cmdRunner.RunCommands[2]).To(Equal([]string{"vmware-rpctool", "info-set guestinfo.userdata.encoding "}))
128+
})
129+
})
130+
})

infrastructure/settings_source_factory.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ type CDROMSourceOptions struct {
6464

6565
func (o CDROMSourceOptions) sourceOptionsInterface() {}
6666

67+
type CloudInitSourceOptions struct{}
68+
69+
func (o CloudInitSourceOptions) sourceOptionsInterface() {}
70+
6771
type InstanceMetadataSourceOptions struct {
6872
URI string
6973
Headers map[string]string
@@ -135,6 +139,12 @@ func (f SettingsSourceFactory) buildWithoutRegistry() (boshsettings.Source, erro
135139
f.logger,
136140
)
137141

142+
case CloudInitSourceOptions:
143+
settingsSource = NewCloudInitSettingsSource(
144+
f.platform,
145+
f.logger,
146+
)
147+
138148
case InstanceMetadataSourceOptions:
139149
settingsSource = NewInstanceMetadataSettingsSource(
140150
typedOpts.URI,
@@ -185,6 +195,10 @@ func (s *SourceOptionsSlice) UnmarshalJSON(data []byte) error {
185195
var o CDROMSourceOptions
186196
err, opts = mapstruc.Decode(m, &o), o
187197

198+
case optType == "CloudInit":
199+
var o CloudInitSourceOptions
200+
err, opts = mapstruc.Decode(m, &o), o
201+
188202
default:
189203
err = bosherr.Errorf("Unknown source type '%s'", optType)
190204
}

0 commit comments

Comments
 (0)