summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.zuul.yaml5
-rw-r--r--doc/source/cli/command-objects/server-migration.rst12
-rw-r--r--doc/source/contributor/developing.rst3
-rw-r--r--lower-constraints.txt1
-rw-r--r--openstackclient/api/object_store_v1.py2
-rw-r--r--openstackclient/common/sdk_utils.py4
-rw-r--r--openstackclient/compute/v2/keypair.py42
-rw-r--r--openstackclient/compute/v2/server.py614
-rw-r--r--openstackclient/compute/v2/server_backup.py5
-rw-r--r--openstackclient/compute/v2/server_group.py17
-rw-r--r--openstackclient/compute/v2/server_image.py4
-rw-r--r--openstackclient/identity/v3/access_rule.py3
-rw-r--r--openstackclient/image/v2/image.py2
-rw-r--r--openstackclient/network/common.py22
-rw-r--r--openstackclient/network/v2/network_meter_rule.py22
-rw-r--r--openstackclient/network/v2/port.py15
-rw-r--r--openstackclient/network/v2/security_group_rule.py17
-rw-r--r--openstackclient/shell.py8
-rw-r--r--openstackclient/tests/functional/compute/v2/test_server.py58
-rw-r--r--openstackclient/tests/unit/compute/v2/fakes.py129
-rw-r--r--openstackclient/tests/unit/compute/v2/test_keypair.py144
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server.py1255
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server_group.py120
-rw-r--r--openstackclient/tests/unit/compute/v2/test_service.py5
-rw-r--r--openstackclient/tests/unit/fakes.py3
-rw-r--r--openstackclient/tests/unit/image/v2/test_image.py6
-rw-r--r--openstackclient/tests/unit/network/v2/fakes.py8
-rw-r--r--openstackclient/tests/unit/network/v2/test_network_meter_rule.py12
-rw-r--r--openstackclient/tests/unit/network/v2/test_port.py62
-rw-r--r--openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py1
-rw-r--r--openstackclient/tests/unit/network/v2/test_security_group_rule_network.py16
-rw-r--r--openstackclient/tests/unit/object/v1/fakes.py3
-rw-r--r--openstackclient/tests/unit/object/v1/test_object_all.py6
-rw-r--r--openstackclient/tests/unit/test_shell.py9
-rw-r--r--openstackclient/tests/unit/utils.py2
-rw-r--r--releasenotes/notes/add-tag-support-server-add-fixed-ip-8de2db58f2a80e85.yaml5
-rw-r--r--releasenotes/notes/add-tag-support-server-add-network-a8590cab5d7babf0.yaml5
-rw-r--r--releasenotes/notes/add-tag-support-server-add-port-7e30aa38202d0839.yaml5
-rw-r--r--releasenotes/notes/add-tag-support-server-add-volume-278e79a22dd482f4.yaml5
-rw-r--r--releasenotes/notes/always-show-direction-for-sg-rule-130efc39bf67d79a.yaml10
-rw-r--r--releasenotes/notes/bp-add-user-id-field-to-the-migrations-table-299b99ccb1f12a1f.yaml5
-rw-r--r--releasenotes/notes/bug-1708570-bb19e1213e887723.yaml5
-rw-r--r--releasenotes/notes/bug-2007489-42e41b14e42128ce.yaml4
-rw-r--r--releasenotes/notes/bug-2007513-ae39456aeb93bb98.yaml4
-rw-r--r--releasenotes/notes/keypair-support-type-6f7c32aab3b61f7b.yaml5
-rw-r--r--releasenotes/notes/properties-with-image-property-field.yaml-c51bf37c3106d6ff.yaml6
-rw-r--r--releasenotes/notes/restore-create-image-duplicates-92e06f64038b120c.yaml6
-rw-r--r--releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml4
-rw-r--r--releasenotes/notes/server-add-tag-63f9cd01dbd82d1b.yaml14
-rw-r--r--releasenotes/notes/story-2004346-add-floating-ip-with-no-ports-399c5559e1699816.yaml7
-rw-r--r--releasenotes/notes/story-2005468-server-use-config-drive-9fc68552365cfefa.yaml8
-rw-r--r--releasenotes/source/index.rst1
-rw-r--r--releasenotes/source/victoria.rst6
-rw-r--r--requirements.txt3
-rw-r--r--setup.cfg3
-rw-r--r--tox.ini3
56 files changed, 2516 insertions, 235 deletions
diff --git a/.zuul.yaml b/.zuul.yaml
index e566ceb8..42c29d16 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,3 +1,4 @@
+---
- job:
name: osc-tox-unit-tips
parent: openstack-tox
@@ -91,6 +92,7 @@
neutron-segments: true
q-metering: true
q-qos: true
+ neutron-tag-ports-during-bulk-creation: true
tox_envlist: functional
- job:
@@ -220,11 +222,10 @@
- osc-tox-unit-tips
- openstack-cover-jobs
- openstack-lower-constraints-jobs
- - openstack-python3-victoria-jobs
+ - openstack-python3-wallaby-jobs
- publish-openstack-docs-pti
- check-requirements
- release-notes-jobs-python3
- - lib-forward-testing-python3
check:
jobs:
- osc-build-image
diff --git a/doc/source/cli/command-objects/server-migration.rst b/doc/source/cli/command-objects/server-migration.rst
new file mode 100644
index 00000000..6e2982cf
--- /dev/null
+++ b/doc/source/cli/command-objects/server-migration.rst
@@ -0,0 +1,12 @@
+================
+server migration
+================
+
+A server migration provides a way to move an instance from one
+host to another. There are four types of migration operation
+supported: live migration, cold migration, resize and evacuation.
+
+Compute v2
+
+.. autoprogram-cliff:: openstack.compute.v2
+ :command: server migration list
diff --git a/doc/source/contributor/developing.rst b/doc/source/contributor/developing.rst
index 5b859199..a3198493 100644
--- a/doc/source/contributor/developing.rst
+++ b/doc/source/contributor/developing.rst
@@ -199,13 +199,12 @@ Example
import copy
import fixtures
- import mock
import os
from osc_lib.api import auth
from osc_lib import utils
- import six
from openstackclient import shell
from openstackclient.tests import utils
+ from unittest import mock
diff --git a/lower-constraints.txt b/lower-constraints.txt
index 403ba4e0..2fa6586e 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -119,7 +119,6 @@ rfc3986==0.3.1
Routes==2.3.1
rsd-lib==0.1.0
simplejson==3.5.1
-six==1.10.0
smmap==0.9.0
statsd==3.2.1
stestr==1.0.0
diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py
index 8092abd0..67c79230 100644
--- a/openstackclient/api/object_store_v1.py
+++ b/openstackclient/api/object_store_v1.py
@@ -17,9 +17,9 @@ import io
import logging
import os
import sys
+import urllib
from osc_lib import utils
-from six.moves import urllib
from openstackclient.api import api
diff --git a/openstackclient/common/sdk_utils.py b/openstackclient/common/sdk_utils.py
index 9f085617..af9c74f9 100644
--- a/openstackclient/common/sdk_utils.py
+++ b/openstackclient/common/sdk_utils.py
@@ -10,8 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import six
-
def get_osc_show_columns_for_sdk_resource(
sdk_resource,
@@ -44,7 +42,7 @@ def get_osc_show_columns_for_sdk_resource(
for col_name in invisible_columns:
if col_name in display_columns:
display_columns.remove(col_name)
- for sdk_attr, osc_attr in six.iteritems(osc_column_map):
+ for sdk_attr, osc_attr in osc_column_map.items():
if sdk_attr in display_columns:
attr_map[osc_attr] = sdk_attr
display_columns.remove(sdk_attr)
diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py
index 2b365ceb..6affaca3 100644
--- a/openstackclient/compute/v2/keypair.py
+++ b/openstackclient/compute/v2/keypair.py
@@ -20,6 +20,7 @@ import logging
import os
import sys
+from novaclient import api_versions
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
@@ -53,6 +54,15 @@ class CreateKeypair(command.ShowOne):
help=_("Filename for private key to save. If not used, "
"print private key in console.")
)
+ parser.add_argument(
+ '--type',
+ metavar='<type>',
+ choices=['ssh', 'x509'],
+ help=_(
+ "Keypair type. Can be ssh or x509. "
+ "(Supported by API versions '2.2' - '2.latest')"
+ ),
+ )
return parser
def take_action(self, parsed_args):
@@ -70,17 +80,28 @@ class CreateKeypair(command.ShowOne):
"exception": e}
)
- keypair = compute_client.keypairs.create(
- parsed_args.name,
- public_key=public_key,
- )
+ kwargs = {
+ 'name': parsed_args.name,
+ 'public_key': public_key,
+ }
+ if parsed_args.type:
+ if compute_client.api_version < api_versions.APIVersion('2.2'):
+ msg = _(
+ '--os-compute-api-version 2.2 or greater is required to '
+ 'support the --type option.'
+ )
+ raise exceptions.CommandError(msg)
+
+ kwargs['key_type'] = parsed_args.type
+
+ keypair = compute_client.keypairs.create(**kwargs)
private_key = parsed_args.private_key
# Save private key into specified file
if private_key:
try:
with io.open(
- os.path.expanduser(parsed_args.private_key), 'w+'
+ os.path.expanduser(parsed_args.private_key), 'w+'
) as p:
p.write(keypair.private_key)
except IOError as e:
@@ -150,10 +171,13 @@ class ListKeypair(command.Lister):
)
data = compute_client.keypairs.list()
- return (columns,
- (utils.get_item_properties(
- s, columns,
- ) for s in data))
+ if compute_client.api_version >= api_versions.APIVersion('2.2'):
+ columns += ("Type", )
+
+ return (
+ columns,
+ (utils.get_item_properties(s, columns) for s in data),
+ )
class ShowKeypair(command.ShowOne):
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index 93e9f966..4aed8ad7 100644
--- a/openstackclient/compute/v2/server.py
+++ b/openstackclient/compute/v2/server.py
@@ -21,15 +21,15 @@ import io
import logging
import os
+import iso8601
from novaclient import api_versions
from novaclient.v2 import servers
from openstack import exceptions as sdk_exceptions
+from osc_lib.cli import format_columns
from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
-from oslo_utils import timeutils
-import six
from openstackclient.i18n import _
from openstackclient.identity import common as identity_common
@@ -38,6 +38,8 @@ from openstackclient.network import common as network_common
LOG = logging.getLogger(__name__)
+IMAGE_STRING_FOR_BFV = 'N/A (booted from volume)'
+
def _format_servers_list_networks(networks):
"""Return a formatted string of a server's networks
@@ -94,7 +96,7 @@ def _get_ip_address(addresses, address_type, ip_address_family):
for network in addresses:
for addy in addresses[network]:
# Case where it is list of strings
- if isinstance(addy, six.string_types):
+ if isinstance(addy, str):
if new_address_type == 'fixed':
return addresses[network][0]
else:
@@ -147,6 +149,12 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
info['image'] = "%s (%s)" % (image.name, image_id)
except Exception:
info['image'] = image_id
+ else:
+ # NOTE(melwitt): An server booted from a volume will have no image
+ # associated with it. We fill in the image with "N/A (booted from
+ # volume)" to help users who want to be able to grep for
+ # boot-from-volume servers when using the CLI.
+ info['image'] = IMAGE_STRING_FOR_BFV
# Convert the flavor blob to a name
flavor_info = info.get('flavor', {})
@@ -166,14 +174,14 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
if 'os-extended-volumes:volumes_attached' in info:
info.update(
{
- 'volumes_attached': utils.format_list_of_dicts(
+ 'volumes_attached': format_columns.ListDictColumn(
info.pop('os-extended-volumes:volumes_attached'))
}
)
if 'security_groups' in info:
info.update(
{
- 'security_groups': utils.format_list_of_dicts(
+ 'security_groups': format_columns.ListDictColumn(
info.pop('security_groups'))
}
)
@@ -182,9 +190,14 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
info['addresses'] = _format_servers_list_networks(server.networks)
# Map 'metadata' field to 'properties'
- info.update(
- {'properties': utils.format_dict(info.pop('metadata'))}
- )
+ if not info['metadata']:
+ info.update(
+ {'properties': utils.format_dict(info.pop('metadata'))}
+ )
+ else:
+ info.update(
+ {'properties': format_columns.DictColumn(info.pop('metadata'))}
+ )
# Migrate tenant_id to project_id naming
if 'tenant_id' in info:
@@ -223,6 +236,14 @@ class AddFixedIP(command.Command):
metavar="<ip-address>",
help=_("Requested fixed IP address"),
)
+ parser.add_argument(
+ '--tag',
+ metavar='<tag>',
+ help=_(
+ 'Tag for the attached interface. '
+ '(supported by --os-compute-api-version 2.52 or above)'
+ )
+ )
return parser
def take_action(self, parsed_args):
@@ -233,11 +254,23 @@ class AddFixedIP(command.Command):
network = compute_client.api.network_find(parsed_args.network)
- server.interface_attach(
- port_id=None,
- net_id=network['id'],
- fixed_ip=parsed_args.fixed_ip_address,
- )
+ kwargs = {
+ 'port_id': None,
+ 'net_id': network['id'],
+ 'fixed_ip': parsed_args.fixed_ip_address,
+ }
+
+ if parsed_args.tag:
+ if compute_client.api_version < api_versions.APIVersion('2.49'):
+ msg = _(
+ '--os-compute-api-version 2.49 or greater is required to '
+ 'support the --tag option'
+ )
+ raise exceptions.CommandError(msg)
+
+ kwargs['tag'] = parsed_args.tag
+
+ server.interface_attach(**kwargs)
class AddFloatingIP(network_common.NetworkAndComputeCommand):
@@ -279,6 +312,10 @@ class AddFloatingIP(network_common.NetworkAndComputeCommand):
parsed_args.server,
)
ports = list(client.ports(device_id=server.id))
+ if not ports:
+ msg = _('No attached ports found to associate floating IP with')
+ raise exceptions.CommandError(msg)
+
# If the fixed IP address was specified, we need to find the
# corresponding port.
if parsed_args.fixed_ip_address:
@@ -341,6 +378,14 @@ class AddPort(command.Command):
metavar="<port>",
help=_("Port to add to the server (name or ID)"),
)
+ parser.add_argument(
+ '--tag',
+ metavar='<tag>',
+ help=_(
+ "Tag for the attached interface. "
+ "(Supported by API versions '2.49' - '2.latest')"
+ )
+ )
return parser
def take_action(self, parsed_args):
@@ -356,7 +401,22 @@ class AddPort(command.Command):
else:
port_id = parsed_args.port
- server.interface_attach(port_id=port_id, net_id=None, fixed_ip=None)
+ kwargs = {
+ 'port_id': port_id,
+ 'net_id': None,
+ 'fixed_ip': None,
+ }
+
+ if parsed_args.tag:
+ if compute_client.api_version < api_versions.APIVersion("2.49"):
+ msg = _(
+ '--os-compute-api-version 2.49 or greater is required to '
+ 'support the --tag option'
+ )
+ raise exceptions.CommandError(msg)
+ kwargs['tag'] = parsed_args.tag
+
+ server.interface_attach(**kwargs)
class AddNetwork(command.Command):
@@ -374,6 +434,14 @@ class AddNetwork(command.Command):
metavar="<network>",
help=_("Network to add to the server (name or ID)"),
)
+ parser.add_argument(
+ '--tag',
+ metavar='<tag>',
+ help=_(
+ 'Tag for the attached interface. '
+ '(supported by --os-compute-api-version 2.49 or above)'
+ ),
+ )
return parser
def take_action(self, parsed_args):
@@ -389,7 +457,23 @@ class AddNetwork(command.Command):
else:
net_id = parsed_args.network
- server.interface_attach(port_id=None, net_id=net_id, fixed_ip=None)
+ kwargs = {
+ 'port_id': None,
+ 'net_id': net_id,
+ 'fixed_ip': None,
+ }
+
+ if parsed_args.tag:
+ if compute_client.api_version < api_versions.APIVersion('2.49'):
+ msg = _(
+ '--os-compute-api-version 2.49 or greater is required to '
+ 'support the --tag option'
+ )
+ raise exceptions.CommandError(msg)
+
+ kwargs['tag'] = parsed_args.tag
+
+ server.interface_attach(**kwargs)
class AddServerSecurityGroup(command.Command):
@@ -446,20 +530,32 @@ class AddServerVolume(command.Command):
metavar='<device>',
help=_('Server internal device name for volume'),
)
+ parser.add_argument(
+ '--tag',
+ metavar='<tag>',
+ help=_(
+ "Tag for the attached volume. "
+ "(Supported by API versions '2.49' - '2.latest')"
+ ),
+ )
termination_group = parser.add_mutually_exclusive_group()
termination_group.add_argument(
'--enable-delete-on-termination',
action='store_true',
- help=_("Specify if the attached volume should be deleted when "
- "the server is destroyed. (Supported with "
- "``--os-compute-api-version`` 2.79 or greater.)"),
+ help=_(
+ "Specify if the attached volume should be deleted when the "
+ "server is destroyed. "
+ "(Supported by API versions '2.79' - '2.latest')"
+ ),
)
termination_group.add_argument(
'--disable-delete-on-termination',
action='store_true',
- help=_("Specify if the attached volume should not be deleted "
- "when the server is destroyed. (Supported with "
- "``--os-compute-api-version`` 2.79 or greater.)"),
+ help=_(
+ "Specify if the attached volume should not be deleted when "
+ "the server is destroyed. "
+ "(Supported by API versions '2.79' - '2.latest')"
+ ),
)
return parser
@@ -476,28 +572,38 @@ class AddServerVolume(command.Command):
parsed_args.volume,
)
- support_set_delete_on_termination = (compute_client.api_version >=
- api_versions.APIVersion('2.79'))
-
- if not support_set_delete_on_termination:
- if parsed_args.enable_delete_on_termination:
- msg = _('--os-compute-api-version 2.79 or greater '
- 'is required to support the '
- '--enable-delete-on-termination option.')
- raise exceptions.CommandError(msg)
- if parsed_args.disable_delete_on_termination:
- msg = _('--os-compute-api-version 2.79 or greater '
- 'is required to support the '
- '--disable-delete-on-termination option.')
- raise exceptions.CommandError(msg)
-
kwargs = {
"device": parsed_args.device
}
+ if parsed_args.tag:
+ if compute_client.api_version < api_versions.APIVersion('2.49'):
+ msg = _(
+ '--os-compute-api-version 2.49 or greater is required to '
+ 'support the --tag option'
+ )
+ raise exceptions.CommandError(msg)
+
+ kwargs['tag'] = parsed_args.tag
+
if parsed_args.enable_delete_on_termination:
+ if compute_client.api_version < api_versions.APIVersion('2.79'):
+ msg = _(
+ '--os-compute-api-version 2.79 or greater is required to '
+ 'support the --enable-delete-on-termination option.'
+ )
+ raise exceptions.CommandError(msg)
+
kwargs['delete_on_termination'] = True
+
if parsed_args.disable_delete_on_termination:
+ if compute_client.api_version < api_versions.APIVersion('2.79'):
+ msg = _(
+ '--os-compute-api-version 2.79 or greater is required to '
+ 'support the --disable-delete-on-termination option.'
+ )
+ raise exceptions.CommandError(msg)
+
kwargs['delete_on_termination'] = False
compute_client.volumes.create_server_volume(
@@ -543,6 +649,11 @@ class CreateServer(command.ShowOne):
'volume.'),
)
parser.add_argument(
+ '--password',
+ metavar='<password>',
+ help=_("Set the password to this server"),
+ )
+ parser.add_argument(
'--flavor',
metavar='<flavor>',
required=True,
@@ -693,12 +804,30 @@ class CreateServer(command.ShowOne):
default={},
help=_('Hints for the scheduler (optional extension)'),
)
- parser.add_argument(
+ config_drive_group = parser.add_mutually_exclusive_group()
+ config_drive_group.add_argument(
+ '--use-config-drive',
+ action='store_true',
+ dest='config_drive',
+ help=_("Enable config drive."),
+ )
+ config_drive_group.add_argument(
+ '--no-config-drive',
+ action='store_false',
+ dest='config_drive',
+ help=_("Disable config drive."),
+ )
+ # TODO(stephenfin): Drop support in the next major version bump after
+ # Victoria
+ config_drive_group.add_argument(
'--config-drive',
metavar='<config-drive-volume>|True',
default=False,
- help=_('Use specified volume as the config drive, '
- 'or \'True\' to use an ephemeral drive'),
+ help=_(
+ "**Deprecated** Use specified volume as the config drive, "
+ "or 'True' to use an ephemeral drive. Replaced by "
+ "'--use-config-drive'."
+ ),
)
parser.add_argument(
'--min',
@@ -719,6 +848,18 @@ class CreateServer(command.ShowOne):
action='store_true',
help=_('Wait for build to complete'),
)
+ parser.add_argument(
+ '--tag',
+ metavar='<tag>',
+ action='append',
+ default=[],
+ dest='tags',
+ help=_(
+ 'Tags for the server. '
+ 'Specify multiple times to add multiple tags. '
+ '(supported by --os-compute-api-version 2.52 or above)'
+ ),
+ )
return parser
def take_action(self, parsed_args):
@@ -751,19 +892,27 @@ class CreateServer(command.ShowOne):
images_matched = []
for img in image_list:
img_dict = {}
+
# exclude any unhashable entries
- for key, value in img.items():
+ img_dict_items = list(img.items())
+ if img.properties:
+ img_dict_items.extend(list(img.properties.items()))
+ for key, value in img_dict_items:
try:
set([key, value])
except TypeError:
+ if key != 'properties':
+ LOG.debug('Skipped the \'%s\' attribute. '
+ 'That cannot be compared. '
+ '(image: %s, value: %s)',
+ key, img.id, value)
pass
else:
img_dict[key] = value
+
if all(k in img_dict and img_dict[k] == v
for k, v in wanted_properties.items()):
images_matched.append(img)
- else:
- return []
return images_matched
images = _match_image(image_client, parsed_args.image_property)
@@ -854,7 +1003,7 @@ class CreateServer(command.ShowOne):
boot_args = [parsed_args.server_name, image, flavor]
# Handle block device by device name order, like: vdb -> vdc -> vdd
- for dev_name in sorted(six.iterkeys(parsed_args.block_device_mapping)):
+ for dev_name in sorted(parsed_args.block_device_mapping):
dev_map = parsed_args.block_device_mapping[dev_name]
dev_map = dev_map.split(':')
if dev_map[0]:
@@ -991,16 +1140,19 @@ class CreateServer(command.ShowOne):
else:
hints[key] = values
- # What does a non-boolean value for config-drive do?
- # --config-drive argument is either a volume id or
- # 'True' (or '1') to use an ephemeral volume
- if str(parsed_args.config_drive).lower() in ("true", "1"):
- config_drive = True
- elif str(parsed_args.config_drive).lower() in ("false", "0",
- "", "none"):
- config_drive = None
+ if isinstance(parsed_args.config_drive, bool):
+ # NOTE(stephenfin): The API doesn't accept False as a value :'(
+ config_drive = parsed_args.config_drive or None
else:
- config_drive = parsed_args.config_drive
+ # TODO(stephenfin): Remove when we drop support for
+ # '--config-drive'
+ if str(parsed_args.config_drive).lower() in ("true", "1"):
+ config_drive = True
+ elif str(parsed_args.config_drive).lower() in ("false", "0",
+ "", "none"):
+ config_drive = None
+ else:
+ config_drive = parsed_args.config_drive
boot_kwargs = dict(
meta=parsed_args.property,
@@ -1012,6 +1164,7 @@ class CreateServer(command.ShowOne):
userdata=userdata,
key_name=parsed_args.key_name,
availability_zone=parsed_args.availability_zone,
+ admin_pass=parsed_args.password,
block_device_mapping_v2=block_device_mapping_v2,
nics=nics,
scheduler_hints=hints,
@@ -1020,6 +1173,16 @@ class CreateServer(command.ShowOne):
if parsed_args.description:
boot_kwargs['description'] = parsed_args.description
+ if parsed_args.tags:
+ if compute_client.api_version < api_versions.APIVersion('2.52'):
+ msg = _(
+ '--os-compute-api-version 2.52 or greater is required to '
+ 'support the --tag option'
+ )
+ raise exceptions.CommandError(msg)
+
+ boot_kwargs['tags'] = parsed_args.tags
+
if parsed_args.host:
if compute_client.api_version < api_versions.APIVersion("2.74"):
msg = _("Specifying --host is not supported for "
@@ -1287,6 +1450,30 @@ class ListServer(command.Lister):
help=_('Only display unlocked servers. '
'Requires ``--os-compute-api-version`` 2.73 or greater.'),
)
+ parser.add_argument(
+ '--tags',
+ metavar='<tag>',
+ action='append',
+ default=[],
+ dest='tags',
+ help=_(
+ 'Only list servers with the specified tag. '
+ 'Specify multiple times to filter on multiple tags. '
+ '(supported by --os-compute-api-version 2.26 or above)'
+ ),
+ )
+ parser.add_argument(
+ '--not-tags',
+ metavar='<tag>',
+ action='append',
+ default=[],
+ dest='not_tags',
+ help=_(
+ 'Only list servers without the specified tag. '
+ 'Specify multiple times to filter on multiple tags. '
+ '(supported by --os-compute-api-version 2.26 or above)'
+ ),
+ )
return parser
def take_action(self, parsed_args):
@@ -1342,6 +1529,27 @@ class ListServer(command.Lister):
'changes-before': parsed_args.changes_before,
'changes-since': parsed_args.changes_since,
}
+
+ if parsed_args.tags:
+ if compute_client.api_version < api_versions.APIVersion('2.26'):
+ msg = _(
+ '--os-compute-api-version 2.26 or greater is required to '
+ 'support the --tag option'
+ )
+ raise exceptions.CommandError(msg)
+
+ search_opts['tags'] = parsed_args.tags
+
+ if parsed_args.not_tags:
+ if compute_client.api_version < api_versions.APIVersion('2.26'):
+ msg = _(
+ '--os-compute-api-version 2.26 or greater is required to '
+ 'support the --not-tag option'
+ )
+ raise exceptions.CommandError(msg)
+
+ search_opts['not-tags'] = parsed_args.not_tags
+
support_locked = (compute_client.api_version >=
api_versions.APIVersion('2.73'))
if not support_locked and (parsed_args.locked or parsed_args.unlocked):
@@ -1362,8 +1570,8 @@ class ListServer(command.Lister):
raise exceptions.CommandError(msg)
try:
- timeutils.parse_isotime(search_opts['changes-before'])
- except ValueError:
+ iso8601.parse_date(search_opts['changes-before'])
+ except (TypeError, iso8601.ParseError):
raise exceptions.CommandError(
_('Invalid changes-before value: %s') %
search_opts['changes-before']
@@ -1371,8 +1579,8 @@ class ListServer(command.Lister):
if search_opts['changes-since']:
try:
- timeutils.parse_isotime(search_opts['changes-since'])
- except ValueError:
+ iso8601.parse_date(search_opts['changes-since'])
+ except (TypeError, iso8601.ParseError):
raise exceptions.CommandError(
_('Invalid changes-since value: %s') %
search_opts['changes-since']
@@ -1520,8 +1728,12 @@ class ListServer(command.Lister):
s.image_name = image.name
s.image_id = s.image['id']
else:
- s.image_name = ''
- s.image_id = ''
+ # NOTE(melwitt): An server booted from a volume will have no
+ # image associated with it. We fill in the Image Name and ID
+ # with "N/A (booted from volume)" to help users who want to be
+ # able to grep for boot-from-volume servers when using the CLI.
+ s.image_name = IMAGE_STRING_FOR_BFV
+ s.image_id = IMAGE_STRING_FOR_BFV
if 'id' in s.flavor:
flavor = flavors.get(s.flavor['id'])
if flavor:
@@ -1768,6 +1980,243 @@ revert to release the new server and restart the old one.""")
raise SystemExit
+class ListMigration(command.Command):
+ _description = _("""List server migrations.""")
+
+ def get_parser(self, prog_name):
+ parser = super(ListMigration, self).get_parser(prog_name)
+ parser.add_argument(
+ "--server",
+ metavar="<server>",
+ dest='server',
+ default=None,
+ help=_('Server to show migration details (name or ID).')
+ )
+ parser.add_argument(
+ "--host",
+ metavar="<host>",
+ default=None,
+ help=_('Fetch migrations for the given host.')
+ )
+ parser.add_argument(
+ "--status",
+ metavar="<status>",
+ default=None,
+ help=_('Fetch migrations for the given status.')
+ )
+ parser.add_argument(
+ "--marker",
+ metavar="<marker>",
+ dest='marker',
+ default=None,
+ help=_("The last migration of the previous page; displays list "
+ "of migrations after 'marker'. Note that the marker is "
+ "the migration UUID. (Supported with "
+ "``--os-compute-api-version`` 2.59 or greater.)")
+ )
+ parser.add_argument(
+ "--limit",
+ metavar="<limit>",
+ dest='limit',
+ type=int,
+ default=None,
+ help=_("Maximum number of migrations to display. Note that there "
+ "is a configurable max limit on the server, and the limit "
+ "that is used will be the minimum of what is requested "
+ "here and what is configured in the server. "
+ "(Supported with ``--os-compute-api-version`` 2.59 "
+ "or greater.)")
+ )
+ parser.add_argument(
+ '--changes-since',
+ dest='changes_since',
+ metavar='<changes-since>',
+ default=None,
+ help=_("List only migrations changed later or equal to a certain "
+ "point of time. The provided time should be an ISO 8061 "
+ "formatted time, e.g. ``2016-03-04T06:27:59Z``. "
+ "(Supported with ``--os-compute-api-version`` 2.59 "
+ "or greater.)")
+ )
+ parser.add_argument(
+ '--changes-before',
+ dest='changes_before',
+ metavar='<changes-before>',
+ default=None,
+ help=_("List only migrations changed earlier or equal to a "
+ "certain point of time. The provided time should be an ISO "
+ "8061 formatted time, e.g. ``2016-03-04T06:27:59Z``. "
+ "(Supported with ``--os-compute-api-version`` 2.66 or "
+ "greater.)")
+ )
+ parser.add_argument(
+ '--project',
+ metavar='<project>',
+ dest='project_id',
+ default=None,
+ help=_("Filter the migrations by the given project ID. "
+ "(Supported with ``--os-compute-api-version`` 2.80 "
+ "or greater.)"),
+ )
+ parser.add_argument(
+ '--user',
+ metavar='<user>',
+ dest='user_id',
+ default=None,
+ help=_("Filter the migrations by the given user ID. "
+ "(Supported with ``--os-compute-api-version`` 2.80 "
+ "or greater.)"),
+ )
+ return parser
+
+ def print_migrations(self, parsed_args, compute_client, migrations):
+ columns = ['Source Node', 'Dest Node', 'Source Compute',
+ 'Dest Compute', 'Dest Host', 'Status',
+ 'Server UUID', 'Old Flavor', 'New Flavor',
+ 'Created At', 'Updated At']
+
+ # Insert migrations UUID after ID
+ if compute_client.api_version >= api_versions.APIVersion("2.59"):
+ columns.insert(0, "UUID")
+
+ # TODO(brinzhang): It also suppports filter migrations by type
+ # since 2.1. https://review.opendev.org/#/c/675117 supported
+ # filtering the migrations by 'migration_type' and 'source_compute'
+ # in novaclient, that will be added in OSC by follow-up.
+ if compute_client.api_version >= api_versions.APIVersion("2.23"):
+ columns.insert(0, "Id")
+ columns.insert(len(columns) - 2, "Type")
+
+ if compute_client.api_version >= api_versions.APIVersion("2.80"):
+ if parsed_args.project_id:
+ columns.insert(len(columns) - 2, "Project")
+ if parsed_args.user_id:
+ columns.insert(len(columns) - 2, "User")
+
+ columns_header = columns
+ return (columns_header, (utils.get_item_properties(
+ mig, columns) for mig in migrations))
+
+ def take_action(self, parsed_args):
+ compute_client = self.app.client_manager.compute
+
+ search_opts = {
+ "host": parsed_args.host,
+ "server": parsed_args.server,
+ "status": parsed_args.status,
+ }
+
+ if (parsed_args.marker or parsed_args.limit or
+ parsed_args.changes_since):
+ if compute_client.api_version < api_versions.APIVersion("2.59"):
+ msg = _("marker, limit and/or changes_since is not supported "
+ "for --os-compute-api-version less than 2.59")
+ raise exceptions.CommandError(msg)
+ if parsed_args.marker:
+ search_opts['marker'] = parsed_args.marker
+ if parsed_args.limit:
+ search_opts['limit'] = parsed_args.limit
+ if parsed_args.changes_since:
+ search_opts['changes_since'] = parsed_args.changes_since
+
+ if parsed_args.changes_before:
+ if compute_client.api_version < api_versions.APIVersion("2.66"):
+ msg = _("changes_before is not supported for "
+ "--os-compute-api-version less than 2.66")
+ raise exceptions.CommandError(msg)
+ search_opts['changes_before'] = parsed_args.changes_before
+
+ if parsed_args.project_id or parsed_args.user_id:
+ if compute_client.api_version < api_versions.APIVersion("2.80"):
+ msg = _("Project and/or user is not supported for "
+ "--os-compute-api-version less than 2.80")
+ raise exceptions.CommandError(msg)
+ if parsed_args.project_id:
+ search_opts['project_id'] = parsed_args.project_id
+ if parsed_args.user_id:
+ search_opts['user_id'] = parsed_args.user_id
+
+ migrations = compute_client.migrations.list(**search_opts)
+
+ return self.print_migrations(parsed_args, compute_client, migrations)
+
+
+class AbortMigration(command.Command):
+ """Cancel an ongoing live migration.
+
+ This command requires ``--os-compute-api-version`` 2.24 or greater.
+ """
+
+ def get_parser(self, prog_name):
+ parser = super(AbortMigration, self).get_parser(prog_name)
+ parser.add_argument(
+ 'server',
+ metavar='<server>',
+ help=_('Server (name or ID)'),
+ )
+ parser.add_argument(
+ 'migration',
+ metavar='<migration>',
+ help=_("Migration (ID)"),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ compute_client = self.app.client_manager.compute
+
+ if compute_client.api_version < api_versions.APIVersion('2.24'):
+ msg = _(
+ '--os-compute-api-version 2.24 or greater is required to '
+ 'support the server migration abort command'
+ )
+ raise exceptions.CommandError(msg)
+
+ server = utils.find_resource(
+ compute_client.servers,
+ parsed_args.server,
+ )
+ compute_client.server_migrations.live_migration_abort(
+ server.id, parsed_args.migration)
+
+
+class ForceCompleteMigration(command.Command):
+ """Force an ongoing live migration to complete.
+
+ This command requires ``--os-compute-api-version`` 2.22 or greater.
+ """
+
+ def get_parser(self, prog_name):
+ parser = super(ForceCompleteMigration, self).get_parser(prog_name)
+ parser.add_argument(
+ 'server',
+ metavar='<server>',
+ help=_('Server (name or ID)'),
+ )
+ parser.add_argument(
+ 'migration',
+ metavar='<migration>',
+ help=_('Migration (ID)')
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ compute_client = self.app.client_manager.compute
+
+ if compute_client.api_version < api_versions.APIVersion('2.22'):
+ msg = _(
+ '--os-compute-api-version 2.22 or greater is required to '
+ 'support the server migration force complete command'
+ )
+ raise exceptions.CommandError(msg)
+
+ server = utils.find_resource(
+ compute_client.servers,
+ parsed_args.server,
+ )
+ compute_client.server_migrations.live_migrate_force_complete(
+ server.id, parsed_args.migration)
+
+
class PauseServer(command.Command):
_description = _("Pause server(s)")
@@ -2433,6 +2882,18 @@ class SetServer(command.Command):
help=_('New server description (supported by '
'--os-compute-api-version 2.19 or above)'),
)
+ parser.add_argument(
+ '--tag',
+ metavar='<tag>',
+ action='append',
+ default=[],
+ dest='tags',
+ help=_(
+ 'Tag for the server. '
+ 'Specify multiple times to add multiple tags. '
+ '(supported by --os-compute-api-version 2.26 or above)'
+ ),
+ )
return parser
def take_action(self, parsed_args):
@@ -2471,6 +2932,17 @@ class SetServer(command.Command):
raise exceptions.CommandError(msg)
server.update(description=parsed_args.description)
+ if parsed_args.tags:
+ if server.api_version < api_versions.APIVersion('2.26'):
+ msg = _(
+ '--os-compute-api-version 2.26 or greater is required to '
+ 'support the --tag option'
+ )
+ raise exceptions.CommandError(msg)
+
+ for tag in parsed_args.tags:
+ server.add_tag(tag=tag)
+
class ShelveServer(command.Command):
_description = _("Shelve server(s)")
@@ -2530,7 +3002,6 @@ class ShowServer(command.ShowOne):
data = _prep_server_detail(compute_client,
self.app.client_manager.image, server,
refresh=False)
-
return zip(*sorted(data.items()))
@@ -2813,7 +3284,7 @@ class UnrescueServer(command.Command):
class UnsetServer(command.Command):
- _description = _("Unset server properties")
+ _description = _("Unset server properties and tags")
def get_parser(self, prog_name):
parser = super(UnsetServer, self).get_parser(prog_name)
@@ -2837,6 +3308,18 @@ class UnsetServer(command.Command):
help=_('Unset server description (supported by '
'--os-compute-api-version 2.19 or above)'),
)
+ parser.add_argument(
+ '--tag',
+ metavar='<tag>',
+ action='append',
+ default=[],
+ dest='tags',
+ help=_(
+ 'Tag to remove from the server. '
+ 'Specify multiple times to remove multiple tags. '
+ '(supported by --os-compute-api-version 2.26 or later'
+ ),
+ )
return parser
def take_action(self, parsed_args):
@@ -2862,6 +3345,17 @@ class UnsetServer(command.Command):
description="",
)
+ if parsed_args.tags:
+ if compute_client.api_version < api_versions.APIVersion('2.26'):
+ msg = _(
+ '--os-compute-api-version 2.26 or greater is required to '
+ 'support the --tag option'
+ )
+ raise exceptions.CommandError(msg)
+
+ for tag in parsed_args.tags:
+ compute_client.servers.delete_tag(server, tag=tag)
+
class UnshelveServer(command.Command):
_description = _("Unshelve server(s)")
diff --git a/openstackclient/compute/v2/server_backup.py b/openstackclient/compute/v2/server_backup.py
index a5d43fc6..b1b821b2 100644
--- a/openstackclient/compute/v2/server_backup.py
+++ b/openstackclient/compute/v2/server_backup.py
@@ -15,10 +15,11 @@
"""Compute v2 Server action implementations"""
+import importlib
+
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
-from oslo_utils import importutils
from openstackclient.i18n import _
@@ -119,7 +120,7 @@ class CreateServerBackup(command.ShowOne):
info['properties'] = utils.format_dict(info.get('properties', {}))
else:
# Get the right image module to format the output
- image_module = importutils.import_module(
+ image_module = importlib.import_module(
self.IMAGE_API_VERSIONS[
self.app.client_manager._api_version['image']
]
diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py
index c49a552f..1af6e28d 100644
--- a/openstackclient/compute/v2/server_group.py
+++ b/openstackclient/compute/v2/server_group.py
@@ -17,6 +17,7 @@
import logging
+from novaclient import api_versions
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
@@ -67,9 +68,13 @@ class CreateServerGroup(command.ShowOne):
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
info = {}
+
+ policy_arg = {'policies': [parsed_args.policy]}
+ if compute_client.api_version >= api_versions.APIVersion("2.64"):
+ policy_arg = {'policy': parsed_args.policy}
server_group = compute_client.server_groups.create(
- name=parsed_args.name,
- policies=[parsed_args.policy])
+ name=parsed_args.name, **policy_arg)
+
info.update(server_group._info)
columns = _get_columns(info)
@@ -136,11 +141,15 @@ class ListServerGroup(command.Lister):
compute_client = self.app.client_manager.compute
data = compute_client.server_groups.list(parsed_args.all_projects)
+ policy_key = 'Policies'
+ if compute_client.api_version >= api_versions.APIVersion("2.64"):
+ policy_key = 'Policy'
+
if parsed_args.long:
column_headers = columns = (
'ID',
'Name',
- 'Policies',
+ policy_key,
'Members',
'Project Id',
'User Id',
@@ -149,7 +158,7 @@ class ListServerGroup(command.Lister):
column_headers = columns = (
'ID',
'Name',
- 'Policies',
+ policy_key,
)
return (column_headers,
diff --git a/openstackclient/compute/v2/server_image.py b/openstackclient/compute/v2/server_image.py
index fea87af8..c12bc2b3 100644
--- a/openstackclient/compute/v2/server_image.py
+++ b/openstackclient/compute/v2/server_image.py
@@ -15,12 +15,12 @@
"""Compute v2 Server action implementations"""
+import importlib
import logging
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
-from oslo_utils import importutils
from openstackclient.i18n import _
@@ -99,7 +99,7 @@ class CreateServerImage(command.ShowOne):
info['properties'] = utils.format_dict(info.get('properties', {}))
else:
# Get the right image module to format the output
- image_module = importutils.import_module(
+ image_module = importlib.import_module(
self.IMAGE_API_VERSIONS[
self.app.client_manager._api_version['image']
]
diff --git a/openstackclient/identity/v3/access_rule.py b/openstackclient/identity/v3/access_rule.py
index 65e78be1..ffda04f9 100644
--- a/openstackclient/identity/v3/access_rule.py
+++ b/openstackclient/identity/v3/access_rule.py
@@ -20,7 +20,6 @@ import logging
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
-import six
from openstackclient.i18n import _
from openstackclient.identity import common
@@ -115,4 +114,4 @@ class ShowAccessRule(command.ShowOne):
access_rule._info.pop('links', None)
- return zip(*sorted(six.iteritems(access_rule._info)))
+ return zip(*sorted(access_rule._info.items()))
diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py
index 029f57a3..4f3e9d0b 100644
--- a/openstackclient/image/v2/image.py
+++ b/openstackclient/image/v2/image.py
@@ -354,7 +354,7 @@ class CreateImage(command.ShowOne):
# Build an attribute dict from the parsed args, only include
# attributes that were actually set on the command line
- kwargs = {}
+ kwargs = {'allow_duplicates': True}
copy_attrs = ('name', 'id',
'container_format', 'disk_format',
'min_disk', 'min_ram', 'tags', 'visibility')
diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py
index e68628b3..47ffbe77 100644
--- a/openstackclient/network/common.py
+++ b/openstackclient/network/common.py
@@ -18,7 +18,6 @@ import logging
import openstack.exceptions
from osc_lib.command import command
from osc_lib import exceptions
-import six
from openstackclient.i18n import _
@@ -54,8 +53,7 @@ def check_missing_extension_if_error(client_manager, attrs):
raise
-@six.add_metaclass(abc.ABCMeta)
-class NetDetectionMixin(object):
+class NetDetectionMixin(metaclass=abc.ABCMeta):
"""Convenience methods for nova-network vs. neutron decisions.
A live environment detects which network type it is running and creates its
@@ -166,8 +164,8 @@ class NetDetectionMixin(object):
pass
-@six.add_metaclass(abc.ABCMeta)
-class NetworkAndComputeCommand(NetDetectionMixin, command.Command):
+class NetworkAndComputeCommand(NetDetectionMixin, command.Command,
+ metaclass=abc.ABCMeta):
"""Network and Compute Command
Command class for commands that support implementation via
@@ -178,8 +176,8 @@ class NetworkAndComputeCommand(NetDetectionMixin, command.Command):
pass
-@six.add_metaclass(abc.ABCMeta)
-class NetworkAndComputeDelete(NetworkAndComputeCommand):
+class NetworkAndComputeDelete(NetworkAndComputeCommand,
+ metaclass=abc.ABCMeta):
"""Network and Compute Delete
Delete class for commands that support implementation via
@@ -222,8 +220,8 @@ class NetworkAndComputeDelete(NetworkAndComputeCommand):
raise exceptions.CommandError(msg)
-@six.add_metaclass(abc.ABCMeta)
-class NetworkAndComputeLister(NetDetectionMixin, command.Lister):
+class NetworkAndComputeLister(NetDetectionMixin, command.Lister,
+ metaclass=abc.ABCMeta):
"""Network and Compute Lister
Lister class for commands that support implementation via
@@ -234,8 +232,8 @@ class NetworkAndComputeLister(NetDetectionMixin, command.Lister):
pass
-@six.add_metaclass(abc.ABCMeta)
-class NetworkAndComputeShowOne(NetDetectionMixin, command.ShowOne):
+class NetworkAndComputeShowOne(NetDetectionMixin, command.ShowOne,
+ metaclass=abc.ABCMeta):
"""Network and Compute ShowOne
ShowOne class for commands that support implementation via
@@ -255,5 +253,5 @@ class NetworkAndComputeShowOne(NetDetectionMixin, command.ShowOne):
except openstack.exceptions.HttpException as exc:
msg = _("Error while executing command: %s") % exc.message
if exc.details:
- msg += ", " + six.text_type(exc.details)
+ msg += ", " + str(exc.details)
raise exceptions.CommandError(msg)
diff --git a/openstackclient/network/v2/network_meter_rule.py b/openstackclient/network/v2/network_meter_rule.py
index 49ff9e1b..1cf0395f 100644
--- a/openstackclient/network/v2/network_meter_rule.py
+++ b/openstackclient/network/v2/network_meter_rule.py
@@ -46,6 +46,10 @@ def _get_attrs(client_manager, parsed_args):
attrs['direction'] = 'egress'
if parsed_args.remote_ip_prefix is not None:
attrs['remote_ip_prefix'] = parsed_args.remote_ip_prefix
+ if parsed_args.source_ip_prefix is not None:
+ attrs['source_ip_prefix'] = parsed_args.source_ip_prefix
+ if parsed_args.destination_ip_prefix is not None:
+ attrs['destination_ip_prefix'] = parsed_args.destination_ip_prefix
if parsed_args.meter is not None:
attrs['metering_label_id'] = parsed_args.meter
if parsed_args.project is not None:
@@ -97,10 +101,22 @@ class CreateMeterRule(command.ShowOne):
parser.add_argument(
'--remote-ip-prefix',
metavar='<remote-ip-prefix>',
- required=True,
+ required=False,
help=_('The remote IP prefix to associate with this rule'),
)
parser.add_argument(
+ '--source-ip-prefix',
+ metavar='<remote-ip-prefix>',
+ required=False,
+ help=_('The source IP prefix to associate with this rule'),
+ )
+ parser.add_argument(
+ '--destination-ip-prefix',
+ metavar='<remote-ip-prefix>',
+ required=False,
+ help=_('The destination IP prefix to associate with this rule'),
+ )
+ parser.add_argument(
'meter',
metavar='<meter>',
help=_('Label to associate with this metering rule (name or ID)'),
@@ -168,12 +184,16 @@ class ListMeterRule(command.Lister):
'excluded',
'direction',
'remote_ip_prefix',
+ 'source_ip_prefix',
+ 'destination_ip_prefix',
)
column_headers = (
'ID',
'Excluded',
'Direction',
'Remote IP Prefix',
+ 'Source IP Prefix',
+ 'Destination IP Prefix',
)
data = client.metering_label_rules()
return (column_headers,
diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py
index 8ea1077a..02ab06c1 100644
--- a/openstackclient/network/v2/port.py
+++ b/openstackclient/network/v2/port.py
@@ -480,12 +480,23 @@ class CreatePort(command.ShowOne):
if parsed_args.qos_policy:
attrs['qos_policy_id'] = client.find_qos_policy(
parsed_args.qos_policy, ignore_missing=False).id
+
+ set_tags_in_post = bool(
+ client.find_extension('tag-ports-during-bulk-creation'))
+ if set_tags_in_post:
+ if parsed_args.no_tag:
+ attrs['tags'] = []
+ if parsed_args.tags:
+ attrs['tags'] = list(set(parsed_args.tags))
+
with common.check_missing_extension_if_error(
self.app.client_manager.network, attrs):
obj = client.create_port(**attrs)
- # tags cannot be set when created, so tags need to be set later.
- _tag.update_tags_for_set(client, obj, parsed_args)
+ if not set_tags_in_post:
+ # tags cannot be set when created, so tags need to be set later.
+ _tag.update_tags_for_set(client, obj, parsed_args)
+
display_columns, columns = _get_columns(obj)
data = utils.get_item_properties(obj, columns, formatters=_formatters)
diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py
index 1fbd97ab..a7703933 100644
--- a/openstackclient/network/v2/security_group_rule.py
+++ b/openstackclient/network/v2/security_group_rule.py
@@ -474,7 +474,7 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister):
action='store_true',
default=False,
help=self.enhance_help_neutron(
- _("List additional fields in output"))
+ _("**Deprecated** This argument is no longer needed"))
)
return parser
@@ -504,15 +504,19 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister):
'Ethertype',
'IP Range',
'Port Range',
+ 'Direction',
+ 'Remote Security Group',
)
- if parsed_args.long:
- column_headers = column_headers + ('Direction',)
- column_headers = column_headers + ('Remote Security Group',)
if parsed_args.group is None:
column_headers = column_headers + ('Security Group',)
return column_headers
def take_action_network(self, client, parsed_args):
+ if parsed_args.long:
+ self.log.warning(_(
+ "The --long option has been deprecated and is no longer needed"
+ ))
+
column_headers = self._get_column_headers(parsed_args)
columns = (
'id',
@@ -520,10 +524,9 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister):
'ether_type',
'remote_ip_prefix',
'port_range',
+ 'direction',
+ 'remote_group_id',
)
- if parsed_args.long:
- columns = columns + ('direction',)
- columns = columns + ('remote_group_id',)
# Get the security group rules using the requested query.
query = {}
diff --git a/openstackclient/shell.py b/openstackclient/shell.py
index 755af24d..bc88e1f1 100644
--- a/openstackclient/shell.py
+++ b/openstackclient/shell.py
@@ -16,13 +16,11 @@
"""Command-line interface to the OpenStack APIs"""
-import locale
import sys
from osc_lib.api import auth
from osc_lib.command import commandmanager
from osc_lib import shell
-import six
import openstackclient
from openstackclient.common import clientmanager
@@ -143,12 +141,6 @@ class OpenStackShell(shell.OpenStackShell):
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
- if six.PY2:
- # Emulate Py3, decode argv into Unicode based on locale so that
- # commands always see arguments as text instead of binary data
- encoding = locale.getpreferredencoding()
- if encoding:
- argv = map(lambda arg: arg.decode(encoding), argv)
return OpenStackShell().run(argv)
diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py
index 6e080e9b..44d9c61f 100644
--- a/openstackclient/tests/functional/compute/v2/test_server.py
+++ b/openstackclient/tests/functional/compute/v2/test_server.py
@@ -16,6 +16,7 @@ import uuid
from tempest.lib import exceptions
+from openstackclient.compute.v2 import server as v2_server
from openstackclient.tests.functional.compute.v2 import common
from openstackclient.tests.functional.volume.v2 import common as volume_common
@@ -230,7 +231,7 @@ class ServerTests(common.ComputeTestCase):
))
# Really, shouldn't this be a list?
self.assertEqual(
- "a='b', c='d'",
+ {'a': 'b', 'c': 'd'},
cmd_output['properties'],
)
@@ -244,7 +245,7 @@ class ServerTests(common.ComputeTestCase):
name
))
self.assertEqual(
- "c='d'",
+ {'c': 'd'},
cmd_output['properties'],
)
@@ -509,6 +510,20 @@ class ServerTests(common.ComputeTestCase):
server['name'],
)
+ # check that image indicates server "booted from volume"
+ self.assertEqual(
+ v2_server.IMAGE_STRING_FOR_BFV,
+ server['image'],
+ )
+ # check server list too
+ servers = json.loads(self.openstack(
+ 'server list -f json'
+ ))
+ self.assertEqual(
+ v2_server.IMAGE_STRING_FOR_BFV,
+ servers[0]['Image']
+ )
+
# check volumes
cmd_output = json.loads(self.openstack(
'volume show -f json ' +
@@ -619,8 +634,8 @@ class ServerTests(common.ComputeTestCase):
server_name
))
volumes_attached = cmd_output['volumes_attached']
- self.assertTrue(volumes_attached.startswith('id='))
- attached_volume_id = volumes_attached.replace('id=', '')
+ self.assertIsNotNone(volumes_attached)
+ attached_volume_id = volumes_attached[0]["id"]
# check the volume that attached on server
cmd_output = json.loads(self.openstack(
@@ -699,8 +714,8 @@ class ServerTests(common.ComputeTestCase):
server_name
))
volumes_attached = cmd_output['volumes_attached']
- self.assertTrue(volumes_attached.startswith('id='))
- attached_volume_id = volumes_attached.replace('id=', '')
+ self.assertIsNotNone(volumes_attached)
+ attached_volume_id = volumes_attached[0]["id"]
# check the volume that attached on server
cmd_output = json.loads(self.openstack(
@@ -773,19 +788,21 @@ class ServerTests(common.ComputeTestCase):
server_name
))
volumes_attached = cmd_output['volumes_attached']
- self.assertTrue(volumes_attached.startswith('id='))
- attached_volume_id = volumes_attached.replace('id=', '')
- # Don't leak the volume when the test exits.
- self.addCleanup(self.openstack, 'volume delete ' + attached_volume_id)
+ self.assertIsNotNone(volumes_attached)
+ attached_volume_id = volumes_attached[0]["id"]
+ for vol in volumes_attached:
+ self.assertIsNotNone(vol['id'])
+ # Don't leak the volume when the test exits.
+ self.addCleanup(self.openstack, 'volume delete ' + vol['id'])
# Since the server is volume-backed the GET /servers/{server_id}
- # response will have image=''.
- self.assertEqual('', cmd_output['image'])
+ # response will have image='N/A (booted from volume)'.
+ self.assertEqual(v2_server.IMAGE_STRING_FOR_BFV, cmd_output['image'])
# check the volume that attached on server
cmd_output = json.loads(self.openstack(
'volume show -f json ' +
- attached_volume_id
+ volumes_attached[0]["id"]
))
# The volume size should be what we specified on the command line.
self.assertEqual(1, int(cmd_output['size']))
@@ -879,14 +896,21 @@ class ServerTests(common.ComputeTestCase):
self.assertIsNotNone(server['id'])
self.assertEqual(server_name, server['name'])
- self.assertIn(str(security_group1['id']), server['security_groups'])
- self.assertIn(str(security_group2['id']), server['security_groups'])
+ sec_grp = ""
+ for sec in server['security_groups']:
+ sec_grp += sec['name']
+ self.assertIn(str(security_group1['id']), sec_grp)
+ self.assertIn(str(security_group2['id']), sec_grp)
self.wait_for_status(server_name, 'ACTIVE')
server = json.loads(self.openstack(
'server show -f json ' + server_name
))
- self.assertIn(sg_name1, server['security_groups'])
- self.assertIn(sg_name2, server['security_groups'])
+ # check if security group exists in list
+ sec_grp = ""
+ for sec in server['security_groups']:
+ sec_grp += sec['name']
+ self.assertIn(sg_name1, sec_grp)
+ self.assertIn(sg_name2, sec_grp)
def test_server_create_with_empty_network_option_latest(self):
"""Test server create with empty network option in nova 2.latest."""
diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py
index 6e12f735..3a06d271 100644
--- a/openstackclient/tests/unit/compute/v2/fakes.py
+++ b/openstackclient/tests/unit/compute/v2/fakes.py
@@ -14,6 +14,7 @@
#
import copy
+import random
from unittest import mock
import uuid
@@ -195,9 +196,15 @@ class FakeComputev2Client(object):
self.server_groups = mock.Mock()
self.server_groups.resource_class = fakes.FakeResource(None, {})
+ self.server_migrations = mock.Mock()
+ self.server_migrations.resource_class = fakes.FakeResource(None, {})
+
self.instance_action = mock.Mock()
self.instance_action.resource_class = fakes.FakeResource(None, {})
+ self.migrations = mock.Mock()
+ self.migrations.resource_class = fakes.FakeResource(None, {})
+
self.auth_token = kwargs['token']
self.management_url = kwargs['endpoint']
@@ -877,6 +884,7 @@ class FakeKeypair(object):
# Set default attributes.
keypair_info = {
'name': 'keypair-name-' + uuid.uuid4().hex,
+ 'type': 'ssh',
'fingerprint': 'dummy',
'public_key': 'dummy',
'user_id': 'user'
@@ -1244,7 +1252,7 @@ class FakeServerGroup(object):
"""Fake one server group"""
@staticmethod
- def create_one_server_group(attrs=None):
+ def _create_one_server_group(attrs=None):
"""Create a fake server group
:param Dictionary attrs:
@@ -1261,7 +1269,6 @@ class FakeServerGroup(object):
'members': [],
'metadata': {},
'name': 'server-group-name-' + uuid.uuid4().hex,
- 'policies': [],
'project_id': 'server-group-project-id-' + uuid.uuid4().hex,
'user_id': 'server-group-user-id-' + uuid.uuid4().hex,
}
@@ -1274,6 +1281,38 @@ class FakeServerGroup(object):
loaded=True)
return server_group
+ @staticmethod
+ def create_one_server_group(attrs=None):
+ """Create a fake server group
+
+ :param Dictionary attrs:
+ A dictionary with all attributes
+ :return:
+ A FakeResource object, with id and other attributes
+ """
+ if attrs is None:
+ attrs = {}
+ attrs.setdefault('policies', ['policy1', 'policy2'])
+ return FakeServerGroup._create_one_server_group(attrs)
+
+
+class FakeServerGroupV264(object):
+ """Fake one server group fo API >= 2.64"""
+
+ @staticmethod
+ def create_one_server_group(attrs=None):
+ """Create a fake server group
+
+ :param Dictionary attrs:
+ A dictionary with all attributes
+ :return:
+ A FakeResource object, with id and other attributes
+ """
+ if attrs is None:
+ attrs = {}
+ attrs.setdefault('policy', 'policy1')
+ return FakeServerGroup._create_one_server_group(attrs)
+
class FakeUsage(object):
"""Fake one or more usage."""
@@ -1539,3 +1578,89 @@ class FakeRateLimit(object):
self.remain = remain
self.unit = unit
self.next_available = next_available
+
+
+class FakeServerMigration(object):
+ """Fake one or more server migrations."""
+
+ @staticmethod
+ def create_one_server_migration(attrs=None, methods=None):
+ """Create a fake server migration.
+
+ :param Dictionary attrs:
+ A dictionary with all attributes
+ :param Dictionary methods:
+ A dictionary with all methods
+ :return:
+ A FakeResource object, with id, type, and so on
+ """
+ attrs = attrs or {}
+ methods = methods or {}
+
+ # Set default attributes.
+ migration_info = {
+ "dest_host": "10.0.2.15",
+ "status": "migrating",
+ "type": "migration",
+ "updated_at": "2017-01-31T08:03:25.000000",
+ "created_at": "2017-01-31T08:03:21.000000",
+ "dest_compute": "compute-" + uuid.uuid4().hex,
+ "id": random.randint(1, 999),
+ "source_node": "node-" + uuid.uuid4().hex,
+ "server": uuid.uuid4().hex,
+ "dest_node": "node-" + uuid.uuid4().hex,
+ "source_compute": "compute-" + uuid.uuid4().hex,
+ "uuid": uuid.uuid4().hex,
+ "old_instance_type_id": uuid.uuid4().hex,
+ "new_instance_type_id": uuid.uuid4().hex,
+ "project": uuid.uuid4().hex,
+ "user": uuid.uuid4().hex
+ }
+
+ # Overwrite default attributes.
+ migration_info.update(attrs)
+
+ migration = fakes.FakeResource(info=copy.deepcopy(migration_info),
+ methods=methods,
+ loaded=True)
+ return migration
+
+ @staticmethod
+ def create_server_migrations(attrs=None, methods=None, count=2):
+ """Create multiple fake server migrations.
+
+ :param Dictionary attrs:
+ A dictionary with all attributes
+ :param Dictionary methods:
+ A dictionary with all methods
+ :param int count:
+ The number of server migrations to fake
+ :return:
+ A list of FakeResource objects faking the server migrations
+ """
+ migrations = []
+ for i in range(0, count):
+ migrations.append(
+ FakeServerMigration.create_one_server_migration(
+ attrs, methods))
+
+ return migrations
+
+ @staticmethod
+ def get_server_migrations(migrations=None, count=2):
+ """Get an iterable MagicMock object with a list of faked migrations.
+
+ If server migrations list is provided, then initialize the Mock object
+ with the list. Otherwise create one.
+
+ :param List migrations:
+ A list of FakeResource objects faking server migrations
+ :param int count:
+ The number of server migrations to fake
+ :return:
+ An iterable Mock object with side_effect set to a list of faked
+ server migrations
+ """
+ if migrations is None:
+ migrations = FakeServerMigration.create_server_migrations(count)
+ return mock.Mock(side_effect=migrations)
diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py
index 1f3f56f9..ca3bfe7c 100644
--- a/openstackclient/tests/unit/compute/v2/test_keypair.py
+++ b/openstackclient/tests/unit/compute/v2/test_keypair.py
@@ -17,6 +17,7 @@ from unittest import mock
from unittest.mock import call
import uuid
+from novaclient import api_versions
from osc_lib import exceptions
from osc_lib import utils
@@ -45,11 +46,13 @@ class TestKeypairCreate(TestKeypair):
self.columns = (
'fingerprint',
'name',
+ 'type',
'user_id'
)
self.data = (
self.keypair.fingerprint,
self.keypair.name,
+ self.keypair.type,
self.keypair.user_id
)
@@ -71,7 +74,7 @@ class TestKeypairCreate(TestKeypair):
columns, data = self.cmd.take_action(parsed_args)
self.keypairs_mock.create.assert_called_with(
- self.keypair.name,
+ name=self.keypair.name,
public_key=None
)
@@ -87,6 +90,7 @@ class TestKeypairCreate(TestKeypair):
self.data = (
self.keypair.fingerprint,
self.keypair.name,
+ self.keypair.type,
self.keypair.user_id
)
@@ -96,7 +100,7 @@ class TestKeypairCreate(TestKeypair):
]
verifylist = [
('public_key', self.keypair.public_key),
- ('name', self.keypair.name)
+ ('name', self.keypair.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -109,8 +113,8 @@ class TestKeypairCreate(TestKeypair):
columns, data = self.cmd.take_action(parsed_args)
self.keypairs_mock.create.assert_called_with(
- self.keypair.name,
- public_key=self.keypair.public_key
+ name=self.keypair.name,
+ public_key=self.keypair.public_key,
)
self.assertEqual(self.columns, columns)
@@ -124,7 +128,7 @@ class TestKeypairCreate(TestKeypair):
]
verifylist = [
('private_key', tmp_pk_file),
- ('name', self.keypair.name)
+ ('name', self.keypair.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -136,8 +140,8 @@ class TestKeypairCreate(TestKeypair):
columns, data = self.cmd.take_action(parsed_args)
self.keypairs_mock.create.assert_called_with(
- self.keypair.name,
- public_key=None
+ name=self.keypair.name,
+ public_key=None,
)
mock_open.assert_called_once_with(tmp_pk_file, 'w+')
@@ -146,6 +150,79 @@ class TestKeypairCreate(TestKeypair):
self.assertEqual(self.columns, columns)
self.assertEqual(self.data, data)
+ def test_keypair_create_with_key_type(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.2')
+
+ for key_type in ['x509', 'ssh']:
+ self.keypair = compute_fakes.FakeKeypair.create_one_keypair(
+ no_pri=True)
+ self.keypairs_mock.create.return_value = self.keypair
+
+ self.data = (
+ self.keypair.fingerprint,
+ self.keypair.name,
+ self.keypair.type,
+ self.keypair.user_id,
+ )
+ arglist = [
+ '--public-key', self.keypair.public_key,
+ self.keypair.name,
+ '--type', key_type,
+ ]
+ verifylist = [
+ ('public_key', self.keypair.public_key),
+ ('name', self.keypair.name),
+ ('type', key_type),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ with mock.patch('io.open') as mock_open:
+ mock_open.return_value = mock.MagicMock()
+ m_file = mock_open.return_value.__enter__.return_value
+ m_file.read.return_value = 'dummy'
+ columns, data = self.cmd.take_action(parsed_args)
+
+ self.keypairs_mock.create.assert_called_with(
+ name=self.keypair.name,
+ public_key=self.keypair.public_key,
+ key_type=key_type,
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.data, data)
+
+ def test_keypair_create_with_key_type_pre_v22(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.1')
+
+ for key_type in ['x509', 'ssh']:
+ arglist = [
+ '--public-key', self.keypair.public_key,
+ self.keypair.name,
+ '--type', 'ssh',
+ ]
+ verifylist = [
+ ('public_key', self.keypair.public_key),
+ ('name', self.keypair.name),
+ ('type', 'ssh'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ with mock.patch('io.open') as mock_open:
+ mock_open.return_value = mock.MagicMock()
+ m_file = mock_open.return_value.__enter__.return_value
+ m_file.read.return_value = 'dummy'
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+
+ self.assertIn(
+ '--os-compute-api-version 2.2 or greater is required',
+ str(ex))
+
class TestKeypairDelete(TestKeypair):
@@ -227,16 +304,6 @@ class TestKeypairList(TestKeypair):
# Return value of self.keypairs_mock.list().
keypairs = compute_fakes.FakeKeypair.create_keypairs(count=1)
- columns = (
- "Name",
- "Fingerprint"
- )
-
- data = ((
- keypairs[0].name,
- keypairs[0].fingerprint
- ), )
-
def setUp(self):
super(TestKeypairList, self).setUp()
@@ -247,8 +314,7 @@ class TestKeypairList(TestKeypair):
def test_keypair_list_no_options(self):
arglist = []
- verifylist = [
- ]
+ verifylist = []
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -261,8 +327,39 @@ class TestKeypairList(TestKeypair):
self.keypairs_mock.list.assert_called_with()
- self.assertEqual(self.columns, columns)
- self.assertEqual(tuple(self.data), tuple(data))
+ self.assertEqual(('Name', 'Fingerprint'), columns)
+ self.assertEqual(
+ ((self.keypairs[0].name, self.keypairs[0].fingerprint), ),
+ tuple(data)
+ )
+
+ def test_keypair_list_v22(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.2')
+
+ arglist = []
+ verifylist = []
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class Lister in cliff, abstract method take_action()
+ # returns a tuple containing the column names and an iterable
+ # containing the data to be listed.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+
+ self.keypairs_mock.list.assert_called_with()
+
+ self.assertEqual(('Name', 'Fingerprint', 'Type'), columns)
+ self.assertEqual(
+ ((
+ self.keypairs[0].name,
+ self.keypairs[0].fingerprint,
+ self.keypairs[0].type,
+ ), ),
+ tuple(data)
+ )
class TestKeypairShow(TestKeypair):
@@ -279,16 +376,18 @@ class TestKeypairShow(TestKeypair):
self.columns = (
"fingerprint",
"name",
+ "type",
"user_id"
)
self.data = (
self.keypair.fingerprint,
self.keypair.name,
+ self.keypair.type,
self.keypair.user_id
)
- def test_show_no_options(self):
+ def test_keypair_show_no_options(self):
arglist = []
verifylist = []
@@ -306,6 +405,7 @@ class TestKeypairShow(TestKeypair):
self.data = (
self.keypair.fingerprint,
self.keypair.name,
+ self.keypair.type,
self.keypair.user_id
)
diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py
index 7e4c71c5..441558fd 100644
--- a/openstackclient/tests/unit/compute/v2/test_server.py
+++ b/openstackclient/tests/unit/compute/v2/test_server.py
@@ -19,12 +19,11 @@ import getpass
from unittest import mock
from unittest.mock import call
+import iso8601
from novaclient import api_versions
from openstack import exceptions as sdk_exceptions
from osc_lib import exceptions
from osc_lib import utils as common_utils
-from oslo_utils import timeutils
-import six
from openstackclient.compute.v2 import server
from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes
@@ -43,6 +42,11 @@ class TestServer(compute_fakes.TestComputev2):
self.servers_mock = self.app.client_manager.compute.servers
self.servers_mock.reset_mock()
+ # Get a shortcut to the compute client ServerMigrationsManager Mock
+ self.server_migrations_mock = \
+ self.app.client_manager.compute.server_migrations
+ self.server_migrations_mock.reset_mock()
+
# Get a shortcut to the compute client volumeManager Mock
self.servers_volumes_mock = self.app.client_manager.compute.volumes
self.servers_volumes_mock.reset_mock()
@@ -178,6 +182,72 @@ class TestServerAddFixedIP(TestServer):
extralist = ['--fixed-ip-address', '5.6.7.8']
self._test_server_add_fixed_ip(extralist, '5.6.7.8')
+ def test_server_add_fixed_ip_with_tag(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.49')
+
+ servers = self.setup_servers_mock(count=1)
+ network = compute_fakes.FakeNetwork.create_one_network()
+ with mock.patch(
+ 'openstackclient.api.compute_v2.APIv2.network_find'
+ ) as net_mock:
+ net_mock.return_value = network
+
+ arglist = [
+ servers[0].id,
+ network['id'],
+ '--fixed-ip-address', '5.6.7.8',
+ '--tag', 'tag1',
+ ]
+ verifylist = [
+ ('server', servers[0].id),
+ ('network', network['id']),
+ ('fixed_ip_address', '5.6.7.8'),
+ ('tag', 'tag1'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ result = self.cmd.take_action(parsed_args)
+
+ servers[0].interface_attach.assert_called_once_with(
+ port_id=None,
+ net_id=network['id'],
+ fixed_ip='5.6.7.8',
+ tag='tag1'
+ )
+ self.assertIsNone(result)
+
+ def test_server_add_fixed_ip_with_tag_pre_v249(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.48')
+
+ servers = self.setup_servers_mock(count=1)
+ network = compute_fakes.FakeNetwork.create_one_network()
+ with mock.patch(
+ 'openstackclient.api.compute_v2.APIv2.network_find'
+ ) as net_mock:
+ net_mock.return_value = network
+
+ arglist = [
+ servers[0].id,
+ network['id'],
+ '--fixed-ip-address', '5.6.7.8',
+ '--tag', 'tag1',
+ ]
+ verifylist = [
+ ('server', servers[0].id),
+ ('network', network['id']),
+ ('fixed_ip_address', '5.6.7.8'),
+ ('tag', 'tag1'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.49 or greater is required',
+ str(ex))
+
@mock.patch(
'openstackclient.api.compute_v2.APIv2.floating_ip_add'
@@ -250,7 +320,7 @@ class TestServerAddFloatingIPNetwork(
# Get the command object to test
self.cmd = server.AddFloatingIP(self.app, self.namespace)
- def test_server_add_floating_ip_default(self):
+ def test_server_add_floating_ip(self):
_server = compute_fakes.FakeServer.create_one_server()
self.servers_mock.get.return_value = _server
_port = network_fakes.FakePort.create_one_port()
@@ -285,8 +355,41 @@ class TestServerAddFloatingIPNetwork(
**attrs
)
- def test_server_add_floating_ip_default_no_external_gateway(self,
- success=False):
+ def test_server_add_floating_ip_no_ports(self):
+ server = compute_fakes.FakeServer.create_one_server()
+ floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip()
+
+ self.servers_mock.get.return_value = server
+ self.network.find_ip = mock.Mock(return_value=floating_ip)
+ self.network.ports = mock.Mock(return_value=[])
+
+ arglist = [
+ server.id,
+ floating_ip['floating_ip_address'],
+ ]
+ verifylist = [
+ ('server', server.id),
+ ('ip_address', floating_ip['floating_ip_address']),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ 'No attached ports found to associate floating IP with',
+ str(ex))
+
+ self.network.find_ip.assert_called_once_with(
+ floating_ip['floating_ip_address'],
+ ignore_missing=False,
+ )
+ self.network.ports.assert_called_once_with(
+ device_id=server.id,
+ )
+
+ def test_server_add_floating_ip_no_external_gateway(self, success=False):
_server = compute_fakes.FakeServer.create_one_server()
self.servers_mock.get.return_value = _server
_port = network_fakes.FakePort.create_one_port()
@@ -339,11 +442,10 @@ class TestServerAddFloatingIPNetwork(
**attrs
)
- def test_server_add_floating_ip_default_one_external_gateway(self):
- self.test_server_add_floating_ip_default_no_external_gateway(
- success=True)
+ def test_server_add_floating_ip_one_external_gateway(self):
+ self.test_server_add_floating_ip_no_external_gateway(success=True)
- def test_server_add_floating_ip_fixed(self):
+ def test_server_add_floating_ip_with_fixed_ip(self):
_server = compute_fakes.FakeServer.create_one_server()
self.servers_mock.get.return_value = _server
_port = network_fakes.FakePort.create_one_port()
@@ -385,7 +487,7 @@ class TestServerAddFloatingIPNetwork(
**attrs
)
- def test_server_add_floating_ip_fixed_no_port_found(self):
+ def test_server_add_floating_ip_with_fixed_ip_no_port_found(self):
_server = compute_fakes.FakeServer.create_one_server()
self.servers_mock.get.return_value = _server
_port = network_fakes.FakePort.create_one_port()
@@ -466,6 +568,59 @@ class TestServerAddPort(TestServer):
self._test_server_add_port('fake-port')
self.find_port.assert_not_called()
+ def test_server_add_port_with_tag(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.49')
+
+ servers = self.setup_servers_mock(count=1)
+ self.find_port.return_value.id = 'fake-port'
+ arglist = [
+ servers[0].id,
+ 'fake-port',
+ '--tag', 'tag1',
+ ]
+ verifylist = [
+ ('server', servers[0].id),
+ ('port', 'fake-port'),
+ ('tag', 'tag1'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.assertIsNone(result)
+
+ servers[0].interface_attach.assert_called_once_with(
+ port_id='fake-port',
+ net_id=None,
+ fixed_ip=None,
+ tag='tag1')
+
+ def test_server_add_port_with_tag_pre_v249(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.48')
+
+ servers = self.setup_servers_mock(count=1)
+ self.find_port.return_value.id = 'fake-port'
+ arglist = [
+ servers[0].id,
+ 'fake-port',
+ '--tag', 'tag1',
+ ]
+ verifylist = [
+ ('server', servers[0].id),
+ ('port', 'fake-port'),
+ ('tag', 'tag1'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.49 or greater is required',
+ str(ex))
+
class TestServerVolume(TestServer):
@@ -503,8 +658,57 @@ class TestServerVolume(TestServer):
servers[0].id, self.volume.id, device='/dev/sdb')
self.assertIsNone(result)
+ def test_server_add_volume_with_tag(self):
+ # requires API 2.49 or later
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.49')
+
+ servers = self.setup_servers_mock(count=1)
+ arglist = [
+ '--device', '/dev/sdb',
+ '--tag', 'foo',
+ servers[0].id,
+ self.volume.id,
+ ]
+ verifylist = [
+ ('server', servers[0].id),
+ ('volume', self.volume.id),
+ ('device', '/dev/sdb'),
+ ('tag', 'foo'),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-class TestServerVolumeV279(TestServerVolume):
+ result = self.cmd.take_action(parsed_args)
+
+ self.servers_volumes_mock.create_server_volume.assert_called_once_with(
+ servers[0].id, self.volume.id, device='/dev/sdb', tag='foo')
+ self.assertIsNone(result)
+
+ def test_server_add_volume_with_tag_pre_v249(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.48')
+
+ servers = self.setup_servers_mock(count=1)
+ arglist = [
+ servers[0].id,
+ self.volume.id,
+ '--tag', 'foo',
+ ]
+ verifylist = [
+ ('server', servers[0].id),
+ ('volume', self.volume.id),
+ ('tag', 'foo'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.49 or greater is required',
+ str(ex))
def test_server_add_volume_with_enable_delete_on_termination(self):
self.app.client_manager.compute.api_version = api_versions.APIVersion(
@@ -561,7 +765,8 @@ class TestServerVolumeV279(TestServerVolume):
self.assertIsNone(result)
def test_server_add_volume_with_enable_delete_on_termination_pre_v279(
- self):
+ self,
+ ):
self.app.client_manager.compute.api_version = api_versions.APIVersion(
'2.78')
@@ -585,7 +790,8 @@ class TestServerVolumeV279(TestServerVolume):
str(ex))
def test_server_add_volume_with_disable_delete_on_termination_pre_v279(
- self):
+ self,
+ ):
self.app.client_manager.compute.api_version = api_versions.APIVersion(
'2.78')
@@ -609,7 +815,8 @@ class TestServerVolumeV279(TestServerVolume):
str(ex))
def test_server_add_volume_with_disable_and_enable_delete_on_termination(
- self):
+ self,
+ ):
self.app.client_manager.compute.api_version = api_versions.APIVersion(
'2.79')
@@ -682,6 +889,62 @@ class TestServerAddNetwork(TestServer):
self._test_server_add_network('fake-network')
self.find_network.assert_not_called()
+ def test_server_add_network_with_tag(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.49')
+
+ servers = self.setup_servers_mock(count=1)
+ self.find_network.return_value.id = 'fake-network'
+
+ arglist = [
+ servers[0].id,
+ 'fake-network',
+ '--tag', 'tag1',
+ ]
+ verifylist = [
+ ('server', servers[0].id),
+ ('network', 'fake-network'),
+ ('tag', 'tag1'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.assertIsNone(result)
+
+ servers[0].interface_attach.assert_called_once_with(
+ port_id=None,
+ net_id='fake-network',
+ fixed_ip=None,
+ tag='tag1'
+ )
+
+ def test_server_add_network_with_tag_pre_v249(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.48')
+
+ servers = self.setup_servers_mock(count=1)
+ self.find_network.return_value.id = 'fake-network'
+
+ arglist = [
+ servers[0].id,
+ 'fake-network',
+ '--tag', 'tag1',
+ ]
+ verifylist = [
+ ('server', servers[0].id),
+ ('network', 'fake-network'),
+ ('tag', 'tag1'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.49 or greater is required',
+ str(ex))
+
@mock.patch(
'openstackclient.api.compute_v2.APIv2.security_group_find'
@@ -832,6 +1095,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics=[],
scheduler_hints={},
@@ -857,6 +1121,8 @@ class TestServerCreate(TestServer):
'--key-name', 'keyname',
'--property', 'Beta=b',
'--security-group', 'securitygroup',
+ '--use-config-drive',
+ '--password', 'passw0rd',
'--hint', 'a=b',
'--hint', 'a=c',
self.new_server.name,
@@ -868,7 +1134,8 @@ class TestServerCreate(TestServer):
('property', {'Beta': 'b'}),
('security_group', ['securitygroup']),
('hint', {'a': ['b', 'c']}),
- ('config_drive', False),
+ ('config_drive', True),
+ ('password', 'passw0rd'),
('server_name', self.new_server.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -897,10 +1164,11 @@ class TestServerCreate(TestServer):
userdata=None,
key_name='keyname',
availability_zone=None,
+ admin_pass='passw0rd',
block_device_mapping_v2=[],
nics=[],
scheduler_hints={'a': ['b', 'c']},
- config_drive=None,
+ config_drive=True,
)
# ServerManager.create(name, image, flavor, **kwargs)
self.servers_mock.create.assert_called_with(
@@ -983,6 +1251,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name='keyname',
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics=[],
scheduler_hints={},
@@ -1069,6 +1338,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics=[{'net-id': 'net1_uuid',
'v4-fixed-ip': '',
@@ -1133,6 +1403,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics='auto',
scheduler_hints={},
@@ -1182,6 +1453,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics='auto',
scheduler_hints={},
@@ -1227,6 +1499,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics='none',
scheduler_hints={},
@@ -1392,6 +1665,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics=[],
scheduler_hints={},
@@ -1442,6 +1716,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics=[],
scheduler_hints={},
@@ -1497,6 +1772,7 @@ class TestServerCreate(TestServer):
userdata=mock_file,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics=[],
scheduler_hints={},
@@ -1543,6 +1819,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[{
'device_name': 'vda',
'uuid': self.volume.id,
@@ -1595,6 +1872,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[{
'device_name': 'vdf',
'uuid': self.volume.id,
@@ -1646,6 +1924,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[{
'device_name': 'vdf',
'uuid': self.volume.id,
@@ -1699,6 +1978,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[{
'device_name': 'vde',
'uuid': self.volume.id,
@@ -1754,6 +2034,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[{
'device_name': 'vds',
'uuid': self.snapshot.id,
@@ -1809,6 +2090,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[
{
'device_name': 'vdb',
@@ -1907,7 +2189,7 @@ class TestServerCreate(TestServer):
self.cmd.take_action, parsed_args)
# Assert it is the error we expect.
self.assertIn('--volume is not allowed with --boot-from-volume',
- six.text_type(ex))
+ str(ex))
def test_server_create_image_property(self):
arglist = [
@@ -1945,6 +2227,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics='none',
meta=None,
@@ -2000,6 +2283,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics='none',
meta=None,
@@ -2048,6 +2332,66 @@ class TestServerCreate(TestServer):
self.cmd.take_action,
parsed_args)
+ def test_server_create_image_property_with_image_list(self):
+ arglist = [
+ '--image-property',
+ 'owner_specified.openstack.object=image/cirros',
+ '--flavor', 'flavor1',
+ '--nic', 'none',
+ self.new_server.name,
+ ]
+
+ verifylist = [
+ ('image_property',
+ {'owner_specified.openstack.object': 'image/cirros'}),
+ ('flavor', 'flavor1'),
+ ('nic', ['none']),
+ ('server_name', self.new_server.name),
+ ]
+ # create a image_info as the side_effect of the fake image_list()
+ image_info = {
+ 'properties': {
+ 'owner_specified.openstack.object': 'image/cirros'
+ }
+ }
+
+ target_image = image_fakes.FakeImage.create_one_image(image_info)
+ another_image = image_fakes.FakeImage.create_one_image({})
+ self.images_mock.return_value = [target_image, another_image]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = dict(
+ files={},
+ reservation_id=None,
+ min_count=1,
+ max_count=1,
+ security_groups=[],
+ userdata=None,
+ key_name=None,
+ availability_zone=None,
+ admin_pass=None,
+ block_device_mapping_v2=[],
+ nics='none',
+ meta=None,
+ scheduler_hints={},
+ config_drive=None,
+ )
+
+ # ServerManager.create(name, image, flavor, **kwargs)
+ self.servers_mock.create.assert_called_with(
+ self.new_server.name,
+ target_image,
+ self.flavor,
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist(), data)
+
def test_server_create_invalid_hint(self):
# Not a key-value pair
arglist = [
@@ -2110,6 +2454,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics='auto',
scheduler_hints={},
@@ -2155,6 +2500,87 @@ class TestServerCreate(TestServer):
self.assertRaises(exceptions.CommandError, self.cmd.take_action,
parsed_args)
+ def test_server_create_with_tag(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.52')
+
+ arglist = [
+ '--image', 'image1',
+ '--flavor', 'flavor1',
+ '--tag', 'tag1',
+ '--tag', 'tag2',
+ self.new_server.name,
+ ]
+ verifylist = [
+ ('image', 'image1'),
+ ('flavor', 'flavor1'),
+ ('tags', ['tag1', 'tag2']),
+ ('config_drive', False),
+ ('server_name', self.new_server.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'meta': None,
+ 'files': {},
+ 'reservation_id': None,
+ 'min_count': 1,
+ 'max_count': 1,
+ 'security_groups': [],
+ 'userdata': None,
+ 'key_name': None,
+ 'availability_zone': None,
+ 'block_device_mapping_v2': [],
+ 'admin_pass': None,
+ 'nics': 'auto',
+ 'scheduler_hints': {},
+ 'config_drive': None,
+ 'tags': ['tag1', 'tag2'],
+ }
+ # ServerManager.create(name, image, flavor, **kwargs)
+ self.servers_mock.create.assert_called_with(
+ self.new_server.name,
+ self.image,
+ self.flavor,
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist(), data)
+ self.assertFalse(self.images_mock.called)
+ self.assertFalse(self.flavors_mock.called)
+
+ def test_server_create_with_tag_pre_v252(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.51')
+
+ arglist = [
+ '--image', 'image1',
+ '--flavor', 'flavor1',
+ '--tag', 'tag1',
+ '--tag', 'tag2',
+ self.new_server.name,
+ ]
+ verifylist = [
+ ('image', 'image1'),
+ ('flavor', 'flavor1'),
+ ('tags', ['tag1', 'tag2']),
+ ('config_drive', False),
+ ('server_name', self.new_server.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.52 or greater is required',
+ str(ex))
+
def test_server_create_with_host_v274(self):
# Explicit host is supported for nova api version 2.74 or above
@@ -2194,6 +2620,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics='auto',
scheduler_hints={},
@@ -2279,6 +2706,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics='auto',
scheduler_hints={},
@@ -2366,6 +2794,7 @@ class TestServerCreate(TestServer):
userdata=None,
key_name=None,
availability_zone=None,
+ admin_pass=None,
block_device_mapping_v2=[],
nics='auto',
scheduler_hints={},
@@ -2609,7 +3038,7 @@ class TestServerList(TestServer):
s.status,
server._format_servers_list_networks(s.networks),
# Image will be an empty string if boot-from-volume
- self.image.name if s.image else s.image,
+ self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
self.flavor.name,
))
self.data_long.append((
@@ -2622,8 +3051,8 @@ class TestServerList(TestServer):
),
server._format_servers_list_networks(s.networks),
# Image will be an empty string if boot-from-volume
- self.image.name if s.image else s.image,
- s.image['id'] if s.image else s.image,
+ self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
+ s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
self.flavor.name,
s.flavor['id'],
getattr(s, 'OS-EXT-AZ:availability_zone'),
@@ -2636,7 +3065,7 @@ class TestServerList(TestServer):
s.status,
server._format_servers_list_networks(s.networks),
# Image will be an empty string if boot-from-volume
- s.image['id'] if s.image else s.image,
+ s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
s.flavor['id']
))
@@ -2886,7 +3315,7 @@ class TestServerList(TestServer):
self.assertEqual(self.columns, columns)
self.assertEqual(tuple(self.data), tuple(data))
- @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError)
+ @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError)
def test_server_list_with_invalid_changes_since(self, mock_parse_isotime):
arglist = [
@@ -2924,12 +3353,13 @@ class TestServerList(TestServer):
self.search_opts['changes-before'] = '2016-03-05T06:27:59Z'
self.search_opts['deleted'] = True
+
self.servers_mock.list.assert_called_with(**self.kwargs)
self.assertEqual(self.columns, columns)
self.assertEqual(tuple(self.data), tuple(data))
- @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError)
+ @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError)
def test_server_list_v266_with_invalid_changes_before(
self, mock_parse_isotime):
self.app.client_manager.compute.api_version = (
@@ -3016,6 +3446,92 @@ class TestServerList(TestServer):
'UNKNOWN', '', '', '')
self.assertEqual(expected_row, partial_server)
+ def test_server_list_with_tag(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.26')
+
+ arglist = [
+ '--tag', 'tag1',
+ '--tag', 'tag2',
+ ]
+ verifylist = [
+ ('tags', ['tag1', 'tag2']),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ self.search_opts['tags'] = ['tag1', 'tag2']
+
+ self.servers_mock.list.assert_called_with(**self.kwargs)
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+
+ def test_server_list_with_tag_pre_v225(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.25')
+
+ arglist = [
+ '--tag', 'tag1',
+ '--tag', 'tag2',
+ ]
+ verifylist = [
+ ('tags', ['tag1', 'tag2']),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.26 or greater is required',
+ str(ex))
+
+ def test_server_list_with_not_tag(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.26')
+
+ arglist = [
+ '--not-tag', 'tag1',
+ '--not-tag', 'tag2',
+ ]
+ verifylist = [
+ ('not_tags', ['tag1', 'tag2']),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ self.search_opts['not-tags'] = ['tag1', 'tag2']
+
+ self.servers_mock.list.assert_called_with(**self.kwargs)
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+
+ def test_server_list_with_not_tag_pre_v226(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.25')
+
+ arglist = [
+ '--not-tag', 'tag1',
+ '--not-tag', 'tag2',
+ ]
+ verifylist = [
+ ('not_tags', ['tag1', 'tag2']),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.26 or greater is required',
+ str(ex))
+
class TestServerLock(TestServer):
@@ -3229,7 +3745,7 @@ class TestServerMigrate(TestServer):
# Make sure it's the error we expect.
self.assertIn('--os-compute-api-version 2.56 or greater is required '
'to use --host without --live-migration.',
- six.text_type(ex))
+ str(ex))
self.servers_mock.get.assert_called_with(self.server.id)
self.assertNotCalled(self.servers_mock.live_migrate)
@@ -3264,7 +3780,7 @@ class TestServerMigrate(TestServer):
# A warning should have been logged for using --live.
mock_warning.assert_called_once()
self.assertIn('The --live option has been deprecated.',
- six.text_type(mock_warning.call_args[0][0]))
+ str(mock_warning.call_args[0][0]))
def test_server_live_migrate_host_pre_2_30(self):
# Tests that the --host option is not supported for --live-migration
@@ -3287,7 +3803,7 @@ class TestServerMigrate(TestServer):
# Make sure it's the error we expect.
self.assertIn('--os-compute-api-version 2.30 or greater is required '
- 'when using --host', six.text_type(ex))
+ 'when using --host', str(ex))
self.servers_mock.get.assert_called_with(self.server.id)
self.assertNotCalled(self.servers_mock.live_migrate)
@@ -3377,7 +3893,7 @@ class TestServerMigrate(TestServer):
# A warning should have been logged for using --live.
mock_warning.assert_called_once()
self.assertIn('The --live option has been deprecated.',
- six.text_type(mock_warning.call_args[0][0]))
+ str(mock_warning.call_args[0][0]))
def test_server_live_migrate_live_and_host_mutex(self):
# Tests specifying both the --live and --host options which are in a
@@ -3523,6 +4039,592 @@ class TestServerMigrate(TestServer):
self.assertNotCalled(self.servers_mock.live_migrate)
+class TestServerMigration(TestServer):
+
+ def setUp(self):
+ super(TestServerMigration, self).setUp()
+
+ # Get a shortcut to the compute client ServerManager Mock
+ self.servers_mock = self.app.client_manager.compute.servers
+ self.servers_mock.reset_mock()
+
+ self.migrations_mock = (
+ self.app.client_manager.compute.migrations)
+ self.migrations_mock.reset_mock()
+
+ self.server = self.setup_servers_mock(1)[0]
+
+ def setup_servers_mock(self, count):
+ servers = compute_fakes.FakeServer.create_servers(count=count)
+
+ # This is the return value for utils.find_resource()
+ self.servers_mock.get = compute_fakes.FakeServer.get_servers(servers)
+ return servers
+
+ def setup_server_migrations_mock(self, count):
+ return compute_fakes.FakeServerMigration.create_server_migrations(
+ count=count)
+
+
+class TestListMigration(TestServerMigration):
+ """Test fetch all migrations."""
+
+ MIGRATION_COLUMNS = [
+ 'Source Node', 'Dest Node', 'Source Compute',
+ 'Dest Compute', 'Dest Host', 'Status', 'Server UUID',
+ 'Old Flavor', 'New Flavor', 'Created At', 'Updated At'
+ ]
+
+ def setUp(self):
+ super(TestListMigration, self).setUp()
+
+ self.cmd = server.ListMigration(self.app, None)
+ self.migrations = self.setup_server_migrations_mock(3)
+ self.migrations_mock.list.return_value = self.migrations
+ self.setup_server_migrations_data(self.migrations)
+
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.20')
+
+ def setup_server_migrations_data(self, migrations):
+ self.data = (common_utils.get_item_properties(
+ s, self.MIGRATION_COLUMNS) for s in migrations)
+
+ def test_server_migraton_list(self):
+ arglist = [
+ '--status', 'migrating'
+ ]
+ verifylist = [
+ ('status', 'migrating')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'status': 'migrating',
+ 'host': None,
+ 'server': None,
+ }
+
+ self.migrations_mock.list.assert_called_with(**kwargs)
+
+ self.assertEqual(self.MIGRATION_COLUMNS, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+
+
+class TestListMigrationV223(TestListMigration):
+ """Test fetch all migrations. """
+
+ MIGRATION_COLUMNS = [
+ 'Source Node', 'Dest Node', 'Source Compute',
+ 'Dest Compute', 'Dest Host', 'Status', 'Server UUID',
+ 'Old Flavor', 'New Flavor', 'Created At', 'Updated At'
+ ]
+
+ def setUp(self):
+ super(TestListMigrationV223, self).setUp()
+ self.cmd = server.ListMigration(self.app, None)
+ self.migrations = self.setup_server_migrations_mock(3)
+ self.migrations_mock.list.return_value = self.migrations
+ self.setup_server_migrations_data(self.migrations)
+
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.23')
+
+ def test_server_migraton_list(self):
+ arglist = [
+ '--status', 'migrating'
+ ]
+ verifylist = [
+ ('status', 'migrating')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'status': 'migrating',
+ 'host': None,
+ 'server': None,
+ }
+
+ self.migrations_mock.list.assert_called_with(**kwargs)
+
+ self.MIGRATION_COLUMNS.insert(0, "Id")
+ self.MIGRATION_COLUMNS.insert(
+ len(self.MIGRATION_COLUMNS) - 2, 'Type')
+ self.assertEqual(self.MIGRATION_COLUMNS, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+
+
+class TestListMigrationV259(TestListMigration):
+ """Test fetch all migrations. """
+
+ MIGRATION_COLUMNS = [
+ 'Id', 'UUID', 'Source Node', 'Dest Node', 'Source Compute',
+ 'Dest Compute', 'Dest Host', 'Status', 'Server UUID',
+ 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At'
+ ]
+
+ def setUp(self):
+ super(TestListMigrationV259, self).setUp()
+ self.cmd = server.ListMigration(self.app, None)
+ self.migrations = self.setup_server_migrations_mock(3)
+ self.migrations_mock.list.return_value = self.migrations
+ self.setup_server_migrations_data(self.migrations)
+
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.59')
+
+ def test_server_migraton_list(self):
+ arglist = [
+ '--status', 'migrating',
+ '--limit', '1',
+ '--marker', 'test_kp',
+ '--changes-since', '2019-08-09T08:03:25Z'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('limit', 1),
+ ('marker', 'test_kp'),
+ ('changes_since', '2019-08-09T08:03:25Z')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'status': 'migrating',
+ 'limit': 1,
+ 'marker': 'test_kp',
+ 'host': None,
+ 'server': None,
+ 'changes_since': '2019-08-09T08:03:25Z',
+ }
+
+ self.migrations_mock.list.assert_called_with(**kwargs)
+
+ self.assertEqual(self.MIGRATION_COLUMNS, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+
+ def test_server_migraton_list_with_limit_pre_v259(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.58')
+ arglist = [
+ '--status', 'migrating',
+ '--limit', '1'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('limit', 1)
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.assertRaises(exceptions.CommandError, self.cmd.take_action,
+ parsed_args)
+
+ def test_server_migraton_list_with_marker_pre_v259(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.58')
+ arglist = [
+ '--status', 'migrating',
+ '--marker', 'test_kp'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('marker', 'test_kp')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.assertRaises(exceptions.CommandError, self.cmd.take_action,
+ parsed_args)
+
+ def test_server_migraton_list_with_changes_since_pre_v259(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.58')
+ arglist = [
+ '--status', 'migrating',
+ '--changes-since', '2019-08-09T08:03:25Z'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('changes_since', '2019-08-09T08:03:25Z')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.assertRaises(exceptions.CommandError, self.cmd.take_action,
+ parsed_args)
+
+
+class TestListMigrationV266(TestListMigration):
+ """Test fetch all migrations by changes-before. """
+
+ MIGRATION_COLUMNS = [
+ 'Id', 'UUID', 'Source Node', 'Dest Node', 'Source Compute',
+ 'Dest Compute', 'Dest Host', 'Status', 'Server UUID',
+ 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At'
+ ]
+
+ def setUp(self):
+ super(TestListMigrationV266, self).setUp()
+ self.cmd = server.ListMigration(self.app, None)
+ self.migrations = self.setup_server_migrations_mock(3)
+ self.migrations_mock.list.return_value = self.migrations
+ self.setup_server_migrations_data(self.migrations)
+
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.66')
+
+ def test_server_migraton_list_with_changes_before(self):
+ arglist = [
+ '--status', 'migrating',
+ '--limit', '1',
+ '--marker', 'test_kp',
+ '--changes-since', '2019-08-07T08:03:25Z',
+ '--changes-before', '2019-08-09T08:03:25Z'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('limit', 1),
+ ('marker', 'test_kp'),
+ ('changes_since', '2019-08-07T08:03:25Z'),
+ ('changes_before', '2019-08-09T08:03:25Z')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'status': 'migrating',
+ 'limit': 1,
+ 'marker': 'test_kp',
+ 'host': None,
+ 'server': None,
+ 'changes_since': '2019-08-07T08:03:25Z',
+ 'changes_before': '2019-08-09T08:03:25Z',
+ }
+
+ self.migrations_mock.list.assert_called_with(**kwargs)
+
+ self.assertEqual(self.MIGRATION_COLUMNS, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+
+ def test_server_migraton_list_with_changes_before_pre_v266(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.65')
+ arglist = [
+ '--status', 'migrating',
+ '--changes-before', '2019-08-09T08:03:25Z'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('changes_before', '2019-08-09T08:03:25Z')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.assertRaises(exceptions.CommandError, self.cmd.take_action,
+ parsed_args)
+
+
+class TestListMigrationV280(TestListMigration):
+ """Test fetch all migrations by user-id and/or project-id. """
+
+ MIGRATION_COLUMNS = [
+ 'Id', 'UUID', 'Source Node', 'Dest Node', 'Source Compute',
+ 'Dest Compute', 'Dest Host', 'Status', 'Server UUID',
+ 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At'
+ ]
+
+ def setUp(self):
+ super(TestListMigrationV280, self).setUp()
+ self.cmd = server.ListMigration(self.app, None)
+ self.migrations = self.setup_server_migrations_mock(3)
+ self.migrations_mock.list.return_value = self.migrations
+ self.setup_server_migrations_data(self.migrations)
+
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.80')
+
+ def test_server_migraton_list_with_project(self):
+ arglist = [
+ '--status', 'migrating',
+ '--limit', '1',
+ '--marker', 'test_kp',
+ '--changes-since', '2019-08-07T08:03:25Z',
+ '--changes-before', '2019-08-09T08:03:25Z',
+ '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('limit', 1),
+ ('marker', 'test_kp'),
+ ('changes_since', '2019-08-07T08:03:25Z'),
+ ('changes_before', '2019-08-09T08:03:25Z'),
+ ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'status': 'migrating',
+ 'limit': 1,
+ 'marker': 'test_kp',
+ 'host': None,
+ 'server': None,
+ 'project_id': '0c2accde-644a-45fa-8c10-e76debc7fbc3',
+ 'changes_since': '2019-08-07T08:03:25Z',
+ 'changes_before': "2019-08-09T08:03:25Z",
+ }
+
+ self.migrations_mock.list.assert_called_with(**kwargs)
+
+ self.MIGRATION_COLUMNS.insert(
+ len(self.MIGRATION_COLUMNS) - 2, "Project")
+ self.assertEqual(self.MIGRATION_COLUMNS, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+ # Clean up global variables MIGRATION_COLUMNS
+ self.MIGRATION_COLUMNS.remove('Project')
+
+ def test_get_migrations_with_project_pre_v280(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.79')
+ arglist = [
+ '--status', 'migrating',
+ '--changes-before', '2019-08-09T08:03:25Z',
+ '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('changes_before', '2019-08-09T08:03:25Z'),
+ ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.assertRaises(exceptions.CommandError, self.cmd.take_action,
+ parsed_args)
+
+ def test_server_migraton_list_with_user(self):
+ arglist = [
+ '--status', 'migrating',
+ '--limit', '1',
+ '--marker', 'test_kp',
+ '--changes-since', '2019-08-07T08:03:25Z',
+ '--changes-before', '2019-08-09T08:03:25Z',
+ '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('limit', 1),
+ ('marker', 'test_kp'),
+ ('changes_since', '2019-08-07T08:03:25Z'),
+ ('changes_before', '2019-08-09T08:03:25Z'),
+ ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'status': 'migrating',
+ 'limit': 1,
+ 'marker': 'test_kp',
+ 'host': None,
+ 'server': None,
+ 'user_id': 'dd214878-ca12-40fb-b035-fa7d2c1e86d6',
+ 'changes_since': '2019-08-07T08:03:25Z',
+ 'changes_before': "2019-08-09T08:03:25Z",
+ }
+
+ self.migrations_mock.list.assert_called_with(**kwargs)
+
+ self.MIGRATION_COLUMNS.insert(
+ len(self.MIGRATION_COLUMNS) - 2, "User")
+ self.assertEqual(self.MIGRATION_COLUMNS, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+ # Clean up global variables MIGRATION_COLUMNS
+ self.MIGRATION_COLUMNS.remove('User')
+
+ def test_get_migrations_with_user_pre_v280(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.79')
+ arglist = [
+ '--status', 'migrating',
+ '--changes-before', '2019-08-09T08:03:25Z',
+ '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('changes_before', '2019-08-09T08:03:25Z'),
+ ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ self.assertRaises(exceptions.CommandError, self.cmd.take_action,
+ parsed_args)
+
+ def test_server_migraton_list_with_project_and_user(self):
+ arglist = [
+ '--status', 'migrating',
+ '--limit', '1',
+ '--changes-since', '2019-08-07T08:03:25Z',
+ '--changes-before', '2019-08-09T08:03:25Z',
+ '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3',
+ '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('limit', 1),
+ ('changes_since', '2019-08-07T08:03:25Z'),
+ ('changes_before', '2019-08-09T08:03:25Z'),
+ ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3'),
+ ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'status': 'migrating',
+ 'limit': 1,
+ 'host': None,
+ 'server': None,
+ 'project_id': '0c2accde-644a-45fa-8c10-e76debc7fbc3',
+ 'user_id': 'dd214878-ca12-40fb-b035-fa7d2c1e86d6',
+ 'changes_since': '2019-08-07T08:03:25Z',
+ 'changes_before': "2019-08-09T08:03:25Z",
+ }
+
+ self.migrations_mock.list.assert_called_with(**kwargs)
+
+ self.MIGRATION_COLUMNS.insert(
+ len(self.MIGRATION_COLUMNS) - 2, "Project")
+ self.MIGRATION_COLUMNS.insert(
+ len(self.MIGRATION_COLUMNS) - 2, "User")
+ self.assertEqual(self.MIGRATION_COLUMNS, columns)
+ self.assertEqual(tuple(self.data), tuple(data))
+ # Clean up global variables MIGRATION_COLUMNS
+ self.MIGRATION_COLUMNS.remove('Project')
+ self.MIGRATION_COLUMNS.remove('User')
+
+ def test_get_migrations_with_project_and_user_pre_v280(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.79')
+ arglist = [
+ '--status', 'migrating',
+ '--changes-before', '2019-08-09T08:03:25Z',
+ '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3',
+ '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6'
+ ]
+ verifylist = [
+ ('status', 'migrating'),
+ ('changes_before', '2019-08-09T08:03:25Z'),
+ ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3'),
+ ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6')
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.assertRaises(exceptions.CommandError, self.cmd.take_action,
+ parsed_args)
+
+
+class TestServerMigrationAbort(TestServer):
+
+ def setUp(self):
+ super(TestServerMigrationAbort, self).setUp()
+
+ self.server = compute_fakes.FakeServer.create_one_server()
+
+ # Return value for utils.find_resource for server.
+ self.servers_mock.get.return_value = self.server
+
+ # Get the command object to test
+ self.cmd = server.AbortMigration(self.app, None)
+
+ def test_migration_abort(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.24')
+
+ arglist = [
+ self.server.id,
+ '2', # arbitrary migration ID
+ ]
+ verifylist = []
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.servers_mock.get.assert_called_with(self.server.id)
+ self.server_migrations_mock.live_migration_abort.assert_called_with(
+ self.server.id, '2',)
+ self.assertIsNone(result)
+
+ def test_migration_abort_pre_v224(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.23')
+
+ arglist = [
+ self.server.id,
+ '2', # arbitrary migration ID
+ ]
+ verifylist = []
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.24 or greater is required',
+ str(ex))
+
+
+class TestServerMigrationForceComplete(TestServer):
+
+ def setUp(self):
+ super(TestServerMigrationForceComplete, self).setUp()
+
+ self.server = compute_fakes.FakeServer.create_one_server()
+
+ # Return value for utils.find_resource for server.
+ self.servers_mock.get.return_value = self.server
+
+ # Get the command object to test
+ self.cmd = server.ForceCompleteMigration(self.app, None)
+
+ def test_migration_force_complete(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.22')
+
+ arglist = [
+ self.server.id,
+ '2', # arbitrary migration ID
+ ]
+ verifylist = []
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.servers_mock.get.assert_called_with(self.server.id)
+ self.server_migrations_mock.live_migrate_force_complete\
+ .assert_called_with(self.server.id, '2',)
+ self.assertIsNone(result)
+
+ def test_migration_force_complete_pre_v222(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.21')
+
+ arglist = [
+ self.server.id,
+ '2', # arbitrary migration ID
+ ]
+ verifylist = []
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.22 or greater is required',
+ str(ex))
+
+
class TestServerPause(TestServer):
def setUp(self):
@@ -4293,7 +5395,7 @@ class TestServerResize(TestServer):
# A warning should have been logged for using --confirm.
mock_warning.assert_called_once()
self.assertIn('The --confirm option has been deprecated.',
- six.text_type(mock_warning.call_args[0][0]))
+ str(mock_warning.call_args[0][0]))
def test_server_resize_revert(self):
arglist = [
@@ -4318,7 +5420,7 @@ class TestServerResize(TestServer):
# A warning should have been logged for using --revert.
mock_warning.assert_called_once()
self.assertIn('The --revert option has been deprecated.',
- six.text_type(mock_warning.call_args[0][0]))
+ str(mock_warning.call_args[0][0]))
@mock.patch.object(common_utils, 'wait_for_status', return_value=True)
def test_server_resize_with_wait_ok(self, mock_wait_for_status):
@@ -4520,6 +5622,8 @@ class TestServerSet(TestServer):
'update': None,
'reset_state': None,
'change_password': None,
+ 'add_tag': None,
+ 'set_tags': None,
}
self.fake_servers = self.setup_servers_mock(2)
@@ -4660,6 +5764,50 @@ class TestServerSet(TestServer):
self.assertRaises(exceptions.CommandError, self.cmd.take_action,
parsed_args)
+ def test_server_set_with_tag(self):
+ self.fake_servers[0].api_version = api_versions.APIVersion('2.26')
+
+ arglist = [
+ '--tag', 'tag1',
+ '--tag', 'tag2',
+ 'foo_vm',
+ ]
+ verifylist = [
+ ('tags', ['tag1', 'tag2']),
+ ('server', 'foo_vm'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.fake_servers[0].add_tag.assert_has_calls([
+ mock.call(tag='tag1'),
+ mock.call(tag='tag2'),
+ ])
+ self.assertIsNone(result)
+
+ def test_server_set_with_tag_pre_v226(self):
+ self.fake_servers[0].api_version = api_versions.APIVersion('2.25')
+
+ arglist = [
+ '--tag', 'tag1',
+ '--tag', 'tag2',
+ 'foo_vm',
+ ]
+ verifylist = [
+ ('tags', ['tag1', 'tag2']),
+ ('server', 'foo_vm'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.26 or greater is required',
+ str(ex))
+
class TestServerShelve(TestServer):
@@ -4985,6 +6133,52 @@ class TestServerUnset(TestServer):
self.assertRaises(exceptions.CommandError, self.cmd.take_action,
parsed_args)
+ def test_server_unset_with_tag(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.26')
+
+ arglist = [
+ '--tag', 'tag1',
+ '--tag', 'tag2',
+ 'foo_vm',
+ ]
+ verifylist = [
+ ('tags', ['tag1', 'tag2']),
+ ('server', 'foo_vm'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.assertIsNone(result)
+
+ self.servers_mock.delete_tag.assert_has_calls([
+ mock.call(self.fake_server, tag='tag1'),
+ mock.call(self.fake_server, tag='tag2'),
+ ])
+
+ def test_server_unset_with_tag_pre_v226(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.25')
+
+ arglist = [
+ '--tag', 'tag1',
+ '--tag', 'tag2',
+ 'foo_vm',
+ ]
+ verifylist = [
+ ('tags', ['tag1', 'tag2']),
+ ('server', 'foo_vm'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.26 or greater is required',
+ str(ex))
+
class TestServerUnshelve(TestServer):
@@ -5166,6 +6360,8 @@ class TestServerGeneral(TestServer):
'tenant_id': u'tenant-id-xxx',
'networks': {u'public': [u'10.20.30.40', u'2001:db8::f']},
'links': u'http://xxx.yyy.com',
+ 'properties': '',
+ 'volumes_attached': [{"id": "6344fe9d-ef20-45b2-91a6"}],
}
_server = compute_fakes.FakeServer.create_one_server(attrs=server_info)
find_resource.side_effect = [_server, _flavor]
@@ -5182,6 +6378,7 @@ class TestServerGeneral(TestServer):
'properties': '',
'OS-EXT-STS:power_state': server._format_servers_list_power_state(
getattr(_server, 'OS-EXT-STS:power_state')),
+ 'volumes_attached': [{"id": "6344fe9d-ef20-45b2-91a6"}],
}
# Call _prep_server_detail().
diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py
index 9cd876ea..359cd2bd 100644
--- a/openstackclient/tests/unit/compute/v2/test_server_group.py
+++ b/openstackclient/tests/unit/compute/v2/test_server_group.py
@@ -15,6 +15,7 @@
from unittest import mock
+from novaclient import api_versions
from osc_lib import exceptions
from osc_lib import utils
@@ -53,6 +54,33 @@ class TestServerGroup(compute_fakes.TestComputev2):
self.server_groups_mock.reset_mock()
+class TestServerGroupV264(TestServerGroup):
+
+ fake_server_group = \
+ compute_fakes.FakeServerGroupV264.create_one_server_group()
+
+ columns = (
+ 'id',
+ 'members',
+ 'name',
+ 'policy',
+ 'project_id',
+ 'user_id',
+ )
+
+ data = (
+ fake_server_group.id,
+ utils.format_list(fake_server_group.members),
+ fake_server_group.name,
+ fake_server_group.policy,
+ fake_server_group.project_id,
+ fake_server_group.user_id,
+ )
+
+ def setUp(self):
+ super(TestServerGroupV264, self).setUp()
+
+
class TestServerGroupCreate(TestServerGroup):
def setUp(self):
@@ -80,6 +108,28 @@ class TestServerGroupCreate(TestServerGroup):
self.assertEqual(self.columns, columns)
self.assertEqual(self.data, data)
+ def test_server_group_create_v264(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.64')
+
+ arglist = [
+ '--policy', 'soft-anti-affinity',
+ 'affinity_group',
+ ]
+ verifylist = [
+ ('policy', 'soft-anti-affinity'),
+ ('name', 'affinity_group'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+ self.server_groups_mock.create.assert_called_once_with(
+ name=parsed_args.name,
+ policy=parsed_args.policy,
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.data, data)
+
class TestServerGroupDelete(TestServerGroup):
@@ -230,6 +280,76 @@ class TestServerGroupList(TestServerGroup):
self.assertEqual(self.list_data_long, tuple(data))
+class TestServerGroupListV264(TestServerGroupV264):
+
+ list_columns = (
+ 'ID',
+ 'Name',
+ 'Policy',
+ )
+
+ list_columns_long = (
+ 'ID',
+ 'Name',
+ 'Policy',
+ 'Members',
+ 'Project Id',
+ 'User Id',
+ )
+
+ list_data = ((
+ TestServerGroupV264.fake_server_group.id,
+ TestServerGroupV264.fake_server_group.name,
+ TestServerGroupV264.fake_server_group.policy,
+ ),)
+
+ list_data_long = ((
+ TestServerGroupV264.fake_server_group.id,
+ TestServerGroupV264.fake_server_group.name,
+ TestServerGroupV264.fake_server_group.policy,
+ utils.format_list(TestServerGroupV264.fake_server_group.members),
+ TestServerGroupV264.fake_server_group.project_id,
+ TestServerGroupV264.fake_server_group.user_id,
+ ),)
+
+ def setUp(self):
+ super(TestServerGroupListV264, self).setUp()
+
+ self.server_groups_mock.list.return_value = [self.fake_server_group]
+ self.cmd = server_group.ListServerGroup(self.app, None)
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.64')
+
+ def test_server_group_list(self):
+ arglist = []
+ verifylist = [
+ ('all_projects', False),
+ ('long', False),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+ self.server_groups_mock.list.assert_called_once_with(False)
+
+ self.assertEqual(self.list_columns, columns)
+ self.assertEqual(self.list_data, tuple(data))
+
+ def test_server_group_list_with_all_projects_and_long(self):
+ arglist = [
+ '--all-projects',
+ '--long',
+ ]
+ verifylist = [
+ ('all_projects', True),
+ ('long', True),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+ self.server_groups_mock.list.assert_called_once_with(True)
+
+ self.assertEqual(self.list_columns_long, columns)
+ self.assertEqual(self.list_data_long, tuple(data))
+
+
class TestServerGroupShow(TestServerGroup):
def setUp(self):
diff --git a/openstackclient/tests/unit/compute/v2/test_service.py b/openstackclient/tests/unit/compute/v2/test_service.py
index 7a036833..87e54747 100644
--- a/openstackclient/tests/unit/compute/v2/test_service.py
+++ b/openstackclient/tests/unit/compute/v2/test_service.py
@@ -18,7 +18,6 @@ from unittest.mock import call
from novaclient import api_versions
from osc_lib import exceptions
-import six
from openstackclient.compute.v2 import service
from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes
@@ -502,7 +501,7 @@ class TestServiceSet(TestService):
self.cmd._find_service_by_host_and_binary,
self.service_mock, 'fake-host', 'nova-compute')
self.assertIn('Compute service for host "fake-host" and binary '
- '"nova-compute" not found.', six.text_type(ex))
+ '"nova-compute" not found.', str(ex))
def test_service_set_find_service_by_host_and_binary_many_results(self):
# Tests that more than one compute service is found by host and binary.
@@ -512,4 +511,4 @@ class TestServiceSet(TestService):
self.service_mock, 'fake-host', 'nova-compute')
self.assertIn('Multiple compute services found for host "fake-host" '
'and binary "nova-compute". Unable to proceed.',
- six.text_type(ex))
+ str(ex))
diff --git a/openstackclient/tests/unit/fakes.py b/openstackclient/tests/unit/fakes.py
index e5476f06..00e0c129 100644
--- a/openstackclient/tests/unit/fakes.py
+++ b/openstackclient/tests/unit/fakes.py
@@ -19,7 +19,6 @@ from unittest import mock
from keystoneauth1 import fixture
import requests
-import six
AUTH_TOKEN = "foobar"
@@ -253,7 +252,7 @@ class FakeResponse(requests.Response):
self.headers.update(headers)
self._content = json.dumps(data)
- if not isinstance(self._content, six.binary_type):
+ if not isinstance(self._content, bytes):
self._content = self._content.encode()
diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py
index 310f6b76..b094817e 100644
--- a/openstackclient/tests/unit/image/v2/test_image.py
+++ b/openstackclient/tests/unit/image/v2/test_image.py
@@ -100,6 +100,7 @@ class TestImageCreate(TestImage):
# ImageManager.create(name=, **)
self.client.create_image.assert_called_with(
name=self.new_image.name,
+ allow_duplicates=True,
container_format=image.DEFAULT_CONTAINER_FORMAT,
disk_format=image.DEFAULT_DISK_FORMAT,
)
@@ -152,6 +153,7 @@ class TestImageCreate(TestImage):
# ImageManager.create(name=, **)
self.client.create_image.assert_called_with(
name=self.new_image.name,
+ allow_duplicates=True,
container_format='ovf',
disk_format='ami',
min_disk=10,
@@ -239,6 +241,7 @@ class TestImageCreate(TestImage):
# ImageManager.create(name=, **)
self.client.create_image.assert_called_with(
name=self.new_image.name,
+ allow_duplicates=True,
container_format=image.DEFAULT_CONTAINER_FORMAT,
disk_format=image.DEFAULT_DISK_FORMAT,
is_protected=self.new_image.is_protected,
@@ -246,7 +249,7 @@ class TestImageCreate(TestImage):
Alpha='1',
Beta='2',
tags=self.new_image.tags,
- filename=imagefile.name
+ filename=imagefile.name,
)
self.assertEqual(
@@ -288,6 +291,7 @@ class TestImageCreate(TestImage):
# ImageManager.create(name=, **)
self.client.create_image.assert_called_with(
name=self.new_image.name,
+ allow_duplicates=True,
container_format=image.DEFAULT_CONTAINER_FORMAT,
disk_format=image.DEFAULT_DISK_FORMAT,
use_import=True
diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py
index 3df4042c..2db83d3b 100644
--- a/openstackclient/tests/unit/network/v2/fakes.py
+++ b/openstackclient/tests/unit/network/v2/fakes.py
@@ -642,7 +642,7 @@ class FakePort(object):
'qos_network_policy_id': 'qos-policy-id-' + uuid.uuid4().hex,
'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex,
'tags': [],
- 'uplink_status_propagation': False,
+ 'propagate_uplink_status': False,
}
# Overwrite default attributes.
@@ -662,8 +662,8 @@ class FakePort(object):
port.project_id = port_attrs['tenant_id']
port.security_group_ids = port_attrs['security_group_ids']
port.qos_policy_id = port_attrs['qos_policy_id']
- port.uplink_status_propagation = port_attrs[
- 'uplink_status_propagation']
+ port.propagate_uplink_status = port_attrs[
+ 'propagate_uplink_status']
return port
@@ -1591,6 +1591,8 @@ class FakeNetworkMeterRule(object):
'excluded': False,
'metering_label_id': 'meter-label-id-' + uuid.uuid4().hex,
'remote_ip_prefix': '10.0.0.0/24',
+ 'source_ip_prefix': '8.8.8.8/32',
+ 'destination_ip_prefix': '10.0.0.0/24',
'tenant_id': 'project-id-' + uuid.uuid4().hex,
}
diff --git a/openstackclient/tests/unit/network/v2/test_network_meter_rule.py b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py
index 8f8922c0..e9224fa6 100644
--- a/openstackclient/tests/unit/network/v2/test_network_meter_rule.py
+++ b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py
@@ -42,20 +42,24 @@ class TestCreateMeterRule(TestMeterRule):
)
columns = (
+ 'destination_ip_prefix',
'direction',
'excluded',
'id',
'metering_label_id',
'project_id',
'remote_ip_prefix',
+ 'source_ip_prefix',
)
data = (
+ new_rule.destination_ip_prefix,
new_rule.direction,
new_rule.excluded,
new_rule.id,
new_rule.metering_label_id,
new_rule.project_id,
new_rule.remote_ip_prefix,
+ new_rule.source_ip_prefix,
)
def setUp(self):
@@ -228,6 +232,8 @@ class TestListMeterRule(TestMeterRule):
'Excluded',
'Direction',
'Remote IP Prefix',
+ 'Source IP Prefix',
+ 'Destination IP Prefix'
)
data = []
@@ -238,6 +244,8 @@ class TestListMeterRule(TestMeterRule):
rule.excluded,
rule.direction,
rule.remote_ip_prefix,
+ rule.source_ip_prefix,
+ rule.destination_ip_prefix
))
def setUp(self):
@@ -270,21 +278,25 @@ class TestShowMeterRule(TestMeterRule):
)
columns = (
+ 'destination_ip_prefix',
'direction',
'excluded',
'id',
'metering_label_id',
'project_id',
'remote_ip_prefix',
+ 'source_ip_prefix',
)
data = (
+ new_rule.destination_ip_prefix,
new_rule.direction,
new_rule.excluded,
new_rule.id,
new_rule.metering_label_id,
new_rule.project_id,
new_rule.remote_ip_prefix,
+ new_rule.source_ip_prefix,
)
def setUp(self):
diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py
index 70fa063d..d8889ae5 100644
--- a/openstackclient/tests/unit/network/v2/test_port.py
+++ b/openstackclient/tests/unit/network/v2/test_port.py
@@ -62,12 +62,12 @@ class TestPort(network_fakes.TestNetworkV2):
'numa_affinity_policy',
'port_security_enabled',
'project_id',
+ 'propagate_uplink_status',
'qos_network_policy_id',
'qos_policy_id',
'security_group_ids',
'status',
'tags',
- 'uplink_status_propagation',
)
data = (
@@ -94,12 +94,12 @@ class TestPort(network_fakes.TestNetworkV2):
fake_port.numa_affinity_policy,
fake_port.port_security_enabled,
fake_port.project_id,
+ fake_port.propagate_uplink_status,
fake_port.qos_network_policy_id,
fake_port.qos_policy_id,
format_columns.ListColumn(fake_port.security_group_ids),
fake_port.status,
format_columns.ListColumn(fake_port.tags),
- fake_port.uplink_status_propagation,
)
return columns, data
@@ -121,6 +121,7 @@ class TestCreatePort(TestPort):
self.network.find_network = mock.Mock(return_value=fake_net)
self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet()
self.network.find_subnet = mock.Mock(return_value=self.fake_subnet)
+ self.network.find_extension = mock.Mock(return_value=[])
# Get the command object to test
self.cmd = port.CreatePort(self.app, self.namespace)
@@ -536,7 +537,7 @@ class TestCreatePort(TestPort):
'name': 'test-port',
})
- def _test_create_with_tag(self, add_tags=True):
+ def _test_create_with_tag(self, add_tags=True, add_tags_in_post=True):
arglist = [
'--network', self._port.network_id,
'test-port',
@@ -555,28 +556,59 @@ class TestCreatePort(TestPort):
else:
verifylist.append(('no_tag', True))
+ self.network.find_extension = mock.Mock(return_value=add_tags_in_post)
+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = (self.cmd.take_action(parsed_args))
- self.network.create_port.assert_called_once_with(
- admin_state_up=True,
- network_id=self._port.network_id,
- name='test-port'
- )
- if add_tags:
- self.network.set_tags.assert_called_once_with(
- self._port,
- tests_utils.CompareBySet(['red', 'blue']))
+ args = {
+ 'admin_state_up': True,
+ 'network_id': self._port.network_id,
+ 'name': 'test-port',
+ }
+ if add_tags_in_post:
+ if add_tags:
+ args['tags'] = sorted(['red', 'blue'])
+ else:
+ args['tags'] = []
+ self.network.create_port.assert_called_once()
+ # Now we need to verify if arguments to call create_port are as
+ # expected,
+ # But we can't simply use assert_called_once_with() method because
+ # duplicates from 'tags' are removed with
+ # list(set(parsed_args.tags)) and that don't quarantee order of
+ # tags list which is used to call create_port().
+ create_port_call_kwargs = self.network.create_port.call_args[1]
+ create_port_call_kwargs['tags'] = sorted(
+ create_port_call_kwargs['tags'])
+ self.assertDictEqual(args, create_port_call_kwargs)
else:
- self.assertFalse(self.network.set_tags.called)
+ self.network.create_port.assert_called_once_with(
+ admin_state_up=True,
+ network_id=self._port.network_id,
+ name='test-port'
+ )
+ if add_tags:
+ self.network.set_tags.assert_called_once_with(
+ self._port,
+ tests_utils.CompareBySet(['red', 'blue']))
+ else:
+ self.assertFalse(self.network.set_tags.called)
+
self.assertEqual(self.columns, columns)
self.assertItemEqual(self.data, data)
def test_create_with_tags(self):
- self._test_create_with_tag(add_tags=True)
+ self._test_create_with_tag(add_tags=True, add_tags_in_post=True)
def test_create_with_no_tag(self):
- self._test_create_with_tag(add_tags=False)
+ self._test_create_with_tag(add_tags=False, add_tags_in_post=True)
+
+ def test_create_with_tags_using_put(self):
+ self._test_create_with_tag(add_tags=True, add_tags_in_post=False)
+
+ def test_create_with_no_tag_using_put(self):
+ self._test_create_with_tag(add_tags=False, add_tags_in_post=False)
def _test_create_with_uplink_status_propagation(self, enable=True):
arglist = [
diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py
index 5720e305..b7e38afb 100644
--- a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py
+++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py
@@ -362,6 +362,7 @@ class TestListSecurityGroupRuleCompute(TestSecurityGroupRuleCompute):
'Ethertype',
'IP Range',
'Port Range',
+ 'Direction',
'Remote Security Group',
)
expected_columns_no_group = \
diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py
index 0a9522b0..01411611 100644
--- a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py
+++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py
@@ -870,7 +870,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork):
_security_group_rules = [_security_group_rule_tcp,
_security_group_rule_icmp]
- expected_columns_with_group_and_long = (
+ expected_columns_with_group = (
'ID',
'IP Protocol',
'Ethertype',
@@ -885,14 +885,15 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork):
'Ethertype',
'IP Range',
'Port Range',
+ 'Direction',
'Remote Security Group',
'Security Group',
)
- expected_data_with_group_and_long = []
+ expected_data_with_group = []
expected_data_no_group = []
for _security_group_rule in _security_group_rules:
- expected_data_with_group_and_long.append((
+ expected_data_with_group.append((
_security_group_rule.id,
_security_group_rule.protocol,
_security_group_rule.ether_type,
@@ -909,6 +910,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork):
_security_group_rule.remote_ip_prefix,
security_group_rule._format_network_port_range(
_security_group_rule),
+ _security_group_rule.direction,
_security_group_rule.remote_group_id,
_security_group_rule.security_group_id,
))
@@ -935,14 +937,12 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork):
self.assertEqual(self.expected_columns_no_group, columns)
self.assertEqual(self.expected_data_no_group, list(data))
- def test_list_with_group_and_long(self):
+ def test_list_with_group(self):
self._security_group_rule_tcp.port_range_min = 80
arglist = [
- '--long',
self._security_group.id,
]
verifylist = [
- ('long', True),
('group', self._security_group.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -952,8 +952,8 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork):
self.network.security_group_rules.assert_called_once_with(**{
'security_group_id': self._security_group.id,
})
- self.assertEqual(self.expected_columns_with_group_and_long, columns)
- self.assertEqual(self.expected_data_with_group_and_long, list(data))
+ self.assertEqual(self.expected_columns_with_group, columns)
+ self.assertEqual(self.expected_data_with_group, list(data))
def test_list_with_ignored_options(self):
self._security_group_rule_tcp.port_range_min = 80
diff --git a/openstackclient/tests/unit/object/v1/fakes.py b/openstackclient/tests/unit/object/v1/fakes.py
index 0ed791a5..1808d5b7 100644
--- a/openstackclient/tests/unit/object/v1/fakes.py
+++ b/openstackclient/tests/unit/object/v1/fakes.py
@@ -14,7 +14,6 @@
#
from keystoneauth1 import session
-import six
from openstackclient.api import object_store_v1 as object_store
from openstackclient.tests.unit import utils
@@ -68,7 +67,7 @@ OBJECT = {
'last_modified': object_modified_1,
}
-object_1_content = six.b('object 1 content')
+object_1_content = b'object 1 content'
OBJECT_2 = {
'name': object_name_2,
diff --git a/openstackclient/tests/unit/object/v1/test_object_all.py b/openstackclient/tests/unit/object/v1/test_object_all.py
index dd587142..7e88409f 100644
--- a/openstackclient/tests/unit/object/v1/test_object_all.py
+++ b/openstackclient/tests/unit/object/v1/test_object_all.py
@@ -12,11 +12,11 @@
#
import copy
+import io
from unittest import mock
from osc_lib import exceptions
from requests_mock.contrib import fixture
-import six
from openstackclient.object.v1 import object as object_cmds
from openstackclient.tests.unit.object.v1 import fakes as object_fakes
@@ -241,9 +241,9 @@ class TestObjectSave(TestObjectAll):
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
- class FakeStdout(six.BytesIO):
+ class FakeStdout(io.BytesIO):
def __init__(self):
- six.BytesIO.__init__(self)
+ io.BytesIO.__init__(self)
self.context_manager_calls = []
def __enter__(self):
diff --git a/openstackclient/tests/unit/test_shell.py b/openstackclient/tests/unit/test_shell.py
index 94f4f44d..366c364e 100644
--- a/openstackclient/tests/unit/test_shell.py
+++ b/openstackclient/tests/unit/test_shell.py
@@ -13,12 +13,12 @@
# under the License.
#
+import importlib
import os
import sys
from unittest import mock
from osc_lib.tests import utils as osc_lib_test_utils
-from oslo_utils import importutils
import wrapt
from openstackclient import shell
@@ -151,12 +151,13 @@ class TestShell(osc_lib_test_utils.TestShell):
super(TestShell, self).setUp()
# TODO(dtroyer): remove this once the shell_class_patch patch is
# released in osc-lib
- self.shell_class = importutils.import_class(self.shell_class_name)
+ mod_str, _sep, class_str = self.shell_class_name.rpartition('.')
+ self.shell_class = getattr(importlib.import_module(mod_str), class_str)
def _assert_admin_token_auth(self, cmd_options, default_args):
with mock.patch(
- self.shell_class_name + ".initialize_app",
- self.app,
+ self.shell_class_name + ".initialize_app",
+ self.app,
):
_shell = osc_lib_test_utils.make_shell(
shell_class=self.shell_class,
diff --git a/openstackclient/tests/unit/utils.py b/openstackclient/tests/unit/utils.py
index 4f1bc46a..4130f18e 100644
--- a/openstackclient/tests/unit/utils.py
+++ b/openstackclient/tests/unit/utils.py
@@ -14,11 +14,11 @@
# under the License.
#
+from io import StringIO
import os
from cliff import columns as cliff_columns
import fixtures
-from six.moves import StringIO
import testtools
from openstackclient.tests.unit import fakes
diff --git a/releasenotes/notes/add-tag-support-server-add-fixed-ip-8de2db58f2a80e85.yaml b/releasenotes/notes/add-tag-support-server-add-fixed-ip-8de2db58f2a80e85.yaml
new file mode 100644
index 00000000..731236eb
--- /dev/null
+++ b/releasenotes/notes/add-tag-support-server-add-fixed-ip-8de2db58f2a80e85.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Add ``--tag`` option to ``server add fixed ip`` command
+ when adding a fixed IP to server. Only available starting
+ with ``--os-compute-api-version 2.49``.
diff --git a/releasenotes/notes/add-tag-support-server-add-network-a8590cab5d7babf0.yaml b/releasenotes/notes/add-tag-support-server-add-network-a8590cab5d7babf0.yaml
new file mode 100644
index 00000000..3442ad6a
--- /dev/null
+++ b/releasenotes/notes/add-tag-support-server-add-network-a8590cab5d7babf0.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Add ``--tag`` option to ``server add network`` command
+ when add network to server. Only available starting
+ with ``--os-compute-api-version 2.49``.
diff --git a/releasenotes/notes/add-tag-support-server-add-port-7e30aa38202d0839.yaml b/releasenotes/notes/add-tag-support-server-add-port-7e30aa38202d0839.yaml
new file mode 100644
index 00000000..a5a63128
--- /dev/null
+++ b/releasenotes/notes/add-tag-support-server-add-port-7e30aa38202d0839.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Add ``--tag`` option to ``server add port`` command when
+ add a port to server. Only available starting with
+ ``--os-compute-api-version 2.49``.
diff --git a/releasenotes/notes/add-tag-support-server-add-volume-278e79a22dd482f4.yaml b/releasenotes/notes/add-tag-support-server-add-volume-278e79a22dd482f4.yaml
new file mode 100644
index 00000000..510218b2
--- /dev/null
+++ b/releasenotes/notes/add-tag-support-server-add-volume-278e79a22dd482f4.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Add ``--tag`` option to ``server add volume`` command when
+ add a volume to server. Only available starting with
+ ``--os-compute-api-version 2.49``.
diff --git a/releasenotes/notes/always-show-direction-for-sg-rule-130efc39bf67d79a.yaml b/releasenotes/notes/always-show-direction-for-sg-rule-130efc39bf67d79a.yaml
new file mode 100644
index 00000000..70dd6750
--- /dev/null
+++ b/releasenotes/notes/always-show-direction-for-sg-rule-130efc39bf67d79a.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ By default listing security group rules now shows the direction.
+ The ``--long`` argument is now redundant and is now ignored as it
+ was only used to display the direction.
+deprecations:
+ - |
+ Deprecate the ``--long`` option for the ``security group list``
+ command. This is no longer needed to display all columns.
diff --git a/releasenotes/notes/bp-add-user-id-field-to-the-migrations-table-299b99ccb1f12a1f.yaml b/releasenotes/notes/bp-add-user-id-field-to-the-migrations-table-299b99ccb1f12a1f.yaml
new file mode 100644
index 00000000..766cd0bf
--- /dev/null
+++ b/releasenotes/notes/bp-add-user-id-field-to-the-migrations-table-299b99ccb1f12a1f.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add ``server migration list`` command. This command allows
+ users to list the status of ongoing server migrations.
diff --git a/releasenotes/notes/bug-1708570-bb19e1213e887723.yaml b/releasenotes/notes/bug-1708570-bb19e1213e887723.yaml
new file mode 100644
index 00000000..1a1cbdbe
--- /dev/null
+++ b/releasenotes/notes/bug-1708570-bb19e1213e887723.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add ``--password`` option to ``server create`` command, allowing users to
+ set the admin password when creating a new instance.
diff --git a/releasenotes/notes/bug-2007489-42e41b14e42128ce.yaml b/releasenotes/notes/bug-2007489-42e41b14e42128ce.yaml
new file mode 100644
index 00000000..99dd5062
--- /dev/null
+++ b/releasenotes/notes/bug-2007489-42e41b14e42128ce.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Add ``server migration abort`` command to abort ongoing live migrations.
diff --git a/releasenotes/notes/bug-2007513-ae39456aeb93bb98.yaml b/releasenotes/notes/bug-2007513-ae39456aeb93bb98.yaml
new file mode 100644
index 00000000..56de14b2
--- /dev/null
+++ b/releasenotes/notes/bug-2007513-ae39456aeb93bb98.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - Add ``server migration force complete`` command to force complete
+ ongoing live migrations.
diff --git a/releasenotes/notes/keypair-support-type-6f7c32aab3b61f7b.yaml b/releasenotes/notes/keypair-support-type-6f7c32aab3b61f7b.yaml
new file mode 100644
index 00000000..549629d8
--- /dev/null
+++ b/releasenotes/notes/keypair-support-type-6f7c32aab3b61f7b.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Add ``--key-type`` option to ``keypair create`` command to set keypair
+ type. Can be ssh or x509. Note that ``--os-compute-api-version 2.2`` or
+ later is required.
diff --git a/releasenotes/notes/properties-with-image-property-field.yaml-c51bf37c3106d6ff.yaml b/releasenotes/notes/properties-with-image-property-field.yaml-c51bf37c3106d6ff.yaml
new file mode 100644
index 00000000..cf082f45
--- /dev/null
+++ b/releasenotes/notes/properties-with-image-property-field.yaml-c51bf37c3106d6ff.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - Support for image search via properties of image. Currently
+ "openstack server create --image-property" only takes image property.
+ Now it can also search image via properties (user defined) too.
+ Story https://storyboard.openstack.org/#!/story/2007860.
diff --git a/releasenotes/notes/restore-create-image-duplicates-92e06f64038b120c.yaml b/releasenotes/notes/restore-create-image-duplicates-92e06f64038b120c.yaml
new file mode 100644
index 00000000..7f36e29c
--- /dev/null
+++ b/releasenotes/notes/restore-create-image-duplicates-92e06f64038b120c.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ Fixes default behaviour of `openstack image create` in allowing images
+ with the same name. In version 5.2.0 this changed to not allow
+ duplicates by default.
diff --git a/releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml b/releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml
new file mode 100644
index 00000000..3a0155a1
--- /dev/null
+++ b/releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+ - The ``openstack server show -f json`` command was not outputting
+ json for security groups, volumes and properties properly.
diff --git a/releasenotes/notes/server-add-tag-63f9cd01dbd82d1b.yaml b/releasenotes/notes/server-add-tag-63f9cd01dbd82d1b.yaml
new file mode 100644
index 00000000..78e7482c
--- /dev/null
+++ b/releasenotes/notes/server-add-tag-63f9cd01dbd82d1b.yaml
@@ -0,0 +1,14 @@
+---
+features:
+ - Add ``--tag`` option to ``server create`` command to add tags when creating
+ a server.
+ Only available starting with ``--os-compute-api-version 2.52``.
+ - Add ``--tag`` option to ``server set`` command to add a tag to an
+ existing server.
+ Only available starting with ``--os-compute-api-version 2.26``.
+ - Add ``--tag`` options to ``server unset`` command to remove a tag from an
+ existing server.
+ Only available starting with ``--os-compute-api-version 2.26``.
+ - Add ``--tags`` and ``--not-tags`` options to ``server list`` command to
+ list instances with and without the specified tag(s), respectively.
+ Only available starting with ``--os-compute-api-version 2.26``.
diff --git a/releasenotes/notes/story-2004346-add-floating-ip-with-no-ports-399c5559e1699816.yaml b/releasenotes/notes/story-2004346-add-floating-ip-with-no-ports-399c5559e1699816.yaml
new file mode 100644
index 00000000..ebcb7f63
--- /dev/null
+++ b/releasenotes/notes/story-2004346-add-floating-ip-with-no-ports-399c5559e1699816.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ Associating a floating IP with a server using the ``server add floating
+ ip`` command requires the server have at least one port associated with it.
+ Previously, this was not validated, meaning the operation would silently
+ fail. This has been resolved.
diff --git a/releasenotes/notes/story-2005468-server-use-config-drive-9fc68552365cfefa.yaml b/releasenotes/notes/story-2005468-server-use-config-drive-9fc68552365cfefa.yaml
new file mode 100644
index 00000000..786ede4f
--- /dev/null
+++ b/releasenotes/notes/story-2005468-server-use-config-drive-9fc68552365cfefa.yaml
@@ -0,0 +1,8 @@
+---
+deprecations:
+ - |
+ The ``--config-drive`` option on the ``openstack server create`` command
+ has been deprecated in favour of the ``--use-config-drive`` and
+ ``--no-config-drive`` arguments. The ``--config-drive`` option expected
+ either a string or bool-like argument, but the nova API has only supported
+ boolean values since API v2.1 was introduced.
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index 889eeb0c..8c276de2 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -6,6 +6,7 @@ OpenStackClient Release Notes
:maxdepth: 1
unreleased
+ victoria
ussuri
train
stein
diff --git a/releasenotes/source/victoria.rst b/releasenotes/source/victoria.rst
new file mode 100644
index 00000000..4efc7b6f
--- /dev/null
+++ b/releasenotes/source/victoria.rst
@@ -0,0 +1,6 @@
+=============================
+Victoria Series Release Notes
+=============================
+
+.. release-notes::
+ :branch: stable/victoria
diff --git a/requirements.txt b/requirements.txt
index 2b7976e5..ee6b6241 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,13 +2,12 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr!=2.1.0,>=2.0.0 # Apache-2.0
-six>=1.10.0 # MIT
cliff!=2.9.0,>=2.8.0 # Apache-2.0
+iso8601>=0.1.11 # MIT
openstacksdk>=0.48.0 # Apache-2.0
osc-lib>=2.0.0 # Apache-2.0
oslo.i18n>=3.15.3 # Apache-2.0
-oslo.utils>=3.33.0 # Apache-2.0
python-keystoneclient>=3.22.0 # Apache-2.0
python-novaclient>=15.1.0 # Apache-2.0
python-cinderclient>=3.3.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 1d9d6df6..8363ec6c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -108,6 +108,9 @@ openstack.compute.v2 =
server_migrate = openstackclient.compute.v2.server:MigrateServer
server_migrate_confirm = openstackclient.compute.v2.server:MigrateConfirm
server_migrate_revert = openstackclient.compute.v2.server:MigrateRevert
+ server_migration_list = openstackclient.compute.v2.server:ListMigration
+ server_migration_abort = openstackclient.compute.v2.server:AbortMigration
+ server_migration_force_complete = openstackclient.compute.v2.server:ForceCompleteMigration
server_pause = openstackclient.compute.v2.server:PauseServer
server_reboot = openstackclient.compute.v2.server:RebootServer
server_rebuild = openstackclient.compute.v2.server:RebuildServer
diff --git a/tox.ini b/tox.ini
index 3a2a307d..c0f6dd5d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
minversion = 3.2.0
-envlist = py37,pep8
+envlist = py38,pep8
skipdist = True
# Automatic envs (pyXX) will only use the python version appropriate to that
# env and ignore basepython inherited from [testenv] if we set
@@ -111,7 +111,6 @@ commands =
[testenv:docs]
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
- -r{toxinidir}/requirements.txt
-r{toxinidir}/doc/requirements.txt
commands =
sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html