Merge ~smoser/cloud-init:feature/ds-init into cloud-init:master

Proposed by Scott Moser
Status: Merged
Merged at revision: 9e904bbc3336b96475bfd00fb3bf1262ae4de49f
Proposed branch: ~smoser/cloud-init:feature/ds-init
Merge into: cloud-init:master
Diff against target: 381 lines (+136/-107)
6 files modified
cloudinit/cmd/main.py (+3/-0)
cloudinit/config/cc_mounts.py (+9/-3)
cloudinit/sources/DataSourceAzure.py (+104/-95)
cloudinit/sources/__init__.py (+12/-0)
cloudinit/stages.py (+7/-0)
tests/unittests/test_datasource/test_azure.py (+1/-9)
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+311205@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Ryan Harper (raharper) :

There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
index 83eb02c..fe37075 100644
--- a/cloudinit/cmd/main.py
+++ b/cloudinit/cmd/main.py
@@ -326,6 +326,9 @@ def main_init(name, args):
326 util.logexc(LOG, "Failed to re-adjust output redirection!")326 util.logexc(LOG, "Failed to re-adjust output redirection!")
327 logging.setupLogging(mods.cfg)327 logging.setupLogging(mods.cfg)
328328
329 # give the activated datasource a chance to adjust
330 init.activate_datasource()
331
329 # Stage 10332 # Stage 10
330 return (init.datasource, run_module_section(mods, name, name))333 return (init.datasource, run_module_section(mods, name, name))
331334
diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py
index dfc4b59..452c9e8 100644
--- a/cloudinit/config/cc_mounts.py
+++ b/cloudinit/config/cc_mounts.py
@@ -312,7 +312,8 @@ def handle_swapcfg(swapcfg):
312def handle(_name, cfg, cloud, log, _args):312def handle(_name, cfg, cloud, log, _args):
313 # fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno313 # fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno
314 def_mnt_opts = "defaults,nobootwait"314 def_mnt_opts = "defaults,nobootwait"
315 if cloud.distro.uses_systemd():315 uses_systemd = cloud.distro.uses_systemd()
316 if uses_systemd:
316 def_mnt_opts = "defaults,nofail,x-systemd.requires=cloud-init.service"317 def_mnt_opts = "defaults,nofail,x-systemd.requires=cloud-init.service"
317318
318 defvals = [None, None, "auto", def_mnt_opts, "0", "2"]319 defvals = [None, None, "auto", def_mnt_opts, "0", "2"]
@@ -447,7 +448,12 @@ def handle(_name, cfg, cloud, log, _args):
447 except Exception:448 except Exception:
448 util.logexc(log, "Failed to make '%s' config-mount", d)449 util.logexc(log, "Failed to make '%s' config-mount", d)
449450
451 activate_cmd = ["mount", "-a"]
452 if uses_systemd:
453 activate_cmd = ["systemctl", "daemon-reload"]
454 fmt = "Activate mounts: %s:" + ' '.join(activate_cmd)
450 try:455 try:
451 util.subp(("mount", "-a"))456 util.subp(activate_cmd)
457 LOG.debug(fmt, "PASS")
452 except util.ProcessExecutionError:458 except util.ProcessExecutionError:
453 util.logexc(log, "Activating mounts via 'mount -a' failed")459 util.logexc(log, fmt, "FAIL")
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index b802b03..22f9004 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -19,7 +19,6 @@
19import base6419import base64
20import contextlib20import contextlib
21import crypt21import crypt
22import fnmatch
23from functools import partial22from functools import partial
24import os23import os
25import os.path24import os.path
@@ -28,7 +27,6 @@ from xml.dom import minidom
28import xml.etree.ElementTree as ET27import xml.etree.ElementTree as ET
2928
30from cloudinit import log as logging29from cloudinit import log as logging
31from cloudinit.settings import PER_ALWAYS
32from cloudinit import sources30from cloudinit import sources
33from cloudinit.sources.helpers.azure import get_metadata_from_fabric31from cloudinit.sources.helpers.azure import get_metadata_from_fabric
34from cloudinit import util32from cloudinit import util
@@ -42,6 +40,9 @@ BOUNCE_COMMAND = [
42 'sh', '-xc',40 'sh', '-xc',
43 "i=$interface; x=0; ifdown $i || x=$?; ifup $i || x=$?; exit $x"41 "i=$interface; x=0; ifdown $i || x=$?; ifup $i || x=$?; exit $x"
44]42]
43# azure systems will always have a resource disk, and 66-azure-ephemeral.rules
44# ensures that it gets linked to this path.
45RESOURCE_DISK_PATH = '/dev/disk/cloud/azure_resource'
4546
46BUILTIN_DS_CONFIG = {47BUILTIN_DS_CONFIG = {
47 'agent_command': AGENT_START,48 'agent_command': AGENT_START,
@@ -53,7 +54,7 @@ BUILTIN_DS_CONFIG = {
53 'command': BOUNCE_COMMAND,54 'command': BOUNCE_COMMAND,
54 'hostname_command': 'hostname',55 'hostname_command': 'hostname',
55 },56 },
56 'disk_aliases': {'ephemeral0': '/dev/sdb'},57 'disk_aliases': {'ephemeral0': RESOURCE_DISK_PATH},
57 'dhclient_lease_file': '/var/lib/dhcp/dhclient.eth0.leases',58 'dhclient_lease_file': '/var/lib/dhcp/dhclient.eth0.leases',
58}59}
5960
@@ -245,15 +246,6 @@ class DataSourceAzureNet(sources.DataSource):
245 self.metadata['instance-id'] = util.read_dmi_data('system-uuid')246 self.metadata['instance-id'] = util.read_dmi_data('system-uuid')
246 self.metadata.update(fabric_data)247 self.metadata.update(fabric_data)
247248
248 found_ephemeral = find_fabric_formatted_ephemeral_disk()
249 if found_ephemeral:
250 self.ds_cfg['disk_aliases']['ephemeral0'] = found_ephemeral
251 LOG.debug("using detected ephemeral0 of %s", found_ephemeral)
252
253 cc_modules_override = support_new_ephemeral(self.sys_cfg)
254 if cc_modules_override:
255 self.cfg['cloud_init_modules'] = cc_modules_override
256
257 return True249 return True
258250
259 def device_name_to_device(self, name):251 def device_name_to_device(self, name):
@@ -266,97 +258,104 @@ class DataSourceAzureNet(sources.DataSource):
266 # quickly (local check only) if self.instance_id is still valid258 # quickly (local check only) if self.instance_id is still valid
267 return sources.instance_id_matches_system_uuid(self.get_instance_id())259 return sources.instance_id_matches_system_uuid(self.get_instance_id())
268260
269261 def activate(self, cfg, is_new_instance):
270def count_files(mp):262 address_ephemeral_resize(is_new_instance=is_new_instance)
271 return len(fnmatch.filter(os.listdir(mp), '*[!cdrom]*'))263 return
272264
273265
274def find_fabric_formatted_ephemeral_part():266def can_dev_be_reformatted(devpath):
275 """267 # determine if the ephemeral block device path devpath
276 Locate the first fabric formatted ephemeral device.268 # is newly formatted after a resize.
277 """269 if not os.path.exists(devpath):
278 potential_locations = ['/dev/disk/cloud/azure_resource-part1',270 return False, 'device %s does not exist' % devpath
279 '/dev/disk/azure/resource-part1']271
280 device_location = None272 realpath = os.path.realpath(devpath)
281 for potential_location in potential_locations:273 LOG.debug('Resolving realpath of %s -> %s', devpath, realpath)
282 if os.path.exists(potential_location):274
283 device_location = potential_location275 # it is possible that the block device might exist, but the kernel
276 # have not yet read the partition table and sent events. we udevadm settle
277 # to hope to resolve that. Better here would probably be to test and see,
278 # and then settle if we didn't find anything and try again.
279 if util.which("udevadm"):
280 util.subp(["udevadm", "settle"])
281
282 # devpath of /dev/sd[a-z] or /dev/disk/cloud/azure_resource
283 # where partitions are "<devpath>1" or "<devpath>-part1" or "<devpath>p1"
284 part1path = None
285 for suff in ("-part", "p", ""):
286 cand = devpath + suff + "1"
287 if os.path.exists(cand):
288 if os.path.exists(devpath + suff + "2"):
289 msg = ('device %s had more than 1 partition: %s, %s' %
290 devpath, cand, devpath + suff + "2")
291 return False, msg
292 part1path = cand
284 break293 break
285 if device_location is None:
286 LOG.debug("no azure resource disk partition path found")
287 return None
288 ntfs_devices = util.find_devs_with("TYPE=ntfs")
289 real_device = os.path.realpath(device_location)
290 if real_device in ntfs_devices:
291 return device_location
292 LOG.debug("'%s' existed (%s) but was not ntfs formated",
293 device_location, real_device)
294 return None
295
296
297def find_fabric_formatted_ephemeral_disk():
298 """
299 Get the ephemeral disk.
300 """
301 part_dev = find_fabric_formatted_ephemeral_part()
302 if part_dev:
303 return part_dev.split('-')[0]
304 return None
305294
295 if part1path is None:
296 return False, 'device %s was not partitioned' % devpath
306297
307def support_new_ephemeral(cfg):298 real_part1path = os.path.realpath(part1path)
308 """299 ntfs_devices = util.find_devs_with("TYPE=ntfs", no_cache=True)
309 Windows Azure makes ephemeral devices ephemeral to boot; a ephemeral device300 LOG.debug('ntfs_devices found = %s', ntfs_devices)
310 may be presented as a fresh device, or not.301 if real_part1path not in ntfs_devices:
302 msg = ('partition 1 (%s -> %s) on device %s was not ntfs formatted' %
303 (part1path, real_part1path, devpath))
304 return False, msg
311305
312 Since the knowledge of when a disk is supposed to be plowed under is306 def count_files(mp):
313 specific to Windows Azure, the logic resides here in the datasource. When a307 ignored = {'dataloss_warning_readme.txt'}
314 new ephemeral device is detected, cloud-init overrides the default308 return len([f for f in os.listdir(mp) if f.lower() not in ignored])
315 frequency for both disk-setup and mounts for the current boot only.
316 """
317 device = find_fabric_formatted_ephemeral_part()
318 if not device:
319 LOG.debug("no default fabric formated ephemeral0.1 found")
320 return None
321 LOG.debug("fabric formated ephemeral0.1 device at %s", device)
322309
323 file_count = 0310 bmsg = ('partition 1 (%s -> %s) on device %s was ntfs formatted' %
311 (part1path, real_part1path, devpath))
324 try:312 try:
325 file_count = util.mount_cb(device, count_files)313 file_count = util.mount_cb(part1path, count_files)
326 except Exception:314 except util.MountFailedError as e:
327 return None315 return False, bmsg + ' but mount of %s failed: %s' % (part1path, e)
328 LOG.debug("fabric prepared ephmeral0.1 has %s files on it", file_count)316
329317 if file_count != 0:
330 if file_count >= 1:318 return False, bmsg + ' but had %d files on it.' % file_count
331 LOG.debug("fabric prepared ephemeral0.1 will be preserved")319
332 return None320 return True, bmsg + ' and had no important files. Safe for reformatting.'
321
322
323def address_ephemeral_resize(devpath=RESOURCE_DISK_PATH, maxwait=120,
324 is_new_instance=False):
325 # wait for ephemeral disk to come up
326 naplen = .2
327 missing = wait_for_files([devpath], maxwait=maxwait, naplen=naplen,
328 log_pre="Azure ephemeral disk: ")
329
330 if missing:
331 LOG.warn("ephemeral device '%s' did not appear after %d seconds.",
332 devpath, maxwait)
333 return
334
335 result = False
336 msg = None
337 if is_new_instance:
338 result, msg = (True, "First instance boot.")
333 else:339 else:
334 # if device was already mounted, then we need to unmount it340 result, msg = can_dev_be_reformatted(devpath)
335 # race conditions could allow for a check-then-unmount341
336 # to have a false positive. so just unmount and then check.342 LOG.debug("reformattable=%s: %s" % (result, msg))
337 try:343 if not result:
338 util.subp(['umount', device])344 return
339 except util.ProcessExecutionError as e:345
340 if device in util.mounts():346 for mod in ['disk_setup', 'mounts']:
341 LOG.warn("Failed to unmount %s, will not reformat.", device)347 sempath = '/var/lib/cloud/instance/sem/config_' + mod
342 LOG.debug("Failed umount: %s", e)348 bmsg = 'Marker "%s" for module "%s"' % (sempath, mod)
343 return None349 if os.path.exists(sempath):
344350 try:
345 LOG.debug("cloud-init will format ephemeral0.1 this boot.")351 os.unlink(sempath)
346 LOG.debug("setting disk_setup and mounts modules 'always' for this boot")352 LOG.debug(bmsg + " removed.")
347353 except Exception as e:
348 cc_modules = cfg.get('cloud_init_modules')354 # python3 throws FileNotFoundError, python2 throws OSError
349 if not cc_modules:355 LOG.warn(bmsg + ": remove failed! (%s)" % e)
350 return None
351
352 mod_list = []
353 for mod in cc_modules:
354 if mod in ("disk_setup", "mounts"):
355 mod_list.append([mod, PER_ALWAYS])
356 LOG.debug("set module '%s' to 'always' for this boot", mod)
357 else:356 else:
358 mod_list.append(mod)357 LOG.debug(bmsg + " did not exist.")
359 return mod_list358 return
360359
361360
362def perform_hostname_bounce(hostname, cfg, prev_hostname):361def perform_hostname_bounce(hostname, cfg, prev_hostname):
@@ -408,15 +407,25 @@ def pubkeys_from_crt_files(flist):
408 return pubkeys407 return pubkeys
409408
410409
411def wait_for_files(flist, maxwait=60, naplen=.5):410def wait_for_files(flist, maxwait=60, naplen=.5, log_pre=""):
412 need = set(flist)411 need = set(flist)
413 waited = 0412 waited = 0
414 while waited < maxwait:413 while True:
415 need -= set([f for f in need if os.path.exists(f)])414 need -= set([f for f in need if os.path.exists(f)])
416 if len(need) == 0:415 if len(need) == 0:
416 LOG.debug("%sAll files appeared after %s seconds: %s",
417 log_pre, waited, flist)
417 return []418 return []
419 if waited == 0:
420 LOG.info("%sWaiting up to %s seconds for the following files: %s",
421 log_pre, maxwait, flist)
422 if waited + naplen > maxwait:
423 break
418 time.sleep(naplen)424 time.sleep(naplen)
419 waited += naplen425 waited += naplen
426
427 LOG.warn("%sStill missing files after %s seconds: %s",
428 log_pre, maxwait, need)
420 return need429 return need
421430
422431
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index d139527..13fb7c6 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -261,6 +261,18 @@ class DataSource(object):
261 def first_instance_boot(self):261 def first_instance_boot(self):
262 return262 return
263263
264 def activate(self, cfg, is_new_instance):
265 """activate(cfg, is_new_instance)
266
267 This is called before the init_modules will be called.
268 The cfg is fully up to date config, it contains a merged view of
269 system config, datasource config, user config, vendor config.
270 It should be used rather than the sys_cfg passed to __init__.
271
272 is_new_instance is a boolean indicating if this is a new instance.
273 """
274 return
275
264276
265def normalize_pubkey_data(pubkey_data):277def normalize_pubkey_data(pubkey_data):
266 keys = []278 keys = []
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index 47deac6..86a1378 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -371,6 +371,13 @@ class Init(object):
371 self._store_userdata()371 self._store_userdata()
372 self._store_vendordata()372 self._store_vendordata()
373373
374 def activate_datasource(self):
375 if self.datasource is None:
376 raise RuntimeError("Datasource is None, cannot activate.")
377 self.datasource.activate(cfg=self.cfg,
378 is_new_instance=self.is_new_instance())
379 self._write_to_cache()
380
374 def _store_userdata(self):381 def _store_userdata(self):
375 raw_ud = self.datasource.get_userdata_raw()382 raw_ud = self.datasource.get_userdata_raw()
376 if raw_ud is None:383 if raw_ud is None:
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index e90e903..0712700 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -349,7 +349,7 @@ class TestAzureDataSource(TestCase):
349 cfg = dsrc.get_config_obj()349 cfg = dsrc.get_config_obj()
350350
351 self.assertEqual(dsrc.device_name_to_device("ephemeral0"),351 self.assertEqual(dsrc.device_name_to_device("ephemeral0"),
352 "/dev/sdb")352 DataSourceAzure.RESOURCE_DISK_PATH)
353 assert 'disk_setup' in cfg353 assert 'disk_setup' in cfg
354 assert 'fs_setup' in cfg354 assert 'fs_setup' in cfg
355 self.assertIsInstance(cfg['disk_setup'], dict)355 self.assertIsInstance(cfg['disk_setup'], dict)
@@ -462,14 +462,6 @@ class TestAzureBounce(TestCase):
462 mock.patch.object(DataSourceAzure, 'list_possible_azure_ds_devs',462 mock.patch.object(DataSourceAzure, 'list_possible_azure_ds_devs',
463 mock.MagicMock(return_value=[])))463 mock.MagicMock(return_value=[])))
464 self.patches.enter_context(464 self.patches.enter_context(
465 mock.patch.object(DataSourceAzure,
466 'find_fabric_formatted_ephemeral_disk',
467 mock.MagicMock(return_value=None)))
468 self.patches.enter_context(
469 mock.patch.object(DataSourceAzure,
470 'find_fabric_formatted_ephemeral_part',
471 mock.MagicMock(return_value=None)))
472 self.patches.enter_context(
473 mock.patch.object(DataSourceAzure, 'get_metadata_from_fabric',465 mock.patch.object(DataSourceAzure, 'get_metadata_from_fabric',
474 mock.MagicMock(return_value={})))466 mock.MagicMock(return_value={})))
475 self.patches.enter_context(467 self.patches.enter_context(

Subscribers

People subscribed via source and target branches