diff --git a/.gitignore b/.gitignore index eb56fde..7a5e1bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,11 @@ -test-scripts/ -*.cache/ -*.egg-info/ -*.eggs/ - -*.pyc -*.pdf -*.cache -*.cre.js -*.default.js -*.settings.js - -dist/ +.eggs/ +.idea/workspace.xml +.idea/tasks.xml +.idea/libraries/ +.idea/dictionaries/ +.cache/ +*.iml +*.egg-info build/ - -.installed.cfg -.DS_Store - -cr8000.js -translate.xml +dist/ +**/__pycache__/ diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..6374de0 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9ab5580 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 4b16683..9f8f978 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # PyBar -Generate asset QR codes for a physical inventory procedure +Generate a QR code for a physical inventory procedure based on an individual asset's hardware specifications ### Install @@ -8,4 +8,4 @@ Generate asset QR codes for a physical inventory procedure ### Test -`python3 setup.py pytest` +`python3 setup.py test` diff --git a/pybar/asset.py b/pybar/asset.py index 9913fdc..380d830 100644 --- a/pybar/asset.py +++ b/pybar/asset.py @@ -1,7 +1,27 @@ -class Asset: - """The current machine and it's associated specs""" - def __init__(self): - pass +class Asset(object): + """ + Represents the object to be entered into a Snipe-IT inventory database. - def is_valid(self): + An Asset object is built from a SystemProfile object and it's + attributes, which is then used to assemble the QR code. + """ + + def __init__(self, systemprofile): + """ + Instantiates an Asset object with several attributes, + all which can be used to build a QR code. + """ + self.systemprofile = systemprofile + self.cpu_name = systemprofile.cpu_name + self.cpu_processors = systemprofile.cpu_processors + self.cpu_speed = systemprofile.cpu_speed + self.cpu_cores = systemprofile.cpu_cores + self.memory = systemprofile.memory + self.serial = systemprofile.serial + self.model = systemprofile.model + self.name = systemprofile.name + + @staticmethod + def is_valid(): + """TODO""" return None diff --git a/pybar/diskutil.py b/pybar/diskutil.py new file mode 100644 index 0000000..bc3303b --- /dev/null +++ b/pybar/diskutil.py @@ -0,0 +1,27 @@ +import re +import shlex +import subprocess + +BASE_COMMAND = '/usr/sbin/diskutil' + + +def get_physical_disk_identifiers(diskutil_list_output=None): + diskutil_list_output = diskutil_list_output or list_all() + physical_disk_id_pattern = re.compile(r'(/dev/disk\d+) \(\w+, physical\).*') + + return re.findall(physical_disk_id_pattern, diskutil_list_output) + + +def get_disk_info(disk_identifier): + return _get_output_of_diskutil_command(arguments=f'info {disk_identifier}') + + +def list_all(): + return _get_output_of_diskutil_command(arguments='list') + + +def _get_output_of_diskutil_command(arguments=None): + arguments = arguments or '' + full_command = shlex.split(' '.join([BASE_COMMAND, arguments])) + + return subprocess.check_output(full_command).decode('utf-8') diff --git a/pybar/gui.py b/pybar/gui.py new file mode 100644 index 0000000..7be6d98 --- /dev/null +++ b/pybar/gui.py @@ -0,0 +1,18 @@ +import tkinter as tk + +win = tk.Tk() +win.title("PyBar") +# tk.Label()(win, text="Label").grid(column=0, row=0) +label = tk.Label(win, text="Hello") +label.grid(column=0, row=0) + + +def click(): + action.configure() + label.configure(foreground="red") + + +action = tk.Button(win, text="Generate QR Code", command=click) +action.grid(column=1, row=0) + +win.mainloop() diff --git a/pybar/instructions.py b/pybar/instructions.py new file mode 100644 index 0000000..accb3be --- /dev/null +++ b/pybar/instructions.py @@ -0,0 +1,6 @@ +class Instructions: + """Create Instructions object""" + + def __init__(self): + """TODO""" + pass diff --git a/pybar/ios.py b/pybar/ios.py deleted file mode 100644 index e69de29..0000000 diff --git a/pybar/macdisk.py b/pybar/macdisk.py new file mode 100644 index 0000000..fa1156e --- /dev/null +++ b/pybar/macdisk.py @@ -0,0 +1,52 @@ +import re +import yaml +from pybar import diskutil + + +def create_from_diskutil_info_output(output): + return Disk(yaml.load(output)) + + +def get_all_physical_disks(): + return [ + create_from_diskutil_info_output(diskutil.get_disk_info(disk_identifier)) + for disk_identifier in diskutil.get_physical_disk_identifiers() + ] + + +class Disk: + + def __init__(self, attributes=None): + self.attributes = attributes or {} + + @property + def device_location(self): + return self.attributes.get('Device Location') + + @property + def is_internal(self): + return self.device_location == 'Internal' + + @property + def is_external(self): + return self.device_location == 'External' + + @property + def device_name(self): + return self.attributes.get('Device / Media Name') + + @property + def is_ssd(self): + return self.attributes.get('Solid State') + + @property + def verbose_disk_size(self): + return self.attributes.get('Disk Size') or self.attributes.get('Total Size') + + @property + def size(self): + disk_size_pattern = re.compile(r'(?P\d+\.?\d* [MGT]?B) .*$') + disk_size_match = re.match(disk_size_pattern, self.verbose_disk_size) + + if disk_size_match: + return disk_size_match.group('disk_size') diff --git a/pybar/osx.py b/pybar/osx.py deleted file mode 100644 index e69de29..0000000 diff --git a/pybar/qr_builder.py b/pybar/qr_builder.py index f9cd951..c838792 100644 --- a/pybar/qr_builder.py +++ b/pybar/qr_builder.py @@ -1,6 +1,9 @@ -import instructions +from qrcode import QRCode -fieldset = {'trashcan': instructions.trashcan} -for fieldset in fieldsets: - pass +class AssetQRCode(QRCode): + """TODO""" + + def __init__(self): + """TODO""" + pass diff --git a/pybar/system_profile.py b/pybar/system_profile.py deleted file mode 100644 index 71e4e75..0000000 --- a/pybar/system_profile.py +++ /dev/null @@ -1,3 +0,0 @@ -class SystemProfile: - def __init__(self): - pass diff --git a/pybar/systemprofile.py b/pybar/systemprofile.py new file mode 100644 index 0000000..fc97918 --- /dev/null +++ b/pybar/systemprofile.py @@ -0,0 +1,116 @@ +import platform +import subprocess +import yaml + + +class SystemProfile(object): + """Represents the machine's "system profile" before it is parsed and + converted into an Asset object. + + A SystemProfile object should be able to be used to access several system + profile specs, even if they are not used by the Asset class. + + A SystemProfile object should also be able to be used the same way, + regardless of which operating system the specs were generated from""" + + def __init__(self): + """TODO""" + self.os_type = platform.system() + + def operating_system(self): + if self.os_type == 'Darwin': + mac_hardware() + elif self.os_type == 'Windows': + windows() + else: + raise OSError( + '{os}: Unknown operating system'.format(os=self.os_type)) + + def storage(self): + pass + + @property + def serial(self): + serial = mac_hardware().get('Serial Number (system)') + assert isinstance(serial, str) + return serial + + @property + def cpu_name(self): + name = mac_hardware().get('Processor Name') + assert isinstance(name, str) + return name + + @property + def cpu_processors(self): + processors = mac_hardware().get('Number of Processors') + assert isinstance(processors, int) + return processors + + @property + def cpu_cores(self): + cores = mac_hardware().get('Total Number of Cores') + assert isinstance(cores, int) + return cores + + @property + def cpu_speed(self): + speed = mac_hardware().get('Processor Speed') + assert isinstance(speed, str) + return speed + + @property + def memory(self): + memory = mac_hardware().get('Memory') + return memory + + @property + def model(self): + model = mac_hardware().get('Model Identifier') + return model + + @property + def name(self): + name = mac_hardware().get('Model Name') + return name + + +def _mac_system_profiler(data_type): + """ + This function is passed one of several strings that is then parsed using + the yaml module and can then be utilized in other mac_data_type functions. + + Used only for '/usr/sbin/system_profiler' argument 'SPHardwareDataType' + :param data_type: + :return: data + """ + command = [ + '/usr/sbin/system_profiler', 'SP' + str.title(data_type) + 'DataType'] + data = yaml.load(subprocess.check_output(command)) + return data + + +def mac_hardware(): + """ + This function is used as the primary means of obtaining basic Mac + hardware components. + """ + system_profiler_hardware = _mac_system_profiler('hardware') + hardware_components = system_profiler_hardware['Hardware']['Hardware Overview'] + return hardware_components + + +def mac_storage(): + """ + This function will be used as the primary means of obtaining data about + the a Mac's storage specifications. + """ + pass + + +def windows(): + """ + This function is used as the primary means of obtaining basic Windows + machine hardware components. + """ + pass diff --git a/pybar/windows.py b/pybar/windows.py deleted file mode 100644 index e69de29..0000000 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b7e4789 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/setup.py b/setup.py index c26a36d..465e5af 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,21 @@ -#!/usr/bin/env python - -from setuptools import setup +try: + from setuptools import setup +except ImportError: + from distutils.core import setup setup(name='PyBar', - version='0.0.1', + version='0.0.2', license='MIT', - description='Generate QRCodes for a Physical Inventory', + description='Generate QRCodes for a physical inventory', author='Eric Hanko', author_email='v-erhank@microsoft.com', packages=['pybar'], long_description=open('README.md').read(), - install_requires=["qrcode >= 5.3.0"], + install_requires=[ + "qrcode >= 5.3.0", + "PyYAML >= 3.12", + "pytest-runner", + "pytest"], setup_requires=['pytest-runner'], tests_require=['pytest'], ) diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/asset_test.py b/test/asset_test.py deleted file mode 100644 index 9dbaead..0000000 --- a/test/asset_test.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -from pybar.asset import Asset - -test_data = { - 'owner': 'Hanko', - 'serial': 'HZ1KF3L90', - 'cpu': ('Intel', 'Core i7', '2.9GHz') -} - - -class TestAsset: - def test_empty_asset_instantiation_works(self): - Asset() - - def test_empty_asset_is_not_valid(self): - asset = Asset() - assert not asset.is_valid() - - def test_asset_is_valid_with_known_good_test_data(self): - pass diff --git a/test/system_profile_test.py b/test/system_profile_test.py deleted file mode 100644 index 91ef63b..0000000 --- a/test/system_profile_test.py +++ /dev/null @@ -1,7 +0,0 @@ -import pytest -from pybar.system_profile import SystemProfile - - -class TestSystemProfile: - def test_empty_profile_instantiation_works(self): - SystemProfile() diff --git a/tests/test_asset_object.py b/tests/test_asset_object.py new file mode 100644 index 0000000..a3e986d --- /dev/null +++ b/tests/test_asset_object.py @@ -0,0 +1,88 @@ +import pytest +from pybar.asset import Asset +from pybar.systemprofile import SystemProfile + + +sp = SystemProfile() +asset = Asset(sp) + + +def test_empty_asset_instantiation_works(): + Asset(sp) + + +def test_empty_asset_is_not_valid(): + assert not asset.is_valid() + + +def test_presence_of_cpu_name_attribute(): + assert hasattr(asset, 'cpu_name') + + +def test_cpu_attribute_is_tuple(): + assert isinstance(asset.cpu_name, str) + + +def test_presence_of_cpu_speed_attribute(): + assert hasattr(asset, 'cpu_speed') + + +def test_cpu_speed_is_integer(): + assert isinstance(asset.cpu_speed, str) + + +def test_presence_of_cpu_processors_attribute(): + assert hasattr(asset, 'cpu_processors') + + +def test_cpu_processor_is_string(): + assert isinstance(asset.cpu_processors, int) + + +def test_presence_of_cpu_cores_attribute(): + assert hasattr(asset, 'cpu_cores') + + +def test_cpu_cores_is_integer(): + assert isinstance(asset.cpu_cores, int) + + +def test_presence_of_memory_attribute(): + assert hasattr(asset, 'memory') + + +def test_memory_attribute_is_string(): + assert isinstance(asset.memory, str) + + +def test_presence_of_serial_attribute(): + assert hasattr(asset, 'serial') + + +def test_serial_attribute_is_string(): + assert isinstance(asset.serial, str) + + +def test_presence_of_name_attribute(): + assert hasattr(asset, 'name') + + +def test_name_attribute_is_string(): + assert isinstance(asset.model, str) + + +def test_presence_of_model_attribute(): + assert hasattr(asset, 'model') + + +def test_model_attribute_is_string(): + assert isinstance(asset.model, str) + + +def test_serial_is_correct_length(): + assert len(asset.serial) == 12 + + +@pytest.mark.skip +def test_asset_is_valid_with_known_good_test_data(): + pass diff --git a/tests/test_getting_physical_disk_identifiers.py b/tests/test_getting_physical_disk_identifiers.py new file mode 100644 index 0000000..f26efea --- /dev/null +++ b/tests/test_getting_physical_disk_identifiers.py @@ -0,0 +1,58 @@ +from pybar import diskutil + +diskutil_list_output = '''/dev/disk0 (internal, physical): + #: TYPE NAME SIZE IDENTIFIER + 0: GUID_partition_scheme *751.3 GB disk0 + 1: EFI EFI 209.7 MB disk0s1 + 2: Apple_CoreStorage Macintosh HD 750.4 GB disk0s2 + 3: Apple_Boot Recovery HD 650.1 MB disk0s3 +/dev/disk1 (internal, virtual): + #: TYPE NAME SIZE IDENTIFIER + 0: Apple_HFS Macintosh HD +750.1 GB disk1 + Logical Volume on disk0s2 + 2F555A8B-D884-485F-985A-3B7ADF7BFCB5 + Unlocked Encrypted +/dev/disk2 (disk image): + #: TYPE NAME SIZE IDENTIFIER + 0: Apple_partition_scheme +19.8 MB disk2 + 1: Apple_partition_map 32.3 KB disk2s1 + 2: Apple_HFS Flash Player 19.7 MB disk2s2 +/dev/disk3 (disk image): + #: TYPE NAME SIZE IDENTIFIER + 0: GUID_partition_scheme +35.8 MB disk3 + 1: Apple_HFS Synergy 35.8 MB disk3s1 +/dev/disk4 (external, physical): + #: TYPE NAME SIZE IDENTIFIER + 0: FDisk_partition_scheme *2.0 TB disk4 + 1: Windows_NTFS My Passport 2.0 TB disk4s1 +/dev/disk5 (external, physical): + #: TYPE NAME SIZE IDENTIFIER + 0: GUID_partition_scheme *2.0 TB disk5 + 1: EFI EFI 209.7 MB disk5s1 + 2: Apple_HFS Builds 1.5 TB disk5s2 + 3: Apple_HFS Source 499.7 GB disk5s3 +/dev/disk6 (external, physical): + #: TYPE NAME SIZE IDENTIFIER + 0: GUID_partition_scheme *1.0 TB disk6 + 1: EFI EFI 209.7 MB disk6s1 + 2: Apple_RAID 999.9 GB disk6s2 + 3: Apple_Boot Boot OS X 134.2 MB disk6s3 +/dev/disk7 (external, physical): + #: TYPE NAME SIZE IDENTIFIER + 0: GUID_partition_scheme *1.0 TB disk7 + 1: EFI EFI 209.7 MB disk7s1 + 2: Apple_RAID 999.9 GB disk7s2 + 3: Apple_Boot Boot OS X 134.2 MB disk7s3 +/dev/disk8 (external, virtual): + #: TYPE NAME SIZE IDENTIFIER + 0: Apple_HFS RedBackup +2.0 TB disk8 + +''' + + +def test_only_physical_drives_included(): + expected_physical_disks = [ + '/dev/disk0', '/dev/disk4', '/dev/disk5', '/dev/disk6', '/dev/disk7'] + + assert expected_physical_disks == diskutil.get_physical_disk_identifiers( + diskutil_list_output) diff --git a/tests/test_qr_builder.py b/tests/test_qr_builder.py new file mode 100644 index 0000000..1af521d --- /dev/null +++ b/tests/test_qr_builder.py @@ -0,0 +1,10 @@ +from pybar.qr_builder import AssetQRCode + + +def test_empty_asset_qr_code_can_be_instantiated(): + AssetQRCode() + + +def test_asset_qr_code_as_attributes_of_inherited_class(): + qr = AssetQRCode() + assert hasattr(qr, 'add_data') diff --git a/tests/test_representing_external_hdd.py b/tests/test_representing_external_hdd.py new file mode 100644 index 0000000..02c3279 --- /dev/null +++ b/tests/test_representing_external_hdd.py @@ -0,0 +1,54 @@ +from pybar import macdisk + +diskutil_output = ''' Device Identifier: disk5 + Device Node: /dev/disk5 + Whole: Yes + Part of Whole: disk2 + Device / Media Name: G-DRIVE PRO Thunderbolt + + Volume Name: Not applicable (no file system) + Mounted: Not applicable (no file system) + File System: None + + Content (IOContent): GUID_partition_scheme + OS Can Be Installed: No + Media Type: Generic + Protocol: SATA + SMART Status: Verified + + Disk Size: 2.0 TB (2000179691520 Bytes) (exactly 3906600960 512-Byte-Units) + Device Block Size: 512 Bytes + + Read-Only Media: No + Read-Only Volume: Not applicable (no file system) + + Device Location: External + Removable Media: Fixed + + Solid State: No + Virtual: No + OS 9 Drivers: No + Low Level Format: Not supported +''' + +test_disk = macdisk.create_from_diskutil_info_output(diskutil_output) + + +def test_disk_is_not_internal(): + assert test_disk.is_internal is False + + +def test_disk_is_external(): + assert test_disk.is_external + + +def test_device_name_is_correct(): + assert test_disk.device_name == 'G-DRIVE PRO Thunderbolt' + + +def test_disk_is_not_ssd(): + assert test_disk.is_ssd is False + + +def test_size_is_correct(): + assert test_disk.size == '2.0 TB' diff --git a/tests/test_representing_internal_ssd.py b/tests/test_representing_internal_ssd.py new file mode 100644 index 0000000..d8c7389 --- /dev/null +++ b/tests/test_representing_internal_ssd.py @@ -0,0 +1,58 @@ +from pybar import macdisk + +diskutil_output = ''' Device Identifier: disk0 + Device Node: /dev/disk0 + Whole: Yes + Part of Whole: disk0 + Device / Media Name: APPLE SSD SM768E + + Volume Name: Not applicable (no file system) + + Mounted: Not applicable (no file system) + + File System: None + + Content (IOContent): GUID_partition_scheme + OS Can Be Installed: No + Media Type: Generic + Protocol: SATA + SMART Status: Verified + + Total Size: 751.3 GB (751277983744 Bytes) (exactly 1467339812 512-Byte-Units) + Volume Free Space: Not applicable (no file system) + Device Block Size: 512 Bytes + + Read-Only Media: No + Read-Only Volume: Not applicable (no file system) + + Device Location: Internal + Removable Media: No + + Solid State: Yes + Virtual: No + OS 9 Drivers: No + Low Level Format: Not supported + +''' + +test_disk = macdisk.create_from_diskutil_info_output(diskutil_output) + + +def test_disk_is_internal(): + assert test_disk.is_internal + + +def test_disk_is_not_external(): + assert test_disk.is_external is False + + +def test_device_name_is_correct(): + assert test_disk.device_name == 'APPLE SSD SM768E' + + +def test_disk_is_ssd(): + assert test_disk.is_ssd + + +def test_size_is_correct(): + assert test_disk.size == '751.3 GB' diff --git a/tests/test_systemprofile_object.py b/tests/test_systemprofile_object.py new file mode 100644 index 0000000..5ed7bae --- /dev/null +++ b/tests/test_systemprofile_object.py @@ -0,0 +1,120 @@ +import pytest +from pybar.systemprofile import SystemProfile, mac_hardware + +hardware_test_data_as_dict = { + 'Hardware': + { + 'Hardware Overview': + { + 'Model Name': 'MacBook Pro', + 'Model Identifier': 'MacBookPro11,2', + 'Processor Name': 'Intel Core i7', + 'Processor Speed': '2.2 GHz', + 'Number of Processors': 1, + 'Total Number of Cores': 4, + 'L2 Cache (per Core)': '256 KB', + 'L3 Cache': '6 MB', 'Memory': '16 GB', + 'Boot ROM Version': 'MBP112.0138.B21', + 'SMC Version (system)': '2.18f15', + 'Serial Number (system)': 'C02NT9WJG3QC', + 'Hardware UUID': '7BE2608D-6373-52C7-B5FB-442C261A71A4'}}} + +hardware_test_data_as_yaml = """ + Hardware: + + Hardware Overview: + + Model Name: Mac Pro + Model Identifier: MacPro6,1 + Processor Name: Quad-Core Intel Xeon E5 + Processor Speed: 3.7 GHz + Number of Processors: 1 + Total Number of Cores: 4 + L2 Cache (per Core): 256 KB + L3 Cache: 10 MB + Memory: 32 GB + Boot ROM Version: MP61.0116.B21 + SMC Version (system): 2.20f18 + Illumination Version: 1.4a6 + Serial Number (system): F5KQH0P9F9VN + Hardware UUID: 4D4C19C7-19C4-5678-A936-A419C4609AFD""" + + +def test_empty_profile_instantiation_works(): + SystemProfile() + + +def test_that_system_profile_object_operating_system_attribute(): + sp = SystemProfile() + assert hasattr(sp, "operating_system") + + +def test_that_system_profile_object_has_storage_attribute(): + sp = SystemProfile() + assert hasattr(sp, "storage") + + +def test_that_system_profile_object_has_serial_attribute(): + sp = SystemProfile() + assert hasattr(sp, "serial") + + +def test_that_system_profile_object_has_cpu_name_attribute(): + sp = SystemProfile() + assert hasattr(sp, "cpu_name") + + +def test_that_system_profile_object_has_cpu_speed_attribute(): + sp = SystemProfile() + assert hasattr(sp, "cpu_speed") + + +def test_that_system_profile_object_has_cpu_processors_attribute(): + sp = SystemProfile() + assert hasattr(sp, "cpu_processors") + + +def test_that_system_profile_object_has_cpu_cores_attribute(): + sp = SystemProfile() + assert hasattr(sp, "cpu_cores") + + +def test_that_system_profile_object_has_model_attribute(): + sp = SystemProfile() + assert hasattr(sp, "model") + + +def test_that_system_profile_object_has_name_attribute(): + sp = SystemProfile() + assert hasattr(sp, "name") + + +def test_that_system_profile_object_has_memory_attribute(): + sp = SystemProfile() + assert hasattr(sp, "memory") + + +@pytest.mark.skip +def test_when_ios_device_is_connected(): + pass + + +@pytest.mark.skip +def test_ability_to_get_components_from_system_profile_object(): + pass + + +def test_mac_hardware_method_output_data_type_is_dictionary(): + assert isinstance(mac_hardware(), dict) + + +def test_system_profiler_has_os_type_attribute(): + sp = SystemProfile() + assert sp.os_type + + +def test_operating_system_method_fails_when_operating_system_is_not_darwin_or_windows(): + sp = SystemProfile() + sp.os_type = 'Linux' + with pytest.raises(OSError): + sp.operating_system()