diff --git a/src/client.py b/src/client.py index cb5fbd131..3faca0e9b 100755 --- a/src/client.py +++ b/src/client.py @@ -21,7 +21,7 @@ # # -# Copyright 2024 OmniOS Community Edition (OmniOSce) Association. +# Copyright 2025 OmniOS Community Edition (OmniOSce) Association. # Copyright 2024 Oxide Computer Company # Copyright (c) 2007, 2024, Oracle and/or its affiliates. # @@ -41,8 +41,8 @@ # Environment variables # # PKG_IMAGE - root path of target image -# PKG_IMAGE_TYPE [entire, partial, user] - type of image -# XXX or is this in the Image configuration? +# PKG_SUCCESS_ON_NOP - when an operation completes with nothing to do, exit with +# the success code (0) instead of the NOP one (4). try: import pkg.site_paths @@ -200,6 +200,8 @@ tmpdirs = [] tmpfiles = [] +EXIT_NOP_VAL = EXIT_OK if os.environ.get("PKG_SUCCESS_ON_NOP") else EXIT_NOP + @atexit.register def cleanup(): @@ -620,7 +622,8 @@ def print_cmds(cmd_list, cmd_dic): --help or -? Environment: - PKG_IMAGE""" + PKG_IMAGE + PKG_SUCCESS_ON_NOP""" ) ) else: @@ -898,6 +901,8 @@ def gen(meta=False): if errors: _generate_error_messages(out_json["status"], errors) + if out_json["status"] == EXIT_NOP: + return EXIT_NOP_VAL return out_json["status"] @@ -2499,7 +2504,7 @@ def __api_op( if _op == PKG_OP_FIX and _noexecute and _quiet_plan: return _verify_exit_code(_api_inst) if _api_inst.planned_nothingtodo(): - return EXIT_NOP + return EXIT_NOP_VAL if _noexecute or _stage == API_STAGE_PLAN: return EXIT_OK else: @@ -2818,6 +2823,8 @@ def __handle_client_json_api_output(out_json, op, api_inst): display_repo_failures(out_json["data"]["repo_status"]) __display_plan_messages(api_inst, frozenset([OP_STAGE_PREP, OP_STAGE_EXEC])) + if out_json["status"] == EXIT_NOP: + return EXIT_NOP_VAL return out_json["status"] @@ -3525,7 +3532,7 @@ def autoremove( if not pargs: msg(_("No removable packages for this image.")) - return EXIT_NOP + return EXIT_NOP_VAL out_json = client_api._uninstall( PKG_OP_UNINSTALL, @@ -3602,6 +3609,8 @@ def verify( # Since the verify output has been handled by display_plan_cb, only # status code needs to be returned. + if out_json["status"] == EXIT_NOP: + return EXIT_NOP_VAL return out_json["status"] @@ -3714,6 +3723,8 @@ def fix( if "errors" in out_json: _generate_error_messages(out_json["status"], out_json["errors"], cmd=op) + if out_json["status"] == EXIT_NOP: + return EXIT_NOP_VAL return out_json["status"] @@ -3937,7 +3948,7 @@ def set_mediator( return EXIT_OOPS else: msg(_("No changes required.")) - return EXIT_NOP + return EXIT_NOP_VAL if api_inst.get_dehydrated_publishers(): msg( @@ -4038,7 +4049,7 @@ def unset_mediator( return EXIT_OOPS else: msg(_("No changes required.")) - return EXIT_NOP + return EXIT_NOP_VAL if not quiet: __display_plan(api_inst, verbose, noexecute) @@ -4172,7 +4183,7 @@ def unfreeze(api_inst, args): try: pkgs = api_inst.freeze_pkgs(pargs, unfreeze=True, dry_run=dry_run) if not pkgs: - return EXIT_NOP + return EXIT_NOP_VAL for s in pkgs: logger.info(_("{0} was unfrozen.").format(s)) return EXIT_OK @@ -5594,6 +5605,8 @@ def publisher_set( add_info={"repo_uri": repo_uri}, ) + if out_json["status"] == EXIT_NOP: + return EXIT_NOP_VAL return out_json["status"] @@ -5608,6 +5621,8 @@ def publisher_unset(api_inst, pargs): out_json["status"], out_json["errors"], cmd="unset-publisher" ) + if out_json["status"] == EXIT_NOP: + return EXIT_NOP_VAL return out_json["status"] @@ -7202,7 +7217,7 @@ def update_format(api_inst, pargs): return EXIT_OK logger.info(_("Image format already current.")) - return EXIT_NOP + return EXIT_NOP_VAL def print_version(pargs): diff --git a/src/man/pkg.1 b/src/man/pkg.1 index 68135861b..ed1667fb6 100644 --- a/src/man/pkg.1 +++ b/src/man/pkg.1 @@ -1,7 +1,7 @@ .\" Copyright (c) 2007, 2016, Oracle and/or its affiliates. All rights reserved. -.\" Copyright 2023 OmniOS Community Edition (OmniOSce) Association. .\" Copyright 2024 Oxide Computer Company -.Dd December 10, 2024 +.\" Copyright 2025 OmniOS Community Edition (OmniOSce) Association. +.Dd June 25, 2025 .Dt PKG 1 .Os .Sh NAME @@ -4331,6 +4331,12 @@ before a connection is aborted. A value of 0 means do not abort the operation. .Pp Default value: 5 +.It Sy PKG_CLIENT_MAX_TIMEOUT +Maximum number of transport attempts per host before the client aborts the +operation. +A value of 0 means do not abort the operation. +.Pp +Default value: 4 .It Sy PKG_CONCURRENCY The number of child images to update in parallel. Ignored if the @@ -4353,12 +4359,16 @@ If is 0 or a negative number, all child images are updated in parallel. .Pp Default value: 1 -.It Sy PKG_CLIENT_MAX_TIMEOUT -Maximum number of transport attempts per host before the client aborts the -operation. -A value of 0 means do not abort the operation. +.It Sy PKG_SUCCESS_ON_NOP +When set to a non-zero value, cause +.Nm +operations that result in there being no changes to apply to exit +successfully, with exit status 0 +.Pq Command succeeded , +rather than with exit status 4 +.Pq \&No changes were made - nothing to do . .Pp -Default value: 4 +Default value: 0 .It Sy http_proxy , Sy https_proxy HTTP or HTTPS proxy server. .El diff --git a/src/tests/pkg5testenv.py b/src/tests/pkg5testenv.py index 733f31a02..b9ac2e79d 100644 --- a/src/tests/pkg5testenv.py +++ b/src/tests/pkg5testenv.py @@ -129,6 +129,11 @@ def setup_environment(path_to_proto, debug=False, system_test=False): if k.startswith("PKG_") or k.lower().endswith("_proxy"): del os.environ[k] + # This environment variable changes the exit status of operations that + # result in no changes being required. Unset it so tests get the expected + # behaviour. + os.environ.pop("PKG_SUCCESS_ON_NOP", None) + # # Tell package manager where its application data files live. #