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

Driver metadata validation #220

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions shellfoundry/utilities/shell_package_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions shellfoundry/utilities/validations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .shell_name_validations import ShellNameValidations
from .shell_generation_validation import ShellGenerationValidations
from .driver_metadata_validations import DriverMetadataValidations
75 changes: 75 additions & 0 deletions shellfoundry/utilities/validations/driver_metadata_validations.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 = """
<Driver Description="shell description" MainClass="driver.NutShellDriver" Name="NutShellDriver" Version="1.0.0">
<Layout>
<Category Name="Hidden Commands">
<Command Description="" DisplayName="Orchestration Save" Name="orchestration_save" />
<Command Description="" DisplayName="Orchestration Restore" Name="orchestration_restore" />
</Category>
</Layout>
</Driver>
"""


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

# <editor-fold desc="Discovery">

def get_inventory(self, context):
return AutoLoadDetails([], [])

# </editor-fold>

# <editor-fold desc="Orchestration Save and Restore Standard">
def orchestration_save(self, context, cancellation_context, mode, custom_params):
pass

def orchestration_restore(self, context, cancellation_context, saved_artifact_info, custom_params):
pass

# </editor-fold>
""")
# 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.""")