From 3d5794b3bcdb92c82a48c745e1710f7ed599ae59 Mon Sep 17 00:00:00 2001 From: vsoch Date: Sun, 27 Mar 2022 12:41:52 -0600 Subject: [PATCH 1/5] removing checks for versions across all files and 2.x image commands import, export the reason being so we can easily support apptainer and singularityce installs, which both provide an executable singularity but with different versions! Also, people should not be using the 2.x command groups at this point because there are too many security issues. We can support this in some small way by not making it easy/possible to do here, unless they want to install an older version. This will fix #190 Signed-off-by: vsoch --- CHANGELOG.md | 1 + spython/{image/__init__.py => image.py} | 2 +- spython/image/cmd/__init__.py | 46 ----------- spython/image/cmd/create.py | 35 -------- spython/image/cmd/export.py | 34 -------- spython/image/cmd/importcmd.py | 25 ------ spython/image/cmd/utils.py | 31 ------- spython/instance/cmd/start.py | 7 +- spython/instance/cmd/stop.py | 9 +- spython/main/__init__.py | 23 ++---- spython/main/build.py | 3 - spython/main/export.py | 105 ++++-------------------- spython/main/inspect.py | 8 +- spython/main/instances.py | 4 - spython/main/pull.py | 10 +-- spython/utils/fileio.py | 9 +- spython/version.py | 2 +- 17 files changed, 43 insertions(+), 311 deletions(-) rename spython/{image/__init__.py => image.py} (98%) delete mode 100644 spython/image/cmd/__init__.py delete mode 100644 spython/image/cmd/create.py delete mode 100644 spython/image/cmd/export.py delete mode 100644 spython/image/cmd/importcmd.py delete mode 100644 spython/image/cmd/utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b0fc333..95c5a3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The client here will eventually be released as "spython" (and eventually to singularity on pypi), and the versions here will coincide with these releases. ## [master](https://github.com/singularityhub/singularity-cli/tree/master) + - version checks removed to support Singularity 3.x and above (0.2.0) - adding support for SIF (oras pull) (0.1.18) - updating CI/tests and fixing deprecations (warnings) (0.1.17) - fixing bug with defining comments earlier (0.1.16) diff --git a/spython/image/__init__.py b/spython/image.py similarity index 98% rename from spython/image/__init__.py rename to spython/image.py index 36cc4ed..8538cef 100644 --- a/spython/image/__init__.py +++ b/spython/image.py @@ -10,7 +10,7 @@ from spython.utils import split_uri -class ImageBase(object): +class ImageBase: def __str__(self): protocol = getattr(self, "protocol", None) if protocol: diff --git a/spython/image/cmd/__init__.py b/spython/image/cmd/__init__.py deleted file mode 100644 index dbdadf4..0000000 --- a/spython/image/cmd/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 2017-2022 Vanessa Sochat. - -# This Source Code Form is subject to the terms of the -# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed -# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -def generate_image_commands(): - """The Image client holds the Singularity image command group, mainly - deprecated commands (image.import) and additional command helpers - that are commonly use but not provided by Singularity - - The levels of verbosity (debug and quiet) are passed from the main - client via the environment variable MESSAGELEVEL. - - These commands are added to Client.image under main/__init__.py to - expose subcommands: - - Client.image.export - Client.image.imprt - Client.image.decompress - Client.image.create - - """ - - class ImageClient(object): - group = "image" - - from spython.main.base.logger import println - from spython.main.base.command import init_command, run_command - from .utils import compress, decompress - from .create import create - from .importcmd import importcmd - from .export import export - - ImageClient.create = create - ImageClient.imprt = importcmd - ImageClient.export = export - ImageClient.decompress = decompress - ImageClient.compress = compress - ImageClient.println = println - ImageClient.init_command = init_command - ImageClient.run_command = run_command - - cli = ImageClient() - return cli diff --git a/spython/image/cmd/create.py b/spython/image/cmd/create.py deleted file mode 100644 index 4be6831..0000000 --- a/spython/image/cmd/create.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (C) 2017-2022 Vanessa Sochat. - -# This Source Code Form is subject to the terms of the -# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed -# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -import os -from spython.logger import bot - - -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 diff --git a/spython/image/cmd/export.py b/spython/image/cmd/export.py deleted file mode 100644 index 51342b4..0000000 --- a/spython/image/cmd/export.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) 2017-2022 Vanessa Sochat. - -# This Source Code Form is subject to the terms of the -# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed -# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -from spython.logger import bot -import tempfile - - -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 diff --git a/spython/image/cmd/importcmd.py b/spython/image/cmd/importcmd.py deleted file mode 100644 index 88402b7..0000000 --- a/spython/image/cmd/importcmd.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (C) 2017-2022 Vanessa Sochat. - -# This Source Code Form is subject to the terms of the -# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed -# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -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 diff --git a/spython/image/cmd/utils.py b/spython/image/cmd/utils.py deleted file mode 100644 index 08f0ae8..0000000 --- a/spython/image/cmd/utils.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (C) 2017-2022 Vanessa Sochat. - -# This Source Code Form is subject to the terms of the -# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed -# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -import os -from spython.logger import bot - - -def compress(self, image_path): - """compress will (properly) compress an image""" - if os.path.exists(image_path): - compressed_image = "%s.gz" % image_path - os.system("gzip -c -6 %s > %s" % (image_path, compressed_image)) - return compressed_image - - bot.exit("Cannot find image %s" % image_path) - - -def decompress(self, image_path, quiet=True): - """decompress will (properly) decompress an image""" - - if not os.path.exists(image_path): - bot.exit("Cannot find image %s" % image_path) - - extracted_file = image_path.replace(".gz", "") - cmd = ["gzip", "-d", "-f", image_path] - self.run_command(cmd, quiet=quiet) # exits if return code != 0 - return extracted_file diff --git a/spython/instance/cmd/start.py b/spython/instance/cmd/start.py index c68e899..485061e 100644 --- a/spython/instance/cmd/start.py +++ b/spython/instance/cmd/start.py @@ -56,12 +56,7 @@ def start( image = self._image - # Derive subgroup command based on singularity version - subgroup = "instance.start" - if "version 3" in self.version(): - subgroup = ["instance", "start"] - - cmd = self._init_command(subgroup, singularity_options) + cmd = self._init_command(["instance", "start"], singularity_options) # Set options and args args = args or self.args diff --git a/spython/instance/cmd/stop.py b/spython/instance/cmd/stop.py index 2e5aff5..d4bde8f 100644 --- a/spython/instance/cmd/stop.py +++ b/spython/instance/cmd/stop.py @@ -36,12 +36,9 @@ def stop( check_install() - subgroup = "instance.stop" - - if "version 3" in self.version(): - subgroup = ["instance", "stop"] - if timeout: - subgroup += ["-t", str(timeout)] + subgroup = ["instance", "stop"] + if timeout: + subgroup += ["-t", str(timeout)] cmd = self._init_command(subgroup, singularity_options) diff --git a/spython/main/__init__.py b/spython/main/__init__.py index 107d7a6..37a2c74 100644 --- a/spython/main/__init__.py +++ b/spython/main/__init__.py @@ -16,7 +16,6 @@ def get_client(quiet=False, debug=False): debug: turn on debugging mode """ - from spython.utils import get_singularity_version from .base import Client as client client.quiet = quiet @@ -46,11 +45,6 @@ def get_client(quiet=False, debug=False): client.shell = shell client.pull = pull - # Command Groups, Images - from spython.image.cmd import generate_image_commands # deprecated - - client.image = generate_image_commands() - # Commands Groups, Instances from spython.instance.cmd import ( generate_instance_commands, @@ -61,15 +55,14 @@ def get_client(quiet=False, debug=False): client.instance.version = client.version # Commands Groups, OCI (Singularity version 3 and up) - if "version 3" in get_singularity_version(): - from spython.oci.cmd import generate_oci_commands - - client.oci = generate_oci_commands()() # first () runs function, second - # initializes OciImage class - client.oci.debug = client.debug - client.oci.quiet = client.quiet - client.oci.OciImage.quiet = client.quiet - client.oci.OciImage.debug = client.debug + from spython.oci.cmd import generate_oci_commands + + client.oci = generate_oci_commands()() # first () runs function, second + # initializes OciImage class + client.oci.debug = client.debug + client.oci.quiet = client.quiet + client.oci.OciImage.quiet = client.quiet + client.oci.OciImage.debug = client.debug # Initialize cli = client() diff --git a/spython/main/build.py b/spython/main/build.py index 3cbbe2d..76b8488 100644 --- a/spython/main/build.py +++ b/spython/main/build.py @@ -67,9 +67,6 @@ def build( # 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") diff --git a/spython/main/export.py b/spython/main/export.py index d3a22f4..1cc5027 100644 --- a/spython/main/export.py +++ b/spython/main/export.py @@ -6,8 +6,6 @@ from spython.logger import bot -import tempfile -import shutil import os @@ -21,7 +19,7 @@ def export( singularity_options=None, ): - """export will export an image, sudo must be used. If we have Singularity + """export will export an image, sudo must be used. Since we have Singularity versions after 3, export is replaced with building into a sandbox. Parameters @@ -36,87 +34,20 @@ def export( 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 _export( - self, - image_path, - pipe=False, - output_file=None, - command=None, - singularity_options=None, -): - """the older deprecated function, running export for previous - versions of Singularity that support it - - USAGE: singularity [...] export [export options...] - Export will dump a tar stream of the container image contents to standard - out (stdout). - note: This command must be executed as root. - EXPORT OPTIONS: - -f/--file Output to a file instead of a pipe - --command Replace the tar command (DEFAULT: 'tar cf - .') - EXAMPLES: - $ sudo singularity export /tmp/Debian.img > /tmp/Debian.tar - $ sudo singularity export /tmp/Debian.img | gzip -9 > /tmp/Debian.tar.gz - $ sudo singularity export -f Debian.tar /tmp/Debian.img - - """ - sudo = True - cmd = self._init_command("export", singularity_options) - - # If the user has specified export to pipe, we don't need a file - if pipe: - cmd.append(image_path) - - else: - _, tmptar = tempfile.mkstemp(suffix=".tar") - os.remove(tmptar) - cmd = cmd + ["-f", tmptar, image_path] - self._run_command(cmd, sudo=sudo) - - # Was there an error? - if not os.path.exists(tmptar): - print("Error generating image tar") - return None - - # if user has specified output file, move it there, return path - if output_file is not None: - shutil.copyfile(tmptar, output_file) - return output_file - else: - return tmptar - - # Otherwise, return output of pipe - return self._run_command(cmd, sudo=sudo) + # 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, + ) diff --git a/spython/main/inspect.py b/spython/main/inspect.py index 9fc8343..079131d 100644 --- a/spython/main/inspect.py +++ b/spython/main/inspect.py @@ -38,13 +38,7 @@ def 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"] - + options = ["e", "d", "l", "r", "H", "t"] for x in options: cmd.append("-%s" % x) diff --git a/spython/main/instances.py b/spython/main/instances.py index 202f2ad..4fe0efe 100644 --- a/spython/main/instances.py +++ b/spython/main/instances.py @@ -45,10 +45,6 @@ def list_instances( 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 diff --git a/spython/main/pull.py b/spython/main/pull.py index d4068af..38baa8c 100644 --- a/spython/main/pull.py +++ b/spython/main/pull.py @@ -16,7 +16,7 @@ def pull( image=None, name=None, pull_folder="", - ext=None, + ext="sif", force=False, capture=False, stream=False, @@ -48,9 +48,6 @@ def 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() @@ -72,9 +69,8 @@ def pull( # 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 + name = final_image + pull_folder = None # Don't use pull_folder else: final_image = name diff --git a/spython/utils/fileio.py b/spython/utils/fileio.py index cbd27b1..1f6ebf1 100644 --- a/spython/utils/fileio.py +++ b/spython/utils/fileio.py @@ -49,7 +49,8 @@ def write_file(filename, content, mode="w"): def write_json(json_obj, filename, mode="w", print_pretty=True): - """write_json will (optionally,pretty print) a json object to file + """ + write_json will (optionally,pretty print) a json object to file :param json_obj: the dict to print to json :param filename: the output file to write to :param pretty_print: if True, will use nicer formatting @@ -63,7 +64,8 @@ def write_json(json_obj, filename, mode="w", print_pretty=True): def read_file(filename, mode="r", readlines=True): - """write_file will open a file, "filename" and write content, "content" + """ + write_file will open a file, "filename" and write content, "content" and properly close the file """ with open(filename, mode) as filey: @@ -75,7 +77,8 @@ def read_file(filename, mode="r", readlines=True): def read_json(filename, mode="r"): - """read_json reads in a json file and returns + """ + read_json reads in a json file and returns the data structure as dict. """ with open(filename, mode) as filey: diff --git a/spython/version.py b/spython/version.py index b596615..02cba16 100644 --- a/spython/version.py +++ b/spython/version.py @@ -5,7 +5,7 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -__version__ = "0.1.18" +__version__ = "0.2.0" AUTHOR = "Vanessa Sochat" AUTHOR_EMAIL = "vsoch@users.noreply.github.com" NAME = "spython" From 8cfc90d8926718a19a7c8543191ed36e4f809c04 Mon Sep 17 00:00:00 2001 From: vsoch Date: Sun, 27 Mar 2022 12:44:39 -0600 Subject: [PATCH 2/5] remove extraneous object Signed-off-by: vsoch --- spython/README.md | 2 +- spython/logger/progress.py | 2 +- spython/main/parse/parsers/base.py | 2 +- spython/main/parse/recipe.py | 5 +++-- spython/main/parse/writers/base.py | 2 +- spython/tests/test_instances.py | 2 +- spython/utils/misc.py | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/spython/README.md b/spython/README.md index 03da753..0752a8b 100644 --- a/spython/README.md +++ b/spython/README.md @@ -4,7 +4,7 @@ This will briefly outline the content of the folders here. ## Main Functions - [main](main) holds the primary client functions to interact with Singularity (e.g., exec, run, pull), and the subfolders within represent command groups (e.g., instance, image) along with the base of the client ([base](main/base)). This folder is where **commands** go! - - [image](image) is a class that represents and holds an image object, in the case that the user wants to initialize a client with an image. This holds the ImageClass, which is only needed to instantiate an image. + - [image](image.py) is a class that represents and holds an image object, in the case that the user wants to initialize a client with an image. This holds the ImageClass, which is only needed to instantiate an image. - [instance](instance) is a class that represents and holds an instance object. The user can instantiate the class, and then run it's sub functions to interact with it. The higher level "list" command is provided on the level of the client. - [cli](cli): is the actual entry point that connects the user to the python API client from the command line. The user inputs are parsed, and then passed into functions from main. diff --git a/spython/logger/progress.py b/spython/logger/progress.py index d2f6a2e..1d8bbe0 100644 --- a/spython/logger/progress.py +++ b/spython/logger/progress.py @@ -25,7 +25,7 @@ ETA_SMA_WINDOW = 9 -class ProgressBar(object): +class ProgressBar: def __enter__(self): return self diff --git a/spython/main/parse/parsers/base.py b/spython/main/parse/parsers/base.py index 89e73ca..bc37b64 100644 --- a/spython/main/parse/parsers/base.py +++ b/spython/main/parse/parsers/base.py @@ -14,7 +14,7 @@ from ..recipe import Recipe -class ParserBase(object): +class ParserBase: """a parser Base is intended to provide helper functions for a parser, namely to read lines in files, and otherwise interact with outputs. Input should be some recipe (text file to describe a container build) diff --git a/spython/main/parse/recipe.py b/spython/main/parse/recipe.py index 8c06418..27b48cc 100644 --- a/spython/main/parse/recipe.py +++ b/spython/main/parse/recipe.py @@ -5,8 +5,9 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -class Recipe(object): - """a recipe includes an environment, labels, runscript or command, +class Recipe: + """ + a recipe includes an environment, labels, runscript or command, and install sequence. This object is interacted with by a Parser (intended to popualte the recipe with content) and a Writer (intended to write a recipe to file). The parsers and writers are located in diff --git a/spython/main/parse/writers/base.py b/spython/main/parse/writers/base.py index dbd8def..342f426 100644 --- a/spython/main/parse/writers/base.py +++ b/spython/main/parse/writers/base.py @@ -11,7 +11,7 @@ from spython.utils import write_file -class WriterBase(object): +class WriterBase: def __init__(self, recipe=None): """a writer base will take a recipe object (parser.base.Recipe) and provide helpers for writing to file. diff --git a/spython/tests/test_instances.py b/spython/tests/test_instances.py index 7496183..ebd8aaa 100644 --- a/spython/tests/test_instances.py +++ b/spython/tests/test_instances.py @@ -35,7 +35,7 @@ def test_has_no_instances(): assert instances == [] -class TestInstanceFuncs(object): +class TestInstanceFuncs: @pytest.fixture(autouse=True) def test_instance_cmds(self, docker_container): image = docker_container[1] diff --git a/spython/utils/misc.py b/spython/utils/misc.py index 712c13d..cef001e 100644 --- a/spython/utils/misc.py +++ b/spython/utils/misc.py @@ -20,7 +20,7 @@ def setEnvVar(name, value): os.environ[name] = value -class ScopedEnvVar(object): +class ScopedEnvVar: """Temporarly change an environment variable Usage: From 34f609d8e2b4b0eccd01b49d3d26394a16aa8cb5 Mon Sep 17 00:00:00 2001 From: vsoch Date: Sun, 27 Mar 2022 12:46:20 -0600 Subject: [PATCH 3/5] wrong filename Signed-off-by: vsoch --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 35fee76..d0b8a91 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,7 +27,7 @@ jobs: run: | export PATH="/usr/share/miniconda/bin:$PATH" source activate black - pyflakes spython/oci spython/image spython/instance spython/main + pyflakes spython/oci spython/image.py spython/instance spython/main pytest: runs-on: ubuntu-latest From 8c776b3dc5e2620783f081527de7b7bcbea054bb Mon Sep 17 00:00:00 2001 From: vsoch Date: Sun, 27 Mar 2022 12:49:55 -0600 Subject: [PATCH 4/5] export command removed for 2.x Signed-off-by: vsoch --- spython/main/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spython/main/__init__.py b/spython/main/__init__.py index 37a2c74..c85830f 100644 --- a/spython/main/__init__.py +++ b/spython/main/__init__.py @@ -30,14 +30,13 @@ def get_client(quiet=False, debug=False): from .instances import list_instances, stopall # global instance commands from .run import run from .pull import pull - from .export import export, _export + from .export import export # Actions client.apps = apps client.build = build client.execute = execute client.export = export - client._export = _export client.help = helpcmd client.inspect = inspect client.instances = list_instances From f0fc58992448116ef25748c749a1a8b674354680 Mon Sep 17 00:00:00 2001 From: vsoch Date: Sun, 27 Mar 2022 12:55:35 -0600 Subject: [PATCH 5/5] reference to non-existent image command! Signed-off-by: vsoch --- spython/main/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spython/main/__init__.py b/spython/main/__init__.py index c85830f..4fdeccc 100644 --- a/spython/main/__init__.py +++ b/spython/main/__init__.py @@ -67,10 +67,9 @@ def get_client(quiet=False, debug=False): cli = client() # Pass on verbosity - for subclient in [cli.image, cli.instance]: - subclient.debug = cli.debug - subclient.quiet = cli.quiet - subclient.version = cli.version + cli.instance.debug = cli.debug + cli.instance.quiet = cli.quiet + cli.instance.version = cli.version return cli