diff options
| -rw-r--r-- | .testr.conf | 1 | ||||
| -rw-r--r-- | doc/source/backwards-incompatible.rst | 12 | ||||
| -rw-r--r-- | functional/common/test.py | 31 | ||||
| -rw-r--r-- | functional/tests/compute/__init__.py | 0 | ||||
| -rw-r--r-- | functional/tests/compute/v2/__init__.py | 0 | ||||
| -rw-r--r-- | functional/tests/compute/v2/test_keypair.py | 44 | ||||
| -rw-r--r-- | functional/tests/object/v1/test_container.py | 42 | ||||
| -rw-r--r-- | openstackclient/common/clientmanager.py | 3 | ||||
| -rw-r--r-- | openstackclient/compute/v2/security_group.py | 39 | ||||
| -rw-r--r-- | openstackclient/shell.py | 16 | ||||
| -rw-r--r-- | openstackclient/tests/test_shell.py | 256 | ||||
| -rw-r--r-- | requirements.txt | 6 | ||||
| -rw-r--r-- | tox.ini | 1 |
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 @@ -17,6 +17,7 @@ commands = flake8 [testenv:functional] setenv = OS_TEST_PATH=./functional/tests +passenv = OS_* [testenv:venv] commands = {posargs} |
