Merge ~raharper/curtin:fix/dname_to_serial_wwn_if_available into curtin:master

Proposed by Ryan Harper
Status: Merged
Approved by: Ryan Harper
Approved revision: 7047f4ec13eda8a3adbcf1f5488cb6180a06171b
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~raharper/curtin:fix/dname_to_serial_wwn_if_available
Merge into: curtin:master
Diff against target: 974 lines (+484/-63)
13 files modified
curtin/commands/block_meta.py (+74/-15)
curtin/udev.py (+37/-0)
doc/topics/storage.rst (+37/-2)
examples/tests/basic.yaml (+2/-0)
examples/tests/basic_scsi.yaml (+15/-0)
examples/tests/nvme.yaml (+2/-2)
examples/tests/nvme_bcache.yaml (+1/-1)
tests/unittests/helpers.py (+7/-0)
tests/unittests/test_make_dname.py (+172/-30)
tests/unittests/test_udev.py (+68/-0)
tests/vmtests/__init__.py (+55/-5)
tests/vmtests/test_basic.py (+8/-4)
tests/vmtests/test_nvme.py (+6/-4)
Reviewer Review Type Date Requested Status
Scott Moser (community) Approve
Server Team CI bot continuous-integration Approve
Chad Smith Pending
Review via email: mp+358116@code.launchpad.net

Commit message

dname: persistent names based on serial or wwn

Currently /dev/disk/by-dname symlinks are not created
if a device does not have a partition table or a super block.
For devices which do have a serial or wwn number, curtin will
append an additional rule to the disk rules file which includes
both the partition table/superblock UUID as well as rule matching
the device provided serial or wwn. This ensures that even if
a block device is wiped that the dname will continue to function.

Other changes:
- add method to discover udev database value of a block device
  ID_SERIAL value.
- refactor nvme vmtests to use 'serial' values
- don't hardcode nvme serials based on their enumerated index
- ensure we pass nvme serials to both install and boot phases
- update disks_to_check for basic and nvme to ensure we have a
  dname for the disk itself

LP: #1735839

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :
7132714... by Ryan Harper

vmtest: fix PsuedoVMBaseClass, add test_dname_rules test

18175ca... by Ryan Harper

dname_byid: handle iscsi devices without serial, wwn value by lookup

In our vmtest, at least, the iscsi devices do not have a 'serial' or
'wwn' config attribute. This prevents dname rules from including a
rule that references a disk attribute. Allow the byid rule generator
to query udev info on that block device to determine if it has a
serial or wwn value to use.

0e4470d... by Ryan Harper

dname: handle disks without serial, wwn config by udevinfo

Restructure dname byid to look up the serial/wwn values if
present and use them, allowing for both values to be present.
This is needed for the vmtest iscsi cases where we do not set
a serial or wwn value in the config, but the devices to have
a value.

ada685e... by Ryan Harper

Drop disk_serial_to_udev_info_value method, use info directly

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

vmtest results showed issues with iscsi tests, so refactored how we get the serial/wwn values so they work with iscsi.

Restarted the vmtest run

https://jenkins.ubuntu.com/server/job/curtin-vmtest-devel-debug/129/

c64b480... by Ryan Harper

Drop debug

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
6b6931a... by Ryan Harper

vmtest: handle serial number with space matching when checking dname rules

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

forgot to fix multipath, fixed issue with spaces in serial values.

https://jenkins.ubuntu.com/server/job/curtin-vmtest-devel-debug/130/

Revision history for this message
Scott Moser (smoser) wrote :

Commit message:
> Currently /dev/disk/by-dname symlinks are not created
> if a device does not have a partition table or a super block.

More clear as:

  Currently /dev/disk/by-dname symlinks are only created if
  a device has a partition table or a filesystem.

Revision history for this message
Scott Moser (smoser) wrote :

more review tomororw

Revision history for this message
Scott Moser (smoser) wrote :

one comment in line.

I had once envisioned thath we might just write a shell or other program
that interpreted config and dump that into the installed system.

either way, we're dumping files into the installed system that wont ever
be serviced, so its not a terrible suggestion.

my idea would have been to have more "config" like format in /etc/curtin/disks
and a udev rule that fired that read that data.

data format might look like this:
   name=mydisk1 serial=foo-serial

c3b5cf9... by Ryan Harper

block-meta: generate automatic -part%n partition rules for disks with dname

Revision history for this message
Ryan Harper (raharper) wrote :

> one comment in line.
>
> I had once envisioned thath we might just write a shell or other program
> that interpreted config and dump that into the installed system.
>
> either way, we're dumping files into the installed system that wont ever
> be serviced, so its not a terrible suggestion.
>
> my idea would have been to have more "config" like format in /etc/curtin/disks
> and a udev rule that fired that read that data.
>
> data format might look like this:
> name=mydisk1 serial=foo-serial

I mentioned this idea at the product roadmap sprint and the discussion ended with the
understanding that the current unmanaged rules was the most ideal without a lot more work
centered around locally managing the configuration file and with what tool/cli would we do that.

d79733c... by Ryan Harper

dname_byid: raise exception error, simplify rule construction

Revision history for this message
Ryan Harper (raharper) :
Revision history for this message
Chad Smith (chad.smith) wrote :

Some nits and questions. thanks for this.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for review, fixing up most suggestions.

f91292a... by Ryan Harper

Add docstrings and small refactor to address feedback.

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

I have some questions inline.

One generall question though...
What happens if maas provides a dname for a device that does not have a wwn or id?
Will install fail?

That can be considered "correct" behavior, but I dont know if maas has been
sending dname (or allows the user to do so) in other cases. I'm not sure
that we can start failing an install when previously we just did
the wrong thing and dname didnt work.

Last, we'd like to add some doc on dname. Cove
 - behavior when a name is given to a partition
 - behavior when a name is given to a md or vg

Revision history for this message
Ryan Harper (raharper) wrote :

> I have some questions inline.
>
> One generall question though...
> What happens if maas provides a dname for a device that does not have a wwn or
> id?
> Will install fail?

It already fails due to curtin not finding the disk specified by the 'serial' or 'wwn' attribute.
While maas *could* send path only; I don't think they do for real machines, and for kvm pods, they are using serial attributes as well.

worth having a sync with MAAS and ask what expectations they'd have and how they handle disks detected wtih no serial/wwn.

>
> That can be considered "correct" behavior, but I dont know if maas has been
> sending dname (or allows the user to do so) in other cases. I'm not sure
> that we can start failing an install when previously we just did
> the wrong thing and dname didnt work.

Agreed. I generally don't think dname is a fatal thing, but let's sync with MAAS on this.

>
>
> Last, we'd like to add some doc on dname. Cove
> - behavior when a name is given to a partition
> - behavior when a name is given to a md or vg

ACK, will add.

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the review, I'll update with docs and unittests.

Revision history for this message
Scott Moser (smoser) :
e6b37d0... by Ryan Harper

block-meta: make_dname_byid refactor for easier unittesting, add unittests

9d1446e... by Ryan Harper

udev: refactor udevadm_info to use just --query=property and add unittests

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
b6b9009... by Ryan Harper

Update docstrings for udevadm_info

Revision history for this message
Scott Moser (smoser) wrote :

some little things.

f129c88... by Ryan Harper

docs: update documentation around dname for disks and partitions

3189f59... by Ryan Harper

block_meta: update dname debug message to indicate rule creation, not writing

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks, fixing up

e0c3384... by Ryan Harper

unittest: make_dname, add test for passing info dict, use assertEqual

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

At this point, I think this is probably fine.
I put together what I think are some test case cleanups at:
  http://paste.ubuntu.com/p/XkJ2KMW5x7/

I'd like you to pull them, but wont insist on it.

Also, I think we have a bug with multipath disks.
As it is, I'm pretty sure that ifyou put a name on a disk that was
a multipath disk, you'll get a symlink from /dev/disk/by-name/my-name -> /dev/diskX
when what we want would be /dev/mapper/mpathX

For example, on some of our power systems, /dev/sda and /dev/sdg are
multiple paths to the same disk. If we put a name on one of those, I think
we'd get either
  /dev/disk/by-name -> /dev/sda or /dev/disk/by-name -> /dev/sdg
but what we want is
  /dev/disk/by-name -> /dev/mapper/mpath0

I'm fine to leave that as it is for now, but I think ultimately we
will want to have some fix for that.

Revision history for this message
Ryan Harper (raharper) wrote :

On Wed, Nov 28, 2018 at 11:20 AM Scott Moser <email address hidden> wrote:
>
> At this point, I think this is probably fine.
> I put together what I think are some test case cleanups at:
> http://paste.ubuntu.com/p/XkJ2KMW5x7/
>
> I'd like you to pull them, but wont insist on it.

I'll pull

>
> Also, I think we have a bug with multipath disks.
> As it is, I'm pretty sure that ifyou put a name on a disk that was
> a multipath disk, you'll get a symlink from /dev/disk/by-name/my-name -> /dev/diskX
> when what we want would be /dev/mapper/mpathX
>
> For example, on some of our power systems, /dev/sda and /dev/sdg are
> multiple paths to the same disk. If we put a name on one of those, I think
> we'd get either
> /dev/disk/by-name -> /dev/sda or /dev/disk/by-name -> /dev/sdg
> but what we want is
> /dev/disk/by-name -> /dev/mapper/mpath0
>
> I'm fine to leave that as it is for now, but I think ultimately we
> will want to have some fix for that.

https://bugs.launchpad.net/ubuntu/+source/curtin/+bug/1805673

>
>
> --
> https://code.launchpad.net/~raharper/curtin/+git/curtin/+merge/358116
> You are the owner of ~raharper/curtin:fix/dname_to_serial_wwn_if_available.

68571d0... by Scott Moser

unittest: dname unittest cleanup

 - move test_udevinfo_not_called_if_info_provided
 - add test_udevinfo_called_if_info_not_provided
   if info is provided, udevinfo should not be called.
 - add '/dev/' prefix to "mypath" variables to make them more expected.
 - split test_disk_with_id_or_wwn into 2 tests
     test_disk_with_only_id
     test_disk_with_only_wwn
   also here use self.assertEqual as it makes it more clear
   what the expected result is.
 - make test_disk_with_both_id_wwn call with info=

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
7047f4e... by Ryan Harper

unittest: add test_udev tests

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
index ef0a48a..8decab5 100644
--- a/curtin/commands/block_meta.py
+++ b/curtin/commands/block_meta.py
@@ -8,7 +8,8 @@ from curtin.log import LOG, logged_time
8from curtin.reporter import events8from curtin.reporter import events
99
10from . import populate_one_subcmd10from . import populate_one_subcmd
11from curtin.udev import compose_udev_equality, udevadm_settle, udevadm_trigger11from curtin.udev import (compose_udev_equality, udevadm_settle,
12 udevadm_trigger, udevadm_info)
1213
13import glob14import glob
14import os15import os
@@ -221,12 +222,43 @@ def sanitize_dname(dname):
221 return ''.join(c if c in valid else '-' for c in dname)222 return ''.join(c if c in valid else '-' for c in dname)
222223
223224
225def make_dname_byid(path, error_msg=None, info=None):
226 """ Returns a list of udev equalities for a given disk path
227
228 :param path: string of a kernel device path to a block device
229 :param error_msg: more information about path for log/errors
230 :param info: dict of udevadm info key, value pairs of device specified by
231 path.
232 :returns: list of udev equalities (lists)
233 :raises: ValueError if path is not a disk.
234 :raises: RuntimeError if there is no serial or wwn.
235 """
236 error_msg = str(path) + ("" if not error_msg else " [%s]" % error_msg)
237 if info is None:
238 info = udevadm_info(path=path)
239 devtype = info.get('DEVTYPE')
240 if devtype != "disk":
241 raise ValueError(
242 "Disk tag udev rules are only for disks, %s has devtype=%s" %
243 (error_msg, devtype))
244
245 byid_keys = ['ID_SERIAL', 'ID_WWN_WITH_EXTENSION']
246 present = [k for k in byid_keys if info.get(k)]
247 if not present:
248 raise RuntimeError(
249 "Cannot create disk tag udev rule for %s, "
250 "missing 'serial' or 'wwn' value" % error_msg)
251
252 return [[compose_udev_equality('ENV{%s}' % k, info[k]) for k in present]]
253
254
224def make_dname(volume, storage_config):255def make_dname(volume, storage_config):
225 state = util.load_command_environment()256 state = util.load_command_environment()
226 rules_dir = os.path.join(state['scratch'], "rules.d")257 rules_dir = os.path.join(state['scratch'], "rules.d")
227 vol = storage_config.get(volume)258 vol = storage_config.get(volume)
228 path = get_path_to_storage_volume(volume, storage_config)259 path = get_path_to_storage_volume(volume, storage_config)
229 ptuuid = None260 ptuuid = None
261 byid = None
230 dname = vol.get('name')262 dname = vol.get('name')
231 if vol.get('type') in ["partition", "disk"]:263 if vol.get('type') in ["partition", "disk"]:
232 (out, _err) = util.subp(["blkid", "-o", "export", path], capture=True,264 (out, _err) = util.subp(["blkid", "-o", "export", path], capture=True,
@@ -235,28 +267,41 @@ def make_dname(volume, storage_config):
235 if "PTUUID" in line or "PARTUUID" in line:267 if "PTUUID" in line or "PARTUUID" in line:
236 ptuuid = line.split('=')[-1]268 ptuuid = line.split('=')[-1]
237 break269 break
270 if vol.get('type') == 'disk':
271 byid = make_dname_byid(path, error_msg="id=%s" % vol.get('id'))
238 # we may not always be able to find a uniq identifier on devices with names272 # we may not always be able to find a uniq identifier on devices with names
239 if not ptuuid and vol.get('type') in ["disk", "partition"]:273 if (not ptuuid and not byid) and vol.get('type') in ["disk", "partition"]:
240 LOG.warning("Can't find a uuid for volume: {}. Skipping dname.".format(274 LOG.warning("Can't find a uuid for volume: {}. Skipping dname.".format(
241 volume))275 volume))
242 return276 return
243277
244 rule = [278 matches = []
279 base_rule = [
245 compose_udev_equality("SUBSYSTEM", "block"),280 compose_udev_equality("SUBSYSTEM", "block"),
246 compose_udev_equality("ACTION", "add|change"),281 compose_udev_equality("ACTION", "add|change"),
247 ]282 ]
248 if vol.get('type') == "disk":283 if vol.get('type') == "disk":
249 rule.append(compose_udev_equality('ENV{DEVTYPE}', "disk"))284 if ptuuid:
250 rule.append(compose_udev_equality('ENV{ID_PART_TABLE_UUID}', ptuuid))285 matches += [[compose_udev_equality('ENV{DEVTYPE}', "disk"),
286 compose_udev_equality('ENV{ID_PART_TABLE_UUID}',
287 ptuuid)]]
288 for rule in byid:
289 matches += [
290 [compose_udev_equality('ENV{DEVTYPE}', "disk")] + rule]
251 elif vol.get('type') == "partition":291 elif vol.get('type') == "partition":
252 rule.append(compose_udev_equality('ENV{DEVTYPE}', "partition"))292 # if partition has its own name, bind that to the existing PTUUID
253 dname = storage_config.get(vol.get('device')).get('name') + \293 if dname:
254 "-part%s" % determine_partition_number(volume, storage_config)294 matches += [[compose_udev_equality('ENV{DEVTYPE}', "partition"),
255 rule.append(compose_udev_equality('ENV{ID_PART_ENTRY_UUID}', ptuuid))295 compose_udev_equality('ENV{ID_PART_ENTRY_UUID}',
296 ptuuid)]]
297 else:
298 # disks generate dname-part%n rules automatically
299 LOG.debug('No partition-specific dname')
300 return
256 elif vol.get('type') == "raid":301 elif vol.get('type') == "raid":
257 md_data = mdadm.mdadm_query_detail(path)302 md_data = mdadm.mdadm_query_detail(path)
258 md_uuid = md_data.get('MD_UUID')303 md_uuid = md_data.get('MD_UUID')
259 rule.append(compose_udev_equality("ENV{MD_UUID}", md_uuid))304 matches += [[compose_udev_equality("ENV{MD_UUID}", md_uuid)]]
260 elif vol.get('type') == "bcache":305 elif vol.get('type') == "bcache":
261 # bind dname to bcache backing device's dev.uuid as the bcache minor306 # bind dname to bcache backing device's dev.uuid as the bcache minor
262 # device numbers are not stable across reboots.307 # device numbers are not stable across reboots.
@@ -265,12 +310,12 @@ def make_dname(volume, storage_config):
265 bcache_super = bcache.superblock_asdict(device=backing_dev)310 bcache_super = bcache.superblock_asdict(device=backing_dev)
266 if bcache_super and bcache_super['sb.version'].startswith('1'):311 if bcache_super and bcache_super['sb.version'].startswith('1'):
267 bdev_uuid = bcache_super['dev.uuid']312 bdev_uuid = bcache_super['dev.uuid']
268 rule.append(compose_udev_equality("ENV{CACHED_UUID}", bdev_uuid))313 matches += [[compose_udev_equality("ENV{CACHED_UUID}", bdev_uuid)]]
269 bcache.write_label(sanitize_dname(dname), backing_dev)314 bcache.write_label(sanitize_dname(dname), backing_dev)
270 elif vol.get('type') == "lvm_partition":315 elif vol.get('type') == "lvm_partition":
271 volgroup_name = storage_config.get(vol.get('volgroup')).get('name')316 volgroup_name = storage_config.get(vol.get('volgroup')).get('name')
272 dname = "%s-%s" % (volgroup_name, dname)317 dname = "%s-%s" % (volgroup_name, dname)
273 rule.append(compose_udev_equality("ENV{DM_NAME}", dname))318 matches += [[compose_udev_equality("ENV{DM_NAME}", dname)]]
274 else:319 else:
275 raise ValueError('cannot make dname for device with type: {}'320 raise ValueError('cannot make dname for device with type: {}'
276 .format(vol.get('type')))321 .format(vol.get('type')))
@@ -283,11 +328,25 @@ def make_dname(volume, storage_config):
283 LOG.warning(328 LOG.warning(
284 "dname modified to remove invalid chars. old: '{}' new: '{}'"329 "dname modified to remove invalid chars. old: '{}' new: '{}'"
285 .format(dname, sanitized))330 .format(dname, sanitized))
286 rule.append("SYMLINK+=\"disk/by-dname/%s\"\n" % sanitized)331 content = ['# Written by curtin']
287 LOG.debug("Writing dname udev rule '{}'".format(str(rule)))332 for match in matches:
333 rule = (base_rule + match +
334 ["SYMLINK+=\"disk/by-dname/%s\"\n" % sanitized])
335 LOG.debug("Creating dname udev rule '{}'".format(str(rule)))
336 content.append(', '.join(rule))
337
338 if vol.get('type') == 'disk':
339 for brule in byid:
340 rule = (base_rule +
341 [compose_udev_equality('ENV{DEVTYPE}', 'partition')] +
342 brule +
343 ['SYMLINK+="disk/by-dname/%s-part%%n"\n' % sanitized])
344 LOG.debug("Creating dname udev rule '{}'".format(str(rule)))
345 content.append(', '.join(rule))
346
288 util.ensure_dir(rules_dir)347 util.ensure_dir(rules_dir)
289 rule_file = os.path.join(rules_dir, '{}.rules'.format(sanitized))348 rule_file = os.path.join(rules_dir, '{}.rules'.format(sanitized))
290 util.write_file(rule_file, ', '.join(rule))349 util.write_file(rule_file, '\n'.join(content))
291350
292351
293def get_poolname(info, storage_config):352def get_poolname(info, storage_config):
diff --git a/curtin/udev.py b/curtin/udev.py
index 13d9cc5..106a7e7 100644
--- a/curtin/udev.py
+++ b/curtin/udev.py
@@ -61,4 +61,41 @@ def udevadm_trigger(devices):
61 util.subp(['udevadm', 'trigger'] + list(devices))61 util.subp(['udevadm', 'trigger'] + list(devices))
62 udevadm_settle()62 udevadm_settle()
6363
64
65def udevadm_info(path=None):
66 """ Return a dictionary populated by properties of the device specified
67 in the `path` variable via querying udev 'property' database.
68
69 :params: path: path to device, either /dev or /sys
70 :returns: dictionary of key=value pairs as exported from the udev database
71 :raises: ValueError path is None, ProcessExecutionError on exec error.
72 """
73 if not path:
74 raise ValueError('Invalid path: "%s"' % path)
75
76 info_cmd = ['udevadm', 'info', '--query=property', path]
77 output, _ = util.subp(info_cmd, capture=True)
78
79 # strip for trailing empty line
80 info = {}
81 for line in output.splitlines():
82 if not line:
83 continue
84 # maxsplit=2 gives us key and remaininng part of line is value
85 # py2.7 on Trusty doesn't have keyword, pass as argument
86 key, value = line.split('=', 2)
87 if not value:
88 value = None
89 if value:
90 # devlinks is a list of paths separated by space
91 # convert to a list for easy use
92 if key == 'DEVLINKS':
93 info[key] = value.split()
94 else:
95 # preserve spaces in values, to match udev database
96 info[key] = value
97
98 return info
99
100
64# vi: ts=4 expandtab syntax=python101# vi: ts=4 expandtab syntax=python
diff --git a/doc/topics/storage.rst b/doc/topics/storage.rst
index 92fdbc3..f9a9fe8 100644
--- a/doc/topics/storage.rst
+++ b/doc/topics/storage.rst
@@ -175,7 +175,15 @@ not need to be preserved.
175If the ``name`` key is present, curtin will create a udev rule that makes a175If the ``name`` key is present, curtin will create a udev rule that makes a
176symbolic link to the disk with the given name value. This makes it easy to find176symbolic link to the disk with the given name value. This makes it easy to find
177disks on an installed system. The links are created in177disks on an installed system. The links are created in
178``/dev/disk/by-dname/<name>``.178``/dev/disk/by-dname/<name>``. The udev rules will utilize two types of disk
179metadata to construct the link. For disks with ``serial`` and/or ``wwn`` values
180these will be used to ensure the name persists even if the contents of the disk
181change. For legacy purposes, curtin also emits a rule utilizing metadata on
182the disk contents, typically a partition UUID value, this also preserves these
183links for disks which lack persistent attributes such as a ``serial`` or
184``wwn``, typically found on virtualized environments where such values are left
185unset.
186
179A link to each partition on the disk will also be created at187A link to each partition on the disk will also be created at
180``/dev/disk/by-dname/<name>-part<number>``, so if ``name: maindisk`` is set,188``/dev/disk/by-dname/<name>-part<number>``, so if ``name: maindisk`` is set,
181the disk will be at ``/dev/disk/by-dname/maindisk`` and the first partition on189the disk will be at ``/dev/disk/by-dname/maindisk`` and the first partition on
@@ -276,6 +284,17 @@ filesystem or be mounted anywhere on the system.
276If the preserve flag is set to true, curtin will verify that the partition284If the preserve flag is set to true, curtin will verify that the partition
277exists and will not modify the partition.285exists and will not modify the partition.
278286
287**name**: *<name>*
288
289If the ``name`` key is present, curtin will create a udev rule that makes a
290symbolic link to the partition with the given name value. The links are created
291in ``/dev/disk/by-dname/<name>``.
292
293For partitions, the udev rule created relies upon disk contents, in this case
294the partition entry UUID. This will remain in effect unless the underlying disk
295on which the partition resides has the partition table modified or wiped.
296
297
279**Config Example**::298**Config Example**::
280299
281 - id: disk0-part1300 - id: disk0-part1
@@ -284,6 +303,7 @@ exists and will not modify the partition.
284 size: 8GB303 size: 8GB
285 device: disk0304 device: disk0
286 flag: boot305 flag: boot
306 name: boot_partition
287307
288.. _format:308.. _format:
289309
@@ -504,6 +524,12 @@ scheme for Logical Volumes follows the pattern
504with ``name`` *lv1* on a ``lvm_volgroup`` named *vg1* would have the path524with ``name`` *lv1* on a ``lvm_volgroup`` named *vg1* would have the path
505``/dev/disk/by-dname/vg1-lv1``.525``/dev/disk/by-dname/vg1-lv1``.
506526
527.. note::
528
529 dname values for contructed devices (such as lvm) only remain persistent
530 as long as the device metadata does not change. If users modify the device
531 such that device metadata is changed then the udev rule may no longer apply.
532
507**volgroup**: *<volgroup id>*533**volgroup**: *<volgroup id>*
508534
509The ``volgroup`` key specifies the ``id`` of the Volume Group in which to535The ``volgroup`` key specifies the ``id`` of the Volume Group in which to
@@ -600,7 +626,9 @@ The ``name`` key specifies the name of the md device.
600.. note::626.. note::
601627
602 Curtin creates a udev rule to create a link to the md device in628 Curtin creates a udev rule to create a link to the md device in
603 ``/dev/disk/by-dname/<name>`` using the specified name.629 ``/dev/disk/by-dname/<name>`` using the specified name. The dname
630 symbolic link is only persistent as long as the raid metadata is
631 not modifed or destroyed.
604632
605**raidlevel**: *0, 1, 5, 6, 10*633**raidlevel**: *0, 1, 5, 6, 10*
606634
@@ -677,6 +705,13 @@ reads/writes from the cache. None effectively disables bcache.
677If the ``name`` key is present, curtin will create a link to the device at705If the ``name`` key is present, curtin will create a link to the device at
678``/dev/disk/by-dname/<name>``.706``/dev/disk/by-dname/<name>``.
679707
708.. note::
709
710 dname values for contructed devices (such as bcache) only remain persistent
711 as long as the device metadata does not change. If users modify the device
712 such that device metadata is changed then the udev rule may no longer apply.
713
714
680**Config Example**::715**Config Example**::
681716
682 - id: bcache0717 - id: bcache0
diff --git a/examples/tests/basic.yaml b/examples/tests/basic.yaml
index f0ddc5d..71730c0 100644
--- a/examples/tests/basic.yaml
+++ b/examples/tests/basic.yaml
@@ -26,6 +26,7 @@ storage:
26 number: 326 number: 3
27 size: 1GB27 size: 1GB
28 device: sda28 device: sda
29 name: swap
29 - id: sda1_root30 - id: sda1_root
30 type: format31 type: format
31 fstype: ext432 fstype: ext4
@@ -88,6 +89,7 @@ storage:
88 device: pnum_disk89 device: pnum_disk
89 flag: prep90 flag: prep
90 wipe: zero91 wipe: zero
92 name: prep
91 - id: pnum_disk_p393 - id: pnum_disk_p3
92 type: partition94 type: partition
93 number: 1095 number: 10
diff --git a/examples/tests/basic_scsi.yaml b/examples/tests/basic_scsi.yaml
index 0ea8a51..aa62137 100644
--- a/examples/tests/basic_scsi.yaml
+++ b/examples/tests/basic_scsi.yaml
@@ -25,14 +25,20 @@ storage:
25 number: 325 number: 3
26 size: 1GB26 size: 1GB
27 device: sda27 device: sda
28 name: swap
28 - id: sda1_root29 - id: sda1_root
29 type: format30 type: format
30 fstype: ext431 fstype: ext4
31 volume: sda132 volume: sda1
33 label: 'cloudimg-rootfs'
32 - id: sda2_home34 - id: sda2_home
33 type: format35 type: format
34 fstype: ext436 fstype: ext4
35 volume: sda237 volume: sda2
38 - id: sda3_swap
39 type: format
40 fstype: swap
41 volume: sda3
36 - id: sda1_mount42 - id: sda1_mount
37 type: mount43 type: mount
38 path: /44 path: /
@@ -46,6 +52,10 @@ storage:
46 wwn: '0x080258d13ea95ae5'52 wwn: '0x080258d13ea95ae5'
47 name: sparedisk53 name: sparedisk
48 wipe: superblock54 wipe: superblock
55 - id: sparedisk_fat_fmt_id
56 type: format
57 fstype: fat32
58 volume: sparedisk_id
49 - id: btrfs_disk_id59 - id: btrfs_disk_id
50 type: disk60 type: disk
51 wwn: '0x22dc58dc023c7008'61 wwn: '0x22dc58dc023c7008'
@@ -78,8 +88,13 @@ storage:
78 device: pnum_disk88 device: pnum_disk
79 flag: prep89 flag: prep
80 wipe: zero90 wipe: zero
91 name: prep
81 - id: pnum_disk_p392 - id: pnum_disk_p3
82 type: partition93 type: partition
83 number: 1094 number: 10
84 size: 1GB95 size: 1GB
85 device: pnum_disk96 device: pnum_disk
97 - id: swap_mnt
98 type: mount
99 path: "none"
100 device: sda3_swap
diff --git a/examples/tests/nvme.yaml b/examples/tests/nvme.yaml
index b2a1276..4cf7735 100644
--- a/examples/tests/nvme.yaml
+++ b/examples/tests/nvme.yaml
@@ -44,7 +44,7 @@ storage:
44 device: main_disk_home44 device: main_disk_home
45 - id: nvme_disk45 - id: nvme_disk
46 type: disk46 type: disk
47 path: /dev/nvme0n147 serial: nvme-a
48 name: nvme_disk48 name: nvme_disk
49 wipe: superblock49 wipe: superblock
50 ptable: gpt50 ptable: gpt
@@ -63,7 +63,7 @@ storage:
63 device: nvme_disk63 device: nvme_disk
64 - id: nvme_disk264 - id: nvme_disk2
65 type: disk65 type: disk
66 path: /dev/nvme1n166 serial: nvme-b
67 wipe: superblock67 wipe: superblock
68 ptable: msdos68 ptable: msdos
69 name: second_nvme69 name: second_nvme
diff --git a/examples/tests/nvme_bcache.yaml b/examples/tests/nvme_bcache.yaml
index 2ee0ad3..4fefd94 100644
--- a/examples/tests/nvme_bcache.yaml
+++ b/examples/tests/nvme_bcache.yaml
@@ -19,7 +19,7 @@ storage:
19 model: INTEL SSDPEDME400G419 model: INTEL SSDPEDME400G4
20 name: nvme0n120 name: nvme0n1
21 ptable: gpt21 ptable: gpt
22 path: /dev/nvme0n122 serial: nvme-a
23 type: disk23 type: disk
24 wipe: superblock24 wipe: superblock
25 - device: sda25 - device: sda
diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
index 58e068b..1268880 100644
--- a/tests/unittests/helpers.py
+++ b/tests/unittests/helpers.py
@@ -5,7 +5,9 @@ import imp
5import importlib5import importlib
6import mock6import mock
7import os7import os
8import random
8import shutil9import shutil
10import string
9import tempfile11import tempfile
10from unittest import TestCase12from unittest import TestCase
1113
@@ -67,6 +69,11 @@ class CiTestCase(TestCase):
67 return os.path.normpath(69 return os.path.normpath(
68 os.path.abspath(os.path.sep.join((_dir, path))))70 os.path.abspath(os.path.sep.join((_dir, path))))
6971
72 def random_string(self, length=8):
73 """ return a random lowercase string with default length of 8"""
74 return ''.join(
75 random.choice(string.ascii_lowercase) for _ in range(length))
76
7077
71def dir2dict(startdir, prefix=None):78def dir2dict(startdir, prefix=None):
72 flist = {}79 flist = {}
diff --git a/tests/unittests/test_make_dname.py b/tests/unittests/test_make_dname.py
index 2b92a88..76c7b28 100644
--- a/tests/unittests/test_make_dname.py
+++ b/tests/unittests/test_make_dname.py
@@ -13,10 +13,16 @@ class TestMakeDname(CiTestCase):
13 state = {'scratch': '/tmp/null'}13 state = {'scratch': '/tmp/null'}
14 rules_d = '/tmp/null/rules.d'14 rules_d = '/tmp/null/rules.d'
15 rule_file = '/tmp/null/rules.d/{}.rules'15 rule_file = '/tmp/null/rules.d/{}.rules'
16 disk_serial = 'abcdefg'
17 disk_wwn = '0x1234567890'
16 storage_config = {18 storage_config = {
17 'disk1': {'type': 'disk', 'id': 'disk1', 'name': 'main_disk'},19 'disk1': {'type': 'disk', 'id': 'disk1', 'name': 'main_disk',
20 'serial': disk_serial},
21 'disk_noid': {'type': 'disk', 'id': 'disk_noid', 'name': 'main_disk'},
18 'disk1p1': {'type': 'partition', 'id': 'disk1p1', 'device': 'disk1'},22 'disk1p1': {'type': 'partition', 'id': 'disk1p1', 'device': 'disk1'},
19 'disk2': {'type': 'disk', 'id': 'disk2',23 'disk1p2': {'type': 'partition', 'id': 'disk1p2', 'device': 'disk1',
24 'name': 'custom-partname'},
25 'disk2': {'type': 'disk', 'id': 'disk2', 'wwn': disk_wwn,
20 'name': 'in_valid/name!@#$% &*(+disk'},26 'name': 'in_valid/name!@#$% &*(+disk'},
21 'disk2p1': {'type': 'partition', 'id': 'disk2p1', 'device': 'disk2'},27 'disk2p1': {'type': 'partition', 'id': 'disk2p1', 'device': 'disk2'},
22 'md_id': {'type': 'raid', 'id': 'md_id', 'name': 'mdadm_name'},28 'md_id': {'type': 'raid', 'id': 'md_id', 'name': 'mdadm_name'},
@@ -27,7 +33,8 @@ class TestMakeDname(CiTestCase):
27 'lpart2_id': {'type': 'lvm_partition', 'id': 'lpart2_id',33 'lpart2_id': {'type': 'lvm_partition', 'id': 'lpart2_id',
28 'name': 'lvm part/2', 'volgroup': 'lvol_id'},34 'name': 'lvm part/2', 'volgroup': 'lvol_id'},
29 'bcache1_id': {'type': 'bcache', 'id': 'bcache1_id',35 'bcache1_id': {'type': 'bcache', 'id': 'bcache1_id',
30 'name': 'my-cached-data'}36 'name': 'my-cached-data'},
37 'iscsi1': {'type': 'disk', 'id': 'iscsi1', 'name': 'iscsi_disk1'}
31 }38 }
32 bcache_super_show = {39 bcache_super_show = {
33 'sb.version': '1 [backing device]',40 'sb.version': '1 [backing device]',
@@ -57,20 +64,37 @@ class TestMakeDname(CiTestCase):
57 rule.append('SYMLINK+="disk/by-dname/{}"\n'.format(target))64 rule.append('SYMLINK+="disk/by-dname/{}"\n'.format(target))
58 return ', '.join(rule)65 return ', '.join(rule)
5966
67 def _content(self, rules=[]):
68 return "\n".join(['# Written by curtin'] + rules)
69
70 @mock.patch('curtin.commands.block_meta.udevadm_info')
60 @mock.patch('curtin.commands.block_meta.LOG')71 @mock.patch('curtin.commands.block_meta.LOG')
61 @mock.patch('curtin.commands.block_meta.get_path_to_storage_volume')72 @mock.patch('curtin.commands.block_meta.get_path_to_storage_volume')
62 @mock.patch('curtin.commands.block_meta.util')73 @mock.patch('curtin.commands.block_meta.util')
63 def test_make_dname_disk(self, mock_util, mock_get_path, mock_log):74 def test_make_dname_disk(self, mock_util, mock_get_path, mock_log,
75 mock_udev):
64 disk_ptuuid = str(uuid.uuid1())76 disk_ptuuid = str(uuid.uuid1())
65 mock_util.subp.side_effect = self._make_mock_subp_blkid(77 mock_util.subp.side_effect = self._make_mock_subp_blkid(
66 disk_ptuuid, self.disk_blkid)78 disk_ptuuid, self.disk_blkid)
67 mock_util.load_command_environment.return_value = self.state79 mock_util.load_command_environment.return_value = self.state
68 rule_identifiers = [80
69 ('DEVTYPE', 'disk'),81 rule_identifiers = [('ID_PART_TABLE_UUID', disk_ptuuid)]
70 ('ID_PART_TABLE_UUID', disk_ptuuid)82 id_rule_identifiers = [('ID_SERIAL', self.disk_serial)]
71 ]83 wwn_rule_identifiers = [('ID_WWN_WITH_EXTENSION', self.disk_wwn)]
84
85 def _drule(devtype, match):
86 return [('DEVTYPE', devtype)] + [m for m in match]
87
88 def drule(match):
89 return _drule('disk', match)
90
91 def prule(match):
92 return _drule('partition', match)
7293
73 # simple run94 # simple run
95 mock_udev.side_effect = (
96 [{'DEVTYPE': 'disk', 'ID_SERIAL': self.disk_serial},
97 {'DEVTYPE': 'disk', 'ID_WWN_WITH_EXTENSION': self.disk_wwn}])
74 res_dname = 'main_disk'98 res_dname = 'main_disk'
75 block_meta.make_dname('disk1', self.storage_config)99 block_meta.make_dname('disk1', self.storage_config)
76 mock_util.ensure_dir.assert_called_with(self.rules_d)100 mock_util.ensure_dir.assert_called_with(self.rules_d)
@@ -78,7 +102,13 @@ class TestMakeDname(CiTestCase):
78 self.assertFalse(mock_log.warning.called)102 self.assertFalse(mock_log.warning.called)
79 mock_util.write_file.assert_called_with(103 mock_util.write_file.assert_called_with(
80 self.rule_file.format(res_dname),104 self.rule_file.format(res_dname),
81 self._formatted_rule(rule_identifiers, res_dname))105 self._content(
106 [self._formatted_rule(drule(rule_identifiers),
107 res_dname),
108 self._formatted_rule(drule(id_rule_identifiers),
109 res_dname),
110 self._formatted_rule(prule(id_rule_identifiers),
111 "%s-part%%n" % res_dname)]))
82112
83 # run invalid dname113 # run invalid dname
84 res_dname = 'in_valid-name----------disk'114 res_dname = 'in_valid-name----------disk'
@@ -86,12 +116,39 @@ class TestMakeDname(CiTestCase):
86 self.assertTrue(mock_log.warning.called)116 self.assertTrue(mock_log.warning.called)
87 mock_util.write_file.assert_called_with(117 mock_util.write_file.assert_called_with(
88 self.rule_file.format(res_dname),118 self.rule_file.format(res_dname),
89 self._formatted_rule(rule_identifiers, res_dname))119 self._content(
120 [self._formatted_rule(drule(rule_identifiers),
121 res_dname),
122 self._formatted_rule(drule(wwn_rule_identifiers),
123 res_dname),
124 self._formatted_rule(prule(wwn_rule_identifiers),
125 "%s-part%%n" % res_dname)]))
90126
127 # iscsi disk with no config, but info returns serial and wwn
128 mock_udev.side_effect = (
129 [{'DEVTYPE': 'disk', 'ID_SERIAL': self.disk_serial,
130 'DEVTYPE': 'disk', 'ID_WWN_WITH_EXTENSION': self.disk_wwn}])
131 res_dname = 'iscsi_disk1'
132 block_meta.make_dname('iscsi1', self.storage_config)
133 mock_util.ensure_dir.assert_called_with(self.rules_d)
134 self.assertTrue(mock_log.debug.called)
135 both_rules = (id_rule_identifiers + wwn_rule_identifiers)
136 mock_util.write_file.assert_called_with(
137 self.rule_file.format(res_dname),
138 self._content(
139 [self._formatted_rule(drule(rule_identifiers), res_dname),
140 self._formatted_rule(drule(both_rules), res_dname),
141 self._formatted_rule(prule(both_rules),
142 "%s-part%%n" % res_dname)]))
143
144 @mock.patch('curtin.commands.block_meta.udevadm_info')
91 @mock.patch('curtin.commands.block_meta.LOG')145 @mock.patch('curtin.commands.block_meta.LOG')
92 @mock.patch('curtin.commands.block_meta.get_path_to_storage_volume')146 @mock.patch('curtin.commands.block_meta.get_path_to_storage_volume')
93 @mock.patch('curtin.commands.block_meta.util')147 @mock.patch('curtin.commands.block_meta.util')
94 def test_make_dname_failures(self, mock_util, mock_get_path, mock_log):148 def test_make_dname_failures(self, mock_util, mock_get_path, mock_log,
149 mock_udev):
150 mock_udev.side_effect = ([{'DEVTYPE': 'disk'}, {'DEVTYPE': 'disk'}])
151
95 mock_util.subp.side_effect = self._make_mock_subp_blkid(152 mock_util.subp.side_effect = self._make_mock_subp_blkid(
96 '', self.trusty_blkid)153 '', self.trusty_blkid)
97 mock_util.load_command_environment.return_value = self.state154 mock_util.load_command_environment.return_value = self.state
@@ -99,10 +156,14 @@ class TestMakeDname(CiTestCase):
99 warning_msg = "Can't find a uuid for volume: {}. Skipping dname."156 warning_msg = "Can't find a uuid for volume: {}. Skipping dname."
100157
101 # disk with no PT_UUID158 # disk with no PT_UUID
102 block_meta.make_dname('disk1', self.storage_config)159 disk = 'disk_noid'
103 mock_log.warning.assert_called_with(warning_msg.format('disk1'))160 with self.assertRaises(RuntimeError):
104 self.assertFalse(mock_util.write_file.called)161 block_meta.make_dname(disk, self.storage_config)
162 mock_log.warning.assert_called_with(warning_msg.format(disk))
163 self.assertFalse(mock_util.write_file.called)
105164
165 mock_util.subp.side_effect = self._make_mock_subp_blkid(
166 '', self.trusty_blkid)
106 # partition with no PART_UUID167 # partition with no PART_UUID
107 block_meta.make_dname('disk1p1', self.storage_config)168 block_meta.make_dname('disk1p1', self.storage_config)
108 mock_log.warning.assert_called_with(warning_msg.format('disk1p1'))169 mock_log.warning.assert_called_with(warning_msg.format('disk1p1'))
@@ -123,22 +184,15 @@ class TestMakeDname(CiTestCase):
123 ]184 ]
124185
125 # simple run186 # simple run
126 res_dname = 'main_disk-part1'187 res_dname = 'custom-partname'
127 block_meta.make_dname('disk1p1', self.storage_config)188 block_meta.make_dname('disk1p2', self.storage_config)
128 mock_util.ensure_dir.assert_called_with(self.rules_d)189 mock_util.ensure_dir.assert_called_with(self.rules_d)
129 self.assertTrue(mock_log.debug.called)190 self.assertTrue(mock_log.debug.called)
130 self.assertFalse(mock_log.warning.called)191 self.assertFalse(mock_log.warning.called)
131 mock_util.write_file.assert_called_with(192 mock_util.write_file.assert_called_with(
132 self.rule_file.format(res_dname),193 self.rule_file.format(res_dname),
133 self._formatted_rule(rule_identifiers, res_dname))194 self._content(
134195 [self._formatted_rule(rule_identifiers, res_dname)]))
135 # run invalid dname
136 res_dname = 'in_valid-name----------disk-part1'
137 block_meta.make_dname('disk2p1', self.storage_config)
138 self.assertTrue(mock_log.warning.called)
139 mock_util.write_file.assert_called_with(
140 self.rule_file.format(res_dname),
141 self._formatted_rule(rule_identifiers, res_dname))
142196
143 @mock.patch('curtin.commands.block_meta.mdadm')197 @mock.patch('curtin.commands.block_meta.mdadm')
144 @mock.patch('curtin.commands.block_meta.LOG')198 @mock.patch('curtin.commands.block_meta.LOG')
@@ -158,7 +212,8 @@ class TestMakeDname(CiTestCase):
158 self.assertFalse(mock_log.warning.called)212 self.assertFalse(mock_log.warning.called)
159 mock_util.write_file.assert_called_with(213 mock_util.write_file.assert_called_with(
160 self.rule_file.format(res_dname),214 self.rule_file.format(res_dname),
161 self._formatted_rule(rule_identifiers, res_dname))215 self._content(
216 [self._formatted_rule(rule_identifiers, res_dname)]))
162217
163 # invalid name218 # invalid name
164 res_dname = 'mdadm-name'219 res_dname = 'mdadm-name'
@@ -166,7 +221,8 @@ class TestMakeDname(CiTestCase):
166 self.assertTrue(mock_log.warning.called)221 self.assertTrue(mock_log.warning.called)
167 mock_util.write_file.assert_called_with(222 mock_util.write_file.assert_called_with(
168 self.rule_file.format(res_dname),223 self.rule_file.format(res_dname),
169 self._formatted_rule(rule_identifiers, res_dname))224 self._content(
225 [self._formatted_rule(rule_identifiers, res_dname)]))
170226
171 @mock.patch('curtin.commands.block_meta.LOG')227 @mock.patch('curtin.commands.block_meta.LOG')
172 @mock.patch('curtin.commands.block_meta.get_path_to_storage_volume')228 @mock.patch('curtin.commands.block_meta.get_path_to_storage_volume')
@@ -183,7 +239,8 @@ class TestMakeDname(CiTestCase):
183 self.assertFalse(mock_log.warning.called)239 self.assertFalse(mock_log.warning.called)
184 mock_util.write_file.assert_called_with(240 mock_util.write_file.assert_called_with(
185 self.rule_file.format(res_dname),241 self.rule_file.format(res_dname),
186 self._formatted_rule(rule_identifiers, res_dname))242 self._content(
243 [self._formatted_rule(rule_identifiers, res_dname)]))
187244
188 # with invalid name245 # with invalid name
189 res_dname = 'vg1-lvm-part-2'246 res_dname = 'vg1-lvm-part-2'
@@ -192,7 +249,8 @@ class TestMakeDname(CiTestCase):
192 self.assertTrue(mock_log.warning.called)249 self.assertTrue(mock_log.warning.called)
193 mock_util.write_file.assert_called_with(250 mock_util.write_file.assert_called_with(
194 self.rule_file.format(res_dname),251 self.rule_file.format(res_dname),
195 self._formatted_rule(rule_identifiers, res_dname))252 self._content(
253 [self._formatted_rule(rule_identifiers, res_dname)]))
196254
197 @mock.patch('curtin.commands.block_meta.LOG')255 @mock.patch('curtin.commands.block_meta.LOG')
198 @mock.patch('curtin.commands.block_meta.bcache')256 @mock.patch('curtin.commands.block_meta.bcache')
@@ -213,7 +271,8 @@ class TestMakeDname(CiTestCase):
213 self.assertFalse(mock_log.warning.called)271 self.assertFalse(mock_log.warning.called)
214 mock_util.write_file.assert_called_with(272 mock_util.write_file.assert_called_with(
215 self.rule_file.format(res_dname),273 self.rule_file.format(res_dname),
216 self._formatted_rule(rule_identifiers, res_dname))274 self._content(
275 [self._formatted_rule(rule_identifiers, res_dname)]))
217276
218 def test_sanitize_dname(self):277 def test_sanitize_dname(self):
219 unsanitized_to_sanitized = [278 unsanitized_to_sanitized = [
@@ -226,4 +285,87 @@ class TestMakeDname(CiTestCase):
226 for (unsanitized, sanitized) in unsanitized_to_sanitized:285 for (unsanitized, sanitized) in unsanitized_to_sanitized:
227 self.assertEqual(block_meta.sanitize_dname(unsanitized), sanitized)286 self.assertEqual(block_meta.sanitize_dname(unsanitized), sanitized)
228287
288
289class TestMakeDnameById(CiTestCase):
290
291 @mock.patch('curtin.commands.block_meta.udevadm_info')
292 def test_bad_path(self, m_udev):
293 """test dname_byid raises ValueError on invalid path."""
294 mypath = None
295 with self.assertRaises(ValueError):
296 block_meta.make_dname_byid(mypath)
297
298 @mock.patch('curtin.commands.block_meta.udevadm_info')
299 def test_non_disk(self, m_udev):
300 """test dname_byid raises ValueError on DEVTYPE != 'disk'"""
301 mypath = "/dev/" + self.random_string()
302 m_udev.return_value = {'DEVTYPE': 'not_a_disk'}
303 with self.assertRaises(ValueError):
304 block_meta.make_dname_byid(mypath)
305
306 @mock.patch('curtin.commands.block_meta.udevadm_info')
307 def test_disk_with_no_id_wwn(self, m_udev):
308 """test dname_byid raises RuntimeError on device without ID or WWN."""
309 mypath = "/dev/" + self.random_string()
310 m_udev.return_value = {'DEVTYPE': 'disk'}
311 with self.assertRaises(RuntimeError):
312 block_meta.make_dname_byid(mypath)
313
314 @mock.patch('curtin.commands.block_meta.udevadm_info')
315 def test_udevinfo_not_called_if_info_provided(self, m_udev):
316 """dname_byid does not invoke udevadm_info if using info dict"""
317 myserial = self.random_string()
318 self.assertEqual(
319 [['ENV{ID_SERIAL}=="%s"' % myserial]],
320 block_meta.make_dname_byid(
321 self.random_string(),
322 info={'DEVTYPE': 'disk', 'ID_SERIAL': myserial}))
323 self.assertEqual(0, m_udev.call_count)
324
325 @mock.patch('curtin.commands.block_meta.udevadm_info')
326 def test_udevinfo_called_if_info_not_provided(self, m_udev):
327 """dname_byid should call udevadm_info if no data given."""
328 myserial = self.random_string()
329 mypath = "/dev/" + self.random_string()
330 m_udev.return_value = {
331 'DEVTYPE': 'disk', 'ID_SERIAL': myserial, 'DEVNAME': mypath}
332 self.assertEqual(
333 [['ENV{ID_SERIAL}=="%s"' % myserial]],
334 block_meta.make_dname_byid(mypath))
335 self.assertEqual(
336 [mock.call(path=mypath)], m_udev.call_args_list)
337
338 def test_disk_with_only_serial(self):
339 """test dname_byid returns rules for ID_SERIAL"""
340 mypath = "/dev/" + self.random_string()
341 myserial = self.random_string()
342 info = {'DEVTYPE': 'disk', 'DEVNAME': mypath, 'ID_SERIAL': myserial}
343 self.assertEqual(
344 [['ENV{ID_SERIAL}=="%s"' % myserial]],
345 block_meta.make_dname_byid(mypath, info=info))
346
347 def test_disk_with_only_wwn(self):
348 """test dname_byid returns rules for ID_WWN_WITH_EXTENSION"""
349 mypath = "/dev/" + self.random_string()
350 mywwn = self.random_string()
351 info = {'DEVTYPE': 'disk', 'DEVNAME': mypath,
352 'ID_WWN_WITH_EXTENSION': mywwn}
353 self.assertEqual(
354 [['ENV{ID_WWN_WITH_EXTENSION}=="%s"' % mywwn]],
355 block_meta.make_dname_byid(mypath, info=info))
356
357 def test_disk_with_both_id_wwn(self):
358 """test dname_byid returns rules with both ID_SERIAL and ID_WWN"""
359 mypath = "/dev/" + self.random_string()
360 myserial = self.random_string()
361 mywwn = self.random_string()
362 info = {'DEVTYPE': 'disk', 'ID_SERIAL': myserial,
363 'ID_WWN_WITH_EXTENSION': mywwn,
364 'DEVNAME': mypath}
365 self.assertEqual(
366 [['ENV{ID_SERIAL}=="%s"' % myserial,
367 'ENV{ID_WWN_WITH_EXTENSION}=="%s"' % mywwn]],
368 block_meta.make_dname_byid(mypath, info=info))
369
370
229# vi: ts=4 expandtab syntax=python371# vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_udev.py b/tests/unittests/test_udev.py
230new file mode 100644372new file mode 100644
index 0000000..0a070d5
--- /dev/null
+++ b/tests/unittests/test_udev.py
@@ -0,0 +1,68 @@
1# This file is part of curtin. See LICENSE file for copyright and license info.
2
3import mock
4
5from curtin import udev
6from curtin import util
7from .helpers import CiTestCase
8
9
10UDEVADM_INFO_QUERY = """\
11DEVLINKS=/dev/disk/by-id/nvme-eui.0025388b710116a1
12DEVNAME=/dev/nvme0n1
13DEVPATH=/devices/pci0000:00/0000:00:1c.4/0000:05:00.0/nvme/nvme0/nvme0n1
14DEVTYPE=disk
15ID_PART_TABLE_TYPE=gpt
16ID_PART_TABLE_UUID=ea0b9ddc-a114-4e01-b257-750d86e3a944
17ID_SERIAL=SAMSUNG MZVLB1T0HALR-000L7_S3TPNY0JB00151
18ID_SERIAL_SHORT=S3TPNY0JB00151
19MAJOR=259
20MINOR=0
21SUBSYSTEM=block
22TAGS=:systemd:
23USEC_INITIALIZED=2026691
24"""
25
26INFO_DICT = {
27 'DEVLINKS': ['/dev/disk/by-id/nvme-eui.0025388b710116a1'],
28 'DEVNAME': '/dev/nvme0n1',
29 'DEVPATH':
30 '/devices/pci0000:00/0000:00:1c.4/0000:05:00.0/nvme/nvme0/nvme0n1',
31 'DEVTYPE': 'disk',
32 'ID_PART_TABLE_TYPE': 'gpt',
33 'ID_PART_TABLE_UUID': 'ea0b9ddc-a114-4e01-b257-750d86e3a944',
34 'ID_SERIAL': 'SAMSUNG MZVLB1T0HALR-000L7_S3TPNY0JB00151',
35 'ID_SERIAL_SHORT': 'S3TPNY0JB00151',
36 'MAJOR': '259',
37 'MINOR': '0',
38 'SUBSYSTEM': 'block',
39 'TAGS': ':systemd:',
40 'USEC_INITIALIZED': '2026691'
41}
42
43
44class TestUdevInfo(CiTestCase):
45
46 @mock.patch('curtin.util.subp')
47 def test_udevadm_info(self, m_subp):
48 """ udevadm_info returns dictionary for specified device """
49 mypath = '/dev/nvme0n1'
50 m_subp.return_value = (UDEVADM_INFO_QUERY, "")
51 info = udev.udevadm_info(mypath)
52 m_subp.assert_called_with(
53 ['udevadm', 'info', '--query=property', mypath], capture=True)
54 self.assertEqual(sorted(INFO_DICT), sorted(info))
55
56 def test_udevadm_info_no_path(self):
57 """ udevadm_info raises ValueError for invalid path value"""
58 mypath = None
59 with self.assertRaises(ValueError):
60 udev.udevadm_info(mypath)
61
62 @mock.patch('curtin.util.subp')
63 def test_udevadm_info_path_not_exists(self, m_subp):
64 """ udevadm_info raises ProcessExecutionError for invalid path value"""
65 mypath = self.random_string()
66 m_subp.side_effect = util.ProcessExecutionError()
67 with self.assertRaises(util.ProcessExecutionError):
68 udev.udevadm_info(mypath)
diff --git a/tests/vmtests/__init__.py b/tests/vmtests/__init__.py
index 51eda5c..5ffa632 100644
--- a/tests/vmtests/__init__.py
+++ b/tests/vmtests/__init__.py
@@ -21,6 +21,7 @@ from curtin.block import iscsi
2121
22from .report_webhook_logger import CaptureReporting22from .report_webhook_logger import CaptureReporting
23from curtin.commands.install import INSTALL_PASS_MSG23from curtin.commands.install import INSTALL_PASS_MSG
24from curtin.commands.block_meta import sanitize_dname
2425
25from .image_sync import query as imagesync_query26from .image_sync import query as imagesync_query
26from .image_sync import mirror as imagesync_mirror27from .image_sync import mirror as imagesync_mirror
@@ -955,9 +956,18 @@ class VMBaseClass(TestCase):
955 storage_config = cls.get_class_storage_config()956 storage_config = cls.get_class_storage_config()
956 cls.disk_wwns = ["wwn=%s" % x.get('wwn') for x in storage_config957 cls.disk_wwns = ["wwn=%s" % x.get('wwn') for x in storage_config
957 if 'wwn' in x]958 if 'wwn' in x]
958 cls.disk_serials = ["serial=%s" % x.get('serial')959 cls.disk_serials = []
959 for x in storage_config if 'serial' in x]960 cls.nvme_serials = []
961 for x in storage_config:
962 if 'serial' in x:
963 serial = x.get('serial')
964 if serial.startswith('nvme'):
965 cls.nvme_serials.append("serial=%s" % serial)
966 else:
967 cls.disk_serials.append("serial=%s" % serial)
960968
969 logger.info("disk_serials: %s", cls.disk_serials)
970 logger.info("nvme_serials: %s", cls.nvme_serials)
961 target_disk = "{}:{}:{}:{}:".format(cls.td.target_disk,971 target_disk = "{}:{}:{}:{}:".format(cls.td.target_disk,
962 "",972 "",
963 cls.disk_driver,973 cls.disk_driver,
@@ -989,11 +999,13 @@ class VMBaseClass(TestCase):
989 disks.extend(['--disk', extra_disk])999 disks.extend(['--disk', extra_disk])
9901000
991 # build nvme disk args if needed1001 # build nvme disk args if needed
1002 logger.info('nvme disks: %s', cls.nvme_disks)
992 for (disk_no, disk_sz) in enumerate(cls.nvme_disks):1003 for (disk_no, disk_sz) in enumerate(cls.nvme_disks):
993 dpath = os.path.join(cls.td.disks, 'nvme_disk_%d.img' % disk_no)1004 dpath = os.path.join(cls.td.disks, 'nvme_disk_%d.img' % disk_no)
1005 nvme_serial = cls.nvme_serials[disk_no]
994 nvme_disk = '{}:{}:nvme:{}:{}'.format(dpath, disk_sz,1006 nvme_disk = '{}:{}:nvme:{}:{}'.format(dpath, disk_sz,
995 cls.disk_block_size,1007 cls.disk_block_size,
996 "serial=nvme-%d" % disk_no)1008 "%s" % nvme_serial)
997 disks.extend(['--disk', nvme_disk])1009 disks.extend(['--disk', nvme_disk])
9981010
999 # build iscsi disk args if needed1011 # build iscsi disk args if needed
@@ -1222,6 +1234,8 @@ class VMBaseClass(TestCase):
1222 dpath = os.path.join(cls.td.disks, 'nvme_disk_%d.img' % disk_no)1234 dpath = os.path.join(cls.td.disks, 'nvme_disk_%d.img' % disk_no)
1223 disk = '--disk={},driver={},format={},{}'.format(1235 disk = '--disk={},driver={},format={},{}'.format(
1224 dpath, disk_driver, TARGET_IMAGE_FORMAT, bsize_args)1236 dpath, disk_driver, TARGET_IMAGE_FORMAT, bsize_args)
1237 if len(cls.nvme_serials):
1238 disk += ",%s" % cls.nvme_serials[disk_no]
1225 nvme_disks.extend([disk])1239 nvme_disks.extend([disk])
12261240
1227 # unlike NVMe disks, we do not want to configure the iSCSI disks1241 # unlike NVMe disks, we do not want to configure the iSCSI disks
@@ -1541,8 +1555,41 @@ class VMBaseClass(TestCase):
1541 for diskname, part in self.disk_to_check:1555 for diskname, part in self.disk_to_check:
1542 if part is not 0:1556 if part is not 0:
1543 link = diskname + "-part" + str(part)1557 link = diskname + "-part" + str(part)
1544 self.assertIn(link, contents)1558 self.assertIn(link, contents.splitlines())
1545 self.assertIn(diskname, contents)1559 self.assertIn(diskname, contents.splitlines())
1560
1561 @skip_if_flag('expected_failure')
1562 def test_dname_rules(self, disk_to_check=None):
1563 if self.target_distro != "ubuntu":
1564 raise SkipTest("dname not present in non-ubuntu releases")
1565
1566 if not disk_to_check:
1567 disk_to_check = self.disk_to_check
1568 if disk_to_check is None:
1569 logger.debug('test_dname_rules: no disks to check')
1570 return
1571 logger.debug('test_dname_rules: checking disks: %s', disk_to_check)
1572 self.output_files_exist(["udev_rules.d"])
1573
1574 cfg = yaml.load(self.load_collect_file("root/curtin-install-cfg.yaml"))
1575 stgcfg = cfg.get("storage", {}).get("config", [])
1576 disks = [ent for ent in stgcfg if (ent.get('type') == 'disk' and
1577 'name' in ent)]
1578 key_to_udev = {
1579 'serial': 'ID_SERIAL',
1580 'wwn': 'ID_WWN_WITH_EXTENSION',
1581 }
1582 for disk in disks:
1583 dname_file = "%s.rules" % sanitize_dname(disk.get('name'))
1584 contents = self.load_collect_file("udev_rules.d/%s" % dname_file)
1585 for key, key_name in key_to_udev.items():
1586 value = disk.get(key)
1587 if value:
1588 # serials may include spaces, udev replaces them with # _
1589 if ' ' in value:
1590 value = value.replace(' ', '_')
1591 self.assertIn(key_name, contents)
1592 self.assertIn(value, contents)
15461593
1547 @skip_if_flag('expected_failure')1594 @skip_if_flag('expected_failure')
1548 def test_reporting_data(self):1595 def test_reporting_data(self):
@@ -1765,6 +1812,9 @@ class PsuedoVMBaseClass(VMBaseClass):
1765 def test_dname(self, disk_to_check=None):1812 def test_dname(self, disk_to_check=None):
1766 pass1813 pass
17671814
1815 def test_dname_rules(self, disk_to_check=None):
1816 pass
1817
1768 def test_interfacesd_eth0_removed(self):1818 def test_interfacesd_eth0_removed(self):
1769 pass1819 pass
17701820
diff --git a/tests/vmtests/test_basic.py b/tests/vmtests/test_basic.py
index 5cb8c30..48f07d6 100644
--- a/tests/vmtests/test_basic.py
+++ b/tests/vmtests/test_basic.py
@@ -17,9 +17,14 @@ class TestBasicAbs(VMBaseClass):
17 dirty_disks = True17 dirty_disks = True
18 conf_file = "examples/tests/basic.yaml"18 conf_file = "examples/tests/basic.yaml"
19 extra_disks = ['128G', '128G', '4G']19 extra_disks = ['128G', '128G', '4G']
20 nvme_disks = ['4G']20 disk_to_check = [('btrfs_volume', 0),
21 disk_to_check = [('main_disk_with_in---valid--dname', 1),21 ('main_disk_with_in---valid--dname', 0),
22 ('main_disk_with_in---valid--dname', 2)]22 ('main_disk_with_in---valid--dname', 1),
23 ('main_disk_with_in---valid--dname', 2),
24 ('pnum_disk', 0),
25 ('pnum_disk', 1),
26 ('pnum_disk', 10),
27 ('sparedisk', 0)]
23 extra_collect_scripts = [textwrap.dedent("""28 extra_collect_scripts = [textwrap.dedent("""
24 cd OUTPUT_COLLECT_D29 cd OUTPUT_COLLECT_D
25 blkid -o export /dev/vda | cat >blkid_output_vda30 blkid -o export /dev/vda | cat >blkid_output_vda
@@ -249,7 +254,6 @@ class TestBasicScsiAbs(TestBasicAbs):
249 conf_file = "examples/tests/basic_scsi.yaml"254 conf_file = "examples/tests/basic_scsi.yaml"
250 disk_driver = 'scsi-hd'255 disk_driver = 'scsi-hd'
251 extra_disks = ['128G', '128G', '4G']256 extra_disks = ['128G', '128G', '4G']
252 nvme_disks = ['4G']
253 extra_collect_scripts = [textwrap.dedent("""257 extra_collect_scripts = [textwrap.dedent("""
254 cd OUTPUT_COLLECT_D258 cd OUTPUT_COLLECT_D
255 blkid -o export /dev/sda | cat >blkid_output_sda259 blkid -o export /dev/sda | cat >blkid_output_sda
diff --git a/tests/vmtests/test_nvme.py b/tests/vmtests/test_nvme.py
index 7755b53..60d9d86 100644
--- a/tests/vmtests/test_nvme.py
+++ b/tests/vmtests/test_nvme.py
@@ -19,9 +19,10 @@ class TestNvmeAbs(VMBaseClass):
19 conf_file = "examples/tests/nvme.yaml"19 conf_file = "examples/tests/nvme.yaml"
20 extra_disks = []20 extra_disks = []
21 nvme_disks = ['4G', '4G']21 nvme_disks = ['4G', '4G']
22 disk_to_check = [('main_disk', 1), ('main_disk', 2), ('main_disk', 15),22 disk_to_check = [
23 ('nvme_disk', 1), ('nvme_disk', 2), ('nvme_disk', 3),23 ('main_disk', 1), ('main_disk', 2), ('main_disk', 15),
24 ('second_nvme', 1)]24 ('nvme_disk', 0), ('nvme_disk', 1), ('nvme_disk', 2), ('nvme_disk', 3),
25 ('second_nvme', 0), ('second_nvme', 1)]
25 extra_collect_scripts = [textwrap.dedent("""26 extra_collect_scripts = [textwrap.dedent("""
26 cd OUTPUT_COLLECT_D27 cd OUTPUT_COLLECT_D
27 ls /sys/class/ > sys_class28 ls /sys/class/ > sys_class
@@ -97,7 +98,8 @@ class TestNvmeBcacheAbs(TestNvmeAbs):
97 extra_disks = ['10G']98 extra_disks = ['10G']
98 nvme_disks = ['6G']99 nvme_disks = ['6G']
99 uefi = True100 uefi = True
100 disk_to_check = [('sda', 1), ('sda', 2), ('sda', 3)]101 disk_to_check = [('sda', 1), ('sda', 2), ('sda', 3),
102 ('sdb', 0), ('nvme0n1', 0)]
101103
102 extra_collect_scripts = [textwrap.dedent("""104 extra_collect_scripts = [textwrap.dedent("""
103 cd OUTPUT_COLLECT_D105 cd OUTPUT_COLLECT_D

Subscribers

People subscribed via source and target branches