Added core and CLI
* Added core functionality of valitation tool: - Package loading - Validators discovery(plugins) - Validation of package * Added CLI runner Partial-blueprint: package-validation Change-Id: Ia03914f7e48e5a43ef290deb95e08c66446c08ef
This commit is contained in:
parent
0a97def364
commit
c59ed6dec5
0
muranopkgcheck/cmd/__init__.py
Normal file
0
muranopkgcheck/cmd/__init__.py
Normal file
138
muranopkgcheck/cmd/run.py
Normal file
138
muranopkgcheck/cmd/run.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from muranopkgcheck import log
|
||||||
|
|
||||||
|
LOG = log.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cli_args(args=None):
|
||||||
|
|
||||||
|
usage_string = 'murano-pkg-checker [options] <path to package>'
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='murano-pkg-checker arguments',
|
||||||
|
formatter_class=argparse.HelpFormatter,
|
||||||
|
usage=usage_string
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('--select',
|
||||||
|
dest='select',
|
||||||
|
required=False,
|
||||||
|
type=str,
|
||||||
|
help='select errors and warnings (e.g. E001,W002)')
|
||||||
|
|
||||||
|
parser.add_argument('--ignore',
|
||||||
|
dest='ignore',
|
||||||
|
required=False,
|
||||||
|
type=str,
|
||||||
|
help='skip errors and warnings (e.g. E042,W007)')
|
||||||
|
|
||||||
|
parser.add_argument('--verbose', '-v',
|
||||||
|
dest='verbose',
|
||||||
|
default=0,
|
||||||
|
action='count',
|
||||||
|
help='Verbosity level')
|
||||||
|
|
||||||
|
parser.add_argument('--discover',
|
||||||
|
dest='discover',
|
||||||
|
action='store_true',
|
||||||
|
help='Run discovery packages')
|
||||||
|
|
||||||
|
parser.add_argument('path',
|
||||||
|
type=str,
|
||||||
|
help='Path to package or catalog')
|
||||||
|
|
||||||
|
return parser.parse_args(args=args)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logs(args):
|
||||||
|
if args.verbose == 0:
|
||||||
|
log.setup(level=log.CRITICAL)
|
||||||
|
elif args.verbose == 1:
|
||||||
|
log.setup(level=log.ERROR)
|
||||||
|
elif args.verbose == 2:
|
||||||
|
log.setup(level=log.INFO)
|
||||||
|
else:
|
||||||
|
log.setup(level=log.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def run(args, pkg_path=None):
|
||||||
|
from muranopkgcheck import manager
|
||||||
|
m = manager.Manager(pkg_path or args.path)
|
||||||
|
m.load_plugins()
|
||||||
|
if args.select:
|
||||||
|
select = args.select.split(',')
|
||||||
|
else:
|
||||||
|
select = None
|
||||||
|
if args.ignore:
|
||||||
|
ignore = args.ignore.split(',')
|
||||||
|
else:
|
||||||
|
ignore = None
|
||||||
|
errors = m.validate(select=select, ignore=ignore)
|
||||||
|
fmt = manager.PlainTextFormatter()
|
||||||
|
return fmt.format(errors)
|
||||||
|
|
||||||
|
|
||||||
|
def discover(args):
|
||||||
|
errors = []
|
||||||
|
for dirpath, dirnames, filenames in os.walk(args.path):
|
||||||
|
items = dirnames
|
||||||
|
for item in items:
|
||||||
|
if item.startswith('.'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
path = os.path.join(dirpath, item)
|
||||||
|
pkg_errors = run(args, path)
|
||||||
|
LOG.info("Package {} discovered".format(path))
|
||||||
|
if pkg_errors:
|
||||||
|
errors.append("Errors in package {}\n{}\n"
|
||||||
|
"".format(path, pkg_errors))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return '\n'.join(errors)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_cli_args()
|
||||||
|
setup_logs(args)
|
||||||
|
global LOG
|
||||||
|
LOG = log.get_logger(__name__)
|
||||||
|
try:
|
||||||
|
if args.discover:
|
||||||
|
errors = discover(args)
|
||||||
|
else:
|
||||||
|
errors = run(args)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
LOG.critical(e)
|
||||||
|
return 2
|
||||||
|
except Exception as e:
|
||||||
|
LOG.critical(six.text_type(e))
|
||||||
|
LOG.exception(e)
|
||||||
|
return 3
|
||||||
|
if errors:
|
||||||
|
print(errors)
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
print('No errors found!')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
17
muranopkgcheck/consts.py
Normal file
17
muranopkgcheck/consts.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
MANIFEST_PATH = 'manifest.yaml'
|
||||||
|
DEFAULT_FORMAT = 'MuranoPL'
|
||||||
|
DEFAULT_FORMAT_VERSION = '1.0'
|
60
muranopkgcheck/error.py
Normal file
60
muranopkgcheck/error.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
class CheckError(Exception):
|
||||||
|
|
||||||
|
def __init__(self, code, message, filename=None,
|
||||||
|
line=0, column=0, source=None):
|
||||||
|
self.code = code
|
||||||
|
self.message = message
|
||||||
|
self.filename = filename
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
fields = ('code', 'message', 'filename', 'line', 'column', 'source')
|
||||||
|
serialized = {}
|
||||||
|
for f in fields:
|
||||||
|
serialized[f] = self.__getattribute__(f)
|
||||||
|
return serialized
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'CheckError({0})'.format(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
def error(code, message, filename=None, line=0, column=0, source=None):
|
||||||
|
return CheckError(code=code, message=message, filename=filename,
|
||||||
|
line=line, column=column, source=source)
|
||||||
|
|
||||||
|
|
||||||
|
def _report(code):
|
||||||
|
def _report_(message, yaml_obj=None, filename=None):
|
||||||
|
meta = getattr(yaml_obj, '__yaml_meta__', None)
|
||||||
|
kwargs = {}
|
||||||
|
if meta is not None:
|
||||||
|
kwargs['line'] = meta.line + 1
|
||||||
|
kwargs['column'] = meta.column + 1
|
||||||
|
kwargs['source'] = meta.get_snippet()
|
||||||
|
kwargs['filename'] = filename or meta.name
|
||||||
|
return CheckError(code=code, message=message, **kwargs)
|
||||||
|
return _report_
|
||||||
|
|
||||||
|
|
||||||
|
class Report(object):
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return _report(name)
|
||||||
|
|
||||||
|
report = Report()
|
45
muranopkgcheck/log.py
Normal file
45
muranopkgcheck/log.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
CRITICAL = logging.CRITICAL
|
||||||
|
ERROR = logging.ERROR
|
||||||
|
WARNING = logging.WARNING
|
||||||
|
INFO = logging.INFO
|
||||||
|
DEBUG = logging.DEBUG
|
||||||
|
|
||||||
|
LOG_FORMAT = "%(asctime)s %(name)s:%(lineno)d %(levelname)s %(message)s"
|
||||||
|
DEFAULT_LEVEL = logging.DEBUG
|
||||||
|
LOG_HANDLER = None
|
||||||
|
LOG_LEVEL = DEFAULT_LEVEL
|
||||||
|
|
||||||
|
|
||||||
|
def setup(log_format=LOG_FORMAT, level=DEFAULT_LEVEL):
|
||||||
|
console_log_handler = logging.StreamHandler()
|
||||||
|
console_log_handler.setFormatter(logging.Formatter(log_format))
|
||||||
|
global LOG_HANDLER, LOG_LEVEL
|
||||||
|
LOG_HANDLER = console_log_handler
|
||||||
|
LOG_LEVEL = level
|
||||||
|
|
||||||
|
|
||||||
|
def get_logger(name):
|
||||||
|
logger = logging.getLogger(name)
|
||||||
|
for h in logger.handlers:
|
||||||
|
logger.removeHandler(h)
|
||||||
|
logger.addHandler(LOG_HANDLER)
|
||||||
|
logger.setLevel(LOG_LEVEL)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
setup()
|
101
muranopkgcheck/manager.py
Normal file
101
muranopkgcheck/manager.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import types
|
||||||
|
|
||||||
|
import stevedore
|
||||||
|
|
||||||
|
from muranopkgcheck import error
|
||||||
|
from muranopkgcheck import log
|
||||||
|
from muranopkgcheck import pkg_loader
|
||||||
|
from muranopkgcheck.validators import VALIDATORS
|
||||||
|
|
||||||
|
LOG = log.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Formatter(object):
|
||||||
|
|
||||||
|
def format(self, error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PlainTextFormatter(Formatter):
|
||||||
|
|
||||||
|
def format(self, errors):
|
||||||
|
lines = []
|
||||||
|
for e in errors:
|
||||||
|
if e.filename:
|
||||||
|
lines.append('{filename}:{line}:{column}: {code} {message}'
|
||||||
|
''.format(**e.to_dict()))
|
||||||
|
else:
|
||||||
|
lines.append('{code} {message}'
|
||||||
|
''.format(**e.to_dict()))
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
class Manager(object):
|
||||||
|
|
||||||
|
def __init__(self, pkg_path):
|
||||||
|
self.pkg = pkg_loader.load_package(pkg_path)
|
||||||
|
self.validators = VALIDATORS
|
||||||
|
self.plugins = None
|
||||||
|
|
||||||
|
def _to_list(self, error_chain, select=None, ignore=None):
|
||||||
|
errors = []
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
e = next(error_chain, None)
|
||||||
|
if e is None:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
LOG.exception('Checker failed')
|
||||||
|
e = error.report.E000(
|
||||||
|
'Checker failed more information in logs')
|
||||||
|
|
||||||
|
if isinstance(e, types.GeneratorType):
|
||||||
|
errors.extend(self._to_list(e, select, ignore))
|
||||||
|
else:
|
||||||
|
if ((select and e.code not in select)
|
||||||
|
or (ignore and e.code in ignore)):
|
||||||
|
continue
|
||||||
|
errors.append(e)
|
||||||
|
|
||||||
|
return sorted(errors, key=lambda err: err.code)
|
||||||
|
|
||||||
|
def load_plugins(self):
|
||||||
|
if self.plugins is not None:
|
||||||
|
return
|
||||||
|
self.plugins = stevedore.ExtensionManager(
|
||||||
|
'muranopkgcheck.plugins', invoke_on_load=True,
|
||||||
|
propagate_map_exceptions=True,
|
||||||
|
on_load_failure_callback=self.failure_hook)
|
||||||
|
plugin_validators = list(itertools.chain(
|
||||||
|
*(p.obj.validators() for p in self.plugins)
|
||||||
|
))
|
||||||
|
self.validators += plugin_validators
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def failure_hook(_, ep, err):
|
||||||
|
LOG.error('Could not load %r: %s', ep.name, err)
|
||||||
|
raise err
|
||||||
|
|
||||||
|
def validate(self, validators=None, select=None, ignore=None):
|
||||||
|
validators = validators or self.validators
|
||||||
|
report_chains = []
|
||||||
|
for validator in validators:
|
||||||
|
v = validator(self.pkg)
|
||||||
|
report_chains.append(v.run())
|
||||||
|
return self._to_list(itertools.chain(*report_chains), select, ignore)
|
188
muranopkgcheck/pkg_loader.py
Normal file
188
muranopkgcheck/pkg_loader.py
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
import six
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from muranopkgcheck import consts
|
||||||
|
from muranopkgcheck import log
|
||||||
|
from muranopkgcheck import yaml_loader
|
||||||
|
|
||||||
|
LOG = log.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FileWrapper(object):
|
||||||
|
|
||||||
|
def __init__(self, pkg, path):
|
||||||
|
self._path = path
|
||||||
|
with pkg.open_file(path) as file_:
|
||||||
|
self._raw = file_.read()
|
||||||
|
|
||||||
|
with pkg.open_file(path) as file_:
|
||||||
|
try:
|
||||||
|
self._yaml = list(yaml.load_all(file_, yaml_loader.YamlLoader))
|
||||||
|
except yaml.YAMLError:
|
||||||
|
self._yaml = None
|
||||||
|
|
||||||
|
def raw(self):
|
||||||
|
return self._raw
|
||||||
|
|
||||||
|
def yaml(self):
|
||||||
|
return self._yaml
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class BaseLoader(object):
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
self._cached_files = dict()
|
||||||
|
self.format = consts.DEFAULT_FORMAT
|
||||||
|
self.format_version = consts.DEFAULT_FORMAT_VERSION
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _try_load(cls, path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def try_load(cls, path):
|
||||||
|
loader = cls._try_load(path)
|
||||||
|
if loader is not None and loader.exists(consts.MANIFEST_PATH):
|
||||||
|
loader.try_set_format()
|
||||||
|
return loader
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def list_files(self, subdir=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def open_file(self, path, mode='r'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def exists(self, name):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def search_for(self, regex='.*', subdir=None):
|
||||||
|
r = re.compile(regex)
|
||||||
|
return (f for f in self.list_files(subdir) if r.match(f))
|
||||||
|
|
||||||
|
def read(self, path):
|
||||||
|
if path in self._cached_files:
|
||||||
|
return self._cached_files[path]
|
||||||
|
self._cached_files[path] = FileWrapper(self, path)
|
||||||
|
return self._cached_files[path]
|
||||||
|
|
||||||
|
def try_set_format(self):
|
||||||
|
if self.exists(consts.MANIFEST_PATH):
|
||||||
|
manifest = self.read(consts.MANIFEST_PATH).yaml()
|
||||||
|
if manifest and 'Format' in manifest:
|
||||||
|
if '/' in str(manifest['Format']):
|
||||||
|
fmt, version = manifest['Format'].split('/', 1)
|
||||||
|
self.format = fmt
|
||||||
|
self.format_version = version
|
||||||
|
else:
|
||||||
|
self.format_version = str(manifest['Format'])
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryLoader(BaseLoader):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _try_load(cls, path):
|
||||||
|
if os.path.isdir(path):
|
||||||
|
return cls(path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def open_file(self, path, mode='r'):
|
||||||
|
return open(os.path.join(self.path, path), mode)
|
||||||
|
|
||||||
|
def list_files(self, subdir=None):
|
||||||
|
path = self.path
|
||||||
|
if subdir is not None:
|
||||||
|
path = os.path.join(path, subdir)
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for dirpath, dirnames, filenames in os.walk(path):
|
||||||
|
files.extend(
|
||||||
|
os.path.relpath(
|
||||||
|
os.path.join(dirpath, filename), self.path)
|
||||||
|
for filename in filenames)
|
||||||
|
if subdir is None:
|
||||||
|
return files
|
||||||
|
subdir_len = len(subdir)
|
||||||
|
return [file_[subdir_len:].lstrip('/') for file_ in files]
|
||||||
|
|
||||||
|
def exists(self, name):
|
||||||
|
return os.path.exists(os.path.join(self.path, name))
|
||||||
|
|
||||||
|
|
||||||
|
class ZipLoader(BaseLoader):
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
super(ZipLoader, self).__init__(path)
|
||||||
|
self._zipfile = zipfile.ZipFile(self.path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _try_load(cls, path):
|
||||||
|
try:
|
||||||
|
return cls(path)
|
||||||
|
except (IOError, zipfile.BadZipfile):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def open_file(self, name, mode='r'):
|
||||||
|
return self._zipfile.open(name, mode)
|
||||||
|
|
||||||
|
def list_files(self, subdir=None):
|
||||||
|
files = self._zipfile.namelist()
|
||||||
|
if subdir is None:
|
||||||
|
return files
|
||||||
|
subdir_len = len(subdir)
|
||||||
|
return [file_[subdir_len:].lstrip('/') for file_ in files
|
||||||
|
if file_.startswith(subdir)]
|
||||||
|
|
||||||
|
def exists(self, name):
|
||||||
|
try:
|
||||||
|
self._zipfile.getinfo(name)
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not name.endswith('/'):
|
||||||
|
try:
|
||||||
|
self._zipfile.getinfo(name + '/')
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
PACKAGE_LOADERS = [DirectoryLoader, ZipLoader]
|
||||||
|
|
||||||
|
|
||||||
|
def load_package(path):
|
||||||
|
for loader_cls in PACKAGE_LOADERS:
|
||||||
|
loader = loader_cls.try_load(path)
|
||||||
|
if loader is not None:
|
||||||
|
return loader
|
||||||
|
else:
|
||||||
|
LOG.debug("{} failed to load '{}'"
|
||||||
|
"".format(loader_cls.__name__, path))
|
||||||
|
else:
|
||||||
|
raise ValueError("Cannot load package: '{}'".format(path))
|
29
muranopkgcheck/plugin.py
Normal file
29
muranopkgcheck/plugin.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class Plugin(object):
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def validators(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def errors(self):
|
||||||
|
pass
|
67
muranopkgcheck/tests/test_manager.py
Normal file
67
muranopkgcheck/tests/test_manager.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from muranopkgcheck import error
|
||||||
|
from muranopkgcheck import manager
|
||||||
|
from muranopkgcheck.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class PlainTextFormatterTest(base.TestCase):
|
||||||
|
|
||||||
|
def test_format(self):
|
||||||
|
|
||||||
|
class FakeYamlMeta(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.line = 0
|
||||||
|
self.column = 0
|
||||||
|
self.name = 'fake'
|
||||||
|
|
||||||
|
def get_snippet(self):
|
||||||
|
return 'fake'
|
||||||
|
|
||||||
|
class FakeYamlNode(str):
|
||||||
|
|
||||||
|
def __init__(self, value):
|
||||||
|
super(FakeYamlNode, self).__init__()
|
||||||
|
self.value = value
|
||||||
|
self.__yaml_meta__ = FakeYamlMeta()
|
||||||
|
|
||||||
|
formater = manager.PlainTextFormatter()
|
||||||
|
fake_yaml_node = FakeYamlNode('Fake!!!')
|
||||||
|
|
||||||
|
errors = [error.report.E007('Fake!!!', fake_yaml_node)]
|
||||||
|
self.assertEqual('fake:1:1: E007 Fake!!!', formater.format(errors))
|
||||||
|
|
||||||
|
|
||||||
|
class ManagerTest(base.TestCase):
|
||||||
|
|
||||||
|
@mock.patch('muranopkgcheck.manager.pkg_loader')
|
||||||
|
def test_validate(self, m_pkg_loader):
|
||||||
|
fake_error = error.report.E007('Fake!')
|
||||||
|
|
||||||
|
def error_generator():
|
||||||
|
yield fake_error
|
||||||
|
MockValidator = mock.Mock()
|
||||||
|
m_validator = MockValidator.return_value
|
||||||
|
m_validator.run.return_value = (e for e in [
|
||||||
|
fake_error,
|
||||||
|
error_generator()
|
||||||
|
])
|
||||||
|
mgr = manager.Manager('fake')
|
||||||
|
errors = mgr.validate(validators=[MockValidator])
|
||||||
|
self.assertEqual([fake_error, fake_error], errors)
|
||||||
|
m_pkg_loader.load_package.assert_called_once_with('fake')
|
@ -1,24 +0,0 @@
|
|||||||
# Copyright 2010-2011 OpenStack Foundation
|
|
||||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from muranopkgcheck.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestCase(base.TestCase):
|
|
||||||
|
|
||||||
"""Test case base class for all unit tests."""
|
|
||||||
|
|
||||||
def test_stub(self):
|
|
||||||
pass
|
|
155
muranopkgcheck/tests/test_pkg_loader.py
Normal file
155
muranopkgcheck/tests/test_pkg_loader.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from muranopkgcheck import consts
|
||||||
|
from muranopkgcheck import pkg_loader
|
||||||
|
from muranopkgcheck.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class FileWrapperTest(base.TestCase):
|
||||||
|
|
||||||
|
def test_file_wrapper(self):
|
||||||
|
fake_pkg = mock.Mock()
|
||||||
|
fake_pkg.open_file.side_effect = \
|
||||||
|
lambda f: mock.mock_open(read_data='text')()
|
||||||
|
f = pkg_loader.FileWrapper(fake_pkg, 'fake_path')
|
||||||
|
self.assertEqual('text', f.raw())
|
||||||
|
self.assertEqual(['text'], f.yaml())
|
||||||
|
|
||||||
|
fake_pkg.open_file.side_effect = \
|
||||||
|
lambda f: mock.mock_open(read_data='!@#$%')()
|
||||||
|
f = pkg_loader.FileWrapper(fake_pkg, 'fake_path')
|
||||||
|
self.assertEqual('!@#$%', f.raw())
|
||||||
|
self.assertEqual(None, f.yaml())
|
||||||
|
|
||||||
|
|
||||||
|
class FakeLoader(pkg_loader.BaseLoader):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _try_load(cls, path):
|
||||||
|
return cls(path)
|
||||||
|
|
||||||
|
def open_file(self, path, mode='r'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def exists(self, name):
|
||||||
|
if name == consts.MANIFEST_PATH:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def list_files(self, subdir=None):
|
||||||
|
if subdir is None:
|
||||||
|
return ['1.yaml', '2.sh', 'sub/3.yaml']
|
||||||
|
else:
|
||||||
|
return ['3.yaml']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseLoaderTest(base.TestCase):
|
||||||
|
|
||||||
|
@mock.patch.object(FakeLoader, '_try_load')
|
||||||
|
@mock.patch.object(FakeLoader, 'try_set_format')
|
||||||
|
def test_try_load(self, m_format, m_load):
|
||||||
|
m_load.return_value = FakeLoader('fake')
|
||||||
|
FakeLoader.try_load('fake')
|
||||||
|
m_load.assert_called_once_with('fake')
|
||||||
|
m_format.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch.object(FakeLoader, '_try_load')
|
||||||
|
def test_try_set_version(self, m_load):
|
||||||
|
m_file_wrapper = mock.Mock()
|
||||||
|
m_file = m_file_wrapper.return_value
|
||||||
|
m_file.yaml.return_value = {'Format': 'Fake/42'}
|
||||||
|
with mock.patch('muranopkgcheck.pkg_loader.FileWrapper',
|
||||||
|
m_file_wrapper):
|
||||||
|
m_load.return_value = FakeLoader('fake')
|
||||||
|
loader = FakeLoader.try_load('fake')
|
||||||
|
self.assertEqual('Fake', loader.format)
|
||||||
|
self.assertEqual('42', loader.format_version)
|
||||||
|
m_file.yaml.assert_called_once_with()
|
||||||
|
|
||||||
|
m_load.return_value = FakeLoader('fake')
|
||||||
|
m_file.yaml.return_value = {'Format': '4.2'}
|
||||||
|
loader = FakeLoader.try_load('fake')
|
||||||
|
self.assertEqual(consts.DEFAULT_FORMAT, loader.format)
|
||||||
|
self.assertEqual('4.2', loader.format_version)
|
||||||
|
|
||||||
|
def test_search_for(self):
|
||||||
|
fake = FakeLoader('fake')
|
||||||
|
self.assertEqual(['1.yaml', 'sub/3.yaml'],
|
||||||
|
list(fake.search_for('.*\.yaml$')))
|
||||||
|
self.assertEqual(['3.yaml'],
|
||||||
|
list(fake.search_for('.*\.yaml$', subdir='sub')))
|
||||||
|
|
||||||
|
def test_read(self):
|
||||||
|
fake = FakeLoader('fake')
|
||||||
|
m_file_wrapper = mock.Mock()
|
||||||
|
m_file = m_file_wrapper.return_value
|
||||||
|
with mock.patch('muranopkgcheck.pkg_loader.FileWrapper',
|
||||||
|
m_file_wrapper):
|
||||||
|
loaded = fake.read('fake')
|
||||||
|
self.assertEqual(m_file, loaded)
|
||||||
|
# check that cache works
|
||||||
|
loaded = fake.read('fake')
|
||||||
|
self.assertEqual(m_file, loaded)
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryLoaderTest(base.TestCase):
|
||||||
|
|
||||||
|
def _load_fake_pkg(self):
|
||||||
|
with mock.patch('muranopkgcheck.pkg_loader.os.path.isdir') as m_isdir:
|
||||||
|
with mock.patch.object(pkg_loader.DirectoryLoader,
|
||||||
|
'try_set_format') as m:
|
||||||
|
with mock.patch.object(pkg_loader.DirectoryLoader,
|
||||||
|
'exists') as m_exists:
|
||||||
|
m_exists.return_value = True
|
||||||
|
m_isdir.return_value = True
|
||||||
|
loader = pkg_loader.DirectoryLoader.try_load('fake')
|
||||||
|
m.assert_called_once_with()
|
||||||
|
return loader
|
||||||
|
|
||||||
|
def test_try_load(self):
|
||||||
|
# NOTE(sslypushenko) Using mock.patch here as decorator breaks pdb
|
||||||
|
pkg = self._load_fake_pkg()
|
||||||
|
self.assertEqual('fake', pkg.path)
|
||||||
|
with mock.patch('muranopkgcheck.pkg_loader.os.path.isdir') as m_isdir:
|
||||||
|
m_isdir.return_value = False
|
||||||
|
pkg = pkg_loader.DirectoryLoader.try_load('fake')
|
||||||
|
self.assertEqual(None, pkg)
|
||||||
|
|
||||||
|
def test_list_files(self):
|
||||||
|
# NOTE(sslypushenko) Using mock.patch here as decorator breaks pdb
|
||||||
|
pkg = self._load_fake_pkg()
|
||||||
|
with mock.patch('muranopkgcheck.pkg_loader.os.walk') as m_walk:
|
||||||
|
m_walk.return_value = (item for item in [
|
||||||
|
('fake', ['subdir'], ['1', '2']),
|
||||||
|
('fake/subdir', [], ['3', '4']),
|
||||||
|
])
|
||||||
|
self.assertEqual(['1', '2', 'subdir/3', 'subdir/4'],
|
||||||
|
pkg.list_files())
|
||||||
|
m_walk.return_value = (item for item in [
|
||||||
|
('fake/subdir', [], ['3', '4']),
|
||||||
|
])
|
||||||
|
self.assertEqual(['3', '4'],
|
||||||
|
pkg.list_files(subdir='subdir'))
|
||||||
|
|
||||||
|
def test_exist(self):
|
||||||
|
# NOTE(sslypushenko) Using mock.patch here as decorator breaks pdb
|
||||||
|
pkg = self._load_fake_pkg()
|
||||||
|
with mock.patch('muranopkgcheck.pkg_loader'
|
||||||
|
'.os.path.exists') as m_exists:
|
||||||
|
m_exists.return_value = True
|
||||||
|
self.assertTrue(pkg.exists('1.yaml'))
|
||||||
|
m_exists.return_value = False
|
||||||
|
self.assertFalse(pkg.exists('1.yaml'))
|
16
muranopkgcheck/validators/__init__.py
Normal file
16
muranopkgcheck/validators/__init__.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
VALIDATORS = [
|
||||||
|
]
|
87
muranopkgcheck/yaml_loader.py
Normal file
87
muranopkgcheck/yaml_loader.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import six
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
__all__ = ['YamlLoader']
|
||||||
|
|
||||||
|
|
||||||
|
class YamlMetadata(object):
|
||||||
|
def __init__(self, mark):
|
||||||
|
self.mark = mark
|
||||||
|
|
||||||
|
@property
|
||||||
|
def line(self):
|
||||||
|
return self.mark.line
|
||||||
|
|
||||||
|
@property
|
||||||
|
def column(self):
|
||||||
|
return self.mark.column
|
||||||
|
|
||||||
|
def get_snippet(self, indent=4, max_length=75):
|
||||||
|
return self.mark.get_snippet(indent, max_length)
|
||||||
|
|
||||||
|
|
||||||
|
class YamlObject(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class YamlMapping(YamlObject, dict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class YamlSequence(YamlObject, list):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class YamlString(YamlObject, six.text_type):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
BaseLoader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader)
|
||||||
|
|
||||||
|
|
||||||
|
class YamlLoader(BaseLoader):
|
||||||
|
|
||||||
|
def construct_yaml_seq(self, node):
|
||||||
|
data = YamlSequence()
|
||||||
|
yield data
|
||||||
|
data.extend(self.construct_sequence(node))
|
||||||
|
data.__yaml_meta__ = node.start_mark
|
||||||
|
|
||||||
|
def construct_yaml_str(self, node):
|
||||||
|
value = super(YamlLoader, self).construct_yaml_str(node)
|
||||||
|
value = YamlString(value)
|
||||||
|
value.__yaml_meta__ = node.start_mark
|
||||||
|
return value
|
||||||
|
|
||||||
|
def construct_yaml_map(self, node):
|
||||||
|
data = YamlMapping()
|
||||||
|
yield data
|
||||||
|
value = self.construct_mapping(node)
|
||||||
|
data.update(value)
|
||||||
|
data.__yaml_meta__ = node.start_mark
|
||||||
|
|
||||||
|
YamlLoader.add_constructor(
|
||||||
|
u'tag:yaml.org,2002:seq',
|
||||||
|
YamlLoader.construct_yaml_seq)
|
||||||
|
|
||||||
|
YamlLoader.add_constructor(
|
||||||
|
u'tag:yaml.org,2002:str',
|
||||||
|
YamlLoader.construct_yaml_str)
|
||||||
|
|
||||||
|
YamlLoader.add_constructor(
|
||||||
|
u'tag:yaml.org,2002:map',
|
||||||
|
YamlLoader.construct_yaml_map)
|
@ -3,3 +3,7 @@
|
|||||||
# process, which may cause wedges in the gate later.
|
# process, which may cause wedges in the gate later.
|
||||||
|
|
||||||
pbr>=1.6 # Apache-2.0
|
pbr>=1.6 # Apache-2.0
|
||||||
|
PyYAML>=3.1.0 # MIT
|
||||||
|
yaql>=1.1.0 # Apache 2.0 License
|
||||||
|
six>=1.9.0 # MIT
|
||||||
|
stevedore>=1.16.0 # Apache-2.0
|
||||||
|
@ -31,6 +31,10 @@ all_files = 1
|
|||||||
[upload_sphinx]
|
[upload_sphinx]
|
||||||
upload-dir = doc/build/html
|
upload-dir = doc/build/html
|
||||||
|
|
||||||
|
[entry_points]
|
||||||
|
console_scripts =
|
||||||
|
murano-pkg-check = muranopkgcheck.cmd.run:main
|
||||||
|
|
||||||
[build_releasenotes]
|
[build_releasenotes]
|
||||||
all_files = 1
|
all_files = 1
|
||||||
build-dir = releasenotes/build
|
build-dir = releasenotes/build
|
||||||
|
2
tox.ini
2
tox.ini
@ -5,7 +5,7 @@ skipsdist = True
|
|||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
usedevelop = True
|
usedevelop = True
|
||||||
install_command = pip install -U {opts} {packages}
|
install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
|
||||||
setenv =
|
setenv =
|
||||||
VIRTUAL_ENV={envdir}
|
VIRTUAL_ENV={envdir}
|
||||||
deps =
|
deps =
|
||||||
|
Loading…
Reference in New Issue
Block a user