Skip to content

Commit a8ff1b1

Browse files
authored
fix: parsing vApp properties while converting from OVF envelope to VM ConfigSpec(#3964)
Signed-off-by: Hemanth kumar Pannem <hemanth-kumar.pannem@broadcom.com>
1 parent e205430 commit a8ff1b1

File tree

5 files changed

+728
-1
lines changed

5 files changed

+728
-1
lines changed

ovf/configspec.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,17 @@ func (e Envelope) toVAppConfig(
10741074
if p.UserConfigurable == nil {
10751075
p.UserConfigurable = types.NewBool(false)
10761076
}
1077+
// Parse the value using the vApp config parser.
1078+
// Empty default values (including whitespace-only) are allowed.
1079+
value = strings.TrimSpace(value)
1080+
parsedValue := value
1081+
if value != "" {
1082+
var err error
1083+
parsedValue, err = parseVAppConfigValue(p, value)
1084+
if err != nil {
1085+
return err
1086+
}
1087+
}
10771088
np := types.VAppPropertySpec{
10781089
ArrayUpdateSpec: types.ArrayUpdateSpec{
10791090
Operation: types.ArrayUpdateOperationAdd,
@@ -1087,7 +1098,7 @@ func (e Envelope) toVAppConfig(
10871098
Label: deref(p.Label),
10881099
Type: p.Type,
10891100
UserConfigurable: p.UserConfigurable,
1090-
DefaultValue: value,
1101+
DefaultValue: parsedValue,
10911102
Value: "",
10921103
Description: deref(p.Description),
10931104
},

ovf/configspec_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
1213

1314
"github.com/vmware/govmomi/vim25/types"
1415
)
@@ -154,6 +155,45 @@ func TestEnvelopeToConfigSpec(t *testing.T) {
154155
})
155156
})
156157

158+
t.Run("vApp property type parsing", func(t *testing.T) {
159+
e := testEnvelope(t, "fixtures/properties.ovf")
160+
cs, err := e.ToConfigSpec()
161+
require.NoError(t, err)
162+
163+
va, ok := cs.VAppConfig.(*types.VAppConfigSpec)
164+
require.True(t, ok)
165+
166+
findProp := func(id string) *types.VAppPropertyInfo {
167+
for i := range va.Property {
168+
if va.Property[i].Info.Id == id {
169+
return va.Property[i].Info
170+
}
171+
}
172+
return nil
173+
}
174+
175+
t.Run("boolean default value is normalised to canonical form", func(t *testing.T) {
176+
p := findProp("enable_ssh")
177+
require.NotNil(t, p, "enable_ssh property not found in VAppConfigSpec")
178+
// OVF envelope has ovf:value="false" (lowercase); vSphere API requires "False".
179+
assert.Equal(t, "False", p.DefaultValue)
180+
})
181+
182+
t.Run("empty default value is preserved as empty string", func(t *testing.T) {
183+
p := findProp("ntp-server")
184+
require.NotNil(t, p, "ntp-server property not found in VAppConfigSpec")
185+
// OVF envelope has no ovf:value; empty default must pass through without error.
186+
assert.Equal(t, "", p.DefaultValue)
187+
})
188+
189+
t.Run("whitespace-padded default value is trimmed", func(t *testing.T) {
190+
p := findProp("whitespace_int")
191+
require.NotNil(t, p, "whitespace_int property not found in VAppConfigSpec")
192+
// OVF envelope has ovf:value=" 42 "; trimmed value must be stored.
193+
assert.Equal(t, "42", p.DefaultValue)
194+
})
195+
})
196+
157197
t.Run("Photon 5", func(t *testing.T) {
158198
e := testEnvelope(t, "fixtures/photon5.ovf")
159199
cs, err := e.ToConfigSpec()

ovf/fixtures/properties.ovf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@
150150
<Label>NFS mount for transfer file location</Label>
151151
<Description>Ex: 10.0.0.1:/transfer</Description>
152152
</Property>
153+
<Property ovf:key="whitespace_int" ovf:type="int" ovf:userConfigurable="false" ovf:value=" 42 ">
154+
<Label>Whitespace-padded int</Label>
155+
</Property>
153156
</ProductSection>
154157
<ProductSection ovf:class="vm" ovf:required="false">
155158
<Info>VM specific properties</Info>

ovf/vappconfig_parser.go

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
// © Broadcom. All Rights Reserved.
2+
// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package ovf
6+
7+
import (
8+
"fmt"
9+
"net"
10+
"regexp"
11+
"strconv"
12+
"strings"
13+
)
14+
15+
const maxVAppPropStringLen = 65535
16+
17+
var (
18+
strWithMinLenRx = regexp.MustCompile(`^(?:string|password)\(\.\.(\d+)\)$`)
19+
strWithMaxLenRx = regexp.MustCompile(`^(?:string|password)\((\d+)\.\.\)$`)
20+
strWithMinMaxLenRx = regexp.MustCompile(`^(?:string|password)\((\d+)\.\.(\d+)\)$`)
21+
intWithMinMaxSizeRx = regexp.MustCompile(`^int\(([+-]?\d+)\.\.([+-]?\d+)\)$`)
22+
realWithMinMaxSizeRx = regexp.MustCompile(`^real\(([+-]?(?:\d+(?:\.\d*)?|\.\d+))\.\.([+-]?(?:\d+(?:\.\d*)?|\.\d+))\)$`)
23+
)
24+
25+
func parseVAppConfigValue(p Property, val string) (string, error) {
26+
val = strings.TrimSpace(val)
27+
parsedValue := ""
28+
29+
switch p.Type {
30+
case "string", "password":
31+
//
32+
// A generic string. Max length 65535 (64k).
33+
//
34+
if l := len(val); l > maxVAppPropStringLen {
35+
return "", newParseLenErr(p, l, 0, maxVAppPropStringLen)
36+
}
37+
parsedValue = val
38+
39+
case "boolean":
40+
//
41+
// A boolean. The value can be "True" or "False".
42+
//
43+
if ok, _ := strconv.ParseBool(val); ok {
44+
parsedValue = "True"
45+
} else {
46+
parsedValue = "False"
47+
}
48+
49+
case "int":
50+
//
51+
// An integer value. Is semantically equivalent to
52+
// int(-2147483648..2147483647) e.g. signed int32.
53+
//
54+
if _, err := strconv.ParseInt(val, 10, 32); err != nil {
55+
return "", newParseErr(p, val)
56+
}
57+
parsedValue = val
58+
59+
case "real":
60+
//
61+
// An IEEE 8-byte floating-point value, i.e. a float64.
62+
//
63+
if _, err := strconv.ParseFloat(val, 64); err != nil {
64+
return "", newParseErr(p, val)
65+
}
66+
parsedValue = val
67+
68+
case "ip":
69+
//
70+
// An IPv4 address in dot-decimal notation or an IPv6 address in
71+
// colon-hexadecimal notation.
72+
//
73+
if v, _, err := parseIP(val); v == nil || err != nil {
74+
return "", newParseErr(p, val)
75+
}
76+
parsedValue = val
77+
78+
case "ip:network":
79+
//
80+
// An IP address in dot-notation (IPv4) and colon-hexadecimal (IPv6)
81+
// on a particular network. The behavior of this type depends on the
82+
// ipAllocationPolicy.
83+
//
84+
85+
// TODO(akutz) Figure out the correct parsing strategy.
86+
parsedValue = val
87+
88+
case "expression":
89+
//
90+
// The default value specifies an expression that is calculated
91+
// by the system.
92+
//
93+
94+
// TODO(akutz) Figure out the correct parsing strategy.
95+
parsedValue = val
96+
97+
default:
98+
if m := strWithMinLenRx.FindStringSubmatch(p.Type); len(m) > 0 {
99+
//
100+
// A string with minimum character length x.
101+
//
102+
minLen, _ := strconv.Atoi(m[1])
103+
if l := len(val); l < minLen {
104+
return "", newParseLenErr(p, l, minLen, maxVAppPropStringLen)
105+
}
106+
parsedValue = val
107+
108+
} else if m := strWithMaxLenRx.FindStringSubmatch(p.Type); len(m) > 0 {
109+
//
110+
// A string with maximum character length x.
111+
//
112+
maxLen, _ := strconv.Atoi(m[1])
113+
if l := len(val); l > maxLen {
114+
return "", newParseLenErr(p, l, 0, maxLen)
115+
}
116+
parsedValue = val
117+
118+
} else if m := strWithMinMaxLenRx.FindStringSubmatch(p.Type); len(m) > 0 {
119+
//
120+
// A string with minimum character length x and maximum
121+
// character length y.
122+
//
123+
minLen, _ := strconv.Atoi(m[1])
124+
maxLen, _ := strconv.Atoi(m[2])
125+
126+
if minLen > maxLen {
127+
return "", newParseMinMaxErr(p, int64(minLen), int64(maxLen))
128+
}
129+
130+
if l := len(val); l < minLen || l > maxLen {
131+
return "", newParseLenErr(p, l, minLen, maxLen)
132+
}
133+
parsedValue = val
134+
135+
} else if m := intWithMinMaxSizeRx.FindStringSubmatch(p.Type); len(m) > 0 {
136+
//
137+
// An integer value with a minimum size x and a maximum size y.
138+
// For example int(0..255) is a number between 0 and 255 both
139+
// included. This is also a way to specify that the number must
140+
// be a uint8. There is always a lower and lower bound. Max
141+
// number of digits is 100 including any sign. If exported to
142+
// OVF the value will be truncated to max of uint64 or int64.
143+
//
144+
minSize, _ := strconv.ParseInt(m[1], 10, 64)
145+
maxSize, _ := strconv.ParseInt(m[2], 10, 64)
146+
147+
if minSize > maxSize {
148+
return "", newParseMinMaxErr(p, minSize, maxSize)
149+
}
150+
151+
if minSize >= 0 {
152+
v, err := strconv.ParseUint(val, 10, 64)
153+
if err != nil {
154+
return "", newParseErr(p, val)
155+
}
156+
umin, umax := uint64(minSize), uint64(maxSize) //nolint:gosec
157+
if v < umin || v > umax {
158+
return "", newParseUintSizeErr(p, v, umin, umax)
159+
}
160+
} else {
161+
v, err := strconv.ParseInt(val, 10, 64)
162+
if err != nil {
163+
return "", newParseErr(p, val)
164+
}
165+
if v < minSize || v > maxSize {
166+
return "", newParseIntSizeErr(p, v, minSize, maxSize)
167+
}
168+
}
169+
170+
parsedValue = val
171+
172+
} else if m := realWithMinMaxSizeRx.FindStringSubmatch(p.Type); len(m) > 0 {
173+
//
174+
// An IEEE 8-byte floating-point value with a minimum size x and
175+
// a maximum size y. For example real(-1.5..1.5) must be a
176+
// number between -1.5 and 1.5. Because of the nature of float
177+
// some conversions can truncate the value. Real must be encoded
178+
// according to CIM.
179+
//
180+
minSize, _ := strconv.ParseFloat(m[1], 64)
181+
maxSize, _ := strconv.ParseFloat(m[2], 64)
182+
183+
if minSize > maxSize {
184+
return "", newParseRealMinMaxErr(p, minSize, maxSize)
185+
}
186+
187+
v, err := strconv.ParseFloat(val, 64)
188+
if err != nil {
189+
return "", newParseErr(p, val)
190+
}
191+
if v < minSize || v > maxSize {
192+
return "", newParseRealSizeErr(p, v, minSize, maxSize)
193+
}
194+
195+
parsedValue = val
196+
197+
} else {
198+
parsedValue = val
199+
}
200+
}
201+
202+
return parsedValue, nil
203+
}
204+
205+
// parseIP returns the parsed IP address and optional network. Please note, this
206+
// function supports parsing IP addresses with or without the network length.
207+
func parseIP(s string) (net.IP, *net.IPNet, error) {
208+
if strings.Contains(s, "/") {
209+
return net.ParseCIDR(s)
210+
}
211+
ip := net.ParseIP(s)
212+
return ip, nil, nil
213+
}
214+
215+
func newParseErr(p Property, val string) error {
216+
if strings.HasPrefix(p.Type, "password") {
217+
return fmt.Errorf(
218+
"failed to parse prop=%q, type=%s",
219+
p.Key,
220+
p.Type)
221+
}
222+
return fmt.Errorf(
223+
"failed to parse prop=%q, type=%s, value=%v",
224+
p.Key,
225+
p.Type,
226+
val)
227+
}
228+
229+
func newParseLenErr(p Property, actLen, minLen, maxLen int) error {
230+
return fmt.Errorf(
231+
"failed to parse prop=%q, type=%s due to length: "+
232+
"len=%d, min=%d, max=%d",
233+
p.Key,
234+
p.Type,
235+
actLen,
236+
minLen,
237+
maxLen)
238+
}
239+
240+
func newParseIntSizeErr(p Property, val, minSize, maxSize int64) error {
241+
return fmt.Errorf(
242+
"failed to parse prop=%q, type=%s due to size: "+
243+
"val=%d, min=%d, max=%d",
244+
p.Key,
245+
p.Type,
246+
val,
247+
minSize,
248+
maxSize)
249+
}
250+
251+
func newParseUintSizeErr(p Property, val, minSize, maxSize uint64) error {
252+
return fmt.Errorf(
253+
"failed to parse prop=%q, type=%s due to size: "+
254+
"val=%d, min=%d, max=%d",
255+
p.Key,
256+
p.Type,
257+
val,
258+
minSize,
259+
maxSize)
260+
}
261+
262+
func newParseRealSizeErr(p Property, val, minSize, maxSize float64) error {
263+
return fmt.Errorf(
264+
"failed to parse prop=%q, type=%s due to size: "+
265+
"val=%f, min=%f, max=%f",
266+
p.Key,
267+
p.Type,
268+
val,
269+
minSize,
270+
maxSize)
271+
}
272+
273+
func newParseMinMaxErr(p Property, minSize, maxSize int64) error {
274+
return fmt.Errorf(
275+
"failed to parse prop=%q, type=%s due to min=%d > max=%d",
276+
p.Key,
277+
p.Type,
278+
minSize,
279+
maxSize)
280+
}
281+
282+
func newParseRealMinMaxErr(p Property, minSize, maxSize float64) error {
283+
return fmt.Errorf(
284+
"failed to parse prop=%q, type=%s due to min=%f > max=%f",
285+
p.Key,
286+
p.Type,
287+
minSize,
288+
maxSize)
289+
}

0 commit comments

Comments
 (0)