Merge lp:~smoser/cloud-init/trunk.smartos-fabric into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Scott Moser
Status: Merged
Merge reported by: Scott Moser
Merged at revision: not available
Proposed branch: lp:~smoser/cloud-init/trunk.smartos-fabric
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 590 lines (+437/-29)
2 files modified
cloudinit/sources/DataSourceSmartOS.py (+92/-24)
tests/unittests/test_datasource/test_smartos.py (+345/-5)
To merge this branch: bzr merge lp:~smoser/cloud-init/trunk.smartos-fabric
Reviewer Review Type Date Requested Status
Scott Moser Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+300115@code.launchpad.net

Commit message

SmartOS: more improvements for network configuration

This improves smart os network configuration
 - fix the SocketClient which was previously completely broken.
 - adds support for configuring dns servers and dns search (based off the
   sdc:dns_domain).
 - support 'sdc:gateways' information from the datasource for configuring
   default routes.
 - add converted network information to output when module is run as a main

TODO: add support for sdc:routes as described at http://eng.joyent.com/mdata/datadict.html

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
1258. By Scott Moser

add knowledge of the 'sdc:routes' key (but do not use it)

1259. By Scott Moser

add test case covering not all nics have 'gateways' entry.

also for brevity import convert_smartos_network_data as convert_net.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :
Revision history for this message
Scott Moser (smoser) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/sources/DataSourceSmartOS.py'
2--- cloudinit/sources/DataSourceSmartOS.py 2016-07-13 22:18:46 +0000
3+++ cloudinit/sources/DataSourceSmartOS.py 2016-07-25 15:23:44 +0000
4@@ -60,11 +60,15 @@
5 'availability_zone': ('sdc:datacenter_name', True),
6 'vendor-data': ('sdc:vendor-data', False),
7 'operator-script': ('sdc:operator-script', False),
8+ 'hostname': ('sdc:hostname', True),
9+ 'dns_domain': ('sdc:dns_domain', True),
10 }
11
12 SMARTOS_ATTRIB_JSON = {
13 # Cloud-init Key : (SmartOS Key known JSON)
14 'network-data': 'sdc:nics',
15+ 'dns_servers': 'sdc:resolvers',
16+ 'routes': 'sdc:routes',
17 }
18
19 SMARTOS_ENV_LX_BRAND = "lx-brand"
20@@ -311,7 +315,10 @@
21 if self._network_config is None:
22 if self.network_data is not None:
23 self._network_config = (
24- convert_smartos_network_data(self.network_data))
25+ convert_smartos_network_data(
26+ network_data=self.network_data,
27+ dns_servers=self.metadata['dns_servers'],
28+ dns_domain=self.metadata['dns_domain']))
29 return self._network_config
30
31
32@@ -445,7 +452,8 @@
33
34
35 class JoyentMetadataSocketClient(JoyentMetadataClient):
36- def __init__(self, socketpath):
37+ def __init__(self, socketpath, smartos_type=SMARTOS_ENV_LX_BRAND):
38+ super(JoyentMetadataSocketClient, self).__init__(smartos_type)
39 self.socketpath = socketpath
40
41 def open_transport(self):
42@@ -461,7 +469,7 @@
43
44
45 class JoyentMetadataSerialClient(JoyentMetadataClient):
46- def __init__(self, device, timeout=10, smartos_type=None):
47+ def __init__(self, device, timeout=10, smartos_type=SMARTOS_ENV_KVM):
48 super(JoyentMetadataSerialClient, self).__init__(smartos_type)
49 self.device = device
50 self.timeout = timeout
51@@ -583,7 +591,8 @@
52 device=serial_device, timeout=serial_timeout,
53 smartos_type=smartos_type)
54 elif smartos_type == SMARTOS_ENV_LX_BRAND:
55- return JoyentMetadataSocketClient(socketpath=metadata_sockfile)
56+ return JoyentMetadataSocketClient(socketpath=metadata_sockfile,
57+ smartos_type=smartos_type)
58
59 raise ValueError("Unknown value for smartos_type: %s" % smartos_type)
60
61@@ -671,8 +680,9 @@
62 return None
63
64
65-# Covert SMARTOS 'sdc:nics' data to network_config yaml
66-def convert_smartos_network_data(network_data=None):
67+# Convert SMARTOS 'sdc:nics' data to network_config yaml
68+def convert_smartos_network_data(network_data=None,
69+ dns_servers=None, dns_domain=None):
70 """Return a dictionary of network_config by parsing provided
71 SMARTOS sdc:nics configuration data
72
73@@ -706,9 +716,7 @@
74 'broadcast',
75 'dns_nameservers',
76 'dns_search',
77- 'gateway',
78 'metric',
79- 'netmask',
80 'pointopoint',
81 'routes',
82 'scope',
83@@ -716,6 +724,29 @@
84 ],
85 }
86
87+ if dns_servers:
88+ if not isinstance(dns_servers, (list, tuple)):
89+ dns_servers = [dns_servers]
90+ else:
91+ dns_servers = []
92+
93+ if dns_domain:
94+ if not isinstance(dns_domain, (list, tuple)):
95+ dns_domain = [dns_domain]
96+ else:
97+ dns_domain = []
98+
99+ def is_valid_ipv4(addr):
100+ return '.' in addr
101+
102+ def is_valid_ipv6(addr):
103+ return ':' in addr
104+
105+ pgws = {
106+ 'ipv4': {'match': is_valid_ipv4, 'gw': None},
107+ 'ipv6': {'match': is_valid_ipv6, 'gw': None},
108+ }
109+
110 config = []
111 for nic in network_data:
112 cfg = dict((k, v) for k, v in nic.items()
113@@ -727,18 +758,40 @@
114 cfg.update({'mac_address': nic['mac']})
115
116 subnets = []
117- for ip, gw in zip(nic['ips'], nic['gateways']):
118- subnet = dict((k, v) for k, v in nic.items()
119- if k in valid_keys['subnet'])
120- subnet.update({
121- 'type': 'static',
122- 'address': ip,
123- 'gateway': gw,
124- })
125+ for ip in nic.get('ips', []):
126+ if ip == "dhcp":
127+ subnet = {'type': 'dhcp4'}
128+ else:
129+ subnet = dict((k, v) for k, v in nic.items()
130+ if k in valid_keys['subnet'])
131+ subnet.update({
132+ 'type': 'static',
133+ 'address': ip,
134+ })
135+
136+ proto = 'ipv4' if is_valid_ipv4(ip) else 'ipv6'
137+ # Only use gateways for 'primary' nics
138+ if 'primary' in nic and nic.get('primary', False):
139+ # the ips and gateways list may be N to M, here
140+ # we map the ip index into the gateways list,
141+ # and handle the case that we could have more ips
142+ # than gateways. we only consume the first gateway
143+ if not pgws[proto]['gw']:
144+ gateways = [gw for gw in nic.get('gateways', [])
145+ if pgws[proto]['match'](gw)]
146+ if len(gateways):
147+ pgws[proto]['gw'] = gateways[0]
148+ subnet.update({'gateway': pgws[proto]['gw']})
149+
150 subnets.append(subnet)
151 cfg.update({'subnets': subnets})
152 config.append(cfg)
153
154+ if dns_servers:
155+ config.append(
156+ {'type': 'nameserver', 'address': dns_servers,
157+ 'search': dns_domain})
158+
159 return {'version': 1, 'config': config}
160
161
162@@ -761,21 +814,36 @@
163 sys.exit(1)
164 if len(sys.argv) == 1:
165 keys = (list(SMARTOS_ATTRIB_JSON.keys()) +
166- list(SMARTOS_ATTRIB_MAP.keys()))
167+ list(SMARTOS_ATTRIB_MAP.keys()) + ['network_config'])
168 else:
169 keys = sys.argv[1:]
170
171- data = {}
172- for key in keys:
173+ def load_key(client, key, data):
174+ if key in data:
175+ return data[key]
176+
177 if key in SMARTOS_ATTRIB_JSON:
178 keyname = SMARTOS_ATTRIB_JSON[key]
179- data[key] = jmc.get_json(keyname)
180+ data[key] = client.get_json(keyname)
181+ elif key == "network_config":
182+ for depkey in ('network-data', 'dns_servers', 'dns_domain'):
183+ load_key(client, depkey, data)
184+ data[key] = convert_smartos_network_data(
185+ network_data=data['network-data'],
186+ dns_servers=data['dns_servers'],
187+ dns_domain=data['dns_domain'])
188 else:
189 if key in SMARTOS_ATTRIB_MAP:
190 keyname, strip = SMARTOS_ATTRIB_MAP[key]
191 else:
192 keyname, strip = (key, False)
193- val = jmc.get(keyname, strip=strip)
194- data[key] = jmc.get(keyname, strip=strip)
195-
196- print(json.dumps(data, indent=1))
197+ data[key] = client.get(keyname, strip=strip)
198+
199+ return data[key]
200+
201+ data = {}
202+ for key in keys:
203+ load_key(client=jmc, key=key, data=data)
204+
205+ print(json.dumps(data, indent=1, sort_keys=True,
206+ separators=(',', ': ')))
207
208=== modified file 'tests/unittests/test_datasource/test_smartos.py'
209--- tests/unittests/test_datasource/test_smartos.py 2016-06-10 21:22:17 +0000
210+++ tests/unittests/test_datasource/test_smartos.py 2016-07-25 15:23:44 +0000
211@@ -36,6 +36,8 @@
212
213 from cloudinit import serial
214 from cloudinit.sources import DataSourceSmartOS
215+from cloudinit.sources.DataSourceSmartOS import (
216+ convert_smartos_network_data as convert_net)
217
218 import six
219
220@@ -86,6 +88,229 @@
221 ]
222 """)
223
224+
225+SDC_NICS_ALT = json.loads("""
226+[
227+ {
228+ "interface": "net0",
229+ "mac": "90:b8:d0:ae:64:51",
230+ "vlan_id": 324,
231+ "nic_tag": "external",
232+ "gateway": "8.12.42.1",
233+ "gateways": [
234+ "8.12.42.1"
235+ ],
236+ "netmask": "255.255.255.0",
237+ "ip": "8.12.42.51",
238+ "ips": [
239+ "8.12.42.51/24"
240+ ],
241+ "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
242+ "model": "virtio",
243+ "mtu": 1500,
244+ "primary": true
245+ },
246+ {
247+ "interface": "net1",
248+ "mac": "90:b8:d0:bd:4f:9c",
249+ "vlan_id": 600,
250+ "nic_tag": "internal",
251+ "netmask": "255.255.255.0",
252+ "ip": "10.210.1.217",
253+ "ips": [
254+ "10.210.1.217/24"
255+ ],
256+ "network_uuid": "98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
257+ "model": "virtio",
258+ "mtu": 1500
259+ }
260+]
261+""")
262+
263+SDC_NICS_DHCP = json.loads("""
264+[
265+ {
266+ "interface": "net0",
267+ "mac": "90:b8:d0:ae:64:51",
268+ "vlan_id": 324,
269+ "nic_tag": "external",
270+ "gateway": "8.12.42.1",
271+ "gateways": [
272+ "8.12.42.1"
273+ ],
274+ "netmask": "255.255.255.0",
275+ "ip": "8.12.42.51",
276+ "ips": [
277+ "8.12.42.51/24"
278+ ],
279+ "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
280+ "model": "virtio",
281+ "mtu": 1500,
282+ "primary": true
283+ },
284+ {
285+ "interface": "net1",
286+ "mac": "90:b8:d0:bd:4f:9c",
287+ "vlan_id": 600,
288+ "nic_tag": "internal",
289+ "netmask": "255.255.255.0",
290+ "ip": "10.210.1.217",
291+ "ips": [
292+ "dhcp"
293+ ],
294+ "network_uuid": "98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
295+ "model": "virtio",
296+ "mtu": 1500
297+ }
298+]
299+""")
300+
301+SDC_NICS_MIP = json.loads("""
302+[
303+ {
304+ "interface": "net0",
305+ "mac": "90:b8:d0:ae:64:51",
306+ "vlan_id": 324,
307+ "nic_tag": "external",
308+ "gateway": "8.12.42.1",
309+ "gateways": [
310+ "8.12.42.1"
311+ ],
312+ "netmask": "255.255.255.0",
313+ "ip": "8.12.42.51",
314+ "ips": [
315+ "8.12.42.51/24",
316+ "8.12.42.52/24"
317+ ],
318+ "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
319+ "model": "virtio",
320+ "mtu": 1500,
321+ "primary": true
322+ },
323+ {
324+ "interface": "net1",
325+ "mac": "90:b8:d0:bd:4f:9c",
326+ "vlan_id": 600,
327+ "nic_tag": "internal",
328+ "netmask": "255.255.255.0",
329+ "ip": "10.210.1.217",
330+ "ips": [
331+ "10.210.1.217/24",
332+ "10.210.1.151/24"
333+ ],
334+ "network_uuid": "98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
335+ "model": "virtio",
336+ "mtu": 1500
337+ }
338+]
339+""")
340+
341+SDC_NICS_MIP_IPV6 = json.loads("""
342+[
343+ {
344+ "interface": "net0",
345+ "mac": "90:b8:d0:ae:64:51",
346+ "vlan_id": 324,
347+ "nic_tag": "external",
348+ "gateway": "8.12.42.1",
349+ "gateways": [
350+ "8.12.42.1"
351+ ],
352+ "netmask": "255.255.255.0",
353+ "ip": "8.12.42.51",
354+ "ips": [
355+ "2001:4800:78ff:1b:be76:4eff:fe06:96b3/64",
356+ "8.12.42.51/24"
357+ ],
358+ "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
359+ "model": "virtio",
360+ "mtu": 1500,
361+ "primary": true
362+ },
363+ {
364+ "interface": "net1",
365+ "mac": "90:b8:d0:bd:4f:9c",
366+ "vlan_id": 600,
367+ "nic_tag": "internal",
368+ "netmask": "255.255.255.0",
369+ "ip": "10.210.1.217",
370+ "ips": [
371+ "10.210.1.217/24"
372+ ],
373+ "network_uuid": "98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
374+ "model": "virtio",
375+ "mtu": 1500
376+ }
377+]
378+""")
379+
380+SDC_NICS_IPV4_IPV6 = json.loads("""
381+[
382+ {
383+ "interface": "net0",
384+ "mac": "90:b8:d0:ae:64:51",
385+ "vlan_id": 324,
386+ "nic_tag": "external",
387+ "gateway": "8.12.42.1",
388+ "gateways": ["8.12.42.1", "2001::1", "2001::2"],
389+ "netmask": "255.255.255.0",
390+ "ip": "8.12.42.51",
391+ "ips": ["2001::10/64", "8.12.42.51/24", "2001::11/64",
392+ "8.12.42.52/32"],
393+ "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
394+ "model": "virtio",
395+ "mtu": 1500,
396+ "primary": true
397+ },
398+ {
399+ "interface": "net1",
400+ "mac": "90:b8:d0:bd:4f:9c",
401+ "vlan_id": 600,
402+ "nic_tag": "internal",
403+ "netmask": "255.255.255.0",
404+ "ip": "10.210.1.217",
405+ "ips": ["10.210.1.217/24"],
406+ "gateways": ["10.210.1.210"],
407+ "network_uuid": "98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
408+ "model": "virtio",
409+ "mtu": 1500
410+ }
411+]
412+""")
413+
414+SDC_NICS_SINGLE_GATEWAY = json.loads("""
415+[
416+ {
417+ "interface":"net0",
418+ "mac":"90:b8:d0:d8:82:b4",
419+ "vlan_id":324,
420+ "nic_tag":"external",
421+ "gateway":"8.12.42.1",
422+ "gateways":["8.12.42.1"],
423+ "netmask":"255.255.255.0",
424+ "ip":"8.12.42.26",
425+ "ips":["8.12.42.26/24"],
426+ "network_uuid":"992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
427+ "model":"virtio",
428+ "mtu":1500,
429+ "primary":true
430+ },
431+ {
432+ "interface":"net1",
433+ "mac":"90:b8:d0:0a:51:31",
434+ "vlan_id":600,
435+ "nic_tag":"internal",
436+ "netmask":"255.255.255.0",
437+ "ip":"10.210.1.27",
438+ "ips":["10.210.1.27/24"],
439+ "network_uuid":"98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
440+ "model":"virtio",
441+ "mtu":1500
442+ }
443+]
444+""")
445+
446+
447 MOCK_RETURNS = {
448 'hostname': 'test-host',
449 'root_authorized_keys': 'ssh-rsa AAAAB3Nz...aC1yc2E= keyname',
450@@ -524,20 +749,135 @@
451
452
453 class TestNetworkConversion(TestCase):
454-
455 def test_convert_simple(self):
456 expected = {
457 'version': 1,
458 'config': [
459 {'name': 'net0', 'type': 'physical',
460 'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
461- 'netmask': '255.255.255.0',
462 'address': '8.12.42.102/24'}],
463 'mtu': 1500, 'mac_address': '90:b8:d0:f5:e4:f5'},
464 {'name': 'net1', 'type': 'physical',
465- 'subnets': [{'type': 'static', 'gateway': '192.168.128.1',
466- 'netmask': '255.255.252.0',
467+ 'subnets': [{'type': 'static',
468 'address': '192.168.128.93/22'}],
469 'mtu': 8500, 'mac_address': '90:b8:d0:a5:ff:cd'}]}
470- found = DataSourceSmartOS.convert_smartos_network_data(SDC_NICS)
471+ found = convert_net(SDC_NICS)
472+ self.assertEqual(expected, found)
473+
474+ def test_convert_simple_alt(self):
475+ expected = {
476+ 'version': 1,
477+ 'config': [
478+ {'name': 'net0', 'type': 'physical',
479+ 'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
480+ 'address': '8.12.42.51/24'}],
481+ 'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
482+ {'name': 'net1', 'type': 'physical',
483+ 'subnets': [{'type': 'static',
484+ 'address': '10.210.1.217/24'}],
485+ 'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
486+ found = convert_net(SDC_NICS_ALT)
487+ self.assertEqual(expected, found)
488+
489+ def test_convert_simple_dhcp(self):
490+ expected = {
491+ 'version': 1,
492+ 'config': [
493+ {'name': 'net0', 'type': 'physical',
494+ 'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
495+ 'address': '8.12.42.51/24'}],
496+ 'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
497+ {'name': 'net1', 'type': 'physical',
498+ 'subnets': [{'type': 'dhcp4'}],
499+ 'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
500+ found = convert_net(SDC_NICS_DHCP)
501+ self.assertEqual(expected, found)
502+
503+ def test_convert_simple_multi_ip(self):
504+ expected = {
505+ 'version': 1,
506+ 'config': [
507+ {'name': 'net0', 'type': 'physical',
508+ 'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
509+ 'address': '8.12.42.51/24'},
510+ {'type': 'static',
511+ 'address': '8.12.42.52/24'}],
512+ 'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
513+ {'name': 'net1', 'type': 'physical',
514+ 'subnets': [{'type': 'static',
515+ 'address': '10.210.1.217/24'},
516+ {'type': 'static',
517+ 'address': '10.210.1.151/24'}],
518+ 'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
519+ found = convert_net(SDC_NICS_MIP)
520+ self.assertEqual(expected, found)
521+
522+ def test_convert_with_dns(self):
523+ expected = {
524+ 'version': 1,
525+ 'config': [
526+ {'name': 'net0', 'type': 'physical',
527+ 'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
528+ 'address': '8.12.42.51/24'}],
529+ 'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
530+ {'name': 'net1', 'type': 'physical',
531+ 'subnets': [{'type': 'dhcp4'}],
532+ 'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'},
533+ {'type': 'nameserver',
534+ 'address': ['8.8.8.8', '8.8.8.1'], 'search': ["local"]}]}
535+ found = convert_net(
536+ network_data=SDC_NICS_DHCP, dns_servers=['8.8.8.8', '8.8.8.1'],
537+ dns_domain="local")
538+ self.assertEqual(expected, found)
539+
540+ def test_convert_simple_multi_ipv6(self):
541+ expected = {
542+ 'version': 1,
543+ 'config': [
544+ {'name': 'net0', 'type': 'physical',
545+ 'subnets': [{'type': 'static', 'address':
546+ '2001:4800:78ff:1b:be76:4eff:fe06:96b3/64'},
547+ {'type': 'static', 'gateway': '8.12.42.1',
548+ 'address': '8.12.42.51/24'}],
549+ 'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
550+ {'name': 'net1', 'type': 'physical',
551+ 'subnets': [{'type': 'static',
552+ 'address': '10.210.1.217/24'}],
553+ 'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
554+ found = convert_net(SDC_NICS_MIP_IPV6)
555+ self.assertEqual(expected, found)
556+
557+ def test_convert_simple_both_ipv4_ipv6(self):
558+ expected = {
559+ 'version': 1,
560+ 'config': [
561+ {'mac_address': '90:b8:d0:ae:64:51', 'mtu': 1500,
562+ 'name': 'net0', 'type': 'physical',
563+ 'subnets': [{'address': '2001::10/64', 'gateway': '2001::1',
564+ 'type': 'static'},
565+ {'address': '8.12.42.51/24',
566+ 'gateway': '8.12.42.1',
567+ 'type': 'static'},
568+ {'address': '2001::11/64', 'type': 'static'},
569+ {'address': '8.12.42.52/32', 'type': 'static'}]},
570+ {'mac_address': '90:b8:d0:bd:4f:9c', 'mtu': 1500,
571+ 'name': 'net1', 'type': 'physical',
572+ 'subnets': [{'address': '10.210.1.217/24',
573+ 'type': 'static'}]}]}
574+ found = convert_net(SDC_NICS_IPV4_IPV6)
575+ self.assertEqual(expected, found)
576+
577+ def test_gateways_not_on_all_nics(self):
578+ expected = {
579+ 'version': 1,
580+ 'config': [
581+ {'mac_address': '90:b8:d0:d8:82:b4', 'mtu': 1500,
582+ 'name': 'net0', 'type': 'physical',
583+ 'subnets': [{'address': '8.12.42.26/24',
584+ 'gateway': '8.12.42.1', 'type': 'static'}]},
585+ {'mac_address': '90:b8:d0:0a:51:31', 'mtu': 1500,
586+ 'name': 'net1', 'type': 'physical',
587+ 'subnets': [{'address': '10.210.1.27/24',
588+ 'type': 'static'}]}]}
589+ found = convert_net(SDC_NICS_SINGLE_GATEWAY)
590 self.assertEqual(expected, found)