Пример #1
0
def watchdog(type, message, variables = [], severity = WATCHDOG_NOTICE, \
    link = None):
    """
   Log a system message.
  
   @param type
     The category to which this message belongs.
   @param message
     The message to store in the log. See t() for documentation
     on how message and variables interact. Keep message
     translatable by not concatenating dynamic values into it!
   @param variables
     Array of variables to replace in the message on display or
     NULL if message is already translated or not possible to
     translate.
   @param severity
     The severity of the message, as per RFC 3164
   @param link
     A link to associate with the message.
  
   @see watchdog_severity_levels()
  """
    php.static(watchdog, 'in_error_state', False)
    # It is possible that the error handling will itself trigger an error.
    # In that case, we could
    # end up in an infinite loop.  To avoid that, we implement a simple
    # static semaphore.
    if (not watchdog.in_error_state):
        watchdog.in_error_state = True
        # Prepare the fields to be logged
        log_message = {
            'type': type,
            'message': message,
            'variables': variables,
            'severity': severity,
            'link': link,
            'user': lib_appglobals.user,
            'request_uri': lib_appglobals.base_root + request_uri(),
            'referer': php.SERVER['HTTP_REFERER'],
            'ip': ip_address(),
            'timestamp': REQUEST_TIME
        }
        # Call the logging hooks to log/process the message
        for plugin_ in lib_plugin.implements('watchdog', True):
            lib_plugin.invoke(plugin_, 'watchdog', log_message)
    watchdog.in_error_state = False
Пример #2
0
def watchdog(type, message, variables=[], severity=WATCHDOG_NOTICE, link=None):
    """
   Log a system message.
  
   @param type
     The category to which this message belongs.
   @param message
     The message to store in the log. See t() for documentation
     on how message and variables interact. Keep message
     translatable by not concatenating dynamic values into it!
   @param variables
     Array of variables to replace in the message on display or
     NULL if message is already translated or not possible to
     translate.
   @param severity
     The severity of the message, as per RFC 3164
   @param link
     A link to associate with the message.
  
   @see watchdog_severity_levels()
  """
    php.static(watchdog, "in_error_state", False)
    # It is possible that the error handling will itself trigger an error.
    # In that case, we could
    # end up in an infinite loop.  To avoid that, we implement a simple
    # static semaphore.
    if not watchdog.in_error_state:
        watchdog.in_error_state = True
        # Prepare the fields to be logged
        log_message = {
            "type": type,
            "message": message,
            "variables": variables,
            "severity": severity,
            "link": link,
            "user": lib_appglobals.user,
            "request_uri": lib_appglobals.base_root + request_uri(),
            "referer": php.SERVER["HTTP_REFERER"],
            "ip": ip_address(),
            "timestamp": REQUEST_TIME,
        }
        # Call the logging hooks to log/process the message
        for plugin_ in lib_plugin.implements("watchdog", True):
            lib_plugin.invoke(plugin_, "watchdog", log_message)
    watchdog.in_error_state = False
Пример #3
0
def registry_cache_hook_implementations(hook, write_to_persistent_cache=False):
    """
   Save hook implementations cache.
  
   @param hook
     Array with the hook name and list of plugins that implement it.
   @param write_to_persistent_cache
     Whether to write to the persistent cache.
  """
    php.static(registry_cache_hook_implementations, "implementations", {})
    if hook:
        # Newer is always better, so overwrite anything that's come before.
        registry_cache_hook_implementations.implementations[hook["hook"]] = hook["plugins"]
    if write_to_persistent_cache == True:
        # Only write this to cache if the implementations data we are going to cache
        # is different to what we loaded earlier in the request.
        if registry_cache_hook_implementations.implementations != lib_plugin.implements():
            cache_set("hooks", registry_cache_hook_implementations.implementations, "cache_registry")
Пример #4
0
def registry_cache_hook_implementations(hook, \
    write_to_persistent_cache = False):
    """
   Save hook implementations cache.
  
   @param hook
     Array with the hook name and list of plugins that implement it.
   @param write_to_persistent_cache
     Whether to write to the persistent cache.
  """
    php.static(registry_cache_hook_implementations, 'implementations', {})
    if (hook):
        # Newer is always better, so overwrite anything that's come before.
        registry_cache_hook_implementations.implementations[hook['hook']] = \
          hook['plugins']
    if (write_to_persistent_cache == True):
        # Only write this to cache if the implementations data we are going to cache
        # is different to what we loaded earlier in the request.
        if (registry_cache_hook_implementations.implementations != \
            lib_plugin.implements()):
            cache_set('hooks', registry_cache_hook_implementations.implementations, \
              'cache_registry')
Пример #5
0
class RPCLogging(Plugin):
    """This is a JSON-RPC service that provides logging functionality.
    
    As well as exposing rpc methods, it also provides configuration functions. 
    
    """

    _jsonrpcName = "logging"

    implements(IRPCService)

    def __init__(self, env):
        #setup logs from config file
        self.env = env
        self.config = env.config
        self.log = env.log

        # make sure the directory exists for the default log path
        path = self.config.get('logging', {}).get('default',
                                                  {}).get('path', None)
        if path != None:
            path = os.path.dirname(path)
            if not os.path.exists(path):
                os.makedirs(path)

        self.logs = {
        }  # mapping between lognames and the file handler instance we auto create

        for log in self.config.get('logging', {}).get('logs', {}):
            self.setup_log(log)

    @serviceProcedure(
        summary="Logs a message to the named logger with a given log level.",
        params=[String('name'),
                Number('level'),
                String('message')],
        ret=None)
    def log(self, name, level, message):
        """Logs a message to the named logger with a given log level."""

        if not isinstance(name, (str, unicode)) and \
            not isinstance(level, (int, float)) and \
            not isinstance(message, (str, unicode)):
            # this method does not return so just finish
            return

        log = logging.getLogger(name)

        if log.handlers == []:
            self.setup_log(name)

        log.log(level, message)

    def setup_log(self, name):
        cfg = self.config.get('logging', {})
        logs = cfg.get('logs', {})

        loggercfg = None
        fmtcfg = None
        default = False
        if logs.has_key(name):
            loggercfg = logs[name].get('logger', None)
            fmtcfg = logs[name].get('formatter', None)

        if loggercfg == None:
            loggercfg = cfg.get('default', {}).get('logger', {})
            default = True
        if fmtcfg == None:
            fmtcfg = cfg.get('default', {}).get('formatter', {})

        if default:
            fname = os.path.join(loggercfg.get('path', '.'), '%s.log' % name)
            level = loggercfg.get('level', 30)
        else:
            fname = loggercfg.get('path', None)

            if fname == None:
                fname = os.path.join(
                    cfg.get('default', {}).get('logger',
                                               {}).get('path', 'logs'),
                    '%s.log' % name)
            else:  # make sure the directory for the file exists
                path = os.path.dirname(fname)
                if not os.path.exists(path):
                    os.makedirs(path)

            level = loggercfg.get('level', None)
            if level == None:
                level = cfg.get('default', {}).get('logger',
                                                   {}).get('level', 30)

        mode = loggercfg.get('mode', 'a')

        if loggercfg.get('type', 0) == 0:
            handler = logging.FileHandler(fname, mode)
        else:
            maxbytes = loggercfg.get('maxbytes', 4096)
            buckupcount = loggercfg.get('buckupcount', 3)
            handler = logging.handlers.RotatingFileHandler(
                fname, mode, maxbytes, backupcount)

        fmt = fmtcfg.get('format', '%(asctime)s %(levelname)-8s %(message)s')
        datefmt = fmtcfg.get('dateformat', None)
        if datefmt == None:
            formatter = logging.Formatter(fmt)
        else:
            formatter = logging.Formatter(fmt, datefmt)

        handler.setFormatter(formatter)
        handler.setLevel(level)

        self.logs[name] = handler

        log = logging.getLogger(name)
        log.addHandler(handler)

    def reset_config(self):
        """Call this after the config has been changed to reconfigure the logs."""

        for name, hdlr in self.logs.iteritems():
            log = logging.getLogger(name)
            log.removeHandler(hdlr)
            self.setup_log(name)
Пример #6
0
class Update(Plugin):
    """This is a JSON-RPC service that provides updating functionality."""

    _jsonrpcName = "update"

    implements(IRPCService, IDownloadManipulator)

    #===============================================================================
    # service methods
    #===============================================================================

    @serviceProcedure(
        summary=
        "returns true if the system supplies a given update, false otherwise.",
        params=[String('name')],
        ret=Boolean())
    def hasUpdate(self, name):
        """returns true if the system supplies a given update, false otherwise.
        
        @param name: string containing the name of the update to check.
        @return: True if the system supplies the update, False otherwise.
        
        """

        return (self.getUpdateMetadata(name) != None)

    @serviceProcedure(summary="Returns the version of the update system.",
                      ret=Array())
    def systemVersion(self):
        """Returns the version of the update system.
        
        @return: An Array containing 2 numbers, the major and minor version
            numbers of the update system.
        
        """

        return _VERSION

    @serviceProcedure(
        summary=
        "Returns a string representation of the update systems version.",
        ret=String())
    def systemVersionString(self):
        """Returns a string representation of the update systems version.
        
        @return: a string representation of the update systems version.
        
        """

        return "%d.%d" % tuple(_VERSION)

    @serviceProcedure(
        summary=
        "Returns a string representation of the update systems version.",
        params=[String('update')],
        ret=Array())
    def version(self, update):
        """Returns the numeric version of a given update.
        
        @param update: the name of the update.
        @return: An Array containing 2 numbers, with the major and minor version numbers of the update.
        
        """

        meta = self.getUpdateMetadata(update)
        if meta == None:
            raise ApplicationError('unknown update %s' % update)
        else:
            return meta['version']

    @serviceProcedure(
        summary=
        "Returns the string representation of the version of a given update.",
        params=[String('update')],
        ret=String())
    def versionString(self, update):
        """Returns the string representation of the version of a given update.
        
        @param update: The name of the update.
        @return: A string containing the version of the update.
        
        """

        meta = self.getUpdateMetadata(update)
        if meta == None:
            raise ApplicationError('unknown update %s' % update)
        else:
            return '%d.%d' % tuple(meta['version'])\

    @serviceProcedure(summary="Returns the meta data of a given update.",
                      params=[String('update')],
                      ret=Object())
    def metaData(self, update):
        """Returns the meta data of a given update.
        
        @param update: The name of the update.
        @return: An Object containging the meta data with the following attributes::
            name: string <package name>,
            version: [major, minor] <update version>,
            timestamp: number <timestamp of update creation>,
            description: string <description>, (optional)
            files: [
              {
                file: string <file name>,
                dlpath: string <download path>,
                path: string <install path>,    (optional default local root directory)
              },
              ...
            ]

        """

        meta = self.getUpdateMetadata(update)
        if meta == None:
            raise ApplicationError('unknown update %s' % update)

        out = {}
        out['name'] = meta['name']
        out['version'] = meta['version']
        out['timestamp'] = meta['timestamp']
        if meta.had_key('description'):
            out['description'] = meta['description']
        files = []

        dlroot = self.config.get('update', {}).get('dlroot',
                                                   'update').strip('/')

        for f in meta['files']:
            tmp = {}
            tmp['file'] = f['file']
            tmp['dlpath'] = '/%s/%s/%s' % (dlroot, update, f['file'])
            if f.has_key('path'):
                tmp['path'] = f['path']
            files.append(tmp)

        out['files'] = files

        return out

    @serviceProcedure(
        summary="Returns the creation timestamp of a given update.",
        params=[String('update')],
        ret=Number())
    def timestamp(self, update):
        """Returns the creation timestamp of a given update.
        
        @param update: The name of the update
        @return: A float with the updates timestamp in seconds since Epoch (January 1, 1970).
        
        """

        meta = self.getUpdateMetadata(update)
        if meta == None:
            raise ApplicationError('unknown update %s' % update)

        return meta['timestamp']

    @serviceProcedure(
        summary=
        "Returns the creation timestamp of a given update as an ISO 8601 Format string.",
        params=[String('update')],
        ret=String())
    def timestampString(self, update):
        """Returns the creation timestamp of a given update as an ISO 8601 Format string.
        
        @param update: The name of the update.
        @return: The creation timestamp as an ISO Formated string.
        
        """

        meta = self.getUpdateMetadata(update)
        if meta == None:
            raise ApplicationError('unknown update %s' % update)

        return time.strftime('%Y-%m-%dT%H:%M:%S',
                             time.localtime(meta['timestamp']))

    @serviceProcedure(summary="Returns the description of a given update.",
                      params=[String('update')],
                      ret=String())
    def description(self, update):
        """Returns the description of a given update.
        
        @param update: The name of the update.
        @return: The updates description or an empty string if no description is given.
        
        """

        meta = self.getUpdateMetadata(update)
        if meta == None:
            raise ApplicationError('unknown update %s' % update)

        return meta.get('description', '')

    @serviceProcedure(
        summary="Returns an array of file meta data for a given update.",
        params=[String('update')],
        ret=Array())
    def files(self, update):
        """Returns an array of file meta data for a given update.
        
        @param update: The name of the update.
        @return: An Array of Objects with members::
            file: string <file name>,
            dlpath: string <download path>,
            path: string <install path>,    (optional default local root directory)
        
        """

        meta = self.getUpdateMetadata(update)
        if meta == None:
            raise ApplicationError('unknown update %s' % update)

        files = []

        dlroot = self.config.get('update', {}).get('dlroot',
                                                   'update').strip('/')

        for f in meta['files']:
            tmp = {}
            tmp['file'] = f['file']
            tmp['dlpath'] = '/%s/%s/%s' % (dlroot, update, f['file'])
            if f.has_key('path'):
                tmp['path'] = f['path']
            files.append(tmp)

        return files

    @serviceProcedure(
        summary="Returns the meta data for file within a given update.",
        params=[String('update'), String('filename')],
        ret=Object())
    def fileMetaData(self, update, filename):
        """Returns the meta data for file within a given update.
        
        @param update: The name of the update.
        @param filename: The file to retrieve the meta data for.
        @return: An Objects with members::
            file: string <file name>,
            dlpath: string <download path>,
            path: string <install path>,    (optional default local root directory)
        
        """

        meta = self.getUpdateMetadata(update)
        if meta == None:
            raise ApplicationError('unknown update %s' % update)

        dlroot = self.config.get('update', {}).get('dlroot',
                                                   'update').strip('/')

        for f in meta['files']:
            if f['file'] == filename:
                tmp = {}
                tmp['file'] = f['file']
                tmp['dlpath'] = '/%s/%s/%s' % (dlroot, update, f['file'])
                if f.has_key('path'):
                    tmp['path'] = f['path']

                return tmp
        else:
            raise ApplicationError('update %s does not contain file %s' %
                                   (update, filename))

    @serviceProcedure(
        summary="Returns the install path for a file within a given update.",
        params=[String('update'), String('filename')],
        ret=String())
    def filePath(self, update, filename):
        """Returns the install path for a file within a given update.
        
        @param update: The name of the update.
        @param filename: The file to retrieve the meta data for.
        @return: A string containing the install path for the file.
        
        """

        meta = self.getUpdateMetadata(update)
        if meta == None:
            raise ApplicationError('unknown update %s' % update)

        for f in meta['files']:
            if f['file'] == filename:
                path = ''
                if f.has_key('path'):
                    path = f['path']

                return path
        else:
            raise ApplicationError('update %s does not contain file %s' %
                                   (update, filename))

    #===============================================================================
    # IDownloadManipulator Methods
    #===============================================================================

    def download(self, head, tail, rootpath):
        """Fetch the data to be sent.
        
        This should return a string with the data to be returned.
        
        @param head: A string with the requested path that precedes tail.
        @param tail: the requested path relative to rootpath.
        @param rootpath: A string with the local rootpath for the path of the request. 
        """

        tail_ = tail.lstrip('/')
        tmp = tail_.split('/')
        update = tmp[0]
        path = '/'.join(tmp[1:])

        meta = self.getUpdateMetadata(update)
        if meta == None:
            raise ApplicationError('unknown update %s' % update)

        for f in meta['files']:
            if f['file'] == path:
                file_meta = f
        else:
            raise IOError('File %s is missing from update %s' %
                          (path, meta['name']))

        # is this a file system based update
        if meta['is_dir'] == True:
            data = open(os.path.join(meta['path'], path)).read()
        else:  # the update is an archive
            arch = zipfile.ZipFile(meta['path'], 'r', zipfile.ZIP_DEFLATED)
            data = arch.read(path)

        hash = hashlib.sha1()
        hash.update(data)

        if file_meta['hash'] != hash.hexdigest():
            self.log.debug(
                'file (%s) sha1 hash does not match update configuration.' %
                path)
            raise ApplicationError(
                'file (%s) sha1 hash does not match update configuration.' %
                path)

        return data

    def handles(self, head, tail):
        """Utility method to tell if the plugin handles a particular request.
        
        This will allow a plugin to only handle graphic files by only returning
        true if the file extension is .jpg, .png, etc.
        
        @param head: A string with the requested path that precedes tail.
        @param tail: the requested path relative to rootpath.
        @return: True if this plugin will handle the request, False otherwise. 
        """

        return True  # we handle all requests

    #===============================================================================
    # Utility Methods
    #===============================================================================

    def getUpdateMetadata(self, update):
        """This method returns the metadata for an update.
        
        @param update: string containing the name of the update to fetch the metadata for.
        @return: a dictionary containing Update metadata, if the update exists
            and is enabled, None otherwise.
        
        """

        path = ''

        for update_cfg in self.config.get('update', {}).get('updates', {}):
            if update_cfg['name'] == update:
                if not update_cfg.get('enabled', True):
                    return None

                path = update_cfg['path']
                if not os.path.exists(path):
                    return None
                break
        else:
            root = self.config.get('update', {}).get('rootpath', 'updates')
            for i in os.listdir(root):
                if i == update:
                    path = os.path.join(root, i)
                    break
            else:
                return None

        # have a path to a possible update, first check if its a file system update
        # or an archive update
        if os.path.isdir(path):
            update_path = os.path.join(path, 'update')
            if not os.path.exists(update_path):
                self.log.debug(
                    'update %s does not have an update configuration file' %
                    update)
                return None
            try:
                update_meta = load(open(filename), Loader=Loader)
            except:
                # the CLoader has problems with throwing exceptions so call the python version here to get a meaningful exception
                try:
                    load(open(filename))
                except YAMLError, e:
                    self.log.debug(
                        'update %s has invalid configuration file: %s' %
                        (update, str(e)))
                    return None
            update_meta['is_dir'] = True
        else:
Пример #7
0
class FileTransfer(Plugin):
    """This is a JSON-RPC service that provides download and uploading capabilities."""
    
    _jsonrpcName = "file"
    
    implements(IRPCService)
    
    download_plugins = ExtensionPoint(IDownloadManipulator)
    upload_plugins = ExtensionPoint(IUploadManipulator)
    listdir_plugins = ExtensionPoint(IListDir)
    
    @serviceProcedure(summary="This method is used to request a file from the server.",
                      params=[String('file'), Boolean('compress')],
                      ret=Object())
    def download(self, file, compress = False):
        """This method is used to request a file from the server.
        
        @param file: path of the file to download.
        @param compress: if True the contents of the file will be compressed
            with zlib before sending.
        @return: a JSON-RPC Object with keys data, crc, and compressed.
        
        """
        
        cfg = getConfig(file)
        dl_plugins = dict([[x.__class__.__name__, x] for x in self.download_plugins])
        for plugin in cfg['plugins']:
            if dl_plugins.has_key(plugin):
                plugin = dl_plugins[plugin]
                if plugin.handles(cfg['head'], cfg['tail']):
                    data = plugin.download(cfg['head'], cfg['tail'], cfg['rootpath'])
                    break
            else:
                self.log.debug("FileTransfer.download: Skipping plugin %s, not found" % plugin)
        else:   # default download handling, just send the file
            path = os.path.normpath(os.path.join(cfg['rootpath'], cfg['tail']))
            if not os.path.exists(path):
                raise ApplicationError('IOError: No such file or directory: %s' % file)
            if not os.isfile(path):
                raise ApplicationError('IOError: %s is a directory' % file)
            data = open(path, 'rb').read()
        
        # compute the crc of the data
        crc = crc16(data)
        
        # compress the data if so requested
        if compress:
            data = zlib.compress(data)
        
        # encode the data in base64
        data = base64.b64encode(data)
        
        return {'data': data, 'crc': crc}
    
    @serviceProcedure(summary="This method uploads a file to the server.",
                      params=[String('data'), Number('crc'), Boolean('compress')],
                      ret=None)
    def upload(self, filename, data, crc, compressed = False):
        """This method uploads a file to the server.
        
        @param filename: file name of the file to download.
        @param data: the contents of the file being uploaded in base64 format.
        @param crc: the CRC-16-IBM (CRC16) Cyclic Redundancy Check for the data.
        @param compressed: if True the data will be decompressed with zlib.
        
        """
        
        # decode the base64 encoded string
        data_ = base64.b64decode(data)
        # decompress the data if needed
        if compressed:
            data_ = zlib.decompress(data)
        
        # make sure we recieved what we were expecting by computing the crc value
        if crc16(data_) != crc:
            self.log.debug('FileTransfer.upload: crc values did not match for request %s' % filename)
            raise JSONRPCAssertionError('crc value does not match')
        
        cfg = getConfig(file)
        ul_plugins = dict([[x.__class__.__name__, x] for x in self.upload_plugins])
        for plugin in cfg['plugins']:
            if ul_plugins.has_key(plugin):
                plugin = ul_plugins[plugin]
                if plugin.handles(cfg['head'], cfg['tail']):
                    plugin.upload(cfg['head'], cfg['tail'], data_, cfg['rootpath'])
                    break
            else:
                self.log.debug("FileTransfer.upload: Skipping plugin %s, not found" % plugin)
        else:   # default upload handling, just save the data
            path = os.path.normpath(os.path.join(cfg['rootpath'], cfg['tail']))
            if not os.path.exists(path):
                raise ApplicationError('IOError: No such file or directory: %s' % filename)
            if not os.isfile(path):
                raise ApplicationError('IOError: %s is a directory' % filename)
            
            f = open(path, 'wb')
            f.write(data_)
            f.close()
    
    @serviceProcedure(summary="Returns a directory listing of the given path.",
                      params=[String('path')],
                      ret=Array())
    def listDir(self, path):
        """Returns a directory listing of the given path.
        
        @param dir: the directory to list the contents of.
        @return: An Array of Objects each one listing the contents of the directory.
            Each Object has the keys name, size,
            dir (if true object is a directory),
            and readonly (if true object is read-only).
        
        """
        
        cfg = getConfig(path)
        list_plugins = dict([[x.__class__.__name__, x] for x in self.listdir_plugins])
        for plugin in cfg['plugins']:
            if list_plugins.has_key(plugin):
                plugin = list_plugins[plugin]
                data = plugin.listDir(cfg['head'], cfg['tail'], cfg['rootpath'])
                break
            else:
                self.log.debug("FileTransfer.listDir: Skipping plugin %s, not found" % plugin)
        else:   # default listDir handling
            path = os.path.normpath(os.path.join(cfg['rootpath'], cfg['tail']))
            if not os.path.exists(path):
                raise ApplicationError('IOError: No such file or directory: %s' % path)
            if not os.isdir(path):
                raise ApplicationError('IOError: %s is not a directory' % path)
            
            out = []
            for i in os.listdir(path):
                name = i
                size = os.path.getsize(os.path.join(path, i))
                isdir = os.path.isdir(os.path.join(path, i))
                readonly = os.access(os.path.join(path, i), os.W_OK)
                out.append(makeDirectoryEntry(name, size, isdir, readonly))
                
            return out
            
    def getConfig(self, path):
        """Get the configuration for the supplied path.
        
        @param path: the requested file path.
        @return: A dictionary with the configuration values for the given path.
            It has the keys rootpath, readonly and plugin from the directory
            configuration as well as the keys head and tail that contain the
            configuration path and the remainder of the path relative to
            rootpath.
        
        """
        tpath = path
        if tpath == '':
            tpath = '/'
            
        cfg = self.config.get('file', {})
        pathcfg = {}
        head = path
        tail = ''
        for p in cfg.get('directories', {}):
            prefix = os.path.commonprefix((tpath, p))
            if prefix != '' and len(p) == len(prefix):
                pathcfg = cfg['directories'][p]
                head = prefix
                tail = tpath[len(prefix):]
                if tail[0] == '/':
                    tail = tail[1:]
                break
        
        out = {}
        
        if pathcfg.has_key('rootpath'):
            out['rootpath'] = pathcfg['rootpath']
        else:
            out['rootpath'] = cfg.get('rootpath', 'servdocs')
            head = '/'
            tail = tpath[1:]
        
        if not os.path.isabs(out['rootpath']):
            out['rootpath'] = os.path.abspath(out['rootpath'])
        
        out['readonly'] = pathcfg.get('readonly', False)
        out['plugins'] = pathcfg.get('plugins', [])
        out['head'] = head
        out['tail'] = tail
        
        return out