def stopall(self, sudo=False, quiet=True, singularity_options=None): """stop ALL instances. This command is only added to the command group as it doesn't make sense to call from a single instance Parameters ========== sudo: if the command should be done with sudo (exposes different set of instances) """ from spython.utils import check_install check_install() subgroup = "instance.stop" if "version 3" in self.version(): subgroup = ["instance", "stop"] cmd = self._init_command(subgroup, singularity_options) cmd = cmd + ["--all"] # Does the user want to see the command printed? if not (quiet or self.quiet): bot.info(" ".join(cmd)) output = run_command(cmd, sudo=sudo, quiet=quiet) if output["return_code"] != 0: message = "%s : return code %s" % (output["message"], output["return_code"]) bot.error(message) return output["return_code"] return output["return_code"]
def get(self, name, return_json=False, quiet=False, singularity_options=None): """get is a list for a single instance. It is assumed to be running, and we need to look up the PID, etc. """ from spython.utils import check_install check_install() # Ensure compatible for singularity prior to 3.0, and after 3.0 subgroup = "instance.list" if "version 3" in self.version(): subgroup = ["instance", "list"] cmd = self._init_command(subgroup, singularity_options) cmd.append(name) output = self.run_command(cmd, quiet=True) # Success, we have instances if output["return_code"] == 0: # Only print the table if we are returning json if not quiet: print("".join(output["message"])) # Prepare json result from table header = ["daemon_name", "pid", "container_image"] instances = parse_table(output["message"][0], header) # Does the user want instance objects instead? listing = [] if not return_json: for i in instances: new_instance = Instance( pid=i["pid"], name=i["daemon_name"], image=i["container_image"], start=False, ) listing.append(new_instance) instances = listing # Couldn't get UID elif output["return_code"] == 255: bot.error("Couldn't get UID") # Return code of 0 else: bot.info("No instances found.") # If we are given a name, return just one if name is not None and len(instances) == 1: instances = instances[0] return instances
def _load_from(self, line): '''load the From section of the recipe for the Dockerfile. ''' # Remove any comments line = line.split('#', 1)[0] line = re.sub('(F|f)(R|r)(O|o)(M|m):', '', line).strip() bot.info('FROM %s' % line) self.config['from'] = line
def stop( self, name=None, sudo=False, sudo_options=None, timeout=None, singularity_options=None, quiet=True, ): """stop an instance. This is done by default when an instance is created. Parameters ========== name: a name for the instance sudo: if the user wants to run the command with sudo singularity_options: a list of options to provide to the singularity client quiet: Do not print verbose output. timeout: forcebly kill non-stopped instance after the timeout specified in seconds USAGE: singularity [...] instance.stop [...] <instance name> """ from spython.utils import check_install, run_command check_install() subgroup = "instance.stop" if "version 3" in self.version(): subgroup = ["instance", "stop"] if timeout: subgroup += ["-t", str(timeout)] cmd = self._init_command(subgroup, singularity_options) # If name is provided assume referencing an instance instance_name = self.name if name is not None: instance_name = name cmd = cmd + [instance_name] # Print verbose output if not (quiet or self.quiet): bot.info(" ".join(cmd)) output = run_command(cmd, sudo=sudo, sudo_options=sudo_options, quiet=True) if output["return_code"] != 0: message = "%s : return code %s" % (output["message"], output["return_code"]) bot.error(message) return output["return_code"] return output["return_code"]
def pull(self, image=None, name=None, pull_folder='', ext="simg"): '''pull will pull a singularity hub or Docker image Parameters ========== image: the complete image uri. If not provided, the client loaded is used pull_folder: if not defined, pulls to $PWD (''). If defined, pulls to user specified location instead. Docker and Singularity Hub Naming --------------------------------- name: a custom name to use, to override default ext: if no name specified, the default extension to use. ''' self.check_install() cmd = self._init_command('pull') # No image provided, default to use the client's loaded image if image is None: image = self._get_uri() # If it's still None, no go! if image is None: bot.exit('You must provide an image uri, or use client.load() first.') # Singularity Only supports shub and Docker pull if not re.search('^(shub|docker)://', image): bot.exit("pull only valid for docker and shub. Use sregistry client.") # Did the user ask for a custom pull folder? if pull_folder: self.setenv('SINGULARITY_PULLFOLDER', pull_folder) # If we still don't have a custom name, base off of image uri. if name is None: name = self._get_filename(image, ext) cmd = cmd + ["--name", name] cmd.append(image) bot.info(' '.join(cmd)) self._run_command(cmd, capture=False) final_image = os.path.join(pull_folder, name) if os.path.exists(final_image): bot.info(final_image) return final_image
def check_install(software='singularity', quiet=True): '''check_install will attempt to run the singularity command, and return True if installed. The command line utils will not run without this check. ''' cmd = [software, '--version'] found = False try: version = run_command(cmd, quiet=True) except: # FileNotFoundError return found if version is not None: if version['return_code'] == 0: found = True if quiet is False: version = version['message'] bot.info("Found %s version %s" % (software.upper(), version)) return found
def check_install(software="singularity", quiet=True): """check_install will attempt to run the singularity command, and return True if installed. The command line utils will not run without this check. """ cmd = [software, "--version"] found = False try: version = run_command(cmd, quiet=True) except: # FileNotFoundError return found if version is not None: if version["return_code"] == 0: found = True if not quiet: version = version["message"] bot.info("Found %s version %s" % (software.upper(), version)) return found
def write(self, output_file=None, force=False): '''convert a recipe to a specified format, and write to file, meaning we use the loaded recipe to write to an output file. If the output file is not specified, a temporary file is used. Parameters ========== output_file: the file to save to, not required (estimates default) force: if True, if file exists, over-write existing file ''' if output_file is None: output_file = self._get_conversion_outfile() # Cut out early if file exists and we aren't overwriting if os.path.exists(output_file) and not force: bot.exit('%s exists, and force is False.' % output_file) # Do the conversion if function is provided by subclass if hasattr(self, 'convert'): converted = self.convert() bot.info('Saving to %s' % output_file) write_file(output_file, converted)
def save(self, output_file=None, convert_to=None, runscript="/bin/bash", force=False): '''save will convert a recipe to a specified format (defaults to the opposite of the recipe type originally loaded, (e.g., docker--> singularity and singularity-->docker) and write to an output file, if specified. If not specified, a temporary file is used. Parameters ========== output_file: the file to save to, not required (estimates default) convert_to: can be manually forced (docker or singularity) runscript: default runscript (entrypoint) to use force: if True, override discovery from Dockerfile ''' converted = self.convert(convert_to, runscript, force) if output_file is None: output_file = self._get_conversion_outfile(convert_to=None) bot.info('Saving to %s' % output_file) write_file(output_file, converted)
def instances(self, name=None, return_json=False, quiet=False): '''list instances. For Singularity, this is provided as a command sub group. singularity instance.list Return codes provided are different from standard linux: see https://github.com/singularityware/singularity/issues/1706 Parameters ========== return_json: return a json list of instances instead of objects (False) name: if defined, return the list for just one instance (used to ged pid) Return Code -- Reason 0 -- Instances Found 1 -- No Instances, libexecdir value not found, functions file not found 255 -- Couldn't get UID ''' from spython.instance.cmd.iutils import parse_table from spython.utils import check_install check_install() subgroup = 'instance.list' if get_singularity_version().find("version 3"): subgroup = ["instance", "list"] cmd = self._init_command(subgroup) # If the user has provided a name, we want to see a particular instance if name is not None: cmd.append(name) output = run_command(cmd, quiet=True) instances = None # Success, we have instances if output['return_code'] == 0: # Only print the table if we are returning json if quiet is False: print(''.join(output['message'])) # Prepare json result from table header = ['daemon_name', 'pid', 'container_image'] instances = parse_table(output['message'][0], header) # Does the user want instance objects instead? listing = [] if return_json is False: for i in instances: new_instance = self.instance(pid=i['pid'], name=i['daemon_name'], image=i['container_image'], start=False) listing.append(new_instance) instances = listing # Couldn't get UID elif output['return_code'] == 255: bot.error("Couldn't get UID") # Return code of 0 else: bot.info('No instances found.') # If we are given a name, return just one if name is not None and instances is not None: if len(instances) == 1: instances = instances[0] return instances
def pull(self, image=None, name=None, pull_folder='', ext=None, force=False, capture=False, stream=False, quiet=False): '''pull will pull a singularity hub or Docker image Parameters ========== image: the complete image uri. If not provided, the client loaded is used pull_folder: if not defined, pulls to $PWD (''). If defined, pulls to user specified location instead. Docker and Singularity Hub Naming --------------------------------- name: a custom name to use, to override default ext: if no name specified, the default extension to use. ''' from spython.utils import check_install check_install() cmd = self._init_command('pull') # Quiet is honored if set by the client, or user quiet = quiet or self.quiet if not ext: ext = 'sif' if 'version 3' in self.version() else 'simg' # No image provided, default to use the client's loaded image if image is None: image = self._get_uri() # If it's still None, no go! if image is None: bot.exit('You must provide an image uri, or use client.load() first.') # Singularity Only supports shub and Docker pull if not re.search('^(shub|docker)://', image): bot.exit("pull only valid for docker and shub. Use sregistry client.") # If we still don't have a custom name, base off of image uri. if name is None: name = self._get_filename(image, ext) if pull_folder: final_image = os.path.join(pull_folder, os.path.basename(name)) # Regression Singularity 3.* onward, PULLFOLDER not honored # https://github.com/sylabs/singularity/issues/2788 if 'version 3' in self.version(): name = final_image pull_folder = None # Don't use pull_folder else: final_image = name cmd = cmd + ["--name", name] if force: cmd = cmd + ["--force"] cmd.append(image) if not quiet: bot.info(' '.join(cmd)) with ScopedEnvVar('SINGULARITY_PULLFOLDER', pull_folder): # Option 1: Streaming we just run to show user if not stream: self._run_command(cmd, capture=capture, quiet=quiet) # Option 3: A custom name we can predict (not commit/hash) and can also show else: return final_image, stream_command(cmd, sudo=False) if os.path.exists(final_image) and not quiet: bot.info(final_image) return final_image
def run( self, image=None, args=None, app=None, sudo=False, writable=False, contain=False, bind=None, stream=False, nv=False, options=None, singularity_options=None, return_result=False, quiet=False, ): """ run will run the container, with or withour arguments (which should be provided in a list) Parameters ========== image: full path to singularity image args: args to include with the run app: if not None, execute a command in context of an app writable: This option makes the file system accessible as read/write options: an optional list of options to provide to run. singularity_options: a list of options to provide to the singularity client contain: This option disables the automatic sharing of writable filesystems on your host bind: list or single string of bind paths. This option allows you to map directories on your host system to directories within your container using bind mounts stream: if True, return <generator> for the user to run nv: if True, load Nvidia Drivers in runtime (default False) return_result: if True, return entire json object with return code and message result (default is False) quiet: print the command to the user """ from spython.utils import check_install check_install() cmd = self._init_command("run", singularity_options) # Does the user want to see the command printed? quiet = quiet or self.quiet # nv option leverages any GPU cards if nv: cmd += ["--nv"] # No image provided, default to use the client's loaded image if image is None: image = self._get_uri() # If an instance is provided, grab it's name if isinstance(image, self.instance): image = image.get_uri() # If image is still None, not defined by user or previously with client if image is None: bot.exit("Please load or provide an image.") # Does the user want to use bind paths option? if bind is not None: cmd += self._generate_bind_list(bind) # Does the user want to run an app? if app is not None: cmd = cmd + ["--app", app] # Does the user want writable? if writable: cmd.append("--writable") # Add options if options is not None: cmd = cmd + options cmd = cmd + [image] if args is not None: if not isinstance(args, list): args = args.split(" ") cmd = cmd + args if not quiet: bot.info(" ".join(cmd)) if not stream: result = self._run_command(cmd, sudo=sudo, return_result=return_result) else: return stream_command(cmd, sudo=sudo) # If the user wants the raw result object if return_result: return result # Otherwise, we parse the result if it was successful if result: result = result.strip("\n") try: result = json.loads(result) except: pass return result
def list_instances(self, name=None, return_json=False, quiet=False, sudo=False): """list instances. For Singularity, this is provided as a command sub group. singularity instance.list Return codes provided are different from standard linux: see https://github.com/singularityware/singularity/issues/1706 Parameters ========== return_json: return a json list of instances instead of objects (False) name: if defined, return the list for just one instance (used to ged pid) Return Code -- Reason 0 -- Instances Found 1 -- No Instances, libexecdir value not found, functions file not found 255 -- Couldn't get UID """ from spython.instance.cmd.iutils import parse_table from spython.utils import check_install check_install() subgroup = "instance.list" if "version 3" in self.version(): subgroup = ["instance", "list"] cmd = self._init_command(subgroup) # If the user has provided a name, we want to see a particular instance if name is not None: cmd.append(name) output = run_command(cmd, quiet=True, sudo=sudo) instances = [] # Success, we have instances if output["return_code"] == 0: # Only print the table if we are returning json if not quiet: print("".join(output["message"])) # Prepare json result from table # Singularity after 3.5.2 has an added ipaddress try: header = ["daemon_name", "pid", "container_image"] instances = parse_table(output["message"][0], header) except: header = ["daemon_name", "pid", "ip", "container_image"] instances = parse_table(output["message"][0], header) # Does the user want instance objects instead? listing = [] if not return_json: for i in instances: # If the user has provided a name, only add instance matches if name is not None: if name != i["daemon_name"]: continue # Otherwise, add instances to the listing new_instance = self.instance( pid=i["pid"], name=i["daemon_name"], image=i["container_image"], start=False, ) listing.append(new_instance) instances = listing # Couldn't get UID elif output["return_code"] == 255: bot.error("Couldn't get UID") # Return code of 0 else: bot.info("No instances found.") # If we are given a name, return just one if name is not None and instances not in [None, []]: if len(instances) == 1: instances = instances[0] return instances
def inspect( self, image=None, json=True, app=None, quiet=True, singularity_options=None ): """inspect will show labels, defile, runscript, and tests for an image Parameters ========== image: path of image to inspect json: print json instead of raw text (default True) quiet: Don't print result to the screen (default True) app: if defined, return help in context of an app singularity_options: a list of options to provide to the singularity client """ check_install() # No image provided, default to use the client's loaded image if not image: image = self._get_uri() # If there still isn't an image, exit on error if not image: bot.exit("Please provide an image to inspect.") cmd = self._init_command("inspect", singularity_options) if app: cmd = cmd + ["--app", app] options = ["e", "d", "l", "r", "hf", "t"] # After Singularity 3.0, helpfile was changed to H from if "version 3" in self.version(): options = ["e", "d", "l", "r", "H", "t"] for x in options: cmd.append("-%s" % x) if json: cmd.append("--json") cmd.append(image) # Does the user want to see the command printed? if not (quiet or self.quiet): bot.info(" ".join(cmd)) result = run_command(cmd, quiet=quiet) if result["return_code"] == 0: result = jsonp.loads(result["message"][0]) # Unify output to singularity 3 format if "data" in result: result = result["data"] # Fix up labels result = parse_labels(result) if not quiet: print(jsonp.dumps(result, indent=4)) return result
def list_instances( self, name=None, return_json=False, quiet=False, sudo=False, sudo_options=None, singularity_options=None, ): """list instances. For Singularity, this is provided as a command sub group. singularity instance list Return codes provided are different from standard linux: see https://github.com/singularityware/singularity/issues/1706 Since we expect json output, we don't support older versions of Singularity. Parameters ========== return_json: return a json list of instances instead of objects (False) name: if defined, return the list for just one instance (used to ged pid) singularity_options: a list of options to provide to the singularity client Return Code -- Reason 0 -- Instances Found 1 -- No Instances, libexecdir value not found, functions file not found 255 -- Couldn't get UID """ from spython.utils import check_install check_install() subgroup = ["instance", "list", "--json"] if "version 3" not in self.version(): bot.exit("This version of Singularity Python does not support < 3.0.") cmd = self._init_command(subgroup, singularity_options) # If the user has provided a name, we want to see a particular instance if name is not None: cmd.append(name) # Does the user want to see the command printed? if not (quiet or self.quiet): bot.info(" ".join(cmd)) output = run_command(cmd, quiet=True, sudo=sudo, sudo_options=sudo_options) instances = [] # Success, we have instances if output["return_code"] == 0: instances = json.loads(output["message"][0]).get("instances", {}) # Does the user want instance objects instead? listing = [] if not return_json: for i in instances: # If the user has provided a name, only add instance matches if name is not None: if name != i["instance"]: continue # Otherwise, add instances to the listing new_instance = self.instance( pid=i.get("pid"), ip_address=i.get("ip"), name=i.get("instance") or i.get("daemon_name"), log_err_path=i.get("logErrPath"), log_out_path=i.get("logOutPath"), image=i.get("img") or i.get("container_image"), start=False, ) listing.append(new_instance) instances = listing # Couldn't get UID elif output["return_code"] == 255: bot.error("Couldn't get UID") # Return code of 0 else: bot.info("No instances found.") # If we are given a name, return just one if name is not None and instances and len(instances) == 1: instances = instances[0] return instances
def shell( self, image, app=None, writable=False, contain=False, bind=None, nv=False, options=None, singularity_options=None, sudo=False, quiet=True, ): """shell into a container. A user is advised to use singularity to do this directly, however this function is useful for supporting tools. Parameters ========== image: full path to singularity image app: if not None, execute a shell in context of an app writable: This option makes the file system accessible as read/write contain: This option disables the automatic sharing of writable filesystems on your host options: an optional list of options to provide to shell. singularity_options: a list of options to provide to the singularity client bind: list or single string of bind paths. This option allows you to map directories on your host system to directories within your container using bind mounts nv: if True, load Nvidia Drivers in runtime (default False) """ from spython.utils import check_install check_install() cmd = self._init_command("shell", singularity_options) # nv option leverages any GPU cards if nv: cmd += ["--nv"] # Does the user want to use bind paths option? if bind is not None: cmd += self._generate_bind_list(bind) # Does the user want to run an app? if app is not None: cmd = cmd + ["--app", app] # Add additional options if options is not None: cmd = cmd + options if writable: cmd.append("--writable") # Finally, add the image or uri cmd.append(image) singularity = which("singularity") # Does the user want to see the command printed? if not (quiet or self.quiet): bot.info(" ".join(cmd)) if writable or sudo: os.execvp("sudo", ["sudo"] + cmd) else: os.execvp(singularity, cmd)
def execute( self, image=None, command=None, app=None, writable=False, contain=False, bind=None, stream=False, nv=False, return_result=False, options=None, singularity_options=None, sudo=False, sudo_options=None, quiet=True, environ=None, ): """execute: send a command to a container Parameters ========== image: full path to singularity image command: command to send to container app: if not None, execute a command in context of an app writable: This option makes the file system accessible as read/write contain: This option disables the automatic sharing of writable filesystems on your host options: an optional list of options to provide to execute. singularity_options: a list of options to provide to the singularity client bind: list or single string of bind paths. This option allows you to map directories on your host system to directories within your container using bind mounts nv: if True, load Nvidia Drivers in runtime (default False) return_result: if True, return entire json object with return code and message result not (default) quiet: Do not print verbose output. environ: extra environment to add. """ from spython.utils import check_install check_install() cmd = self._init_command("exec", singularity_options) # nv option leverages any GPU cards if nv: cmd += ["--nv"] # If the image is given as a list, it's probably the command if isinstance(image, list): command = image image = None if command is not None: # No image provided, default to use the client's loaded image if image is None: image = self._get_uri() self.quiet = True # If an instance is provided, grab it's name if isinstance(image, self.instance): image = image.get_uri() # If image is still None, not defined by user or previously with client if image is None: bot.exit("Please load or provide an image.") # Does the user want to use bind paths option? if bind is not None: cmd += self._generate_bind_list(bind) # Does the user want to run an app? if app is not None: cmd = cmd + ["--app", app] if writable: cmd.append("--writable") # Add additional options if options is not None: cmd = cmd + options if not isinstance(command, list): command = command.split(" ") cmd = cmd + [image] + command # Does the user want to see the command printed? if not (quiet or self.quiet): bot.info(" ".join(cmd)) if not stream: return self._run_command( cmd, sudo=sudo, sudo_options=sudo_options, return_result=return_result, quiet=quiet, environ=environ, ) return stream_command(cmd, sudo=sudo, sudo_options=sudo_options) bot.exit("Please include a command (list) to execute.")
def pull(self, image=None, name=None, pull_folder='', ext="simg", force=False, capture=False, name_by_commit=False, name_by_hash=False, stream=False): '''pull will pull a singularity hub or Docker image Parameters ========== image: the complete image uri. If not provided, the client loaded is used pull_folder: if not defined, pulls to $PWD (''). If defined, pulls to user specified location instead. Docker and Singularity Hub Naming --------------------------------- name: a custom name to use, to override default ext: if no name specified, the default extension to use. ''' from spython.utils import check_install check_install() cmd = self._init_command('pull') # No image provided, default to use the client's loaded image if image is None: image = self._get_uri() # If it's still None, no go! if image is None: bot.exit('You must provide an image uri, or use client.load() first.') # Singularity Only supports shub and Docker pull if not re.search('^(shub|docker)://', image): bot.exit("pull only valid for docker and shub. Use sregistry client.") # Did the user ask for a custom pull folder? if pull_folder: self.setenv('SINGULARITY_PULLFOLDER', pull_folder) # If we still don't have a custom name, base off of image uri. # Determine how to tell client to name the image, preference to hash if name_by_hash is True: cmd.append('--hash') elif name_by_commit is True: cmd.append('--commit') elif name is None: name = self._get_filename(image, ext) # Only add name if we aren't naming by hash or commit if not name_by_commit and not name_by_hash: cmd = cmd + ["--name", name] if force is True: cmd = cmd + ["--force"] cmd.append(image) bot.info(' '.join(cmd)) # If name is still None, make empty string if name is None: name = '' final_image = os.path.join(pull_folder, name) # Option 1: For hash or commit, need return value to get final_image if name_by_commit or name_by_hash: # Set pull to temporary location tmp_folder = tempfile.mkdtemp() self.setenv('SINGULARITY_PULLFOLDER', tmp_folder) self._run_command(cmd, capture=capture) try: tmp_image = os.path.join(tmp_folder, os.listdir(tmp_folder)[0]) final_image = os.path.join(pull_folder, os.path.basename(tmp_image)) shutil.move(tmp_image, final_image) shutil.rmtree(tmp_folder) except: bot.error('Issue pulling image with commit or hash, try without?') # Option 2: Streaming we just run to show user elif stream is False: self._run_command(cmd, capture=capture) # Option 3: A custom name we can predict (not commit/hash) and can also show else: return final_image, stream_command(cmd, sudo=False) if os.path.exists(final_image): bot.info(final_image) return final_image
def build( self, recipe=None, image=None, isolated=False, sandbox=False, writable=False, build_folder=None, robot_name=False, ext="sif", sudo=True, stream=False, force=False, options=None, quiet=False, return_result=False, sudo_options=None, singularity_options=None, ): """build a singularity image, optionally for an isolated build (requires sudo). If you specify to stream, expect the image name and an iterator to be returned. image, builder = Client.build(...) Parameters ========== recipe: the path to the recipe file (or source to build from). If not defined, we look for "Singularity" file in $PWD image: the image to build (if None, will use arbitary name isolated: if True, run build with --isolated flag sandbox: if True, create a writable sandbox writable: if True, use writable ext3 (sandbox takes preference) build_folder: where the container should be built. ext: the image extension to use. robot_name: boolean, default False. if you don't give your image a name (with "image") then a fun robot name will be generated instead. Highly recommended :) sudo: give sudo to the command (or not) default is True for build sudo_options: options to pass to sudo (e.g. --preserve-env=SINGULARITY_CACHEDIR,SINGULARITY_TMPDIR) options: for all other options, specify them in this list. singularity_options: a list of options to provide to the singularity client quiet: quiet verbose printing from the client. return_result: if True, return complete error code / message dictionary """ from spython.utils import check_install check_install() cmd = self._init_command("build", singularity_options) # If no extra options options = options or [] if "version 3" in self.version(): ext = "sif" # Force the build if the image / sandbox exists if force: cmd.append("--force") # No image provided, default to use the client's loaded image if recipe is None: recipe = self._get_uri() # If it's still None, try default build recipe if recipe is None: recipe = "Singularity" if not os.path.exists(recipe): bot.exit("Cannot find %s, exiting." % image) if image is None: if re.search("(docker|shub|library)://", recipe) and not robot_name: image = self._get_filename(recipe, ext) else: image = "%s.%s" % (self.RobotNamer.generate(), ext) # Does the user want a custom build folder? if build_folder is not None: if not os.path.exists(build_folder): bot.exit("%s does not exist!" % build_folder) image = os.path.join(build_folder, image) # The user wants to run an isolated build if isolated: cmd.append("--isolated") if sandbox: cmd.append("--sandbox") elif writable: cmd.append("--writable") cmd = cmd + options + [image, recipe] # Does the user want to see the command printed? if not (quiet or self.quiet): bot.info(" ".join(cmd)) if not stream: self._run_command( cmd, sudo=sudo, sudo_options=sudo_options, quiet=quiet, return_result=return_result, capture=False, ) else: # Here we return the expected image, and an iterator! # The caller must iterate over return image, stream_command(cmd, sudo=sudo, sudo_options=sudo_options) if os.path.exists(image): return image