From b13deb5e35c228c3bda01a23df791683568b07d9 Mon Sep 17 00:00:00 2001 From: Mike Ross Date: Thu, 8 Mar 2018 23:28:29 +0000 Subject: [PATCH] Releasing version 2.4.18 --- CHANGELOG.rst | 39 ++- requirements.txt | 2 +- setup.py | 2 +- src/oci_cli/__init__.py | 2 + src/oci_cli/bin/OciTabExpansion.ps1 | 31 +- src/oci_cli/cli.py | 2 + src/oci_cli/cli_clients.py | 3 + src/oci_cli/cli_setup.py | 7 +- src/oci_cli/cli_util.py | 39 ++- src/oci_cli/core_cli_extended.py | 20 +- src/oci_cli/custom_types/cli_datetime.py | 9 +- src/oci_cli/email_cli_extended.py | 19 ++ src/oci_cli/generated/compute_cli.py | 4 +- src/oci_cli/generated/dns_cli.py | 111 +++++-- src/oci_cli/generated/email_cli.py | 345 ++++++++++++++++++++++ src/oci_cli/objectstorage_cli_extended.py | 17 +- src/oci_cli/retry_utils.py | 45 ++- src/oci_cli/version.py | 2 +- tests/output/inline_help_dump.txt | 332 ++++++++++++++++++++- tests/test_audit.py | 16 +- tests/test_blockstorage.py | 2 +- tests/test_default_values_from_file.py | 39 +++ tests/test_dns.py | 101 ++++++- tests/test_email.py | 105 +++++++ tests/test_list_filter.py | 10 +- tests/test_object_storage.py | 8 + 26 files changed, 1230 insertions(+), 82 deletions(-) create mode 100644 src/oci_cli/email_cli_extended.py create mode 100644 src/oci_cli/generated/email_cli.py create mode 100644 tests/test_email.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 02aa25df..bc2584ba 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,28 +7,55 @@ All notable changes to this project will be documented in this file. The format is based on `Keep a Changelog `__. +2.4.18 - 2018-03-08 +--------------------- +Added +~~~~~~~~~~ +* Support for the Email Service. (``oci email``) + + * A sample test using the email feature can be found on `Github `_ + +* Support for the following features in the Core Services: + + * paravirtualized volume attachments (--type option for ``oci compute volume-attachment attach``) + * variable size boot volumes (--boot-volume-size-in-gbs option for ``oci compute instance launch``) + +* Support for auto-pagination for the Domain Name System Service. (--all, --page-size options for ``oci dns record domain get``, ``oci dns record rrset get``, ``oci dns record zone get``) +* Support for no-overwrite flag for the object put operation for the Object Service (--no-overwrite for ``oci os object put``). + +Fixed +~~~~~~~~~~ +* Updated config / key file permissions logic on Windows to depend on well known SIDs instead of account / group name to + fix localization issues. This affects ``oci setup config``, ``oci setup repair-file-permissions``, and the general + config / key file permissions check performed by other commands. + 2.4.17 - 2018-02-22 --------------------- Added ~~~~~~~~~~ -* Added support for the File Storage Service. (``oci fs``) -* Added support for Path Route Sets in the Load Balancer Service. An example can be found on `Github `_ (``oci lb path-route-set``) -* Added tagging support for *Bucket* resources in the Object Storage Service +* Support for the File Storage Service. (``oci fs``) +* Support for Path Route Sets in the Load Balancer Service. An example can be found on `Github `_ (``oci lb path-route-set``) +* Tagging support for *Bucket* resources in the Object Storage Service + * Create a bucket with tags: ``oci os bucket create --defined-tags --freeform-tags`` * Update a bucket with tags: ``oci os bucket update --defined-tags --freeform-tags`` * List buckets and display defined and freeform tags in the results: ``oci os bucket list --fields tags`` -* Added support for specifying a restore period for archived objects in the *RestoreObjects* operation of the Object Storage service. (``oci os object restore --hours``) -* Added support for filtering by *backupId* in *ListDbSystems* operation in the Database Service (``oci db system list --backup-id``) -* Added support for getting plink (the `PuTTY `_ command line interface) compatible instance console connection string for Windows users (``oci compute instance-console-connection get-plink-connection-string``) + +* Support for specifying a restore period for archived objects in the *RestoreObjects* operation of the Object Storage service. (``oci os object restore --hours``) +* Support for filtering by *backupId* in *ListDbSystems* operation in the Database Service (``oci db system list --backup-id``) +* Support for getting plink (the `PuTTY `_ command line interface) compatible instance console connection string for Windows users (``oci compute instance-console-connection get-plink-connection-string``) 2.4.16 - 2018-02-08 --------------------- Added ~~~~~~~~~~ * Support for Domain Name System Service (oci dns) + * An example on using the Domain Name System Service can be found on `GitHub `_. + * Support for Reserved Public IPs in Virtual Networking Service (oci network public-ip) * Support for the following features in Block Storage Service + * Automated and policy-based scheduled backups (oci bv volume-backup-policy | volume-backup-policy-assignment) * Read-only volume attachments (--is-read-only option while attaching volume) * Incremental backups (--type option while creating a volume backup) diff --git a/requirements.txt b/requirements.txt index 0d9ca853..ee5836f2 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.15 +oci==1.3.16 packaging==16.8 pluggy==0.4.0 py==1.4.33 diff --git a/setup.py b/setup.py index 65914529..70429e19 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def open_relative(*path): requires = [ - 'oci==1.3.15', + 'oci==1.3.16', 'arrow==0.10.0', 'certifi', 'click==6.7', diff --git a/src/oci_cli/__init__.py b/src/oci_cli/__init__.py index ca97b96a..d8a883e7 100644 --- a/src/oci_cli/__init__.py +++ b/src/oci_cli/__init__.py @@ -11,6 +11,7 @@ from .generated import compute_cli # noqa: F401 from .generated import database_cli # noqa: F401 from .generated import dns_cli # noqa: F401 +from .generated import email_cli # noqa: F401 from .generated import filestorage_cli # noqa: F401 from .generated import identity_cli # noqa: F401 from .generated import loadbalancer_cli # noqa: F401 @@ -24,6 +25,7 @@ from . import dns_cli_extended # noqa: F401 from . import identity_cli_extended # noqa: F401 from . import objectstorage_cli_extended # noqa: F401 +from . import email_cli_extended # noqa: F401 from . import filestorage_cli_extended # noqa: F401 from . import file_filters # noqa: F401 from . import final_command_processor # noqa: F401 diff --git a/src/oci_cli/bin/OciTabExpansion.ps1 b/src/oci_cli/bin/OciTabExpansion.ps1 index 432e6199..19f62436 100644 --- a/src/oci_cli/bin/OciTabExpansion.ps1 +++ b/src/oci_cli/bin/OciTabExpansion.ps1 @@ -1,7 +1,7 @@ # Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. $ociTopLevelCommands = @( - 'audit', 'bv', 'compute', 'db', 'dns', 'fs', 'iam', 'lb', 'network', 'os', 'setup' + 'audit', 'bv', 'compute', 'db', 'dns', 'email', 'fs', 'iam', 'lb', 'network', 'os', 'setup' ) $ociSubcommands = @{ @@ -46,6 +46,9 @@ $ociSubcommands = @{ 'dns record rrset' = 'delete get patch update' 'dns record zone' = 'get patch update' 'dns zone' = 'create delete get list update' + 'email' = 'sender suppression' + 'email sender' = 'create delete get list' + 'email suppression' = 'create delete get list' 'fs' = 'export export-set file-system mount-target snapshot' 'fs export' = 'create delete get list' 'fs export-set' = 'get list update' @@ -155,7 +158,7 @@ $ociCommandsToLongParams = @{ 'compute instance detach-vnic' = 'compartment-id force from-json help max-wait-seconds vnic-id wait-for-state wait-interval-seconds' 'compute instance get' = 'from-json help instance-id' 'compute instance get-windows-initial-creds' = 'from-json help instance-id' - 'compute instance launch' = 'assign-public-ip availability-domain compartment-id defined-tags display-name extended-metadata freeform-tags from-json help hostname-label image-id ipxe-script-file max-wait-seconds metadata private-ip shape skip-source-dest-check source-boot-volume-id source-details ssh-authorized-keys-file subnet-id user-data-file vnic-display-name wait-for-state wait-interval-seconds' + 'compute instance launch' = 'assign-public-ip availability-domain boot-volume-size-in-gbs compartment-id defined-tags display-name extended-metadata freeform-tags from-json help hostname-label image-id ipxe-script-file max-wait-seconds metadata private-ip shape skip-source-dest-check source-boot-volume-id source-details ssh-authorized-keys-file subnet-id user-data-file vnic-display-name wait-for-state wait-interval-seconds' 'compute instance list' = 'all availability-domain compartment-id display-name from-json help lifecycle-state limit page page-size sort-by sort-order' 'compute instance list-vnics' = 'all from-json help instance-id limit page page-size' 'compute instance terminate' = 'force from-json help if-match instance-id max-wait-seconds preserve-boot-volume wait-for-state wait-interval-seconds' @@ -213,14 +216,14 @@ $ociCommandsToLongParams = @{ 'db system-shape list' = 'all availability-domain compartment-id from-json help limit page page-size' 'db version list' = 'all compartment-id db-system-shape from-json help limit page page-size' 'dns record domain delete' = 'compartment-id domain force from-json help if-match if-unmodified-since zone-name-or-id' - 'dns record domain get' = 'compartment-id domain from-json help if-modified-since if-none-match limit page rtype sort-by sort-order zone-name-or-id zone-version' + 'dns record domain get' = 'all compartment-id domain from-json help if-modified-since if-none-match limit page page-size rtype sort-by sort-order zone-name-or-id zone-version' 'dns record domain patch' = 'compartment-id domain from-json help if-match if-unmodified-since items zone-name-or-id' 'dns record domain update' = 'compartment-id domain force from-json help if-match if-unmodified-since items zone-name-or-id' 'dns record rrset delete' = 'compartment-id domain force from-json help if-match if-unmodified-since rtype zone-name-or-id' - 'dns record rrset get' = 'compartment-id domain from-json help if-modified-since if-none-match limit page rtype zone-name-or-id zone-version' + 'dns record rrset get' = 'all compartment-id domain from-json help if-modified-since if-none-match limit page page-size rtype zone-name-or-id zone-version' 'dns record rrset patch' = 'compartment-id domain from-json help if-match if-unmodified-since items rtype zone-name-or-id' 'dns record rrset update' = 'compartment-id domain force from-json help if-match if-unmodified-since items rtype zone-name-or-id' - 'dns record zone get' = 'compartment-id domain domain-contains from-json help if-modified-since if-none-match limit page rtype sort-by sort-order zone-name-or-id zone-version' + 'dns record zone get' = 'all compartment-id domain domain-contains from-json help if-modified-since if-none-match limit page page-size rtype sort-by sort-order zone-name-or-id zone-version' 'dns record zone patch' = 'compartment-id from-json help if-match if-unmodified-since items zone-name-or-id' 'dns record zone update' = 'compartment-id force from-json help if-match if-unmodified-since items zone-name-or-id' 'dns zone create' = 'compartment-id external-masters from-json help max-wait-seconds name wait-for-state wait-interval-seconds zone-type' @@ -228,6 +231,14 @@ $ociCommandsToLongParams = @{ 'dns zone get' = 'compartment-id from-json help if-modified-since if-none-match zone-name-or-id' 'dns zone list' = 'all compartment-id from-json help lifecycle-state limit name name-contains page page-size sort-by sort-order time-created-greater-than-or-equal-to time-created-less-than zone-type' 'dns zone update' = 'compartment-id external-masters force from-json help if-match if-unmodified-since max-wait-seconds wait-for-state wait-interval-seconds zone-name-or-id' + 'email sender create' = 'compartment-id email-address from-json help max-wait-seconds wait-for-state wait-interval-seconds' + 'email sender delete' = 'force from-json help max-wait-seconds sender-id wait-for-state wait-interval-seconds' + 'email sender get' = 'from-json help sender-id' + 'email sender list' = 'all compartment-id email-address from-json help lifecycle-state limit page page-size sort-by sort-order' + 'email suppression create' = 'compartment-id email-address from-json help' + 'email suppression delete' = 'force from-json help suppression-id' + 'email suppression get' = 'from-json help suppression-id' + 'email suppression list' = 'all compartment-id email-address from-json help limit page page-size sort-by sort-order time-created-greater-than-or-equal-to time-created-less-than' 'fs export create' = 'export-set-id file-system-id from-json help max-wait-seconds path wait-for-state wait-interval-seconds' 'fs export delete' = 'export-id force from-json help if-match max-wait-seconds wait-for-state wait-interval-seconds' 'fs export get' = 'export-id from-json help' @@ -430,7 +441,7 @@ $ociCommandsToLongParams = @{ 'os object get' = 'bucket-name file from-json help if-match if-none-match multipart-download-threshold name namespace parallel-download-count part-size range' 'os object head' = 'bucket-name from-json help if-match if-none-match name namespace' 'os object list' = 'all bucket-name delimiter end fields from-json help limit namespace page-size prefix start' - 'os object put' = 'bucket-name content-encoding content-language content-md5 content-type disable-parallel-uploads file force from-json help if-match metadata name namespace no-multipart parallel-upload-count part-size' + 'os object put' = 'bucket-name content-encoding content-language content-md5 content-type disable-parallel-uploads file force from-json help if-match metadata name namespace no-multipart no-overwrite parallel-upload-count part-size' 'os object rename' = 'bucket bucket-name from-json help name namespace namespace-name new-if-match new-if-none-match new-name new-obj-if-match-e-tag new-obj-if-none-match-e-tag source-name src-if-match src-obj-if-match-e-tag' 'os object restore' = 'bucket from-json help hours name namespace' 'os object restore-status' = 'bucket-name from-json help name namespace' @@ -568,6 +579,14 @@ $ociCommandsToShortParams = @{ 'dns zone get' = '? c h' 'dns zone list' = '? c h' 'dns zone update' = '? c h' + 'email sender create' = '? c h' + 'email sender delete' = '? h' + 'email sender get' = '? h' + 'email sender list' = '? c h' + 'email suppression create' = '? c h' + 'email suppression delete' = '? h' + 'email suppression get' = '? h' + 'email suppression list' = '? c h' 'fs export create' = '? h' 'fs export delete' = '? h' 'fs export get' = '? h' diff --git a/src/oci_cli/cli.py b/src/oci_cli/cli.py index 88f8c164..0c5438b4 100644 --- a/src/oci_cli/cli.py +++ b/src/oci_cli/cli.py @@ -9,6 +9,7 @@ from .generated import compute_cli # noqa: F401 from .generated import database_cli # noqa: F401 from .generated import dns_cli # noqa: F401 +from .generated import email_cli # noqa: F401 from .generated import filestorage_cli # noqa: F401 from .generated import identity_cli # noqa: F401 from .generated import loadbalancer_cli # noqa: F401 @@ -19,6 +20,7 @@ from . import core_cli_extended # noqa: F401 from . import database_cli_extended # noqa: F401 from . import dns_cli_extended # noqa: F401 +from . import email_cli_extended # noqa: F401 from . import filestorage_cli_extended # noqa: F401 from . import identity_cli_extended # noqa: F401 from . import lb_cli_extended # noqa: F401 diff --git a/src/oci_cli/cli_clients.py b/src/oci_cli/cli_clients.py index 337c27f3..d0496d43 100644 --- a/src/oci_cli/cli_clients.py +++ b/src/oci_cli/cli_clients.py @@ -8,6 +8,7 @@ from oci.core import VirtualNetworkClient from oci.database import DatabaseClient from oci.dns import DnsClient +from oci.email import EmailClient from oci.file_storage import FileStorageClient from oci.identity import IdentityClient from oci.load_balancer import LoadBalancerClient @@ -19,6 +20,7 @@ "compute": ComputeClient, "database": DatabaseClient, "dns": DnsClient, + "email": EmailClient, "file_storage": FileStorageClient, "identity": IdentityClient, "load_balancer": LoadBalancerClient, @@ -31,6 +33,7 @@ "core": oci.core.models.core_type_mapping, "database": oci.database.models.database_type_mapping, "dns": oci.dns.models.dns_type_mapping, + "email": oci.email.models.email_type_mapping, "file_storage": oci.file_storage.models.file_storage_type_mapping, "identity": oci.identity.models.identity_type_mapping, "load_balancer": oci.load_balancer.models.load_balancer_type_mapping, diff --git a/src/oci_cli/cli_setup.py b/src/oci_cli/cli_setup.py index e57e4d46..86acf52f 100644 --- a/src/oci_cli/cli_setup.py +++ b/src/oci_cli/cli_setup.py @@ -447,17 +447,18 @@ def apply_user_only_access_permissions(path): # - thus if the user elects to place a new file (config or key) in an existing directory, we will not change the # permissions of that directory but will explicitly set the permissions on that file username = os.environ['USERNAME'] + admin_grp = '*S-1-5-32-544' + system_usr = '*S-1-5-18' try: if os.path.isfile(path): subprocess.check_output('icacls "{path}" /reset'.format(path=path), stderr=subprocess.STDOUT) - subprocess.check_output('icacls "{path}" /inheritance:r /grant:r {username}:F /grant Administrators:F /grant System:F'.format(path=path, username=username), stderr=subprocess.STDOUT) + subprocess.check_output('icacls "{path}" /inheritance:r /grant:r {username}:F /grant {admin_grp}:F /grant {system_usr}:F'.format(path=path, username=username, admin_grp=admin_grp, system_usr=system_usr), stderr=subprocess.STDOUT) else: if os.listdir(path): # safety check to make sure we aren't changing permissions of existing files raise RuntimeError("Failed attempting to set permissions on existing folder that is not empty.") - subprocess.check_output('icacls "{path}" /reset'.format(path=path), stderr=subprocess.STDOUT) - subprocess.check_output('icacls "{path}" /inheritance:r /grant:r {username}:(OI)(CI)F /grant:r Administrators:(OI)(CI)F /grant:r System:(OI)(CI)F'.format(path=path, username=username), stderr=subprocess.STDOUT) + subprocess.check_output('icacls "{path}" /inheritance:r /grant:r {username}:(OI)(CI)F /grant:r {admin_grp}:(OI)(CI)F /grant:r {system_usr}:(OI)(CI)F'.format(path=path, username=username, admin_grp=admin_grp, system_usr=system_usr), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as exc_info: print("Error occurred while attempting to set permissions for {path}: {exception}".format(path=path, exception=str(exc_info))) diff --git a/src/oci_cli/cli_util.py b/src/oci_cli/cli_util.py index a65ea182..dce2201c 100644 --- a/src/oci_cli/cli_util.py +++ b/src/oci_cli/cli_util.py @@ -53,7 +53,8 @@ DISPLAY_HEADERS = { "etag", "opc-next-page", - "opc-work-request-id" + "opc-work-request-id", + "opc-total-items" } @@ -98,7 +99,9 @@ "instance_action.command_name": "action", "volume_backup_group.command_name": "backup", "file_storage_group.help": "File Storage Service", - "file_storage_group.command_name": "fs" + "file_storage_group.command_name": "fs", + "email_group.help": "Email Delivery Service", + "dns_group.help": "DNS Zone Management Service" } @@ -931,6 +934,9 @@ def get_default_value_from_defaults_file(ctx, param_name, param_type, param_take def convert_value_from_param_type(value, param_type, param_takes_multiple): + # Inline import to avoid a circular dependency + from .custom_types import CLI_DATETIME + if value is None: return value @@ -954,11 +960,16 @@ def convert_value_from_param_type(value, param_type, param_takes_multiple): return float(expanded_value) elif param_type == click.INT: return int(expanded_value) + elif param_type == CLI_DATETIME: + return CLI_DATETIME.convert(expanded_value, None, None) else: return expanded_value def convert_value_from_param_type_accepting_multiple(value, param_type): + # Inline import to avoid a circular dependency + from .custom_types import CLI_DATETIME + # Since our splitting into multiples relies on a string split, we can't do anything if it's # not a string if not isinstance(value, six.string_types): @@ -983,6 +994,8 @@ def convert_value_from_param_type_accepting_multiple(value, param_type): converted_values.append(float(stripped_val)) elif param_type == click.INT: converted_values.append(int(stripped_val)) + elif param_type == CLI_DATETIME: + converted_values.append(CLI_DATETIME.convert(stripped_val, None, None)) else: converted_values.append(stripped_val) @@ -1097,18 +1110,20 @@ def warn_on_invalid_file_permissions(filepath): # If any other users or groups have permissions to the file, a warning will be printed indicating which additional groups # have permissions and should not. def windows_warn_on_invalid_file_permissions(filename): - username = os.environ.get('USERNAME') - userdomain = os.environ.get('USERDOMAIN') - - current_user_identity = '{}\\{}'.format(userdomain, username) - identities_allowed_permissions = [current_user_identity, "NT AUTHORITY\\SYSTEM", "BUILTIN\\Administrators"] - identities_list_string = ','.join(['\\"{}\\"'.format(name) for name in identities_allowed_permissions]) - # one line powershell command to output newline separated list of all users / groups - # with access to a given file that are not in 'identities_allowed_permissions' + # with access to a given file that are not in BUILTIN\Administrators, NT Authority\System or current user try: - cmd = '(Get-Acl {}).Access | Where {{-not ($_.IdentityReference -in {})}} | Select-Object -ExpandProperty IdentityReference | Format-Table -HideTableHeaders'.format(filename, identities_list_string) - output = subprocess.check_output('powershell.exe "{}"'.format(cmd)) + cmd = ( + '$ex_perms=@();' + '$defaults=@();' + '$macls=(Get-Acl {filename}).Access.IdentityReference;' + '$defaults+=[wmi]\"win32_SID.SID=\'S-1-5-32-544\'\"|%{{$_.ReferencedDomainName + \"\\\" + $_.AccountName}};' + '$defaults+=[wmi]\"win32_SID.SID=\'S-1-5-18\'\"|%{{$_.ReferencedDomainName + \"\\\" + $_.AccountName}};' + '$defaults+=\"$env:USERDOMAIN\" + \"\\\" + \"$env:USERNAME\";' + 'foreach ($i in $macls){{foreach ($m in $defaults){{if($i -eq $m){{$found=$true;}}}};if(!$found){{$ex_perms+=$i}};$found=$false;}};' + '"$ex_perms";'.format(filename=filename) + ) + output = subprocess.check_output(["powershell.exe", '{}'.format(cmd)], shell=True).strip() except Exception: # if somehow executing this throws an exception we don't want to prevent use of the CLI so return here return diff --git a/src/oci_cli/core_cli_extended.py b/src/oci_cli/core_cli_extended.py index 17d4754b..4d1663b4 100644 --- a/src/oci_cli/core_cli_extended.py +++ b/src/oci_cli/core_cli_extended.py @@ -139,6 +139,8 @@ cli_util.update_param_help(virtualnetwork_cli.create_security_list, 'ingress_security_rules', '', append=True, example=network_create_security_list_ingress_security_rules_example) cli_util.update_param_help(virtualnetwork_cli.update_security_list, 'ingress_security_rules', '', append=True, example=network_create_security_list_ingress_security_rules_example) +cli_util.get_param(compute_cli.attach_volume, 'type').type = custom_types.CliCaseInsensitiveChoice(['iscsi', 'paravirtualized']) + # Formatting of the help for the bcms network private-ip list command virtualnetwork_cli.list_private_ips.help = """Lists the [PrivateIp] objects based on one of these filters: @@ -413,6 +415,7 @@ def list_vnics(ctx, from_json, instance_id, limit, page, all_pages, page_size): @click.option('--user-data-file', callback=cli_util.handle_optional_param, type=click.File('rb'), help="""A file containing data that Cloud-Init can use to run custom scripts or provide custom Cloud-Init configuration. This parameter is a convenience wrapper around the 'user_data' field of the --metadata parameter. Populating both values in the same call will result in an error. For more info see Cloud-Init documentation: https://cloudinit.readthedocs.org/en/latest/topics/format.html.""") @click.option('--ssh-authorized-keys-file', callback=cli_util.handle_optional_param, type=click.File('r'), help="""A file containing one or more public SSH keys to be included in the ~/.ssh/authorized_keys file for the default user on the instance. Use a newline character to separate multiple keys. The SSH keys must be in the format necessary for the authorized_keys file. This parameter is a convenience wrapper around the 'ssh_authorized_keys' field of the --metadata parameter. Populating both values in the same call will result in an error. For more info see documentation: https://docs.us-phoenix-1.oraclecloud.com/api/#/en/iaas/20160918/requests/LaunchInstanceDetails.""") @click.option('--source-boot-volume-id', callback=cli_util.handle_optional_param, help="""The OCID of the boot volume used to boot the instance. This is a shortcut for specifying a boot volume source via the --source-details complex JSON parameter. If this parameter is provided, you cannot provide the --source-details or --image-id parameters.""") +@click.option('--boot-volume-size-in-gbs', callback=cli_util.handle_optional_param, type=click.INT, help="""The size of the boot volume in GBs. Minimum value is 50 GB and maximum value is 16384 GB (16TB). This is a shortcut for specifying a boot volume size via the --source-details complex JSON parameter. If this parameter is provided, you cannot provide the --source-details or --source-boot-volume-id parameters.""") @click.pass_context @json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={'create-vnic-details': {'module': 'core', 'class': 'CreateVnicDetails'}, 'defined-tags': {'module': 'core', 'class': 'dict(str, dict(str, object))'}, 'extended-metadata': {'module': 'core', 'class': 'dict(str, object)'}, 'freeform-tags': {'module': 'core', 'class': 'dict(str, string)'}, 'metadata': {'module': 'core', 'class': 'dict(str, string)'}, 'source-details': {'module': 'core', 'class': 'InstanceSourceDetails'}}, output_type={'module': 'core', 'class': 'Instance'}) @cli_util.wrap_exceptions @@ -437,14 +440,18 @@ def launch_instance_extended(ctx, **kwargs): else: metadata['ssh_authorized_keys'] = ssh_authorized_keys_file.read() - if kwargs.get('source_details') and (kwargs.get('image_id') or kwargs.get('source_boot_volume_id')): + if kwargs.get('source_details') and (kwargs.get('image_id') or kwargs.get('source_boot_volume_id') or kwargs.get('boot_volume_size_in_gbs')): raise click.UsageError( - 'Cannot specify --source-details and either --image-id or --source-boot-volume-id' + 'Cannot specify --source-details with any of: --image-id, --source-boot-volume-id, or --boot-volume-size-in-gbs' ) if kwargs.get('image_id') and kwargs.get('source_boot_volume_id'): raise click.UsageError( 'Cannot specify both an --image-id and a --source-boot-volume-id to be used to boot the instance' ) + if kwargs.get('boot_volume_size_in_gbs') and kwargs.get('source_boot_volume_id'): + raise click.UsageError( + 'Cannot specify both a --source-boot-volume-id and a --boot-volume-size-in-gbs to be used to boot the instance' + ) kwargs['metadata'] = json.dumps(metadata) @@ -471,7 +478,11 @@ def launch_instance_extended(ctx, **kwargs): kwargs['create_vnic_details'] = json.dumps(create_vnic_details) if kwargs.get('image_id'): - kwargs['source_details'] = json.dumps({'sourceType': 'image', 'imageId': kwargs['image_id']}) + source_details = {'sourceType': 'image', 'imageId': kwargs['image_id']} + if kwargs.get('boot_volume_size_in_gbs'): + source_details['bootVolumeSizeInGBs'] = kwargs.get('boot_volume_size_in_gbs') + + kwargs['source_details'] = json.dumps(source_details) if kwargs.get('source_boot_volume_id'): kwargs['source_details'] = json.dumps({'sourceType': 'bootVolume', 'bootVolumeId': kwargs['source_boot_volume_id']}) @@ -485,8 +496,9 @@ def launch_instance_extended(ctx, **kwargs): del kwargs['vnic_display_name'] del kwargs['skip_source_dest_check'] - # Only remove the source_boot_volume_id parameter. image_id is an existing parameter so the underlying + # Remove the source_boot_volume_id and boot_volume_size_in_gbs parameters. image_id is an existing parameter so the underlying # CLI operation will accept it + kwargs.pop('boot_volume_size_in_gbs', None) kwargs.pop('source_boot_volume_id', None) json_skeleton_utils.remove_json_skeleton_params_from_dict(kwargs) diff --git a/src/oci_cli/custom_types/cli_datetime.py b/src/oci_cli/custom_types/cli_datetime.py index ab577fad..b352ae2e 100644 --- a/src/oci_cli/custom_types/cli_datetime.py +++ b/src/oci_cli/custom_types/cli_datetime.py @@ -96,12 +96,11 @@ def convert(self, value, param, ctx): if parsed_value is None: raise click.BadParameter('Value {} is not in a supported datetime format. {}'.format(value, self.VALID_DATETIME_MESSAGE)) - # Return a valid RFC3339 datetime string. No timezone conversion happens here since the time we have at this point is either: + # Return a valid RFC3339 datetime string with a Z-offset. This is for consistency with how other SDKs behave, and also + # for compatibility with services. # - # - In UTC already because we were provided epoch seconds or YYY-MM-DD (where we assume midnight UTC) - # - Our other supported formats have an explicit timezone specifier. In that case we send it through to the service - # as-is rather than converting the type for the caller and potentially mangling their input - return parsed_value.format('YYYY-MM-DDTHH:mm:ss.SSSZZ') + # Applying the UTC conversion is a no-op if the timezone is already UTC + return '{}Z'.format(parsed_value.to('utc').format('YYYY-MM-DDTHH:mm:ss.SSS')) def get_matching_datetime_string_format(self, value): for datetime_format in self.VALID_DATETIME_FORMATS: diff --git a/src/oci_cli/email_cli_extended.py b/src/oci_cli/email_cli_extended.py new file mode 100644 index 00000000..fdf9056e --- /dev/null +++ b/src/oci_cli/email_cli_extended.py @@ -0,0 +1,19 @@ +# coding: utf-8 +# Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + +from . import cli_util +from .generated import email_cli + +# create sender update compartment_id and email_address to be required +cli_util.update_param_help(email_cli.create_sender, 'compartment_id', ' [required]', append=True) +cli_util.get_param(email_cli.create_sender, 'compartment_id').callback = cli_util.handle_required_param + +cli_util.update_param_help(email_cli.create_sender, 'email_address', ' [required]', append=True) +cli_util.get_param(email_cli.create_sender, 'email_address').callback = cli_util.handle_required_param + +# create suppression update compartment_id and email_address to be required +cli_util.update_param_help(email_cli.create_suppression, 'compartment_id', ' [required]', append=True) +cli_util.get_param(email_cli.create_suppression, 'compartment_id').callback = cli_util.handle_required_param + +cli_util.update_param_help(email_cli.create_suppression, 'email_address', ' [required]', append=True) +cli_util.get_param(email_cli.create_suppression, 'email_address').callback = cli_util.handle_required_param diff --git a/src/oci_cli/generated/compute_cli.py b/src/oci_cli/generated/compute_cli.py index ec13065e..8e69d162 100644 --- a/src/oci_cli/generated/compute_cli.py +++ b/src/oci_cli/generated/compute_cli.py @@ -196,7 +196,7 @@ def attach_vnic(ctx, from_json, wait_for_state, max_wait_seconds, wait_interval_ @volume_attachment_group.command(name=cli_util.override('attach_volume.command_name', 'attach'), help="""Attaches the specified storage volume to the specified instance.""") @click.option('--instance-id', callback=cli_util.handle_required_param, help="""The OCID of the instance. [required]""") -@click.option('--type', callback=cli_util.handle_required_param, help="""The type of volume. The only supported value is \"iscsi\". [required]""") +@click.option('--type', callback=cli_util.handle_required_param, help="""The type of volume. The only supported value are \"iscsi\" and \"paravirtualized\". [required]""") @click.option('--volume-id', callback=cli_util.handle_required_param, help="""The OCID of the volume. [required]""") @click.option('--display-name', callback=cli_util.handle_optional_param, help="""A user-friendly name. Does not have to be unique, and it cannot be changed. Avoid entering confidential information.""") @click.option('--is-read-only', callback=cli_util.handle_optional_param, type=click.BOOL, help="""Whether the attachment was created in read-only mode.""") @@ -1655,7 +1655,7 @@ def list_vnic_attachments(ctx, from_json, all_pages, page_size, compartment_id, @volume_attachment_group.command(name=cli_util.override('list_volume_attachments.command_name', 'list'), help="""Lists the volume attachments in the specified compartment. You can filter the list by specifying an instance OCID, volume OCID, or both. -Currently, the only supported volume attachment type is [IScsiVolumeAttachment].""") +Currently, the only supported volume attachment type are [IScsiVolumeAttachment] and [ParavirtualizedVolumeAttachment].""") @click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID of the compartment. [required]""") @click.option('--availability-domain', callback=cli_util.handle_optional_param, help="""The name of the Availability Domain. diff --git a/src/oci_cli/generated/dns_cli.py b/src/oci_cli/generated/dns_cli.py index c700056e..966cb615 100644 --- a/src/oci_cli/generated/dns_cli.py +++ b/src/oci_cli/generated/dns_cli.py @@ -250,12 +250,17 @@ def delete_zone(ctx, from_json, wait_for_state, max_wait_seconds, wait_interval_ @click.option('--sort-by', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["rtype", "ttl"]), help="""The field by which to sort records.""") @click.option('--sort-order', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["ASC", "DESC"]), help="""The order to sort the resources.""") @click.option('--compartment-id', callback=cli_util.handle_optional_param, help="""The OCID of the compartment the resource belongs to.""") +@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': 'dns', 'class': 'RecordCollection'}) @cli_util.wrap_exceptions -def get_domain_records(ctx, from_json, zone_name_or_id, domain, if_none_match, if_modified_since, limit, page, zone_version, rtype, sort_by, sort_order, compartment_id): +def get_domain_records(ctx, from_json, all_pages, page_size, zone_name_or_id, domain, if_none_match, if_modified_since, limit, page, zone_version, rtype, sort_by, sort_order, compartment_id): + + if all_pages and limit: + raise click.UsageError('If you provide the --all option you cannot provide the --limit option') if isinstance(zone_name_or_id, six.string_types) and len(zone_name_or_id.strip()) == 0: raise click.UsageError('Parameter --zone-name-or-id cannot be whitespace or empty string') @@ -282,11 +287,31 @@ def get_domain_records(ctx, from_json, zone_name_or_id, domain, if_none_match, i if compartment_id is not None: kwargs['compartment_id'] = compartment_id client = cli_util.build_client('dns', ctx) - result = client.get_domain_records( - zone_name_or_id=zone_name_or_id, - domain=domain, - **kwargs - ) + if all_pages: + if page_size: + kwargs['limit'] = page_size + + result = retry_utils.list_call_get_all_results_with_default_retries( + client.get_domain_records, + zone_name_or_id=zone_name_or_id, + domain=domain, + **kwargs + ) + elif limit is not None: + result = retry_utils.list_call_get_up_to_limit_with_default_retries( + client.get_domain_records, + limit, + page_size, + zone_name_or_id=zone_name_or_id, + domain=domain, + **kwargs + ) + else: + result = client.get_domain_records( + zone_name_or_id=zone_name_or_id, + domain=domain, + **kwargs + ) cli_util.render_response(result, ctx) @@ -300,12 +325,17 @@ def get_domain_records(ctx, from_json, zone_name_or_id, domain, if_none_match, i @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('--zone-version', callback=cli_util.handle_optional_param, help="""The version of the zone for which data is requested.""") @click.option('--compartment-id', callback=cli_util.handle_optional_param, help="""The OCID of the compartment the resource belongs to.""") +@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': 'dns', 'class': 'RRSet'}) @cli_util.wrap_exceptions -def get_rr_set(ctx, from_json, zone_name_or_id, domain, rtype, if_none_match, if_modified_since, limit, page, zone_version, compartment_id): +def get_rr_set(ctx, from_json, all_pages, page_size, zone_name_or_id, domain, rtype, if_none_match, if_modified_since, limit, page, zone_version, compartment_id): + + if all_pages and limit: + raise click.UsageError('If you provide the --all option you cannot provide the --limit option') if isinstance(zone_name_or_id, six.string_types) and len(zone_name_or_id.strip()) == 0: raise click.UsageError('Parameter --zone-name-or-id cannot be whitespace or empty string') @@ -329,12 +359,34 @@ def get_rr_set(ctx, from_json, zone_name_or_id, domain, rtype, if_none_match, if if compartment_id is not None: kwargs['compartment_id'] = compartment_id client = cli_util.build_client('dns', ctx) - result = client.get_rr_set( - zone_name_or_id=zone_name_or_id, - domain=domain, - rtype=rtype, - **kwargs - ) + if all_pages: + if page_size: + kwargs['limit'] = page_size + + result = retry_utils.list_call_get_all_results_with_default_retries( + client.get_rr_set, + zone_name_or_id=zone_name_or_id, + domain=domain, + rtype=rtype, + **kwargs + ) + elif limit is not None: + result = retry_utils.list_call_get_up_to_limit_with_default_retries( + client.get_rr_set, + limit, + page_size, + zone_name_or_id=zone_name_or_id, + domain=domain, + rtype=rtype, + **kwargs + ) + else: + result = client.get_rr_set( + zone_name_or_id=zone_name_or_id, + domain=domain, + rtype=rtype, + **kwargs + ) cli_util.render_response(result, ctx) @@ -380,12 +432,17 @@ def get_zone(ctx, from_json, zone_name_or_id, if_none_match, if_modified_since, @click.option('--sort-by', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["domain", "rtype", "ttl"]), help="""The field by which to sort records.""") @click.option('--sort-order', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["ASC", "DESC"]), help="""The order to sort the resources.""") @click.option('--compartment-id', callback=cli_util.handle_optional_param, help="""The OCID of the compartment the resource belongs to.""") +@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': 'dns', 'class': 'RecordCollection'}) @cli_util.wrap_exceptions -def get_zone_records(ctx, from_json, zone_name_or_id, if_none_match, if_modified_since, limit, page, zone_version, domain, domain_contains, rtype, sort_by, sort_order, compartment_id): +def get_zone_records(ctx, from_json, all_pages, page_size, zone_name_or_id, if_none_match, if_modified_since, limit, page, zone_version, domain, domain_contains, rtype, sort_by, sort_order, compartment_id): + + if all_pages and limit: + raise click.UsageError('If you provide the --all option you cannot provide the --limit option') if isinstance(zone_name_or_id, six.string_types) and len(zone_name_or_id.strip()) == 0: raise click.UsageError('Parameter --zone-name-or-id cannot be whitespace or empty string') @@ -413,10 +470,28 @@ def get_zone_records(ctx, from_json, zone_name_or_id, if_none_match, if_modified if compartment_id is not None: kwargs['compartment_id'] = compartment_id client = cli_util.build_client('dns', ctx) - result = client.get_zone_records( - zone_name_or_id=zone_name_or_id, - **kwargs - ) + if all_pages: + if page_size: + kwargs['limit'] = page_size + + result = retry_utils.list_call_get_all_results_with_default_retries( + client.get_zone_records, + zone_name_or_id=zone_name_or_id, + **kwargs + ) + elif limit is not None: + result = retry_utils.list_call_get_up_to_limit_with_default_retries( + client.get_zone_records, + limit, + page_size, + zone_name_or_id=zone_name_or_id, + **kwargs + ) + else: + result = client.get_zone_records( + zone_name_or_id=zone_name_or_id, + **kwargs + ) cli_util.render_response(result, ctx) diff --git a/src/oci_cli/generated/email_cli.py b/src/oci_cli/generated/email_cli.py new file mode 100644 index 00000000..7e5f2a9d --- /dev/null +++ b/src/oci_cli/generated/email_cli.py @@ -0,0 +1,345 @@ +# coding: utf-8 +# Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + +from __future__ import print_function +import click +import oci # noqa: F401 +import six # noqa: F401 +import sys # noqa: F401 +from ..cli_root import cli +from .. import cli_util +from .. import json_skeleton_utils +from .. import retry_utils # noqa: F401 +from .. import custom_types # noqa: F401 +from ..aliasing import CommandGroupWithAlias + + +@cli.command(cli_util.override('email_group.command_name', 'email'), cls=CommandGroupWithAlias, help=cli_util.override('email_group.help', """API spec for managing OCI Email Delivery services.""")) +@cli_util.help_option_group +def email_group(): + pass + + +@click.command(cli_util.override('sender_group.command_name', 'sender'), cls=CommandGroupWithAlias, help="""The full information representing an approved sender.""") +@cli_util.help_option_group +def sender_group(): + pass + + +@click.command(cli_util.override('suppression_group.command_name', 'suppression'), cls=CommandGroupWithAlias, help="""The full information representing an email suppression.""") +@cli_util.help_option_group +def suppression_group(): + pass + + +email_group.add_command(sender_group) +email_group.add_command(suppression_group) + + +@sender_group.command(name=cli_util.override('create_sender.command_name', 'create'), help="""Creates a sender for a tenancy in a given compartment.""") +@click.option('--compartment-id', callback=cli_util.handle_optional_param, help="""The OCID of the compartment that contains the sender.""") +@click.option('--email-address', callback=cli_util.handle_optional_param, help="""The email address of the sender.""") +@click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "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': 'email', 'class': 'Sender'}) +@cli_util.wrap_exceptions +def create_sender(ctx, from_json, wait_for_state, max_wait_seconds, wait_interval_seconds, compartment_id, email_address): + kwargs = {} + + details = {} + + if compartment_id is not None: + details['compartmentId'] = compartment_id + + if email_address is not None: + details['emailAddress'] = email_address + + client = cli_util.build_client('email', ctx) + result = client.create_sender( + create_sender_details=details, + **kwargs + ) + if wait_for_state: + if hasattr(client, 'get_sender') and callable(getattr(client, 'get_sender')): + 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_sender, 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) + + +@suppression_group.command(name=cli_util.override('create_suppression.command_name', 'create'), help="""Adds recipient email addresses to the suppression list for a tenancy.""") +@click.option('--compartment-id', callback=cli_util.handle_optional_param, help="""The OCID of the compartment to contain the suppression. Since suppressions are at the customer level, this must be the tenancy OCID.""") +@click.option('--email-address', callback=cli_util.handle_optional_param, help="""The recipient email address of the suppression.""") +@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': 'email', 'class': 'Suppression'}) +@cli_util.wrap_exceptions +def create_suppression(ctx, from_json, compartment_id, email_address): + kwargs = {} + + details = {} + + if compartment_id is not None: + details['compartmentId'] = compartment_id + + if email_address is not None: + details['emailAddress'] = email_address + + client = cli_util.build_client('email', ctx) + result = client.create_suppression( + create_suppression_details=details, + **kwargs + ) + cli_util.render_response(result, ctx) + + +@sender_group.command(name=cli_util.override('delete_sender.command_name', 'delete'), help="""Deletes an approved sender for a tenancy in a given compartment for a provided `senderId`.""") +@click.option('--sender-id', callback=cli_util.handle_required_param, help="""The unique OCID of the sender. [required]""") +@cli_util.confirm_delete_option +@click.option('--wait-for-state', type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "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_sender(ctx, from_json, wait_for_state, max_wait_seconds, wait_interval_seconds, sender_id): + + if isinstance(sender_id, six.string_types) and len(sender_id.strip()) == 0: + raise click.UsageError('Parameter --sender-id cannot be whitespace or empty string') + kwargs = {} + client = cli_util.build_client('email', ctx) + result = client.delete_sender( + sender_id=sender_id, + **kwargs + ) + if wait_for_state: + if hasattr(client, 'get_sender') and callable(getattr(client, 'get_sender')): + 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_sender, sender_id), 'lifecycle_state', wait_for_state, succeed_on_not_found=True, **wait_period_kwargs) + except oci.exceptions.ServiceError as e: + # We make an initial service call so we can pass the result to oci.wait_until(), however if we are waiting on the + # outcome of a delete operation it is possible that the resource is already gone and so the initial service call + # will result in an exception that reflects a HTTP 404. In this case, we can exit with success (rather than raising + # the exception) since this would have been the behaviour in the waiter anyway (as for delete we provide the argument + # succeed_on_not_found=True to the waiter). + # + # Any non-404 should still result in the exception being thrown. + if e.status == 404: + pass + else: + raise + 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) + + +@suppression_group.command(name=cli_util.override('delete_suppression.command_name', 'delete'), help="""Removes a suppressed recipient email address from the suppression list for a tenancy in a given compartment for a provided `suppressionId`.""") +@click.option('--suppression-id', callback=cli_util.handle_required_param, help="""The unique OCID of the suppression. [required]""") +@cli_util.confirm_delete_option +@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_suppression(ctx, from_json, suppression_id): + + if isinstance(suppression_id, six.string_types) and len(suppression_id.strip()) == 0: + raise click.UsageError('Parameter --suppression-id cannot be whitespace or empty string') + kwargs = {} + client = cli_util.build_client('email', ctx) + result = client.delete_suppression( + suppression_id=suppression_id, + **kwargs + ) + cli_util.render_response(result, ctx) + + +@sender_group.command(name=cli_util.override('get_sender.command_name', 'get'), help="""Gets an approved sender for a given `senderId`.""") +@click.option('--sender-id', callback=cli_util.handle_required_param, help="""The unique OCID of the sender. [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': 'email', 'class': 'Sender'}) +@cli_util.wrap_exceptions +def get_sender(ctx, from_json, sender_id): + + if isinstance(sender_id, six.string_types) and len(sender_id.strip()) == 0: + raise click.UsageError('Parameter --sender-id cannot be whitespace or empty string') + kwargs = {} + client = cli_util.build_client('email', ctx) + result = client.get_sender( + sender_id=sender_id, + **kwargs + ) + cli_util.render_response(result, ctx) + + +@suppression_group.command(name=cli_util.override('get_suppression.command_name', 'get'), help="""Gets the details of a suppressed recipient email address for a given `suppressionId`. Each suppression is given a unique OCID.""") +@click.option('--suppression-id', callback=cli_util.handle_required_param, help="""The unique OCID of the suppression. [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': 'email', 'class': 'Suppression'}) +@cli_util.wrap_exceptions +def get_suppression(ctx, from_json, suppression_id): + + if isinstance(suppression_id, six.string_types) and len(suppression_id.strip()) == 0: + raise click.UsageError('Parameter --suppression-id cannot be whitespace or empty string') + kwargs = {} + client = cli_util.build_client('email', ctx) + result = client.get_suppression( + suppression_id=suppression_id, + **kwargs + ) + cli_util.render_response(result, ctx) + + +@sender_group.command(name=cli_util.override('list_senders.command_name', 'list'), help="""Gets a collection of approved sender email addresses and sender IDs.""") +@click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID for the compartment. [required]""") +@click.option('--lifecycle-state', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["CREATING", "ACTIVE", "DELETING", "DELETED"]), help="""The current state of a sender.""") +@click.option('--email-address', callback=cli_util.handle_optional_param, help="""The email address of the approved sender.""") +@click.option('--page', callback=cli_util.handle_optional_param, help="""The value of the `opc-next-page` response header from the previous GET request.""") +@click.option('--limit', callback=cli_util.handle_optional_param, type=click.INT, help="""The maximum number of items to return in a paginated GET request.""") +@click.option('--sort-by', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["TIMECREATED", "EMAILADDRESS"]), help="""The field to sort by. The `TIMECREATED` value returns the list in in descending order by default. The `EMAILADDRESS` value returns the list in ascending order by default. Use the `SortOrderQueryParam` to change the direction of the returned list of items.""") +@click.option('--sort-order', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["ASC", "DESC"]), help="""The sort order to use, either ascending or descending order.""") +@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': 'email', 'class': 'list[SenderSummary]'}) +@cli_util.wrap_exceptions +def list_senders(ctx, from_json, all_pages, page_size, compartment_id, lifecycle_state, email_address, page, limit, sort_by, sort_order): + + if all_pages and limit: + raise click.UsageError('If you provide the --all option you cannot provide the --limit option') + kwargs = {} + if lifecycle_state is not None: + kwargs['lifecycle_state'] = lifecycle_state + if email_address is not None: + kwargs['email_address'] = email_address + if page is not None: + kwargs['page'] = page + if limit is not None: + kwargs['limit'] = limit + if sort_by is not None: + kwargs['sort_by'] = sort_by + if sort_order is not None: + kwargs['sort_order'] = sort_order + client = cli_util.build_client('email', ctx) + if all_pages: + if page_size: + kwargs['limit'] = page_size + + result = retry_utils.list_call_get_all_results_with_default_retries( + client.list_senders, + compartment_id=compartment_id, + **kwargs + ) + elif limit is not None: + result = retry_utils.list_call_get_up_to_limit_with_default_retries( + client.list_senders, + limit, + page_size, + compartment_id=compartment_id, + **kwargs + ) + else: + result = client.list_senders( + compartment_id=compartment_id, + **kwargs + ) + cli_util.render_response(result, ctx) + + +@suppression_group.command(name=cli_util.override('list_suppressions.command_name', 'list'), help="""Gets a list of suppressed recipient email addresses for a user. The `compartmentId` for suppressions must be a tenancy OCID. The returned list is sorted by creation time in descending order.""") +@click.option('--compartment-id', callback=cli_util.handle_required_param, help="""The OCID for the compartment. [required]""") +@click.option('--email-address', callback=cli_util.handle_optional_param, help="""The email address of the suppression.""") +@click.option('--time-created-greater-than-or-equal-to', callback=cli_util.handle_optional_param, type=custom_types.CLI_DATETIME, help="""Search for suppressions that were created within a specific date range, using this parameter to specify the earliest creation date for the returned list (inclusive). Specifying this parameter without the corresponding `timeCreatedLessThan` parameter will retrieve suppressions created from the given `timeCreatedGreaterThanOrEqualTo` to the current time, in \"YYYY-MM-ddThh:mmZ\" format with a Z offset, as defined by RFC 3339. + +**Example:** 2016-12-19T16:39:57.600Z""") +@click.option('--time-created-less-than', callback=cli_util.handle_optional_param, type=custom_types.CLI_DATETIME, help="""Search for suppressions that were created within a specific date range, using this parameter to specify the latest creation date for the returned list (exclusive). Specifying this parameter without the corresponding `timeCreatedGreaterThanOrEqualTo` parameter will retrieve all suppressions created before the specified end date, in \"YYYY-MM-ddThh:mmZ\" format with a Z offset, as defined by RFC 3339. + +**Example:** 2016-12-19T16:39:57.600Z""") +@click.option('--page', callback=cli_util.handle_optional_param, help="""The value of the `opc-next-page` response header from the previous GET request.""") +@click.option('--limit', callback=cli_util.handle_optional_param, type=click.INT, help="""The maximum number of items to return in a paginated GET request.""") +@click.option('--sort-by', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["TIMECREATED", "EMAILADDRESS"]), help="""The field to sort by. The `TIMECREATED` value returns the list in in descending order by default. The `EMAILADDRESS` value returns the list in ascending order by default. Use the `SortOrderQueryParam` to change the direction of the returned list of items.""") +@click.option('--sort-order', callback=cli_util.handle_optional_param, type=custom_types.CliCaseInsensitiveChoice(["ASC", "DESC"]), help="""The sort order to use, either ascending or descending order.""") +@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': 'email', 'class': 'list[SuppressionSummary]'}) +@cli_util.wrap_exceptions +def list_suppressions(ctx, from_json, all_pages, page_size, compartment_id, email_address, time_created_greater_than_or_equal_to, time_created_less_than, page, limit, sort_by, sort_order): + + if all_pages and limit: + raise click.UsageError('If you provide the --all option you cannot provide the --limit option') + kwargs = {} + if email_address is not None: + kwargs['email_address'] = email_address + if time_created_greater_than_or_equal_to is not None: + kwargs['time_created_greater_than_or_equal_to'] = time_created_greater_than_or_equal_to + if time_created_less_than is not None: + kwargs['time_created_less_than'] = time_created_less_than + if page is not None: + kwargs['page'] = page + if limit is not None: + kwargs['limit'] = limit + if sort_by is not None: + kwargs['sort_by'] = sort_by + if sort_order is not None: + kwargs['sort_order'] = sort_order + client = cli_util.build_client('email', ctx) + if all_pages: + if page_size: + kwargs['limit'] = page_size + + result = retry_utils.list_call_get_all_results_with_default_retries( + client.list_suppressions, + compartment_id=compartment_id, + **kwargs + ) + elif limit is not None: + result = retry_utils.list_call_get_up_to_limit_with_default_retries( + client.list_suppressions, + limit, + page_size, + compartment_id=compartment_id, + **kwargs + ) + else: + result = client.list_suppressions( + compartment_id=compartment_id, + **kwargs + ) + cli_util.render_response(result, ctx) diff --git a/src/oci_cli/objectstorage_cli_extended.py b/src/oci_cli/objectstorage_cli_extended.py index abb38726..9dd1d901 100644 --- a/src/oci_cli/objectstorage_cli_extended.py +++ b/src/oci_cli/objectstorage_cli_extended.py @@ -220,6 +220,7 @@ def object_list(ctx, from_json, namespace, bucket_name, prefix, start, end, limi @click.option('--content-language', callback=handle_optional_param, help='The content language of the object.') @click.option('--content-encoding', callback=handle_optional_param, help='The content encoding of the object.') @click.option('--force', is_flag=True, callback=handle_optional_param, help='If the object already exists, overwrite the existing object without a confirmation prompt.') +@click.option('--no-overwrite', is_flag=True, callback=handle_optional_param, help='If the object already exists, do not overwrite the existing object.') @click.option('--no-multipart', is_flag=True, callback=handle_optional_param, help='Do not use multipart uploads to upload the file in parts. By default files above 128 MiB will be uploaded in multiple parts, then combined server-side.') @click.option('--part-size', type=click.INT, callback=handle_optional_param, @@ -233,7 +234,7 @@ def object_list(ctx, from_json, namespace, bucket_name, prefix, start, end, limi @click.pass_context @json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={'metadata': {'module': 'object_storage', 'class': 'dict(str, str)'}}, output_type={'module': 'object_storage', 'class': 'ObjectSummary'}) @wrap_exceptions -def object_put(ctx, from_json, namespace, bucket_name, name, file, if_match, content_md5, metadata, content_type, content_language, content_encoding, force, no_multipart, part_size, disable_parallel_uploads, parallel_upload_count): +def object_put(ctx, from_json, namespace, bucket_name, name, file, if_match, content_md5, metadata, content_type, content_language, content_encoding, force, no_overwrite, no_multipart, part_size, disable_parallel_uploads, parallel_upload_count): """ Creates a new object or overwrites an existing one. @@ -279,6 +280,9 @@ def object_put(ctx, from_json, namespace, bucket_name, name, file, if_match, con kwargs['if_none_match'] = '*' else: kwargs['if_match'] = etag + if no_overwrite: + click.echo('The object already exists and was not overwritten', file=sys.stderr) + ctx.exit(0) if not click.confirm("WARNING: This object already exists. Are you sure you want to overwrite it?"): ctx.abort() @@ -1306,12 +1310,17 @@ def object_resume_put(ctx, from_json, namespace, bucket_name, name, file, upload @click.pass_context @json_skeleton_utils.json_skeleton_generation_handler(input_params_to_complex_types={}) @wrap_exceptions -def restore_objects(ctx, from_json, namespace, bucket, name, hours): +def restore_objects(ctx, **kwargs): details = {} + + namespace = kwargs['namespace'] + bucket = kwargs['bucket'] + name = kwargs['name'] + details['objectName'] = name - if hours is not None: - details['hours'] = hours + if kwargs['hours'] is not None: + details['hours'] = kwargs['hours'] client = build_client('object_storage', ctx) kwargs = {'opc_client_request_id': ctx.obj['request_id']} diff --git a/src/oci_cli/retry_utils.py b/src/oci_cli/retry_utils.py index 8a58ace8..df3a9d2e 100644 --- a/src/oci_cli/retry_utils.py +++ b/src/oci_cli/retry_utils.py @@ -1,7 +1,7 @@ # coding: utf-8 # Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. -from oci import exceptions +from oci import dns, exceptions from oci import Response from requests.exceptions import Timeout from requests.exceptions import ConnectionError @@ -43,6 +43,8 @@ def list_call_get_up_to_limit(list_func_ref, record_limit, page_size, retry_stra remaining_items_to_fetch = record_limit call_result = None aggregated_results = [] + is_dns_record_collection = False + dns_record_collection_class = None while keep_paginating and remaining_items_to_fetch > 0: if page_size: func_kwargs['limit'] = min(page_size, remaining_items_to_fetch) @@ -50,8 +52,15 @@ def list_call_get_up_to_limit(list_func_ref, record_limit, page_size, retry_stra func_kwargs['limit'] = min(func_kwargs['limit'], remaining_items_to_fetch) call_result = call_function_with_retries(list_func_ref, retry_strategy_name, **func_kwargs) - aggregated_results.extend(call_result.data) - remaining_items_to_fetch -= len(call_result.data) + + if isinstance(call_result.data, dns.models.RecordCollection) or isinstance(call_result.data, dns.models.RRSet): + is_dns_record_collection = True + dns_record_collection_class = call_result.data.__class__ + aggregated_results.extend(call_result.data.items) + remaining_items_to_fetch -= len(call_result.data.items) + else: + aggregated_results.extend(call_result.data) + remaining_items_to_fetch -= len(call_result.data) if call_result.next_page is not None: func_kwargs['page'] = call_result.next_page @@ -59,7 +68,16 @@ def list_call_get_up_to_limit(list_func_ref, record_limit, page_size, retry_stra keep_paginating = call_result.has_next_page # Truncate the list to the first limit items, as potentially we could have gotten more than what the caller asked for - final_response = Response(call_result.status, call_result.headers, aggregated_results[:record_limit], call_result.request) + if is_dns_record_collection: + final_response = Response( + call_result.status, + call_result.headers, + dns_record_collection_class(items=aggregated_results[:record_limit]), + call_result.request + ) + else: + final_response = Response(call_result.status, call_result.headers, aggregated_results[:record_limit], call_result.request) + return final_response @@ -71,10 +89,17 @@ def list_call_get_all_results(list_func_ref, retry_strategy_name, **func_kwargs) keep_paginating = True call_result = None aggregated_results = [] + is_dns_record_collection = False + dns_record_collection_class = None while keep_paginating: call_result = call_function_with_retries(list_func_ref, retry_strategy_name, **func_kwargs) - aggregated_results.extend(call_result.data) + if isinstance(call_result.data, dns.models.RecordCollection) or isinstance(call_result.data, dns.models.RRSet): + is_dns_record_collection = True + dns_record_collection_class = call_result.data.__class__ + aggregated_results.extend(call_result.data.items) + else: + aggregated_results.extend(call_result.data) if call_result.next_page is not None: func_kwargs['page'] = call_result.next_page @@ -97,7 +122,15 @@ def list_call_get_all_results(list_func_ref, retry_strategy_name, **func_kwargs) 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) + if is_dns_record_collection: + final_response = Response( + call_result.status, + call_result.headers, + dns_record_collection_class(items=post_processed_results), + call_result.request + ) + else: + final_response = Response(call_result.status, call_result.headers, post_processed_results, call_result.request) return final_response diff --git a/src/oci_cli/version.py b/src/oci_cli/version.py index eb4f469b..b41e235a 100644 --- a/src/oci_cli/version.py +++ b/src/oci_cli/version.py @@ -1,4 +1,4 @@ # coding: utf-8 # Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. -__version__ = '2.4.17' +__version__ = '2.4.18' diff --git a/tests/output/inline_help_dump.txt b/tests/output/inline_help_dump.txt index 2e977906..737cf431 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.17 of the CLI. +This file contains all the help for every possible command in version 2.4.18 of the CLI. This file is generated by running test_help.py, which dumps the output of --help for every command. @@ -100,7 +100,8 @@ Commands: bv Block Volume Service compute Compute Service db Database Service - dns API for managing DNS zones, records, and... + dns DNS Zone Management Service + email Email Delivery Service fs File Storage Service iam Identity and Access Management Service lb Load Balancing Service @@ -2970,6 +2971,14 @@ Options: complex JSON parameter. If this parameter is provided, you cannot provide the --source- details or --image-id parameters. + --boot-volume-size-in-gbs INTEGER + The size of the boot volume in GBs. Minimum + value is 50 GB and maximum value is 16384 GB + (16TB). This is a shortcut for specifying a + boot volume size via the --source-details + complex JSON parameter. If this parameter is + provided, you cannot provide the --source- + details or --source-boot-volume-id parameters. --from-json TEXT Provide input to this command as a JSON document from a file. @@ -3603,8 +3612,8 @@ Usage: oci compute volume-attachment attach [OPTIONS] Options: --instance-id TEXT The OCID of the instance. [required] - --type TEXT The type of volume. The only supported value - is "iscsi". [required] + --type [iscsi|paravirtualized] The type of volume. The only supported value + are "iscsi" and "paravirtualized". [required] --volume-id TEXT The OCID of the volume. [required] --display-name TEXT A user-friendly name. Does not have to be unique, and it cannot be changed. Avoid @@ -3704,8 +3713,8 @@ Usage: oci compute volume-attachment list [OPTIONS] Lists the volume attachments in the specified compartment. You can filter the list by specifying an instance OCID, volume OCID, or both. - Currently, the only supported volume attachment type is - [IScsiVolumeAttachment]. + Currently, the only supported volume attachment type are + [IScsiVolumeAttachment] and [ParavirtualizedVolumeAttachment]. Options: -c, --compartment-id TEXT The OCID of the compartment. [required] @@ -5457,7 +5466,7 @@ Options: $ oci dns --help Usage: oci dns [OPTIONS] COMMAND [ARGS]... - API for managing DNS zones, records, and policies. + DNS Zone Management Service Options: -?, -h, --help Show this message and exit. @@ -5570,6 +5579,11 @@ Options: --sort-order [ASC|DESC] The order to sort the resources. -c, --compartment-id TEXT The OCID of the compartment the resource belongs to. + --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. @@ -5782,6 +5796,11 @@ Options: requested. -c, --compartment-id TEXT The OCID of the compartment the resource belongs to. + --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. @@ -5959,6 +5978,12 @@ Options: --sort-order [ASC|DESC] The order to sort the resources. -c, --compartment-id TEXT The OCID of the compartment the resource belongs to. + --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. @@ -6373,6 +6398,297 @@ Options: value will be used -?, -h, --help Show this message and exit. +++++++++++++++++++++++++++++++++++++++++++++++ +$ oci email --help +Usage: oci email [OPTIONS] COMMAND [ARGS]... + + Email Delivery Service + +Options: + -?, -h, --help Show this message and exit. + +Commands: + sender The full information representing an approved... + suppression The full information representing an email... + +++++++++++++++++++++++++++++++++++++++++++++++ +$ oci email sender --help +Usage: oci email sender [OPTIONS] COMMAND [ARGS]... + + The full information representing an approved sender. + +Options: + -?, -h, --help Show this message and exit. + +Commands: + create Creates a sender for a tenancy in a given... + delete Deletes an approved sender for a tenancy in a... + get Gets an approved sender for a given... + list Gets a collection of approved sender email... + +++++++++++++++++++++++++++++++++++++++++++++++ +$ oci email sender create --help +Usage: oci email sender create [OPTIONS] + + Creates a sender for a tenancy in a given compartment. + +Options: + -c, --compartment-id TEXT The OCID of the compartment that contains the + sender. [required] + --email-address TEXT The email address of the sender. [required] + --wait-for-state [CREATING|ACTIVE|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 email sender delete --help +Usage: oci email sender delete [OPTIONS] + + Deletes an approved sender for a tenancy in a given compartment for a + provided `senderId`. + +Options: + --sender-id TEXT The unique OCID of the sender. [required] + --force Perform deletion without prompting for + confirmation. + --wait-for-state [CREATING|ACTIVE|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 email sender get --help +Usage: oci email sender get [OPTIONS] + + Gets an approved sender for a given `senderId`. + +Options: + --sender-id TEXT The unique OCID of the sender. [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 email sender list --help +Usage: oci email sender list [OPTIONS] + + Gets a collection of approved sender email addresses and sender IDs. + +Options: + -c, --compartment-id TEXT The OCID for the compartment. [required] + --lifecycle-state [CREATING|ACTIVE|DELETING|DELETED] + The current state of a sender. + --email-address TEXT The email address of the approved sender. + --page TEXT The value of the `opc-next-page` response + header from the previous GET request. + --limit INTEGER The maximum number of items to return in a + paginated GET request. + --sort-by [TIMECREATED|EMAILADDRESS] + The field to sort by. The `TIMECREATED` value + returns the list in in descending order by + default. The `EMAILADDRESS` value returns the + list in ascending order by default. Use the + `SortOrderQueryParam` to change the direction + of the returned list of items. + --sort-order [ASC|DESC] The sort order to use, either ascending or + descending order. + --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 email suppression --help +Usage: oci email suppression [OPTIONS] COMMAND [ARGS]... + + The full information representing an email suppression. + +Options: + -?, -h, --help Show this message and exit. + +Commands: + create Adds recipient email addresses to the... + delete Removes a suppressed recipient email address... + get Gets the details of a suppressed recipient... + list Gets a list of suppressed recipient email... + +++++++++++++++++++++++++++++++++++++++++++++++ +$ oci email suppression create --help +Usage: oci email suppression create [OPTIONS] + + Adds recipient email addresses to the suppression list for a tenancy. + +Options: + -c, --compartment-id TEXT The OCID of the compartment to contain the + suppression. Since suppressions are at the customer + level, this must be the tenancy OCID. [required] + --email-address TEXT The recipient email address of the suppression. + [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 email suppression delete --help +Usage: oci email suppression delete [OPTIONS] + + Removes a suppressed recipient email address from the suppression list for a + tenancy in a given compartment for a provided `suppressionId`. + +Options: + --suppression-id TEXT The unique OCID of the suppression. [required] + --force Perform deletion without prompting for confirmation. + --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 email suppression get --help +Usage: oci email suppression get [OPTIONS] + + Gets the details of a suppressed recipient email address for a given + `suppressionId`. Each suppression is given a unique OCID. + +Options: + --suppression-id TEXT The unique OCID of the suppression. [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 email suppression list --help +Usage: oci email suppression list [OPTIONS] + + Gets a list of suppressed recipient email addresses for a user. The + `compartmentId` for suppressions must be a tenancy OCID. The returned list + is sorted by creation time in descending order. + +Options: + -c, --compartment-id TEXT The OCID for the compartment. [required] + --email-address TEXT The email address of the suppression. + --time-created-greater-than-or-equal-to DATETIME + Search for suppressions that were created + within a specific date range, using this + parameter to specify the earliest creation + date for the returned list (inclusive). + Specifying this parameter without the + corresponding `timeCreatedLessThan` parameter + will retrieve suppressions created from the + given `timeCreatedGreaterThanOrEqualTo` to the + current time, in "YYYY-MM-ddThh:mmZ" format + with a Z offset, as defined by RFC 3339. + **Example:** 2016-12-19T16:39:57.600Z + --time-created-less-than DATETIME + Search for suppressions that were created + within a specific date range, using this + parameter to specify the latest creation date + for the returned list (exclusive). Specifying + this parameter without the corresponding + `timeCreatedGreaterThanOrEqualTo` parameter + will retrieve all suppressions created before + the specified end date, in "YYYY-MM-ddThh:mmZ" + format with a Z offset, as defined by RFC + 3339. + + **Example:** 2016-12-19T16:39:57.600Z + --page TEXT The value of the `opc-next-page` response + header from the previous GET request. + --limit INTEGER The maximum number of items to return in a + paginated GET request. + --sort-by [TIMECREATED|EMAILADDRESS] + The field to sort by. The `TIMECREATED` value + returns the list in in descending order by + default. The `EMAILADDRESS` value returns the + list in ascending order by default. Use the + `SortOrderQueryParam` to change the direction + of the returned list of items. + --sort-order [ASC|DESC] The sort order to use, either ascending or + descending order. + --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 fs --help Usage: oci fs [OPTIONS] COMMAND [ARGS]... @@ -17195,6 +17511,8 @@ Options: --content-encoding TEXT The content encoding of the object. --force If the object already exists, overwrite the existing object without a confirmation prompt. + --no-overwrite If the object already exists, do not overwrite + the existing object. --no-multipart Do not use multipart uploads to upload the file in parts. By default files above 128 MiB will be uploaded in multiple parts, then diff --git a/tests/test_audit.py b/tests/test_audit.py index 02ab9d8a..f504aeac 100644 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -5,6 +5,8 @@ import unittest import json import oci_cli +import pytz +from dateutil.parser import parse from . import command_coverage_validator from . import test_config_container from . import util @@ -24,7 +26,8 @@ def test_all_operations(self, validator): self.validator = validator self.subtest_event_list() - self.subtest_config_get() + # Not present in the preview spec + # self.subtest_config_get() def subtest_config_get(self): util.set_admin_pass_phrase() @@ -37,13 +40,24 @@ def subtest_config_get(self): def subtest_event_list(self): end_time = datetime.datetime.utcnow() start_time = end_time + datetime.timedelta(days=-1) + result = self.invoke(['audit', 'event', 'list', '--compartment-id', util.COMPARTMENT_ID, '--start-time', start_time.strftime(DATETIME_FORMAT), '--end-time', end_time.strftime(DATETIME_FORMAT)]) assert result.exit_code == 0 if result.output: response = json.loads(result.output) + + # Some jitter because audit needs a RFC3339 date but only works with minute precision + end_time_with_zone = pytz.utc.localize(end_time) + datetime.timedelta(minutes=5) + start_time_with_zone = pytz.utc.localize(start_time) + datetime.timedelta(minutes=-5) + for event in response["data"]: assert util.COMPARTMENT_ID == event["compartment-id"] + if not test_config_container.using_vcr_with_mock_responses(): + parsed_date = parse(event["event-time"]) + assert parsed_date >= start_time_with_zone + assert parsed_date <= end_time_with_zone + def invoke(self, commands, debug=False, ** args): self.validator.register_call(commands) diff --git a/tests/test_blockstorage.py b/tests/test_blockstorage.py index 1e611bd4..37018840 100644 --- a/tests/test_blockstorage.py +++ b/tests/test_blockstorage.py @@ -203,7 +203,7 @@ def subtest_volume_backup_operations(self): assert 50 == int(parsed_result['data']['size-in-gbs']) # We initially created a 50GB volume volume_id = util.find_id_in_response(result.output) - util.wait_until(['bv', 'volume', 'get', '--volume-id', volume_id], 'AVAILABLE', max_wait_seconds=180) + util.wait_until(['bv', 'volume', 'get', '--volume-id', volume_id], 'AVAILABLE', max_wait_seconds=600) result = self.invoke(['volume', 'delete', '--volume-id', volume_id, '--force']) util.validate_response(result) diff --git a/tests/test_default_values_from_file.py b/tests/test_default_values_from_file.py index f1410a2e..1d0e4848 100644 --- a/tests/test_default_values_from_file.py +++ b/tests/test_default_values_from_file.py @@ -265,6 +265,45 @@ def test_variable_expansion(): assert coalesced_value == 'You should substitute me' +def test_datetime_conversions_in_default_file(): + command_with_different_datetime_formats = click.Command( + 'unit-test-command', + params=[ + click.Option(['--param1'], type=oci_cli.custom_types.CLI_DATETIME), + click.Option(['--param2'], type=oci_cli.custom_types.CLI_DATETIME), + click.Option(['--param3'], type=oci_cli.custom_types.CLI_DATETIME), + click.Option(['--param4'], type=oci_cli.custom_types.CLI_DATETIME), + click.Option(['--param5'], type=oci_cli.custom_types.CLI_DATETIME), + click.Option(['--param6'], type=oci_cli.custom_types.CLI_DATETIME) + ] + ) + context = set_up_context(command_with_different_datetime_formats) + context.obj['default_values_from_file']['param1'] = '2001-03-01' + context.obj['default_values_from_file']['param2'] = '2017-05-31T17:15:15.123+08:00' + context.obj['default_values_from_file']['param3'] = '2017-06-01T06:15:15-07:00' + context.obj['default_values_from_file']['param4'] = '2017-12-31T00:15:15.444Z' + context.obj['default_values_from_file']['param5'] = '2017-11-30T06:15:15Z' + context.obj['default_values_from_file']['param6'] = '1519862400' # 1 March 2018 00:00:00 UTC + + coalesced_value = oci_cli.cli_util.coalesce_provided_and_default_value(context, 'param1', None, False) + assert oci_cli.custom_types.CLI_DATETIME.convert('2001-03-01', None, None) == coalesced_value + + coalesced_value = oci_cli.cli_util.coalesce_provided_and_default_value(context, 'param2', None, False) + assert oci_cli.custom_types.CLI_DATETIME.convert('2017-05-31T17:15:15.123+08:00', None, None) == coalesced_value + + coalesced_value = oci_cli.cli_util.coalesce_provided_and_default_value(context, 'param3', None, False) + assert oci_cli.custom_types.CLI_DATETIME.convert('2017-06-01T06:15:15-07:00', None, None) == coalesced_value + + coalesced_value = oci_cli.cli_util.coalesce_provided_and_default_value(context, 'param4', None, False) + assert oci_cli.custom_types.CLI_DATETIME.convert('2017-12-31T00:15:15.444Z', None, None) == coalesced_value + + coalesced_value = oci_cli.cli_util.coalesce_provided_and_default_value(context, 'param5', None, False) + assert oci_cli.custom_types.CLI_DATETIME.convert('2017-11-30T06:15:15Z', None, None) == coalesced_value + + coalesced_value = oci_cli.cli_util.coalesce_provided_and_default_value(context, 'param6', None, False) + assert oci_cli.custom_types.CLI_DATETIME.convert('1519862400', None, None) == coalesced_value + + def set_up_context(command=click.Command('unit-test-command')): context_obj = {'default_values_from_file': {}, 'parameter_aliases': {}} diff --git a/tests/test_dns.py b/tests/test_dns.py index 9f4ea8bb..86a8225a 100644 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -27,12 +27,12 @@ def zone(dns_client, runner, config_file, config_profile): result = invoke(runner, config_file, config_profile, params) util.validate_response(result) - - zone_id = json.loads(result.output)['data']['id'] zone_name = json.loads(result.output)['data']['name'] oci.wait_until(dns_client, dns_client.get_zone(zone_name), evaluate_response=lambda r: r.data.id != '', max_wait_seconds=300) + zone_id = dns_client.get_zone(zone_name).data.id + yield zone_id, zone_name with test_config_container.create_vcr().use_cassette('dns_test_cleanup.yml'): @@ -119,6 +119,30 @@ def test_get_zone_records(zone, runner, config_file, config_profile): result = invoke(runner, config_file, config_profile, params) util.validate_response(result) + params = [ + 'record', 'zone', 'get', + '--zone-name-or-id', zone_name, + '--all' + ] + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + parsed_result = json.loads(result.output) + assert len(parsed_result['data']['items']) > 0 + assert int(parsed_result['opc-total-items']) == len(parsed_result['data']['items']) + assert_all_records_in_list_have_not_none_fields(parsed_result['data']['items']) + + params = [ + 'record', 'zone', 'get', + '--zone-name-or-id', zone_name, + '--limit', '2', + '--page-size', '1' + ] + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + parsed_result = json.loads(result.output) + assert len(parsed_result['data']['items']) == 2 + assert_all_records_in_list_have_not_none_fields(parsed_result['data']['items']) + def test_patch_zone_records(zone, runner, config_file, config_profile): zone_id = zone[0] @@ -325,6 +349,32 @@ def test_get_domain_records(zone, runner, config_file, config_profile): result = invoke(runner, config_file, config_profile, params) util.validate_response(result) + params = [ + 'record', 'domain', 'get', + '--zone-name-or-id', zone_name, + '--domain', zone_name, + '--all' + ] + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + parsed_result = json.loads(result.output) + assert len(parsed_result['data']['items']) > 0 + assert int(parsed_result['opc-total-items']) == len(parsed_result['data']['items']) + assert_all_records_in_list_have_not_none_fields(parsed_result['data']['items']) + + params = [ + 'record', 'zone', 'get', + '--zone-name-or-id', zone_name, + '--domain', zone_name, + '--limit', '2', + '--page-size', '1' + ] + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + parsed_result = json.loads(result.output) + assert len(parsed_result['data']['items']) == 2 + assert_all_records_in_list_have_not_none_fields(parsed_result['data']['items']) + def test_patch_domain_records(zone, runner, config_file, config_profile): zone_id = zone[0] @@ -532,6 +582,38 @@ def test_get_rrset_records(zone, runner, config_file, config_profile): result = invoke(runner, config_file, config_profile, params) util.validate_response(result) + params = [ + 'record', 'rrset', 'get', + '--zone-name-or-id', zone_name, + '--domain', zone_name, + '--rtype', 'NS', + '--all' + ] + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + parsed_result = json.loads(result.output) + assert len(parsed_result['data']['items']) > 0 + assert int(parsed_result['opc-total-items']) == len(parsed_result['data']['items']) + for item in parsed_result['data']['items']: + assert_record_fields_not_none(item) + assert item['rtype'] == 'NS' + + params = [ + 'record', 'rrset', 'get', + '--zone-name-or-id', zone_name, + '--domain', zone_name, + '--rtype', 'NS', + '--limit', '2', + '--page-size', '1' + ] + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + parsed_result = json.loads(result.output) + assert len(parsed_result['data']['items']) == 2 + for item in parsed_result['data']['items']: + assert_record_fields_not_none(item) + assert item['rtype'] == 'NS' + def test_patch_rrset_records(zone, runner, config_file, config_profile): zone_id = zone[0] @@ -724,3 +806,18 @@ def invoke(runner, config_file, config_profile, params, debug=False, root_params result = runner.invoke(oci_cli.cli, root_params + ['--config-file', config_file, '--profile', config_profile, 'dns'] + params, ** args) return result + + +def assert_all_records_in_list_have_not_none_fields(record_list): + for record in record_list: + assert_record_fields_not_none(record) + + +def assert_record_fields_not_none(record): + assert record['domain'] is not None + assert record['record-hash'] is not None + assert record['is-protected'] is not None + assert record['rdata'] is not None + assert record['rrset-version'] is not None + assert record['rtype'] is not None + assert record['ttl'] is not None diff --git a/tests/test_email.py b/tests/test_email.py new file mode 100644 index 00000000..4648c519 --- /dev/null +++ b/tests/test_email.py @@ -0,0 +1,105 @@ +# coding: utf-8 +# Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + +import json +import oci_cli +import pytest + +from . import util +from . import test_config_container + + +util.set_admin_pass_phrase() + + +@pytest.fixture(autouse=True, scope='function') +def vcr_fixture(request): + with test_config_container.create_vcr().use_cassette('email_{name}.yml'.format(name=request.function.__name__)): + yield + + +def test_sender_crud(runner, config_file, config_profile): + sender_id = None + try: + params = [ + 'sender', 'create', + '--email-address', util.random_name('clisender', insert_underscore=False) + '@oracle.com', + '--compartment-id', util.COMPARTMENT_ID + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + sender_id = json.loads(result.output)['data']['id'] + + util.wait_until(['email', 'sender', 'get', '--sender-id', sender_id], 'ACTIVE', max_wait_seconds=600) + + params = [ + 'sender', 'list', + '-c', util.COMPARTMENT_ID + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + assert len(json.loads(result.output)['data']) > 0 + finally: + if sender_id: + params = [ + 'sender', 'delete', + '--sender-id', sender_id, + '--force' + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + +def test_suppression_crud(runner, config_file): + config_profile = 'ADMIN' + suppression_id = None + try: + params = [ + 'suppression', 'create', + '--email-address', util.random_name('cli_suppression_email', insert_underscore=False) + '@oracle.com', + '--compartment-id', util.TENANT_ID + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + suppression_id = json.loads(result.output)['data']['id'] + + params = [ + 'suppression', 'get', + '--suppression-id', suppression_id + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + params = [ + 'suppression', 'list', + '-c', util.TENANT_ID + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + assert len(json.loads(result.output)['data']) > 0 + finally: + if suppression_id: + params = [ + 'suppression', 'delete', + '--suppression-id', suppression_id, + '--force' + ] + + result = invoke(runner, config_file, config_profile, params) + util.validate_response(result) + + +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, 'email'] + params, ** args) + else: + result = runner.invoke(oci_cli.cli, root_params + ['--config-file', config_file, '--profile', config_profile, 'email'] + params, ** args) + + return result diff --git a/tests/test_list_filter.py b/tests/test_list_filter.py index b3894857..5deac02f 100644 --- a/tests/test_list_filter.py +++ b/tests/test_list_filter.py @@ -26,10 +26,16 @@ def test_list_images_by_display_name_without_results(): @test_config_container.RecordReplayWithNoClickContext('list_filter') def test_list_images_by_display_name_with_results(): + result = invoke(['compute', 'image', 'list', '-c', util.COMPARTMENT_ID, '--all']) + images = json.loads(result.output) + + windows_image_display_name = next(image['display-name'] for image in images['data'] if 'Windows' in image['display-name']) + print(str(windows_image_display_name)) + 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'], + ['compute', 'image', 'list', '-c', util.COMPARTMENT_ID, '--display-name', windows_image_display_name], 'display-name', - 'Windows-Server-2012-R2-Standard-Edition-VM-2017.07.25-0', + windows_image_display_name, 1 ) diff --git a/tests/test_object_storage.py b/tests/test_object_storage.py index 79989264..a9fde71d 100644 --- a/tests/test_object_storage.py +++ b/tests/test_object_storage.py @@ -328,6 +328,14 @@ def test_object_put_confirmation_prompt(runner, config_file, config_profile, con assertEquals(content_input_file_size, int(json_head['content-length'])) assertEquals(etag, json_head['etag']) + # Test no-overwrite + result = invoke(runner, config_file, config_profile, put_required_args + ['--no-overwrite']) + assert result.exit_code == 0 + json_head = json.loads(invoke(runner, config_file, config_profile, head_required_args).output) + # Make sure that etag and content length haven't changed. + assertEquals(content_input_file_size, int(json_head['content-length'])) + assertEquals(etag, json_head['etag']) + # Test force result = invoke(runner, config_file, config_profile, put_required_args + ['--force']) validate_response(result)