Enable filesystem creation on Azure, many disk_setup cleanups

There are a lot of cleanups here around Azure, SmartOS and disk_setup.
disk_setup correctly identifies disk "aliases" (block device mappings
from ec2), anywhere where you would use a device name.

You can also specify these mappings to the Azure or SmartOS datasource
in their datasource config (device_aliases).

Also, stop Azure from calling blkid repeatedly in its tests, which
really pounded my laptop.
This commit is contained in:
Scott Moser
2013-09-27 19:38:05 -04:00
8 changed files with 244 additions and 144 deletions

View File

@@ -3,6 +3,7 @@
- small fix for OVF datasource for iso transport on non-iso9660 filesystem
- determine if upstart version is suitable for
'initctl reload-configuration' (LP: #1124384). If so, then invoke it.
supports setting up instance-store disk with partition table and filesystem.
- add Azure datasource.
- add support for SuSE / SLES [Juerg Haefliger]
- add a trailing carriage return to chpasswd input, which reportedly

View File

@@ -41,7 +41,8 @@ def handle(_name, cfg, cloud, log, _args):
"""
disk_setup = cfg.get("disk_setup")
if isinstance(disk_setup, dict):
log.info("Partitioning disks.")
update_disk_setup_devices(disk_setup, cloud.device_name_to_device)
log.debug("Partitioning disks: %s", str(disk_setup))
for disk, definition in disk_setup.items():
if not isinstance(definition, dict):
log.warn("Invalid disk definition for %s" % disk)
@@ -51,13 +52,14 @@ def handle(_name, cfg, cloud, log, _args):
log.debug("Creating new partition table/disk")
util.log_time(logfunc=LOG.debug,
msg="Creating partition on %s" % disk,
func=mkpart, args=(disk, cloud, definition))
func=mkpart, args=(disk, definition))
except Exception as e:
util.logexc(LOG, "Failed partitioning operation\n%s" % e)
fs_setup = cfg.get("fs_setup")
if isinstance(fs_setup, list):
log.info("Creating file systems.")
log.debug("setting up filesystems: %s", str(fs_setup))
update_fs_setup_devices(fs_setup, cloud.device_name_to_device)
for definition in fs_setup:
if not isinstance(definition, dict):
log.warn("Invalid file system definition: %s" % definition)
@@ -68,31 +70,48 @@ def handle(_name, cfg, cloud, log, _args):
device = definition.get('device')
util.log_time(logfunc=LOG.debug,
msg="Creating fs for %s" % device,
func=mkfs, args=(cloud, definition))
func=mkfs, args=(definition,))
except Exception as e:
util.logexc(LOG, "Failed during filesystem operation\n%s" % e)
def is_default_device(name, cloud, fallback=None):
"""
Ask the cloud datasource if the 'name' maps to a default
device. If so, return that value, otherwise return 'name', or
fallback if so defined.
"""
def update_disk_setup_devices(disk_setup, tformer):
# update 'disk_setup' dictionary anywhere were a device may occur
# update it with the response from 'tformer'
for origname in disk_setup.keys():
transformed = tformer(origname)
if transformed is None or transformed == origname:
continue
if transformed in disk_setup:
LOG.info("Replacing %s in disk_setup for translation of %s",
origname, transformed)
del disk_setup[transformed]
_dev = None
try:
_dev = cloud.device_name_to_device(name)
except Exception as e:
util.logexc(LOG, "Failed to find mapping for %s" % e)
disk_setup[transformed] = disk_setup[origname]
disk_setup[transformed]['_origname'] = origname
del disk_setup[origname]
LOG.debug("updated disk_setup device entry '%s' to '%s'",
origname, transformed)
if _dev:
return _dev
if fallback:
return fallback
def update_fs_setup_devices(disk_setup, tformer):
# update 'fs_setup' dictionary anywhere were a device may occur
# update it with the response from 'tformer'
for definition in disk_setup:
if not isinstance(definition, dict):
LOG.warn("entry in disk_setup not a dict: %s", definition)
continue
return name
origname = definition.get('device')
if origname is None:
continue
transformed = tformer(origname)
if transformed is None or transformed == origname:
continue
definition['_origname'] = origname
definition['device'] = transformed
def value_splitter(values, start=None):
@@ -195,6 +214,10 @@ def find_device_node(device, fs_type=None, label=None, valid_targets=None,
Note: This works with GPT partition tables!
"""
# label of None is same as no label
if label is None:
label = ""
if not valid_targets:
valid_targets = ['disk', 'part']
@@ -219,8 +242,8 @@ def find_device_node(device, fs_type=None, label=None, valid_targets=None,
for key, value in value_splitter(part):
d[key.lower()] = value
if d['fstype'] == fs_type and \
((label_match and d['label'] == label) or not label_match):
if (d['fstype'] == fs_type and
((label_match and d['label'] == label) or not label_match)):
# If we find a matching device, we return that
return ('/dev/%s' % d['name'], True)
@@ -397,8 +420,8 @@ def get_partition_mbr_layout(size, layout):
# Create a single partition
return "0,"
if (len(layout) == 0 and isinstance(layout, list)) or \
not isinstance(layout, list):
if ((len(layout) == 0 and isinstance(layout, list)) or
not isinstance(layout, list)):
raise Exception("Partition layout is invalid")
last_part_num = len(layout)
@@ -414,8 +437,7 @@ def get_partition_mbr_layout(size, layout):
if isinstance(part, list):
if len(part) != 2:
raise Exception("Partition was incorrectly defined: %s" % \
part)
raise Exception("Partition was incorrectly defined: %s" % part)
percent, part_type = part
part_size = int((float(size) * (float(percent) / 100)) / 1024)
@@ -488,12 +510,11 @@ def exec_mkpart(table_type, device, layout):
return get_dyn_func("exec_mkpart_%s", table_type, device, layout)
def mkpart(device, cloud, definition):
def mkpart(device, definition):
"""
Creates the partition table.
Parameters:
cloud: the cloud object
definition: dictionary describing how to create the partition.
The following are supported values in the dict:
@@ -508,29 +529,18 @@ def mkpart(device, cloud, definition):
overwrite = definition.get('overwrite', False)
layout = definition.get('layout', False)
table_type = definition.get('table_type', 'mbr')
_device = is_default_device(device, cloud)
# Check if the default device is a partition or not
LOG.debug("Checking against default devices")
if _device and (_device != device):
if not is_device_valid(_device):
_device = _device[:-1]
if not is_device_valid(_device):
raise Exception("Unable to find backing block device for %s" % \
device)
else:
LOG.debug("Mapped %s to physical device %s" % (device, _device))
device = _device
if (isinstance(layout, bool) and not layout) or not layout:
LOG.debug("Device is not to be partitioned, skipping")
return # Device is not to be partitioned
# This prevents you from overwriting the device
LOG.debug("Checking if device %s is a valid device" % device)
LOG.debug("Checking if device %s is a valid device", device)
if not is_device_valid(device):
raise Exception("Device %s is not a disk device!" % device)
raise Exception("Device %s is not a disk device!", device)
LOG.debug("Checking if device layout matches")
if check_partition_layout(table_type, device, layout):
@@ -549,13 +559,13 @@ def mkpart(device, cloud, definition):
part_definition = get_partition_layout(table_type, device_size, layout)
LOG.debug(" Layout is: %s" % part_definition)
LOG.debug("Creating partition table on %s" % device)
LOG.debug("Creating partition table on %s", device)
exec_mkpart(table_type, device, part_definition)
LOG.debug("Partition table created for %s" % device)
LOG.debug("Partition table created for %s", device)
def mkfs(cloud, fs_cfg):
def mkfs(fs_cfg):
"""
Create a file system on the device.
@@ -576,54 +586,45 @@ def mkfs(cloud, fs_cfg):
When 'cmd' is provided then no other parameter is required.
"""
fs_cfg['partition'] = 'any'
label = fs_cfg.get('label')
device = fs_cfg.get('device')
partition = str(fs_cfg.get('partition'))
partition = str(fs_cfg.get('partition', 'any'))
fs_type = fs_cfg.get('filesystem')
fs_cmd = fs_cfg.get('cmd', [])
fs_opts = fs_cfg.get('extra_opts', [])
overwrite = fs_cfg.get('overwrite', False)
# This allows you to define the default ephemeral or swap
LOG.debug("Checking %s against default devices" % device)
_device = is_default_device(label, cloud, fallback=device)
if _device and (_device != device):
if not is_device_valid(_device):
raise Exception("Unable to find backing block device for %s" % \
device)
else:
LOG.debug("Mapped %s to physical device %s" % (device, _device))
device = _device
LOG.debug("Checking %s against default devices", device)
if not partition or partition.isdigit():
# Handle manual definition of partition
if partition.isdigit():
device = "%s%s" % (device, partition)
LOG.debug("Manual request of partition %s for %s" % (
partition, device))
LOG.debug("Manual request of partition %s for %s",
partition, device)
# Check to see if the fs already exists
LOG.debug("Checking device %s" % device)
LOG.debug("Checking device %s", device)
check_label, check_fstype, _ = check_fs(device)
LOG.debug("Device %s has %s %s" % (device, check_label, check_fstype))
LOG.debug("Device %s has %s %s", device, check_label, check_fstype)
if check_label == label and check_fstype == fs_type:
LOG.debug("Existing file system found at %s" % device)
LOG.debug("Existing file system found at %s", device)
if not overwrite:
LOG.warn("Device %s has required file system" % device)
LOG.debug("Device %s has required file system", device)
return
else:
LOG.warn("Destroying filesystem on %s" % device)
LOG.warn("Destroying filesystem on %s", device)
else:
LOG.debug("Device %s is cleared for formating" % device)
LOG.debug("Device %s is cleared for formating", device)
elif partition and str(partition).lower() in ('auto', 'any'):
# For auto devices, we match if the filesystem does exist
odevice = device
LOG.debug("Identifying device to create %s filesytem on" % label)
LOG.debug("Identifying device to create %s filesytem on", label)
# any mean pick the first match on the device with matching fs_type
label_match = True
@@ -632,33 +633,32 @@ def mkfs(cloud, fs_cfg):
device, reuse = find_device_node(device, fs_type=fs_type, label=label,
label_match=label_match)
LOG.debug("Automatic device for %s identified as %s" % (
odevice, device))
LOG.debug("Automatic device for %s identified as %s", odevice, device)
if reuse:
LOG.debug("Found filesystem match, skipping formating.")
return
if not device:
LOG.debug("No device aviable that matches request.")
LOG.debug("Skipping fs creation for %s" % fs_cfg)
LOG.debug("No device aviable that matches request. "
"Skipping fs creation for %s", fs_cfg)
return
else:
LOG.debug("Error in device identification handling.")
return
LOG.debug("File system %s will be created on %s" % (label, device))
LOG.debug("File system %s will be created on %s", label, device)
# Make sure the device is defined
if not device:
LOG.critical("Device is not known: %s" % fs_cfg)
LOG.warn("Device is not known: %s", device)
return
# Check that we can create the FS
if not label or not fs_type:
LOG.debug("Command to create filesystem %s is bad. Skipping." % \
label)
if not (fs_type or fs_cmd):
raise Exception("No way to create filesystem '%s'. fs_type or fs_cmd "
"must be set.", label)
# Create the commands
if fs_cmd:
@@ -673,7 +673,8 @@ def mkfs(cloud, fs_cfg):
mkfs_cmd = util.which("mk%s" % fs_type)
if not mkfs_cmd:
LOG.critical("Unable to locate command to create filesystem.")
LOG.warn("Cannot create fstype '%s'. No mkfs.%s command", fs_type,
fs_type)
return
fs_cmd = [mkfs_cmd, device]
@@ -685,8 +686,8 @@ def mkfs(cloud, fs_cfg):
if fs_opts:
fs_cmd.extend(fs_opts)
LOG.debug("Creating file system %s on %s" % (label, device))
LOG.debug(" Using cmd: %s" % "".join(fs_cmd))
LOG.debug("Creating file system %s on %s", label, device)
LOG.debug(" Using cmd: %s", "".join(fs_cmd))
try:
util.subp(fs_cmd)
except Exception as e:

View File

@@ -44,8 +44,20 @@ BUILTIN_DS_CONFIG = {
'policy': True,
'command': BOUNCE_COMMAND,
'hostname_command': 'hostname',
}
},
'disk_aliases': {'ephemeral0': '/dev/sdb'},
}
BUILTIN_CLOUD_CONFIG = {
'disk_setup': {
'ephemeral0': {'table_type': 'mbr',
'layout': True,
'overwrite': False}
},
'fs_setup': [{'filesystem': 'ext4', 'device': 'ephemeral0',
'partition': 'auto'}],
}
DS_CFG_PATH = ['datasource', DS_NAME]
@@ -94,7 +106,7 @@ class DataSourceAzureNet(sources.DataSource):
(md, self.userdata_raw, cfg, files) = ret
self.seed = cdev
self.metadata = util.mergemanydict([md, DEFAULT_METADATA])
self.cfg = cfg
self.cfg = util.mergemanydict([cfg, BUILTIN_CLOUD_CONFIG])
found = cdev
LOG.debug("found datasource in %s", cdev)
@@ -112,8 +124,8 @@ class DataSourceAzureNet(sources.DataSource):
self.metadata['random_seed'] = seed
# now update ds_cfg to reflect contents pass in config
usercfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {})
self.ds_cfg = util.mergemanydict([usercfg, self.ds_cfg])
user_ds_cfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {})
self.ds_cfg = util.mergemanydict([user_ds_cfg, self.ds_cfg])
mycfg = self.ds_cfg
# walinux agent writes files world readable, but expects
@@ -161,9 +173,11 @@ class DataSourceAzureNet(sources.DataSource):
pubkeys = pubkeys_from_crt_files(fp_files)
self.metadata['public-keys'] = pubkeys
return True
def device_name_to_device(self, name):
return self.ds_cfg['disk_aliases'].get(name)
def get_config_obj(self):
return self.cfg
@@ -349,7 +363,7 @@ def read_azure_ovf(contents):
try:
dom = minidom.parseString(contents)
except Exception as e:
raise NonAzureDataSource("invalid xml: %s" % e)
raise BrokenAzureDataSource("invalid xml: %s" % e)
results = find_child(dom.documentElement,
lambda n: n.localName == "ProvisioningSection")

View File

@@ -72,14 +72,17 @@ BUILTIN_DS_CONFIG = {
'iptables_disable'],
'base64_keys': [],
'base64_all': False,
'ephemeral_disk': '/dev/vdb',
'disk_aliases': {'ephemeral0': '/dev/vdb'},
}
BUILTIN_CLOUD_CONFIG = {
'disk_setup': {
'ephemeral0': {'table_type': 'mbr',
'layout': True,
'layout': False,
'overwrite': False}
},
'fs_setup': [{'label': 'ephemeral0', 'filesystem': 'ext3',
'device': '/dev/xvdb', 'partition': 'auto'}],
'device': 'ephemeral0', 'partition': 'auto'}],
}
@@ -94,9 +97,7 @@ class DataSourceSmartOS(sources.DataSource):
BUILTIN_DS_CONFIG])
self.metadata = {}
self.cfg = {}
self.cfg['disk_setup'] = self.ds_cfg.get('disk_setup')
self.cfg['fs_setup'] = self.ds_cfg.get('fs_setup')
self.cfg = BUILTIN_CLOUD_CONFIG
self.seed = self.ds_cfg.get("serial_device")
self.seed_timeout = self.ds_cfg.get("serial_timeout")
@@ -154,8 +155,7 @@ class DataSourceSmartOS(sources.DataSource):
return True
def device_name_to_device(self, name):
if 'ephemeral0' in name:
return self.ds_cfg['ephemeral_disk']
return self.ds_cfg['disk_aliases'].get(name)
def get_config_obj(self):
return self.cfg

View File

@@ -19,36 +19,36 @@ Default disk definitions for AWS
Default disk definitions for Windows Azure
------------------------------------------
(Not implemented yet due to conflict with WALinuxAgent in Ubuntu)
device_aliases: {'ephemeral0': '/dev/sdb'}
disk_setup:
/dev/sdb:
ephemeral0:
type: mbr
layout: True
overwrite: False
fs_setup:
- label: ephemeral0
filesystem: ext4
device: ephemeral0
partition: auto
Default disk definitions for SmartOS
------------------------------------
device_aliases: {'ephemeral0': '/dev/sdb'}
disk_setup:
ephemeral0:
type: mbr
layout: False
overwrite: False
fs_setup:
- label: ephemeral0
filesystem: ext3
device: ephemeral0
partition: any
Default disk definitions for SmartOS
------------------------------------
ephemeral_disk: /dev/vdb
disk_setup:
/dev/vdb:
type: mbr
layout: True
overwrite: False
fs_setup:
- label: ephemeral0
filesystem: ext3
device: /dev/vdb
partition: 1
partition: auto
Cavaut for SmartOS: if ephemeral disk is not defined, then the disk will
not be automatically added to the mounts.
@@ -188,13 +188,43 @@ Where:
of the ephemeral storage layer.
<PART_VALUE>: The valid options are:
"auto": auto is a special in the sense that you are telling cloud-init
not to care whether there is a partition or not. Auto will put the
first partition that does not contain a file system already. In
the absence of a partition table, it will put it directly on the
disk.
"auto|any": tell cloud-init not to care whether there is a partition
or not. Auto will use the first partition that does not contain a
file system already. In the absence of a partition table, it will
put it directly on the disk.
"none": Put the partition directly on the disk.
"auto": If a file system that matches the specification in terms of
label, type and device, then cloud-init will skip the creation of
the file system.
"any": If a file system that matches the file system type and device,
then cloud-init will skip the creation of the file system.
Devices are selected based on first-detected, starting with partitions
and then the raw disk. Consider the following:
NAME FSTYPE LABEL
xvdb
|-xvdb1 ext4
|-xvdb2
|-xvdb3 btrfs test
\-xvdb4 ext4 test
If you ask for 'auto', label of 'test, and file system of 'ext4'
then cloud-init will select the 2nd partition, even though there
is a partition match at the 4th partition.
If you ask for 'any' and a label of 'test', then cloud-init will
select the 1st partition.
If you ask for 'auto' and don't define label, then cloud-init will
select the 1st partition.
In general, if you have a specific partition configuration in mind,
you should define either the device or the partition number. 'auto'
and 'any' are specifically intended for formating ephemeral storage or
for simple schemes.
"none": Put the file system directly on the device.
<NUM>: where NUM is the actual partition number.

View File

@@ -73,15 +73,21 @@ or not to base64 decode something:
(i.e. /etc/cloud/cloud.cfg.d) that sets which values should not be
base64 decoded.
ephemeral_disk:
disk_aliases and ephemeral disk:
---------------
By default, SmartOS only supports a single ephemeral disk. That disk is
completely empty (un-partitioned with no filesystem).
In order to instruct Cloud-init which disk to auto-mount. By default,
SmartOS only supports a single ephemeral disk.
The SmartOS datasource has built-in cloud-config which instructs the
'disk_setup' module to partition and format the ephemeral disk.
The default SmartOS configuration will prepare the ephemeral disk and format
it for you. SmartOS does not, by default, prepare the ephemeral disk for you.
You can control the disk_setup then in 2 ways:
1. through the datasource config, you can change the 'alias' of
ephermeral0 to reference another device. The default is:
'disk_aliases': {'ephemeral0': '/dev/vdb'},
Which means anywhere disk_setup sees a device named 'ephemeral0'
then /dev/vdb will be substituted.
2. you can provide disk_setup or fs_setup data in user-data to overwrite
the datasource's built-in values.
If you change ephemeral_disk, you should also consider changing
the default disk formatting parameters. See
doc/examples/cloud-config-disk-setup.txt for information on using this.
See doc/examples/cloud-config-disk-setup.txt for information on disk_setup.

View File

@@ -120,8 +120,7 @@ class TestAzureDataSource(MockerTestCase):
mod = DataSourceAzure
if data.get('dsdevs'):
self.apply_patches([(mod, 'list_possible_azure_ds_devs', dsdevs)])
self.apply_patches([(mod, 'list_possible_azure_ds_devs', dsdevs)])
self.apply_patches([(mod, 'invoke_agent', _invoke_agent),
(mod, 'write_files', _write_files),
@@ -154,9 +153,12 @@ class TestAzureDataSource(MockerTestCase):
def test_user_cfg_set_agent_command_plain(self):
# set dscfg in via plaintext
cfg = {'agent_command': "my_command"}
# we must have friendly-to-xml formatted plaintext in yaml_cfg
# not all plaintext is expected to work.
yaml_cfg = "{agent_command: my_command}\n"
cfg = yaml.safe_load(yaml_cfg)
odata = {'HostName': "myhost", 'UserName': "myuser",
'dscfg': {'text': yaml.dump(cfg), 'encoding': 'plain'}}
'dscfg': {'text': yaml_cfg, 'encoding': 'plain'}}
data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
dsrc = self._get_ds(data)
@@ -290,11 +292,59 @@ class TestAzureDataSource(MockerTestCase):
self.assertEqual(data.get('apply_hostname_bounce', "N/A"), "N/A")
def test_default_ephemeral(self):
# make sure the ephemeral device works
odata = {}
data = {'ovfcontent': construct_valid_ovf_env(data=odata),
'sys_cfg': {}}
dsrc = self._get_ds(data)
ret = dsrc.get_data()
self.assertTrue(ret)
cfg = dsrc.get_config_obj()
self.assertEquals(dsrc.device_name_to_device("ephemeral0"),
"/dev/sdb")
assert 'disk_setup' in cfg
assert 'fs_setup' in cfg
self.assertIsInstance(cfg['disk_setup'], dict)
self.assertIsInstance(cfg['fs_setup'], list)
def test_provide_disk_aliases(self):
# Make sure that user can affect disk aliases
dscfg = {'disk_aliases': {'ephemeral0': '/dev/sdc'}}
odata = {'HostName': "myhost", 'UserName': "myuser",
'dscfg': {'text': base64.b64encode(yaml.dump(dscfg)),
'encoding': 'base64'}}
usercfg = {'disk_setup': {'/dev/sdc': {'something': '...'},
'ephemeral0': False}}
userdata = '#cloud-config' + yaml.dump(usercfg) + "\n"
ovfcontent = construct_valid_ovf_env(data=odata, userdata=userdata)
data = {'ovfcontent': ovfcontent, 'sys_cfg': {}}
dsrc = self._get_ds(data)
ret = dsrc.get_data()
self.assertTrue(ret)
cfg = dsrc.get_config_obj()
self.assertTrue(cfg)
self.assertEquals(dsrc.device_name_to_device("ephemeral0"),
"/dev/sdc")
def test_userdata_arrives(self):
userdata = "This is my user-data"
xml = construct_valid_ovf_env(data={}, userdata=userdata)
data = {'ovfcontent': xml}
dsrc = self._get_ds(data)
dsrc.get_data()
self.assertEqual(userdata, dsrc.userdata_raw)
class TestReadAzureOvf(MockerTestCase):
def test_invalid_xml_raises_non_azure_ds(self):
invalid_xml = "<foo>" + construct_valid_ovf_env(data={})
self.assertRaises(DataSourceAzure.NonAzureDataSource,
self.assertRaises(DataSourceAzure.BrokenAzureDataSource,
DataSourceAzure.read_azure_ovf, invalid_xml)
def test_load_with_pubkeys(self):

View File

@@ -79,7 +79,6 @@ class MockSerial(object):
if self.last in self.mockdata:
if not self.mocked_out:
self.mocked_out = [x for x in self._format_out()]
print self.mocked_out
if len(self.mocked_out) > self.count:
self.count += 1
@@ -275,26 +274,25 @@ class TestSmartOSDataSource(MockerTestCase):
self.assertIsInstance(cfg['disk_setup'], dict)
self.assertIsInstance(cfg['fs_setup'], list)
def test_override_builtin_ds(self):
def test_override_disk_aliases(self):
# Test to make sure that the built-in DS is overriden
data = {}
data['disk_setup'] = {'test_dev': {}}
data['fs_setup'] = [{'label': 'test_dev'}]
data['serial_device'] = '/dev/ttyS2'
dsrc = self._get_ds(ds_cfg=data)
cfg = dsrc.get_config_obj()
builtin = DataSourceSmartOS.BUILTIN_DS_CONFIG
mydscfg = {'disk_aliases': {'FOO': '/dev/bar'}}
# expect that these values are in builtin, or this is pointless
for k in mydscfg:
self.assertIn(k, builtin)
dsrc = self._get_ds(ds_cfg=mydscfg)
ret = dsrc.get_data()
self.assertTrue(ret)
assert 'disk_setup' in cfg
assert 'fs_setup' in cfg
self.assertIsInstance(cfg['disk_setup'], dict)
self.assertIsInstance(cfg['fs_setup'], list)
assert 'test_dev' in cfg['disk_setup']
assert 'test_dev' in cfg['fs_setup'][0]['label']
self.assertEqual(mydscfg['disk_aliases']['FOO'],
dsrc.ds_cfg['disk_aliases']['FOO'])
self.assertEquals(data['serial_device'], dsrc.seed)
self.assertEqual(dsrc.device_name_to_device('FOO'),
mydscfg['disk_aliases']['FOO'])
def apply_patches(patches):