summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.testr.conf1
-rw-r--r--doc/source/backwards-incompatible.rst12
-rw-r--r--functional/common/test.py31
-rw-r--r--functional/tests/compute/__init__.py0
-rw-r--r--functional/tests/compute/v2/__init__.py0
-rw-r--r--functional/tests/compute/v2/test_keypair.py44
-rw-r--r--functional/tests/object/v1/test_container.py42
-rw-r--r--openstackclient/common/clientmanager.py3
-rw-r--r--openstackclient/compute/v2/security_group.py39
-rw-r--r--openstackclient/shell.py16
-rw-r--r--openstackclient/tests/test_shell.py256
-rw-r--r--requirements.txt6
-rw-r--r--tox.ini1
13 files changed, 399 insertions, 52 deletions
diff --git a/.testr.conf b/.testr.conf
index 7388cc3c..90c80c18 100644
--- a/.testr.conf
+++ b/.testr.conf
@@ -6,3 +6,4 @@ test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
test_id_option=--load-list $IDFILE
test_list_option=--list
+group_regex=([^\.]+\.)+
diff --git a/doc/source/backwards-incompatible.rst b/doc/source/backwards-incompatible.rst
index 817f2a30..437f9324 100644
--- a/doc/source/backwards-incompatible.rst
+++ b/doc/source/backwards-incompatible.rst
@@ -35,6 +35,18 @@ List of Backwards Incompatible Changes
* Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1404073
* Commit: https://review.openstack.org/#/c/143242/
+3. Command `openstack security group rule delete` now requires rule id
+
+ Previously, the command was `openstack security group rule delete --proto
+ <proto> [--src-ip <ip-address> --dst-port <port-range>] <group>`,
+ whereas now it is: `openstack security group rule delete <rule>`.
+
+ * In favor of: Using `openstack security group rule delete <rule>`.
+ * As of: 1.2.1
+ * Removed in: NA
+ * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1450872
+ * Commit: https://review.openstack.org/#/c/179446/
+
For Developers
==============
diff --git a/functional/common/test.py b/functional/common/test.py
index 4a92def0..7beaf39a 100644
--- a/functional/common/test.py
+++ b/functional/common/test.py
@@ -28,12 +28,12 @@ EXAMPLE_DIR = os.path.join(ROOT_DIR, 'examples')
def execute(cmd, fail_ok=False, merge_stderr=False):
"""Executes specified command for the given action."""
- cmd = shlex.split(cmd.encode('utf-8'))
+ cmdlist = shlex.split(cmd.encode('utf-8'))
result = ''
result_err = ''
stdout = subprocess.PIPE
stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE
- proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
+ proc = subprocess.Popen(cmdlist, stdout=stdout, stderr=stderr)
result, result_err = proc.communicate()
if not fail_ok and proc.returncode != 0:
raise exceptions.CommandFailed(proc.returncode, cmd, result,
@@ -50,6 +50,33 @@ class TestCase(testtools.TestCase):
"""Executes openstackclient command for the given action."""
return execute('openstack ' + cmd, fail_ok=fail_ok)
+ @classmethod
+ def get_show_opts(cls, fields=[]):
+ return ' -f value ' + ' '.join(['-c ' + it for it in fields])
+
+ @classmethod
+ def get_list_opts(cls, headers=[]):
+ opts = ' -f csv --quote none '
+ opts = opts + ' '.join(['-c ' + it for it in headers])
+ return opts
+
+ @classmethod
+ def assertOutput(cls, expected, actual):
+ if expected != actual:
+ raise Exception(expected + ' != ' + actual)
+
+ @classmethod
+ def assertInOutput(cls, expected, actual):
+ if expected not in actual:
+ raise Exception(expected + ' not in ' + actual)
+
+ @classmethod
+ def cleanup_tmpfile(cls, filename):
+ try:
+ os.remove(filename)
+ except OSError:
+ pass
+
def assert_table_structure(self, items, field_names):
"""Verify that all items have keys listed in field_names."""
for item in items:
diff --git a/functional/tests/compute/__init__.py b/functional/tests/compute/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/functional/tests/compute/__init__.py
diff --git a/functional/tests/compute/v2/__init__.py b/functional/tests/compute/v2/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/functional/tests/compute/v2/__init__.py
diff --git a/functional/tests/compute/v2/test_keypair.py b/functional/tests/compute/v2/test_keypair.py
new file mode 100644
index 00000000..0909c2d6
--- /dev/null
+++ b/functional/tests/compute/v2/test_keypair.py
@@ -0,0 +1,44 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import uuid
+
+from functional.common import test
+
+
+class KeypairTests(test.TestCase):
+ """Functional tests for compute keypairs. """
+ NAME = uuid.uuid4().hex
+ HEADERS = ['Name']
+ FIELDS = ['deleted_at', 'name', 'updated_at']
+
+ @classmethod
+ def setUpClass(cls):
+ private_key = cls.openstack('keypair create ' + cls.NAME)
+ cls.assertInOutput('-----BEGIN RSA PRIVATE KEY-----', private_key)
+ cls.assertInOutput('-----END RSA PRIVATE KEY-----', private_key)
+
+ @classmethod
+ def tearDownClass(cls):
+ raw_output = cls.openstack('keypair delete ' + cls.NAME)
+ cls.assertOutput('', raw_output)
+
+ def test_keypair_list(self):
+ opts = self.get_list_opts(self.HEADERS)
+ raw_output = self.openstack('keypair list' + opts)
+ self.assertIn(self.NAME, raw_output)
+
+ def test_keypair_show(self):
+ opts = self.get_show_opts(self.FIELDS)
+ raw_output = self.openstack('keypair show ' + self.NAME + opts)
+ expected = "None\n" + self.NAME + "\nNone\n"
+ self.assertEqual(expected, raw_output)
diff --git a/functional/tests/object/v1/test_container.py b/functional/tests/object/v1/test_container.py
new file mode 100644
index 00000000..9ea14cde
--- /dev/null
+++ b/functional/tests/object/v1/test_container.py
@@ -0,0 +1,42 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import uuid
+
+from functional.common import test
+
+
+class ContainerTests(test.TestCase):
+ """Functional tests for object containers. """
+ NAME = uuid.uuid4().hex
+
+ @classmethod
+ def setUpClass(cls):
+ opts = cls.get_list_opts(['container'])
+ raw_output = cls.openstack('container create ' + cls.NAME + opts)
+ expected = 'container\n' + cls.NAME + '\n'
+ cls.assertOutput(expected, raw_output)
+
+ @classmethod
+ def tearDownClass(cls):
+ raw_output = cls.openstack('container delete ' + cls.NAME)
+ cls.assertOutput('', raw_output)
+
+ def test_container_list(self):
+ opts = self.get_list_opts(['Name'])
+ raw_output = self.openstack('container list' + opts)
+ self.assertIn(self.NAME, raw_output)
+
+ def test_container_show(self):
+ opts = self.get_show_opts(['container'])
+ raw_output = self.openstack('container show ' + self.NAME + opts)
+ self.assertEqual(self.NAME + "\n", raw_output)
diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py
index ca5ece0d..85e367c4 100644
--- a/openstackclient/common/clientmanager.py
+++ b/openstackclient/common/clientmanager.py
@@ -30,6 +30,8 @@ LOG = logging.getLogger(__name__)
PLUGIN_MODULES = []
+USER_AGENT = 'python-openstackclient'
+
class ClientCache(object):
"""Descriptor class for caching created client handles."""
@@ -163,6 +165,7 @@ class ClientManager(object):
auth=self.auth,
session=request_session,
verify=self._verify,
+ user_agent=USER_AGENT,
)
return
diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py
index 55405810..d860bf80 100644
--- a/openstackclient/compute/v2/security_group.py
+++ b/openstackclient/compute/v2/security_group.py
@@ -328,28 +328,9 @@ class DeleteSecurityGroupRule(command.Command):
def get_parser(self, prog_name):
parser = super(DeleteSecurityGroupRule, self).get_parser(prog_name)
parser.add_argument(
- 'group',
- metavar='<group>',
- help='Security group rule to delete (name or ID)',
- )
- parser.add_argument(
- "--proto",
- metavar="<proto>",
- default="tcp",
- help="IP protocol (icmp, tcp, udp; default: tcp)",
- )
- parser.add_argument(
- "--src-ip",
- metavar="<ip-address>",
- default="0.0.0.0/0",
- help="Source IP (may use CIDR notation; default: 0.0.0.0/0)",
- )
- parser.add_argument(
- "--dst-port",
- metavar="<port-range>",
- action=parseractions.RangeAction,
- help="Destination port, may be a range: 137:139 (default: 0; "
- "only required for proto tcp and udp)",
+ 'rule',
+ metavar='<rule>',
+ help='Security group rule ID to delete',
)
return parser
@@ -357,19 +338,7 @@ class DeleteSecurityGroupRule(command.Command):
self.log.debug('take_action(%s)', parsed_args)
compute_client = self.app.client_manager.compute
- group = utils.find_resource(
- compute_client.security_groups,
- parsed_args.group,
- )
- from_port, to_port = parsed_args.dst_port
- # sigh...delete by ID?
- compute_client.security_group_rules.delete(
- group.id,
- parsed_args.proto,
- from_port,
- to_port,
- parsed_args.src_ip,
- )
+ compute_client.security_group_rules.delete(parsed_args.rule)
return
diff --git a/openstackclient/shell.py b/openstackclient/shell.py
index 5e291021..da985cbc 100644
--- a/openstackclient/shell.py
+++ b/openstackclient/shell.py
@@ -187,11 +187,13 @@ class OpenStackShell(app.App):
verify_group = parser.add_mutually_exclusive_group()
verify_group.add_argument(
'--verify',
- action='store_true',
+ default=None,
+ action='store_false',
help='Verify server certificate (default)',
)
verify_group.add_argument(
'--insecure',
+ default=None,
action='store_true',
help='Disable server certificate verification',
)
@@ -224,12 +226,6 @@ class OpenStackShell(app.App):
# Parent __init__ parses argv into self.options
super(OpenStackShell, self).initialize_app(argv)
- # Resolve the verify/insecure exclusive pair here as cloud_config
- # doesn't know about verify
- self.options.insecure = (
- self.options.insecure and not self.options.verify
- )
-
# Set the default plugin to token_endpoint if rl and token are given
if (self.options.url and self.options.token):
# Use service token authentication
@@ -253,10 +249,8 @@ class OpenStackShell(app.App):
if cacert:
self.verify = cacert
else:
- self.verify = not getattr(self.cloud.config, 'insecure', False)
-
- # Neutralize verify option
- self.options.verify = None
+ self.verify = not self.cloud.config.get('insecure', False)
+ self.verify = self.cloud.config.get('verify', self.verify)
# Save default domain
self.default_domain = self.options.os_default_domain
diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py
index a43be954..492b60de 100644
--- a/openstackclient/tests/test_shell.py
+++ b/openstackclient/tests/test_shell.py
@@ -13,6 +13,7 @@
# under the License.
#
+import copy
import mock
import os
@@ -36,7 +37,6 @@ DEFAULT_TOKEN = "token"
DEFAULT_SERVICE_URL = "http://127.0.0.1:8771/v3.0/"
DEFAULT_AUTH_PLUGIN = "v2password"
-
DEFAULT_COMPUTE_API_VERSION = "2"
DEFAULT_IDENTITY_API_VERSION = "2"
DEFAULT_IMAGE_API_VERSION = "2"
@@ -49,6 +49,46 @@ LIB_IMAGE_API_VERSION = "1"
LIB_VOLUME_API_VERSION = "1"
LIB_NETWORK_API_VERSION = "2"
+CLOUD_1 = {
+ 'clouds': {
+ 'scc': {
+ 'auth': {
+ 'auth_url': DEFAULT_AUTH_URL,
+ 'project_name': DEFAULT_PROJECT_NAME,
+ 'username': 'zaphod',
+ },
+ 'region_name': 'occ-cloud',
+ 'donut': 'glazed',
+ }
+ }
+}
+
+CLOUD_2 = {
+ 'clouds': {
+ 'megacloud': {
+ 'cloud': 'megadodo',
+ 'auth': {
+ 'project_name': 'heart-o-gold',
+ 'username': 'zaphod',
+ },
+ 'region_name': 'occ-cloud',
+ }
+ }
+}
+
+PUBLIC_1 = {
+ 'public-clouds': {
+ 'megadodo': {
+ 'auth': {
+ 'auth_url': DEFAULT_AUTH_URL,
+ 'project_name': DEFAULT_PROJECT_NAME,
+ },
+ 'region_name': 'occ-public',
+ 'donut': 'cake',
+ }
+ }
+}
+
def make_shell():
"""Create a new command shell and mock out some bits."""
@@ -516,3 +556,217 @@ class TestShellCli(TestShell):
"network_api_version": LIB_NETWORK_API_VERSION
}
self._assert_cli(flag, kwargs)
+
+ @mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
+ def test_shell_args_cloud_no_vendor(self, config_mock):
+ config_mock.return_value = copy.deepcopy(CLOUD_1)
+ _shell = make_shell()
+
+ fake_execute(
+ _shell,
+ "--os-cloud scc list user",
+ )
+ self.assertEqual(
+ 'scc',
+ _shell.cloud.name,
+ )
+
+ # These come from clouds.yaml
+ self.assertEqual(
+ DEFAULT_AUTH_URL,
+ _shell.cloud.config['auth']['auth_url'],
+ )
+ self.assertEqual(
+ DEFAULT_PROJECT_NAME,
+ _shell.cloud.config['auth']['project_name'],
+ )
+ self.assertEqual(
+ 'zaphod',
+ _shell.cloud.config['auth']['username'],
+ )
+ self.assertEqual(
+ 'occ-cloud',
+ _shell.cloud.config['region_name'],
+ )
+ self.assertEqual(
+ 'glazed',
+ _shell.cloud.config['donut'],
+ )
+
+ @mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
+ @mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
+ def test_shell_args_cloud_public(self, config_mock, public_mock):
+ config_mock.return_value = copy.deepcopy(CLOUD_2)
+ public_mock.return_value = copy.deepcopy(PUBLIC_1)
+ _shell = make_shell()
+
+ fake_execute(
+ _shell,
+ "--os-cloud megacloud list user",
+ )
+ self.assertEqual(
+ 'megacloud',
+ _shell.cloud.name,
+ )
+
+ # These come from clouds-public.yaml
+ self.assertEqual(
+ DEFAULT_AUTH_URL,
+ _shell.cloud.config['auth']['auth_url'],
+ )
+ self.assertEqual(
+ 'cake',
+ _shell.cloud.config['donut'],
+ )
+
+ # These come from clouds.yaml
+ self.assertEqual(
+ 'heart-o-gold',
+ _shell.cloud.config['auth']['project_name'],
+ )
+ self.assertEqual(
+ 'zaphod',
+ _shell.cloud.config['auth']['username'],
+ )
+ self.assertEqual(
+ 'occ-cloud',
+ _shell.cloud.config['region_name'],
+ )
+
+ @mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
+ @mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
+ def test_shell_args_precedence(self, config_mock, vendor_mock):
+ config_mock.return_value = copy.deepcopy(CLOUD_2)
+ vendor_mock.return_value = copy.deepcopy(PUBLIC_1)
+ _shell = make_shell()
+
+ # Test command option overriding config file value
+ fake_execute(
+ _shell,
+ "--os-cloud megacloud --os-region-name krikkit list user",
+ )
+ self.assertEqual(
+ 'megacloud',
+ _shell.cloud.name,
+ )
+
+ # These come from clouds-public.yaml
+ self.assertEqual(
+ DEFAULT_AUTH_URL,
+ _shell.cloud.config['auth']['auth_url'],
+ )
+ self.assertEqual(
+ 'cake',
+ _shell.cloud.config['donut'],
+ )
+
+ # These come from clouds.yaml
+ self.assertEqual(
+ 'heart-o-gold',
+ _shell.cloud.config['auth']['project_name'],
+ )
+ self.assertEqual(
+ 'zaphod',
+ _shell.cloud.config['auth']['username'],
+ )
+ self.assertEqual(
+ 'krikkit',
+ _shell.cloud.config['region_name'],
+ )
+
+
+class TestShellCliEnv(TestShell):
+ def setUp(self):
+ super(TestShellCliEnv, self).setUp()
+ env = {
+ 'OS_REGION_NAME': 'occ-env',
+ }
+ self.orig_env, os.environ = os.environ, env.copy()
+
+ def tearDown(self):
+ super(TestShellCliEnv, self).tearDown()
+ os.environ = self.orig_env
+
+ @mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
+ @mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
+ def test_shell_args_precedence_1(self, config_mock, vendor_mock):
+ config_mock.return_value = copy.deepcopy(CLOUD_2)
+ vendor_mock.return_value = copy.deepcopy(PUBLIC_1)
+ _shell = make_shell()
+
+ # Test env var
+ fake_execute(
+ _shell,
+ "--os-cloud megacloud list user",
+ )
+ self.assertEqual(
+ 'megacloud',
+ _shell.cloud.name,
+ )
+
+ # These come from clouds-public.yaml
+ self.assertEqual(
+ DEFAULT_AUTH_URL,
+ _shell.cloud.config['auth']['auth_url'],
+ )
+ self.assertEqual(
+ 'cake',
+ _shell.cloud.config['donut'],
+ )
+
+ # These come from clouds.yaml
+ self.assertEqual(
+ 'heart-o-gold',
+ _shell.cloud.config['auth']['project_name'],
+ )
+ self.assertEqual(
+ 'zaphod',
+ _shell.cloud.config['auth']['username'],
+ )
+ self.assertEqual(
+ 'occ-env',
+ _shell.cloud.config['region_name'],
+ )
+
+ @mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
+ @mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
+ def test_shell_args_precedence_2(self, config_mock, vendor_mock):
+ config_mock.return_value = copy.deepcopy(CLOUD_2)
+ vendor_mock.return_value = copy.deepcopy(PUBLIC_1)
+ _shell = make_shell()
+
+ # Test command option overriding config file value
+ fake_execute(
+ _shell,
+ "--os-cloud megacloud --os-region-name krikkit list user",
+ )
+ self.assertEqual(
+ 'megacloud',
+ _shell.cloud.name,
+ )
+
+ # These come from clouds-public.yaml
+ self.assertEqual(
+ DEFAULT_AUTH_URL,
+ _shell.cloud.config['auth']['auth_url'],
+ )
+ self.assertEqual(
+ 'cake',
+ _shell.cloud.config['donut'],
+ )
+
+ # These come from clouds.yaml
+ self.assertEqual(
+ 'heart-o-gold',
+ _shell.cloud.config['auth']['project_name'],
+ )
+ self.assertEqual(
+ 'zaphod',
+ _shell.cloud.config['auth']['username'],
+ )
+
+ # These come from the command line
+ self.assertEqual(
+ 'krikkit',
+ _shell.cloud.config['region_name'],
+ )
diff --git a/requirements.txt b/requirements.txt
index 2a9e8ff3..2196fb8e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
-pbr>=0.6,!=0.7,<1.0
+pbr>=0.11,<2.0
six>=1.9.0
Babel>=1.3
@@ -12,10 +12,10 @@ oslo.config>=1.11.0 # Apache-2.0
oslo.i18n>=1.5.0 # Apache-2.0
oslo.utils>=1.4.0 # Apache-2.0
oslo.serialization>=1.4.0 # Apache-2.0
-python-glanceclient>=0.15.0
+python-glanceclient>=0.17.1
python-keystoneclient>=1.3.0
python-novaclient>=2.22.0
-python-cinderclient>=1.1.0
+python-cinderclient>=1.2.1
python-neutronclient>=2.3.11,<3
requests>=2.5.2
stevedore>=1.3.0 # Apache-2.0
diff --git a/tox.ini b/tox.ini
index fdac2b33..2ee9cdbc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -17,6 +17,7 @@ commands = flake8
[testenv:functional]
setenv = OS_TEST_PATH=./functional/tests
+passenv = OS_*
[testenv:venv]
commands = {posargs}