diff --git a/doc/source/cli/command-objects/registered-limit.rst b/doc/source/cli/command-objects/registered-limit.rst
new file mode 100644
index 0000000000..586fd1ffcc
--- /dev/null
+++ b/doc/source/cli/command-objects/registered-limit.rst
@@ -0,0 +1,140 @@
+================
+registered limit
+================
+
+Identity v3
+
+Registered limits are used to define default limits for resources within a
+deployment.
+
+registered limit create
+-----------------------
+
+Create a new registered limit
+
+.. program:: registered limit create
+.. code:: bash
+
+    openstack registered limit create
+        [--description <description>]
+        [--region <region>]
+        --service <service>
+        --default-limit <default-limit>
+        <resource-name>
+
+.. option:: --description <description>
+
+   Useful description of the registered limit or its purpose
+
+.. option:: --region <region>
+
+   Region that the limit should be applied to
+
+.. describe:: --service  <service>
+
+   The service that is responsible for the resource being limited (required)
+
+.. describe:: --default-limit <default-limit>
+
+   The default limit for projects to assume unless explicitly overridden
+   (required)
+
+.. describe:: <resource-name>
+
+   The name of the resource to limit (e.g. cores or volumes)
+
+registered limit delete
+-----------------------
+
+Delete registered limit(s)
+
+.. program:: registered limit delete
+.. code:: bash
+
+    openstack registered limit delete
+        <registered-limit-id> [<registered-limit-id> ...]
+
+
+.. describe:: <registered-limit-id>
+
+    Registered limit(s) to delete (ID)
+
+registered limit list
+---------------------
+
+List registered limits
+
+.. program:: registered limit list
+.. code:: bash
+
+    openstack registered limit list
+        [--service <service>]
+        [--resource-name <resource-name>]
+        [--region <region>]
+
+.. option:: --service <service>
+
+    The service to filter the response by (name or ID)
+
+.. option:: --resource-name <resource-name>
+
+    The name of the resource to filter the response by
+
+.. option:: --region <region>
+
+   The region name to filter the response by
+
+registered limit show
+---------------------
+
+Display details about a registered limit
+
+.. program:: registered limit show
+.. code:: bash
+
+    openstack registered limit show
+        <registered-limit-id>
+
+.. describe:: <registered-limit-id>
+
+    Registered limit to display (ID)
+
+registered limit set
+--------------------
+
+Update a registered limit
+
+.. program:: registered limit set
+.. code:: bash
+
+    openstack registered limit set
+        [--service <service>]
+        [--resource-name <resource-name>]
+        [--default-limit <default-limit>]
+        [--description <description>]
+        [--region <region>]
+        <registered-limit-id>
+
+.. option:: --service <service>
+
+    The service of the registered limit to update (ID or name)
+
+.. option:: --resource-name <resource-name>
+
+    The name of the resource for the limit
+
+.. option:: --default-limit <default-limit>
+
+    The default limit for projects to assume for a given resource
+
+.. option:: --description <description>
+
+    A useful description of the limit or its purpose
+
+.. option:: --region <region>
+
+    The region the limit should apply to
+
+.. describe:: <registered-limit-id>
+
+    Registered limit to display (ID)
diff --git a/lower-constraints.txt b/lower-constraints.txt
index 88c75cf47b..bcfc57b37f 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -95,7 +95,7 @@ python-heatclient==1.10.0
 python-ironic-inspector-client==1.5.0
 python-ironicclient==2.3.0
 python-karborclient==0.6.0
-python-keystoneclient==3.15.0
+python-keystoneclient==3.17.0
 python-mimeparse==1.6.0
 python-mistralclient==3.1.0
 python-muranoclient==0.8.2
diff --git a/openstackclient/identity/v3/registered_limit.py b/openstackclient/identity/v3/registered_limit.py
new file mode 100644
index 0000000000..72e07297e6
--- /dev/null
+++ b/openstackclient/identity/v3/registered_limit.py
@@ -0,0 +1,260 @@
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
+#   not use this file except in compliance with the License. You may obtain
+#   a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#   License for the specific language governing permissions and limitations
+#   under the License.
+#
+
+"""Registered limits action implementations."""
+
+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 as common_utils
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateRegisteredLimit(command.ShowOne):
+    _description = _("Create a registered limit")
+
+    def get_parser(self, prog_name):
+        parser = super(CreateRegisteredLimit, self).get_parser(prog_name)
+        parser.add_argument(
+            '--description',
+            metavar='<description>',
+            help=_('Description of the registered limit'),
+        )
+        parser.add_argument(
+            '--region',
+            metavar='<region>',
+            help=_('Region for the registered limit to affect'),
+        )
+        parser.add_argument(
+            '--service',
+            metavar='<service>',
+            required=True,
+            help=_('Service responsible for the resource to limit (required)'),
+        )
+        parser.add_argument(
+            '--default-limit',
+            type=int,
+            metavar='<default-limit>',
+            required=True,
+            help=_('The default limit for the resources to assume (required)'),
+        )
+        parser.add_argument(
+            'resource_name',
+            metavar='<resource-name>',
+            help=_('The name of the resource to limit'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+
+        service = utils.find_resource(
+            identity_client.services, parsed_args.service
+        )
+        region = None
+        if parsed_args.region:
+            region = utils.find_resource(
+                identity_client.regions, parsed_args.region
+            )
+
+        registered_limit = identity_client.registered_limits.create(
+            service,
+            parsed_args.resource_name,
+            parsed_args.default_limit,
+            description=parsed_args.description,
+            region=region
+        )
+
+        registered_limit._info.pop('links', None)
+        return zip(*sorted(six.iteritems(registered_limit._info)))
+
+
+class DeleteRegisteredLimit(command.Command):
+    _description = _("Delete a registered limit")
+
+    def get_parser(self, prog_name):
+        parser = super(DeleteRegisteredLimit, self).get_parser(prog_name)
+        parser.add_argument(
+            'registered_limit_id',
+            metavar='<registered-limit-id>',
+            nargs="+",
+            help=_('Registered limit to delete (ID)'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+
+        errors = 0
+        for registered_limit_id in parsed_args.registered_limit_id:
+            try:
+                identity_client.registered_limits.delete(registered_limit_id)
+            except Exception as e:
+                errors += 1
+                from pprint import pprint
+                pprint(type(e))
+                LOG.error(_("Failed to delete registered limit with ID "
+                            "'%(id)s': %(e)s"),
+                          {'id': registered_limit_id, 'e': e})
+
+        if errors > 0:
+            total = len(parsed_args.registered_limit_id)
+            msg = (_("%(errors)s of %(total)s registered limits failed to "
+                   "delete.") % {'errors': errors, 'total': total})
+            raise exceptions.CommandError(msg)
+
+
+class ListRegisteredLimit(command.Lister):
+    _description = _("List registered limits")
+
+    def get_parser(self, prog_name):
+        parser = super(ListRegisteredLimit, self).get_parser(prog_name)
+        parser.add_argument(
+            '--service',
+            metavar='<service>',
+            help=_('Service responsible for the resource to limit'),
+        )
+        parser.add_argument(
+            '--resource-name',
+            metavar='<resource-name>',
+            dest='resource_name',
+            help=_('The name of the resource to limit'),
+        )
+        parser.add_argument(
+            '--region',
+            metavar='<region>',
+            help=_('Region for the limit to affect.'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+
+        service = None
+        if parsed_args.service:
+            service = common_utils.find_service(
+                identity_client, parsed_args.service
+            )
+        region = None
+        if parsed_args.region:
+            region = utils.find_resource(
+                identity_client.regions, parsed_args.region
+            )
+
+        registered_limits = identity_client.registered_limits.list(
+            service=service,
+            resource_name=parsed_args.resource_name,
+            region=region
+        )
+
+        columns = (
+            'ID', 'Service ID', 'Resource Name', 'Default Limit',
+            'Description', 'Region ID'
+        )
+        return (
+            columns,
+            (utils.get_item_properties(s, columns) for s in registered_limits),
+        )
+
+
+class SetRegisteredLimit(command.ShowOne):
+    _description = _("Update information about a registered limit")
+
+    def get_parser(self, prog_name):
+        parser = super(SetRegisteredLimit, self).get_parser(prog_name)
+        parser.add_argument(
+            'registered_limit_id',
+            metavar='<registered-limit-id>',
+            help=_('Registered limit to update (ID)'),
+        )
+        parser.add_argument(
+            '--service',
+            metavar='<service>',
+            help=_('Service responsible for the resource to limit'),
+        )
+        parser.add_argument(
+            '--resource-name',
+            metavar='<resource-name>',
+            help=_('The name of the resource to limit'),
+        )
+        parser.add_argument(
+            '--default-limit',
+            metavar='<default-limit>',
+            type=int,
+            help=_('The default limit for the resources to assume'),
+        )
+        parser.add_argument(
+            '--description',
+            metavar='<description>',
+            help=_('Description of the registered limit'),
+        )
+        parser.add_argument(
+            '--region',
+            metavar='<region>',
+            help=_('Region for the registered limit to affect.'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+
+        service = None
+        if parsed_args.service:
+            service = common_utils.find_service(
+                identity_client, parsed_args.service
+            )
+
+        region = None
+        if parsed_args.region:
+            region = utils.find_resource(
+                identity_client.regions, parsed_args.region
+            )
+
+        registered_limit = identity_client.registered_limits.update(
+            parsed_args.registered_limit_id,
+            service=service,
+            resource_name=parsed_args.resource_name,
+            default_limit=parsed_args.default_limit,
+            description=parsed_args.description,
+            region=region
+        )
+
+        registered_limit._info.pop('links', None)
+        return zip(*sorted(six.iteritems(registered_limit._info)))
+
+
+class ShowRegisteredLimit(command.ShowOne):
+    _description = _("Display registered limit details")
+
+    def get_parser(self, prog_name):
+        parser = super(ShowRegisteredLimit, self).get_parser(prog_name)
+        parser.add_argument(
+            'registered_limit_id',
+            metavar='<registered-limit-id>',
+            help=_('Registered limit to display (ID)'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+        registered_limit = identity_client.registered_limits.get(
+            parsed_args.registered_limit_id
+        )
+        registered_limit._info.pop('links', None)
+        return zip(*sorted(six.iteritems(registered_limit._info)))
diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py
index 54132be58c..525a31a218 100644
--- a/openstackclient/tests/functional/identity/v3/common.py
+++ b/openstackclient/tests/functional/identity/v3/common.py
@@ -54,6 +54,11 @@ class IdentityTests(base.TestCase):
                                      'Auth URL']
     IMPLIED_ROLE_LIST_HEADERS = ['Prior Role ID', 'Prior Role Name',
                                  'Implied Role ID', 'Implied Role Name']
+    REGISTERED_LIMIT_FIELDS = ['id', 'service_id', 'resource_name',
+                               'default_limit', 'description', 'region_id']
+    REGISTERED_LIMIT_LIST_HEADERS = ['ID', 'Service ID', 'Resource Name',
+                                     'Default Limit', 'Description',
+                                     'Region ID']
 
     @classmethod
     def setUpClass(cls):
@@ -319,3 +324,35 @@ class IdentityTests(base.TestCase):
         items = self.parse_show(raw_output)
         self.assert_show_fields(items, self.SERVICE_PROVIDER_FIELDS)
         return service_provider
+
+    def _create_dummy_registered_limit(self, add_clean_up=True):
+        service_name = self._create_dummy_service()
+        resource_name = data_utils.rand_name('resource_name')
+        params = {
+            'service_name': service_name,
+            'default_limit': 10,
+            'resource_name': resource_name
+        }
+        raw_output = self.openstack(
+            'registered limit create'
+            ' --service %(service_name)s'
+            ' --default-limit %(default_limit)s'
+            ' %(resource_name)s' % params
+        )
+        items = self.parse_show(raw_output)
+        registered_limit_id = self._extract_value_from_items('id', items)
+
+        if add_clean_up:
+            self.addCleanup(
+                self.openstack,
+                'registered limit delete %s' % registered_limit_id
+            )
+
+        self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS)
+        return registered_limit_id
+
+    def _extract_value_from_items(self, key, items):
+        for d in items:
+            for k, v in d.iteritems():
+                if k == key:
+                    return v
diff --git a/openstackclient/tests/functional/identity/v3/test_registered_limit.py b/openstackclient/tests/functional/identity/v3/test_registered_limit.py
new file mode 100644
index 0000000000..09e90ce206
--- /dev/null
+++ b/openstackclient/tests/functional/identity/v3/test_registered_limit.py
@@ -0,0 +1,184 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib.common.utils import data_utils
+
+from openstackclient.tests.functional.identity.v3 import common
+
+
+class RegisteredLimitTestCase(common.IdentityTests):
+
+    def test_registered_limit_create_with_service_name(self):
+        self._create_dummy_registered_limit()
+
+    def test_registered_limit_create_with_service_id(self):
+        service_name = self._create_dummy_service()
+        raw_output = self.openstack(
+            'service show'
+            ' %(service_name)s' % {'service_name': service_name}
+        )
+        service_items = self.parse_show(raw_output)
+        service_id = self._extract_value_from_items('id', service_items)
+
+        raw_output = self.openstack(
+            'registered limit create'
+            ' --service %(service_id)s'
+            ' --default-limit %(default_limit)s'
+            ' %(resource_name)s' % {
+                'service_id': service_id,
+                'default_limit': 10,
+                'resource_name': 'cores'
+            }
+        )
+        items = self.parse_show(raw_output)
+        registered_limit_id = self._extract_value_from_items('id', items)
+        self.addCleanup(
+            self.openstack,
+            'registered limit delete'
+            ' %(registered_limit_id)s' % {
+                'registered_limit_id': registered_limit_id
+            }
+        )
+
+        self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS)
+
+    def test_registered_limit_create_with_options(self):
+        service_name = self._create_dummy_service()
+        region_id = self._create_dummy_region()
+        params = {
+            'service_name': service_name,
+            'resource_name': 'cores',
+            'default_limit': 10,
+            'description': 'default limit for cores',
+            'region_id': region_id
+        }
+
+        raw_output = self.openstack(
+            'registered limit create'
+            ' --description \'%(description)s\''
+            ' --region %(region_id)s'
+            ' --service %(service_name)s'
+            ' --default-limit %(default_limit)s'
+            ' %(resource_name)s' % params
+        )
+        items = self.parse_show(raw_output)
+        registered_limit_id = self._extract_value_from_items('id', items)
+        self.addCleanup(
+            self.openstack,
+            'registered limit delete %(registered_limit_id)s' % {
+                'registered_limit_id': registered_limit_id
+            }
+        )
+
+        self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS)
+
+    def test_registered_limit_show(self):
+        registered_limit_id = self._create_dummy_registered_limit()
+        raw_output = self.openstack(
+            'registered limit show %(registered_limit_id)s' % {
+                'registered_limit_id': registered_limit_id
+            }
+        )
+        items = self.parse_show(raw_output)
+        self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS)
+
+    def test_registered_limit_set_region_id(self):
+        region_id = self._create_dummy_region()
+        registered_limit_id = self._create_dummy_registered_limit()
+
+        params = {
+            'registered_limit_id': registered_limit_id,
+            'region_id': region_id
+        }
+        raw_output = self.openstack(
+            'registered limit set'
+            ' %(registered_limit_id)s'
+            ' --region %(region_id)s' % params
+        )
+        items = self.parse_show(raw_output)
+        self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS)
+
+    def test_registered_limit_set_description(self):
+        registered_limit_id = self._create_dummy_registered_limit()
+        params = {
+            'registered_limit_id': registered_limit_id,
+            'description': 'updated description'
+        }
+        raw_output = self.openstack(
+            'registered limit set'
+            ' %(registered_limit_id)s'
+            ' --description \'%(description)s\'' % params
+        )
+        items = self.parse_show(raw_output)
+        self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS)
+
+    def test_registered_limit_set_service(self):
+        registered_limit_id = self._create_dummy_registered_limit()
+        service_name = self._create_dummy_service()
+        params = {
+            'registered_limit_id': registered_limit_id,
+            'service': service_name
+        }
+        raw_output = self.openstack(
+            'registered limit set'
+            ' %(registered_limit_id)s'
+            ' --service %(service)s' % params
+        )
+        items = self.parse_show(raw_output)
+        self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS)
+
+    def test_registered_limit_set_default_limit(self):
+        registered_limit_id = self._create_dummy_registered_limit()
+        params = {
+            'registered_limit_id': registered_limit_id,
+            'default_limit': 20
+        }
+        raw_output = self.openstack(
+            'registered limit set'
+            ' %(registered_limit_id)s'
+            ' --default-limit %(default_limit)s' % params
+        )
+        items = self.parse_show(raw_output)
+        self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS)
+
+    def test_registered_limit_set_resource_name(self):
+        registered_limit_id = self._create_dummy_registered_limit()
+        resource_name = data_utils.rand_name('resource_name')
+        params = {
+            'registered_limit_id': registered_limit_id,
+            'resource_name': resource_name
+        }
+        raw_output = self.openstack(
+            'registered limit set'
+            ' %(registered_limit_id)s'
+            ' --resource-name %(resource_name)s' % params
+        )
+        items = self.parse_show(raw_output)
+        self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS)
+
+    def test_registered_limit_list(self):
+        self._create_dummy_registered_limit()
+        raw_output = self.openstack('registered limit list')
+        items = self.parse_listing(raw_output)
+        self.assert_table_structure(items, self.REGISTERED_LIMIT_LIST_HEADERS)
+
+    def test_registered_limit_delete(self):
+        registered_limit_id = self._create_dummy_registered_limit(
+            add_clean_up=False
+        )
+        raw_output = self.openstack(
+            'registered limit delete'
+            ' %(registered_limit_id)s' % {
+                'registered_limit_id': registered_limit_id
+            }
+        )
+        self.assertEqual(0, len(raw_output))
diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py
index 7aa9cd7ce2..3cae45157d 100644
--- a/openstackclient/tests/unit/identity/v3/fakes.py
+++ b/openstackclient/tests/unit/identity/v3/fakes.py
@@ -486,6 +486,27 @@ APP_CRED_OPTIONS = {
     'secret': app_cred_secret
 }
 
+registered_limit_id = 'registered-limit-id'
+registered_limit_default_limit = 10
+registered_limit_description = 'default limit of foobars'
+registered_limit_resource_name = 'foobars'
+REGISTERED_LIMIT = {
+    'id': registered_limit_id,
+    'default_limit': registered_limit_default_limit,
+    'resource_name': registered_limit_resource_name,
+    'service_id': service_id,
+    'description': None,
+    'region_id': None
+}
+REGISTERED_LIMIT_OPTIONS = {
+    'id': registered_limit_id,
+    'default_limit': registered_limit_default_limit,
+    'resource_name': registered_limit_resource_name,
+    'service_id': service_id,
+    'description': registered_limit_description,
+    'region_id': region_id
+}
+
 
 def fake_auth_ref(fake_token, fake_service=None):
     """Create an auth_ref using keystoneauth's fixtures"""
@@ -578,6 +599,8 @@ class FakeIdentityv3Client(object):
                                                                          {})
         self.inference_rules = mock.Mock()
         self.inference_rules.resource_class = fakes.FakeResource(None, {})
+        self.registered_limits = mock.Mock()
+        self.registered_limits.resource_class = fakes.FakeResource(None, {})
 
 
 class FakeFederationManager(object):
diff --git a/openstackclient/tests/unit/identity/v3/test_registered_limit.py b/openstackclient/tests/unit/identity/v3/test_registered_limit.py
new file mode 100644
index 0000000000..262ca4f98f
--- /dev/null
+++ b/openstackclient/tests/unit/identity/v3/test_registered_limit.py
@@ -0,0 +1,510 @@
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
+#   not use this file except in compliance with the License. You may obtain
+#   a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#   License for the specific language governing permissions and limitations
+#   under the License.
+
+import copy
+
+from keystoneauth1.exceptions import http as ksa_exceptions
+from osc_lib import exceptions
+
+from openstackclient.identity.v3 import registered_limit
+from openstackclient.tests.unit import fakes
+from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
+
+
+class TestRegisteredLimit(identity_fakes.TestIdentityv3):
+
+    def setUp(self):
+        super(TestRegisteredLimit, self).setUp()
+
+        identity_manager = self.app.client_manager.identity
+        self.registered_limit_mock = identity_manager.registered_limits
+
+        self.services_mock = identity_manager.services
+        self.services_mock.reset_mock()
+
+        self.regions_mock = identity_manager.regions
+        self.regions_mock.reset_mock()
+
+
+class TestRegisteredLimitCreate(TestRegisteredLimit):
+
+    def setUp(self):
+        super(TestRegisteredLimitCreate, self).setUp()
+
+        self.service = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.SERVICE),
+            loaded=True
+        )
+        self.services_mock.get.return_value = self.service
+
+        self.region = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.REGION),
+            loaded=True
+        )
+        self.regions_mock.get.return_value = self.region
+
+        self.cmd = registered_limit.CreateRegisteredLimit(self.app, None)
+
+    def test_registered_limit_create_without_options(self):
+        self.registered_limit_mock.create.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.REGISTERED_LIMIT),
+            loaded=True
+        )
+
+        resource_name = identity_fakes.registered_limit_resource_name
+        default_limit = identity_fakes.registered_limit_default_limit
+        arglist = [
+            '--service', identity_fakes.service_id,
+            '--default-limit', '10',
+            resource_name,
+        ]
+
+        verifylist = [
+            ('service', identity_fakes.service_id),
+            ('default_limit', default_limit),
+            ('resource_name', resource_name)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        kwargs = {'description': None, 'region': None}
+        self.registered_limit_mock.create.assert_called_with(
+            self.service, resource_name, default_limit, **kwargs
+        )
+
+        collist = ('default_limit', 'description', 'id', 'region_id',
+                   'resource_name', 'service_id')
+
+        self.assertEqual(collist, columns)
+        datalist = (
+            identity_fakes.registered_limit_default_limit,
+            None,
+            identity_fakes.registered_limit_id,
+            None,
+            identity_fakes.registered_limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+    def test_registered_limit_create_with_options(self):
+        self.registered_limit_mock.create.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.REGISTERED_LIMIT_OPTIONS),
+            loaded=True
+        )
+
+        resource_name = identity_fakes.registered_limit_resource_name
+        default_limit = identity_fakes.registered_limit_default_limit
+        description = identity_fakes.registered_limit_description
+        arglist = [
+            '--region', identity_fakes.region_id,
+            '--description', description,
+            '--service', identity_fakes.service_id,
+            '--default-limit', '10',
+            resource_name
+        ]
+
+        verifylist = [
+            ('region', identity_fakes.region_id),
+            ('description', description),
+            ('service', identity_fakes.service_id),
+            ('default_limit', default_limit),
+            ('resource_name', resource_name)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        kwargs = {'description': description, 'region': self.region}
+        self.registered_limit_mock.create.assert_called_with(
+            self.service, resource_name, default_limit, **kwargs
+        )
+
+        collist = ('default_limit', 'description', 'id', 'region_id',
+                   'resource_name', 'service_id')
+
+        self.assertEqual(collist, columns)
+        datalist = (
+            identity_fakes.registered_limit_default_limit,
+            description,
+            identity_fakes.registered_limit_id,
+            identity_fakes.region_id,
+            identity_fakes.registered_limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+
+class TestRegisteredLimitDelete(TestRegisteredLimit):
+
+    def setUp(self):
+        super(TestRegisteredLimitDelete, self).setUp()
+
+        self.cmd = registered_limit.DeleteRegisteredLimit(self.app, None)
+
+    def test_registered_limit_delete(self):
+        self.registered_limit_mock.delete.return_value = None
+
+        arglist = [identity_fakes.registered_limit_id]
+        verifylist = [
+            ('registered_limit_id', [identity_fakes.registered_limit_id])
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        self.registered_limit_mock.delete.assert_called_with(
+            identity_fakes.registered_limit_id
+        )
+        self.assertIsNone(result)
+
+    def test_registered_limit_delete_with_exception(self):
+        return_value = ksa_exceptions.NotFound()
+        self.registered_limit_mock.delete.side_effect = return_value
+
+        arglist = ['fake-registered-limit-id']
+        verifylist = [
+            ('registered_limit_id', ['fake-registered-limit-id'])
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        try:
+            self.cmd.take_action(parsed_args)
+            self.fail('CommandError should be raised.')
+        except exceptions.CommandError as e:
+            self.assertEqual(
+                '1 of 1 registered limits failed to delete.', str(e)
+            )
+
+
+class TestRegisteredLimitShow(TestRegisteredLimit):
+
+    def setUp(self):
+        super(TestRegisteredLimitShow, self).setUp()
+
+        self.registered_limit_mock.get.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.REGISTERED_LIMIT),
+            loaded=True
+        )
+
+        self.cmd = registered_limit.ShowRegisteredLimit(self.app, None)
+
+    def test_registered_limit_show(self):
+        arglist = [identity_fakes.registered_limit_id]
+        verifylist = [
+            ('registered_limit_id', identity_fakes.registered_limit_id)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.registered_limit_mock.get.assert_called_with(
+            identity_fakes.registered_limit_id
+        )
+
+        collist = (
+            'default_limit', 'description', 'id', 'region_id', 'resource_name',
+            'service_id'
+        )
+        self.assertEqual(collist, columns)
+        datalist = (
+            identity_fakes.registered_limit_default_limit,
+            None,
+            identity_fakes.registered_limit_id,
+            None,
+            identity_fakes.registered_limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+
+class TestRegisteredLimitSet(TestRegisteredLimit):
+
+    def setUp(self):
+        super(TestRegisteredLimitSet, self).setUp()
+        self.cmd = registered_limit.SetRegisteredLimit(self.app, None)
+
+    def test_registered_limit_set_description(self):
+        registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT)
+        registered_limit['description'] = (
+            identity_fakes.registered_limit_description
+        )
+        self.registered_limit_mock.update.return_value = fakes.FakeResource(
+            None, registered_limit, loaded=True
+        )
+
+        arglist = [
+            '--description', identity_fakes.registered_limit_description,
+            identity_fakes.registered_limit_id
+        ]
+        verifylist = [
+            ('description', identity_fakes.registered_limit_description),
+            ('registered_limit_id', identity_fakes.registered_limit_id)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.registered_limit_mock.update.assert_called_with(
+            identity_fakes.registered_limit_id,
+            service=None,
+            resource_name=None,
+            default_limit=None,
+            description=identity_fakes.registered_limit_description,
+            region=None
+        )
+
+        collist = (
+            'default_limit', 'description', 'id', 'region_id', 'resource_name',
+            'service_id'
+        )
+        self.assertEqual(collist, columns)
+        datalist = (
+            identity_fakes.registered_limit_default_limit,
+            identity_fakes.registered_limit_description,
+            identity_fakes.registered_limit_id,
+            None,
+            identity_fakes.registered_limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+    def test_registered_limit_set_default_limit(self):
+        registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT)
+        default_limit = 20
+        registered_limit['default_limit'] = default_limit
+        self.registered_limit_mock.update.return_value = fakes.FakeResource(
+            None, registered_limit, loaded=True
+        )
+
+        arglist = [
+            '--default-limit', str(default_limit),
+            identity_fakes.registered_limit_id
+        ]
+        verifylist = [
+            ('default_limit', default_limit),
+            ('registered_limit_id', identity_fakes.registered_limit_id)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.registered_limit_mock.update.assert_called_with(
+            identity_fakes.registered_limit_id,
+            service=None,
+            resource_name=None,
+            default_limit=default_limit,
+            description=None,
+            region=None
+        )
+
+        collist = (
+            'default_limit', 'description', 'id', 'region_id', 'resource_name',
+            'service_id'
+        )
+        self.assertEqual(collist, columns)
+        datalist = (
+            default_limit,
+            None,
+            identity_fakes.registered_limit_id,
+            None,
+            identity_fakes.registered_limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+    def test_registered_limit_set_resource_name(self):
+        registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT)
+        resource_name = 'volumes'
+        registered_limit['resource_name'] = resource_name
+        self.registered_limit_mock.update.return_value = fakes.FakeResource(
+            None, registered_limit, loaded=True
+        )
+
+        arglist = [
+            '--resource-name', resource_name,
+            identity_fakes.registered_limit_id
+        ]
+        verifylist = [
+            ('resource_name', resource_name),
+            ('registered_limit_id', identity_fakes.registered_limit_id)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.registered_limit_mock.update.assert_called_with(
+            identity_fakes.registered_limit_id,
+            service=None,
+            resource_name=resource_name,
+            default_limit=None,
+            description=None,
+            region=None
+        )
+
+        collist = (
+            'default_limit', 'description', 'id', 'region_id', 'resource_name',
+            'service_id'
+        )
+        self.assertEqual(collist, columns)
+        datalist = (
+            identity_fakes.registered_limit_default_limit,
+            None,
+            identity_fakes.registered_limit_id,
+            None,
+            resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+    def test_registered_limit_set_service(self):
+        registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT)
+        service = identity_fakes.FakeService.create_one_service()
+        registered_limit['service_id'] = service.id
+        self.registered_limit_mock.update.return_value = fakes.FakeResource(
+            None, registered_limit, loaded=True
+        )
+        self.services_mock.get.return_value = service
+
+        arglist = [
+            '--service', service.id,
+            identity_fakes.registered_limit_id
+        ]
+        verifylist = [
+            ('service', service.id),
+            ('registered_limit_id', identity_fakes.registered_limit_id)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.registered_limit_mock.update.assert_called_with(
+            identity_fakes.registered_limit_id,
+            service=service,
+            resource_name=None,
+            default_limit=None,
+            description=None,
+            region=None
+        )
+
+        collist = (
+            'default_limit', 'description', 'id', 'region_id', 'resource_name',
+            'service_id'
+        )
+        self.assertEqual(collist, columns)
+        datalist = (
+            identity_fakes.registered_limit_default_limit,
+            None,
+            identity_fakes.registered_limit_id,
+            None,
+            identity_fakes.registered_limit_resource_name,
+            service.id
+        )
+        self.assertEqual(datalist, data)
+
+    def test_registered_limit_set_region(self):
+        registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT)
+        region = identity_fakes.REGION
+        region['id'] = 'RegionTwo'
+        region = fakes.FakeResource(
+            None,
+            copy.deepcopy(region),
+            loaded=True
+        )
+        registered_limit['region_id'] = region.id
+        self.registered_limit_mock.update.return_value = fakes.FakeResource(
+            None, registered_limit, loaded=True
+        )
+        self.regions_mock.get.return_value = region
+
+        arglist = [
+            '--region', region.id,
+            identity_fakes.registered_limit_id
+        ]
+        verifylist = [
+            ('region', region.id),
+            ('registered_limit_id', identity_fakes.registered_limit_id)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.registered_limit_mock.update.assert_called_with(
+            identity_fakes.registered_limit_id,
+            service=None,
+            resource_name=None,
+            default_limit=None,
+            description=None,
+            region=region
+        )
+
+        collist = (
+            'default_limit', 'description', 'id', 'region_id', 'resource_name',
+            'service_id'
+        )
+        self.assertEqual(collist, columns)
+        datalist = (
+            identity_fakes.registered_limit_default_limit,
+            None,
+            identity_fakes.registered_limit_id,
+            region.id,
+            identity_fakes.registered_limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+
+class TestRegisteredLimitList(TestRegisteredLimit):
+
+    def setUp(self):
+        super(TestRegisteredLimitList, self).setUp()
+
+        self.registered_limit_mock.get.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.REGISTERED_LIMIT),
+            loaded=True
+        )
+
+        self.cmd = registered_limit.ShowRegisteredLimit(self.app, None)
+
+    def test_limit_show(self):
+        arglist = [identity_fakes.registered_limit_id]
+        verifylist = [
+            ('registered_limit_id', identity_fakes.registered_limit_id)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.registered_limit_mock.get.assert_called_with(
+            identity_fakes.registered_limit_id
+        )
+
+        collist = (
+            'default_limit', 'description', 'id', 'region_id', 'resource_name',
+            'service_id'
+        )
+        self.assertEqual(collist, columns)
+        datalist = (
+            identity_fakes.registered_limit_default_limit,
+            None,
+            identity_fakes.registered_limit_id,
+            None,
+            identity_fakes.registered_limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
diff --git a/releasenotes/notes/bp-unified-limits-58f166401534a4ff.yaml b/releasenotes/notes/bp-unified-limits-58f166401534a4ff.yaml
new file mode 100644
index 0000000000..20050bb264
--- /dev/null
+++ b/releasenotes/notes/bp-unified-limits-58f166401534a4ff.yaml
@@ -0,0 +1,7 @@
+---
+features:
+  - |
+    [`bp unified-limits <https://blueprints.launchpad.net/keystone/+spec/unified-limit>`_]
+    Support has been added for managing registered limits in keystone via the
+    ``registered limit`` command. Registered limits define limits of resources
+    for projects to assume by default.
diff --git a/requirements.txt b/requirements.txt
index c5795fd5ed..175ce74c5c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,6 +12,6 @@ osc-lib>=1.8.0 # Apache-2.0
 oslo.i18n>=3.15.3 # Apache-2.0
 oslo.utils>=3.33.0 # Apache-2.0
 python-glanceclient>=2.8.0 # Apache-2.0
-python-keystoneclient>=3.15.0 # Apache-2.0
+python-keystoneclient>=3.17.0 # Apache-2.0
 python-novaclient>=9.1.0 # Apache-2.0
 python-cinderclient>=3.3.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index ac45a77dcd..af7bbbf20e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -301,6 +301,12 @@ openstack.identity.v3 =
     region_set = openstackclient.identity.v3.region:SetRegion
     region_show = openstackclient.identity.v3.region:ShowRegion
 
+    registered_limit_create = openstackclient.identity.v3.registered_limit:CreateRegisteredLimit
+    registered_limit_delete = openstackclient.identity.v3.registered_limit:DeleteRegisteredLimit
+    registered_limit_list = openstackclient.identity.v3.registered_limit:ListRegisteredLimit
+    registered_limit_set = openstackclient.identity.v3.registered_limit:SetRegisteredLimit
+    registered_limit_show = openstackclient.identity.v3.registered_limit:ShowRegisteredLimit
+
     request_token_authorize = openstackclient.identity.v3.token:AuthorizeRequestToken
     request_token_create = openstackclient.identity.v3.token:CreateRequestToken