diff --git a/shellfoundry/utilities/shell_package_builder.py b/shellfoundry/utilities/shell_package_builder.py index bd8eb49..1c670c1 100644 --- a/shellfoundry/utilities/shell_package_builder.py +++ b/shellfoundry/utilities/shell_package_builder.py @@ -10,6 +10,8 @@ from shellfoundry.utilities.archive_creator import ArchiveCreator from shellfoundry.utilities.shell_package import ShellPackage from shellfoundry.utilities.temp_dir_context import TempDirContext +from shellfoundry.utilities.validations import DriverMetadataValidations +from shellfoundry.exceptions import FatalError class ShellPackageBuilder(object): @@ -106,6 +108,12 @@ def _remove_all_pyc(package_path): def _create_driver(path, package_path, dir_path, driver_name, mandatory=True): dir_to_zip = os.path.join(path, dir_path) if os.path.exists(dir_to_zip): + try: + driver_validation = DriverMetadataValidations() + driver_validation.validate_driver_metadata(dir_to_zip) + except Exception as ex: + raise FatalError(ex.message) + zip_file_path = os.path.join(package_path, driver_name) ArchiveCreator.make_archive(zip_file_path, "zip", dir_to_zip) elif mandatory: diff --git a/shellfoundry/utilities/validations/__init__.py b/shellfoundry/utilities/validations/__init__.py index bda2c22..12ab2af 100644 --- a/shellfoundry/utilities/validations/__init__.py +++ b/shellfoundry/utilities/validations/__init__.py @@ -1,2 +1,3 @@ from .shell_name_validations import ShellNameValidations from .shell_generation_validation import ShellGenerationValidations +from .driver_metadata_validations import DriverMetadataValidations diff --git a/shellfoundry/utilities/validations/driver_metadata_validations.py b/shellfoundry/utilities/validations/driver_metadata_validations.py new file mode 100644 index 0000000..ca24f18 --- /dev/null +++ b/shellfoundry/utilities/validations/driver_metadata_validations.py @@ -0,0 +1,75 @@ +import os +import xml.etree.ElementTree as etree +import ast +import _ast + + +class DriverMetadataValidations(object): + + @staticmethod + def _parse_xml(xml_string): + # type: (str) -> etree.Element + """ + :param xml_string: xml string + :return: etree.Element xml element from the input string + """ + parser = etree.XMLParser(encoding='utf-8') + return etree.fromstring(xml_string, parser) + + @staticmethod + def _get_driver_commands(driver_path, driver_name): + """ + :param driver_path: + :return: dict: public command names dictionary + """ + if os.path.exists(driver_path): + with open(driver_path) as mf: + tree = ast.parse(mf.read()) + + module_classes = [] + for i in tree.body: + if isinstance(i, _ast.ClassDef): + if i.name == driver_name: + module_classes.append(i) + + commands = {} + for f in module_classes[0].body: + if isinstance(f, _ast.FunctionDef) and not f.name.startswith('_'): + args = f.args.args + if len(args) >= 2: + if args[0].id == 'self' and args[1].id == 'context': + commands[f.name] = [a.id for a in f.args.args] + + return commands + else: + return {} + + def validate_driver_metadata(self, driver_path): + """ Validate driver metadata + :param str driver_path: path to the driver directory + """ + + metadata_path = os.path.join(driver_path, 'drivermetadata.xml') + driver_path = os.path.join(driver_path, 'driver.py') + if os.path.exists(metadata_path): + with open(metadata_path, 'r') as f: + metadata_str = f.read() + + metadata_xml = self._parse_xml(metadata_str) + metadata_commands = metadata_xml.findall('.//Command') + + driver_commands = self._get_driver_commands(driver_path, 'NutShellDriver') + + missing = [] + for mc in metadata_commands: + if mc.attrib['Name'] in driver_commands: + continue + else: + missing.append(mc.attrib['Name']) + + if len(missing) > 0: + err = 'The following commands do not exist in the driver.py but still mentioned in ' \ + 'the DriverMetadata.xml file: {}.\nPlease update the metadata or driver files accordingly.'.format(', '.join(missing)) + raise Exception(err) + + # TODO: add validation for command inputs as well diff --git a/tests/test_utilities/test_validations/test_driver_metadata_validations.py b/tests/test_utilities/test_validations/test_driver_metadata_validations.py new file mode 100644 index 0000000..20e8aeb --- /dev/null +++ b/tests/test_utilities/test_validations/test_driver_metadata_validations.py @@ -0,0 +1,107 @@ +from mock import patch +from pyfakefs import fake_filesystem_unittest +from tests.asserts import * + +from shellfoundry.utilities.validations import DriverMetadataValidations + +drivermetadataxml = """ + + + + + + + + + """ + + +class TestDriverMetadataValidations(fake_filesystem_unittest.TestCase): + def setUp(self): + self.setUpPyfakefs() + + def test_not_failing_when_more_commands_in_driver_than_in_metadata(self): + # Arrange + self.fs.CreateFile('nut_shell/src/drivermetadata.xml', contents=drivermetadataxml) + self.fs.CreateFile('nut_shell/src/requirements.txt') + self.fs.CreateFile('nut_shell/src/driver.py', contents=""" +from cloudshell.shell.core.resource_driver_interface import ResourceDriverInterface +from cloudshell.shell.core.driver_context import InitCommandContext, ResourceCommandContext, AutoLoadResource, \ + AutoLoadAttribute, AutoLoadDetails, CancellationContext + +class NutShellDriver (ResourceDriverInterface): + + def __init__(self): + pass + + def initialize(self, context): + pass + + def _internal_function(self, context): + pass + + @staticmethod + def mystatic(): + pass + + def cleanup(self): + pass + + # + + def get_inventory(self, context): + return AutoLoadDetails([], []) + + # + + # + def orchestration_save(self, context, cancellation_context, mode, custom_params): + pass + + def orchestration_restore(self, context, cancellation_context, saved_artifact_info, custom_params): + pass + + # + """) + # os.chdir('nut_shell') + + driver_path = 'nut_shell/src' + validations = DriverMetadataValidations() + + # Act + try: + validations.validate_driver_metadata(driver_path) + except Exception as ex: + self.fail('validate_driver_metadata raised an exception when it shouldn\'t: ' + ex.message) + + def test_fails_when_command_in_metadata_but_not_in_driver(self): + # Arrange + self.fs.CreateFile('nut_shell/src/drivermetadata.xml', contents=drivermetadataxml) + self.fs.CreateFile('nut_shell/src/requirements.txt') + self.fs.CreateFile('nut_shell/src/driver.py', contents=""" +from cloudshell.shell.core.resource_driver_interface import ResourceDriverInterface +from cloudshell.shell.core.driver_context import InitCommandContext, ResourceCommandContext, AutoLoadResource, \ + AutoLoadAttribute, AutoLoadDetails, CancellationContext + +class NutShellDriver (ResourceDriverInterface): + + def __init__(self): + pass + + def orchestration_save(self, context, cancellation_context, mode, custom_params): + pass + + """) + # os.chdir('nut_shell') + + driver_path = 'nut_shell/src' + validations = DriverMetadataValidations() + + # Act + with self.assertRaises(Exception) as context: + validations.validate_driver_metadata(driver_path) + + # Assert + self.assertEqual(context.exception.message, """The following commands do not exist in the driver.py but still mentioned in the DriverMetadata.xml file: orchestration_restore. +Please update the metadata or driver files accordingly.""") +