From 09f074ba0a36ba61e3a0bed51aa6cfd59d6f502b Mon Sep 17 00:00:00 2001 From: Mike Ross Date: Fri, 26 Jan 2018 00:59:41 +0000 Subject: [PATCH] Releasing version 2.4.15 --- CHANGELOG.rst | 8 + requirements.txt | 3 +- setup.py | 2 +- src/oci_cli/bin/OciTabExpansion.ps1 | 17 +- src/oci_cli/cli_constants.py | 17 + src/oci_cli/cli_root.py | 52 +- src/oci_cli/cli_setup.py | 4 +- src/oci_cli/cli_util.py | 58 +- src/oci_cli/generated/identity_cli.py | 363 +++++-- src/oci_cli/generated/loadbalancer_cli.py | 40 +- src/oci_cli/generated/objectstorage_cli.py | 12 +- src/oci_cli/identity_cli_extended.py | 1 + src/oci_cli/lb_cli_extended.py | 16 +- .../get_object_tasks.py | 12 +- src/oci_cli/objectstorage_cli_extended.py | 4 +- src/oci_cli/retry_utils.py | 22 +- src/oci_cli/version.py | 2 +- tests/conftest.py | 267 ++--- tests/output/inline_help_dump.txt | 815 ++++++++++----- tests/test_audit.py | 4 + tests/test_blockstorage.py | 2 + tests/test_canned_queries_in_cli_rc_file.py | 94 +- tests/test_cli_setup.py | 6 +- tests/test_compute.py | 6 +- tests/test_config_container.py | 67 ++ .../test_default_files_command_invocation.py | 95 +- tests/test_identity.py | 67 +- .../test_json_skeleton_command_invocation.py | 305 +++--- tests/test_launch_instance_options.py | 2 + tests/test_list_filter.py | 29 +- tests/test_load_balancer.py | 982 +++++++++++------- tests/test_load_balancer_waiters.py | 206 ---- tests/test_root_options.py | 34 +- tests/test_secondary_private_ip.py | 25 +- tests/test_tag_management.py | 66 +- tests/test_tagging.py | 465 +++++---- tests/test_utils.py | 48 +- tests/test_virtualnetwork.py | 38 +- tests/util.py | 27 +- 39 files changed, 2582 insertions(+), 1701 deletions(-) create mode 100644 src/oci_cli/cli_constants.py create mode 100644 tests/test_config_container.py delete mode 100644 tests/test_load_balancer_waiters.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f68d2bf80..cfdadfb10 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,14 @@ All notable changes to this project will be documented in this file. The format is based on `Keep a Changelog `__. +2.4.15 - 2018-01-25 +--------------------- +Added +~~~~~~~~~~ +* Support for using the ``ObjectReadWithoutList`` public access type when creating and updating buckets +* Support for managing dynamic groups (oci iam dynamic-group) +* Support for instance principal auth (using --auth instance_principal option) + 2.4.14 - 2018-01-11 -------------------- Added diff --git a/requirements.txt b/requirements.txt index 182e93f96..d751bd744 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ Jinja2==2.9.6 jmespath==0.9.3 ndg-httpsclient==0.4.2 mock==2.0.0 -oci==1.3.12 +oci==1.3.13 packaging==16.8 pluggy==0.4.0 py==1.4.33 @@ -30,4 +30,5 @@ sphinx==1.6.4 sphinx-rtd-theme==0.2.5b1 terminaltables==3.1.0 tox==2.9.1 +vcrpy==1.11.1 virtualenv==15.1.0 diff --git a/setup.py b/setup.py index 892a79e84..a631542d8 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def open_relative(*path): requires = [ - 'oci==1.3.12', + 'oci==1.3.13', 'arrow==0.10.0', 'certifi', 'click==6.7', diff --git a/src/oci_cli/bin/OciTabExpansion.ps1 b/src/oci_cli/bin/OciTabExpansion.ps1 index 65058321c..f77f7aa07 100644 --- a/src/oci_cli/bin/OciTabExpansion.ps1 +++ b/src/oci_cli/bin/OciTabExpansion.ps1 @@ -36,10 +36,11 @@ $ociSubcommands = @{ 'db system' = 'get launch list patch terminate update' 'db system-shape' = 'list' 'db version' = 'list' - 'iam' = 'availability-domain compartment customer-secret-key group policy region region-subscription tag tag-namespace user' + 'iam' = 'availability-domain compartment customer-secret-key dynamic-group group policy region region-subscription tag tag-namespace user' 'iam availability-domain' = 'list' 'iam compartment' = 'create get list update' 'iam customer-secret-key' = 'create delete list update' + 'iam dynamic-group' = 'create delete get list update' 'iam group' = 'add-user create delete get list list-users remove-user update' 'iam policy' = 'create delete get list update' 'iam region' = 'list' @@ -195,6 +196,11 @@ $ociCommandsToLongParams = @{ 'iam customer-secret-key delete' = 'customer-secret-key-id force from-json help if-match user-id' 'iam customer-secret-key list' = 'from-json help user-id' 'iam customer-secret-key update' = 'customer-secret-key-id display-name from-json help if-match user-id' + 'iam dynamic-group create' = 'compartment-id description from-json help matching-rule max-wait-seconds name wait-for-state wait-interval-seconds' + 'iam dynamic-group delete' = 'dynamic-group-id force from-json help if-match max-wait-seconds wait-for-state wait-interval-seconds' + 'iam dynamic-group get' = 'dynamic-group-id from-json help' + 'iam dynamic-group list' = 'all compartment-id from-json help limit page page-size' + 'iam dynamic-group update' = 'description dynamic-group-id from-json help if-match matching-rule max-wait-seconds wait-for-state wait-interval-seconds' 'iam group add-user' = 'from-json group-id help user-id' 'iam group create' = 'compartment-id defined-tags description freeform-tags from-json help max-wait-seconds name wait-for-state wait-interval-seconds' 'iam group delete' = 'force from-json group-id help if-match max-wait-seconds wait-for-state wait-interval-seconds' @@ -255,9 +261,9 @@ $ociCommandsToLongParams = @{ 'lb certificate list' = 'from-json help load-balancer-id' 'lb health-checker get' = 'backend-set-name from-json help load-balancer-id' 'lb health-checker update' = 'backend-set-name from-json help interval-in-millis load-balancer-id max-wait-seconds port protocol response-body-regex retries return-code timeout-in-millis url-path wait-for-state wait-interval-seconds' - 'lb listener create' = 'default-backend-set-name from-json help load-balancer-id max-wait-seconds name port protocol ssl-certificate-name ssl-verify-depth ssl-verify-peer-certificate wait-for-state wait-interval-seconds' + 'lb listener create' = 'connection-configuration-idle-timeout default-backend-set-name from-json help load-balancer-id max-wait-seconds name port protocol ssl-certificate-name ssl-verify-depth ssl-verify-peer-certificate wait-for-state wait-interval-seconds' 'lb listener delete' = 'force from-json help listener-name load-balancer-id max-wait-seconds wait-for-state wait-interval-seconds' - 'lb listener update' = 'default-backend-set-name force from-json help listener-name load-balancer-id max-wait-seconds port protocol ssl-certificate-name ssl-verify-depth ssl-verify-peer-certificate wait-for-state wait-interval-seconds' + 'lb listener update' = 'connection-configuration-idle-timeout default-backend-set-name force from-json help listener-name load-balancer-id max-wait-seconds port protocol ssl-certificate-name ssl-verify-depth ssl-verify-peer-certificate wait-for-state wait-interval-seconds' 'lb load-balancer create' = 'backend-sets certificates compartment-id display-name from-json help is-private listeners max-wait-seconds shape-name subnet-ids wait-for-state wait-interval-seconds' 'lb load-balancer delete' = 'force from-json help load-balancer-id max-wait-seconds wait-for-state wait-interval-seconds' 'lb load-balancer get' = 'from-json help load-balancer-id' @@ -476,6 +482,11 @@ $ociCommandsToShortParams = @{ 'iam customer-secret-key delete' = '? h' 'iam customer-secret-key list' = '? h' 'iam customer-secret-key update' = '? h' + 'iam dynamic-group create' = '? c h' + 'iam dynamic-group delete' = '? h' + 'iam dynamic-group get' = '? h' + 'iam dynamic-group list' = '? c h' + 'iam dynamic-group update' = '? h' 'iam group add-user' = '? h' 'iam group create' = '? c h' 'iam group delete' = '? h' diff --git a/src/oci_cli/cli_constants.py b/src/oci_cli/cli_constants.py new file mode 100644 index 000000000..4f950332e --- /dev/null +++ b/src/oci_cli/cli_constants.py @@ -0,0 +1,17 @@ +# coding: utf-8 +# Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved. + +CLI_RC_FALLBACK_LOCATION = '~/.oci/cli-defaults' +CLI_RC_DEFAULT_LOCATION = '~/.oci/oci_cli_rc' +CLI_RC_CANNED_QUERIES_SECTION_NAME = 'OCI_CLI_CANNED_QUERIES' +CLI_RC_COMMAND_ALIASES_SECTION_NAME = 'OCI_CLI_COMMAND_ALIASES' +CLI_RC_PARAM_ALIASES_SECTION_NAME = 'OCI_CLI_PARAM_ALIASES' +CLI_RC_GENERIC_SETTINGS_SECTION_NAME = 'OCI_CLI_SETTINGS' + +OCI_CLI_PROFILE_ENV_VAR = 'OCI_CLI_PROFILE' +CLI_RC_GENERIC_SETTINGS_DEFAULT_PROFILE_KEY = 'default_profile' +CLI_RC_GENERIC_SETTINGS_USE_CLICK_HELP = 'use_click_help' + +OCI_CLI_AUTH_ENV_VAR = 'OCI_CLI_AUTH' +OCI_CLI_AUTH_INSTANCE_PRINCIPAL = 'instance_principal' +OCI_CLI_AUTH_API_KEY = 'api_key' diff --git a/src/oci_cli/cli_root.py b/src/oci_cli/cli_root.py index b03df549f..a53c4a014 100644 --- a/src/oci_cli/cli_root.py +++ b/src/oci_cli/cli_root.py @@ -15,6 +15,8 @@ from . import help_text_producer from . import cli_util +from . import cli_constants + # Enable WARN logging to surface important warnings attached to loading # defaults, automatic coercion, or fallback values/endpoints that may impact # the user's security. @@ -31,21 +33,12 @@ BMCS_DEPRECATION_NOTICE = """WARNING: Invoking the CLI using 'bmcs' is deprecated and will be removed in future versions, starting in March 2018. To avoid interruption at that time, please move to invoking the CLI using 'oci' instead.""" -CLI_RC_FALLBACK_LOCATION = '~/.oci/cli-defaults' -CLI_RC_DEFAULT_LOCATION = '~/.oci/oci_cli_rc' -CLI_RC_CANNED_QUERIES_SECTION_NAME = 'OCI_CLI_CANNED_QUERIES' -CLI_RC_COMMAND_ALIASES_SECTION_NAME = 'OCI_CLI_COMMAND_ALIASES' -CLI_RC_PARAM_ALIASES_SECTION_NAME = 'OCI_CLI_PARAM_ALIASES' -CLI_RC_GENERIC_SETTINGS_SECTION_NAME = 'OCI_CLI_SETTINGS' - -OCI_CLI_PROFILE_ENV_VAR = 'OCI_CLI_PROFILE' -CLI_RC_GENERIC_SETTINGS_DEFAULT_PROFILE_KEY = 'default_profile' -CLI_RC_GENERIC_SETTINGS_USE_CLICK_HELP = 'use_click_help' +OCI_CLI_AUTH_CHOICES = [cli_constants.OCI_CLI_AUTH_API_KEY, cli_constants.OCI_CLI_AUTH_INSTANCE_PRINCIPAL] def eager_load_cli_rc_file(ctx, param, value): - expanded_rc_default_location = os.path.expandvars(os.path.expanduser(CLI_RC_DEFAULT_LOCATION)) - expanded_rc_fallback_location = os.path.expandvars(os.path.expanduser(CLI_RC_FALLBACK_LOCATION)) + expanded_rc_default_location = os.path.expandvars(os.path.expanduser(cli_constants.CLI_RC_DEFAULT_LOCATION)) + expanded_rc_fallback_location = os.path.expandvars(os.path.expanduser(cli_constants.CLI_RC_FALLBACK_LOCATION)) file_location = os.path.expandvars(os.path.expanduser(value)) ctx.obj = { @@ -92,7 +85,7 @@ def populate_aliases_canned_queries_and_settings(ctx, parser_without_defaults): def populate_settings(ctx, parser_without_defaults): - raw_settings = get_section_without_defaults(parser_without_defaults, CLI_RC_GENERIC_SETTINGS_SECTION_NAME) + raw_settings = get_section_without_defaults(parser_without_defaults, cli_constants.CLI_RC_GENERIC_SETTINGS_SECTION_NAME) settings = {} if raw_settings: @@ -103,7 +96,7 @@ def populate_settings(ctx, parser_without_defaults): def populate_command_aliases(ctx, parser_without_defaults): - raw_command_aliases = get_section_without_defaults(parser_without_defaults, CLI_RC_COMMAND_ALIASES_SECTION_NAME) + raw_command_aliases = get_section_without_defaults(parser_without_defaults, cli_constants.CLI_RC_COMMAND_ALIASES_SECTION_NAME) # Global aliases, e.g. a "ls=list" mapping would mean someone could do "compute image ls" or "os bucket ls" or "network subnet ls". These aliases # must be a single word only @@ -130,7 +123,7 @@ def populate_command_aliases(ctx, parser_without_defaults): def populate_parameter_aliases(ctx, parser_without_defaults): - raw_parameter_aliases = get_section_without_defaults(parser_without_defaults, CLI_RC_PARAM_ALIASES_SECTION_NAME) + raw_parameter_aliases = get_section_without_defaults(parser_without_defaults, cli_constants.CLI_RC_PARAM_ALIASES_SECTION_NAME) canonical_param_to_alias = {} @@ -169,7 +162,7 @@ def populate_parameter_aliases(ctx, parser_without_defaults): def populate_canned_queries(ctx, parser_without_defaults): - raw_canned_queries = get_section_without_defaults(parser_without_defaults, CLI_RC_CANNED_QUERIES_SECTION_NAME) + raw_canned_queries = get_section_without_defaults(parser_without_defaults, cli_constants.CLI_RC_CANNED_QUERIES_SECTION_NAME) if raw_canned_queries: ctx.obj['canned_queries'] = dict(raw_canned_queries) @@ -201,7 +194,7 @@ def get_section_without_defaults(parser_without_defaults, section_name): default=Sentinel(DEFAULT_PROFILE), show_default=False, help='The profile in the config file to load. This profile will also be used to locate any default parameter values which have been specified in the OCI CLI-specific configuration file. [default: DEFAULT]') @click.option('--cli-rc-file', '--defaults-file', - default=CLI_RC_DEFAULT_LOCATION, show_default=True, + default=cli_constants.CLI_RC_DEFAULT_LOCATION, show_default=True, is_eager=True, callback=eager_load_cli_rc_file, help='The path to the OCI CLI-specific configuration file, containing parameter default values and other configuration information such as command aliases and predefined queries. The --defaults-file option is deprecated and you should use the --cli-rc-file option instead.') @click.option('--opc-request-id', '--opc-client-request-id', '--request-id', 'request_id', @@ -215,6 +208,7 @@ def get_section_without_defaults(parser_without_defaults, section_name): Queries can be entered directly on the command line or referenced from the [OCI_CLI_COMMAND_ALIASES] section of your configuration file by using the syntax query://, for example query://get_id_and_name """) @click.option('--raw-output', is_flag=True, help='If the output of a given query is a single string value, this will return the string without surrounding quotes') +@click.option('--auth', type=click.Choice(choices=OCI_CLI_AUTH_CHOICES), help='The type of auth to use for the API request. By default the API key in your config file will be used. This value can also be provided in the {auth_env_var} environment variable.'.format(auth_env_var=cli_constants.OCI_CLI_AUTH_ENV_VAR)) @click.option('--generate-full-command-json-input', is_flag=True, is_eager=True, help="""Prints out a JSON document which represents all possible options that can be provided to this command. This JSON document can be saved to a file, modified with the appropriate option values, and then passed back via the --from-json option. This provides an alternative to typing options out on the command line.""") @@ -224,7 +218,7 @@ def get_section_without_defaults(parser_without_defaults, section_name): @click.option('-d', '--debug', is_flag=True, help='Show additional debug information.') @click.option('-?', '-h', '--help', is_flag=True, help='Show this message and exit.') @click.pass_context -def cli(ctx, config_file, profile, defaults_file, request_id, region, endpoint, cert_bundle, output, query, raw_output, generate_full_command_json_input, generate_param_json_input, debug, help): +def cli(ctx, config_file, profile, defaults_file, request_id, region, endpoint, cert_bundle, output, query, raw_output, auth, generate_full_command_json_input, generate_param_json_input, debug, help): if ctx.command_path == 'bmcs': click.echo(click.style(BMCS_DEPRECATION_NOTICE, fg='red'), file=sys.stderr) @@ -241,13 +235,22 @@ def cli(ctx, config_file, profile, defaults_file, request_id, region, endpoint, # # --profile cannot be specified as a regular default because we use it to determine which # section of the default file to read from - if OCI_CLI_PROFILE_ENV_VAR in os.environ: - profile = os.environ[OCI_CLI_PROFILE_ENV_VAR] - elif 'settings' in ctx.obj and CLI_RC_GENERIC_SETTINGS_DEFAULT_PROFILE_KEY in ctx.obj['settings']: - profile = ctx.obj['settings'][CLI_RC_GENERIC_SETTINGS_DEFAULT_PROFILE_KEY] + if cli_constants.OCI_CLI_PROFILE_ENV_VAR in os.environ: + profile = os.environ[cli_constants.OCI_CLI_PROFILE_ENV_VAR] + elif 'settings' in ctx.obj and cli_constants.CLI_RC_GENERIC_SETTINGS_DEFAULT_PROFILE_KEY in ctx.obj['settings']: + profile = ctx.obj['settings'][cli_constants.CLI_RC_GENERIC_SETTINGS_DEFAULT_PROFILE_KEY] else: profile = DEFAULT_PROFILE + if auth is None: + # if --auth is not supplied, fallback accordingly: + # - if OCI_CLI_AUTH exists, use that + if cli_constants.OCI_CLI_AUTH_ENV_VAR in os.environ: + if os.environ[cli_constants.OCI_CLI_AUTH_ENV_VAR] in OCI_CLI_AUTH_CHOICES: + auth = os.environ[cli_constants.OCI_CLI_AUTH_ENV_VAR] + else: + raise click.BadParameter('invalid choice: {arg_value}. (choose from {choices})'.format(arg_value=os.environ[cli_constants.OCI_CLI_AUTH_ENV_VAR], choices=', '.join(OCI_CLI_AUTH_CHOICES)), param_hint='OCI_CLI_AUTH') + initial_dict = { 'config_file': config_file, 'profile': profile, @@ -261,7 +264,8 @@ def cli(ctx, config_file, profile, defaults_file, request_id, region, endpoint, 'raw_output': raw_output, 'generate_full_command_json_input': generate_full_command_json_input, 'generate_param_json_input': generate_param_json_input, - 'debug': debug + 'debug': debug, + 'auth': auth } if not ctx.obj: @@ -273,7 +277,7 @@ def cli(ctx, config_file, profile, defaults_file, request_id, region, endpoint, if help: ctx.obj['help'] = True - if is_top_level_help(ctx) and not cli_util.parse_boolean(ctx.obj.get('settings', {}).get(CLI_RC_GENERIC_SETTINGS_USE_CLICK_HELP, False)): + if is_top_level_help(ctx) and not cli_util.parse_boolean(ctx.obj.get('settings', {}).get(cli_constants.CLI_RC_GENERIC_SETTINGS_USE_CLICK_HELP, False)): help_text_producer.render_help_text(ctx, [sys.argv[1]]) diff --git a/src/oci_cli/cli_setup.py b/src/oci_cli/cli_setup.py index 572f6d1ef..04ef87cc3 100644 --- a/src/oci_cli/cli_setup.py +++ b/src/oci_cli/cli_setup.py @@ -3,9 +3,9 @@ from __future__ import print_function import click -from .cli_root import cli, CLI_RC_CANNED_QUERIES_SECTION_NAME, CLI_RC_COMMAND_ALIASES_SECTION_NAME, CLI_RC_PARAM_ALIASES_SECTION_NAME +from .cli_root import cli +from .cli_constants import CLI_RC_CANNED_QUERIES_SECTION_NAME, CLI_RC_COMMAND_ALIASES_SECTION_NAME, CLI_RC_PARAM_ALIASES_SECTION_NAME, CLI_RC_DEFAULT_LOCATION from . import cli_util -from .cli_root import CLI_RC_DEFAULT_LOCATION import base64 import hashlib diff --git a/src/oci_cli/cli_util.py b/src/oci_cli/cli_util.py index 30af8d68e..3c0b77dae 100644 --- a/src/oci_cli/cli_util.py +++ b/src/oci_cli/cli_util.py @@ -39,6 +39,8 @@ from . import string_utils from . import help_text_producer +from . import cli_constants + try: # PY3+ import collections.abc as abc @@ -100,16 +102,12 @@ GENERIC_JSON_FORMAT_HELP = """This must be provided in JSON format. See API reference for additional help.""" - PARAM_LOOKUP_HEIRARCHY_TOP_LEVEL = '' - DEFAULT_FILE_CONVERT_PARAM_TRUTHY_VALUES = ['1', 'y', 't', 'yes', 'true', 'on'] - CLOCK_SKEW_WARNING_THRESHOLD_MINUTES = 5 - MODULE_TO_TYPE_MAPPINGS = MODULE_TO_TYPE_MAPPINGS @@ -118,10 +116,28 @@ def override(key, default): def build_client(service_name, ctx): - client_config = build_config(ctx.obj) + instance_principal_auth = 'auth' in ctx.obj and ctx.obj['auth'] == cli_constants.OCI_CLI_AUTH_INSTANCE_PRINCIPAL + + signer = None + kwargs = {} + client_config = {} + + try: + client_config = build_config(ctx.obj) + except exceptions.ConfigFileNotFound as e: + # config file is not required to be present for instance principal auth + if not instance_principal_auth: + sys.exit("ERROR: " + str(e)) + + if instance_principal_auth: + try: + signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner() + except Exception as e: + sys.exit("ERROR: Failed retrieving certificates from localhost. Instance principal auth is only possible from OCI compute instances. \nException: {}".format(str(e))) + kwargs['signer'] = signer try: - config.validate_config(client_config) + config.validate_config(client_config, **kwargs) except exceptions.InvalidConfig as bad_config: table = render_config_errors(bad_config) template = "ERROR: The config file at {config_file} is invalid:\n\n{errors}" @@ -130,7 +146,8 @@ def build_client(service_name, ctx): errors=table )) - warn_on_invalid_file_permissions(os.path.expanduser(client_config['key_file'])) + if 'key_file' in client_config: + warn_on_invalid_file_permissions(os.path.expanduser(client_config['key_file'])) # Add to ctx for later by the operations. ctx.obj["config"] = client_config @@ -145,10 +162,10 @@ def build_client(service_name, ctx): client_class = CLIENT_MAP[service_name] try: - client = client_class(client_config) + client = client_class(client_config, **kwargs) except exceptions.MissingPrivateKeyPassphrase: client_config['pass_phrase'] = prompt_for_passphrase() - client = client_class(client_config) + client = client_class(client_config, **kwargs) if ctx.obj['endpoint']: client.base_client.endpoint = ctx.obj['endpoint'] @@ -176,8 +193,6 @@ def build_config(command_args): try: client_config = config.from_file(file_location=command_args['config_file'], profile_name=command_args['profile']) - except exceptions.ConfigFileNotFound as e: - sys.exit("ERROR: " + str(e)) except exceptions.ProfileNotFound as e: sys.exit("ERROR: " + str(e)) @@ -602,9 +617,8 @@ def filter_object_headers(headers, whitelist): def help_callback(ctx, param, value): - from . import cli_root if ctx.obj.get("help", False): - if not parse_boolean(ctx.obj.get('settings', {}).get(cli_root.CLI_RC_GENERIC_SETTINGS_USE_CLICK_HELP, False)): + if not parse_boolean(ctx.obj.get('settings', {}).get(cli_constants.CLI_RC_GENERIC_SETTINGS_USE_CLICK_HELP, False)): help_text_producer.render_help_text(ctx) # We should only fall down here if the man/text-formatted help is unavailable or if the customer wanted @@ -614,7 +628,6 @@ def help_callback(ctx, param, value): def group_help_callback(ctx, param, value): - from . import cli_root args = sys.argv[1:] filtered_args = [] for a in args: @@ -625,7 +638,7 @@ def group_help_callback(ctx, param, value): # we'll just fall back to click's handling of group help. Note that using ctx.get_help() directly doesn't # work in this group help scenario, so we have to rely on click to do the right thing if ctx.obj.get("help", False): - if not parse_boolean(ctx.obj.get('settings', {}).get(cli_root.CLI_RC_GENERIC_SETTINGS_USE_CLICK_HELP, False)): + if not parse_boolean(ctx.obj.get('settings', {}).get(cli_constants.CLI_RC_GENERIC_SETTINGS_USE_CLICK_HELP, False)): help_text_producer.render_help_text(ctx, filtered_args) @@ -750,10 +763,14 @@ def load_context_obj_values_from_defaults(ctx): populate_dict_key_with_default_value(ctx, 'output', click.STRING) populate_dict_key_with_default_value(ctx, 'query', click.STRING) populate_dict_key_with_default_value(ctx, 'generate_param_json_input', click.STRING, param_name='generate-param-json-input') + populate_dict_key_with_default_value(ctx, 'auth', click.STRING) - if ctx.obj['output'] is None: + if 'output' not in ctx.obj or ctx.obj['output'] is None: ctx.obj['output'] = 'json' + if 'auth' not in ctx.obj or ctx.obj['auth'] is None: + ctx.obj['auth'] = cli_constants.OCI_CLI_AUTH_API_KEY + if 'debug' in ctx.obj: if not ctx.obj['debug']: # False for debug means not provided, so just load it if there is a default value. If there's nothing there, then this'll be @@ -1081,8 +1098,13 @@ def windows_warn_on_invalid_file_permissions(filename): if len(output) == 0: return - disallowed_identities = [line.strip() for line in output.decode().splitlines() if line] - warning = 'WARNING: Permissions for file {filename} are too open. The following users / groups have permissions to the file and should not: {identities}. To fix this please execute the following command: oci setup repair-file-permissions --file {filename}'.format(filename=filename, identities=', '.join(disallowed_identities)) + try: + disallowed_identities = [line.strip() for line in output.decode(sys.stdout.encoding).splitlines() if line] + warning = 'WARNING: Permissions for file {filename} are too open. The following users / groups have permissions to the file and should not: {identities}. To fix this please execute the following command: oci setup repair-file-permissions --file {filename}'.format(filename=filename, identities=', '.join(disallowed_identities)) + except ValueError: + # ValueError is the superclass exception of the various decoding errors we can receive. If we receive an error, + # still try and show a message + warning = 'WARNING: Permissions for file {filename} are too open. To fix this please execute the following command: oci setup repair-file-permissions --file {filename}'.format(filename=filename) click.echo(warning, file=sys.stderr) diff --git a/src/oci_cli/generated/identity_cli.py b/src/oci_cli/generated/identity_cli.py index 8e14582f1..bcd81afec 100644 --- a/src/oci_cli/generated/identity_cli.py +++ b/src/oci_cli/generated/identity_cli.py @@ -20,7 +20,8 @@ def identity_group(): pass -@click.command(cli_util.override('tag_namespace_group.command_name', 'tag-namespace'), cls=CommandGroupWithAlias, help="""A bag of tags that is attached to a compartment and has unique existence in tenancy.""") +@click.command(cli_util.override('tag_namespace_group.command_name', 'tag-namespace'), cls=CommandGroupWithAlias, help="""A managed container for defined tags. A tag namespace is unique in a tenancy. A tag namespace can't be deleted. +For more information, see [Managing Tags and Tag Namespaces].""") @cli_util.help_option_group def tag_namespace_group(): pass @@ -152,12 +153,27 @@ def compartment_group(): pass -@click.command(cli_util.override('tag_group.command_name', 'tag'), cls=CommandGroupWithAlias, help="""A tag definition that belongs to a specific tagNamespace.""") +@click.command(cli_util.override('tag_group.command_name', 'tag'), cls=CommandGroupWithAlias, help="""A tag definition that belongs to a specific tag namespace. "Defined tags" must be set up in your tenancy before +you can apply them to resources. +For more information, see [Managing Tags and Tag Namespaces].""") @cli_util.help_option_group def tag_group(): pass +@click.command(cli_util.override('dynamic_group_group.command_name', 'dynamic-group'), cls=CommandGroupWithAlias, help="""An dynamic group defines a matching rule. Every bare metal/vm instance is deployed with an instance certificate. +The certificate contains metadata about the instance. It contains the instance OCID and the compartment OCID, along +with a few other optional properties. When an API call is made using this instance certificate as the authenticator, +the certificate may be matched to one or multiple dynamic groups. Depending on policies written against these +dynamic groups, the instance will get access to that API. + +This works like regular user/group memebership. But in that case the membership is a static relationship, whereas +in dynamic group, the membership of an instance certificate to dynamic groups are determined during runtime.""") +@cli_util.help_option_group +def dynamic_group_group(): + pass + + @click.command(cli_util.override('region_group.command_name', 'region'), cls=CommandGroupWithAlias, help="""A localized geographic area, such as Phoenix, AZ. Oracle Cloud Infrastructure is hosted in regions and Availability Domains. A region is composed of several Availability Domains. An Availability Domain is one or more data centers located within a region. For more information, see [Regions and Availability Domains]. @@ -297,8 +313,8 @@ def add_user_to_group(ctx, from_json, wait_for_state, max_wait_seconds, wait_int @click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the tenancy containing the compartment. [required]""") @click.option('--name', callback=cli_util.handle_required_param, help="""The name you assign to the compartment during creation. The name must be unique across all compartments in the tenancy. [required]""") @click.option('--description', callback=cli_util.handle_required_param, help="""The description you assign to the compartment during creation. Does not have to be unique, and it's changeable. [required]""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") @click.option('--max-wait-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum time to wait for the resource to reach the lifecycle state defined by --wait-for-state. Defaults to 1200 seconds.""") @click.option('--wait-interval-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""Check every --wait-interval-seconds to see whether the resource to see if it has reached the lifecycle state defined by --wait-for-state. Defaults to 30 seconds.""") @@ -375,6 +391,60 @@ def create_customer_secret_key(ctx, from_json, display_name, user_id): cli_util.render_response(result, ctx) +@dynamic_group_group.command(name=cli_util.override('create_dynamic_group.command_name', 'create'), help="""Creates a new dynamic group in your tenancy. + +You must specify your tenancy's OCID as the compartment ID in the request object (remember that the tenancy is simply the root compartment). Notice that IAM resources (users, groups, compartments, and some policies) reside within the tenancy itself, unlike cloud resources such as compute instances, which typically reside within compartments inside the tenancy. For information about OCIDs, see [Resource Identifiers]. + +You must also specify a *name* for the dynamic group, which must be unique across all dynamic groups in your tenancy, and cannot be changed. Note that this name has to be also unique accross all groups in your tenancy. You can use this name or the OCID when writing policies that apply to the dynamic group. For more information about policies, see [How Policies Work]. + +You must also specify a *description* for the dynamic group (although it can be an empty string). It does not have to be unique, and you can change it anytime with [UpdateDynamicGroup]. + +After you send your request, the new object's `lifecycleState` will temporarily be CREATING. Before using the object, first make sure its `lifecycleState` has changed to ACTIVE.""") +@click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the tenancy containing the group. [required]""") +@click.option('--name', callback=cli_util.handle_required_param, help="""The name you assign to the group during creation. The name must be unique across all groups in the tenancy and cannot be changed. [required]""") +@click.option('--matching-rule', callback=cli_util.handle_required_param, help="""The matching rule to dynamically match an instance certificate to this dynamic group [required]""") +@click.option('--description', callback=cli_util.handle_required_param, help="""The description you assign to the group during creation. Does not have to be unique, and it's changeable. [required]""") +@click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") +@click.option('--max-wait-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum time to wait for the resource to reach the lifecycle state defined by --wait-for-state. Defaults to 1200 seconds.""") +@click.option('--wait-interval-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""Check every --wait-interval-seconds to see whether the resource to see if it has reached the lifecycle state defined by --wait-for-state. Defaults to 30 seconds.""") +@json_skeleton_utils.get_cli_json_input_option({}) +@cli_util.help_option +@click.pass_context +@json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={}, output_type={'module': 'identity', 'class': 'DynamicGroup'}) +@cli_util.wrap_exceptions +def create_dynamic_group(ctx, from_json, wait_for_state, max_wait_seconds, wait_interval_seconds, compartment_id, name, matching_rule, description): + kwargs = {} + + details = {} + details['compartmentId'] = compartment_id + details['name'] = name + details['matchingRule'] = matching_rule + details['description'] = description + + client = cli_util.build_client('identity', ctx) + result = client.create_dynamic_group( + create_dynamic_group_details=details, + **kwargs + ) + if wait_for_state: + if hasattr(client, 'get_dynamic_group') and callable(getattr(client, 'get_dynamic_group')): + try: + wait_period_kwargs = {} + if max_wait_seconds: + wait_period_kwargs['max_wait_seconds'] = max_wait_seconds + if wait_interval_seconds: + wait_period_kwargs['max_interval_seconds'] = wait_interval_seconds + + click.echo('Action completed. Waiting until the resource has entered state: {}'.format(wait_for_state), file=sys.stderr) + result = oci.wait_until(client, retry_utils.call_funtion_with_default_retries(client.get_dynamic_group, result.data.id), 'lifecycle_state', wait_for_state, **wait_period_kwargs) + except Exception as e: + # If we fail, we should show an error, but we should still provide the information to the customer + click.echo('Failed to wait until the resource entered the specified state. Outputting last known resource state', file=sys.stderr) + else: + click.echo('Unable to wait for the resource to enter the specified state', file=sys.stderr) + cli_util.render_response(result, ctx) + + @group_group.command(name=cli_util.override('create_group.command_name', 'create'), help="""Creates a new group in your tenancy. You must specify your tenancy's OCID as the compartment ID in the request object (remember that the tenancy is simply the root compartment). Notice that IAM resources (users, groups, compartments, and some policies) reside within the tenancy itself, unlike cloud resources such as compute instances, which typically reside within compartments inside the tenancy. For information about OCIDs, see [Resource Identifiers]. @@ -389,8 +459,8 @@ def create_customer_secret_key(ctx, from_json, display_name, user_id): @click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the tenancy containing the group. [required]""") @click.option('--name', callback=cli_util.handle_required_param, help="""The name you assign to the group during creation. The name must be unique across all groups in the tenancy and cannot be changed. [required]""") @click.option('--description', callback=cli_util.handle_required_param, help="""The description you assign to the group during creation. Does not have to be unique, and it's changeable. [required]""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") @click.option('--max-wait-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum time to wait for the resource to reach the lifecycle state defined by --wait-for-state. Defaults to 1200 seconds.""") @click.option('--wait-interval-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""Check every --wait-interval-seconds to see whether the resource to see if it has reached the lifecycle state defined by --wait-for-state. Defaults to 30 seconds.""") @@ -455,8 +525,8 @@ def create_group(ctx, from_json, wait_for_state, max_wait_seconds, wait_interval @click.option('--protocol', callback=cli_util.handle_required_param, type=custom_types.CliCaseInsensitiveChoice(["SAML2"]), help="""The protocol used for federation. Example: `SAML2` [required]""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") @click.option('--max-wait-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum time to wait for the resource to reach the lifecycle state defined by --wait-for-state. Defaults to 1200 seconds.""") @click.option('--wait-interval-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""Check every --wait-interval-seconds to see whether the resource to see if it has reached the lifecycle state defined by --wait-for-state. Defaults to 30 seconds.""") @@ -591,9 +661,9 @@ def create_or_reset_ui_password(ctx, from_json, user_id): @click.option('--name', callback=cli_util.handle_required_param, help="""The name you assign to the policy during creation. The name must be unique across all policies in the tenancy and cannot be changed. [required]""") @click.option('--statements', callback=cli_util.handle_required_param, type=custom_types.CLI_COMPLEX_TYPE, help="""An array of policy statements written in the policy language. See [How Policies Work] and [Common Policies]. [required]""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--description', callback=cli_util.handle_required_param, help="""The description you assign to the policy during creation. Does not have to be unique, and it's changeable. [required]""") -@click.option('--version-date', callback=cli_util.handle_optional_param, type=custom_types.CLI_DATETIME, help="""The version of the policy. If null or set to an empty string, when a request comes in for authorization, the policy will be evaluated according to the current behavior of the services at that moment. If set to a particular date (YYYY-MM-DD), the policy will be evaluated according to the behavior of the services on that date.""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--version-date', callback=cli_util.handle_optional_param, help="""The version of the policy. If null or set to an empty string, when a request comes in for authorization, the policy will be evaluated according to the current behavior of the services at that moment. If set to a particular date (YYYY-MM-DD), the policy will be evaluated according to the behavior of the services on that date.""") +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") @click.option('--max-wait-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum time to wait for the resource to reach the lifecycle state defined by --wait-for-state. Defaults to 1200 seconds.""") @click.option('--wait-interval-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""Check every --wait-interval-seconds to see whether the resource to see if it has reached the lifecycle state defined by --wait-for-state. Defaults to 30 seconds.""") @@ -704,18 +774,18 @@ def create_swift_password(ctx, from_json, description, user_id): cli_util.render_response(result, ctx) -@tag_group.command(name=cli_util.override('create_tag.command_name', 'create'), help="""Creates a new tag in a given tagNamespace. +@tag_group.command(name=cli_util.override('create_tag.command_name', 'create'), help="""Creates a new tag in the specified tag namespace. -You have to specify either the id or the name of the tagNamespace that will contain this tag definition. +You must specify either the OCID or the name of the tag namespace that will contain this tag definition. -You must also specify a *name* for the tag, which must be unique across all tags in the tagNamespace and cannot be changed. All ascii characters are allowed except spaces and dots. Note that names are case insenstive, that means you can not have two different tags with same name but with different casing in one tagNamespace. If you specify a name that's already in use in the tagNamespace, you'll get a 409 error. +You must also specify a *name* for the tag, which must be unique across all tags in the tag namespace and cannot be changed. The name can contain any ASCII character except the space (_) or period (.) characters. Names are case insensitive. That means, for example, \"myTag\" and \"mytag\" are not allowed in the same namespace. If you specify a name that's already in use in the tag namespace, a 409 error is returned. -You must also specify a *description* for the tag. It does not have to be unique, and you can change it anytime with [UpdateTag].""") -@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tagNamespace [required]""") -@click.option('--name', callback=cli_util.handle_required_param, help="""The name of the tag which must be unique across all tags in the tagNamespace and cannot be changed. [required]""") -@click.option('--description', callback=cli_util.handle_required_param, help="""The description of the tag. [required]""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +You must also specify a *description* for the tag. It does not have to be unique, and you can change it with [UpdateTag].""") +@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tag namespace. [required]""") +@click.option('--name', callback=cli_util.handle_required_param, help="""The name you assign to the tag during creation. The name must be unique within the tag namespace and cannot be changed. [required]""") +@click.option('--description', callback=cli_util.handle_required_param, help="""The description you assign to the tag during creation. [required]""") +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @json_skeleton_utils.get_cli_json_input_option({'freeform-tags': {'module': 'identity', 'class': 'dict(str, string)'}, 'defined-tags': {'module': 'identity', 'class': 'dict(str, dict(str, object))'}}) @cli_util.help_option @click.pass_context @@ -746,18 +816,20 @@ def create_tag(ctx, from_json, tag_namespace_id, name, description, freeform_tag cli_util.render_response(result, ctx) -@tag_namespace_group.command(name=cli_util.override('create_tag_namespace.command_name', 'create'), help="""Creates a new tagNamespace in a given compartment. +@tag_namespace_group.command(name=cli_util.override('create_tag_namespace.command_name', 'create'), help="""Creates a new tag namespace in the specified compartment. You must specify the compartment ID in the request object (remember that the tenancy is simply the root compartment). -You must also specify a *name* for the namespace, which must be unique across all namespaces in your tenancy and cannot be changed. All ascii characters are allowed except spaces and dots. Note that names are case insenstive, that means you can not have two different namespaces with same name but with different casing in one tenancy. Once you created a namespace, you can not change the name If you specify a name that's already in use in the tennacy, you'll get a 409 error. +You must also specify a *name* for the namespace, which must be unique across all namespaces in your tenancy and cannot be changed. The name can contain any ASCII character except the space (_) or period (.). Names are case insensitive. That means, for example, \"myNamespace\" and \"mynamespace\" are not allowed in the same tenancy. Once you created a namespace, you cannot change the name. If you specify a name that's already in use in the tenancy, a 409 error is returned. -You must also specify a *description* for the namespace. It does not have to be unique, and you can change it anytime with [UpdateTagNamespace].""") -@click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the tenancy containing the user. [required]""") -@click.option('--name', callback=cli_util.handle_required_param, help="""The name of the tagNamespace. It must be unique across all tagNamespaces in the tenancy and cannot be changed. [required]""") -@click.option('--description', callback=cli_util.handle_required_param, help="""The description of the tagNamespace. [required]""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +You must also specify a *description* for the namespace. It does not have to be unique, and you can change it with [UpdateTagNamespace]. + +Tag namespaces cannot be deleted, but they can be retired. See [Retiring Key Definitions and Namespace Definitions] for more information.""") +@click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the tenancy containing the tag namespace. [required]""") +@click.option('--name', callback=cli_util.handle_required_param, help="""The name you assign to the tag namespace during creation. It must be unique across all tag namespaces in the tenancy and cannot be changed. [required]""") +@click.option('--description', callback=cli_util.handle_required_param, help="""The description you assign to the tag namespace during creation. [required]""") +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @json_skeleton_utils.get_cli_json_input_option({'freeform-tags': {'module': 'identity', 'class': 'dict(str, string)'}, 'defined-tags': {'module': 'identity', 'class': 'dict(str, dict(str, object))'}}) @cli_util.help_option @click.pass_context @@ -801,8 +873,8 @@ def create_tag_namespace(ctx, from_json, compartment_id, name, description, free @click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the tenancy containing the user. [required]""") @click.option('--name', callback=cli_util.handle_required_param, help="""The name you assign to the user during creation. This is the user's login for the Console. The name must be unique across all users in the tenancy and cannot be changed. [required]""") @click.option('--description', callback=cli_util.handle_required_param, help="""The description you assign to the user during creation. Does not have to be unique, and it's changeable. [required]""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") @click.option('--max-wait-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum time to wait for the resource to reach the lifecycle state defined by --wait-for-state. Defaults to 1200 seconds.""") @click.option('--wait-interval-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""Check every --wait-interval-seconds to see whether the resource to see if it has reached the lifecycle state defined by --wait-for-state. Defaults to 30 seconds.""") @@ -909,6 +981,49 @@ def delete_customer_secret_key(ctx, from_json, user_id, customer_secret_key_id, cli_util.render_response(result, ctx) +@dynamic_group_group.command(name=cli_util.override('delete_dynamic_group.command_name', 'delete'), help="""Deletes the specified dynamic group.""") +@click.option('--dynamic-group-id', callback=cli_util.handle_required_param, help="""The OCID of the dynamic group. [required]""") +@click.option('--if-match', callback=cli_util.handle_optional_param, help="""For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if-match` parameter to the value of the etag from a previous GET or POST response for that resource. The resource will be updated or deleted only if the etag you provide matches the resource's current etag value.""") +@cli_util.confirm_delete_option +@click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") +@click.option('--max-wait-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum time to wait for the resource to reach the lifecycle state defined by --wait-for-state. Defaults to 1200 seconds.""") +@click.option('--wait-interval-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""Check every --wait-interval-seconds to see whether the resource to see if it has reached the lifecycle state defined by --wait-for-state. Defaults to 30 seconds.""") +@json_skeleton_utils.get_cli_json_input_option({}) +@cli_util.help_option +@click.pass_context +@json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={}) +@cli_util.wrap_exceptions +def delete_dynamic_group(ctx, from_json, wait_for_state, max_wait_seconds, wait_interval_seconds, dynamic_group_id, if_match): + + if isinstance(dynamic_group_id, six.string_types) and len(dynamic_group_id.strip()) == 0: + raise click.UsageError('Parameter --dynamic-group-id cannot be whitespace or empty string') + kwargs = {} + if if_match is not None: + kwargs['if_match'] = if_match + client = cli_util.build_client('identity', ctx) + result = client.delete_dynamic_group( + dynamic_group_id=dynamic_group_id, + **kwargs + ) + if wait_for_state: + if hasattr(client, 'get_dynamic_group') and callable(getattr(client, 'get_dynamic_group')): + try: + wait_period_kwargs = {} + if max_wait_seconds: + wait_period_kwargs['max_wait_seconds'] = max_wait_seconds + if wait_interval_seconds: + wait_period_kwargs['max_interval_seconds'] = wait_interval_seconds + + click.echo('Action completed. Waiting until the resource has entered state: {}'.format(wait_for_state), file=sys.stderr) + oci.wait_until(client, retry_utils.call_funtion_with_default_retries(client.get_dynamic_group, dynamic_group_id), 'lifecycle_state', wait_for_state, succeed_on_not_found=True, **wait_period_kwargs) + except Exception as e: + # If we fail, we should show an error, but we should still provide the information to the customer + click.echo('Failed to wait until the resource entered the specified state. Please retrieve the resource to find its current state', file=sys.stderr) + else: + click.echo('Unable to wait for the resource to enter the specified state', file=sys.stderr) + cli_util.render_response(result, ctx) + + @group_group.command(name=cli_util.override('delete_group.command_name', 'delete'), help="""Deletes the specified group. The group must be empty.""") @click.option('--group-id', callback=cli_util.handle_required_param, help="""The OCID of the group. [required]""") @click.option('--if-match', callback=cli_util.handle_optional_param, help="""For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if-match` parameter to the value of the etag from a previous GET or POST response for that resource. The resource will be updated or deleted only if the etag you provide matches the resource's current etag value.""") @@ -1161,6 +1276,26 @@ def get_compartment(ctx, from_json, compartment_id): cli_util.render_response(result, ctx) +@dynamic_group_group.command(name=cli_util.override('get_dynamic_group.command_name', 'get'), help="""Gets the specified dynamic group's information.""") +@click.option('--dynamic-group-id', callback=cli_util.handle_required_param, help="""The OCID of the dynamic group. [required]""") +@json_skeleton_utils.get_cli_json_input_option({}) +@cli_util.help_option +@click.pass_context +@json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={}, output_type={'module': 'identity', 'class': 'DynamicGroup'}) +@cli_util.wrap_exceptions +def get_dynamic_group(ctx, from_json, dynamic_group_id): + + if isinstance(dynamic_group_id, six.string_types) and len(dynamic_group_id.strip()) == 0: + raise click.UsageError('Parameter --dynamic-group-id cannot be whitespace or empty string') + kwargs = {} + client = cli_util.build_client('identity', ctx) + result = client.get_dynamic_group( + dynamic_group_id=dynamic_group_id, + **kwargs + ) + cli_util.render_response(result, ctx) + + @group_group.command(name=cli_util.override('get_group.command_name', 'get'), help="""Gets the specified group's information. This operation does not return a list of all the users in the group. To do that, use [ListUserGroupMemberships] and provide the group's OCID as a query parameter in the request.""") @@ -1249,8 +1384,8 @@ def get_policy(ctx, from_json, policy_id): @tag_group.command(name=cli_util.override('get_tag.command_name', 'get'), help="""Gets the specified tag's information.""") -@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tagNamespace [required]""") -@click.option('--tag-name', callback=cli_util.handle_required_param, help="""The name of the tag [required]""") +@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tag namespace. [required]""") +@click.option('--tag-name', callback=cli_util.handle_required_param, help="""The name of the tag. [required]""") @json_skeleton_utils.get_cli_json_input_option({}) @cli_util.help_option @click.pass_context @@ -1273,8 +1408,8 @@ def get_tag(ctx, from_json, tag_namespace_id, tag_name): cli_util.render_response(result, ctx) -@tag_namespace_group.command(name=cli_util.override('get_tag_namespace.command_name', 'get'), help="""Gets the specified tagNamespace's information.""") -@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tagNamespace [required]""") +@tag_namespace_group.command(name=cli_util.override('get_tag_namespace.command_name', 'get'), help="""Gets the specified tag namespace's information.""") +@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tag namespace. [required]""") @json_skeleton_utils.get_cli_json_input_option({}) @cli_util.help_option @click.pass_context @@ -1458,6 +1593,52 @@ def list_customer_secret_keys(ctx, from_json, user_id): cli_util.render_response(result, ctx) +@dynamic_group_group.command(name=cli_util.override('list_dynamic_groups.command_name', 'list'), help="""Lists the dynamic groups in your tenancy. You must specify your tenancy's OCID as the value for the compartment ID (remember that the tenancy is simply the root compartment). See [Where to Get the Tenancy's OCID and User's OCID].""") +@click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the compartment (remember that the tenancy is simply the root compartment). [required]""") +@click.option('--page', callback=cli_util.handle_optional_param, help="""The value of the `opc-next-page` response header from the previous \"List\" call.""") +@click.option('--limit', callback=cli_util.handle_optional_param, type=click.INT, help="""The maximum number of items to return in a paginated \"List\" call.""") +@click.option('--all', 'all_pages', is_flag=True, callback=cli_util.handle_optional_param, help="""Fetches all pages of results. If you provide this option, then you cannot provide the --limit option.""") +@click.option('--page-size', type=click.INT, callback=cli_util.handle_optional_param, help="""When fetching results, the number of results to fetch per call. Only valid when used with --all or --limit, and ignored otherwise.""") +@json_skeleton_utils.get_cli_json_input_option({}) +@cli_util.help_option +@click.pass_context +@json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={}, output_type={'module': 'identity', 'class': 'list[DynamicGroup]'}) +@cli_util.wrap_exceptions +def list_dynamic_groups(ctx, from_json, all_pages, page_size, compartment_id, page, limit): + + if all_pages and limit: + raise click.UsageError('If you provide the --all option you cannot provide the --limit option') + kwargs = {} + if page is not None: + kwargs['page'] = page + if limit is not None: + kwargs['limit'] = limit + client = cli_util.build_client('identity', ctx) + if all_pages: + if page_size: + kwargs['limit'] = page_size + + result = retry_utils.list_call_get_all_results_with_default_retries( + client.list_dynamic_groups, + compartment_id=compartment_id, + **kwargs + ) + elif limit is not None: + result = retry_utils.list_call_get_up_to_limit_with_default_retries( + client.list_dynamic_groups, + limit, + page_size, + compartment_id=compartment_id, + **kwargs + ) + else: + result = client.list_dynamic_groups( + compartment_id=compartment_id, + **kwargs + ) + cli_util.render_response(result, ctx) + + @group_group.command(name=cli_util.override('list_groups.command_name', 'list'), help="""Lists the groups in your tenancy. You must specify your tenancy's OCID as the value for the compartment ID (remember that the tenancy is simply the root compartment). See [Where to Get the Tenancy's OCID and User's OCID].""") @click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the compartment (remember that the tenancy is simply the root compartment). [required]""") @click.option('--page', callback=cli_util.handle_optional_param, help="""The value of the `opc-next-page` response header from the previous \"List\" call.""") @@ -1706,11 +1887,11 @@ def list_swift_passwords(ctx, from_json, user_id): cli_util.render_response(result, ctx) -@tag_namespace_group.command(name=cli_util.override('list_tag_namespaces.command_name', 'list'), help="""List the tagNamespaces in a given compartment.""") +@tag_namespace_group.command(name=cli_util.override('list_tag_namespaces.command_name', 'list'), help="""Lists the tag namespaces in the specified compartment.""") @click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the compartment (remember that the tenancy is simply the root compartment). [required]""") @click.option('--page', callback=cli_util.handle_optional_param, help="""The value of the `opc-next-page` response header from the previous \"List\" call.""") @click.option('--limit', callback=cli_util.handle_optional_param, type=click.INT, help="""The maximum number of items to return in a paginated \"List\" call.""") -@click.option('--include-subcompartments', callback=cli_util.handle_optional_param, type=click.BOOL, help="""An optional boolean parameter for whether or not to retrieve all tagNamespaces in sub compartments. In case of absence of this parameter, only tagNamespaces that exist directly in this compartment will be retrieved.""") +@click.option('--include-subcompartments', callback=cli_util.handle_optional_param, type=click.BOOL, help="""An optional boolean parameter indicating whether to retrieve all tag namespaces in subcompartments. If this parameter is not specified, only the tag namespaces defined in the specified compartment are retrieved.""") @click.option('--all', 'all_pages', is_flag=True, callback=cli_util.handle_optional_param, help="""Fetches all pages of results. If you provide this option, then you cannot provide the --limit option.""") @click.option('--page-size', type=click.INT, callback=cli_util.handle_optional_param, help="""When fetching results, the number of results to fetch per call. Only valid when used with --all or --limit, and ignored otherwise.""") @json_skeleton_utils.get_cli_json_input_option({}) @@ -1755,8 +1936,8 @@ def list_tag_namespaces(ctx, from_json, all_pages, page_size, compartment_id, pa cli_util.render_response(result, ctx) -@tag_group.command(name=cli_util.override('list_tags.command_name', 'list'), help="""List the tags that are defined in a given tagNamespace.""") -@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tagNamespace [required]""") +@tag_group.command(name=cli_util.override('list_tags.command_name', 'list'), help="""Lists the tag definitions in the specified tag namespace.""") +@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tag namespace. [required]""") @click.option('--page', callback=cli_util.handle_optional_param, help="""The value of the `opc-next-page` response header from the previous \"List\" call.""") @click.option('--limit', callback=cli_util.handle_optional_param, type=click.INT, help="""The maximum number of items to return in a paginated \"List\" call.""") @click.option('--all', 'all_pages', is_flag=True, callback=cli_util.handle_optional_param, help="""Fetches all pages of results. If you provide this option, then you cannot provide the --limit option.""") @@ -1932,8 +2113,8 @@ def remove_user_from_group(ctx, from_json, user_group_membership_id, if_match): @click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the compartment. [required]""") @click.option('--description', callback=cli_util.handle_optional_param, help="""The description you assign to the compartment. Does not have to be unique, and it's changeable.""") @click.option('--name', callback=cli_util.handle_optional_param, help="""The new name you assign to the compartment. The name must be unique across all compartments in the tenancy.""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--if-match', callback=cli_util.handle_optional_param, help="""For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if-match` parameter to the value of the etag from a previous GET or POST response for that resource. The resource will be updated or deleted only if the etag you provide matches the resource's current etag value.""") @click.option('--force', callback=cli_util.handle_optional_param, help="""Perform update without prompting for confirmation.""", is_flag=True) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") @@ -2031,11 +2212,65 @@ def update_customer_secret_key(ctx, from_json, user_id, customer_secret_key_id, cli_util.render_response(result, ctx) +@dynamic_group_group.command(name=cli_util.override('update_dynamic_group.command_name', 'update'), help="""Updates the specified dynamic group.""") +@click.option('--dynamic-group-id', callback=cli_util.handle_required_param, help="""The OCID of the dynamic group. [required]""") +@click.option('--description', callback=cli_util.handle_optional_param, help="""The description you assign to the dynamic group. Does not have to be unique, and it's changeable.""") +@click.option('--matching-rule', callback=cli_util.handle_optional_param, help="""The matching rule to dynamically match an instance certificate to this dynamic group""") +@click.option('--if-match', callback=cli_util.handle_optional_param, help="""For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if-match` parameter to the value of the etag from a previous GET or POST response for that resource. The resource will be updated or deleted only if the etag you provide matches the resource's current etag value.""") +@click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") +@click.option('--max-wait-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum time to wait for the resource to reach the lifecycle state defined by --wait-for-state. Defaults to 1200 seconds.""") +@click.option('--wait-interval-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""Check every --wait-interval-seconds to see whether the resource to see if it has reached the lifecycle state defined by --wait-for-state. Defaults to 30 seconds.""") +@json_skeleton_utils.get_cli_json_input_option({}) +@cli_util.help_option +@click.pass_context +@json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={}, output_type={'module': 'identity', 'class': 'DynamicGroup'}) +@cli_util.wrap_exceptions +def update_dynamic_group(ctx, from_json, wait_for_state, max_wait_seconds, wait_interval_seconds, dynamic_group_id, description, matching_rule, if_match): + + if isinstance(dynamic_group_id, six.string_types) and len(dynamic_group_id.strip()) == 0: + raise click.UsageError('Parameter --dynamic-group-id cannot be whitespace or empty string') + kwargs = {} + if if_match is not None: + kwargs['if_match'] = if_match + + details = {} + + if description is not None: + details['description'] = description + + if matching_rule is not None: + details['matchingRule'] = matching_rule + + client = cli_util.build_client('identity', ctx) + result = client.update_dynamic_group( + dynamic_group_id=dynamic_group_id, + update_dynamic_group_details=details, + **kwargs + ) + if wait_for_state: + if hasattr(client, 'get_dynamic_group') and callable(getattr(client, 'get_dynamic_group')): + try: + wait_period_kwargs = {} + if max_wait_seconds: + wait_period_kwargs['max_wait_seconds'] = max_wait_seconds + if wait_interval_seconds: + wait_period_kwargs['max_interval_seconds'] = wait_interval_seconds + + click.echo('Action completed. Waiting until the resource has entered state: {}'.format(wait_for_state), file=sys.stderr) + result = oci.wait_until(client, retry_utils.call_funtion_with_default_retries(client.get_dynamic_group, result.data.id), 'lifecycle_state', wait_for_state, **wait_period_kwargs) + except Exception as e: + # If we fail, we should show an error, but we should still provide the information to the customer + click.echo('Failed to wait until the resource entered the specified state. Outputting last known resource state', file=sys.stderr) + else: + click.echo('Unable to wait for the resource to enter the specified state', file=sys.stderr) + cli_util.render_response(result, ctx) + + @group_group.command(name=cli_util.override('update_group.command_name', 'update'), help="""Updates the specified group.""") @click.option('--group-id', callback=cli_util.handle_required_param, help="""The OCID of the group. [required]""") @click.option('--description', callback=cli_util.handle_optional_param, help="""The description you assign to the group. Does not have to be unique, and it's changeable.""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--if-match', callback=cli_util.handle_optional_param, help="""For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if-match` parameter to the value of the etag from a previous GET or POST response for that resource. The resource will be updated or deleted only if the etag you provide matches the resource's current etag value.""") @click.option('--force', callback=cli_util.handle_optional_param, help="""Perform update without prompting for confirmation.""", is_flag=True) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") @@ -2100,8 +2335,8 @@ def update_group(ctx, from_json, force, wait_for_state, max_wait_seconds, wait_i Example: `SAML2` [required]""") @click.option('--description', callback=cli_util.handle_optional_param, help="""The description you assign to the `IdentityProvider`. Does not have to be unique, and it's changeable.""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--if-match', callback=cli_util.handle_optional_param, help="""For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if-match` parameter to the value of the etag from a previous GET or POST response for that resource. The resource will be updated or deleted only if the etag you provide matches the resource's current etag value.""") @click.option('--force', callback=cli_util.handle_optional_param, help="""Perform update without prompting for confirmation.""", is_flag=True) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") @@ -2226,9 +2461,9 @@ def update_idp_group_mapping(ctx, from_json, wait_for_state, max_wait_seconds, w @click.option('--policy-id', callback=cli_util.handle_required_param, help="""The OCID of the policy. [required]""") @click.option('--description', callback=cli_util.handle_optional_param, help="""The description you assign to the policy. Does not have to be unique, and it's changeable.""") @click.option('--statements', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""An array of policy statements written in the policy language. See [How Policies Work] and [Common Policies].""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--version-date', callback=cli_util.handle_optional_param, type=custom_types.CLI_DATETIME, help="""The version of the policy. If null or set to an empty string, when a request comes in for authorization, the policy will be evaluated according to the current behavior of the services at that moment. If set to a particular date (YYYY-MM-DD), the policy will be evaluated according to the behavior of the services on that date.""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--version-date', callback=cli_util.handle_optional_param, help="""The version of the policy. If null or set to an empty string, when a request comes in for authorization, the policy will be evaluated according to the current behavior of the services at that moment. If set to a particular date (YYYY-MM-DD), the policy will be evaluated according to the behavior of the services on that date.""") +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--if-match', callback=cli_util.handle_optional_param, help="""For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if-match` parameter to the value of the etag from a previous GET or POST response for that resource. The resource will be updated or deleted only if the etag you provide matches the resource's current etag value.""") @click.option('--force', callback=cli_util.handle_optional_param, help="""Perform update without prompting for confirmation.""", is_flag=True) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") @@ -2329,13 +2564,13 @@ def update_swift_password(ctx, from_json, user_id, swift_password_id, descriptio cli_util.render_response(result, ctx) -@tag_group.command(name=cli_util.override('update_tag.command_name', 'update'), help="""Updates the the specified tag. Only description and isRetired can be updated. Retiring a tag will also retire the related rules. You can not a tag with the same name as a retired tag. Tags must be unique within their tag namespace but can be repeated across namespaces. You cannot add a tag with the same name as a retired tag in the same tag namespace.""") -@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tagNamespace [required]""") -@click.option('--tag-name', callback=cli_util.handle_required_param, help="""The name of the tag [required]""") -@click.option('--description', callback=cli_util.handle_optional_param, help="""The description of the tag.""") -@click.option('--is-retired', callback=cli_util.handle_optional_param, type=click.BOOL, help="""whether or not the tag is retired""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@tag_group.command(name=cli_util.override('update_tag.command_name', 'update'), help="""Updates the the specified tag definition. You can update `description`, and `isRetired`.""") +@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tag namespace. [required]""") +@click.option('--tag-name', callback=cli_util.handle_required_param, help="""The name of the tag. [required]""") +@click.option('--description', callback=cli_util.handle_optional_param, help="""The description you assign to the tag during creation.""") +@click.option('--is-retired', callback=cli_util.handle_optional_param, type=click.BOOL, help="""Whether the tag is retired. See [Retiring Key Definitions and Namespace Definitions].""") +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--force', callback=cli_util.handle_optional_param, help="""Perform update without prompting for confirmation.""", is_flag=True) @json_skeleton_utils.get_cli_json_input_option({'freeform-tags': {'module': 'identity', 'class': 'dict(str, string)'}, 'defined-tags': {'module': 'identity', 'class': 'dict(str, dict(str, object))'}}) @cli_util.help_option @@ -2379,12 +2614,16 @@ def update_tag(ctx, from_json, force, tag_namespace_id, tag_name, description, i cli_util.render_response(result, ctx) -@tag_namespace_group.command(name=cli_util.override('update_tag_namespace.command_name', 'update'), help="""Updates the the specified tagNamespace. Only description, isRetired and assigned tags can be updated. Updating isRetired to be true will retire the namespace, all the contained tags and the related rules. Reactivating a namespace will not reactivate any tag definition that was retired when the namespace was retired. They will have to be individually reactivated *after* the namespace is reactivated. You can't add a namespace with the same name as a retired namespace in the same tenant.""") -@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tagNamespace [required]""") -@click.option('--description', callback=cli_util.handle_optional_param, help="""The description of the tagNamespace.""") -@click.option('--is-retired', callback=cli_util.handle_optional_param, type=click.BOOL, help="""whether or not the tagNamespace is retired""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@tag_namespace_group.command(name=cli_util.override('update_tag_namespace.command_name', 'update'), help="""Updates the the specified tag namespace. You can't update the namespace name. + +Updating `isRetired` to 'true' retires the namespace and all the tag definitions in the namespace. Reactivating a namespace (changing `isRetired` from 'true' to 'false') does not reactivate tag definitions. To reactivate the tag definitions, you must reactivate each one indvidually *after* you reactivate the namespace, using [UpdateTag]. For more information about retiring tag namespaces, see [Retiring Key Definitions and Namespace Definitions]. + +You can't add a namespace with the same name as a retired namespace in the same tenancy.""") +@click.option('--tag-namespace-id', callback=cli_util.handle_required_param, help="""The OCID of the tag namespace. [required]""") +@click.option('--description', callback=cli_util.handle_optional_param, help="""The description you assign to the tag namespace.""") +@click.option('--is-retired', callback=cli_util.handle_optional_param, type=click.BOOL, help="""Whether the tag namespace is retired. See [Retiring Key Definitions and Namespace Definitions].""") +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--force', callback=cli_util.handle_optional_param, help="""Perform update without prompting for confirmation.""", is_flag=True) @json_skeleton_utils.get_cli_json_input_option({'freeform-tags': {'module': 'identity', 'class': 'dict(str, string)'}, 'defined-tags': {'module': 'identity', 'class': 'dict(str, dict(str, object))'}}) @cli_util.help_option @@ -2427,8 +2666,8 @@ def update_tag_namespace(ctx, from_json, force, tag_namespace_id, description, i @user_group.command(name=cli_util.override('update_user.command_name', 'update'), help="""Updates the description of the specified user.""") @click.option('--user-id', callback=cli_util.handle_required_param, help="""The OCID of the user. [required]""") @click.option('--description', callback=cli_util.handle_optional_param, help="""The description you assign to the user. Does not have to be unique, and it's changeable.""") -@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Simple key-value pair that is applied without any predefined name, type or scope. Exists for cross-compatibility only. Example: `{\"bar-key\": \"value\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Usage of predefined tag keys. These predefined keys are scoped to namespaces. Example: `{\"foo-namespace\": {\"bar-key\": \"foo-value\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--freeform-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Free-form tags for this resource. Each tag is a simple key-value pair with no predefined name, type, or namespace. For more information, see [Resource Tags]. Example: `{\"Department\": \"Finance\"}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) +@click.option('--defined-tags', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Defined tags for this resource. Each key is predefined and scoped to a namespace. For more information, see [Resource Tags]. Example: `{\"Operations\": {\"CostCenter\": \"42\"}}`""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--if-match', callback=cli_util.handle_optional_param, help="""For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if-match` parameter to the value of the etag from a previous GET or POST response for that resource. The resource will be updated or deleted only if the etag you provide matches the resource's current etag value.""") @click.option('--force', callback=cli_util.handle_optional_param, help="""Perform update without prompting for confirmation.""", is_flag=True) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "INACTIVE", "DELETING", "DELETED"]), callback=cli_util.handle_optional_param, help="""This operation creates, modifies or deletes a resource that has a defined lifecycle state. Specify this option to perform the action and then wait until the resource reaches a given lifecycle state.""") diff --git a/src/oci_cli/generated/loadbalancer_cli.py b/src/oci_cli/generated/loadbalancer_cli.py index a1a0f895c..7ba6a1e8c 100644 --- a/src/oci_cli/generated/loadbalancer_cli.py +++ b/src/oci_cli/generated/loadbalancer_cli.py @@ -377,7 +377,9 @@ def create_certificate(ctx, from_json, wait_for_state, max_wait_seconds, wait_in @listener_group.command(name=cli_util.override('create_listener.command_name', 'create'), help="""Adds a listener to a load balancer.""") -@click.option('--default-backend-set-name', callback=cli_util.handle_required_param, help="""The name of the associated backend set. [required]""") +@click.option('--default-backend-set-name', callback=cli_util.handle_required_param, help="""The name of the associated backend set. + +Example: `My_backend_set` [required]""") @click.option('--name', callback=cli_util.handle_required_param, help="""A friendly name for the listener. It must be unique and it cannot be changed. Avoid entering confidential information. Example: `My listener` [required]""") @@ -388,16 +390,17 @@ def create_certificate(ctx, from_json, wait_for_state, max_wait_seconds, wait_in Example: `HTTP` [required]""") @click.option('--load-balancer-id', callback=cli_util.handle_required_param, help="""The [OCID] of the load balancer on which to add a listener. [required]""") +@click.option('--connection-configuration', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--ssl-configuration', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["ACCEPTED", "IN_PROGRESS", "FAILED", "SUCCEEDED"]), callback=cli_util.handle_optional_param, help="""This operation asynchronously creates, modifies or deletes a resource and uses a work request to track the progress of the operation. Specify this option to perform the action and then wait until the work request reaches a certain state.""") @click.option('--max-wait-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum time to wait for the work request to reach the state defined by --wait-for-state. Defaults to 1200 seconds.""") @click.option('--wait-interval-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""Check every --wait-interval-seconds to see whether the work request to see if it has reached the state defined by --wait-for-state. Defaults to 30 seconds.""") -@json_skeleton_utils.get_cli_json_input_option({'ssl-configuration': {'module': 'load_balancer', 'class': 'SSLConfigurationDetails'}}) +@json_skeleton_utils.get_cli_json_input_option({'connection-configuration': {'module': 'load_balancer', 'class': 'ConnectionConfiguration'}, 'ssl-configuration': {'module': 'load_balancer', 'class': 'SSLConfigurationDetails'}}) @cli_util.help_option @click.pass_context -@json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={'ssl-configuration': {'module': 'load_balancer', 'class': 'SSLConfigurationDetails'}}) +@json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={'connection-configuration': {'module': 'load_balancer', 'class': 'ConnectionConfiguration'}, 'ssl-configuration': {'module': 'load_balancer', 'class': 'SSLConfigurationDetails'}}) @cli_util.wrap_exceptions -def create_listener(ctx, from_json, wait_for_state, max_wait_seconds, wait_interval_seconds, default_backend_set_name, name, port, protocol, load_balancer_id, ssl_configuration): +def create_listener(ctx, from_json, wait_for_state, max_wait_seconds, wait_interval_seconds, default_backend_set_name, name, port, protocol, load_balancer_id, connection_configuration, ssl_configuration): if isinstance(load_balancer_id, six.string_types) and len(load_balancer_id.strip()) == 0: raise click.UsageError('Parameter --load-balancer-id cannot be whitespace or empty string') @@ -410,6 +413,9 @@ def create_listener(ctx, from_json, wait_for_state, max_wait_seconds, wait_inter details['port'] = port details['protocol'] = protocol + if connection_configuration is not None: + details['connectionConfiguration'] = cli_util.parse_json_parameter("connection_configuration", connection_configuration) + if ssl_configuration is not None: details['sslConfiguration'] = cli_util.parse_json_parameter("ssl_configuration", ssl_configuration) @@ -1124,10 +1130,10 @@ def list_load_balancer_healths(ctx, from_json, all_pages, page_size, compartment @click.option('--detail', callback=cli_util.handle_optional_param, help="""The level of detail to return for each result. Can be `full` or `simple`. Example: `full`""") -@click.option('--sort-by', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["TIMECREATED", "DISPLAYNAME"]), help="""The field to sort by. Only one sort order may be provided. Time created is default ordered as descending. Display name is default ordered as ascending.""") -@click.option('--sort-order', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["ASC", "DESC"]), help="""The sort order to use, either 'asc' or 'desc'""") -@click.option('--display-name', callback=cli_util.handle_optional_param, help="""A filter to only return resources that match the given display name exactly.""") -@click.option('--lifecycle-state', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["CREATING", "FAILED", "ACTIVE", "DELETING", "DELETED"]), help="""A filter to only return resources that match the given lifecycle state.""") +@click.option('--sort-by', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["TIMECREATED", "DISPLAYNAME"]), help="""The field to sort by. You can provide one sort order (`sortOrder`). Default order for TIMECREATED is descending. Default order for DISPLAYNAME is ascending. The DISPLAYNAME sort order is case sensitive.""") +@click.option('--sort-order', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["ASC", "DESC"]), help="""The sort order to use, either ascending (`ASC`) or descending (`DESC`). The DISPLAYNAME sort order is case sensitive.""") +@click.option('--display-name', callback=cli_util.handle_optional_param, help="""A filter to return only resources that match the given display name exactly.""") +@click.option('--lifecycle-state', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["CREATING", "FAILED", "ACTIVE", "DELETING", "DELETED"]), help="""A filter to return only resources that match the given lifecycle state.""") @click.option('--all', 'all_pages', is_flag=True, callback=cli_util.handle_optional_param, help="""Fetches all pages of results. If you provide this option, then you cannot provide the --limit option.""") @click.option('--page-size', type=click.INT, callback=cli_util.handle_optional_param, help="""When fetching results, the number of results to fetch per call. Only valid when used with --all or --limit, and ignored otherwise.""") @json_skeleton_utils.get_cli_json_input_option({}) @@ -1620,7 +1626,9 @@ def update_health_checker(ctx, from_json, wait_for_state, max_wait_seconds, wait @listener_group.command(name=cli_util.override('update_listener.command_name', 'update'), help="""Updates a listener for a given load balancer.""") -@click.option('--default-backend-set-name', callback=cli_util.handle_required_param, help="""The name of the associated backend set. [required]""") +@click.option('--default-backend-set-name', callback=cli_util.handle_required_param, help="""The name of the associated backend set. + +Example: `My_backend_set` [required]""") @click.option('--port', callback=cli_util.handle_required_param, type=click.INT, help="""The communication port for the listener. Example: `80` [required]""") @@ -1631,17 +1639,18 @@ def update_health_checker(ctx, from_json, wait_for_state, max_wait_seconds, wait @click.option('--listener-name', callback=cli_util.handle_required_param, help="""The name of the listener to update. Example: `My listener` [required]""") +@click.option('--connection-configuration', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--ssl-configuration', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) @click.option('--force', callback=cli_util.handle_optional_param, help="""Perform update without prompting for confirmation.""", is_flag=True) @click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["ACCEPTED", "IN_PROGRESS", "FAILED", "SUCCEEDED"]), callback=cli_util.handle_optional_param, help="""This operation asynchronously creates, modifies or deletes a resource and uses a work request to track the progress of the operation. Specify this option to perform the action and then wait until the work request reaches a certain state.""") @click.option('--max-wait-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum time to wait for the work request to reach the state defined by --wait-for-state. Defaults to 1200 seconds.""") @click.option('--wait-interval-seconds', type=click.INT, callback=cli_util.handle_optional_param, help="""Check every --wait-interval-seconds to see whether the work request to see if it has reached the state defined by --wait-for-state. Defaults to 30 seconds.""") -@json_skeleton_utils.get_cli_json_input_option({'ssl-configuration': {'module': 'load_balancer', 'class': 'SSLConfigurationDetails'}}) +@json_skeleton_utils.get_cli_json_input_option({'connection-configuration': {'module': 'load_balancer', 'class': 'ConnectionConfiguration'}, 'ssl-configuration': {'module': 'load_balancer', 'class': 'SSLConfigurationDetails'}}) @cli_util.help_option @click.pass_context -@json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={'ssl-configuration': {'module': 'load_balancer', 'class': 'SSLConfigurationDetails'}}) +@json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={'connection-configuration': {'module': 'load_balancer', 'class': 'ConnectionConfiguration'}, 'ssl-configuration': {'module': 'load_balancer', 'class': 'SSLConfigurationDetails'}}) @cli_util.wrap_exceptions -def update_listener(ctx, from_json, force, wait_for_state, max_wait_seconds, wait_interval_seconds, default_backend_set_name, port, protocol, load_balancer_id, listener_name, ssl_configuration): +def update_listener(ctx, from_json, force, wait_for_state, max_wait_seconds, wait_interval_seconds, default_backend_set_name, port, protocol, load_balancer_id, listener_name, connection_configuration, ssl_configuration): if isinstance(load_balancer_id, six.string_types) and len(load_balancer_id.strip()) == 0: raise click.UsageError('Parameter --load-balancer-id cannot be whitespace or empty string') @@ -1649,8 +1658,8 @@ def update_listener(ctx, from_json, force, wait_for_state, max_wait_seconds, wai if isinstance(listener_name, six.string_types) and len(listener_name.strip()) == 0: raise click.UsageError('Parameter --listener-name cannot be whitespace or empty string') if not force: - if ssl_configuration: - if not click.confirm("WARNING: Updates to ssl-configuration will replace any existing values. Are you sure you want to continue?"): + if connection_configuration or ssl_configuration: + if not click.confirm("WARNING: Updates to connection-configuration and ssl-configuration will replace any existing values. Are you sure you want to continue?"): ctx.abort() kwargs = {} kwargs['opc_request_id'] = cli_util.use_or_generate_request_id(ctx.obj['request_id']) @@ -1660,6 +1669,9 @@ def update_listener(ctx, from_json, force, wait_for_state, max_wait_seconds, wai details['port'] = port details['protocol'] = protocol + if connection_configuration is not None: + details['connectionConfiguration'] = cli_util.parse_json_parameter("connection_configuration", connection_configuration) + if ssl_configuration is not None: details['sslConfiguration'] = cli_util.parse_json_parameter("ssl_configuration", ssl_configuration) diff --git a/src/oci_cli/generated/objectstorage_cli.py b/src/oci_cli/generated/objectstorage_cli.py index 15d4d67a8..623468ba0 100644 --- a/src/oci_cli/generated/objectstorage_cli.py +++ b/src/oci_cli/generated/objectstorage_cli.py @@ -145,7 +145,7 @@ def commit_multipart_upload(ctx, from_json, namespace_name, bucket_name, object_ @click.option('--name', callback=cli_util.handle_required_param, help="""The name of the bucket. Valid characters are uppercase or lowercase letters, numbers, and dashes. Bucket names must be unique within the namespace. Avoid entering confidential information. example: Example: my-new-bucket1 [required]""") @click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The ID of the compartment in which to create the bucket. [required]""") @click.option('--metadata', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Arbitrary string, up to 4KB, of keys and values for user-defined metadata.""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--public-access-type', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["NoPublicAccess", "ObjectRead"]), help="""The type of public access enabled on this bucket. A bucket is set to `NoPublicAccess` by default, which only allows an authenticated caller to access the bucket and its contents. When `ObjectRead` is enabled on the bucket, public access is allowed for the `GetObject`, `HeadObject`, and `ListObjects` operations.""") +@click.option('--public-access-type', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["NoPublicAccess", "ObjectRead", "ObjectReadWithoutList"]), help="""The type of public access enabled on this bucket. A bucket is set to `NoPublicAccess` by default, which only allows an authenticated caller to access the bucket and its contents. When `ObjectRead` is enabled on the bucket, public access is allowed for the `GetObject`, `HeadObject`, and `ListObjects` operations. When `ObjectReadWithoutList` is enabled on the bucket, public access is allowed for the `GetObject` and `HeadObject` operations.""") @click.option('--storage-tier', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["Standard", "Archive"]), help="""The type of storage tier of this bucket. A bucket is set to 'Standard' tier by default, which means the bucket will be put in the standard storage tier. When 'Archive' tier type is set explicitly, the bucket is put in the Archive Storage tier. The 'storageTier' property is immutable after bucket is created.""") @json_skeleton_utils.get_cli_json_input_option({'metadata': {'module': 'object_storage', 'class': 'dict(str, string)'}}) @cli_util.help_option @@ -404,7 +404,7 @@ def get_bucket(ctx, from_json, namespace_name, bucket_name, if_match, if_none_ma cli_util.render_response(result, ctx) -@namespace_group.command(name=cli_util.override('get_namespace.command_name', 'get'), help="""Gets the name of the namespace for the user making the request. An account name must be unique, must start with a letter, and can have up to 15 lowercase letters and numbers. You cannot use spaces or special characters.""") +@namespace_group.command(name=cli_util.override('get_namespace.command_name', 'get'), help="""Namespaces are unique. Namespaces are either the tenancy name or a random string automatically generated during account creation. You cannot edit a namespace.""") @json_skeleton_utils.get_cli_json_input_option({}) @cli_util.help_option @click.pass_context @@ -585,7 +585,7 @@ def head_object(ctx, from_json, namespace_name, bucket_name, object_name, if_mat To use this and other API operations, you must be authorized in an IAM policy. If you're not authorized, talk to an administrator. If you're an administrator who needs to write policies to give users access, see [Getting Started with Policies].""") @click.option('--namespace-name', callback=cli_util.handle_required_param, help="""The top-level namespace used for the request. [required]""") -@click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The ID of the compartment in which to create the bucket. [required]""") +@click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The ID of the compartment in which to list buckets. [required]""") @click.option('--limit', callback=cli_util.handle_optional_param, type=click.INT, help="""The maximum number of items to return.""") @click.option('--page', callback=cli_util.handle_optional_param, help="""The page at which to start retrieving results.""") @click.option('--all', 'all_pages', is_flag=True, callback=cli_util.handle_optional_param, help="""Fetches all pages of results. If you provide this option, then you cannot provide the --limit option.""") @@ -771,7 +771,7 @@ def list_multipart_uploads(ctx, from_json, all_pages, page_size, namespace_name, @click.option('--start', callback=cli_util.handle_optional_param, help="""Object names returned by a list query must be greater or equal to this parameter.""") @click.option('--end', callback=cli_util.handle_optional_param, help="""Object names returned by a list query must be strictly less than this parameter.""") @click.option('--limit', callback=cli_util.handle_optional_param, type=click.INT, help="""The maximum number of items to return.""") -@click.option('--delimiter', callback=cli_util.handle_optional_param, help="""When this parameter is set, only objects whose names do not contain the delimiter character (after an optionally specified prefix) are returned. Scanned objects whose names contain the delimiter have part of their name up to the last occurrence of the delimiter (after the optional prefix) returned as a set of prefixes. Note that only '/' is a supported delimiter character at this time.""") +@click.option('--delimiter', callback=cli_util.handle_optional_param, help="""When this parameter is set, only objects whose names do not contain the delimiter character (after an optionally specified prefix) are returned in the objects key of the response body. Scanned objects whose names contain the delimiter have the part of their name up to the first occurrence of the delimiter (including the optional prefix) returned as a set of prefixes. Note that only '/' is a supported delimiter character at this time.""") @click.option('--fields', callback=cli_util.handle_optional_param, help="""Object summary in list of objects includes the 'name' field. This parameter can also include 'size' (object size in bytes), 'md5', and 'timeCreated' (object creation date and time) fields. Value of this parameter should be a comma-separated, case-insensitive list of those field names. For example 'name,timeCreated,md5'. Allowed values are: name, size, timeCreated, md5""") @json_skeleton_utils.get_cli_json_input_option({}) @cli_util.help_option @@ -974,7 +974,7 @@ def rename_object(ctx, from_json, namespace_name, bucket_name, source_name, new_ cli_util.render_response(result, ctx) -@bucket_group.command(name=cli_util.override('restore_objects.command_name', 'restore-objects'), help="""Restore one or more objects specified by objectName parameter.""") +@object_group.command(name=cli_util.override('restore_objects.command_name', 'restore'), help="""Restore one or more objects specified by objectName parameter.""") @click.option('--namespace-name', callback=cli_util.handle_required_param, help="""The top-level namespace used for the request. [required]""") @click.option('--bucket-name', callback=cli_util.handle_required_param, help="""The name of the bucket. Avoid entering confidential information. Example: `my-new-bucket1` [required]""") @click.option('--object-name', callback=cli_util.handle_required_param, help="""A object which was in an archived state and need to be restored. [required]""") @@ -1011,7 +1011,7 @@ def restore_objects(ctx, from_json, namespace_name, bucket_name, object_name): @click.option('--bucket-name', callback=cli_util.handle_required_param, help="""The name of the bucket. Avoid entering confidential information. Example: `my-new-bucket1` [required]""") @click.option('--compartment-id', callback=cli_util.handle_optional_param, help="""The compartmentId for the compartment to which the bucket is targeted to move to.""") @click.option('--metadata', callback=cli_util.handle_optional_param, type=custom_types.CLI_COMPLEX_TYPE, help="""Arbitrary string, up to 4KB, of keys and values for user-defined metadata.""" + custom_types.cli_complex_type.COMPLEX_TYPE_HELP) -@click.option('--public-access-type', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["NoPublicAccess", "ObjectRead"]), help="""The type of public access enabled on this bucket. A bucket is set to `NoPublicAccess` by default, which only allows an authenticated caller to access the bucket and its contents. When `ObjectRead` is enabled on the bucket, public access is allowed for the `GetObject`, `HeadObject`, and `ListObjects` operations.""") +@click.option('--public-access-type', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["NoPublicAccess", "ObjectRead", "ObjectReadWithoutList"]), help="""The type of public access enabled on this bucket. A bucket is set to `NoPublicAccess` by default, which only allows an authenticated caller to access the bucket and its contents. When `ObjectRead` is enabled on the bucket, public access is allowed for the `GetObject`, `HeadObject`, and `ListObjects` operations. When `ObjectReadWithoutList` is enabled on the bucket, public access is allowed for the `GetObject` and `HeadObject` operations.""") @click.option('--if-match', callback=cli_util.handle_optional_param, help="""The entity tag to match. For creating and committing a multipart upload to an object, this is the entity tag of the target object. For uploading a part, this is the entity tag of the target part.""") @json_skeleton_utils.get_cli_json_input_option({'metadata': {'module': 'object_storage', 'class': 'dict(str, string)'}}) @cli_util.help_option diff --git a/src/oci_cli/identity_cli_extended.py b/src/oci_cli/identity_cli_extended.py index a8d09ea0d..47252b7e9 100644 --- a/src/oci_cli/identity_cli_extended.py +++ b/src/oci_cli/identity_cli_extended.py @@ -14,6 +14,7 @@ identity_cli.identity_group.add_command(identity_cli.availability_domain_group) identity_cli.identity_group.add_command(identity_cli.compartment_group) identity_cli.identity_group.add_command(identity_cli.customer_secret_key_group) +identity_cli.identity_group.add_command(identity_cli.dynamic_group_group) identity_cli.identity_group.add_command(identity_cli.group_group) identity_cli.identity_group.add_command(identity_cli.policy_group) identity_cli.identity_group.add_command(identity_cli.region_group) diff --git a/src/oci_cli/lb_cli_extended.py b/src/oci_cli/lb_cli_extended.py index a81dae923..f9db8132b 100644 --- a/src/oci_cli/lb_cli_extended.py +++ b/src/oci_cli/lb_cli_extended.py @@ -41,6 +41,14 @@ def process_ssl_configuration_kwargs(kwargs): kwargs.pop('ssl_verify_peer_certificate') +def process_connection_configuration_kwargs(kwargs): + if 'connection_configuration_idle_timeout' in kwargs and kwargs['connection_configuration_idle_timeout'] is not None: + connection_configuration = {'idleTimeout': kwargs['connection_configuration_idle_timeout']} + kwargs['connection_configuration'] = json.dumps(connection_configuration) + + kwargs.pop('connection_configuration_idle_timeout', None) + + def process_session_persistence_configuration_kwargs(kwargs): session_persistence_configuration = {} if kwargs['session_persistence_cookie_name'] is not None: @@ -192,11 +200,12 @@ def update_backend_set(ctx, **kwargs): ctx.invoke(loadbalancer_cli.update_backend_set, **kwargs) -@cli_util.copy_params_from_generated_command(loadbalancer_cli.create_listener, params_to_exclude=['ssl_configuration']) +@cli_util.copy_params_from_generated_command(loadbalancer_cli.create_listener, params_to_exclude=['ssl_configuration', 'connection_configuration']) @loadbalancer_cli.listener_group.command(name='create', help="""Adds a listener to a load balancer.""") @click.option('--ssl-certificate-name', type=click.STRING, callback=cli_util.handle_optional_param, help="""A friendly name for the certificate bundle. It must be unique and it cannot be changed. Valid certificate bundle names include only alphanumeric characters, dashes, and underscores. Certificate bundle names cannot contain spaces. Avoid entering confidential information.""") @click.option('--ssl-verify-depth', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum depth for peer certificate chain verification.""") @click.option('--ssl-verify-peer-certificate', type=click.BOOL, callback=cli_util.handle_optional_param, help="""Whether the load balancer listener should verify peer certificates.""") +@click.option('--connection-configuration-idle-timeout', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum idle time, in seconds, allowed between two successive receive or two successive send operations between the client and backend servers.""") @json_skeleton_utils.get_cli_json_input_option({'ssl-configuration': {'module': 'load_balancer', 'class': 'SSLConfigurationDetails'}}) @cli_util.help_option @click.pass_context @@ -204,15 +213,17 @@ def update_backend_set(ctx, **kwargs): @cli_util.wrap_exceptions def create_listener(ctx, **kwargs): process_ssl_configuration_kwargs(kwargs) + process_connection_configuration_kwargs(kwargs) ctx.invoke(loadbalancer_cli.create_listener, **kwargs) -@cli_util.copy_params_from_generated_command(loadbalancer_cli.update_listener, params_to_exclude=['ssl_configuration']) +@cli_util.copy_params_from_generated_command(loadbalancer_cli.update_listener, params_to_exclude=['ssl_configuration', 'connection_configuration']) @loadbalancer_cli.listener_group.command(name='update', help="""Updates a listener for a given load balancer.""") @click.option('--ssl-certificate-name', type=click.STRING, callback=cli_util.handle_optional_param, help="""A friendly name for the certificate bundle. It must be unique and it cannot be changed. Valid certificate bundle names include only alphanumeric characters, dashes, and underscores. Certificate bundle names cannot contain spaces. Avoid entering confidential information.""") @click.option('--ssl-verify-depth', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum depth for peer certificate chain verification.""") @click.option('--ssl-verify-peer-certificate', type=click.BOOL, callback=cli_util.handle_optional_param, help="""Whether the load balancer listener should verify peer certificates.""") +@click.option('--connection-configuration-idle-timeout', type=click.INT, callback=cli_util.handle_optional_param, help="""The maximum idle time, in seconds, allowed between two successive receive or two successive send operations between the client and backend servers.""") @json_skeleton_utils.get_cli_json_input_option({'ssl-configuration': {'module': 'load_balancer', 'class': 'SSLConfigurationDetails'}}) @cli_util.help_option @click.pass_context @@ -220,5 +231,6 @@ def create_listener(ctx, **kwargs): @cli_util.wrap_exceptions def update_listener(ctx, **kwargs): process_ssl_configuration_kwargs(kwargs) + process_connection_configuration_kwargs(kwargs) ctx.invoke(loadbalancer_cli.update_listener, **kwargs) diff --git a/src/oci_cli/object_storage_transfer_manager/get_object_tasks.py b/src/oci_cli/object_storage_transfer_manager/get_object_tasks.py index 293c177b0..2355b07c4 100644 --- a/src/oci_cli/object_storage_transfer_manager/get_object_tasks.py +++ b/src/oci_cli/object_storage_transfer_manager/get_object_tasks.py @@ -187,6 +187,9 @@ def do_work_hook(self): # This blocks until something makes the PendingWrites object releases the lock self.all_done_lock.acquire() + # Make sure that the IO pool has no pending tasks (it probably shouldn't because we've released the lock by this point) + self.io_writer_pool.wait_for_completion() + self._handle_errors(errors) if self.auto_close_destination_file: @@ -293,6 +296,7 @@ def do_work_hook(self): if self.part_completed_callback: self.part_completed_callback(part[1].tell() / 2) part[1].close() + self.pending_writes.increment_written_parts() if self.part_completed_callback: self.part_completed_callback(total_size) @@ -308,8 +312,10 @@ class PendingWrites(object): def __init__(self, all_done_lock): self.pending = [] self.next_part = 0 # Always start at zero + self._written_parts = 0 self._total_parts = 0 self.all_done_lock = all_done_lock + self.written_parts_lock = threading.Lock() self.all_done_lock.acquire() @@ -321,6 +327,10 @@ def total_parts(self): def total_parts(self, value): self._total_parts = value + def increment_written_parts(self): + with self.written_parts_lock: + self._written_parts += 1 + def process_pending_write(self, tuple_counter, data): heapq.heappush(self.pending, (tuple_counter, data)) @@ -332,7 +342,7 @@ def process_pending_write(self, tuple_counter, data): return able_to_write def check_if_all_parts_written(self): - if not self.pending and self.next_part == self._total_parts: + if not self.pending and self._written_parts == self._total_parts: self.all_done_lock.release() def release_lock_on_error(self, **kwargs): diff --git a/src/oci_cli/objectstorage_cli_extended.py b/src/oci_cli/objectstorage_cli_extended.py index cdad63fa4..563d92d46 100644 --- a/src/oci_cli/objectstorage_cli_extended.py +++ b/src/oci_cli/objectstorage_cli_extended.py @@ -47,12 +47,13 @@ [!sequence]: Matches any character not in sequence """ +objectstorage_cli.get_namespace.short_help = 'Gets the name of the namespace for the user' + objectstorage_cli.os_group.add_command(objectstorage_cli.namespace_group) get_param(objectstorage_cli.get_namespace_metadata, 'namespace_name').opts.extend(['--namespace', '-ns']) get_param(objectstorage_cli.update_namespace_metadata, 'namespace_name').opts.extend(['--namespace', '-ns']) objectstorage_cli.os_group.add_command(objectstorage_cli.bucket_group) -objectstorage_cli.bucket_group.commands.pop(objectstorage_cli.restore_objects.name) objectstorage_cli.bucket_group.commands.pop(objectstorage_cli.head_bucket.name) get_param(objectstorage_cli.create_bucket, 'namespace_name').opts.extend(['--namespace', '-ns']) get_param(objectstorage_cli.delete_bucket, 'bucket_name').opts.extend(['--name']) @@ -73,6 +74,7 @@ objectstorage_cli.object_group.commands.pop(objectstorage_cli.list_multipart_upload_parts.name) objectstorage_cli.object_group.commands.pop(objectstorage_cli.list_multipart_uploads.name) objectstorage_cli.object_group.commands.pop(objectstorage_cli.put_object.name) +objectstorage_cli.object_group.commands.pop(objectstorage_cli.restore_objects.name) objectstorage_cli.object_group.commands.pop(objectstorage_cli.upload_part.name) get_param(objectstorage_cli.delete_object, 'bucket_name').opts.extend(['-bn']) get_param(objectstorage_cli.delete_object, 'namespace_name').opts.extend(['--namespace', '-ns']) diff --git a/src/oci_cli/retry_utils.py b/src/oci_cli/retry_utils.py index 5ec35d14e..797947921 100644 --- a/src/oci_cli/retry_utils.py +++ b/src/oci_cli/retry_utils.py @@ -3,7 +3,7 @@ from requests.exceptions import Timeout from requests.exceptions import ConnectionError - +import datetime import retrying @@ -85,13 +85,13 @@ def list_call_get_all_results(list_func_ref, retry_strategy_name, **func_kwargs) if 'sort_order' in func_kwargs: sort_direction = func_kwargs['sort_order'].upper() - post_processed_results = sorted(aggregated_results, key=lambda r: getattr(r, 'display_name'), reverse=(sort_direction == 'DESC')) + post_processed_results = sorted(aggregated_results, key=lambda r: retrieve_attribute_for_sort(r, 'display_name'), reverse=(sort_direction == 'DESC')) elif func_kwargs['sort_by'].upper() == 'TIMECREATED': sort_direction = 'DESC' if 'sort_order' in func_kwargs: sort_direction = func_kwargs['sort_order'].upper() - post_processed_results = sorted(aggregated_results, key=lambda r: getattr(r, 'time_created'), reverse=(sort_direction == 'DESC')) + post_processed_results = sorted(aggregated_results, key=lambda r: retrieve_attribute_for_sort(r, 'time_created'), reverse=(sort_direction == 'DESC')) # Most of this is just dummy since we're discarding the intermediate requests final_response = Response(call_result.status, call_result.headers, post_processed_results, call_result.request) @@ -99,6 +99,22 @@ def list_call_get_all_results(list_func_ref, retry_strategy_name, **func_kwargs) return final_response +# Retrieves an attribute and returns a default value if it doesn't exist. This default be specified as a keyword argument, but if none is given +# then the method can vend a default value (the min datetime for the time_created field and an empty string otherwise) +def retrieve_attribute_for_sort(target_obj, attribute_name, **kwargs): + getattr_result = getattr(target_obj, attribute_name) + if getattr_result is not None: + return getattr_result + + if 'default' in kwargs: + return kwargs['default'] + + if attribute_name == 'time_created': + return datetime.datetime.min + else: + return '' + + def call_funtion_with_default_retries(func_ref, *func_args, **func_kwargs): return call_function_with_retries(func_ref, DEFAULT_RETRY_STRATEGY_NAME, *func_args, **func_kwargs) diff --git a/src/oci_cli/version.py b/src/oci_cli/version.py index 2abcecc06..adb4c0da9 100644 --- a/src/oci_cli/version.py +++ b/src/oci_cli/version.py @@ -1,4 +1,4 @@ # coding: utf-8 # Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved. -__version__ = '2.4.14' +__version__ = '2.4.15' diff --git a/tests/conftest.py b/tests/conftest.py index 321355e36..93353f69e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ from cryptography.hazmat.primitives import hashes from oci_cli import cli_util from . import tag_data_container +from . import test_config_container import datetime import oci @@ -23,6 +24,11 @@ def pytest_addoption(parser): parser.addoption("--fast", action="store_true", default=False, help="Skip slow tests, as marked with the @slow annotation.") parser.addoption("--enable-long-running", action="store_true", default=False, help="Enables tests marked with the @enable_long_running annotation") parser.addoption("--config-profile", action="store", help="profile to use from the config file", default=oci.config.DEFAULT_PROFILE) + parser.addoption("--vcr-record-mode", action="store", default='once', help="Record mode option for VCRpy library.") + + +def pytest_configure(config): + test_config_container.vcr_mode = config.getoption("--vcr-record-mode") @pytest.fixture(scope='session') @@ -164,155 +170,160 @@ def key_pair_files(): def vcn_and_subnets(network_client): from . import util - # create VCN - vcn_name = util.random_name('cli_lb_test_vcn') - cidr_block = "10.0.0.0/16" - vcn_dns_label = util.random_name('vcn', insert_underscore=False) - - create_vcn_details = oci.core.models.CreateVcnDetails() - create_vcn_details.cidr_block = cidr_block - create_vcn_details.display_name = vcn_name - create_vcn_details.compartment_id = os.environ['OCI_CLI_COMPARTMENT_ID'] - create_vcn_details.dns_label = vcn_dns_label - - result = network_client.create_vcn(create_vcn_details) - vcn_ocid = result.data.id - assert result.status == 200 - - oci.wait_until(network_client, network_client.get_vcn(vcn_ocid), 'lifecycle_state', 'AVAILABLE', max_wait_seconds=300) - - # create subnet in first AD - subnet_name = util.random_name('cli_lb_test_subnet') - cidr_block = "10.0.1.0/24" - subnet_dns_label = util.random_name('subnet', insert_underscore=False) - - create_subnet_details = oci.core.models.CreateSubnetDetails() - create_subnet_details.compartment_id = os.environ['OCI_CLI_COMPARTMENT_ID'] - create_subnet_details.availability_domain = util.availability_domain() - create_subnet_details.display_name = subnet_name - create_subnet_details.vcn_id = vcn_ocid - create_subnet_details.cidr_block = cidr_block - create_subnet_details.dns_label = subnet_dns_label - - result = network_client.create_subnet(create_subnet_details) - subnet_ocid_1 = result.data.id - assert result.status == 200 - - oci.wait_until(network_client, network_client.get_subnet(subnet_ocid_1), 'lifecycle_state', 'AVAILABLE', max_wait_seconds=300) - - # create subnet in second AD - subnet_name = util.random_name('cli_lb_test_subnet') - cidr_block = "10.0.0.0/24" - subnet_dns_label = util.random_name('subnet', insert_underscore=False) - - create_subnet_details = oci.core.models.CreateSubnetDetails() - create_subnet_details.compartment_id = os.environ['OCI_CLI_COMPARTMENT_ID'] - create_subnet_details.availability_domain = util.second_availability_domain() - create_subnet_details.display_name = subnet_name - create_subnet_details.vcn_id = vcn_ocid - create_subnet_details.cidr_block = cidr_block - create_subnet_details.dns_label = subnet_dns_label - - result = network_client.create_subnet(create_subnet_details) - subnet_ocid_2 = result.data.id - assert result.status == 200 - - oci.wait_until(network_client, network_client.get_subnet(subnet_ocid_2), 'lifecycle_state', 'AVAILABLE', max_wait_seconds=300) + with test_config_container.create_vcr().use_cassette('_conftest_fixture_vcn_and_subnets.yml'): + # create VCN + vcn_name = util.random_name('cli_lb_test_vcn') + cidr_block = "10.0.0.0/16" + vcn_dns_label = util.random_name('vcn', insert_underscore=False) + + create_vcn_details = oci.core.models.CreateVcnDetails() + create_vcn_details.cidr_block = cidr_block + create_vcn_details.display_name = vcn_name + create_vcn_details.compartment_id = os.environ['OCI_CLI_COMPARTMENT_ID'] + create_vcn_details.dns_label = vcn_dns_label + + result = network_client.create_vcn(create_vcn_details) + vcn_ocid = result.data.id + assert result.status == 200 + + oci.wait_until(network_client, network_client.get_vcn(vcn_ocid), 'lifecycle_state', 'AVAILABLE', max_wait_seconds=300) + + # create subnet in first AD + subnet_name = util.random_name('cli_lb_test_subnet') + cidr_block = "10.0.1.0/24" + subnet_dns_label = util.random_name('subnet', insert_underscore=False) + + create_subnet_details = oci.core.models.CreateSubnetDetails() + create_subnet_details.compartment_id = os.environ['OCI_CLI_COMPARTMENT_ID'] + create_subnet_details.availability_domain = util.availability_domain() + create_subnet_details.display_name = subnet_name + create_subnet_details.vcn_id = vcn_ocid + create_subnet_details.cidr_block = cidr_block + create_subnet_details.dns_label = subnet_dns_label + + result = network_client.create_subnet(create_subnet_details) + subnet_ocid_1 = result.data.id + assert result.status == 200 + + oci.wait_until(network_client, network_client.get_subnet(subnet_ocid_1), 'lifecycle_state', 'AVAILABLE', max_wait_seconds=300) + + # create subnet in second AD + subnet_name = util.random_name('cli_lb_test_subnet') + cidr_block = "10.0.0.0/24" + subnet_dns_label = util.random_name('subnet2', insert_underscore=False) + + create_subnet_details = oci.core.models.CreateSubnetDetails() + create_subnet_details.compartment_id = os.environ['OCI_CLI_COMPARTMENT_ID'] + create_subnet_details.availability_domain = util.second_availability_domain() + create_subnet_details.display_name = subnet_name + create_subnet_details.vcn_id = vcn_ocid + create_subnet_details.cidr_block = cidr_block + create_subnet_details.dns_label = subnet_dns_label + + result = network_client.create_subnet(create_subnet_details) + subnet_ocid_2 = result.data.id + assert result.status == 200 + + oci.wait_until(network_client, network_client.get_subnet(subnet_ocid_2), 'lifecycle_state', 'AVAILABLE', max_wait_seconds=300) yield [vcn_ocid, subnet_ocid_1, subnet_ocid_2] - # delete VCN and subnets - network_client.delete_subnet(subnet_ocid_1) + # For some reason VCR doesn't like that the post-yield stuff here is all in one cassette. Splitting into different cassettes seems to work + with test_config_container.create_vcr().use_cassette('_conftest_fixture_vcn_and_subnets_delete.yml'): + # delete VCN and subnets + network_client.delete_subnet(subnet_ocid_1) - try: - oci.wait_until(network_client, network_client.get_subnet(subnet_ocid_1), 'lifecycle_state', 'TERMINATED', max_wait_seconds=600) - except oci.exceptions.ServiceError as error: - if not hasattr(error, 'status') or error.status != 404: - util.print_latest_exception(error) + try: + oci.wait_until(network_client, network_client.get_subnet(subnet_ocid_1), 'lifecycle_state', 'TERMINATED', max_wait_seconds=600) + except oci.exceptions.ServiceError as error: + if not hasattr(error, 'status') or error.status != 404: + util.print_latest_exception(error) - network_client.delete_subnet(subnet_ocid_2) + network_client.delete_subnet(subnet_ocid_2) - try: - oci.wait_until(network_client, network_client.get_subnet(subnet_ocid_2), 'lifecycle_state', 'TERMINATED', max_wait_seconds=600) - except oci.exceptions.ServiceError as error: - if not hasattr(error, 'status') or error.status != 404: - util.print_latest_exception(error) + try: + oci.wait_until(network_client, network_client.get_subnet(subnet_ocid_2), 'lifecycle_state', 'TERMINATED', max_wait_seconds=600) + except oci.exceptions.ServiceError as error: + if not hasattr(error, 'status') or error.status != 404: + util.print_latest_exception(error) - network_client.delete_vcn(vcn_ocid) + network_client.delete_vcn(vcn_ocid) @pytest.fixture(scope='session') def tag_namespace_and_tags(identity_client, test_id): - if not os.environ.get('OCI_CLI_TAG_NAMESPACE_ID'): - tag_namespace_name = 'cli_tag_ns_{}'.format(test_id) - create_tag_namespace_response = identity_client.create_tag_namespace( - oci.identity.models.CreateTagNamespaceDetails( - compartment_id=os.environ['OCI_CLI_COMPARTMENT_ID'], - name=tag_namespace_name, - description='Python SDK integ test namespace' + with test_config_container.create_vcr().use_cassette('_conftest_fixture_tag_namespace_and_tags.yml'): + if not os.environ.get('OCI_CLI_TAG_NAMESPACE_ID'): + tag_namespace_name = 'cli_tag_ns_{}'.format(test_id) + create_tag_namespace_response = identity_client.create_tag_namespace( + oci.identity.models.CreateTagNamespaceDetails( + compartment_id=os.environ['OCI_CLI_COMPARTMENT_ID'], + name=tag_namespace_name, + description='Python SDK integ test namespace' + ) ) - ) - tag_namespace = create_tag_namespace_response.data - else: - print('Reusing tag namespace: {}'.format(os.environ.get('OCI_CLI_TAG_NAMESPACE_ID'))) - get_tag_namespace_response = identity_client.get_tag_namespace(os.environ.get('OCI_CLI_TAG_NAMESPACE_ID')) - - if get_tag_namespace_response.data.is_retired: - update_tag_namespace_response = identity_client.update_tag_namespace( - get_tag_namespace_response.data.id, - oci.identity.models.UpdateTagNamespaceDetails(is_retired=False) + tag_namespace = create_tag_namespace_response.data + else: + print('Reusing tag namespace: {}'.format(os.environ.get('OCI_CLI_TAG_NAMESPACE_ID'))) + get_tag_namespace_response = identity_client.get_tag_namespace(os.environ.get('OCI_CLI_TAG_NAMESPACE_ID')) + + if get_tag_namespace_response.data.is_retired: + update_tag_namespace_response = identity_client.update_tag_namespace( + get_tag_namespace_response.data.id, + oci.identity.models.UpdateTagNamespaceDetails(is_retired=False) + ) + tag_namespace = update_tag_namespace_response.data + else: + tag_namespace = get_tag_namespace_response.data + + tags = [] + if not os.environ.get('OCI_CLI_TAG_ONE_NAME'): + tag_one_name = 'cli_tag_{}'.format(test_id) + create_tag_response = identity_client.create_tag( + tag_namespace.id, + oci.identity.models.CreateTagDetails(name=tag_one_name, description='CLI integration test tag') ) - tag_namespace = update_tag_namespace_response.data + tags.append(create_tag_response.data) else: - tag_namespace = get_tag_namespace_response.data - - tags = [] - if not os.environ.get('OCI_CLI_TAG_ONE_NAME'): - tag_one_name = 'cli_tag_{}'.format(test_id) - create_tag_response = identity_client.create_tag( - tag_namespace.id, - oci.identity.models.CreateTagDetails(name=tag_one_name, description='CLI integration test tag') - ) - tags.append(create_tag_response.data) - else: - print('Reusing tag: {}'.format(os.environ.get('OCI_CLI_TAG_ONE_NAME'))) - tags.append(get_and_reactivate_tag(identity_client, tag_namespace, os.environ.get('OCI_CLI_TAG_ONE_NAME'))) - - if not os.environ.get('OCI_CLI_TAG_TWO_NAME'): - tag_two_name = 'cli_tag2_{}'.format(test_id) - create_tag_response = identity_client.create_tag( - tag_namespace.id, - oci.identity.models.CreateTagDetails(name=tag_two_name, description='CLI integration test tag') - ) - tags.append(create_tag_response.data) - else: - print('Reusing tag: {}'.format(os.environ.get('OCI_CLI_TAG_TWO_NAME'))) - tags.append(get_and_reactivate_tag(identity_client, tag_namespace, os.environ.get('OCI_CLI_TAG_TWO_NAME'))) + print('Reusing tag: {}'.format(os.environ.get('OCI_CLI_TAG_ONE_NAME'))) + tags.append(get_and_reactivate_tag(identity_client, tag_namespace, os.environ.get('OCI_CLI_TAG_ONE_NAME'))) + + if not os.environ.get('OCI_CLI_TAG_TWO_NAME'): + tag_two_name = 'cli_tag2_{}'.format(test_id) + create_tag_response = identity_client.create_tag( + tag_namespace.id, + oci.identity.models.CreateTagDetails(name=tag_two_name, description='CLI integration test tag') + ) + tags.append(create_tag_response.data) + else: + print('Reusing tag: {}'.format(os.environ.get('OCI_CLI_TAG_TWO_NAME'))) + tags.append(get_and_reactivate_tag(identity_client, tag_namespace, os.environ.get('OCI_CLI_TAG_TWO_NAME'))) - # Avoid eventual consistency issue where we try and use tags we just created and get a 404 (though it - # would succeed on retry) - time.sleep(10) + # Avoid eventual consistency issue where we try and use tags we just created and get a 404 (though it + # would succeed on retry) + time.sleep(10) - tag_data_container.tag_namespace = tag_namespace - tag_data_container.tags = tags + tag_data_container.tag_namespace = tag_namespace + tag_data_container.tags = tags yield {'namespace': tag_namespace, 'tags': tags} - # Only retire if we're not reusing a namespace or tags, otherwise on parallel runs we can get conflicts since one run retires a tag that - # another run expects to not be retired - if not os.environ.get('OCI_CLI_TAG_NAMESPACE_ID') and not os.environ.get('OCI_CLI_TAG_ONE_NAME') and not os.environ.get('OCI_CLI_TAG_TWO_NAME'): - for tag in tags: - identity_client.update_tag( - tag.tag_namespace_id, - tag.name, - oci.identity.models.UpdateTagDetails(is_retired=True) - ) + with test_config_container.create_vcr().use_cassette('_conftest_fixture_tag_namespace_and_tags_retire.yml'): + # Only retire if we're not reusing a namespace or tags, otherwise on parallel runs we can get conflicts since one run retires a tag that + # another run expects to not be retired + if not os.environ.get('OCI_CLI_TAG_NAMESPACE_ID') and not os.environ.get('OCI_CLI_TAG_ONE_NAME') and not os.environ.get('OCI_CLI_TAG_TWO_NAME'): + for tag in tags: + identity_client.update_tag( + tag.tag_namespace_id, + tag.name, + oci.identity.models.UpdateTagDetails(is_retired=True) + ) - update_tag_namespace_response = identity_client.update_tag_namespace( - tag_namespace.id, - oci.identity.models.UpdateTagNamespaceDetails(is_retired=True) - ) + update_tag_namespace_response = identity_client.update_tag_namespace( + tag_namespace.id, + oci.identity.models.UpdateTagNamespaceDetails(is_retired=True) + ) def get_and_reactivate_tag(identity_client, tag_namespace, tag_name): diff --git a/tests/output/inline_help_dump.txt b/tests/output/inline_help_dump.txt index a1e6241e3..76245887f 100644 --- a/tests/output/inline_help_dump.txt +++ b/tests/output/inline_help_dump.txt @@ -1,4 +1,4 @@ -This file contains all the help for every possible command in version 2.4.14 of the CLI. +This file contains all the help for every possible command in version 2.4.15 of the CLI. This file is generated by running test_help.py, which dumps the output of --help for every command. @@ -68,6 +68,11 @@ Options: --raw-output If the output of a given query is a single string value, this will return the string without surrounding quotes + --auth [api_key|instance_principal] + The type of auth to use for the API request. + By default the API key in your config file + will be used. This value can also be provided + in the OCI_CLI_AUTH environment variable. --generate-full-command-json-input Prints out a JSON document which represents all possible options that can be provided to @@ -5235,12 +5240,13 @@ Commands: availability-domain One or more isolated, fault-tolerant Oracle... compartment A collection of related resources. customer-secret-key A `CustomerSecretKey` is an Oracle-provided... + dynamic-group An dynamic group defines a matching rule. group A collection of users who all need the same... policy A document that specifies the type of access... region A localized geographic area, such as Phoenix,... region-subscription An object that represents your tenancy's... tag A tag definition that belongs to a specific... - tag-namespace A bag of tags that is attached to a... + tag-namespace A managed container for defined tags. user An individual employee or system that needs... ++++++++++++++++++++++++++++++++++++++++++++++ @@ -5346,14 +5352,33 @@ Options: --description TEXT The description you assign to the compartment during creation. Does not have to be unique, and it's changeable. [required] - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar- - key": "value"}` - This is a complex type whose - value must be valid JSON. The value can be - provided as a string on the command line or - passed in as a file using + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is + a simple key-value pair with no predefined + name, type, or namespace. For more + information, see [Resource Tags]. Example: + `{"Department": "Finance"}` + This is a complex + type whose value must be valid JSON. The value + can be provided as a string on the command + line or passed in as a file using + the + file://path/to/file syntax. + + The --generate- + param-json-input option can be used to + generate an example of the JSON which must be + provided. We recommend storing this example + in + a file, modifying it as needed and then + passing it back in via the file:// syntax. + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` + This is + a complex type whose value must be valid JSON. + The value can be provided as a string on the + command line or passed in as a file using the file://path/to/file syntax. @@ -5364,21 +5389,6 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: - `{"foo-namespace": {"bar-key": "foo-value"}}` - This is a complex type whose value must be - valid JSON. The value can be provided as a - string on the command line or passed in as a - file using - the file://path/to/file syntax. - The --generate-param-json-input option can be - used to generate an example of the JSON which - must be provided. We recommend storing this - example - in a file, modifying it as needed and - then passing it back in via the file:// - syntax. --wait-for-state [CREATING|ACTIVE|INACTIVE|DELETING|DELETED] This operation creates, modifies or deletes a resource that has a defined lifecycle state. @@ -5473,14 +5483,33 @@ Options: --name TEXT The new name you assign to the compartment. The name must be unique across all compartments in the tenancy. - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar- - key": "value"}` - This is a complex type whose - value must be valid JSON. The value can be - provided as a string on the command line or - passed in as a file using + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is + a simple key-value pair with no predefined + name, type, or namespace. For more + information, see [Resource Tags]. Example: + `{"Department": "Finance"}` + This is a complex + type whose value must be valid JSON. The value + can be provided as a string on the command + line or passed in as a file using + the + file://path/to/file syntax. + + The --generate- + param-json-input option can be used to + generate an example of the JSON which must be + provided. We recommend storing this example + in + a file, modifying it as needed and then + passing it back in via the file:// syntax. + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` + This is + a complex type whose value must be valid JSON. + The value can be provided as a string on the + command line or passed in as a file using the file://path/to/file syntax. @@ -5491,21 +5520,6 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: - `{"foo-namespace": {"bar-key": "foo-value"}}` - This is a complex type whose value must be - valid JSON. The value can be provided as a - string on the command line or passed in as a - file using - the file://path/to/file syntax. - The --generate-param-json-input option can be - used to generate an example of the JSON which - must be provided. We recommend storing this - example - in a file, modifying it as needed and - then passing it back in via the file:// - syntax. --if-match TEXT For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if- match` parameter to the value of the etag from @@ -5668,6 +5682,229 @@ Options: value will be used -?, -h, --help Show this message and exit. +++++++++++++++++++++++++++++++++++++++++++++++ +$ oci iam dynamic-group --help +Usage: oci iam dynamic-group [OPTIONS] COMMAND [ARGS]... + + An dynamic group defines a matching rule. Every bare metal/vm instance is + deployed with an instance certificate. The certificate contains metadata + about the instance. It contains the instance OCID and the compartment OCID, + along with a few other optional properties. When an API call is made using + this instance certificate as the authenticator, the certificate may be + matched to one or multiple dynamic groups. Depending on policies written + against these dynamic groups, the instance will get access to that API. + + This works like regular user/group memebership. But in that case the + membership is a static relationship, whereas in dynamic group, the + membership of an instance certificate to dynamic groups are determined + during runtime. + +Options: + -?, -h, --help Show this message and exit. + +Commands: + create Creates a new dynamic group in your tenancy. + delete Deletes the specified dynamic group. + get Gets the specified dynamic group's... + list Lists the dynamic groups in your tenancy. + update Updates the specified dynamic group. + +++++++++++++++++++++++++++++++++++++++++++++++ +$ oci iam dynamic-group create --help +Usage: oci iam dynamic-group create [OPTIONS] + + Creates a new dynamic group in your tenancy. + + You must specify your tenancy's OCID as the compartment ID in the request + object (remember that the tenancy is simply the root compartment). Notice + that IAM resources (users, groups, compartments, and some policies) reside + within the tenancy itself, unlike cloud resources such as compute instances, + which typically reside within compartments inside the tenancy. For + information about OCIDs, see [Resource Identifiers]. + + You must also specify a *name* for the dynamic group, which must be unique + across all dynamic groups in your tenancy, and cannot be changed. Note that + this name has to be also unique accross all groups in your tenancy. You can + use this name or the OCID when writing policies that apply to the dynamic + group. For more information about policies, see [How Policies Work]. + + You must also specify a *description* for the dynamic group (although it can + be an empty string). It does not have to be unique, and you can change it + anytime with [UpdateDynamicGroup]. + + After you send your request, the new object's `lifecycleState` will + temporarily be CREATING. Before using the object, first make sure its + `lifecycleState` has changed to ACTIVE. + +Options: + -c, --compartment-id TEXT The OCID of the tenancy containing the group. + [required] + --name TEXT The name you assign to the group during + creation. The name must be unique across all + groups in the tenancy and cannot be changed. + [required] + --matching-rule TEXT The matching rule to dynamically match an + instance certificate to this dynamic group + [required] + --description TEXT The description you assign to the group during + creation. Does not have to be unique, and it's + changeable. [required] + --wait-for-state [CREATING|ACTIVE|INACTIVE|DELETING|DELETED] + This operation creates, modifies or deletes a + resource that has a defined lifecycle state. + Specify this option to perform the action and + then wait until the resource reaches a given + lifecycle state. + --max-wait-seconds INTEGER The maximum time to wait for the resource to + reach the lifecycle state defined by --wait- + for-state. Defaults to 1200 seconds. + --wait-interval-seconds INTEGER + Check every --wait-interval-seconds to see + whether the resource to see if it has reached + the lifecycle state defined by --wait-for- + state. Defaults to 30 seconds. + --from-json TEXT Provide input to this command as a JSON + document from a file. + + Options can still be + provided on the command line. If an option + exists in both the JSON document and the + command line then the command line specified + value will be used + -?, -h, --help Show this message and exit. + +++++++++++++++++++++++++++++++++++++++++++++++ +$ oci iam dynamic-group delete --help +Usage: oci iam dynamic-group delete [OPTIONS] + + Deletes the specified dynamic group. + +Options: + --dynamic-group-id TEXT The OCID of the dynamic group. [required] + --if-match TEXT For optimistic concurrency control. In the PUT + or DELETE call for a resource, set the `if- + match` parameter to the value of the etag from + a previous GET or POST response for that + resource. The resource will be updated or + deleted only if the etag you provide matches + the resource's current etag value. + --force Perform deletion without prompting for + confirmation. + --wait-for-state [CREATING|ACTIVE|INACTIVE|DELETING|DELETED] + This operation creates, modifies or deletes a + resource that has a defined lifecycle state. + Specify this option to perform the action and + then wait until the resource reaches a given + lifecycle state. + --max-wait-seconds INTEGER The maximum time to wait for the resource to + reach the lifecycle state defined by --wait- + for-state. Defaults to 1200 seconds. + --wait-interval-seconds INTEGER + Check every --wait-interval-seconds to see + whether the resource to see if it has reached + the lifecycle state defined by --wait-for- + state. Defaults to 30 seconds. + --from-json TEXT Provide input to this command as a JSON + document from a file. + + Options can still be + provided on the command line. If an option + exists in both the JSON document and the + command line then the command line specified + value will be used + -?, -h, --help Show this message and exit. + +++++++++++++++++++++++++++++++++++++++++++++++ +$ oci iam dynamic-group get --help +Usage: oci iam dynamic-group get [OPTIONS] + + Gets the specified dynamic group's information. + +Options: + --dynamic-group-id TEXT The OCID of the dynamic group. [required] + --from-json TEXT Provide input to this command as a JSON document from + a file. + + Options can still be provided on the command + line. If an option exists in both the JSON document + and the command line then the command line specified + value will be used + -?, -h, --help Show this message and exit. + +++++++++++++++++++++++++++++++++++++++++++++++ +$ oci iam dynamic-group list --help +Usage: oci iam dynamic-group list [OPTIONS] + + Lists the dynamic groups in your tenancy. You must specify your tenancy's + OCID as the value for the compartment ID (remember that the tenancy is + simply the root compartment). See [Where to Get the Tenancy's OCID and + User's OCID]. + +Options: + -c, --compartment-id TEXT The OCID of the compartment (remember that the + tenancy is simply the root compartment). [required] + --page TEXT The value of the `opc-next-page` response header + from the previous "List" call. + --limit INTEGER The maximum number of items to return in a + paginated "List" call. + --all Fetches all pages of results. If you provide this + option, then you cannot provide the --limit option. + --page-size INTEGER When fetching results, the number of results to + fetch per call. Only valid when used with --all or + --limit, and ignored otherwise. + --from-json TEXT Provide input to this command as a JSON document + from a file. + + Options can still be provided on the + command line. If an option exists in both the JSON + document and the command line then the command line + specified value will be used + -?, -h, --help Show this message and exit. + +++++++++++++++++++++++++++++++++++++++++++++++ +$ oci iam dynamic-group update --help +Usage: oci iam dynamic-group update [OPTIONS] + + Updates the specified dynamic group. + +Options: + --dynamic-group-id TEXT The OCID of the dynamic group. [required] + --description TEXT The description you assign to the dynamic + group. Does not have to be unique, and it's + changeable. + --matching-rule TEXT The matching rule to dynamically match an + instance certificate to this dynamic group + --if-match TEXT For optimistic concurrency control. In the PUT + or DELETE call for a resource, set the `if- + match` parameter to the value of the etag from + a previous GET or POST response for that + resource. The resource will be updated or + deleted only if the etag you provide matches + the resource's current etag value. + --wait-for-state [CREATING|ACTIVE|INACTIVE|DELETING|DELETED] + This operation creates, modifies or deletes a + resource that has a defined lifecycle state. + Specify this option to perform the action and + then wait until the resource reaches a given + lifecycle state. + --max-wait-seconds INTEGER The maximum time to wait for the resource to + reach the lifecycle state defined by --wait- + for-state. Defaults to 1200 seconds. + --wait-interval-seconds INTEGER + Check every --wait-interval-seconds to see + whether the resource to see if it has reached + the lifecycle state defined by --wait-for- + state. Defaults to 30 seconds. + --from-json TEXT Provide input to this command as a JSON + document from a file. + + Options can still be + provided on the command line. If an option + exists in both the JSON document and the + command line then the command line specified + value will be used + -?, -h, --help Show this message and exit. + ++++++++++++++++++++++++++++++++++++++++++++++ $ oci iam group --help Usage: oci iam group [OPTIONS] COMMAND [ARGS]... @@ -5755,14 +5992,33 @@ Options: --description TEXT The description you assign to the group during creation. Does not have to be unique, and it's changeable. [required] - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar- - key": "value"}` - This is a complex type whose - value must be valid JSON. The value can be - provided as a string on the command line or - passed in as a file using + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is + a simple key-value pair with no predefined + name, type, or namespace. For more + information, see [Resource Tags]. Example: + `{"Department": "Finance"}` + This is a complex + type whose value must be valid JSON. The value + can be provided as a string on the command + line or passed in as a file using + the + file://path/to/file syntax. + + The --generate- + param-json-input option can be used to + generate an example of the JSON which must be + provided. We recommend storing this example + in + a file, modifying it as needed and then + passing it back in via the file:// syntax. + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` + This is + a complex type whose value must be valid JSON. + The value can be provided as a string on the + command line or passed in as a file using the file://path/to/file syntax. @@ -5773,21 +6029,6 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: - `{"foo-namespace": {"bar-key": "foo-value"}}` - This is a complex type whose value must be - valid JSON. The value can be provided as a - string on the command line or passed in as a - file using - the file://path/to/file syntax. - The --generate-param-json-input option can be - used to generate an example of the JSON which - must be provided. We recommend storing this - example - in a file, modifying it as needed and - then passing it back in via the file:// - syntax. --wait-for-state [CREATING|ACTIVE|INACTIVE|DELETING|DELETED] This operation creates, modifies or deletes a resource that has a defined lifecycle state. @@ -5963,14 +6204,33 @@ Options: --group-id TEXT The OCID of the group. [required] --description TEXT The description you assign to the group. Does not have to be unique, and it's changeable. - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar- - key": "value"}` - This is a complex type whose - value must be valid JSON. The value can be - provided as a string on the command line or - passed in as a file using + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is + a simple key-value pair with no predefined + name, type, or namespace. For more + information, see [Resource Tags]. Example: + `{"Department": "Finance"}` + This is a complex + type whose value must be valid JSON. The value + can be provided as a string on the command + line or passed in as a file using + the + file://path/to/file syntax. + + The --generate- + param-json-input option can be used to + generate an example of the JSON which must be + provided. We recommend storing this example + in + a file, modifying it as needed and then + passing it back in via the file:// syntax. + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` + This is + a complex type whose value must be valid JSON. + The value can be provided as a string on the + command line or passed in as a file using the file://path/to/file syntax. @@ -5981,21 +6241,6 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: - `{"foo-namespace": {"bar-key": "foo-value"}}` - This is a complex type whose value must be - valid JSON. The value can be provided as a - string on the command line or passed in as a - file using - the file://path/to/file syntax. - The --generate-param-json-input option can be - used to generate an example of the JSON which - must be provided. We recommend storing this - example - in a file, modifying it as needed and - then passing it back in via the file:// - syntax. --if-match TEXT For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if- match` parameter to the value of the etag from @@ -6124,14 +6369,33 @@ Options: particular date (YYYY-MM-DD), the policy will be evaluated according to the behavior of the services on that date. - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar- - key": "value"}` - This is a complex type whose - value must be valid JSON. The value can be - provided as a string on the command line or - passed in as a file using + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is + a simple key-value pair with no predefined + name, type, or namespace. For more + information, see [Resource Tags]. Example: + `{"Department": "Finance"}` + This is a complex + type whose value must be valid JSON. The value + can be provided as a string on the command + line or passed in as a file using + the + file://path/to/file syntax. + + The --generate- + param-json-input option can be used to + generate an example of the JSON which must be + provided. We recommend storing this example + in + a file, modifying it as needed and then + passing it back in via the file:// syntax. + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` + This is + a complex type whose value must be valid JSON. + The value can be provided as a string on the + command line or passed in as a file using the file://path/to/file syntax. @@ -6142,21 +6406,6 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: - `{"foo-namespace": {"bar-key": "foo-value"}}` - This is a complex type whose value must be - valid JSON. The value can be provided as a - string on the command line or passed in as a - file using - the file://path/to/file syntax. - The --generate-param-json-input option can be - used to generate an example of the JSON which - must be provided. We recommend storing this - example - in a file, modifying it as needed and - then passing it back in via the file:// - syntax. --wait-for-state [CREATING|ACTIVE|INACTIVE|DELETING|DELETED] This operation creates, modifies or deletes a resource that has a defined lifecycle state. @@ -6450,7 +6699,9 @@ Options: $ oci iam tag --help Usage: oci iam tag [OPTIONS] COMMAND [ARGS]... - A tag definition that belongs to a specific tagNamespace. + A tag definition that belongs to a specific tag namespace. "Defined tags" + must be set up in your tenancy before you can apply them to resources. For + more information, see [Managing Tags and Tag Namespaces]. Options: -?, -h, --help Show this message and exit. @@ -6467,31 +6718,33 @@ Commands: $ oci iam tag create --help Usage: oci iam tag create [OPTIONS] - Creates a new tag in a given tagNamespace. + Creates a new tag in the specified tag namespace. - You have to specify either the id or the name of the tagNamespace that will + You must specify either the OCID or the name of the tag namespace that will contain this tag definition. You must also specify a *name* for the tag, which must be unique across all - tags in the tagNamespace and cannot be changed. All ascii characters are - allowed except spaces and dots. Note that names are case insenstive, that - means you can not have two different tags with same name but with different - casing in one tagNamespace. If you specify a name that's already in use in - the tagNamespace, you'll get a 409 error. + tags in the tag namespace and cannot be changed. The name can contain any + ASCII character except the space (_) or period (.) characters. Names are + case insensitive. That means, for example, "myTag" and "mytag" are not + allowed in the same namespace. If you specify a name that's already in use + in the tag namespace, a 409 error is returned. You must also specify a *description* for the tag. It does not have to be - unique, and you can change it anytime with [UpdateTag]. + unique, and you can change it with [UpdateTag]. Options: - --tag-namespace-id TEXT The OCID of the tagNamespace [required] - --name TEXT The name of the tag which must be unique across - all tags in the tagNamespace and cannot be - changed. [required] - --description TEXT The description of the tag. [required] - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar-key": - "value"}` + --tag-namespace-id TEXT The OCID of the tag namespace. [required] + --name TEXT The name you assign to the tag during creation. + The name must be unique within the tag namespace + and cannot be changed. [required] + --description TEXT The description you assign to the tag during + creation. [required] + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is a + simple key-value pair with no predefined name, + type, or namespace. For more information, see + [Resource Tags]. Example: `{"Department": + "Finance"}` This is a complex type whose value must be valid JSON. The value can be provided as a string on the command line or passed in as a @@ -6505,9 +6758,10 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: `{"foo- - namespace": {"bar-key": "foo-value"}}` + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` This is a complex type whose value must be valid JSON. The value can be provided as a string on the command @@ -6538,8 +6792,8 @@ Usage: oci iam tag get [OPTIONS] Gets the specified tag's information. Options: - --tag-namespace-id TEXT The OCID of the tagNamespace [required] - --tag-name TEXT The name of the tag [required] + --tag-namespace-id TEXT The OCID of the tag namespace. [required] + --tag-name TEXT The name of the tag. [required] --from-json TEXT Provide input to this command as a JSON document from a file. @@ -6553,10 +6807,10 @@ Options: $ oci iam tag list --help Usage: oci iam tag list [OPTIONS] - List the tags that are defined in a given tagNamespace. + Lists the tag definitions in the specified tag namespace. Options: - --tag-namespace-id TEXT The OCID of the tagNamespace [required] + --tag-namespace-id TEXT The OCID of the tag namespace. [required] --page TEXT The value of the `opc-next-page` response header from the previous "List" call. --limit INTEGER The maximum number of items to return in a paginated @@ -6582,8 +6836,8 @@ Usage: oci iam tag reactivate [OPTIONS] Reactivate tag so it can be used Options: - --tag-namespace-id TEXT The OCID of the tagNamespace [required] - --tag-name TEXT The name of the tag [required] + --tag-namespace-id TEXT The OCID of the tag namespace. [required] + --tag-name TEXT The name of the tag. [required] --from-json TEXT Provide input to this command as a JSON document from a file. @@ -6604,8 +6858,8 @@ Usage: oci iam tag retire [OPTIONS] retired tag in the same tag namespace. Options: - --tag-namespace-id TEXT The OCID of the tagNamespace [required] - --tag-name TEXT The name of the tag [required] + --tag-namespace-id TEXT The OCID of the tag namespace. [required] + --tag-name TEXT The name of the tag. [required] --from-json TEXT Provide input to this command as a JSON document from a file. @@ -6622,13 +6876,15 @@ Usage: oci iam tag update [OPTIONS] Updates the the specified tag Options: - --tag-namespace-id TEXT The OCID of the tagNamespace [required] - --tag-name TEXT The name of the tag [required] - --description TEXT The description of the tag. - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar-key": - "value"}` + --tag-namespace-id TEXT The OCID of the tag namespace. [required] + --tag-name TEXT The name of the tag. [required] + --description TEXT The description you assign to the tag during + creation. + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is a + simple key-value pair with no predefined name, + type, or namespace. For more information, see + [Resource Tags]. Example: `{"Department": + "Finance"}` This is a complex type whose value must be valid JSON. The value can be provided as a string on the command line or passed in as a @@ -6642,9 +6898,10 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: `{"foo- - namespace": {"bar-key": "foo-value"}}` + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` This is a complex type whose value must be valid JSON. The value can be provided as a string on the command @@ -6674,8 +6931,9 @@ Options: $ oci iam tag-namespace --help Usage: oci iam tag-namespace [OPTIONS] COMMAND [ARGS]... - A bag of tags that is attached to a compartment and has unique existence in - tenancy. + A managed container for defined tags. A tag namespace is unique in a + tenancy. A tag namespace can't be deleted. For more information, see + [Managing Tags and Tag Namespaces]. Options: -?, -h, --help Show this message and exit. @@ -6692,33 +6950,39 @@ Commands: $ oci iam tag-namespace create --help Usage: oci iam tag-namespace create [OPTIONS] - Creates a new tagNamespace in a given compartment. + Creates a new tag namespace in the specified compartment. You must specify the compartment ID in the request object (remember that the tenancy is simply the root compartment). You must also specify a *name* for the namespace, which must be unique - across all namespaces in your tenancy and cannot be changed. All ascii - characters are allowed except spaces and dots. Note that names are case - insenstive, that means you can not have two different namespaces with same - name but with different casing in one tenancy. Once you created a namespace, - you can not change the name If you specify a name that's already in use in - the tennacy, you'll get a 409 error. + across all namespaces in your tenancy and cannot be changed. The name can + contain any ASCII character except the space (_) or period (.). Names are + case insensitive. That means, for example, "myNamespace" and "mynamespace" + are not allowed in the same tenancy. Once you created a namespace, you + cannot change the name. If you specify a name that's already in use in the + tenancy, a 409 error is returned. You must also specify a *description* for the namespace. It does not have to - be unique, and you can change it anytime with [UpdateTagNamespace]. + be unique, and you can change it with [UpdateTagNamespace]. + + Tag namespaces cannot be deleted, but they can be retired. See [Retiring Key + Definitions and Namespace Definitions] for more information. Options: - -c, --compartment-id TEXT The OCID of the tenancy containing the user. + -c, --compartment-id TEXT The OCID of the tenancy containing the tag + namespace. [required] + --name TEXT The name you assign to the tag namespace during + creation. It must be unique across all tag + namespaces in the tenancy and cannot be changed. [required] - --name TEXT The name of the tagNamespace. It must be unique - across all tagNamespaces in the tenancy and - cannot be changed. [required] - --description TEXT The description of the tagNamespace. [required] - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar-key": - "value"}` + --description TEXT The description you assign to the tag namespace + during creation. [required] + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is a + simple key-value pair with no predefined name, + type, or namespace. For more information, see + [Resource Tags]. Example: `{"Department": + "Finance"}` This is a complex type whose value must be valid JSON. The value can be provided as a string on the command line or passed in as a @@ -6732,9 +6996,10 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: `{"foo- - namespace": {"bar-key": "foo-value"}}` + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` This is a complex type whose value must be valid JSON. The value can be provided as a string on the command @@ -6762,10 +7027,10 @@ Options: $ oci iam tag-namespace get --help Usage: oci iam tag-namespace get [OPTIONS] - Gets the specified tagNamespace's information. + Gets the specified tag namespace's information. Options: - --tag-namespace-id TEXT The OCID of the tagNamespace [required] + --tag-namespace-id TEXT The OCID of the tag namespace. [required] --from-json TEXT Provide input to this command as a JSON document from a file. @@ -6779,7 +7044,7 @@ Options: $ oci iam tag-namespace list --help Usage: oci iam tag-namespace list [OPTIONS] - List the tagNamespaces in a given compartment. + Lists the tag namespaces in the specified compartment. Options: -c, --compartment-id TEXT The OCID of the compartment (remember that the @@ -6790,12 +7055,11 @@ Options: --limit INTEGER The maximum number of items to return in a paginated "List" call. --include-subcompartments BOOLEAN - An optional boolean parameter for whether or - not to retrieve all tagNamespaces in sub - compartments. In case of absence of this - parameter, only tagNamespaces that exist - directly in this compartment will be - retrieved. + An optional boolean parameter indicating + whether to retrieve all tag namespaces in + subcompartments. If this parameter is not + specified, only the tag namespaces defined in + the specified compartment are retrieved. --all Fetches all pages of results. If you provide this option, then you cannot provide the --limit option. @@ -6821,7 +7085,7 @@ Usage: oci iam tag-namespace reactivate [OPTIONS] have to be individually reactivated *after* the namespace is reactivated. Options: - --tag-namespace-id TEXT The OCID of the tagNamespace [required] + --tag-namespace-id TEXT The OCID of the tag namespace. [required] --from-json TEXT Provide input to this command as a JSON document from a file. @@ -6842,7 +7106,7 @@ Usage: oci iam tag-namespace retire [OPTIONS] with the same name as a retired namespace in the same tenant. Options: - --tag-namespace-id TEXT The OCID of the tagNamespace [required] + --tag-namespace-id TEXT The OCID of the tag namespace. [required] --from-json TEXT Provide input to this command as a JSON document from a file. @@ -6859,12 +7123,13 @@ Usage: oci iam tag-namespace update [OPTIONS] Updates the specified tagNamespace Options: - --tag-namespace-id TEXT The OCID of the tagNamespace [required] - --description TEXT The description of the tagNamespace. - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar-key": - "value"}` + --tag-namespace-id TEXT The OCID of the tag namespace. [required] + --description TEXT The description you assign to the tag namespace. + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is a + simple key-value pair with no predefined name, + type, or namespace. For more information, see + [Resource Tags]. Example: `{"Department": + "Finance"}` This is a complex type whose value must be valid JSON. The value can be provided as a string on the command line or passed in as a @@ -6878,9 +7143,10 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: `{"foo- - namespace": {"bar-key": "foo-value"}}` + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` This is a complex type whose value must be valid JSON. The value can be provided as a string on the command @@ -7110,14 +7376,33 @@ Options: --description TEXT The description you assign to the user during creation. Does not have to be unique, and it's changeable. [required] - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar- - key": "value"}` - This is a complex type whose - value must be valid JSON. The value can be - provided as a string on the command line or - passed in as a file using + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is + a simple key-value pair with no predefined + name, type, or namespace. For more + information, see [Resource Tags]. Example: + `{"Department": "Finance"}` + This is a complex + type whose value must be valid JSON. The value + can be provided as a string on the command + line or passed in as a file using + the + file://path/to/file syntax. + + The --generate- + param-json-input option can be used to + generate an example of the JSON which must be + provided. We recommend storing this example + in + a file, modifying it as needed and then + passing it back in via the file:// syntax. + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` + This is + a complex type whose value must be valid JSON. + The value can be provided as a string on the + command line or passed in as a file using the file://path/to/file syntax. @@ -7128,21 +7413,6 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: - `{"foo-namespace": {"bar-key": "foo-value"}}` - This is a complex type whose value must be - valid JSON. The value can be provided as a - string on the command line or passed in as a - file using - the file://path/to/file syntax. - The --generate-param-json-input option can be - used to generate an example of the JSON which - must be provided. We recommend storing this - example - in a file, modifying it as needed and - then passing it back in via the file:// - syntax. --wait-for-state [CREATING|ACTIVE|INACTIVE|DELETING|DELETED] This operation creates, modifies or deletes a resource that has a defined lifecycle state. @@ -7462,14 +7732,33 @@ Options: --user-id TEXT The OCID of the user. [required] --description TEXT The description you assign to the user. Does not have to be unique, and it's changeable. - --freeform-tags COMPLEX TYPE Simple key-value pair that is applied without - any predefined name, type or scope. Exists for - cross-compatibility only. Example: `{"bar- - key": "value"}` - This is a complex type whose - value must be valid JSON. The value can be - provided as a string on the command line or - passed in as a file using + --freeform-tags COMPLEX TYPE Free-form tags for this resource. Each tag is + a simple key-value pair with no predefined + name, type, or namespace. For more + information, see [Resource Tags]. Example: + `{"Department": "Finance"}` + This is a complex + type whose value must be valid JSON. The value + can be provided as a string on the command + line or passed in as a file using + the + file://path/to/file syntax. + + The --generate- + param-json-input option can be used to + generate an example of the JSON which must be + provided. We recommend storing this example + in + a file, modifying it as needed and then + passing it back in via the file:// syntax. + --defined-tags COMPLEX TYPE Defined tags for this resource. Each key is + predefined and scoped to a namespace. For more + information, see [Resource Tags]. Example: + `{"Operations": {"CostCenter": "42"}}` + This is + a complex type whose value must be valid JSON. + The value can be provided as a string on the + command line or passed in as a file using the file://path/to/file syntax. @@ -7480,21 +7769,6 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --defined-tags COMPLEX TYPE Usage of predefined tag keys. These predefined - keys are scoped to namespaces. Example: - `{"foo-namespace": {"bar-key": "foo-value"}}` - This is a complex type whose value must be - valid JSON. The value can be provided as a - string on the command line or passed in as a - file using - the file://path/to/file syntax. - The --generate-param-json-input option can be - used to generate an example of the JSON which - must be provided. We recommend storing this - example - in a file, modifying it as needed and - then passing it back in via the file:// - syntax. --if-match TEXT For optimistic concurrency control. In the PUT or DELETE call for a resource, set the `if- match` parameter to the value of the etag from @@ -8556,7 +8830,7 @@ Usage: oci lb listener create [OPTIONS] Options: --default-backend-set-name TEXT The name of the associated backend set. - [required] + Example: `My_backend_set` [required] --name TEXT A friendly name for the listener. It must be unique and it cannot be changed. Avoid entering confidential information. @@ -8598,6 +8872,11 @@ Options: --ssl-verify-peer-certificate BOOLEAN Whether the load balancer listener should verify peer certificates. + --connection-configuration-idle-timeout INTEGER + The maximum idle time, in seconds, allowed + between two successive receive or two + successive send operations between the client + and backend servers. --from-json TEXT Provide input to this command as a JSON document from a file. @@ -8666,7 +8945,7 @@ Usage: oci lb listener update [OPTIONS] Options: --default-backend-set-name TEXT The name of the associated backend set. - [required] + Example: `My_backend_set` [required] --port INTEGER The communication port for the listener. Example: `80` [required] --protocol TEXT The protocol on which the listener accepts @@ -8708,6 +8987,11 @@ Options: --ssl-verify-peer-certificate BOOLEAN Whether the load balancer listener should verify peer certificates. + --connection-configuration-idle-timeout INTEGER + The maximum idle time, in seconds, allowed + between two successive receive or two + successive send operations between the client + and backend servers. --from-json TEXT Provide input to this command as a JSON document from a file. @@ -9009,15 +9293,18 @@ Options: Example: `full` --sort-by [TIMECREATED|DISPLAYNAME] - The field to sort by. Only one sort order may - be provided. Time created is default ordered - as descending. Display name is default - ordered as ascending. - --sort-order [ASC|DESC] The sort order to use, either 'asc' or 'desc' - --display-name TEXT A filter to only return resources that match + The field to sort by. You can provide one + sort order (`sortOrder`). Default order for + TIMECREATED is descending. Default order for + DISPLAYNAME is ascending. The DISPLAYNAME sort + order is case sensitive. + --sort-order [ASC|DESC] The sort order to use, either ascending + (`ASC`) or descending (`DESC`). The + DISPLAYNAME sort order is case sensitive. + --display-name TEXT A filter to return only resources that match the given display name exactly. --lifecycle-state [CREATING|FAILED|ACTIVE|DELETING|DELETED] - A filter to only return resources that match + A filter to return only resources that match the given lifecycle state. --all Fetches all pages of results. If you provide this option, then you cannot provide the @@ -13119,7 +13406,7 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --public-access-type [NoPublicAccess|ObjectRead] + --public-access-type [NoPublicAccess|ObjectRead|ObjectReadWithoutList] The type of public access enabled on this bucket. A bucket is set to `NoPublicAccess` by default, which only allows an authenticated @@ -13127,6 +13414,9 @@ Options: When `ObjectRead` is enabled on the bucket, public access is allowed for the `GetObject`, `HeadObject`, and `ListObjects` operations. + When `ObjectReadWithoutList` is enabled on the + bucket, public access is allowed for the + `GetObject` and `HeadObject` operations. --storage-tier [Standard|Archive] The type of storage tier of this bucket. A bucket is set to 'Standard' tier by default, @@ -13228,8 +13518,8 @@ Options: -ns, --namespace-name, --namespace TEXT The top-level namespace used for the request. [required] - -c, --compartment-id TEXT The ID of the compartment in which to create - the bucket. [required] + -c, --compartment-id TEXT The ID of the compartment in which to list + buckets. [required] --limit INTEGER The maximum number of items to return. --page TEXT The page at which to start retrieving results. --all Fetches all pages of results. If you provide @@ -13279,7 +13569,7 @@ Options: in a file, modifying it as needed and then passing it back in via the file:// syntax. - --public-access-type [NoPublicAccess|ObjectRead] + --public-access-type [NoPublicAccess|ObjectRead|ObjectReadWithoutList] The type of public access enabled on this bucket. A bucket is set to `NoPublicAccess` by default, which only allows an authenticated @@ -13287,6 +13577,9 @@ Options: When `ObjectRead` is enabled on the bucket, public access is allowed for the `GetObject`, `HeadObject`, and `ListObjects` operations. + When `ObjectReadWithoutList` is enabled on the + bucket, public access is allowed for the + `GetObject` and `HeadObject` operations. --if-match TEXT The entity tag to match. For creating and committing a multipart upload to an object, this is the entity tag of the target object. @@ -13379,7 +13672,7 @@ Options: -?, -h, --help Show this message and exit. Commands: - get Gets the name of the namespace for the user... + get Gets the name of the namespace for the user get-metadata Get the metadata for the namespace, which... update-metadata Change the default Swift/S3 compartmentId of... @@ -13387,9 +13680,9 @@ Commands: $ oci os ns get --help Usage: oci os ns get [OPTIONS] - Gets the name of the namespace for the user making the request. An account - name must be unique, must start with a letter, and can have up to 15 - lowercase letters and numbers. You cannot use spaces or special characters. + Namespaces are unique. Namespaces are either the tenancy name or a random + string automatically generated during account creation. You cannot edit a + namespace. Options: --from-json TEXT Provide input to this command as a JSON document from a diff --git a/tests/test_audit.py b/tests/test_audit.py index f9097c67b..01a424340 100644 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -6,6 +6,7 @@ import json import oci_cli from . import command_coverage_validator +from . import test_config_container from . import util @@ -14,7 +15,10 @@ class TestAudit(unittest.TestCase): + # For recording, don't match on the query string because that includes the date range for the query + # (and that will change between runs) @command_coverage_validator.CommandCoverageValidator(oci_cli.audit_cli.audit_group, expected_not_called_count=2) + @test_config_container.RecordReplay('audit', match_on=['method', 'scheme', 'host', 'port', 'path']) def test_all_operations(self, validator): """Successfully calls every operation with basic options.""" self.validator = validator diff --git a/tests/test_blockstorage.py b/tests/test_blockstorage.py index f60d1d849..102dcfbc0 100644 --- a/tests/test_blockstorage.py +++ b/tests/test_blockstorage.py @@ -4,6 +4,7 @@ import json import unittest from . import command_coverage_validator +from . import test_config_container from . import util from .test_list_filter import retrieve_list_and_ensure_sorted import oci_cli @@ -13,6 +14,7 @@ class TestBlockStorage(unittest.TestCase): @util.slow @command_coverage_validator.CommandCoverageValidator(oci_cli.blockstorage_cli.blockstorage_group, expected_not_called_count=4) + @test_config_container.RecordReplay('blockstorage') def test_all_operations(self, validator): """Successfully calls every operation with basic options.""" self.validator = validator diff --git a/tests/test_canned_queries_in_cli_rc_file.py b/tests/test_canned_queries_in_cli_rc_file.py index fb0327fb4..1848c512f 100644 --- a/tests/test_canned_queries_in_cli_rc_file.py +++ b/tests/test_canned_queries_in_cli_rc_file.py @@ -1,4 +1,5 @@ from . import util +from . import test_config_container import json import os @@ -21,66 +22,67 @@ def default_file_with_canned_queries(): def test_query_when_listing_and_getting_instances(default_file_with_canned_queries): - result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://get_id_and_display_name_from_list', '--defaults-file', default_file_with_canned_queries]) - assert result.exit_code == 0 + with test_config_container.create_vcr().use_cassette('test_canned_queries_listing_getting_instances.yml'): + result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://get_id_and_display_name_from_list', '--defaults-file', default_file_with_canned_queries]) + assert result.exit_code == 0 - if result.output != '': - parsed_result = json.loads(result.output) - for d in parsed_result: - keys = d.keys() - assert len(keys) == 2 - assert 'id' in d.keys() - assert 'display-name' in d.keys() + if result.output != '': + parsed_result = json.loads(result.output) + for d in parsed_result: + keys = d.keys() + assert len(keys) == 2 + assert 'id' in d.keys() + assert 'display-name' in d.keys() - if len(parsed_result) > 0: - instance_id = parsed_result[0]['id'] + if len(parsed_result) > 0: + instance_id = parsed_result[0]['id'] - result = invoke(['compute', 'instance', 'get', '--instance-id', instance_id, '--query', 'query://get_id_and_display_name_from_single_result', '--defaults-file', default_file_with_canned_queries]) - assert result.exit_code == 0 + result = invoke(['compute', 'instance', 'get', '--instance-id', instance_id, '--query', 'query://get_id_and_display_name_from_single_result', '--defaults-file', default_file_with_canned_queries]) + assert result.exit_code == 0 - parsed_result = json.loads(result.output) - assert len(parsed_result.keys()) == 2 - assert 'id' in parsed_result.keys() - assert 'display-name' in parsed_result.keys() + parsed_result = json.loads(result.output) + assert len(parsed_result.keys()) == 2 + assert 'id' in parsed_result.keys() + assert 'display-name' in parsed_result.keys() - result = invoke(['compute', 'instance', 'get', '--instance-id', instance_id]) + result = invoke(['compute', 'instance', 'get', '--instance-id', instance_id]) + parsed_result = json.loads(result.output) + expected_string = '"{},{}'.format(parsed_result['data']['id'], parsed_result['data']['display-name']) + + result = invoke(['compute', 'instance', 'get', '--instance-id', instance_id, '--query', 'query://get_id_display_name_and_lifecycle_state_from_single_result_as_csv', '--defaults-file', default_file_with_canned_queries]) + assert result.exit_code == 0 + assert expected_string in result.output + + result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID]) parsed_result = json.loads(result.output) - expected_string = '"{},{}'.format(parsed_result['data']['id'], parsed_result['data']['display-name']) - result = invoke(['compute', 'instance', 'get', '--instance-id', instance_id, '--query', 'query://get_id_display_name_and_lifecycle_state_from_single_result_as_csv', '--defaults-file', default_file_with_canned_queries]) - assert result.exit_code == 0 - assert expected_string in result.output + expected_result = [] + for d in parsed_result['data']: + expected_result.append('"{},{}'.format(d['id'], d['display-name'])) - result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID]) - parsed_result = json.loads(result.output) + result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://get_id_display_name_and_lifecycle_state_from_list_as_csv', '--defaults-file', default_file_with_canned_queries]) + assert result.exit_code == 0 - expected_result = [] - for d in parsed_result['data']: - expected_result.append('"{},{}'.format(d['id'], d['display-name'])) + result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://get_top_5_results', '--defaults-file', default_file_with_canned_queries]) + assert result.exit_code == 0 + parsed_result = json.loads(result.output) + assert len(parsed_result) <= 5 # no guarantee of the exact number of results, but our JMESPath query shouldn't return any more than 5 - result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://get_id_display_name_and_lifecycle_state_from_list_as_csv', '--defaults-file', default_file_with_canned_queries]) - assert result.exit_code == 0 + result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://get_last_2_results', '--defaults-file', default_file_with_canned_queries]) + assert result.exit_code == 0 + parsed_result = json.loads(result.output) + assert len(parsed_result) <= 2 # no guarantee of the exact number of results, but our JMESPath query shouldn't return any more than 2 - result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://get_top_5_results', '--defaults-file', default_file_with_canned_queries]) + # The display name we're matching on is junk, so should not match any result + result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://filter_by_display_name_contains_text', '--defaults-file', default_file_with_canned_queries]) assert result.exit_code == 0 - parsed_result = json.loads(result.output) - assert len(parsed_result) <= 5 # no guarantee of the exact number of results, but our JMESPath query shouldn't return any more than 5 + if result.output != '': + assert 'Query returned empty result, no output to show' in result.output - result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://get_last_2_results', '--defaults-file', default_file_with_canned_queries]) + result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://filter_by_display_name_contains_text_and_get_attributes', '--defaults-file', default_file_with_canned_queries]) assert result.exit_code == 0 - parsed_result = json.loads(result.output) - assert len(parsed_result) <= 2 # no guarantee of the exact number of results, but our JMESPath query shouldn't return any more than 2 - - # The display name we're matching on is junk, so should not match any result - result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://filter_by_display_name_contains_text', '--defaults-file', default_file_with_canned_queries]) - assert result.exit_code == 0 - if result.output != '': - assert 'Query returned empty result, no output to show' in result.output - - result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--query', 'query://filter_by_display_name_contains_text_and_get_attributes', '--defaults-file', default_file_with_canned_queries]) - assert result.exit_code == 0 - if result.output != '': - assert 'Query returned empty result, no output to show' in result.output + if result.output != '': + assert 'Query returned empty result, no output to show' in result.output def test_query_does_not_exist(default_file_with_canned_queries): diff --git a/tests/test_cli_setup.py b/tests/test_cli_setup.py index c5c8842ad..ae8c9eef1 100644 --- a/tests/test_cli_setup.py +++ b/tests/test_cli_setup.py @@ -442,9 +442,9 @@ def subtest_repair_file_permissions(self): @util.log_test def subtest_oci_cli_rc_file(self): result = self.invoke(['setup', 'oci-cli-rc', '--file', CONFIG_FILENAME]) - assert 'Predefined queries written under section {}'.format(oci_cli.cli_root.CLI_RC_CANNED_QUERIES_SECTION_NAME) in result.output - assert 'Command aliases written under section {}'.format(oci_cli.cli_root.CLI_RC_COMMAND_ALIASES_SECTION_NAME) in result.output - assert 'Parameter aliases written under section {}'.format(oci_cli.cli_root.CLI_RC_PARAM_ALIASES_SECTION_NAME) in result.output + assert 'Predefined queries written under section {}'.format(oci_cli.cli_constants.CLI_RC_CANNED_QUERIES_SECTION_NAME) in result.output + assert 'Command aliases written under section {}'.format(oci_cli.cli_constants.CLI_RC_COMMAND_ALIASES_SECTION_NAME) in result.output + assert 'Parameter aliases written under section {}'.format(oci_cli.cli_constants.CLI_RC_PARAM_ALIASES_SECTION_NAME) in result.output with open(CONFIG_FILENAME, 'rb') as f: contents = f.read().decode() diff --git a/tests/test_compute.py b/tests/test_compute.py index 3b14b33ed..b1d48bebe 100644 --- a/tests/test_compute.py +++ b/tests/test_compute.py @@ -8,6 +8,7 @@ import unittest from . import command_coverage_validator from . import tag_data_container +from . import test_config_container from . import util import oci_cli @@ -20,6 +21,7 @@ class TestCompute(unittest.TestCase): @util.slow @command_coverage_validator.CommandCoverageValidator(oci_cli.compute_cli.compute_group, expected_not_called_count=8) + @test_config_container.RecordReplay('compute') def test_all_operations(self, validator): """Successfully calls every operation with basic options. The exceptions are the image import and export commands as they are handled by test_image_import_export.py @@ -377,7 +379,7 @@ def subtest_instance_console_connections(self): self.assertIsNotNone(instance_console_connection_details['data']['id']) self.assertIsNotNone(instance_console_connection_details['data']['lifecycle-state']) self.assertEquals(self.instance_ocid, instance_console_connection_details['data']['instance-id']) - self.assertEquals(util.SSH_AUTHORISED_KEY_FINGERPRINT, instance_console_connection_details['data']['fingerprint']) + self.assertIsNotNone(instance_console_connection_details['data']['fingerprint']) self.assertEquals(util.COMPARTMENT_ID, instance_console_connection_details['data']['compartment-id']) result = self.invoke(['compute', 'instance-console-connection', 'get', '--instance-console-connection-id', instance_console_connection_details['data']['id']]) @@ -434,7 +436,7 @@ def subtest_instance_console_connections(self): def subtest_instance_console_connections_tagging(self): tag_names_to_values = {} for t in tag_data_container.tags: - tag_names_to_values[t.name] = 'somevalue {}'.format(int(time.time())) + tag_names_to_values[t.name] = 'somevalue {}'.format(t.name) tag_data_container.write_defined_tags_to_file( os.path.join('tests', 'temp', 'defined_tags_1.json'), tag_data_container.tag_namespace, diff --git a/tests/test_config_container.py b/tests/test_config_container.py new file mode 100644 index 000000000..f9fd9a7e3 --- /dev/null +++ b/tests/test_config_container.py @@ -0,0 +1,67 @@ +# coding: utf-8 +# Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + +# A module which holds test configuration information. This is intended to be a shared space where tests +# can figure out what kind of recording (via VCR) is being done and also so that they can take actions (e.g. waiting) +# in a VCR compatible/friendly way + +import oci +import vcr + +vcr_mode = None + + +def using_vcr_with_mock_responses(): + return vcr_mode != 'all' + + +def create_vcr(**kwargs): + vcr_to_use = vcr.VCR( + serializer='yaml', + cassette_library_dir='tests/fixtures/cassettes', + record_mode=vcr_mode + ) + + if 'match_on' in kwargs: + vcr_to_use.match_on = kwargs['match_on'] + + return vcr_to_use + + +# A wrapper around the standard waiter functionality so that if we're mocking responses we don't sleep as we normally +# would (since responses are just going to get returned immediately by VCR anyway) +def do_wait(client, *args, **kwargs): + if vcr_mode == 'none': + kwargs['max_interval_seconds'] = 0 + + oci.wait_until(client, *args, **kwargs) + + +class RecordReplay(object): + def __init__(self, test_namespace='', **vcr_additional_kwargs): + self.test_namespace = test_namespace + self.vcr_additional_kwargs = vcr_additional_kwargs + + def __call__(self, func): + my_vcr = create_vcr(**self.vcr_additional_kwargs) + + def wrapped_call(ctx, *args, **kwargs): + with my_vcr.use_cassette(self.test_namespace + '_' + func.__name__ + '_cassette.yml'): + func(ctx, *args, **kwargs) + + return wrapped_call + + +class RecordReplayWithNoClickContext(object): + def __init__(self, test_namespace='', **vcr_additional_kwargs): + self.test_namespace = test_namespace + self.vcr_additional_kwargs = vcr_additional_kwargs + + def __call__(self, func): + my_vcr = create_vcr(**self.vcr_additional_kwargs) + + def wrapped_call(*args, **kwargs): + with my_vcr.use_cassette(self.test_namespace + '_' + func.__name__ + '_cassette.yml'): + func(*args, **kwargs) + + return wrapped_call diff --git a/tests/test_default_files_command_invocation.py b/tests/test_default_files_command_invocation.py index ff4da7942..9de6e48da 100644 --- a/tests/test_default_files_command_invocation.py +++ b/tests/test_default_files_command_invocation.py @@ -8,6 +8,7 @@ import string import time import unittest +from . import test_config_container from . import util IPXE_SCRIPT_FILE = 'tests/resources/ipxe_script_example.txt' @@ -97,52 +98,54 @@ def test_invoke_with_default_file_param_accepts_multiple_values(self): @util.slow def test_invoke_with_file_paths_and_json_in_default_file(self): - self.create_network_resources() - - try: - instance_name = util.random_name('cli_test_instance_options') - image_id = util.oracle_linux_image() - shape = 'VM.Standard1.2' - hostname_label = util.random_name('bminstance', insert_underscore=False) - vnic_display_name = 'vnic_display_name' - private_ip = '10.0.0.15' - assign_public_ip = 'true' - - launch_instance_result = util.invoke_command( - ['compute', 'instance', 'launch', - '--compartment-id', util.COMPARTMENT_ID, - '--availability-domain', util.availability_domain(), - '--display-name', instance_name, - '--subnet-id', self.subnet_ocid, - '--image-id', image_id, - '--shape', shape, - '--hostname-label', hostname_label, - '--private-ip', private_ip, - '--assign-public-ip', assign_public_ip, - '--vnic-display-name', vnic_display_name, - '--defaults-file', 'tests/resources/default_files/launch_instance_default']) - - if (launch_instance_result.output and 'LimitExceeded' in launch_instance_result.output) or (launch_instance_result.exception and 'LimitExceeded' in str(launch_instance_result.exception)): - pytest.skip('Skipping test_launch_instance as we received a limit exceeded error from the service') - - temp_instance_ocid = util.find_id_in_response(launch_instance_result.output) - util.validate_response(launch_instance_result, expect_etag=True) - - extended_metadata_result = json.loads(launch_instance_result.output)['data']['extended-metadata'] - assert extended_metadata_result['a'] == '1' - assert extended_metadata_result['b']['c'] == '3' - - content = None - with open(IPXE_SCRIPT_FILE, mode='r') as file: - content = file.read() - - assert 'ipxe-script' in launch_instance_result.output - # Just look at the first few characters. Once we hit a line break the formatting will differ. - assert content[:5] in launch_instance_result.output - - self.delete_instance(temp_instance_ocid) - finally: - self.clean_up_network_resources() + with test_config_container.create_vcr().use_cassette('default_files_command_invoke_with_file_paths.yml'): + self.create_network_resources() + + try: + instance_name = util.random_name('cli_test_instance_options') + image_id = util.oracle_linux_image() + shape = 'VM.Standard1.2' + hostname_label = util.random_name('bminstance', insert_underscore=False) + vnic_display_name = 'vnic_display_name' + private_ip = '10.0.0.15' + assign_public_ip = 'true' + + launch_instance_result = util.invoke_command([ + 'compute', 'instance', 'launch', + '--compartment-id', util.COMPARTMENT_ID, + '--availability-domain', util.availability_domain(), + '--display-name', instance_name, + '--subnet-id', self.subnet_ocid, + '--image-id', image_id, + '--shape', shape, + '--hostname-label', hostname_label, + '--private-ip', private_ip, + '--assign-public-ip', assign_public_ip, + '--vnic-display-name', vnic_display_name, + '--defaults-file', 'tests/resources/default_files/launch_instance_default' + ]) + + if (launch_instance_result.output and 'LimitExceeded' in launch_instance_result.output) or (launch_instance_result.exception and 'LimitExceeded' in str(launch_instance_result.exception)): + pytest.skip('Skipping test_launch_instance as we received a limit exceeded error from the service') + + temp_instance_ocid = util.find_id_in_response(launch_instance_result.output) + util.validate_response(launch_instance_result, expect_etag=True) + + extended_metadata_result = json.loads(launch_instance_result.output)['data']['extended-metadata'] + assert extended_metadata_result['a'] == '1' + assert extended_metadata_result['b']['c'] == '3' + + content = None + with open(IPXE_SCRIPT_FILE, mode='r') as file: + content = file.read() + + assert 'ipxe-script' in launch_instance_result.output + # Just look at the first few characters. Once we hit a line break the formatting will differ. + assert content[:5] in launch_instance_result.output + + self.delete_instance(temp_instance_ocid) + finally: + self.clean_up_network_resources() def create_network_resources(self): vcn_name = util.random_name('cli_test_default_file') diff --git a/tests/test_identity.py b/tests/test_identity.py index b0396c640..da5bcae9b 100644 --- a/tests/test_identity.py +++ b/tests/test_identity.py @@ -3,11 +3,10 @@ import json import pytest -import random -import time import unittest from . import command_coverage_validator from . import tag_data_container +from . import test_config_container from . import util import oci_cli import os.path @@ -27,23 +26,26 @@ def setUp(self): # The operations not called are: # - region list, region-subscription create/list # - tag and tag-namespace operations (x12). These are handled via test_tagging and test_tag_management - @command_coverage_validator.CommandCoverageValidator(oci_cli.identity_cli.identity_group, expected_not_called_count=15) + @command_coverage_validator.CommandCoverageValidator(oci_cli.identity_cli.identity_group, expected_not_called_count=20) + @test_config_container.RecordReplay('identity') def test_all_operations(self, validator): """Successfully calls every operation with basic options. Exceptions are region list, region-subscription list region-subscription create""" self.validator = validator - self.subtest_availability_domain_operations() - self.subtest_compartment_operations() - self.subtest_compartment_rename() - self.subtest_user_operations() - self.subtest_group_operations() - self.subtest_user_group_membership_operations() - self.subtest_api_key_operations() - self.subtest_ui_password_operations() - self.subtest_swift_password_operations() - self.subtest_policy_operations() - self.subtest_customer_secret_key_operations() - self.subtest_cleanup() + try: + self.subtest_availability_domain_operations() + self.subtest_compartment_operations() + self.subtest_compartment_rename() + self.subtest_user_operations() + self.subtest_group_operations() + self.subtest_user_group_membership_operations() + self.subtest_api_key_operations() + self.subtest_ui_password_operations() + self.subtest_swift_password_operations() + self.subtest_policy_operations() + self.subtest_customer_secret_key_operations() + finally: + self.subtest_cleanup() def subtest_availability_domain_operations(self): result = self.invoke(['availability-domain', 'list', '--compartment-id', util.TENANT_ID]) @@ -61,7 +63,7 @@ def subtest_compartment_operations(self): result = self.invoke(['compartment', 'get', '--compartment-id', util.COMPARTMENT_ID]) self.validate_response(result, expect_etag=True) - update_description = 'Compartment used by CLI integration tests. ' + str(random.randint(0, 1000000)) + update_description = 'Compartment used by CLI integration tests. {}'.format(util.random_number_string()) result = self.invoke( ['compartment', 'update', '--compartment-id', util.COMPARTMENT_ID, '--description', update_description]) self.validate_response(result, expect_etag=True) @@ -70,7 +72,7 @@ def subtest_compartment_operations(self): def subtest_compartment_rename(self): compartment_to_rename = self.get_compartment_to_rename() - updated_name = '{}{}'.format(self.RENAME_COMPARTMENT_PREFIX, int(time.time())) + updated_name = '{}{}'.format(self.RENAME_COMPARTMENT_PREFIX, util.random_number_string()) updated_description = 'Updated {}'.format(updated_name) result = self.invoke(['compartment', 'update', '--compartment-id', compartment_to_rename['id'], '--name', updated_name, '--description', updated_description]) self.validate_response(result, expect_etag=True) @@ -91,10 +93,6 @@ def subtest_user_operations(self): result = self.invoke(['user', 'list', '--compartment-id', util.TENANT_ID, '--limit', '1000']) self.validate_response(result, extra_validation=self.validate_user) - # Call again with debug data for extra validation. - result = self.invoke(['user', 'list', '--compartment-id', util.TENANT_ID, '--limit', '1000'], debug=True) - self.validate_response(result, extra_validation=self.validate_user, includes_debug_data=True) - self.user_description = 'UPDATED ' + self.user_description result = self.invoke(['user', 'update', '--user-id', self.user_ocid, '--description', self.user_description]) self.validate_response(result, extra_validation=self.validate_user, expect_etag=True) @@ -427,18 +425,20 @@ def subtest_customer_secret_key_operations(self): assert item['lifecycle-state'] in self.VALID_DELETED_CUSTOMER_SECRET_KEY_STATES def subtest_cleanup(self): - result = self.invoke(['user', 'delete', '--user-id', self.user_ocid], input='n') - assert result.exit_code != 0 + if hasattr(self, 'user_ocid'): + result = self.invoke(['user', 'delete', '--user-id', self.user_ocid], input='n') + assert result.exit_code != 0 - result = self.invoke(['user', 'delete', '--user-id', self.user_ocid], input='y') - self.validate_response(result, json_response_expected=False) + result = self.invoke(['user', 'delete', '--user-id', self.user_ocid], input='y') + self.validate_response(result, json_response_expected=False) - result = self.invoke(['user', 'list', '--compartment-id', util.TENANT_ID, '--limit', '1000']) - self.validate_response(result) - assert self.user_ocid not in result.output + result = self.invoke(['user', 'list', '--compartment-id', util.TENANT_ID, '--limit', '1000']) + self.validate_response(result) + assert self.user_ocid not in result.output - result = self.invoke(['group', 'delete', '--group-id', self.group_ocid, '--force']) - self.validate_response(result) + if hasattr(self, 'group_ocid'): + result = self.invoke(['group', 'delete', '--group-id', self.group_ocid, '--force']) + self.validate_response(result) def validate_group(self, result): assert self.group_ocid in result.output @@ -462,7 +462,8 @@ def common_validation(result): def invoke(self, params, debug=False, ** args): commands = ['iam'] + params - self.validator.register_call(commands) + if hasattr(self, 'validator'): + self.validator.register_call(commands) if debug is True: commands = ['--debug'] + commands @@ -497,7 +498,7 @@ def get_compartment_to_rename(self): result = self.invoke([ 'compartment', 'create', '--compartment-id', util.TENANT_ID, - '--name', '{}{}'.format(self.RENAME_COMPARTMENT_PREFIX, int(time.time())), + '--name', '{}{}'.format(self.RENAME_COMPARTMENT_PREFIX, util.random_number_string()), '--description', 'Compartment for CLI compartment rename testing' ]) parsed_result = json.loads(result.output) @@ -531,7 +532,7 @@ def compare_secret_key_dicts(self, dict_one, dict_two): def update_policy_with_tags(self, policy_ocid): tag_names_to_values = {} for t in tag_data_container.tags: - tag_names_to_values[t.name] = 'update_policy {}'.format(int(time.time())) + tag_names_to_values[t.name] = 'update_policy {} 1'.format(t.name) tag_data_container.write_defined_tags_to_file( os.path.join('tests', 'temp', 'defined_tags_1.json'), tag_data_container.tag_namespace, diff --git a/tests/test_json_skeleton_command_invocation.py b/tests/test_json_skeleton_command_invocation.py index fc213aace..1ad2b0a61 100644 --- a/tests/test_json_skeleton_command_invocation.py +++ b/tests/test_json_skeleton_command_invocation.py @@ -3,7 +3,7 @@ import os.path import pytest import shutil -import time +from . import test_config_container from . import util @@ -78,64 +78,68 @@ def prepare_input_file_folder(): shutil.rmtree(INPUT_FILE_FOLDER) -@pytest.fixture(scope='module', autouse=True) +@pytest.fixture(scope='module') def network_resources(): - vcn_name = util.random_name('cli_test_json_skeleton') - cidr_block = "10.0.0.0/16" - vcn_dns_label = util.random_name('vcn', insert_underscore=False) - - result = invoke([ - 'network', 'vcn', 'create', - '--compartment-id', util.COMPARTMENT_ID, - '--display-name', vcn_name, - '--cidr-block', cidr_block, - '--dns-label', vcn_dns_label - ]) - vcn_ocid = util.find_id_in_response(result.output) - util.wait_until(['network', 'vcn', 'get', '--vcn-id', vcn_ocid], 'AVAILABLE', max_wait_seconds=300) - - subnet_name = util.random_name('cli_test_compute_subnet') - subnet_dns_label = util.random_name('subnet', insert_underscore=False) - - result = invoke([ - 'network', 'subnet', 'create', - '--compartment-id', util.COMPARTMENT_ID, - '--availability-domain', util.availability_domain(), - '--display-name', subnet_name, - '--vcn-id', vcn_ocid, - '--cidr-block', cidr_block, - '--dns-label', subnet_dns_label - ]) - subnet_ocid = util.find_id_in_response(result.output) - util.validate_response(result, expect_etag=True) - util.wait_until(['network', 'subnet', 'get', '--subnet-id', subnet_ocid], 'AVAILABLE', max_wait_seconds=300) - - yield (vcn_ocid, subnet_ocid) - - result = invoke(['network', 'subnet', 'delete', '--subnet-id', subnet_ocid, '--force']) - util.validate_response(result) - util.wait_until(['network', 'subnet', 'get', '--subnet-id', subnet_ocid], 'TERMINATED', max_wait_seconds=600, succeed_if_not_found=True) - - result = util.invoke_command(['network', 'vcn', 'delete', '--vcn-id', vcn_ocid, '--force']) - util.validate_response(result) - util.wait_until(['network', 'vcn', 'get', '--vcn-id', vcn_ocid], 'TERMINATED', max_wait_seconds=600, succeed_if_not_found=True) - - + with test_config_container.create_vcr().use_cassette('json_skeleton_command_invoke_fixture_network_resources.yml'): + vcn_name = util.random_name('cli_test_json_skeleton') + cidr_block = "10.0.0.0/16" + vcn_dns_label = util.random_name('vcn', insert_underscore=False) + + result = invoke([ + 'network', 'vcn', 'create', + '--compartment-id', util.COMPARTMENT_ID, + '--display-name', vcn_name, + '--cidr-block', cidr_block, + '--dns-label', vcn_dns_label + ]) + vcn_ocid = util.find_id_in_response(result.output) + util.wait_until(['network', 'vcn', 'get', '--vcn-id', vcn_ocid], 'AVAILABLE', max_wait_seconds=300) + + subnet_name = util.random_name('cli_test_compute_subnet') + subnet_dns_label = util.random_name('subnet', insert_underscore=False) + + result = invoke([ + 'network', 'subnet', 'create', + '--compartment-id', util.COMPARTMENT_ID, + '--availability-domain', util.availability_domain(), + '--display-name', subnet_name, + '--vcn-id', vcn_ocid, + '--cidr-block', cidr_block, + '--dns-label', subnet_dns_label + ]) + subnet_ocid = util.find_id_in_response(result.output) + util.validate_response(result, expect_etag=True) + util.wait_until(['network', 'subnet', 'get', '--subnet-id', subnet_ocid], 'AVAILABLE', max_wait_seconds=300) + + yield (vcn_ocid, subnet_ocid) + + result = invoke(['network', 'subnet', 'delete', '--subnet-id', subnet_ocid, '--force']) + util.validate_response(result) + util.wait_until(['network', 'subnet', 'get', '--subnet-id', subnet_ocid], 'TERMINATED', max_wait_seconds=600, succeed_if_not_found=True) + + result = util.invoke_command(['network', 'vcn', 'delete', '--vcn-id', vcn_ocid, '--force']) + util.validate_response(result) + util.wait_until(['network', 'vcn', 'get', '--vcn-id', vcn_ocid], 'TERMINATED', max_wait_seconds=600, succeed_if_not_found=True) + + +@test_config_container.RecordReplayWithNoClickContext('json_skeleton_command_invoke') def test_list_buckets(): result = invoke(['os', 'bucket', 'list', '--from-json', 'file://{}'.format(os.path.join(INPUT_FILE_FOLDER, 'list_buckets.json'))]) parsed_result = json.loads(result.output) assert len(parsed_result['data']) >= 0 +@test_config_container.RecordReplayWithNoClickContext('json_skeleton_command_invoke') def test_list_buckets_with_override(): # This will use the "testing-fake-namespace" as it was directly provided result = invoke(['os', 'bucket', 'list', '--namespace', 'testing-fake-namespace', '--from-json', 'file://{}'.format(os.path.join(INPUT_FILE_FOLDER, 'list_buckets.json'))]) assert result.output == '' +# This test cannot be mocked because VCR doesn't play nicely with the FileReadCallbackStream implementation in the Python SDK def test_create_update_bucket_and_put_object(): base_path = os.path.join('tests', 'resources', 'json_input') - bucket_name = 'json_skeleton_bucket_{}'.format(int(time.time())) + bucket_name = 'json_skeleton_bucket_{}'.format(util.random_number_string()) result = invoke(['os', 'bucket', 'create', '--compartment-id', util.COMPARTMENT_ID, '--namespace', util.NAMESPACE, '--name', bucket_name, '--from-json', 'file://{}'.format(os.path.join(base_path, 'bucket_create.json'))]) parsed_result = json.loads(result.output) @@ -155,6 +159,7 @@ def test_create_update_bucket_and_put_object(): '--file', os.path.join(base_path, 'bucket_create.json'), '--from-json', 'file://{}'.format(os.path.join(base_path, 'object_put.json')) ]) + print(result.output) result = invoke([ 'os', 'object', 'head', @@ -173,115 +178,117 @@ def test_create_update_bucket_and_put_object(): def test_create_with_complex_param_in_json(network_resources): - input_file_path = os.path.join(INPUT_FILE_FOLDER, 'create-security-list.json') - with(open(input_file_path, 'w')) as f: - f.write(CREATE_SECURITY_LIST_TEMPLATE.format(util.COMPARTMENT_ID, network_resources[0])) - - result = invoke(['network', 'security-list', 'create', '--from-json', 'file://{}'.format(input_file_path)]) - security_list_ocid = util.find_id_in_response(result.output) - parsed_output = json.loads(result.output) - - expected_egress = [ - {'destination': '0.0.0.0/0', 'icmp-options': None, 'is-stateless': None, 'protocol': 'all', 'tcp-options': None, 'udp-options': None} - ] - expected_ingress = [ - { - 'icmp-options': None, - 'is-stateless': None, - 'protocol': '6', - 'source': '0.0.0.0/0', - 'tcp-options': {'destination-port-range': {'max': 22, 'min': 22}, 'source-port-range': None}, - 'udp-options': None - }, - { - 'icmp-options': {'code': 4, 'type': 3}, - 'is-stateless': None, - 'protocol': '1', - 'source': '0.0.0.0/0', - 'tcp-options': None, - 'udp-options': None - }, - { - 'icmp-options': {'code': None, 'type': 3}, - 'is-stateless': None, - 'protocol': '1', - 'source': '10.0.0.0/16', - 'tcp-options': None, - 'udp-options': None - } - ] - - assert parsed_output['data']['display-name'] == 'JSON Skeleton Test List' - assert parsed_output['data']['egress-security-rules'] == expected_egress - assert parsed_output['data']['ingress-security-rules'] == expected_ingress - - invoke(['network', 'security-list', 'delete', '--security-list-id', security_list_ocid, '--force']) - util.wait_until(['network', 'security-list', 'get', '--security-list-id', security_list_ocid], 'TERMINATED', max_wait_seconds=600, succeed_if_not_found=True) + with test_config_container.create_vcr().use_cassette('json_skeleton_command_invoke_create_with_complex_param_in_json.yml'): + input_file_path = os.path.join(INPUT_FILE_FOLDER, 'create-security-list.json') + with(open(input_file_path, 'w')) as f: + f.write(CREATE_SECURITY_LIST_TEMPLATE.format(util.COMPARTMENT_ID, network_resources[0])) + + result = invoke(['network', 'security-list', 'create', '--from-json', 'file://{}'.format(input_file_path)]) + security_list_ocid = util.find_id_in_response(result.output) + parsed_output = json.loads(result.output) + + expected_egress = [ + {'destination': '0.0.0.0/0', 'icmp-options': None, 'is-stateless': None, 'protocol': 'all', 'tcp-options': None, 'udp-options': None} + ] + expected_ingress = [ + { + 'icmp-options': None, + 'is-stateless': None, + 'protocol': '6', + 'source': '0.0.0.0/0', + 'tcp-options': {'destination-port-range': {'max': 22, 'min': 22}, 'source-port-range': None}, + 'udp-options': None + }, + { + 'icmp-options': {'code': 4, 'type': 3}, + 'is-stateless': None, + 'protocol': '1', + 'source': '0.0.0.0/0', + 'tcp-options': None, + 'udp-options': None + }, + { + 'icmp-options': {'code': None, 'type': 3}, + 'is-stateless': None, + 'protocol': '1', + 'source': '10.0.0.0/16', + 'tcp-options': None, + 'udp-options': None + } + ] + + assert parsed_output['data']['display-name'] == 'JSON Skeleton Test List' + assert parsed_output['data']['egress-security-rules'] == expected_egress + assert parsed_output['data']['ingress-security-rules'] == expected_ingress + + invoke(['network', 'security-list', 'delete', '--security-list-id', security_list_ocid, '--force']) + util.wait_until(['network', 'security-list', 'get', '--security-list-id', security_list_ocid], 'TERMINATED', max_wait_seconds=600, succeed_if_not_found=True) @util.slow def test_launch_instance(network_resources): - launch_instance_json = 'file://{}'.format(os.path.join('tests', 'resources', 'json_input', 'launch_instance.json')) - image_id = util.oracle_linux_image() - shape = 'VM.Standard1.4' # This overrides the shape in the JSON - hostname_label = util.random_name('bminstance', insert_underscore=False) - private_ip = '10.0.0.15' - - launch_instance_result = util.invoke_command([ - 'compute', 'instance', 'launch', - '--compartment-id', util.COMPARTMENT_ID, - '--availability-domain', util.availability_domain(), - '--subnet-id', network_resources[1], - '--image-id', image_id, - '--shape', shape, - '--hostname-label', hostname_label, - '--private-ip', private_ip, - '--from-json', launch_instance_json - ]) - - if (launch_instance_result.output and 'LimitExceeded' in launch_instance_result.output) or (launch_instance_result.exception and 'LimitExceeded' in str(launch_instance_result.exception)): - pytest.skip('Skipping test_launch_instance as we received a limit exceeded error from the service') - - instance_ocid = util.find_id_in_response(launch_instance_result.output) - - parsed_result = json.loads(launch_instance_result.output) - assert parsed_result['data']['shape'] == 'VM.Standard1.4' - assert parsed_result['data']['display-name'] == 'From JSON Skeleton' - assert parsed_result['data']['metadata'] == {'meta1': 'meta2', 'meta3': 'meta4'} - - expected_extended_metadata = { - 'a': '1', - 'b': { - 'c': '3', - 'd': {} - }, - 'preserve_underscore': 'underscores retained', - 'preserve-snake': 'snake casing-retained' - } - extended_metadata_result = parsed_result['data']['extended-metadata'] - assert expected_extended_metadata == extended_metadata_result - - content = None - with open(os.path.join('tests', 'resources', 'ipxe_script_example.txt'), mode='r') as file: - content = file.read() - - assert 'ipxe-script' in launch_instance_result.output - # Just look at the first few characters. Once we hit a line break the formatting will differ. - assert content[:5] in launch_instance_result.output - - util.wait_until(['compute', 'instance', 'get', '--instance-id', instance_ocid], 'RUNNING', max_wait_seconds=600) - - # We can also provide JSON as a string if desired - result = invoke(['compute', 'instance', 'list-vnics', '--from-json', '{{"instanceId": "{}"}}'.format(instance_ocid)]) - parsed_result = json.loads(result.output) - assert len(parsed_result['data']) == 1 - assert parsed_result['data'][0]['public-ip'] is None # No public IP in launch_instance.json - assert parsed_result['data'][0]['display-name'] == 'JSON Skeleton VNIC' - assert parsed_result['data'][0]['private-ip'] == '10.0.0.15' - - result = invoke(['compute', 'instance', 'terminate', '--instance-id', instance_ocid, '--force']) - util.validate_response(result) - util.wait_until(['compute', 'instance', 'get', '--instance-id', instance_ocid], 'TERMINATED', max_wait_seconds=600, succeed_if_not_found=True) + with test_config_container.create_vcr().use_cassette('json_skeleton_command_invoke_launch_instance.yml'): + launch_instance_json = 'file://{}'.format(os.path.join('tests', 'resources', 'json_input', 'launch_instance.json')) + image_id = util.oracle_linux_image() + shape = 'VM.Standard1.4' # This overrides the shape in the JSON + hostname_label = util.random_name('bminstance', insert_underscore=False) + private_ip = '10.0.0.15' + + launch_instance_result = util.invoke_command([ + 'compute', 'instance', 'launch', + '--compartment-id', util.COMPARTMENT_ID, + '--availability-domain', util.availability_domain(), + '--subnet-id', network_resources[1], + '--image-id', image_id, + '--shape', shape, + '--hostname-label', hostname_label, + '--private-ip', private_ip, + '--from-json', launch_instance_json + ]) + + if (launch_instance_result.output and 'LimitExceeded' in launch_instance_result.output) or (launch_instance_result.exception and 'LimitExceeded' in str(launch_instance_result.exception)): + pytest.skip('Skipping test_launch_instance as we received a limit exceeded error from the service') + + instance_ocid = util.find_id_in_response(launch_instance_result.output) + + parsed_result = json.loads(launch_instance_result.output) + assert parsed_result['data']['shape'] == 'VM.Standard1.4' + assert parsed_result['data']['display-name'] == 'From JSON Skeleton' + assert parsed_result['data']['metadata'] == {'meta1': 'meta2', 'meta3': 'meta4'} + + expected_extended_metadata = { + 'a': '1', + 'b': { + 'c': '3', + 'd': {} + }, + 'preserve_underscore': 'underscores retained', + 'preserve-snake': 'snake casing-retained' + } + extended_metadata_result = parsed_result['data']['extended-metadata'] + assert expected_extended_metadata == extended_metadata_result + + content = None + with open(os.path.join('tests', 'resources', 'ipxe_script_example.txt'), mode='r') as file: + content = file.read() + + assert 'ipxe-script' in launch_instance_result.output + # Just look at the first few characters. Once we hit a line break the formatting will differ. + assert content[:5] in launch_instance_result.output + + util.wait_until(['compute', 'instance', 'get', '--instance-id', instance_ocid], 'RUNNING', max_wait_seconds=600) + + # We can also provide JSON as a string if desired + result = invoke(['compute', 'instance', 'list-vnics', '--from-json', '{{"instanceId": "{}"}}'.format(instance_ocid)]) + parsed_result = json.loads(result.output) + assert len(parsed_result['data']) == 1 + assert parsed_result['data'][0]['public-ip'] is None # No public IP in launch_instance.json + assert parsed_result['data'][0]['display-name'] == 'JSON Skeleton VNIC' + assert parsed_result['data'][0]['private-ip'] == '10.0.0.15' + + result = invoke(['compute', 'instance', 'terminate', '--instance-id', instance_ocid, '--force']) + util.validate_response(result) + util.wait_until(['compute', 'instance', 'get', '--instance-id', instance_ocid], 'TERMINATED', max_wait_seconds=600, succeed_if_not_found=True) def test_generate_example_metadata_on_object_put(): diff --git a/tests/test_launch_instance_options.py b/tests/test_launch_instance_options.py index 5b5df173a..fc2c7854a 100644 --- a/tests/test_launch_instance_options.py +++ b/tests/test_launch_instance_options.py @@ -3,6 +3,7 @@ import json import unittest +from . import test_config_container from . import util import oci_cli @@ -14,6 +15,7 @@ class TestLaunchInstanceOptions(unittest.TestCase): @util.slow + @test_config_container.RecordReplayWithNoClickContext('launch_instance_options') def test_main(self): self.instance_ocids = [] diff --git a/tests/test_list_filter.py b/tests/test_list_filter.py index 2b3a85f0a..6b76ceac3 100644 --- a/tests/test_list_filter.py +++ b/tests/test_list_filter.py @@ -1,23 +1,27 @@ from . import util +from . import test_config_container import json import pytest -import time +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_images_by_state_no_results(): retrieve_list_by_field_and_check(['compute', 'image', 'list', '-c', util.COMPARTMENT_ID, '--lifecycle-state', 'DISABLED'], 'lifecycle-state', 'DISABLED', 0) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_images_by_state_with_results(): retrieve_list_by_field_and_check(['compute', 'image', 'list', '-c', util.COMPARTMENT_ID, '--lifecycle-state', 'AVAILABLE'], 'lifecycle-state', 'AVAILABLE', match_at_least_one=True) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_images_by_display_name_without_results(): - display_name = str(time.time()) + display_name = 'this-is-a-fake-name-{}'.format(util.random_number_string()) retrieve_list_by_field_and_check(['compute', 'image', 'list', '-c', util.COMPARTMENT_ID, '--display-name', display_name], 'display-name', display_name, 0) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_images_by_display_name_with_results(): retrieve_list_by_field_and_check( ['compute', 'image', 'list', '-c', util.COMPARTMENT_ID, '--display-name', 'Windows-Server-2012-R2-Standard-Edition-VM-2017.07.25-0'], @@ -27,15 +31,18 @@ def test_list_images_by_display_name_with_results(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_instances_by_state(): retrieve_list_by_field_and_check(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--lifecycle-state', 'TERMINATED', '--all'], 'lifecycle-state', 'TERMINATED') +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_instances_by_display_name_without_results(): # Yes, this test will break if someone calls their instance "purple monkey dishwasher" retrieve_list_by_field_and_check(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--display-name', 'purple monkey dishwasher', '--all'], 'display-name', 'purple monkey dishwasher') +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_instances_by_display_name_with_results(): all_instances = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--all']) util.validate_response(all_instances) @@ -49,6 +56,7 @@ def test_list_instances_by_display_name_with_results(): pytest.skip('Skipped test as there are no instances in any state') +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_instances_with_ad_sorted_by_display_name(): retrieve_list_and_ensure_sorted( ['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--availability-domain', util.availability_domain(), '--sort-by', 'DISPLAYNAME', '--sort-order', 'asc'], @@ -62,6 +70,7 @@ def test_list_instances_with_ad_sorted_by_display_name(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_instances_all_by_display_name_is_sorted(): retrieve_list_and_ensure_sorted( ['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--sort-by', 'DISPLAYNAME', '--sort-order', 'asc', '--all'], @@ -75,6 +84,7 @@ def test_list_instances_all_by_display_name_is_sorted(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_instances_all_by_time_created_is_sorted(): retrieve_list_and_ensure_sorted( ['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--sort-by', 'TIMECREATED', '--sort-order', 'asc', '--all'], @@ -88,6 +98,7 @@ def test_list_instances_all_by_time_created_is_sorted(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_instances_with_ad_sorted_by_time_created(): retrieve_list_and_ensure_sorted( ['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--availability-domain', util.availability_domain(), '--sort-by', 'TIMECREATED', '--sort-order', 'asc'], @@ -101,11 +112,13 @@ def test_list_instances_with_ad_sorted_by_time_created(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_console_histories_by_state(): # The enums are case insensitive so "requested" == "REQUESTED" retrieve_list_by_field_and_check(['compute', 'console-history', 'list', '-c', util.COMPARTMENT_ID, '--lifecycle-state', 'requested'], 'lifecycle-state', 'REQUESTED') +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_console_histories_with_ad_sorted_by_time_created(): retrieve_list_and_ensure_sorted( ['compute', 'console-history', 'list', '-c', util.COMPARTMENT_ID, '--availability-domain', util.availability_domain(), '--sort-by', 'TIMECREATED', '--sort-order', 'asc'], @@ -119,15 +132,18 @@ def test_list_console_histories_with_ad_sorted_by_time_created(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_volumes_by_state_no_results(): # Hopefully there are never any faulty volumes retrieve_list_by_field_and_check(['bv', 'volume', 'list', '-c', util.COMPARTMENT_ID, '--lifecycle-state', 'FAULTY'], 'lifecycle-state', 'FAULTY', 0) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_volumes_by_state(): retrieve_list_by_field_and_check(['bv', 'volume', 'list', '-c', util.COMPARTMENT_ID, '--lifecycle-state', 'TERMINATED'], 'lifecycle-state', 'TERMINATED') +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_volumes_sorted_by_display_name(): retrieve_list_and_ensure_sorted( ['bv', 'volume', 'list', '-c', util.COMPARTMENT_ID, '--availability-domain', util.availability_domain(), '--sort-by', 'DISPLAYNAME', '--sort-order', 'asc'], @@ -141,6 +157,7 @@ def test_volumes_sorted_by_display_name(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_volumes_sorted_by_time_created(): retrieve_list_and_ensure_sorted( ['bv', 'volume', 'list', '-c', util.COMPARTMENT_ID, '--availability-domain', util.availability_domain(), '--sort-by', 'TIMECREATED', '--sort-order', 'asc', '--all'], @@ -154,15 +171,18 @@ def test_volumes_sorted_by_time_created(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_volume_backups_by_state_no_results(): # Hopefully there are never any faulty backups retrieve_list_by_field_and_check(['bv', 'backup', 'list', '-c', util.COMPARTMENT_ID, '--lifecycle-state', 'FAULTY'], 'lifecycle-state', 'FAULTY', 0) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_volume_backups_by_state(): retrieve_list_by_field_and_check(['bv', 'backup', 'list', '-c', util.COMPARTMENT_ID, '--lifecycle-state', 'TERMINATED'], 'lifecycle-state', 'TERMINATED') +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_volume_backups_sorted_by_display_name(): retrieve_list_and_ensure_sorted( ['bv', 'backup', 'list', '-c', util.COMPARTMENT_ID, '--sort-by', 'DISPLAYNAME', '--sort-order', 'asc'], @@ -176,6 +196,7 @@ def test_volume_backups_sorted_by_display_name(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_volume_backups_sorted_by_time_created(): retrieve_list_and_ensure_sorted( ['bv', 'backup', 'list', '-c', util.COMPARTMENT_ID, '--sort-by', 'TIMECREATED', '--sort-order', 'asc', '--all'], @@ -189,10 +210,12 @@ def test_volume_backups_sorted_by_time_created(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_vcns_by_state(): retrieve_list_by_field_and_check(['network', 'vcn', 'list', '-c', util.COMPARTMENT_ID, '--lifecycle-state', 'AVAILABLE', '--all'], 'lifecycle-state', 'AVAILABLE') +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_vcns_sorted_by_display_name(): retrieve_list_and_ensure_sorted( ['network', 'vcn', 'list', '-c', util.COMPARTMENT_ID, '--sort-by', 'DISPLAYNAME', '--sort-order', 'asc'], @@ -206,6 +229,7 @@ def test_vcns_sorted_by_display_name(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_vcns_sorted_by_time_created(): retrieve_list_and_ensure_sorted( ['network', 'vcn', 'list', '-c', util.COMPARTMENT_ID, '--sort-by', 'TIMECREATED', '--sort-order', 'asc', '--all'], @@ -219,6 +243,7 @@ def test_vcns_sorted_by_time_created(): ) +@test_config_container.RecordReplayWithNoClickContext('list_filter') def test_must_provide_ad_when_sorting(): result = invoke(['compute', 'instance', 'list', '-c', util.COMPARTMENT_ID, '--sort-by', 'DISPLAYNAME']) assert 'You must provide an --availability-domain when doing a --sort-by, unless you specify the --all parameter' in result.output diff --git a/tests/test_load_balancer.py b/tests/test_load_balancer.py index ac6018e13..5f875381d 100644 --- a/tests/test_load_balancer.py +++ b/tests/test_load_balancer.py @@ -5,6 +5,7 @@ import oci_cli import os import pytest +from . import test_config_container from . import util LB_PROVISIONING_TIME_SEC = 300 # 5 minutes @@ -17,501 +18,758 @@ @pytest.fixture(scope='module') def load_balancer(runner, config_file, config_profile, vcn_and_subnets): - subnet_ocid_1 = vcn_and_subnets[1] - subnet_ocid_2 = vcn_and_subnets[2] + with test_config_container.create_vcr().use_cassette('test_load_balancer_fixture_lb.yml'): + subnet_ocid_1 = vcn_and_subnets[1] + subnet_ocid_2 = vcn_and_subnets[2] - params = [ - 'load-balancer', 'create', - '-c', util.COMPARTMENT_ID, - '--display-name', util.random_name('cli_lb'), - '--shape-name', '100Mbps', - '--subnet-ids', '["{}","{}"]'.format(subnet_ocid_1, subnet_ocid_2) - ] + params = [ + 'load-balancer', 'create', + '-c', util.COMPARTMENT_ID, + '--display-name', util.random_name('cli_lb'), + '--shape-name', '100Mbps', + '--subnet-ids', '["{}","{}"]'.format(subnet_ocid_1, subnet_ocid_2) + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - # create lb returns work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] + # create lb returns work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) - util.validate_response(get_work_request_result) + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) + util.validate_response(get_work_request_result) - lb_ocid = json.loads(get_work_request_result.output)['data']['load-balancer-id'] + lb_ocid = json.loads(get_work_request_result.output)['data']['load-balancer-id'] - yield lb_ocid + yield lb_ocid - params = [ - 'load-balancer', 'delete', - '--load-balancer-id', lb_ocid, - '--force' - ] + with test_config_container.create_vcr().use_cassette('test_load_balancer_fixture_lb_delete.yml'): + params = [ + 'load-balancer', 'delete', + '--load-balancer-id', lb_ocid, + '--force' + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - util.wait_until(['lb', 'load-balancer', 'get', '--load-balancer-id', lb_ocid], 'TERMINATED', max_wait_seconds=LB_PROVISIONING_TIME_SEC, succeed_if_not_found=True) + util.wait_until(['lb', 'load-balancer', 'get', '--load-balancer-id', lb_ocid], 'TERMINATED', max_wait_seconds=LB_PROVISIONING_TIME_SEC, succeed_if_not_found=True) @pytest.fixture(scope='module') def backend_set(runner, config_file, config_profile, load_balancer): - backend_set_name = util.random_name('cli_lb_backend_set') + with test_config_container.create_vcr().use_cassette('test_load_balancer_fixture_backend_set.yml'): + backend_set_name = util.random_name('cli_lb_backend_set') - params = [ - 'backend-set', 'create', - '--name', backend_set_name, - '--policy', 'ROUND_ROBIN', - '--load-balancer-id', load_balancer, - '--health-checker-protocol', 'HTTP', - '--health-checker-return-code', '200', - '--health-checker-url-path', '/healthcheck', - '--health-checker-interval-in-ms', '60000', # 1 minute - '--session-persistence-cookie-name', '*', - '--session-persistence-disable-fallback', 'false' - ] + params = [ + 'backend-set', 'create', + '--name', backend_set_name, + '--policy', 'ROUND_ROBIN', + '--load-balancer-id', load_balancer, + '--health-checker-protocol', 'HTTP', + '--health-checker-return-code', '200', + '--health-checker-url-path', '/healthcheck', + '--health-checker-interval-in-ms', '60000', # 1 minute + '--session-persistence-cookie-name', '*', + '--session-persistence-disable-fallback', 'false' + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - # create lb returns work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] + # create lb returns work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) - util.validate_response(get_work_request_result) + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) + util.validate_response(get_work_request_result) - yield backend_set_name + yield backend_set_name - params = [ - 'backend-set', 'delete', - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set_name, - '--force' - ] + with test_config_container.create_vcr().use_cassette('test_load_balancer_fixture_backend_set_delete.yml'): + params = [ + 'backend-set', 'delete', + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set_name, + '--force' + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) - util.validate_response(get_work_request_result) + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) + util.validate_response(get_work_request_result) -@pytest.fixture +@pytest.fixture(scope='module') def backend(runner, config_file, config_profile, load_balancer, backend_set): - ip_address = '10.0.0.10' - port = '80' - params = [ - 'backend', 'create', - '--ip-address', ip_address, - '--port', port, - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set, - '--weight', '3' - ] - - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) - - # returns work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] - - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) - util.validate_response(get_work_request_result) - - # backend name defaults to "ipaddress:port" - backend_name = "{}:{}".format(ip_address, port) - yield backend_name - - params = [ - 'backend', 'delete', - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set, - '--backend-name', backend_name, - '--force' - ] - - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + with test_config_container.create_vcr().use_cassette('test_load_balancer_fixture_backend.yml'): + ip_address = '10.0.0.10' + port = '80' + params = [ + 'backend', 'create', + '--ip-address', ip_address, + '--port', port, + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set, + '--weight', '3' + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + # returns work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] + + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) + util.validate_response(get_work_request_result) + + # backend name defaults to "ipaddress:port" + backend_name = "{}:{}".format(ip_address, port) + yield backend_name + + with test_config_container.create_vcr().use_cassette('test_load_balancer_fixture_backend_delete.yml'): + params = [ + 'backend', 'delete', + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set, + '--backend-name', backend_name, + '--force' + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + # returns work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] + + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) + util.validate_response(get_work_request_result) - # returns work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) - util.validate_response(get_work_request_result) - - -@pytest.fixture +@pytest.fixture(scope='module') def certificate(runner, config_file, config_profile, load_balancer, key_pair_files): - private_key_filename = key_pair_files[1] - certificate_filename = key_pair_files[2] + with test_config_container.create_vcr().use_cassette('test_load_balancer_fixture_certificate.yml'): + private_key_filename = key_pair_files[1] + certificate_filename = key_pair_files[2] - cert_name = util.random_name('cli_lb_certificate') + cert_name = util.random_name('cli_lb_certificate') - params = [ - 'certificate', 'create', - '--certificate-name', cert_name, - '--load-balancer-id', load_balancer, - '--ca-certificate-file', certificate_filename, - '--private-key-file', private_key_filename, - '--public-certificate-file', certificate_filename, - '--passphrase', LB_PRIVATE_KEY_PASSPHRASE - ] + params = [ + 'certificate', 'create', + '--certificate-name', cert_name, + '--load-balancer-id', load_balancer, + '--ca-certificate-file', certificate_filename, + '--private-key-file', private_key_filename, + '--public-certificate-file', certificate_filename, + '--passphrase', LB_PRIVATE_KEY_PASSPHRASE + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - # returns work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] + # returns work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) - util.validate_response(get_work_request_result) + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) + util.validate_response(get_work_request_result) - yield cert_name + yield cert_name - # delete cert - params = [ - 'certificate', 'delete', - '--load-balancer-id', load_balancer, - '--certificate-name', cert_name, - '--force' - ] + with test_config_container.create_vcr().use_cassette('test_load_balancer_fixture_certificate_delete.yml'): + # delete cert + params = [ + 'certificate', 'delete', + '--load-balancer-id', load_balancer, + '--certificate-name', cert_name, + '--force' + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) - util.validate_response(get_work_request_result) + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) + util.validate_response(get_work_request_result) @util.slow def test_load_balancer_operations(runner, config_file, config_profile, load_balancer): - # list - params = [ - 'load-balancer', 'list', - '-c', util.COMPARTMENT_ID - ] + with test_config_container.create_vcr().use_cassette('test_load_balancer_lb_operations.yml'): + # list + params = [ + 'load-balancer', 'list', + '-c', util.COMPARTMENT_ID + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - load_balancers = json.loads(result.output)['data'] + load_balancers = json.loads(result.output)['data'] - found_lb = False - for lb in load_balancers: - if lb['id'] == load_balancer: - found_lb = True + found_lb = False + for lb in load_balancers: + if lb['id'] == load_balancer: + found_lb = True - assert found_lb + assert found_lb - # update - # params = [ - # 'load-balancer', 'update', - # '--load-balancer-id', load_balancer, - # '--display-name', util.random_name('cli_lb_updated') - # ] + # update + # params = [ + # 'load-balancer', 'update', + # '--load-balancer-id', load_balancer, + # '--display-name', util.random_name('cli_lb_updated') + # ] - # result = invoke(runner, config_file, config_profile, params) - # util.validate_response(result) + # result = invoke(runner, config_file, config_profile, params) + # util.validate_response(result) - # # returns work request - # response = json.loads(result.output) - # work_request_ocid = response['opc-work-request-id'] + # # returns work request + # response = json.loads(result.output) + # work_request_ocid = response['opc-work-request-id'] - # get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) - # util.validate_response(get_work_request_result) + # get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) + # util.validate_response(get_work_request_result) - # get - params = [ - 'load-balancer', 'get', - '--load-balancer-id', load_balancer - ] + # get + params = [ + 'load-balancer', 'get', + '--load-balancer-id', load_balancer + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - # assert 'cli_lb_updated' in json.loads(result.output)['data']['display-name'] + # assert 'cli_lb_updated' in json.loads(result.output)['data']['display-name'] @util.slow def test_certificate_operations(runner, config_file, config_profile, load_balancer, certificate): - params = [ - 'certificate', 'list', - '--load-balancer-id', load_balancer - ] + with test_config_container.create_vcr().use_cassette('test_load_balancer_cert_operations.yml'): + params = [ + 'certificate', 'list', + '--load-balancer-id', load_balancer + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - response = json.loads(result.output) - found_cert = False - for cert in response['data']: - if cert['certificate-name'] == certificate: - found_cert = True + response = json.loads(result.output) + found_cert = False + for cert in response['data']: + if cert['certificate-name'] == certificate: + found_cert = True - assert found_cert + assert found_cert @util.slow def test_backend_set_operations(runner, config_file, config_profile, load_balancer, backend_set): - # fixture handles create / delete - params = [ - 'backend-set', 'list', - '--load-balancer-id', load_balancer - ] + with test_config_container.create_vcr().use_cassette('test_load_balancer_backend_set_operations.yml'): + # fixture handles create / delete + params = [ + 'backend-set', 'list', + '--load-balancer-id', load_balancer + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + params = [ + 'backend-set', 'get', + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + params = [ + 'backend-set', 'update', + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set, + '--backends', '[]', + '--policy', 'ROUND_ROBIN', + '--health-checker-protocol', 'HTTP', + '--health-checker-url-path', '/healthchecker', + '--force' + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + # returns work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] + + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) + util.validate_response(get_work_request_result) - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) - - params = [ - 'backend-set', 'get', - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set - ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) +@util.slow +def test_backend_operations(runner, config_file, config_profile, load_balancer, backend_set, backend): + with test_config_container.create_vcr().use_cassette('test_load_balancer_backend_operations.yml'): + # fixture handles create / delete + params = [ + 'backend', 'list', + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + params = [ + 'backend', 'update', + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set, + '--backend-name', backend, + '--weight', '2', + '--offline', 'true', + '--backup', 'false', + '--drain', 'false' + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + # returns work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] + + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) + util.validate_response(get_work_request_result) - params = [ - 'backend-set', 'update', - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set, - '--backends', '[]', - '--policy', 'ROUND_ROBIN', - '--health-checker-protocol', 'HTTP', - '--health-checker-url-path', '/healthchecker', - '--force' - ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) +@util.slow +def test_listener_operations(runner, config_file, config_profile, load_balancer, backend_set, certificate): + with test_config_container.create_vcr().use_cassette('test_load_balancer_listener_operations.yml'): + # create listener + listener_name = util.random_name('cli_listener') + params = [ + 'listener', 'create', + '--default-backend-set-name', backend_set, + '--load-balancer-id', load_balancer, + '--name', listener_name, + '--port', '8080', + '--protocol', 'HTTP', + '--ssl-certificate-name', certificate + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + # returns a work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] + + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) + util.validate_response(get_work_request_result) + + # update listener + params = [ + 'listener', 'update', + '--listener-name', listener_name, + '--default-backend-set-name', backend_set, + '--load-balancer-id', load_balancer, + '--port', '8080', + '--protocol', 'HTTP', + '--ssl-certificate-name', certificate, + '--force' + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + # returns a work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] + + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) + util.validate_response(get_work_request_result) + + # delete listener + params = [ + 'listener', 'delete', + '--load-balancer-id', load_balancer, + '--listener-name', listener_name, + '--force' + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + # returns a work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] + + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) + util.validate_response(get_work_request_result) - # returns work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) - util.validate_response(get_work_request_result) +@util.slow +def test_listener_with_connection_timeout_operations(runner, config_file, config_profile, load_balancer, backend_set): + with test_config_container.create_vcr().use_cassette('test_load_balancer_listener_with_connection_timeout_operations.yml'): + listener_name = util.random_name('cli_listener_ct') + params = [ + 'listener', 'create', + '--default-backend-set-name', backend_set, + '--load-balancer-id', load_balancer, + '--name', listener_name, + '--port', '8080', + '--protocol', 'HTTP', + '--connection-configuration-idle-timeout', '100', + '--wait-for-state', 'SUCCEEDED' + ] + result = invoke(runner, config_file, config_profile, params) + _validate_work_request_result(result, load_balancer) + + result = invoke(runner, config_file, config_profile, ['load-balancer', 'get', '--load-balancer-id', load_balancer]) + parsed_result = json.loads(result.output) + assert parsed_result['data']['listeners'][listener_name]['connection-configuration']['idle-timeout'] == 100 + + params = [ + 'listener', 'update', + '--listener-name', listener_name, + '--default-backend-set-name', backend_set, + '--load-balancer-id', load_balancer, + '--port', '8080', + '--protocol', 'HTTP', + '--connection-configuration-idle-timeout', '75', + '--force', + '--wait-for-state', 'SUCCEEDED' + ] + result = invoke(runner, config_file, config_profile, params) + _validate_work_request_result(result, load_balancer) + + result = invoke(runner, config_file, config_profile, ['load-balancer', 'get', '--load-balancer-id', load_balancer]) + parsed_result = json.loads(result.output) + assert parsed_result['data']['listeners'][listener_name]['connection-configuration']['idle-timeout'] == 75 + + params = [ + 'listener', 'delete', + '--load-balancer-id', load_balancer, + '--listener-name', listener_name, + '--force', + '--wait-for-state', 'SUCCEEDED' + ] + result = invoke(runner, config_file, config_profile, params) + _validate_work_request_result(result, load_balancer) @util.slow -def test_backend_operations(runner, config_file, config_profile, load_balancer, backend_set, backend): - # fixture handles create / delete - params = [ - 'backend', 'list', - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set - ] +def test_load_balancer_health_operations(runner, config_file, config_profile, load_balancer): + with test_config_container.create_vcr().use_cassette('test_load_balancer_lb_health_operations.yml'): + params = [ + 'load-balancer-health', 'get', + '--load-balancer-id', load_balancer + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - params = [ - 'backend', 'update', - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set, - '--backend-name', backend, - '--weight', '2', - '--offline', 'true', - '--backup', 'false', - '--drain', 'false' - ] + params = [ + 'load-balancer-health', 'list', + '-c', util.COMPARTMENT_ID + ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - # returns work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=DEFAULT_WAIT_TIME) - util.validate_response(get_work_request_result) +@util.slow +def test_backend_set_health_operations(runner, config_file, config_profile, load_balancer, backend_set): + with test_config_container.create_vcr().use_cassette('test_load_balancer_backend_set_health_operations.yml'): + params = [ + 'backend-set-health', 'get', + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set + ] + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) -@util.slow -def test_listener_operations(runner, config_file, config_profile, load_balancer, backend_set, certificate): - # create listener - listener_name = util.random_name('cli_listener') - params = [ - 'listener', 'create', - '--default-backend-set-name', backend_set, - '--load-balancer-id', load_balancer, - '--name', listener_name, - '--port', '8080', - '--protocol', 'HTTP', - '--ssl-certificate-name', certificate - ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) +@util.slow +def test_backend_health_operations(runner, config_file, config_profile, load_balancer, backend_set, backend): + with test_config_container.create_vcr().use_cassette('test_load_balancer_backend_health_operations.yml'): + params = [ + 'backend-health', 'get', + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set, + '--backend-name', backend + ] - # returns a work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) - util.validate_response(get_work_request_result) - # update listener - params = [ - 'listener', 'update', - '--listener-name', listener_name, - '--default-backend-set-name', backend_set, - '--load-balancer-id', load_balancer, - '--port', '8080', - '--protocol', 'HTTP', - '--ssl-certificate-name', certificate, - '--force' - ] +@util.slow +def test_health_checker_operations(runner, config_file, config_profile, load_balancer, backend_set): + with test_config_container.create_vcr().use_cassette('test_load_balancer_health_checker_operations.yml'): + params = [ + 'health-checker', 'update', + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set, + '--interval-in-millis', '15000', + '--port', '80', + '--protocol', 'HTTP', + '--response-body-regex', '.*', + '--retries', '3', + '--return-code', '200', + '--timeout-in-millis', '1000', + '--url-path', '/healthcheck' + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + # health-checker update returns work request + response = json.loads(result.output) + work_request_ocid = response['opc-work-request-id'] + + get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) + util.validate_response(get_work_request_result) + + params = [ + 'health-checker', 'get', + '--load-balancer-id', load_balancer, + '--backend-set-name', backend_set + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + assert 15000 == json.loads(result.output)['data']['interval-in-millis'] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) - # returns a work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] +def test_list_lb_shapes(runner, config_file, config_profile): + with test_config_container.create_vcr().use_cassette('test_load_balancer_lb_shapes.yml'): + params = [ + 'shape', 'list', + '-c', util.COMPARTMENT_ID + ] - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) - util.validate_response(get_work_request_result) + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - # delete listener - params = [ - 'listener', 'delete', - '--load-balancer-id', load_balancer, - '--listener-name', listener_name, - '--force' - ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) +def test_list_lb_protocols(runner, config_file, config_profile): + with test_config_container.create_vcr().use_cassette('test_load_balancer_lb_protocols.yml'): + params = [ + 'protocol', 'list', + '-c', util.COMPARTMENT_ID + ] - # returns a work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) - util.validate_response(get_work_request_result) +def test_list_lb_policy(runner, config_file, config_profile): + with test_config_container.create_vcr().use_cassette('test_load_balancer_lb_policy.yml'): + params = [ + 'policy', 'list', + '-c', util.COMPARTMENT_ID + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + +def test_load_balancer_operations_with_waiters(runner, config_file, config_profile, vcn_and_subnets, key_pair_files): + with test_config_container.create_vcr().use_cassette('test_load_balancer_ops_with_waiters.yml'): + subnet_ocid_1 = vcn_and_subnets[1] + subnet_ocid_2 = vcn_and_subnets[2] + + lb_name = util.random_name('cli_lb') + params = [ + 'load-balancer', 'create', + '-c', util.COMPARTMENT_ID, + '--display-name', lb_name, + '--shape-name', '100Mbps', + '--subnet-ids', '["{}","{}"]'.format(subnet_ocid_1, subnet_ocid_2), + '--wait-for-state', 'SUCCEEDED' + ] + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result, json_response_expected=False) + load_balancer = util.get_json_from_mixed_string(result.output) + assert load_balancer['data']['lifecycle-state'] == 'ACTIVE' + assert 'loadbalancer' in load_balancer['data']['id'] + assert load_balancer['data']['display-name'] == lb_name + assert load_balancer['data']['shape-name'] == '100Mbps' + assert len(load_balancer['data']['subnet-ids']) == 2 + assert subnet_ocid_1 in load_balancer['data']['subnet-ids'] + assert subnet_ocid_2 in load_balancer['data']['subnet-ids'] + + _do_backend_and_backend_set_waiters(runner, load_balancer['data']['id'], config_file, config_profile) + _do_certificate_waiters(runner, load_balancer['data']['id'], config_file, config_profile, key_pair_files) + + params = [ + 'load-balancer', 'delete', + '--load-balancer-id', load_balancer['data']['id'], + '--force', + '--wait-for-state', 'SUCCEEDED' + ] + result = invoke(runner, config_file, config_profile, params) + _validate_work_request_result(result, load_balancer['data']['id']) + + +def _do_backend_and_backend_set_waiters(runner, load_balancer_id, config_file, config_profile): + backend_set_name = util.random_name('cli_lb_backend_set') -@util.slow -def test_load_balancer_health_operations(runner, config_file, config_profile, load_balancer): params = [ - 'load-balancer-health', 'get', - '--load-balancer-id', load_balancer + 'backend-set', 'create', + '--name', backend_set_name, + '--policy', 'ROUND_ROBIN', + '--load-balancer-id', load_balancer_id, + '--health-checker-protocol', 'HTTP', + '--health-checker-return-code', '200', + '--health-checker-url-path', '/healthcheck', + '--health-checker-interval-in-ms', '60000', # 1 minute + '--session-persistence-cookie-name', '*', + '--session-persistence-disable-fallback', 'false', + '--wait-for-state', 'SUCCEEDED' ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + _validate_work_request_result(result, load_balancer_id) + ip_address = '10.0.0.10' + port = '80' params = [ - 'load-balancer-health', 'list', - '-c', util.COMPARTMENT_ID + 'backend', 'create', + '--ip-address', ip_address, + '--port', port, + '--load-balancer-id', load_balancer_id, + '--backend-set-name', backend_set_name, + '--weight', '3', + '--wait-for-state', 'SUCCEEDED' ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) - + _validate_work_request_result(result, load_balancer_id) -@util.slow -def test_backend_set_health_operations(runner, config_file, config_profile, load_balancer, backend_set): + backend_name = "{}:{}".format(ip_address, port) params = [ - 'backend-set-health', 'get', - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set + 'backend', 'update', + '--load-balancer-id', load_balancer_id, + '--backend-set-name', backend_set_name, + '--backend-name', backend_name, + '--weight', '2', + '--offline', 'true', + '--backup', 'false', + '--drain', 'false', + '--wait-for-state', 'SUCCEEDED' ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) - + _validate_work_request_result(result, load_balancer_id) -@util.slow -def test_backend_health_operations(runner, config_file, config_profile, load_balancer, backend_set, backend): params = [ - 'backend-health', 'get', - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set, - '--backend-name', backend + 'backend', 'delete', + '--load-balancer-id', load_balancer_id, + '--backend-set-name', backend_set_name, + '--backend-name', backend_name, + '--force', + '--wait-for-state', 'SUCCEEDED' ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + _validate_work_request_result(result, load_balancer_id) + _do_listener_waiters(runner, load_balancer_id, backend_set_name, config_file, config_profile) -@util.slow -def test_health_checker_operations(runner, config_file, config_profile, load_balancer, backend_set): params = [ - 'health-checker', 'update', - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set, - '--interval-in-millis', '15000', - '--port', '80', - '--protocol', 'HTTP', - '--response-body-regex', '.*', - '--retries', '3', - '--return-code', '200', - '--timeout-in-millis', '1000', - '--url-path', '/healthcheck' + 'backend-set', 'delete', + '--load-balancer-id', load_balancer_id, + '--backend-set-name', backend_set_name, + '--force', + '--wait-for-state', 'SUCCEEDED' ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + _validate_work_request_result(result, load_balancer_id) - # health-checker update returns work request - response = json.loads(result.output) - work_request_ocid = response['opc-work-request-id'] - get_work_request_result = util.wait_until(['lb', 'work-request', 'get', '--work-request-id', work_request_ocid], 'SUCCEEDED', max_wait_seconds=LB_PROVISIONING_TIME_SEC) - util.validate_response(get_work_request_result) +def _do_certificate_waiters(runner, load_balancer_id, config_file, config_profile, key_pair_files): + private_key_filename = key_pair_files[1] + certificate_filename = key_pair_files[2] + + cert_name = util.random_name('cli_lb_certificate') params = [ - 'health-checker', 'get', - '--load-balancer-id', load_balancer, - '--backend-set-name', backend_set + 'certificate', 'create', + '--certificate-name', cert_name, + '--load-balancer-id', load_balancer_id, + '--ca-certificate-file', certificate_filename, + '--private-key-file', private_key_filename, + '--public-certificate-file', certificate_filename, + '--passphrase', 'secret!', + '--wait-for-state', 'SUCCEEDED' ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) - - assert 15000 == json.loads(result.output)['data']['interval-in-millis'] + _validate_work_request_result(result, load_balancer_id) - -def test_list_lb_shapes(runner, config_file, config_profile): params = [ - 'shape', 'list', - '-c', util.COMPARTMENT_ID + 'certificate', 'delete', + '--load-balancer-id', load_balancer_id, + '--certificate-name', cert_name, + '--force', + '--wait-for-state', 'SUCCEEDED' ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + _validate_work_request_result(result, load_balancer_id) -def test_list_lb_protocols(runner, config_file, config_profile): +def _do_listener_waiters(runner, load_balancer_id, backend_set_name, config_file, config_profile): + listener_name = util.random_name('cli_listener') params = [ - 'protocol', 'list', - '-c', util.COMPARTMENT_ID + 'listener', 'create', + '--default-backend-set-name', backend_set_name, + '--load-balancer-id', load_balancer_id, + '--name', listener_name, + '--port', '8080', + '--protocol', 'HTTP', + '--wait-for-state', 'SUCCEEDED' ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) - + _validate_work_request_result(result, load_balancer_id) -def test_list_lb_policy(runner, config_file, config_profile): params = [ - 'policy', 'list', - '-c', util.COMPARTMENT_ID + 'listener', 'update', + '--listener-name', listener_name, + '--default-backend-set-name', backend_set_name, + '--load-balancer-id', load_balancer_id, + '--port', '8080', + '--protocol', 'HTTP', + '--force', + '--wait-for-state', 'SUCCEEDED' ] + result = invoke(runner, config_file, config_profile, params) + _validate_work_request_result(result, load_balancer_id) + params = [ + 'listener', 'delete', + '--load-balancer-id', load_balancer_id, + '--listener-name', listener_name, + '--force', + '--wait-for-state', 'SUCCEEDED' + ] result = invoke(runner, config_file, config_profile, params) - util.validate_response(result) + _validate_work_request_result(result, load_balancer_id) + + +def _validate_work_request_result(result, load_balancer_id): + util.validate_response(result, json_response_expected=False) + assert 'Action completed. Waiting until the work request has entered state: SUCCEEDED' in result.output + + work_request = util.get_json_from_mixed_string(result.output) + assert work_request['data']['load-balancer-id'] == load_balancer_id + assert work_request['data']['lifecycle-state'] == 'SUCCEEDED' def invoke(runner, config_file, config_profile, params, debug=False, root_params=None, strip_progress_bar=True, strip_multipart_stderr_output=True, ** args): diff --git a/tests/test_load_balancer_waiters.py b/tests/test_load_balancer_waiters.py deleted file mode 100644 index 23fd4f2ad..000000000 --- a/tests/test_load_balancer_waiters.py +++ /dev/null @@ -1,206 +0,0 @@ -# coding: utf-8 -# Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved. - -import oci_cli -from . import util - -LB_PROVISIONING_TIME_SEC = 300 # 5 minutes - -DEFAULT_WAIT_TIME = 120 # 1 minute - - -@util.slow -def test_load_balancer_operations_with_waiters(runner, config_file, config_profile, vcn_and_subnets, key_pair_files): - subnet_ocid_1 = vcn_and_subnets[1] - subnet_ocid_2 = vcn_and_subnets[2] - - lb_name = util.random_name('cli_lb') - params = [ - 'load-balancer', 'create', - '-c', util.COMPARTMENT_ID, - '--display-name', lb_name, - '--shape-name', '100Mbps', - '--subnet-ids', '["{}","{}"]'.format(subnet_ocid_1, subnet_ocid_2), - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - util.validate_response(result, json_response_expected=False) - load_balancer = util.get_json_from_mixed_string(result.output) - assert load_balancer['data']['lifecycle-state'] == 'ACTIVE' - assert 'loadbalancer' in load_balancer['data']['id'] - assert load_balancer['data']['display-name'] == lb_name - assert load_balancer['data']['shape-name'] == '100Mbps' - assert len(load_balancer['data']['subnet-ids']) == 2 - assert subnet_ocid_1 in load_balancer['data']['subnet-ids'] - assert subnet_ocid_2 in load_balancer['data']['subnet-ids'] - - _do_backend_and_backend_set(runner, load_balancer['data']['id'], config_file, config_profile) - _do_certificate(runner, load_balancer['data']['id'], config_file, config_profile, key_pair_files) - - params = [ - 'load-balancer', 'delete', - '--load-balancer-id', load_balancer['data']['id'], - '--force', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer['data']['id']) - - -def _do_backend_and_backend_set(runner, load_balancer_id, config_file, config_profile): - backend_set_name = util.random_name('cli_lb_backend_set') - - params = [ - 'backend-set', 'create', - '--name', backend_set_name, - '--policy', 'ROUND_ROBIN', - '--load-balancer-id', load_balancer_id, - '--health-checker-protocol', 'HTTP', - '--health-checker-return-code', '200', - '--health-checker-url-path', '/healthcheck', - '--health-checker-interval-in-ms', '60000', # 1 minute - '--session-persistence-cookie-name', '*', - '--session-persistence-disable-fallback', 'false', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer_id) - - ip_address = '10.0.0.10' - port = '80' - params = [ - 'backend', 'create', - '--ip-address', ip_address, - '--port', port, - '--load-balancer-id', load_balancer_id, - '--backend-set-name', backend_set_name, - '--weight', '3', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer_id) - - backend_name = "{}:{}".format(ip_address, port) - params = [ - 'backend', 'update', - '--load-balancer-id', load_balancer_id, - '--backend-set-name', backend_set_name, - '--backend-name', backend_name, - '--weight', '2', - '--offline', 'true', - '--backup', 'false', - '--drain', 'false', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer_id) - - params = [ - 'backend', 'delete', - '--load-balancer-id', load_balancer_id, - '--backend-set-name', backend_set_name, - '--backend-name', backend_name, - '--force', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer_id) - - _do_listener(runner, load_balancer_id, backend_set_name, config_file, config_profile) - - params = [ - 'backend-set', 'delete', - '--load-balancer-id', load_balancer_id, - '--backend-set-name', backend_set_name, - '--force', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer_id) - - -def _do_certificate(runner, load_balancer_id, config_file, config_profile, key_pair_files): - private_key_filename = key_pair_files[1] - certificate_filename = key_pair_files[2] - - cert_name = util.random_name('cli_lb_certificate') - - params = [ - 'certificate', 'create', - '--certificate-name', cert_name, - '--load-balancer-id', load_balancer_id, - '--ca-certificate-file', certificate_filename, - '--private-key-file', private_key_filename, - '--public-certificate-file', certificate_filename, - '--passphrase', 'secret!', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer_id) - - params = [ - 'certificate', 'delete', - '--load-balancer-id', load_balancer_id, - '--certificate-name', cert_name, - '--force', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer_id) - - -def _do_listener(runner, load_balancer_id, backend_set_name, config_file, config_profile): - listener_name = util.random_name('cli_listener') - params = [ - 'listener', 'create', - '--default-backend-set-name', backend_set_name, - '--load-balancer-id', load_balancer_id, - '--name', listener_name, - '--port', '8080', - '--protocol', 'HTTP', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer_id) - - params = [ - 'listener', 'update', - '--listener-name', listener_name, - '--default-backend-set-name', backend_set_name, - '--load-balancer-id', load_balancer_id, - '--port', '8080', - '--protocol', 'HTTP', - '--force', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer_id) - - params = [ - 'listener', 'delete', - '--load-balancer-id', load_balancer_id, - '--listener-name', listener_name, - '--force', - '--wait-for-state', 'SUCCEEDED' - ] - result = invoke(runner, config_file, config_profile, params) - _validate_work_request_result(result, load_balancer_id) - - -def _validate_work_request_result(result, load_balancer_id): - util.validate_response(result, json_response_expected=False) - assert 'Action completed. Waiting until the work request has entered state: SUCCEEDED' in result.output - - work_request = util.get_json_from_mixed_string(result.output) - assert work_request['data']['load-balancer-id'] == load_balancer_id - assert work_request['data']['lifecycle-state'] == 'SUCCEEDED' - - -def invoke(runner, config_file, config_profile, params, debug=False, root_params=None, strip_progress_bar=True, strip_multipart_stderr_output=True, ** args): - root_params = root_params or [] - if debug is True: - result = runner.invoke(oci_cli.cli, root_params + ['--debug', '--config-file', config_file, '--profile', config_profile, 'lb'] + params, ** args) - else: - result = runner.invoke(oci_cli.cli, root_params + ['--config-file', config_file, '--profile', config_profile, 'lb'] + params, ** args) - - return result diff --git a/tests/test_root_options.py b/tests/test_root_options.py index 6a0110d04..3f8496e26 100644 --- a/tests/test_root_options.py +++ b/tests/test_root_options.py @@ -2,7 +2,9 @@ # Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved. import oci_cli +import oci import os +from mock import patch def test_control_case(runner, config_file): @@ -59,16 +61,16 @@ def test_profile_option_overrides_default_setting(runner, config_file): def test_profile_option_overrides_environment_variable(runner, config_file): - os.environ[oci_cli.cli_root.OCI_CLI_PROFILE_ENV_VAR] = 'INVALID_PROFILE' + os.environ[oci_cli.cli_constants.OCI_CLI_PROFILE_ENV_VAR] = 'INVALID_PROFILE' result = invoke_example_operation(runner, ['--profile', 'DEFAULT'], config_file) - del os.environ[oci_cli.cli_root.OCI_CLI_PROFILE_ENV_VAR] + del os.environ[oci_cli.cli_constants.OCI_CLI_PROFILE_ENV_VAR] assert 0 == result.exit_code def test_profile_env_var_overrides_default_setting(runner, config_file): - os.environ[oci_cli.cli_root.OCI_CLI_PROFILE_ENV_VAR] = 'DEFAULT' + os.environ[oci_cli.cli_constants.OCI_CLI_PROFILE_ENV_VAR] = 'DEFAULT' result = invoke_example_operation(runner, ['--cli-rc-file', 'tests/resources/default_files/settings_with_invalid_default_profile'], config_file) - del os.environ[oci_cli.cli_root.OCI_CLI_PROFILE_ENV_VAR] + del os.environ[oci_cli.cli_constants.OCI_CLI_PROFILE_ENV_VAR] assert 0 == result.exit_code @@ -78,6 +80,30 @@ def test_default_profile_setting_from_cli_rc_file(runner, config_file): assert 1 == result.exit_code +def test_auth_instance_principal_param(runner, config_file): + with patch.object(oci.auth.signers.InstancePrincipalsSecurityTokenSigner, '__init__', return_value=None) as mock_init: + result = invoke_example_operation(runner, ['--auth', 'instance_principal'], 'non-existent-config') + assert mock_init.called + + +def test_auth_instance_principal_env_var(runner, config_file): + os.environ[oci_cli.cli_constants.OCI_CLI_AUTH_ENV_VAR] = 'instance_principal' + with patch.object(oci.auth.signers.InstancePrincipalsSecurityTokenSigner, '__init__', return_value=None) as mock_init: + result = invoke_example_operation(runner, [], 'non-existent-config') + del os.environ[oci_cli.cli_constants.OCI_CLI_AUTH_ENV_VAR] + assert mock_init.called + + +def test_auth_instance_principal_env_var_invalid(runner, config_file): + os.environ[oci_cli.cli_constants.OCI_CLI_AUTH_ENV_VAR] = 'instance_pri' + with patch.object(oci.auth.signers.InstancePrincipalsSecurityTokenSigner, '__init__', return_value=None) as mock_init: + result = invoke_example_operation(runner, [], 'non-existent-config') + del os.environ[oci_cli.cli_constants.OCI_CLI_AUTH_ENV_VAR] + + assert result.exit_code != 1 + assert 'Invalid value for OCI_CLI_AUTH' in result.output + + def invoke_example_operation(runner, root_args, config_file): args = root_args + (['--config-file', config_file] if config_file else []) + ['os', 'ns', 'get'] return runner.invoke(oci_cli.cli, args) diff --git a/tests/test_secondary_private_ip.py b/tests/test_secondary_private_ip.py index 61bd466be..533fd2c19 100644 --- a/tests/test_secondary_private_ip.py +++ b/tests/test_secondary_private_ip.py @@ -7,8 +7,8 @@ import socket import string import struct -import time import unittest +from . import test_config_container from . import util from . import tag_data_container @@ -17,13 +17,14 @@ class TestSecondaryPrivateIp(unittest.TestCase): @util.slow def test_secondary_ip_operations(self): - # We delegate to an internal method and have a try-catch so that we have - # an opportunity to clean up resources after the meat of the test is over - try: - self.subtest_secondary_ip_operations() - self.subtest_tagging_secondary_ip() - finally: - self.clean_up_resources() + with test_config_container.create_vcr().use_cassette('secondary_ip_operations.yml'): + # We delegate to an internal method and have a try-catch so that we have + # an opportunity to clean up resources after the meat of the test is over + try: + self.subtest_secondary_ip_operations() + self.subtest_tagging_secondary_ip() + finally: + self.clean_up_resources() def subtest_secondary_ip_operations(self): self.set_up_vcn_and_subnet("10.0.0.0/16") @@ -286,7 +287,7 @@ def subtest_tagging_secondary_ip(self): tag_names_to_values = {} for t in tag_data_container.tags: - tag_names_to_values[t.name] = 'somevalue {}'.format(int(time.time())) + tag_names_to_values[t.name] = 'somevalue {}'.format(t.name) tag_data_container.write_defined_tags_to_file( os.path.join('tests', 'temp', 'defined_tags_1.json'), tag_data_container.tag_namespace, @@ -316,7 +317,7 @@ def subtest_tagging_secondary_ip(self): tag_names_to_values = {} for t in tag_data_container.tags: - tag_names_to_values[t.name] = 'somevalue2 {}'.format(int(time.time())) + tag_names_to_values[t.name] = 'somevalue2 {}'.format(t.name) tag_data_container.write_defined_tags_to_file( os.path.join('tests', 'temp', 'defined_tags_2.json'), tag_data_container.tag_namespace, @@ -479,7 +480,7 @@ def ensure_private_ip_record_not_present(self, private_ips, target_private_ip_oc # Fudges an OCID by flipping its last character to something different than what it currently is def fudge_ocid(self, ocid): - available_characters = set(list(string.ascii_lowercase) + list(string.digits)) + available_characters = list(string.ascii_lowercase) + list(string.digits) available_characters.remove('0') available_characters.remove('1') available_characters.remove('8') @@ -507,7 +508,7 @@ def get_ip_addresses_from_cidr(self, cidr_string): for i in range(start, end): ip_addresses.add(socket.inet_ntoa(struct.pack('>I', i))) - return ip_addresses + return sorted(list(ip_addresses)) def invoke(self, commands, debug=False, ** args): if debug is True: diff --git a/tests/test_tag_management.py b/tests/test_tag_management.py index fedca6919..6103edc6c 100644 --- a/tests/test_tag_management.py +++ b/tests/test_tag_management.py @@ -8,46 +8,50 @@ import random from . import tag_data_container +from . import test_config_container from . import util @util.slow def test_update_retire_reactivate_namespace_and_tag(identity_client, tag_namespace_and_tags): - if os.environ.get('OCI_CLI_TAG_MGMT_USE_EXISTING_TAG_AND_NAMESPACE'): - tag_namespace_id = tag_data_container.tag_namespace.id - tag_name = tag_data_container.tags[0].name - print('Reusing existing tag namespace {} and tag {}'.format(tag_namespace_id, tag_name)) - else: - suffix = str(random.randint(1, int(time.time()))) - namespace_name = ('cliTagNamespace_{}'.format(suffix)).lower() - tag_name = ('cliTag_{}'.format(suffix)).lower() - - result = invoke(['iam', 'tag-namespace', 'create', '-c', util.COMPARTMENT_ID, '--name', namespace_name, '--description', 'initial description']) - util.validate_response(result) - parsed_result = json.loads(result.output) - tag_namespace_id = parsed_result['data']['id'] - assert namespace_name == parsed_result['data']['name'] - assert 'initial description' == parsed_result['data']['description'] - assert not parsed_result['data']['is-retired'] - - result = invoke(['iam', 'tag', 'create', '--tag-namespace-id', tag_namespace_id, '--name', tag_name, '--description', 'tag description']) - util.validate_response(result) - parsed_result = json.loads(result.output) - assert tag_name == parsed_result['data']['name'] - assert 'tag description' == parsed_result['data']['description'] - assert not parsed_result['data']['is-retired'] - - apply_tags_to_tag_namespace(tag_namespace_id) - apply_tags_to_tag(tag_namespace_id, tag_name) - update_retire_reactivate_operations(tag_namespace_id, tag_name) - get_and_list_operations(identity_client, tag_namespace_id, tag_name) + with test_config_container.create_vcr().use_cassette('tag_management.yml'): + if os.environ.get('OCI_CLI_TAG_MGMT_USE_EXISTING_TAG_AND_NAMESPACE'): + tag_namespace_id = tag_data_container.tag_namespace.id + tag_name = tag_data_container.tags[0].name + print('Reusing existing tag namespace {} and tag {}'.format(tag_namespace_id, tag_name)) + + tag_data_container.ensure_namespace_and_tags_active(invoke) + else: + suffix = str(random.randint(1, int(time.time()))) + namespace_name = ('cliTagNamespace_{}'.format(suffix)).lower() + tag_name = ('cliTag_{}'.format(suffix)).lower() + + result = invoke(['iam', 'tag-namespace', 'create', '-c', util.COMPARTMENT_ID, '--name', namespace_name, '--description', 'initial description']) + util.validate_response(result) + parsed_result = json.loads(result.output) + tag_namespace_id = parsed_result['data']['id'] + assert namespace_name == parsed_result['data']['name'] + assert 'initial description' == parsed_result['data']['description'] + assert not parsed_result['data']['is-retired'] + + result = invoke(['iam', 'tag', 'create', '--tag-namespace-id', tag_namespace_id, '--name', tag_name, '--description', 'tag description']) + util.validate_response(result) + parsed_result = json.loads(result.output) + assert tag_name == parsed_result['data']['name'] + assert 'tag description' == parsed_result['data']['description'] + assert not parsed_result['data']['is-retired'] + + apply_tags_to_tag_namespace(tag_namespace_id) + apply_tags_to_tag(tag_namespace_id, tag_name) + update_retire_reactivate_operations(tag_namespace_id, tag_name) + get_and_list_operations(identity_client, tag_namespace_id, tag_name) def apply_tags_to_tag_namespace(tag_namespace_id): tag_data_container.ensure_namespace_and_tags_active(invoke) tag_names_to_values = { - tag_data_container.tags[0].name: 'tag_ns_mgmt {}'.format(int(time.time())) + tag_data_container.tags[0].name: 'tag_ns_mgmt {}'.format(util.random_number_string()) } tag_data_container.write_defined_tags_to_file( os.path.join('tests', 'temp', 'defined_tags_1.json'), @@ -89,7 +93,7 @@ def apply_tags_to_tag_namespace(tag_namespace_id): # Overwrite with different tags tag_names_to_values = { - tag_data_container.tags[1].name: 'tag_ns_mgmt {}'.format(int(time.time())) + tag_data_container.tags[1].name: 'tag_ns_mgmt update {}'.format(util.random_number_string()) } tag_data_container.write_defined_tags_to_file( os.path.join('tests', 'temp', 'defined_tags_1.json'), @@ -152,7 +156,7 @@ def apply_tags_to_tag(tag_namespace_id, tag_name): tag_names_to_values = {} for t in tag_data_container.tags: - tag_names_to_values[t.name] = 'tag_mgmt {}'.format(int(time.time())) + tag_names_to_values[t.name] = 'tag_mgmt {}'.format(util.random_number_string()) tag_data_container.write_defined_tags_to_file( os.path.join('tests', 'temp', 'defined_tags_1.json'), tag_data_container.tag_namespace, diff --git a/tests/test_tagging.py b/tests/test_tagging.py index 1846860a4..e912f2cb2 100644 --- a/tests/test_tagging.py +++ b/tests/test_tagging.py @@ -2,6 +2,7 @@ # Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved. from . import tag_data_container +from . import test_config_container from . import util import json @@ -13,42 +14,44 @@ @pytest.fixture(scope='module') def network_resources(): - vcn_name = util.random_name('cli_test_tagging') - cidr_block = "10.0.0.0/16" - vcn_dns_label = util.random_name('vcn', insert_underscore=False) - - result = invoke([ - 'network', 'vcn', 'create', - '--compartment-id', util.COMPARTMENT_ID, - '--display-name', vcn_name, - '--cidr-block', cidr_block, - '--dns-label', vcn_dns_label, - '--wait-for-state', 'AVAILABLE' - ]) - vcn_ocid = util.get_json_from_mixed_string(result.output)['data']['id'] - - subnet_name = util.random_name('cli_test_compute_subnet') - subnet_dns_label = util.random_name('subnet', insert_underscore=False) - - result = invoke([ - 'network', 'subnet', 'create', - '--compartment-id', util.COMPARTMENT_ID, - '--availability-domain', util.availability_domain(), - '--display-name', subnet_name, - '--vcn-id', vcn_ocid, - '--cidr-block', cidr_block, - '--dns-label', subnet_dns_label, - '--wait-for-state', 'AVAILABLE' - ]) - subnet_ocid = util.get_json_from_mixed_string(result.output)['data']['id'] - - yield (vcn_ocid, subnet_ocid) - - result = invoke(['network', 'subnet', 'delete', '--subnet-id', subnet_ocid, '--force', '--wait-for-state', 'TERMINATED']) - util.validate_response(result, json_response_expected=False) - - result = util.invoke_command(['network', 'vcn', 'delete', '--vcn-id', vcn_ocid, '--force', '--wait-for-state', 'TERMINATED']) - util.validate_response(result, json_response_expected=False) + with test_config_container.create_vcr().use_cassette('test_tagging_fixture_network.yml'): + vcn_name = util.random_name('cli_test_tagging') + cidr_block = "10.0.0.0/16" + vcn_dns_label = util.random_name('vcn', insert_underscore=False) + + result = invoke([ + 'network', 'vcn', 'create', + '--compartment-id', util.COMPARTMENT_ID, + '--display-name', vcn_name, + '--cidr-block', cidr_block, + '--dns-label', vcn_dns_label, + '--wait-for-state', 'AVAILABLE' + ]) + vcn_ocid = util.get_json_from_mixed_string(result.output)['data']['id'] + + subnet_name = util.random_name('cli_test_compute_subnet') + subnet_dns_label = util.random_name('subnet', insert_underscore=False) + + result = invoke([ + 'network', 'subnet', 'create', + '--compartment-id', util.COMPARTMENT_ID, + '--availability-domain', util.availability_domain(), + '--display-name', subnet_name, + '--vcn-id', vcn_ocid, + '--cidr-block', cidr_block, + '--dns-label', subnet_dns_label, + '--wait-for-state', 'AVAILABLE' + ]) + subnet_ocid = util.get_json_from_mixed_string(result.output)['data']['id'] + + yield (vcn_ocid, subnet_ocid) + + with test_config_container.create_vcr().use_cassette('test_tagging_fixture_network_delete.yml'): + result = invoke(['network', 'subnet', 'delete', '--subnet-id', subnet_ocid, '--force', '--wait-for-state', 'TERMINATED']) + util.validate_response(result, json_response_expected=False) + + result = util.invoke_command(['network', 'vcn', 'delete', '--vcn-id', vcn_ocid, '--force', '--wait-for-state', 'TERMINATED']) + util.validate_response(result, json_response_expected=False) def test_commands_with_tags_can_generate_json(): @@ -103,235 +106,237 @@ def test_commands_with_tags_can_generate_json(): @util.slow def test_launch_update_instance_with_tags(tag_namespace_and_tags, network_resources): - tag_data_container.ensure_namespace_and_tags_active(invoke) - - tag_names_to_values = {} - for t in tag_data_container.tags: - tag_names_to_values[t.name] = 'launch_instance {}'.format(int(time.time())) - tag_data_container.write_defined_tags_to_file( - os.path.join('tests', 'temp', 'defined_tags_1.json'), - tag_data_container.tag_namespace, - tag_names_to_values - ) - - instance_ocid = None - try: - # Launch with tags. Because of eventual consistency, we may get a 404 the first time we try a tag and if that happens - # we should retry - attempts = 0 - while attempts <= 3: - result = invoke([ - 'compute', 'instance', 'launch', - '--compartment-id', util.COMPARTMENT_ID, - '--availability-domain', util.availability_domain(), - '--display-name', util.random_name('cli_tag_test_instance'), - '--subnet-id', network_resources[1], - '--image-id', util.oracle_linux_image(), - '--shape', 'VM.Standard1.1', - '--wait-for-state', 'RUNNING', - '--freeform-tags', 'file://tests/resources/tagging/freeform_tags_1.json', - '--defined-tags', 'file://tests/temp/defined_tags_1.json' - ]) - if result.exit_code == 0: # This is a bit coarse-grained. We could also crack open the response and check that it's a 404 - break - else: - attempts += 1 - time.sleep(5) - util.validate_response(result, json_response_expected=False, expect_etag=True) - instance_data = util.get_json_from_mixed_string(result.output)['data'] - instance_ocid = instance_data['id'] - - expected_freeform = {'tagOne': 'value1', 'tag_Two': 'value two'} - expected_defined = {tag_data_container.tag_namespace.name: tag_names_to_values} - assert expected_freeform == instance_data['freeform-tags'] - assert expected_defined == instance_data['defined-tags'] - - result = invoke([ - 'compute', 'instance', 'get', - '--instance-id', instance_ocid - ]) - parsed_result = json.loads(result.output) - assert expected_freeform == parsed_result['data']['freeform-tags'] - assert expected_defined == parsed_result['data']['defined-tags'] + with test_config_container.create_vcr().use_cassette('test_tagging_instance.yml'): + tag_data_container.ensure_namespace_and_tags_active(invoke) - # Update to different tags - tag_names_to_values.pop(tag_data_container.tags[0].name) + tag_names_to_values = {} + for t in tag_data_container.tags: + tag_names_to_values[t.name] = 'launch_instance {}'.format(util.random_number_string()) tag_data_container.write_defined_tags_to_file( os.path.join('tests', 'temp', 'defined_tags_1.json'), tag_data_container.tag_namespace, tag_names_to_values ) - attempts = 0 - while attempts <= 3: + instance_ocid = None + try: + # Launch with tags. Because of eventual consistency, we may get a 404 the first time we try a tag and if that happens + # we should retry + attempts = 0 + while attempts <= 3: + result = invoke([ + 'compute', 'instance', 'launch', + '--compartment-id', util.COMPARTMENT_ID, + '--availability-domain', util.availability_domain(), + '--display-name', util.random_name('cli_tag_test_instance'), + '--subnet-id', network_resources[1], + '--image-id', util.oracle_linux_image(), + '--shape', 'VM.Standard1.1', + '--wait-for-state', 'RUNNING', + '--freeform-tags', 'file://tests/resources/tagging/freeform_tags_1.json', + '--defined-tags', 'file://tests/temp/defined_tags_1.json' + ]) + if result.exit_code == 0: # This is a bit coarse-grained. We could also crack open the response and check that it's a 404 + break + else: + attempts += 1 + time.sleep(5) + util.validate_response(result, json_response_expected=False, expect_etag=True) + instance_data = util.get_json_from_mixed_string(result.output)['data'] + instance_ocid = instance_data['id'] + + expected_freeform = {'tagOne': 'value1', 'tag_Two': 'value two'} + expected_defined = {tag_data_container.tag_namespace.name: tag_names_to_values} + assert expected_freeform == instance_data['freeform-tags'] + assert expected_defined == instance_data['defined-tags'] + result = invoke([ - 'compute', 'instance', 'update', - '--instance-id', instance_ocid, - '--freeform-tags', 'file://tests/resources/tagging/freeform_tags_2.json', - '--defined-tags', 'file://tests/temp/defined_tags_1.json', - '--force' + 'compute', 'instance', 'get', + '--instance-id', instance_ocid ]) - if result.exit_code == 0: - break - else: - attempts += 1 - time.sleep(5) - util.validate_response(result) - parsed_result = json.loads(result.output) - - expected_freeform = {'tagOne': 'value three'} - expected_defined = {tag_data_container.tag_namespace.name: tag_names_to_values} - assert expected_freeform == parsed_result['data']['freeform-tags'] - assert expected_defined == parsed_result['data']['defined-tags'] + parsed_result = json.loads(result.output) + assert expected_freeform == parsed_result['data']['freeform-tags'] + assert expected_defined == parsed_result['data']['defined-tags'] + + # Update to different tags + tag_names_to_values.pop(tag_data_container.tags[0].name) + tag_data_container.write_defined_tags_to_file( + os.path.join('tests', 'temp', 'defined_tags_1.json'), + tag_data_container.tag_namespace, + tag_names_to_values + ) + + attempts = 0 + while attempts <= 3: + result = invoke([ + 'compute', 'instance', 'update', + '--instance-id', instance_ocid, + '--freeform-tags', 'file://tests/resources/tagging/freeform_tags_2.json', + '--defined-tags', 'file://tests/temp/defined_tags_1.json', + '--force' + ]) + if result.exit_code == 0: + break + else: + attempts += 1 + time.sleep(5) + util.validate_response(result) + parsed_result = json.loads(result.output) - result = invoke([ - 'compute', 'instance', 'get', - '--instance-id', instance_ocid - ]) - parsed_result = json.loads(result.output) - assert expected_freeform == parsed_result['data']['freeform-tags'] - assert expected_defined == parsed_result['data']['defined-tags'] + expected_freeform = {'tagOne': 'value three'} + expected_defined = {tag_data_container.tag_namespace.name: tag_names_to_values} + assert expected_freeform == parsed_result['data']['freeform-tags'] + assert expected_defined == parsed_result['data']['defined-tags'] - # Nuke tags by passing an empty JSON object - result = invoke([ - 'compute', 'instance', 'update', - '--instance-id', instance_ocid, - '--freeform-tags', '{}', - '--defined-tags', '{}', - '--force' - ]) - util.validate_response(result) - parsed_result = json.loads(result.output) - assert {} == parsed_result['data']['freeform-tags'] - assert {} == parsed_result['data']['defined-tags'] + result = invoke([ + 'compute', 'instance', 'get', + '--instance-id', instance_ocid + ]) + parsed_result = json.loads(result.output) + assert expected_freeform == parsed_result['data']['freeform-tags'] + assert expected_defined == parsed_result['data']['defined-tags'] - result = invoke([ - 'compute', 'instance', 'get', - '--instance-id', instance_ocid - ]) - parsed_result = json.loads(result.output) - assert {} == parsed_result['data']['freeform-tags'] - assert {} == parsed_result['data']['defined-tags'] - finally: - if instance_ocid: + # Nuke tags by passing an empty JSON object result = invoke([ - 'compute', 'instance', 'terminate', + 'compute', 'instance', 'update', '--instance-id', instance_ocid, - '--force', - '--wait-for-state', 'TERMINATED' + '--freeform-tags', '{}', + '--defined-tags', '{}', + '--force' ]) - util.validate_response(result, json_response_expected=False) - + util.validate_response(result) + parsed_result = json.loads(result.output) + assert {} == parsed_result['data']['freeform-tags'] + assert {} == parsed_result['data']['defined-tags'] -@util.slow -def test_create_update_volume_with_tags(tag_namespace_and_tags): - tag_data_container.ensure_namespace_and_tags_active(invoke) - - tag_names_to_values = {} - tag_names_to_values[tag_data_container.tags[0].name] = 'create_volume {}'.format(int(time.time())) - tag_data_container.write_defined_tags_to_file( - os.path.join('tests', 'temp', 'defined_tags_1.json'), - tag_data_container.tag_namespace, - tag_names_to_values - ) - - volume_id = None - try: - # Create with tags - volume_name = util.random_name('cli_test_volume') - - attempts = 0 - while attempts <= 3: result = invoke([ - 'bv', 'volume', 'create', - '--availability-domain', util.availability_domain(), - '--compartment-id', util.COMPARTMENT_ID, - '--display-name', volume_name, - '--size-in-gbs', '50', - '--freeform-tags', 'file://tests/resources/tagging/freeform_tags_2.json', - '--defined-tags', 'file://tests/temp/defined_tags_1.json', - '--wait-for-state', 'AVAILABLE' + 'compute', 'instance', 'get', + '--instance-id', instance_ocid ]) - if result.exit_code == 0: - break - else: - attempts += 1 - time.sleep(5) - util.validate_response(result, json_response_expected=False) - volume_data = util.get_json_from_mixed_string(result.output)['data'] - volume_id = volume_data['id'] + parsed_result = json.loads(result.output) + assert {} == parsed_result['data']['freeform-tags'] + assert {} == parsed_result['data']['defined-tags'] + finally: + if instance_ocid: + result = invoke([ + 'compute', 'instance', 'terminate', + '--instance-id', instance_ocid, + '--force', + '--wait-for-state', 'TERMINATED' + ]) + util.validate_response(result, json_response_expected=False) - expected_freeform = {'tagOne': 'value three'} - expected_defined = {tag_data_container.tag_namespace.name: tag_names_to_values} - assert expected_freeform == volume_data['freeform-tags'] - assert expected_defined == volume_data['defined-tags'] - result = invoke(['bv', 'volume', 'get', '--volume-id', volume_id]) - parsed_result = json.loads(result.output) - assert expected_freeform == parsed_result['data']['freeform-tags'] - assert expected_defined == parsed_result['data']['defined-tags'] +@util.slow +def test_create_update_volume_with_tags(tag_namespace_and_tags): + with test_config_container.create_vcr().use_cassette('test_tagging_volume.yml'): + tag_data_container.ensure_namespace_and_tags_active(invoke) - # Update to different tags replaces - for t in tag_data_container.tags: - tag_names_to_values[t.name] = 'create_volume {}'.format(int(time.time())) + tag_names_to_values = {} + tag_names_to_values[tag_data_container.tags[0].name] = 'create_volume {}'.format(util.random_number_string()) tag_data_container.write_defined_tags_to_file( os.path.join('tests', 'temp', 'defined_tags_1.json'), tag_data_container.tag_namespace, tag_names_to_values ) - attempts = 0 - while attempts <= 3: - result = invoke([ - 'bv', 'volume', 'update', - '--volume-id', volume_id, - '--freeform-tags', 'file://tests/resources/tagging/freeform_tags_1.json', - '--defined-tags', 'file://tests/temp/defined_tags_1.json', - '--force' - ]) - if result.exit_code == 0: - break - else: - attempts += 1 - time.sleep(5) - util.validate_response(result) - parsed_result = json.loads(result.output) - - expected_freeform = {'tagOne': 'value1', 'tag_Two': 'value two'} - expected_defined = {tag_data_container.tag_namespace.name: tag_names_to_values} - assert expected_freeform == parsed_result['data']['freeform-tags'] - assert expected_defined == parsed_result['data']['defined-tags'] + volume_id = None + try: + # Create with tags + volume_name = util.random_name('cli_test_volume') + + attempts = 0 + while attempts <= 3: + result = invoke([ + 'bv', 'volume', 'create', + '--availability-domain', util.availability_domain(), + '--compartment-id', util.COMPARTMENT_ID, + '--display-name', volume_name, + '--size-in-gbs', '50', + '--freeform-tags', 'file://tests/resources/tagging/freeform_tags_2.json', + '--defined-tags', 'file://tests/temp/defined_tags_1.json', + '--wait-for-state', 'AVAILABLE' + ]) + if result.exit_code == 0: + break + else: + attempts += 1 + time.sleep(5) + util.validate_response(result, json_response_expected=False) + volume_data = util.get_json_from_mixed_string(result.output)['data'] + volume_id = volume_data['id'] + + expected_freeform = {'tagOne': 'value three'} + expected_defined = {tag_data_container.tag_namespace.name: tag_names_to_values} + assert expected_freeform == volume_data['freeform-tags'] + assert expected_defined == volume_data['defined-tags'] + + result = invoke(['bv', 'volume', 'get', '--volume-id', volume_id]) + parsed_result = json.loads(result.output) + assert expected_freeform == parsed_result['data']['freeform-tags'] + assert expected_defined == parsed_result['data']['defined-tags'] + + # Update to different tags replaces + for t in tag_data_container.tags: + tag_names_to_values[t.name] = 'create_volume {}'.format(util.random_number_string()) + tag_data_container.write_defined_tags_to_file( + os.path.join('tests', 'temp', 'defined_tags_1.json'), + tag_data_container.tag_namespace, + tag_names_to_values + ) + + attempts = 0 + while attempts <= 3: + result = invoke([ + 'bv', 'volume', 'update', + '--volume-id', volume_id, + '--freeform-tags', 'file://tests/resources/tagging/freeform_tags_1.json', + '--defined-tags', 'file://tests/temp/defined_tags_1.json', + '--force' + ]) + if result.exit_code == 0: + break + else: + attempts += 1 + time.sleep(5) + util.validate_response(result) + parsed_result = json.loads(result.output) - result = invoke(['bv', 'volume', 'get', '--volume-id', volume_id]) - parsed_result = json.loads(result.output) - assert expected_freeform == parsed_result['data']['freeform-tags'] - assert expected_defined == parsed_result['data']['defined-tags'] + expected_freeform = {'tagOne': 'value1', 'tag_Two': 'value two'} + expected_defined = {tag_data_container.tag_namespace.name: tag_names_to_values} + assert expected_freeform == parsed_result['data']['freeform-tags'] + assert expected_defined == parsed_result['data']['defined-tags'] - # Nuke tags by passing empty JSON objects - result = invoke([ - 'bv', 'volume', 'update', - '--volume-id', volume_id, - '--freeform-tags', '{}', - '--defined-tags', '{}', - '--force' - ]) - util.validate_response(result) - parsed_result = json.loads(result.output) - assert {} == parsed_result['data']['freeform-tags'] - assert {} == parsed_result['data']['defined-tags'] + result = invoke(['bv', 'volume', 'get', '--volume-id', volume_id]) + parsed_result = json.loads(result.output) + assert expected_freeform == parsed_result['data']['freeform-tags'] + assert expected_defined == parsed_result['data']['defined-tags'] - result = invoke(['bv', 'volume', 'get', '--volume-id', volume_id]) - parsed_result = json.loads(result.output) - assert {} == parsed_result['data']['freeform-tags'] - assert {} == parsed_result['data']['defined-tags'] - finally: - if volume_id: + # Nuke tags by passing empty JSON objects result = invoke([ - 'bv', 'volume', 'delete', + 'bv', 'volume', 'update', '--volume-id', volume_id, + '--freeform-tags', '{}', + '--defined-tags', '{}', '--force' ]) util.validate_response(result) + parsed_result = json.loads(result.output) + assert {} == parsed_result['data']['freeform-tags'] + assert {} == parsed_result['data']['defined-tags'] + + result = invoke(['bv', 'volume', 'get', '--volume-id', volume_id]) + parsed_result = json.loads(result.output) + assert {} == parsed_result['data']['freeform-tags'] + assert {} == parsed_result['data']['defined-tags'] + finally: + if volume_id: + result = invoke([ + 'bv', 'volume', 'delete', + '--volume-id', volume_id, + '--force' + ]) + util.validate_response(result) def invoke(commands, debug=False, ** args): diff --git a/tests/test_utils.py b/tests/test_utils.py index e23c9b11f..bfcf4f4c5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,34 +12,34 @@ import mock -@mock.patch('oci_cli.cli_util.arrow') -@mock.patch('oci_cli.cli_util.requests') -def test_get_clock_skew_with_no_skew(mock_requests, mock_arrow): - response = requests.Response() - response.headers['Date'] = 'Wed, 01 Jan 2014 00:00:30 GMT' - mock_requests.head = mock.Mock(return_value=response) +def test_get_clock_skew_with_no_skew(): + with mock.patch('oci_cli.cli_util.arrow') as mock_arrow: + with mock.patch('oci_cli.cli_util.requests') as mock_requests: + response = requests.Response() + response.headers['Date'] = 'Wed, 01 Jan 2014 00:00:30 GMT' + mock_requests.head = mock.Mock(return_value=response) - mock_arrow.get = arrow.get - mock_arrow.utcnow = mock.Mock(return_value=arrow.get(2014, 1, 1)) + mock_arrow.get = arrow.get + mock_arrow.utcnow = mock.Mock(return_value=arrow.get(2014, 1, 1)) - config = { - 'region': 'us-phoenix-1' - } + config = { + 'region': 'us-phoenix-1' + } - with util.capture() as out: - warn_if_clock_skew_present(config) - assert 'WARNING' not in out[1].getvalue() + with util.capture() as out: + warn_if_clock_skew_present(config) + assert 'WARNING' not in out[1].getvalue() -@mock.patch('oci_cli.cli_util.arrow') -def test_get_clock_skew_detects_skew(mock_arrow): - mock_arrow.get = arrow.get - mock_arrow.utcnow = mock.Mock(return_value=arrow.get(2014, 1, 1)) +def test_get_clock_skew_detects_skew(): + with mock.patch('oci_cli.cli_util.arrow') as mock_arrow: + mock_arrow.get = arrow.get + mock_arrow.utcnow = mock.Mock(return_value=arrow.get(2014, 1, 1)) - config = { - 'region': 'us-phoenix-1' - } + config = { + 'region': 'us-phoenix-1' + } - with util.capture() as out: - warn_if_clock_skew_present(config) - assert 'WARNING' in out[1].getvalue() + with util.capture() as out: + warn_if_clock_skew_present(config) + assert 'WARNING' in out[1].getvalue() diff --git a/tests/test_virtualnetwork.py b/tests/test_virtualnetwork.py index 9e90bb982..5a8280677 100644 --- a/tests/test_virtualnetwork.py +++ b/tests/test_virtualnetwork.py @@ -5,6 +5,7 @@ import pytest import unittest from . import command_coverage_validator +from . import test_config_container from . import util from .test_list_filter import retrieve_list_by_field_and_check, retrieve_list_and_ensure_sorted import oci_cli @@ -23,24 +24,25 @@ def test_all_operations(self, validator): these are handlde in test_secondary_private_ip.py""" self.validator = validator - try: - self.subtest_vcn_operations() - self.subtest_security_list_operations() - self.subtest_security_list_stateless_rules() - self.subtest_subnet_operations() - self.subtest_internet_gateway_operations() - self.subtest_cpe_operations() - self.subtest_dhcp_option_operations() - self.subtest_drg_operations() - self.subtest_drg_attachment_operations() - self.subtest_ip_sec_connection_operations() - self.subtest_route_table_operations() - - if hasattr(self, 'drg_capacity_issue'): - pytest.skip('Skipped DRG tests due to capacity issues') - finally: - time.sleep(20) - self.subtest_delete() + with test_config_container.create_vcr().use_cassette('virtual_network.yml'): + try: + self.subtest_vcn_operations() + self.subtest_security_list_operations() + self.subtest_security_list_stateless_rules() + self.subtest_subnet_operations() + self.subtest_internet_gateway_operations() + self.subtest_cpe_operations() + self.subtest_dhcp_option_operations() + self.subtest_drg_operations() + self.subtest_drg_attachment_operations() + self.subtest_ip_sec_connection_operations() + self.subtest_route_table_operations() + + if hasattr(self, 'drg_capacity_issue'): + pytest.skip('Skipped DRG tests due to capacity issues') + finally: + time.sleep(20) + self.subtest_delete() @util.log_test def subtest_vcn_operations(self): diff --git a/tests/util.py b/tests/util.py index 41a3f7ab1..043a01806 100644 --- a/tests/util.py +++ b/tests/util.py @@ -17,6 +17,7 @@ import oci_cli.cli_util import oci from oci.object_storage.transfer.constants import MEBIBYTE +from . import test_config_container try: # PY3+ @@ -108,9 +109,14 @@ def init_availability_domain_variables(): first_ad = availability_domains[0]['name'] second_ad = availability_domains[1]['name'] else: - chosen_domains = random.sample(availability_domains, 2) - first_ad = chosen_domains[0]['name'] - second_ad = chosen_domains[1]['name'] + # We need consistency in the vended availability domains if we're mocking, so don't randomize + if test_config_container.using_vcr_with_mock_responses(): + first_ad = availability_domains[0]['name'] + second_ad = availability_domains[1]['name'] + else: + chosen_domains = random.sample(availability_domains, 2) + first_ad = chosen_domains[0]['name'] + second_ad = chosen_domains[1]['name'] enable_long_running = pytest.mark.skipif( @@ -120,7 +126,17 @@ def init_availability_domain_variables(): def random_name(prefix, insert_underscore=True): - return prefix + ('_' if insert_underscore else '') + str(random.randint(0, 1000000)) + if test_config_container.using_vcr_with_mock_responses(): + return prefix + ('_' if insert_underscore else '') + 'vcr' + else: + return prefix + ('_' if insert_underscore else '') + str(random.randint(0, 1000000)) + + +def random_number_string(): + if test_config_container.using_vcr_with_mock_responses(): + return '10000' + else: + return str(random.randint(0, 10000)) def bucket_regional_prefix(): @@ -265,7 +281,8 @@ def wait_until(get_command, state, max_wait_seconds=30, max_interval_seconds=15, elif json.loads(result.output)['data'][state_property_name] == state: break - time.sleep(sleep_interval_seconds) + if test_config_container.vcr_mode != 'none': + time.sleep(sleep_interval_seconds) # Double the sleep each time up to the maximum. sleep_interval_seconds = min(sleep_interval_seconds * 2, max_interval_seconds)