#!/usr/bin/python ''' Validate App-Center .ini files ''' import os import sys import ConfigParser import re from optparse import OptionParser DIRS = ( '/var/univention/buildsystem2/mirror/appcenter/meta-inf', '/var/univention/buildsystem2/mirror/appcenter.test/meta-inf', ) IGNORE_FILES = ( 'categories.ini', ) IGNORE_DIRS = ( 'REMOVED', ) def main(): for top in parse_args(): if os.path.isfile(top): AppCheck(top).check() elif os.path.isdir(top): for pname in find(top): AppCheck(pname).check() else: print >> sys.stderr, 'SKIP: "%s"' % top def parse_args(): description = sys.modules[__name__].__doc__ parser = OptionParser(description=description) options, args = parser.parse_args() return args or DIRS def find(top): for root, dirs, files in os.walk(top): zap(files, IGNORE_FILES) for fname in files: if not fname.endswith('.ini'): continue pname = os.path.join(root, fname) yield pname zap(dirs, IGNORE_DIRS) def zap(names, remove): for name in remove: try: names.remove(name) except ValueError: pass class AppCheck(object): def __init__(self, pname): self.pname = pname self.config = ConfigParser.RawConfigParser() self.config.read((pname,)) def check(self): for section in self.config.sections(): with Section(self, section) as sec: if section == 'Application': sec.check_all(sec.APPLICATION) elif section.startswith('Sizing: '): sec.check_all(sec.SIZING) else: sec.check_all(sec.TRANSLATION) def log(self, msg, *args): print self.pname, msg % args class Error(Exception): pass class Final(Exception): pass class Fatal(Error, Final): pass class Value(object): DOMAIN = r'(?:[0-9A-Za-z]+(?:[0-9A-Za-z-]*[0-9A-Za-z])?\.)+[0-9A-Za-z-]+(?:[0-9A-Za-z-]*[0-9A-Za-z])?' PATH = r'/[#$%&+,./0-9:;=?@A-Z_a-z-]*' RE_EMAIL = re.compile(r'^.+@' + DOMAIN + r'$') RE_WWW = re.compile(r'^https?://' + DOMAIN + r'(?::\d+)?(?:' + PATH + r')?$') RE_CAPACITY = re.compile(r'\d+(?:\s*[G]B)?$') RE_URL = re.compile(r'^(?:https?://' + DOMAIN + r'(?::\d+)?)?' + PATH + r'$') def __init__(self, value): self.value = value def __str__(self): return str(self.value) def require(self): if not self.value: raise Fatal('Empty') def optional(self): if not self.value: raise Final() def is_bool(self): if self.value in ('True', 'False', 'true', 'false'): return raise Error('Not boolean: %s' % self.value) def is_60c(self): if len(self.value) <= 60: return raise Error('Over 60c long: >%s<>%s<' % (self.value[:61], self.value[61:])) def is_email(self): if self.RE_EMAIL.match(self.value): return raise Error('No email: "%s"' % self.value) def is_www(self): if self.RE_WWW.match(self.value): return raise Error('No WWW: "%s"' % self.value) def is_url(self): if self.RE_URL.match(self.value): return raise Error('No URL: "%s"' % self.value) def is_role(self): ALLOWED = set(('domaincontroller_master', 'domaincontroller_backup', 'domaincontroller_slave', 'memberserver')) if set(self.value.split(',')) - ALLOWED: raise Error('Invalid server role: "%s"' % self.value) def is_arch(self): ALLOWED = set(('amd64', 'i386')) if set(self.value.split(',')) - ALLOWED: raise Error('Invalid architectures: "%s"' % self.value) def is_capacity(self): if self.RE_CAPACITY.match(self.value): return raise Error('No capacity: "%s"' % self.value) def is_category(self): ALLOWED = set(('admin', 'services', 'False')) if self.value in ALLOWED: return raise Error('Not an allowed category: "%s"' % self.value) class Section(object): APPLICATION = { 'ADMemberIssueHide': ('optional',), 'ADMemberIssuePassword': ('optional',), 'Categories': ('optional',), 'Code': ('optional',), # required 'ConflictedApps': ('optional',), 'ConflictedSystemPackages': ('optional',), 'Contact': ('require', 'is_email',), 'DefaultPackagesMaster': ('optional',), 'DefaultPackages': ('require',), 'Description': ('require', 'is_60c',), 'EmailRequired': ('optional',), 'EndOfLife': ('optional', 'is_bool',), 'ID': ('require',), 'LicenseFile': ('optional',), 'LongDescription': ('require',), 'Maintainer': ('optional',), 'MinPhysicalRAM': ('optional', 'is_capacity',), 'Name': ('require',), 'NotificationEmail': ('optional', 'is_email',), 'NotifyVendor': ('optional', 'is_bool',), 'RequiredApps': ('optional',), 'Screenshot': ('optional',), 'ServerRole': ('optional', 'is_role',), 'ShopURL': ('optional', 'is_www',), 'SupportedArchitectures': ('optional', 'is_arch',), 'SupportURL': ('optional', 'is_www',), 'UCSOverviewCategory': ('optional', 'is_category',), 'UMCModuleFlavor': ('optional',), 'UMCModuleName': ('optional',), 'UserActivationRequired': ('optional', 'is_bool',), 'UseShop': ('optional', 'is_bool',), 'UseShop': ('optional', 'is_bool',), 'vendor': ('optional',), 'Version': ('require',), 'VisibleInAppCatalogue': ('optional', 'is_bool',), 'WebInterfaceName': ('optional',), 'WebInterface': ('optional', 'is_url',), 'WebsiteMaintainer': ('optional', 'is_www',), 'Website': ('optional', 'is_www',), 'Websitevendor': ('optional', 'is_www',), 'WithoutRepository': ('optional', 'is_bool',), } SIZING = { 'CPU': ('optional',), 'RAM': ('optional', 'is_capacity',), 'Disk': ('optional', 'is_capacity',), } TRANSLATION = { 'Name': ('optional',) + APPLICATION['Name'], 'Website': APPLICATION['Website'], 'SupportURL': APPLICATION['SupportURL'], 'ShopURL': APPLICATION['ShopURL'], 'Description': APPLICATION['Description'], 'LongDescription': APPLICATION['LongDescription'], } def __init__(self, check, section): self.config = check.config self.section = section self.log = check.log def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): if not exc_type: return if issubclass(exc_type, ConfigParser.NoSectionError): self.log('[%s] missing', self.section) def check_all(self, options): # print >> sys.stderr, self.section, self.config.options(self.section) for option, checks in options.iteritems(): self.check(option, *checks) self.config.remove_option(self.section, option) for option, value in self.config.items(self.section): self.log('[%s].%s: Remaining %s', self.section, option, value) def check(self, option, *checks): value = self.get(option) # print >> sys.stderr, self.section, option, value for cname in checks: try: check = getattr(value, cname) check() except Fatal as ex: self.log('[%s].%s: %s', self.section, option, ex) return except Error as ex: self.log('[%s].%s: %s', self.section, option, ex) except Final as ex: return def get(self, option): try: value = self.config.get(self.section, option) except ConfigParser.NoOptionError as ex: # print >> sys.stderr, ex value = None return Value(value) if __name__ == '__main__': main()