Merge lp:~raharper/curtin/trunk.debug-mdadm into lp:~curtin-dev/curtin/trunk
- trunk.debug-mdadm
- Merge into trunk
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 |
Related bugs: |
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.
Server Team CI bot (server-team-bot) wrote : | # |
Scott Moser (smoser) wrote : | # |
I think this looks good.
there are some small requests for enhancement inline.
Ryan Harper (raharper) wrote : | # |
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/
> > --- curtin/
> > +++ curtin/
> > @@ -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(
>
> 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_
> > + 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.
> > raise ValueError("%s: not an existing file or block device",
> path)
> >
> > + pt_names = []
> > if partitions and is_block:
> > ptdata = sysfs_partition
> > for kname, ptnum, start, size in ptdata:
> > - offsets.
> > - offsets.
> > + pt_names.
> > + # offsets.
> > + # offsets.
> > + 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_
> 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(
>
> which device ? perhaps this fail path could...
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_
> > + 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
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.
# 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)
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
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.
>
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".
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".
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:/
> mdadm/+merge/315805
> You are the owner of lp:~raharper/curtin/trunk.debug-mdadm.
>
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:/
> 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
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:444
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
close.
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/
> > --- curtin/
> > +++ curtin/
> > @@ -62,6 +63,25 @@
> > return os.path.
> 'cache'))
> >
> >
> > +def umount_
> > + """
> > + Attempt to unmount a device and any other devices which may be
> > + mounted on top of the devices mountpoint
> > + """
> > + proc_mounts = block.get_
> > + 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]
> + "/")]
>
> > + 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-
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(
> > + util.do_umount(mp)
> > +
> > +
> > def shutdown_
> > """
> > Shut down bcache for specified bcache device
>
>
> --
> https:/
> 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
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:448
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 449. By Ryan Harper
-
remove debugging prints in block.wipe_file
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.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:449
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
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).
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:/
> mdadm/+merge/315805
> You are the owner of lp:~raharper/curtin/trunk.debug-mdadm.
>
Preview Diff
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) |
PASSED: Continuous integration, rev:443 /jenkins. ubuntu. com/server/ job/curtin- ci/299/ /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= vm-amd64/ 299 /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= vm-i386/ 299 /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= vm-ppc64el/ 299 /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= vm-s390x/ 299
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/curtin- ci/299/ rebuild
https:/