diff --git a/lib/ansible/callbacks.py b/lib/ansible/callbacks.py index 5b5058e1feb..cf223fef366 100644 --- a/lib/ansible/callbacks.py +++ b/lib/ansible/callbacks.py @@ -25,7 +25,7 @@ from ansible.color import stringc dirname = os.path.dirname(__file__) callbacks = utils.import_plugins(os.path.join(dirname, 'callback_plugins')) -callbacks = [ c.CallbackModule() for c in callbacks.values() if c.__name__ != '__init__' ] +callbacks = [ c.CallbackModule() for c in callbacks.values() ] cowsay = None if os.path.exists("/usr/bin/cowsay"): diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index 61b191171fb..bd4b8407b4d 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -43,6 +43,9 @@ try: except ImportError: HAS_ATFORK=False +dirname = os.path.dirname(__file__) +action_plugins = utils.import_plugins(os.path.join(dirname, 'action_plugins')) + ################################################ def _executor_hook(job_queue, result_queue): @@ -136,6 +139,11 @@ class Runner(object): # ensure we are using unique tmp paths random.seed() + # instantiate plugin classes + self.action_plugins = {} + for (k,v) in action_plugins.iteritems(): + self.action_plugins[k] = v.ActionModule(self) + # ***************************************************** def _delete_remote_files(self, conn, files): @@ -209,217 +217,6 @@ class Runner(object): # ***************************************************** - def _execute_raw(self, conn, tmp, inject=None): - ''' execute a non-module command for bootstrapping, or if there's no python on a device ''' - return ReturnData(conn=conn, result=dict( - stdout=self._low_level_exec_command(conn, self.module_args.encode('utf-8'), tmp, sudoable = True) - )) - - # *************************************************** - - def _execute_normal_module(self, conn, tmp, module_name, inject=None): - ''' transfer & execute a module that is not 'copy' or 'template' ''' - - # shell and command are the same module - if module_name == 'shell': - module_name = 'command' - self.module_args += " #USE_SHELL" - - vv("REMOTE_MODULE %s %s" % (module_name, self.module_args), host=conn.host) - exec_rc = self._execute_module(conn, tmp, module_name, self.module_args, inject=inject) - return exec_rc - - # ***************************************************** - - def _execute_async_module(self, conn, tmp, module_name, inject=None): - ''' transfer the given module name, plus the async module, then run it ''' - - # shell and command module are the same - module_args = self.module_args - if module_name == 'shell': - module_name = 'command' - module_args += " #USE_SHELL" - - (module_path, is_new_style) = self._copy_module(conn, tmp, module_name, inject) - self._low_level_exec_command(conn, "chmod a+rx %s" % module_path, tmp) - - return self._execute_module(conn, tmp, 'async_wrapper', module_args, - async_module=module_path, - async_jid=self.generated_jid, - async_limit=self.background, - inject=inject - ) - - # ***************************************************** - - def _execute_copy(self, conn, tmp, inject=None): - ''' handler for file transfer operations ''' - - # load up options - options = utils.parse_kv(self.module_args) - source = options.get('src', None) - dest = options.get('dest', None) - if (source is None and not 'first_available_file' in inject) or dest is None: - result=dict(failed=True, msg="src and dest are required") - return ReturnData(conn=conn, result=result) - - # if we have first_available_file in our vars - # look up the files and use the first one we find as src - if 'first_available_file' in inject: - found = False - for fn in inject.get('first_available_file'): - fn = utils.template(fn, inject) - if os.path.exists(fn): - source = fn - found = True - break - if not found: - results=dict(failed=True, msg="could not find src in first_available_file list") - return ReturnData(conn=conn, results=results) - - source = utils.template(source, inject) - source = utils.path_dwim(self.basedir, source) - - local_md5 = utils.md5(source) - if local_md5 is None: - result=dict(failed=True, msg="could not find src=%s" % source) - return ReturnData(conn=conn, result=result) - - remote_md5 = self._remote_md5(conn, tmp, dest) - - exec_rc = None - if local_md5 != remote_md5: - # transfer the file to a remote tmp location - tmp_src = tmp + os.path.basename(source) - conn.put_file(source, tmp_src) - # fix file permissions when the copy is done as a different user - if self.sudo and self.sudo_user != 'root': - self._low_level_exec_command(conn, "chmod a+r %s" % tmp_src, tmp) - - # run the copy module - self.module_args = "%s src=%s" % (self.module_args, tmp_src) - return self._execute_module(conn, tmp, 'copy', self.module_args, inject=inject).daisychain('file') - - else: - # no need to transfer the file, already correct md5 - result = dict(changed=False, md5sum=remote_md5, transferred=False) - return ReturnData(conn=conn, result=result).daisychain('file') - - # ***************************************************** - - def _execute_fetch(self, conn, tmp, inject=None): - ''' handler for fetch operations ''' - - # load up options - options = utils.parse_kv(self.module_args) - source = options.get('src', None) - dest = options.get('dest', None) - if source is None or dest is None: - results = dict(failed=True, msg="src and dest are required") - return ReturnData(conn=conn, result=results) - - # apply templating to source argument - source = utils.template(source, inject) - # apply templating to dest argument - dest = utils.template(dest, inject) - - # files are saved in dest dir, with a subdir for each host, then the filename - dest = "%s/%s/%s" % (utils.path_dwim(self.basedir, dest), conn.host, source) - dest = dest.replace("//","/") - - # calculate md5 sum for the remote file - remote_md5 = self._remote_md5(conn, tmp, source) - - # these don't fail because you may want to transfer a log file that possibly MAY exist - # but keep going to fetch other log files - if remote_md5 == '0': - result = dict(msg="unable to calculate the md5 sum of the remote file", file=source, changed=False) - return ReturnData(conn=conn, result=result) - if remote_md5 == '1': - result = dict(msg="the remote file does not exist, not transferring, ignored", file=source, changed=False) - return ReturnData(conn=conn, result=result) - if remote_md5 == '2': - result = dict(msg="no read permission on remote file, not transferring, ignored", file=source, changed=False) - return ReturnData(conn=conn, result=result) - - # calculate md5 sum for the local file - local_md5 = utils.md5(dest) - - if remote_md5 != local_md5: - # create the containing directories, if needed - if not os.path.isdir(os.path.dirname(dest)): - os.makedirs(os.path.dirname(dest)) - - # fetch the file and check for changes - conn.fetch_file(source, dest) - new_md5 = utils.md5(dest) - if new_md5 != remote_md5: - result = dict(failed=True, md5sum=new_md5, msg="md5 mismatch", file=source) - return ReturnData(conn=conn, result=result) - result = dict(changed=True, md5sum=new_md5) - return ReturnData(conn=conn, result=result) - else: - result = dict(changed=False, md5sum=local_md5, file=source) - return ReturnData(conn=conn, result=result) - - # ***************************************************** - - def _execute_template(self, conn, tmp, inject=None): - ''' handler for template operations ''' - - if not self.is_playbook: - raise errors.AnsibleError("in current versions of ansible, templates are only usable in playbooks") - - # load up options - options = utils.parse_kv(self.module_args) - source = options.get('src', None) - dest = options.get('dest', None) - if (source is None and 'first_available_file' not in inject) or dest is None: - result = dict(failed=True, msg="src and dest are required") - return ReturnData(conn=conn, comm_ok=False, result=result) - - # if we have first_available_file in our vars - # look up the files and use the first one we find as src - if 'first_available_file' in inject: - found = False - for fn in self.module_vars.get('first_available_file'): - fn = utils.template(fn, inject) - if os.path.exists(fn): - source = fn - found = True - break - if not found: - result = dict(failed=True, msg="could not find src in first_available_file list") - return ReturnData(conn=conn, comm_ok=False, result=result) - - source = utils.template(source, inject) - - # template the source data locally & transfer - try: - resultant = utils.template_from_file(self.basedir, source, inject) - except Exception, e: - result = dict(failed=True, msg=str(e)) - return ReturnData(conn=conn, comm_ok=False, result=result) - - xfered = self._transfer_str(conn, tmp, 'source', resultant) - - # run the copy module, queue the file module - self.module_args = "%s src=%s dest=%s" % (self.module_args, xfered, dest) - return self._execute_module(conn, tmp, 'copy', self.module_args, inject=inject).daisychain('file') - - # ***************************************************** - - def _execute_assemble(self, conn, tmp, inject=None): - ''' handler for assemble operations ''' - - # FIXME: once assemble is ported over to the use the new common logic, this method - # will be unneccessary as it can decide to daisychain via it's own module returns. - # and this function can be deleted. - - return self._execute_module(conn, tmp, 'assemble', self.module_args, inject=inject).daisychain('file') - - # ***************************************************** - def _executor(self, host): ''' handler for multiprocessing library ''' @@ -564,14 +361,14 @@ class Runner(object): tmp = self._make_tmp_path(conn) result = None - handler = getattr(self, "_execute_%s" % self.module_name, None) + handler = self.action_plugins.get(self.module_name, None) if handler: - result = handler(conn, tmp, inject=inject) + result = handler.run(conn, tmp, module_name, inject) else: if self.background == 0: - result = self._execute_normal_module(conn, tmp, module_name, inject=inject) + result = self.action_plugins['normal'].run(conn, tmp, module_name, inject) else: - result = self._execute_async_module(conn, tmp, module_name, inject=inject) + result = self.action_plugins['async'].run(conn, tmp, module_name, inject) if result.is_successful() and 'daisychain' in result.result: self.module_name = result.result['daisychain'] diff --git a/lib/ansible/runner/action_plugins/assemble.py b/lib/ansible/runner/action_plugins/assemble.py new file mode 100644 index 00000000000..d529caf4dfe --- /dev/null +++ b/lib/ansible/runner/action_plugins/assemble.py @@ -0,0 +1,43 @@ +# (c) 2012, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import os +import pwd +import random +import traceback +import tempfile + +import ansible.constants as C +from ansible import utils +from ansible import errors +from ansible import module_common +from ansible.runner.return_data import ReturnData + +class ActionModule(object): + + def __init__(self, runner): + self.runner = runner + + def run(self, conn, tmp, module_name, inject=None): + ''' handler for assemble operations ''' + + # FIXME: since assemble is ported over to the use the new common logic, this method + # is actually unneccessary as it can decide to daisychain via it's own module returns. + # make assemble return daisychain_args and this will go away. + + return self.runner._execute_module(conn, tmp, 'assemble', self.runner.module_args, inject=inject).daisychain('file') + diff --git a/lib/ansible/runner/action_plugins/async.py b/lib/ansible/runner/action_plugins/async.py new file mode 100644 index 00000000000..19a5a186fdc --- /dev/null +++ b/lib/ansible/runner/action_plugins/async.py @@ -0,0 +1,53 @@ +# (c) 2012, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import os +import pwd +import random +import traceback +import tempfile + +import ansible.constants as C +from ansible import utils +from ansible import errors +from ansible import module_common +from ansible.runner.return_data import ReturnData + +class ActionModule(object): + + def __init__(self, runner): + self.runner = runner + + def run(self, conn, tmp, module_name, inject): + ''' transfer the given module name, plus the async module, then run it ''' + + # shell and command module are the same + module_args = self.runner.module_args + if module_name == 'shell': + module_name = 'command' + module_args += " #USE_SHELL" + + (module_path, is_new_style) = self.runner._copy_module(conn, tmp, module_name, inject) + self.runner._low_level_exec_command(conn, "chmod a+rx %s" % module_path, tmp) + + return self.runner._execute_module(conn, tmp, 'async_wrapper', module_args, + async_module=module_path, + async_jid=self.runner.generated_jid, + async_limit=self.runner.background, + inject=inject + ) + diff --git a/lib/ansible/runner/action_plugins/copy.py b/lib/ansible/runner/action_plugins/copy.py new file mode 100644 index 00000000000..2747ad959be --- /dev/null +++ b/lib/ansible/runner/action_plugins/copy.py @@ -0,0 +1,87 @@ +# (c) 2012, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import os +import pwd +import random +import traceback +import tempfile + +import ansible.constants as C +from ansible import utils +from ansible import errors +from ansible import module_common +from ansible.runner.return_data import ReturnData + +class ActionModule(object): + + def __init__(self, runner): + self.runner = runner + + def run(self, conn, tmp, module_name, inject): + ''' handler for file transfer operations ''' + + # load up options + options = utils.parse_kv(self.runner.module_args) + source = options.get('src', None) + dest = options.get('dest', None) + if (source is None and not 'first_available_file' in inject) or dest is None: + result=dict(failed=True, msg="src and dest are required") + return ReturnData(conn=conn, result=result) + + # if we have first_available_file in our vars + # look up the files and use the first one we find as src + if 'first_available_file' in inject: + found = False + for fn in inject.get('first_available_file'): + fn = utils.template(fn, inject) + if os.path.exists(fn): + source = fn + found = True + break + if not found: + results=dict(failed=True, msg="could not find src in first_available_file list") + return ReturnData(conn=conn, results=results) + + source = utils.template(source, inject) + source = utils.path_dwim(self.runner.basedir, source) + + local_md5 = utils.md5(source) + if local_md5 is None: + result=dict(failed=True, msg="could not find src=%s" % source) + return ReturnData(conn=conn, result=result) + + remote_md5 = self.runner._remote_md5(conn, tmp, dest) + + exec_rc = None + if local_md5 != remote_md5: + # transfer the file to a remote tmp location + tmp_src = tmp + os.path.basename(source) + conn.put_file(source, tmp_src) + # fix file permissions when the copy is done as a different user + if self.runner.sudo and self.runner.sudo_user != 'root': + self.runner._low_level_exec_command(conn, "chmod a+r %s" % tmp_src, tmp) + + # run the copy module + self.runner.module_args = "%s src=%s" % (self.runner.module_args, tmp_src) + return self.runner._execute_module(conn, tmp, 'copy', self.runner.module_args, inject=inject).daisychain('file') + + else: + # no need to transfer the file, already correct md5 + result = dict(changed=False, md5sum=remote_md5, transferred=False) + return ReturnData(conn=conn, result=result).daisychain('file') + diff --git a/lib/ansible/runner/action_plugins/fetch.py b/lib/ansible/runner/action_plugins/fetch.py new file mode 100644 index 00000000000..a487094bbc6 --- /dev/null +++ b/lib/ansible/runner/action_plugins/fetch.py @@ -0,0 +1,89 @@ +# (c) 2012, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import os +import pwd +import random +import traceback +import tempfile + +import ansible.constants as C +from ansible import utils +from ansible import errors +from ansible import module_common +from ansible.runner.return_data import ReturnData + +class ActionModule(object): + + def __init__(self, runner): + self.runner = runner + + def run(self, conn, tmp, module_name, inject): + ''' handler for fetch operations ''' + + # load up options + options = utils.parse_kv(self.runner.module_args) + source = options.get('src', None) + dest = options.get('dest', None) + if source is None or dest is None: + results = dict(failed=True, msg="src and dest are required") + return ReturnData(conn=conn, result=results) + + # apply templating to source argument + source = utils.template(source, inject) + # apply templating to dest argument + dest = utils.template(dest, inject) + + # files are saved in dest dir, with a subdir for each host, then the filename + dest = "%s/%s/%s" % (utils.path_dwim(self.runner.basedir, dest), conn.host, source) + dest = dest.replace("//","/") + + # calculate md5 sum for the remote file + remote_md5 = self.runner._remote_md5(conn, tmp, source) + + # these don't fail because you may want to transfer a log file that possibly MAY exist + # but keep going to fetch other log files + if remote_md5 == '0': + result = dict(msg="unable to calculate the md5 sum of the remote file", file=source, changed=False) + return ReturnData(conn=conn, result=result) + if remote_md5 == '1': + result = dict(msg="the remote file does not exist, not transferring, ignored", file=source, changed=False) + return ReturnData(conn=conn, result=result) + if remote_md5 == '2': + result = dict(msg="no read permission on remote file, not transferring, ignored", file=source, changed=False) + return ReturnData(conn=conn, result=result) + + # calculate md5 sum for the local file + local_md5 = utils.md5(dest) + + if remote_md5 != local_md5: + # create the containing directories, if needed + if not os.path.isdir(os.path.dirname(dest)): + os.makedirs(os.path.dirname(dest)) + + # fetch the file and check for changes + conn.fetch_file(source, dest) + new_md5 = utils.md5(dest) + if new_md5 != remote_md5: + result = dict(failed=True, md5sum=new_md5, msg="md5 mismatch", file=source) + return ReturnData(conn=conn, result=result) + result = dict(changed=True, md5sum=new_md5) + return ReturnData(conn=conn, result=result) + else: + result = dict(changed=False, md5sum=local_md5, file=source) + return ReturnData(conn=conn, result=result) + diff --git a/lib/ansible/runner/action_plugins/normal.py b/lib/ansible/runner/action_plugins/normal.py new file mode 100644 index 00000000000..b5130a3d55f --- /dev/null +++ b/lib/ansible/runner/action_plugins/normal.py @@ -0,0 +1,47 @@ +# (c) 2012, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import os +import pwd +import random +import traceback +import tempfile + +import ansible.constants as C +from ansible import utils +from ansible import errors +from ansible import module_common +from ansible.runner.return_data import ReturnData +from ansible.callbacks import vv, vvv + +class ActionModule(object): + + def __init__(self, runner): + self.runner = runner + + def run(self, conn, tmp, module_name, inject): + ''' transfer & execute a module that is not 'copy' or 'template' ''' + + # shell and command are the same module + if module_name == 'shell': + module_name = 'command' + self.runner.module_args += " #USE_SHELL" + + vv("REMOTE_MODULE %s %s" % (module_name, self.runner.module_args), host=conn.host) + return self.runner._execute_module(conn, tmp, module_name, self.runner.module_args, inject=inject) + + diff --git a/lib/ansible/runner/action_plugins/raw.py b/lib/ansible/runner/action_plugins/raw.py new file mode 100644 index 00000000000..ba52809e174 --- /dev/null +++ b/lib/ansible/runner/action_plugins/raw.py @@ -0,0 +1,39 @@ +# (c) 2012, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import os +import pwd +import random +import traceback +import tempfile + +import ansible.constants as C +from ansible import utils +from ansible import errors +from ansible import module_common +from ansible.runner.return_data import ReturnData + +class ActionModule(object): + + def __init__(self, runner): + self.runner = runner + + def run(self, conn, tmp, module_name, inject): + return ReturnData(conn=conn, result=dict( + stdout=self.runner._low_level_exec_command(conn, self.runner.module_args.encode('utf-8'), tmp, sudoable=True) + )) + diff --git a/lib/ansible/runner/action_plugins/template.py b/lib/ansible/runner/action_plugins/template.py new file mode 100644 index 00000000000..257da4a3a54 --- /dev/null +++ b/lib/ansible/runner/action_plugins/template.py @@ -0,0 +1,78 @@ +# (c) 2012, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import os +import pwd +import random +import traceback +import tempfile + +import ansible.constants as C +from ansible import utils +from ansible import errors +from ansible import module_common +from ansible.runner.return_data import ReturnData + +class ActionModule(object): + + def __init__(self, runner): + self.runner = runner + + def run(self, conn, tmp, module_name, inject): + ''' handler for template operations ''' + + if not self.runner.is_playbook: + raise errors.AnsibleError("in current versions of ansible, templates are only usable in playbooks") + + # load up options + options = utils.parse_kv(self.runner.module_args) + source = options.get('src', None) + dest = options.get('dest', None) + if (source is None and 'first_available_file' not in inject) or dest is None: + result = dict(failed=True, msg="src and dest are required") + return ReturnData(conn=conn, comm_ok=False, result=result) + + # if we have first_available_file in our vars + # look up the files and use the first one we find as src + if 'first_available_file' in inject: + found = False + for fn in self.runner.module_vars.get('first_available_file'): + fn = utils.template(fn, inject) + if os.path.exists(fn): + source = fn + found = True + break + if not found: + result = dict(failed=True, msg="could not find src in first_available_file list") + return ReturnData(conn=conn, comm_ok=False, result=result) + + source = utils.template(source, inject) + + # template the source data locally & transfer + try: + resultant = utils.template_from_file(self.runner.basedir, source, inject) + except Exception, e: + result = dict(failed=True, msg=str(e)) + return ReturnData(conn=conn, comm_ok=False, result=result) + + xfered = self.runner._transfer_str(conn, tmp, 'source', resultant) + + # run the copy module, queue the file module + self.module_args = "%s src=%s dest=%s" % (self.runner.module_args, xfered, dest) + return self._runner.execute_module(conn, tmp, 'copy', self.runner.module_args, inject=inject).daisychain('file') + + diff --git a/lib/ansible/utils.py b/lib/ansible/utils.py index 47dbe411dd4..e6f7712c60b 100644 --- a/lib/ansible/utils.py +++ b/lib/ansible/utils.py @@ -485,6 +485,8 @@ def filter_leading_non_json_lines(buf): def import_plugins(directory): modules = {} for path in glob.glob(os.path.join(directory, '*.py')): + if path.startswith("_"): + continue name, ext = os.path.splitext(os.path.basename(path)) if not name.startswith("_"): modules[name] = imp.load_source(name, path)