Add keystoneng module

This patch adds new keystoneng module
Current Salt 2016 keystone module does not support keystone v3 api.
Salt 2017 keystone module does not support version discovery for
keystone api.
The keystoneng module is based on upstream Salt 2017 keystone module and
has version discovery feature.

The same changes were merged to Salt upsream development branch.
Keystoneng should be switched back to native keystone module when
Salt will support version discovery for keystone API.

Patch Set 25: keystoneng.py is original Salt keystone.py from
              https://raw.githubusercontent.com/saltstack/salt/2017.7.3/salt/modules/keystone.py
Patch Set 26: keystoneng.py with version discovery and other enhancements.

Change-Id: Id8af7bb9f970786fec9586c327da8f8e803bd829
Related-PROD: PROD-15801
diff --git a/_modules/keystoneng.py b/_modules/keystoneng.py
new file mode 100644
index 0000000..66e0ac7
--- /dev/null
+++ b/_modules/keystoneng.py
@@ -0,0 +1,1418 @@
+# -*- coding: utf-8 -*-
+'''
+Module for handling openstack keystone calls.
+
+:optdepends:    - keystoneclient Python adapter
+:configuration: This module is not usable until the following are specified
+    either in a pillar or in the minion's config file:
+
+    .. code-block:: yaml
+
+        keystone.user: admin
+        keystone.password: verybadpass
+        keystone.tenant: admin
+        keystone.tenant_id: f80919baedab48ec8931f200c65a50df
+        keystone.auth_url: 'http://127.0.0.1:5000/v2.0/'
+
+    OR (for token based authentication)
+
+    .. code-block:: yaml
+
+        keystone.token: 'ADMIN'
+        keystone.endpoint: 'http://127.0.0.1:35357/v2.0'
+
+    If configuration for multiple openstack accounts is required, they can be
+    set up as different configuration profiles. For example:
+
+    .. code-block:: yaml
+
+        openstack1:
+          keystone.user: admin
+          keystone.password: verybadpass
+          keystone.tenant: admin
+          keystone.tenant_id: f80919baedab48ec8931f200c65a50df
+          keystone.auth_url: 'http://127.0.0.1:5000/v2.0/'
+
+        openstack2:
+          keystone.user: admin
+          keystone.password: verybadpass
+          keystone.tenant: admin
+          keystone.tenant_id: f80919baedab48ec8931f200c65a50df
+          keystone.auth_url: 'http://127.0.0.2:5000/v2.0/'
+
+        openstack_version3:
+          keystone.user: admin
+          keystone.password: verybadpass
+          keystone.tenant: admin
+          keystone.tenant_id: f80919baedab48ec8931f200c65a50df
+          keystone.auth_url: 'http://127.0.0.2:5000/v3'
+
+        openstack_nonversioned:
+          keystone.user: admin
+          keystone.password: verybadpass
+          keystone.tenant: admin
+          keystone.tenant_id: f80919baedab48ec8931f200c65a50df
+          keystone.auth_url: 'http://127.0.0.2:5000'
+
+    With this configuration in place, any of the keystone functions can make use
+    of a configuration profile by declaring it explicitly.
+    For example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.tenant_list profile=openstack1
+'''
+
+# Import Python libs
+from __future__ import absolute_import
+import logging
+
+# Import Salt Libs
+import salt.utils.http
+
+# Import 3rd-party libs
+from salt.ext import six
+HAS_KEYSTONE = False
+try:
+    # pylint: disable=import-error
+    from keystoneclient import client
+    from keystoneclient.v2_0 import client as keystoneclient_v2
+    import keystoneclient.exceptions
+    HAS_KEYSTONE = True
+    from keystoneclient.v3 import client as keystoneclient_v3
+    from keystoneclient import discover
+    from keystoneauth1 import session
+    from keystoneauth1.identity import generic
+    from keystoneauth1 import token_endpoint
+    # pylint: enable=import-error
+except ImportError:
+    pass
+
+log = logging.getLogger(__name__)
+
+
+def __virtual__():
+    '''
+    Only load this module if keystone
+    is installed on this minion.
+    '''
+    if HAS_KEYSTONE:
+        return 'keystoneng'
+    return (False, 'keystone execution module cannot be loaded: keystoneclient python library not available.')
+
+__opts__ = {}
+
+
+def _get_kwargs(profile=None, **connection_args):
+    '''
+    get connection args
+    '''
+    if profile:
+        prefix = profile + ":keystone."
+    else:
+        prefix = "keystone."
+
+    def get(key, default=None):
+        '''
+        look in connection_args first, then default to config file
+        '''
+        return connection_args.get('connection_' + key,
+                                   __salt__['config.get'](prefix + key, default))
+
+    user = get('user', 'admin')
+    password = get('password', 'ADMIN')
+    tenant = get('tenant', 'admin')
+    tenant_id = get('tenant_id')
+    auth_url = get('auth_url', 'http://127.0.0.1:5000/v2.0/')
+    insecure = get('insecure', False)
+    token = get('token')
+    endpoint = get('endpoint', 'http://127.0.0.1:5000/v2.0')
+    if token:
+        kwargs = {'token': token,
+                  'endpoint': endpoint}
+    else:
+        kwargs = {'username': user,
+                  'password': password,
+                  'tenant_name': tenant,
+                  'tenant_id': tenant_id,
+                  'auth_url': auth_url}
+        # 'insecure' keyword not supported by all v2.0 keystone clients
+        #   this ensures it's only passed in when defineda
+        if get('user_id'): kwargs['user_id']=get('user_id')
+        if get('user_domain_id'): kwargs['user_domain_id']=get('user_domain_id')
+        if get('domain_id'): kwargs['domain_id']=get('domain_id')
+        if get('domain_name'): kwargs['domain_name']=get('domain_name')
+        if get('project_id'): kwargs['project_id']=get('project_id')
+        if get('project_name'): kwargs['project_name']=get('project_name')
+        if get('project_domain_id'): kwargs['project_domain_id']=get('project_domain_id')
+        if get('region_name'): kwargs['region_name']=get('region_name')
+        if get('endpoint'): kwargs['endpoint']=get('endpoint')
+        if get('timeout'): kwargs['timeout']=get('timeout')
+        if get('user_domain_name'): kwargs['user_domain_name']=get('user_domain_name')
+        if get('project_domain_name'): kwargs['project_domain_name']=get('project_domain_name')
+        kwargs['insecure'] = get('insecure', False)
+        if get('version'):
+            version_list = str(get('version')).split('.')
+            if len(version_list) == 2:
+                kwargs['version'] = (version_list[0], version_list[1])
+            else:
+                kwargs['version'] = (version_list[0])
+
+    return kwargs
+
+
+def _client_version(keystone_client):
+    '''
+    Returns keystone client version
+    '''
+    if isinstance(keystone_client, keystoneclient_v3.Client):
+        return 3
+    if isinstance(keystone_client, keystoneclient_v2.Client):
+        return 2
+    return None
+
+
+def _project_mgr(keystone_client):
+    '''
+    Returns keystoneclient.v3.Client.projects object if keystone client version > 2
+    or keystoneclient.v2_0.Client.tenants for other cases.
+    '''
+    if _client_version(keystone_client) > 2:
+        return keystone_client.projects
+    return keystone_client.tenants
+
+
+def api_version(profile=None, **connection_args):
+    '''
+    Returns the API version derived from endpoint's response.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.api_version
+    '''
+    keystone_client = auth(profile, **connection_args)
+    if isinstance(keystone_client, keystoneclient_v3.Client):
+        return 3
+    if isinstance(keystone_client, keystoneclient_v2.Client):
+        return 2
+    return None
+
+
+def auth(profile=None, **connection_args):
+    '''
+    Set up keystone credentials. Only intended to be used within Keystone-enabled modules.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.auth
+    '''
+    kwargs = _get_kwargs(profile=profile, **connection_args)
+    if 'token' in kwargs:
+        auth = token_endpoint.Token(endpoint=kwargs['endpoint'], token=kwargs['token'])
+    else:
+        # keystoneauth1 Password class does not accept some args. Therefore remove it from args for auth.
+        auth_connection_args=kwargs.copy()
+        auth_connection_args.pop('region_name', None)
+        auth_connection_args.pop('version', None)
+        auth_connection_args.pop('insecure', None)
+        auth = generic.Password(**auth_connection_args)
+    if 'insecure' in kwargs:
+         certs_verify = False
+    else:
+        certs_verify = True
+    sess = session.Session(auth=auth, verify=certs_verify)
+    keystone_client=client.Client(session=sess, **kwargs)
+    return keystone_client
+
+
+def ec2_credentials_create(user_id=None, name=None,
+                           tenant_id=None, tenant=None,
+                           profile=None, **connection_args):
+    '''
+    Create EC2-compatible credentials for user per tenant
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.ec2_credentials_create name=admin tenant=admin
+
+        salt '*' keystone.ec2_credentials_create \
+        user_id=c965f79c4f864eaaa9c3b41904e67082 \
+        tenant_id=722787eb540849158668370dc627ec5f
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if name:
+        user_id = user_get(name=name, profile=profile,
+                           **connection_args)[name]['id']
+    if not user_id:
+        return {'Error': 'Could not resolve User ID'}
+
+    if tenant:
+        tenant_id = tenant_get(name=tenant, profile=profile,
+                               **connection_args)[tenant]['id']
+    if not tenant_id:
+        return {'Error': 'Could not resolve Tenant ID'}
+
+    newec2 = kstone.ec2.create(user_id, tenant_id)
+    return {'access': newec2.access,
+            'secret': newec2.secret,
+            'tenant_id': newec2.tenant_id,
+            'user_id': newec2.user_id}
+
+
+def ec2_credentials_delete(user_id=None, name=None, access_key=None,
+                           profile=None, **connection_args):
+    '''
+    Delete EC2-compatible credentials
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.ec2_credentials_delete \
+        860f8c2c38ca4fab989f9bc56a061a64 access_key=5f66d2f24f604b8bb9cd28886106f442
+
+        salt '*' keystone.ec2_credentials_delete name=admin \
+        access_key=5f66d2f24f604b8bb9cd28886106f442
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if name:
+        user_id = user_get(name=name, profile=None, **connection_args)[name]['id']
+    if not user_id:
+        return {'Error': 'Could not resolve User ID'}
+    kstone.ec2.delete(user_id, access_key)
+    return 'ec2 key "{0}" deleted under user id "{1}"'.format(access_key,
+                                                              user_id)
+
+
+def ec2_credentials_get(user_id=None, name=None, access=None,
+                        profile=None, **connection_args):
+    '''
+    Return ec2_credentials for a user (keystone ec2-credentials-get)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.ec2_credentials_get c965f79c4f864eaaa9c3b41904e67082 access=722787eb540849158668370
+        salt '*' keystone.ec2_credentials_get user_id=c965f79c4f864eaaa9c3b41904e67082 access=722787eb540849158668370
+        salt '*' keystone.ec2_credentials_get name=nova access=722787eb540849158668370dc627ec5f
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+    if name:
+        for user in kstone.users.list():
+            if user.name == name:
+                user_id = user.id
+                break
+    if not user_id:
+        return {'Error': 'Unable to resolve user id'}
+    if not access:
+        return {'Error': 'Access key is required'}
+    ec2_credentials = kstone.ec2.get(user_id=user_id, access=access,
+                                     profile=profile, **connection_args)
+    ret[ec2_credentials.user_id] = {'user_id': ec2_credentials.user_id,
+                                    'tenant': ec2_credentials.tenant_id,
+                                    'access': ec2_credentials.access,
+                                    'secret': ec2_credentials.secret}
+    return ret
+
+
+def ec2_credentials_list(user_id=None, name=None, profile=None,
+                         **connection_args):
+    '''
+    Return a list of ec2_credentials for a specific user (keystone ec2-credentials-list)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.ec2_credentials_list 298ce377245c4ec9b70e1c639c89e654
+        salt '*' keystone.ec2_credentials_list user_id=298ce377245c4ec9b70e1c639c89e654
+        salt '*' keystone.ec2_credentials_list name=jack
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+    if name:
+        for user in kstone.users.list():
+            if user.name == name:
+                user_id = user.id
+                break
+    if not user_id:
+        return {'Error': 'Unable to resolve user id'}
+    for ec2_credential in kstone.ec2.list(user_id):
+        ret[ec2_credential.user_id] = {'user_id': ec2_credential.user_id,
+                                       'tenant_id': ec2_credential.tenant_id,
+                                       'access': ec2_credential.access,
+                                       'secret': ec2_credential.secret}
+    return ret
+
+
+def endpoint_get(service, region=None, profile=None, interface=None, **connection_args):
+    '''
+    Return a specific endpoint (keystone endpoint-get)
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt 'v2' keystone.endpoint_get nova [region=RegionOne]
+
+        salt 'v3' keystone.endpoint_get nova interface=admin [region=RegionOne]
+    '''
+    auth(profile, **connection_args)
+    services = service_list(profile, **connection_args)
+    if service not in services:
+        return {'Error': 'Could not find the specified service'}
+    service_id = services[service]['id']
+    endpoints = endpoint_list(profile, **connection_args)
+
+    e = [_f for _f in [e
+          if e['service_id'] == service_id and
+            (e['region'] == region if region else True) and
+            (e['interface'] == interface if interface else True)
+            else None for e in endpoints.values()] if _f]
+    if len(e) > 1:
+        return {'Error': 'Multiple endpoints found ({0}) for the {1} service. Please specify region.'.format(e, service)}
+    if len(e) == 1:
+        return e[0]
+    return {'Error': 'Could not find endpoint for the specified service'}
+
+
+def endpoint_list(profile=None, **connection_args):
+    '''
+    Return a list of available endpoints (keystone endpoints-list)
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.endpoint_list
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+
+    for endpoint in kstone.endpoints.list():
+        ret[endpoint.id] = dict((value, getattr(endpoint, value)) for value in dir(endpoint)
+                                if not value.startswith('_') and
+                                isinstance(getattr(endpoint, value), (six.string_types, dict, bool)))
+    return ret
+
+
+def endpoint_create(service, publicurl=None, internalurl=None, adminurl=None,
+                    region=None, profile=None, url=None, interface=None, **connection_args):
+    '''
+    Create an endpoint for an Openstack service
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt 'v2' keystone.endpoint_create nova 'http://public/url' 'http://internal/url' 'http://adminurl/url' region
+
+        salt 'v3' keystone.endpoint_create nova url='http://public/url' interface='public' region='RegionOne'
+    '''
+    kstone = auth(profile, **connection_args)
+    keystone_service = service_get(name=service, profile=profile,
+                                   **connection_args)
+    if not keystone_service or 'Error' in keystone_service:
+        return {'Error': 'Could not find the specified service'}
+
+    if _client_version(kstone) > 2:
+        kstone.endpoints.create(service=keystone_service[service]['id'],
+                                region_id=region,
+                                url=url,
+                                interface=interface)
+    else:
+        kstone.endpoints.create(region=region,
+                                service_id=keystone_service[service]['id'],
+                                publicurl=publicurl,
+                                adminurl=adminurl,
+                                internalurl=internalurl)
+    return endpoint_get(service, region, profile, interface, **connection_args)
+
+
+def endpoint_delete(service, region=None, profile=None, interface=None, **connection_args):
+    '''
+    Delete endpoints of an Openstack service
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt 'v2' keystone.endpoint_delete nova [region=RegionOne]
+
+        salt 'v3' keystone.endpoint_delete nova interface=admin [region=RegionOne]
+    '''
+    kstone = auth(profile, **connection_args)
+    endpoint = endpoint_get(service, region, profile, interface, **connection_args)
+    if not endpoint or 'Error' in endpoint:
+        return {'Error': 'Could not find any endpoints for the service'}
+    kstone.endpoints.delete(endpoint['id'])
+    endpoint = endpoint_get(service, region, profile, interface, **connection_args)
+    if not endpoint or 'Error' in endpoint:
+        return True
+
+
+def role_create(name, profile=None, **connection_args):
+    '''
+    Create a named role.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.role_create admin
+    '''
+
+    kstone = auth(profile, **connection_args)
+    if 'Error' not in role_get(name=name, profile=profile, **connection_args):
+        return {'Error': 'Role "{0}" already exists'.format(name)}
+    kstone.roles.create(name)
+    return role_get(name=name, profile=profile, **connection_args)
+
+
+def role_delete(role_id=None, name=None, profile=None,
+                **connection_args):
+    '''
+    Delete a role (keystone role-delete)
+
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.role_delete c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.role_delete role_id=c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.role_delete name=admin
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if name:
+        for role in kstone.roles.list():
+            if role.name == name:
+                role_id = role.id
+                break
+    if not role_id:
+        return {'Error': 'Unable to resolve role id'}
+
+    role = kstone.roles.get(role_id)
+    kstone.roles.delete(role)
+
+    ret = 'Role ID {0} deleted'.format(role_id)
+    if name:
+        ret += ' ({0})'.format(name)
+    return ret
+
+
+def role_get(role_id=None, name=None, profile=None, **connection_args):
+    '''
+    Return a specific roles (keystone role-get)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.role_get c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.role_get role_id=c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.role_get name=nova
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+    if name:
+        for role in kstone.roles.list():
+            if role.name == name:
+                role_id = role.id
+                break
+    if not role_id:
+        return {'Error': 'Unable to resolve role id'}
+    role = kstone.roles.get(role_id)
+
+    ret[role.name] = {'id': role.id,
+                      'name': role.name}
+    return ret
+
+
+def role_list(profile=None, **connection_args):
+    '''
+    Return a list of available roles (keystone role-list)
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.role_list
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+    for role in kstone.roles.list():
+        ret[role.name] = dict((value, getattr(role, value)) for value in dir(role)
+                              if not value.startswith('_') and
+                              isinstance(getattr(role, value), (six.string_types, dict, bool)))
+    return ret
+
+
+def service_create(name, service_type, description=None, profile=None,
+                   **connection_args):
+    '''
+    Add service to Keystone service catalog
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.service_create nova compute \
+'OpenStack Compute Service'
+    '''
+    kstone = auth(profile, **connection_args)
+    service = kstone.services.create(name, service_type, description=description)
+    return service_get(service.id, profile=profile, **connection_args)
+
+
+def service_delete(service_id=None, name=None, profile=None, **connection_args):
+    '''
+    Delete a service from Keystone service catalog
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.service_delete c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.service_delete name=nova
+    '''
+    kstone = auth(profile, **connection_args)
+    if name:
+        service_id = service_get(name=name, profile=profile,
+                                 **connection_args)[name]['id']
+    kstone.services.delete(service_id)
+    return 'Keystone service ID "{0}" deleted'.format(service_id)
+
+
+def service_get(service_id=None, name=None, profile=None, **connection_args):
+    '''
+    Return a specific services (keystone service-get)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.service_get c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.service_get service_id=c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.service_get name=nova
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+    if name:
+        for service in kstone.services.list():
+            if service.name == name:
+                service_id = service.id
+                break
+    if not service_id:
+        return {'Error': 'Unable to resolve service id'}
+    service = kstone.services.get(service_id)
+    ret[service.name] = dict((value, getattr(service, value)) for value in dir(service)
+                             if not value.startswith('_') and
+                             isinstance(getattr(service, value), (six.string_types, dict, bool)))
+    return ret
+
+
+def service_list(profile=None, **connection_args):
+    '''
+    Return a list of available services (keystone services-list)
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.service_list
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+    for service in kstone.services.list():
+        ret[service.name] = dict((value, getattr(service, value)) for value in dir(service)
+                                 if not value.startswith('_') and
+                                 isinstance(getattr(service, value), (six.string_types, dict, bool)))
+    return ret
+
+
+def tenant_create(name, description=None, enabled=True, profile=None,
+                  **connection_args):
+    '''
+    Create a keystone tenant
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.tenant_create nova description='nova tenant'
+        salt '*' keystone.tenant_create test enabled=False
+    '''
+    kstone = auth(profile, **connection_args)
+    new = _project_mgr(kstone).create(name, description, enabled)
+    return tenant_get(new.id, profile=profile, **connection_args)
+
+
+def project_create(name, domain, description=None, enabled=True, profile=None,
+                   **connection_args):
+    '''
+    Create a keystone project.
+    Overrides keystone tenant_create form api V2. For keystone api V3.
+
+    .. versionadded:: 2016.11.0
+
+    name
+        The project name, which must be unique within the owning domain.
+
+    domain
+        The domain name.
+
+    description
+        The project description.
+
+    enabled
+        Enables or disables the project.
+
+    profile
+        Configuration profile - if configuration for multiple openstack accounts required.
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.project_create nova default description='Nova Compute Project'
+        salt '*' keystone.project_create test default enabled=False
+    '''
+    kstone = auth(profile, **connection_args)
+    new = _project_mgr(kstone).create(name=name, domain=domain,
+                                                 description=description, enabled=enabled)
+    return tenant_get(new.id, profile=profile, **connection_args)
+
+
+def tenant_delete(tenant_id=None, name=None, profile=None, **connection_args):
+    '''
+    Delete a tenant (keystone tenant-delete)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.tenant_delete c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.tenant_delete tenant_id=c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.tenant_delete name=demo
+    '''
+    kstone = auth(profile, **connection_args)
+    if name:
+        for tenant in _project_mgr(kstone).list():
+            if tenant.name == name:
+                tenant_id = tenant.id
+                break
+    if not tenant_id:
+        return {'Error': 'Unable to resolve tenant id'}
+    _project_mgr(kstone).delete(tenant_id)
+    ret = 'Tenant ID {0} deleted'.format(tenant_id)
+    if name:
+
+        ret += ' ({0})'.format(name)
+    return ret
+
+
+def project_delete(project_id=None, name=None, profile=None, **connection_args):
+    '''
+    Delete a project (keystone project-delete).
+    Overrides keystone tenant-delete form api V2. For keystone api V3 only.
+
+    .. versionadded:: 2016.11.0
+
+    project_id
+        The project id.
+
+    name
+        The project name.
+
+    profile
+        Configuration profile - if configuration for multiple openstack accounts required.
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.project_delete c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.project_delete project_id=c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.project_delete name=demo
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if _client_version(kstone) > 2:
+        return tenant_delete(tenant_id=project_id, name=name, profile=None, **connection_args)
+    else:
+        return False
+
+
+def tenant_get(tenant_id=None, name=None, profile=None,
+               **connection_args):
+    '''
+    Return a specific tenants (keystone tenant-get)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.tenant_get c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.tenant_get tenant_id=c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.tenant_get name=nova
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+
+    if name:
+        for tenant in _project_mgr(kstone).list():
+            if tenant.name == name:
+                tenant_id = tenant.id
+                break
+    if not tenant_id:
+        return {'Error': 'Unable to resolve tenant id'}
+    tenant = _project_mgr(kstone).get(tenant_id)
+    ret[tenant.name] = dict((value, getattr(tenant, value)) for value in dir(tenant)
+                            if not value.startswith('_') and
+                            isinstance(getattr(tenant, value), (six.string_types, dict, bool)))
+    return ret
+
+
+def project_get(project_id=None, name=None, profile=None, **connection_args):
+    '''
+    Return a specific projects (keystone project-get)
+    Overrides keystone tenant-get form api V2.
+    For keystone api V3 only.
+
+    .. versionadded:: 2016.11.0
+
+    project_id
+        The project id.
+
+    name
+        The project name.
+
+    profile
+        Configuration profile - if configuration for multiple openstack accounts required.
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.project_get c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.project_get project_id=c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.project_get name=nova
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if _client_version(kstone) > 2:
+        return tenant_get(tenant_id=project_id, name=name, profile=None, **connection_args)
+    else:
+        return False
+
+
+def tenant_list(profile=None, **connection_args):
+    '''
+    Return a list of available tenants (keystone tenants-list)
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.tenant_list
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+
+    for tenant in _project_mgr(kstone).list():
+        ret[tenant.name] = dict((value, getattr(tenant, value)) for value in dir(tenant)
+                                if not value.startswith('_') and
+                                isinstance(getattr(tenant, value), (six.string_types, dict, bool)))
+    return ret
+
+
+def project_list(profile=None, **connection_args):
+    '''
+    Return a list of available projects (keystone projects-list).
+    Overrides keystone tenants-list form api V2.
+    For keystone api V3 only.
+
+    .. versionadded:: 2016.11.0
+
+    profile
+        Configuration profile - if configuration for multiple openstack accounts required.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.project_list
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if _client_version(kstone) > 2:
+        return tenant_list(profile, **connection_args)
+    else:
+        return False
+
+
+def tenant_update(tenant_id=None, name=None, description=None,
+                  enabled=None, profile=None, **connection_args):
+    '''
+    Update a tenant's information (keystone tenant-update)
+    The following fields may be updated: name, description, enabled.
+    Can only update name if targeting by ID
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.tenant_update name=admin enabled=True
+        salt '*' keystone.tenant_update c965f79c4f864eaaa9c3b41904e67082 name=admin email=admin@domain.com
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if not tenant_id:
+        for tenant in _project_mgr(kstone).list():
+            if tenant.name == name:
+                tenant_id = tenant.id
+                break
+    if not tenant_id:
+        return {'Error': 'Unable to resolve tenant id'}
+
+    tenant = _project_mgr(kstone).get(tenant_id)
+    if not name:
+        name = tenant.name
+    if not description:
+        description = tenant.description
+    if enabled is None:
+        enabled = tenant.enabled
+    updated = _project_mgr(kstone).update(tenant_id, name=name, description=description, enabled=enabled)
+
+    return dict((value, getattr(updated, value)) for value in dir(updated)
+                if not value.startswith('_') and
+                isinstance(getattr(updated, value), (six.string_types, dict, bool)))
+
+
+def project_update(project_id=None, name=None, description=None,
+                   enabled=None, profile=None, **connection_args):
+    '''
+    Update a tenant's information (keystone project-update)
+    The following fields may be updated: name, description, enabled.
+    Can only update name if targeting by ID
+
+    Overrides keystone tenant_update form api V2.
+    For keystone api V3 only.
+
+    .. versionadded:: 2016.11.0
+
+    project_id
+        The project id.
+
+    name
+        The project name, which must be unique within the owning domain.
+
+    description
+        The project description.
+
+    enabled
+        Enables or disables the project.
+
+    profile
+        Configuration profile - if configuration for multiple openstack accounts required.
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.project_update name=admin enabled=True
+        salt '*' keystone.project_update c965f79c4f864eaaa9c3b41904e67082 name=admin email=admin@domain.com
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if _client_version(kstone) > 2:
+        return tenant_update(tenant_id=project_id, name=name, description=description,
+                             enabled=enabled, profile=profile, **connection_args)
+    else:
+        return False
+
+
+def token_get(profile=None, **connection_args):
+    '''
+    Return the configured tokens (keystone token-get)
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.token_get c965f79c4f864eaaa9c3b41904e67082
+    '''
+    kstone = auth(profile, **connection_args)
+    token = kstone.service_catalog.get_token()
+    return {'id': token['id'],
+            'expires': token['expires'],
+            'user_id': token['user_id'],
+            'tenant_id': token['tenant_id']}
+
+
+def user_list(profile=None, **connection_args):
+    '''
+    Return a list of available users (keystone user-list)
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.user_list
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+    for user in kstone.users.list():
+        ret[user.name] = dict((value, getattr(user, value, None)) for value in dir(user)
+                              if not value.startswith('_') and
+                              isinstance(getattr(user, value, None), (six.string_types, dict, bool)))
+        tenant_id = getattr(user, 'tenantId', None)
+        if tenant_id:
+            ret[user.name]['tenant_id'] = tenant_id
+    return ret
+
+
+def user_get(user_id=None, name=None, profile=None, **connection_args):
+    '''
+    Return a specific users (keystone user-get)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.user_get c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.user_get user_id=c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.user_get name=nova
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+    if name:
+        for user in kstone.users.list():
+            if user.name == name:
+                user_id = user.id
+                break
+    if not user_id:
+        return {'Error': 'Unable to resolve user id'}
+    try:
+        user = kstone.users.get(user_id)
+    except keystoneclient.exceptions.NotFound:
+        msg = 'Could not find user \'{0}\''.format(user_id)
+        log.error(msg)
+        return {'Error': msg}
+
+    ret[user.name] = dict((value, getattr(user, value, None)) for value in dir(user)
+                          if not value.startswith('_') and
+                          isinstance(getattr(user, value, None), (six.string_types, dict, bool)))
+
+    tenant_id = getattr(user, 'tenantId', None)
+    if tenant_id:
+        ret[user.name]['tenant_id'] = tenant_id
+    return ret
+
+
+def user_create(name, password, email, tenant_id=None,
+                enabled=True, profile=None, project_id=None, description=None, **connection_args):
+    '''
+    Create a user (keystone user-create)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.user_create name=jack password=zero email=jack@halloweentown.org \
+        tenant_id=a28a7b5a999a455f84b1f5210264375e enabled=True
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if _client_version(kstone) > 2:
+        if tenant_id and not project_id:
+            project_id = tenant_id
+        item = kstone.users.create(name=name,
+                                   password=password,
+                                   email=email,
+                                   project_id=project_id,
+                                   enabled=enabled,
+                                   description=description)
+    else:
+        item = kstone.users.create(name=name,
+                                   password=password,
+                                   email=email,
+                                   tenant_id=tenant_id,
+                                   enabled=enabled)
+    return user_get(item.id, profile=profile, **connection_args)
+
+
+def user_delete(user_id=None, name=None, profile=None, **connection_args):
+    '''
+    Delete a user (keystone user-delete)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.user_delete c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.user_delete user_id=c965f79c4f864eaaa9c3b41904e67082
+        salt '*' keystone.user_delete name=nova
+    '''
+    kstone = auth(profile, **connection_args)
+    if name:
+        for user in kstone.users.list():
+            if user.name == name:
+                user_id = user.id
+                break
+    if not user_id:
+        return {'Error': 'Unable to resolve user id'}
+    kstone.users.delete(user_id)
+    ret = 'User ID {0} deleted'.format(user_id)
+    if name:
+
+        ret += ' ({0})'.format(name)
+    return ret
+
+
+def user_update(user_id=None, name=None, email=None, enabled=None,
+                tenant=None, profile=None, project=None, description=None, **connection_args):
+    '''
+    Update a user's information (keystone user-update)
+    The following fields may be updated: name, email, enabled, tenant.
+    Because the name is one of the fields, a valid user id is required.
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.user_update user_id=c965f79c4f864eaaa9c3b41904e67082 name=newname
+        salt '*' keystone.user_update c965f79c4f864eaaa9c3b41904e67082 name=newname email=newemail@domain.com
+    '''
+    kstone = auth(profile, **connection_args)
+    if not user_id:
+        for user in kstone.users.list():
+            if user.name == name:
+                user_id = user.id
+                break
+        if not user_id:
+            return {'Error': 'Unable to resolve user id'}
+    user = kstone.users.get(user_id)
+    # Keep previous settings if not updating them
+    if not name:
+        name = user.name
+    if not email:
+        email = user.email
+    if enabled is None:
+        enabled = user.enabled
+
+    if _client_version(kstone) > 2:
+        if description is None:
+            description = getattr(user, 'description', None)
+        else:
+            description = str(description)
+
+        project_id = None
+        if project:
+            for proj in kstone.projects.list():
+                if proj.name == project:
+                    project_id = proj.id
+                    break
+        if not project_id:
+            project_id = getattr(user, 'project_id', None)
+
+        kstone.users.update(user=user_id, name=name, email=email, enabled=enabled, description=description,
+                            project_id=project_id)
+    else:
+        kstone.users.update(user=user_id, name=name, email=email, enabled=enabled)
+
+        tenant_id = None
+        if tenant:
+            for tnt in kstone.tenants.list():
+                if tnt.name == tenant:
+                    tenant_id = tnt.id
+                    break
+            if tenant_id:
+                kstone.users.update_tenant(user_id, tenant_id)
+
+    ret = 'Info updated for user ID {0}'.format(user_id)
+    return ret
+
+
+def user_verify_password(user_id=None, name=None, password=None,
+                         profile=None, **connection_args):
+    '''
+    Verify a user's password
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.user_verify_password name=test password=foobar
+        salt '*' keystone.user_verify_password user_id=c965f79c4f864eaaa9c3b41904e67082 password=foobar
+    '''
+    kstone = auth(profile, **connection_args)
+    if 'connection_endpoint' in connection_args:
+        auth_url = connection_args.get('connection_endpoint')
+    else:
+        if _client_version(kstone) > 2:
+            auth_url = __salt__['config.option']('keystone.endpoint',
+                                                 'http://127.0.0.1:35357/v3')
+        else:
+            auth_url = __salt__['config.option']('keystone.endpoint',
+                                                 'http://127.0.0.1:35357/v2.0')
+
+    if user_id:
+        for user in kstone.users.list():
+            if user.id == user_id:
+                name = user.name
+                break
+    if not name:
+        return {'Error': 'Unable to resolve user name'}
+    kwargs = {'username': name,
+              'password': password,
+              'auth_url': auth_url}
+    try:
+        if _client_version(kstone) > 2:
+            client3.Client(**kwargs)
+        else:
+            client.Client(**kwargs)
+    except (keystoneclient.exceptions.Unauthorized,
+            keystoneclient.exceptions.AuthorizationFailure):
+        return False
+    return True
+
+
+def user_password_update(user_id=None, name=None, password=None,
+                         profile=None, **connection_args):
+    '''
+    Update a user's password (keystone user-password-update)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.user_password_update c965f79c4f864eaaa9c3b41904e67082 password=12345
+        salt '*' keystone.user_password_update user_id=c965f79c4f864eaaa9c3b41904e67082 password=12345
+        salt '*' keystone.user_password_update name=nova password=12345
+    '''
+    kstone = auth(profile, **connection_args)
+    if name:
+        for user in kstone.users.list():
+            if user.name == name:
+                user_id = user.id
+                break
+    if not user_id:
+        return {'Error': 'Unable to resolve user id'}
+
+    if _client_version(kstone) > 2:
+        kstone.users.update(user=user_id, password=password)
+    else:
+        kstone.users.update_password(user=user_id, password=password)
+    ret = 'Password updated for user ID {0}'.format(user_id)
+    if name:
+        ret += ' ({0})'.format(name)
+    return ret
+
+
+def user_role_add(user_id=None, user=None, tenant_id=None,
+                  tenant=None, role_id=None, role=None, profile=None,
+                  project_id=None, project_name=None, **connection_args):
+    '''
+    Add role for user in tenant (keystone user-role-add)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.user_role_add \
+user_id=298ce377245c4ec9b70e1c639c89e654 \
+tenant_id=7167a092ece84bae8cead4bf9d15bb3b \
+role_id=ce377245c4ec9b70e1c639c89e8cead4
+        salt '*' keystone.user_role_add user=admin tenant=admin role=admin
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if project_id and not tenant_id:
+        tenant_id = project_id
+    elif project_name and not tenant:
+        tenant = project_name
+
+    if user:
+        user_id = user_get(name=user, profile=profile,
+                           **connection_args)[user].get('id')
+    else:
+        user = next(six.iterkeys(user_get(user_id, profile=profile,
+                                          **connection_args)))['name']
+    if not user_id:
+        return {'Error': 'Unable to resolve user id'}
+
+    if tenant:
+        tenant_id = tenant_get(name=tenant, profile=profile,
+                               **connection_args)[tenant].get('id')
+    else:
+        tenant = next(six.iterkeys(tenant_get(tenant_id, profile=profile,
+                                              **connection_args)))['name']
+    if not tenant_id:
+        return {'Error': 'Unable to resolve tenant/project id'}
+
+    if role:
+        role_id = role_get(name=role, profile=profile,
+                           **connection_args)[role]['id']
+    else:
+        role = next(six.iterkeys(role_get(role_id, profile=profile,
+                                          **connection_args)))['name']
+    if not role_id:
+        return {'Error': 'Unable to resolve role id'}
+
+    if _client_version(kstone) > 2:
+        kstone.roles.grant(role_id, user=user_id, project=tenant_id)
+    else:
+        kstone.roles.add_user_role(user_id, role_id, tenant_id)
+    ret_msg = '"{0}" role added for user "{1}" for "{2}" tenant/project'
+    return ret_msg.format(role, user, tenant)
+
+
+def user_role_remove(user_id=None, user=None, tenant_id=None,
+                     tenant=None, role_id=None, role=None,
+                     profile=None, project_id=None, project_name=None, **connection_args):
+    '''
+    Remove role for user in tenant (keystone user-role-remove)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.user_role_remove \
+user_id=298ce377245c4ec9b70e1c639c89e654 \
+tenant_id=7167a092ece84bae8cead4bf9d15bb3b \
+role_id=ce377245c4ec9b70e1c639c89e8cead4
+        salt '*' keystone.user_role_remove user=admin tenant=admin role=admin
+    '''
+    kstone = auth(profile, **connection_args)
+
+    if project_id and not tenant_id:
+        tenant_id = project_id
+    elif project_name and not tenant:
+        tenant = project_name
+
+    if user:
+        user_id = user_get(name=user, profile=profile,
+                           **connection_args)[user].get('id')
+    else:
+        user = next(six.iterkeys(user_get(user_id, profile=profile,
+                                          **connection_args)))['name']
+    if not user_id:
+        return {'Error': 'Unable to resolve user id'}
+
+    if tenant:
+        tenant_id = tenant_get(name=tenant, profile=profile,
+                               **connection_args)[tenant].get('id')
+    else:
+        tenant = next(six.iterkeys(tenant_get(tenant_id, profile=profile,
+                                              **connection_args)))['name']
+    if not tenant_id:
+        return {'Error': 'Unable to resolve tenant/project id'}
+
+    if role:
+        role_id = role_get(name=role, profile=profile,
+                           **connection_args)[role]['id']
+    else:
+        role = next(six.iterkeys(role_get(role_id)))['name']
+    if not role_id:
+        return {'Error': 'Unable to resolve role id'}
+
+    if _client_version(kstone) > 2:
+        kstone.roles.revoke(role_id, user=user_id, project=tenant_id)
+    else:
+        kstone.roles.remove_user_role(user_id, role_id, tenant_id)
+    ret_msg = '"{0}" role removed for user "{1}" under "{2}" tenant'
+    return ret_msg.format(role, user, tenant)
+
+
+def user_role_list(user_id=None, tenant_id=None, user_name=None,
+                   tenant_name=None, profile=None, project_id=None, project_name=None, **connection_args):
+    '''
+    Return a list of available user_roles (keystone user-roles-list)
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' keystone.user_role_list \
+user_id=298ce377245c4ec9b70e1c639c89e654 \
+tenant_id=7167a092ece84bae8cead4bf9d15bb3b
+        salt '*' keystone.user_role_list user_name=admin tenant_name=admin
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = {}
+
+    if project_id and not tenant_id:
+        tenant_id = project_id
+    elif project_name and not tenant_name:
+        tenant_name = project_name
+
+    if user_name:
+        for user in kstone.users.list():
+            if user.name == user_name:
+                user_id = user.id
+                break
+    if tenant_name:
+        for tenant in _project_mgr(kstone).list():
+            if tenant.name == tenant_name:
+                tenant_id = tenant.id
+                break
+    if not user_id or not tenant_id:
+        return {'Error': 'Unable to resolve user or tenant/project id'}
+
+    if _client_version(kstone) > 2:
+        for role in kstone.roles.list(user=user_id, project=tenant_id):
+            ret[role.name] = dict((value, getattr(role, value)) for value in dir(role)
+                                  if not value.startswith('_') and
+                                  isinstance(getattr(role, value), (six.string_types, dict, bool)))
+    else:
+        for role in kstone.roles.roles_for_user(user=user_id, tenant=tenant_id):
+            ret[role.name] = {'id': role.id,
+                              'name': role.name,
+                              'user_id': user_id,
+                              'tenant_id': tenant_id}
+    return ret
+
+
+def _item_list(profile=None, **connection_args):
+    '''
+    Template for writing list functions
+    Return a list of available items (keystone items-list)
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' keystone.item_list
+    '''
+    kstone = auth(profile, **connection_args)
+    ret = []
+    for item in kstone.items.list():
+        ret.append(item.__dict__)
+        # ret[item.name] = {
+        #         'id': item.id,
+        #         'name': item.name,
+        #         }
+    return ret
+
+    # The following is a list of functions that need to be incorporated in the
+    # keystone module. This list should be updated as functions are added.
+    #
+    # endpoint-create     Create a new endpoint associated with a service
+    # endpoint-delete     Delete a service endpoint
+    # discover            Discover Keystone servers and show authentication
+    #                     protocols and
+    # bootstrap           Grants a new role to a new user on a new tenant, after
+    #                     creating each.
diff --git a/_states/keystoneng.py b/_states/keystoneng.py
new file mode 100644
index 0000000..060ccfb
--- /dev/null
+++ b/_states/keystoneng.py
@@ -0,0 +1,810 @@
+# -*- coding: utf-8 -*-
+'''
+Management of Keystone users
+============================
+
+:depends:   - keystoneclient Python module
+:configuration: See :py:mod:`salt.modules.keystone` for setup instructions.
+
+.. code-block:: yaml
+
+    Keystone tenants:
+      keystoneng.tenant_present:
+        - names:
+          - admin
+          - demo
+          - service
+
+    Keystone roles:
+      keystoneng.role_present:
+        - names:
+          - admin
+          - Member
+
+    admin:
+      keystoneng.user_present:
+        - password: R00T_4CC3SS
+        - email: admin@domain.com
+        - roles:
+            admin:   # tenants
+              - admin  # roles
+            service:
+              - admin
+              - Member
+        - require:
+          - keystone: Keystone tenants
+          - keystone: Keystone roles
+
+    nova:
+      keystoneng.user_present:
+        - password: '$up3rn0v4'
+        - email: nova@domain.com
+        - tenant: service
+        - roles:
+            service:
+              - admin
+        - require:
+          - keystone: Keystone tenants
+          - keystone: Keystone roles
+
+    demo:
+      keystoneng.user_present:
+        - password: 'd3m0n$trati0n'
+        - email: demo@domain.com
+        - tenant: demo
+        - roles:
+            demo:
+              - Member
+        - require:
+          - keystone: Keystone tenants
+          - keystone: Keystone roles
+
+    nova service:
+      keystoneng.service_present:
+        - name: nova
+        - service_type: compute
+        - description: OpenStack Compute Service
+
+'''
+
+
+def __virtual__():
+    '''
+    Only load if the keystone module is in __salt__
+    '''
+    return 'keystoneng' if 'keystoneng.auth' in __salt__ else False
+
+
+_OS_IDENTITY_API_VERSION = 2
+_TENANT_ID = 'tenant_id'
+
+
+def _api_version(profile=None, **connection_args):
+    '''
+    Sets global variables _OS_IDENTITY_API_VERSION and _TENANT_ID
+    depending on API version.
+    '''
+    global _TENANT_ID
+    global _OS_IDENTITY_API_VERSION
+    try:
+        if float(__salt__['keystoneng.api_version'](profile=profile, **connection_args)) >= 3:
+            _TENANT_ID = 'project_id'
+            _OS_IDENTITY_API_VERSION = 3
+    except KeyError:
+        pass
+
+
+def user_present(name,
+                 password,
+                 email,
+                 tenant=None,
+                 enabled=True,
+                 roles=None,
+                 profile=None,
+                 password_reset=True,
+                 project=None,
+                 **connection_args):
+    '''
+    Ensure that the keystone user is present with the specified properties.
+
+    name
+        The name of the user to manage
+
+    password
+        The password to use for this user.
+
+        .. note::
+
+            If the user already exists and a different password was set for
+            the user than the one specified here, the password for the user
+            will be updated. Please set the ``password_reset`` option to
+            ``False`` if this is not the desired behavior.
+
+    password_reset
+        Whether or not to reset password after initial set. Defaults to
+        ``True``.
+
+    email
+        The email address for this user
+
+    tenant
+        The tenant (name) for this user
+
+    project
+        The project (name) for this user (overrides tenant in api v3)
+
+    enabled
+        Availability state for this user
+
+    roles
+        The roles the user should have under given tenants.
+        Passed as a dictionary mapping tenant names to a list
+        of roles in this tenant, i.e.::
+
+            roles:
+                admin:   # tenant
+                  - admin  # role
+                service:
+                  - admin
+                  - Member
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'User "{0}" will be updated'.format(name)}
+
+    _api_version(profile=profile, **connection_args)
+
+    if project and not tenant:
+        tenant = project
+
+    # Validate tenant if set
+    if tenant is not None:
+        tenantdata = __salt__['keystoneng.tenant_get'](name=tenant,
+                                                     profile=profile,
+                                                     **connection_args)
+        if 'Error' in tenantdata:
+            ret['result'] = False
+            ret['comment'] = 'Tenant / project "{0}" does not exist'.format(tenant)
+            return ret
+        tenant_id = tenantdata[tenant]['id']
+    else:
+        tenant_id = None
+
+    # Check if user is already present
+    user = __salt__['keystoneng.user_get'](name=name, profile=profile,
+                                         **connection_args)
+    if 'Error' not in user:
+
+        change_email = False
+        change_enabled = False
+        change_tenant = False
+        change_password = False
+
+        if user[name].get('email', None) != email:
+            change_email = True
+
+        if user[name].get('enabled', None) != enabled:
+            change_enabled = True
+
+        if tenant and (_TENANT_ID not in user[name] or
+                       user[name].get(_TENANT_ID, None) != tenant_id):
+            change_tenant = True
+
+        if (password_reset is True and
+            not __salt__['keystoneng.user_verify_password'](name=name,
+                                                          password=password,
+                                                          profile=profile,
+                                                          **connection_args)):
+            change_password = True
+
+        if __opts__.get('test') and (change_email or change_enabled or change_tenant or change_password):
+            ret['result'] = None
+            ret['comment'] = 'User "{0}" will be updated'.format(name)
+            if change_email is True:
+                ret['changes']['Email'] = 'Will be updated'
+            if change_enabled is True:
+                ret['changes']['Enabled'] = 'Will be True'
+            if change_tenant is True:
+                ret['changes']['Tenant'] = 'Will be added to "{0}" tenant'.format(tenant)
+            if change_password is True:
+                ret['changes']['Password'] = 'Will be updated'
+            return ret
+
+        ret['comment'] = 'User "{0}" is already present'.format(name)
+
+        if change_email:
+            __salt__['keystoneng.user_update'](name=name, email=email, profile=profile, **connection_args)
+            ret['comment'] = 'User "{0}" has been updated'.format(name)
+            ret['changes']['Email'] = 'Updated'
+
+        if change_enabled:
+            __salt__['keystoneng.user_update'](name=name, enabled=enabled, profile=profile, **connection_args)
+            ret['comment'] = 'User "{0}" has been updated'.format(name)
+            ret['changes']['Enabled'] = 'Now {0}'.format(enabled)
+
+        if change_tenant:
+            __salt__['keystoneng.user_update'](name=name, tenant=tenant, profile=profile, **connection_args)
+            ret['comment'] = 'User "{0}" has been updated'.format(name)
+            ret['changes']['Tenant'] = 'Added to "{0}" tenant'.format(tenant)
+
+        if change_password:
+            __salt__['keystoneng.user_password_update'](name=name, password=password, profile=profile,
+                                                      **connection_args)
+            ret['comment'] = 'User "{0}" has been updated'.format(name)
+            ret['changes']['Password'] = 'Updated'
+
+        if roles:
+            for tenant in roles:
+                args = dict({'user_name': name, 'tenant_name':
+                             tenant, 'profile': profile}, **connection_args)
+                tenant_roles = __salt__['keystoneng.user_role_list'](**args)
+                for role in roles[tenant]:
+                    if role not in tenant_roles:
+                        if __opts__.get('test'):
+                            ret['result'] = None
+                            ret['comment'] = 'User roles "{0}" will been updated'.format(name)
+                            return ret
+                        addargs = dict({'user': name, 'role': role,
+                                        'tenant': tenant,
+                                        'profile': profile},
+                                       **connection_args)
+                        newrole = __salt__['keystoneng.user_role_add'](**addargs)
+                        if 'roles' in ret['changes']:
+                            ret['changes']['roles'].append(newrole)
+                        else:
+                            ret['changes']['roles'] = [newrole]
+                roles_to_remove = list(set(tenant_roles) - set(roles[tenant]))
+                for role in roles_to_remove:
+                    if __opts__.get('test'):
+                        ret['result'] = None
+                        ret['comment'] = 'User roles "{0}" will been updated'.format(name)
+                        return ret
+                    addargs = dict({'user': name, 'role': role,
+                                    'tenant': tenant,
+                                    'profile': profile},
+                                   **connection_args)
+                    oldrole = __salt__['keystoneng.user_role_remove'](**addargs)
+                    if 'roles' in ret['changes']:
+                        ret['changes']['roles'].append(oldrole)
+                    else:
+                        ret['changes']['roles'] = [oldrole]
+    else:
+        # Create that user!
+        if __opts__.get('test'):
+            ret['result'] = None
+            ret['comment'] = 'Keystone user "{0}" will be added'.format(name)
+            ret['changes']['User'] = 'Will be created'
+            return ret
+        __salt__['keystoneng.user_create'](name=name,
+                                         password=password,
+                                         email=email,
+                                         tenant_id=tenant_id,
+                                         enabled=enabled,
+                                         profile=profile,
+                                         **connection_args)
+        if roles:
+            for tenant in roles:
+                for role in roles[tenant]:
+                    __salt__['keystoneng.user_role_add'](user=name,
+                                                       role=role,
+                                                       tenant=tenant,
+                                                       profile=profile,
+                                                       **connection_args)
+        ret['comment'] = 'Keystone user {0} has been added'.format(name)
+        ret['changes']['User'] = 'Created'
+
+    return ret
+
+
+def user_absent(name, profile=None, **connection_args):
+    '''
+    Ensure that the keystone user is absent.
+
+    name
+        The name of the user that should not exist
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'User "{0}" is already absent'.format(name)}
+
+    # Check if user is present
+    user = __salt__['keystoneng.user_get'](name=name, profile=profile,
+                                         **connection_args)
+    if 'Error' not in user:
+        if __opts__.get('test'):
+            ret['result'] = None
+            ret['comment'] = 'User "{0}" will be deleted'.format(name)
+            return ret
+        # Delete that user!
+        __salt__['keystoneng.user_delete'](name=name, profile=profile,
+                                         **connection_args)
+        ret['comment'] = 'User "{0}" has been deleted'.format(name)
+        ret['changes']['User'] = 'Deleted'
+
+    return ret
+
+
+def tenant_present(name, description=None, enabled=True, profile=None,
+                   **connection_args):
+    '''
+    Ensures that the keystone tenant exists
+
+    name
+        The name of the tenant to manage
+
+    description
+        The description to use for this tenant
+
+    enabled
+        Availability state for this tenant
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Tenant / project "{0}" already exists'.format(name)}
+
+    _api_version(profile=profile, **connection_args)
+
+    # Check if tenant is already present
+    tenant = __salt__['keystoneng.tenant_get'](name=name,
+                                             profile=profile,
+                                             **connection_args)
+
+    if 'Error' not in tenant:
+        if tenant[name].get('description', None) != description:
+            if __opts__.get('test'):
+                ret['result'] = None
+                ret['comment'] = 'Tenant / project "{0}" will be updated'.format(name)
+                ret['changes']['Description'] = 'Will be updated'
+                return ret
+            __salt__['keystoneng.tenant_update'](name=name,
+                                               description=description,
+                                               enabled=enabled,
+                                               profile=profile,
+                                               **connection_args)
+            ret['comment'] = 'Tenant / project "{0}" has been updated'.format(name)
+            ret['changes']['Description'] = 'Updated'
+        if tenant[name].get('enabled', None) != enabled:
+            if __opts__.get('test'):
+                ret['result'] = None
+                ret['comment'] = 'Tenant / project "{0}" will be updated'.format(name)
+                ret['changes']['Enabled'] = 'Will be {0}'.format(enabled)
+                return ret
+            __salt__['keystoneng.tenant_update'](name=name,
+                                               description=description,
+                                               enabled=enabled,
+                                               profile=profile,
+                                               **connection_args)
+            ret['comment'] = 'Tenant / project "{0}" has been updated'.format(name)
+            ret['changes']['Enabled'] = 'Now {0}'.format(enabled)
+    else:
+        if __opts__.get('test'):
+            ret['result'] = None
+            ret['comment'] = 'Tenant / project "{0}" will be added'.format(name)
+            ret['changes']['Tenant'] = 'Will be created'
+            return ret
+        # Create tenant
+        if _OS_IDENTITY_API_VERSION > 2:
+            created = __salt__['keystoneng.project_create'](name=name, domain='default', description=description,
+                                                          enabled=enabled, profile=profile, **connection_args)
+        else:
+            created = __salt__['keystoneng.tenant_create'](name=name, description=description, enabled=enabled,
+                                                         profile=profile, **connection_args)
+        ret['changes']['Tenant'] = 'Created' if created is True else 'Failed'
+        ret['result'] = created
+        ret['comment'] = 'Tenant / project "{0}" has been added'.format(name)
+    return ret
+
+
+def tenant_absent(name, profile=None, **connection_args):
+    '''
+    Ensure that the keystone tenant is absent.
+
+    name
+        The name of the tenant that should not exist
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Tenant / project "{0}" is already absent'.format(name)}
+
+    # Check if tenant is present
+    tenant = __salt__['keystoneng.tenant_get'](name=name,
+                                             profile=profile,
+                                             **connection_args)
+    if 'Error' not in tenant:
+        if __opts__.get('test'):
+            ret['result'] = None
+            ret['comment'] = 'Tenant / project "{0}" will be deleted'.format(name)
+            return ret
+        # Delete tenant
+        __salt__['keystoneng.tenant_delete'](name=name, profile=profile,
+                                           **connection_args)
+        ret['comment'] = 'Tenant / project "{0}" has been deleted'.format(name)
+        ret['changes']['Tenant/Project'] = 'Deleted'
+
+    return ret
+
+
+def project_present(name, description=None, enabled=True, profile=None,
+                    **connection_args):
+    '''
+    Ensures that the keystone project exists
+    Alias for tenant_present from V2 API to fulfill
+    V3 API naming convention.
+
+    .. versionadded:: 2016.11.0
+
+    name
+        The name of the project to manage
+
+    description
+        The description to use for this project
+
+    enabled
+        Availability state for this project
+
+    .. code-block:: yaml
+
+        nova:
+            keystoneng.project_present:
+                - enabled: True
+                - description: 'Nova Compute Service'
+
+    '''
+
+    return tenant_present(name, description=description, enabled=enabled, profile=profile,
+                          **connection_args)
+
+
+def project_absent(name, profile=None, **connection_args):
+    '''
+    Ensure that the keystone project is absent.
+    Alias for tenant_absent from V2 API to fulfill
+    V3 API naming convention.
+
+    .. versionadded:: 2016.11.0
+
+    name
+        The name of the project that should not exist
+
+    .. code-block:: yaml
+
+        delete_nova:
+            keystoneng.project_absent:
+                - name: nova
+    '''
+
+    return tenant_absent(name, profile=profile, **connection_args)
+
+
+def role_present(name, profile=None, **connection_args):
+    ''''
+    Ensures that the keystone role exists
+
+    name
+        The name of the role that should be present
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Role "{0}" already exists'.format(name)}
+
+    # Check if role is already present
+    role = __salt__['keystoneng.role_get'](name=name, profile=profile,
+                                         **connection_args)
+
+    if 'Error' not in role:
+        return ret
+    else:
+        if __opts__.get('test'):
+            ret['result'] = None
+            ret['comment'] = 'Role "{0}" will be added'.format(name)
+            return ret
+        # Create role
+        __salt__['keystoneng.role_create'](name, profile=profile,
+                                         **connection_args)
+        ret['comment'] = 'Role "{0}" has been added'.format(name)
+        ret['changes']['Role'] = 'Created'
+    return ret
+
+
+def role_absent(name, profile=None, **connection_args):
+    '''
+    Ensure that the keystone role is absent.
+
+    name
+        The name of the role that should not exist
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Role "{0}" is already absent'.format(name)}
+
+    # Check if role is present
+    role = __salt__['keystoneng.role_get'](name=name, profile=profile,
+                                         **connection_args)
+    if 'Error' not in role:
+        if __opts__.get('test'):
+            ret['result'] = None
+            ret['comment'] = 'Role "{0}" will be deleted'.format(name)
+            return ret
+        # Delete role
+        __salt__['keystoneng.role_delete'](name=name, profile=profile,
+                                         **connection_args)
+        ret['comment'] = 'Role "{0}" has been deleted'.format(name)
+        ret['changes']['Role'] = 'Deleted'
+
+    return ret
+
+
+def service_present(name, service_type, description=None,
+                    profile=None, **connection_args):
+    '''
+    Ensure service present in Keystone catalog
+
+    name
+        The name of the service
+
+    service_type
+        The type of Openstack Service
+
+    description (optional)
+        Description of the service
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Service "{0}" already exists'.format(name)}
+
+    # Check if service is already present
+    role = __salt__['keystoneng.service_get'](name=name,
+                                            profile=profile,
+                                            **connection_args)
+
+    if 'Error' not in role:
+        return ret
+    else:
+        if __opts__.get('test'):
+            ret['result'] = None
+            ret['comment'] = 'Service "{0}" will be added'.format(name)
+            return ret
+        # Create service
+        __salt__['keystoneng.service_create'](name, service_type,
+                                            description,
+                                            profile=profile,
+                                            **connection_args)
+        ret['comment'] = 'Service "{0}" has been added'.format(name)
+        ret['changes']['Service'] = 'Created'
+
+    return ret
+
+
+def service_absent(name, profile=None, **connection_args):
+    '''
+    Ensure that the service doesn't exist in Keystone catalog
+
+    name
+        The name of the service that should not exist
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Service "{0}" is already absent'.format(name)}
+
+    # Check if service is present
+    role = __salt__['keystoneng.service_get'](name=name,
+                                            profile=profile,
+                                            **connection_args)
+    if 'Error' not in role:
+        if __opts__.get('test'):
+            ret['result'] = None
+            ret['comment'] = 'Service "{0}" will be deleted'.format(name)
+            return ret
+        # Delete service
+        __salt__['keystoneng.service_delete'](name=name,
+                                            profile=profile,
+                                            **connection_args)
+        ret['comment'] = 'Service "{0}" has been deleted'.format(name)
+        ret['changes']['Service'] = 'Deleted'
+
+    return ret
+
+
+def endpoint_present(name,
+                     publicurl=None,
+                     internalurl=None,
+                     adminurl=None,
+                     region=None,
+                     profile=None,
+                     url=None,
+                     interface=None, **connection_args):
+    '''
+    Ensure the specified endpoints exists for service
+
+    name
+        The Service name
+
+    publicurl
+        The public url of service endpoint (for V2 API)
+
+    internalurl
+        The internal url of service endpoint (for V2 API)
+
+    adminurl
+        The admin url of the service endpoint (for V2 API)
+
+    region
+        The region of the endpoint
+
+    url
+        The endpoint URL (for V3 API)
+
+    interface
+        The interface type, which describes the visibility
+        of the endpoint. (for V3 API)
+
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+    endpoint = __salt__['keystoneng.endpoint_get'](name, region,
+                                                 profile=profile,
+                                                 interface=interface,
+                                                 **connection_args)
+
+    def _changes(desc):
+        return ret.get('comment', '') + desc + '\n'
+
+    def _create_endpoint():
+        if _OS_IDENTITY_API_VERSION > 2:
+            ret['changes'] = __salt__['keystoneng.endpoint_create'](
+                name,
+                region=region,
+                url=url,
+                interface=interface,
+                profile=profile,
+                **connection_args)
+        else:
+            ret['changes'] = __salt__['keystoneng.endpoint_create'](
+                name,
+                region=region,
+                publicurl=publicurl,
+                adminurl=adminurl,
+                internalurl=internalurl,
+                profile=profile,
+                **connection_args)
+
+    if endpoint and 'Error' not in endpoint and endpoint.get('region') == region:
+
+        if _OS_IDENTITY_API_VERSION > 2:
+
+            change_url = False
+            change_interface = False
+
+            if endpoint.get('url', None) != url:
+                ret['comment'] = _changes('URL changes from "{0}" to "{1}"'.format(endpoint.get('url', None), url))
+                change_url = True
+
+            if endpoint.get('interface', None) != interface:
+                ret['comment'] = _changes('Interface changes from "{0}" to "{1}"'.format(endpoint.get('interface', None), interface))
+                change_interface = True
+
+            if __opts__.get('test') and (change_url or change_interface):
+                ret['result'] = None
+                ret['changes']['Endpoint'] = 'Will be updated'
+                ret['comment'] += 'Endpoint for service "{0}" will be updated'.format(name)
+                return ret
+
+            if change_url:
+                ret['changes']['url'] = url
+
+            if change_interface:
+                ret['changes']['interface'] = interface
+
+        else:
+            change_publicurl = False
+            change_adminurl = False
+            change_internalurl = False
+
+            if endpoint.get('publicurl', None) != publicurl:
+                change_publicurl = True
+
+                ret['comment'] = _changes('Public URL changes from "{0}" to "{1}"'.format(
+                    endpoint.get('publicurl', None), publicurl)
+                )
+
+            if endpoint.get('adminurl', None) != adminurl:
+                change_adminurl = True
+                ret['comment'] = _changes('Admin URL changes from "{0}" to "{1}"'.format(
+                    endpoint.get('adminurl', None), adminurl)
+                )
+
+            if endpoint.get('internalurl', None) != internalurl:
+                change_internalurl = True
+                ret['comment'] = _changes(
+                    'Internal URL changes from "{0}" to "{1}"'.format(
+                        endpoint.get('internalurl', None),
+                        internalurl
+                    )
+                )
+
+            if __opts__.get('test') and (change_publicurl or change_adminurl or change_internalurl):
+                ret['result'] = None
+                ret['comment'] += 'Endpoint for service "{0}" will be updated'.format(name)
+                ret['changes']['Endpoint'] = 'Will be updated'
+                return ret
+
+            if change_publicurl:
+                ret['changes']['publicurl'] = publicurl
+
+            if change_adminurl:
+                ret['changes']['adminurl'] = adminurl
+
+            if change_internalurl:
+                ret['changes']['internalurl'] = internalurl
+
+        if ret['comment']:  # changed
+            __salt__['keystoneng.endpoint_delete'](name, region, profile=profile, interface=interface, **connection_args)
+            _create_endpoint()
+            ret['comment'] += 'Endpoint for service "{0}" has been updated'.format(name)
+
+    else:
+        # Add new endpoint
+        if __opts__.get('test'):
+            ret['result'] = None
+            ret['changes']['Endpoint'] = 'Will be created'
+            ret['comment'] = 'Endpoint for service "{0}" will be added'.format(name)
+            return ret
+        _create_endpoint()
+        ret['comment'] = 'Endpoint for service "{0}" has been added'.format(name)
+
+    if ret['comment'] == '':  # => no changes
+        ret['comment'] = 'Endpoint for service "{0}" already exists'.format(name)
+    return ret
+
+
+def endpoint_absent(name, region=None, profile=None, interface=None, **connection_args):
+    '''
+    Ensure that the endpoint for a service doesn't exist in Keystone catalog
+
+    name
+        The name of the service whose endpoints should not exist
+
+    region (optional)
+        The region of the endpoint.  Defaults to ``RegionOne``.
+
+    interface
+        The interface type, which describes the visibility
+        of the endpoint. (for V3 API)
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Endpoint for service "{0}"{1} is already absent'.format(name,
+                      ', interface "{0}",'.format(interface) if interface is not None else '')}
+
+    # Check if service is present
+    endpoint = __salt__['keystoneng.endpoint_get'](name, region,
+                                                 profile=profile,
+                                                 interface=interface,
+                                                 **connection_args)
+    if not endpoint:
+        return ret
+    else:
+        if __opts__.get('test'):
+            ret['result'] = None
+            ret['comment'] = 'Endpoint for service "{0}" will be deleted'.format(name)
+            return ret
+        # Delete service
+        __salt__['keystoneng.endpoint_delete'](name, region,
+                                             profile=profile,
+                                             interface=interface,
+                                             **connection_args)
+        ret['comment'] = 'Endpoint for service "{0}"{1} has been deleted'.format(name,
+                         ', interface "{0}",'.format(interface) if interface is not None else '')
+        ret['changes']['endpoint'] = 'Deleted'
+    return ret
diff --git a/keystone/client/project.sls b/keystone/client/project.sls
index 856c78f..5e07b45 100644
--- a/keystone/client/project.sls
+++ b/keystone/client/project.sls
@@ -4,7 +4,7 @@
 {%- if client.tenant is defined %}
 
 keystone_client_roles:
-  keystone.role_present:
+  keystoneng.role_present:
   - names: {{ client.roles }}
   - connection_user: {{ client.server.user }}
   - connection_password: {{ client.server.password }}
@@ -14,19 +14,19 @@
 {%- for tenant_name, tenant in client.get('tenant', {}).iteritems() %}
 
 keystone_tenant_{{ tenant_name }}:
-  keystone.tenant_present:
+  keystoneng.tenant_present:
   - name: {{ tenant_name }}
   - connection_user: {{ client.server.user }}
   - connection_password: {{ client.server.password }}
   - connection_tenant: {{ client.server.tenant }}
   - connection_auth_url: 'http://{{ client.server.host }}:{{ client.server.public_port }}/v2.0/'
   - require:
-    - keystone: keystone_client_roles
+    - keystoneng: keystone_client_roles
 
 {%- for user_name, user in tenant.get('user', {}).iteritems() %}
 
 keystone_{{ tenant_name }}_user_{{ user_name }}:
-  keystone.user_present:
+  keystoneng.user_present:
   - name: {{ user_name }}
   - password: {{ user.password }}
   - email: {{ user.get('email', 'root@localhost') }}
@@ -45,7 +45,7 @@
   - connection_tenant: {{ client.server.tenant }}
   - connection_auth_url: 'http://{{ client.server.host }}:{{ client.server.public_port }}/v2.0/'
   - require:
-    - keystone: keystone_tenant_{{ tenant_name }}
+    - keystoneng: keystone_tenant_{{ tenant_name }}
 
 {%- endfor %}
 
diff --git a/keystone/client/server.sls b/keystone/client/server.sls
index 416590b..fb7597b 100644
--- a/keystone/client/server.sls
+++ b/keystone/client/server.sls
@@ -28,7 +28,7 @@
 {%- if server.roles is defined %}
 
 keystone_{{ server_name }}_roles:
-  keystone.role_present:
+  keystoneng.role_present:
   - names: {{ server.roles }}
   {%- if server.admin.token is defined %}
   - connection_token: {{ connection_args.token }}
@@ -45,7 +45,7 @@
 {% for service_name, service in server.get('service', {}).iteritems() %}
 
 keystone_{{ server_name }}_service_{{ service_name }}:
-  keystone.service_present:
+  keystoneng.service_present:
   - name: {{ service_name }}
   - service_type: {{ service.type }}
   - description: {{ service.description }}
@@ -62,14 +62,14 @@
 {%- for endpoint in service.get('endpoints', ()) %}
 
 keystone_{{ server_name }}_service_{{ service_name }}_endpoint_{{ endpoint.region }}:
-  keystone.endpoint_present:
+  keystoneng.endpoint_present:
   - name: {{ service_name }}
   - publicurl: '{{ endpoint.get('public_protocol', 'http') }}://{{ endpoint.public_address }}{% if not (endpoint.get('public_protocol', 'http') == 'https' and endpoint.public_port|int == 443) %}:{{ endpoint.public_port }}{% endif %}{{ endpoint.public_path }}'
   - internalurl: '{{ endpoint.get('internal_protocol', 'http') }}://{{ endpoint.internal_address }}{% if not (endpoint.get('internal_protocol', 'http') == 'https' and endpoint.internal_port|int == 443) %}:{{ endpoint.internal_port }}{% endif %}{{ endpoint.internal_path }}'
   - adminurl: '{{ endpoint.get('admin_protocol', 'http') }}://{{ endpoint.admin_address }}{% if not (endpoint.get('admin_protocol', 'http') == 'https' and endpoint.admin_port|int == 443) %}:{{ endpoint.admin_port }}{% endif %}{{ endpoint.admin_path }}'
   - region: {{ endpoint.region }}
   - require:
-    - keystone: keystone_{{ server_name }}_service_{{ service_name }}
+    - keystoneng: keystone_{{ server_name }}_service_{{ service_name }}
   {%- if server.admin.token is defined %}
   - connection_token: {{ connection_args.token }}
   - connection_endpoint: {{ connection_args.endpoint }}
@@ -87,7 +87,7 @@
 {%- for tenant_name, tenant in server.get('project', {}).iteritems() %}
 
 keystone_{{ server_name }}_tenant_{{ tenant_name }}:
-  keystone.tenant_present:
+  keystoneng.tenant_present:
   - name: {{ tenant_name }}
   {%- if tenant.description is defined %}
   - description: {{ tenant.description }}
@@ -112,14 +112,14 @@
     - {{ quota_name }}: {{ quota_value }}
     {%- endfor %}
     - require:
-      - keystone: keystone_{{ server_name }}_tenant_{{ tenant_name }}
+      - keystoneng: keystone_{{ server_name }}_tenant_{{ tenant_name }}
 
 {%- endif %}
 
 {%- for user_name, user in tenant.get('user', {}).iteritems() %}
 
 keystone_{{ server_name }}_tenant_{{ tenant_name }}_user_{{ user_name }}:
-  keystone.user_present:
+  keystoneng.user_present:
   - name: {{ user_name }}
   - password: {{ user.password }}
   {%- if user.email is defined %}
@@ -136,8 +136,8 @@
         - Member
         {%- endif %}
   - require:
-    - keystone: keystone_{{ server_name }}_tenant_{{ tenant_name }}
-    - keystone: keystone_{{ server_name }}_roles
+    - keystoneng: keystone_{{ server_name }}_tenant_{{ tenant_name }}
+    - keystoneng: keystone_{{ server_name }}_roles
   {%- if server.admin.token is defined %}
   - connection_token: {{ connection_args.token }}
   - connection_endpoint: {{ connection_args.endpoint }}
diff --git a/keystone/meta/salt.yml b/keystone/meta/salt.yml
index 4a3f557..f287824 100644
--- a/keystone/meta/salt.yml
+++ b/keystone/meta/salt.yml
@@ -15,13 +15,13 @@
   keystone:
     {%- if pillar.keystone.get('server', {'enabled': False}).enabled %}
     keystone.token: '{{ server.service_token }}'
-    keystone.endpoint: '{{ server.bind.private_protocol }}://{{ server.bind.address }}:{{ server.bind.private_port }}/v2.0'
+    keystone.endpoint: '{{ server.bind.private_protocol }}://{{ server.bind.address }}:{{ server.bind.private_port }}/{{ server.bind.get('private_api_version', 'v2.0') }}'
     {%- else %}
       {%- if client.get('server', {}).get('user') %}
     keystone.user: '{{ client.server.user }}'
     keystone.password: '{{ client.server.password }}'
     keystone.tenant: '{{ client.server.tenant }}'
-    keystone.auth_url: '{{ client.server.get('public_protocol', 'http') }}://{{ client.server.host }}:{{ client.server.public_port }}/v2.0/'
+    keystone.auth_url: '{{ client.server.get('public_protocol', 'http') }}://{{ client.server.host }}:{{ client.server.public_port }}/{{ client.server.get('public_api_version', 'v2.0') }}'
       {%- endif %}
     {%- endif %}
 
@@ -30,9 +30,13 @@
       {%- set protocol = identity.admin.get('protocol', 'http') %}
 
       {%- if identity.admin.get('api_version', '2') == '3' %}
-        {%- set version = "v3" %}
-      {%- else %}
-        {%- set version = "v2.0" %}
+        {%- set version = "/v3" %}
+      {%- endif %}
+      {%- if identity.admin.get('api_version', '2') == '2' %}
+        {%- set version = "/v2.0" %}
+      {%- endif %}
+      {%- if identity.admin.get('api_version', '2') == '' %}
+        {%- set version = "" %}
       {%- endif %}
 
       {%- if identity.admin.user is defined %}
@@ -42,7 +46,7 @@
       keystone.user: '{{ identity.admin.user }}'
       keystone.password: '{{ identity.admin.password }}'
       keystone.tenant: '{{ identity.admin.project }}'
-      keystone.auth_url: '{{ protocol+'://'+identity.admin.host+':'+identity.admin.port|string+'/'+version }}'
+      keystone.auth_url: '{{ protocol+'://'+identity.admin.host+':'+identity.admin.port|string+version }}'
       keystone.region_name: '{{ identity.admin.region_name }}'
       keystone.use_keystoneauth: {{ identity.admin.get('use_keystoneauth', false) }}
 
diff --git a/keystone/server.sls b/keystone/server.sls
index 99337f9..4e4cba6 100644
--- a/keystone/server.sls
+++ b/keystone/server.sls
@@ -338,7 +338,7 @@
 {%- if not salt['pillar.get']('linux:system:repo:mirantis_openstack', False) %}
 
 keystone_service_tenant:
-  keystone.tenant_present:
+  keystoneng.tenant_present:
   - name: {{ server.service_tenant }}
   - connection_token: {{ server.service_token }}
   - connection_endpoint: 'http://{{ server.bind.address }}:{{ server.bind.private_port }}/v2.0'
@@ -346,25 +346,25 @@
     - cmd: keystone_syncdb
 
 keystone_admin_tenant:
-  keystone.tenant_present:
+  keystoneng.tenant_present:
   - name: {{ server.admin_tenant }}
   - connection_token: {{ server.service_token }}
   - connection_endpoint: 'http://{{ server.bind.address }}:{{ server.bind.private_port }}/v2.0'
   - require:
-    - keystone: keystone_service_tenant
+    - keystoneng: keystone_service_tenant
 
 keystone_roles:
-  keystone.role_present:
+  keystoneng.role_present:
   - names: {{ server.roles }}
   - connection_token: {{ server.service_token }}
   - connection_endpoint: 'http://{{ server.bind.address }}:{{ server.bind.private_port }}/v2.0'
   - require:
-    - keystone: keystone_service_tenant
+    - keystoneng: keystone_service_tenant
 
 {%- if not server.get('ldap', {}).get('read_only', False) %}
 
 keystone_admin_user:
-  keystone.user_present:
+  keystoneng.user_present:
   - name: {{ server.admin_name }}
   - password: {{ server.admin_password }}
   - email: {{ server.admin_email }}
@@ -375,8 +375,8 @@
   - connection_token: {{ server.service_token }}
   - connection_endpoint: 'http://{{ server.bind.address }}:{{ server.bind.private_port }}/v2.0'
   - require:
-    - keystone: keystone_admin_tenant
-    - keystone: keystone_roles
+    - keystoneng: keystone_admin_tenant
+    - keystoneng: keystone_roles
 
 {%- endif %}
 
@@ -385,17 +385,17 @@
 {%- for service_name, service in server.get('service', {}).iteritems() %}
 
 keystone_{{ service_name }}_service:
-  keystone.service_present:
+  keystoneng.service_present:
   - name: {{ service_name }}
   - service_type: {{ service.type }}
   - description: {{ service.description }}
   - connection_token: {{ server.service_token }}
   - connection_endpoint: 'http://{{ server.bind.address }}:{{ server.bind.private_port }}/v2.0'
   - require:
-    - keystone: keystone_roles
+    - keystoneng: keystone_roles
 
 keystone_{{ service_name }}_{{ service.get('region', 'RegionOne') }}_endpoint:
-  keystone.endpoint_present:
+  keystoneng.endpoint_present:
   - name: {{ service.get('service', service_name) }}
   - publicurl: '{{ service.bind.get('public_protocol', 'http') }}://{{ service.bind.public_address }}:{{ service.bind.public_port }}{{ service.bind.public_path }}'
   - internalurl: '{{ service.bind.get('internal_protocol', 'http') }}://{{ service.bind.internal_address }}:{{ service.bind.internal_port }}{{ service.bind.internal_path }}'
@@ -404,12 +404,12 @@
   - connection_token: {{ server.service_token }}
   - connection_endpoint: 'http://{{ server.bind.address }}:{{ server.bind.private_port }}/v2.0'
   - require:
-    - keystone: keystone_{{ service_name }}_service
+    - keystoneng: keystone_{{ service_name }}_service
 
 {% if service.user is defined %}
 
 keystone_user_{{ service.user.name }}:
-  keystone.user_present:
+  keystoneng.user_present:
   - name: {{ service.user.name }}
   - password: {{ service.user.password }}
   - email: {{ server.admin_email }}
@@ -420,7 +420,7 @@
   - connection_token: {{ server.service_token }}
   - connection_endpoint: 'http://{{ server.bind.address }}:{{ server.bind.private_port }}/v2.0'
   - require:
-    - keystone: keystone_roles
+    - keystoneng: keystone_roles
 
 {% endif %}
 
@@ -429,17 +429,17 @@
 {%- for tenant_name, tenant in server.get('tenant', {}).iteritems() %}
 
 keystone_tenant_{{ tenant_name }}:
-  keystone.tenant_present:
+  keystoneng.tenant_present:
   - name: {{ tenant_name }}
   - connection_token: {{ server.service_token }}
   - connection_endpoint: 'http://{{ server.bind.address }}:{{ server.bind.private_port }}/v2.0'
   - require:
-    - keystone: keystone_roles
+    - keystoneng: keystone_roles
 
 {%- for user_name, user in tenant.get('user', {}).iteritems() %}
 
 keystone_user_{{ user_name }}:
-  keystone.user_present:
+  keystoneng.user_present:
   - name: {{ user_name }}
   - password: {{ user.password }}
   - email: {{ user.get('email', 'root@localhost') }}
@@ -454,7 +454,7 @@
   - connection_token: {{ server.service_token }}
   - connection_endpoint: 'http://{{ server.bind.address }}:{{ server.bind.private_port }}/v2.0'
   - require:
-    - keystone: keystone_tenant_{{ tenant_name }}
+    - keystoneng: keystone_tenant_{{ tenant_name }}
 
 {%- endfor %}