-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add find unused disk module and its unit tests #5
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good overall. You should probably run pylint
on it (pylint is a command and is also the name of the package that provides that command in rhel/fedora). Feel free to ignore errors/warnings for too-long lines.
library/find_unused_disk_module.py
Outdated
|
||
# Ensure that no known signatures exist on the disk, with the exception of | ||
# partition tables. (blkid -p <device>) | ||
def no_signature(run_command, disk): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In python the function-level documentation is called a docstring, and would look something like this:
def no_signature(run_command, disk):
""" Return True if the disk contains no known signatures/metadata. """
...
https://www.python.org/dev/peps/pep-0257/#one-line-docstrings gives a good overview of how docstrings should look for simple functions.
library/find_unused_disk_module.py
Outdated
ansible_facts = facts.ansible_facts(module) | ||
run_command = module.run_command | ||
all_disks = run_command(['lsblk', '-o', 'kname', '-l', '-n', '-p']) | ||
for disk in all_disks[1].split(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you moved this top-level logic for identifying unused disks into a separate function you could easily cover it with a unit test.
library/find_unused_disk_module.py
Outdated
DOCUMENTATION = ''' | ||
--- | ||
module: find_unused_disk_module | ||
short_description: Get first unused disk |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably need to return a list of unused disks rather than just one. Maybe return up to ten by default with an optional argument to set a max number of disks to return?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good! I'll work on that
library/find_unused_disk_module.py
Outdated
# Ensure that the disk has no holders to eliminate the possibility of it being a | ||
# multipath or dmraid member device. (/sys/class/block/<device>/holders/ empty) | ||
def no_holders(run_command, disk_name): | ||
holders = run_command(['ls', '/sys/class/block/' + disk_name + '/holders/']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use os.listdir
[1] instead of `ls' here. In general, https://docs.python.org is your friend.
library/find_unused_disk_module.py
Outdated
def can_open(open, close, disk): | ||
try: | ||
exclusive = open(disk, os.O_EXCL) | ||
close(exclusive) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure that when exclusive goes out of scope (ie: when the function returns) it will be automatically closed.
library/find_unused_disk_module.py
Outdated
for disk in all_disks[1].split(): | ||
disk_name = os.path.basename(disk) | ||
# Ensure that, if there is a partition table on the disk, it contains no partitions. | ||
try: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A more typical way to do this would be something like this:
if (not ansible_facts['devices'][disk_name]['partitions'] and
no_signature(run_command, disk) and
no_holders(run_command, disk_name) and
can_open(os.open, os.close, disk)):
result['disk'] = disk
module.exit_json(**result)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made it a try-except because some devices won't have a 'partitions' variable and the command ansible_facts['devices'][disk_name]['partitions'] will fail with some form of string not found exception. Although I agree that it is messier...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case maybe do something like this:
try:
has_partitions = ansible_facts['devices'][disk_name]['partitions']
except Exception:
has_partitions = False
if (not has_partitions and
no_signature(run_command, disk) and
...
It's good practice to isolate only the code that directly causes and handles the exception within the try/except blocks. It makes the flow clearer, among other things. Ideally, find out what exceptions you expect it to raise and only catch those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bah, that isn't quite right. In the try
you'd have to coerce that to a bool
, so something like:
has_partitions = (ansible_facts[...] == dict())
or something along those lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better yet, just do it explicitly: has_partitions = bool(ansible_facts[...])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's really smart, done!
When running pylint, I get warnings about putting the Imports at the top of the module. However, since Ansible documentation clearly states otherwise, I am disregarding them as well as the missing docstring at beginning of file error since the module is explained according to Ansible. (https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html) |
All done making edits for first round of comments, let me know how it looks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many of these things could have been pointed out in the last review -- sorry for that.
It looks good other than these few little things.
library/find_unused_disk_module.py
Outdated
has_partitions = False | ||
# Check if partition table contains no partitions. | ||
if has_partitions and len(ansible_facts['devices'][disk_name]['partitions']) == 0: | ||
has_partitions = False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The has_partitions
bool
should already be False
for an empty dict or list, so this shouldn't be necessary.
library/find_unused_disk_module.py
Outdated
|
||
def get_disks(run_command): | ||
"""Return list of devices on the system""" | ||
all_disks = run_command(['lsblk', '-o', 'kname', '-l', '-n', '-p']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, sorry for this -- I should have noticed it the first time through -- but there are other, better ways to do this.
For one thing, that command will include partitions, which are necessarily not disks.
You could get the exact same list by running os.listdir('/sys/class/block')
; it's better in that you're using a system call instead of running an executable, but you'll still have the partitions. You could avoid that by instead listing '/sys/block' (Yes, this one contains a subset of the former. Yes, that's a bit goofy.)
Another option that would eliminate the partitions is to use ansible_facts['devices'].keys()
. It also omits the partitions (it hides them under the disks that contain them). Again, this is better than running an executable since you're just looking at data that's already been gathered.
There might be more work to do here since it's not always trivial to figure out what is and is not a "disk". That can be addressed later, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I use the complete device paths for some of the other commands (os.open, blkid), but the commands that returns only disks (that you suggested) list them by just name. Would it be wrong to assume their location? For example, if ansible_facts['devices'].keys()
returns sda and dm-0, would it be ok to assume they are located at /dev/sda and /dev/dm-0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is safe to assume that they will all be in /dev. The ansible facts get the list of devices by looking at /sys/block, which lists "kernel" devices (always in /dev) -- see [1].
library/find_unused_disk_module.py
Outdated
if not has_partitions and no_signature(run_command, disk) and no_holders(disk_name) and can_open(disk): | ||
result['disk'].append(disk) | ||
disk_count = disk_count + 1 | ||
if disk_count >= module.params['max_return']: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could eliminate disk_count
and instead check len(result['disk'])
.
tests/unit/unused_disk.py
Outdated
def test_can_open_false(monkeypatch): | ||
def mock_return(args, flag): | ||
raise OSError | ||
return -1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This (return -1
) will never run since you are raising an exception beforehand.
See for in depth description: https://gist.github.com/dwlehman/eceec2fb443408080b70d4fe2ea70459