Skip to content
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

Merged
merged 1 commit into from
Jul 3, 2018

Conversation

edamamez
Copy link

@edamamez edamamez commented Jun 21, 2018

Copy link
Owner

@dwlehman dwlehman left a 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.


# Ensure that no known signatures exist on the disk, with the exception of
# partition tables. (blkid -p <device>)
def no_signature(run_command, disk):
Copy link
Owner

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.

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():
Copy link
Owner

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.

DOCUMENTATION = '''
---
module: find_unused_disk_module
short_description: Get first unused disk
Copy link
Owner

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?

Copy link
Author

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

# 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/'])
Copy link
Owner

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.

[1] https://docs.python.org/3/library/os.html#os.listdir

def can_open(open, close, disk):
try:
exclusive = open(disk, os.O_EXCL)
close(exclusive)
Copy link
Owner

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.

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:
Copy link
Owner

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)

Copy link
Author

@edamamez edamamez Jun 27, 2018

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...

Copy link
Owner

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.

Copy link
Owner

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.

Copy link
Owner

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[...])

Copy link
Author

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!

@edamamez
Copy link
Author

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)

@edamamez
Copy link
Author

All done making edits for first round of comments, let me know how it looks!

Copy link
Owner

@dwlehman dwlehman left a 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.

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
Copy link
Owner

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.


def get_disks(run_command):
"""Return list of devices on the system"""
all_disks = run_command(['lsblk', '-o', 'kname', '-l', '-n', '-p'])
Copy link
Owner

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.

Copy link
Author

@edamamez edamamez Jul 3, 2018

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?

Copy link
Owner

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].

[1] https://github.com/ansible/ansible/blob/0691c9c099d56929321412df1f5842423a2dfdc0/lib/ansible/module_utils/facts/hardware/linux.py#L548

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']:
Copy link
Owner

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']).

def test_can_open_false(monkeypatch):
def mock_return(args, flag):
raise OSError
return -1
Copy link
Owner

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants