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
1=== modified file 'curtin/block/__init__.py'
2--- curtin/block/__init__.py 2016-11-18 15:12:19 +0000
3+++ curtin/block/__init__.py 2017-02-01 15:10:46 +0000
4@@ -15,12 +15,14 @@
5 # You should have received a copy of the GNU Affero General Public License
6 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
7
8+from contextlib import contextmanager
9 import errno
10+import itertools
11 import os
12+import shlex
13 import stat
14-import shlex
15+import sys
16 import tempfile
17-import itertools
18
19 from curtin import util
20 from curtin.block import lvm
21@@ -165,6 +167,18 @@
22 return os.path.normpath(path)
23
24
25+def get_holders(device):
26+ """
27+ Look up any block device holders, return list of knames
28+ """
29+ # block.sys_block_path works when given a /sys or /dev path
30+ sysfs_path = sys_block_path(device)
31+ # get holders
32+ holders = os.listdir(os.path.join(sysfs_path, 'holders'))
33+ LOG.debug("devname '%s' had holders: %s", device, holders)
34+ return holders
35+
36+
37 def _lsblock_pairs_to_dict(lines):
38 """
39 parse lsblock output and convert to dict
40@@ -723,6 +737,40 @@
41 check_dos_signature(device))
42
43
44+@contextmanager
45+def exclusive_open(path):
46+ """
47+ Obtain an exclusive file-handle to the file/device specified
48+ """
49+ print('exclusive_open running')
50+ mode = 'rb+'
51+ fd = None
52+ if not os.path.exists(path):
53+ raise ValueError("No such file at path: %s" % path)
54+
55+ try:
56+ fd = os.open(path, os.O_RDWR | os.O_EXCL)
57+ try:
58+ fd_needs_closing = True
59+ with os.fdopen(fd, mode) as fo:
60+ yield fo
61+ fd_needs_closing = False
62+ except OSError:
63+ LOG.exception("Failed to create file-object from fd")
64+ raise
65+ finally:
66+ # python2 leaves fd open if there os.fdopen fails
67+ if fd_needs_closing and sys.version_info.major == 2:
68+ os.close(fd)
69+ except OSError:
70+ LOG.exception("Failed to exclusively open path: %s", path)
71+ holders = get_holders(path)
72+ LOG.error('Device holders with exclusive access: %s', holders)
73+ mount_points = util.list_device_mounts(path)
74+ LOG.error('Device mounts: %s', mount_points)
75+ raise
76+
77+
78 def wipe_file(path, reader=None, buflen=4 * 1024 * 1024):
79 """
80 wipe the existing file at path.
81@@ -742,7 +790,7 @@
82 LOG.debug("%s is %s bytes. wiping with buflen=%s",
83 path, size, buflen)
84
85- with open(path, "rb+") as fp:
86+ with exclusive_open(path) as fp:
87 while True:
88 pbuf = readfunc(buflen)
89 pos = fp.tell()
90@@ -772,11 +820,17 @@
91 if not (is_block or os.path.isfile(path)):
92 raise ValueError("%s: not an existing file or block device", path)
93
94+ pt_names = []
95 if partitions and is_block:
96 ptdata = sysfs_partition_data(path)
97 for kname, ptnum, start, size in ptdata:
98- offsets.append(start)
99- offsets.append(start + size - zero_size)
100+ pt_names.append((dev_path(kname), kname, ptnum))
101+ pt_names.reverse()
102+
103+ for (pt, kname, ptnum) in pt_names:
104+ LOG.debug('Wiping path: dev:%s kname:%s partnum:%s',
105+ pt, kname, ptnum)
106+ quick_zero(pt, partitions=False)
107
108 LOG.debug("wiping 1M on %s at offsets %s", path, offsets)
109 return zero_file_at_offsets(path, offsets, buflen=buflen, count=count)
110@@ -796,7 +850,8 @@
111 buf = b'\0' * buflen
112 tot = buflen * count
113 msg_vals = {'path': path, 'tot': buflen * count}
114- with open(path, "rb+") as fp:
115+
116+ with exclusive_open(path) as fp:
117 # get the size by seeking to end.
118 fp.seek(0, 2)
119 size = fp.tell()
120
121=== modified file 'curtin/block/clear_holders.py'
122--- curtin/block/clear_holders.py 2016-08-30 21:56:13 +0000
123+++ curtin/block/clear_holders.py 2017-02-01 15:10:46 +0000
124@@ -25,6 +25,7 @@
125
126 from curtin import (block, udev, util)
127 from curtin.block import lvm
128+from curtin.block import mdadm
129 from curtin.log import LOG
130
131
132@@ -118,8 +119,8 @@
133 """
134 blockdev = block.sysfs_to_devpath(device)
135 LOG.debug('using mdadm.mdadm_stop on dev: %s', blockdev)
136- block.mdadm.mdadm_stop(blockdev)
137- block.mdadm.mdadm_remove(blockdev)
138+ mdadm.mdadm_stop(blockdev)
139+ mdadm.mdadm_remove(blockdev)
140
141
142 def wipe_superblock(device):
143@@ -369,7 +370,7 @@
144 # but there was not enough to start the array, the call to wipe_volume on
145 # all disks and partitions should be sufficient to remove the mdadm
146 # metadata
147- block.mdadm.mdadm_assemble(scan=True, ignore_errors=True)
148+ mdadm.mdadm_assemble(scan=True, ignore_errors=True)
149 # the bcache module needs to be present to properly detect bcache devs
150 # on some systems (precise without hwe kernel) it may not be possible to
151 # lad the bcache module bcause it is not present in the kernel. if this
152
153=== modified file 'curtin/block/mdadm.py'
154--- curtin/block/mdadm.py 2016-08-30 21:56:13 +0000
155+++ curtin/block/mdadm.py 2017-02-01 15:10:46 +0000
156@@ -28,6 +28,7 @@
157 from subprocess import CalledProcessError
158
159 from curtin.block import (dev_short, dev_path, is_valid_device, sys_block_path)
160+from curtin.block import get_holders
161 from curtin import (util, udev)
162 from curtin.log import LOG
163
164@@ -138,7 +139,12 @@
165 # mdadm assemble returns 2 when called on an array that is already
166 # assembled. this is not an error, so accept return code of 2
167 # all other return codes can be accepted with ignore_error set to true
168- util.subp(cmd, capture=True, rcs=[0, 1, 2])
169+ scan, err = util.subp(cmd, capture=True, rcs=[0, 1, 2])
170+ LOG.debug('mdadm assemble scan results:\n%s\n%s', scan, err)
171+ scan, err = util.subp(['mdadm', '--detail', '--scan', '-v'],
172+ capture=True, rcs=[0, 1])
173+ LOG.debug('mdadm detail scan after assemble:\n%s\n%s',
174+ scan, err)
175 except util.ProcessExecutionError:
176 LOG.warning("mdadm_assemble had unexpected return code")
177 if not ignore_errors:
178@@ -177,6 +183,11 @@
179 cmd.append("--name=%s" % md_name)
180
181 for device in devices:
182+ # mdadm can be sticky, double check
183+ holders = get_holders(device)
184+ if len(holders) > 0:
185+ LOG.warning('Detected holders during mdadm creation: %s', holders)
186+ raise OSError('Failed to remove holders from %s', device)
187 # Zero out device superblock just in case device has been used for raid
188 # before, as this will cause many issues
189 util.subp(["mdadm", "--zero-superblock", device], capture=True)
190@@ -206,7 +217,12 @@
191 LOG.debug('available md modules: \n%s' % out)
192 else:
193 LOG.debug('no available md modules found')
194+
195+ for dev in devices + spares:
196+ h = get_holders(dev)
197+ LOG.debug('Device %s has holders: %s', dev, h)
198 raise
199+
200 util.subp(["udevadm", "control", "--start-exec-queue"])
201 util.subp(["udevadm", "settle",
202 "--exit-if-exists=%s" % md_devname])
203@@ -241,14 +257,17 @@
204 assert_valid_devpath(devpath)
205
206 LOG.info("mdadm stopping: %s" % devpath)
207- util.subp(["mdadm", "--stop", devpath], rcs=[0, 1], capture=True)
208+ out, err = util.subp(["mdadm", "--stop", devpath], capture=True)
209+ LOG.debug("mdadm stop:\n%s\n%s", out, err)
210
211
212 def mdadm_remove(devpath):
213 assert_valid_devpath(devpath)
214
215 LOG.info("mdadm removing: %s" % devpath)
216- util.subp(["mdadm", "--remove", devpath], rcs=[0, 1], capture=True)
217+ out, err = util.subp(["mdadm", "--remove", devpath],
218+ rcs=[0], capture=True)
219+ LOG.debug("mdadm remove:\n%s\n%s", out, err)
220
221
222 def mdadm_query_detail(md_devname, export=MDADM_USE_EXPORT):
223
224=== modified file 'curtin/commands/block_meta.py'
225--- curtin/commands/block_meta.py 2016-12-02 02:01:20 +0000
226+++ curtin/commands/block_meta.py 2017-02-01 15:10:46 +0000
227@@ -367,6 +367,11 @@
228 util.subp(["parted", disk, "--script", "mklabel", "msdos"])
229 else:
230 raise ValueError('invalid partition table type: %s', ptable)
231+ holders = clear_holders.get_holders(disk)
232+ if len(holders) > 0:
233+ LOG.info('Detected block holders on disk %s: %s', disk, holders)
234+ clear_holders.clear_holders(disk)
235+ clear_holders.assert_clear(disk)
236
237 # Make the name if needed
238 if info.get('name'):
239@@ -538,6 +543,15 @@
240 else:
241 raise ValueError("parent partition has invalid partition table")
242
243+ # check if we've triggered hidden metadata like md, lvm or bcache
244+ part_kname = get_path_to_storage_volume(info.get('id'), storage_config)
245+ holders = clear_holders.get_holders(part_kname)
246+ if len(holders) > 0:
247+ LOG.debug('Detected block holders on partition %s: %s', part_kname,
248+ holders)
249+ clear_holders.clear_holders(part_kname)
250+ clear_holders.assert_clear(part_kname)
251+
252 # Wipe the partition if told to do so, do not wipe dos extended partitions
253 # as this may damage the extended partition table
254 if config.value_as_boolean(info.get('wipe')):
255
256=== modified file 'curtin/util.py'
257--- curtin/util.py 2017-01-27 19:44:39 +0000
258+++ curtin/util.py 2017-02-01 15:10:46 +0000
259@@ -274,6 +274,19 @@
260 return False
261
262
263+def list_device_mounts(device):
264+ # return mount entry if device is in /proc/mounts
265+ mounts = ""
266+ with open("/proc/mounts", "r") as fp:
267+ mounts = fp.read()
268+
269+ dev_mounts = []
270+ for line in mounts.splitlines():
271+ if line.split()[0] == device:
272+ dev_mounts.append(line)
273+ return dev_mounts
274+
275+
276 def do_mount(src, target, opts=None):
277 # mount src at target with opts and return True
278 # if already mounted, return False
279
280=== modified file 'tests/unittests/test_block.py'
281--- tests/unittests/test_block.py 2016-11-18 15:12:19 +0000
282+++ tests/unittests/test_block.py 2017-02-01 15:10:46 +0000
283@@ -4,6 +4,7 @@
284 import mock
285 import tempfile
286 import shutil
287+import sys
288
289 from collections import OrderedDict
290
291@@ -252,6 +253,77 @@
292 found = util.load_file(trgfile)
293 self.assertEqual(data, found)
294
295+ def test_exclusive_open_raise_missing(self):
296+ myfile = self.tfile("no-such-file")
297+
298+ with self.assertRaises(ValueError):
299+ with block.exclusive_open(myfile) as fp:
300+ fp.close()
301+
302+ @mock.patch('os.close')
303+ @mock.patch('os.fdopen')
304+ @mock.patch('os.open')
305+ def test_exclusive_open(self, mock_os_open, mock_os_fdopen, mock_os_close):
306+ flen = 1024
307+ myfile = self.tfile("my_exclusive_file")
308+ util.write_file(myfile, flen * b'\1', omode="wb")
309+ mock_fd = 3
310+ mock_os_open.return_value = mock_fd
311+
312+ with block.exclusive_open(myfile) as fp:
313+ fp.close()
314+
315+ mock_os_open.assert_called_with(myfile, os.O_RDWR | os.O_EXCL)
316+ mock_os_fdopen.assert_called_with(mock_fd, 'rb+')
317+ self.assertEqual([], mock_os_close.call_args_list)
318+
319+ @mock.patch('os.close')
320+ @mock.patch('curtin.util.list_device_mounts')
321+ @mock.patch('curtin.block.get_holders')
322+ @mock.patch('os.open')
323+ def test_exclusive_open_non_exclusive_exception(self, mock_os_open,
324+ mock_holders,
325+ mock_list_mounts,
326+ mock_os_close):
327+ flen = 1024
328+ myfile = self.tfile("my_exclusive_file")
329+ util.write_file(myfile, flen * b'\1', omode="wb")
330+ mock_os_open.side_effect = OSError("NO_O_EXCL")
331+ mock_holders.return_value = ['md1']
332+ mock_list_mounts.return_value = []
333+
334+ with self.assertRaises(OSError):
335+ with block.exclusive_open(myfile) as fp:
336+ fp.close()
337+
338+ mock_os_open.assert_called_with(myfile, os.O_RDWR | os.O_EXCL)
339+ mock_holders.assert_called_with(myfile)
340+ mock_list_mounts.assert_called_with(myfile)
341+ self.assertEqual([], mock_os_close.call_args_list)
342+
343+ @mock.patch('os.close')
344+ @mock.patch('os.fdopen')
345+ @mock.patch('os.open')
346+ def test_exclusive_open_fdopen_failure(self, mock_os_open,
347+ mock_os_fdopen, mock_os_close):
348+ flen = 1024
349+ myfile = self.tfile("my_exclusive_file")
350+ util.write_file(myfile, flen * b'\1', omode="wb")
351+ mock_fd = 3
352+ mock_os_open.return_value = mock_fd
353+ mock_os_fdopen.side_effect = OSError("EBADF")
354+
355+ with self.assertRaises(OSError):
356+ with block.exclusive_open(myfile) as fp:
357+ fp.close()
358+
359+ mock_os_open.assert_called_with(myfile, os.O_RDWR | os.O_EXCL)
360+ mock_os_fdopen.assert_called_with(mock_fd, 'rb+')
361+ if sys.version_info.major == 2:
362+ mock_os_close.assert_called_with(mock_fd)
363+ else:
364+ self.assertEqual([], mock_os_close.call_args_list)
365+
366
367 class TestWipeVolume(TestCase):
368 dev = '/dev/null'
369
370=== modified file 'tests/unittests/test_block_mdadm.py'
371--- tests/unittests/test_block_mdadm.py 2016-09-14 19:52:06 +0000
372+++ tests/unittests/test_block_mdadm.py 2017-02-01 15:10:46 +0000
373@@ -34,17 +34,26 @@
374
375 def test_mdadm_assemble_scan(self):
376 mdadm.mdadm_assemble(scan=True)
377- self.mock_util.subp.assert_called_with(
378- ["mdadm", "--assemble", "--scan", "-v"], capture=True,
379- rcs=[0, 1, 2])
380+ assemble_calls = [
381+ call(["mdadm", "--assemble", "--scan", "-v"], capture=True,
382+ rcs=[0, 1, 2]),
383+ call(["mdadm", "--detail", "--scan", "-v"], capture=True,
384+ rcs=[0, 1]),
385+ ]
386+ self.mock_util.subp.assert_has_calls(assemble_calls)
387 self.assertTrue(self.mock_udev.udevadm_settle.called)
388
389 def test_mdadm_assemble_md_devname(self):
390 md_devname = "/dev/md0"
391 mdadm.mdadm_assemble(md_devname=md_devname)
392- self.mock_util.subp.assert_called_with(
393- ["mdadm", "--assemble", md_devname, "--run"], capture=True,
394- rcs=[0, 1, 2])
395+
396+ assemble_calls = [
397+ call(["mdadm", "--assemble", md_devname, "--run"],
398+ capture=True, rcs=[0, 1, 2]),
399+ call(["mdadm", "--detail", "--scan", "-v"], capture=True,
400+ rcs=[0, 1]),
401+ ]
402+ self.mock_util.subp.assert_has_calls(assemble_calls)
403 self.assertTrue(self.mock_udev.udevadm_settle.called)
404
405 def test_mdadm_assemble_md_devname_short(self):
406@@ -61,9 +70,13 @@
407 md_devname = "/dev/md0"
408 devices = ["/dev/vdc1", "/dev/vdd1"]
409 mdadm.mdadm_assemble(md_devname=md_devname, devices=devices)
410- self.mock_util.subp.assert_called_with(
411- ["mdadm", "--assemble", md_devname, "--run"] + devices,
412- capture=True, rcs=[0, 1, 2])
413+ assemble_calls = [
414+ call(["mdadm", "--assemble", md_devname, "--run"] + devices,
415+ capture=True, rcs=[0, 1, 2]),
416+ call(["mdadm", "--detail", "--scan", "-v"], capture=True,
417+ rcs=[0, 1]),
418+ ]
419+ self.mock_util.subp.assert_has_calls(assemble_calls)
420 self.assertTrue(self.mock_udev.udevadm_settle.called)
421
422 def test_mdadm_assemble_exec_error(self):
423@@ -85,10 +98,12 @@
424 super(TestBlockMdadmCreate, self).setUp()
425 self.add_patch('curtin.block.mdadm.util', 'mock_util')
426 self.add_patch('curtin.block.mdadm.is_valid_device', 'mock_valid')
427+ self.add_patch('curtin.block.mdadm.get_holders', 'mock_holders')
428
429 # Common mock settings
430 self.mock_valid.return_value = True
431 self.mock_util.lsb_release.return_value = {'codename': 'precise'}
432+ self.mock_holders.return_value = []
433
434 def prepare_mock(self, md_devname, raidlevel, devices, spares):
435 side_effects = []
436@@ -333,7 +348,7 @@
437 device = "/dev/vdc"
438 mdadm.mdadm_stop(device)
439 expected_calls = [
440- call(["mdadm", "--stop", device], rcs=[0, 1], capture=True),
441+ call(["mdadm", "--stop", device], capture=True),
442 ]
443 self.mock_util.subp.assert_has_calls(expected_calls)
444
445@@ -359,7 +374,7 @@
446 device = "/dev/vdc"
447 mdadm.mdadm_remove(device)
448 expected_calls = [
449- call(["mdadm", "--remove", device], rcs=[0, 1], capture=True),
450+ call(["mdadm", "--remove", device], rcs=[0], capture=True),
451 ]
452 self.mock_util.subp.assert_has_calls(expected_calls)
453
454
455=== modified file 'tests/unittests/test_clear_holders.py'
456--- tests/unittests/test_clear_holders.py 2016-09-14 19:38:32 +0000
457+++ tests/unittests/test_clear_holders.py 2017-02-01 15:10:46 +0000
458@@ -172,13 +172,14 @@
459 ['cryptsetup', 'remove', self.test_blockdev], capture=True)
460
461 @mock.patch('curtin.block.clear_holders.LOG')
462+ @mock.patch('curtin.block.clear_holders.mdadm')
463 @mock.patch('curtin.block.clear_holders.block')
464- def test_shutdown_mdadm(self, mock_block, mock_log):
465+ def test_shutdown_mdadm(self, mock_block, mock_mdadm, mock_log):
466 """test clear_holders.shutdown_mdadm"""
467 mock_block.sysfs_to_devpath.return_value = self.test_blockdev
468 clear_holders.shutdown_mdadm(self.test_syspath)
469- mock_block.mdadm.mdadm_stop.assert_called_with(self.test_blockdev)
470- mock_block.mdadm.mdadm_remove.assert_called_with(self.test_blockdev)
471+ mock_mdadm.mdadm_stop.assert_called_with(self.test_blockdev)
472+ mock_mdadm.mdadm_remove.assert_called_with(self.test_blockdev)
473 self.assertTrue(mock_log.debug.called)
474
475 @mock.patch('curtin.block.clear_holders.LOG')
476@@ -320,7 +321,7 @@
477 mock_gen_holders_tree.return_value = self.example_holders_trees[1][1]
478 clear_holders.assert_clear(device)
479
480- @mock.patch('curtin.block.clear_holders.block.mdadm')
481+ @mock.patch('curtin.block.clear_holders.mdadm')
482 @mock.patch('curtin.block.clear_holders.util')
483 def test_start_clear_holders_deps(self, mock_util, mock_mdadm):
484 clear_holders.start_clear_holders_deps()
485
486=== added file 'tests/unittests/test_commands_block_meta.py'
487--- tests/unittests/test_commands_block_meta.py 1970-01-01 00:00:00 +0000
488+++ tests/unittests/test_commands_block_meta.py 2017-02-01 15:10:46 +0000
489@@ -0,0 +1,118 @@
490+from unittest import TestCase
491+from mock import patch
492+
493+from curtin.commands import block_meta
494+
495+
496+class BlockMetaTestBase(TestCase):
497+ def setUp(self):
498+ super(BlockMetaTestBase, self).setUp()
499+
500+ def add_patch(self, target, attr):
501+ """Patches specified target object and sets it as attr on test
502+ instance also schedules cleanup"""
503+ m = patch(target, autospec=True)
504+ p = m.start()
505+ self.addCleanup(m.stop)
506+ setattr(self, attr, p)
507+
508+
509+class TestBlockMeta(BlockMetaTestBase):
510+ def setUp(self):
511+ super(TestBlockMeta, self).setUp()
512+ # self.target = tempfile.mkdtemp()
513+
514+ basepath = 'curtin.commands.block_meta.'
515+ self.add_patch(basepath + 'get_path_to_storage_volume', 'mock_getpath')
516+ self.add_patch(basepath + 'make_dname', 'mock_make_dname')
517+ self.add_patch('curtin.util.subp', 'mock_subp')
518+ self.add_patch('curtin.block.get_part_table_type',
519+ 'mock_block_get_part_table_type')
520+ self.add_patch('curtin.block.wipe_volume',
521+ 'mock_block_wipe_volume')
522+ self.add_patch('curtin.block.path_to_kname',
523+ 'mock_block_path_to_kname')
524+ self.add_patch('curtin.block.sys_block_path',
525+ 'mock_block_sys_block_path')
526+ self.add_patch('curtin.block.clear_holders.get_holders',
527+ 'mock_get_holders')
528+ self.add_patch('curtin.block.clear_holders.clear_holders',
529+ 'mock_clear_holders')
530+ self.add_patch('curtin.block.clear_holders.assert_clear',
531+ 'mock_assert_clear')
532+
533+ self.target = "my_target"
534+ self.config = {
535+ 'storage': {
536+ 'version': 1,
537+ 'config': [
538+ {'grub_device': True,
539+ 'id': 'sda',
540+ 'name': 'sda',
541+ 'path': '/wark/xxx',
542+ 'ptable': 'msdos',
543+ 'type': 'disk',
544+ 'wipe': 'superblock'},
545+ {'device': 'sda',
546+ 'flag': 'boot',
547+ 'id': 'sda-part1',
548+ 'name': 'sda-part1',
549+ 'number': 1,
550+ 'offset': '4194304B',
551+ 'size': '511705088B',
552+ 'type': 'partition',
553+ 'uuid': 'fc7ab24c-b6bf-460f-8446-d3ac362c0625',
554+ 'wipe': 'superblock'}
555+ ],
556+ }
557+ }
558+ self.storage_config = (
559+ block_meta.extract_storage_ordered_dict(self.config))
560+
561+ def test_disk_handler_calls_clear_holder(self):
562+ info = self.storage_config.get('sda')
563+ disk = info.get('path')
564+ self.mock_getpath.return_value = disk
565+ self.mock_block_get_part_table_type.return_value = 'dos'
566+ self.mock_subp.side_effect = iter([
567+ (0, 0), # parted mklabel
568+ ])
569+ holders = ['md1']
570+ self.mock_get_holders.return_value = holders
571+
572+ block_meta.disk_handler(info, self.storage_config)
573+
574+ print("clear_holders: %s" % self.mock_clear_holders.call_args_list)
575+ print("assert_clear: %s" % self.mock_assert_clear.call_args_list)
576+ self.mock_clear_holders.assert_called_with(disk)
577+ self.mock_assert_clear.assert_called_with(disk)
578+
579+ def test_partition_handler_calls_clear_holder(self):
580+ disk_info = self.storage_config.get('sda')
581+ part_info = self.storage_config.get('sda-part1')
582+ disk_kname = disk_info.get('path')
583+ part_kname = disk_kname + '1'
584+ self.mock_getpath.side_effect = iter([
585+ disk_info.get('id'),
586+ part_kname,
587+ part_kname,
588+ part_kname,
589+ ])
590+
591+ self.mock_block_get_part_table_type.return_value = 'dos'
592+ kname = 'xxx'
593+ self.mock_block_path_to_kname.return_value = kname
594+ self.mock_block_sys_block_path.return_value = '/sys/class/block/xxx'
595+ self.mock_subp.side_effect = iter([
596+ ("", 0), # parted mkpart
597+ ("", 0), # ??
598+ ])
599+ holders = ['md1']
600+ self.mock_get_holders.return_value = holders
601+
602+ block_meta.partition_handler(part_info, self.storage_config)
603+
604+ print("clear_holders: %s" % self.mock_clear_holders.call_args_list)
605+ print("assert_clear: %s" % self.mock_assert_clear.call_args_list)
606+ self.mock_clear_holders.assert_called_with(part_kname)
607+ self.mock_assert_clear.assert_called_with(part_kname)

Subscribers

People subscribed via source and target branches