Merge ~chad.smith/cloud-init:ubuntu/bionic into cloud-init:ubuntu/bionic

Proposed by Chad Smith
Status: Merged
Merged at revision: 1e2786819243ae011e80b94c07c3a9a06710abb1
Proposed branch: ~chad.smith/cloud-init:ubuntu/bionic
Merge into: cloud-init:ubuntu/bionic
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
cloud-init Commiters Pending
Review via email: mp+372893@code.launchpad.net

Commit message

New upstream snapshot for release into Bionic

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.gitignore b/.gitignore
index 80c509e..b9b98e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,14 @@ stage
12*.snap12*.snap
13*.cover13*.cover
14.idea/14.idea/
15
16# Ignore packaging artifacts
17cloud-init.dsc
18cloud-init_*.build
19cloud-init_*.buildinfo
20cloud-init_*.changes
21cloud-init_*.deb
22cloud-init_*.dsc
23cloud-init_*.orig.tar.gz
24cloud-init_*.tar.xz
25cloud-init_*.upload
diff --git a/Makefile b/Makefile
index 4ace227..2c6d0c8 100644
--- a/Makefile
+++ b/Makefile
@@ -106,7 +106,9 @@ deb-src:
106 echo sudo apt-get install devscripts; exit 1; }106 echo sudo apt-get install devscripts; exit 1; }
107 $(PYVER) ./packages/bddeb -S -d107 $(PYVER) ./packages/bddeb -S -d
108108
109doc:
110 tox -e doc
109111
110.PHONY: test pyflakes pyflakes3 clean pep8 rpm srpm deb deb-src yaml112.PHONY: test pyflakes pyflakes3 clean pep8 rpm srpm deb deb-src yaml
111.PHONY: check_version pip-test-requirements pip-requirements clean_pyc113.PHONY: check_version pip-test-requirements pip-requirements clean_pyc
112.PHONY: unittest unittest3 style-check114.PHONY: unittest unittest3 style-check doc
diff --git a/cloudinit/atomic_helper.py b/cloudinit/atomic_helper.py
index 587b994..1f61faa 100644
--- a/cloudinit/atomic_helper.py
+++ b/cloudinit/atomic_helper.py
@@ -1,11 +1,13 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3import json3import json
4import logging
4import os5import os
5import stat6import stat
6import tempfile7import tempfile
78
8_DEF_PERMS = 0o6449_DEF_PERMS = 0o644
10LOG = logging.getLogger(__name__)
911
1012
11def write_file(filename, content, mode=_DEF_PERMS,13def write_file(filename, content, mode=_DEF_PERMS,
@@ -23,6 +25,10 @@ def write_file(filename, content, mode=_DEF_PERMS,
23 try:25 try:
24 tf = tempfile.NamedTemporaryFile(dir=os.path.dirname(filename),26 tf = tempfile.NamedTemporaryFile(dir=os.path.dirname(filename),
25 delete=False, mode=omode)27 delete=False, mode=omode)
28 LOG.debug(
29 "Atomically writing to file %s (via temporary file %s) - %s: [%o]"
30 " %d bytes/chars",
31 filename, tf.name, omode, mode, len(content))
26 tf.write(content)32 tf.write(content)
27 tf.close()33 tf.close()
28 os.chmod(tf.name, mode)34 os.chmod(tf.name, mode)
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index ea707c0..5de5c6d 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -109,6 +109,127 @@ def is_bond(devname):
109 return os.path.exists(sys_dev_path(devname, "bonding"))109 return os.path.exists(sys_dev_path(devname, "bonding"))
110110
111111
112def has_master(devname):
113 return os.path.exists(sys_dev_path(devname, path="master"))
114
115
116def is_netfailover(devname, driver=None):
117 """ netfailover driver uses 3 nics, master, primary and standby.
118 this returns True if the device is either the primary or standby
119 as these devices are to be ignored.
120 """
121 if driver is None:
122 driver = device_driver(devname)
123 if is_netfail_primary(devname, driver) or is_netfail_standby(devname,
124 driver):
125 return True
126 return False
127
128
129def get_dev_features(devname):
130 """ Returns a str from reading /sys/class/net/<devname>/device/features."""
131 features = ''
132 try:
133 features = read_sys_net(devname, 'device/features')
134 except Exception:
135 pass
136 return features
137
138
139def has_netfail_standby_feature(devname):
140 """ Return True if VIRTIO_NET_F_STANDBY bit (62) is set.
141
142 https://github.com/torvalds/linux/blob/ \
143 089cf7f6ecb266b6a4164919a2e69bd2f938374a/ \
144 include/uapi/linux/virtio_net.h#L60
145 """
146 features = get_dev_features(devname)
147 if not features or len(features) < 64:
148 return False
149 return features[62] == "1"
150
151
152def is_netfail_master(devname, driver=None):
153 """ A device is a "netfail master" device if:
154
155 - The device does NOT have the 'master' sysfs attribute
156 - The device driver is 'virtio_net'
157 - The device has the standby feature bit set
158
159 Return True if all of the above is True.
160 """
161 if has_master(devname):
162 return False
163
164 if driver is None:
165 driver = device_driver(devname)
166
167 if driver != "virtio_net":
168 return False
169
170 if not has_netfail_standby_feature(devname):
171 return False
172
173 return True
174
175
176def is_netfail_primary(devname, driver=None):
177 """ A device is a "netfail primary" device if:
178
179 - the device has a 'master' sysfs file
180 - the device driver is not 'virtio_net'
181 - the 'master' sysfs file points to device with virtio_net driver
182 - the 'master' device has the 'standby' feature bit set
183
184 Return True if all of the above is True.
185 """
186 # /sys/class/net/<devname>/master -> ../../<master devname>
187 master_sysfs_path = sys_dev_path(devname, path='master')
188 if not os.path.exists(master_sysfs_path):
189 return False
190
191 if driver is None:
192 driver = device_driver(devname)
193
194 if driver == "virtio_net":
195 return False
196
197 master_devname = os.path.basename(os.path.realpath(master_sysfs_path))
198 master_driver = device_driver(master_devname)
199 if master_driver != "virtio_net":
200 return False
201
202 master_has_standby = has_netfail_standby_feature(master_devname)
203 if not master_has_standby:
204 return False
205
206 return True
207
208
209def is_netfail_standby(devname, driver=None):
210 """ A device is a "netfail standby" device if:
211
212 - The device has a 'master' sysfs attribute
213 - The device driver is 'virtio_net'
214 - The device has the standby feature bit set
215
216 Return True if all of the above is True.
217 """
218 if not has_master(devname):
219 return False
220
221 if driver is None:
222 driver = device_driver(devname)
223
224 if driver != "virtio_net":
225 return False
226
227 if not has_netfail_standby_feature(devname):
228 return False
229
230 return True
231
232
112def is_renamed(devname):233def is_renamed(devname):
113 """234 """
114 /* interface name assignment types (sysfs name_assign_type attribute) */235 /* interface name assignment types (sysfs name_assign_type attribute) */
@@ -227,6 +348,9 @@ def find_fallback_nic(blacklist_drivers=None):
227 if is_bond(interface):348 if is_bond(interface):
228 # skip any bonds349 # skip any bonds
229 continue350 continue
351 if is_netfailover(interface):
352 # ignore netfailover primary/standby interfaces
353 continue
230 carrier = read_sys_net_int(interface, 'carrier')354 carrier = read_sys_net_int(interface, 'carrier')
231 if carrier:355 if carrier:
232 connected.append(interface)356 connected.append(interface)
@@ -273,9 +397,14 @@ def generate_fallback_config(blacklist_drivers=None, config_driver=None):
273 if not target_name:397 if not target_name:
274 # can't read any interfaces addresses (or there are none); give up398 # can't read any interfaces addresses (or there are none); give up
275 return None399 return None
276 target_mac = read_sys_net_safe(target_name, 'address')400
277 cfg = {'dhcp4': True, 'set-name': target_name,401 # netfail cannot use mac for matching, they have duplicate macs
278 'match': {'macaddress': target_mac.lower()}}402 if is_netfail_master(target_name):
403 match = {'name': target_name}
404 else:
405 match = {
406 'macaddress': read_sys_net_safe(target_name, 'address').lower()}
407 cfg = {'dhcp4': True, 'set-name': target_name, 'match': match}
279 if config_driver:408 if config_driver:
280 driver = device_driver(target_name)409 driver = device_driver(target_name)
281 if driver:410 if driver:
@@ -661,6 +790,10 @@ def get_interfaces():
661 continue790 continue
662 if is_bond(name):791 if is_bond(name):
663 continue792 continue
793 if has_master(name):
794 continue
795 if is_netfailover(name):
796 continue
664 mac = get_interface_mac(name)797 mac = get_interface_mac(name)
665 # some devices may not have a mac (tun0)798 # some devices may not have a mac (tun0)
666 if not mac:799 if not mac:
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
index d2e38f0..999db98 100644
--- a/cloudinit/net/tests/test_init.py
+++ b/cloudinit/net/tests/test_init.py
@@ -157,6 +157,12 @@ class TestReadSysNet(CiTestCase):
157 ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))157 ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
158 self.assertTrue(net.is_bond('eth0'))158 self.assertTrue(net.is_bond('eth0'))
159159
160 def test_has_master(self):
161 """has_master is True when /sys/net/devname/master exists."""
162 self.assertFalse(net.has_master('enP1s1'))
163 ensure_file(os.path.join(self.sysdir, 'enP1s1', 'master'))
164 self.assertTrue(net.has_master('enP1s1'))
165
160 def test_is_vlan(self):166 def test_is_vlan(self):
161 """is_vlan is True when /sys/net/devname/uevent has DEVTYPE=vlan."""167 """is_vlan is True when /sys/net/devname/uevent has DEVTYPE=vlan."""
162 ensure_file(os.path.join(self.sysdir, 'eth0', 'uevent'))168 ensure_file(os.path.join(self.sysdir, 'eth0', 'uevent'))
@@ -204,6 +210,10 @@ class TestGenerateFallbackConfig(CiTestCase):
204 self.add_patch('cloudinit.net.util.is_container', 'm_is_container',210 self.add_patch('cloudinit.net.util.is_container', 'm_is_container',
205 return_value=False)211 return_value=False)
206 self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')212 self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')
213 self.add_patch('cloudinit.net.is_netfailover', 'm_netfail',
214 return_value=False)
215 self.add_patch('cloudinit.net.is_netfail_master', 'm_netfail_master',
216 return_value=False)
207217
208 def test_generate_fallback_finds_connected_eth_with_mac(self):218 def test_generate_fallback_finds_connected_eth_with_mac(self):
209 """generate_fallback_config finds any connected device with a mac."""219 """generate_fallback_config finds any connected device with a mac."""
@@ -268,6 +278,61 @@ class TestGenerateFallbackConfig(CiTestCase):
268 ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))278 ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
269 self.assertIsNone(net.generate_fallback_config())279 self.assertIsNone(net.generate_fallback_config())
270280
281 def test_generate_fallback_config_skips_netfail_devs(self):
282 """gen_fallback_config ignores netfail primary,sby no mac on master."""
283 mac = 'aa:bb:cc:aa:bb:cc' # netfailover devs share the same mac
284 for iface in ['ens3', 'ens3sby', 'enP0s1f3']:
285 write_file(os.path.join(self.sysdir, iface, 'carrier'), '1')
286 write_file(
287 os.path.join(self.sysdir, iface, 'addr_assign_type'), '0')
288 write_file(
289 os.path.join(self.sysdir, iface, 'address'), mac)
290
291 def is_netfail(iface, _driver=None):
292 # ens3 is the master
293 if iface == 'ens3':
294 return False
295 return True
296 self.m_netfail.side_effect = is_netfail
297
298 def is_netfail_master(iface, _driver=None):
299 # ens3 is the master
300 if iface == 'ens3':
301 return True
302 return False
303 self.m_netfail_master.side_effect = is_netfail_master
304 expected = {
305 'ethernets': {
306 'ens3': {'dhcp4': True, 'match': {'name': 'ens3'},
307 'set-name': 'ens3'}},
308 'version': 2}
309 result = net.generate_fallback_config()
310 self.assertEqual(expected, result)
311
312
313class TestNetFindFallBackNic(CiTestCase):
314
315 with_logs = True
316
317 def setUp(self):
318 super(TestNetFindFallBackNic, self).setUp()
319 sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
320 self.m_sys_path = sys_mock.start()
321 self.sysdir = self.tmp_dir() + '/'
322 self.m_sys_path.return_value = self.sysdir
323 self.addCleanup(sys_mock.stop)
324 self.add_patch('cloudinit.net.util.is_container', 'm_is_container',
325 return_value=False)
326 self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')
327
328 def test_generate_fallback_finds_first_connected_eth_with_mac(self):
329 """find_fallback_nic finds any connected device with a mac."""
330 write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
331 write_file(os.path.join(self.sysdir, 'eth1', 'carrier'), '1')
332 mac = 'aa:bb:cc:aa:bb:cc'
333 write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
334 self.assertEqual('eth1', net.find_fallback_nic())
335
271336
272class TestGetDeviceList(CiTestCase):337class TestGetDeviceList(CiTestCase):
273338
@@ -365,6 +430,37 @@ class TestGetInterfaceMAC(CiTestCase):
365 expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]430 expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
366 self.assertEqual(expected, net.get_interfaces())431 self.assertEqual(expected, net.get_interfaces())
367432
433 def test_get_interfaces_by_mac_skips_master_devs(self):
434 """Ignore interfaces with a master device which would have dup mac."""
435 mac1 = mac2 = 'aa:bb:cc:aa:bb:cc'
436 write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
437 write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac1)
438 write_file(os.path.join(self.sysdir, 'eth1', 'master'), "blah")
439 write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
440 write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac2)
441 expected = [('eth2', mac2, None, None)]
442 self.assertEqual(expected, net.get_interfaces())
443
444 @mock.patch('cloudinit.net.is_netfailover')
445 def test_get_interfaces_by_mac_skips_netfailvoer(self, m_netfail):
446 """Ignore interfaces if netfailover primary or standby."""
447 mac = 'aa:bb:cc:aa:bb:cc' # netfailover devs share the same mac
448 for iface in ['ens3', 'ens3sby', 'enP0s1f3']:
449 write_file(
450 os.path.join(self.sysdir, iface, 'addr_assign_type'), '0')
451 write_file(
452 os.path.join(self.sysdir, iface, 'address'), mac)
453
454 def is_netfail(iface, _driver=None):
455 # ens3 is the master
456 if iface == 'ens3':
457 return False
458 else:
459 return True
460 m_netfail.side_effect = is_netfail
461 expected = [('ens3', mac, None, None)]
462 self.assertEqual(expected, net.get_interfaces())
463
368464
369class TestInterfaceHasOwnMAC(CiTestCase):465class TestInterfaceHasOwnMAC(CiTestCase):
370466
@@ -922,3 +1018,234 @@ class TestWaitForPhysdevs(CiTestCase):
922 self.m_get_iface_mac.return_value = {}1018 self.m_get_iface_mac.return_value = {}
923 net.wait_for_physdevs(netcfg, strict=False)1019 net.wait_for_physdevs(netcfg, strict=False)
924 self.assertEqual(5 * len(physdevs), self.m_udev_settle.call_count)1020 self.assertEqual(5 * len(physdevs), self.m_udev_settle.call_count)
1021
1022
1023class TestNetFailOver(CiTestCase):
1024
1025 with_logs = True
1026
1027 def setUp(self):
1028 super(TestNetFailOver, self).setUp()
1029 self.add_patch('cloudinit.net.util', 'm_util')
1030 self.add_patch('cloudinit.net.read_sys_net', 'm_read_sys_net')
1031 self.add_patch('cloudinit.net.device_driver', 'm_device_driver')
1032
1033 def test_get_dev_features(self):
1034 devname = self.random_string()
1035 features = self.random_string()
1036 self.m_read_sys_net.return_value = features
1037
1038 self.assertEqual(features, net.get_dev_features(devname))
1039 self.assertEqual(1, self.m_read_sys_net.call_count)
1040 self.assertEqual(mock.call(devname, 'device/features'),
1041 self.m_read_sys_net.call_args_list[0])
1042
1043 def test_get_dev_features_none_returns_empty_string(self):
1044 devname = self.random_string()
1045 self.m_read_sys_net.side_effect = Exception('error')
1046 self.assertEqual('', net.get_dev_features(devname))
1047 self.assertEqual(1, self.m_read_sys_net.call_count)
1048 self.assertEqual(mock.call(devname, 'device/features'),
1049 self.m_read_sys_net.call_args_list[0])
1050
1051 @mock.patch('cloudinit.net.get_dev_features')
1052 def test_has_netfail_standby_feature(self, m_dev_features):
1053 devname = self.random_string()
1054 standby_features = ('0' * 62) + '1' + '0'
1055 m_dev_features.return_value = standby_features
1056 self.assertTrue(net.has_netfail_standby_feature(devname))
1057
1058 @mock.patch('cloudinit.net.get_dev_features')
1059 def test_has_netfail_standby_feature_short_is_false(self, m_dev_features):
1060 devname = self.random_string()
1061 standby_features = self.random_string()
1062 m_dev_features.return_value = standby_features
1063 self.assertFalse(net.has_netfail_standby_feature(devname))
1064
1065 @mock.patch('cloudinit.net.get_dev_features')
1066 def test_has_netfail_standby_feature_not_present_is_false(self,
1067 m_dev_features):
1068 devname = self.random_string()
1069 standby_features = '0' * 64
1070 m_dev_features.return_value = standby_features
1071 self.assertFalse(net.has_netfail_standby_feature(devname))
1072
1073 @mock.patch('cloudinit.net.get_dev_features')
1074 def test_has_netfail_standby_feature_no_features_is_false(self,
1075 m_dev_features):
1076 devname = self.random_string()
1077 standby_features = None
1078 m_dev_features.return_value = standby_features
1079 self.assertFalse(net.has_netfail_standby_feature(devname))
1080
1081 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1082 @mock.patch('cloudinit.net.os.path.exists')
1083 def test_is_netfail_master(self, m_exists, m_standby):
1084 devname = self.random_string()
1085 driver = 'virtio_net'
1086 m_exists.return_value = False # no master sysfs attr
1087 m_standby.return_value = True # has standby feature flag
1088 self.assertTrue(net.is_netfail_master(devname, driver))
1089
1090 @mock.patch('cloudinit.net.sys_dev_path')
1091 def test_is_netfail_master_checks_master_attr(self, m_sysdev):
1092 devname = self.random_string()
1093 driver = 'virtio_net'
1094 m_sysdev.return_value = self.random_string()
1095 self.assertFalse(net.is_netfail_master(devname, driver))
1096 self.assertEqual(1, m_sysdev.call_count)
1097 self.assertEqual(mock.call(devname, path='master'),
1098 m_sysdev.call_args_list[0])
1099
1100 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1101 @mock.patch('cloudinit.net.os.path.exists')
1102 def test_is_netfail_master_wrong_driver(self, m_exists, m_standby):
1103 devname = self.random_string()
1104 driver = self.random_string()
1105 self.assertFalse(net.is_netfail_master(devname, driver))
1106
1107 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1108 @mock.patch('cloudinit.net.os.path.exists')
1109 def test_is_netfail_master_has_master_attr(self, m_exists, m_standby):
1110 devname = self.random_string()
1111 driver = 'virtio_net'
1112 m_exists.return_value = True # has master sysfs attr
1113 self.assertFalse(net.is_netfail_master(devname, driver))
1114
1115 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1116 @mock.patch('cloudinit.net.os.path.exists')
1117 def test_is_netfail_master_no_standby_feat(self, m_exists, m_standby):
1118 devname = self.random_string()
1119 driver = 'virtio_net'
1120 m_exists.return_value = False # no master sysfs attr
1121 m_standby.return_value = False # no standby feature flag
1122 self.assertFalse(net.is_netfail_master(devname, driver))
1123
1124 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1125 @mock.patch('cloudinit.net.os.path.exists')
1126 @mock.patch('cloudinit.net.sys_dev_path')
1127 def test_is_netfail_primary(self, m_sysdev, m_exists, m_standby):
1128 devname = self.random_string()
1129 driver = self.random_string() # device not virtio_net
1130 master_devname = self.random_string()
1131 m_sysdev.return_value = "%s/%s" % (self.random_string(),
1132 master_devname)
1133 m_exists.return_value = True # has master sysfs attr
1134 self.m_device_driver.return_value = 'virtio_net' # master virtio_net
1135 m_standby.return_value = True # has standby feature flag
1136 self.assertTrue(net.is_netfail_primary(devname, driver))
1137 self.assertEqual(1, self.m_device_driver.call_count)
1138 self.assertEqual(mock.call(master_devname),
1139 self.m_device_driver.call_args_list[0])
1140 self.assertEqual(1, m_standby.call_count)
1141 self.assertEqual(mock.call(master_devname),
1142 m_standby.call_args_list[0])
1143
1144 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1145 @mock.patch('cloudinit.net.os.path.exists')
1146 @mock.patch('cloudinit.net.sys_dev_path')
1147 def test_is_netfail_primary_wrong_driver(self, m_sysdev, m_exists,
1148 m_standby):
1149 devname = self.random_string()
1150 driver = 'virtio_net'
1151 self.assertFalse(net.is_netfail_primary(devname, driver))
1152
1153 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1154 @mock.patch('cloudinit.net.os.path.exists')
1155 @mock.patch('cloudinit.net.sys_dev_path')
1156 def test_is_netfail_primary_no_master(self, m_sysdev, m_exists, m_standby):
1157 devname = self.random_string()
1158 driver = self.random_string() # device not virtio_net
1159 m_exists.return_value = False # no master sysfs attr
1160 self.assertFalse(net.is_netfail_primary(devname, driver))
1161
1162 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1163 @mock.patch('cloudinit.net.os.path.exists')
1164 @mock.patch('cloudinit.net.sys_dev_path')
1165 def test_is_netfail_primary_bad_master(self, m_sysdev, m_exists,
1166 m_standby):
1167 devname = self.random_string()
1168 driver = self.random_string() # device not virtio_net
1169 master_devname = self.random_string()
1170 m_sysdev.return_value = "%s/%s" % (self.random_string(),
1171 master_devname)
1172 m_exists.return_value = True # has master sysfs attr
1173 self.m_device_driver.return_value = 'XXXX' # master not virtio_net
1174 self.assertFalse(net.is_netfail_primary(devname, driver))
1175
1176 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1177 @mock.patch('cloudinit.net.os.path.exists')
1178 @mock.patch('cloudinit.net.sys_dev_path')
1179 def test_is_netfail_primary_no_standby(self, m_sysdev, m_exists,
1180 m_standby):
1181 devname = self.random_string()
1182 driver = self.random_string() # device not virtio_net
1183 master_devname = self.random_string()
1184 m_sysdev.return_value = "%s/%s" % (self.random_string(),
1185 master_devname)
1186 m_exists.return_value = True # has master sysfs attr
1187 self.m_device_driver.return_value = 'virtio_net' # master virtio_net
1188 m_standby.return_value = False # master has no standby feature flag
1189 self.assertFalse(net.is_netfail_primary(devname, driver))
1190
1191 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1192 @mock.patch('cloudinit.net.os.path.exists')
1193 def test_is_netfail_standby(self, m_exists, m_standby):
1194 devname = self.random_string()
1195 driver = 'virtio_net'
1196 m_exists.return_value = True # has master sysfs attr
1197 m_standby.return_value = True # has standby feature flag
1198 self.assertTrue(net.is_netfail_standby(devname, driver))
1199
1200 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1201 @mock.patch('cloudinit.net.os.path.exists')
1202 def test_is_netfail_standby_wrong_driver(self, m_exists, m_standby):
1203 devname = self.random_string()
1204 driver = self.random_string()
1205 self.assertFalse(net.is_netfail_standby(devname, driver))
1206
1207 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1208 @mock.patch('cloudinit.net.os.path.exists')
1209 def test_is_netfail_standby_no_master(self, m_exists, m_standby):
1210 devname = self.random_string()
1211 driver = 'virtio_net'
1212 m_exists.return_value = False # has master sysfs attr
1213 self.assertFalse(net.is_netfail_standby(devname, driver))
1214
1215 @mock.patch('cloudinit.net.has_netfail_standby_feature')
1216 @mock.patch('cloudinit.net.os.path.exists')
1217 def test_is_netfail_standby_no_standby_feature(self, m_exists, m_standby):
1218 devname = self.random_string()
1219 driver = 'virtio_net'
1220 m_exists.return_value = True # has master sysfs attr
1221 m_standby.return_value = False # has standby feature flag
1222 self.assertFalse(net.is_netfail_standby(devname, driver))
1223
1224 @mock.patch('cloudinit.net.is_netfail_standby')
1225 @mock.patch('cloudinit.net.is_netfail_primary')
1226 def test_is_netfailover_primary(self, m_primary, m_standby):
1227 devname = self.random_string()
1228 driver = self.random_string()
1229 m_primary.return_value = True
1230 m_standby.return_value = False
1231 self.assertTrue(net.is_netfailover(devname, driver))
1232
1233 @mock.patch('cloudinit.net.is_netfail_standby')
1234 @mock.patch('cloudinit.net.is_netfail_primary')
1235 def test_is_netfailover_standby(self, m_primary, m_standby):
1236 devname = self.random_string()
1237 driver = self.random_string()
1238 m_primary.return_value = False
1239 m_standby.return_value = True
1240 self.assertTrue(net.is_netfailover(devname, driver))
1241
1242 @mock.patch('cloudinit.net.is_netfail_standby')
1243 @mock.patch('cloudinit.net.is_netfail_primary')
1244 def test_is_netfailover_returns_false(self, m_primary, m_standby):
1245 devname = self.random_string()
1246 driver = self.random_string()
1247 m_primary.return_value = False
1248 m_standby.return_value = False
1249 self.assertFalse(net.is_netfailover(devname, driver))
1250
1251# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
index 5c017bf..1010745 100644
--- a/cloudinit/sources/DataSourceEc2.py
+++ b/cloudinit/sources/DataSourceEc2.py
@@ -473,7 +473,7 @@ def identify_aws(data):
473473
474474
475def identify_brightbox(data):475def identify_brightbox(data):
476 if data['serial'].endswith('brightbox.com'):476 if data['serial'].endswith('.brightbox.com'):
477 return CloudNames.BRIGHTBOX477 return CloudNames.BRIGHTBOX
478478
479479
diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
index dd941d2..b156189 100644
--- a/cloudinit/sources/DataSourceOVF.py
+++ b/cloudinit/sources/DataSourceOVF.py
@@ -40,11 +40,15 @@ from cloudinit.sources.helpers.vmware.imc.guestcust_state \
40from cloudinit.sources.helpers.vmware.imc.guestcust_util import (40from cloudinit.sources.helpers.vmware.imc.guestcust_util import (
41 enable_nics,41 enable_nics,
42 get_nics_to_enable,42 get_nics_to_enable,
43 set_customization_status43 set_customization_status,
44 get_tools_config
44)45)
4546
46LOG = logging.getLogger(__name__)47LOG = logging.getLogger(__name__)
4748
49CONFGROUPNAME_GUESTCUSTOMIZATION = "deployPkg"
50GUESTCUSTOMIZATION_ENABLE_CUST_SCRIPTS = "enable-custom-scripts"
51
4852
49class DataSourceOVF(sources.DataSource):53class DataSourceOVF(sources.DataSource):
5054
@@ -148,6 +152,21 @@ class DataSourceOVF(sources.DataSource):
148 product_marker, os.path.join(self.paths.cloud_dir, 'data'))152 product_marker, os.path.join(self.paths.cloud_dir, 'data'))
149 special_customization = product_marker and not hasmarkerfile153 special_customization = product_marker and not hasmarkerfile
150 customscript = self._vmware_cust_conf.custom_script_name154 customscript = self._vmware_cust_conf.custom_script_name
155 custScriptConfig = get_tools_config(
156 CONFGROUPNAME_GUESTCUSTOMIZATION,
157 GUESTCUSTOMIZATION_ENABLE_CUST_SCRIPTS,
158 "true")
159 if custScriptConfig.lower() == "false":
160 # Update the customization status if there is a
161 # custom script is disabled
162 if special_customization and customscript:
163 msg = "Custom script is disabled by VM Administrator"
164 LOG.debug(msg)
165 set_customization_status(
166 GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
167 GuestCustErrorEnum.GUESTCUST_ERROR_SCRIPT_DISABLED)
168 raise RuntimeError(msg)
169
151 ccScriptsDir = os.path.join(170 ccScriptsDir = os.path.join(
152 self.paths.get_cpath("scripts"),171 self.paths.get_cpath("scripts"),
153 "per-instance")172 "per-instance")
diff --git a/cloudinit/sources/DataSourceOracle.py b/cloudinit/sources/DataSourceOracle.py
index 1cb0636..eec8740 100644
--- a/cloudinit/sources/DataSourceOracle.py
+++ b/cloudinit/sources/DataSourceOracle.py
@@ -16,7 +16,7 @@ Notes:
16"""16"""
1717
18from cloudinit.url_helper import combine_url, readurl, UrlError18from cloudinit.url_helper import combine_url, readurl, UrlError
19from cloudinit.net import dhcp, get_interfaces_by_mac19from cloudinit.net import dhcp, get_interfaces_by_mac, is_netfail_master
20from cloudinit import net20from cloudinit import net
21from cloudinit import sources21from cloudinit import sources
22from cloudinit import util22from cloudinit import util
@@ -108,6 +108,56 @@ def _add_network_config_from_opc_imds(network_config):
108 'match': {'macaddress': mac_address}}108 'match': {'macaddress': mac_address}}
109109
110110
111def _ensure_netfailover_safe(network_config):
112 """
113 Search network config physical interfaces to see if any of them are
114 a netfailover master. If found, we prevent matching by MAC as the other
115 failover devices have the same MAC but need to be ignored.
116
117 Note: we rely on cloudinit.net changes which prevent netfailover devices
118 from being present in the provided network config. For more details about
119 netfailover devices, refer to cloudinit.net module.
120
121 :param network_config
122 A v1 or v2 network config dict with the primary NIC, and possibly
123 secondary nic configured. This dict will be mutated.
124
125 """
126 # ignore anything that's not an actual network-config
127 if 'version' not in network_config:
128 return
129
130 if network_config['version'] not in [1, 2]:
131 LOG.debug('Ignoring unknown network config version: %s',
132 network_config['version'])
133 return
134
135 mac_to_name = get_interfaces_by_mac()
136 if network_config['version'] == 1:
137 for cfg in [c for c in network_config['config'] if 'type' in c]:
138 if cfg['type'] == 'physical':
139 if 'mac_address' in cfg:
140 mac = cfg['mac_address']
141 cur_name = mac_to_name.get(mac)
142 if not cur_name:
143 continue
144 elif is_netfail_master(cur_name):
145 del cfg['mac_address']
146
147 elif network_config['version'] == 2:
148 for _, cfg in network_config.get('ethernets', {}).items():
149 if 'match' in cfg:
150 macaddr = cfg.get('match', {}).get('macaddress')
151 if macaddr:
152 cur_name = mac_to_name.get(macaddr)
153 if not cur_name:
154 continue
155 elif is_netfail_master(cur_name):
156 del cfg['match']['macaddress']
157 del cfg['set-name']
158 cfg['match']['name'] = cur_name
159
160
111class DataSourceOracle(sources.DataSource):161class DataSourceOracle(sources.DataSource):
112162
113 dsname = 'Oracle'163 dsname = 'Oracle'
@@ -208,9 +258,13 @@ class DataSourceOracle(sources.DataSource):
208 We nonetheless return cmdline provided config if present258 We nonetheless return cmdline provided config if present
209 and fallback to generate fallback."""259 and fallback to generate fallback."""
210 if self._network_config == sources.UNSET:260 if self._network_config == sources.UNSET:
261 # this is v1
211 self._network_config = cmdline.read_initramfs_config()262 self._network_config = cmdline.read_initramfs_config()
263
212 if not self._network_config:264 if not self._network_config:
265 # this is now v2
213 self._network_config = self.distro.generate_fallback_config()266 self._network_config = self.distro.generate_fallback_config()
267
214 if self.ds_cfg.get('configure_secondary_nics'):268 if self.ds_cfg.get('configure_secondary_nics'):
215 try:269 try:
216 # Mutate self._network_config to include secondary VNICs270 # Mutate self._network_config to include secondary VNICs
@@ -219,6 +273,12 @@ class DataSourceOracle(sources.DataSource):
219 util.logexc(273 util.logexc(
220 LOG,274 LOG,
221 "Failed to fetch secondary network configuration!")275 "Failed to fetch secondary network configuration!")
276
277 # we need to verify that the nic selected is not a netfail over
278 # device and, if it is a netfail master, then we need to avoid
279 # emitting any match by mac
280 _ensure_netfailover_safe(self._network_config)
281
222 return self._network_config282 return self._network_config
223283
224284
diff --git a/cloudinit/sources/helpers/vmware/imc/guestcust_error.py b/cloudinit/sources/helpers/vmware/imc/guestcust_error.py
index db5a00d..65ae739 100644
--- a/cloudinit/sources/helpers/vmware/imc/guestcust_error.py
+++ b/cloudinit/sources/helpers/vmware/imc/guestcust_error.py
@@ -10,5 +10,6 @@ class GuestCustErrorEnum(object):
10 """Specifies different errors of Guest Customization engine"""10 """Specifies different errors of Guest Customization engine"""
1111
12 GUESTCUST_ERROR_SUCCESS = 012 GUESTCUST_ERROR_SUCCESS = 0
13 GUESTCUST_ERROR_SCRIPT_DISABLED = 6
1314
14# vi: ts=4 expandtab15# vi: ts=4 expandtab
diff --git a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py
index a590f32..eb78172 100644
--- a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py
+++ b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py
@@ -7,6 +7,7 @@
77
8import logging8import logging
9import os9import os
10import re
10import time11import time
1112
12from cloudinit import util13from cloudinit import util
@@ -117,4 +118,40 @@ def enable_nics(nics):
117 logger.warning("Can't connect network interfaces after %d attempts",118 logger.warning("Can't connect network interfaces after %d attempts",
118 enableNicsWaitRetries)119 enableNicsWaitRetries)
119120
121
122def get_tools_config(section, key, defaultVal):
123 """ Return the value of [section] key from VMTools configuration.
124
125 @param section: String of section to read from VMTools config
126 @returns: String value from key in [section] or defaultVal if
127 [section] is not present or vmware-toolbox-cmd is
128 not installed.
129 """
130
131 if not util.which('vmware-toolbox-cmd'):
132 logger.debug(
133 'vmware-toolbox-cmd not installed, returning default value')
134 return defaultVal
135
136 retValue = defaultVal
137 cmd = ['vmware-toolbox-cmd', 'config', 'get', section, key]
138
139 try:
140 (outText, _) = util.subp(cmd)
141 m = re.match(r'([a-zA-Z0-9 ]+)=(.*)', outText)
142 if m:
143 retValue = m.group(2).strip()
144 logger.debug("Get tools config: [%s] %s = %s",
145 section, key, retValue)
146 else:
147 logger.debug(
148 "Tools config: [%s] %s is not found, return default value: %s",
149 section, key, retValue)
150 except util.ProcessExecutionError as e:
151 logger.error("Failed running %s[%s]", cmd, e.exit_code)
152 logger.exception(e)
153
154 return retValue
155
156
120# vi: ts=4 expandtab157# vi: ts=4 expandtab
diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py
index 2a70bbc..85b6db9 100644
--- a/cloudinit/sources/tests/test_oracle.py
+++ b/cloudinit/sources/tests/test_oracle.py
@@ -8,6 +8,7 @@ from cloudinit.tests import helpers as test_helpers
88
9from textwrap import dedent9from textwrap import dedent
10import argparse10import argparse
11import copy
11import httpretty12import httpretty
12import json13import json
13import mock14import mock
@@ -586,4 +587,150 @@ class TestNetworkConfigFromOpcImds(test_helpers.CiTestCase):
586 self.assertEqual('10.0.0.231', secondary_nic_cfg['addresses'][0])587 self.assertEqual('10.0.0.231', secondary_nic_cfg['addresses'][0])
587588
588589
590class TestNetworkConfigFiltersNetFailover(test_helpers.CiTestCase):
591
592 with_logs = True
593
594 def setUp(self):
595 super(TestNetworkConfigFiltersNetFailover, self).setUp()
596 self.add_patch(DS_PATH + '.get_interfaces_by_mac',
597 'm_get_interfaces_by_mac')
598 self.add_patch(DS_PATH + '.is_netfail_master', 'm_netfail_master')
599
600 def test_ignore_bogus_network_config(self):
601 netcfg = {'something': 'here'}
602 passed_netcfg = copy.copy(netcfg)
603 oracle._ensure_netfailover_safe(passed_netcfg)
604 self.assertEqual(netcfg, passed_netcfg)
605
606 def test_ignore_network_config_unknown_versions(self):
607 netcfg = {'something': 'here', 'version': 3}
608 passed_netcfg = copy.copy(netcfg)
609 oracle._ensure_netfailover_safe(passed_netcfg)
610 self.assertEqual(netcfg, passed_netcfg)
611
612 def test_checks_v1_type_physical_interfaces(self):
613 mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
614 self.m_get_interfaces_by_mac.return_value = {
615 mac_addr: nic_name,
616 }
617 netcfg = {'version': 1, 'config': [
618 {'type': 'physical', 'name': nic_name, 'mac_address': mac_addr,
619 'subnets': [{'type': 'dhcp4'}]}]}
620 passed_netcfg = copy.copy(netcfg)
621 self.m_netfail_master.return_value = False
622 oracle._ensure_netfailover_safe(passed_netcfg)
623 self.assertEqual(netcfg, passed_netcfg)
624 self.assertEqual([mock.call(nic_name)],
625 self.m_netfail_master.call_args_list)
626
627 def test_checks_v1_skips_non_phys_interfaces(self):
628 mac_addr, nic_name = '00:00:17:02:2b:b1', 'bond0'
629 self.m_get_interfaces_by_mac.return_value = {
630 mac_addr: nic_name,
631 }
632 netcfg = {'version': 1, 'config': [
633 {'type': 'bond', 'name': nic_name, 'mac_address': mac_addr,
634 'subnets': [{'type': 'dhcp4'}]}]}
635 passed_netcfg = copy.copy(netcfg)
636 oracle._ensure_netfailover_safe(passed_netcfg)
637 self.assertEqual(netcfg, passed_netcfg)
638 self.assertEqual(0, self.m_netfail_master.call_count)
639
640 def test_removes_master_mac_property_v1(self):
641 nic_master, mac_master = 'ens3', self.random_string()
642 nic_other, mac_other = 'ens7', self.random_string()
643 nic_extra, mac_extra = 'enp0s1f2', self.random_string()
644 self.m_get_interfaces_by_mac.return_value = {
645 mac_master: nic_master,
646 mac_other: nic_other,
647 mac_extra: nic_extra,
648 }
649 netcfg = {'version': 1, 'config': [
650 {'type': 'physical', 'name': nic_master,
651 'mac_address': mac_master},
652 {'type': 'physical', 'name': nic_other, 'mac_address': mac_other},
653 {'type': 'physical', 'name': nic_extra, 'mac_address': mac_extra},
654 ]}
655
656 def _is_netfail_master(iface):
657 if iface == 'ens3':
658 return True
659 return False
660 self.m_netfail_master.side_effect = _is_netfail_master
661 expected_cfg = {'version': 1, 'config': [
662 {'type': 'physical', 'name': nic_master},
663 {'type': 'physical', 'name': nic_other, 'mac_address': mac_other},
664 {'type': 'physical', 'name': nic_extra, 'mac_address': mac_extra},
665 ]}
666 oracle._ensure_netfailover_safe(netcfg)
667 self.assertEqual(expected_cfg, netcfg)
668
669 def test_checks_v2_type_ethernet_interfaces(self):
670 mac_addr, nic_name = '00:00:17:02:2b:b1', 'ens3'
671 self.m_get_interfaces_by_mac.return_value = {
672 mac_addr: nic_name,
673 }
674 netcfg = {'version': 2, 'ethernets': {
675 nic_name: {'dhcp4': True, 'critical': True, 'set-name': nic_name,
676 'match': {'macaddress': mac_addr}}}}
677 passed_netcfg = copy.copy(netcfg)
678 self.m_netfail_master.return_value = False
679 oracle._ensure_netfailover_safe(passed_netcfg)
680 self.assertEqual(netcfg, passed_netcfg)
681 self.assertEqual([mock.call(nic_name)],
682 self.m_netfail_master.call_args_list)
683
684 def test_skips_v2_non_ethernet_interfaces(self):
685 mac_addr, nic_name = '00:00:17:02:2b:b1', 'wlps0'
686 self.m_get_interfaces_by_mac.return_value = {
687 mac_addr: nic_name,
688 }
689 netcfg = {'version': 2, 'wifis': {
690 nic_name: {'dhcp4': True, 'critical': True, 'set-name': nic_name,
691 'match': {'macaddress': mac_addr}}}}
692 passed_netcfg = copy.copy(netcfg)
693 oracle._ensure_netfailover_safe(passed_netcfg)
694 self.assertEqual(netcfg, passed_netcfg)
695 self.assertEqual(0, self.m_netfail_master.call_count)
696
697 def test_removes_master_mac_property_v2(self):
698 nic_master, mac_master = 'ens3', self.random_string()
699 nic_other, mac_other = 'ens7', self.random_string()
700 nic_extra, mac_extra = 'enp0s1f2', self.random_string()
701 self.m_get_interfaces_by_mac.return_value = {
702 mac_master: nic_master,
703 mac_other: nic_other,
704 mac_extra: nic_extra,
705 }
706 netcfg = {'version': 2, 'ethernets': {
707 nic_extra: {'dhcp4': True, 'set-name': nic_extra,
708 'match': {'macaddress': mac_extra}},
709 nic_other: {'dhcp4': True, 'set-name': nic_other,
710 'match': {'macaddress': mac_other}},
711 nic_master: {'dhcp4': True, 'set-name': nic_master,
712 'match': {'macaddress': mac_master}},
713 }}
714
715 def _is_netfail_master(iface):
716 if iface == 'ens3':
717 return True
718 return False
719 self.m_netfail_master.side_effect = _is_netfail_master
720
721 expected_cfg = {'version': 2, 'ethernets': {
722 nic_master: {'dhcp4': True, 'match': {'name': nic_master}},
723 nic_extra: {'dhcp4': True, 'set-name': nic_extra,
724 'match': {'macaddress': mac_extra}},
725 nic_other: {'dhcp4': True, 'set-name': nic_other,
726 'match': {'macaddress': mac_other}},
727 }}
728 oracle._ensure_netfailover_safe(netcfg)
729 import pprint
730 pprint.pprint(netcfg)
731 print('---- ^^ modified ^^ ---- vv original vv ----')
732 pprint.pprint(expected_cfg)
733 self.assertEqual(expected_cfg, netcfg)
734
735
589# vi: ts=4 expandtab736# vi: ts=4 expandtab
diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
index 23fddd0..4dad2af 100644
--- a/cloudinit/tests/helpers.py
+++ b/cloudinit/tests/helpers.py
@@ -6,7 +6,9 @@ import functools
6import httpretty6import httpretty
7import logging7import logging
8import os8import os
9import random
9import shutil10import shutil
11import string
10import sys12import sys
11import tempfile13import tempfile
12import time14import time
@@ -243,6 +245,12 @@ class CiTestCase(TestCase):
243 myds.metadata.update(metadata)245 myds.metadata.update(metadata)
244 return cloud.Cloud(myds, self.paths, sys_cfg, mydist, None)246 return cloud.Cloud(myds, self.paths, sys_cfg, mydist, None)
245247
248 @classmethod
249 def random_string(cls, length=8):
250 """ return a random lowercase string with default length of 8"""
251 return ''.join(
252 random.choice(string.ascii_lowercase) for _ in range(length))
253
246254
247class ResourceUsingTestCase(CiTestCase):255class ResourceUsingTestCase(CiTestCase):
248256
diff --git a/debian/changelog b/debian/changelog
index 7d71be2..31760d7 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,23 @@
1cloud-init (19.2-36-g059d049c-0ubuntu1~18.04.1) bionic; urgency=medium
2
3 * New upstream snapshot. (LP: #1844334)
4 - net: add is_master check for filtering device list
5 - docs: more complete list of availability [Joshua Powers]
6 - docs: start FAQ page [Joshua Powers]
7 - docs: cleanup output & order of datasource page [Joshua Powers]
8 - Brightbox: restrict detection to require full domain match
9 .brightbox.com [Scott Moser]
10 - VMWware: add option into VMTools config to enable/disable custom script.
11 [Xiaofeng Wang]
12 - net,Oracle: Add support for netfailover detection
13 - atomic_helper: add DEBUG logging to write_file
14 - doc: document doc, create makefile and tox target [Joshua Powers]
15 - .gitignore: ignore files produced by package builds
16 - docs: fix whitespace, spelling, and line length [Joshua Powers]
17 - docs: remove unnecessary file in doc directory [Joshua Powers]
18
19 -- Chad Smith <chad.smith@canonical.com> Tue, 17 Sep 2019 08:17:04 -0600
20
1cloud-init (19.2-24-ge7881d5c-0ubuntu1~18.04.1) bionic; urgency=medium21cloud-init (19.2-24-ge7881d5c-0ubuntu1~18.04.1) bionic; urgency=medium
222
3 * New upstream snapshot. (LP: #1841099)23 * New upstream snapshot. (LP: #1841099)
diff --git a/doc/README b/doc/README
4deleted file mode 10064424deleted file mode 100644
index 8355919..0000000
--- a/doc/README
+++ /dev/null
@@ -1,4 +0,0 @@
1This project is cloud-init it is hosted on launchpad at
2https://launchpad.net/cloud-init
3
4The package was previously named ec2-init.
diff --git a/doc/rtd/conf.py b/doc/rtd/conf.py
index 4174477..9b27484 100644
--- a/doc/rtd/conf.py
+++ b/doc/rtd/conf.py
@@ -17,7 +17,8 @@ from cloudinit.config.schema import get_schema_doc
17# ]17# ]
1818
19# General information about the project.19# General information about the project.
20project = 'Cloud-Init'20project = 'cloud-init'
21copyright = '2019, Canonical Ltd.'
2122
22# -- General configuration ----------------------------------------------------23# -- General configuration ----------------------------------------------------
2324
@@ -59,15 +60,7 @@ show_authors = False
5960
60# The theme to use for HTML and HTML Help pages. See the documentation for61# The theme to use for HTML and HTML Help pages. See the documentation for
61# a list of builtin themes.62# a list of builtin themes.
62html_theme = 'default'63html_theme = 'sphinx_rtd_theme'
63
64# Theme options are theme-specific and customize the look and feel of a theme
65# further. For a list of options available for each theme, see the
66# documentation.
67html_theme_options = {
68 "bodyfont": "Ubuntu, Arial, sans-serif",
69 "headfont": "Ubuntu, Arial, sans-serif"
70}
7164
72# The name of an image file (relative to this directory) to place at the top65# The name of an image file (relative to this directory) to place at the top
73# of the sidebar.66# of the sidebar.
diff --git a/doc/rtd/index.rst b/doc/rtd/index.rst
index 20a99a3..c670b20 100644
--- a/doc/rtd/index.rst
+++ b/doc/rtd/index.rst
@@ -1,14 +1,5 @@
1.. _index:1.. _index:
22
3.. http://thomas-cokelaer.info/tutorials/sphinx/rest_syntax.html
4.. As suggested at link above for headings use:
5.. # with overline, for parts
6.. * with overline, for chapters
7.. =, for sections
8.. -, for subsections
9.. ^, for subsubsections
10.. “, for paragraphs
11
12#############3#############
13Documentation4Documentation
14#############5#############
diff --git a/doc/rtd/topics/availability.rst b/doc/rtd/topics/availability.rst
index ef5ae7b..3f215b1 100644
--- a/doc/rtd/topics/availability.rst
+++ b/doc/rtd/topics/availability.rst
@@ -1,21 +1,64 @@
1************1.. _availability:
2
2Availability3Availability
3************4************
45
5It is currently installed in the `Ubuntu Cloud Images`_ and also in the official `Ubuntu`_ images available on EC2, Azure, GCE and many other clouds.6Below outlines the current availability of cloud-init across
7distributions and clouds, both public and private.
8
9.. note::
10
11 If a distribution or cloud does not show up in the list below contact
12 them and ask for images to be generated using cloud-init!
613
7Versions for other systems can be (or have been) created for the following distributions:14Distributions
15=============
16
17Cloud-init has support across all major Linux distributions and
18FreeBSD:
819
9- Ubuntu20- Ubuntu
21- SLES/openSUSE
22- RHEL/CentOS
10- Fedora23- Fedora
24- Gentoo Linux
11- Debian25- Debian
12- RHEL26- ArchLinux
13- CentOS27- FreeBSD
14- *and more...*28
29Clouds
30======
31
32Cloud-init provides support across a wide ranging list of execution
33environments in the public cloud:
34
35- Amazon Web Services
36- Microsoft Azure
37- Google Cloud Platform
38- Oracle Cloud Infrastructure
39- Softlayer
40- Rackspace Public Cloud
41- IBM Cloud
42- Digital Ocean
43- Bigstep
44- Hetzner
45- Joyent
46- CloudSigma
47- Alibaba Cloud
48- OVH
49- OpenNebula
50- Exoscale
51- Scaleway
52- CloudStack
53- AltCloud
54- SmartOS
1555
16So ask your distribution provider where you can obtain an image with it built-in if one is not already available ☺56Additionally, cloud-init is supported on these private clouds:
1757
58- Bare metal installs
59- OpenStack
60- LXD
61- KVM
62- Metal-as-a-Service (MAAS)
1863
19.. _Ubuntu Cloud Images: http://cloud-images.ubuntu.com/64.. vi: textwidth=79
20.. _Ubuntu: http://www.ubuntu.com/
21.. vi: textwidth=78
diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst
index 2148cd5..8e58be9 100644
--- a/doc/rtd/topics/datasources.rst
+++ b/doc/rtd/topics/datasources.rst
@@ -1,89 +1,57 @@
1.. _datasources:1.. _datasources:
22
3***********
4Datasources3Datasources
5***********4***********
65
7What is a datasource?
8=====================
9
10Datasources are sources of configuration data for cloud-init that typically6Datasources are sources of configuration data for cloud-init that typically
11come from the user (aka userdata) or come from the stack that created the7come from the user (e.g. userdata) or come from the cloud that created the
12configuration drive (aka metadata). Typical userdata would include files,8configuration drive (e.g. metadata). Typical userdata would include files,
13yaml, and shell scripts while typical metadata would include server name,9yaml, and shell scripts while typical metadata would include server name,
14instance id, display name and other cloud specific details. Since there are10instance id, display name and other cloud specific details.
15multiple ways to provide this data (each cloud solution seems to prefer its
16own way) internally a datasource abstract class was created to allow for a
17single way to access the different cloud systems methods to provide this data
18through the typical usage of subclasses.
19
20Any metadata processed by cloud-init's datasources is persisted as
21``/run/cloud-init/instance-data.json``. Cloud-init provides tooling
22to quickly introspect some of that data. See :ref:`instance_metadata` for
23more information.
24
25
26Datasource API
27--------------
28The current interface that a datasource object must provide is the following:
29
30.. sourcecode:: python
3111
32 # returns a mime multipart message that contains12Since there are multiple ways to provide this data (each cloud solution seems
33 # all the various fully-expanded components that13to prefer its own way) internally a datasource abstract class was created to
34 # were found from processing the raw userdata string14allow for a single way to access the different cloud systems methods to provide
35 # - when filtering only the mime messages targeting15this data through the typical usage of subclasses.
36 # this instance id will be returned (or messages with
37 # no instance id)
38 def get_userdata(self, apply_filter=False)
39
40 # returns the raw userdata string (or none)
41 def get_userdata_raw(self)
4216
43 # returns a integer (or none) which can be used to identify17Any metadata processed by cloud-init's datasources is persisted as
44 # this instance in a group of instances which are typically18``/run/cloud-init/instance-data.json``. Cloud-init provides tooling to quickly
45 # created from a single command, thus allowing programmatic19introspect some of that data. See :ref:`instance_metadata` for more
46 # filtering on this launch index (or other selective actions)20information.
47 @property
48 def launch_index(self)
49
50 # the data sources' config_obj is a cloud-config formatted
51 # object that came to it from ways other than cloud-config
52 # because cloud-config content would be handled elsewhere
53 def get_config_obj(self)
54
55 #returns a list of public ssh keys
56 def get_public_ssh_keys(self)
57
58 # translates a device 'short' name into the actual physical device
59 # fully qualified name (or none if said physical device is not attached
60 # or does not exist)
61 def device_name_to_device(self, name)
6221
63 # gets the locale string this instance should be applying22Known Sources
64 # which typically used to adjust the instances locale settings files23=============
65 def get_locale(self)
6624
67 @property25The following is a list of documents for each supported datasource:
68 def availability_zone(self)
6926
70 # gets the instance id that was assigned to this instance by the27.. toctree::
71 # cloud provider or when said instance id does not exist in the backing28 :titlesonly:
72 # metadata this will return 'iid-datasource'
73 def get_instance_id(self)
7429
75 # gets the fully qualified domain name that this host should be using30 datasources/aliyun.rst
76 # when configuring network or hostname releated settings, typically31 datasources/altcloud.rst
77 # assigned either by the cloud provider or the user creating the vm32 datasources/ec2.rst
78 def get_hostname(self, fqdn=False)33 datasources/azure.rst
34 datasources/cloudsigma.rst
35 datasources/cloudstack.rst
36 datasources/configdrive.rst
37 datasources/digitalocean.rst
38 datasources/exoscale.rst
39 datasources/fallback.rst
40 datasources/gce.rst
41 datasources/maas.rst
42 datasources/nocloud.rst
43 datasources/opennebula.rst
44 datasources/openstack.rst
45 datasources/oracle.rst
46 datasources/ovf.rst
47 datasources/smartos.rst
7948
80 def get_package_mirror_info(self)
8149
50Creation
51========
8252
83Adding a new Datasource
84-----------------------
85The datasource objects have a few touch points with cloud-init. If you53The datasource objects have a few touch points with cloud-init. If you
86are interested in adding a new datasource for your cloud platform you'll54are interested in adding a new datasource for your cloud platform you will
87need to take care of the following items:55need to take care of the following items:
8856
89* **Identify a mechanism for positive identification of the platform**:57* **Identify a mechanism for positive identification of the platform**:
@@ -139,31 +107,61 @@ need to take care of the following items:
139 file in ``doc/datasources/<cloudplatform>.rst``107 file in ``doc/datasources/<cloudplatform>.rst``
140108
141109
142Datasource Documentation110API
143========================111===
144The following is a list of the implemented datasources.
145Follow for more information.
146112
147.. toctree::113The current interface that a datasource object must provide is the following:
148 :maxdepth: 2
149114
150 datasources/aliyun.rst115.. sourcecode:: python
151 datasources/altcloud.rst116
152 datasources/azure.rst117 # returns a mime multipart message that contains
153 datasources/cloudsigma.rst118 # all the various fully-expanded components that
154 datasources/cloudstack.rst119 # were found from processing the raw user data string
155 datasources/configdrive.rst120 # - when filtering only the mime messages targeting
156 datasources/digitalocean.rst121 # this instance id will be returned (or messages with
157 datasources/ec2.rst122 # no instance id)
158 datasources/exoscale.rst123 def get_userdata(self, apply_filter=False)
159 datasources/maas.rst124
160 datasources/nocloud.rst125 # returns the raw userdata string (or none)
161 datasources/opennebula.rst126 def get_userdata_raw(self)
162 datasources/openstack.rst127
163 datasources/oracle.rst128 # returns a integer (or none) which can be used to identify
164 datasources/ovf.rst129 # this instance in a group of instances which are typically
165 datasources/smartos.rst130 # created from a single command, thus allowing programmatic
166 datasources/fallback.rst131 # filtering on this launch index (or other selective actions)
167 datasources/gce.rst132 @property
133 def launch_index(self)
134
135 # the data sources' config_obj is a cloud-config formatted
136 # object that came to it from ways other than cloud-config
137 # because cloud-config content would be handled elsewhere
138 def get_config_obj(self)
139
140 #returns a list of public ssh keys
141 def get_public_ssh_keys(self)
142
143 # translates a device 'short' name into the actual physical device
144 # fully qualified name (or none if said physical device is not attached
145 # or does not exist)
146 def device_name_to_device(self, name)
147
148 # gets the locale string this instance should be applying
149 # which typically used to adjust the instances locale settings files
150 def get_locale(self)
151
152 @property
153 def availability_zone(self)
154
155 # gets the instance id that was assigned to this instance by the
156 # cloud provider or when said instance id does not exist in the backing
157 # metadata this will return 'iid-datasource'
158 def get_instance_id(self)
159
160 # gets the fully qualified domain name that this host should be using
161 # when configuring network or hostname related settings, typically
162 # assigned either by the cloud provider or the user creating the vm
163 def get_hostname(self, fqdn=False)
164
165 def get_package_mirror_info(self)
168166
169.. vi: textwidth=78167.. vi: textwidth=79
diff --git a/doc/rtd/topics/datasources/altcloud.rst b/doc/rtd/topics/datasources/altcloud.rst
index eeb197f..9d7e3de 100644
--- a/doc/rtd/topics/datasources/altcloud.rst
+++ b/doc/rtd/topics/datasources/altcloud.rst
@@ -3,24 +3,25 @@
3Alt Cloud3Alt Cloud
4=========4=========
55
6The datasource altcloud will be used to pick up user data on `RHEVm`_ and `vSphere`_.6The datasource altcloud will be used to pick up user data on `RHEVm`_ and
7`vSphere`_.
78
8RHEVm9RHEVm
9-----10-----
1011
11For `RHEVm`_ v3.0 the userdata is injected into the VM using floppy12For `RHEVm`_ v3.0 the userdata is injected into the VM using floppy
12injection via the `RHEVm`_ dashboard "Custom Properties". 13injection via the `RHEVm`_ dashboard "Custom Properties".
1314
14The format of the Custom Properties entry must be:15The format of the Custom Properties entry must be:
1516
16::17::
17 18
18 floppyinject=user-data.txt:<base64 encoded data>19 floppyinject=user-data.txt:<base64 encoded data>
1920
20For example to pass a simple bash script:21For example to pass a simple bash script:
2122
22.. sourcecode:: sh23.. sourcecode:: sh
23 24
24 % cat simple_script.bash25 % cat simple_script.bash
25 #!/bin/bash26 #!/bin/bash
26 echo "Hello Joe!" >> /tmp/JJV_Joe_out.txt27 echo "Hello Joe!" >> /tmp/JJV_Joe_out.txt
@@ -38,7 +39,7 @@ set the "Custom Properties" when creating the RHEMv v3.0 VM to:
38**NOTE:** The prefix with file name must be: ``floppyinject=user-data.txt:``39**NOTE:** The prefix with file name must be: ``floppyinject=user-data.txt:``
3940
40It is also possible to launch a `RHEVm`_ v3.0 VM and pass optional user41It is also possible to launch a `RHEVm`_ v3.0 VM and pass optional user
41data to it using the Delta Cloud. 42data to it using the Delta Cloud.
4243
43For more information on Delta Cloud see: http://deltacloud.apache.org44For more information on Delta Cloud see: http://deltacloud.apache.org
4445
@@ -46,12 +47,12 @@ vSphere
46-------47-------
4748
48For VMWare's `vSphere`_ the userdata is injected into the VM as an ISO49For VMWare's `vSphere`_ the userdata is injected into the VM as an ISO
49via the cdrom. This can be done using the `vSphere`_ dashboard 50via the cdrom. This can be done using the `vSphere`_ dashboard
50by connecting an ISO image to the CD/DVD drive.51by connecting an ISO image to the CD/DVD drive.
5152
52To pass this example script to cloud-init running in a `vSphere`_ VM53To pass this example script to cloud-init running in a `vSphere`_ VM
53set the CD/DVD drive when creating the vSphere VM to point to an54set the CD/DVD drive when creating the vSphere VM to point to an
54ISO on the data store. 55ISO on the data store.
5556
56**Note:** The ISO must contain the user data.57**Note:** The ISO must contain the user data.
5758
@@ -61,13 +62,13 @@ Create the ISO
61^^^^^^^^^^^^^^62^^^^^^^^^^^^^^
6263
63.. sourcecode:: sh64.. sourcecode:: sh
64 65
65 % mkdir my-iso66 % mkdir my-iso
6667
67NOTE: The file name on the ISO must be: ``user-data.txt``68NOTE: The file name on the ISO must be: ``user-data.txt``
6869
69.. sourcecode:: sh70.. sourcecode:: sh
70 71
71 % cp simple_script.bash my-iso/user-data.txt72 % cp simple_script.bash my-iso/user-data.txt
72 % genisoimage -o user-data.iso -r my-iso73 % genisoimage -o user-data.iso -r my-iso
7374
@@ -75,7 +76,7 @@ Verify the ISO
75^^^^^^^^^^^^^^76^^^^^^^^^^^^^^
7677
77.. sourcecode:: sh78.. sourcecode:: sh
78 79
79 % sudo mkdir /media/vsphere_iso80 % sudo mkdir /media/vsphere_iso
80 % sudo mount -o loop user-data.iso /media/vsphere_iso81 % sudo mount -o loop user-data.iso /media/vsphere_iso
81 % cat /media/vsphere_iso/user-data.txt82 % cat /media/vsphere_iso/user-data.txt
@@ -84,7 +85,7 @@ Verify the ISO
84Then, launch the `vSphere`_ VM the ISO user-data.iso attached as a CDROM.85Then, launch the `vSphere`_ VM the ISO user-data.iso attached as a CDROM.
8586
86It is also possible to launch a `vSphere`_ VM and pass optional user87It is also possible to launch a `vSphere`_ VM and pass optional user
87data to it using the Delta Cloud. 88data to it using the Delta Cloud.
8889
89For more information on Delta Cloud see: http://deltacloud.apache.org90For more information on Delta Cloud see: http://deltacloud.apache.org
9091
diff --git a/doc/rtd/topics/datasources/azure.rst b/doc/rtd/topics/datasources/azure.rst
index b41cddd..8328dfa 100644
--- a/doc/rtd/topics/datasources/azure.rst
+++ b/doc/rtd/topics/datasources/azure.rst
@@ -82,7 +82,8 @@ The settings that may be configured are:
82 provided command to obtain metadata.82 provided command to obtain metadata.
83 * **apply_network_config**: Boolean set to True to use network configuration83 * **apply_network_config**: Boolean set to True to use network configuration
84 described by Azure's IMDS endpoint instead of fallback network config of84 described by Azure's IMDS endpoint instead of fallback network config of
85 dhcp on eth0. Default is True. For Ubuntu 16.04 or earlier, default is False.85 dhcp on eth0. Default is True. For Ubuntu 16.04 or earlier, default is
86 False.
86 * **data_dir**: Path used to read metadata files and write crawled data.87 * **data_dir**: Path used to read metadata files and write crawled data.
87 * **dhclient_lease_file**: The fallback lease file to source when looking for88 * **dhclient_lease_file**: The fallback lease file to source when looking for
88 custom DHCP option 245 from Azure fabric.89 custom DHCP option 245 from Azure fabric.
diff --git a/doc/rtd/topics/datasources/cloudstack.rst b/doc/rtd/topics/datasources/cloudstack.rst
index a3101ed..95b9587 100644
--- a/doc/rtd/topics/datasources/cloudstack.rst
+++ b/doc/rtd/topics/datasources/cloudstack.rst
@@ -7,7 +7,7 @@ CloudStack
7sshkey thru the Virtual-Router. The datasource obtains the VR address via7sshkey thru the Virtual-Router. The datasource obtains the VR address via
8dhcp lease information given to the instance.8dhcp lease information given to the instance.
9For more details on meta-data and user-data,9For more details on meta-data and user-data,
10refer the `CloudStack Administrator Guide`_. 10refer the `CloudStack Administrator Guide`_.
1111
12URLs to access user-data and meta-data from the Virtual Machine. Here 10.1.1.112URLs to access user-data and meta-data from the Virtual Machine. Here 10.1.1.1
13is the Virtual Router IP:13is the Virtual Router IP:
diff --git a/doc/rtd/topics/datasources/configdrive.rst b/doc/rtd/topics/datasources/configdrive.rst
index f1a488a..f4c5a34 100644
--- a/doc/rtd/topics/datasources/configdrive.rst
+++ b/doc/rtd/topics/datasources/configdrive.rst
@@ -64,7 +64,7 @@ The following criteria are required to as a config drive:
64::64::
6565
66 openstack/66 openstack/
67 - 2012-08-10/ or latest/ 67 - 2012-08-10/ or latest/
68 - meta_data.json68 - meta_data.json
69 - user_data (not mandatory)69 - user_data (not mandatory)
70 - content/70 - content/
@@ -83,7 +83,7 @@ only) file in the following ways.
8383
84::84::
8585
86 dsmode: 86 dsmode:
87 values: local, net, pass87 values: local, net, pass
88 default: pass88 default: pass
8989
@@ -97,10 +97,10 @@ The difference between 'local' and 'net' is that local will not require
97networking to be up before user-data actions (or boothooks) are run.97networking to be up before user-data actions (or boothooks) are run.
9898
99::99::
100 100
101 instance-id:101 instance-id:
102 default: iid-dsconfigdrive102 default: iid-dsconfigdrive
103 103
104This is utilized as the metadata's instance-id. It should generally104This is utilized as the metadata's instance-id. It should generally
105be unique, as it is what is used to determine "is this a new instance".105be unique, as it is what is used to determine "is this a new instance".
106106
@@ -108,18 +108,18 @@ be unique, as it is what is used to determine "is this a new instance".
108108
109 public-keys:109 public-keys:
110 default: None110 default: None
111 111
112If present, these keys will be used as the public keys for the112If present, these keys will be used as the public keys for the
113instance. This value overrides the content in authorized_keys.113instance. This value overrides the content in authorized_keys.
114114
115Note: it is likely preferable to provide keys via user-data115Note: it is likely preferable to provide keys via user-data
116116
117::117::
118 118
119 user-data:119 user-data:
120 default: None120 default: None
121 121
122This provides cloud-init user-data. See :ref:`examples <yaml_examples>` for 122This provides cloud-init user-data. See :ref:`examples <yaml_examples>` for
123what all can be present here.123what all can be present here.
124124
125.. _OpenStack: http://www.openstack.org/125.. _OpenStack: http://www.openstack.org/
diff --git a/doc/rtd/topics/datasources/digitalocean.rst b/doc/rtd/topics/datasources/digitalocean.rst
index 938ede8..88f1e5f 100644
--- a/doc/rtd/topics/datasources/digitalocean.rst
+++ b/doc/rtd/topics/datasources/digitalocean.rst
@@ -20,8 +20,10 @@ DigitalOcean's datasource can be configured as follows:
20 retries: 320 retries: 3
21 timeout: 221 timeout: 2
2222
23- *retries*: Determines the number of times to attempt to connect to the metadata service23- *retries*: Determines the number of times to attempt to connect to the
24- *timeout*: Determines the timeout in seconds to wait for a response from the metadata service24 metadata service
25- *timeout*: Determines the timeout in seconds to wait for a response from the
26 metadata service
2527
26.. _DigitalOcean: http://digitalocean.com/28.. _DigitalOcean: http://digitalocean.com/
27.. _metadata service: https://developers.digitalocean.com/metadata/29.. _metadata service: https://developers.digitalocean.com/metadata/
diff --git a/doc/rtd/topics/datasources/ec2.rst b/doc/rtd/topics/datasources/ec2.rst
index 76beca9..a90f377 100644
--- a/doc/rtd/topics/datasources/ec2.rst
+++ b/doc/rtd/topics/datasources/ec2.rst
@@ -13,7 +13,7 @@ instance metadata.
13Metadata is accessible via the following URL:13Metadata is accessible via the following URL:
1414
15::15::
16 16
17 GET http://169.254.169.254/2009-04-04/meta-data/17 GET http://169.254.169.254/2009-04-04/meta-data/
18 ami-id18 ami-id
19 ami-launch-index19 ami-launch-index
@@ -34,19 +34,20 @@ Metadata is accessible via the following URL:
34Userdata is accessible via the following URL:34Userdata is accessible via the following URL:
3535
36::36::
37 37
38 GET http://169.254.169.254/2009-04-04/user-data38 GET http://169.254.169.254/2009-04-04/user-data
39 1234,fred,reboot,true | 4512,jimbo, | 173,,,39 1234,fred,reboot,true | 4512,jimbo, | 173,,,
4040
41Note that there are multiple versions of this data provided, cloud-init41Note that there are multiple versions of this data provided, cloud-init
42by default uses **2009-04-04** but newer versions can be supported with42by default uses **2009-04-04** but newer versions can be supported with
43relative ease (newer versions have more data exposed, while maintaining43relative ease (newer versions have more data exposed, while maintaining
44backward compatibility with the previous versions). 44backward compatibility with the previous versions).
4545
46To see which versions are supported from your cloud provider use the following URL:46To see which versions are supported from your cloud provider use the following
47URL:
4748
48::49::
49 50
50 GET http://169.254.169.254/51 GET http://169.254.169.254/
51 1.052 1.0
52 2007-01-1953 2007-01-19
diff --git a/doc/rtd/topics/datasources/exoscale.rst b/doc/rtd/topics/datasources/exoscale.rst
index 27aec9c..9074edc 100644
--- a/doc/rtd/topics/datasources/exoscale.rst
+++ b/doc/rtd/topics/datasources/exoscale.rst
@@ -26,8 +26,8 @@ In the password server case, the following rules apply in order to enable the
26"restore instance password" functionality:26"restore instance password" functionality:
2727
28 * If a password is returned by the password server, it is then marked "saved"28 * If a password is returned by the password server, it is then marked "saved"
29 by the cloud-init datasource. Subsequent boots will skip setting the password29 by the cloud-init datasource. Subsequent boots will skip setting the
30 (the password server will return "saved_password").30 password (the password server will return "saved_password").
31 * When the instance password is reset (via the Exoscale UI), the password31 * When the instance password is reset (via the Exoscale UI), the password
32 server will return the non-empty password at next boot, therefore causing32 server will return the non-empty password at next boot, therefore causing
33 cloud-init to reset the instance's password.33 cloud-init to reset the instance's password.
@@ -38,15 +38,15 @@ Configuration
38Users of this datasource are discouraged from changing the default settings38Users of this datasource are discouraged from changing the default settings
39unless instructed to by Exoscale support.39unless instructed to by Exoscale support.
4040
41The following settings are available and can be set for the datasource in system41The following settings are available and can be set for the datasource in
42configuration (in `/etc/cloud/cloud.cfg.d/`).42system configuration (in `/etc/cloud/cloud.cfg.d/`).
4343
44The settings available are:44The settings available are:
4545
46 * **metadata_url**: The URL for the metadata service (defaults to46 * **metadata_url**: The URL for the metadata service (defaults to
47 ``http://169.254.169.254``)47 ``http://169.254.169.254``)
48 * **api_version**: The API version path on which to query the instance metadata48 * **api_version**: The API version path on which to query the instance
49 (defaults to ``1.0``)49 metadata (defaults to ``1.0``)
50 * **password_server_port**: The port (on the metadata server) on which the50 * **password_server_port**: The port (on the metadata server) on which the
51 password server listens (defaults to ``8080``).51 password server listens (defaults to ``8080``).
52 * **timeout**: the timeout value provided to urlopen for each individual http52 * **timeout**: the timeout value provided to urlopen for each individual http
diff --git a/doc/rtd/topics/datasources/nocloud.rst b/doc/rtd/topics/datasources/nocloud.rst
index 1c5cf96..bc96f7f 100644
--- a/doc/rtd/topics/datasources/nocloud.rst
+++ b/doc/rtd/topics/datasources/nocloud.rst
@@ -57,24 +57,24 @@ Given a disk ubuntu 12.04 cloud image in 'disk.img', you can create a
57sufficient disk by following the example below.57sufficient disk by following the example below.
5858
59::59::
60 60
61 ## create user-data and meta-data files that will be used61 ## create user-data and meta-data files that will be used
62 ## to modify image on first boot62 ## to modify image on first boot
63 $ { echo instance-id: iid-local01; echo local-hostname: cloudimg; } > meta-data63 $ { echo instance-id: iid-local01; echo local-hostname: cloudimg; } > meta-data
64 64
65 $ printf "#cloud-config\npassword: passw0rd\nchpasswd: { expire: False }\nssh_pwauth: True\n" > user-data65 $ printf "#cloud-config\npassword: passw0rd\nchpasswd: { expire: False }\nssh_pwauth: True\n" > user-data
66 66
67 ## create a disk to attach with some user-data and meta-data67 ## create a disk to attach with some user-data and meta-data
68 $ genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-data68 $ genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-data
69 69
70 ## alternatively, create a vfat filesystem with same files70 ## alternatively, create a vfat filesystem with same files
71 ## $ truncate --size 2M seed.img71 ## $ truncate --size 2M seed.img
72 ## $ mkfs.vfat -n cidata seed.img72 ## $ mkfs.vfat -n cidata seed.img
73 ## $ mcopy -oi seed.img user-data meta-data ::73 ## $ mcopy -oi seed.img user-data meta-data ::
74 74
75 ## create a new qcow image to boot, backed by your original image75 ## create a new qcow image to boot, backed by your original image
76 $ qemu-img create -f qcow2 -b disk.img boot-disk.img76 $ qemu-img create -f qcow2 -b disk.img boot-disk.img
77 77
78 ## boot the image and login as 'ubuntu' with password 'passw0rd'78 ## boot the image and login as 'ubuntu' with password 'passw0rd'
79 ## note, passw0rd was set as password through the user-data above,79 ## note, passw0rd was set as password through the user-data above,
80 ## there is no password set on these images.80 ## there is no password set on these images.
@@ -88,12 +88,12 @@ to determine if this is "first boot". So if you are making updates to
88user-data you will also have to change that, or start the disk fresh.88user-data you will also have to change that, or start the disk fresh.
8989
90Also, you can inject an ``/etc/network/interfaces`` file by providing the90Also, you can inject an ``/etc/network/interfaces`` file by providing the
91content for that file in the ``network-interfaces`` field of metadata. 91content for that file in the ``network-interfaces`` field of metadata.
9292
93Example metadata:93Example metadata:
9494
95::95::
96 96
97 instance-id: iid-abcdefg97 instance-id: iid-abcdefg
98 network-interfaces: |98 network-interfaces: |
99 iface eth0 inet static99 iface eth0 inet static
diff --git a/doc/rtd/topics/datasources/opennebula.rst b/doc/rtd/topics/datasources/opennebula.rst
index 7c0367c..8e7c255 100644
--- a/doc/rtd/topics/datasources/opennebula.rst
+++ b/doc/rtd/topics/datasources/opennebula.rst
@@ -21,7 +21,7 @@ Datasource configuration
21Datasource accepts following configuration options.21Datasource accepts following configuration options.
2222
23::23::
24 24
25 dsmode:25 dsmode:
26 values: local, net, disabled26 values: local, net, disabled
27 default: net27 default: net
@@ -30,7 +30,7 @@ Tells if this datasource will be processed in 'local' (pre-networking) or
30'net' (post-networking) stage or even completely 'disabled'.30'net' (post-networking) stage or even completely 'disabled'.
3131
32::32::
33 33
34 parseuser:34 parseuser:
35 default: nobody35 default: nobody
3636
@@ -46,7 +46,7 @@ The following criteria are required:
46 or have a *filesystem* label of **CONTEXT** or **CDROM**46 or have a *filesystem* label of **CONTEXT** or **CDROM**
472. Must contain file *context.sh* with contextualization variables.472. Must contain file *context.sh* with contextualization variables.
48 File is generated by OpenNebula, it has a KEY='VALUE' format and48 File is generated by OpenNebula, it has a KEY='VALUE' format and
49 can be easily read by bash 49 can be easily read by bash
5050
51Contextualization variables51Contextualization variables
52~~~~~~~~~~~~~~~~~~~~~~~~~~~52~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -57,7 +57,7 @@ the OpenNebula documentation. Where multiple similar variables are
57specified, only first found is taken.57specified, only first found is taken.
5858
59::59::
60 60
61 DSMODE61 DSMODE
6262
63Datasource mode configuration override. Values: local, net, disabled.63Datasource mode configuration override. Values: local, net, disabled.
@@ -75,30 +75,30 @@ Datasource mode configuration override. Values: local, net, disabled.
75Static `network configuration`_.75Static `network configuration`_.
7676
77::77::
78 78
79 HOSTNAME79 HOSTNAME
8080
81Instance hostname.81Instance hostname.
8282
83::83::
84 84
85 PUBLIC_IP85 PUBLIC_IP
86 IP_PUBLIC86 IP_PUBLIC
87 ETH0_IP87 ETH0_IP
8888
89If no hostname has been specified, cloud-init will try to create hostname89If no hostname has been specified, cloud-init will try to create hostname
90from instance's IP address in 'local' dsmode. In 'net' dsmode, cloud-init 90from instance's IP address in 'local' dsmode. In 'net' dsmode, cloud-init
91tries to resolve one of its IP addresses to get hostname.91tries to resolve one of its IP addresses to get hostname.
9292
93::93::
94 94
95 SSH_KEY95 SSH_KEY
96 SSH_PUBLIC_KEY96 SSH_PUBLIC_KEY
9797
98One or multiple SSH keys (separated by newlines) can be specified.98One or multiple SSH keys (separated by newlines) can be specified.
9999
100::100::
101 101
102 USER_DATA102 USER_DATA
103 USERDATA103 USERDATA
104104
@@ -111,7 +111,7 @@ This example cloud-init configuration (*cloud.cfg*) enables
111OpenNebula datasource only in 'net' mode.111OpenNebula datasource only in 'net' mode.
112112
113::113::
114 114
115 disable_ec2_metadata: True115 disable_ec2_metadata: True
116 datasource_list: ['OpenNebula']116 datasource_list: ['OpenNebula']
117 datasource:117 datasource:
@@ -123,17 +123,17 @@ Example VM's context section
123~~~~~~~~~~~~~~~~~~~~~~~~~~~~123~~~~~~~~~~~~~~~~~~~~~~~~~~~~
124124
125::125::
126 126
127 CONTEXT=[127 CONTEXT=[
128 PUBLIC_IP="$NIC[IP]",128 PUBLIC_IP="$NIC[IP]",
129 SSH_KEY="$USER[SSH_KEY] 129 SSH_KEY="$USER[SSH_KEY]
130 $USER[SSH_KEY1] 130 $USER[SSH_KEY1]
131 $USER[SSH_KEY2] ",131 $USER[SSH_KEY2] ",
132 USER_DATA="#cloud-config132 USER_DATA="#cloud-config
133 # see https://help.ubuntu.com/community/CloudInit133 # see https://help.ubuntu.com/community/CloudInit
134 134
135 packages: []135 packages: []
136 136
137 mounts:137 mounts:
138 - [vdc,none,swap,sw,0,0]138 - [vdc,none,swap,sw,0,0]
139 runcmd:139 runcmd:
diff --git a/doc/rtd/topics/datasources/openstack.rst b/doc/rtd/topics/datasources/openstack.rst
index 421da08..8ce2a53 100644
--- a/doc/rtd/topics/datasources/openstack.rst
+++ b/doc/rtd/topics/datasources/openstack.rst
@@ -78,6 +78,7 @@ upgrade packages and install ``htop`` on all instances:
78 {"cloud-init": "#cloud-config\npackage_upgrade: True\npackages:\n - htop"}78 {"cloud-init": "#cloud-config\npackage_upgrade: True\npackages:\n - htop"}
7979
80For more general information about how cloud-init handles vendor data,80For more general information about how cloud-init handles vendor data,
81including how it can be disabled by users on instances, see :doc:`/topics/vendordata`.81including how it can be disabled by users on instances, see
82:doc:`/topics/vendordata`.
8283
83.. vi: textwidth=7884.. vi: textwidth=78
diff --git a/doc/rtd/topics/datasources/smartos.rst b/doc/rtd/topics/datasources/smartos.rst
index cb9a128..be11dfb 100644
--- a/doc/rtd/topics/datasources/smartos.rst
+++ b/doc/rtd/topics/datasources/smartos.rst
@@ -15,7 +15,8 @@ second serial console. On Linux, this is /dev/ttyS1. The data is a provided
15via a simple protocol: something queries for the data, the console responds15via a simple protocol: something queries for the data, the console responds
16responds with the status and if "SUCCESS" returns until a single ".\n".16responds with the status and if "SUCCESS" returns until a single ".\n".
1717
18New versions of the SmartOS tooling will include support for base64 encoded data.18New versions of the SmartOS tooling will include support for base64 encoded
19data.
1920
20Meta-data channels21Meta-data channels
21------------------22------------------
@@ -27,7 +28,7 @@ channels of SmartOS.
2728
28 - per the spec, user-data is for consumption by the end-user, not29 - per the spec, user-data is for consumption by the end-user, not
29 provisioning tools30 provisioning tools
30 - cloud-init entirely ignores this channel other than writting it to disk31 - cloud-init entirely ignores this channel other than writing it to disk
31 - removal of the meta-data key means that /var/db/user-data gets removed32 - removal of the meta-data key means that /var/db/user-data gets removed
32 - a backup of previous meta-data is maintained as33 - a backup of previous meta-data is maintained as
33 /var/db/user-data.<timestamp>. <timestamp> is the epoch time when34 /var/db/user-data.<timestamp>. <timestamp> is the epoch time when
@@ -42,8 +43,9 @@ channels of SmartOS.
42 - <timestamp> is the epoch time when cloud-init ran.43 - <timestamp> is the epoch time when cloud-init ran.
43 - when the 'user-script' meta-data key goes missing, the user-script is44 - when the 'user-script' meta-data key goes missing, the user-script is
44 removed from the file system, although a backup is maintained.45 removed from the file system, although a backup is maintained.
45 - if the script is not shebanged (i.e. starts with #!<executable>), then46 - if the script does not start with a shebang (i.e. starts with
46 or is not an executable, cloud-init will add a shebang of "#!/bin/bash"47 #!<executable>), then or is not an executable, cloud-init will add a
48 shebang of "#!/bin/bash"
4749
48* cloud-init:user-data is treated like on other Clouds.50* cloud-init:user-data is treated like on other Clouds.
4951
@@ -133,7 +135,7 @@ or not to base64 decode something:
133 * base64_all: Except for excluded keys, attempt to base64 decode135 * base64_all: Except for excluded keys, attempt to base64 decode
134 the values. If the value fails to decode properly, it will be136 the values. If the value fails to decode properly, it will be
135 returned in its text137 returned in its text
136 * base64_keys: A comma deliminated list of which keys are base64 encoded.138 * base64_keys: A comma delimited list of which keys are base64 encoded.
137 * b64-<key>:139 * b64-<key>:
138 for any key, if there exists an entry in the metadata for 'b64-<key>'140 for any key, if there exists an entry in the metadata for 'b64-<key>'
139 Then 'b64-<key>' is expected to be a plaintext boolean indicating whether141 Then 'b64-<key>' is expected to be a plaintext boolean indicating whether
diff --git a/doc/rtd/topics/debugging.rst b/doc/rtd/topics/debugging.rst
index e13d915..afcf267 100644
--- a/doc/rtd/topics/debugging.rst
+++ b/doc/rtd/topics/debugging.rst
@@ -68,18 +68,18 @@ subcommands default to reading /var/log/cloud-init.log.
68 00.00100s (modules-final/config-rightscale_userdata)68 00.00100s (modules-final/config-rightscale_userdata)
69 ...69 ...
7070
71* ``analyze boot`` Make subprocess calls to the kernel in order to get relevant 71* ``analyze boot`` Make subprocess calls to the kernel in order to get relevant
72 pre-cloud-init timestamps, such as the kernel start, kernel finish boot, and cloud-init start.72 pre-cloud-init timestamps, such as the kernel start, kernel finish boot, and cloud-init start.
7373
74.. code-block:: shell-session74.. code-block:: shell-session
7575
76 $ cloud-init analyze boot 76 $ cloud-init analyze boot
77 -- Most Recent Boot Record --77 -- Most Recent Boot Record --
78 Kernel Started at: 2019-06-13 15:59:55.80938578 Kernel Started at: 2019-06-13 15:59:55.809385
79 Kernel ended boot at: 2019-06-13 16:00:00.94474079 Kernel ended boot at: 2019-06-13 16:00:00.944740
80 Kernel time to boot (seconds): 5.13535580 Kernel time to boot (seconds): 5.135355
81 Cloud-init start: 2019-06-13 16:00:05.73839681 Cloud-init start: 2019-06-13 16:00:05.738396
82 Time between Kernel boot and Cloud-init start (seconds): 4.79365682 Time between Kernel boot and Cloud-init start (seconds): 4.793656
8383
8484
85Analyze quickstart - LXC85Analyze quickstart - LXC
diff --git a/doc/rtd/topics/dir_layout.rst b/doc/rtd/topics/dir_layout.rst
index 7a6265e..ebd63ae 100644
--- a/doc/rtd/topics/dir_layout.rst
+++ b/doc/rtd/topics/dir_layout.rst
@@ -2,11 +2,12 @@
2Directory layout2Directory layout
3****************3****************
44
5Cloudinits's directory structure is somewhat different from a regular application::5Cloud-init's directory structure is somewhat different from a regular
6application::
67
7 /var/lib/cloud/8 /var/lib/cloud/
8 - data/9 - data/
9 - instance-id 10 - instance-id
10 - previous-instance-id11 - previous-instance-id
11 - datasource12 - datasource
12 - previous-datasource13 - previous-datasource
@@ -35,38 +36,41 @@ Cloudinits's directory structure is somewhat different from a regular applicatio
3536
36 The main directory containing the cloud-init specific subdirectories.37 The main directory containing the cloud-init specific subdirectories.
37 It is typically located at ``/var/lib`` but there are certain configuration38 It is typically located at ``/var/lib`` but there are certain configuration
38 scenarios where this can be altered. 39 scenarios where this can be altered.
3940
40 TBD, describe this overriding more.41 TBD, describe this overriding more.
4142
42``data/``43``data/``
4344
44 Contains information related to instance ids, datasources and hostnames of the previous45 Contains information related to instance ids, datasources and hostnames of
45 and current instance if they are different. These can be examined as needed to46 the previous and current instance if they are different. These can be
46 determine any information related to a previous boot (if applicable).47 examined as needed to determine any information related to a previous boot
48 (if applicable).
4749
48``handlers/``50``handlers/``
4951
50 Custom ``part-handlers`` code is written out here. Files that end up here are written52 Custom ``part-handlers`` code is written out here. Files that end up here are
51 out with in the scheme of ``part-handler-XYZ`` where ``XYZ`` is the handler number (the53 written out with in the scheme of ``part-handler-XYZ`` where ``XYZ`` is the
52 first handler found starts at 0).54 handler number (the first handler found starts at 0).
5355
5456
55``instance``57``instance``
5658
57 A symlink to the current ``instances/`` subdirectory that points to the currently59 A symlink to the current ``instances/`` subdirectory that points to the
58 active instance (which is active is dependent on the datasource loaded).60 currently active instance (which is active is dependent on the datasource
61 loaded).
5962
60``instances/``63``instances/``
6164
62 All instances that were created using this image end up with instance identifier65 All instances that were created using this image end up with instance
63 subdirectories (and corresponding data for each instance). The currently active66 identifier subdirectories (and corresponding data for each instance). The
64 instance will be symlinked the ``instance`` symlink file defined previously.67 currently active instance will be symlinked the ``instance`` symlink file
68 defined previously.
6569
66``scripts/``70``scripts/``
6771
68 Scripts that are downloaded/created by the corresponding ``part-handler`` will end up72 Scripts that are downloaded/created by the corresponding ``part-handler``
69 in one of these subdirectories.73 will end up in one of these subdirectories.
7074
71``seed/``75``seed/``
7276
@@ -77,6 +81,7 @@ Cloudinits's directory structure is somewhat different from a regular applicatio
77 Cloud-init has a concept of a module semaphore, which basically consists81 Cloud-init has a concept of a module semaphore, which basically consists
78 of the module name and its frequency. These files are used to ensure a module82 of the module name and its frequency. These files are used to ensure a module
79 is only ran `per-once`, `per-instance`, `per-always`. This folder contains83 is only ran `per-once`, `per-instance`, `per-always`. This folder contains
80 semaphore `files` which are only supposed to run `per-once` (not tied to the instance id).84 semaphore `files` which are only supposed to run `per-once` (not tied to the
85 instance id).
8186
82.. vi: textwidth=7887.. vi: textwidth=78
diff --git a/doc/rtd/topics/docs.rst b/doc/rtd/topics/docs.rst
83new file mode 10064488new file mode 100644
index 0000000..1b15377
--- /dev/null
+++ b/doc/rtd/topics/docs.rst
@@ -0,0 +1,84 @@
1.. _docs:
2
3Docs
4****
5
6These docs are hosted on Read the Docs. The following will explain how to
7contribute to and build these docs locally.
8
9The documentation is primarily written in reStructuredText.
10
11
12Building
13========
14
15There is a makefile target to build the documentation for you:
16
17.. code-block:: shell-session
18
19 $ tox -e doc
20
21This will do two things:
22
23- Build the documentation using sphinx
24- Run doc8 against the documentation source code
25
26Once build the HTML files will be viewable in ``doc/rtd_html``. Use your
27web browser to open ``index.html`` to view and navigate the site.
28
29Style Guide
30===========
31
32Headings
33--------
34The headings used across the documentation use the following hierarchy:
35
36- ``*****``: used once atop of a new page
37- ``=====``: each sections on the page
38- ``-----``: subsections
39- ``^^^^^``: sub-subsections
40- ``"""""``: paragraphs
41
42The top level header ``######`` is reserved for the first page.
43
44If under and overline are used, their length must be identical. The length of
45the underline must be at least as long as the title itself
46
47Line Length
48-----------
49Please keep the line lengths to a maximum of **79** characters. This ensures
50that the pages and tables do not get too wide that side scrolling is required.
51
52Header
53------
54Adding a link at the top of the page allows for the page to be referenced by
55other pages. For example for the FAQ page this would be:
56
57.. code-block:: rst
58
59 .. _faq:
60
61Footer
62------
63The footer should include the textwidth
64
65.. code-block:: rst
66
67 .. vi: textwidth=79
68
69Vertical Whitespace
70-------------------
71One newline between each section helps ensure readability of the documentation
72source code.
73
74Common Words
75------------
76There are some common words that should follow specific usage:
77
78- ``cloud-init``: always lower case with a hyphen, unless starting a sentence
79 in which case only the 'C' is capitalized (e.g. ``Cloud-init``).
80- ``metadata``: one word
81- ``user data``: two words, not to be combined
82- ``vendor data``: like user data, it is two words
83
84.. vi: textwidth=79
diff --git a/doc/rtd/topics/examples.rst b/doc/rtd/topics/examples.rst
index c30d226..62b8ee4 100644
--- a/doc/rtd/topics/examples.rst
+++ b/doc/rtd/topics/examples.rst
@@ -134,7 +134,7 @@ Configure instances ssh-keys
134.. literalinclude:: ../../examples/cloud-config-ssh-keys.txt134.. literalinclude:: ../../examples/cloud-config-ssh-keys.txt
135 :language: yaml135 :language: yaml
136 :linenos:136 :linenos:
137 137
138Additional apt configuration138Additional apt configuration
139============================139============================
140140
diff --git a/doc/rtd/topics/faq.rst b/doc/rtd/topics/faq.rst
141new file mode 100644141new file mode 100644
index 0000000..16e19c2
--- /dev/null
+++ b/doc/rtd/topics/faq.rst
@@ -0,0 +1,43 @@
1.. _faq:
2
3FAQ
4***
5
6Getting help
7============
8
9Having trouble? We would like to help!
10
11- Use the search bar at the upper left to search these docs
12- Ask a question in the ``#cloud-init`` IRC channel on Freenode
13- Join and ask questions on the `cloud-init mailing list <https://launchpad.net/~cloud-init>`_
14- Find a bug? `Report bugs on Launchpad <https://bugs.launchpad.net/cloud-init/+filebug>`_
15
16
17Media
18=====
19
20Below are some videos, blog posts, and white papers about cloud-init from a
21variety of sources.
22
23- `Cloud Instance Initialization with cloud-init (Whitepaper)`_
24- `cloud-init Summit 2018`_
25- `cloud-init - The cross-cloud Magic Sauce (PDF)`_
26- `cloud-init Summit 2017`_
27- `cloud-init - Building clouds one Linux box at a time (Video)`_
28- `cloud-init - Building clouds one Linux box at a time (PDF)`_
29- `Metadata and cloud-init`_
30- `The beauty of cloud-init`_
31- `Introduction to cloud-init`_
32
33.. _Cloud Instance Initialization with cloud-init (Whitepaper): https://ubuntu.com/blog/cloud-instance-initialisation-with-cloud-init
34.. _cloud-init Summit 2018: https://powersj.io/post/cloud-init-summit18/
35.. _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
36.. _cloud-init Summit 2017: https://powersj.io/post/cloud-init-summit17/
37.. _cloud-init - Building clouds one Linux box at a time (Video): https://www.youtube.com/watch?v=1joQfUZQcPg
38.. _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
39.. _Metadata and cloud-init: https://www.youtube.com/watch?v=RHVhIWifVqU
40.. _The beauty of cloud-init: http://brandon.fuller.name/archives/2011/05/02/06.40.57/
41.. _Introduction to cloud-init: http://www.youtube.com/watch?v=-zL3BdbKyGY
42
43.. vi: textwidth=79
diff --git a/doc/rtd/topics/format.rst b/doc/rtd/topics/format.rst
index 74d1fee..7605040 100644
--- a/doc/rtd/topics/format.rst
+++ b/doc/rtd/topics/format.rst
@@ -4,22 +4,24 @@
4User-Data Formats4User-Data Formats
5*****************5*****************
66
7User data that will be acted upon by cloud-init must be in one of the following types.7User data that will be acted upon by cloud-init must be in one of the following
8types.
89
9Gzip Compressed Content10Gzip Compressed Content
10=======================11=======================
1112
12Content found to be gzip compressed will be uncompressed.13Content found to be gzip compressed will be uncompressed.
13The uncompressed data will then be used as if it were not compressed. 14The uncompressed data will then be used as if it were not compressed.
14This is typically useful because user-data is limited to ~16384 [#]_ bytes.15This is typically useful because user-data is limited to ~16384 [#]_ bytes.
1516
16Mime Multi Part Archive17Mime Multi Part Archive
17=======================18=======================
1819
19This list of rules is applied to each part of this multi-part file. 20This list of rules is applied to each part of this multi-part file.
20Using a mime-multi part file, the user can specify more than one type of data.21Using a mime-multi part file, the user can specify more than one type of data.
2122
22For example, both a user data script and a cloud-config type could be specified.23For example, both a user data script and a cloud-config type could be
24specified.
2325
24Supported content-types:26Supported content-types:
2527
@@ -66,7 +68,8 @@ User-Data Script
6668
67Typically used by those who just want to execute a shell script.69Typically used by those who just want to execute a shell script.
6870
69Begins with: ``#!`` or ``Content-Type: text/x-shellscript`` when using a MIME archive.71Begins with: ``#!`` or ``Content-Type: text/x-shellscript`` when using a MIME
72archive.
7073
71.. note::74.. note::
72 New in cloud-init v. 18.4: User-data scripts can also render cloud instance75 New in cloud-init v. 18.4: User-data scripts can also render cloud instance
@@ -83,25 +86,27 @@ Example
83 #!/bin/sh86 #!/bin/sh
84 echo "Hello World. The time is now $(date -R)!" | tee /root/output.txt87 echo "Hello World. The time is now $(date -R)!" | tee /root/output.txt
8588
86 $ euca-run-instances --key mykey --user-data-file myscript.sh ami-a07d95c9 89 $ euca-run-instances --key mykey --user-data-file myscript.sh ami-a07d95c9
8790
88Include File91Include File
89============92============
9093
91This content is a ``include`` file.94This content is a ``include`` file.
9295
93The file contains a list of urls, one per line.96The file contains a list of urls, one per line. Each of the URLs will be read,
94Each of the URLs will be read, and their content will be passed through this same set of rules.97and their content will be passed through this same set of rules. Ie, the
95Ie, the content read from the URL can be gzipped, mime-multi-part, or plain text.98content read from the URL can be gzipped, mime-multi-part, or plain text. If
96If an error occurs reading a file the remaining files will not be read.99an error occurs reading a file the remaining files will not be read.
97100
98Begins with: ``#include`` or ``Content-Type: text/x-include-url`` when using a MIME archive.101Begins with: ``#include`` or ``Content-Type: text/x-include-url`` when using
102a MIME archive.
99103
100Cloud Config Data104Cloud Config Data
101=================105=================
102106
103Cloud-config is the simplest way to accomplish some things107Cloud-config is the simplest way to accomplish some things via user-data. Using
104via user-data. Using cloud-config syntax, the user can specify certain things in a human friendly format. 108cloud-config syntax, the user can specify certain things in a human friendly
109format.
105110
106These things include:111These things include:
107112
@@ -114,9 +119,11 @@ These things include:
114.. note::119.. note::
115 This file must be valid yaml syntax.120 This file must be valid yaml syntax.
116121
117See the :ref:`yaml_examples` section for a commented set of examples of supported cloud config formats.122See the :ref:`yaml_examples` section for a commented set of examples of
123supported cloud config formats.
118124
119Begins with: ``#cloud-config`` or ``Content-Type: text/cloud-config`` when using a MIME archive.125Begins with: ``#cloud-config`` or ``Content-Type: text/cloud-config`` when
126using a MIME archive.
120127
121.. note::128.. note::
122 New in cloud-init v. 18.4: Cloud config dta can also render cloud instance129 New in cloud-init v. 18.4: Cloud config dta can also render cloud instance
@@ -126,25 +133,41 @@ Begins with: ``#cloud-config`` or ``Content-Type: text/cloud-config`` when using
126Upstart Job133Upstart Job
127===========134===========
128135
129Content is placed into a file in ``/etc/init``, and will be consumed by upstart as any other upstart job.136Content is placed into a file in ``/etc/init``, and will be consumed by upstart
137as any other upstart job.
130138
131Begins with: ``#upstart-job`` or ``Content-Type: text/upstart-job`` when using a MIME archive.139Begins with: ``#upstart-job`` or ``Content-Type: text/upstart-job`` when using
140a MIME archive.
132141
133Cloud Boothook142Cloud Boothook
134==============143==============
135144
136This content is ``boothook`` data. It is stored in a file under ``/var/lib/cloud`` and then executed immediately.145This content is ``boothook`` data. It is stored in a file under
137This is the earliest ``hook`` available. Note, that there is no mechanism provided for running only once. The boothook must take care of this itself.146``/var/lib/cloud`` and then executed immediately. This is the earliest ``hook``
138It 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.147available. Note, that there is no mechanism provided for running only once. The
148boothook must take care of this itself.
139149
140Begins with: ``#cloud-boothook`` or ``Content-Type: text/cloud-boothook`` when using a MIME archive.150It is provided with the instance id in the environment variable
151``INSTANCE_ID``. This could be made use of to provide a 'once-per-instance'
152type of functionality.
153
154Begins with: ``#cloud-boothook`` or ``Content-Type: text/cloud-boothook`` when
155using a MIME archive.
141156
142Part Handler157Part Handler
143============158============
144159
145This 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).160This is a ``part-handler``: It contains custom code for either supporting new
146This must be python code that contains a ``list_types`` function and a ``handle_part`` function. 161mime-types in multi-part user data, or overriding the existing handlers for
147Once 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.162supported mime-types. It will be written to a file in ``/var/lib/cloud/data``
163based on its filename (which is generated).
164
165This must be python code that contains a ``list_types`` function and a
166``handle_part`` function. Once the section is read the ``list_types`` method
167will be called. It must return a list of mime-types that this part-handler
168handles. Because mime parts are processed in order, a ``part-handler`` part
169must precede any parts with mime-types it is expected to handle in the same
170user data.
148171
149The ``handle_part`` function must be defined like:172The ``handle_part`` function must be defined like:
150173
@@ -156,11 +179,13 @@ The ``handle_part`` function must be defined like:
156 # filename = the filename of the part (or a generated filename if none is present in mime data)179 # filename = the filename of the part (or a generated filename if none is present in mime data)
157 # payload = the parts' content180 # payload = the parts' content
158181
159Cloud-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.182Cloud-init will then call the ``handle_part`` function once before it handles
160The ``'__begin__'`` and ``'__end__'`` sentinels allow the part handler to do initialization or teardown before or after183any parts, once per part received, and once after all parts have been handled.
161receiving any parts.184The ``'__begin__'`` and ``'__end__'`` sentinels allow the part handler to do
185initialization or teardown before or after receiving any parts.
162186
163Begins with: ``#part-handler`` or ``Content-Type: text/part-handler`` when using a MIME archive.187Begins with: ``#part-handler`` or ``Content-Type: text/part-handler`` when
188using a MIME archive.
164189
165Example190Example
166-------191-------
diff --git a/doc/rtd/topics/merging.rst b/doc/rtd/topics/merging.rst
index 5f7ca18..2b5e5da 100644
--- a/doc/rtd/topics/merging.rst
+++ b/doc/rtd/topics/merging.rst
@@ -68,8 +68,10 @@ Cloud-init provides merging for the following built-in types:
68The ``Dict`` merger has the following options which control what is done with68The ``Dict`` merger has the following options which control what is done with
69values contained within the config.69values contained within the config.
7070
71- ``allow_delete``: Existing values not present in the new value can be deleted, defaults to False71- ``allow_delete``: Existing values not present in the new value can be
72- ``no_replace``: Do not replace an existing value if one is already present, enabled by default.72 deleted, defaults to False
73- ``no_replace``: Do not replace an existing value if one is already present,
74 enabled by default.
73- ``replace``: Overwrite existing values with new ones.75- ``replace``: Overwrite existing values with new ones.
7476
75The ``List`` merger has the following options which control what is done with77The ``List`` merger has the following options which control what is done with
@@ -77,7 +79,8 @@ the values contained within the config.
7779
78- ``append``: Add new value to the end of the list, defaults to False.80- ``append``: Add new value to the end of the list, defaults to False.
79- ``prepend``: Add new values to the start of the list, defaults to False.81- ``prepend``: Add new values to the start of the list, defaults to False.
80- ``no_replace``: Do not replace an existing value if one is already present, enabled by default.82- ``no_replace``: Do not replace an existing value if one is already present,
83 enabled by default.
81- ``replace``: Overwrite existing values with new ones.84- ``replace``: Overwrite existing values with new ones.
8285
83The ``Str`` merger has the following options which control what is done with86The ``Str`` merger has the following options which control what is done with
@@ -88,10 +91,13 @@ the values contained within the config.
88Common options for all merge types which control how recursive merging is91Common options for all merge types which control how recursive merging is
89done on other types.92done on other types.
9093
91- ``recurse_dict``: If True merge the new values of the dictionary, defaults to True.94- ``recurse_dict``: If True merge the new values of the dictionary, defaults to
92- ``recurse_list``: If True merge the new values of the list, defaults to False.95 True.
96- ``recurse_list``: If True merge the new values of the list, defaults to
97 False.
93- ``recurse_array``: Alias for ``recurse_list``.98- ``recurse_array``: Alias for ``recurse_list``.
94- ``recurse_str``: If True merge the new values of the string, defaults to False.99- ``recurse_str``: If True merge the new values of the string, defaults to
100 False.
95101
96102
97Customizability103Customizability
diff --git a/doc/rtd/topics/moreinfo.rst b/doc/rtd/topics/moreinfo.rst
98deleted file mode 100644104deleted file mode 100644
index 9c3b7fb..0000000
--- a/doc/rtd/topics/moreinfo.rst
+++ /dev/null
@@ -1,13 +0,0 @@
1****************
2More information
3****************
4
5Useful external references
6==========================
7
8- `The beauty of cloudinit`_
9- `Introduction to cloud-init`_ (video)
10
11.. _Introduction to cloud-init: http://www.youtube.com/watch?v=-zL3BdbKyGY
12.. _The beauty of cloudinit: http://brandon.fuller.name/archives/2011/05/02/06.40.57/
13.. vi: textwidth=78
diff --git a/doc/rtd/topics/network-config-format-v2.rst b/doc/rtd/topics/network-config-format-v2.rst
index 50f5fa6..7f85755 100644
--- a/doc/rtd/topics/network-config-format-v2.rst
+++ b/doc/rtd/topics/network-config-format-v2.rst
@@ -54,11 +54,11 @@ Physical devices
5454
55: (Examples: ethernet, wifi) These can dynamically come and go between55: (Examples: ethernet, wifi) These can dynamically come and go between
56 reboots and even during runtime (hotplugging). In the generic case, they56 reboots and even during runtime (hotplugging). In the generic case, they
57 can be selected by ``match:`` rules on desired properties, such as name/name57 can be selected by ``match:`` rules on desired properties, such as
58 pattern, MAC address, driver, or device paths. In general these will match58 name/name pattern, MAC address, driver, or device paths. In general these
59 any number of devices (unless they refer to properties which are unique59 will match any number of devices (unless they refer to properties which are
60 such as the full path or MAC address), so without further knowledge about60 unique such as the full path or MAC address), so without further knowledge
61 the hardware these will always be considered as a group.61 about the hardware these will always be considered as a group.
6262
63 It is valid to specify no match rules at all, in which case the ID field is63 It is valid to specify no match rules at all, in which case the ID field is
64 simply the interface name to be matched. This is mostly useful if you want64 simply the interface name to be matched. This is mostly useful if you want
@@ -228,8 +228,8 @@ Example: ::
228228
229**parameters**: *<(mapping)>*229**parameters**: *<(mapping)>*
230230
231Customization parameters for special bonding options. Time values are specified231Customization parameters for special bonding options. Time values are
232in seconds unless otherwise specified.232specified in seconds unless otherwise specified.
233233
234**mode**: *<(scalar)>*234**mode**: *<(scalar)>*
235235
@@ -367,8 +367,8 @@ Example: ::
367367
368**parameters**: <*(mapping)>*368**parameters**: <*(mapping)>*
369369
370Customization parameters for special bridging options. Time values are specified370Customization parameters for special bridging options. Time values are
371in seconds unless otherwise specified.371specified in seconds unless otherwise specified.
372372
373**ageing-time**: <*(scalar)>*373**ageing-time**: <*(scalar)>*
374374
diff --git a/tests/unittests/test_datasource/test_ovf.py b/tests/unittests/test_datasource/test_ovf.py
index 349d54c..a615470 100644
--- a/tests/unittests/test_datasource/test_ovf.py
+++ b/tests/unittests/test_datasource/test_ovf.py
@@ -169,19 +169,56 @@ class TestDatasourceOVF(CiTestCase):
169 MARKER-ID = 12345345169 MARKER-ID = 12345345
170 """)170 """)
171 util.write_file(conf_file, conf_content)171 util.write_file(conf_file, conf_content)
172 with self.assertRaises(CustomScriptNotFound) as context:172 with mock.patch(MPATH + 'get_tools_config', return_value='true'):
173 wrap_and_call(173 with self.assertRaises(CustomScriptNotFound) as context:
174 'cloudinit.sources.DataSourceOVF',174 wrap_and_call(
175 {'util.read_dmi_data': 'vmware',175 'cloudinit.sources.DataSourceOVF',
176 'util.del_dir': True,176 {'util.read_dmi_data': 'vmware',
177 'search_file': self.tdir,177 'util.del_dir': True,
178 'wait_for_imc_cfg_file': conf_file,178 'search_file': self.tdir,
179 'get_nics_to_enable': ''},179 'wait_for_imc_cfg_file': conf_file,
180 ds.get_data)180 'get_nics_to_enable': ''},
181 ds.get_data)
181 customscript = self.tmp_path('test-script', self.tdir)182 customscript = self.tmp_path('test-script', self.tdir)
182 self.assertIn('Script %s not found!!' % customscript,183 self.assertIn('Script %s not found!!' % customscript,
183 str(context.exception))184 str(context.exception))
184185
186 def test_get_data_cust_script_disabled(self):
187 """If custom script is disabled by VMware tools configuration,
188 raise a RuntimeError.
189 """
190 paths = Paths({'cloud_dir': self.tdir})
191 ds = self.datasource(
192 sys_cfg={'disable_vmware_customization': False}, distro={},
193 paths=paths)
194 # Prepare the conf file
195 conf_file = self.tmp_path('test-cust', self.tdir)
196 conf_content = dedent("""\
197 [CUSTOM-SCRIPT]
198 SCRIPT-NAME = test-script
199 [MISC]
200 MARKER-ID = 12345346
201 """)
202 util.write_file(conf_file, conf_content)
203 # Prepare the custom sript
204 customscript = self.tmp_path('test-script', self.tdir)
205 util.write_file(customscript, "This is the post cust script")
206
207 with mock.patch(MPATH + 'get_tools_config', return_value='false'):
208 with mock.patch(MPATH + 'set_customization_status',
209 return_value=('msg', b'')):
210 with self.assertRaises(RuntimeError) as context:
211 wrap_and_call(
212 'cloudinit.sources.DataSourceOVF',
213 {'util.read_dmi_data': 'vmware',
214 'util.del_dir': True,
215 'search_file': self.tdir,
216 'wait_for_imc_cfg_file': conf_file,
217 'get_nics_to_enable': ''},
218 ds.get_data)
219 self.assertIn('Custom script is disabled by VM Administrator',
220 str(context.exception))
221
185 def test_get_data_non_vmware_seed_platform_info(self):222 def test_get_data_non_vmware_seed_platform_info(self):
186 """Platform info properly reports when on non-vmware platforms."""223 """Platform info properly reports when on non-vmware platforms."""
187 paths = Paths({'cloud_dir': self.tdir, 'run_dir': self.tdir})224 paths = Paths({'cloud_dir': self.tdir, 'run_dir': self.tdir})
diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
index 587e699..de87be2 100644
--- a/tests/unittests/test_ds_identify.py
+++ b/tests/unittests/test_ds_identify.py
@@ -195,6 +195,10 @@ class DsIdentifyBase(CiTestCase):
195 return self._check_via_dict(195 return self._check_via_dict(
196 data, RC_FOUND, dslist=[data.get('ds'), DS_NONE])196 data, RC_FOUND, dslist=[data.get('ds'), DS_NONE])
197197
198 def _test_ds_not_found(self, name):
199 data = copy.deepcopy(VALID_CFG[name])
200 return self._check_via_dict(data, RC_NOT_FOUND)
201
198 def _check_via_dict(self, data, rc, dslist=None, **kwargs):202 def _check_via_dict(self, data, rc, dslist=None, **kwargs):
199 ret = self._call_via_dict(data, **kwargs)203 ret = self._call_via_dict(data, **kwargs)
200 good = False204 good = False
@@ -244,9 +248,13 @@ class TestDsIdentify(DsIdentifyBase):
244 self._test_ds_found('Ec2-xen')248 self._test_ds_found('Ec2-xen')
245249
246 def test_brightbox_is_ec2(self):250 def test_brightbox_is_ec2(self):
247 """EC2: product_serial ends with 'brightbox.com'"""251 """EC2: product_serial ends with '.brightbox.com'"""
248 self._test_ds_found('Ec2-brightbox')252 self._test_ds_found('Ec2-brightbox')
249253
254 def test_bobrightbox_is_not_brightbox(self):
255 """EC2: bobrightbox.com in product_serial is not brightbox'"""
256 self._test_ds_not_found('Ec2-brightbox-negative')
257
250 def test_gce_by_product_name(self):258 def test_gce_by_product_name(self):
251 """GCE identifies itself with product_name."""259 """GCE identifies itself with product_name."""
252 self._test_ds_found('GCE')260 self._test_ds_found('GCE')
@@ -724,7 +732,11 @@ VALID_CFG = {
724 },732 },
725 'Ec2-brightbox': {733 'Ec2-brightbox': {
726 'ds': 'Ec2',734 'ds': 'Ec2',
727 'files': {P_PRODUCT_SERIAL: 'facc6e2f.brightbox.com\n'},735 'files': {P_PRODUCT_SERIAL: 'srv-otuxg.gb1.brightbox.com\n'},
736 },
737 'Ec2-brightbox-negative': {
738 'ds': 'Ec2',
739 'files': {P_PRODUCT_SERIAL: 'tricky-host.bobrightbox.com\n'},
728 },740 },
729 'GCE': {741 'GCE': {
730 'ds': 'GCE',742 'ds': 'GCE',
diff --git a/tests/unittests/test_vmware/test_guestcust_util.py b/tests/unittests/test_vmware/test_guestcust_util.py
731new file mode 100644743new file mode 100644
index 0000000..b8fa994
--- /dev/null
+++ b/tests/unittests/test_vmware/test_guestcust_util.py
@@ -0,0 +1,65 @@
1# Copyright (C) 2019 Canonical Ltd.
2# Copyright (C) 2019 VMware INC.
3#
4# Author: Xiaofeng Wang <xiaofengw@vmware.com>
5#
6# This file is part of cloud-init. See LICENSE file for license information.
7
8from cloudinit import util
9from cloudinit.sources.helpers.vmware.imc.guestcust_util import (
10 get_tools_config,
11)
12from cloudinit.tests.helpers import CiTestCase, mock
13
14
15class TestGuestCustUtil(CiTestCase):
16 def test_get_tools_config_not_installed(self):
17 """
18 This test is designed to verify the behavior if vmware-toolbox-cmd
19 is not installed.
20 """
21 with mock.patch.object(util, 'which', return_value=None):
22 self.assertEqual(
23 get_tools_config('section', 'key', 'defaultVal'), 'defaultVal')
24
25 def test_get_tools_config_internal_exception(self):
26 """
27 This test is designed to verify the behavior if internal exception
28 is raised.
29 """
30 with mock.patch.object(util, 'which', return_value='/dummy/path'):
31 with mock.patch.object(util, 'subp',
32 return_value=('key=value', b''),
33 side_effect=util.ProcessExecutionError(
34 "subp failed", exit_code=99)):
35 # verify return value is 'defaultVal', not 'value'.
36 self.assertEqual(
37 get_tools_config('section', 'key', 'defaultVal'),
38 'defaultVal')
39
40 def test_get_tools_config_normal(self):
41 """
42 This test is designed to verify the value could be parsed from
43 key = value of the given [section]
44 """
45 with mock.patch.object(util, 'which', return_value='/dummy/path'):
46 # value is not blank
47 with mock.patch.object(util, 'subp',
48 return_value=('key = value ', b'')):
49 self.assertEqual(
50 get_tools_config('section', 'key', 'defaultVal'),
51 'value')
52 # value is blank
53 with mock.patch.object(util, 'subp',
54 return_value=('key = ', b'')):
55 self.assertEqual(
56 get_tools_config('section', 'key', 'defaultVal'),
57 '')
58 # value contains =
59 with mock.patch.object(util, 'subp',
60 return_value=('key=Bar=Wark', b'')):
61 self.assertEqual(
62 get_tools_config('section', 'key', 'defaultVal'),
63 'Bar=Wark')
64
65# vi: ts=4 expandtab
diff --git a/tools/ds-identify b/tools/ds-identify
index e0d4865..2447d14 100755
--- a/tools/ds-identify
+++ b/tools/ds-identify
@@ -891,9 +891,8 @@ ec2_identify_platform() {
891 local default="$1"891 local default="$1"
892 local serial="${DI_DMI_PRODUCT_SERIAL}"892 local serial="${DI_DMI_PRODUCT_SERIAL}"
893893
894 # brightbox https://bugs.launchpad.net/cloud-init/+bug/1661693
895 case "$serial" in894 case "$serial" in
896 *brightbox.com) _RET="Brightbox"; return 0;;895 *.brightbox.com) _RET="Brightbox"; return 0;;
897 esac896 esac
898897
899 # AWS http://docs.aws.amazon.com/AWSEC2/898 # AWS http://docs.aws.amazon.com/AWSEC2/
diff --git a/tox.ini b/tox.ini
index 1f01eb7..f5baf32 100644
--- a/tox.ini
+++ b/tox.ini
@@ -53,8 +53,13 @@ exclude = .venv,.tox,dist,doc,*egg,.git,build,tools
5353
54[testenv:doc]54[testenv:doc]
55basepython = python355basepython = python3
56deps = sphinx56deps =
57commands = {envpython} -m sphinx {posargs:doc/rtd doc/rtd_html}57 doc8
58 sphinx
59 sphinx_rtd_theme
60commands =
61 {envpython} -m sphinx {posargs:doc/rtd doc/rtd_html}
62 doc8 doc/rtd
5863
59[testenv:xenial]64[testenv:xenial]
60commands =65commands =

Subscribers

People subscribed via source and target branches