예제 #1
0
    def _multistage(self, fromHeader):
        """Given a from header, determine if we have a multistage build, and
        update the recipe parser active in case that we do. If we are dealing
        with the first layer and it's named, we also update the default
        name "spython-base" to be what the recipe intended.

        Parameters
        ==========
        fromHeader: the fromHeader parsed from self.from, possibly with AS
        """
        # Derive if there is a named layer
        match = re.search("AS (?P<layer>.+)", fromHeader, flags=re.I)
        if match:
            layer = match.groups("layer")[0].strip()

            # If it's the first layer named incorrectly, we need to rename
            if len(self.recipe) == 1 and list(
                    self.recipe)[0] == "spython-base":
                self.recipe[layer] = deepcopy(self.recipe[self.active_layer])
                del self.recipe[self.active_layer]
            else:
                self.active_layer_num += 1
                self.recipe[layer] = Recipe(self.filename,
                                            self.active_layer_num)
            self.active_layer = layer
            bot.debug("Active layer #%s updated to %s" %
                      (self.active_layer_num, self.active_layer))
예제 #2
0
    def parse(self):
        """parse is the base function for parsing the recipe, and extracting
           elements into the correct data structures. Everything is parsed into
           lists or dictionaries that can be assembled again on demand. 
    
           Singularity: we parse files/labels first, then install. 
                        cd first in a line is parsed as WORKDIR

        """
        # If the recipe isn't loaded, load it
        if not hasattr(self, "config"):
            self.load_recipe()

        # Parse each section
        for section, lines in self.config.items():
            bot.debug(section)

            # Get the correct parsing function
            parser = self._get_mapping(section)

            # Parse it, if appropriate
            if parser:
                parser(lines)

        return self.recipe
예제 #3
0
    def _add_section(self, line, section=None):
        '''parse a line for a section, and return the parsed section (if not
           None)

           Parameters
           ==========
           line: the line to parse
           section: the current (or previous) section

           Resulting data structure is:
           config['post'] (in lowercase)

        '''
        # Remove any comments
        line = line.split('#', 1)[0].strip()

        # Is there a section name?
        parts = line.split(' ')
        if len(parts) > 1:
            name = ' '.join(parts[1:])
        section = re.sub('[%]|(\s+)', '', parts[0]).lower()

        if section not in self.config:
            self.config[section] = []
            bot.debug("Adding section %s" % section)

        return section
예제 #4
0
    def _add_section(self, line, section=None):
        """parse a line for a section, and return the parsed section (if not
           None)

           Parameters
           ==========
           line: the line to parse
           section: the current (or previous) section

           Resulting data structure is:
           config['post'] (in lowercase)

        """
        # Remove any comments
        line = line.split("#", 1)[0].strip()

        # Is there a section name?
        parts = line.split(" ")
        section = re.sub(r"[%]|(\s+)", "", parts[0]).lower()

        if section not in self.config:
            self.config[section] = []
            bot.debug("Adding section %s" % section)

        return section
예제 #5
0
    def _arg(self, line):
        """singularity doesn't have support for ARG, so instead will issue
        a warning to the console for the user to export the variable
        with SINGULARITY prefixed at build.

        Parameters
        ==========
        line: the line from the recipe file to parse for ARG

        """
        line = self._setup("ARG", line)

        # Args are treated like envars, so we add them to install
        environ = self.parse_env([x for x in line if "=" in x])
        self.recipe[self.active_layer].install += environ

        # Try to extract arguments from the line
        for arg in line:

            # An undefined arg cannot be used
            if "=" not in arg:
                bot.warning(
                    "ARG is not supported for Singularity, and must be defined with "
                    "a default to be parsed. Skipping %s" % arg)
                continue

            arg, value = arg.split("=", 1)
            arg = arg.strip()
            value = value.strip()
            bot.debug("Updating ARG %s to %s" % (arg, value))
            self.args[arg] = value
예제 #6
0
    def _from(self, line):
        """get the FROM container image name from a FROM line!

        Parameters
        ==========
        line: the line from the recipe file to parse for FROM

        """
        self.recipe[self.active_layer].fromHeader = line
        bot.debug("FROM %s" % self.recipe[self.active_layer].fromHeader)
예제 #7
0
    def _from(self, line):
        ''' get the FROM container image name from a FROM line!

           Parameters
           ==========
           line: the line from the recipe file to parse for FROM

        '''
        self.fromHeader = line
        bot.debug('FROM %s' % self.fromHeader)
예제 #8
0
    def _setup(self, action, line):
        ''' replace the command name from the group, alert the user of content,
            and clean up empty spaces
        '''
        bot.debug('[in]  %s' % line)

        # Replace ACTION at beginning
        line = re.sub('^%s' % action, '', line)

        # Split into components
        return [x for x in self._split_line(line) if x not in ['', None]]
예제 #9
0
    def load_recipe(self):
        """load_recipe will return a loaded in singularity recipe. The idea
        is that these sections can then be parsed into a Dockerfile,
        or printed back into their original form.

        Returns
        =======
        config: a parsed recipe Singularity recipe
        """

        # Comments between sections, add to top of file
        lines = self.lines[:]
        fromHeader = None
        stage = None
        comments = []

        while lines:

            # Clean up white trailing/leading space
            line = lines.pop(0)
            stripped = line.strip()

            # Bootstrap Line
            if re.search("bootstrap", line, re.IGNORECASE):
                self._check_bootstrap(stripped)
                section = None

            # From Line
            elif re.search("from:", stripped, re.IGNORECASE):
                fromHeader = stripped
                if stage is None:
                    self._load_from(fromHeader)

            # Identify stage
            elif re.search("stage:", stripped, re.IGNORECASE):
                stage = re.sub("stage:", "", stripped.lower()).strip()
                self._multistage("as %s" % stage)
                self._load_from(fromHeader)

            # Comment
            elif stripped.startswith("#") and stripped not in comments:
                comments.append(stripped)

            # Section
            elif stripped.startswith("%"):
                section, layer = self._get_section(stripped)
                bot.debug("Found section %s" % section)

            # If we have a section, and are adding it
            elif section is not None:
                lines = [line] + lines
                self._load_section(lines=lines, section=section, layer=layer)

            self._comments(comments)
예제 #10
0
    def version(self):
        '''return the version of singularity
        '''

        if not check_install():
            bot.warning("Singularity version not found, so it's likely not installed.")
        else:
            cmd = ['singularity','--version']
            version = self._run_command(cmd).strip('\n')
            bot.debug("Singularity %s being used." % version)  
            return version
예제 #11
0
    def _from(self, line):
        ''' get the FROM container image name from a FROM line!

           Parameters
           ==========
           line: the line from the recipe file to parse for FROM

        '''
        self.fromHeader = self._setup('FROM', line)
        if "scratch" in self.fromHeader:
            bot.warning('scratch is no longer available on Docker Hub.')
        bot.debug('FROM %s' % self.fromHeader)
예제 #12
0
    def load_recipe(self):
        '''load will return a loaded in singularity recipe. The idea
           is that these sections can then be parsed into a Dockerfile,
           or printed back into their original form.

           Returns
           =======
           config: a parsed recipe Singularity recipe
        '''

        # Comments between sections, add to top of file
        lines = self.lines.copy()
        comments = []

        # Start with a fresh config!
        self.config = dict()
     
        section = None
        name = None

        while len(lines) > 0:

            # Clean up white trailing/leading space
            line = lines.pop(0)
            stripped = line.strip()

            # Bootstrap Line
            if re.search('(b|B)(o|O){2}(t|T)(s|S)(t|T)(r|R)(a|A)(p|P)', line):
                self._load_bootstrap(stripped)

            # From Line
            if re.search('(f|F)(r|R)(O|o)(m|M)', stripped):
                self._load_from(stripped)

            # Comment
            if stripped.startswith("#"):
                comments.append(stripped)
                continue

            # Section
            elif stripped.startswith('%'):
                section = self._add_section(stripped)
                bot.debug("Adding section title %s" %section)

            # If we have a section, and are adding it
            elif section is not None:
                lines = [line] + lines
                self._load_section(lines=lines,
                                   section=section)

            self.config['comments'] = comments
예제 #13
0
    def _setup(self, action, line):
        """replace the command name from the group, alert the user of content,
        and clean up empty spaces
        """
        bot.debug("[in]  %s" % line)

        # Replace ACTION at beginning
        line = re.sub("^%s" % action, "", line)

        # Handle continuation lines without ACTION by padding with leading space
        line = " " + line

        # Split into components
        return [x for x in self._split_line(line) if x not in ["", None]]
예제 #14
0
    def _from(self, line):
        """ get the FROM container image name from a FROM line!

           Parameters
           ==========
           line: the line from the recipe file to parse for FROM
           recipe: the recipe object to populate.
        """
        fromHeader = self._setup("FROM", line)

        # Singularity does not support AS level
        self.recipe.fromHeader = re.sub("AS .+", "", fromHeader[0], flags=re.I)

        if "scratch" in self.recipe.fromHeader:
            bot.warning("scratch is no longer available on Docker Hub.")
        bot.debug("FROM %s" % self.recipe.fromHeader)
예제 #15
0
    def _from(self, line):
        ''' get the FROM container image name from a FROM line!

           Parameters
           ==========
           line: the line from the recipe file to parse for FROM

        '''
        fromHeader = self._setup('FROM', line)

        # Singularity does not support AS level
        self.fromHeader = re.sub("AS .+", "", fromHeader[0], flags=re.I)

        if "scratch" in self.fromHeader:
            bot.warning('scratch is no longer available on Docker Hub.')
        bot.debug('FROM %s' % self.fromHeader)
예제 #16
0
def generate_bind_list(self, bindlist=None):
    """generate bind string will take a single string or list of binds, and
     return a list that can be added to an exec or run command. For example,
     the following map as follows:

    ['/host:/container', '/both'] --> ["--bind", "/host:/container","--bind","/both" ]
    ['/both']                     --> ["--bind", "/both"]
    '/host:container'             --> ["--bind", "/host:container"]
     None                         --> []

     An empty bind or otherwise value of None should return an empty list.
     The binds are also checked on the host.

     Parameters
     ==========
     bindlist: a string or list of bind mounts

    """
    binds = []

    # Case 1: No binds provided
    if not bindlist:
        return binds

    # Case 2: provides a long string or non list, and must be split
    if not isinstance(bindlist, list):
        bindlist = bindlist.split(" ")

    for bind in bindlist:

        # Still cannot be None
        if bind:
            bot.debug("Adding bind %s" % bind)
            binds += ["--bind", bind]

            # Check that exists on host
            host = bind.split(":")[0]
            if not os.path.exists(host):
                bot.error("%s does not exist on host." % bind)
                sys.exit(1)

    return binds
예제 #17
0
def set_verbosity(args):
    '''determine the message level in the environment to set based on args.
    '''
    level = "INFO"

    if args.debug:
        level = "DEBUG"
    elif args.quiet:
        level = "QUIET"

    os.environ['MESSAGELEVEL'] = level
    os.putenv('MESSAGELEVEL', level)
    os.environ['SINGULARITY_MESSAGELEVEL'] = level
    os.putenv('SINGULARITY_MESSAGELEVEL', level)
    
    # Import logger to set
    from spython.logger import bot
    bot.debug('Logging level %s' %level)
    import spython

    bot.debug("Singularity Python Version: %s" % spython.__version__)
예제 #18
0
    def _create_section(self, attribute, name=None, stage=None):
        """create a section based on key, value recipe pairs,
         This is used for files or label

        Parameters
        ==========
        attribute: the name of the data section, either labels or files
        name: the name to write to the recipe file (e.g., %name).
              if not defined, the attribute name is used.

        """

        # Default section name is the same as attribute
        if name is None:
            name = attribute

        # Put a space between sections
        section = ["\n"]

        # Only continue if we have the section and it's not empty
        try:
            section = getattr(self.recipe[self.stage], attribute)
        except AttributeError:
            bot.debug("Recipe does not have section for %s" % attribute)
            return section

        # if the section is empty, don't print it
        if not section:
            return section

        # Files
        if attribute in ["files", "labels"]:
            return create_keyval_section(section, name, stage)

        # An environment section needs exports
        if attribute in ["environ"]:
            return create_env_section(section, name)

        # Post, Setup
        return finish_section(section, name)
예제 #19
0
    def _from(self, line):
        """get the FROM container image name from a FROM line. If we have
        already seen a FROM statement, this is indicative of adding
        another image (multistage build).

        Parameters
        ==========
        line: the line from the recipe file to parse for FROM
        recipe: the recipe object to populate.
        """
        fromHeader = self._setup("FROM", line)

        # Do we have a multistge build to update the active layer?
        self._multistage(fromHeader[0])

        # Now extract the from header, make args replacements
        self.recipe[self.active_layer].fromHeader = self._replace_from_dict(
            re.sub("AS .+", "", fromHeader[0], flags=re.I), self.args)

        if "scratch" in self.recipe[self.active_layer].fromHeader:
            bot.warning("scratch is no longer available on Docker Hub.")
        bot.debug("FROM %s" % self.recipe[self.active_layer].fromHeader)