Merge ~oddbloke/cloud-init/+git/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel

Proposed by Dan Watkins
Status: Merged
Merged at revision: 8acfb2ee407f8611ac54ddce2ebb470769df0e6e
Proposed branch: ~oddbloke/cloud-init/+git/cloud-init:ubuntu/devel
Merge into: cloud-init:ubuntu/devel
Diff against target: 2624 lines (+1361/-289)
42 files modified
.gitignore (+11/-0)
Makefile (+3/-1)
cloudinit/atomic_helper.py (+6/-0)
cloudinit/net/__init__.py (+136/-3)
cloudinit/net/tests/test_init.py (+327/-0)
cloudinit/sources/DataSourceEc2.py (+1/-1)
cloudinit/sources/DataSourceOVF.py (+20/-1)
cloudinit/sources/DataSourceOracle.py (+61/-1)
cloudinit/sources/helpers/vmware/imc/guestcust_error.py (+1/-0)
cloudinit/sources/helpers/vmware/imc/guestcust_util.py (+37/-0)
cloudinit/sources/tests/test_oracle.py (+147/-0)
cloudinit/tests/helpers.py (+8/-0)
debian/changelog (+20/-0)
dev/null (+0/-13)
doc/rtd/conf.py (+3/-10)
doc/rtd/index.rst (+0/-9)
doc/rtd/topics/availability.rst (+53/-10)
doc/rtd/topics/datasources.rst (+92/-94)
doc/rtd/topics/datasources/altcloud.rst (+12/-11)
doc/rtd/topics/datasources/azure.rst (+2/-1)
doc/rtd/topics/datasources/cloudstack.rst (+1/-1)
doc/rtd/topics/datasources/configdrive.rst (+8/-8)
doc/rtd/topics/datasources/digitalocean.rst (+4/-2)
doc/rtd/topics/datasources/ec2.rst (+6/-5)
doc/rtd/topics/datasources/exoscale.rst (+6/-6)
doc/rtd/topics/datasources/nocloud.rst (+8/-8)
doc/rtd/topics/datasources/opennebula.rst (+15/-15)
doc/rtd/topics/datasources/openstack.rst (+2/-1)
doc/rtd/topics/datasources/smartos.rst (+7/-5)
doc/rtd/topics/debugging.rst (+7/-7)
doc/rtd/topics/dir_layout.rst (+22/-17)
doc/rtd/topics/docs.rst (+84/-0)
doc/rtd/topics/examples.rst (+1/-1)
doc/rtd/topics/faq.rst (+43/-0)
doc/rtd/topics/format.rst (+53/-28)
doc/rtd/topics/merging.rst (+12/-6)
doc/rtd/topics/network-config-format-v2.rst (+9/-9)
tests/unittests/test_datasource/test_ovf.py (+46/-9)
tests/unittests/test_ds_identify.py (+14/-2)
tests/unittests/test_vmware/test_guestcust_util.py (+65/-0)
tools/ds-identify (+1/-2)
tox.ini (+7/-2)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Needs Fixing
cloud-init Commiters Pending
Review via email: mp+372864@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1146/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1146//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1147/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1147//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1148/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1148//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1149/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1149//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1150/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1150//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1151/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1151//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1152/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1152//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1153/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1153//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1154/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1154//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1155/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    FAILED: Ubuntu LTS: Build

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1155//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1156/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1156//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1157/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1157//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~daniel-thewatkins/cloud-init/+git/cloud-init/+merge/372864/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1158/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1158//rebuild

review: Needs Fixing (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index 80c509e..b9b98e7 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -12,3 +12,14 @@ stage
6 *.snap
7 *.cover
8 .idea/
9+
10+# Ignore packaging artifacts
11+cloud-init.dsc
12+cloud-init_*.build
13+cloud-init_*.buildinfo
14+cloud-init_*.changes
15+cloud-init_*.deb
16+cloud-init_*.dsc
17+cloud-init_*.orig.tar.gz
18+cloud-init_*.tar.xz
19+cloud-init_*.upload
20diff --git a/Makefile b/Makefile
21index 4ace227..2c6d0c8 100644
22--- a/Makefile
23+++ b/Makefile
24@@ -106,7 +106,9 @@ deb-src:
25 echo sudo apt-get install devscripts; exit 1; }
26 $(PYVER) ./packages/bddeb -S -d
27
28+doc:
29+ tox -e doc
30
31 .PHONY: test pyflakes pyflakes3 clean pep8 rpm srpm deb deb-src yaml
32 .PHONY: check_version pip-test-requirements pip-requirements clean_pyc
33-.PHONY: unittest unittest3 style-check
34+.PHONY: unittest unittest3 style-check doc
35diff --git a/cloudinit/atomic_helper.py b/cloudinit/atomic_helper.py
36index 587b994..1f61faa 100644
37--- a/cloudinit/atomic_helper.py
38+++ b/cloudinit/atomic_helper.py
39@@ -1,11 +1,13 @@
40 # This file is part of cloud-init. See LICENSE file for license information.
41
42 import json
43+import logging
44 import os
45 import stat
46 import tempfile
47
48 _DEF_PERMS = 0o644
49+LOG = logging.getLogger(__name__)
50
51
52 def write_file(filename, content, mode=_DEF_PERMS,
53@@ -23,6 +25,10 @@ def write_file(filename, content, mode=_DEF_PERMS,
54 try:
55 tf = tempfile.NamedTemporaryFile(dir=os.path.dirname(filename),
56 delete=False, mode=omode)
57+ LOG.debug(
58+ "Atomically writing to file %s (via temporary file %s) - %s: [%o]"
59+ " %d bytes/chars",
60+ filename, tf.name, omode, mode, len(content))
61 tf.write(content)
62 tf.close()
63 os.chmod(tf.name, mode)
64diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
65index ea707c0..5de5c6d 100644
66--- a/cloudinit/net/__init__.py
67+++ b/cloudinit/net/__init__.py
68@@ -109,6 +109,127 @@ def is_bond(devname):
69 return os.path.exists(sys_dev_path(devname, "bonding"))
70
71
72+def has_master(devname):
73+ return os.path.exists(sys_dev_path(devname, path="master"))
74+
75+
76+def is_netfailover(devname, driver=None):
77+ """ netfailover driver uses 3 nics, master, primary and standby.
78+ this returns True if the device is either the primary or standby
79+ as these devices are to be ignored.
80+ """
81+ if driver is None:
82+ driver = device_driver(devname)
83+ if is_netfail_primary(devname, driver) or is_netfail_standby(devname,
84+ driver):
85+ return True
86+ return False
87+
88+
89+def get_dev_features(devname):
90+ """ Returns a str from reading /sys/class/net/<devname>/device/features."""
91+ features = ''
92+ try:
93+ features = read_sys_net(devname, 'device/features')
94+ except Exception:
95+ pass
96+ return features
97+
98+
99+def has_netfail_standby_feature(devname):
100+ """ Return True if VIRTIO_NET_F_STANDBY bit (62) is set.
101+
102+ https://github.com/torvalds/linux/blob/ \
103+ 089cf7f6ecb266b6a4164919a2e69bd2f938374a/ \
104+ include/uapi/linux/virtio_net.h#L60
105+ """
106+ features = get_dev_features(devname)
107+ if not features or len(features) < 64:
108+ return False
109+ return features[62] == "1"
110+
111+
112+def is_netfail_master(devname, driver=None):
113+ """ A device is a "netfail master" device if:
114+
115+ - The device does NOT have the 'master' sysfs attribute
116+ - The device driver is 'virtio_net'
117+ - The device has the standby feature bit set
118+
119+ Return True if all of the above is True.
120+ """
121+ if has_master(devname):
122+ return False
123+
124+ if driver is None:
125+ driver = device_driver(devname)
126+
127+ if driver != "virtio_net":
128+ return False
129+
130+ if not has_netfail_standby_feature(devname):
131+ return False
132+
133+ return True
134+
135+
136+def is_netfail_primary(devname, driver=None):
137+ """ A device is a "netfail primary" device if:
138+
139+ - the device has a 'master' sysfs file
140+ - the device driver is not 'virtio_net'
141+ - the 'master' sysfs file points to device with virtio_net driver
142+ - the 'master' device has the 'standby' feature bit set
143+
144+ Return True if all of the above is True.
145+ """
146+ # /sys/class/net/<devname>/master -> ../../<master devname>
147+ master_sysfs_path = sys_dev_path(devname, path='master')
148+ if not os.path.exists(master_sysfs_path):
149+ return False
150+
151+ if driver is None:
152+ driver = device_driver(devname)
153+
154+ if driver == "virtio_net":
155+ return False
156+
157+ master_devname = os.path.basename(os.path.realpath(master_sysfs_path))
158+ master_driver = device_driver(master_devname)
159+ if master_driver != "virtio_net":
160+ return False
161+
162+ master_has_standby = has_netfail_standby_feature(master_devname)
163+ if not master_has_standby:
164+ return False
165+
166+ return True
167+
168+
169+def is_netfail_standby(devname, driver=None):
170+ """ A device is a "netfail standby" device if:
171+
172+ - The device has a 'master' sysfs attribute
173+ - The device driver is 'virtio_net'
174+ - The device has the standby feature bit set
175+
176+ Return True if all of the above is True.
177+ """
178+ if not has_master(devname):
179+ return False
180+
181+ if driver is None:
182+ driver = device_driver(devname)
183+
184+ if driver != "virtio_net":
185+ return False
186+
187+ if not has_netfail_standby_feature(devname):
188+ return False
189+
190+ return True
191+
192+
193 def is_renamed(devname):
194 """
195 /* interface name assignment types (sysfs name_assign_type attribute) */
196@@ -227,6 +348,9 @@ def find_fallback_nic(blacklist_drivers=None):
197 if is_bond(interface):
198 # skip any bonds
199 continue
200+ if is_netfailover(interface):
201+ # ignore netfailover primary/standby interfaces
202+ continue
203 carrier = read_sys_net_int(interface, 'carrier')
204 if carrier:
205 connected.append(interface)
206@@ -273,9 +397,14 @@ def generate_fallback_config(blacklist_drivers=None, config_driver=None):
207 if not target_name:
208 # can't read any interfaces addresses (or there are none); give up
209 return None
210- target_mac = read_sys_net_safe(target_name, 'address')
211- cfg = {'dhcp4': True, 'set-name': target_name,
212- 'match': {'macaddress': target_mac.lower()}}
213+
214+ # netfail cannot use mac for matching, they have duplicate macs
215+ if is_netfail_master(target_name):
216+ match = {'name': target_name}
217+ else:
218+ match = {
219+ 'macaddress': read_sys_net_safe(target_name, 'address').lower()}
220+ cfg = {'dhcp4': True, 'set-name': target_name, 'match': match}
221 if config_driver:
222 driver = device_driver(target_name)
223 if driver:
224@@ -661,6 +790,10 @@ def get_interfaces():
225 continue
226 if is_bond(name):
227 continue
228+ if has_master(name):
229+ continue
230+ if is_netfailover(name):
231+ continue
232 mac = get_interface_mac(name)
233 # some devices may not have a mac (tun0)
234 if not mac:
235diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
236index d2e38f0..999db98 100644
237--- a/cloudinit/net/tests/test_init.py
238+++ b/cloudinit/net/tests/test_init.py
239@@ -157,6 +157,12 @@ class TestReadSysNet(CiTestCase):
240 ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
241 self.assertTrue(net.is_bond('eth0'))
242
243+ def test_has_master(self):
244+ """has_master is True when /sys/net/devname/master exists."""
245+ self.assertFalse(net.has_master('enP1s1'))
246+ ensure_file(os.path.join(self.sysdir, 'enP1s1', 'master'))
247+ self.assertTrue(net.has_master('enP1s1'))
248+
249 def test_is_vlan(self):
250 """is_vlan is True when /sys/net/devname/uevent has DEVTYPE=vlan."""
251 ensure_file(os.path.join(self.sysdir, 'eth0', 'uevent'))
252@@ -204,6 +210,10 @@ class TestGenerateFallbackConfig(CiTestCase):
253 self.add_patch('cloudinit.net.util.is_container', 'm_is_container',
254 return_value=False)
255 self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')
256+ self.add_patch('cloudinit.net.is_netfailover', 'm_netfail',
257+ return_value=False)
258+ self.add_patch('cloudinit.net.is_netfail_master', 'm_netfail_master',
259+ return_value=False)
260
261 def test_generate_fallback_finds_connected_eth_with_mac(self):
262 """generate_fallback_config finds any connected device with a mac."""
263@@ -268,6 +278,61 @@ class TestGenerateFallbackConfig(CiTestCase):
264 ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
265 self.assertIsNone(net.generate_fallback_config())
266
267+ def test_generate_fallback_config_skips_netfail_devs(self):
268+ """gen_fallback_config ignores netfail primary,sby no mac on master."""
269+ mac = 'aa:bb:cc:aa:bb:cc' # netfailover devs share the same mac
270+ for iface in ['ens3', 'ens3sby', 'enP0s1f3']:
271+ write_file(os.path.join(self.sysdir, iface, 'carrier'), '1')
272+ write_file(
273+ os.path.join(self.sysdir, iface, 'addr_assign_type'), '0')
274+ write_file(
275+ os.path.join(self.sysdir, iface, 'address'), mac)
276+
277+ def is_netfail(iface, _driver=None):
278+ # ens3 is the master
279+ if iface == 'ens3':
280+ return False
281+ return True
282+ self.m_netfail.side_effect = is_netfail
283+
284+ def is_netfail_master(iface, _driver=None):
285+ # ens3 is the master
286+ if iface == 'ens3':
287+ return True
288+ return False
289+ self.m_netfail_master.side_effect = is_netfail_master
290+ expected = {
291+ 'ethernets': {
292+ 'ens3': {'dhcp4': True, 'match': {'name': 'ens3'},
293+ 'set-name': 'ens3'}},
294+ 'version': 2}
295+ result = net.generate_fallback_config()
296+ self.assertEqual(expected, result)
297+
298+
299+class TestNetFindFallBackNic(CiTestCase):
300+
301+ with_logs = True
302+
303+ def setUp(self):
304+ super(TestNetFindFallBackNic, self).setUp()
305+ sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
306+ self.m_sys_path = sys_mock.start()
307+ self.sysdir = self.tmp_dir() + '/'
308+ self.m_sys_path.return_value = self.sysdir
309+ self.addCleanup(sys_mock.stop)
310+ self.add_patch('cloudinit.net.util.is_container', 'm_is_container',
311+ return_value=False)
312+ self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')
313+
314+ def test_generate_fallback_finds_first_connected_eth_with_mac(self):
315+ """find_fallback_nic finds any connected device with a mac."""
316+ write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
317+ write_file(os.path.join(self.sysdir, 'eth1', 'carrier'), '1')
318+ mac = 'aa:bb:cc:aa:bb:cc'
319+ write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
320+ self.assertEqual('eth1', net.find_fallback_nic())
321+
322
323 class TestGetDeviceList(CiTestCase):
324
325@@ -365,6 +430,37 @@ class TestGetInterfaceMAC(CiTestCase):
326 expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
327 self.assertEqual(expected, net.get_interfaces())
328
329+ def test_get_interfaces_by_mac_skips_master_devs(self):
330+ """Ignore interfaces with a master device which would have dup mac."""
331+ mac1 = mac2 = 'aa:bb:cc:aa:bb:cc'
332+ write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
333+ write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac1)
334+ write_file(os.path.join(self.sysdir, 'eth1', 'master'), "blah")
335+ write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
336+ write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac2)
337+ expected = [('eth2', mac2, None, None)]
338+ self.assertEqual(expected, net.get_interfaces())
339+
340+ @mock.patch('cloudinit.net.is_netfailover')
341+ def test_get_interfaces_by_mac_skips_netfailvoer(self, m_netfail):
342+ """Ignore interfaces if netfailover primary or standby."""
343+ mac = 'aa:bb:cc:aa:bb:cc' # netfailover devs share the same mac
344+ for iface in ['ens3', 'ens3sby', 'enP0s1f3']:
345+ write_file(
346+ os.path.join(self.sysdir, iface, 'addr_assign_type'), '0')
347+ write_file(
348+ os.path.join(self.sysdir, iface, 'address'), mac)
349+
350+ def is_netfail(iface, _driver=None):
351+ # ens3 is the master
352+ if iface == 'ens3':
353+ return False
354+ else:
355+ return True
356+ m_netfail.side_effect = is_netfail
357+ expected = [('ens3', mac, None, None)]
358+ self.assertEqual(expected, net.get_interfaces())
359+
360
361 class TestInterfaceHasOwnMAC(CiTestCase):
362
363@@ -922,3 +1018,234 @@ class TestWaitForPhysdevs(CiTestCase):
364 self.m_get_iface_mac.return_value = {}
365 net.wait_for_physdevs(netcfg, strict=False)
366 self.assertEqual(5 * len(physdevs), self.m_udev_settle.call_count)
367+
368+
369+class TestNetFailOver(CiTestCase):
370+
371+ with_logs = True
372+
373+ def setUp(self):
374+ super(TestNetFailOver, self).setUp()
375+ self.add_patch('cloudinit.net.util', 'm_util')
376+ self.add_patch('cloudinit.net.read_sys_net', 'm_read_sys_net')
377+ self.add_patch('cloudinit.net.device_driver', 'm_device_driver')
378+
379+ def test_get_dev_features(self):
380+ devname = self.random_string()
381+ features = self.random_string()
382+ self.m_read_sys_net.return_value = features
383+
384+ self.assertEqual(features, net.get_dev_features(devname))
385+ self.assertEqual(1, self.m_read_sys_net.call_count)
386+ self.assertEqual(mock.call(devname, 'device/features'),
387+ self.m_read_sys_net.call_args_list[0])
388+
389+ def test_get_dev_features_none_returns_empty_string(self):
390+ devname = self.random_string()
391+ self.m_read_sys_net.side_effect = Exception('error')
392+ self.assertEqual('', net.get_dev_features(devname))
393+ self.assertEqual(1, self.m_read_sys_net.call_count)
394+ self.assertEqual(mock.call(devname, 'device/features'),
395+ self.m_read_sys_net.call_args_list[0])
396+
397+ @mock.patch('cloudinit.net.get_dev_features')
398+ def test_has_netfail_standby_feature(self, m_dev_features):
399+ devname = self.random_string()
400+ standby_features = ('0' * 62) + '1' + '0'
401+ m_dev_features.return_value = standby_features
402+ self.assertTrue(net.has_netfail_standby_feature(devname))
403+
404+ @mock.patch('cloudinit.net.get_dev_features')
405+ def test_has_netfail_standby_feature_short_is_false(self, m_dev_features):
406+ devname = self.random_string()
407+ standby_features = self.random_string()
408+ m_dev_features.return_value = standby_features
409+ self.assertFalse(net.has_netfail_standby_feature(devname))
410+
411+ @mock.patch('cloudinit.net.get_dev_features')
412+ def test_has_netfail_standby_feature_not_present_is_false(self,
413+ m_dev_features):
414+ devname = self.random_string()
415+ standby_features = '0' * 64
416+ m_dev_features.return_value = standby_features
417+ self.assertFalse(net.has_netfail_standby_feature(devname))
418+
419+ @mock.patch('cloudinit.net.get_dev_features')
420+ def test_has_netfail_standby_feature_no_features_is_false(self,
421+ m_dev_features):
422+ devname = self.random_string()
423+ standby_features = None
424+ m_dev_features.return_value = standby_features
425+ self.assertFalse(net.has_netfail_standby_feature(devname))
426+
427+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
428+ @mock.patch('cloudinit.net.os.path.exists')
429+ def test_is_netfail_master(self, m_exists, m_standby):
430+ devname = self.random_string()
431+ driver = 'virtio_net'
432+ m_exists.return_value = False # no master sysfs attr
433+ m_standby.return_value = True # has standby feature flag
434+ self.assertTrue(net.is_netfail_master(devname, driver))
435+
436+ @mock.patch('cloudinit.net.sys_dev_path')
437+ def test_is_netfail_master_checks_master_attr(self, m_sysdev):
438+ devname = self.random_string()
439+ driver = 'virtio_net'
440+ m_sysdev.return_value = self.random_string()
441+ self.assertFalse(net.is_netfail_master(devname, driver))
442+ self.assertEqual(1, m_sysdev.call_count)
443+ self.assertEqual(mock.call(devname, path='master'),
444+ m_sysdev.call_args_list[0])
445+
446+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
447+ @mock.patch('cloudinit.net.os.path.exists')
448+ def test_is_netfail_master_wrong_driver(self, m_exists, m_standby):
449+ devname = self.random_string()
450+ driver = self.random_string()
451+ self.assertFalse(net.is_netfail_master(devname, driver))
452+
453+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
454+ @mock.patch('cloudinit.net.os.path.exists')
455+ def test_is_netfail_master_has_master_attr(self, m_exists, m_standby):
456+ devname = self.random_string()
457+ driver = 'virtio_net'
458+ m_exists.return_value = True # has master sysfs attr
459+ self.assertFalse(net.is_netfail_master(devname, driver))
460+
461+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
462+ @mock.patch('cloudinit.net.os.path.exists')
463+ def test_is_netfail_master_no_standby_feat(self, m_exists, m_standby):
464+ devname = self.random_string()
465+ driver = 'virtio_net'
466+ m_exists.return_value = False # no master sysfs attr
467+ m_standby.return_value = False # no standby feature flag
468+ self.assertFalse(net.is_netfail_master(devname, driver))
469+
470+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
471+ @mock.patch('cloudinit.net.os.path.exists')
472+ @mock.patch('cloudinit.net.sys_dev_path')
473+ def test_is_netfail_primary(self, m_sysdev, m_exists, m_standby):
474+ devname = self.random_string()
475+ driver = self.random_string() # device not virtio_net
476+ master_devname = self.random_string()
477+ m_sysdev.return_value = "%s/%s" % (self.random_string(),
478+ master_devname)
479+ m_exists.return_value = True # has master sysfs attr
480+ self.m_device_driver.return_value = 'virtio_net' # master virtio_net
481+ m_standby.return_value = True # has standby feature flag
482+ self.assertTrue(net.is_netfail_primary(devname, driver))
483+ self.assertEqual(1, self.m_device_driver.call_count)
484+ self.assertEqual(mock.call(master_devname),
485+ self.m_device_driver.call_args_list[0])
486+ self.assertEqual(1, m_standby.call_count)
487+ self.assertEqual(mock.call(master_devname),
488+ m_standby.call_args_list[0])
489+
490+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
491+ @mock.patch('cloudinit.net.os.path.exists')
492+ @mock.patch('cloudinit.net.sys_dev_path')
493+ def test_is_netfail_primary_wrong_driver(self, m_sysdev, m_exists,
494+ m_standby):
495+ devname = self.random_string()
496+ driver = 'virtio_net'
497+ self.assertFalse(net.is_netfail_primary(devname, driver))
498+
499+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
500+ @mock.patch('cloudinit.net.os.path.exists')
501+ @mock.patch('cloudinit.net.sys_dev_path')
502+ def test_is_netfail_primary_no_master(self, m_sysdev, m_exists, m_standby):
503+ devname = self.random_string()
504+ driver = self.random_string() # device not virtio_net
505+ m_exists.return_value = False # no master sysfs attr
506+ self.assertFalse(net.is_netfail_primary(devname, driver))
507+
508+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
509+ @mock.patch('cloudinit.net.os.path.exists')
510+ @mock.patch('cloudinit.net.sys_dev_path')
511+ def test_is_netfail_primary_bad_master(self, m_sysdev, m_exists,
512+ m_standby):
513+ devname = self.random_string()
514+ driver = self.random_string() # device not virtio_net
515+ master_devname = self.random_string()
516+ m_sysdev.return_value = "%s/%s" % (self.random_string(),
517+ master_devname)
518+ m_exists.return_value = True # has master sysfs attr
519+ self.m_device_driver.return_value = 'XXXX' # master not virtio_net
520+ self.assertFalse(net.is_netfail_primary(devname, driver))
521+
522+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
523+ @mock.patch('cloudinit.net.os.path.exists')
524+ @mock.patch('cloudinit.net.sys_dev_path')
525+ def test_is_netfail_primary_no_standby(self, m_sysdev, m_exists,
526+ m_standby):
527+ devname = self.random_string()
528+ driver = self.random_string() # device not virtio_net
529+ master_devname = self.random_string()
530+ m_sysdev.return_value = "%s/%s" % (self.random_string(),
531+ master_devname)
532+ m_exists.return_value = True # has master sysfs attr
533+ self.m_device_driver.return_value = 'virtio_net' # master virtio_net
534+ m_standby.return_value = False # master has no standby feature flag
535+ self.assertFalse(net.is_netfail_primary(devname, driver))
536+
537+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
538+ @mock.patch('cloudinit.net.os.path.exists')
539+ def test_is_netfail_standby(self, m_exists, m_standby):
540+ devname = self.random_string()
541+ driver = 'virtio_net'
542+ m_exists.return_value = True # has master sysfs attr
543+ m_standby.return_value = True # has standby feature flag
544+ self.assertTrue(net.is_netfail_standby(devname, driver))
545+
546+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
547+ @mock.patch('cloudinit.net.os.path.exists')
548+ def test_is_netfail_standby_wrong_driver(self, m_exists, m_standby):
549+ devname = self.random_string()
550+ driver = self.random_string()
551+ self.assertFalse(net.is_netfail_standby(devname, driver))
552+
553+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
554+ @mock.patch('cloudinit.net.os.path.exists')
555+ def test_is_netfail_standby_no_master(self, m_exists, m_standby):
556+ devname = self.random_string()
557+ driver = 'virtio_net'
558+ m_exists.return_value = False # has master sysfs attr
559+ self.assertFalse(net.is_netfail_standby(devname, driver))
560+
561+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
562+ @mock.patch('cloudinit.net.os.path.exists')
563+ def test_is_netfail_standby_no_standby_feature(self, m_exists, m_standby):
564+ devname = self.random_string()
565+ driver = 'virtio_net'
566+ m_exists.return_value = True # has master sysfs attr
567+ m_standby.return_value = False # has standby feature flag
568+ self.assertFalse(net.is_netfail_standby(devname, driver))
569+
570+ @mock.patch('cloudinit.net.is_netfail_standby')
571+ @mock.patch('cloudinit.net.is_netfail_primary')
572+ def test_is_netfailover_primary(self, m_primary, m_standby):
573+ devname = self.random_string()
574+ driver = self.random_string()
575+ m_primary.return_value = True
576+ m_standby.return_value = False
577+ self.assertTrue(net.is_netfailover(devname, driver))
578+
579+ @mock.patch('cloudinit.net.is_netfail_standby')
580+ @mock.patch('cloudinit.net.is_netfail_primary')
581+ def test_is_netfailover_standby(self, m_primary, m_standby):
582+ devname = self.random_string()
583+ driver = self.random_string()
584+ m_primary.return_value = False
585+ m_standby.return_value = True
586+ self.assertTrue(net.is_netfailover(devname, driver))
587+
588+ @mock.patch('cloudinit.net.is_netfail_standby')
589+ @mock.patch('cloudinit.net.is_netfail_primary')
590+ def test_is_netfailover_returns_false(self, m_primary, m_standby):
591+ devname = self.random_string()
592+ driver = self.random_string()
593+ m_primary.return_value = False
594+ m_standby.return_value = False
595+ self.assertFalse(net.is_netfailover(devname, driver))
596+
597+# vi: ts=4 expandtab
598diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
599index 5c017bf..1010745 100644
600--- a/cloudinit/sources/DataSourceEc2.py
601+++ b/cloudinit/sources/DataSourceEc2.py
602@@ -473,7 +473,7 @@ def identify_aws(data):
603
604
605 def identify_brightbox(data):
606- if data['serial'].endswith('brightbox.com'):
607+ if data['serial'].endswith('.brightbox.com'):
608 return CloudNames.BRIGHTBOX
609
610
611diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
612index dd941d2..b156189 100644
613--- a/cloudinit/sources/DataSourceOVF.py
614+++ b/cloudinit/sources/DataSourceOVF.py
615@@ -40,11 +40,15 @@ from cloudinit.sources.helpers.vmware.imc.guestcust_state \
616 from cloudinit.sources.helpers.vmware.imc.guestcust_util import (
617 enable_nics,
618 get_nics_to_enable,
619- set_customization_status
620+ set_customization_status,
621+ get_tools_config
622 )
623
624 LOG = logging.getLogger(__name__)
625
626+CONFGROUPNAME_GUESTCUSTOMIZATION = "deployPkg"
627+GUESTCUSTOMIZATION_ENABLE_CUST_SCRIPTS = "enable-custom-scripts"
628+
629
630 class DataSourceOVF(sources.DataSource):
631
632@@ -148,6 +152,21 @@ class DataSourceOVF(sources.DataSource):
633 product_marker, os.path.join(self.paths.cloud_dir, 'data'))
634 special_customization = product_marker and not hasmarkerfile
635 customscript = self._vmware_cust_conf.custom_script_name
636+ custScriptConfig = get_tools_config(
637+ CONFGROUPNAME_GUESTCUSTOMIZATION,
638+ GUESTCUSTOMIZATION_ENABLE_CUST_SCRIPTS,
639+ "true")
640+ if custScriptConfig.lower() == "false":
641+ # Update the customization status if there is a
642+ # custom script is disabled
643+ if special_customization and customscript:
644+ msg = "Custom script is disabled by VM Administrator"
645+ LOG.debug(msg)
646+ set_customization_status(
647+ GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
648+ GuestCustErrorEnum.GUESTCUST_ERROR_SCRIPT_DISABLED)
649+ raise RuntimeError(msg)
650+
651 ccScriptsDir = os.path.join(
652 self.paths.get_cpath("scripts"),
653 "per-instance")
654diff --git a/cloudinit/sources/DataSourceOracle.py b/cloudinit/sources/DataSourceOracle.py
655index 1cb0636..eec8740 100644
656--- a/cloudinit/sources/DataSourceOracle.py
657+++ b/cloudinit/sources/DataSourceOracle.py
658@@ -16,7 +16,7 @@ Notes:
659 """
660
661 from cloudinit.url_helper import combine_url, readurl, UrlError
662-from cloudinit.net import dhcp, get_interfaces_by_mac
663+from cloudinit.net import dhcp, get_interfaces_by_mac, is_netfail_master
664 from cloudinit import net
665 from cloudinit import sources
666 from cloudinit import util
667@@ -108,6 +108,56 @@ def _add_network_config_from_opc_imds(network_config):
668 'match': {'macaddress': mac_address}}
669
670
671+def _ensure_netfailover_safe(network_config):
672+ """
673+ Search network config physical interfaces to see if any of them are
674+ a netfailover master. If found, we prevent matching by MAC as the other
675+ failover devices have the same MAC but need to be ignored.
676+
677+ Note: we rely on cloudinit.net changes which prevent netfailover devices
678+ from being present in the provided network config. For more details about
679+ netfailover devices, refer to cloudinit.net module.
680+
681+ :param network_config
682+ A v1 or v2 network config dict with the primary NIC, and possibly
683+ secondary nic configured. This dict will be mutated.
684+
685+ """
686+ # ignore anything that's not an actual network-config
687+ if 'version' not in network_config:
688+ return
689+
690+ if network_config['version'] not in [1, 2]:
691+ LOG.debug('Ignoring unknown network config version: %s',
692+ network_config['version'])
693+ return
694+
695+ mac_to_name = get_interfaces_by_mac()
696+ if network_config['version'] == 1:
697+ for cfg in [c for c in network_config['config'] if 'type' in c]:
698+ if cfg['type'] == 'physical':
699+ if 'mac_address' in cfg:
700+ mac = cfg['mac_address']
701+ cur_name = mac_to_name.get(mac)
702+ if not cur_name:
703+ continue
704+ elif is_netfail_master(cur_name):
705+ del cfg['mac_address']
706+
707+ elif network_config['version'] == 2:
708+ for _, cfg in network_config.get('ethernets', {}).items():
709+ if 'match' in cfg:
710+ macaddr = cfg.get('match', {}).get('macaddress')
711+ if macaddr:
712+ cur_name = mac_to_name.get(macaddr)
713+ if not cur_name:
714+ continue
715+ elif is_netfail_master(cur_name):
716+ del cfg['match']['macaddress']
717+ del cfg['set-name']
718+ cfg['match']['name'] = cur_name
719+
720+
721 class DataSourceOracle(sources.DataSource):
722
723 dsname = 'Oracle'
724@@ -208,9 +258,13 @@ class DataSourceOracle(sources.DataSource):
725 We nonetheless return cmdline provided config if present
726 and fallback to generate fallback."""
727 if self._network_config == sources.UNSET:
728+ # this is v1
729 self._network_config = cmdline.read_initramfs_config()
730+
731 if not self._network_config:
732+ # this is now v2
733 self._network_config = self.distro.generate_fallback_config()
734+
735 if self.ds_cfg.get('configure_secondary_nics'):
736 try:
737 # Mutate self._network_config to include secondary VNICs
738@@ -219,6 +273,12 @@ class DataSourceOracle(sources.DataSource):
739 util.logexc(
740 LOG,
741 "Failed to fetch secondary network configuration!")
742+
743+ # we need to verify that the nic selected is not a netfail over
744+ # device and, if it is a netfail master, then we need to avoid
745+ # emitting any match by mac
746+ _ensure_netfailover_safe(self._network_config)
747+
748 return self._network_config
749
750
751diff --git a/cloudinit/sources/helpers/vmware/imc/guestcust_error.py b/cloudinit/sources/helpers/vmware/imc/guestcust_error.py
752index db5a00d..65ae739 100644
753--- a/cloudinit/sources/helpers/vmware/imc/guestcust_error.py
754+++ b/cloudinit/sources/helpers/vmware/imc/guestcust_error.py
755@@ -10,5 +10,6 @@ class GuestCustErrorEnum(object):
756 """Specifies different errors of Guest Customization engine"""
757
758 GUESTCUST_ERROR_SUCCESS = 0
759+ GUESTCUST_ERROR_SCRIPT_DISABLED = 6
760
761 # vi: ts=4 expandtab
762diff --git a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py
763index a590f32..eb78172 100644
764--- a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py
765+++ b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py
766@@ -7,6 +7,7 @@
767
768 import logging
769 import os
770+import re
771 import time
772
773 from cloudinit import util
774@@ -117,4 +118,40 @@ def enable_nics(nics):
775 logger.warning("Can't connect network interfaces after %d attempts",
776 enableNicsWaitRetries)
777
778+
779+def get_tools_config(section, key, defaultVal):
780+ """ Return the value of [section] key from VMTools configuration.
781+
782+ @param section: String of section to read from VMTools config
783+ @returns: String value from key in [section] or defaultVal if
784+ [section] is not present or vmware-toolbox-cmd is
785+ not installed.
786+ """
787+
788+ if not util.which('vmware-toolbox-cmd'):
789+ logger.debug(
790+ 'vmware-toolbox-cmd not installed, returning default value')
791+ return defaultVal
792+
793+ retValue = defaultVal
794+ cmd = ['vmware-toolbox-cmd', 'config', 'get', section, key]
795+
796+ try:
797+ (outText, _) = util.subp(cmd)
798+ m = re.match(r'([a-zA-Z0-9 ]+)=(.*)', outText)
799+ if m:
800+ retValue = m.group(2).strip()
801+ logger.debug("Get tools config: [%s] %s = %s",
802+ section, key, retValue)
803+ else:
804+ logger.debug(
805+ "Tools config: [%s] %s is not found, return default value: %s",
806+ section, key, retValue)
807+ except util.ProcessExecutionError as e:
808+ logger.error("Failed running %s[%s]", cmd, e.exit_code)
809+ logger.exception(e)
810+
811+ return retValue
812+
813+
814 # vi: ts=4 expandtab
815diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py
816index 2a70bbc..85b6db9 100644
817--- a/cloudinit/sources/tests/test_oracle.py
818+++ b/cloudinit/sources/tests/test_oracle.py
819@@ -8,6 +8,7 @@ from cloudinit.tests import helpers as test_helpers
820
821 from textwrap import dedent
822 import argparse
823+import copy
824 import httpretty
825 import json
826 import mock
827@@ -586,4 +587,150 @@ class TestNetworkConfigFromOpcImds(test_helpers.CiTestCase):
828 self.assertEqual('10.0.0.231', secondary_nic_cfg['addresses'][0])
829
830
831+class TestNetworkConfigFiltersNetFailover(test_helpers.CiTestCase):
832+
833+ with_logs = True
834+
835+ def setUp(self):
836+ super(TestNetworkConfigFiltersNetFailover, self).setUp()
837+ self.add_patch(DS_PATH + '.get_interfaces_by_mac',
838+ 'm_get_interfaces_by_mac')
839+ self.add_patch(DS_PATH + '.is_netfail_master', 'm_netfail_master')
840+
841+ def test_ignore_bogus_network_config(self):
842+ netcfg = {'something': 'here'}
843+ passed_netcfg = copy.copy(netcfg)
844+ oracle._ensure_netfailover_safe(passed_netcfg)
845+ self.assertEqual(netcfg, passed_netcfg)
846+
847+ def test_ignore_network_config_unknown_versions(self):
848+ netcfg = {'something': 'here', 'version': 3}
849+ passed_netcfg = copy.copy(netcfg)
850+ oracle._ensure_netfailover_safe(passed_netcfg)
851+ self.assertEqual(netcfg, passed_netcfg)
852+
853+ def test_checks_v1_type_physical_interfaces(self):
854+ mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
855+ self.m_get_interfaces_by_mac.return_value = {
856+ mac_addr: nic_name,
857+ }
858+ netcfg = {'version': 1, 'config': [
859+ {'type': 'physical', 'name': nic_name, 'mac_address': mac_addr,
860+ 'subnets': [{'type': 'dhcp4'}]}]}
861+ passed_netcfg = copy.copy(netcfg)
862+ self.m_netfail_master.return_value = False
863+ oracle._ensure_netfailover_safe(passed_netcfg)
864+ self.assertEqual(netcfg, passed_netcfg)
865+ self.assertEqual([mock.call(nic_name)],
866+ self.m_netfail_master.call_args_list)
867+
868+ def test_checks_v1_skips_non_phys_interfaces(self):
869+ mac_addr, nic_name = '00:00:17:02:2b:b1', 'bond0'
870+ self.m_get_interfaces_by_mac.return_value = {
871+ mac_addr: nic_name,
872+ }
873+ netcfg = {'version': 1, 'config': [
874+ {'type': 'bond', 'name': nic_name, 'mac_address': mac_addr,
875+ 'subnets': [{'type': 'dhcp4'}]}]}
876+ passed_netcfg = copy.copy(netcfg)
877+ oracle._ensure_netfailover_safe(passed_netcfg)
878+ self.assertEqual(netcfg, passed_netcfg)
879+ self.assertEqual(0, self.m_netfail_master.call_count)
880+
881+ def test_removes_master_mac_property_v1(self):
882+ nic_master, mac_master = 'ens3', self.random_string()
883+ nic_other, mac_other = 'ens7', self.random_string()
884+ nic_extra, mac_extra = 'enp0s1f2', self.random_string()
885+ self.m_get_interfaces_by_mac.return_value = {
886+ mac_master: nic_master,
887+ mac_other: nic_other,
888+ mac_extra: nic_extra,
889+ }
890+ netcfg = {'version': 1, 'config': [
891+ {'type': 'physical', 'name': nic_master,
892+ 'mac_address': mac_master},
893+ {'type': 'physical', 'name': nic_other, 'mac_address': mac_other},
894+ {'type': 'physical', 'name': nic_extra, 'mac_address': mac_extra},
895+ ]}
896+
897+ def _is_netfail_master(iface):
898+ if iface == 'ens3':
899+ return True
900+ return False
901+ self.m_netfail_master.side_effect = _is_netfail_master
902+ expected_cfg = {'version': 1, 'config': [
903+ {'type': 'physical', 'name': nic_master},
904+ {'type': 'physical', 'name': nic_other, 'mac_address': mac_other},
905+ {'type': 'physical', 'name': nic_extra, 'mac_address': mac_extra},
906+ ]}
907+ oracle._ensure_netfailover_safe(netcfg)
908+ self.assertEqual(expected_cfg, netcfg)
909+
910+ def test_checks_v2_type_ethernet_interfaces(self):
911+ mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
912+ self.m_get_interfaces_by_mac.return_value = {
913+ mac_addr: nic_name,
914+ }
915+ netcfg = {'version': 2, 'ethernets': {
916+ nic_name: {'dhcp4': True, 'critical': True, 'set-name': nic_name,
917+ 'match': {'macaddress': mac_addr}}}}
918+ passed_netcfg = copy.copy(netcfg)
919+ self.m_netfail_master.return_value = False
920+ oracle._ensure_netfailover_safe(passed_netcfg)
921+ self.assertEqual(netcfg, passed_netcfg)
922+ self.assertEqual([mock.call(nic_name)],
923+ self.m_netfail_master.call_args_list)
924+
925+ def test_skips_v2_non_ethernet_interfaces(self):
926+ mac_addr, nic_name = '00:00:17:02:2b:b1', 'wlps0'
927+ self.m_get_interfaces_by_mac.return_value = {
928+ mac_addr: nic_name,
929+ }
930+ netcfg = {'version': 2, 'wifis': {
931+ nic_name: {'dhcp4': True, 'critical': True, 'set-name': nic_name,
932+ 'match': {'macaddress': mac_addr}}}}
933+ passed_netcfg = copy.copy(netcfg)
934+ oracle._ensure_netfailover_safe(passed_netcfg)
935+ self.assertEqual(netcfg, passed_netcfg)
936+ self.assertEqual(0, self.m_netfail_master.call_count)
937+
938+ def test_removes_master_mac_property_v2(self):
939+ nic_master, mac_master = 'ens3', self.random_string()
940+ nic_other, mac_other = 'ens7', self.random_string()
941+ nic_extra, mac_extra = 'enp0s1f2', self.random_string()
942+ self.m_get_interfaces_by_mac.return_value = {
943+ mac_master: nic_master,
944+ mac_other: nic_other,
945+ mac_extra: nic_extra,
946+ }
947+ netcfg = {'version': 2, 'ethernets': {
948+ nic_extra: {'dhcp4': True, 'set-name': nic_extra,
949+ 'match': {'macaddress': mac_extra}},
950+ nic_other: {'dhcp4': True, 'set-name': nic_other,
951+ 'match': {'macaddress': mac_other}},
952+ nic_master: {'dhcp4': True, 'set-name': nic_master,
953+ 'match': {'macaddress': mac_master}},
954+ }}
955+
956+ def _is_netfail_master(iface):
957+ if iface == 'ens3':
958+ return True
959+ return False
960+ self.m_netfail_master.side_effect = _is_netfail_master
961+
962+ expected_cfg = {'version': 2, 'ethernets': {
963+ nic_master: {'dhcp4': True, 'match': {'name': nic_master}},
964+ nic_extra: {'dhcp4': True, 'set-name': nic_extra,
965+ 'match': {'macaddress': mac_extra}},
966+ nic_other: {'dhcp4': True, 'set-name': nic_other,
967+ 'match': {'macaddress': mac_other}},
968+ }}
969+ oracle._ensure_netfailover_safe(netcfg)
970+ import pprint
971+ pprint.pprint(netcfg)
972+ print('---- ^^ modified ^^ ---- vv original vv ----')
973+ pprint.pprint(expected_cfg)
974+ self.assertEqual(expected_cfg, netcfg)
975+
976+
977 # vi: ts=4 expandtab
978diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
979index 23fddd0..4dad2af 100644
980--- a/cloudinit/tests/helpers.py
981+++ b/cloudinit/tests/helpers.py
982@@ -6,7 +6,9 @@ import functools
983 import httpretty
984 import logging
985 import os
986+import random
987 import shutil
988+import string
989 import sys
990 import tempfile
991 import time
992@@ -243,6 +245,12 @@ class CiTestCase(TestCase):
993 myds.metadata.update(metadata)
994 return cloud.Cloud(myds, self.paths, sys_cfg, mydist, None)
995
996+ @classmethod
997+ def random_string(cls, length=8):
998+ """ return a random lowercase string with default length of 8"""
999+ return ''.join(
1000+ random.choice(string.ascii_lowercase) for _ in range(length))
1001+
1002
1003 class ResourceUsingTestCase(CiTestCase):
1004
1005diff --git a/debian/changelog b/debian/changelog
1006index 8999751..4195d2c 100644
1007--- a/debian/changelog
1008+++ b/debian/changelog
1009@@ -1,3 +1,23 @@
1010+cloud-init (19.2-36-g059d049c-0ubuntu1) eoan; urgency=medium
1011+
1012+ * New upstream snapshot.
1013+ - net: add is_master check for filtering device list (LP: #1844191)
1014+ - docs: more complete list of availability [Joshua Powers]
1015+ - docs: start FAQ page [Joshua Powers]
1016+ - docs: cleanup output & order of datasource page [Joshua Powers]
1017+ - Brightbox: restrict detection to require full domain match
1018+ .brightbox.com [Scott Moser]
1019+ - VMWware: add option into VMTools config to enable/disable custom script.
1020+ [Xiaofeng Wang]
1021+ - net,Oracle: Add support for netfailover detection
1022+ - atomic_helper: add DEBUG logging to write_file (LP: #1843276)
1023+ - doc: document doc, create makefile and tox target [Joshua Powers]
1024+ - .gitignore: ignore files produced by package builds
1025+ - docs: fix whitespace, spelling, and line length [Joshua Powers]
1026+ - docs: remove unnecessary file in doc directory [Joshua Powers]
1027+
1028+ -- Daniel Watkins <oddbloke@ubuntu.com> Tue, 17 Sep 2019 12:12:27 +0200
1029+
1030 cloud-init (19.2-24-ge7881d5c-0ubuntu1) eoan; urgency=medium
1031
1032 * New upstream snapshot.
1033diff --git a/doc/README b/doc/README
1034deleted file mode 100644
1035index 8355919..0000000
1036--- a/doc/README
1037+++ /dev/null
1038@@ -1,4 +0,0 @@
1039-This project is cloud-init it is hosted on launchpad at
1040-https://launchpad.net/cloud-init
1041-
1042-The package was previously named ec2-init.
1043diff --git a/doc/rtd/conf.py b/doc/rtd/conf.py
1044index 4174477..9b27484 100644
1045--- a/doc/rtd/conf.py
1046+++ b/doc/rtd/conf.py
1047@@ -17,7 +17,8 @@ from cloudinit.config.schema import get_schema_doc
1048 # ]
1049
1050 # General information about the project.
1051-project = 'Cloud-Init'
1052+project = 'cloud-init'
1053+copyright = '2019, Canonical Ltd.'
1054
1055 # -- General configuration ----------------------------------------------------
1056
1057@@ -59,15 +60,7 @@ show_authors = False
1058
1059 # The theme to use for HTML and HTML Help pages. See the documentation for
1060 # a list of builtin themes.
1061-html_theme = 'default'
1062-
1063-# Theme options are theme-specific and customize the look and feel of a theme
1064-# further. For a list of options available for each theme, see the
1065-# documentation.
1066-html_theme_options = {
1067- "bodyfont": "Ubuntu, Arial, sans-serif",
1068- "headfont": "Ubuntu, Arial, sans-serif"
1069-}
1070+html_theme = 'sphinx_rtd_theme'
1071
1072 # The name of an image file (relative to this directory) to place at the top
1073 # of the sidebar.
1074diff --git a/doc/rtd/index.rst b/doc/rtd/index.rst
1075index 20a99a3..c670b20 100644
1076--- a/doc/rtd/index.rst
1077+++ b/doc/rtd/index.rst
1078@@ -1,14 +1,5 @@
1079 .. _index:
1080
1081-.. http://thomas-cokelaer.info/tutorials/sphinx/rest_syntax.html
1082-.. As suggested at link above for headings use:
1083-.. # with overline, for parts
1084-.. * with overline, for chapters
1085-.. =, for sections
1086-.. -, for subsections
1087-.. ^, for subsubsections
1088-.. “, for paragraphs
1089-
1090 #############
1091 Documentation
1092 #############
1093diff --git a/doc/rtd/topics/availability.rst b/doc/rtd/topics/availability.rst
1094index ef5ae7b..3f215b1 100644
1095--- a/doc/rtd/topics/availability.rst
1096+++ b/doc/rtd/topics/availability.rst
1097@@ -1,21 +1,64 @@
1098-************
1099+.. _availability:
1100+
1101 Availability
1102 ************
1103
1104-It is currently installed in the `Ubuntu Cloud Images`_ and also in the official `Ubuntu`_ images available on EC2, Azure, GCE and many other clouds.
1105+Below outlines the current availability of cloud-init across
1106+distributions and clouds, both public and private.
1107+
1108+.. note::
1109+
1110+ If a distribution or cloud does not show up in the list below contact
1111+ them and ask for images to be generated using cloud-init!
1112
1113-Versions for other systems can be (or have been) created for the following distributions:
1114+Distributions
1115+=============
1116+
1117+Cloud-init has support across all major Linux distributions and
1118+FreeBSD:
1119
1120 - Ubuntu
1121+- SLES/openSUSE
1122+- RHEL/CentOS
1123 - Fedora
1124+- Gentoo Linux
1125 - Debian
1126-- RHEL
1127-- CentOS
1128-- *and more...*
1129+- ArchLinux
1130+- FreeBSD
1131+
1132+Clouds
1133+======
1134+
1135+Cloud-init provides support across a wide ranging list of execution
1136+environments in the public cloud:
1137+
1138+- Amazon Web Services
1139+- Microsoft Azure
1140+- Google Cloud Platform
1141+- Oracle Cloud Infrastructure
1142+- Softlayer
1143+- Rackspace Public Cloud
1144+- IBM Cloud
1145+- Digital Ocean
1146+- Bigstep
1147+- Hetzner
1148+- Joyent
1149+- CloudSigma
1150+- Alibaba Cloud
1151+- OVH
1152+- OpenNebula
1153+- Exoscale
1154+- Scaleway
1155+- CloudStack
1156+- AltCloud
1157+- SmartOS
1158
1159-So ask your distribution provider where you can obtain an image with it built-in if one is not already available ☺
1160+Additionally, cloud-init is supported on these private clouds:
1161
1162+- Bare metal installs
1163+- OpenStack
1164+- LXD
1165+- KVM
1166+- Metal-as-a-Service (MAAS)
1167
1168-.. _Ubuntu Cloud Images: http://cloud-images.ubuntu.com/
1169-.. _Ubuntu: http://www.ubuntu.com/
1170-.. vi: textwidth=78
1171+.. vi: textwidth=79
1172diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst
1173index 2148cd5..8e58be9 100644
1174--- a/doc/rtd/topics/datasources.rst
1175+++ b/doc/rtd/topics/datasources.rst
1176@@ -1,89 +1,57 @@
1177 .. _datasources:
1178
1179-***********
1180 Datasources
1181 ***********
1182
1183-What is a datasource?
1184-=====================
1185-
1186 Datasources are sources of configuration data for cloud-init that typically
1187-come from the user (aka userdata) or come from the stack that created the
1188-configuration drive (aka metadata). Typical userdata would include files,
1189+come from the user (e.g. userdata) or come from the cloud that created the
1190+configuration drive (e.g. metadata). Typical userdata would include files,
1191 yaml, and shell scripts while typical metadata would include server name,
1192-instance id, display name and other cloud specific details. Since there are
1193-multiple ways to provide this data (each cloud solution seems to prefer its
1194-own way) internally a datasource abstract class was created to allow for a
1195-single way to access the different cloud systems methods to provide this data
1196-through the typical usage of subclasses.
1197-
1198-Any metadata processed by cloud-init's datasources is persisted as
1199-``/run/cloud-init/instance-data.json``. Cloud-init provides tooling
1200-to quickly introspect some of that data. See :ref:`instance_metadata` for
1201-more information.
1202-
1203-
1204-Datasource API
1205---------------
1206-The current interface that a datasource object must provide is the following:
1207-
1208-.. sourcecode:: python
1209+instance id, display name and other cloud specific details.
1210
1211- # returns a mime multipart message that contains
1212- # all the various fully-expanded components that
1213- # were found from processing the raw userdata string
1214- # - when filtering only the mime messages targeting
1215- # this instance id will be returned (or messages with
1216- # no instance id)
1217- def get_userdata(self, apply_filter=False)
1218-
1219- # returns the raw userdata string (or none)
1220- def get_userdata_raw(self)
1221+Since there are multiple ways to provide this data (each cloud solution seems
1222+to prefer its own way) internally a datasource abstract class was created to
1223+allow for a single way to access the different cloud systems methods to provide
1224+this data through the typical usage of subclasses.
1225
1226- # returns a integer (or none) which can be used to identify
1227- # this instance in a group of instances which are typically
1228- # created from a single command, thus allowing programmatic
1229- # filtering on this launch index (or other selective actions)
1230- @property
1231- def launch_index(self)
1232-
1233- # the data sources' config_obj is a cloud-config formatted
1234- # object that came to it from ways other than cloud-config
1235- # because cloud-config content would be handled elsewhere
1236- def get_config_obj(self)
1237-
1238- #returns a list of public ssh keys
1239- def get_public_ssh_keys(self)
1240-
1241- # translates a device 'short' name into the actual physical device
1242- # fully qualified name (or none if said physical device is not attached
1243- # or does not exist)
1244- def device_name_to_device(self, name)
1245+Any metadata processed by cloud-init's datasources is persisted as
1246+``/run/cloud-init/instance-data.json``. Cloud-init provides tooling to quickly
1247+introspect some of that data. See :ref:`instance_metadata` for more
1248+information.
1249
1250- # gets the locale string this instance should be applying
1251- # which typically used to adjust the instances locale settings files
1252- def get_locale(self)
1253+Known Sources
1254+=============
1255
1256- @property
1257- def availability_zone(self)
1258+The following is a list of documents for each supported datasource:
1259
1260- # gets the instance id that was assigned to this instance by the
1261- # cloud provider or when said instance id does not exist in the backing
1262- # metadata this will return 'iid-datasource'
1263- def get_instance_id(self)
1264+.. toctree::
1265+ :titlesonly:
1266
1267- # gets the fully qualified domain name that this host should be using
1268- # when configuring network or hostname releated settings, typically
1269- # assigned either by the cloud provider or the user creating the vm
1270- def get_hostname(self, fqdn=False)
1271+ datasources/aliyun.rst
1272+ datasources/altcloud.rst
1273+ datasources/ec2.rst
1274+ datasources/azure.rst
1275+ datasources/cloudsigma.rst
1276+ datasources/cloudstack.rst
1277+ datasources/configdrive.rst
1278+ datasources/digitalocean.rst
1279+ datasources/exoscale.rst
1280+ datasources/fallback.rst
1281+ datasources/gce.rst
1282+ datasources/maas.rst
1283+ datasources/nocloud.rst
1284+ datasources/opennebula.rst
1285+ datasources/openstack.rst
1286+ datasources/oracle.rst
1287+ datasources/ovf.rst
1288+ datasources/smartos.rst
1289
1290- def get_package_mirror_info(self)
1291
1292+Creation
1293+========
1294
1295-Adding a new Datasource
1296------------------------
1297 The datasource objects have a few touch points with cloud-init. If you
1298-are interested in adding a new datasource for your cloud platform you'll
1299+are interested in adding a new datasource for your cloud platform you will
1300 need to take care of the following items:
1301
1302 * **Identify a mechanism for positive identification of the platform**:
1303@@ -139,31 +107,61 @@ need to take care of the following items:
1304 file in ``doc/datasources/<cloudplatform>.rst``
1305
1306
1307-Datasource Documentation
1308-========================
1309-The following is a list of the implemented datasources.
1310-Follow for more information.
1311+API
1312+===
1313
1314-.. toctree::
1315- :maxdepth: 2
1316+The current interface that a datasource object must provide is the following:
1317
1318- datasources/aliyun.rst
1319- datasources/altcloud.rst
1320- datasources/azure.rst
1321- datasources/cloudsigma.rst
1322- datasources/cloudstack.rst
1323- datasources/configdrive.rst
1324- datasources/digitalocean.rst
1325- datasources/ec2.rst
1326- datasources/exoscale.rst
1327- datasources/maas.rst
1328- datasources/nocloud.rst
1329- datasources/opennebula.rst
1330- datasources/openstack.rst
1331- datasources/oracle.rst
1332- datasources/ovf.rst
1333- datasources/smartos.rst
1334- datasources/fallback.rst
1335- datasources/gce.rst
1336+.. sourcecode:: python
1337+
1338+ # returns a mime multipart message that contains
1339+ # all the various fully-expanded components that
1340+ # were found from processing the raw user data string
1341+ # - when filtering only the mime messages targeting
1342+ # this instance id will be returned (or messages with
1343+ # no instance id)
1344+ def get_userdata(self, apply_filter=False)
1345+
1346+ # returns the raw userdata string (or none)
1347+ def get_userdata_raw(self)
1348+
1349+ # returns a integer (or none) which can be used to identify
1350+ # this instance in a group of instances which are typically
1351+ # created from a single command, thus allowing programmatic
1352+ # filtering on this launch index (or other selective actions)
1353+ @property
1354+ def launch_index(self)
1355+
1356+ # the data sources' config_obj is a cloud-config formatted
1357+ # object that came to it from ways other than cloud-config
1358+ # because cloud-config content would be handled elsewhere
1359+ def get_config_obj(self)
1360+
1361+ #returns a list of public ssh keys
1362+ def get_public_ssh_keys(self)
1363+
1364+ # translates a device 'short' name into the actual physical device
1365+ # fully qualified name (or none if said physical device is not attached
1366+ # or does not exist)
1367+ def device_name_to_device(self, name)
1368+
1369+ # gets the locale string this instance should be applying
1370+ # which typically used to adjust the instances locale settings files
1371+ def get_locale(self)
1372+
1373+ @property
1374+ def availability_zone(self)
1375+
1376+ # gets the instance id that was assigned to this instance by the
1377+ # cloud provider or when said instance id does not exist in the backing
1378+ # metadata this will return 'iid-datasource'
1379+ def get_instance_id(self)
1380+
1381+ # gets the fully qualified domain name that this host should be using
1382+ # when configuring network or hostname related settings, typically
1383+ # assigned either by the cloud provider or the user creating the vm
1384+ def get_hostname(self, fqdn=False)
1385+
1386+ def get_package_mirror_info(self)
1387
1388-.. vi: textwidth=78
1389+.. vi: textwidth=79
1390diff --git a/doc/rtd/topics/datasources/altcloud.rst b/doc/rtd/topics/datasources/altcloud.rst
1391index eeb197f..9d7e3de 100644
1392--- a/doc/rtd/topics/datasources/altcloud.rst
1393+++ b/doc/rtd/topics/datasources/altcloud.rst
1394@@ -3,24 +3,25 @@
1395 Alt Cloud
1396 =========
1397
1398-The datasource altcloud will be used to pick up user data on `RHEVm`_ and `vSphere`_.
1399+The datasource altcloud will be used to pick up user data on `RHEVm`_ and
1400+`vSphere`_.
1401
1402 RHEVm
1403 -----
1404
1405 For `RHEVm`_ v3.0 the userdata is injected into the VM using floppy
1406-injection via the `RHEVm`_ dashboard "Custom Properties".
1407+injection via the `RHEVm`_ dashboard "Custom Properties".
1408
1409 The format of the Custom Properties entry must be:
1410
1411 ::
1412-
1413+
1414 floppyinject=user-data.txt:<base64 encoded data>
1415
1416 For example to pass a simple bash script:
1417
1418 .. sourcecode:: sh
1419-
1420+
1421 % cat simple_script.bash
1422 #!/bin/bash
1423 echo "Hello Joe!" >> /tmp/JJV_Joe_out.txt
1424@@ -38,7 +39,7 @@ set the "Custom Properties" when creating the RHEMv v3.0 VM to:
1425 **NOTE:** The prefix with file name must be: ``floppyinject=user-data.txt:``
1426
1427 It is also possible to launch a `RHEVm`_ v3.0 VM and pass optional user
1428-data to it using the Delta Cloud.
1429+data to it using the Delta Cloud.
1430
1431 For more information on Delta Cloud see: http://deltacloud.apache.org
1432
1433@@ -46,12 +47,12 @@ vSphere
1434 -------
1435
1436 For VMWare's `vSphere`_ the userdata is injected into the VM as an ISO
1437-via the cdrom. This can be done using the `vSphere`_ dashboard
1438+via the cdrom. This can be done using the `vSphere`_ dashboard
1439 by connecting an ISO image to the CD/DVD drive.
1440
1441 To pass this example script to cloud-init running in a `vSphere`_ VM
1442 set the CD/DVD drive when creating the vSphere VM to point to an
1443-ISO on the data store.
1444+ISO on the data store.
1445
1446 **Note:** The ISO must contain the user data.
1447
1448@@ -61,13 +62,13 @@ Create the ISO
1449 ^^^^^^^^^^^^^^
1450
1451 .. sourcecode:: sh
1452-
1453+
1454 % mkdir my-iso
1455
1456 NOTE: The file name on the ISO must be: ``user-data.txt``
1457
1458 .. sourcecode:: sh
1459-
1460+
1461 % cp simple_script.bash my-iso/user-data.txt
1462 % genisoimage -o user-data.iso -r my-iso
1463
1464@@ -75,7 +76,7 @@ Verify the ISO
1465 ^^^^^^^^^^^^^^
1466
1467 .. sourcecode:: sh
1468-
1469+
1470 % sudo mkdir /media/vsphere_iso
1471 % sudo mount -o loop user-data.iso /media/vsphere_iso
1472 % cat /media/vsphere_iso/user-data.txt
1473@@ -84,7 +85,7 @@ Verify the ISO
1474 Then, launch the `vSphere`_ VM the ISO user-data.iso attached as a CDROM.
1475
1476 It is also possible to launch a `vSphere`_ VM and pass optional user
1477-data to it using the Delta Cloud.
1478+data to it using the Delta Cloud.
1479
1480 For more information on Delta Cloud see: http://deltacloud.apache.org
1481
1482diff --git a/doc/rtd/topics/datasources/azure.rst b/doc/rtd/topics/datasources/azure.rst
1483index b41cddd..8328dfa 100644
1484--- a/doc/rtd/topics/datasources/azure.rst
1485+++ b/doc/rtd/topics/datasources/azure.rst
1486@@ -82,7 +82,8 @@ The settings that may be configured are:
1487 provided command to obtain metadata.
1488 * **apply_network_config**: Boolean set to True to use network configuration
1489 described by Azure's IMDS endpoint instead of fallback network config of
1490- dhcp on eth0. Default is True. For Ubuntu 16.04 or earlier, default is False.
1491+ dhcp on eth0. Default is True. For Ubuntu 16.04 or earlier, default is
1492+ False.
1493 * **data_dir**: Path used to read metadata files and write crawled data.
1494 * **dhclient_lease_file**: The fallback lease file to source when looking for
1495 custom DHCP option 245 from Azure fabric.
1496diff --git a/doc/rtd/topics/datasources/cloudstack.rst b/doc/rtd/topics/datasources/cloudstack.rst
1497index a3101ed..95b9587 100644
1498--- a/doc/rtd/topics/datasources/cloudstack.rst
1499+++ b/doc/rtd/topics/datasources/cloudstack.rst
1500@@ -7,7 +7,7 @@ CloudStack
1501 sshkey thru the Virtual-Router. The datasource obtains the VR address via
1502 dhcp lease information given to the instance.
1503 For more details on meta-data and user-data,
1504-refer the `CloudStack Administrator Guide`_.
1505+refer the `CloudStack Administrator Guide`_.
1506
1507 URLs to access user-data and meta-data from the Virtual Machine. Here 10.1.1.1
1508 is the Virtual Router IP:
1509diff --git a/doc/rtd/topics/datasources/configdrive.rst b/doc/rtd/topics/datasources/configdrive.rst
1510index f1a488a..f4c5a34 100644
1511--- a/doc/rtd/topics/datasources/configdrive.rst
1512+++ b/doc/rtd/topics/datasources/configdrive.rst
1513@@ -64,7 +64,7 @@ The following criteria are required to as a config drive:
1514 ::
1515
1516 openstack/
1517- - 2012-08-10/ or latest/
1518+ - 2012-08-10/ or latest/
1519 - meta_data.json
1520 - user_data (not mandatory)
1521 - content/
1522@@ -83,7 +83,7 @@ only) file in the following ways.
1523
1524 ::
1525
1526- dsmode:
1527+ dsmode:
1528 values: local, net, pass
1529 default: pass
1530
1531@@ -97,10 +97,10 @@ The difference between 'local' and 'net' is that local will not require
1532 networking to be up before user-data actions (or boothooks) are run.
1533
1534 ::
1535-
1536+
1537 instance-id:
1538 default: iid-dsconfigdrive
1539-
1540+
1541 This is utilized as the metadata's instance-id. It should generally
1542 be unique, as it is what is used to determine "is this a new instance".
1543
1544@@ -108,18 +108,18 @@ be unique, as it is what is used to determine "is this a new instance".
1545
1546 public-keys:
1547 default: None
1548-
1549+
1550 If present, these keys will be used as the public keys for the
1551 instance. This value overrides the content in authorized_keys.
1552
1553 Note: it is likely preferable to provide keys via user-data
1554
1555 ::
1556-
1557+
1558 user-data:
1559 default: None
1560-
1561-This provides cloud-init user-data. See :ref:`examples <yaml_examples>` for
1562+
1563+This provides cloud-init user-data. See :ref:`examples <yaml_examples>` for
1564 what all can be present here.
1565
1566 .. _OpenStack: http://www.openstack.org/
1567diff --git a/doc/rtd/topics/datasources/digitalocean.rst b/doc/rtd/topics/datasources/digitalocean.rst
1568index 938ede8..88f1e5f 100644
1569--- a/doc/rtd/topics/datasources/digitalocean.rst
1570+++ b/doc/rtd/topics/datasources/digitalocean.rst
1571@@ -20,8 +20,10 @@ DigitalOcean's datasource can be configured as follows:
1572 retries: 3
1573 timeout: 2
1574
1575-- *retries*: Determines the number of times to attempt to connect to the metadata service
1576-- *timeout*: Determines the timeout in seconds to wait for a response from the metadata service
1577+- *retries*: Determines the number of times to attempt to connect to the
1578+ metadata service
1579+- *timeout*: Determines the timeout in seconds to wait for a response from the
1580+ metadata service
1581
1582 .. _DigitalOcean: http://digitalocean.com/
1583 .. _metadata service: https://developers.digitalocean.com/metadata/
1584diff --git a/doc/rtd/topics/datasources/ec2.rst b/doc/rtd/topics/datasources/ec2.rst
1585index 76beca9..a90f377 100644
1586--- a/doc/rtd/topics/datasources/ec2.rst
1587+++ b/doc/rtd/topics/datasources/ec2.rst
1588@@ -13,7 +13,7 @@ instance metadata.
1589 Metadata is accessible via the following URL:
1590
1591 ::
1592-
1593+
1594 GET http://169.254.169.254/2009-04-04/meta-data/
1595 ami-id
1596 ami-launch-index
1597@@ -34,19 +34,20 @@ Metadata is accessible via the following URL:
1598 Userdata is accessible via the following URL:
1599
1600 ::
1601-
1602+
1603 GET http://169.254.169.254/2009-04-04/user-data
1604 1234,fred,reboot,true | 4512,jimbo, | 173,,,
1605
1606 Note that there are multiple versions of this data provided, cloud-init
1607 by default uses **2009-04-04** but newer versions can be supported with
1608 relative ease (newer versions have more data exposed, while maintaining
1609-backward compatibility with the previous versions).
1610+backward compatibility with the previous versions).
1611
1612-To see which versions are supported from your cloud provider use the following URL:
1613+To see which versions are supported from your cloud provider use the following
1614+URL:
1615
1616 ::
1617-
1618+
1619 GET http://169.254.169.254/
1620 1.0
1621 2007-01-19
1622diff --git a/doc/rtd/topics/datasources/exoscale.rst b/doc/rtd/topics/datasources/exoscale.rst
1623index 27aec9c..9074edc 100644
1624--- a/doc/rtd/topics/datasources/exoscale.rst
1625+++ b/doc/rtd/topics/datasources/exoscale.rst
1626@@ -26,8 +26,8 @@ In the password server case, the following rules apply in order to enable the
1627 "restore instance password" functionality:
1628
1629 * If a password is returned by the password server, it is then marked "saved"
1630- by the cloud-init datasource. Subsequent boots will skip setting the password
1631- (the password server will return "saved_password").
1632+ by the cloud-init datasource. Subsequent boots will skip setting the
1633+ password (the password server will return "saved_password").
1634 * When the instance password is reset (via the Exoscale UI), the password
1635 server will return the non-empty password at next boot, therefore causing
1636 cloud-init to reset the instance's password.
1637@@ -38,15 +38,15 @@ Configuration
1638 Users of this datasource are discouraged from changing the default settings
1639 unless instructed to by Exoscale support.
1640
1641-The following settings are available and can be set for the datasource in system
1642-configuration (in `/etc/cloud/cloud.cfg.d/`).
1643+The following settings are available and can be set for the datasource in
1644+system configuration (in `/etc/cloud/cloud.cfg.d/`).
1645
1646 The settings available are:
1647
1648 * **metadata_url**: The URL for the metadata service (defaults to
1649 ``http://169.254.169.254``)
1650- * **api_version**: The API version path on which to query the instance metadata
1651- (defaults to ``1.0``)
1652+ * **api_version**: The API version path on which to query the instance
1653+ metadata (defaults to ``1.0``)
1654 * **password_server_port**: The port (on the metadata server) on which the
1655 password server listens (defaults to ``8080``).
1656 * **timeout**: the timeout value provided to urlopen for each individual http
1657diff --git a/doc/rtd/topics/datasources/nocloud.rst b/doc/rtd/topics/datasources/nocloud.rst
1658index 1c5cf96..bc96f7f 100644
1659--- a/doc/rtd/topics/datasources/nocloud.rst
1660+++ b/doc/rtd/topics/datasources/nocloud.rst
1661@@ -57,24 +57,24 @@ Given a disk ubuntu 12.04 cloud image in 'disk.img', you can create a
1662 sufficient disk by following the example below.
1663
1664 ::
1665-
1666+
1667 ## create user-data and meta-data files that will be used
1668 ## to modify image on first boot
1669 $ { echo instance-id: iid-local01; echo local-hostname: cloudimg; } > meta-data
1670-
1671+
1672 $ printf "#cloud-config\npassword: passw0rd\nchpasswd: { expire: False }\nssh_pwauth: True\n" > user-data
1673-
1674+
1675 ## create a disk to attach with some user-data and meta-data
1676 $ genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-data
1677-
1678+
1679 ## alternatively, create a vfat filesystem with same files
1680 ## $ truncate --size 2M seed.img
1681 ## $ mkfs.vfat -n cidata seed.img
1682 ## $ mcopy -oi seed.img user-data meta-data ::
1683-
1684+
1685 ## create a new qcow image to boot, backed by your original image
1686 $ qemu-img create -f qcow2 -b disk.img boot-disk.img
1687-
1688+
1689 ## boot the image and login as 'ubuntu' with password 'passw0rd'
1690 ## note, passw0rd was set as password through the user-data above,
1691 ## there is no password set on these images.
1692@@ -88,12 +88,12 @@ to determine if this is "first boot". So if you are making updates to
1693 user-data you will also have to change that, or start the disk fresh.
1694
1695 Also, you can inject an ``/etc/network/interfaces`` file by providing the
1696-content for that file in the ``network-interfaces`` field of metadata.
1697+content for that file in the ``network-interfaces`` field of metadata.
1698
1699 Example metadata:
1700
1701 ::
1702-
1703+
1704 instance-id: iid-abcdefg
1705 network-interfaces: |
1706 iface eth0 inet static
1707diff --git a/doc/rtd/topics/datasources/opennebula.rst b/doc/rtd/topics/datasources/opennebula.rst
1708index 7c0367c..8e7c255 100644
1709--- a/doc/rtd/topics/datasources/opennebula.rst
1710+++ b/doc/rtd/topics/datasources/opennebula.rst
1711@@ -21,7 +21,7 @@ Datasource configuration
1712 Datasource accepts following configuration options.
1713
1714 ::
1715-
1716+
1717 dsmode:
1718 values: local, net, disabled
1719 default: net
1720@@ -30,7 +30,7 @@ Tells if this datasource will be processed in 'local' (pre-networking) or
1721 'net' (post-networking) stage or even completely 'disabled'.
1722
1723 ::
1724-
1725+
1726 parseuser:
1727 default: nobody
1728
1729@@ -46,7 +46,7 @@ The following criteria are required:
1730 or have a *filesystem* label of **CONTEXT** or **CDROM**
1731 2. Must contain file *context.sh* with contextualization variables.
1732 File is generated by OpenNebula, it has a KEY='VALUE' format and
1733- can be easily read by bash
1734+ can be easily read by bash
1735
1736 Contextualization variables
1737 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
1738@@ -57,7 +57,7 @@ the OpenNebula documentation. Where multiple similar variables are
1739 specified, only first found is taken.
1740
1741 ::
1742-
1743+
1744 DSMODE
1745
1746 Datasource mode configuration override. Values: local, net, disabled.
1747@@ -75,30 +75,30 @@ Datasource mode configuration override. Values: local, net, disabled.
1748 Static `network configuration`_.
1749
1750 ::
1751-
1752+
1753 HOSTNAME
1754
1755 Instance hostname.
1756
1757 ::
1758-
1759+
1760 PUBLIC_IP
1761 IP_PUBLIC
1762 ETH0_IP
1763
1764 If no hostname has been specified, cloud-init will try to create hostname
1765-from instance's IP address in 'local' dsmode. In 'net' dsmode, cloud-init
1766+from instance's IP address in 'local' dsmode. In 'net' dsmode, cloud-init
1767 tries to resolve one of its IP addresses to get hostname.
1768
1769 ::
1770-
1771+
1772 SSH_KEY
1773 SSH_PUBLIC_KEY
1774
1775 One or multiple SSH keys (separated by newlines) can be specified.
1776
1777 ::
1778-
1779+
1780 USER_DATA
1781 USERDATA
1782
1783@@ -111,7 +111,7 @@ This example cloud-init configuration (*cloud.cfg*) enables
1784 OpenNebula datasource only in 'net' mode.
1785
1786 ::
1787-
1788+
1789 disable_ec2_metadata: True
1790 datasource_list: ['OpenNebula']
1791 datasource:
1792@@ -123,17 +123,17 @@ Example VM's context section
1793 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1794
1795 ::
1796-
1797+
1798 CONTEXT=[
1799 PUBLIC_IP="$NIC[IP]",
1800- SSH_KEY="$USER[SSH_KEY]
1801- $USER[SSH_KEY1]
1802+ SSH_KEY="$USER[SSH_KEY]
1803+ $USER[SSH_KEY1]
1804 $USER[SSH_KEY2] ",
1805 USER_DATA="#cloud-config
1806 # see https://help.ubuntu.com/community/CloudInit
1807-
1808+
1809 packages: []
1810-
1811+
1812 mounts:
1813 - [vdc,none,swap,sw,0,0]
1814 runcmd:
1815diff --git a/doc/rtd/topics/datasources/openstack.rst b/doc/rtd/topics/datasources/openstack.rst
1816index 421da08..8ce2a53 100644
1817--- a/doc/rtd/topics/datasources/openstack.rst
1818+++ b/doc/rtd/topics/datasources/openstack.rst
1819@@ -78,6 +78,7 @@ upgrade packages and install ``htop`` on all instances:
1820 {"cloud-init": "#cloud-config\npackage_upgrade: True\npackages:\n - htop"}
1821
1822 For more general information about how cloud-init handles vendor data,
1823-including how it can be disabled by users on instances, see :doc:`/topics/vendordata`.
1824+including how it can be disabled by users on instances, see
1825+:doc:`/topics/vendordata`.
1826
1827 .. vi: textwidth=78
1828diff --git a/doc/rtd/topics/datasources/smartos.rst b/doc/rtd/topics/datasources/smartos.rst
1829index cb9a128..be11dfb 100644
1830--- a/doc/rtd/topics/datasources/smartos.rst
1831+++ b/doc/rtd/topics/datasources/smartos.rst
1832@@ -15,7 +15,8 @@ second serial console. On Linux, this is /dev/ttyS1. The data is a provided
1833 via a simple protocol: something queries for the data, the console responds
1834 responds with the status and if "SUCCESS" returns until a single ".\n".
1835
1836-New versions of the SmartOS tooling will include support for base64 encoded data.
1837+New versions of the SmartOS tooling will include support for base64 encoded
1838+data.
1839
1840 Meta-data channels
1841 ------------------
1842@@ -27,7 +28,7 @@ channels of SmartOS.
1843
1844 - per the spec, user-data is for consumption by the end-user, not
1845 provisioning tools
1846- - cloud-init entirely ignores this channel other than writting it to disk
1847+ - cloud-init entirely ignores this channel other than writing it to disk
1848 - removal of the meta-data key means that /var/db/user-data gets removed
1849 - a backup of previous meta-data is maintained as
1850 /var/db/user-data.<timestamp>. <timestamp> is the epoch time when
1851@@ -42,8 +43,9 @@ channels of SmartOS.
1852 - <timestamp> is the epoch time when cloud-init ran.
1853 - when the 'user-script' meta-data key goes missing, the user-script is
1854 removed from the file system, although a backup is maintained.
1855- - if the script is not shebanged (i.e. starts with #!<executable>), then
1856- or is not an executable, cloud-init will add a shebang of "#!/bin/bash"
1857+ - if the script does not start with a shebang (i.e. starts with
1858+ #!<executable>), then or is not an executable, cloud-init will add a
1859+ shebang of "#!/bin/bash"
1860
1861 * cloud-init:user-data is treated like on other Clouds.
1862
1863@@ -133,7 +135,7 @@ or not to base64 decode something:
1864 * base64_all: Except for excluded keys, attempt to base64 decode
1865 the values. If the value fails to decode properly, it will be
1866 returned in its text
1867- * base64_keys: A comma deliminated list of which keys are base64 encoded.
1868+ * base64_keys: A comma delimited list of which keys are base64 encoded.
1869 * b64-<key>:
1870 for any key, if there exists an entry in the metadata for 'b64-<key>'
1871 Then 'b64-<key>' is expected to be a plaintext boolean indicating whether
1872diff --git a/doc/rtd/topics/debugging.rst b/doc/rtd/topics/debugging.rst
1873index e13d915..afcf267 100644
1874--- a/doc/rtd/topics/debugging.rst
1875+++ b/doc/rtd/topics/debugging.rst
1876@@ -68,18 +68,18 @@ subcommands default to reading /var/log/cloud-init.log.
1877 00.00100s (modules-final/config-rightscale_userdata)
1878 ...
1879
1880-* ``analyze boot`` Make subprocess calls to the kernel in order to get relevant
1881+* ``analyze boot`` Make subprocess calls to the kernel in order to get relevant
1882 pre-cloud-init timestamps, such as the kernel start, kernel finish boot, and cloud-init start.
1883
1884 .. code-block:: shell-session
1885
1886- $ cloud-init analyze boot
1887+ $ cloud-init analyze boot
1888 -- Most Recent Boot Record --
1889- Kernel Started at: 2019-06-13 15:59:55.809385
1890- Kernel ended boot at: 2019-06-13 16:00:00.944740
1891- Kernel time to boot (seconds): 5.135355
1892- Cloud-init start: 2019-06-13 16:00:05.738396
1893- Time between Kernel boot and Cloud-init start (seconds): 4.793656
1894+ Kernel Started at: 2019-06-13 15:59:55.809385
1895+ Kernel ended boot at: 2019-06-13 16:00:00.944740
1896+ Kernel time to boot (seconds): 5.135355
1897+ Cloud-init start: 2019-06-13 16:00:05.738396
1898+ Time between Kernel boot and Cloud-init start (seconds): 4.793656
1899
1900
1901 Analyze quickstart - LXC
1902diff --git a/doc/rtd/topics/dir_layout.rst b/doc/rtd/topics/dir_layout.rst
1903index 7a6265e..ebd63ae 100644
1904--- a/doc/rtd/topics/dir_layout.rst
1905+++ b/doc/rtd/topics/dir_layout.rst
1906@@ -2,11 +2,12 @@
1907 Directory layout
1908 ****************
1909
1910-Cloudinits's directory structure is somewhat different from a regular application::
1911+Cloud-init's directory structure is somewhat different from a regular
1912+application::
1913
1914 /var/lib/cloud/
1915 - data/
1916- - instance-id
1917+ - instance-id
1918 - previous-instance-id
1919 - datasource
1920 - previous-datasource
1921@@ -35,38 +36,41 @@ Cloudinits's directory structure is somewhat different from a regular applicatio
1922
1923 The main directory containing the cloud-init specific subdirectories.
1924 It is typically located at ``/var/lib`` but there are certain configuration
1925- scenarios where this can be altered.
1926+ scenarios where this can be altered.
1927
1928 TBD, describe this overriding more.
1929
1930 ``data/``
1931
1932- Contains information related to instance ids, datasources and hostnames of the previous
1933- and current instance if they are different. These can be examined as needed to
1934- determine any information related to a previous boot (if applicable).
1935+ Contains information related to instance ids, datasources and hostnames of
1936+ the previous and current instance if they are different. These can be
1937+ examined as needed to determine any information related to a previous boot
1938+ (if applicable).
1939
1940 ``handlers/``
1941
1942- Custom ``part-handlers`` code is written out here. Files that end up here are written
1943- out with in the scheme of ``part-handler-XYZ`` where ``XYZ`` is the handler number (the
1944- first handler found starts at 0).
1945+ Custom ``part-handlers`` code is written out here. Files that end up here are
1946+ written out with in the scheme of ``part-handler-XYZ`` where ``XYZ`` is the
1947+ handler number (the first handler found starts at 0).
1948
1949
1950 ``instance``
1951
1952- A symlink to the current ``instances/`` subdirectory that points to the currently
1953- active instance (which is active is dependent on the datasource loaded).
1954+ A symlink to the current ``instances/`` subdirectory that points to the
1955+ currently active instance (which is active is dependent on the datasource
1956+ loaded).
1957
1958 ``instances/``
1959
1960- All instances that were created using this image end up with instance identifier
1961- subdirectories (and corresponding data for each instance). The currently active
1962- instance will be symlinked the ``instance`` symlink file defined previously.
1963+ All instances that were created using this image end up with instance
1964+ identifier subdirectories (and corresponding data for each instance). The
1965+ currently active instance will be symlinked the ``instance`` symlink file
1966+ defined previously.
1967
1968 ``scripts/``
1969
1970- Scripts that are downloaded/created by the corresponding ``part-handler`` will end up
1971- in one of these subdirectories.
1972+ Scripts that are downloaded/created by the corresponding ``part-handler``
1973+ will end up in one of these subdirectories.
1974
1975 ``seed/``
1976
1977@@ -77,6 +81,7 @@ Cloudinits's directory structure is somewhat different from a regular applicatio
1978 Cloud-init has a concept of a module semaphore, which basically consists
1979 of the module name and its frequency. These files are used to ensure a module
1980 is only ran `per-once`, `per-instance`, `per-always`. This folder contains
1981- semaphore `files` which are only supposed to run `per-once` (not tied to the instance id).
1982+ semaphore `files` which are only supposed to run `per-once` (not tied to the
1983+ instance id).
1984
1985 .. vi: textwidth=78
1986diff --git a/doc/rtd/topics/docs.rst b/doc/rtd/topics/docs.rst
1987new file mode 100644
1988index 0000000..1b15377
1989--- /dev/null
1990+++ b/doc/rtd/topics/docs.rst
1991@@ -0,0 +1,84 @@
1992+.. _docs:
1993+
1994+Docs
1995+****
1996+
1997+These docs are hosted on Read the Docs. The following will explain how to
1998+contribute to and build these docs locally.
1999+
2000+The documentation is primarily written in reStructuredText.
2001+
2002+
2003+Building
2004+========
2005+
2006+There is a makefile target to build the documentation for you:
2007+
2008+.. code-block:: shell-session
2009+
2010+ $ tox -e doc
2011+
2012+This will do two things:
2013+
2014+- Build the documentation using sphinx
2015+- Run doc8 against the documentation source code
2016+
2017+Once build the HTML files will be viewable in ``doc/rtd_html``. Use your
2018+web browser to open ``index.html`` to view and navigate the site.
2019+
2020+Style Guide
2021+===========
2022+
2023+Headings
2024+--------
2025+The headings used across the documentation use the following hierarchy:
2026+
2027+- ``*****``: used once atop of a new page
2028+- ``=====``: each sections on the page
2029+- ``-----``: subsections
2030+- ``^^^^^``: sub-subsections
2031+- ``"""""``: paragraphs
2032+
2033+The top level header ``######`` is reserved for the first page.
2034+
2035+If under and overline are used, their length must be identical. The length of
2036+the underline must be at least as long as the title itself
2037+
2038+Line Length
2039+-----------
2040+Please keep the line lengths to a maximum of **79** characters. This ensures
2041+that the pages and tables do not get too wide that side scrolling is required.
2042+
2043+Header
2044+------
2045+Adding a link at the top of the page allows for the page to be referenced by
2046+other pages. For example for the FAQ page this would be:
2047+
2048+.. code-block:: rst
2049+
2050+ .. _faq:
2051+
2052+Footer
2053+------
2054+The footer should include the textwidth
2055+
2056+.. code-block:: rst
2057+
2058+ .. vi: textwidth=79
2059+
2060+Vertical Whitespace
2061+-------------------
2062+One newline between each section helps ensure readability of the documentation
2063+source code.
2064+
2065+Common Words
2066+------------
2067+There are some common words that should follow specific usage:
2068+
2069+- ``cloud-init``: always lower case with a hyphen, unless starting a sentence
2070+ in which case only the 'C' is capitalized (e.g. ``Cloud-init``).
2071+- ``metadata``: one word
2072+- ``user data``: two words, not to be combined
2073+- ``vendor data``: like user data, it is two words
2074+
2075+.. vi: textwidth=79
2076diff --git a/doc/rtd/topics/examples.rst b/doc/rtd/topics/examples.rst
2077index c30d226..62b8ee4 100644
2078--- a/doc/rtd/topics/examples.rst
2079+++ b/doc/rtd/topics/examples.rst
2080@@ -134,7 +134,7 @@ Configure instances ssh-keys
2081 .. literalinclude:: ../../examples/cloud-config-ssh-keys.txt
2082 :language: yaml
2083 :linenos:
2084-
2085+
2086 Additional apt configuration
2087 ============================
2088
2089diff --git a/doc/rtd/topics/faq.rst b/doc/rtd/topics/faq.rst
2090new file mode 100644
2091index 0000000..16e19c2
2092--- /dev/null
2093+++ b/doc/rtd/topics/faq.rst
2094@@ -0,0 +1,43 @@
2095+.. _faq:
2096+
2097+FAQ
2098+***
2099+
2100+Getting help
2101+============
2102+
2103+Having trouble? We would like to help!
2104+
2105+- Use the search bar at the upper left to search these docs
2106+- Ask a question in the ``#cloud-init`` IRC channel on Freenode
2107+- Join and ask questions on the `cloud-init mailing list <https://launchpad.net/~cloud-init>`_
2108+- Find a bug? `Report bugs on Launchpad <https://bugs.launchpad.net/cloud-init/+filebug>`_
2109+
2110+
2111+Media
2112+=====
2113+
2114+Below are some videos, blog posts, and white papers about cloud-init from a
2115+variety of sources.
2116+
2117+- `Cloud Instance Initialization with cloud-init (Whitepaper)`_
2118+- `cloud-init Summit 2018`_
2119+- `cloud-init - The cross-cloud Magic Sauce (PDF)`_
2120+- `cloud-init Summit 2017`_
2121+- `cloud-init - Building clouds one Linux box at a time (Video)`_
2122+- `cloud-init - Building clouds one Linux box at a time (PDF)`_
2123+- `Metadata and cloud-init`_
2124+- `The beauty of cloud-init`_
2125+- `Introduction to cloud-init`_
2126+
2127+.. _Cloud Instance Initialization with cloud-init (Whitepaper): https://ubuntu.com/blog/cloud-instance-initialisation-with-cloud-init
2128+.. _cloud-init Summit 2018: https://powersj.io/post/cloud-init-summit18/
2129+.. _cloud-init - The cross-cloud Magic Sauce (PDF): https://events.linuxfoundation.org/wp-content/uploads/2017/12/cloud-init-The-cross-cloud-Magic-Sauce-Scott-Moser-Chad-Smith-Canonical.pdf
2130+.. _cloud-init Summit 2017: https://powersj.io/post/cloud-init-summit17/
2131+.. _cloud-init - Building clouds one Linux box at a time (Video): https://www.youtube.com/watch?v=1joQfUZQcPg
2132+.. _cloud-init - Building clouds one Linux box at a time (PDF): https://annex.debconf.org/debconf-share/debconf17/slides/164-cloud-init_Building_clouds_one_Linux_box_at_a_time.pdf
2133+.. _Metadata and cloud-init: https://www.youtube.com/watch?v=RHVhIWifVqU
2134+.. _The beauty of cloud-init: http://brandon.fuller.name/archives/2011/05/02/06.40.57/
2135+.. _Introduction to cloud-init: http://www.youtube.com/watch?v=-zL3BdbKyGY
2136+
2137+.. vi: textwidth=79
2138diff --git a/doc/rtd/topics/format.rst b/doc/rtd/topics/format.rst
2139index 74d1fee..7605040 100644
2140--- a/doc/rtd/topics/format.rst
2141+++ b/doc/rtd/topics/format.rst
2142@@ -4,22 +4,24 @@
2143 User-Data Formats
2144 *****************
2145
2146-User data that will be acted upon by cloud-init must be in one of the following types.
2147+User data that will be acted upon by cloud-init must be in one of the following
2148+types.
2149
2150 Gzip Compressed Content
2151 =======================
2152
2153 Content found to be gzip compressed will be uncompressed.
2154-The uncompressed data will then be used as if it were not compressed.
2155+The uncompressed data will then be used as if it were not compressed.
2156 This is typically useful because user-data is limited to ~16384 [#]_ bytes.
2157
2158 Mime Multi Part Archive
2159 =======================
2160
2161-This list of rules is applied to each part of this multi-part file.
2162+This list of rules is applied to each part of this multi-part file.
2163 Using a mime-multi part file, the user can specify more than one type of data.
2164
2165-For example, both a user data script and a cloud-config type could be specified.
2166+For example, both a user data script and a cloud-config type could be
2167+specified.
2168
2169 Supported content-types:
2170
2171@@ -66,7 +68,8 @@ User-Data Script
2172
2173 Typically used by those who just want to execute a shell script.
2174
2175-Begins with: ``#!`` or ``Content-Type: text/x-shellscript`` when using a MIME archive.
2176+Begins with: ``#!`` or ``Content-Type: text/x-shellscript`` when using a MIME
2177+archive.
2178
2179 .. note::
2180 New in cloud-init v. 18.4: User-data scripts can also render cloud instance
2181@@ -83,25 +86,27 @@ Example
2182 #!/bin/sh
2183 echo "Hello World. The time is now $(date -R)!" | tee /root/output.txt
2184
2185- $ euca-run-instances --key mykey --user-data-file myscript.sh ami-a07d95c9
2186+ $ euca-run-instances --key mykey --user-data-file myscript.sh ami-a07d95c9
2187
2188 Include File
2189 ============
2190
2191 This content is a ``include`` file.
2192
2193-The file contains a list of urls, one per line.
2194-Each of the URLs will be read, and their content will be passed through this same set of rules.
2195-Ie, the content read from the URL can be gzipped, mime-multi-part, or plain text.
2196-If an error occurs reading a file the remaining files will not be read.
2197+The file contains a list of urls, one per line. Each of the URLs will be read,
2198+and their content will be passed through this same set of rules. Ie, the
2199+content read from the URL can be gzipped, mime-multi-part, or plain text. If
2200+an error occurs reading a file the remaining files will not be read.
2201
2202-Begins with: ``#include`` or ``Content-Type: text/x-include-url`` when using a MIME archive.
2203+Begins with: ``#include`` or ``Content-Type: text/x-include-url`` when using
2204+a MIME archive.
2205
2206 Cloud Config Data
2207 =================
2208
2209-Cloud-config is the simplest way to accomplish some things
2210-via user-data. Using cloud-config syntax, the user can specify certain things in a human friendly format.
2211+Cloud-config is the simplest way to accomplish some things via user-data. Using
2212+cloud-config syntax, the user can specify certain things in a human friendly
2213+format.
2214
2215 These things include:
2216
2217@@ -114,9 +119,11 @@ These things include:
2218 .. note::
2219 This file must be valid yaml syntax.
2220
2221-See the :ref:`yaml_examples` section for a commented set of examples of supported cloud config formats.
2222+See the :ref:`yaml_examples` section for a commented set of examples of
2223+supported cloud config formats.
2224
2225-Begins with: ``#cloud-config`` or ``Content-Type: text/cloud-config`` when using a MIME archive.
2226+Begins with: ``#cloud-config`` or ``Content-Type: text/cloud-config`` when
2227+using a MIME archive.
2228
2229 .. note::
2230 New in cloud-init v. 18.4: Cloud config dta can also render cloud instance
2231@@ -126,25 +133,41 @@ Begins with: ``#cloud-config`` or ``Content-Type: text/cloud-config`` when using
2232 Upstart Job
2233 ===========
2234
2235-Content is placed into a file in ``/etc/init``, and will be consumed by upstart as any other upstart job.
2236+Content is placed into a file in ``/etc/init``, and will be consumed by upstart
2237+as any other upstart job.
2238
2239-Begins with: ``#upstart-job`` or ``Content-Type: text/upstart-job`` when using a MIME archive.
2240+Begins with: ``#upstart-job`` or ``Content-Type: text/upstart-job`` when using
2241+a MIME archive.
2242
2243 Cloud Boothook
2244 ==============
2245
2246-This content is ``boothook`` data. It is stored in a file under ``/var/lib/cloud`` and then executed immediately.
2247-This is the earliest ``hook`` available. Note, that there is no mechanism provided for running only once. The boothook must take care of this itself.
2248-It is provided with the instance id in the environment variable ``INSTANCE_ID``. This could be made use of to provide a 'once-per-instance' type of functionality.
2249+This content is ``boothook`` data. It is stored in a file under
2250+``/var/lib/cloud`` and then executed immediately. This is the earliest ``hook``
2251+available. Note, that there is no mechanism provided for running only once. The
2252+boothook must take care of this itself.
2253
2254-Begins with: ``#cloud-boothook`` or ``Content-Type: text/cloud-boothook`` when using a MIME archive.
2255+It is provided with the instance id in the environment variable
2256+``INSTANCE_ID``. This could be made use of to provide a 'once-per-instance'
2257+type of functionality.
2258+
2259+Begins with: ``#cloud-boothook`` or ``Content-Type: text/cloud-boothook`` when
2260+using a MIME archive.
2261
2262 Part Handler
2263 ============
2264
2265-This is a ``part-handler``: It contains custom code for either supporting new mime-types in multi-part user data, or overriding the existing handlers for supported mime-types. It will be written to a file in ``/var/lib/cloud/data`` based on its filename (which is generated).
2266-This must be python code that contains a ``list_types`` function and a ``handle_part`` function.
2267-Once the section is read the ``list_types`` method will be called. It must return a list of mime-types that this part-handler handles. Because mime parts are processed in order, a ``part-handler`` part must precede any parts with mime-types it is expected to handle in the same user data.
2268+This is a ``part-handler``: It contains custom code for either supporting new
2269+mime-types in multi-part user data, or overriding the existing handlers for
2270+supported mime-types. It will be written to a file in ``/var/lib/cloud/data``
2271+based on its filename (which is generated).
2272+
2273+This must be python code that contains a ``list_types`` function and a
2274+``handle_part`` function. Once the section is read the ``list_types`` method
2275+will be called. It must return a list of mime-types that this part-handler
2276+handles. Because mime parts are processed in order, a ``part-handler`` part
2277+must precede any parts with mime-types it is expected to handle in the same
2278+user data.
2279
2280 The ``handle_part`` function must be defined like:
2281
2282@@ -156,11 +179,13 @@ The ``handle_part`` function must be defined like:
2283 # filename = the filename of the part (or a generated filename if none is present in mime data)
2284 # payload = the parts' content
2285
2286-Cloud-init will then call the ``handle_part`` function once before it handles any parts, once per part received, and once after all parts have been handled.
2287-The ``'__begin__'`` and ``'__end__'`` sentinels allow the part handler to do initialization or teardown before or after
2288-receiving any parts.
2289+Cloud-init will then call the ``handle_part`` function once before it handles
2290+any parts, once per part received, and once after all parts have been handled.
2291+The ``'__begin__'`` and ``'__end__'`` sentinels allow the part handler to do
2292+initialization or teardown before or after receiving any parts.
2293
2294-Begins with: ``#part-handler`` or ``Content-Type: text/part-handler`` when using a MIME archive.
2295+Begins with: ``#part-handler`` or ``Content-Type: text/part-handler`` when
2296+using a MIME archive.
2297
2298 Example
2299 -------
2300diff --git a/doc/rtd/topics/merging.rst b/doc/rtd/topics/merging.rst
2301index 5f7ca18..2b5e5da 100644
2302--- a/doc/rtd/topics/merging.rst
2303+++ b/doc/rtd/topics/merging.rst
2304@@ -68,8 +68,10 @@ Cloud-init provides merging for the following built-in types:
2305 The ``Dict`` merger has the following options which control what is done with
2306 values contained within the config.
2307
2308-- ``allow_delete``: Existing values not present in the new value can be deleted, defaults to False
2309-- ``no_replace``: Do not replace an existing value if one is already present, enabled by default.
2310+- ``allow_delete``: Existing values not present in the new value can be
2311+ deleted, defaults to False
2312+- ``no_replace``: Do not replace an existing value if one is already present,
2313+ enabled by default.
2314 - ``replace``: Overwrite existing values with new ones.
2315
2316 The ``List`` merger has the following options which control what is done with
2317@@ -77,7 +79,8 @@ the values contained within the config.
2318
2319 - ``append``: Add new value to the end of the list, defaults to False.
2320 - ``prepend``: Add new values to the start of the list, defaults to False.
2321-- ``no_replace``: Do not replace an existing value if one is already present, enabled by default.
2322+- ``no_replace``: Do not replace an existing value if one is already present,
2323+ enabled by default.
2324 - ``replace``: Overwrite existing values with new ones.
2325
2326 The ``Str`` merger has the following options which control what is done with
2327@@ -88,10 +91,13 @@ the values contained within the config.
2328 Common options for all merge types which control how recursive merging is
2329 done on other types.
2330
2331-- ``recurse_dict``: If True merge the new values of the dictionary, defaults to True.
2332-- ``recurse_list``: If True merge the new values of the list, defaults to False.
2333+- ``recurse_dict``: If True merge the new values of the dictionary, defaults to
2334+ True.
2335+- ``recurse_list``: If True merge the new values of the list, defaults to
2336+ False.
2337 - ``recurse_array``: Alias for ``recurse_list``.
2338-- ``recurse_str``: If True merge the new values of the string, defaults to False.
2339+- ``recurse_str``: If True merge the new values of the string, defaults to
2340+ False.
2341
2342
2343 Customizability
2344diff --git a/doc/rtd/topics/moreinfo.rst b/doc/rtd/topics/moreinfo.rst
2345deleted file mode 100644
2346index 9c3b7fb..0000000
2347--- a/doc/rtd/topics/moreinfo.rst
2348+++ /dev/null
2349@@ -1,13 +0,0 @@
2350-****************
2351-More information
2352-****************
2353-
2354-Useful external references
2355-==========================
2356-
2357-- `The beauty of cloudinit`_
2358-- `Introduction to cloud-init`_ (video)
2359-
2360-.. _Introduction to cloud-init: http://www.youtube.com/watch?v=-zL3BdbKyGY
2361-.. _The beauty of cloudinit: http://brandon.fuller.name/archives/2011/05/02/06.40.57/
2362-.. vi: textwidth=78
2363diff --git a/doc/rtd/topics/network-config-format-v2.rst b/doc/rtd/topics/network-config-format-v2.rst
2364index 50f5fa6..7f85755 100644
2365--- a/doc/rtd/topics/network-config-format-v2.rst
2366+++ b/doc/rtd/topics/network-config-format-v2.rst
2367@@ -54,11 +54,11 @@ Physical devices
2368
2369 : (Examples: ethernet, wifi) These can dynamically come and go between
2370 reboots and even during runtime (hotplugging). In the generic case, they
2371- can be selected by ``match:`` rules on desired properties, such as name/name
2372- pattern, MAC address, driver, or device paths. In general these will match
2373- any number of devices (unless they refer to properties which are unique
2374- such as the full path or MAC address), so without further knowledge about
2375- the hardware these will always be considered as a group.
2376+ can be selected by ``match:`` rules on desired properties, such as
2377+ name/name pattern, MAC address, driver, or device paths. In general these
2378+ will match any number of devices (unless they refer to properties which are
2379+ unique such as the full path or MAC address), so without further knowledge
2380+ about the hardware these will always be considered as a group.
2381
2382 It is valid to specify no match rules at all, in which case the ID field is
2383 simply the interface name to be matched. This is mostly useful if you want
2384@@ -228,8 +228,8 @@ Example: ::
2385
2386 **parameters**: *<(mapping)>*
2387
2388-Customization parameters for special bonding options. Time values are specified
2389-in seconds unless otherwise specified.
2390+Customization parameters for special bonding options. Time values are
2391+specified in seconds unless otherwise specified.
2392
2393 **mode**: *<(scalar)>*
2394
2395@@ -367,8 +367,8 @@ Example: ::
2396
2397 **parameters**: <*(mapping)>*
2398
2399-Customization parameters for special bridging options. Time values are specified
2400-in seconds unless otherwise specified.
2401+Customization parameters for special bridging options. Time values are
2402+specified in seconds unless otherwise specified.
2403
2404 **ageing-time**: <*(scalar)>*
2405
2406diff --git a/tests/unittests/test_datasource/test_ovf.py b/tests/unittests/test_datasource/test_ovf.py
2407index 349d54c..a615470 100644
2408--- a/tests/unittests/test_datasource/test_ovf.py
2409+++ b/tests/unittests/test_datasource/test_ovf.py
2410@@ -169,19 +169,56 @@ class TestDatasourceOVF(CiTestCase):
2411 MARKER-ID = 12345345
2412 """)
2413 util.write_file(conf_file, conf_content)
2414- with self.assertRaises(CustomScriptNotFound) as context:
2415- wrap_and_call(
2416- 'cloudinit.sources.DataSourceOVF',
2417- {'util.read_dmi_data': 'vmware',
2418- 'util.del_dir': True,
2419- 'search_file': self.tdir,
2420- 'wait_for_imc_cfg_file': conf_file,
2421- 'get_nics_to_enable': ''},
2422- ds.get_data)
2423+ with mock.patch(MPATH + 'get_tools_config', return_value='true'):
2424+ with self.assertRaises(CustomScriptNotFound) as context:
2425+ wrap_and_call(
2426+ 'cloudinit.sources.DataSourceOVF',
2427+ {'util.read_dmi_data': 'vmware',
2428+ 'util.del_dir': True,
2429+ 'search_file': self.tdir,
2430+ 'wait_for_imc_cfg_file': conf_file,
2431+ 'get_nics_to_enable': ''},
2432+ ds.get_data)
2433 customscript = self.tmp_path('test-script', self.tdir)
2434 self.assertIn('Script %s not found!!' % customscript,
2435 str(context.exception))
2436
2437+ def test_get_data_cust_script_disabled(self):
2438+ """If custom script is disabled by VMware tools configuration,
2439+ raise a RuntimeError.
2440+ """
2441+ paths = Paths({'cloud_dir': self.tdir})
2442+ ds = self.datasource(
2443+ sys_cfg={'disable_vmware_customization': False}, distro={},
2444+ paths=paths)
2445+ # Prepare the conf file
2446+ conf_file = self.tmp_path('test-cust', self.tdir)
2447+ conf_content = dedent("""\
2448+ [CUSTOM-SCRIPT]
2449+ SCRIPT-NAME = test-script
2450+ [MISC]
2451+ MARKER-ID = 12345346
2452+ """)
2453+ util.write_file(conf_file, conf_content)
2454+ # Prepare the custom sript
2455+ customscript = self.tmp_path('test-script', self.tdir)
2456+ util.write_file(customscript, "This is the post cust script")
2457+
2458+ with mock.patch(MPATH + 'get_tools_config', return_value='false'):
2459+ with mock.patch(MPATH + 'set_customization_status',
2460+ return_value=('msg', b'')):
2461+ with self.assertRaises(RuntimeError) as context:
2462+ wrap_and_call(
2463+ 'cloudinit.sources.DataSourceOVF',
2464+ {'util.read_dmi_data': 'vmware',
2465+ 'util.del_dir': True,
2466+ 'search_file': self.tdir,
2467+ 'wait_for_imc_cfg_file': conf_file,
2468+ 'get_nics_to_enable': ''},
2469+ ds.get_data)
2470+ self.assertIn('Custom script is disabled by VM Administrator',
2471+ str(context.exception))
2472+
2473 def test_get_data_non_vmware_seed_platform_info(self):
2474 """Platform info properly reports when on non-vmware platforms."""
2475 paths = Paths({'cloud_dir': self.tdir, 'run_dir': self.tdir})
2476diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
2477index 587e699..de87be2 100644
2478--- a/tests/unittests/test_ds_identify.py
2479+++ b/tests/unittests/test_ds_identify.py
2480@@ -195,6 +195,10 @@ class DsIdentifyBase(CiTestCase):
2481 return self._check_via_dict(
2482 data, RC_FOUND, dslist=[data.get('ds'), DS_NONE])
2483
2484+ def _test_ds_not_found(self, name):
2485+ data = copy.deepcopy(VALID_CFG[name])
2486+ return self._check_via_dict(data, RC_NOT_FOUND)
2487+
2488 def _check_via_dict(self, data, rc, dslist=None, **kwargs):
2489 ret = self._call_via_dict(data, **kwargs)
2490 good = False
2491@@ -244,9 +248,13 @@ class TestDsIdentify(DsIdentifyBase):
2492 self._test_ds_found('Ec2-xen')
2493
2494 def test_brightbox_is_ec2(self):
2495- """EC2: product_serial ends with 'brightbox.com'"""
2496+ """EC2: product_serial ends with '.brightbox.com'"""
2497 self._test_ds_found('Ec2-brightbox')
2498
2499+ def test_bobrightbox_is_not_brightbox(self):
2500+ """EC2: bobrightbox.com in product_serial is not brightbox'"""
2501+ self._test_ds_not_found('Ec2-brightbox-negative')
2502+
2503 def test_gce_by_product_name(self):
2504 """GCE identifies itself with product_name."""
2505 self._test_ds_found('GCE')
2506@@ -724,7 +732,11 @@ VALID_CFG = {
2507 },
2508 'Ec2-brightbox': {
2509 'ds': 'Ec2',
2510- 'files': {P_PRODUCT_SERIAL: 'facc6e2f.brightbox.com\n'},
2511+ 'files': {P_PRODUCT_SERIAL: 'srv-otuxg.gb1.brightbox.com\n'},
2512+ },
2513+ 'Ec2-brightbox-negative': {
2514+ 'ds': 'Ec2',
2515+ 'files': {P_PRODUCT_SERIAL: 'tricky-host.bobrightbox.com\n'},
2516 },
2517 'GCE': {
2518 'ds': 'GCE',
2519diff --git a/tests/unittests/test_vmware/test_guestcust_util.py b/tests/unittests/test_vmware/test_guestcust_util.py
2520new file mode 100644
2521index 0000000..b8fa994
2522--- /dev/null
2523+++ b/tests/unittests/test_vmware/test_guestcust_util.py
2524@@ -0,0 +1,65 @@
2525+# Copyright (C) 2019 Canonical Ltd.
2526+# Copyright (C) 2019 VMware INC.
2527+#
2528+# Author: Xiaofeng Wang <xiaofengw@vmware.com>
2529+#
2530+# This file is part of cloud-init. See LICENSE file for license information.
2531+
2532+from cloudinit import util
2533+from cloudinit.sources.helpers.vmware.imc.guestcust_util import (
2534+ get_tools_config,
2535+)
2536+from cloudinit.tests.helpers import CiTestCase, mock
2537+
2538+
2539+class TestGuestCustUtil(CiTestCase):
2540+ def test_get_tools_config_not_installed(self):
2541+ """
2542+ This test is designed to verify the behavior if vmware-toolbox-cmd
2543+ is not installed.
2544+ """
2545+ with mock.patch.object(util, 'which', return_value=None):
2546+ self.assertEqual(
2547+ get_tools_config('section', 'key', 'defaultVal'), 'defaultVal')
2548+
2549+ def test_get_tools_config_internal_exception(self):
2550+ """
2551+ This test is designed to verify the behavior if internal exception
2552+ is raised.
2553+ """
2554+ with mock.patch.object(util, 'which', return_value='/dummy/path'):
2555+ with mock.patch.object(util, 'subp',
2556+ return_value=('key=value', b''),
2557+ side_effect=util.ProcessExecutionError(
2558+ "subp failed", exit_code=99)):
2559+ # verify return value is 'defaultVal', not 'value'.
2560+ self.assertEqual(
2561+ get_tools_config('section', 'key', 'defaultVal'),
2562+ 'defaultVal')
2563+
2564+ def test_get_tools_config_normal(self):
2565+ """
2566+ This test is designed to verify the value could be parsed from
2567+ key = value of the given [section]
2568+ """
2569+ with mock.patch.object(util, 'which', return_value='/dummy/path'):
2570+ # value is not blank
2571+ with mock.patch.object(util, 'subp',
2572+ return_value=('key = value ', b'')):
2573+ self.assertEqual(
2574+ get_tools_config('section', 'key', 'defaultVal'),
2575+ 'value')
2576+ # value is blank
2577+ with mock.patch.object(util, 'subp',
2578+ return_value=('key = ', b'')):
2579+ self.assertEqual(
2580+ get_tools_config('section', 'key', 'defaultVal'),
2581+ '')
2582+ # value contains =
2583+ with mock.patch.object(util, 'subp',
2584+ return_value=('key=Bar=Wark', b'')):
2585+ self.assertEqual(
2586+ get_tools_config('section', 'key', 'defaultVal'),
2587+ 'Bar=Wark')
2588+
2589+# vi: ts=4 expandtab
2590diff --git a/tools/ds-identify b/tools/ds-identify
2591index e0d4865..2447d14 100755
2592--- a/tools/ds-identify
2593+++ b/tools/ds-identify
2594@@ -891,9 +891,8 @@ ec2_identify_platform() {
2595 local default="$1"
2596 local serial="${DI_DMI_PRODUCT_SERIAL}"
2597
2598- # brightbox https://bugs.launchpad.net/cloud-init/+bug/1661693
2599 case "$serial" in
2600- *brightbox.com) _RET="Brightbox"; return 0;;
2601+ *.brightbox.com) _RET="Brightbox"; return 0;;
2602 esac
2603
2604 # AWS http://docs.aws.amazon.com/AWSEC2/
2605diff --git a/tox.ini b/tox.ini
2606index 1f01eb7..f5baf32 100644
2607--- a/tox.ini
2608+++ b/tox.ini
2609@@ -53,8 +53,13 @@ exclude = .venv,.tox,dist,doc,*egg,.git,build,tools
2610
2611 [testenv:doc]
2612 basepython = python3
2613-deps = sphinx
2614-commands = {envpython} -m sphinx {posargs:doc/rtd doc/rtd_html}
2615+deps =
2616+ doc8
2617+ sphinx
2618+ sphinx_rtd_theme
2619+commands =
2620+ {envpython} -m sphinx {posargs:doc/rtd doc/rtd_html}
2621+ doc8 doc/rtd
2622
2623 [testenv:xenial]
2624 commands =

Subscribers

People subscribed via source and target branches