""" Tools for compiling dictionaries automatically. Fabric will automatically execute sets of commands remotely, and will log in via SSH automatically to do this, assuming you have the proper credentials. To install Fabric, follow the instructions on the website: http://fabfile.org/ Then run the following to check that it works. fab --list ## Basic commands In order to perform a task for a specific instance, you must specify a few things. fab LOCATION DICT TASKS So, to compile the lexicon locally for baakoeh, you would run: $ fab local baakoeh compile_dictionary To do the same remotely, and restart the service, you would run: $ fab gtweb sanat compile_dictionary restart_service With the latter, Fabric will connect via SSH and run commands remotely. You may be asked for your SSH password. """ # TODO: fab local start_project PROJNAME # - create infrastructure for new project # - configs/sample.config.yaml.in -> configs/PROJNAME.config.yaml.in # - templates/about.blank.html -> configs/about.PROJNAME.html # - grey box search notice template # - mkdir: # # TODO: fab local add_language PROJNAME LANGISO # - create basic files for languages # - mkdir: # configs/language_specific_rules/tagsets/ import os, sys from fabric.decorators import roles from fabric.api import ( cd , run , local , env , task , settings , prompt ) from fabric.operations import ( sudo ) from fabric.colors import red, green, cyan, yellow from fabric.contrib.console import confirm from fabric.utils import abort import socket # Hosts that have an nds- init.d script running_service = [ 'gtweb.uit.no', 'gtlab.uit.no', 'gtoahpa.uit.no', 'sapir.artsrn.ualberta.ca', # sapir 'arrl-web003', ] no_fst_install = [ 'gtoahpa.uit.no', ] location_restriction_notice = { 'gtoahpa.uit.no': [ 'sanit' , 'baakoeh' ] , 'gtweb.uit.no': [ 'dikaneisdi' , 'erey' , 'kyv' , 'muter' , 'saan' , 'saanih' , 'sanat' , 'sonad' , 'vada' , 'valks' ] # sapir , 'arrl-web003': [ 'gunaha' , 'kidwinan' , 'guusaaw' , 'itwewina' ] } def get_projects(): """ Find all existing projects which can be included as an env argument """ import os conf_suffix = ".config.yaml.in" avail_projects = [] for d, ds, fs in os.walk('.'): for f in fs: if f.endswith(conf_suffix): avail_projects.append( f.replace(conf_suffix, '') ) return avail_projects def get_project(): avail_projects = get_projects() proj_arg = [a for a in sys.argv if a in avail_projects] if len(proj_arg) > 0: proj_arg = proj_arg[0] else: proj_arg = False return proj_arg @task(aliases=get_projects()) def set_proj(): """ Set the project. This is aliased to whatever existing project names there are. ... Assuming no project name will be 'local' or 'compile' """ proj = get_project() env.clean_first = False if proj: env.current_dict = proj else: print >> sys.stderr, "This is not a valid project name." sys.exit() host = socket.gethostname() if host in running_service: has_restriction = sum(location_restriction_notice.values(), []) if proj in has_restriction: host_rest = location_restriction_notice.get(host, False) if host_rest: if proj not in host_rest: print >> sys.stderr, red("%s is not on the current host <%s>." % (proj, host)) cont = raw_input(red('Continue anyway? [Y/N] \n')) if cont != 'Y': sys.exit() return @task def local(*args, **kwargs): """ Run a command using the local environment. """ from fabric.operations import local as lrun import os env.run = lrun env.hosts = ['localhost'] gthome = os.environ.get('GTHOME') if gthome is None: sys.exit("GTHOME environment variable is not set.") env.path_base = os.getcwd() env.svn_path = gthome env.dict_path = os.path.join(env.path_base, 'dicts') env.neahtta_path = env.path_base env.i18n_path = os.path.join(env.path_base, 'translations') # Make command needs to include explicit path to file, because of # fabric. env.make_cmd = "make -C %s -f %s" % ( env.dict_path , os.path.join(env.dict_path, 'Makefile') ) env.remote_no_fst = False return env env.no_svn_up = False env.use_ssh_config = True # env.key_filename = '~/.ssh/neahtta' if ['local', 'gtweb', 'gtoahpa'] not in sys.argv: env = local(env) from config import yaml env.real_hostname = socket.gethostname() # set up environments # Assume local unless otherwise noted @task def no_svn_up(): """ Do not SVN up """ env.no_svn_up = True @task def gtweb(): """ Run a command remotely on gtweb """ env.run = run env.hosts = ['neahtta@gtweb.uit.no'] env.path_base = '/home/neahtta' env.svn_path = env.path_base + '/gtsvn' env.dict_path = env.path_base + '/neahtta/dicts' env.neahtta_path = env.path_base + '/neahtta' env.i18n_path = env.path_base + '/neahtta/translations' env.make_cmd = "make -C %s -f %s" % ( env.dict_path , os.path.join(env.dict_path, 'Makefile') ) env.remote_no_fst = False @task def gtoahpa(): """ Run a command remotely on gtoahpa """ env.run = run env.hosts = ['neahtta@gtoahpa.uit.no'] env.path_base = '/home/neahtta' env.svn_path = env.path_base + '/gtsvn' env.dict_path = env.path_base + '/neahtta/dicts' env.neahtta_path = env.path_base + '/neahtta' env.i18n_path = env.path_base + '/neahtta/translations' env.make_cmd = "make -C %s -f %s" % ( env.dict_path , os.path.join(env.dict_path, 'Makefile') ) env.remote_no_fst = True @task def update_configs(): """ SVN up the config files """ if env.no_svn_up: print(yellow("** skipping svn up **")) return with cd(env.neahtta_path): paths = [ 'configs/', 'translations/', ] print(cyan("** svn up **")) for p in paths: _p = os.path.join(env.neahtta_path, p) with cd(_p): env.run('svn up ' + _p) def read_config(proj): import yaml def gettext_yaml_wrapper(loader, node): from flask.ext.babel import lazy_gettext as _ return node.value yaml.add_constructor('!gettext', gettext_yaml_wrapper) _path = 'configs/%s.config.yaml' % proj try: open(_path, 'r').read() except IOError: if env.real_hostname not in running_service: _path = 'configs/%s.config.yaml.in' % proj print(yellow("** Production config not found, using development (*.in)")) else: print(red("** Production config not found, and on a production server. Exiting.")) sys.exit() with open(_path, 'r') as F: config = yaml.load(F) return config # @task # def install_geo(): @task def update_gtsvn(): """ SVN up the various ~/gtsvn/ directories """ if env.no_svn_up: print(yellow("** skipping svn up **")) return with cd(env.svn_path): config = read_config(env.current_dict) svn_langs = [l.get('iso') for l in config.get('Languages') if not l.get('variant', False)] svn_lang_paths = [ 'langs/%s' % l for l in svn_langs ] # TODO: replace langs with specific list of langs from config # file paths = [ 'giella-core/', 'words/', 'art/dicts/', ] + svn_lang_paths print(cyan("** svn up **")) for p in paths: _p = os.path.join(env.svn_path, p) with cd(_p): try: svn_up_cmd = env.run('svn up ' + _p) except: abort( red("\n* svn up failed in <%s>. Prehaps the tree is locked?" % _p) + '\n' + \ red(" Correct this (maybe with `svn cleanup`) and rerun the command, or run with `no_svn_up`.") ) # TODO: necessary to run autogen just in case? print(cyan("** Compiling giella-core **")) giella_core = os.path.join(env.svn_path , 'giella-core') with cd(giella_core): make_file = env.svn_path + '/giella-core/Makefile' make_ = "make -C %s -f %s" % ( giella_core , make_file ) result = env.run(make_) @task def restart_service(dictionary=False): """ Restarts the service. """ if not dictionary: dictionary = env.current_dict fail = False # Not a big issue, but figure this out for local development. # if env.real_hostname not in running_service: # print env.real_hostname # print(green("** No need to restart, nds-<%s> not available on this host. **" % dictionary)) # return with cd(env.neahtta_path): _path = '%s.wsgi' % env.current_dict try: os.utime(_path, None) touched = True except Exception, e: touched = False if touched: print(cyan("** Restarting service for <%s> **" % dictionary)) sys.exit() print(cyan("** Restarting service for <%s> **" % dictionary)) restart = env.run("sudo service nds-%s restart" % dictionary) if not restart.failed: print(green("** <%s> Service has restarted successfully **" % dictionary)) else: fail = True if fail: print(red("** something went wrong while restarting <%s> **" % dictionary)) @task def compile_dictionary(dictionary=False, restart=False): """ Compile a dictionary project on the server, and restart the corresponding service. $ fab compile_dictionary:DICT """ failed = False if not dictionary: dictionary = env.current_dict update_gtsvn() with cd(env.dict_path): env.run("svn up Makefile") result = env.run(env.make_cmd + " %s-lexica" % dictionary) if result.failed: failed = True if restart: restart_service(dictionary) if failed: print(red("** Something went wrong while compiling <%s> **" % dictionary)) @task def compile(dictionary=False,restart=False): """ Compile a dictionary, fsts and lexica, on the server. $ fab compile:DICT NB: if the hostname is gtoahpa.uit.no (set in no_fst_install list above), only the lexicon will be compiled, FSTs will not be compiled or installed. """ hup = False failed = False print(cyan("Executing on <%s>" % env.real_hostname)) if not dictionary: dictionary = env.current_dict update_configs() update_gtsvn() with cd(env.dict_path) and settings(warn_only=True): if env.no_svn_up: print(yellow("** Skipping svn up of Makefile")) else: env.run("svn up Makefile") if env.real_hostname in no_fst_install or env.remote_no_fst: print(yellow("** Skip FST compile for gtoahpa **")) print(cyan("** Compiling lexicon for <%s> **" % dictionary)) result = env.run(env.make_cmd + " %s-lexica" % dictionary) skip_fst = True else: skip_fst = False print(cyan("** Compiling lexicon and FSTs for <%s> **" % dictionary)) if env.clean_first in ['Y', 'y']: clean_result = env.run(env.make_cmd + " %s-clean" % dictionary) result = env.run(env.make_cmd + " %s" % dictionary) if not result.succeeded: print(red("** There was some problem building the FSTs for this dictionary.")) print(red("** Remove and check out individual language directories first?")) print(red("** WARNING: this will run `rm -rf $GTHOME/langs/LANG for each")) print(red("** language in the current project. If you have")) print(red("** ocal changes, they will be lost.")) prompt('[Y/n]', key='clean_first') failed = True if env.clean_first in ['Y', 'y']: compile(dictionary, restart) if not skip_fst: print(cyan("** Installing FSTs for <%s> **" % dictionary)) result = env.run(env.make_cmd + " %s-install" % dictionary) if result.failed: failed = True if restart: restart_service(dictionary) if failed: print(red("** Something went wrong while compiling <%s> **" % dictionary)) else: print(cyan("** <%s> FSTs and Lexicon compiled okay, should be safe to restart. **" % dictionary)) @task def compile_fst(iso='x'): """ Compile a dictionary project on the server. $ fab compile_dictionary:DICT """ hup = False dictionary = env.current_dict update_gtsvn() # TODO: need a make path to clean existing dictionary with cd(env.dict_path): # env.run("svn up Makefile") print(cyan("** Compiling FST for <%s> **" % iso)) clear_tmp = env.run(env.make_cmd + " rm-%s" % iso) make_fsts = env.run(env.make_cmd + " %s" % iso) make_fsts = env.run(env.make_cmd + " %s-%s-install" % (dictionary, iso)) if make_fsts.failed: print(red("** Something went wrong while compiling <%s> **" % iso)) else: print(cyan("** FST <%s> compiled **" % iso)) @task def test_configuration(): """ Test the configuration and check language files for errors. """ _path = 'configs/%s.config.yaml' % env.current_dict try: open(_path, 'r').read() except IOError: if env.real_hostname not in running_service: _path = 'configs/%s.config.yaml.in' % env.current_dict print(yellow("** Production config not found, using development (*.in)")) else: print(red("** Production config not found, and on a production server. Exiting.")) sys.exit() # TODO: this assumes virtualenv is enabled, need to explicitly enable _dict = env.current_dict with cd(env.dict_path): print(cyan("** Checking paths and testing XML for <%s> **" % _dict)) cmd ="NDS_CONFIG=%s python manage.py chk-fst-paths" % _path test_cmd = env.run(cmd) if test_cmd.failed: print(red("** Something went wrong while testing <%s> **" % _dict)) else: print(cyan("** Everything seems to work **")) @task def extract_strings(): """ Extract all the translation strings to the template and *.po files. """ print(cyan("** Extracting strings")) cmd = "pybabel extract -F babel.cfg -k gettext -o translations/messages.pot ." extract_cmd = env.run(cmd) if extract_cmd.failed: print(red("** Extraction failed, aborting.")) else: print(cyan("** Extraction worked, updating files.")) cmd = "pybabel update -i translations/messages.pot -d translations" update_cmd = env.run(cmd) if update_cmd.failed: print(red("** Update failed.")) else: print(green("** Update worked. You may now check in or translate.")) @task def update_strings(): if env.no_svn_up: print(yellow("** skipping svn up **")) compile_strings() return with cd(env.i18n_path): env.run("svn up") compile_strings() @task def find_babel(): import babel print babel # TODO: handle babel.core.UnknownLocaleError: unknown locale 'hdn', with # cleaner error message @task def compile_strings(): """ Compile .po strings to .mo strings for use in the live server. """ if hasattr(env, 'current_dict'): config = 'configs/%s.config.yaml.in' % env.current_dict with open(config, 'r') as F: _y = yaml.load(F.read()) langs = _y.get('ApplicationSettings', {}).get('locales_available') for lang in langs: # run for each language cmd = "pybabel compile -d translations -l %s" % lang compile_cmd = env.run(cmd) if compile_cmd.failed: print(red("** Compilation failed, aborting.")) else: print(green("** Compilation successful.")) else: cmd = "pybabel compile -d translations" with settings(warn_only=True): compile_cmd = env.run(cmd, capture=True) if compile_cmd.failed: if 'babel.core.UnknownLocaleError' in compile_cmd.stderr: error_line = [l for l in compile_cmd.stderr.splitlines() if 'babel.core.UnknownLocaleError' in l] print(red("** String compilation failed, aborting: ") + cyan(''.join(error_line))) print("") print(yellow(" Either: ")) print(yellow(" * rerun the command with the project name, i.e., `fab PROJNAME compile_strings`.")) print(yellow(" * Troubleshoot missing locale. (see Troubleshooting doc)")) else: print(compile_cmd.stderr) print(red("** Compilation failed, aborting.")) else: print(compile_cmd.stdout) print(green("** Compilation successful.")) def where(iso): """ Searches Config and Config.in files for languages defined in Languages. Returns list of tuples. (config_path, short_name, iso) """ configs = [] for d, path, fs in os.walk('configs/'): for f in fs: if f.endswith(('.config.yaml.in', '.config.yaml')): configs.append(os.path.join(d, f)) def test_lang(i): if isinstance(iso, list): if i.get('iso') in iso: return True else: if i.get('iso') == iso: return True return False locations = [] for config in configs: with open(config, 'r') as F: _y = yaml.load(F.read()) short_name = _y.get('ApplicationSettings', {}).get('short_name') langs = filter(test_lang, _y.get('Languages')) for l in langs: locations.append((config, short_name, l.get('iso'))) return locations @task def where_is(iso='x'): """ Search *.in files for language ISOs to return projects that the language is present in. """ if '+' in iso: iso = iso.split('+') locations = where(iso) for config, shortname, l in locations: print '%s : %s\t\t%s' % (l, shortname, config) def search_running(): """ Find all running services, return tuple of shortname and pidfile path """ pidfile_suffix = "-pidfile.pid" pids = [] for d, ds, fs in os.walk('.'): for f in fs: if f.endswith(pidfile_suffix): pids.append( (f.replace(pidfile_suffix, ''), os.path.join(d,f)) ) return pids @task def find_running(): hostname = env.real_hostname for shortname, pidfile in search_running(): print "%s running on %s (%s)" % (green(shortname), yellow(hostname), pidfile) @task def restart_running(): hostname = env.real_hostname find_running() with cd(env.neahtta_path): running_services = search_running() failures = [] for s, pid in running_services: print(cyan("** Restarting service for <%s> **" % s)) stop = env.run("sudo service nds-%s stop" % s) if not stop.failed: start = env.run("sudo service nds-%s start" % s) if not start.failed: print(green("** <%s> Service has restarted successfully **" % s)) else: failures.append((s, pid)) else: failures.append((s, pid)) if len(failures) > 0: print(red("** something went wrong while restarting the following **")) for f in failures: print (s, pid) @task def runserver(): """ Run the development server.""" cmd = "pybabel compile -d translations" _path = 'configs/%s.config.yaml' % env.current_dict try: open(_path, 'r').read() except IOError: if env.real_hostname not in running_service: _path = 'configs/%s.config.yaml.in' % env.current_dict print(yellow("** Production config not found, using development (*.in)")) else: print(red("** Production config not found, and on a production server. Exiting.")) sys.exit() cmd ="NDS_CONFIG=%s python neahtta.py dev" % _path print(green("** Go.")) run_cmd = env.run(cmd) if run_cmd.failed: print(red("** Starting failed for some reason.")) @task def doctest(): """ Run unit tests embedded in code """ doctests = [ 'morphology/utils.py', ] doctest_cmd = 'python -m doctest -v %s' for _file in doctests: test_cmd = env.run(doctest_cmd % _file) @task def test_project(): """ Test the configuration and check language files for errors. """ yaml_path = 'configs/%s.config.yaml.in' % env.current_dict _dict = env.current_dict with cd(env.dict_path): print(cyan("** Running tests for %s" % _dict)) cmd ="NDS_CONFIG=%s python -m unittest tests.yaml_tests" % (yaml_path) test_cmd = env.run(cmd) if test_cmd.failed: print(red("** Something went wrong while testing <%s> **" % _dict)) @task def unittests(): """ Test the configuration and check language files for errors. TODO: this is going away in favor of the better new thing: `test_project`, `doctest`, and `test` """ yaml_path = 'configs/%s.config.yaml' % env.current_dict try: with open(yaml_path, 'r') as F: _y = yaml.load(F) except IOError: if env.real_hostname not in running_service: yaml_path = 'configs/%s.config.yaml.in' % env.current_dict print(yellow("** Production config not found, using development (*.in)")) with open(yaml_path, 'r') as F: _y = yaml.load(F) else: print(red("** Production config not found, and on a production server. Exiting.")) sys.exit() # TODO: this assumes virtualenv is enabled, need to explicitly enable _dict = env.current_dict with cd(env.dict_path): unittest_modules = _y.get('UnitTests', False) if not unittest_modules: print(red("** `UnitTests` not found in %s. Example:" % yaml_path)) print >> sys.stderr, "" print >> sys.stderr, " UnitTests:" print >> sys.stderr, ' - "tests.LANG1_lexicon"' print >> sys.stderr, ' - "tests.LANG2_lexicon"' print >> sys.stderr, "" sys.exit() for unittest in unittest_modules: # unittest_file = unittest.replace('/', '.') + '.py' # try: # open(os.path.join(env.path_base, unittest_file.replace('/', '.') + '.py'), 'r').read() # except: # print(yellow("** File does not exist for %s" % unittest_file)) # continue print(cyan("** Running tests for %s" % unittest)) cmd ="NDS_CONFIG=%s python -m unittest %s" % (yaml_path, unittest) test_cmd = env.run(cmd) if test_cmd.failed: print(red("** Something went wrong while testing <%s> **" % _dict)) @task def test(): doctest() test_project() def commit_gtweb_tag(): """ svn rm nds-stable-gtweb svn commit nds-stable-gtweb -m "preparing for update" https://gtsvn.uit.no/langtech/tags/apps/dicts/nds svn copy https://gtsvn.uit.no/langtech/trunk/apps/dicts/nds nds-stable-gtweb """ def get_status_code(host, path="/"): """ This function retreives the status code of a website by requesting HEAD data from the host. This means that it only requests the headers. If the host cannot be reached or something else goes wrong, it returns None instead. """ import httplib try: conn = httplib.HTTPConnection(host) conn.request("HEAD", path) return conn.getresponse().status except StandardError: return None @task def test_running(): hosts = [ "kyv.oahpa.no", "saanih.oahpa.no", "valks.oahpa.no", "muter.oahpa.no", "saanih.oahpa.no", "saan.oahpa.no", "sonad.oahpa.no", "vada.oahpa.no", ] for h in hosts: code = get_status_code(h) if code != 200: col = red msg = 'ERROR? ' + str(code) + ' ' else: col = green msg = '' print(col(msg + h))