#!/usr/bin/python3 # Copyright (C) 2012, Benjamin Drung # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """Validates a given Debian or Ubuntu distro-info CSV file.""" import csv import datetime import optparse import os import sys _COLUMNS = { "debian": ("version", "codename", "series", "created", "release", "eol"), "ubuntu": ("version", "codename", "series", "created", "release", "eol", "eol-server"), } _DATES = ("created", "release", "eol", "eol-server") _EARLIER_DATES = ( ("created", "release"), ("release", "eol"), ("eol", "eol-server"), ) _STRINGS = { "debian": ("codename", "series"), "ubuntu": ("version", "codename", "series"), } def convert_date(string): """Convert a date string in ISO 8601 into a datetime object.""" if not string: date = None else: parts = [int(x) for x in string.split("-")] if len(parts) == 3: (year, month, day) = parts date = datetime.date(year, month, day) else: raise ValueError("Date not in ISO 8601 format.") return date def error(filename, line, message, *args): """Prints an error message""" print >> sys.stderr, "%s:%i: %s." % (filename, line, message % args) if not 'xrange' in globals(): xrange = range def validate(filename, distro): """Validates a given CSV file. Returns True if the given CSV file is valid and otherwise False. """ failures = 0 content = open(filename).readlines() # Remove comments for line in xrange(len(content)): if content[line].startswith("#"): content[line] = "\n" csvreader = csv.DictReader(content) for row in csvreader: # Check for missing columns for column in _COLUMNS[distro]: if not column in row: msg = "Column `%s' is missing" error(filename, csvreader.line_num, msg, column) failures += 1 # Check for additinal columns for column in row: if not column in _COLUMNS[distro]: msg = "Additional column `%s' is specified" error(filename, csvreader.line_num, msg, column) failures += 1 # Check required strings columns for column in _STRINGS[distro]: if column in row and not row[column]: msg = "Empty column `%s' specified" error(filename, csvreader.line_num, msg, column) failures += 1 # Check dates for column in _DATES: if column in row: try: row[column] = convert_date(row[column]) except ValueError: msg = "Invalid date `%s' in column `%s'" error(filename, csvreader.line_num, msg, row[column], column) failures += 1 row[column] = None # Check required date columns column = "created" if column in row and not row[column]: msg = "No date specified in column `%s'" error(filename, csvreader.line_num, msg, column) failures += 1 # Compare dates for (date1, date2) in _EARLIER_DATES: if date2 in row and row[date2]: if date1 in row and row[date1]: # date1 needs to be earlier than date2 if row[date1] >= row[date2]: msg = ("Date %s of column `%s' needs to be later " "than %s of column `%s'") error(filename, csvreader.line_num, msg, row[date2].isoformat(), date2, row[date1].isoformat(), date1) failures += 1 else: # date1 needs to be specified if date1 is specified msg = ("A date needs to be specified in column `%s' due " "to the given date in column `%s'") error(filename, csvreader.line_num, msg, date1, date2) failures += 1 return failures == 0 def main(): """Main function with command line parameter parsing.""" script_name = os.path.basename(sys.argv[0]) usage = "%s -d|-u csv-file" % (script_name) parser = optparse.OptionParser(usage=usage) parser.add_option("-d", "--debian", dest="debian", action="store_true", default=False, help="validate a Debian CSV file") parser.add_option("-u", "--ubuntu", dest="ubuntu", action="store_true", default=False, help="validate an Ubuntu CSV file") (options, args) = parser.parse_args() if len(args) == 0: parser.error("No CSV file specified.") elif len(args) > 1: parser.error("Multiple CSV files specified: %s" % (", ".join(args))) if len([x for x in [options.debian, options.ubuntu] if x]) != 1: parser.error("You have to select exactly one of --debian, --ubuntu.") if options.debian: distro = "debian" else: distro = "ubuntu" if validate(args[0], distro): return 0 else: return 1 if __name__ == "__main__": sys.exit(main())