#! /usr/bin/env python # # Written by Gabor Gombas # Licensed under the same terms as the rest of BOINC. """ Manage applications and platforms from the command line. Run "appmgr --help" to list the available commands. Run "appmgr --help" to get the description of a command. """ import time, os, re, string from optparse import OptionParser from xml.dom.minidom import parseString import boinc_path_config from Boinc import database, db_mid, configxml # The list of standard platforms standard_platforms = { 'windows_intelx86': 'Microsoft Windows (98 or later) running on an Intel x86-compatible CPU', 'windows_x86_64': 'Microsoft Windows running on an AMD x86_64 or Intel EM64T CPU', 'i686-pc-linux-gnu': 'Linux running on an Intel x86-compatible CPU', 'x86_64-pc-linux-gnu': 'Linux running on an AMD x86_64 or Intel EM64T CPU', 'powerpc-apple-darwin': 'Mac OS X 10.3 or later running on Motorola PowerPC', 'i686-apple-darwin': 'Mac OS 10.4 or later running on Intel', 'x86_64-apple-darwin': 'Intel 64-bit Mac OS 10.5 or later', 'sparc-sun-solaris2.7': 'Solaris 2.7 running on a SPARC-compatible CPU', 'sparc-sun-solaris': 'Solaris 2.8 or later running on a SPARC-compatible CPU', 'sparc64-sun-solaris': 'Solaris 2.8 or later running on a SPARC 64-bit CPU', 'powerpc64-ps3-linux-gnu': 'Sony Playstation 3 running Linux', 'anonymous': 'anonymous'} def remove_directory(dir): """Remove a directory and all files inside it.""" for file in os.listdir(dir): os.unlink(os.path.join(dir, file)) os.rmdir(dir) def remove_app_directory(appver, config): """Remove files for an app version under /apps""" appdir = os.path.join(config.app_dir, appver.app.name) for path in map(lambda file: os.path.join(appdir, file), os.listdir(appdir)): match = re.match('^[^.]+_([0-9]+)[.]([0-9]+)_([^.]+?(?:[0-9][0-9.]*[0-9])?)(?:__([^.])+)?(?:[.][a-zA-Z0-9\_\-]*)?$', os.path.basename(path)) if not match: continue major, minor, platform_name, plan_class = match.groups() version_num = int(major) * 100 + int(minor) if plan_class is None: plan_class = '' if platform_name != appver.platform.name: continue if appver.version_num != version_num: continue if appver.plan_class != plan_class: continue if os.path.isdir(path): remove_directory(path) else: os.unlink(path) # If all versions are gone, remove the parent directory as well try: os.rmdir(appdir) except: pass def do_delete_app_version(appver): """Delete both the files and the database entry for an app version.""" config = configxml.default_config().config # Remove files/directories under app_dir appdir = os.path.join(config.app_dir, appver.app.name) if os.path.isdir(appdir): remove_app_directory(appver, config) # Remove files under download_dir # appver.xml_doc does not have a parent node, so we have to add one xml = parseString('' + appver.xml_doc + '') for file in xml.getElementsByTagName('file_info'): name = '' for node in file.getElementsByTagName('name')[0].childNodes: if node.nodeType == node.TEXT_NODE: name += node.data if not name: continue os.unlink(os.path.join(config.download_dir, name)) tmp = str(appver) appver.remove() print "Removed " + tmp def add_platform(args): usage = """%prog add_platform Add a new platform definition to the database.""" parser = OptionParser(usage = usage) options, extra_args = parser.parse_args(args) if len(extra_args) != 2: parser.error("Wrong number of arguments") platform = database.Platform(create_time = time.time(), name = extra_args[0], user_friendly_name = extra_args[1], deprecated = 0) platform.commit() print "Added " + str(platform) def del_platform(args): usage = """%prog delete_platform Deletes a platform from the database. If the --force flag is specified, it also deletes all application versions for this platform. Otherwise, if there is an application version available for this platform, the command will fail.""" parser = OptionParser(usage = usage) parser.add_option("-f", "--force", dest = "force", action = "store_true", default = False, help = "remove any app versions that still exist for this platform") options, extra_args = parser.parse_args(args) if len(extra_args) != 1: parser.error("Wrong number of arguments") platforms = database.Platforms.find(name = extra_args[0]) if len(platforms) < 1: raise SystemExit("Unknown platform name") platform = platforms[0] # If there are still application versions defined for this platform, then # either bail out, or if --force was given, blow those application versions # first for appver in database.AppVersions.find(platform = platform): if not options.force: raise SystemExit("There are still application versions for this " "platform, bailing out") do_delete_app_version(appver) # The id is gone after .remove(), so we have to save the stringified form tmp = str(platform) platform.remove() print "Deleted " + tmp def update_platform(args): usage = """%prog update_platform [options] Update platform data.""" parser = OptionParser(usage = usage) parser.add_option("--deprecated", action = "store_const", const = 1, help = "mark the platform as deprecated") parser.add_option("--no-deprecated", dest = "deprecated", action = "store_const", const = 0, help = "remove the deprecation mark") parser.add_option("--user_friendly_name", metavar = "DESC", help = "set the user-friendly name of the platform") options, extra_args = parser.parse_args(args) if len(extra_args) != 1: parser.error("Wrong number of arguments") platforms = database.Platforms.find(name = extra_args[0]) if not platforms: raise SystemExit("Unknown platform name") platform = platforms[0] if options.deprecated is not None: platform.deprecated = options.deprecated if options.user_friendly_name: platform.user_friendly_name = options.user_friendly_name platform.commit() print "Updated " + str(platform) def list_platform(args): usage = """%prog list_platform List all defined platforms.""" parser = OptionParser(usage = usage) parser.add_option("--short", action = "store_true", help = "show the short names only") options, extra_args = parser.parse_args(args) if extra_args: parser.error("Extra arguments on the command line") # Ensure consistent ordering database.Platforms.select_args = 'ORDER BY name' for platform in database.Platforms.find(): if options.short: print platform.name else: desc = platform.name + ": " + platform.user_friendly_name if platform.deprecated: desc += " (Deprecated)" print desc def add_standard_platforms(args): usage = """%prog add_standard_platforms Add all standard platform definitions to the database. If some of them already exist they will not be modified.""" parser = OptionParser(usage = usage) options, extra_args = parser.parse_args(args) if extra_args: parser.error("Extra arguments on the command line") for name in standard_platforms.keys(): if database.Platforms.find(name = name): continue platform = database.Platform(create_time = time.time(), name = name, user_friendly_name = standard_platforms[name], deprecated = 0) platform.commit() print "Added " + str(platform) def add_app(args): usage = """%prog add [options] Register a new application in the database.""" hr_types = { 'no': 0, 'fine': 1, 'coarse': 2} parser = OptionParser(usage = usage) parser.add_option("--hr", choices = hr_types.keys(), default = "no", metavar = "(no|fine|coarse)", help = "set the homogeneous redundancy type") parser.add_option("--beta", action = "store_const", default = 0, const = 1, help = "the application is still in beta testing, and only " "users participating in the test program will run it") parser.add_option("--weight", type = "float", default = 1, metavar = "NUM", help = "set the weight of the application when application " "interleaving is enabled in the feeder") parser.add_option("--target_nresults", type = "int", default = 0, metavar = "NUM", help = "default number of replicas for adaptive replication") options, extra_args = parser.parse_args(args) if len(extra_args) != 2: parser.error("Wrong number of arguments") app = database.App(create_time = time.time(), name = extra_args[0], min_version = 0, deprecated = 0, user_friendly_name = extra_args[1], homogeneous_redundancy = hr_types[options.hr], weight = options.weight, beta = options.beta, target_nresults = options.target_nresults) try: app.commit() except Exception, e: print "Failed to add application" return print "Added " + str(app) def del_app(args): usage = """%prog delete [options] Delete application versions. This command deletes the matching database entries and all corresponding files under /apps and /download. If more than one of the --version, --platform and --plan_class options are specified, they are AND'ed together. If no version/platform/plan class options are given, all versions of the application are deleted and the application is removed from the 'app' table as well.""" parser = OptionParser(usage = usage) parser.add_option("--version", type = "float", metavar = "VER", help = "delete versions with the given version number") parser.add_option("--platform", metavar = "NAME", help = "delete versions for the given platform") parser.add_option("--plan_class", metavar = "NAME", help = "delete versions for the given plan class") options, extra_args = parser.parse_args(args) if len(extra_args) != 1: parser.error("Wrong number of arguments") apps = database.Apps.find(name = extra_args[0]) if not apps: raise SystemExit("No such application") app = apps[0] kwargs = { 'app': app } if options.version: kwargs['version_num'] = int(options.version * 100) if options.platform: platforms = database.Platforms.find(name = options.platform) if not platforms: raise SystemExit("Unknown platform name") kwargs['platform'] = platforms[0] if options.plan_class: kwargs['plan_class'] = options.plan_class for appver in database.AppVersions.find(**kwargs): do_delete_app_version(appver) # If the user requested to remove all versions, then remove the entry # from the 'app' table as well if not options.version and not options.platform and not options.plan_class: tmp = str(app) app.remove() print "Removed " + tmp def update_app(args): usage = """%prog update [options] Update the properties of the given application.""" hr_types = { 'no': 0, 'fine': 1, 'coarse': 2} parser = OptionParser(usage = usage) parser.add_option("--hr", choices = hr_types.keys(), default = "no", metavar = "(no|fine|coarse)", help = "set the homogeneous redundancy type") parser.add_option("--beta", action = "store_const", const = 1, help = "the application is still in beta testing, and only " "users participating in the test program will run it") parser.add_option("--no-beta", action = "store_const", dest = "beta", const = 0, help = "the application is no more in beta testing") parser.add_option("--weight", type = "float", metavar = "NUM", help = "set the weight of the application when application " "interleaving is enabled in the feeder") parser.add_option("--target_nresults", type = "int", metavar = "NUM", help = "default number of replicas for adaptive replication") parser.add_option("--user_friendly_name", metavar = "DESC", help = "set the user-friendly description of the application") parser.add_option("--min_version", type = "float", metavar = "VER", help = "set the minimum app version clients allowed to use") parser.add_option("--deprecated", action = "store_const", const = 1, help = "mark the application as deprecated") parser.add_option("--no-deprecated", dest = "deprecated", action = "store_const", const = 0, help = "remove the deprecation mark") options, extra_args = parser.parse_args(args) if len(extra_args) != 1: parser.error("Wrong number of arguments") apps = database.Apps.find(name = extra_args[0]) if not apps: raise SystemExit("No such application") app = apps[0] if options.hr is not None: app.homogeneous_redundancy = hr_types[options.hr] if options.beta is not None: app.beta = options.beta if options.weight is not None: app.weight = options.weight if options.target_nresults is not None: app.target_nresults = options.target_nresults if options.user_friendly_name: app.user_friendly_name = options.user_friendly_name if options.min_version is not None: app.min_version = int(options.min_version * 100) if options.deprecated is not None: app.deprecated = options.deprecated app.commit() print "Updated " + str(app) def list_app(args): usage = """%prog list List all applications and application versions.""" parser = OptionParser(usage = usage) parser.add_option("--no-versions", action = "store_true", default = False, help = "do not list application versions") options, extra_args = parser.parse_args(args) if extra_args: parser.error("Extra arguments on the command line") # Ensure consistent ordering for the output database.Apps.select_args = 'ORDER BY name' # It would be nice to order by platform name... database.AppVersions.select_args = 'ORDER BY version_num, platformid, plan_class' hr_classes = {0: 'No', 1: 'Fine-grained', 2: 'Coarse'} for app in database.Apps.find(): title = app.name + ": " + app.user_friendly_name print title print "-" * len(title) props = [] if app.deprecated: props.append("Deprecated.") if app.beta: props.append("Beta.") if app.min_version: props.append("Min. version: %.2f." % (float(app.min_version) / 100)) props.append(hr_classes[app.homogeneous_redundancy] + ' homogeneous redundancy.') props.append("Weight %g." % app.weight) if app.target_nresults: props.append(str(app.target_nresults) + " adaptive replicas.") print string.join(props, ' ') if options.no_versions: print continue print "Versions:\n" for appver in database.AppVersions.find(app = app): desc = "\t%5.2f" % (float(appver.version_num) / 100) desc += " " + appver.platform.name props = [] if appver.plan_class: props.append("Plan: " + appver.plan_class + ".") if appver.deprecated: props.append("Deprecated.") if appver.min_core_version: props.append("Min. core version: %g." % \ (float(appver.min_core_version) / 100)) if appver.max_core_version: props.append("Max. core version: %g." % \ (float(appver.max_core_version) / 100)) if props: desc += " (" + string.join(props, ' ') + ')' print desc print def update_appver(args): usage = """%prog update_appver [options] Update the properties of the given application version(s). If none of the --version, --platform, --plan_class options are specified, then all versions for the given application are updated.""" parser = OptionParser(usage = usage) parser.add_option("--version", type = "float", metavar = "VER", help = "update just the given version number") parser.add_option("--platform", metavar = "NAME", help = "update just the given platform") parser.add_option("--plan_class", metavar = "NAME", help = "update just the given plan class") parser.add_option("--min_core_version", type = "float", metavar = "VER", help = "set the minimum usable core client version") parser.add_option("--max_core_version", type = "float", metavar = "VER", help = "set the maximum usable core client version") parser.add_option("--deprecated", action = "store_const", const = 1, help = "mark this version as deprecated") parser.add_option("--no-deprecated", dest = "deprecated", action = "store_const", const = 0, help = "remove the deprecation mark") options, extra_args = parser.parse_args(args) if len(extra_args) != 1: parser.error("Wrong number of arguments") apps = database.Apps.find(name = extra_args[0]) if not apps: raise SystemExit("No such application") app = apps[0] kwargs = { 'app': app } if options.version: kwargs['version_num'] = int(options.version * 100) if options.platform: platforms = database.Platforms.find(name = options.platform) if not platforms: raise SystemExit("Unknown platform name") kwargs['platform'] = platforms[0] if options.plan_class: kwargs['plan_class'] = options.plan_class for appver in database.AppVersions.find(**kwargs): if options.deprecated is not None: appver.deprecated = options.deprecated if options.min_core_version is not None: appver.min_core_version = int(options.min_core_version * 100) if options.max_core_version is not None: appver.max_core_version = int(options.max_core_version * 100) appver.commit() print "Updated " + str(appver) command_table = { 'add': add_app, 'delete': del_app, 'update': update_app, 'list': list_app, 'update_appver': update_appver, 'add_platform': add_platform, 'delete_platform': del_platform, 'update_platform': update_platform, 'list_platform': list_platform, 'add_standard_platforms': add_standard_platforms} usage = """%%prog [global options] [command arguments] Available commands: %s Use "%%prog --help" to get a description of the command.""" % \ string.join(map(lambda x: "\t" + x, sorted(command_table.keys())), "\n") parser = OptionParser(usage=usage) parser.disable_interspersed_args() global_options, args = parser.parse_args() if not args: parser.error("Command is not provided") if not args[0] in command_table: parser.error("Unknown command " + args[0]) database.connect() command_table[args[0]](args[1:])