Modify Ansible's Keystone module to support password changes. Fixes #5.
diff --git a/external/keystone_user.py b/external/keystone_user.py
new file mode 100644
index 0000000..0139ad4
--- /dev/null
+++ b/external/keystone_user.py
@@ -0,0 +1,406 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Based on Jimmy Tang's implementation
+
+DOCUMENTATION = '''
+---
+module: keystone_user
+version_added: "1.2"
+short_description: Manage OpenStack Identity (keystone) users, tenants and roles
+description:
+ - Manage users,tenants, roles from OpenStack.
+options:
+ login_user:
+ description:
+ - login username to authenticate to keystone
+ required: false
+ default: admin
+ login_password:
+ description:
+ - Password of login user
+ required: false
+ default: 'yes'
+ login_tenant_name:
+ description:
+ - The tenant login_user belongs to
+ required: false
+ default: None
+ version_added: "1.3"
+ token:
+ description:
+ - The token to be uses in case the password is not specified
+ required: false
+ default: None
+ endpoint:
+ description:
+ - The keystone url for authentication
+ required: false
+ default: 'http://127.0.0.1:35357/v2.0/'
+ user:
+ description:
+ - The name of the user that has to added/removed from OpenStack
+ required: false
+ default: None
+ password:
+ description:
+ - The password to be assigned to the user
+ required: false
+ default: None
+ tenant:
+ description:
+ - The tenant name that has be added/removed
+ required: false
+ default: None
+ tenant_description:
+ description:
+ - A description for the tenant
+ required: false
+ default: None
+ email:
+ description:
+ - An email address for the user
+ required: false
+ default: None
+ role:
+ description:
+ - The name of the role to be assigned or created
+ required: false
+ default: None
+ state:
+ description:
+ - Indicate desired state of the resource
+ choices: ['present', 'absent']
+ default: present
+requirements: [ python-keystoneclient ]
+author: Lorin Hochstein
+'''
+
+EXAMPLES = '''
+# Create a tenant
+- keystone_user: tenant=demo tenant_description="Default Tenant"
+
+# Create a user
+- keystone_user: user=john tenant=demo password=secrete
+
+# Apply the admin role to the john user in the demo tenant
+- keystone_user: role=admin user=john tenant=demo
+'''
+
+try:
+ from keystoneclient.v2_0 import client
+except ImportError:
+ keystoneclient_found = False
+else:
+ keystoneclient_found = True
+
+
+def authenticate(endpoint, token, login_user, login_password, login_tenant_name):
+ """Return a keystone client object"""
+
+ if token:
+ return client.Client(endpoint=endpoint, token=token)
+ else:
+ return client.Client(auth_url=endpoint, username=login_user,
+ password=login_password, tenant_name=login_tenant_name)
+
+
+def tenant_exists(keystone, tenant):
+ """ Return True if tenant already exists"""
+ return tenant in [x.name for x in keystone.tenants.list()]
+
+
+def user_exists(keystone, user):
+ """" Return True if user already exists"""
+ return user in [x.name for x in keystone.users.list()]
+
+
+def get_tenant(keystone, name):
+ """ Retrieve a tenant by name"""
+ tenants = [x for x in keystone.tenants.list() if x.name == name]
+ count = len(tenants)
+ if count == 0:
+ raise KeyError("No keystone tenants with name %s" % name)
+ elif count > 1:
+ raise ValueError("%d tenants with name %s" % (count, name))
+ else:
+ return tenants[0]
+
+
+def get_user(keystone, name):
+ """ Retrieve a user by name"""
+ users = [x for x in keystone.users.list() if x.name == name]
+ count = len(users)
+ if count == 0:
+ raise KeyError("No keystone users with name %s" % name)
+ elif count > 1:
+ raise ValueError("%d users with name %s" % (count, name))
+ else:
+ return users[0]
+
+
+def get_role(keystone, name):
+ """ Retrieve a role by name"""
+ roles = [x for x in keystone.roles.list() if x.name == name]
+ count = len(roles)
+ if count == 0:
+ raise KeyError("No keystone roles with name %s" % name)
+ elif count > 1:
+ raise ValueError("%d roles with name %s" % (count, name))
+ else:
+ return roles[0]
+
+
+def get_tenant_id(keystone, name):
+ return get_tenant(keystone, name).id
+
+
+def get_user_id(keystone, name):
+ return get_user(keystone, name).id
+
+
+def ensure_tenant_exists(keystone, tenant_name, tenant_description,
+ check_mode):
+ """ Ensure that a tenant exists.
+
+ Return (True, id) if a new tenant was created, (False, None) if it
+ already existed.
+ """
+
+ # Check if tenant already exists
+ try:
+ tenant = get_tenant(keystone, tenant_name)
+ except KeyError:
+ # Tenant doesn't exist yet
+ pass
+ else:
+ if tenant.description == tenant_description:
+ return (False, tenant.id)
+ else:
+ # We need to update the tenant description
+ if check_mode:
+ return (True, tenant.id)
+ else:
+ tenant.update(description=tenant_description)
+ return (True, tenant.id)
+
+ # We now know we will have to create a new tenant
+ if check_mode:
+ return (True, None)
+
+ ks_tenant = keystone.tenants.create(tenant_name=tenant_name,
+ description=tenant_description,
+ enabled=True)
+ return (True, ks_tenant.id)
+
+
+def ensure_tenant_absent(keystone, tenant, check_mode):
+ """ Ensure that a tenant does not exist
+
+ Return True if the tenant was removed, False if it didn't exist
+ in the first place
+ """
+ if not tenant_exists(keystone, tenant):
+ return False
+
+ # We now know we will have to delete the tenant
+ if check_mode:
+ return True
+
+def ensure_user_exists_and_is_current(keystone, endpoint, user_name, password, email, tenant_name,
+ check_mode):
+ """ Check if user exists and has the same email and password
+
+ Return (True, id) if a new user was created or one was updated, (False, id) if the user is
+ up to date
+ """
+
+ # Check if tenant already exists
+ try:
+ user = get_user(keystone, user_name)
+ except KeyError:
+ # Tenant doesn't exist yet
+ pass
+ else:
+ # User does exist, check if it's current
+ try:
+ authenticate(endpoint, None, user_name, password, tenant_name)
+ except:
+ pass
+ else:
+ # It's current, we're done
+ return (False, user.id)
+
+ # We now know we will have to create a new user
+ if check_mode:
+ return (True, None)
+
+ tenant = get_tenant(keystone, tenant_name)
+
+ if (not user):
+ user = keystone.users.create(name=user_name, password=password,
+ email=email, tenant_id=tenant.id)
+ else:
+ user = keystone.users.update_password(user.id, password)
+
+ return (True, user.id)
+
+
+def ensure_role_exists(keystone, user_name, tenant_name, role_name,
+ check_mode):
+ """ Check if role exists
+
+ Return (True, id) if a new role was created or if the role was newly
+ assigned to the user for the tenant. (False, id) if the role already
+ exists and was already assigned to the user ofr the tenant.
+
+ """
+ # Check if the user has the role in the tenant
+ user = get_user(keystone, user_name)
+ tenant = get_tenant(keystone, tenant_name)
+ roles = [x for x in keystone.roles.roles_for_user(user, tenant)
+ if x.name == role_name]
+ count = len(roles)
+
+ if count == 1:
+ # If the role is in there, we are done
+ role = roles[0]
+ return (False, role.id)
+ elif count > 1:
+ # Too many roles with the same name, throw an error
+ raise ValueError("%d roles with name %s" % (count, role_name))
+
+ # At this point, we know we will need to make changes
+ if check_mode:
+ return (True, None)
+
+ # Get the role if it exists
+ try:
+ role = get_role(keystone, role_name)
+ except KeyError:
+ # Role doesn't exist yet
+ role = keystone.roles.create(role_name)
+
+ # Associate the role with the user in the admin
+ keystone.roles.add_user_role(user, role, tenant)
+ return (True, role.id)
+
+
+def ensure_user_absent(keystone, user, check_mode):
+ raise NotImplementedError("Not yet implemented")
+
+
+def ensure_role_absent(keystone, uesr, tenant, role, check_mode):
+ raise NotImplementedError("Not yet implemented")
+
+
+def main():
+
+ argument_spec = openstack_argument_spec()
+ argument_spec.update(dict(
+ tenant_description=dict(required=False),
+ email=dict(required=False),
+ user=dict(required=False),
+ tenant=dict(required=False),
+ password=dict(required=False),
+ role=dict(required=False),
+ state=dict(default='present', choices=['present', 'absent']),
+ endpoint=dict(required=False,
+ default="http://127.0.0.1:35357/v2.0"),
+ token=dict(required=False),
+ login_user=dict(required=False),
+ login_password=dict(required=False),
+ login_tenant_name=dict(required=False)
+ ))
+ # keystone operations themselves take an endpoint, not a keystone auth_url
+ del(argument_spec['auth_url'])
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ mutually_exclusive=[['token', 'login_user'],
+ ['token', 'login_password'],
+ ['token', 'login_tenant_name']]
+ )
+
+ if not keystoneclient_found:
+ module.fail_json(msg="the python-keystoneclient module is required")
+
+ user = module.params['user']
+ password = module.params['password']
+ tenant = module.params['tenant']
+ tenant_description = module.params['tenant_description']
+ email = module.params['email']
+ role = module.params['role']
+ state = module.params['state']
+ endpoint = module.params['endpoint']
+ token = module.params['token']
+ login_user = module.params['login_user']
+ login_password = module.params['login_password']
+ login_tenant_name = module.params['login_tenant_name']
+
+ keystone = authenticate(endpoint, token, login_user, login_password, login_tenant_name)
+
+ check_mode = module.check_mode
+
+ try:
+ d = dispatch(keystone, user, password, tenant, tenant_description,
+ email, role, state, endpoint, token, login_user,
+ login_password, check_mode)
+ except Exception, e:
+ if check_mode:
+ # If we have a failure in check mode
+ module.exit_json(changed=True,
+ msg="exception: %s" % e)
+ else:
+ module.fail_json(msg="exception: %s" % e)
+ else:
+ module.exit_json(**d)
+
+
+def dispatch(keystone, user=None, password=None, tenant=None,
+ tenant_description=None, email=None, role=None,
+ state="present", endpoint=None, token=None, login_user=None,
+ login_password=None, check_mode=False):
+ """ Dispatch to the appropriate method.
+
+ Returns a dict that will be passed to exit_json
+
+ tenant user role state
+ ------ ---- ---- --------
+ X present ensure_tenant_exists
+ X absent ensure_tenant_absent
+ X X present ensure_user_exists
+ X X absent ensure_user_absent
+ X X X present ensure_role_exists
+ X X X absent ensure_role_absent
+
+
+ """
+ changed = False
+ id = None
+ if tenant and not user and not role and state == "present":
+ changed, id = ensure_tenant_exists(keystone, tenant,
+ tenant_description, check_mode)
+ elif tenant and not user and not role and state == "absent":
+ changed = ensure_tenant_absent(keystone, tenant, check_mode)
+ elif tenant and user and not role and state == "present":
+ changed, id = ensure_user_exists_and_is_current(keystone, endpoint, user, password,
+ email, tenant, check_mode)
+ elif tenant and user and not role and state == "absent":
+ changed = ensure_user_absent(keystone, user, check_mode)
+ elif tenant and user and role and state == "present":
+ changed, id = ensure_role_exists(keystone, user, tenant, role,
+ check_mode)
+ elif tenant and user and role and state == "absent":
+ changed = ensure_role_absent(keystone, user, tenant, role, check_mode)
+ else:
+ # Should never reach here
+ raise ValueError("Code should never reach here")
+
+ return dict(changed=changed, id=id)
+
+# import module snippets
+from ansible.module_utils.basic import *
+from ansible.module_utils.openstack import *
+if __name__ == '__main__':
+ main()