# module for setting up a new project (either a real project or a test project # see tools/makeproject, test/testbase.py). # TODO: make sure things work if build_dir != src_dir from __future__ import print_function import boinc_path_config from Boinc import database, db_mid, configxml, tools from Boinc.boinc_db import * import os, sys, glob, time, shutil, re, random class Options: pass options = Options() errors = Options() errors.count = 0 options.have_init = False options.install_method = None options.is_test = False options.drop_db_first = False def init(): if options.have_init: return options.have_init = True if options.install_method == 'copy': options.install_function = shutil.copy elif options.install_method == 'link' or options.install_method == 'hardlink': options.install_function = my_link elif options.install_method == 'symlink' or options.install_method == 'softlink': options.install_function = my_symlink else: fatal_error("Invalid install method: %s"%options.install_method) def verbose_echo(level, line): print(line) sys.stdout.flush() def fatal_error(msg): errors.count += 1 verbose_echo(0, "FATAL ERROR: "+msg) sys.exit(1) def error(msg, fatal=0): if fatal: fatal_error(msg) errors.count += 1 verbose_echo(0, "ERROR: "+msg) def verbose_sleep(msg, wait): front = msg + ' [sleep ' back = ']' for i in range(1,wait+1): verbose_echo(1, msg + ' [sleep ' + ('.'*i).ljust(wait) + ']') time.sleep(1) def get_env_var(name, default = None): value = os.environ.get(name, default) if value is None: print("Environment variable %s not defined" % name) sys.exit(1) return value def shell_call(cmd, doexec=False, failok=False): if doexec: os.execl('/bin/sh', 'sh', '-c', cmd) error("Command failed: "+cmd, fatal=(not failok)) os._exit(1) if os.system(cmd): error("Command failed: "+cmd, fatal=(not failok)) return 1 return 0 def verbose_shell_call(cmd, doexec=False, failok=False): verbose_echo(2, " "+cmd) return shell_call(cmd, doexec, failok) def destpath(src,dest): if dest.endswith('/'): return dest + os.path.basename(src) else: return dest # my_symlink and my_link just add the filename to the exception object if one # is raised - don't know why it's not already there def my_symlink(src,dest): dest = destpath(src,dest) try: os.symlink(src,dest) except OSError as e: e.filename = src + ' -> ' + dest raise def my_link(src,dest): dest = destpath(src,dest) try: os.link(src,dest) except OSError as e: e.filename = src + ' -> ' + dest raise # install = options.install_function def install(src, dest, unless_exists=False): if unless_exists and os.path.exists(dest): return try: options.install_function(src, dest) except: print('failed to copy ' + src + ' to ' + dest) return def install_glob(glob_source, dest, failok=False): dest = os.path.join(dest, '') # append '/' if necessary for src in glob.glob(glob_source): if not os.path.isdir(src): install(src, dest) def macro_substitute(macro, replacement, infile, outfile): open(outfile, 'w').write(open(infile).read().replace(macro, replacement)) def macro_substitute_inplace(macro, replacement, inoutfile): old = inoutfile + '.old' os.rename(inoutfile, old) macro_substitute(macro, replacement, old, inoutfile) def check_program_exists(prog): if not os.path.isfile(prog): fatal_error(""" Executable not found: %s Did you `make' yet? """ % prog) def check_core_client_executable(): check_program_exists(builddir('client', version.CLIENT_BIN_FILENAME)) def check_app_executable(app): check_program_exists(builddir('apps', app)) def make_executable(name): os.chmod(name, 755) def force_symlink(src, dest): if os.path.exists(dest): os.unlink(dest) my_symlink(src, dest) def 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): return s[:-len(suffix)] else: return s def _url_to_filename(url): s="" for c in url.replace('http://',''): if (c.isalnum()): s += c else: s += '_' return _remove_trail(s,'_') def account_file_name(url): return 'account_' + _url_to_filename(url) + '.xml' def srcdir(location): return os.path.join(options.srcdir, location) def builddir(location): return os.path.join(boinc_path_config.TOP_BUILD_DIR, location) def run_tool(cmd): verbose_shell_call(builddir('tools', cmd)) def _gen_key_p(private_key, public_key): shell_call("%s/crypt_prog -genkey 1024 %s %s >/dev/null" % ( builddir('lib'), private_key, public_key)) def _gen_key(key): _gen_key_p(key+'_private', key+'_public') def get_int(s): '''Convert a string to an int; return 0 on error.''' try: return int(s) except: return 0 def unique(list): d = {} for i in list: d[i] = 1 return d.keys() def map_xml(dic, keys): if not isinstance(dic,dict): dic = dic.__dict__ s = '' for key in keys: s += "<%s>%s\n" % (key, dic[key], key) return s[:-1] def generate_shmem_key(): return '0x1111%x' % random.randrange(0,2**16) def _check_vars(dict, **names): for key in names: value = names[key] if not key in dict: if value is None: raise SystemExit('error in test script: required parameter "%s" not specified'%key) dict[key] = value for key in dict: if not key in names: raise SystemExit('error in test script: extraneous parameter "%s" unknown'%key) # CAN REMOVE THE FOLLOWING 8 FNS?? # def db_query(db, query): # db.query(query) # result = db.use_result() # return result and result.fetch_row(0,1) def num_results(): return database.Results.count() def num_results_unsent(): return database.Results.count(server_state = RESULT_SERVER_STATE_UNSENT) def num_results_in_progress(): return database.Results.count(server_state = RESULT_SERVER_STATE_IN_PROGRESS) def num_results_over(): return database.Results.count(server_state = RESULT_SERVER_STATE_OVER) def num_wus(): return database.Workunits.count() def num_wus_assimilated(): return database.Workunits.count(assimilate_state = ASSIMILATE_DONE) def num_wus_to_transition(): return database.Workunits.count(_extra_params = ['transition_time<%d'%(time.time()+30*86400)]) def build_command_line(cmd, kwargs): for (key, value) in kwargs: cmd += " -%s '%s'" %(key,value) return cmd def create_project_dirs(dest_dir): def mkdir2(d): try: os.makedirs(d) except OSError as e: if not os.path.isdir(d): raise SystemExit(e) directories = ('', 'cgi-bin', 'bin', 'py', 'py/Boinc', 'templates', 'upload', 'download', 'apps', 'html', 'html/cache', 'html/inc', 'html/inc/password_compat', 'html/inc/random_compat', 'html/inc/ReCaptcha', 'html/inc/ReCaptcha/RequestMethod', 'html/languages', 'html/languages/compiled', 'html/languages/translations', 'html/languages/project_specific_translations', 'html/ops', 'html/ops/ffmail', 'html/ops/mass_email', 'html/ops/remind_email', 'html/project', 'html/stats', 'html/user', 'html/user/img', 'html/user_profile', 'html/user_profile/images' ) [ mkdir2(os.path.join(dest_dir, x)) for x in directories ] # For all directories that apache will put files in, # make them group-writeable and setGID. # Assuming that the "apache" user belongs to our primary group, # any files or dirs created by apache will be owned by # our primary group (not Apache's). # directories = [ 'upload', 'html/cache', 'html/inc', 'html/languages', 'html/languages/compiled', 'html/user_profile/images', ] for d in directories: os.chmod(os.path.join(dest_dir, d), 0o2770) def install_boinc_files(dest_dir, install_web_files, install_server_files): """Copy files from source dir to project dir. Used by the upgrade script, so don't copy sample files to real name.""" def dest(*dirs): location = dest_dir for d in dirs: location = os.path.join(location, d ) return location create_project_dirs(dest_dir) # copy html/ops files in all cases. # The critical one is db_update.php, # which is needed even for a server_only upgrade install_glob(srcdir('html/ops/*.php'), dest('html/ops/')) if install_web_files: install_glob(srcdir('html/inc/*.inc'), dest('html/inc/')) install_glob(srcdir('html/inc/*.php'), dest('html/inc/')) install_glob(srcdir('html/inc/password_compat/*.inc'), dest('html/inc/password_compat/')) install_glob(srcdir('html/inc/random_compat/*.inc'), dest('html/inc/random_compat/')) install_glob(srcdir('html/inc/ReCaptcha/*.php'), dest('html/inc/ReCaptcha/')) install_glob(srcdir('html/inc/ReCaptcha/RequestMethod/*.php'), dest('html/inc/ReCaptcha/RequestMethod')) install_glob(srcdir('html/inc/*.dat'), dest('html/inc/')) install_glob(srcdir('html/ops/*.css'), dest('html/ops/')) install_glob(srcdir('html/ops/ffmail/sample*'), dest('html/ops/ffmail/')) install_glob(srcdir('html/ops/mass_email/sample*'), dest('html/ops/mass_email/')) install_glob(srcdir('html/ops/remind_email/sample*'), dest('html/ops/remind_email/')) install_glob(srcdir('html/user/*.php'), dest('html/user/')) install_glob(srcdir('html/user/*.inc'), dest('html/user/')) install_glob(srcdir('html/user/*.css'), dest('html/user/')) install_glob(srcdir('html/user/*.txt'), dest('html/user/')) install_glob(srcdir('html/user/*.js'), dest('html/user/')) install_glob(srcdir('html/user/*.png'), dest('html/user/img')) install_glob(srcdir('html/user/*.gif'), dest('html/user/img')) install_glob(srcdir('html/user/img/*.*'), dest('html/user/img')) if not os.path.exists(dest('html/user/motd.php')): shutil.copy(srcdir('html/user/sample_motd.php'), dest('html/user/motd.php')) os.system("rm -f "+dest('html/languages/translations/*')) install_glob(srcdir('html/languages/translations/*.po'), dest('html/languages/translations/')) # copy Python stuff install(srcdir('sched/start' ), dest('bin/start' )) force_symlink(dest('bin', 'start'), dest('bin', 'stop')) force_symlink(dest('bin', 'start'), dest('bin', 'status')) python_files = [ '__init__.py', 'add_util.py', 'boinc_db.py', 'boinc_project_path.py', 'boincxml.py', 'configxml.py', 'database.py', 'db_base.py', 'db_mid.py', 'projectxml.py', 'sched_messages.py', 'tools.py', 'util.py' ] for s in python_files: install(srcdir("py/Boinc/" + s), dest('py/Boinc', s)) content = ''' # Generated by make_project import sys, os sys.path.insert(0, os.path.join('{dest_dir}', 'py')) '''.format(dest_dir=dest_dir) f = open(dest('bin', 'boinc_path_config.py'), "w") f.write(content) f.close() if not install_server_files: return # copy backend (C++) programs; # rename current web daemons in case they're in use if os.path.isfile(dest('cgi-bin', 'cgi')): os.rename(dest('cgi-bin', 'cgi'), dest('cgi-bin', 'cgi.old')) if os.path.isfile(dest('cgi-bin', 'fcgi')): os.rename(dest('cgi-bin', 'fcgi'), dest('cgi-bin', 'fcgi.old')) install(builddir('sched','fcgi'), dest('cgi-bin','fcgi')) if os.path.isfile(dest('cgi-bin', 'file_upload_handler')): os.rename(dest('cgi-bin', 'file_upload_handler'), dest('cgi-bin', 'file_upload_handler.old')) cgi_script = [ 'cgi', 'file_upload_handler'] for f in cgi_script: install(builddir('sched/' + f), dest('cgi-bin',f)) command = [ 'adjust_user_priority', 'antique_file_deleter', 'assimilator.py', 'census', 'db_dump', 'db_purge', 'delete_file', 'feeder', 'file_deleter', 'get_file', 'make_work', 'pshelper', 'put_file', 'pymw_assimilator.py', 'sample_assimilator', 'sample_bitwise_validator', 'sample_dummy_assimilator', 'sample_substr_validator', 'sample_trivial_validator', 'sample_work_generator', 'script_assimilator', 'script_validator', 'show_shmem', 'single_job_assimilator', 'size_regulator', 'transitioner', 'transitioner_catchup.php', 'trickle_credit', 'trickle_deadline', 'trickle_echo', 'update_stats', 'wu_check', ] for f in command: install(builddir('sched/' + f), dest('bin',f)) command = [ 'vda', 'vdad' ] for f in command: install(builddir('vda/' + f), dest('bin',f)) command = [ 'appmgr', 'boinc_submit', 'cancel_jobs', 'create_work', 'dbcheck_files_exist', 'demo_query', 'demo_submit', 'dir_hier_move', 'dir_hier_path', 'grep_logs', 'manage_privileges', 'parse_config', 'run_in_ops', 'sign_executable', 'stage_file', 'stage_file_native', 'update_versions', 'xadd', ] for f in command: install(builddir('tools/' + f), dest('bin',f)) install(srcdir('lib/crypt_prog'), dest('bin','crypt_prog')) install(srcdir('sched/db_dump_spec.xml' ), dest('','db_dump_spec.xml' )) class Project: def __init__(self, short_name, long_name, cgi_url, project_dir=None, key_dir=None, master_url=None, db_name=None, host=None, web_only=False, no_db=False, production=False ): init() self.production = production self.web_only = web_only self.no_db = no_db 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.dest('config.xml')).init_empty() config = self.config.config # this is where default project config is defined config.long_name = self.long_name config.db_user = options.db_user config.db_name = db_name or options.user_name + '_' + self.short_name config.db_passwd = options.db_passwd config.db_host = options.db_host config.shmem_key = generate_shmem_key() config.uldl_dir_fanout = 1024 config.host = host config.min_sendwork_interval = 0 config.max_wus_to_send = 50 config.daily_result_quota = 500 config.disable_account_creation = 0 config.disable_account_creation_rpc = 0 config.account_creation_rpc_require_consent = 0 config.disable_web_account_creation = 0 config.enable_login_mustagree_termsofuse = 0 config.enable_privacy_by_default = 0 config.show_results = 1 config.cache_md5_info = 1 config.sched_debug_level = 3 config.fuh_debug_level = 3 config.one_result_per_user_per_wu = 0 config.send_result_abort = 1 config.dont_generate_upload_certificates = 1 config.ignore_upload_certificates = 1 config.enable_delete_account = 0 if web_only: config.no_computing = 1 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.upload_url = os.path.join(cgi_url , 'file_upload_handler') 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') config.app_dir = os.path.join(self.project_dir, 'apps') config.log_dir = self.project_dir+'log_'+config.host if production: config.min_sendwork_interval = 6 self.scheduler_url = os.path.join(cgi_url , 'cgi') def dest(self, *dirs): location = self.project_dir for x in dirs: location = os.path.join(location, x) return location def keydir(self, location): return os.path.join(self.config.config.key_dir, location) def logdir(self): return os.path.join(self.project_dir, "log_"+self.config.config.host) def create_keys(self): if not os.path.exists(self.config.config.key_dir): os.mkdir(self.config.config.key_dir) _gen_key(self.keydir('upload')) _gen_key(self.keydir('code_sign')) def create_logdir(self): os.mkdir(self.logdir()) os.chmod(self.logdir(), 0o2770) def keys_exist(self): keys = ['upload_private', 'upload_public', 'code_sign_private', 'code_sign_public' ] for key in keys: if not os.path.exists(self.keydir(key)): return False return True # create new project. Called only from make_project def install_project(self): if os.path.exists(self.dest()): raise SystemExit('Project directory "%s" already exists; this would clobber it!'%self.dest()) verbose_echo(1, "Creating directories") create_project_dirs(self.project_dir) if not self.web_only: if not self.keys_exist(): verbose_echo(1, "Generating encryption keys") self.create_keys() # copy the user and administrative PHP files to the project dir, verbose_echo(1, "Copying files") # Create the project log directory self.create_logdir() install_boinc_files(self.dest(), True, not self.web_only) # copy sample web files to final names install(srcdir('html/user/sample_index.php'), self.dest('html/user/index.php')) install(srcdir('html/user/sample_bootstrap.min.css'), self.dest('html/user/bootstrap.min.css')) install(srcdir('html/user/sample_bootstrap.min.js'), self.dest('html/user/bootstrap.min.js')) install(srcdir('html/user/sample_jquery.min.js'), self.dest('html/user/jquery.min.js')) install(srcdir('html/project.sample/project.inc'), self.dest('html/project/project.inc')) install(srcdir('html/project.sample/project_specific_prefs.inc'), self.dest('html/project/project_specific_prefs.inc')) install(srcdir('html/project.sample/cache_parameters.inc'), self.dest('html/project/cache_parameters.inc')) install(srcdir('tools/project.xml'), self.dest('project.xml')) install(srcdir('tools/gui_urls.xml'), self.dest('gui_urls.xml')) if not self.production: install(srcdir('test/uc_result'), self.dest('templates/uc_result')) install(srcdir('test/uc_wu_nodelete'), self.dest('templates/uc_wu')) content = ''' '''.format(url=self.scheduler_url.strip()) f = open(self.dest('html/user', 'schedulers.txt'), 'w') f.write(content) f.close() if self.no_db: verbose_echo(1, "Not setting up database (--no_db was specified)") else: verbose_echo(1, "Setting up database") database.create_database( srcdir = options.srcdir, config = self.config.config, drop_first = options.drop_db_first ) verbose_echo(1, "Writing config files") self.config.write() # create symbolic links to the CGI and HTML directories verbose_echo(1, "Linking CGI programs") if options.__dict__.get('cgi_dir'): force_symlink(self.dest('cgi-bin'), os.path.join(options.cgi_dir, self.short_name)) if options.__dict__.get('html_dir'): force_symlink(self.dest('html/user'), os.path.join(options.html_dir, self.short_name)) force_symlink(self.dest('html/ops'), os.path.join(options.html_dir, self.short_name+'_admin')) def http_password(self, user, password): 'Adds http password protection to the html/ops directory' passwd_file = self.dest('html/ops', '.htpassword') content = ''' AuthName '{long_name} Administration' AuthType Basic AuthUserFile {passwd_file} require valid-user '''.format(long_name=self.long_name, passwd_file=passwd_file) f = open(self.dest('html/ops', '.htaccess'), 'w') f.write(content) f.close() shell_call("htpassword -bc %s %s %s" % (passwd_file, user, password)) def _run_sched_prog(self, prog, args='', logfile=None): verbose_shell_call("cd %s && ./%s %s >> %s.log 2>&1" % (self.dest('bin'), prog, args, (logfile or prog))) def start_servers(self): self.started = True self._run_sched_prog('start', '-v --enable') verbose_sleep("Starting servers for project '%s'" % self.short_name, 1) def _build_sched_commandlines(self, progname, kwargs): '''Given a KWARGS dictionary build a list of command lines string depending on the program.''' each_app = False if progname == 'feeder': _check_vars(kwargs) elif progname == 'transitioner': _check_vars(kwargs) elif progname == 'make_work': work = kwargs.get('work', self.work) _check_vars(kwargs, cushion=30, max_wus=0, wu_name=work.wu_template) elif progname == 'sample_bitwise_validator': _check_vars(kwargs) each_app = True elif progname == 'file_deleter': _check_vars(kwargs) elif progname == 'antique_file_deleter': _check_vars(kwargs) elif progname == 'sample_dummy_assimilator': _check_vars(kwargs) each_app = True else: raise SystemExit("test script error: invalid progname '%s'"%progname) cmdline = build_command_line('', kwargs) if each_app: return [ '-app %s %s'%(av.app.name,cmdline) for av in self.app_versions ] else: return [cmdline] def sched_run(self, prog, **kwargs): for cmdline in self._build_sched_commandlines(prog, kwargs): 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.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): cgi_bin = [ 'stripchart.cgi', 'stripchart', 'stripchart.cnf', 'looper', 'db_looper', 'datafiles', 'get_load', 'dir_size' ] for f in cgi_bin: self.copy(os.path.join('stripchart', f), 'cgi-bin/') macro_substitute('BOINC_DB_NAME', self.db_name, srcdir('stripchart/samples/db_count'), self.dest('bin/db_count')) make_executable(self.dest('bin/db_count')) self._run_sched_prog('looper' , 'get_load 1' , 'get_load') self._run_sched_prog('db_looper' , '"result" 1' , 'count_results') self._run_sched_prog('db_looper' , '"workunit where assimilate_state=2" 1' , 'assimilated_wus') self._run_sched_prog('looper' , '"dir_size ../download" 1' , 'download_size') self._run_sched_prog('looper' , '"dir_size ../upload" 1' , 'upload_size') def stop(self): verbose_echo(1,"Stopping server(s) for project '%s'"%self.short_name) self._run_sched_prog('start', '-v --disable') self.started = False def maybe_stop(self): if self.started: self.stop() def query_noyes(str): verbose_echo(0,'') return tools.query_noyes(str) def query_yesno(str): verbose_echo(0,'') return tools.query_yesno(str)