summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.zuul.yaml8
-rw-r--r--Dockerfile4
-rw-r--r--bindep.txt1
-rw-r--r--doc/source/cli/command-objects/user.rst126
-rw-r--r--doc/source/conf.py13
-rw-r--r--lower-constraints.txt10
-rw-r--r--openstackclient/identity/v3/user.py120
-rw-r--r--openstackclient/network/v2/floating_ip_port_forwarding.py20
-rw-r--r--openstackclient/network/v2/router.py97
-rw-r--r--openstackclient/network/v2/security_group.py34
-rw-r--r--openstackclient/tests/functional/network/v2/test_router.py43
-rw-r--r--openstackclient/tests/functional/network/v2/test_security_group.py4
-rw-r--r--openstackclient/tests/unit/identity/v3/fakes.py3
-rw-r--r--openstackclient/tests/unit/identity/v3/test_user.py852
-rw-r--r--openstackclient/tests/unit/network/v2/fakes.py2
-rw-r--r--openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py15
-rw-r--r--openstackclient/tests/unit/network/v2/test_router.py140
-rw-r--r--openstackclient/tests/unit/network/v2/test_security_group_network.py10
-rw-r--r--releasenotes/notes/add-description-field-in-port-forwarding-c536e077b243d517.yaml6
-rw-r--r--releasenotes/notes/add_options_to_user_create_and_set-302401520f36d153.yaml19
-rw-r--r--releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml12
-rw-r--r--releasenotes/notes/stateful-security-group-a21fa8498e866b90.yaml6
-rw-r--r--requirements.txt2
-rw-r--r--setup.cfg6
-rw-r--r--setup.py9
25 files changed, 1519 insertions, 43 deletions
diff --git a/.zuul.yaml b/.zuul.yaml
index 7a7e264c..4901a1ed 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -174,8 +174,8 @@
description: Build Docker images.
allowed-projects: openstack/python-openstackclient
requires:
- - python-builder-container-image
- - python-base-container-image
+ - python-builder-3.7-container-image
+ - python-base-3.7-container-image
provides: osc-container-image
vars: &osc_image_vars
docker_images:
@@ -188,8 +188,8 @@
description: Build Docker images and upload to Docker Hub.
allowed-projects: openstack/python-openstackclient
requires:
- - python-builder-container-image
- - python-base-container-image
+ - python-builder-3.7-container-image
+ - python-base-3.7-container-image
provides: osc-container-image
secrets:
- name: docker_credentials
diff --git a/Dockerfile b/Dockerfile
index 9ff8084c..ca60bb0e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,12 +13,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-FROM docker.io/opendevorg/python-builder as builder
+FROM docker.io/opendevorg/python-builder:3.7 as builder
COPY . /tmp/src
RUN assemble
-FROM docker.io/opendevorg/python-base
+FROM docker.io/opendevorg/python-base:3.7
COPY --from=builder /output/ /output
RUN /output/install-from-bindep
diff --git a/bindep.txt b/bindep.txt
index c5ae6c60..cf94d2a8 100644
--- a/bindep.txt
+++ b/bindep.txt
@@ -2,6 +2,7 @@
# see https://docs.openstack.org/infra/bindep/ for additional information.
gcc [compile test]
+libc6-dev [compile test platform:dpkg]
libffi-devel [platform:rpm]
libffi-dev [compile test platform:dpkg]
libffi6 [platform:dpkg]
diff --git a/doc/source/cli/command-objects/user.rst b/doc/source/cli/command-objects/user.rst
index 632d0e25..d0fc3f87 100644
--- a/doc/source/cli/command-objects/user.rst
+++ b/doc/source/cli/command-objects/user.rst
@@ -19,6 +19,12 @@ Create new user
[--password-prompt]
[--email <email-address>]
[--description <description>]
+ [--multi-factor-auth-rule <rule>]
+ [--ignore-lockout-failure-attempts| --no-ignore-lockout-failure-attempts]
+ [--ignore-password-expiry| --no-ignore-password-expiry]
+ [--ignore-change-password-upon-first-use| --no-ignore-change-password-upon-first-use]
+ [--enable-lock-password| --disable-lock-password]
+ [--enable-multi-factor-auth| --disable-multi-factor-auth]
[--enable | --disable]
[--or-show]
<user-name>
@@ -56,6 +62,63 @@ Create new user
.. versionadded:: 3
+.. option:: --ignore-lockout-failure-attempts
+
+ Opt into ignoring the number of times a user has authenticated and
+ locking out the user as a result
+
+.. option:: --no-ignore-lockout-failure-attempts
+
+ Opt out of ignoring the number of times a user has authenticated
+ and locking out the user as a result
+
+.. option:: --ignore-change-password-upon-first-use
+
+ Control if a user should be forced to change their password immediately
+ after they log into keystone for the first time. Opt into ignoring
+ the user to change their password during first time login in keystone.
+
+.. option:: --no-ignore-change-password-upon-first-use
+
+ Control if a user should be forced to change their password immediately
+ after they log into keystone for the first time. Opt out of ignoring
+ the user to change their password during first time login in keystone.
+
+.. option:: --ignore-password-expiry
+
+ Opt into allowing user to continue using passwords that may be
+ expired
+
+.. option:: --no-ignore-password-expiry
+
+ Opt out of allowing user to continue using passwords that may be
+ expired
+
+.. option:: --enable-lock-password
+
+ Disables the ability for a user to change its password through
+ self-service APIs
+
+.. option:: --disable-lock-password
+
+ Enables the ability for a user to change its password through
+ self-service APIs
+
+.. option:: --enable-multi-factor-auth
+
+ Enables the MFA (Multi Factor Auth)
+
+.. option:: --disable-multi-factor-auth
+
+ Disables the MFA (Multi Factor Auth)
+
+.. option:: --multi-factor-auth-rule <rule>
+
+ Set multi-factor auth rules. For example, to set a rule requiring the
+ "password" and "totp" auth methods to be provided,
+ use: "--multi-factor-auth-rule password,totp".
+ May be provided multiple times to set different rule combinations.
+
.. option:: --enable
Enable user (default)
@@ -146,6 +209,12 @@ Set user properties
[--password-prompt]
[--email <email-address>]
[--description <description>]
+ [--multi-factor-auth-rule <rule>]
+ [--ignore-lockout-failure-attempts| --no-ignore-lockout-failure-attempts]
+ [--ignore-password-expiry| --no-ignore-password-expiry]
+ [--ignore-change-password-upon-first-use| --no-ignore-change-password-upon-first-use]
+ [--enable-lock-password| --disable-lock-password]
+ [--enable-multi-factor-auth| --disable-multi-factor-auth]
[--enable|--disable]
<user>
@@ -187,6 +256,63 @@ Set user properties
.. versionadded:: 3
+.. option:: --ignore-lockout-failure-attempts
+
+ Opt into ignoring the number of times a user has authenticated and
+ locking out the user as a result
+
+.. option:: --no-ignore-lockout-failure-attempts
+
+ Opt out of ignoring the number of times a user has authenticated
+ and locking out the user as a result
+
+.. option:: --ignore-change-password-upon-first-use
+
+ Control if a user should be forced to change their password immediately
+ after they log into keystone for the first time. Opt into ignoring
+ the user to change their password during first time login in keystone.
+
+.. option:: --no-ignore-change-password-upon-first-use
+
+ Control if a user should be forced to change their password immediately
+ after they log into keystone for the first time. Opt out of ignoring
+ the user to change their password during first time login in keystone.
+
+.. option:: --ignore-password-expiry
+
+ Opt into allowing user to continue using passwords that may be
+ expired
+
+.. option:: --no-ignore-password-expiry
+
+ Opt out of allowing user to continue using passwords that may be
+ expired
+
+.. option:: --enable-lock-password
+
+ Disables the ability for a user to change its password through
+ self-service APIs
+
+.. option:: --disable-lock-password
+
+ Enables the ability for a user to change its password through
+ self-service APIs
+
+.. option:: --enable-multi-factor-auth
+
+ Enables the MFA (Multi Factor Auth)
+
+.. option:: --disable-multi-factor-auth
+
+ Disables the MFA (Multi Factor Auth)
+
+.. option:: --multi-factor-auth-rule <rule>
+
+ Set multi-factor auth rules. For example, to set a rule requiring the
+ "password" and "totp" auth methods to be provided,
+ use: "--multi-factor-auth-rule password,totp".
+ May be provided multiple times to set different rule combinations.
+
.. option:: --enable
Enable user (default)
diff --git a/doc/source/conf.py b/doc/source/conf.py
index ff37783f..4de95fbb 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -12,8 +12,6 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import pbr.version
-
# -- General configuration ----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@@ -55,17 +53,6 @@ master_doc = 'index'
project = u'OpenStack Command Line Client'
copyright = u'2012-2013 OpenStack Foundation'
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-version_info = pbr.version.VersionInfo('python-openstackclient')
-#
-# The short X.Y version.
-version = version_info.version_string()
-# The full version, including alpha/beta/rc tags.
-release = version_info.release_string()
-
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
diff --git a/lower-constraints.txt b/lower-constraints.txt
index dac6922b..b6879366 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -13,11 +13,11 @@ coverage==4.0
cryptography==2.1
ddt==1.0.1
debtcollector==1.2.0
-decorator==3.4.0
+decorator==4.4.1
deprecation==1.0
docker-pycreds==0.2.1
docker==2.4.2
-dogpile.cache==0.6.2
+dogpile.cache==0.6.5
eventlet==0.18.2
extras==1.0.0
fasteners==0.7.0
@@ -25,7 +25,7 @@ fixtures==3.0.0
flake8-import-order==0.13
flake8==2.6.2
future==0.16.0
-futurist==1.2.0
+futurist==2.1.0
gitdb==0.6.4
GitPython==1.0.1
gnocchiclient==3.3.1
@@ -39,7 +39,7 @@ jmespath==0.9.0
jsonpatch==1.16
jsonpointer==1.13
jsonschema==2.6.0
-keystoneauth1==3.16.0
+keystoneauth1==3.18.0
kombu==4.0.0
linecache2==1.0.0
MarkupSafe==1.1.0
@@ -51,7 +51,7 @@ msgpack-python==0.4.0
munch==2.1.0
netaddr==0.7.18
netifaces==0.10.4
-openstacksdk==0.36.0
+openstacksdk==0.44.0
os-client-config==1.28.0
os-service-types==1.7.0
os-testr==1.0.0
diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py
index ca85c5d8..cbc112a0 100644
--- a/openstackclient/identity/v3/user.py
+++ b/openstackclient/identity/v3/user.py
@@ -30,6 +30,114 @@ from openstackclient.identity import common
LOG = logging.getLogger(__name__)
+def _get_options_for_user(identity_client, parsed_args):
+ options = {}
+ if parsed_args.ignore_lockout_failure_attempts:
+ options['ignore_lockout_failure_attempts'] = True
+ if parsed_args.no_ignore_lockout_failure_attempts:
+ options['ignore_lockout_failure_attempts'] = False
+ if parsed_args.ignore_password_expiry:
+ options['ignore_password_expiry'] = True
+ if parsed_args.no_ignore_password_expiry:
+ options['ignore_password_expiry'] = False
+ if parsed_args.ignore_change_password_upon_first_use:
+ options['ignore_change_password_upon_first_use'] = True
+ if parsed_args.no_ignore_change_password_upon_first_use:
+ options['ignore_change_password_upon_first_use'] = False
+ if parsed_args.enable_lock_password:
+ options['lock_password'] = True
+ if parsed_args.disable_lock_password:
+ options['lock_password'] = False
+ if parsed_args.enable_multi_factor_auth:
+ options['multi_factor_auth_enabled'] = True
+ if parsed_args.disable_multi_factor_auth:
+ options['multi_factor_auth_enabled'] = False
+ if parsed_args.multi_factor_auth_rule:
+ auth_rules = [rule.split(",") for rule in
+ parsed_args.multi_factor_auth_rule]
+ if auth_rules:
+ options['multi_factor_auth_rules'] = auth_rules
+ return options
+
+
+def _add_user_options(parser):
+ # Add additional user options
+
+ parser.add_argument(
+ '--ignore-lockout-failure-attempts',
+ action="store_true",
+ help=_('Opt into ignoring the number of times a user has '
+ 'authenticated and locking out the user as a result'),
+ )
+ parser.add_argument(
+ '--no-ignore-lockout-failure-attempts',
+ action="store_true",
+ help=_('Opt out of ignoring the number of times a user has '
+ 'authenticated and locking out the user as a result'),
+ )
+ parser.add_argument(
+ '--ignore-password-expiry',
+ action="store_true",
+ help=_('Opt into allowing user to continue using passwords that '
+ 'may be expired'),
+ )
+ parser.add_argument(
+ '--no-ignore-password-expiry',
+ action="store_true",
+ help=_('Opt out of allowing user to continue using passwords '
+ 'that may be expired'),
+ )
+ parser.add_argument(
+ '--ignore-change-password-upon-first-use',
+ action="store_true",
+ help=_('Control if a user should be forced to change their password '
+ 'immediately after they log into keystone for the first time. '
+ 'Opt into ignoring the user to change their password during '
+ 'first time login in keystone'),
+ )
+ parser.add_argument(
+ '--no-ignore-change-password-upon-first-use',
+ action="store_true",
+ help=_('Control if a user should be forced to change their password '
+ 'immediately after they log into keystone for the first time. '
+ 'Opt out of ignoring the user to change their password during '
+ 'first time login in keystone'),
+ )
+ parser.add_argument(
+ '--enable-lock-password',
+ action="store_true",
+ help=_('Disables the ability for a user to change its password '
+ 'through self-service APIs'),
+ )
+ parser.add_argument(
+ '--disable-lock-password',
+ action="store_true",
+ help=_('Enables the ability for a user to change its password '
+ 'through self-service APIs'),
+ )
+ parser.add_argument(
+ '--enable-multi-factor-auth',
+ action="store_true",
+ help=_('Enables the MFA (Multi Factor Auth)'),
+ )
+ parser.add_argument(
+ '--disable-multi-factor-auth',
+ action="store_true",
+ help=_('Disables the MFA (Multi Factor Auth)'),
+ )
+ parser.add_argument(
+ '--multi-factor-auth-rule',
+ metavar='<rule>',
+ action="append",
+ default=[],
+ help=_('Set multi-factor auth rules. For example, to set a rule '
+ 'requiring the "password" and "totp" auth methods to be '
+ 'provided, use: "--multi-factor-auth-rule password,totp". '
+ 'May be provided multiple times to set different rule '
+ 'combinations.')
+ )
+
+
class CreateUser(command.ShowOne):
_description = _("Create new user")
@@ -72,6 +180,8 @@ class CreateUser(command.ShowOne):
metavar='<description>',
help=_('User description'),
)
+ _add_user_options(parser)
+
enable_group = parser.add_mutually_exclusive_group()
enable_group.add_argument(
'--enable',
@@ -113,6 +223,7 @@ class CreateUser(command.ShowOne):
if not parsed_args.password:
LOG.warning(_("No password was supplied, authentication will fail "
"when a user does not have a password."))
+ options = _get_options_for_user(identity_client, parsed_args)
try:
user = identity_client.users.create(
@@ -122,7 +233,8 @@ class CreateUser(command.ShowOne):
password=parsed_args.password,
email=parsed_args.email,
description=parsed_args.description,
- enabled=enabled
+ enabled=enabled,
+ options=options,
)
except ks_exc.Conflict:
if parsed_args.or_show:
@@ -333,6 +445,8 @@ class SetUser(command.Command):
metavar='<description>',
help=_('Set user description'),
)
+ _add_user_options(parser)
+
enable_group = parser.add_mutually_exclusive_group()
enable_group.add_argument(
'--enable',
@@ -390,6 +504,10 @@ class SetUser(command.Command):
if parsed_args.disable:
kwargs['enabled'] = False
+ options = _get_options_for_user(identity_client, parsed_args)
+ if options:
+ kwargs['options'] = options
+
identity_client.users.update(user.id, **kwargs)
diff --git a/openstackclient/network/v2/floating_ip_port_forwarding.py b/openstackclient/network/v2/floating_ip_port_forwarding.py
index f94bcc06..06b3df8b 100644
--- a/openstackclient/network/v2/floating_ip_port_forwarding.py
+++ b/openstackclient/network/v2/floating_ip_port_forwarding.py
@@ -75,6 +75,12 @@ class CreateFloatingIPPortForwarding(command.ShowOne):
required=True,
help=_("The protocol used in the floating IP "
"port forwarding, for instance: TCP, UDP")
+ ),
+ parser.add_argument(
+ '--description',
+ metavar='<description>',
+ help=_("A text to describe/contextualize the use of the "
+ "port forwarding configuration")
)
parser.add_argument(
'floating_ip',
@@ -113,6 +119,9 @@ class CreateFloatingIPPortForwarding(command.ShowOne):
attrs['internal_ip_address'] = parsed_args.internal_ip_address
attrs['protocol'] = parsed_args.protocol
+ if parsed_args.description is not None:
+ attrs['description'] = parsed_args.description
+
obj = client.create_floating_ip_port_forwarding(
floating_ip.id,
**attrs
@@ -212,6 +221,7 @@ class ListFloatingIPPortForwarding(command.Lister):
'internal_port',
'external_port',
'protocol',
+ 'description',
)
headers = (
'ID',
@@ -220,6 +230,7 @@ class ListFloatingIPPortForwarding(command.Lister):
'Internal Port',
'External Port',
'Protocol',
+ 'Description',
)
query = {}
@@ -296,6 +307,12 @@ class SetFloatingIPPortForwarding(command.Command):
metavar='<protocol>',
choices=['tcp', 'udp'],
help=_("The IP protocol used in the floating IP port forwarding")
+ ),
+ parser.add_argument(
+ '--description',
+ metavar='<description>',
+ help=_("A text to describe/contextualize the use of "
+ "the port forwarding configuration")
)
return parser
@@ -332,6 +349,9 @@ class SetFloatingIPPortForwarding(command.Command):
if parsed_args.protocol:
attrs['protocol'] = parsed_args.protocol
+ if parsed_args.description is not None:
+ attrs['description'] = parsed_args.description
+
client.update_floating_ip_port_forwarding(
floating_ip.id, parsed_args.port_forwarding_id, **attrs)
diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py
index 464dbbec..81b81f98 100644
--- a/openstackclient/network/v2/router.py
+++ b/openstackclient/network/v2/router.py
@@ -168,6 +168,93 @@ class AddSubnetToRouter(command.Command):
subnet_id=subnet.id)
+class AddExtraRoutesToRouter(command.ShowOne):
+ _description = _("Add extra static routes to a router's routing table.")
+
+ def get_parser(self, prog_name):
+ parser = super(AddExtraRoutesToRouter, self).get_parser(prog_name)
+ parser.add_argument(
+ 'router',
+ metavar='<router>',
+ help=_("Router to which extra static routes "
+ "will be added (name or ID).")
+ )
+ parser.add_argument(
+ '--route',
+ metavar='destination=<subnet>,gateway=<ip-address>',
+ action=parseractions.MultiKeyValueAction,
+ dest='routes',
+ default=[],
+ required_keys=['destination', 'gateway'],
+ help=_("Add extra static route to the router. "
+ "destination: destination subnet (in CIDR notation), "
+ "gateway: nexthop IP address. "
+ "Repeat option to add multiple routes. "
+ "Trying to add a route that's already present "
+ "(exactly, including destination and nexthop) "
+ "in the routing table is allowed and is considered "
+ "a successful operation.")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ if parsed_args.routes is not None:
+ for route in parsed_args.routes:
+ route['nexthop'] = route.pop('gateway')
+ client = self.app.client_manager.network
+ router_obj = client.add_extra_routes_to_router(
+ client.find_router(parsed_args.router, ignore_missing=False),
+ body={'router': {'routes': parsed_args.routes}})
+ display_columns, columns = _get_columns(router_obj)
+ data = utils.get_item_properties(
+ router_obj, columns, formatters=_formatters)
+ return (display_columns, data)
+
+
+class RemoveExtraRoutesFromRouter(command.ShowOne):
+ _description = _(
+ "Remove extra static routes from a router's routing table.")
+
+ def get_parser(self, prog_name):
+ parser = super(RemoveExtraRoutesFromRouter, self).get_parser(prog_name)
+ parser.add_argument(
+ 'router',
+ metavar='<router>',
+ help=_("Router from which extra static routes "
+ "will be removed (name or ID).")
+ )
+ parser.add_argument(
+ '--route',
+ metavar='destination=<subnet>,gateway=<ip-address>',
+ action=parseractions.MultiKeyValueAction,
+ dest='routes',
+ default=[],
+ required_keys=['destination', 'gateway'],
+ help=_("Remove extra static route from the router. "
+ "destination: destination subnet (in CIDR notation), "
+ "gateway: nexthop IP address. "
+ "Repeat option to remove multiple routes. "
+ "Trying to remove a route that's already missing "
+ "(fully, including destination and nexthop) "
+ "from the routing table is allowed and is considered "
+ "a successful operation.")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ if parsed_args.routes is not None:
+ for route in parsed_args.routes:
+ route['nexthop'] = route.pop('gateway')
+ client = self.app.client_manager.network
+ router_obj = client.remove_extra_routes_from_router(
+ client.find_router(parsed_args.router, ignore_missing=False),
+ body={'router': {'routes': parsed_args.routes}})
+ display_columns, columns = _get_columns(router_obj)
+ data = utils.get_item_properties(
+ router_obj, columns, formatters=_formatters)
+ return (display_columns, data)
+
+
# TODO(yanxing'an): Use the SDK resource mapped attribute names once the
# OSC minimum requirements include SDK 1.0.
class CreateRouter(command.ShowOne):
@@ -540,17 +627,21 @@ class SetRouter(command.Command):
dest='routes',
default=None,
required_keys=['destination', 'gateway'],
- help=_("Routes associated with the router "
+ help=_("Add routes to the router "
"destination: destination subnet (in CIDR notation) "
"gateway: nexthop IP address "
- "(repeat option to set multiple routes)")
+ "(repeat option to add multiple routes). "
+ "This is deprecated in favor of 'router add/remove route' "
+ "since it is prone to race conditions between concurrent "
+ "clients when not used together with --no-route to "
+ "overwrite the current value of 'routes'.")
)
parser.add_argument(
'--no-route',
action='store_true',
help=_("Clear routes associated with the router. "
"Specify both --route and --no-route to overwrite "
- "current value of route.")
+ "current value of routes.")
)
routes_ha = parser.add_mutually_exclusive_group()
routes_ha.add_argument(
diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py
index 2033af14..aec9c56e 100644
--- a/openstackclient/network/v2/security_group.py
+++ b/openstackclient/network/v2/security_group.py
@@ -120,6 +120,19 @@ class CreateSecurityGroup(common.NetworkAndComputeShowOne):
metavar='<project>',
help=self.enhance_help_neutron(_("Owner's project (name or ID)"))
)
+ stateful_group = parser.add_mutually_exclusive_group()
+ stateful_group.add_argument(
+ "--stateful",
+ action='store_true',
+ default=None,
+ help=_("Security group is stateful (Default)")
+ )
+ stateful_group.add_argument(
+ "--stateless",
+ action='store_true',
+ default=None,
+ help=_("Security group is stateless")
+ )
identity_common.add_project_domain_option_to_parser(
parser, enhance_help=self.enhance_help_neutron)
_tag.add_tag_option_to_parser_for_create(
@@ -138,6 +151,10 @@ class CreateSecurityGroup(common.NetworkAndComputeShowOne):
attrs = {}
attrs['name'] = parsed_args.name
attrs['description'] = self._get_description(parsed_args)
+ if parsed_args.stateful:
+ attrs['stateful'] = True
+ if parsed_args.stateless:
+ attrs['stateful'] = False
if parsed_args.project is not None:
identity_client = self.app.client_manager.identity
project_id = identity_common.find_project(
@@ -315,6 +332,19 @@ class SetSecurityGroup(common.NetworkAndComputeCommand):
metavar="<description>",
help=_("New security group description")
)
+ stateful_group = parser.add_mutually_exclusive_group()
+ stateful_group.add_argument(
+ "--stateful",
+ action='store_true',
+ default=None,
+ help=_("Security group is stateful (Default)")
+ )
+ stateful_group.add_argument(
+ "--stateless",
+ action='store_true',
+ default=None,
+ help=_("Security group is stateless")
+ )
return parser
def update_parser_network(self, parser):
@@ -331,6 +361,10 @@ class SetSecurityGroup(common.NetworkAndComputeCommand):
attrs['name'] = parsed_args.name
if parsed_args.description is not None:
attrs['description'] = parsed_args.description
+ if parsed_args.stateful:
+ attrs['stateful'] = True
+ if parsed_args.stateless:
+ attrs['stateful'] = False
# NOTE(rtheis): Previous behavior did not raise a CommandError
# if there were no updates. Maintain this behavior and issue
# the update.
diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py
index 05aad7a0..0769dca6 100644
--- a/openstackclient/tests/functional/network/v2/test_router.py
+++ b/openstackclient/tests/functional/network/v2/test_router.py
@@ -261,3 +261,46 @@ class RouterTests(common.NetworkTagTests):
new_name
))
self.assertIsNone(cmd_output["external_gateway_info"])
+
+ def test_router_add_remove_route(self):
+ network_name = uuid.uuid4().hex
+ subnet_name = uuid.uuid4().hex
+ router_name = uuid.uuid4().hex
+
+ self.openstack('network create %s' % network_name)
+ self.addCleanup(self.openstack, 'network delete %s' % network_name)
+
+ self.openstack(
+ 'subnet create %s '
+ '--network %s --subnet-range 10.0.0.0/24' % (
+ subnet_name, network_name))
+
+ self.openstack('router create %s' % router_name)
+ self.addCleanup(self.openstack, 'router delete %s' % router_name)
+
+ self.openstack('router add subnet %s %s' % (router_name, subnet_name))
+ self.addCleanup(self.openstack, 'router remove subnet %s %s' % (
+ router_name, subnet_name))
+
+ out1 = json.loads(self.openstack(
+ 'router add route -f json %s '
+ '--route destination=10.0.10.0/24,gateway=10.0.0.10' %
+ router_name)),
+ self.assertEqual(1, len(out1[0]['routes']))
+
+ self.addCleanup(
+ self.openstack, 'router set %s --no-route' % router_name)
+
+ out2 = json.loads(self.openstack(
+ 'router add route -f json %s '
+ '--route destination=10.0.10.0/24,gateway=10.0.0.10 '
+ '--route destination=10.0.11.0/24,gateway=10.0.0.11' %
+ router_name)),
+ self.assertEqual(2, len(out2[0]['routes']))
+
+ out3 = json.loads(self.openstack(
+ 'router remove route -f json %s '
+ '--route destination=10.0.11.0/24,gateway=10.0.0.11 '
+ '--route destination=10.0.12.0/24,gateway=10.0.0.12' %
+ router_name)),
+ self.assertEqual(1, len(out3[0]['routes']))
diff --git a/openstackclient/tests/functional/network/v2/test_security_group.py b/openstackclient/tests/functional/network/v2/test_security_group.py
index 8ae24b72..d46f8db7 100644
--- a/openstackclient/tests/functional/network/v2/test_security_group.py
+++ b/openstackclient/tests/functional/network/v2/test_security_group.py
@@ -42,7 +42,7 @@ class SecurityGroupTests(common.NetworkTests):
def test_security_group_set(self):
other_name = uuid.uuid4().hex
raw_output = self.openstack(
- 'security group set --description NSA --name ' +
+ 'security group set --description NSA --stateless --name ' +
other_name + ' ' + self.NAME
)
self.assertEqual('', raw_output)
@@ -50,8 +50,10 @@ class SecurityGroupTests(common.NetworkTests):
cmd_output = json.loads(self.openstack(
'security group show -f json ' + other_name))
self.assertEqual('NSA', cmd_output['description'])
+ self.assertFalse(cmd_output['stateful'])
def test_security_group_show(self):
cmd_output = json.loads(self.openstack(
'security group show -f json ' + self.NAME))
self.assertEqual(self.NAME, cmd_output['name'])
+ self.assertTrue(cmd_output['stateful'])
diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py
index eb3ce2a3..58d5d14d 100644
--- a/openstackclient/tests/unit/identity/v3/fakes.py
+++ b/openstackclient/tests/unit/identity/v3/fakes.py
@@ -108,6 +108,9 @@ MAPPING_RESPONSE_2 = {
"rules": MAPPING_RULES_2
}
+mfa_opt1 = 'password,totp'
+mfa_opt2 = 'password'
+
project_id = '8-9-64'
project_name = 'beatles'
project_description = 'Fab Four'
diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py
index 4b14bca0..c71435ba 100644
--- a/openstackclient/tests/unit/identity/v3/test_user.py
+++ b/openstackclient/tests/unit/identity/v3/test_user.py
@@ -111,6 +111,7 @@ class TestUserCreate(TestUser):
'description': None,
'domain': None,
'email': None,
+ 'options': {},
'enabled': True,
'password': None,
}
@@ -150,6 +151,7 @@ class TestUserCreate(TestUser):
'description': None,
'domain': None,
'email': None,
+ 'options': {},
'enabled': True,
'password': 'secret',
}
@@ -190,6 +192,7 @@ class TestUserCreate(TestUser):
'description': None,
'domain': None,
'email': None,
+ 'options': {},
'enabled': True,
'password': 'abc123',
}
@@ -228,6 +231,7 @@ class TestUserCreate(TestUser):
'domain': None,
'email': 'barney@example.com',
'enabled': True,
+ 'options': {},
'password': None,
}
# UserManager.create(name=, domain=, project=, password=, email=,
@@ -265,6 +269,7 @@ class TestUserCreate(TestUser):
'domain': None,
'email': None,
'enabled': True,
+ 'options': {},
'password': None,
}
# UserManager.create(name=, domain=, project=, password=, email=,
@@ -311,6 +316,7 @@ class TestUserCreate(TestUser):
'description': None,
'domain': None,
'email': None,
+ 'options': {},
'enabled': True,
'password': None,
}
@@ -356,6 +362,7 @@ class TestUserCreate(TestUser):
'description': None,
'domain': self.domain.id,
'email': None,
+ 'options': {},
'enabled': True,
'password': None,
}
@@ -392,6 +399,7 @@ class TestUserCreate(TestUser):
'description': None,
'domain': None,
'email': None,
+ 'options': {},
'enabled': True,
'password': None,
}
@@ -428,6 +436,7 @@ class TestUserCreate(TestUser):
'description': None,
'domain': None,
'email': None,
+ 'options': {},
'enabled': False,
'password': None,
}
@@ -438,6 +447,471 @@ class TestUserCreate(TestUser):
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist, data)
+ def test_user_create_ignore_lockout_failure_attempts(self):
+ arglist = [
+ '--ignore-lockout-failure-attempts',
+ self.user.name,
+ ]
+ verifylist = [
+ ('ignore_lockout_failure_attempts', True),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'ignore_lockout_failure_attempts': True},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_no_ignore_lockout_failure_attempts(self):
+ arglist = [
+ '--no-ignore-lockout-failure-attempts',
+ self.user.name,
+ ]
+ verifylist = [
+ ('no_ignore_lockout_failure_attempts', True),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'ignore_lockout_failure_attempts': False},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_ignore_password_expiry(self):
+ arglist = [
+ '--ignore-password-expiry',
+ self.user.name,
+ ]
+ verifylist = [
+ ('ignore_password_expiry', True),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'ignore_password_expiry': True},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_no_ignore_password_expiry(self):
+ arglist = [
+ '--no-ignore-password-expiry',
+ self.user.name,
+ ]
+ verifylist = [
+ ('no_ignore_password_expiry', True),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'ignore_password_expiry': False},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_ignore_change_password_upon_first_use(self):
+ arglist = [
+ '--ignore-change-password-upon-first-use',
+ self.user.name,
+ ]
+ verifylist = [
+ ('ignore_change_password_upon_first_use', True),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'ignore_change_password_upon_first_use': True},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_no_ignore_change_password_upon_first_use(self):
+ arglist = [
+ '--no-ignore-change-password-upon-first-use',
+ self.user.name,
+ ]
+ verifylist = [
+ ('no_ignore_change_password_upon_first_use', True),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'ignore_change_password_upon_first_use': False},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_enables_lock_password(self):
+ arglist = [
+ '--enable-lock-password',
+ self.user.name,
+ ]
+ verifylist = [
+ ('enable_lock_password', True),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'lock_password': True},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_disables_lock_password(self):
+ arglist = [
+ '--disable-lock-password',
+ self.user.name,
+ ]
+ verifylist = [
+ ('disable_lock_password', True),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'lock_password': False},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_enable_multi_factor_auth(self):
+ arglist = [
+ '--enable-multi-factor-auth',
+ self.user.name,
+ ]
+ verifylist = [
+ ('enable_multi_factor_auth', True),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'multi_factor_auth_enabled': True},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_disable_multi_factor_auth(self):
+ arglist = [
+ '--disable-multi-factor-auth',
+ self.user.name,
+ ]
+ verifylist = [
+ ('disable_multi_factor_auth', True),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'multi_factor_auth_enabled': False},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_option_with_multi_factor_auth_rule(self):
+ arglist = [
+ '--multi-factor-auth-rule', identity_fakes.mfa_opt1,
+ '--multi-factor-auth-rule', identity_fakes.mfa_opt2,
+ self.user.name,
+ ]
+ verifylist = [
+ ('multi_factor_auth_rule', [identity_fakes.mfa_opt1,
+ identity_fakes.mfa_opt2]),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'multi_factor_auth_rules': [["password", "totp"],
+ ["password"]]},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
+ def test_user_create_with_multiple_options(self):
+ arglist = [
+ '--ignore-password-expiry',
+ '--disable-multi-factor-auth',
+ '--multi-factor-auth-rule', identity_fakes.mfa_opt1,
+ self.user.name,
+ ]
+ verifylist = [
+ ('ignore_password_expiry', True),
+ ('disable_multi_factor_auth', True),
+ ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]),
+ ('enable', False),
+ ('disable', False),
+ ('name', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # In base command class ShowOne in cliff, abstract method take_action()
+ # returns a two-part tuple with a tuple of column names and a tuple of
+ # data to be shown.
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'name': self.user.name,
+ 'default_project': None,
+ 'description': None,
+ 'domain': None,
+ 'email': None,
+ 'enabled': True,
+ 'options': {'ignore_password_expiry': True,
+ 'multi_factor_auth_enabled': False,
+ 'multi_factor_auth_rules': [["password", "totp"]]},
+ 'password': None,
+ }
+ # UserManager.create(name=, domain=, project=, password=, email=,
+ # description=, enabled=, default_project=)
+ self.users_mock.create.assert_called_with(
+ **kwargs
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.datalist, data)
+
class TestUserDelete(TestUser):
@@ -1007,6 +1481,384 @@ class TestUserSet(TestUser):
)
self.assertIsNone(result)
+ def test_user_set_ignore_lockout_failure_attempts(self):
+ arglist = [
+ '--ignore-lockout-failure-attempts',
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('ignore_lockout_failure_attempts', True),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'ignore_lockout_failure_attempts': True},
+ }
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_no_ignore_lockout_failure_attempts(self):
+ arglist = [
+ '--no-ignore-lockout-failure-attempts',
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('no_ignore_lockout_failure_attempts', True),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'ignore_lockout_failure_attempts': False},
+ }
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_ignore_password_expiry(self):
+ arglist = [
+ '--ignore-password-expiry',
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('ignore_password_expiry', True),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'ignore_password_expiry': True},
+ }
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_no_ignore_password_expiry(self):
+ arglist = [
+ '--no-ignore-password-expiry',
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('no_ignore_password_expiry', True),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'ignore_password_expiry': False},
+ }
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_ignore_change_password_upon_first_use(self):
+ arglist = [
+ '--ignore-change-password-upon-first-use',
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('ignore_change_password_upon_first_use', True),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'ignore_change_password_upon_first_use': True},
+ }
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_no_ignore_change_password_upon_first_use(self):
+ arglist = [
+ '--no-ignore-change-password-upon-first-use',
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('no_ignore_change_password_upon_first_use', True),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'ignore_change_password_upon_first_use': False},
+ }
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_enable_lock_password(self):
+ arglist = [
+ '--enable-lock-password',
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('enable_lock_password', True),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'lock_password': True},
+ }
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_disable_lock_password(self):
+ arglist = [
+ '--disable-lock-password',
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('disable_lock_password', True),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'lock_password': False},
+ }
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_enable_multi_factor_auth(self):
+ arglist = [
+ '--enable-multi-factor-auth',
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('enable_multi_factor_auth', True),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'multi_factor_auth_enabled': True},
+ }
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_disable_multi_factor_auth(self):
+ arglist = [
+ '--disable-multi-factor-auth',
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('disable_multi_factor_auth', True),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'multi_factor_auth_enabled': False},
+ }
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_option_multi_factor_auth_rule(self):
+ arglist = [
+ '--multi-factor-auth-rule', identity_fakes.mfa_opt1,
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'multi_factor_auth_rules': [["password", "totp"]]}}
+
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_user_set_with_multiple_options(self):
+ arglist = [
+ '--ignore-password-expiry',
+ '--enable-multi-factor-auth',
+ '--multi-factor-auth-rule', identity_fakes.mfa_opt1,
+ self.user.name,
+ ]
+ verifylist = [
+ ('name', None),
+ ('password', None),
+ ('email', None),
+ ('ignore_password_expiry', True),
+ ('enable_multi_factor_auth', True),
+ ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]),
+ ('project', None),
+ ('enable', False),
+ ('disable', False),
+ ('user', self.user.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ # Set expected values
+ kwargs = {
+ 'enabled': True,
+ 'options': {'ignore_password_expiry': True,
+ 'multi_factor_auth_enabled': True,
+ 'multi_factor_auth_rules': [["password", "totp"]]}}
+
+ # UserManager.update(user, name=, domain=, project=, password=,
+ # email=, description=, enabled=, default_project=)
+ self.users_mock.update.assert_called_with(
+ self.user.id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
class TestUserSetPassword(TestUser):
diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py
index a553f501..2b88986a 100644
--- a/openstackclient/tests/unit/network/v2/fakes.py
+++ b/openstackclient/tests/unit/network/v2/fakes.py
@@ -1227,6 +1227,7 @@ class FakeSecurityGroup(object):
'id': 'security-group-id-' + uuid.uuid4().hex,
'name': 'security-group-name-' + uuid.uuid4().hex,
'description': 'security-group-description-' + uuid.uuid4().hex,
+ 'stateful': True,
'project_id': 'project-id-' + uuid.uuid4().hex,
'security_group_rules': [],
'tags': []
@@ -1843,6 +1844,7 @@ class FakeFloatingIPPortForwarding(object):
'internal_port': randint(1, 65535),
'external_port': randint(1, 65535),
'protocol': 'tcp',
+ 'description': 'some description',
}
# Overwrite default attributes.
diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py
index ea6cdd26..1028c18a 100644
--- a/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py
+++ b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py
@@ -62,6 +62,7 @@ class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding):
self.app, self.namespace)
self.columns = (
+ 'description',
'external_port',
'floatingip_id',
'id',
@@ -73,6 +74,7 @@ class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding):
)
self.data = (
+ self.new_port_forwarding.description,
self.new_port_forwarding.external_port,
self.new_port_forwarding.floatingip_id,
self.new_port_forwarding.id,
@@ -102,6 +104,8 @@ class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding):
self.new_port_forwarding.floatingip_id,
'--internal-ip-address',
self.new_port_forwarding.internal_ip_address,
+ '--description',
+ self.new_port_forwarding.description,
]
verifylist = [
('port', self.new_port_forwarding.internal_port_id),
@@ -111,6 +115,7 @@ class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding):
('floating_ip', self.new_port_forwarding.floatingip_id),
('internal_ip_address', self.new_port_forwarding.
internal_ip_address),
+ ('description', self.new_port_forwarding.description),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
@@ -126,6 +131,7 @@ class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding):
'internal_port_id': self.new_port_forwarding.
internal_port_id,
'protocol': self.new_port_forwarding.protocol,
+ 'description': self.new_port_forwarding.description,
})
self.assertEqual(self.columns, columns)
self.assertEqual(self.data, data)
@@ -251,7 +257,8 @@ class TestListFloatingIPPortForwarding(TestFloatingIPPortForwarding):
'Internal IP Address',
'Internal Port',
'External Port',
- 'Protocol'
+ 'Protocol',
+ 'Description',
)
def setUp(self):
@@ -273,6 +280,7 @@ class TestListFloatingIPPortForwarding(TestFloatingIPPortForwarding):
port_forwarding.internal_port,
port_forwarding.external_port,
port_forwarding.protocol,
+ port_forwarding.description,
))
self.network.floating_ip_port_forwardings = mock.Mock(
return_value=self.port_forwardings
@@ -393,6 +401,7 @@ class TestSetFloatingIPPortForwarding(TestFloatingIPPortForwarding):
'--internal-protocol-port', '100',
'--external-protocol-port', '200',
'--protocol', 'tcp',
+ '--description', 'some description',
self._port_forwarding.floatingip_id,
self._port_forwarding.id,
]
@@ -402,6 +411,7 @@ class TestSetFloatingIPPortForwarding(TestFloatingIPPortForwarding):
('internal_protocol_port', 100),
('external_protocol_port', 200),
('protocol', 'tcp'),
+ ('description', 'some description'),
('floating_ip', self._port_forwarding.floatingip_id),
('port_forwarding_id', self._port_forwarding.id),
]
@@ -415,6 +425,7 @@ class TestSetFloatingIPPortForwarding(TestFloatingIPPortForwarding):
'internal_port': 100,
'external_port': 200,
'protocol': 'tcp',
+ 'description': 'some description',
}
self.network.update_floating_ip_port_forwarding.assert_called_with(
self._port_forwarding.floatingip_id,
@@ -428,6 +439,7 @@ class TestShowFloatingIPPortForwarding(TestFloatingIPPortForwarding):
# The port forwarding to show.
columns = (
+ 'description',
'external_port',
'floatingip_id',
'id',
@@ -450,6 +462,7 @@ class TestShowFloatingIPPortForwarding(TestFloatingIPPortForwarding):
)
)
self.data = (
+ self._port_forwarding.description,
self._port_forwarding.external_port,
self._port_forwarding.floatingip_id,
self._port_forwarding.id,
diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py
index 38861b0a..09b4957c 100644
--- a/openstackclient/tests/unit/network/v2/test_router.py
+++ b/openstackclient/tests/unit/network/v2/test_router.py
@@ -776,6 +776,146 @@ class TestRemoveSubnetFromRouter(TestRouter):
self.assertIsNone(result)
+class TestAddExtraRoutesToRouter(TestRouter):
+
+ _router = network_fakes.FakeRouter.create_one_router()
+
+ def setUp(self):
+ super(TestAddExtraRoutesToRouter, self).setUp()
+ self.network.add_extra_routes_to_router = mock.Mock(
+ return_value=self._router)
+ self.cmd = router.AddExtraRoutesToRouter(self.app, self.namespace)
+ self.network.find_router = mock.Mock(return_value=self._router)
+
+ def test_add_no_extra_route(self):
+ arglist = [
+ self._router.id,
+ ]
+ verifylist = [
+ ('router', self._router.id),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.network.add_extra_routes_to_router.assert_called_with(
+ self._router, body={'router': {'routes': []}})
+ self.assertEqual(2, len(result))
+
+ def test_add_one_extra_route(self):
+ arglist = [
+ self._router.id,
+ '--route', 'destination=dst1,gateway=gw1',
+ ]
+ verifylist = [
+ ('router', self._router.id),
+ ('routes', [{'destination': 'dst1', 'gateway': 'gw1'}]),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.network.add_extra_routes_to_router.assert_called_with(
+ self._router, body={'router': {'routes': [
+ {'destination': 'dst1', 'nexthop': 'gw1'},
+ ]}})
+ self.assertEqual(2, len(result))
+
+ def test_add_multiple_extra_routes(self):
+ arglist = [
+ self._router.id,
+ '--route', 'destination=dst1,gateway=gw1',
+ '--route', 'destination=dst2,gateway=gw2',
+ ]
+ verifylist = [
+ ('router', self._router.id),
+ ('routes', [
+ {'destination': 'dst1', 'gateway': 'gw1'},
+ {'destination': 'dst2', 'gateway': 'gw2'},
+ ]),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.network.add_extra_routes_to_router.assert_called_with(
+ self._router, body={'router': {'routes': [
+ {'destination': 'dst1', 'nexthop': 'gw1'},
+ {'destination': 'dst2', 'nexthop': 'gw2'},
+ ]}})
+ self.assertEqual(2, len(result))
+
+
+class TestRemoveExtraRoutesFromRouter(TestRouter):
+
+ _router = network_fakes.FakeRouter.create_one_router()
+
+ def setUp(self):
+ super(TestRemoveExtraRoutesFromRouter, self).setUp()
+ self.network.remove_extra_routes_from_router = mock.Mock(
+ return_value=self._router)
+ self.cmd = router.RemoveExtraRoutesFromRouter(self.app, self.namespace)
+ self.network.find_router = mock.Mock(return_value=self._router)
+
+ def test_remove_no_extra_route(self):
+ arglist = [
+ self._router.id,
+ ]
+ verifylist = [
+ ('router', self._router.id),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.network.remove_extra_routes_from_router.assert_called_with(
+ self._router, body={'router': {'routes': []}})
+ self.assertEqual(2, len(result))
+
+ def test_remove_one_extra_route(self):
+ arglist = [
+ self._router.id,
+ '--route', 'destination=dst1,gateway=gw1',
+ ]
+ verifylist = [
+ ('router', self._router.id),
+ ('routes', [{'destination': 'dst1', 'gateway': 'gw1'}]),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.network.remove_extra_routes_from_router.assert_called_with(
+ self._router, body={'router': {'routes': [
+ {'destination': 'dst1', 'nexthop': 'gw1'},
+ ]}})
+ self.assertEqual(2, len(result))
+
+ def test_remove_multiple_extra_routes(self):
+ arglist = [
+ self._router.id,
+ '--route', 'destination=dst1,gateway=gw1',
+ '--route', 'destination=dst2,gateway=gw2',
+ ]
+ verifylist = [
+ ('router', self._router.id),
+ ('routes', [
+ {'destination': 'dst1', 'gateway': 'gw1'},
+ {'destination': 'dst2', 'gateway': 'gw2'},
+ ]),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.network.remove_extra_routes_from_router.assert_called_with(
+ self._router, body={'router': {'routes': [
+ {'destination': 'dst1', 'nexthop': 'gw1'},
+ {'destination': 'dst2', 'nexthop': 'gw2'},
+ ]}})
+ self.assertEqual(2, len(result))
+
+
class TestSetRouter(TestRouter):
# The router to set.
diff --git a/openstackclient/tests/unit/network/v2/test_security_group_network.py b/openstackclient/tests/unit/network/v2/test_security_group_network.py
index 67908fa8..7c1d7fb6 100644
--- a/openstackclient/tests/unit/network/v2/test_security_group_network.py
+++ b/openstackclient/tests/unit/network/v2/test_security_group_network.py
@@ -49,6 +49,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork):
'name',
'project_id',
'rules',
+ 'stateful',
'tags',
)
@@ -58,6 +59,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork):
_security_group.name,
_security_group.project_id,
security_group.NetworkSecurityGroupRulesColumn([]),
+ _security_group.stateful,
_security_group.tags,
)
@@ -101,6 +103,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork):
'--description', self._security_group.description,
'--project', self.project.name,
'--project-domain', self.domain.name,
+ '--stateful',
self._security_group.name,
]
verifylist = [
@@ -108,6 +111,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork):
('name', self._security_group.name),
('project', self.project.name),
('project_domain', self.domain.name),
+ ('stateful', self._security_group.stateful),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -115,6 +119,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork):
self.network.create_security_group.assert_called_once_with(**{
'description': self._security_group.description,
+ 'stateful': self._security_group.stateful,
'name': self._security_group.name,
'tenant_id': self.project.id,
})
@@ -421,11 +426,13 @@ class TestSetSecurityGroupNetwork(TestSecurityGroupNetwork):
arglist = [
'--name', new_name,
'--description', new_description,
+ '--stateful',
self._security_group.name,
]
verifylist = [
('description', new_description),
('group', self._security_group.name),
+ ('stateful', self._security_group.stateful),
('name', new_name),
]
@@ -435,6 +442,7 @@ class TestSetSecurityGroupNetwork(TestSecurityGroupNetwork):
attrs = {
'description': new_description,
'name': new_name,
+ 'stateful': True,
}
self.network.update_security_group.assert_called_once_with(
self._security_group,
@@ -489,6 +497,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork):
'name',
'project_id',
'rules',
+ 'stateful',
'tags',
)
@@ -499,6 +508,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork):
_security_group.project_id,
security_group.NetworkSecurityGroupRulesColumn(
[_security_group_rule._info]),
+ _security_group.stateful,
_security_group.tags,
)
diff --git a/releasenotes/notes/add-description-field-in-port-forwarding-c536e077b243d517.yaml b/releasenotes/notes/add-description-field-in-port-forwarding-c536e077b243d517.yaml
new file mode 100644
index 00000000..6df5bb3a
--- /dev/null
+++ b/releasenotes/notes/add-description-field-in-port-forwarding-c536e077b243d517.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add a new option `--description` to
+ ``floating ip port forwarding create`` and
+ ``floating ip port forwarding set`` commands.
diff --git a/releasenotes/notes/add_options_to_user_create_and_set-302401520f36d153.yaml b/releasenotes/notes/add_options_to_user_create_and_set-302401520f36d153.yaml
new file mode 100644
index 00000000..698a6f18
--- /dev/null
+++ b/releasenotes/notes/add_options_to_user_create_and_set-302401520f36d153.yaml
@@ -0,0 +1,19 @@
+---
+features:
+ - |
+ Added the below mentioned parameters to the user create and set commands.
+
+ * --ignore-lockout-failure-attempts
+ * --no-ignore-lockout-failure-attempts
+ * --ignore-password-expiry
+ * --no-ignore-password-expiry
+ * --ignore-change-password-upon-first-use
+ * --no-ignore-change-password-upon-first-use
+ * --enable-lock-password
+ * --disable-lock-password
+ * --enable-multi-factor-auth
+ * --disable-multi-factor-auth
+ * --multi-factor-auth-rule
+
+ This will now allow users to set user options via CLI.
+ <https://docs.openstack.org/keystone/latest/admin/resource-options.html#user-options>
diff --git a/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml b/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml
new file mode 100644
index 00000000..33b5ba7a
--- /dev/null
+++ b/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml
@@ -0,0 +1,12 @@
+---
+features:
+ - |
+ Add new commands ``router add route`` and ``router remove route`` to
+ support new Neutron extension: ``extraroute-atomic`` (see `Neutron RFE
+ <https://bugs.launchpad.net/neutron/+bug/1826396>`_).
+deprecations:
+ - |
+ The use of ``router set --route`` to add extra routes next to already
+ existing extra routes is deprecated in favor of ``router add route
+ --route``, because ``router set --route`` if used from multiple clients
+ concurrently may lead to lost updates.
diff --git a/releasenotes/notes/stateful-security-group-a21fa8498e866b90.yaml b/releasenotes/notes/stateful-security-group-a21fa8498e866b90.yaml
new file mode 100644
index 00000000..9b70bce8
--- /dev/null
+++ b/releasenotes/notes/stateful-security-group-a21fa8498e866b90.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add ``--stateful`` and ``--stateless`` option to the
+ ``security group create`` and ``security group set`` commands
+ to support stateful and stateless security groups.
diff --git a/requirements.txt b/requirements.txt
index b17b6a55..b6f97b4d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,7 +6,7 @@ six>=1.10.0 # MIT
Babel!=2.4.0,>=2.3.4 # BSD
cliff!=2.9.0,>=2.8.0 # Apache-2.0
-openstacksdk>=0.36.0 # Apache-2.0
+openstacksdk>=0.44.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
diff --git a/setup.cfg b/setup.cfg
index 60caf5db..4129be24 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,6 +6,7 @@ description-file =
author = OpenStack
author-email = openstack-discuss@lists.openstack.org
home-page = https://docs.openstack.org/python-openstackclient/latest/
+python-requires = >=3.6
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
@@ -480,11 +481,13 @@ openstack.network.v2 =
port_unset = openstackclient.network.v2.port:UnsetPort
router_add_port = openstackclient.network.v2.router:AddPortToRouter
+ router_add_route = openstackclient.network.v2.router:AddExtraRoutesToRouter
router_add_subnet = openstackclient.network.v2.router:AddSubnetToRouter
router_create = openstackclient.network.v2.router:CreateRouter
router_delete = openstackclient.network.v2.router:DeleteRouter
router_list = openstackclient.network.v2.router:ListRouter
router_remove_port = openstackclient.network.v2.router:RemovePortFromRouter
+ router_remove_route = openstackclient.network.v2.router:RemoveExtraRoutesFromRouter
router_remove_subnet = openstackclient.network.v2.router:RemoveSubnetFromRouter
router_set = openstackclient.network.v2.router:SetRouter
router_show = openstackclient.network.v2.router:ShowRouter
@@ -715,9 +718,6 @@ openstack.volume.v3 =
volume_transfer_request_list = openstackclient.volume.v2.volume_transfer_request:ListTransferRequest
volume_transfer_request_show = openstackclient.volume.v2.volume_transfer_request:ShowTransferRequest
-[upload_sphinx]
-upload-dir = doc/build/html
-
[extract_messages]
keywords = _ gettext ngettext l_ lazy_gettext
mapping_file = babel.cfg
diff --git a/setup.py b/setup.py
index 566d8443..cd35c3c3 100644
--- a/setup.py
+++ b/setup.py
@@ -13,17 +13,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
-# In python < 2.7.4, a lazy loading of package `pbr` will break
-# setuptools if some other modules registered functions in `atexit`.
-# solution from: http://bugs.python.org/issue15881#msg170215
-try:
- import multiprocessing # noqa
-except ImportError:
- pass
-
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)