Skip to content

Commit 07c72dd

Browse files
authored
Merge pull request #56 from oracle/release_2018-04-05
Releasing version 2.4.20
2 parents 39ab1cf + 25ae772 commit 07c72dd

36 files changed

+2918
-2579
lines changed

CHANGELOG.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ All notable changes to this project will be documented in this file.
77
The format is based on `Keep a
88
Changelog <http://keepachangelog.com/>`__.
99

10+
2.4.20 - 2018-04-05
11+
---------------------
12+
Added
13+
~~~~~~~~
14+
* An example of how to scale existing VM instances using the CLI can be found on `Github <https://github.com/oracle/oci-cli/blob/master/scripts/scale_vm_example.sh>`_
15+
* A warning message informing use of ``--all`` flag to get all items during list operations.
16+
17+
Fixed
18+
~~~~~~~~
19+
* Multipart bulk download to correctly enable downloads as per size thresholds set by the user.
20+
* Check all required parameters are present before prompting for deleting resource
21+
22+
Changed
23+
~~~~~~~~
24+
* Use root compartment OCID (tenancy OCID) as default value for --compartment-id in ``oci iam compartment list`` command.
25+
1026
2.4.19 - 2018-03-26
1127
---------------------
1228
Added

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Jinja2==2.9.6
1212
jmespath==0.9.3
1313
ndg-httpsclient==0.4.2
1414
mock==2.0.0
15-
oci==1.3.17
15+
oci==1.3.18
1616
packaging==16.8
1717
pluggy==0.4.0
1818
py==1.4.33

scripts/install/install.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ else
5050
echo "Python not found on system PATH"
5151
fi
5252

53+
# some OSes have python3 as a command but not 'python' (example: Ubuntu 16.04)
54+
# if both python and python3 exist and are a sufficiently recent version, we will prefer python3
55+
command -v python3 >/dev/null 2>&1
56+
if [ $? -eq 0 ]; then
57+
# python is installed so check if the version is valid
58+
# this python command returns an exit code of 0 if the system version is sufficient, and 1 if it is not
59+
python3 -c "import sys; v = sys.version_info; valid = v >= (2, 7, 5) if v[0] == 2 else v >= (3, 5, 0); sys.exit(0) if valid else sys.exit(1)"
60+
if [ $? -eq 0 ]; then
61+
python_exe=python3
62+
# if python is installed and meets the version requirements then we dont need to install it
63+
need_to_install_python=false
64+
else
65+
echo "System version of Python must be either a Python 2 version >= 2.7.5 or a Python 3 version >= 3.5.0."
66+
fi
67+
else
68+
echo "Python3 not found on system PATH"
69+
fi
70+
5371

5472
if [ "$need_to_install_python" = true ]; then
5573
if command -v yum

scripts/scale_vm_example.sh

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/bin/bash
2+
# This script provides an example of how to create an instance with the CLI and scale it up or down to a different shape
3+
#
4+
# Requirements for running this script:
5+
# - OCI CLI v2.4.14 or later (you can check this by running oci --version)
6+
7+
COMPARTMENT_ID="" # Your compartment OCID
8+
SSH_AUTHORIZED_KEYS_FILE=~/.ssh/id_rsa.pub # Your SSH public key to configure for access to the instances
9+
10+
ORIGINAL_INSTANCE_SHAPE='VM.Standard1.1'
11+
SCALED_INSTANCE_SHAPE='VM.Standard1.4'
12+
ORIGINAL_INSTANCE_DISPLAY_NAME='ScaleVMTest_OriginalInstance'
13+
SCALED_INSTANCE_DISPLAY_NAME='ScaleVMTest_ScaledInstance'
14+
15+
# dynamically select image so it is for the correct region
16+
ORACLE_LINUX_IMAGE_ID=$(oci compute image list -c $COMPARTMENT_ID --operating-system 'Oracle Linux' --operating-system-version '7.4' --shape $ORIGINAL_INSTANCE_SHAPE --query 'data[0].id' --raw-output)
17+
AVAILABILITY_DOMAIN=$(oci iam availability-domain list -c $COMPARTMENT_ID --query 'data[0].name' --raw-output)
18+
19+
echo 'Creating VCN...'
20+
21+
VCN_ID=$(oci network vcn create -c $COMPARTMENT_ID --display-name scaleVMExampleVcn --cidr-block 10.0.0.0/16 --wait-for-state AVAILABLE --query 'data.id' --raw-output 2>/dev/null)
22+
echo "VCN OCID: ${VCN_ID}"
23+
24+
echo 'Creating Subnet...'
25+
26+
SUBNET_ID=$(oci network subnet create -c $COMPARTMENT_ID --availability-domain $AVAILABILITY_DOMAIN --display-name scaleVMSubnet --vcn-id $VCN_ID --cidr-block 10.0.0.0/24 --wait-for-state AVAILABLE --query 'data.id' --raw-output 2>/dev/null)
27+
echo "Subnet OCID: $SUBNET_ID"
28+
29+
echo 'Launching instance...'
30+
31+
INSTANCE_ID=`oci compute instance launch \
32+
--image-id $ORACLE_LINUX_IMAGE_ID \
33+
--display-name $ORIGINAL_INSTANCE_DISPLAY_NAME \
34+
--availability-domain $AVAILABILITY_DOMAIN \
35+
--compartment-id $COMPARTMENT_ID \
36+
--shape $ORIGINAL_INSTANCE_SHAPE \
37+
--ssh-authorized-keys-file $SSH_AUTHORIZED_KEYS_FILE \
38+
--subnet-id $SUBNET_ID \
39+
--assign-public-ip true \
40+
--wait-for-state RUNNING \
41+
--query 'data.id' \
42+
--raw-output`
43+
44+
# find the boot volume ID for the boot volume attached to the instance
45+
BOOT_VOLUME_ID=`oci compute boot-volume-attachment list \
46+
--availability-domain $AVAILABILITY_DOMAIN \
47+
--compartment-id $COMPARTMENT_ID \
48+
--query 'data[?"instance-id" == \`'$INSTANCE_ID'\`] | [0]."boot-volume-id"' \
49+
--raw-output`
50+
51+
echo "Boot Volume ID: $BOOT_VOLUME_ID"
52+
53+
# terminate instance but preserve boot volume
54+
# use --force to suppress confirmation prompt for deleting resource
55+
oci compute instance terminate \
56+
--instance-id $INSTANCE_ID \
57+
--preserve-boot-volume true \
58+
--wait-for-state TERMINATED \
59+
--force
60+
61+
# launch a new instance using the original boot volume
62+
SCALED_INSTANCE_ID=`oci compute instance launch \
63+
--display-name $SCALED_INSTANCE_DISPLAY_NAME \
64+
--availability-domain $AVAILABILITY_DOMAIN \
65+
--compartment-id $COMPARTMENT_ID \
66+
--shape $SCALED_INSTANCE_SHAPE \
67+
--ssh-authorized-keys-file $SSH_AUTHORIZED_KEYS_FILE \
68+
--source-boot-volume-id $BOOT_VOLUME_ID \
69+
--subnet-id $SUBNET_ID \
70+
--assign-public-ip true \
71+
--wait-for-state RUNNING \
72+
--query 'data.id' \
73+
--raw-output`
74+
75+
echo "Scaled instance ID: $SCALED_INSTANCE_ID"
76+
77+
# terminate the instance
78+
oci compute instance terminate --instance-id $SCALED_INSTANCE_ID --wait-for-state TERMINATED --force
79+
80+
# delete the subnets
81+
oci network subnet delete --subnet-id $SUBNET_ID --wait-for-state TERMINATED --force
82+
83+
# delete the VCN
84+
oci network vcn delete --vcn-id $VCN_ID --wait-for-state TERMINATED --force
85+
86+
echo 'Success!'

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def open_relative(*path):
3030

3131

3232
requires = [
33-
'oci==1.3.17',
33+
'oci==1.3.18',
3434
'arrow==0.10.0',
3535
'certifi',
3636
'click==6.7',

src/oci_cli/cli_setup.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,11 @@ def setup_group():
136136

137137

138138
@setup_group.command('keys', help="""Generates an RSA key pair. A passphrase for the private key can be provided using either the 'passphrase' or 'passphrase-file' option. If neither option is provided, the user will be prompted for a passphrase via stdin.""")
139-
@click.option('--key-name', default='oci_api_key', help="""A name for the key. Generated key files will be {key-name}.pem and {key-name}_public.pem""")
140-
@click.option('--output-dir', default=default_directory, help="""An optional directory to output the generated keys.""", type=click.Path())
141-
@click.option('--passphrase', help="""An optional passphrase to encrypt the private key.""")
142-
@click.option('--passphrase-file', help="""An optional file with the first line specifying a passphrase to encrypt the private key (or '-' to read from stdin).""", type=click.File(mode='r'))
143-
@click.option('--overwrite', default=False, help="""An option to overwrite existing files without a confirmation prompt.""", is_flag=True)
139+
@cli_util.option('--key-name', default='oci_api_key', help="""A name for the key. Generated key files will be {key-name}.pem and {key-name}_public.pem""")
140+
@cli_util.option('--output-dir', default=default_directory, help="""An optional directory to output the generated keys.""", type=click.Path())
141+
@cli_util.option('--passphrase', help="""An optional passphrase to encrypt the private key.""")
142+
@cli_util.option('--passphrase-file', help="""An optional file with the first line specifying a passphrase to encrypt the private key (or '-' to read from stdin).""", type=click.File(mode='r'))
143+
@cli_util.option('--overwrite', default=False, help="""An option to overwrite existing files without a confirmation prompt.""", is_flag=True)
144144
@cli_util.help_option
145145
def generate_key_pair(key_name, output_dir, passphrase, passphrase_file, overwrite):
146146
if passphrase and passphrase_file:
@@ -241,7 +241,7 @@ def generate_oci_config():
241241
242242
This command will populate the file with some default aliases and predefined queries.
243243
""")
244-
@click.option('--file', default=os.path.expanduser(CLI_RC_DEFAULT_LOCATION), type=click.File(mode='a+b'), required=True, help="The file into which default aliases and predefined queries will be loaded")
244+
@cli_util.option('--file', default=os.path.expanduser(CLI_RC_DEFAULT_LOCATION), type=click.File(mode='a+b'), required=True, help="The file into which default aliases and predefined queries will be loaded")
245245
@cli_util.help_option
246246
def setup_cli_rc(file):
247247
if hasattr(file, 'name') and file.name == '<stdout>':
@@ -365,7 +365,7 @@ def setup_autocomplete_non_windows():
365365

366366
@setup_group.command('repair-file-permissions', help="""Resets permissions on a given file to an appropriate access level for sensitive files. Generally this is used to fix permissions on a private key file or config file to meet the requirements of the CLI.
367367
On Windows, full control will be given to System, Administrators, and the current user. On Unix, Read / Write permissions will be given to the current user.""")
368-
@click.option('--file', required=True, help="""The file to repair permissions on.""")
368+
@cli_util.option('--file', required=True, help="""The file to repair permissions on.""")
369369
@cli_util.help_option
370370
def repair_file_permissions(file):
371371
file = os.path.expanduser(file)

src/oci_cli/cli_util.py

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@
132132

133133
MODULE_TO_TYPE_MAPPINGS = MODULE_TO_TYPE_MAPPINGS
134134

135+
LIST_NOT_ALL_ITEMS_RETURNED_WARNING = "WARNING: This operation supports pagination and not all resources were returned. Re-run using the --all option to auto paginate and list all resources."
136+
135137

136138
def override(key, default):
137139
return OVERRIDES.get(key, default)
@@ -304,6 +306,19 @@ def render(data, headers, ctx, display_all_headers=False, nest_data_in_data_attr
304306
if key is not 'data':
305307
click.echo('{}: {}'.format(key, display_dictionary[key]), file=sys.stderr)
306308

309+
# print out a notice if not all results were returned, and the operation supports the --all parameter
310+
if headers and headers.get('opc-next-page'):
311+
has_all_param = False
312+
if ctx.command.params:
313+
for param in ctx.command.params:
314+
if param.name == 'all_pages':
315+
has_all_param = True
316+
break
317+
318+
if has_all_param:
319+
notice = LIST_NOT_ALL_ITEMS_RETURNED_WARNING
320+
click.echo(click.style(notice, fg='red'), file=sys.stderr)
321+
307322

308323
def print_table(data):
309324
table_data = []
@@ -428,6 +443,13 @@ def wrapped_call(ctx, *args, **kwargs):
428443
if 'missing_required_parameters' in ctx.obj:
429444
raise cli_exceptions.RequiredValueNotInDefaultOrUserInputError('Missing option(s) --{}.'.format(', --'.join(ctx.obj['missing_required_parameters'])))
430445

446+
# check this AFTER checking for required params
447+
# if there are missing required params we want to show that notice, not prompt the user for deletion confirmation
448+
if 'prompt_for_deletion' in ctx.obj and ctx.obj['prompt_for_deletion']:
449+
value = click.confirm("Are you sure you want to delete this resource?")
450+
if not value:
451+
ctx.abort()
452+
431453
func(ctx, *args, **kwargs)
432454
except exceptions.ServiceError as exception:
433455
if exception.status == 401:
@@ -681,11 +703,8 @@ def confirmation_callback(ctx, param, value):
681703
if not ctx.obj['generate_full_command_json_input'] and not ctx.obj['generate_param_json_input']:
682704
# if --force was supplied we don't want to prompt
683705
if not value:
684-
value = click.confirm("Are you sure you want to delete this resource?")
685-
if not value:
686-
ctx.abort()
687-
688-
return value
706+
# propmt for deletion after reading ALL params, because it is unnecessary if we are missing required params
707+
ctx.obj['prompt_for_deletion'] = True
689708

690709

691710
confirm_delete_option = click.option(
@@ -1111,9 +1130,9 @@ def warn_on_invalid_file_permissions(filepath):
11111130
if is_windows():
11121131
windows_warn_on_invalid_file_permissions(filepath)
11131132
else:
1114-
# validate that permissions are user RW only (600)
1115-
user_rw_perms = oct(384) # 600
1116-
if not oct(stat.S_IMODE(os.lstat(filepath).st_mode)) == user_rw_perms:
1133+
# validate that permissions are user R or RW only (400 or 600)
1134+
unwanted_perms = 127 # octal 177
1135+
if (stat.S_IMODE(os.lstat(filepath).st_mode) & unwanted_perms):
11171136
warning = 'WARNING: Permissions on {filepath} are too open. To fix this please execute the following command: oci setup repair-file-permissions --file {filepath} '.format(filepath=filepath)
11181137
click.echo(click.style(warning, fg='red'), file=sys.stderr)
11191138

@@ -1194,20 +1213,85 @@ def handle_optional_param(ctx, param, value):
11941213
return _coalesce_param(ctx, param, value, False)
11951214

11961215

1197-
def _coalesce_param(ctx, param, value, required):
1216+
def handle_param_with_default(required, default):
1217+
def internal_handle_param(ctx, param, value):
1218+
return _coalesce_param(ctx, param, value, required, explicit_default=default)
1219+
1220+
return internal_handle_param
1221+
1222+
1223+
def _coalesce_param(ctx, param, value, required, explicit_default=None):
1224+
# if value is populated (from an explicit argument), use that
1225+
# options with multiple=True with no value explicitly given will be passed as '()' so in that case we want to check defaults file
1226+
if value is not None and value != ():
1227+
return value
1228+
11981229
hyphenated_param_name = param.name.replace('_', '-')
11991230
try:
1231+
value = None
12001232
if isinstance(param.type, click.types.File) and value is None:
1201-
return get_click_file_from_default_values_file(ctx, hyphenated_param_name, param.type.mode, required)
1233+
value = get_click_file_from_default_values_file(ctx, hyphenated_param_name, param.type.mode, required)
12021234
else:
1203-
return coalesce_provided_and_default_value(ctx, hyphenated_param_name, value, required)
1235+
value = coalesce_provided_and_default_value(ctx, hyphenated_param_name, value, required)
1236+
1237+
if value is None and explicit_default is not None:
1238+
value = explicit_default
1239+
1240+
return value
12041241
except cli_exceptions.RequiredValueNotInDefaultOrUserInputError:
1242+
# if there is an explicit default then its not missing so just return explicit_default
1243+
if explicit_default is not None:
1244+
return explicit_default
1245+
12051246
if 'missing_required_parameters' not in ctx.obj:
12061247
ctx.obj['missing_required_parameters'] = []
12071248

12081249
ctx.obj['missing_required_parameters'].append(hyphenated_param_name)
12091250

12101251

1252+
def option(*param_decls, **attrs):
1253+
"""Attaches an option to the command. All positional arguments are
1254+
passed as parameter declarations to :class:`Option`; all keyword
1255+
arguments are forwarded unchanged (except ``cls``).
1256+
This is equivalent to creating an :class:`Option` instance manually
1257+
and attaching it to the :attr:`Command.params` list.
1258+
1259+
:param cls: the option class to instantiate. This defaults to
1260+
:class:`Option`.
1261+
"""
1262+
def decorator(f):
1263+
default = None
1264+
# remove default from option declaration because it will override defaults file
1265+
if 'default' in attrs:
1266+
default = attrs['default']
1267+
del attrs['default']
1268+
1269+
# add default value to help text
1270+
if 'help' in attrs and 'show_default' in attrs and attrs['show_default']:
1271+
spacer = '' if attrs['help'].endswith(' ') else ' '
1272+
attrs['help'] = '{}{}{}'.format(attrs['help'], spacer, '[default: {}]'.format(str(default)))
1273+
1274+
required = False
1275+
if 'required' in attrs and attrs['required'] and 'help' in attrs:
1276+
required = True
1277+
# add [required] to help text for this param
1278+
if 'help' in attrs:
1279+
spacer = '' if attrs['help'].endswith(' ') else ' '
1280+
attrs['help'] = '{}{}{}'.format(attrs['help'], spacer, '[required]')
1281+
1282+
# for click purposes mark everything as optional so our default file lookup logic still has a chance to run
1283+
del attrs['required']
1284+
1285+
# don't allow 'callback' because it will conflict with the required / optional param callback we add
1286+
if 'callback' in attrs:
1287+
raise ValueError('Cannot specify callback function for option, conflicts with default callback.')
1288+
1289+
attrs.setdefault('callback', handle_param_with_default(required, default))
1290+
1291+
return click.option(*param_decls, **attrs)(f)
1292+
return decorator
1293+
1294+
12111295
# Decodes a byte string using stdout's encoding if we can get it, otherwise decode using the Python default
12121296
def _try_decode_using_stdout(output):
12131297
if hasattr(sys.stdout, 'encoding') and sys.stdout.encoding is not None:

0 commit comments

Comments
 (0)