diff --git a/checkin_notes b/checkin_notes index e07a0888a5..817cb293a2 100755 --- a/checkin_notes +++ b/checkin_notes @@ -6480,3 +6480,30 @@ Eric Oct 2 2003 api/graphics_api.h lib/xml_util.[Ch] +Karl 2003/10/02 + - rewrote make_project and related; a lot of restructuring: + + - make_project only creates the project and database but doesn't add + platform, app, app_version, or core_version. Use `add' for these. + - database actions are through MySQLdb, removing need for mysql client + binary. + + tools/ + make_project + add + update_versions + upgrade (new) + py/Boinc/ + configxml.py + database.py + setup_project.py + tools.py + test/ + testbase.py + test_uc.py + sched/ + boinc_config.py (removed) + db/ + constraints.sql + doc/ + single_host_server.php diff --git a/py/Boinc/database.py b/py/Boinc/database.py index 7604406231..1d43f9ddbe 100644 --- a/py/Boinc/database.py +++ b/py/Boinc/database.py @@ -631,39 +631,42 @@ def _connectp(dbname, user, passwd, host='localhost'): boincdb = MySQLdb.connect(db=dbname,host=host,user=user,passwd=passwd, cursorclass=MySQLdb.cursors.DictCursor) -# def _connectm(module): -# _connectp(module.database, module.username, module.password) - -# def connect(readonly = False): -# """Connect if not already connected or if we're adding write permissions""" -# global boincdb -# if boincdb: -# if not readonly and boincdb.readonly: -# # re-open with write access -# boincdb.close() -# boincdb = None -# else: -# return 0 -# if readonly: -# import password_settings_r -# _connectm(password_settings_r) -# else: -# import password_settings -# _connectm(password_settings) -# boincdb.readonly = readonly -# return 1 - -def connect(config = None): +def connect(config = None, nodb = False): """Connect if not already connected, using config values.""" global boincdb if boincdb: return 0 config = config or configxml.default_config().config - _connectp(config.db_name, + if nodb: + db = '' + else: + db = config.db_name + _connectp(db, config.__dict__.get('db_user',''), config.__dict__.get('db_passwd', '')) return 1 +def _execute_sql_script(cursor, filename): + for query in open(filename).read().split(';'): + query = query.strip() + if not query: continue + cursor.execute(query) + +def create_database(config = None, drop_first = False): + ''' creates a new database. ''' + global boincdb + config = config or configxml.default_config().config + connect(config, nodb=True) + cursor = boincdb.cursor() + if drop_first: + cursor.execute("drop database if exists %s"%config.db_name) + cursor.execute("create database %s"%config.db_name) + cursor.execute("use %s"%config.db_name) + schema_path = os.path.join(boinc_path_config.TOP_SOURCE_DIR, 'db') + for file in ['schema.sql', 'constraints.sql']: + _execute_sql_script(cursor, os.path.join(schema_path, file)) + cursor.close() + # alias connect_default_config = connect diff --git a/py/Boinc/setup_project.py b/py/Boinc/setup_project.py index 338fcfcdd6..8867ad60e5 100644 --- a/py/Boinc/setup_project.py +++ b/py/Boinc/setup_project.py @@ -1,20 +1,14 @@ ## $Id$ # module for setting up a new project (either a real project or a test project -# - see testbase.py). -# -# (This used to be boinc/py/boinc.py.) +# see tools/makeproject, test/testbase.py). -# TODO: make things work if build_dir != src_dir - -# TODO: use database.py +# TODO: make sure things work if build_dir != src_dir import boinc_path_config -import version from Boinc import database, db_mid, configxml, tools from Boinc.boinc_db import * import os, sys, glob, time, shutil, re, random -# import MySQLdb class Options: pass @@ -27,7 +21,6 @@ options.have_init = False options.install_method = None options.echo_verbose = 1 options.is_test = False -options.client_bin_filename = version.CLIENT_BIN_FILENAME options.drop_db_first = False def init(): @@ -164,8 +157,11 @@ def force_symlink(src, dest): os.unlink(dest) my_symlink(src, dest) def rmtree(dir): - if os.path.exists(dir): - shutil.rmtree(dir) + # if os.path.exists(dir): + # shutil.rmtree(dir) + if not dir or dir == '/' or dir == '.' or ' ' in dir: + raise Exception + os.system("rm -rf %s"%dir) def _remove_trail(s, suffix): if s.endswith(suffix): @@ -240,122 +236,91 @@ def _check_vars(dict, **names): # result = db.use_result() # return result and result.fetch_row(0,1) -def num_results(db): +def num_results(): return database.Results.count() -def num_results_unsent(db): +def num_results_unsent(): return database.Results.count(server_state = RESULT_SERVER_STATE_UNSENT) -def num_results_in_progress(db): +def num_results_in_progress(): return database.Results.count(server_state = RESULT_SERVER_STATE_IN_PROGRESS) -def num_results_over(db): +def num_results_over(): return database.Results.count(server_state = RESULT_SERVER_STATE_OVER) -def num_wus(db): +def num_wus(): return database.Workunits.count() -def num_wus_assimilated(db): +def num_wus_assimilated(): return database.Workunits.count(assimilate_state = ASSIMILATE_DONE) -def num_wus_to_transition(db): +def num_wus_to_transition(): return database.Workunits.count(_extra_params = 'transition_time<%d'%(time.time()+30*86400)) -def query_yesno(str): - '''Query user; default Yes''' - verbose_echo(0,'') - print str, "[Y/n] ", - return not raw_input().strip().lower().startswith('n') - -def query_noyes(str): - '''Query user; default No''' - verbose_echo(0,'') - print str, "[y/N] ", - return raw_input().strip().lower().startswith('y') - def build_command_line(cmd, **kwargs): for (key, value) in kwargs.items(): cmd += " -%s '%s'" %(key,value) return cmd -# class Platform: -# def __init__(self, name, user_friendly_name=None): -# self.name = name -# self.user_friendly_name = user_friendly_name or name +def install_boinc_files(dest_dir): + def dir(*dirs): + return apply(os.path.join,(dest_dir,)+dirs) -# class CoreVersion: -# def __init__(self): -# self.version = 1 -# self.platform = Platform(version.PLATFORM) -# self.exec_dir = builddir('client') -# self.exec_name = options.client_bin_filename + install_glob(srcdir('html_user/*.php'), dir('html_user/')) + install_glob(srcdir('html_user/*.inc'), dir('html_user/')) + install_glob(srcdir('html_user/class/*.inc'), dir('html_user/class/')) + install_glob(srcdir('html_user/include/*.inc'), dir('html_user/include/')) + install_glob(srcdir('html_ops/*.php'), dir('html_ops/')) + install_glob(srcdir('html_ops/*.inc'), dir('html_ops/')) + install(builddir('tools/country_select'), dir('html_user/')) -# class App: -# def __init__(self, name): -# assert(name) -# self.name = name + # copy all the backend programs + map(lambda (s): install(builddir('sched',s), dir('cgi-bin',s)), + [ 'cgi', 'file_upload_handler']) + map(lambda (s): install(builddir('sched',s), dir('bin',s)), + [ 'make_work', 'feeder', 'transitioner', 'validate_test', + 'file_deleter', 'assimilator' ]) + map(lambda (s): install(srcdir('sched',s), dir('bin',s)), + [ 'start', 'stop', 'status', + 'grep_logs' ]) + map(lambda (s): install(srcdir('tools',s), dir('bin',s)), + [ 'boinc_path_config.py', 'add', 'dbcheck_files_exist', + 'upgrade' ]) -# class AppVersion: -# def __init__(self, app, appversion = 1, exec_names=None): -# self.exec_names = [] -# self.exec_dir = builddir('apps') -# self.exec_names = exec_names or [app.name] -# self.app = app -# self.version = appversion -# self.platform = Platform(version.PLATFORM) class Project: def __init__(self, - short_name, long_name, appname=None, - project_dir=None, master_url=None, cgi_url=None, - core_versions=None, key_dir=None, - apps=None, app_versions=None, - resource_share=None): + short_name, long_name, + project_dir=None,key_dir=None, + master_url=None, cgi_url=None, + db_name=None): init() - self.config_options = [] - self.config_daemons = [] - self.short_name = short_name or 'test_'+appname - self.long_name = long_name or 'Project ' + self.short_name.replace('_',' ').capitalize() - self.db_passwd = '' - self.shmem_key = generate_shmem_key() - self.resource_share = resource_share or 1 - self.output_level = 3 - self.master_url = master_url or os.path.join(options.html_url , self.short_name , '') - self.download_url = os.path.join(self.master_url, 'download') - self.cgi_url = cgi_url or os.path.join(options.cgi_url, self.short_name) - self.upload_url = os.path.join(self.cgi_url , 'file_upload_handler') - self.scheduler_url = os.path.join(self.cgi_url , 'cgi') - self.project_dir = project_dir or os.path.join(options.projects_dir , self.short_name) - self.download_dir = os.path.join(self.project_dir , 'download') - self.upload_dir = os.path.join(self.project_dir , 'upload') - self.key_dir = key_dir or os.path.join(self.project_dir , 'keys') - self.user_name = options.user_name - self.db_name = self.user_name + '_' + self.short_name + self.short_name = short_name + self.long_name = long_name or 'Project ' + self.short_name.replace('_',' ').capitalize() + + self.project_dir = project_dir or os.path.join(options.projects_dir, self.short_name) + + self.config = configxml.ConfigFile(self.dir('config.xml')).init_empty() + config = self.config.config + + config.user_name = options.user_name + config.db_name = db_name or config.user_name + '_' + self.short_name + config.db_passwd = '' + config.shmem_key = generate_shmem_key() + config.output_level = 3 + + config.master_url = master_url or os.path.join(options.html_url , self.short_name , '') + config.download_url = os.path.join(config.master_url, 'download') + config.cgi_url = cgi_url or os.path.join(options.cgi_url, self.short_name) + config.upload_url = os.path.join(config.cgi_url , 'file_upload_handler') + self.scheduler_url = os.path.join(config.cgi_url , 'cgi') + config.download_dir = os.path.join(self.project_dir , 'download') + config.upload_dir = os.path.join(self.project_dir , 'upload') + config.key_dir = key_dir or os.path.join(self.project_dir , 'keys') self.project_php_file = srcdir('html_user/project.inc.sample') self.project_specific_prefs_php_file = srcdir('html_user/project_specific_prefs.inc.sample') - self.core_versions = core_versions or [CoreVersion()] - self.app_versions = app_versions or [AppVersion(App(appname))] - self.apps = apps or unique(map(lambda av: av.app, self.app_versions)) - self.platforms = [Platform(version.PLATFORM)] - # convenience vars: - self.app_version = self.app_versions[0] - self.app = self.apps[0] def dir(self, *dirs): return apply(os.path.join,(self.project_dir,)+dirs) def keydir(self, *dirs): - return apply(os.path.join,(self.key_dir,)+dirs) - - def run_db_script(self, script): - shell_call('mysql %s < %s' % (self.db_name,srcdir('db', script))) - - def drop_db_if_exists(self): - shell_call('echo "drop database if exists %s" | mysql' % self.db_name) - - def create_db(self): - if options.drop_db_first: - self.drop_db_if_exists() - shell_call('echo "create database %s" | mysql' % self.db_name) - - def db_open(self): - return MySQLdb.connect(db=self.db_name) + return apply(os.path.join,(self.config.config.key_dir,)+dirs) def create_keys(self): if not os.path.exists(self.keydir()): @@ -364,7 +329,7 @@ class Project: _gen_key(self.keydir('code_sign')) def query_create_keys(self): - return query_yesno("Keys don't exist in %s; generate them?"%self.key_dir) + return query_yesno("Keys don't exist in %s; generate them?"%self.keydir()) def keys_exist(self): keys = ['upload_private', 'upload_public', @@ -395,28 +360,23 @@ class Project: self.create_keys() # copy the user and administrative PHP files to the project dir, - verbose_echo(1, "Setting up server files: copying html directories") + verbose_echo(1, "Setting up server files: copying files") + + install_boinc_files(self.dir()) - install_glob(srcdir('html_user/*.php'), self.dir('html_user/')) - install_glob(srcdir('html_user/*.inc'), self.dir('html_user/')) - install_glob(srcdir('html_user/class/*.inc'), self.dir('html_user/class/')) - install_glob(srcdir('html_user/include/*.inc'), self.dir('html_user/include/')) install_glob(srcdir('html_user/*.txt'), self.dir('html_user/')) - install_glob(srcdir('html_ops/*.php'), self.dir('html_ops/')) - install_glob(srcdir('html_ops/*.inc'), self.dir('html_ops/')) - install(builddir('tools/country_select'), self.dir('html_user/')) install(self.project_php_file, self.dir('html_user', 'project_specific', 'project.inc')) install(self.project_specific_prefs_php_file, self.dir('html_user', 'project_specific', 'project_specific_prefs.inc')) - my_symlink(self.download_dir, self.dir('html_user', 'download')) + my_symlink(self.config.config.download_dir, self.dir('html_user', 'download')) + # Copy the sched server in the cgi directory with the cgi names given # source_dir/html_usr/schedulers.txt # - verbose_echo(1, "Setting up server files: copying cgi programs"); if scheduler_file: r = re.compile('([^<]+)', re.IGNORECASE) f = open(self.dir('html_user', scheduler_file)) @@ -432,91 +392,20 @@ class Project: else: scheduler_file = 'schedulers.txt' f = open(self.dir('html_user', scheduler_file), 'w') - print >>f, "" + self.scheduler_url, "" + print >>f, "" + self.scheduler_url.strip(), "" f.close() - # copy all the backend programs - map(lambda (s): install(builddir('sched',s), self.dir('cgi-bin',s)), - [ 'cgi', 'file_upload_handler']) - map(lambda (s): install(builddir('sched',s), self.dir('bin',s)), - [ 'make_work', 'feeder', 'transitioner', 'validate_test', - 'file_deleter', 'assimilator' ]) - map(lambda (s): install(srcdir('sched',s), self.dir('bin',s)), - [ 'start', 'stop', 'status', - 'boinc_config.py', 'grep_logs' ]) - verbose_echo(1, "Setting up database") - self.create_db() - map(self.run_db_script, [ 'schema.sql' ]) - - database.connect() + database.create_database(config = self.config.config, + drop_first = options.drop_db_first) self.project = database.Project(short_name = self.short_name, long_name = self.long_name) self.project.commit() - verbose_echo(1, "Setting up database: adding %d apps(s)" % len(self.apps)) - for app in self.apps: - db.query("insert into app(name, create_time) values ('%s', %d)" %( - app.name, time.time())) + verbose_echo(1, "Setting up server files: writing config files") - self.platforms = unique(map(lambda a: a.platform, self.app_versions)) - verbose_echo(1, "Setting up database: adding %d platform(s)" % len(self.platforms)) - - db.close() - - for platform in self.platforms: - cmd = build_command_line("old_add platform", - db_name = self.db_name, - platform_name = platform.name, - user_friendly_name = platform.user_friendly_name) - run_tool(cmd) - - verbose_echo(1, "Setting up database: adding %d core version(s)" % len(self.core_versions)) - for core_version in self.core_versions: - cmd = build_command_line("old_add core_version", - db_name = self.db_name, - platform_name = core_version.platform.name, - version = core_version.version, - download_dir = self.download_dir, - download_url = self.download_url, - exec_dir = core_version.exec_dir, - exec_files = core_version.exec_name) - run_tool(cmd) - - verbose_echo(1, "Setting up database: adding %d app version(s)" % len(self.app_versions)) - for app_version in self.app_versions: - app = app_version.app - cmd = ("old_add app_version -db_name %s -app_name '%s'" + - " -platform_name %s -version %s -download_dir %s -download_url %s" + - " -code_sign_keyfile %s -exec_dir %s -exec_files") % ( - self.db_name, app.name, app_version.platform.name, - app_version.version, - self.download_dir, - self.download_url, - os.path.join(self.key_dir, 'code_sign_private'), - app_version.exec_dir) - for exec_name in app_version.exec_names: - check_app_executable(exec_name) - cmd += ' ' + exec_name - run_tool(cmd) - - verbose_echo(1, "Setting up server files: writing config files"); - - config = map_xml(self, - [ 'db_name', 'db_passwd', 'shmem_key', - 'key_dir', 'download_url', 'download_dir', - 'upload_url', 'upload_dir', 'project_dir', 'user_name', - 'cgi_url', - 'output_level' ]) - self.config_options = config.split('\n') - self.write_config() - - # edit "index.php" in the user HTML directory to have the right file - # as the source for scheduler_urls; default is schedulers.txt - - macro_substitute_inplace('FILE_NAME', scheduler_file, - self.dir('html_user', 'index.php')) + self.config.write() # create symbolic links to the CGI and HTML directories verbose_echo(1, "Setting up server files: linking cgi programs") @@ -526,12 +415,6 @@ class Project: force_symlink(self.dir('html_user'), os.path.join(options.html_dir, self.short_name)) force_symlink(self.dir('html_ops'), os.path.join(options.html_dir, self.short_name+'_admin')) - # show the URLs for user and admin sites - # admin_url = os.path.join("html_user", self.short_name+'_admin/') - - # verbose_echo(2, "Master URL: " + self.master_url) - # verbose_echo(2, "Admin URL: " + admin_url) - def http_password(self, user, password): 'Adds http password protection to the html_ops directory' passwd_file = self.dir('html_ops', '.htpassword') @@ -585,11 +468,11 @@ class Project: self._run_sched_prog(prog, '-d 3 -one_pass '+cmdline) def sched_install(self, prog, **kwargs): for cmdline in self._build_sched_commandlines(prog, kwargs): - self.config_daemons.append("%s -d 3 %s" %(prog, cmdline)) - self.write_config() - def sched_uninstall(self, prog): - self.config_daemons = filter(lambda l: l.find(prog)==-1, self.config_daemons) - self.write_config() + self.config.daemons.make_node_and_append("daemon").cmd = "%s -d 3 %s" %(prog, cmdline) + self.config.write() + # def sched_uninstall(self, prog): + # self.config_daemons = XXX filter(lambda l: l.find(prog)==-1, self.config_daemons) + # self.config.write() def start_stripcharts(self): map(lambda l: self.copy(os.path.join('stripchart', l), 'cgi-bin/'), @@ -613,15 +496,10 @@ class Project: def maybe_stop(self): if self.started: self.stop() - def write_config(self): - f = open(self.dir('config.xml'), 'w') - print >>f, '' - print >>f, ' ' - for line in self.config_options: - print >>f, " ", line - print >>f, ' ' - print >>f, ' ' - for daemon in self.config_daemons: - print >>f, " %s"%daemon - print >>f, ' ' - print >>f, '' +def query_noyes(str): + verbose_echo(0,'') + return tools.query_noyes(str) + +def query_yesno(str): + verbose_echo(0,'') + return tools.query_yesno(str) diff --git a/py/Boinc/tools.py b/py/Boinc/tools.py index d5cab193ed..85e81e3c11 100644 --- a/py/Boinc/tools.py +++ b/py/Boinc/tools.py @@ -14,10 +14,10 @@ def file_size(path): f.seek(0,2) return f.tell() -def sign_executable(executable_path): +def sign_executable(executable_path, quiet=False): '''Returns signed text for executable''' config = configxml.default_config() - print 'Signing', executable_path + if not quiet: print 'Signing', executable_path code_sign_key = os.path.join(config.config.key_dir, 'code_sign_private') sign_executable_path = os.path.join(boinc_path_config.TOP_BUILD_DIR, 'tools','sign_executable') @@ -29,7 +29,7 @@ def sign_executable(executable_path): raise SystemExit("Couldn't sign executable %s"%executable_path) return signature_text -def process_executable_file(file, signature_text=None): +def process_executable_file(file, signature_text=None, quiet=False): '''Handle a new executable file to be added to the database. 1. Copy file to download_dir if necessary. @@ -42,7 +42,7 @@ def process_executable_file(file, signature_text=None): file_dir, file_base = os.path.split(file) target_path = os.path.join(config.config.download_dir, file_base) if file_dir != config.config.download_dir: - print "Copying %s to %s"%(file_base, config.config.download_dir) + if not quiet: print "Copying %s to %s"%(file_base, config.config.download_dir) shutil.copy(file, target_path) xml = ''' @@ -60,7 +60,7 @@ def process_executable_file(file, signature_text=None): xml += ' %f\n\n' % file_size(target_path) return xml -def process_app_version(app, version_num, exec_files, signature_files={}): +def process_app_version(app, version_num, exec_files, signature_files={}, quiet=False): """Return xml for application version app is an instance of database.App @@ -80,8 +80,8 @@ def process_app_version(app, version_num, exec_files, signature_files={}): if signature_file: signature_text = open(signature_file).read() else: - signature_text = sign_executable(exec_file) - xml_doc += process_executable_file(exec_file, signature_text) + signature_text = sign_executable(exec_file, quiet=quiet) + xml_doc += process_executable_file(exec_file, signature_text, quiet=quiet) xml_doc += ('\n'+ ' %s\n'+ @@ -101,3 +101,13 @@ def process_app_version(app, version_num, exec_files, signature_files={}): xml_doc += '\n' return xml_doc + +def query_yesno(str): + '''Query user; default Yes''' + print str, "[Y/n] ", + return not raw_input().strip().lower().startswith('n') + +def query_noyes(str): + '''Query user; default No''' + print str, "[y/N] ", + return raw_input().strip().lower().startswith('y') diff --git a/sched/boinc_config.py b/sched/boinc_config.py deleted file mode 100755 index 760119d875..0000000000 --- a/sched/boinc_config.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env python - -# $Id$ - -# boinc_config.py - module to read and parse config.xml, run_state.xml - -''' -SYNOPSIS: parses and writes config.xml and run_state.xml - -USAGE: from boinc_config import * - config = BoincConfig('confing.xml').read() - run_state = BoincRunState('run_state.xml').read() - print config.config.db_name - print config.tasks[4].cmd - run_state.enabled = True - new_task = newConfigDict() - new_task.cmd = "echo hi | mail quarl" - config.tasks.append(new_task) - config.write() - run_state.write() - -TODO: create a Boinc package and remove these Boinc prefix qualifiers -''' - -import sys -import xml.dom.minidom - -CONFIG_FILE = '../config.xml' -RUN_STATE_FILE = '../run_state.xml' - -def _append_new_element(parent_node, name): - new_element = xml.dom.minidom.Element(name) - if isinstance(parent_node,xml.dom.minidom.Document): - new_element.ownerDocument = parent_node - else: - assert(parent_node.ownerDocument) - new_element.ownerDocument = parent_node.ownerDocument - parent_node.appendChild(new_element) - return new_element - -def _get_elements(node, name): - return node.getElementsByTagName(name) - -def _get_element(node, name, optional=True): - try: - return _get_elements(node,name)[0] - except IndexError: - if optional: - return _append_new_element(node, name) - raise SystemExit("ERROR: Couldn't find xml node <%s>"% name) - -def _None2Str(object): - if object == None: - return '' - else: - return object - -def _get_element_data(node): - return node and _None2Str(node.firstChild and node.firstChild.data) - -def _get_element_int(node, default=0): - try: - return int(_get_element_data(node)) - except: - return default - -def _get_child_elements(node): - return filter(lambda node: node.nodeType == node.ELEMENT_NODE, node.childNodes) - -def _set_element(node, new_data): - if node.firstChild and node.firstChild.data: - node.firstChild.data = str(new_data) - else: - new_data_node = xml.dom.minidom.Text() - new_data_node.data = str(new_data) - new_data_node.ownerDocument = node.ownerDocument - node.appendChild(new_data_node) - -class ConfigDict: - def __init__(self, dom_node): - self._node = dom_node - self._name = self._node.nodeName - for node in _get_child_elements(self._node): - self.__dict__[node.nodeName] = _get_element_data(node) - def save(self): - for key in self.__dict__: - if key.startswith('_'): continue - _set_element( _get_element(self._node,key,1), str(self.__dict__[key]) ) - def debug_print(self): - for key in self.__dict__.keys(): - print key.rjust(15), '=', self.__dict__[key] - -class ConfigDictList(list): - def __init__(self, dom_node, item_class=ConfigDict): - self._node = dom_node - list.__init__(self, map(item_class, _get_child_elements(self._node))) - def save(self): - map(ConfigDict.save, self) - def make_node_and_append(self, name): - '''Make a new ConfigDict and append it. Returns new ConfigDict.''' - new_element = _append_new_element(self._node, name) - new_cd = ConfigDict(new_element) - self.append(new_cd) - return new_cd - -# base class for xml config files -class XMLConfig: - def __init__(self, filename): - self.filename = filename - def read(self, failopen_ok=False): - try: - self.xml = xml.dom.minidom.parse(self.filename) - except IOError, e: - if not failopen_ok: - raise - print >>sys.stderr, "Warning:", e - # self.xml = xml.dom.minidom.Document() - self.xml = xml.dom.minidom.parseString(self.default_xml) - self._get_elements() - return self - def _get_elements(self): - pass - def write(self, output=None): - self._set_elements() - if not output: - output = open(self.filename,'w') - self.xml.writexml(output) - print >>output - return self - def _set_elements(self): - pass - -# normal config file -class BoincConfig(XMLConfig): - ''' - embodies config.xml - Public attributes: - config - ConfigDict - tasks - list of ConfigDict elements - ''' - def _get_elements(self): - self.xml_boinc = _get_element(self.xml, 'boinc', optional=False) - self.xml_config = _get_element(self.xml_boinc, 'config', optional=False) - self.xml_tasks = _get_element(self.xml_boinc, 'tasks') - self.xml_daemons = _get_element(self.xml_boinc, 'daemons') - self.config = ConfigDict(self.xml_config) - self.daemons = ConfigDictList(self.xml_daemons) - self.tasks = ConfigDictList(self.xml_tasks) - def _set_elements(self): - self.config.save() - self.daemons.save() - self.tasks.save() - def debug_print_all(self): - '''print everything to stdout.''' - - print 'Debug dump of', self.filename - print '-- parsed xml -------------------------------------------------------' - self.xml.writexml(sys.stdout) - print - print '-- Config -----------------------------------------------------------' - self.config.debug_print() - print - print '-- Daemons ------------------------------------------------------------' - for daemon in self.daemons: - daemon.debug_print() - print - print - print '-- Tasks ------------------------------------------------------------' - for task in self.tasks: - task.debug_print() - print - default_xml = '' - -# keeps BoincCron's timestamp status file -class BoincRunState(XMLConfig): - ''' - embodies run_state.xml - Public attributes: - tasks - list of ConfigDict elements - enabled - boolean - ''' - def _get_elements(self): - self.xml_boinc = _get_element(self.xml, 'boinc', optional=False) - self.xml_tasks = _get_element(self.xml_boinc, 'tasks') - self.xml_enabled = _get_element(self.xml_boinc, 'enabled') - self.tasks = ConfigDictList(self.xml_tasks) - self.enabled = _get_element_int(self.xml_enabled) - def _set_elements(self): - _set_element( self.xml_enabled, self.enabled ) - self.tasks.save() - default_xml = '' - -if __name__ == '__main__': - config = BoincConfig('config.xml') - config.read() - # print "setting config.enabled = True" - # config.enabled = True - config.debug_print_all() - print " -- saving xml and rewriting -----------------------------------------------" - config.write(sys.stdout) diff --git a/test/test_uc.py b/test/test_uc.py index cfbe0d04c2..56db824c83 100755 --- a/test/test_uc.py +++ b/test/test_uc.py @@ -9,16 +9,24 @@ from testbase import * class UserUC(User): - def __init__(self): - User.__init__(self) - self.project_prefs = "\nfoobar\n" - self.global_prefs = """ + def init(self): + self.User.__init__() + self.project_prefs = """ + +foobar + + +""" + self.global_prefs = """ + 0 2 1 400000 -""" + + +""" class WorkUC(Work): def __init__(self, redundancy, **kwargs): @@ -28,7 +36,7 @@ class WorkUC(Work): self.input_files = ['input'] self.__dict__.update(kwargs) -class ResultUC(Result): +class ResultUC(ExpectedResult): def __init__(self): self.stderr_out = MATCH_REGEXPS([ "", @@ -36,7 +44,7 @@ class ResultUC(Result): "APP: upper_case: argv[[]0[]] is upper_case", "APP: upper_case ending, wrote \\d+ chars"]) -class ResultComputeErrorUC(ResultComputeError): +class ResultComputeErrorUC(ExpectedResultComputeError): def __init__(self): self.stderr_out = MATCH_REGEXPS([ """ APP: upper_case: starting, argc \\d+"""]) diff --git a/test/testbase.py b/test/testbase.py index 3c1c8cf5e7..f7fd16b441 100644 --- a/test/testbase.py +++ b/test/testbase.py @@ -19,15 +19,16 @@ import cgiserver options.have_init_t = False options.echo_overwrite = False +options.client_bin_filename = version.CLIENT_BIN_FILENAME def test_init(): if options.have_init_t: return options.have_init_t = True - if not os.path.exists('test_uc.py'): + if not os.path.exists('testbase.py'): os.chdir(os.path.join(boinc_path_config.TOP_SOURCE_DIR,'test')) - if not os.path.exists('test_uc.py'): - raise SystemExit('Could not find boinc_db.py anywhere') + if not os.path.exists('testbase.py'): + raise SystemExit('Could not find testbase.py anywhere') #options.program_path = os.path.realpath(os.path.dirname(sys.argv[0])) options.program_path = os.getcwd() @@ -170,13 +171,13 @@ def dict_match(dic, resultdic): format = "result %s: unexpected %s '%s' (expected '%s')" error( format % (id, key, found, expected)) -class Result: +class ExpectedResult: def __init__(self): self.server_state = RESULT_SERVER_STATE_OVER self.client_state = RESULT_FILES_UPLOADED self.outcome = RESULT_OUTCOME_SUCCESS -class ResultComputeError: +class ExpectedResultComputeError: def __init__(self): self.server_state = RESULT_SERVER_STATE_OVER self.client_state = RESULT_COMPUTE_DONE @@ -228,24 +229,38 @@ def get_redundancy_args(num_wu = None, redundancy = None): return (num_wu, redundancy) class TestProject(Project): - def __init__(self, works, expected_result, + def __init__(self, works, expected_result, appname=None, num_wu=None, redundancy=None, users=None, hosts=None, add_to_list=True, + apps=None, app_versions=None, core_versions=None, + resource_share=None, **kwargs): test_init() if add_to_list: all_projects.append(self) - kwargs['short_name'] = kwargs.get('short_name') or 'test_'+kwargs['appname'] + kwargs['short_name'] = kwargs.get('short_name') or 'test_'+appname kwargs['long_name'] = kwargs.get('long_name') or 'Project ' + kwargs['short_name'].replace('_',' ').capitalize() (num_wu, redundancy) = get_redundancy_args(num_wu, redundancy) + self.resource_share = resource_share or 1 self.num_wu = num_wu self.redundancy = redundancy self.expected_result = expected_result self.works = works self.users = users or [User()] self.hosts = hosts or [Host()] + + self.platforms = [Platform()] + self.core_versions = core_versions or [CoreVersion(self.platforms[0])] + self.app_versions = app_versions or [AppVersion(App(appname), + self.platforms[0], + appname)] + self.apps = apps or unique(map(lambda av: av.app, self.app_versions)) + # convenience vars: + self.app_version = self.app_versions[0] + self.app = self.apps[0] + # convenience vars: self.work = self.works[0] self.user = self.users[0] @@ -263,28 +278,6 @@ class TestProject(Project): '''Overrides Project::query_create_keys() to always return true''' return True - def install_project_users(self): - db = self.db_open() - verbose_echo(1, "Setting up database: adding %d user(s)" % len(self.users)) - for user in self.users: - if user.project_prefs: - pp = "\n%s\n\n" % user.project_prefs - else: - pp = '' - if user.global_prefs: - gp = "\n%s\n\n" % user.global_prefs - else: - gp = '' - - db.query(("insert into user values (0, %d, '%s', '%s', '%s', " + - "'Peru', '12345', 0, 0, 0, '%s', '%s', 0, 'home', '', 0, 1, 0)") % ( - time.time(), - user.email_addr, - user.name, - user.authenticator, - gp, - pp)) - def install_works(self): for work in self.works: work.install(self) @@ -295,10 +288,30 @@ class TestProject(Project): host.add_user(user, self) host.install() + def install_platforms_versions(self): + def commit(list): + for item in list: item.commit() + + self.platforms = unique(map(lambda a: a.platform, self.app_versions)) + verbose_echo(1, "Setting up database: adding %d platform(s)" % len(self.platforms)) + commit(self.platforms) + + verbose_echo(1, "Setting up database: adding %d core version(s)" % len(self.core_versions)) + commit(self.core_versions) + + verbose_echo(1, "Setting up database: adding %d apps(s)" % len(self.apps)) + commit(self.apps) + + verbose_echo(1, "Setting up database: adding %d app version(s)" % len(self.app_versions)) + commit(self.app_versions) + + verbose_echo(1, "Setting up database: adding %d user(s)" % len(self.users)) + commit(self.users) + def install(self): self.init_install() self.install_project() - self.install_project_users() + self.install_platforms_versions() self.install_works() self.install_hosts() @@ -318,14 +331,12 @@ class TestProject(Project): If more than X seconds have passed than just assume something is broken and return.''' - db = self.db_open() timeout = time.time() + 3*60 - while (num_wus_assimilated(db) < self.num_wu) or num_wus_to_transition(db): + while (num_wus_assimilated() < self.num_wu) or num_wus_to_transition(): time.sleep(.5) if time.time() > timeout: error("run_finish_wait(): timed out waiting for workunits to assimilate/transition") break - db.close() def check(self): # verbose_sleep("Sleeping to allow server daemons to finish", 5) @@ -337,17 +348,16 @@ class TestProject(Project): # self.check_deleted("upload/uc_wu_%d_0", count=self.num_wu) def progress_meter_ctor(self): - self.db = self.db_open() + pass def progress_meter_status(self): return "WUs: [%dassim/%dtotal/%dtarget] Results: [%dunsent,%dinProg,%dover/%dtotal]" % ( - num_wus_assimilated(self.db), num_wus(self.db), self.num_wu, - num_results_unsent(self.db), - num_results_in_progress(self.db), - num_results_over(self.db), - num_results(self.db)) + num_wus_assimilated(), num_wus(), self.num_wu, + num_results_unsent(), + num_results_in_progress(), + num_results_over(), + num_results()) def progress_meter_dtor(self): - self.db.close() - self.db = None + pass def _disable(self, *path): '''Temporarily disable a file to test exponential backoff''' @@ -382,13 +392,11 @@ class TestProject(Project): exit_status ''' expected_count = expected_count or self.redundancy - db = self.db_open() - rows = db_query(db,"select * from result") - for row in rows: - dict_match(matchresult, row) - db.close() - if len(rows) != expected_count: - error("expected %d results, but found %d" % (expected_count, len(rows))) + results = database.Results.find() + for result in results: + dict_match(matchresult, result.__dict__) + if len(results) != expected_count: + error("expected %d results, but found %d" % (expected_count, len(results))) def check_files_match(self, result, correct, count=None): '''if COUNT is specified then [0,COUNT) is mapped onto the %d in RESULT''' @@ -414,16 +422,47 @@ class TestProject(Project): return errs return check_exists(self.dir(file)) +class Platform(database.Platform): + def __init__(self, name=None, user_friendly_name=None): + database.Platform.__init__(self) + self.name = name or version.PLATFORM + self.user_friendly_name = user_friendly_name or name +class CoreVersion(database.CoreVersion): + def __init__(self, platform): + database.CoreVersion.__init__(self) + self.version_num = 1 + self.platform = platform + self.xml_doc = tools.process_executable_file( + os.path.join(boinc_path_config.TOP_BUILD_DIR,'client', + options.client_bin_filename), + quiet=True) -class User: - '''represents an account on a particular project''' +class User(database.User): def __init__(self): + database.User.__init__(self,id=None) self.name = 'John' self.email_addr = 'john@boinc.org' self.authenticator = "3f7b90793a0175ad0bda68684e8bd136" - self.project_prefs = None - self.global_prefs = None + +class App(database.App): + def __init__(self, name): + database.App.__init__(self,id=None) + self.name = name + self.min_version = 1 + +class AppVersion(database.AppVersion): + def __init__(self, app, platform, exec_file): + database.AppVersion.__init__(self,id=None) + self.app = app + self.version_num = 1 + self.platform = platform + self.xml_doc = tools.process_app_version( + app, self.version_num, + [os.path.join(boinc_path_config.TOP_BUILD_DIR,'apps',exec_file)], + quiet=True) + self.min_core_version = 1 + self.max_core_version = 999 class HostList(list): def run(self, asynch=False): map(lambda i: i.run(asynch=asynch), self) @@ -457,12 +496,12 @@ class Host: verbose_echo(1, "Setting up host '%s': creating account files" % self.name); for (user,project) in map(None,self.users,self.projects): - filename = self.dir(account_file_name(project.master_url)) + filename = self.dir(account_file_name(project.config.config.master_url)) verbose_echo(2, "Setting up host '%s': writing %s" % (self.name, filename)) f = open(filename, "w") print >>f, "" - print >>f, map_xml(project, ['master_url']) + print >>f, map_xml(project.config.config, ['master_url']) print >>f, map_xml(user, ['authenticator']) if user.project_prefs: print >>f, user.project_prefs @@ -509,6 +548,7 @@ class Host: _url_to_filename(project.master_url), filename)) + # TODO: do this in Python class Work: def __init__(self, redundancy, **kwargs): self.input_files = [] @@ -534,7 +574,8 @@ class Work: self.app = project.app_versions[0].app for input_file in unique(self.input_files): install(os.path.realpath(input_file), - os.path.join(project.download_dir,os.path.basename(input_file))) + os.path.join(project.config.config.download_dir, + os.path.basename(input_file))) # simulate multiple data servers by making symbolic links to the # download directory @@ -558,11 +599,11 @@ class Work: os.symlink(handler, newhandler) cmd = build_command_line("create_work", - db_name = project.db_name, - download_dir = project.download_dir, - upload_url = project.upload_url, - download_url = project.download_url, - keyfile = os.path.join(project.key_dir,'upload_private'), + db_name = project.config.config.db_name, + download_dir = project.config.config.download_dir, + upload_url = project.config.config.upload_url, + download_url = project.config.config.download_url, + keyfile = os.path.join(project.config.config.key_dir,'upload_private'), appname = self.app.name, rsc_fpops_est = self.rsc_fpops_est, rsc_fpops_bound = self.rsc_fpops_bound, diff --git a/tools/add b/tools/add index 9622ef47f5..e3e36ac523 100755 --- a/tools/add +++ b/tools/add @@ -2,6 +2,9 @@ # $Id$ + +#XXX TODO: add app should modify config.xml to add application-specific daemons + ''' add items to the BOINC database. diff --git a/tools/make_project b/tools/make_project index 2a924ab82f..7eb16e0402 100755 --- a/tools/make_project +++ b/tools/make_project @@ -8,17 +8,27 @@ # TODO: create 'apps' subdirectory and set it in config.xml # TODO: use configxml module -import sys, os, getopt, re -sys.path.append('../py') -from version import * -from boinc import * +import boinc_path_config +from Boinc.setup_project import * +from Boinc import database, db_mid, configxml, tools +import sys, os, getopt, re, socket + +def gethostname(): + try: + return socket.gethostbyaddr(socket.gethostname())[0] + except: + return 'localhost' + +def isurl(s): + return s.startswith('http://') or s.startswith('https://') argv0 = sys.argv[0] HOME = os.path.expanduser('~') USER = os.environ['USER'] +NODENAME = gethostname() HELP = """ -syntax: %(argv0)s [options] project-dir-name 'Project Long Name' app-exe 'AppName' +syntax: %(argv0)s [options] project ['Project Long Name'] Creates a new project with given name with everything running on a single server. @@ -35,15 +45,17 @@ Dir-options: --base default: $HOME (%(HOME)s) --key_dir default: BASE/keys --project_root default: BASE/projects/PROJECT - --url_base REQUIRED; e.g.: http://maggie.ssl.berkeley.edu/ + --url_base default: http://$NODENAME/ (http://%(NODENAME)s/) --html_user_url default: URL_BASE/PROJECT/ --html_ops_url default: URL_BASE/PROJECT_ops/ --cgi_url default: URL_BASE/PROJECT_cgi/ +Other: + --db_name default: PROJECT + Example command line: - ./make_project --base $HOME/boinc --url_base http://boink/ yah 'YETI @ Home' \\ - upper_case 'UpperCase' + ./make_project --base $HOME/boinc --url_base http://boink/ yah 'YETI @ Home' Then upload_dir = $HOME/boinc/projects/yah/upload and cgi_url = http://boink/yah_cgi/ @@ -104,7 +116,7 @@ options.delete_prev_inst = False for o,a in opts: if o == '-h' or o == '--help': usage() - elif o == '-v': options.echo_verbose = 2 + elif o == '-v': options.echo_verbose = 2 elif o == '--verbose': options.echo_verbose = int(a) elif o == '--no_query': options.no_query = True elif o == '--user_name': options.user_name = a @@ -117,6 +129,7 @@ for o,a in opts: elif o == '--html_user_url': options.html_user_url = a elif o == '--html_ops_url': options.html_ops_url = a elif o == '--cgi_url': options.cgi_url = a + elif o == '--db_name': options.db_name = a # elif o == '--bin_dir': options.bin_dir = a # elif o == '--cgi_bin_dir': options.cgi_bin_dir = a # elif o == '--html_user_dir': options.html_user_dir = a @@ -128,43 +141,45 @@ for o,a in opts: else: raise SystemExit('internal error o=%s'%o) -if len(args) != 4: - syntax_error('Need four arguments') - -(project_shortname, project_longname, app_exe, app_name) = args - -if not options.url_base: - syntax_error('Need --url_base') - -if not options.url_base.startswith('http://'): - syntax_error('url_base needs to be an URL') - -options.url_base = os.path.join(options.url_base, '') +if len(args) == 2: + (project_shortname, project_longname) = args +elif len(args) == 1: + (project_shortname, project_longname) = args[0], args[0].capitalize() +else: + syntax_error('Need one or two arguments') opt_repls = {'PROJECT':project_shortname, 'PROJECT_ops':project_shortname+'_ops', - 'PROJECT_cgi':project_shortname+'_cgi', - 'URL_BASE':options.url_base} -def replopt(str): - for key in opt_repls: - str = re.compile('\\b'+key+'\\b').sub(os.path.join(opt_repls[key],'')[:-1], str) - return str + 'PROJECT_cgi':project_shortname+'_cgi'} +def delete_slash(str): + return os.path.join(str,'')[:-1] def add_slash(str, action=True): if action: return os.path.join(str,'') else: return str +def replopt(str): + for key in opt_repls: + str = str.replace(key, delete_slash(opt_repls[key])) + return str def defopt(name, v, isdir=True): options.__dict__[name] = opt_repls[name.upper()] = add_slash(replopt(options.__dict__.get(name) or v), isdir) -defopt('user_name', USER, isdir=False) -defopt('base', HOME) -defopt('key_dir', 'BASE/keys') -defopt('project_root', 'BASE/projects/PROJECT') +defopt('url_base' , 'http://%s/'%NODENAME) -defopt('html_user_url', 'URL_BASE/PROJECT') -defopt('html_ops_url', 'URL_BASE/PROJECT_ops') -defopt('cgi_url', 'URL_BASE/PROJECT_cgi') +if not isurl(options.url_base): + syntax_error('url_base needs to be an URL') + +defopt('html_user_url' , 'URL_BASE/PROJECT') +defopt('html_ops_url' , 'URL_BASE/PROJECT_ops') +defopt('cgi_url' , 'URL_BASE/PROJECT_cgi') + +defopt('user_name' , USER, isdir=False) +defopt('base' , HOME) +defopt('key_dir' , 'BASE/keys') +defopt('project_root' , 'BASE/projects/PROJECT') + +defopt('db_name' , 'PROJECT', isdir=False) print "Creating project '%s' (short name '%s'):" %(project_longname, project_shortname) for k in ['base', @@ -185,6 +200,7 @@ if os.path.exists(options.project_root): if not options.no_query: if not query_noyes('Delete %s?'%options.project_root): raise SystemExit('Aborted') + print "Deleting", options.project_root rmtree(options.project_root) else: raise SystemExit('Project root already exists! Specify --delete_prev_inst --drop_db_first to clobber') @@ -193,34 +209,38 @@ if not options.no_query: if not query_yesno("Continue?"): raise SystemExit('Aborted') -app = App(app_name) -app_version = AppVersion(app, exec_names=[app_exe]) - options.install_method = 'copy' init() project = Project(project_shortname, project_longname, project_dir = options.project_root, - master_url = options.url_base, + master_url = options.html_user_url, cgi_url = options.cgi_url, key_dir = options.key_dir, - apps=[app], app_versions=[app_version], + db_name = options.db_name ) project.install_project() -project.sched_install('feeder') -project.sched_install('transitioner') -project.sched_install('validate_test') -project.sched_install('assimilator') -project.sched_install('file_deleter') +# project.sched_install('feeder') +# project.sched_install('transitioner') +# project.sched_install('validate_test') +# project.sched_install('assimilator') +# project.sched_install('file_deleter') -print '''Done installing files. +httpd_conf_template_filename = os.path.join(options.project_root, + project_shortname+'.httpd.conf') -You need to manually edit your Apache httpd.conf to add these lines: +proot = delete_slash(options.project_root) +html_user_url = options.html_user_url +html_ops_url = options.html_ops_url - Alias /%(project)s %(proot)s/html_user - Alias /%(project)s_ops %(proot)s/html_ops - ScriptAlias /%(project)s_cgi %(proot)s/cgi-bin +print >>open(httpd_conf_template_filename,'w'), ''' + + ## Settings for BOINC project %(project_longname)s + + Alias /%(project_shortname)s %(proot)s/html_user + Alias /%(project_shortname)s_ops %(proot)s/html_ops + ScriptAlias /%(project_shortname)s_cgi %(proot)s/cgi-bin # Note: projects/*/keys/ should NOT be readable! @@ -243,19 +263,47 @@ You need to manually edit your Apache httpd.conf to add these lines: Order allow,deny Allow from all +''' %locals() -Install this in crontab: +print '''Done installing files. + +Steps to complete installation: + +1. Set permissions for Apache: + + cat %(httpd_conf_template_filename)s >> /etc/apache/httpd.conf && apachectl restart + + # (path to httpd.conf varies) + +2. Add to crontab (as %(USER)s) + (If cron cannot run "start", try using a helper script to set PATH and + PYTHONPATH) 0,5,10,15,20,25,30,35,40,45,50,55 * * * * %(proot)s/bin/start --cron -(You may need to set PATH and/or PYTHONPATH since cron runs with manilla environment) - To start, show status, stop BOINC daemons run: %(proot)s/bin/start %(proot)s/bin/status %(proot)s/bin/stop -'''%{'project':project_shortname, 'proot':os.path.join(options.project_root,'')[:-1]} +Master URL: %(html_user_url)s +Administration URL: %(html_ops_url)s -## TODO: save settings to a file. +Tasks to do: + +1. Add platform(s) + %(proot)s/bin/add platform --name=c64 --user_f="Commodore 64" + %(proot)s/bin/add platform --name=i686-pc-linux-gnu --user_f="Linux x86" + +2. Add application(s) + %(proot)s/bin/add app --name=SpaghettiAtHome + +3. Add core client and application binaries + + 3a. Place compiled clients in %(proot)s/apps/boinc/ and %(proot)s/apps/APP/ + 3b. Run %(proot)s/bin/update_versions + +4. Generate work : read documentation at http://boinc.berkeley.edu/ + +'''%locals() diff --git a/tools/update_versions b/tools/update_versions index 7f700cb96d..cf05c07254 100755 --- a/tools/update_versions +++ b/tools/update_versions @@ -36,11 +36,6 @@ assert(config.app_dir config.download_url ) -def query_yesno(msg): - '''Query y/n; default Y''' - print msg, "[Y/n]? ", - return not raw_input().lower().startswith('n') - objects_to_commit = [] def xsort(list): @@ -131,7 +126,7 @@ print "Commit %d items:" %len(objects_to_commit) for object in objects_to_commit: print " ", object -if not query_yesno("Continue"): +if not tools.query_yesno("Continue"): raise SystemExit for object in objects_to_commit: diff --git a/tools/upgrade b/tools/upgrade new file mode 100755 index 0000000000..c699753408 --- /dev/null +++ b/tools/upgrade @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +# $Id$ + +''' +This program upgrades the project (created using make_project) with current +source/build files. Useful if you are a BOINC developer or closely following +BOINC development. +''' + +import boinc_path_config +from Boinc import boinc_project_path +from Boinc.setup_project import * +import os + +if os.system(os.path.join(boinc_project_path.PROGRAM_PARENT_DIR,'bin/stop')): + raise SystemExit("Couldn't stop BOINC!") + +print "Upgrading files... " + +options.install_method = 'copy' +init() +install_boinc_files(boinc_project_path.PROGRAM_PARENT_DIR) + +print "Upgrading files... done" +print +print "Run `start' to resume BOINC."