Merge lp:~raharper/curtin/trunk.debug-mdadm into lp:~curtin-dev/curtin/trunk

Proposed by Ryan Harper
Status: Merged
Merged at revision: 444
Proposed branch: lp:~raharper/curtin/trunk.debug-mdadm
Merge into: lp:~curtin-dev/curtin/trunk
Diff against target: 607 lines (+335/-27)
9 files modified
curtin/block/__init__.py (+61/-6)
curtin/block/clear_holders.py (+4/-3)
curtin/block/mdadm.py (+22/-3)
curtin/commands/block_meta.py (+14/-0)
curtin/util.py (+13/-0)
tests/unittests/test_block.py (+72/-0)
tests/unittests/test_block_mdadm.py (+26/-11)
tests/unittests/test_clear_holders.py (+5/-4)
tests/unittests/test_commands_block_meta.py (+118/-0)
To merge this branch: bzr merge lp:~raharper/curtin/trunk.debug-mdadm
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
curtin developers Pending
Review via email: mp+315805@code.launchpad.net

Commit message

Add clear_holders checks to disk and partition handlers

In some cases disks may contain block device metadata outside of the
current partition table. Due to Linux udev rules and behaviors;
creating or modifying a disks partition table may trigger udev rules
which may start up mdadm, bcache or lvm services which will obtain
exclusive access to the disk preventing installation from completing.

This branch addresses this issue with the following

- block.wipe_volume now uses os.O_EXCL flag to obtain exclusive access
  to a device; this fails when something else (like mdadm) obtains
  a file handle on the device with O_EXCL flag

- Upon failure to obtain exclusive access, we dump information about
  a devices block holders

- During disk handling, after creating a partition table we check for
  holders and then clear them if present.

- During partition handling, after creating a new partition we check
  for holders and then clear them if present.

Description of the change

Add clear_holders checks to disk and partition handlers

In some cases disks may contain block device metadata outside of the
current partition table. Due to Linux udev rules and behaviors;
creating or modifying a disks partition table may trigger udev rules
which may start up mdadm, bcache or lvm services which will obtain
exclusive access to the disk preventing installation from completing.

This branch addresses this issue with the following

- block.wipe_volume now uses os.O_EXCL flag to obtain exclusive access
  to a device; this fails when something else (like mdadm) obtains
  a file handle on the device with O_EXCL flag

- Upon failure to obtain exclusive access, we dump information about
  a devices block holders

- During disk handling, after creating a partition table we check for
  holders and then clear them if present.

- During partition handling, after creating a new partition we check
  for holders and then clear them if present.

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
Scott Moser (smoser) wrote :

I think this looks good.
there are some small requests for enhancement inline.

Revision history for this message
Ryan Harper (raharper) wrote :
Download full text (8.7 KiB)

Thanks for the review. Comments inline, but mostly ACKs for me to fix.

On Mon, Jan 30, 2017 at 12:32 PM, Scott Moser <email address hidden> wrote:

> I think this looks good.
> there are some small requests for enhancement inline.
>
> Diff comments:
>
> > === modified file 'curtin/block/__init__.py'
> > --- curtin/block/__init__.py 2016-11-18 15:12:19 +0000
> > +++ curtin/block/__init__.py 2017-01-27 17:22:00 +0000
> > @@ -742,7 +754,17 @@
> > LOG.debug("%s is %s bytes. wiping with buflen=%s",
> > path, size, buflen)
> >
> > - with open(path, "rb+") as fp:
> > + try:
> > + fd = os.open(path, os.O_RDWR | os.O_EXCL)
> > + except OSError:
> > + LOG.exception("Failed to exclusively open device")
>
> which device ? I dont think we even print the path to the device at all
> here.
>

It ends up in the exception stacktrace, however, we should be explicit in
the message.
ACK

>
> > + holders = get_holders(path)
> > + LOG.error('Device holders with exclusive access: %s', holders)
> > + mount_points = util.device_is_mounted(path)
> > + LOG.error('Device mounts: %s', mount_points)
> > + raise
> > +
> > + with os.fdopen(fd, "rb+") as fp:
> > while True:
> > pbuf = readfunc(buflen)
> > pos = fp.tell()
>
> i think this is sane, and gets 'fd' above closed except in the case where
> this open fails.
> i think if os.fdopen(fd) fails, then fd will not ever get a close() on it.
> Not that it should fail, but
> " If fdopen() raises an exception, it leaves fd untouched (unclosed)"
>

Lemme look at this.

>
> > @@ -772,11 +794,18 @@
> > if not (is_block or os.path.isfile(path)):
> > raise ValueError("%s: not an existing file or block device",
> path)
> >
> > + pt_names = []
> > if partitions and is_block:
> > ptdata = sysfs_partition_data(path)
> > for kname, ptnum, start, size in ptdata:
> > - offsets.append(start)
> > - offsets.append(start + size - zero_size)
> > + pt_names.append(dev_path(kname))
> > + # offsets.append(start)
> > + # offsets.append(start + size - zero_size)
> > + pt_names.reverse()
> > +
> > + for pt in pt_names:
> > + LOG.debug('Wiping partition: %s', pt)
>
> give the device path to be cleaar:
> Wiping partitition %s on %s, pt, path
>

I think that's implicit in the pt name since it's comprised of
dev_path(kname)
Ie, it's going to be items like '/dev/sda3', '/dev/nvme0n1p1'

>
> > + quick_zero(pt, partitions=False)
> >
> > LOG.debug("wiping 1M on %s at offsets %s", path, offsets)
> > return zero_file_at_offsets(path, offsets, buflen=buflen,
> count=count)
> > @@ -796,7 +825,19 @@
> > buf = b'\0' * buflen
> > tot = buflen * count
> > msg_vals = {'path': path, 'tot': buflen * count}
> > - with open(path, "rb+") as fp:
> > +
> > + # ensure we're the only open fd on the device
> > + try:
> > + fd = os.open(path, os.O_RDWR | os.O_EXCL)
> > + except OSError:
> > + LOG.exception("Failed to exclusively open device")
>
> which device ? perhaps this fail path could...

Read more...

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

On Mon, Jan 30, 2017 at 12:32 PM, Scott Moser <email address hidden> wrote:

> I think this looks good.
> there are some small requests for enhancement inline.
>
> Diff comments:
>
> > + holders = get_holders(path)
> > + LOG.error('Device holders with exclusive access: %s', holders)
> > + mount_points = util.device_is_mounted(path)
> > + LOG.error('Device mounts: %s', mount_points)
> > + raise
> > +
> > + with os.fdopen(fd, "rb+") as fp:
> > while True:
> > pbuf = readfunc(buflen)
> > pos = fp.tell()
>
> i think this is sane, and gets 'fd' above closed except in the case where
> this open fails.
> i think if os.fdopen(fd) fails, then fd will not ever get a close() on it.
> Not that it should fail, but
> " If fdopen() raises an exception, it leaves fd untouched (unclosed)"
>

In the case the os.fdopen fails; I don't think fd is still valid for
closing. Even if it were, we cannot tell
as fd is *just* an int, which may or maynot point to a real file-descriptor
anymore.
If we blindly call os.close(fd), we may encounter an OSError ("Bad file
descriptor") if the fd is already
closed.

Lastly, as we exit wipe_file, the fd will leave scope and Python will reap
the fd.

If you can find a counter-example where we are leaking an fd, please let me
know.

Ryan

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

Heres an example on the case where hte os level file handle does not get closed.
Whether its worth handling, I'll not comment. But it is a real possibility that you
get an open file handle but the os.fdopen() fails, and thus the close never happens
that would happen on the exit from the 'with'.

#!/usr/bin/python3
import os, sys
path = sys.argv[1]
with open(path, "w") as owritefp:
    owritefp.write("hello world\n")

# fd is an open file handle here.
fd = os.open(path, os.O_RDONLY | os.O_EXCL)

# this fails beacuse rb+ is not compatible with O_RDONLY above.
# resulting in no 'close' being called on the "file object" fp.
with os.fdopen(fd, "rb+") as fp:
    fp.write("bye bye\n")

os.close(fd)

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

>
> I think that's implicit in the pt name since it's comprised of
> dev_path(kname)
> Ie, it's going to be items like '/dev/sda3', '/dev/nvme0n1p1'

Yeah, you're right. unless somehow the partition name is wrong, and that is why it failed... ie, /dev/foo!/css1
or some oddity that left us guessing what went wrong.

sm

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

On Mon, Jan 30, 2017 at 6:56 PM, Scott Moser <email address hidden> wrote:

> Heres an example on the case where hte os level file handle does not get
> closed.
> Whether its worth handling, I'll not comment. But it is a real
> possibility that you
> get an open file handle but the os.fdopen() fails, and thus the close
> never happens
> that would happen on the exit from the 'with'.
>
>
> #!/usr/bin/python3
> import os, sys
> path = sys.argv[1]
> with open(path, "w") as owritefp:
> owritefp.write("hello world\n")\
>

Not sure what this is doing here.

> # fd is an open file handle here.
> fd = os.open(path, os.O_RDONLY | os.O_EXCL)
>
> # this fails beacuse rb+ is not compatible with O_RDONLY above.
> # resulting in no 'close' being called on the "file object" fp.
> with os.fdopen(fd, "rb+") as fp:
> fp.write("bye bye\n")
>

> os.close(fd)
>

That's not what's happening;

 #!/usr/bin/python3
import os, sys, errno

path = sys.argv[1]

# fd is an open file handle here.
fd = os.open(path, os.O_RDONLY | os.O_EXCL)

# this fails beacuse rb+ is not compatible with O_RDONLY above.
# resulting in no 'close' being called on the "file object" fp.
print("fd=%s" % fd)
try:
    with os.fdopen(fd, "rb+") as fp:
        msg = "bye bye\n".encode("utf-8")
        fp.write(msg)
except Exception as e:
    print("Failed to write to fd: %s" % e)

print("closing fd:%s" % fd)
try:
    os.close(fd)
    print("close OK")
except OSError as err:
    if err.args[0] == errno.EBADF:
        print("fd isn't a valid file descriptor (man 2 close)")
    else:
        print("some other error")

This outputs:

% python3 fd.py /tmp/foo
fd=3
Failed to write to fd: [Errno 9] Bad file descriptor
closing fd:3
fd isn't a valid file descriptor (man 2 close)

I believe what happens is that the context manager constructs a FileObject
passing in the fd,
The FileObject exit hook handles closing the fd so that when we return from
it, the fd is
already closed.

In fact, that's _exactly_ what happens. If I drop the with and just use
assignments:

try:
    fp = os.fdopen(fd, "rb+")
    msg = "bye bye\n".encode("utf-8")
    fp.write(msg)
except Exception as e:
    print("Failed to write to fd: %s" % e)

And now the output looks like this:

 % python3 fd.py /tmp/foo
fd=3
closing fd:3
close OK

> --
> https://code.launchpad.net/~raharper/curtin/trunk.debug-
> mdadm/+merge/315805
> You are the owner of lp:~raharper/curtin/trunk.debug-mdadm.
>

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

On Mon, Jan 30, 2017 at 6:59 PM, Scott Moser <email address hidden> wrote:

> >
> > I think that's implicit in the pt name since it's comprised of
> > dev_path(kname)
> > Ie, it's going to be items like '/dev/sda3', '/dev/nvme0n1p1'
>
> Yeah, you're right. unless somehow the partition name is wrong, and that
> is why it failed... ie, /dev/foo!/css1
> or some oddity that left us guessing what went wrong.
>

I'll double check; kname comes from lsblk output, and we're just prefixing
that with /dev/ in dev_path
Not sure how that's going to be wrong. But I can print all three (kname,
partnumber, and combined)

>
> sm
>
> --
> https://code.launchpad.net/~raharper/curtin/trunk.debug-
> mdadm/+merge/315805
> You are the owner of lp:~raharper/curtin/trunk.debug-mdadm.
>

444. By Ryan Harper

Address MP feedback

- use err instead of _err when consuming
- Use device == entry[0] to match device mounted
- Rename device_is_mounted to list_device_mounts
- Refactor exclusive open into a contextmanager
- Be more verbose when recursively wiping partitions

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 :

close.

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

On Tue, Jan 31, 2017 at 3:19 PM, Scott Moser <email address hidden> wrote:

> close.
>
>
> Diff comments:
>
> >
> > === modified file 'curtin/block/clear_holders.py'
> > --- curtin/block/clear_holders.py 2016-08-30 21:56:13 +0000
> > +++ curtin/block/clear_holders.py 2017-01-31 20:57:11 +0000
> > @@ -62,6 +63,25 @@
> > return os.path.realpath(os.path.join(sysfs_path, 'bcache',
> 'cache'))
> >
> >
> > +def umount_device(device):
> > + """
> > + Attempt to unmount a device and any other devices which may be
> > + mounted on top of the devices mountpoint
> > + """
> > + proc_mounts = block.get_proc_mounts()
> > + mount_entries = [entry for entry in proc_mounts if device ==
> entry[0]]
> > + if len(mount_entries) > 0:
> > + for me in mount_entries:
> > + LOG.debug('Device is mounted: %s', me)
> > + mount_point = me[1]
> > + submounts = [entry for entry in proc_mounts
> > + if mount_point in entry[1]]
>
> need to do if mount_point + "/" in entry[1]
> if you had:
> /opt
> /opt2
> that would go awry, causing you to attempt to unmount /opt2. and probably
> should do startswith also.
>

I'll add unittests to get it right.

>
> submounts = [entry for entry in proc_mounts if entry[1].startswith(mount_point
> + "/")]
>
> > + umount_list = sorted(submounts, key=lambda x: x[1],
> reverse=True)
>
> i'm pretty sure this is still wrong.
> you dont want to sort submounts on the second field in /proc/mounts. that
> could go awry. You want to walk /proc/mounts in reverse. Not the somewhat
> arbitrary order that C locale sorting by the second field will give you.
>

I'll add unittests, what I really need to sort by the number of submounts,
walk to the leaf and umount on the way back.
You mentioned we have this in mount-image-callback; I'll see about bringing
that logic over to python and have a unittest.

I may drop it from this MR as this isn't needed but it's useful for
restarting control-c'd curtin installs which leave devices mounted.

I also need to add a unittest for that exclusive_open to ensure we take the
right paths, and additional unittest to test that
we invoke clear_holders in the case that we find new holders after
partitioning.

>
> > + for mp in [m[1] for m in umount_list]:
> > + LOG.debug('Umounting submounts at: %s', mp)
> > + util.do_umount(mp)
> > +
> > +
> > def shutdown_bcache(device):
> > """
> > Shut down bcache for specified bcache device
>
>
> --
> https://code.launchpad.net/~raharper/curtin/trunk.debug-
> mdadm/+merge/315805
> You are the owner of lp:~raharper/curtin/trunk.debug-mdadm.
>

445. By Ryan Harper

Remove incomplete unmount_device

446. By Ryan Harper

add unittest for block.exclusive_open

447. By Ryan Harper

add block_meta unittests to validate clear_holders called as needed

448. By Ryan Harper

merge from trunk

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

remove debugging prints in block.wipe_file

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

Re-ran vmtests on diglett:

Ran 1015 tests in 7099.384s

OK (SKIP=1)

The skip is the apt-proxy test which is disabled on diglett.

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 :

looks good to me. Approve.
I didn't take time to read, but please re-read your commit message to make sure it is still relevant (since we've' been through many iterations here).

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

ACK

On Wed, Feb 1, 2017 at 9:46 AM, Scott Moser <email address hidden> wrote:

> looks good to me. Approve.
> I didn't take time to read, but please re-read your commit message to make
> sure it is still relevant (since we've' been through many iterations here).
>
> --
> https://code.launchpad.net/~raharper/curtin/trunk.debug-
> mdadm/+merge/315805
> You are the owner of lp:~raharper/curtin/trunk.debug-mdadm.
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'curtin/block/__init__.py'
--- curtin/block/__init__.py 2016-11-18 15:12:19 +0000
+++ curtin/block/__init__.py 2017-02-01 15:10:46 +0000
@@ -15,12 +15,14 @@
15# You should have received a copy of the GNU Affero General Public License15# You should have received a copy of the GNU Affero General Public License
16# along with Curtin. If not, see <http://www.gnu.org/licenses/>.16# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
1717
18from contextlib import contextmanager
18import errno19import errno
20import itertools
19import os21import os
22import shlex
20import stat23import stat
21import shlex24import sys
22import tempfile25import tempfile
23import itertools
2426
25from curtin import util27from curtin import util
26from curtin.block import lvm28from curtin.block import lvm
@@ -165,6 +167,18 @@
165 return os.path.normpath(path)167 return os.path.normpath(path)
166168
167169
170def get_holders(device):
171 """
172 Look up any block device holders, return list of knames
173 """
174 # block.sys_block_path works when given a /sys or /dev path
175 sysfs_path = sys_block_path(device)
176 # get holders
177 holders = os.listdir(os.path.join(sysfs_path, 'holders'))
178 LOG.debug("devname '%s' had holders: %s", device, holders)
179 return holders
180
181
168def _lsblock_pairs_to_dict(lines):182def _lsblock_pairs_to_dict(lines):
169 """183 """
170 parse lsblock output and convert to dict184 parse lsblock output and convert to dict
@@ -723,6 +737,40 @@
723 check_dos_signature(device))737 check_dos_signature(device))
724738
725739
740@contextmanager
741def exclusive_open(path):
742 """
743 Obtain an exclusive file-handle to the file/device specified
744 """
745 print('exclusive_open running')
746 mode = 'rb+'
747 fd = None
748 if not os.path.exists(path):
749 raise ValueError("No such file at path: %s" % path)
750
751 try:
752 fd = os.open(path, os.O_RDWR | os.O_EXCL)
753 try:
754 fd_needs_closing = True
755 with os.fdopen(fd, mode) as fo:
756 yield fo
757 fd_needs_closing = False
758 except OSError:
759 LOG.exception("Failed to create file-object from fd")
760 raise
761 finally:
762 # python2 leaves fd open if there os.fdopen fails
763 if fd_needs_closing and sys.version_info.major == 2:
764 os.close(fd)
765 except OSError:
766 LOG.exception("Failed to exclusively open path: %s", path)
767 holders = get_holders(path)
768 LOG.error('Device holders with exclusive access: %s', holders)
769 mount_points = util.list_device_mounts(path)
770 LOG.error('Device mounts: %s', mount_points)
771 raise
772
773
726def wipe_file(path, reader=None, buflen=4 * 1024 * 1024):774def wipe_file(path, reader=None, buflen=4 * 1024 * 1024):
727 """775 """
728 wipe the existing file at path.776 wipe the existing file at path.
@@ -742,7 +790,7 @@
742 LOG.debug("%s is %s bytes. wiping with buflen=%s",790 LOG.debug("%s is %s bytes. wiping with buflen=%s",
743 path, size, buflen)791 path, size, buflen)
744792
745 with open(path, "rb+") as fp:793 with exclusive_open(path) as fp:
746 while True:794 while True:
747 pbuf = readfunc(buflen)795 pbuf = readfunc(buflen)
748 pos = fp.tell()796 pos = fp.tell()
@@ -772,11 +820,17 @@
772 if not (is_block or os.path.isfile(path)):820 if not (is_block or os.path.isfile(path)):
773 raise ValueError("%s: not an existing file or block device", path)821 raise ValueError("%s: not an existing file or block device", path)
774822
823 pt_names = []
775 if partitions and is_block:824 if partitions and is_block:
776 ptdata = sysfs_partition_data(path)825 ptdata = sysfs_partition_data(path)
777 for kname, ptnum, start, size in ptdata:826 for kname, ptnum, start, size in ptdata:
778 offsets.append(start)827 pt_names.append((dev_path(kname), kname, ptnum))
779 offsets.append(start + size - zero_size)828 pt_names.reverse()
829
830 for (pt, kname, ptnum) in pt_names:
831 LOG.debug('Wiping path: dev:%s kname:%s partnum:%s',
832 pt, kname, ptnum)
833 quick_zero(pt, partitions=False)
780834
781 LOG.debug("wiping 1M on %s at offsets %s", path, offsets)835 LOG.debug("wiping 1M on %s at offsets %s", path, offsets)
782 return zero_file_at_offsets(path, offsets, buflen=buflen, count=count)836 return zero_file_at_offsets(path, offsets, buflen=buflen, count=count)
@@ -796,7 +850,8 @@
796 buf = b'\0' * buflen850 buf = b'\0' * buflen
797 tot = buflen * count851 tot = buflen * count
798 msg_vals = {'path': path, 'tot': buflen * count}852 msg_vals = {'path': path, 'tot': buflen * count}
799 with open(path, "rb+") as fp:853
854 with exclusive_open(path) as fp:
800 # get the size by seeking to end.855 # get the size by seeking to end.
801 fp.seek(0, 2)856 fp.seek(0, 2)
802 size = fp.tell()857 size = fp.tell()
803858
=== modified file 'curtin/block/clear_holders.py'
--- curtin/block/clear_holders.py 2016-08-30 21:56:13 +0000
+++ curtin/block/clear_holders.py 2017-02-01 15:10:46 +0000
@@ -25,6 +25,7 @@
2525
26from curtin import (block, udev, util)26from curtin import (block, udev, util)
27from curtin.block import lvm27from curtin.block import lvm
28from curtin.block import mdadm
28from curtin.log import LOG29from curtin.log import LOG
2930
3031
@@ -118,8 +119,8 @@
118 """119 """
119 blockdev = block.sysfs_to_devpath(device)120 blockdev = block.sysfs_to_devpath(device)
120 LOG.debug('using mdadm.mdadm_stop on dev: %s', blockdev)121 LOG.debug('using mdadm.mdadm_stop on dev: %s', blockdev)
121 block.mdadm.mdadm_stop(blockdev)122 mdadm.mdadm_stop(blockdev)
122 block.mdadm.mdadm_remove(blockdev)123 mdadm.mdadm_remove(blockdev)
123124
124125
125def wipe_superblock(device):126def wipe_superblock(device):
@@ -369,7 +370,7 @@
369 # but there was not enough to start the array, the call to wipe_volume on370 # but there was not enough to start the array, the call to wipe_volume on
370 # all disks and partitions should be sufficient to remove the mdadm371 # all disks and partitions should be sufficient to remove the mdadm
371 # metadata372 # metadata
372 block.mdadm.mdadm_assemble(scan=True, ignore_errors=True)373 mdadm.mdadm_assemble(scan=True, ignore_errors=True)
373 # the bcache module needs to be present to properly detect bcache devs374 # the bcache module needs to be present to properly detect bcache devs
374 # on some systems (precise without hwe kernel) it may not be possible to375 # on some systems (precise without hwe kernel) it may not be possible to
375 # lad the bcache module bcause it is not present in the kernel. if this376 # lad the bcache module bcause it is not present in the kernel. if this
376377
=== modified file 'curtin/block/mdadm.py'
--- curtin/block/mdadm.py 2016-08-30 21:56:13 +0000
+++ curtin/block/mdadm.py 2017-02-01 15:10:46 +0000
@@ -28,6 +28,7 @@
28from subprocess import CalledProcessError28from subprocess import CalledProcessError
2929
30from curtin.block import (dev_short, dev_path, is_valid_device, sys_block_path)30from curtin.block import (dev_short, dev_path, is_valid_device, sys_block_path)
31from curtin.block import get_holders
31from curtin import (util, udev)32from curtin import (util, udev)
32from curtin.log import LOG33from curtin.log import LOG
3334
@@ -138,7 +139,12 @@
138 # mdadm assemble returns 2 when called on an array that is already139 # mdadm assemble returns 2 when called on an array that is already
139 # assembled. this is not an error, so accept return code of 2140 # assembled. this is not an error, so accept return code of 2
140 # all other return codes can be accepted with ignore_error set to true141 # all other return codes can be accepted with ignore_error set to true
141 util.subp(cmd, capture=True, rcs=[0, 1, 2])142 scan, err = util.subp(cmd, capture=True, rcs=[0, 1, 2])
143 LOG.debug('mdadm assemble scan results:\n%s\n%s', scan, err)
144 scan, err = util.subp(['mdadm', '--detail', '--scan', '-v'],
145 capture=True, rcs=[0, 1])
146 LOG.debug('mdadm detail scan after assemble:\n%s\n%s',
147 scan, err)
142 except util.ProcessExecutionError:148 except util.ProcessExecutionError:
143 LOG.warning("mdadm_assemble had unexpected return code")149 LOG.warning("mdadm_assemble had unexpected return code")
144 if not ignore_errors:150 if not ignore_errors:
@@ -177,6 +183,11 @@
177 cmd.append("--name=%s" % md_name)183 cmd.append("--name=%s" % md_name)
178184
179 for device in devices:185 for device in devices:
186 # mdadm can be sticky, double check
187 holders = get_holders(device)
188 if len(holders) > 0:
189 LOG.warning('Detected holders during mdadm creation: %s', holders)
190 raise OSError('Failed to remove holders from %s', device)
180 # Zero out device superblock just in case device has been used for raid191 # Zero out device superblock just in case device has been used for raid
181 # before, as this will cause many issues192 # before, as this will cause many issues
182 util.subp(["mdadm", "--zero-superblock", device], capture=True)193 util.subp(["mdadm", "--zero-superblock", device], capture=True)
@@ -206,7 +217,12 @@
206 LOG.debug('available md modules: \n%s' % out)217 LOG.debug('available md modules: \n%s' % out)
207 else:218 else:
208 LOG.debug('no available md modules found')219 LOG.debug('no available md modules found')
220
221 for dev in devices + spares:
222 h = get_holders(dev)
223 LOG.debug('Device %s has holders: %s', dev, h)
209 raise224 raise
225
210 util.subp(["udevadm", "control", "--start-exec-queue"])226 util.subp(["udevadm", "control", "--start-exec-queue"])
211 util.subp(["udevadm", "settle",227 util.subp(["udevadm", "settle",
212 "--exit-if-exists=%s" % md_devname])228 "--exit-if-exists=%s" % md_devname])
@@ -241,14 +257,17 @@
241 assert_valid_devpath(devpath)257 assert_valid_devpath(devpath)
242258
243 LOG.info("mdadm stopping: %s" % devpath)259 LOG.info("mdadm stopping: %s" % devpath)
244 util.subp(["mdadm", "--stop", devpath], rcs=[0, 1], capture=True)260 out, err = util.subp(["mdadm", "--stop", devpath], capture=True)
261 LOG.debug("mdadm stop:\n%s\n%s", out, err)
245262
246263
247def mdadm_remove(devpath):264def mdadm_remove(devpath):
248 assert_valid_devpath(devpath)265 assert_valid_devpath(devpath)
249266
250 LOG.info("mdadm removing: %s" % devpath)267 LOG.info("mdadm removing: %s" % devpath)
251 util.subp(["mdadm", "--remove", devpath], rcs=[0, 1], capture=True)268 out, err = util.subp(["mdadm", "--remove", devpath],
269 rcs=[0], capture=True)
270 LOG.debug("mdadm remove:\n%s\n%s", out, err)
252271
253272
254def mdadm_query_detail(md_devname, export=MDADM_USE_EXPORT):273def mdadm_query_detail(md_devname, export=MDADM_USE_EXPORT):
255274
=== modified file 'curtin/commands/block_meta.py'
--- curtin/commands/block_meta.py 2016-12-02 02:01:20 +0000
+++ curtin/commands/block_meta.py 2017-02-01 15:10:46 +0000
@@ -367,6 +367,11 @@
367 util.subp(["parted", disk, "--script", "mklabel", "msdos"])367 util.subp(["parted", disk, "--script", "mklabel", "msdos"])
368 else:368 else:
369 raise ValueError('invalid partition table type: %s', ptable)369 raise ValueError('invalid partition table type: %s', ptable)
370 holders = clear_holders.get_holders(disk)
371 if len(holders) > 0:
372 LOG.info('Detected block holders on disk %s: %s', disk, holders)
373 clear_holders.clear_holders(disk)
374 clear_holders.assert_clear(disk)
370375
371 # Make the name if needed376 # Make the name if needed
372 if info.get('name'):377 if info.get('name'):
@@ -538,6 +543,15 @@
538 else:543 else:
539 raise ValueError("parent partition has invalid partition table")544 raise ValueError("parent partition has invalid partition table")
540545
546 # check if we've triggered hidden metadata like md, lvm or bcache
547 part_kname = get_path_to_storage_volume(info.get('id'), storage_config)
548 holders = clear_holders.get_holders(part_kname)
549 if len(holders) > 0:
550 LOG.debug('Detected block holders on partition %s: %s', part_kname,
551 holders)
552 clear_holders.clear_holders(part_kname)
553 clear_holders.assert_clear(part_kname)
554
541 # Wipe the partition if told to do so, do not wipe dos extended partitions555 # Wipe the partition if told to do so, do not wipe dos extended partitions
542 # as this may damage the extended partition table556 # as this may damage the extended partition table
543 if config.value_as_boolean(info.get('wipe')):557 if config.value_as_boolean(info.get('wipe')):
544558
=== modified file 'curtin/util.py'
--- curtin/util.py 2017-01-27 19:44:39 +0000
+++ curtin/util.py 2017-02-01 15:10:46 +0000
@@ -274,6 +274,19 @@
274 return False274 return False
275275
276276
277def list_device_mounts(device):
278 # return mount entry if device is in /proc/mounts
279 mounts = ""
280 with open("/proc/mounts", "r") as fp:
281 mounts = fp.read()
282
283 dev_mounts = []
284 for line in mounts.splitlines():
285 if line.split()[0] == device:
286 dev_mounts.append(line)
287 return dev_mounts
288
289
277def do_mount(src, target, opts=None):290def do_mount(src, target, opts=None):
278 # mount src at target with opts and return True291 # mount src at target with opts and return True
279 # if already mounted, return False292 # if already mounted, return False
280293
=== modified file 'tests/unittests/test_block.py'
--- tests/unittests/test_block.py 2016-11-18 15:12:19 +0000
+++ tests/unittests/test_block.py 2017-02-01 15:10:46 +0000
@@ -4,6 +4,7 @@
4import mock4import mock
5import tempfile5import tempfile
6import shutil6import shutil
7import sys
78
8from collections import OrderedDict9from collections import OrderedDict
910
@@ -252,6 +253,77 @@
252 found = util.load_file(trgfile)253 found = util.load_file(trgfile)
253 self.assertEqual(data, found)254 self.assertEqual(data, found)
254255
256 def test_exclusive_open_raise_missing(self):
257 myfile = self.tfile("no-such-file")
258
259 with self.assertRaises(ValueError):
260 with block.exclusive_open(myfile) as fp:
261 fp.close()
262
263 @mock.patch('os.close')
264 @mock.patch('os.fdopen')
265 @mock.patch('os.open')
266 def test_exclusive_open(self, mock_os_open, mock_os_fdopen, mock_os_close):
267 flen = 1024
268 myfile = self.tfile("my_exclusive_file")
269 util.write_file(myfile, flen * b'\1', omode="wb")
270 mock_fd = 3
271 mock_os_open.return_value = mock_fd
272
273 with block.exclusive_open(myfile) as fp:
274 fp.close()
275
276 mock_os_open.assert_called_with(myfile, os.O_RDWR | os.O_EXCL)
277 mock_os_fdopen.assert_called_with(mock_fd, 'rb+')
278 self.assertEqual([], mock_os_close.call_args_list)
279
280 @mock.patch('os.close')
281 @mock.patch('curtin.util.list_device_mounts')
282 @mock.patch('curtin.block.get_holders')
283 @mock.patch('os.open')
284 def test_exclusive_open_non_exclusive_exception(self, mock_os_open,
285 mock_holders,
286 mock_list_mounts,
287 mock_os_close):
288 flen = 1024
289 myfile = self.tfile("my_exclusive_file")
290 util.write_file(myfile, flen * b'\1', omode="wb")
291 mock_os_open.side_effect = OSError("NO_O_EXCL")
292 mock_holders.return_value = ['md1']
293 mock_list_mounts.return_value = []
294
295 with self.assertRaises(OSError):
296 with block.exclusive_open(myfile) as fp:
297 fp.close()
298
299 mock_os_open.assert_called_with(myfile, os.O_RDWR | os.O_EXCL)
300 mock_holders.assert_called_with(myfile)
301 mock_list_mounts.assert_called_with(myfile)
302 self.assertEqual([], mock_os_close.call_args_list)
303
304 @mock.patch('os.close')
305 @mock.patch('os.fdopen')
306 @mock.patch('os.open')
307 def test_exclusive_open_fdopen_failure(self, mock_os_open,
308 mock_os_fdopen, mock_os_close):
309 flen = 1024
310 myfile = self.tfile("my_exclusive_file")
311 util.write_file(myfile, flen * b'\1', omode="wb")
312 mock_fd = 3
313 mock_os_open.return_value = mock_fd
314 mock_os_fdopen.side_effect = OSError("EBADF")
315
316 with self.assertRaises(OSError):
317 with block.exclusive_open(myfile) as fp:
318 fp.close()
319
320 mock_os_open.assert_called_with(myfile, os.O_RDWR | os.O_EXCL)
321 mock_os_fdopen.assert_called_with(mock_fd, 'rb+')
322 if sys.version_info.major == 2:
323 mock_os_close.assert_called_with(mock_fd)
324 else:
325 self.assertEqual([], mock_os_close.call_args_list)
326
255327
256class TestWipeVolume(TestCase):328class TestWipeVolume(TestCase):
257 dev = '/dev/null'329 dev = '/dev/null'
258330
=== modified file 'tests/unittests/test_block_mdadm.py'
--- tests/unittests/test_block_mdadm.py 2016-09-14 19:52:06 +0000
+++ tests/unittests/test_block_mdadm.py 2017-02-01 15:10:46 +0000
@@ -34,17 +34,26 @@
3434
35 def test_mdadm_assemble_scan(self):35 def test_mdadm_assemble_scan(self):
36 mdadm.mdadm_assemble(scan=True)36 mdadm.mdadm_assemble(scan=True)
37 self.mock_util.subp.assert_called_with(37 assemble_calls = [
38 ["mdadm", "--assemble", "--scan", "-v"], capture=True,38 call(["mdadm", "--assemble", "--scan", "-v"], capture=True,
39 rcs=[0, 1, 2])39 rcs=[0, 1, 2]),
40 call(["mdadm", "--detail", "--scan", "-v"], capture=True,
41 rcs=[0, 1]),
42 ]
43 self.mock_util.subp.assert_has_calls(assemble_calls)
40 self.assertTrue(self.mock_udev.udevadm_settle.called)44 self.assertTrue(self.mock_udev.udevadm_settle.called)
4145
42 def test_mdadm_assemble_md_devname(self):46 def test_mdadm_assemble_md_devname(self):
43 md_devname = "/dev/md0"47 md_devname = "/dev/md0"
44 mdadm.mdadm_assemble(md_devname=md_devname)48 mdadm.mdadm_assemble(md_devname=md_devname)
45 self.mock_util.subp.assert_called_with(49
46 ["mdadm", "--assemble", md_devname, "--run"], capture=True,50 assemble_calls = [
47 rcs=[0, 1, 2])51 call(["mdadm", "--assemble", md_devname, "--run"],
52 capture=True, rcs=[0, 1, 2]),
53 call(["mdadm", "--detail", "--scan", "-v"], capture=True,
54 rcs=[0, 1]),
55 ]
56 self.mock_util.subp.assert_has_calls(assemble_calls)
48 self.assertTrue(self.mock_udev.udevadm_settle.called)57 self.assertTrue(self.mock_udev.udevadm_settle.called)
4958
50 def test_mdadm_assemble_md_devname_short(self):59 def test_mdadm_assemble_md_devname_short(self):
@@ -61,9 +70,13 @@
61 md_devname = "/dev/md0"70 md_devname = "/dev/md0"
62 devices = ["/dev/vdc1", "/dev/vdd1"]71 devices = ["/dev/vdc1", "/dev/vdd1"]
63 mdadm.mdadm_assemble(md_devname=md_devname, devices=devices)72 mdadm.mdadm_assemble(md_devname=md_devname, devices=devices)
64 self.mock_util.subp.assert_called_with(73 assemble_calls = [
65 ["mdadm", "--assemble", md_devname, "--run"] + devices,74 call(["mdadm", "--assemble", md_devname, "--run"] + devices,
66 capture=True, rcs=[0, 1, 2])75 capture=True, rcs=[0, 1, 2]),
76 call(["mdadm", "--detail", "--scan", "-v"], capture=True,
77 rcs=[0, 1]),
78 ]
79 self.mock_util.subp.assert_has_calls(assemble_calls)
67 self.assertTrue(self.mock_udev.udevadm_settle.called)80 self.assertTrue(self.mock_udev.udevadm_settle.called)
6881
69 def test_mdadm_assemble_exec_error(self):82 def test_mdadm_assemble_exec_error(self):
@@ -85,10 +98,12 @@
85 super(TestBlockMdadmCreate, self).setUp()98 super(TestBlockMdadmCreate, self).setUp()
86 self.add_patch('curtin.block.mdadm.util', 'mock_util')99 self.add_patch('curtin.block.mdadm.util', 'mock_util')
87 self.add_patch('curtin.block.mdadm.is_valid_device', 'mock_valid')100 self.add_patch('curtin.block.mdadm.is_valid_device', 'mock_valid')
101 self.add_patch('curtin.block.mdadm.get_holders', 'mock_holders')
88102
89 # Common mock settings103 # Common mock settings
90 self.mock_valid.return_value = True104 self.mock_valid.return_value = True
91 self.mock_util.lsb_release.return_value = {'codename': 'precise'}105 self.mock_util.lsb_release.return_value = {'codename': 'precise'}
106 self.mock_holders.return_value = []
92107
93 def prepare_mock(self, md_devname, raidlevel, devices, spares):108 def prepare_mock(self, md_devname, raidlevel, devices, spares):
94 side_effects = []109 side_effects = []
@@ -333,7 +348,7 @@
333 device = "/dev/vdc"348 device = "/dev/vdc"
334 mdadm.mdadm_stop(device)349 mdadm.mdadm_stop(device)
335 expected_calls = [350 expected_calls = [
336 call(["mdadm", "--stop", device], rcs=[0, 1], capture=True),351 call(["mdadm", "--stop", device], capture=True),
337 ]352 ]
338 self.mock_util.subp.assert_has_calls(expected_calls)353 self.mock_util.subp.assert_has_calls(expected_calls)
339354
@@ -359,7 +374,7 @@
359 device = "/dev/vdc"374 device = "/dev/vdc"
360 mdadm.mdadm_remove(device)375 mdadm.mdadm_remove(device)
361 expected_calls = [376 expected_calls = [
362 call(["mdadm", "--remove", device], rcs=[0, 1], capture=True),377 call(["mdadm", "--remove", device], rcs=[0], capture=True),
363 ]378 ]
364 self.mock_util.subp.assert_has_calls(expected_calls)379 self.mock_util.subp.assert_has_calls(expected_calls)
365380
366381
=== modified file 'tests/unittests/test_clear_holders.py'
--- tests/unittests/test_clear_holders.py 2016-09-14 19:38:32 +0000
+++ tests/unittests/test_clear_holders.py 2017-02-01 15:10:46 +0000
@@ -172,13 +172,14 @@
172 ['cryptsetup', 'remove', self.test_blockdev], capture=True)172 ['cryptsetup', 'remove', self.test_blockdev], capture=True)
173173
174 @mock.patch('curtin.block.clear_holders.LOG')174 @mock.patch('curtin.block.clear_holders.LOG')
175 @mock.patch('curtin.block.clear_holders.mdadm')
175 @mock.patch('curtin.block.clear_holders.block')176 @mock.patch('curtin.block.clear_holders.block')
176 def test_shutdown_mdadm(self, mock_block, mock_log):177 def test_shutdown_mdadm(self, mock_block, mock_mdadm, mock_log):
177 """test clear_holders.shutdown_mdadm"""178 """test clear_holders.shutdown_mdadm"""
178 mock_block.sysfs_to_devpath.return_value = self.test_blockdev179 mock_block.sysfs_to_devpath.return_value = self.test_blockdev
179 clear_holders.shutdown_mdadm(self.test_syspath)180 clear_holders.shutdown_mdadm(self.test_syspath)
180 mock_block.mdadm.mdadm_stop.assert_called_with(self.test_blockdev)181 mock_mdadm.mdadm_stop.assert_called_with(self.test_blockdev)
181 mock_block.mdadm.mdadm_remove.assert_called_with(self.test_blockdev)182 mock_mdadm.mdadm_remove.assert_called_with(self.test_blockdev)
182 self.assertTrue(mock_log.debug.called)183 self.assertTrue(mock_log.debug.called)
183184
184 @mock.patch('curtin.block.clear_holders.LOG')185 @mock.patch('curtin.block.clear_holders.LOG')
@@ -320,7 +321,7 @@
320 mock_gen_holders_tree.return_value = self.example_holders_trees[1][1]321 mock_gen_holders_tree.return_value = self.example_holders_trees[1][1]
321 clear_holders.assert_clear(device)322 clear_holders.assert_clear(device)
322323
323 @mock.patch('curtin.block.clear_holders.block.mdadm')324 @mock.patch('curtin.block.clear_holders.mdadm')
324 @mock.patch('curtin.block.clear_holders.util')325 @mock.patch('curtin.block.clear_holders.util')
325 def test_start_clear_holders_deps(self, mock_util, mock_mdadm):326 def test_start_clear_holders_deps(self, mock_util, mock_mdadm):
326 clear_holders.start_clear_holders_deps()327 clear_holders.start_clear_holders_deps()
327328
=== added file 'tests/unittests/test_commands_block_meta.py'
--- tests/unittests/test_commands_block_meta.py 1970-01-01 00:00:00 +0000
+++ tests/unittests/test_commands_block_meta.py 2017-02-01 15:10:46 +0000
@@ -0,0 +1,118 @@
1from unittest import TestCase
2from mock import patch
3
4from curtin.commands import block_meta
5
6
7class BlockMetaTestBase(TestCase):
8 def setUp(self):
9 super(BlockMetaTestBase, self).setUp()
10
11 def add_patch(self, target, attr):
12 """Patches specified target object and sets it as attr on test
13 instance also schedules cleanup"""
14 m = patch(target, autospec=True)
15 p = m.start()
16 self.addCleanup(m.stop)
17 setattr(self, attr, p)
18
19
20class TestBlockMeta(BlockMetaTestBase):
21 def setUp(self):
22 super(TestBlockMeta, self).setUp()
23 # self.target = tempfile.mkdtemp()
24
25 basepath = 'curtin.commands.block_meta.'
26 self.add_patch(basepath + 'get_path_to_storage_volume', 'mock_getpath')
27 self.add_patch(basepath + 'make_dname', 'mock_make_dname')
28 self.add_patch('curtin.util.subp', 'mock_subp')
29 self.add_patch('curtin.block.get_part_table_type',
30 'mock_block_get_part_table_type')
31 self.add_patch('curtin.block.wipe_volume',
32 'mock_block_wipe_volume')
33 self.add_patch('curtin.block.path_to_kname',
34 'mock_block_path_to_kname')
35 self.add_patch('curtin.block.sys_block_path',
36 'mock_block_sys_block_path')
37 self.add_patch('curtin.block.clear_holders.get_holders',
38 'mock_get_holders')
39 self.add_patch('curtin.block.clear_holders.clear_holders',
40 'mock_clear_holders')
41 self.add_patch('curtin.block.clear_holders.assert_clear',
42 'mock_assert_clear')
43
44 self.target = "my_target"
45 self.config = {
46 'storage': {
47 'version': 1,
48 'config': [
49 {'grub_device': True,
50 'id': 'sda',
51 'name': 'sda',
52 'path': '/wark/xxx',
53 'ptable': 'msdos',
54 'type': 'disk',
55 'wipe': 'superblock'},
56 {'device': 'sda',
57 'flag': 'boot',
58 'id': 'sda-part1',
59 'name': 'sda-part1',
60 'number': 1,
61 'offset': '4194304B',
62 'size': '511705088B',
63 'type': 'partition',
64 'uuid': 'fc7ab24c-b6bf-460f-8446-d3ac362c0625',
65 'wipe': 'superblock'}
66 ],
67 }
68 }
69 self.storage_config = (
70 block_meta.extract_storage_ordered_dict(self.config))
71
72 def test_disk_handler_calls_clear_holder(self):
73 info = self.storage_config.get('sda')
74 disk = info.get('path')
75 self.mock_getpath.return_value = disk
76 self.mock_block_get_part_table_type.return_value = 'dos'
77 self.mock_subp.side_effect = iter([
78 (0, 0), # parted mklabel
79 ])
80 holders = ['md1']
81 self.mock_get_holders.return_value = holders
82
83 block_meta.disk_handler(info, self.storage_config)
84
85 print("clear_holders: %s" % self.mock_clear_holders.call_args_list)
86 print("assert_clear: %s" % self.mock_assert_clear.call_args_list)
87 self.mock_clear_holders.assert_called_with(disk)
88 self.mock_assert_clear.assert_called_with(disk)
89
90 def test_partition_handler_calls_clear_holder(self):
91 disk_info = self.storage_config.get('sda')
92 part_info = self.storage_config.get('sda-part1')
93 disk_kname = disk_info.get('path')
94 part_kname = disk_kname + '1'
95 self.mock_getpath.side_effect = iter([
96 disk_info.get('id'),
97 part_kname,
98 part_kname,
99 part_kname,
100 ])
101
102 self.mock_block_get_part_table_type.return_value = 'dos'
103 kname = 'xxx'
104 self.mock_block_path_to_kname.return_value = kname
105 self.mock_block_sys_block_path.return_value = '/sys/class/block/xxx'
106 self.mock_subp.side_effect = iter([
107 ("", 0), # parted mkpart
108 ("", 0), # ??
109 ])
110 holders = ['md1']
111 self.mock_get_holders.return_value = holders
112
113 block_meta.partition_handler(part_info, self.storage_config)
114
115 print("clear_holders: %s" % self.mock_clear_holders.call_args_list)
116 print("assert_clear: %s" % self.mock_assert_clear.call_args_list)
117 self.mock_clear_holders.assert_called_with(part_kname)
118 self.mock_assert_clear.assert_called_with(part_kname)

Subscribers

People subscribed via source and target branches