def _logs(self, print_logs=False, ext="out"): """A shared function to print log files. The only differing element is the extension (err or out) """ from spython.utils import check_install check_install() # Formulate the path of the logs hostname = platform.node() logpath = os.path.join( get_userhome(), ".singularity", "instances", "logs", hostname, get_username(), "%s.%s" % (self.name, ext), ) if os.path.exists(logpath): with open(logpath, "r") as filey: logs = filey.read() if print_logs is True: print(logs) else: bot.warning("No log files have been produced.") return logs
def stop(self, name=None, sudo=False): '''start an instance. This is done by default when an instance is created. Parameters ========== image: optionally, an image uri (if called as a command from Client) name: a name for the instance sudo: if the user wants to run the command with sudo USAGE: singularity [...] instance.start [...] <container path> <instance name> ''' from spython.utils import (check_install, run_command) check_install() cmd = self._init_command('instance.stop') # If name is provided assume referencing an instance instance_name = self.name if name is not None: instance_name = name cmd = cmd + [instance_name] output = run_command(cmd, sudo=sudo, 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 apps(self, image=None, full_path=False, root=''): ''' return list of SCIF apps in image. The Singularity software serves a scientific filesystem integration that will install apps to /scif/apps and associated data to /scif/data. For more information about SCIF, see https://sci-f.github.io Parameters ========== full_path: if True, return relative to scif base folder image_path: full path to the image ''' from spython.utils import check_install check_install() # No image provided, default to use the client's loaded image if image is None: image = self._get_uri() cmd = self._init_command('apps') + [image] output = self._run_command(cmd) if full_path: root = '/scif/apps/' if output: output = ''.join(output).split('\n') output = ['%s%s' %(root, x) for x in output if x] return output
def inspect(self, image=None, json=True, app=None): '''inspect will show labels, defile, runscript, and tests for an image Parameters ========== image_path: path of image to inspect json: print json instead of raw text (default True) app: if defined, return help in context of an app ''' from spython.utils import check_install check_install() # No image provided, default to use the client's loaded image if image is None: image = self._get_uri() cmd = self._init_command('inspect') if app is not None: cmd = cmd + ['--app', app] options = ['e', 'd', 'l', 'r', 'hf', 't'] [cmd.append('-%s' % x) for x in options] if json is True: cmd.append('--json') cmd.append(image) output = self._run_command(cmd) #self.println(output,quiet=self.quiet) return output
def stopall(self, sudo=False, quiet=True): """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) cmd = cmd + ["--all"] 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 shell(self, image, app=None, writable=False, contain=False, bind=None, nv=False, options=None, sudo=False): ''' 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. 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') # 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') if writable or sudo: os.execvp("sudo", ["sudo"] + cmd) else: os.execvp(singularity, cmd)
def start(self, image=None, name=None, sudo=False, options=[], capture=False): '''start an instance. This is done by default when an instance is created. Parameters ========== image: optionally, an image uri (if called as a command from Client) name: a name for the instance sudo: if the user wants to run the command with sudo capture: capture output, default is False. With True likely to hang. options: a list of tuples, each an option to give to the start command [("--bind", "/tmp"),...] USAGE: singularity [...] instance.start [...] <container path> <instance name> ''' from spython.utils import (run_command, check_install) check_install() # If no name provided, give it an excellent one! if name is None: name = self.RobotNamer.generate() self.name = name.replace('-', '_') # If an image isn't provided, we have an initialized instance if image is None: # Not having this means it was called as a command, without an image if not hasattr(self, "_image"): bot.error('Please provide an image, or create an Instance first.') sys.exit(1) image = self._image cmd = self._init_command('instance.start') # Add options, if they are provided if not isinstance(options, list): options = options.split(' ') # Assemble the command! cmd = cmd + options + [image, self.name] # Save the options and cmd, if the user wants to see them later self.options = options self.cmd = cmd output = run_command(cmd, sudo=sudo, quiet=True, capture=capture) if output['return_code'] == 0: self._update_metadata() else: message = '%s : return code %s' % (output['message'], output['return_code']) bot.error(message) return self
def test_check_install(self): '''check install is used to check if a particular software is installed. If no command is provided, singularity is assumed to be the test case''' print("Testing utils.check_install") from spython.utils import check_install is_installed = check_install() self.assertTrue(is_installed) is_not_installed = check_install('fakesoftwarename') self.assertTrue(not is_not_installed)
def export( self, image_path, pipe=False, output_file=None, command=None, sudo=False, singularity_options=None, ): """export will export an image, sudo must be used. If we have Singularity versions after 3, export is replaced with building into a sandbox. Parameters ========== image_path: full path to image pipe: export to pipe and not file (default, False) singularity_options: a list of options to provide to the singularity client output_file: if pipe=False, export tar to this file. If not specified, will generate temporary directory. """ from spython.utils import check_install check_install() if "version 3" in self.version() or "2.6" in self.version(): # If export is deprecated, we run a build bot.warning( "Export is not supported for Singularity 3.x. Building to sandbox instead." ) if output_file is None: basename, _ = os.path.splitext(image_path) output_file = self._get_filename(basename, "sandbox", pwd=False) return self.build( recipe=image_path, image=output_file, sandbox=True, force=True, sudo=sudo, singularity_options=singularity_options, ) # If not version 3, run deprecated command elif "2.5" in self.version(): return self._export( image_path=image_path, pipe=pipe, output_file=output_file, command=command, singularity_options=singularity_options, ) bot.warning("Unsupported version of Singularity, %s" % self.version())
def get(self, name, return_json=False, quiet=False): '''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, get_singularity_version) check_install() # Ensure compatible for singularity prior to 3.0, and after 3.0 subgroup = "instance.list" if (get_singularity_version().find("version 3") != -1): subgroup = ["instance", "list"] cmd = self._init_command(subgroup) cmd.append(name) output = run_command(cmd, quiet=True) # 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 = 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 test_check_install(): """check install is used to check if a particular software is installed. If no command is provided, singularity is assumed to be the test case""" print("Testing utils.check_install") from spython.utils import check_install is_installed = check_install() assert is_installed is_not_installed = check_install("fakesoftwarename") assert not is_not_installed
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 inspect(self, image=None, json=True, app=None, quiet=True): '''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 ''' 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') 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) 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 helpcmd(self, command=None): '''help prints the general function help, or help for a specific command Parameters ========== command: the command to get help for, if none, prints general help ''' from spython.utils import check_install check_install() cmd = ['singularity', '--help'] if command is not None: cmd.append(command) return self._run_command(cmd)
def importcmd(self, image_path, input_source): '''import will import (stdin) to the image Parameters ========== image_path: path to image to import to. input_source: input source or file import_type: if not specified, imports whatever function is given ''' from spython.utils import check_install check_install() cmd = ['singularity', 'image.import', image_path, input_source] output = self.run_command(cmd, sudo=False) self.println(output) return image_path
def export(self, image_path, pipe=False, output_file=None, command=None, sudo=False): '''export will export an image, sudo must be used. If we have Singularity versions after 3, export is replaced with building into a sandbox. Parameters ========== image_path: full path to image pipe: export to pipe and not file (default, False) output_file: if pipe=False, export tar to this file. If not specified, will generate temporary directory. ''' from spython.utils import check_install check_install() if 'version 3' in self.version() or '2.6' in self.version(): # If export is deprecated, we run a build bot.warning( 'Export is not supported for Singularity 3.x. Building to sandbox instead.' ) if output_file is None: basename, _ = os.path.splitext(image_path) output_file = self._get_filename(basename, 'sandbox', pwd=False) return self.build(recipe=image_path, image=output_file, sandbox=True, force=True, sudo=sudo) # If not version 3, run deprecated command elif '2.5' in self.version(): return self._export(image_path=image_path, pipe=pipe, output_file=output_file, command=command) bot.warning('Unsupported version of Singularity, %s' % self.version())
def export(self, image_path, tmptar=None): '''export will export an image, sudo must be used. Parameters ========== image_path: full path to image tmptar: if defined, use custom temporary path for tar export ''' from spython.utils import check_install check_install() if tmptar is None: tmptar = "/%s/tmptar.tar" %(tempfile.mkdtemp()) cmd = ['singularity', 'image.export', '-f', tmptar, image_path] output = self.run_command(cmd, sudo=False) return tmptar
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
def stop(self, name=None, sudo=False): """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 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"] cmd = self._init_command(subgroup) # If name is provided assume referencing an instance instance_name = self.name if name is not None: instance_name = name cmd = cmd + [instance_name] output = run_command(cmd, sudo=sudo, 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 create(self, image_path, size=1024, sudo=False): '''create will create a a new image Parameters ========== image_path: full path to image size: image sizein MiB, default is 1024MiB filesystem: supported file systems ext3/ext4 (ext[2/3]: default ext3 ''' from spython.utils import check_install check_install() cmd = self.init_command('image.create') cmd = cmd + ['--size', str(size), image_path] output = self.run_command(cmd, sudo=sudo) self.println(output) if not os.path.exists(image_path): bot.exit("Could not create image %s" % image_path) return image_path
def export(self, image_path, tmptar=None): """export will export an image, sudo must be used. Parameters ========== image_path: full path to image tmptar: if defined, use custom temporary path for tar export """ from spython.utils import check_install check_install() if "version 3" in self.version(): bot.exit("export is deprecated after Singularity 2.*") if tmptar is None: tmptar = "/%s/tmptar.tar" % (tempfile.mkdtemp()) cmd = ["singularity", "image.export", "-f", tmptar, image_path] self.run_command(cmd, sudo=False) return tmptar
def stopall(self, sudo=False, quiet=True): '''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 run_command, check_install check_install() cmd = self._init_command('instance.stop') cmd = cmd + ['--all'] 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 create(self, image_path, size=1024, sudo=False, singularity_options=None): """create will create a a new image Parameters ========== image_path: full path to image size: image sizein MiB, default is 1024MiB filesystem: supported file systems ext3/ext4 (ext[2/3]: default ext3 singularity_options: a list of options to provide to the singularity client """ from spython.utils import check_install check_install() cmd = self.init_command("image.create", singularity_options) cmd = cmd + ["--size", str(size), image_path] output = self.run_command(cmd, sudo=sudo) self.println(output) if not os.path.exists(image_path): bot.exit("Could not create image %s" % image_path) return image_path
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 run( self, image=None, args=None, app=None, sudo=False, writable=False, contain=False, bind=None, stream=False, nv=False, options=None, return_result=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. 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) """ from spython.utils import check_install check_install() cmd = self._init_command("run") # 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 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 _check_install(self): """ensure that singularity is installed, and exit if not. """ if check_install() is not True: bot.exit("Cannot find Singularity! Is it installed?")
def _check_install(self): '''ensure that singularity is installed, and exit if not. ''' if check_install() is not True: bot.error("Cannot find Singularity! Is it installed?") sys.exit(1)
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 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): '''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 options: for all other options, specify them in this list. 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') # If no extra options if not options: options = [] 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)://', 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] if not stream: self._run_command(cmd, sudo=sudo, 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) if os.path.exists(image): return image