You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
158 lines
5.8 KiB
158 lines
5.8 KiB
#!/usr/bin/python
|
|
|
|
# Copyright (C) 2012, Benjamin Drung <bdrung@debian.org>
|
|
#
|
|
# 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)
|
|
|
|
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())
|
|
|