Sapan Bhatia | 16a7f65 | 2015-01-31 05:29:49 +0000 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | # -*- coding: utf-8 -*- |
| 3 | |
| 4 | # Based on Jimmy Tang's implementation |
| 5 | |
| 6 | DOCUMENTATION = ''' |
| 7 | --- |
| 8 | module: keystone_user |
| 9 | version_added: "1.2" |
| 10 | short_description: Manage OpenStack Identity (keystone) users, tenants and roles |
| 11 | description: |
| 12 | - Manage users,tenants, roles from OpenStack. |
| 13 | options: |
| 14 | login_user: |
| 15 | description: |
| 16 | - login username to authenticate to keystone |
| 17 | required: false |
| 18 | default: admin |
| 19 | login_password: |
| 20 | description: |
| 21 | - Password of login user |
| 22 | required: false |
| 23 | default: 'yes' |
| 24 | login_tenant_name: |
| 25 | description: |
| 26 | - The tenant login_user belongs to |
| 27 | required: false |
| 28 | default: None |
| 29 | version_added: "1.3" |
| 30 | token: |
| 31 | description: |
| 32 | - The token to be uses in case the password is not specified |
| 33 | required: false |
| 34 | default: None |
| 35 | endpoint: |
| 36 | description: |
| 37 | - The keystone url for authentication |
| 38 | required: false |
| 39 | default: 'http://127.0.0.1:35357/v2.0/' |
| 40 | user: |
| 41 | description: |
| 42 | - The name of the user that has to added/removed from OpenStack |
| 43 | required: false |
| 44 | default: None |
| 45 | password: |
| 46 | description: |
| 47 | - The password to be assigned to the user |
| 48 | required: false |
| 49 | default: None |
| 50 | tenant: |
| 51 | description: |
| 52 | - The tenant name that has be added/removed |
| 53 | required: false |
| 54 | default: None |
| 55 | tenant_description: |
| 56 | description: |
| 57 | - A description for the tenant |
| 58 | required: false |
| 59 | default: None |
| 60 | email: |
| 61 | description: |
| 62 | - An email address for the user |
| 63 | required: false |
| 64 | default: None |
| 65 | role: |
| 66 | description: |
| 67 | - The name of the role to be assigned or created |
| 68 | required: false |
| 69 | default: None |
| 70 | state: |
| 71 | description: |
| 72 | - Indicate desired state of the resource |
| 73 | choices: ['present', 'absent'] |
| 74 | default: present |
| 75 | requirements: [ python-keystoneclient ] |
| 76 | author: Lorin Hochstein |
| 77 | ''' |
| 78 | |
| 79 | EXAMPLES = ''' |
| 80 | # Create a tenant |
| 81 | - keystone_user: tenant=demo tenant_description="Default Tenant" |
| 82 | |
| 83 | # Create a user |
| 84 | - keystone_user: user=john tenant=demo password=secrete |
| 85 | |
| 86 | # Apply the admin role to the john user in the demo tenant |
| 87 | - keystone_user: role=admin user=john tenant=demo |
| 88 | ''' |
| 89 | |
| 90 | try: |
| 91 | from keystoneclient.v2_0 import client |
| 92 | except ImportError: |
| 93 | keystoneclient_found = False |
| 94 | else: |
| 95 | keystoneclient_found = True |
| 96 | |
| 97 | |
| 98 | def authenticate(endpoint, token, login_user, login_password, login_tenant_name): |
| 99 | """Return a keystone client object""" |
| 100 | |
| 101 | if token: |
| 102 | return client.Client(endpoint=endpoint, token=token) |
| 103 | else: |
| 104 | return client.Client(auth_url=endpoint, username=login_user, |
| 105 | password=login_password, tenant_name=login_tenant_name) |
| 106 | |
| 107 | |
| 108 | def tenant_exists(keystone, tenant): |
| 109 | """ Return True if tenant already exists""" |
| 110 | return tenant in [x.name for x in keystone.tenants.list()] |
| 111 | |
| 112 | |
| 113 | def user_exists(keystone, user): |
| 114 | """" Return True if user already exists""" |
| 115 | return user in [x.name for x in keystone.users.list()] |
| 116 | |
| 117 | |
| 118 | def get_tenant(keystone, name): |
| 119 | """ Retrieve a tenant by name""" |
| 120 | tenants = [x for x in keystone.tenants.list() if x.name == name] |
| 121 | count = len(tenants) |
| 122 | if count == 0: |
| 123 | raise KeyError("No keystone tenants with name %s" % name) |
| 124 | elif count > 1: |
| 125 | raise ValueError("%d tenants with name %s" % (count, name)) |
| 126 | else: |
| 127 | return tenants[0] |
| 128 | |
| 129 | |
| 130 | def get_user(keystone, name): |
| 131 | """ Retrieve a user by name""" |
| 132 | users = [x for x in keystone.users.list() if x.name == name] |
| 133 | count = len(users) |
| 134 | if count == 0: |
| 135 | raise KeyError("No keystone users with name %s" % name) |
| 136 | elif count > 1: |
| 137 | raise ValueError("%d users with name %s" % (count, name)) |
| 138 | else: |
| 139 | return users[0] |
| 140 | |
| 141 | |
| 142 | def get_role(keystone, name): |
| 143 | """ Retrieve a role by name""" |
| 144 | roles = [x for x in keystone.roles.list() if x.name == name] |
| 145 | count = len(roles) |
| 146 | if count == 0: |
| 147 | raise KeyError("No keystone roles with name %s" % name) |
| 148 | elif count > 1: |
| 149 | raise ValueError("%d roles with name %s" % (count, name)) |
| 150 | else: |
| 151 | return roles[0] |
| 152 | |
| 153 | |
| 154 | def get_tenant_id(keystone, name): |
| 155 | return get_tenant(keystone, name).id |
| 156 | |
| 157 | |
| 158 | def get_user_id(keystone, name): |
| 159 | return get_user(keystone, name).id |
| 160 | |
| 161 | |
| 162 | def ensure_tenant_exists(keystone, tenant_name, tenant_description, |
| 163 | check_mode): |
| 164 | """ Ensure that a tenant exists. |
| 165 | |
| 166 | Return (True, id) if a new tenant was created, (False, None) if it |
| 167 | already existed. |
| 168 | """ |
| 169 | |
| 170 | # Check if tenant already exists |
| 171 | try: |
| 172 | tenant = get_tenant(keystone, tenant_name) |
| 173 | except KeyError: |
| 174 | # Tenant doesn't exist yet |
| 175 | pass |
| 176 | else: |
| 177 | if tenant.description == tenant_description: |
| 178 | return (False, tenant.id) |
| 179 | else: |
| 180 | # We need to update the tenant description |
| 181 | if check_mode: |
| 182 | return (True, tenant.id) |
| 183 | else: |
| 184 | tenant.update(description=tenant_description) |
| 185 | return (True, tenant.id) |
| 186 | |
| 187 | # We now know we will have to create a new tenant |
| 188 | if check_mode: |
| 189 | return (True, None) |
| 190 | |
| 191 | ks_tenant = keystone.tenants.create(tenant_name=tenant_name, |
| 192 | description=tenant_description, |
| 193 | enabled=True) |
| 194 | return (True, ks_tenant.id) |
| 195 | |
| 196 | |
| 197 | def ensure_tenant_absent(keystone, tenant, check_mode): |
| 198 | """ Ensure that a tenant does not exist |
| 199 | |
| 200 | Return True if the tenant was removed, False if it didn't exist |
| 201 | in the first place |
| 202 | """ |
| 203 | if not tenant_exists(keystone, tenant): |
| 204 | return False |
| 205 | |
| 206 | # We now know we will have to delete the tenant |
| 207 | if check_mode: |
| 208 | return True |
| 209 | |
| 210 | def ensure_user_exists_and_is_current(keystone, endpoint, user_name, password, email, tenant_name, |
| 211 | check_mode): |
| 212 | """ Check if user exists and has the same email and password |
| 213 | |
| 214 | Return (True, id) if a new user was created or one was updated, (False, id) if the user is |
| 215 | up to date |
| 216 | """ |
| 217 | |
| 218 | # Check if tenant already exists |
| 219 | try: |
| 220 | user = get_user(keystone, user_name) |
| 221 | except KeyError: |
| 222 | # Tenant doesn't exist yet |
Sapan Bhatia | fd67c54 | 2015-02-03 23:53:39 +0000 | [diff] [blame] | 223 | user = None |
Sapan Bhatia | 16a7f65 | 2015-01-31 05:29:49 +0000 | [diff] [blame] | 224 | pass |
| 225 | else: |
| 226 | # User does exist, check if it's current |
| 227 | try: |
| 228 | authenticate(endpoint, None, user_name, password, tenant_name) |
| 229 | except: |
| 230 | pass |
| 231 | else: |
| 232 | # It's current, we're done |
| 233 | return (False, user.id) |
| 234 | |
| 235 | # We now know we will have to create a new user |
| 236 | if check_mode: |
| 237 | return (True, None) |
| 238 | |
| 239 | tenant = get_tenant(keystone, tenant_name) |
| 240 | |
| 241 | if (not user): |
| 242 | user = keystone.users.create(name=user_name, password=password, |
| 243 | email=email, tenant_id=tenant.id) |
| 244 | else: |
| 245 | user = keystone.users.update_password(user.id, password) |
| 246 | |
| 247 | return (True, user.id) |
| 248 | |
| 249 | |
| 250 | def ensure_role_exists(keystone, user_name, tenant_name, role_name, |
| 251 | check_mode): |
| 252 | """ Check if role exists |
| 253 | |
| 254 | Return (True, id) if a new role was created or if the role was newly |
| 255 | assigned to the user for the tenant. (False, id) if the role already |
| 256 | exists and was already assigned to the user ofr the tenant. |
| 257 | |
| 258 | """ |
| 259 | # Check if the user has the role in the tenant |
| 260 | user = get_user(keystone, user_name) |
| 261 | tenant = get_tenant(keystone, tenant_name) |
| 262 | roles = [x for x in keystone.roles.roles_for_user(user, tenant) |
| 263 | if x.name == role_name] |
| 264 | count = len(roles) |
| 265 | |
| 266 | if count == 1: |
| 267 | # If the role is in there, we are done |
| 268 | role = roles[0] |
| 269 | return (False, role.id) |
| 270 | elif count > 1: |
| 271 | # Too many roles with the same name, throw an error |
| 272 | raise ValueError("%d roles with name %s" % (count, role_name)) |
| 273 | |
| 274 | # At this point, we know we will need to make changes |
| 275 | if check_mode: |
| 276 | return (True, None) |
| 277 | |
| 278 | # Get the role if it exists |
| 279 | try: |
| 280 | role = get_role(keystone, role_name) |
| 281 | except KeyError: |
| 282 | # Role doesn't exist yet |
| 283 | role = keystone.roles.create(role_name) |
| 284 | |
| 285 | # Associate the role with the user in the admin |
| 286 | keystone.roles.add_user_role(user, role, tenant) |
| 287 | return (True, role.id) |
| 288 | |
| 289 | |
| 290 | def ensure_user_absent(keystone, user, check_mode): |
| 291 | raise NotImplementedError("Not yet implemented") |
| 292 | |
| 293 | |
| 294 | def ensure_role_absent(keystone, uesr, tenant, role, check_mode): |
| 295 | raise NotImplementedError("Not yet implemented") |
| 296 | |
| 297 | |
| 298 | def main(): |
| 299 | |
| 300 | argument_spec = openstack_argument_spec() |
| 301 | argument_spec.update(dict( |
| 302 | tenant_description=dict(required=False), |
| 303 | email=dict(required=False), |
| 304 | user=dict(required=False), |
| 305 | tenant=dict(required=False), |
| 306 | password=dict(required=False), |
| 307 | role=dict(required=False), |
| 308 | state=dict(default='present', choices=['present', 'absent']), |
| 309 | endpoint=dict(required=False, |
| 310 | default="http://127.0.0.1:35357/v2.0"), |
| 311 | token=dict(required=False), |
| 312 | login_user=dict(required=False), |
| 313 | login_password=dict(required=False), |
| 314 | login_tenant_name=dict(required=False) |
| 315 | )) |
| 316 | # keystone operations themselves take an endpoint, not a keystone auth_url |
| 317 | del(argument_spec['auth_url']) |
| 318 | module = AnsibleModule( |
| 319 | argument_spec=argument_spec, |
| 320 | supports_check_mode=True, |
| 321 | mutually_exclusive=[['token', 'login_user'], |
| 322 | ['token', 'login_password'], |
| 323 | ['token', 'login_tenant_name']] |
| 324 | ) |
| 325 | |
| 326 | if not keystoneclient_found: |
| 327 | module.fail_json(msg="the python-keystoneclient module is required") |
| 328 | |
| 329 | user = module.params['user'] |
| 330 | password = module.params['password'] |
| 331 | tenant = module.params['tenant'] |
| 332 | tenant_description = module.params['tenant_description'] |
| 333 | email = module.params['email'] |
| 334 | role = module.params['role'] |
| 335 | state = module.params['state'] |
| 336 | endpoint = module.params['endpoint'] |
| 337 | token = module.params['token'] |
| 338 | login_user = module.params['login_user'] |
| 339 | login_password = module.params['login_password'] |
| 340 | login_tenant_name = module.params['login_tenant_name'] |
| 341 | |
| 342 | keystone = authenticate(endpoint, token, login_user, login_password, login_tenant_name) |
| 343 | |
| 344 | check_mode = module.check_mode |
| 345 | |
| 346 | try: |
| 347 | d = dispatch(keystone, user, password, tenant, tenant_description, |
| 348 | email, role, state, endpoint, token, login_user, |
| 349 | login_password, check_mode) |
| 350 | except Exception, e: |
| 351 | if check_mode: |
| 352 | # If we have a failure in check mode |
| 353 | module.exit_json(changed=True, |
| 354 | msg="exception: %s" % e) |
| 355 | else: |
| 356 | module.fail_json(msg="exception: %s" % e) |
| 357 | else: |
| 358 | module.exit_json(**d) |
| 359 | |
| 360 | |
| 361 | def dispatch(keystone, user=None, password=None, tenant=None, |
| 362 | tenant_description=None, email=None, role=None, |
| 363 | state="present", endpoint=None, token=None, login_user=None, |
| 364 | login_password=None, check_mode=False): |
| 365 | """ Dispatch to the appropriate method. |
| 366 | |
| 367 | Returns a dict that will be passed to exit_json |
| 368 | |
| 369 | tenant user role state |
| 370 | ------ ---- ---- -------- |
| 371 | X present ensure_tenant_exists |
| 372 | X absent ensure_tenant_absent |
| 373 | X X present ensure_user_exists |
| 374 | X X absent ensure_user_absent |
| 375 | X X X present ensure_role_exists |
| 376 | X X X absent ensure_role_absent |
| 377 | |
| 378 | |
| 379 | """ |
| 380 | changed = False |
| 381 | id = None |
| 382 | if tenant and not user and not role and state == "present": |
| 383 | changed, id = ensure_tenant_exists(keystone, tenant, |
| 384 | tenant_description, check_mode) |
| 385 | elif tenant and not user and not role and state == "absent": |
| 386 | changed = ensure_tenant_absent(keystone, tenant, check_mode) |
| 387 | elif tenant and user and not role and state == "present": |
| 388 | changed, id = ensure_user_exists_and_is_current(keystone, endpoint, user, password, |
| 389 | email, tenant, check_mode) |
| 390 | elif tenant and user and not role and state == "absent": |
| 391 | changed = ensure_user_absent(keystone, user, check_mode) |
| 392 | elif tenant and user and role and state == "present": |
| 393 | changed, id = ensure_role_exists(keystone, user, tenant, role, |
| 394 | check_mode) |
| 395 | elif tenant and user and role and state == "absent": |
| 396 | changed = ensure_role_absent(keystone, user, tenant, role, check_mode) |
| 397 | else: |
| 398 | # Should never reach here |
| 399 | raise ValueError("Code should never reach here") |
| 400 | |
| 401 | return dict(changed=changed, id=id) |
| 402 | |
| 403 | # import module snippets |
| 404 | from ansible.module_utils.basic import * |
| 405 | from ansible.module_utils.openstack import * |
| 406 | if __name__ == '__main__': |
| 407 | main() |