├── templates ├── docker-keystone.service.j2 ├── clouds.yaml.j2 └── openrc.j2 ├── meta └── main.yml ├── handlers └── main.yml ├── defaults └── main.yml ├── tasks ├── certificates.yml ├── main.yml └── access.yml ├── README.md ├── LICENSE └── library └── keystone /templates/docker-keystone.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Keystone Container for CIAO 3 | Requires={{ docker_service }} 4 | After={{ docker_service }} 5 | 6 | [Service] 7 | Restart=always 8 | ExecStart=/usr/bin/docker start -a ciao-keystone 9 | ExecStop=/usr/bin/docker stop -t 2 ciao-keystone 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /templates/clouds.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | # {{ ansible_managed }} 3 | 4 | clouds: 5 | default: 6 | auth: 7 | username: admin 8 | password: {{ keystone_admin_password }} 9 | project_name: admin 10 | tenant_name: admin 11 | project_domain_id: default 12 | user_domain_id: default 13 | auth_url: https://{{ keystone_fqdn }}:35357/v3 14 | identity_api_version: 3 15 | cacert: certificates/keystone/keystone_cert.pem 16 | -------------------------------------------------------------------------------- /templates/openrc.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 3 | export OS_PROJECT_DOMAIN_ID=default 4 | export OS_USER_DOMAIN_ID=default 5 | export OS_PROJECT_NAME=admin 6 | export OS_TENANT_NAME=admin 7 | export OS_USERNAME=admin 8 | export OS_PASSWORD={{ keystone_admin_password }} 9 | export OS_AUTH_URL=https://{{ keystone_fqdn }}:35357/v3 10 | export OS_CACERT=certificates/keystone/keystone_cert.pem 11 | export OS_IDENTITY_API_VERSION=3 12 | export OS_IMAGE_API_VERSION=2 13 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: Alberto Murillo 3 | description: This role installs clearlinux/keystone docker container 4 | company: Intel 5 | 6 | issue_tracker_url: https://github.com/clearlinux/ansible-role-keystone 7 | 8 | license: Apache 9 | 10 | min_ansible_version: 2.1 11 | 12 | platforms: 13 | - name: Ubuntu 14 | versions: 15 | - xenial 16 | - name: ClearLinux 17 | 18 | galaxy_tags: 19 | - ubuntu 20 | - xenial 21 | - clearlinux 22 | - docker 23 | - keystone 24 | 25 | dependencies: [] 26 | -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) 2016 Intel Corporation 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | - name: reload systemd config 17 | command: systemctl daemon-reload 18 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) 2016 Intel Corporation 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # Fully Qualified Domain Name for Keystone node 17 | keystone_fqdn: "{{ ansible_fqdn }}" 18 | 19 | # Password for the 'admin' user in OpenStack 20 | keystone_admin_password: adminUserPassword 21 | 22 | # Path for mysql data 23 | mysql_data: /var/lib/mysql 24 | 25 | # A dictionary of host mappings to ips 26 | etc_hosts: {} 27 | 28 | ### The folllowing variables can be used to specify ### 29 | ### Custom services, projects, users and roles. ### 30 | # keystone_services: 31 | # - service: nova 32 | # type: compute 33 | # description: OpenStack Compute Service 34 | 35 | # keystone_projects: 36 | # - project: demo 37 | # description: Demo Project 38 | 39 | # keystone_users: 40 | # - user: demo 41 | # password: secret 42 | # project: demo 43 | # email: demo@example.com 44 | 45 | keystone_roles: 46 | - user 47 | 48 | # keystone_user_roles: 49 | # - user: demo 50 | # project: demo 51 | # role: demo 52 | -------------------------------------------------------------------------------- /tasks/certificates.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) 2016 Intel Corporation 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | - name: Create a local certificates directory if it does not exist 17 | become: no 18 | connection: local 19 | file: path=certificates/keystone state=directory 20 | 21 | - name: Create self-signed SSL cert 22 | become: no 23 | connection: local 24 | command: > 25 | openssl req -new -nodes -x509 -subj "/CN={{ keystone_fqdn }}" -days 365 26 | -keyout certificates/keystone/keystone_key.pem 27 | -out certificates/keystone/keystone_cert.pem -extensions v3_ca 28 | args: 29 | creates: certificates/keystone/keystone_key.pem 30 | 31 | - name: Create /etc/keystone/ssl directory 32 | file: path=/etc/keystone/ssl state=directory 33 | 34 | - name: Copy keystone certificates 35 | copy: src={{ item.src }} dest={{ item.dest }} 36 | with_items: 37 | - src: certificates/keystone/keystone_key.pem 38 | dest: /etc/keystone/ssl/keystone_key.pem 39 | - src: certificates/keystone/keystone_cert.pem 40 | dest: /etc/keystone/ssl/keystone_cert.pem 41 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) 2016 Intel Corporation 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | - name: Create directory for mariadb database 17 | file: path="{{ mysql_data }}" state=directory 18 | 19 | - include: certificates.yml 20 | 21 | - name: Create docker-keystone systemd unit 22 | template: src=docker-keystone.service.j2 dest=/etc/systemd/system/docker-keystone.service mode=0600 23 | notify: reload systemd config 24 | 25 | - name: Enable docker-keystone systemd unit 26 | service: name=docker-keystone.service enabled=yes 27 | 28 | - name: Start keystone container 29 | docker_container: 30 | name: ciao-keystone 31 | image: clearlinux/keystone 32 | state: started 33 | privileged: True 34 | etc_hosts: "{{ etc_hosts }}" 35 | published_ports: 36 | - 35357:35357 37 | - 5000:5000 38 | env: 39 | IDENTITY_HOST: "{{ keystone_fqdn }}" 40 | KEYSTONE_ADMIN_PASSWORD: "{{ keystone_admin_password }}" 41 | volumes: 42 | - "{{ mysql_data }}:/var/lib/mysql:rw" 43 | - "/etc/keystone/ssl/keystone_key.pem:/etc/nginx/ssl/keystone_key.pem:ro" 44 | - "/etc/keystone/ssl/keystone_cert.pem:/etc/nginx/ssl/keystone_cert.pem:ro" 45 | 46 | - name: Create openrc file 47 | become: no 48 | connection: local 49 | template: src=openrc.j2 dest=./openrc mode=0600 50 | 51 | - name: Create clouds.yaml file 52 | become: no 53 | connection: local 54 | template: src=clouds.yaml.j2 dest=./clouds.yaml mode=0600 55 | 56 | - include: access.yml 57 | connection: local 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clearlinux.keystone 2 | This role installs clearlinux/keystone docker container 3 | 4 | ## Requirements 5 | * docker 6 | 7 | ## Role variables 8 | 9 | Variable | Default Value | Description 10 | -------- | ------------- | ----------- 11 | keystone_fqdn | `{{ ansible_fqdn }}` | Fully Qualified Domain Name for Keystone server 12 | keystone_admin_password | adminUserPassword | Password for the admin user in keystone 13 | mysql_data | /var/lib/mysql | Path to hold mysql database file 14 | 15 | #### The following variables can be used to specify custom services, projects, users and roles 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | 58 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 |
VariableExampleDescription
keystone_services

 27 |     keystone_services:
 28 |       - service: nova
 29 |         type: compute
 30 |         description: OpenStack Compute Service
 31 |   
A list of services to be created
keystone_projects

 38 |     keystone_projects:
 39 |       - project: demo
 40 |         description: Demo Project
 41 |   
A list of projects to be created
keystone_users

 48 |     keystone_users:
 49 |       - user: demo
 50 |         password: secret
 51 |         project: demo
 52 |         email: demo@example.com
 53 |   
A list of users to be created
keystone_roles

 60 |     keystone_roles:
 61 |       - demo
 62 |       - admin
 63 |   
A list of roles to be created
keystone_user_roles

 70 |     keystone_user_roles:
 71 |       - user: demo
 72 |         project: demo
 73 |         role: demo
 74 |   
A list of user, role mappings
79 | 80 | ## Dependencies 81 | None 82 | 83 | ## Example playbook 84 | file *ciao.yml* 85 | ``` 86 | - hosts: controllers 87 | roles: 88 | - clearlinux.keystone 89 | ``` 90 | 91 | file *group_vars/all* 92 | ``` 93 | keystone_fqdn: identity.example.com 94 | keystone_admin_password: adminUserPassword 95 | mysql_data: /var/lib/mysql 96 | 97 | keystone_projects: 98 | - project: demo 99 | description: Demo Project 100 | 101 | keystone_users: 102 | - user: demo 103 | password: demoUserPassword 104 | project: demo 105 | 106 | keystone_roles: 107 | - demo 108 | 109 | keystone_user_roles: 110 | - user: demo 111 | project: demo 112 | role: demo 113 | ``` 114 | 115 | ## Contribution 116 | **Pull Requests and Issues should be opened at [clearlinux/clear-config-management](https://github.com/clearlinux/clear-config-management).** 117 | 118 | ## License 119 | Apache-2.0 120 | 121 | ## Author Information 122 | This role was created by [Leoswaldo Macias](leoswaldo.macias@intel.com) and [Obed Munoz](obed.n.munoz@intel.com) 123 | -------------------------------------------------------------------------------- /tasks/access.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright (c) 2016 Intel Corporation 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | - name: Wait for keystone to be ready 17 | wait_for: host="{{ keystone_fqdn }}" port=5000 state=started 18 | 19 | - name: Create custom services 20 | keystone: 21 | command: "ensure_service" 22 | service_name: "{{ item.service }}" 23 | service_type: "{{ item.type }}" 24 | description: "{{ item.description | default('') }}" 25 | endpoint: "https://{{ keystone_fqdn }}:35357/v3" 26 | login_user: "admin" 27 | login_password: "{{ keystone_admin_password }}" 28 | login_project_name: "admin" 29 | insecure: yes 30 | with_items: "{{ keystone_services | default([]) }}" 31 | 32 | - name: Create custom projects 33 | keystone: 34 | command: ensure_project 35 | project_name: "{{ item.project }}" 36 | domain_name: Default 37 | description: "{{ item.description | default('') }}" 38 | endpoint: "https://{{ keystone_fqdn }}:35357/v3" 39 | login_user: admin 40 | login_password: "{{ keystone_admin_password }}" 41 | login_project_name: "admin" 42 | insecure: yes 43 | with_items: "{{ keystone_projects | default([]) }}" 44 | 45 | - name: Create custom roles 46 | keystone: 47 | command: "ensure_role" 48 | role_name: "{{ item }}" 49 | endpoint: "https://{{ keystone_fqdn }}:35357/v3" 50 | login_user: admin 51 | login_password: "{{ keystone_admin_password }}" 52 | login_project_name: "admin" 53 | insecure: yes 54 | with_items: "{{ keystone_roles | default([]) }}" 55 | 56 | - name: Create custom users 57 | keystone: 58 | command: "ensure_user" 59 | user_name: "{{ item.user }}" 60 | project_name: "{{ item.project }}" 61 | password: "{{ item.password }}" 62 | email: "{{ item.email | default('') }}" 63 | domain_name: "Default" 64 | endpoint: "https://{{ keystone_fqdn }}:35357/v3" 65 | login_user: admin 66 | login_password: "{{ keystone_admin_password }}" 67 | login_project_name: "admin" 68 | insecure: yes 69 | with_items: "{{ keystone_users | default([]) }}" 70 | no_log: true 71 | 72 | - name: Map users and roles 73 | keystone: 74 | command: "ensure_user_role" 75 | user_name: "{{ item.user }}" 76 | project_name: "{{ item.project }}" 77 | role_name: "{{ item.role }}" 78 | endpoint: "https://{{ keystone_fqdn }}:35357/v3" 79 | login_user: "admin" 80 | login_password: "{{ keystone_admin_password }}" 81 | login_project_name: "admin" 82 | insecure: yes 83 | with_items: "{{ keystone_user_roles | default([]) }}" 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /library/keystone: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # (c) 2014, Kevin Carter 4 | # 5 | # Copyright 2014, Rackspace US, Inc. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | # Based on Jimmy Tang's implementation 20 | 21 | DOCUMENTATION = """ 22 | --- 23 | module: keystone 24 | version_added: "1.6.2" 25 | short_description: 26 | - Manage OpenStack Identity (keystone) users, projects, roles, and 27 | endpoints. 28 | description: 29 | - Manage OpenStack Identity (keystone) users, projects, roles, and 30 | endpoints. 31 | options: 32 | return_code: 33 | description: 34 | - Allow for return Codes other than 0 when executing commands. 35 | - This is a comma separated list of acceptable return codes. 36 | default: 0 37 | login_user: 38 | description: 39 | - login username to authenticate to keystone 40 | required: false 41 | default: admin 42 | login_user_domain_name: 43 | description: 44 | - The domain login_user belongs to 45 | required: false 46 | default: None 47 | login_password: 48 | description: 49 | - Password of login user 50 | required: false 51 | default: 'yes' 52 | login_project_name: 53 | description: 54 | - The project login_user belongs to 55 | required: false 56 | default: None 57 | login_project_domain_name: 58 | description: 59 | - The domain login_project belongs to 60 | required: false 61 | default: None 62 | login_tenant_name: 63 | description: 64 | - The tenant login_user belongs to 65 | required: false 66 | default: None 67 | token: 68 | description: 69 | - The token to be uses in case the password is not specified 70 | required: false 71 | default: None 72 | endpoint: 73 | description: 74 | - The keystone url for authentication 75 | required: false 76 | password: 77 | description: 78 | - The password to be assigned to the user 79 | required: false 80 | default: None 81 | user_name: 82 | description: 83 | - The name of the user that has to added/removed from OpenStack 84 | required: false 85 | default: None 86 | project_name: 87 | description: 88 | - The project name that has be added/removed 89 | required: false 90 | default: None 91 | tenant_name: 92 | description: 93 | - The tenant name that has be added/removed 94 | required: false 95 | default: None 96 | role_name: 97 | description: 98 | - The name of the role to be assigned or created 99 | required: false 100 | service_name: 101 | description: 102 | - Name of the service. 103 | required: false 104 | default: None 105 | region_name: 106 | description: 107 | - Name of the region. 108 | required: false 109 | default: None 110 | domain_name: 111 | description: 112 | - Name of the domain to add a project to. 113 | required: false 114 | default: 'Default' 115 | description: 116 | description: 117 | - A description for the project 118 | required: false 119 | default: None 120 | email: 121 | description: 122 | - Email address for the user, this is only used in "ensure_user" 123 | required: false 124 | default: None 125 | service_type: 126 | description: 127 | - Type of service. 128 | required: false 129 | default: None 130 | endpoint_list: 131 | description: 132 | - List of endpoints to add to keystone for a service 133 | required: false 134 | default: None 135 | type: list 136 | group_name: 137 | description: 138 | - A name for the group 139 | required: False 140 | default: None 141 | idp_name: 142 | description: 143 | - A name for the identity provider 144 | required: False 145 | default: None 146 | idp_remote_ids: 147 | description: 148 | - A URL that identifies the remote identity provider 149 | required: False 150 | default: None 151 | idp_enabled: 152 | description: 153 | - Set whether a remote identity provider is enabled 154 | required: False 155 | default: True 156 | sp_name: 157 | description: 158 | - A name for the service provider 159 | required: False 160 | default: None 161 | sp_enabled: 162 | description: 163 | - Set whether a service provider is enabled 164 | required: False 165 | default: True 166 | sp_url: 167 | description: 168 | - URL where the service provider expects to receive SAML assertions 169 | - eg: http(s)://${SP_HOST}:5000/Shibboleth.sso/SAML2/ECP 170 | required: False 171 | default: None 172 | sp_auth_url: 173 | description: 174 | - URL for federated users to request tokens from 175 | - eg: http(s)://${SP_HOST}:5000/v3/OS-FEDERATION 176 | /identity_providers/${IDP_ID}/saml2/auth 177 | required: False 178 | default: None 179 | protocol_name: 180 | description: 181 | - A name for the protocol 182 | required: False 183 | default: None 184 | mapping_name: 185 | description: 186 | - A name for the mapping 187 | required: False 188 | default: None 189 | mapping_rules: 190 | description: 191 | - A dictionary mapping federated users to local groups. 192 | - see: http://specs.openstack.org/openstack/keystone-specs 193 | /api/v3/identity-api-v3-os-federation-ext.html#mappings 194 | required: False 195 | default: None 196 | domain_enabled: 197 | description: 198 | - Name for a domain 199 | required: False 200 | default: True 201 | state: 202 | description: 203 | - Ensuring the endpoint is either present, absent, or update 204 | required: False 205 | default: 'present' 206 | command: 207 | description: 208 | - Indicate desired state of the resource 209 | choices: ['get_tenant', 'get_project', 'get_user', 'get_role', 210 | 'ensure_service', 'ensure_endpoint', 'ensure_role', 211 | 'ensure_user', 'ensure_user_role', 'ensure_tenant', 212 | 'ensure_project', 'ensure_service_provider', 213 | 'ensure_group', 'ensure_identity_provider', 214 | 'ensure_protocol', ensure_mapping', 215 | 'ensure_group_role'] 216 | required: true 217 | insecure: 218 | description: 219 | - Explicitly allow client to perform "insecure" TLS 220 | choices: 221 | - false 222 | - true 223 | default: false 224 | requirements: [ python-keystoneclient ] 225 | author: Kevin Carter 226 | """ 227 | 228 | EXAMPLES = """ 229 | # Create an admin project 230 | - keystone: 231 | command: "ensure_project" 232 | project_name: "admin" 233 | domain_name: "Default" 234 | description: "Admin project" 235 | 236 | # Create a service project 237 | - keystone: 238 | command: "ensure_project" 239 | project_name: "service" 240 | description: "Service project" 241 | 242 | # Create an admin user 243 | - keystone: 244 | command: "ensure_user" 245 | user_name: "admin" 246 | project_name: "admin" 247 | password: "secrete" 248 | email: "admin@some-domain.com" 249 | 250 | # Create an admin role 251 | - keystone: 252 | command: "ensure_role" 253 | role_name: "admin" 254 | 255 | # Create a user 256 | - keystone: 257 | command: "ensure_user" 258 | user_name: "glance" 259 | project_name: "service" 260 | password: "secrete" 261 | domain_name: "Default" 262 | email: "glance@some-domain.com" 263 | 264 | # Add a role to a user 265 | - keystone: 266 | command: "ensure_user_role" 267 | user_name: "glance" 268 | project_name: "service" 269 | role_name: "admin" 270 | 271 | # Add a project role to a group 272 | - keystone: 273 | command: "ensure_group_role" 274 | group_name: "fedgroup" 275 | project_name: "fedproject" 276 | role_name: "_member_" 277 | 278 | # Create a service 279 | - keystone: 280 | command: "ensure_service" 281 | service_name: "glance" 282 | service_type: "image" 283 | description: "Glance Image Service" 284 | 285 | # Create an endpoint 286 | - keystone: 287 | command: "ensure_endpoint" 288 | region_name: "RegionOne" 289 | service_name: "glance" 290 | service_type: "image" 291 | endpoint_list: 292 | - url: "http://127.0.0.1:9292" 293 | interface: "public" 294 | - url: "http://127.0.0.1:9292" 295 | interface: "admin" 296 | - url: "http://127.0.0.1:9292" 297 | interface: "internal" 298 | 299 | # Get project id 300 | - keystone: 301 | command: "get_project" 302 | project_name: "admin" 303 | 304 | # Get user id 305 | - keystone: 306 | command: "get_user" 307 | user_name: "admin" 308 | 309 | # Get role id 310 | - keystone: 311 | command: "get_role" 312 | user_name: "admin" 313 | 314 | """ 315 | 316 | COMMAND_MAP = { 317 | 'get_tenant': { 318 | 'variables': [ 319 | 'project_name', 320 | 'tenant_name' 321 | ] 322 | }, 323 | 'get_project': { 324 | 'variables': [ 325 | 'project_name', 326 | 'tenant_name' 327 | ] 328 | }, 329 | 'get_user': { 330 | 'variables': [ 331 | 'user_name' 332 | ] 333 | }, 334 | 'get_role': { 335 | 'variables': [ 336 | 'role_name', 337 | 'project_name', 338 | 'tenant_name', 339 | 'user_name' 340 | ] 341 | }, 342 | 'ensure_service': { 343 | 'variables': [ 344 | 'service_name', 345 | 'service_type', 346 | 'description' 347 | ] 348 | }, 349 | 'ensure_endpoint': { 350 | 'variables': [ 351 | 'region_name', 352 | 'service_name', 353 | 'service_type', 354 | 'endpoint_list', 355 | 'state' 356 | ] 357 | }, 358 | 'ensure_role': { 359 | 'variables': [ 360 | 'role_name' 361 | ] 362 | }, 363 | 'ensure_user': { 364 | 'variables': [ 365 | 'project_name', 366 | 'tenant_name', 367 | 'user_name', 368 | 'password', 369 | 'email', 370 | 'domain_name' 371 | ] 372 | }, 373 | 'ensure_user_role': { 374 | 'variables': [ 375 | 'user_name', 376 | 'project_name', 377 | 'tenant_name', 378 | 'role_name', 379 | 'domain_name' 380 | ] 381 | }, 382 | 'ensure_group_role': { 383 | 'variables': [ 384 | 'group_name', 385 | 'project_name', 386 | 'role_name', 387 | 'domain_name' 388 | ] 389 | }, 390 | 'ensure_project': { 391 | 'variables': [ 392 | 'project_name', 393 | 'tenant_name', 394 | 'description', 395 | 'domain_name' 396 | ] 397 | }, 398 | 'ensure_tenant': { 399 | 'variables': [ 400 | 'project_name', 401 | 'tenant_name', 402 | 'description', 403 | 'domain_name' 404 | ] 405 | }, 406 | 'ensure_group': { 407 | 'variables': [ 408 | 'group_name', 409 | 'domain_name' 410 | ] 411 | }, 412 | 'ensure_identity_provider': { 413 | 'variables': [ 414 | 'idp_name', 415 | 'idp_remote_ids', 416 | 'idp_enabled' 417 | ] 418 | }, 419 | 'ensure_service_provider': { 420 | 'variables': [ 421 | 'sp_name', 422 | 'sp_url', 423 | 'sp_auth_url', 424 | 'sp_enabled' 425 | ] 426 | }, 427 | 'ensure_protocol': { 428 | 'variables': [ 429 | 'protocol_name', 430 | 'idp_name', 431 | 'mapping_name' 432 | ] 433 | }, 434 | 'ensure_mapping': { 435 | 'variables': [ 436 | 'mapping_name', 437 | 'mapping_rules', 438 | ] 439 | }, 440 | 'ensure_domain': { 441 | 'variables': [ 442 | 'domain_name', 443 | 'domain_enabled' 444 | ] 445 | } 446 | } 447 | 448 | try: 449 | from keystoneclient import exceptions as kexceptions 450 | from keystoneclient.v3 import client 451 | except ImportError: 452 | keystoneclient_found = False 453 | else: 454 | keystoneclient_found = True 455 | 456 | 457 | class ManageKeystone(object): 458 | def __init__(self, module): 459 | """Manage Keystone via Ansible.""" 460 | self.state_change = False 461 | self.keystone = None 462 | 463 | # Load AnsibleModule 464 | self.module = module 465 | 466 | def command_router(self): 467 | """Run the command as its provided to the module.""" 468 | command_name = self.module.params['command'] 469 | if command_name not in COMMAND_MAP: 470 | self.failure( 471 | error='No Command Found', 472 | rc=2, 473 | msg='Command [ %s ] was not found.' % command_name 474 | ) 475 | 476 | action_command = COMMAND_MAP[command_name] 477 | if hasattr(self, '%s' % command_name): 478 | action = getattr(self, '%s' % command_name) 479 | facts = action(variables=action_command['variables']) 480 | if facts is None: 481 | self.module.exit_json(changed=self.state_change) 482 | else: 483 | self.module.exit_json( 484 | changed=self.state_change, 485 | ansible_facts=facts 486 | ) 487 | else: 488 | self.failure( 489 | error='Command not in ManageKeystone class', 490 | rc=2, 491 | msg='Method [ %s ] was not found.' % command_name 492 | ) 493 | 494 | @staticmethod 495 | def _facts(facts): 496 | """Return a dict for our Ansible facts. 497 | 498 | :param facts: ``dict`` Dict with data to return 499 | """ 500 | return {'keystone_facts': facts} 501 | 502 | def _get_vars(self, variables, required=None): 503 | """Return a dict of all variables as found within the module. 504 | 505 | :param variables: ``list`` List of all variables that are available to 506 | use within the Keystone Command. 507 | :param required: ``list`` Name of variables that are required. 508 | """ 509 | return_dict = {} 510 | for variable in variables: 511 | return_dict[variable] = self.module.params.get(variable) 512 | else: 513 | if isinstance(required, list): 514 | for var_name in required: 515 | check = return_dict.get(var_name) 516 | if check is None: 517 | self.failure( 518 | error='Missing [ %s ] from Task or found a None' 519 | ' value' % var_name, 520 | rc=000, 521 | msg='variables %s - available params [ %s ]' 522 | % (variables, self.module.params) 523 | ) 524 | return return_dict 525 | 526 | def failure(self, error, rc, msg): 527 | """Return a Failure when running an Ansible command. 528 | 529 | :param error: ``str`` Error that occurred. 530 | :param rc: ``int`` Return code while executing an Ansible command. 531 | :param msg: ``str`` Message to report. 532 | """ 533 | self.module.fail_json(msg=msg, rc=rc, err=error) 534 | 535 | def _authenticate(self): 536 | """Return a keystone client object.""" 537 | required_vars = ['endpoint'] 538 | variables = [ 539 | 'endpoint', 540 | 'login_user', 541 | 'login_password', 542 | 'login_project_name', 543 | 'login_tenant_name', 544 | 'login_user_domain_name', 545 | 'login_project_domain_name', 546 | 'token', 547 | 'insecure' 548 | ] 549 | variables_dict = self._get_vars(variables, required=required_vars) 550 | 551 | endpoint = variables_dict.pop('endpoint') 552 | login_user = variables_dict.pop('login_user') 553 | login_password = variables_dict.pop('login_password') 554 | login_project_name = (variables_dict.pop('login_project_name', None) or 555 | variables_dict.pop('login_tenant_name')) 556 | user_domain_name = variables_dict.pop('login_user_domain_name', 557 | 'Default') 558 | project_domain_name = variables_dict.pop('login_project_domain_name', 559 | 'Default') 560 | token = variables_dict.pop('token') 561 | insecure = variables_dict.pop('insecure') 562 | 563 | if token is None: 564 | if login_project_name is None: 565 | self.failure( 566 | error='Missing Project Name', 567 | rc=2, 568 | msg='If you do not specify a token you must use a project' 569 | ' name for authentication. Try adding' 570 | ' [ login_project_name ] to the task' 571 | ) 572 | if login_password is None: 573 | self.failure( 574 | error='Missing Password', 575 | rc=2, 576 | msg='If you do not specify a token you must use a password' 577 | ' name for authentication. Try adding' 578 | ' [ login_password ] to the task' 579 | ) 580 | 581 | if token: 582 | self.keystone = client.Client( 583 | insecure=insecure, 584 | endpoint=endpoint, 585 | token=token 586 | ) 587 | else: 588 | self.keystone = client.Client( 589 | insecure=insecure, 590 | auth_url=endpoint, 591 | username=login_user, 592 | user_domain_name=user_domain_name, 593 | password=login_password, 594 | project_name=login_project_name, 595 | project_domain_name=project_domain_name, 596 | ) 597 | 598 | def _get_domain_from_vars(self, variables): 599 | # NOTE(sigmavirus24): Since we don't require domain, this will be None 600 | # in the dictionary. When we pop it, we can't provide a default 601 | # because 'domain' exists and is None. In order to use a default 602 | # value, we need to use `or 'default'` here to make sure we default to 603 | # the default domain. If we don't do it this way, Keystone throws a 604 | # 401 Unauthorized which is just plain wrong. 605 | domain_name = variables.pop('domain_name', None) or 'Default' 606 | 607 | return self._get_domain(name=domain_name) 608 | 609 | def _get_domain(self, name): 610 | """Return domain information. 611 | 612 | :param str name: Name of the domain. 613 | """ 614 | for entry in self.keystone.domains.list(): 615 | if entry.name == name: 616 | return entry 617 | else: 618 | return None 619 | 620 | def _get_project(self, name): 621 | """Return project information. 622 | 623 | Formerly, _get_tenant 624 | 625 | :param name: ``str`` Name of the project. 626 | """ 627 | for entry in self.keystone.projects.list(): 628 | if entry.name == name: 629 | return entry 630 | else: 631 | return None 632 | 633 | def get_tenant(self, variables): 634 | return self.get_project(variables) 635 | 636 | def get_project(self, variables): 637 | """Return a project id. 638 | 639 | This will return `None` if the ``name`` is not found. 640 | 641 | :param variables: ``list`` List of all variables that are available to 642 | use within the Keystone Command. 643 | """ 644 | self._authenticate() 645 | variables_dict = self._get_vars(variables) 646 | project_name = (variables_dict.pop('project_name', None) or 647 | variables_dict.pop('tenant_name')) 648 | project = self._get_project(name=project_name) 649 | if project is None: 650 | self.failure( 651 | error='project [ %s ] was not found.' % project_name, 652 | rc=2, 653 | msg='project was not found, does it exist?' 654 | ) 655 | 656 | return self._facts(facts={'id': project.id}) 657 | 658 | def ensure_tenant(self, variables): 659 | return self.ensure_project(variables) 660 | 661 | def ensure_project(self, variables): 662 | """Create a new project within Keystone if it does not exist. 663 | 664 | Returns the project ID on a successful run. 665 | 666 | :param variables: ``list`` List of all variables that are available to 667 | use within the Keystone Command. 668 | """ 669 | self._authenticate() 670 | variables_dict = self._get_vars(variables) 671 | project_name = (variables_dict.pop('project_name', None) or 672 | variables_dict.pop('tenant_name')) 673 | project_description = variables_dict.pop('description') 674 | if project_description is None: 675 | project_description = 'Project %s' % project_name 676 | 677 | domain = self._get_domain_from_vars(variables_dict) 678 | project = self._get_project(name=project_name) 679 | if project is None: 680 | self.state_change = True 681 | project = self.keystone.projects.create( 682 | name=project_name, 683 | description=project_description, 684 | domain=domain, 685 | enabled=True 686 | ) 687 | 688 | return self._facts(facts={'id': project.id}) 689 | 690 | def _get_user(self, name, domain): 691 | """Return a user information. 692 | 693 | This will return `None` if the ``name`` is not found. 694 | 695 | :param name: ``str`` Name of the user. 696 | """ 697 | for entry in self.keystone.users.list(domain=domain): 698 | if getattr(entry, 'name', None) == name: 699 | return entry 700 | else: 701 | return None 702 | 703 | def get_user(self, variables): 704 | """Return a project id. 705 | 706 | This will return `None` if the ``name`` is not found. 707 | 708 | :param variables: ``list`` List of all variables that are available to 709 | use within the Keystone Command. 710 | """ 711 | self._authenticate() 712 | variables_dict = self._get_vars(variables, required=['user_name']) 713 | user_name = variables_dict.pop('user_name') 714 | domain = self._get_domain_from_vars(variables_dict) 715 | user = self._get_user(name=user_name, domain=domain) 716 | if user is None: 717 | self.failure( 718 | error='user [ %s ] was not found.' % user_name, 719 | rc=2, 720 | msg='user was not found, does it exist?' 721 | ) 722 | 723 | return self._facts(facts={'id': user.id}) 724 | 725 | def ensure_user(self, variables): 726 | """Create a new user within Keystone if it does not exist. 727 | 728 | Returns the user ID on a successful run. 729 | 730 | :param variables: ``list`` List of all variables that are available to 731 | use within the Keystone Command. 732 | """ 733 | self._authenticate() 734 | required_vars = ['user_name', 'password'] 735 | variables_dict = self._get_vars(variables, required=required_vars) 736 | project_name = (variables_dict.pop('project_name', None) or 737 | variables_dict.pop('tenant_name')) 738 | password = variables_dict.pop('password') 739 | user_name = variables_dict.pop('user_name') 740 | email = variables_dict.pop('email') 741 | 742 | domain = self._get_domain_from_vars(variables_dict) 743 | project = self._get_project(name=project_name) 744 | if project is None and project_name is not None: 745 | self.failure( 746 | error='project [ %s ] was not found.' % project_name, 747 | rc=2, 748 | msg='project was not found, does it exist?' 749 | ) 750 | 751 | user = self._get_user(name=user_name, domain=domain) 752 | if user is None: 753 | self.state_change = True 754 | user = self.keystone.users.create( 755 | name=user_name, 756 | password=password, 757 | email=email, 758 | domain=domain, 759 | default_project=project 760 | ) 761 | 762 | return self._facts(facts={'id': user.id}) 763 | 764 | def _get_role(self, name, domain): 765 | """Return a role by name. 766 | 767 | This will return `None` if the ``name`` is not found. 768 | 769 | :param name: ``str`` Name of the role. 770 | :param domain: ``str`` ID of the domain 771 | """ 772 | for entry in self.keystone.roles.list(domain=domain): 773 | if entry.name == name: 774 | return entry 775 | else: 776 | return None 777 | 778 | def _get_group(self, name, domain='Default'): 779 | """Return a group by name. 780 | 781 | This will return `None` if the ``name`` is not found. 782 | 783 | :param name: ``str`` Name of the role. 784 | """ 785 | for entry in self.keystone.groups.list(domain=domain): 786 | if domain is None: 787 | if entry.name == name: 788 | return entry 789 | else: 790 | if entry.name == name and entry.domain_id == domain.id: 791 | return entry 792 | else: 793 | return None 794 | 795 | def get_role(self, variables): 796 | """Return a role by name. 797 | 798 | This will return `None` if the ``name`` is not found. 799 | 800 | :param variables: ``list`` List of all variables that are available to 801 | use within the Keystone Command. 802 | """ 803 | self._authenticate() 804 | variables_dict = self._get_vars(variables, required=['role_name']) 805 | role_name = variables_dict.pop('role_name') 806 | domain = self._get_domain_from_vars(variables_dict) 807 | role_data = self._get_role(name=role_name, domain=domain) 808 | if role_data is None: 809 | self.failure( 810 | error='role [ %s ] was not found.' % role_name, 811 | rc=2, 812 | msg='role was not found, does it exist?' 813 | ) 814 | 815 | return self._facts(facts={'id': role_data.id}) 816 | 817 | def _get_role_data(self, user_name, project_name, role_name, group_name, 818 | domain): 819 | if user_name is not None: 820 | user = self._get_user(name=user_name, domain=domain) 821 | if user is None: 822 | self.failure( 823 | error='user [ %s ] was not found.' % user_name, 824 | rc=2, 825 | msg='User was not found, does it exist?' 826 | ) 827 | else: 828 | user = None 829 | 830 | project = self._get_project(name=project_name) 831 | if project is None and project_name is not None: 832 | self.failure( 833 | error='project [ %s ] was not found.' % project_name, 834 | rc=2, 835 | msg='project was not found, does it exist?' 836 | ) 837 | 838 | role = self._get_role(name=role_name, domain=domain) 839 | if role is None: 840 | self.failure( 841 | error='role [ %s ] was not found.' % role_name, 842 | rc=2, 843 | msg='role was not found, does it exist?' 844 | ) 845 | 846 | if group_name is not None: 847 | group = self._get_group(name=group_name, domain=domain) 848 | if group is None: 849 | self.failure( 850 | error='group [ %s ] was not found.' % group_name, 851 | rc=2, 852 | msg='group was not found, does it exist?' 853 | ) 854 | else: 855 | group = None 856 | 857 | return user, project, role, group 858 | 859 | def ensure_role(self, variables): 860 | """Create a new role within Keystone if it does not exist. 861 | 862 | Returns the user ID on a successful run. 863 | 864 | :param variables: ``list`` List of all variables that are available to 865 | use within the Keystone Command. 866 | """ 867 | self._authenticate() 868 | variables_dict = self._get_vars(variables, required=['role_name']) 869 | domain = self._get_domain_from_vars(variables_dict) 870 | role_name = variables_dict.pop('role_name') 871 | 872 | role = self._get_role(name=role_name, domain=domain) 873 | if role is None: 874 | self.state_change = True 875 | role = self.keystone.roles.create(role_name) 876 | 877 | return self._facts(facts={'id': role.id}) 878 | 879 | def _get_user_roles(self, name, user, project, domain): 880 | role_list = self.keystone.roles.list( 881 | user=user, 882 | project=project, 883 | domain=domain 884 | ) 885 | for entry in role_list: 886 | if entry.name == name: 887 | return entry 888 | else: 889 | return None 890 | 891 | def _get_group_roles(self, name, group, project): 892 | group_list = self.keystone.roles.list( 893 | group=group, 894 | project=project 895 | ) 896 | for entry in group_list: 897 | if entry.name == name: 898 | return entry 899 | else: 900 | return None 901 | 902 | def ensure_user_role(self, variables): 903 | self._authenticate() 904 | required_vars = ['user_name', 'role_name'] 905 | variables_dict = self._get_vars(variables, required=required_vars) 906 | domain = self._get_domain_from_vars(variables_dict) 907 | user_name = variables_dict.pop('user_name') 908 | # NOTE(sigmavirus24): Try to get the project_name, but 909 | # don't error out on it. This will change when the playbooks are 910 | # updated to use project_name instead of tenant_name 911 | project_name = (variables_dict.pop('project_name', None) or 912 | variables_dict.pop('tenant_name')) 913 | role_name = variables_dict.pop('role_name') 914 | 915 | if project_name is not None: 916 | domain = None 917 | 918 | user, project, role, group = self._get_role_data( 919 | user_name=user_name, project_name=project_name, 920 | role_name=role_name, group_name=None, domain=domain 921 | ) 922 | 923 | user_role = self._get_user_roles( 924 | name=role_name, user=user, project=project, domain=domain 925 | ) 926 | 927 | if user_role is None: 928 | self.state_change = True 929 | self.keystone.roles.grant( 930 | user=user, role=role, project=project, domain=domain 931 | ) 932 | user_role = self._get_user_roles( 933 | name=role_name, user=user, project=project, domain=domain 934 | ) 935 | 936 | return self._facts(facts={'id': user_role.id}) 937 | 938 | def ensure_group_role(self, variables): 939 | self._authenticate() 940 | required_vars = ['group_name', 'project_name', 'role_name'] 941 | variables_dict = self._get_vars(variables, required=required_vars) 942 | domain = self._get_domain_from_vars(variables_dict) 943 | group_name = variables_dict.pop('group_name') 944 | project_name = variables_dict.pop('project_name') 945 | role_name = variables_dict.pop('role_name') 946 | 947 | if project_name is not None: 948 | domain = None 949 | 950 | user, project, role, group = self._get_role_data( 951 | group_name=group_name, project_name=project_name, 952 | role_name=role_name, user_name=None, domain=domain 953 | ) 954 | 955 | group_role = self._get_group_roles( 956 | name=role_name, group=group, project=project, domain=domain 957 | ) 958 | 959 | if group_role is None: 960 | self.state_change = True 961 | self.keystone.roles.grant( 962 | group=group, role=role, project=project, domain=domain 963 | ) 964 | group_role = self._get_group_roles( 965 | name=role_name, 966 | group=group, 967 | project=project, 968 | domain=domain 969 | ) 970 | 971 | return self._facts(facts={'id': group_role.id}) 972 | 973 | def ensure_group(self, variables): 974 | """Create a new group within Keystone if it does not exist. 975 | 976 | Returns the group ID on a successful run. 977 | 978 | :param variables: ``list`` List of all variables that are available to 979 | use within the Keystone Command. 980 | """ 981 | 982 | self._authenticate() 983 | required_vars = ['group_name', 'domain_name'] 984 | variables_dict = self._get_vars(variables, required=required_vars) 985 | group_name = variables_dict.pop('group_name') 986 | 987 | domain = self._get_domain_from_vars(variables_dict) 988 | 989 | group = self._get_group( 990 | name=group_name, domain=domain 991 | ) 992 | 993 | if group is None: 994 | self.state_change = True 995 | group = self.keystone.groups.create( 996 | name=group_name, domain=domain 997 | ) 998 | 999 | return self._facts(facts={'id': group.id}) 1000 | 1001 | def _get_service(self, name, srv_type=None): 1002 | for entry in self.keystone.services.list(): 1003 | if srv_type is not None: 1004 | if entry.type == srv_type and name == entry.name: 1005 | return entry 1006 | elif entry.name == name: 1007 | return entry 1008 | else: 1009 | return None 1010 | 1011 | def ensure_service(self, variables): 1012 | """Create a new service within Keystone if it does not exist. 1013 | 1014 | Returns the service ID on a successful run. 1015 | 1016 | :param variables: ``list`` List of all variables that are available to 1017 | use within the Keystone Command. 1018 | """ 1019 | self._authenticate() 1020 | required_vars = ['service_name', 'service_type'] 1021 | variables_dict = self._get_vars(variables, required=required_vars) 1022 | 1023 | service_name = variables_dict.pop('service_name') 1024 | description = variables_dict.pop('description') 1025 | service_type = variables_dict.pop('service_type') 1026 | 1027 | service = self._get_service(name=service_name, srv_type=service_type) 1028 | if service is None or service.type != service_type: 1029 | self.state_change = True 1030 | service = self.keystone.services.create( 1031 | name=service_name, 1032 | type=service_type, 1033 | description=description 1034 | ) 1035 | 1036 | return self._facts(facts={'id': service.id}) 1037 | 1038 | def _get_endpoint_by_details(self, region, service_id, interface): 1039 | """ Getting endpoints per complete definition 1040 | 1041 | Returns the endpoint details for an endpoint matching 1042 | region, service id and interface. 1043 | 1044 | :param interface: ``str`` ‘public’, ‘admin’ or ‘internal’ network 1045 | interface 1046 | :param service_id: service to which the endpoint belongs 1047 | :param region: geographic location of the endpoint 1048 | 1049 | """ 1050 | for entry in self.keystone.endpoints.list(): 1051 | check = [ 1052 | entry.region == region, 1053 | entry.service_id == service_id, 1054 | entry.interface == interface 1055 | ] 1056 | if all(check): 1057 | return entry 1058 | else: 1059 | return None 1060 | 1061 | def _get_endpoint(self, region, url, interface): 1062 | """ Getting endpoints per URL 1063 | Returns the endpoint details for an endpoint matching 1064 | URL, region and interface. 1065 | This interface should be deprecated in next release. 1066 | """ 1067 | for entry in self.keystone.endpoints.list(): 1068 | check = [ 1069 | entry.region == region, 1070 | entry.url == url, 1071 | entry.interface == interface 1072 | ] 1073 | if all(check): 1074 | return entry 1075 | else: 1076 | return None 1077 | 1078 | def ensure_endpoint(self, variables): 1079 | """Ensures the deletion/modification/addition of endpoints 1080 | within Keystone. 1081 | 1082 | Returns the endpoint ID on a successful run. 1083 | 1084 | :param variables: ``list`` List of all variables that are available to 1085 | use within the Keystone Command. 1086 | """ 1087 | self._authenticate() 1088 | required_vars = [ 1089 | 'region_name', 1090 | 'service_name', 1091 | 'service_type', 1092 | 'endpoint_list' 1093 | ] 1094 | variables_dict = self._get_vars(variables, required=required_vars) 1095 | 1096 | service_name = variables_dict.pop('service_name') 1097 | service_type = variables_dict.pop('service_type') 1098 | region = variables_dict.pop('region_name') 1099 | endpoint_list = variables_dict.pop('endpoint_list') 1100 | state = variables_dict.pop('state') 1101 | 1102 | service = self._get_service(name=service_name, srv_type=service_type) 1103 | if service is None: 1104 | self.failure( 1105 | error='service [ %s ] was not found.' % service_name, 1106 | rc=2, 1107 | msg='Service was not found, does it exist?' 1108 | ) 1109 | 1110 | endpoints = {} 1111 | for endpoint_dict in endpoint_list: 1112 | url = endpoint_dict.pop('url') 1113 | interface = endpoint_dict.pop('interface') 1114 | endpoint = self._get_endpoint( 1115 | region=region, 1116 | url=url, 1117 | interface=interface 1118 | ) 1119 | if state == 'present': 1120 | ''' Creating an endpoint for this url 1121 | (if it does not exist) 1122 | ''' 1123 | if endpoint is None: 1124 | self.state_change = True 1125 | endpoint = self.keystone.endpoints.create( 1126 | region=region, 1127 | service=service, 1128 | url=url, 1129 | interface=interface 1130 | ) 1131 | elif state == 'update': 1132 | ''' Checking if there is a similar endpoint with a 1133 | different url. Update it if there is one, create 1134 | if there is none. 1135 | ''' 1136 | similar_endpoint = self._get_endpoint_by_details( 1137 | region=region, 1138 | service_id=service.id, 1139 | interface=interface 1140 | ) 1141 | if similar_endpoint is None: 1142 | self.state_change = True 1143 | endpoint = self.keystone.endpoints.create( 1144 | region=region, 1145 | service=service, 1146 | url=url, 1147 | interface=interface 1148 | ) 1149 | elif similar_endpoint.url != url: 1150 | self.state_change = True 1151 | endpoint = self.keystone.endpoints.update( 1152 | endpoint=similar_endpoint, 1153 | url=url 1154 | ) 1155 | elif state == 'absent': 1156 | if endpoint is not None: 1157 | self.state_change = True 1158 | result = self.keystone.endpoints.delete(endpoint.id) 1159 | if result[0].status_code != 204: 1160 | module.fail() 1161 | 1162 | if state != 'absent': 1163 | endpoints[interface] = endpoint 1164 | return self._facts({'%sid' % interface: endpoint.id 1165 | for interface, endpoint in endpoints.items()}) 1166 | else: 1167 | return self._facts({}) 1168 | 1169 | def _ensure_generic(self, manager, required_vars, variables): 1170 | """Try and create a new 'thing' in keystone. 1171 | 1172 | Thing type is determined by the manager passed in. 1173 | 1174 | :param: manager - openstack object manager eg self.keystone.groups 1175 | :param: required_vars - dictionary: 1176 | ansible module argument name : manager argument name 1177 | eg {'group_name': 'name'} 1178 | 1179 | :returns: Facts dictionary with things = 1180 | 1181 | 1182 | TODO: make this handle updates as well as creates 1183 | TODO (maybe, if we decide to use this module long term): 1184 | migrate other ensures to use this 1185 | """ 1186 | 1187 | # Get values for variables 1188 | variables_dict = self._get_vars(variables, 1189 | required=required_vars.keys()) 1190 | 1191 | # Translate ansible module argument names to manager expected names 1192 | args_dict = {required_vars[k]: v for k, v in variables_dict.items()} 1193 | 1194 | try: 1195 | manager.create(**args_dict) 1196 | self.state_change = True 1197 | except kexceptions.Conflict: 1198 | self.state_change = False 1199 | 1200 | try: 1201 | return self._facts(facts={ 1202 | manager.collection_key: 1203 | [x.to_dict() for x in manager.list()] 1204 | }) 1205 | except TypeError: 1206 | # some managers require arguments to their list functions :/ 1207 | # return no facts in this case. 1208 | return self._facts(facts={}) 1209 | 1210 | def ensure_identity_provider(self, variables): 1211 | self._authenticate() 1212 | return self._ensure_generic( 1213 | manager=self.keystone.federation.identity_providers, 1214 | required_vars={'idp_name': 'id', 1215 | 'idp_remote_ids': 'remote_ids', 1216 | 'idp_enabled': 'enabled'}, 1217 | variables=variables 1218 | ) 1219 | 1220 | def ensure_service_provider(self, variables): 1221 | self._authenticate() 1222 | return self._ensure_generic( 1223 | manager=self.keystone.federation.service_providers, 1224 | required_vars={'sp_name': 'id', 1225 | 'sp_auth_url': 'auth_url', 1226 | 'sp_url': 'sp_url', 1227 | 'sp_enabled': 'enabled'}, 1228 | variables=variables 1229 | ) 1230 | 1231 | def ensure_protocol(self, variables): 1232 | """Facts not returned 1233 | 1234 | This is because you can't list protocols without 1235 | specifying an identity provider 1236 | """ 1237 | 1238 | self._authenticate() 1239 | return self._ensure_generic( 1240 | manager=self.keystone.federation.protocols, 1241 | required_vars={'protocol_name': 'protocol_id', 1242 | 'idp_name': 'identity_provider', 1243 | 'mapping_name': 'mapping'}, 1244 | variables=variables 1245 | ) 1246 | 1247 | def ensure_mapping(self, variables): 1248 | self._authenticate() 1249 | return self._ensure_generic( 1250 | manager=self.keystone.federation.mappings, 1251 | required_vars={'mapping_name': 'mapping_id', 1252 | 'mapping_rules': 'rules'}, 1253 | variables=variables 1254 | ) 1255 | 1256 | def ensure_domain(self, variables): 1257 | self._authenticate() 1258 | return self._ensure_generic( 1259 | manager=self.keystone.domains, 1260 | required_vars={'domain_name': 'name', 1261 | 'domain_enabled': 'enabled'}, 1262 | variables=variables 1263 | ) 1264 | 1265 | 1266 | def main(): 1267 | module = AnsibleModule( 1268 | argument_spec=dict( 1269 | login_user=dict( 1270 | required=False 1271 | ), 1272 | login_user_domain_name=dict( 1273 | required=False 1274 | ), 1275 | login_password=dict( 1276 | required=False 1277 | ), 1278 | login_tenant_name=dict( 1279 | required=False 1280 | ), 1281 | login_project_name=dict( 1282 | required=False 1283 | ), 1284 | login_project_domain_name=dict( 1285 | required=False 1286 | ), 1287 | token=dict( 1288 | required=False 1289 | ), 1290 | password=dict( 1291 | required=False 1292 | ), 1293 | endpoint=dict( 1294 | required=True, 1295 | ), 1296 | user_name=dict( 1297 | required=False 1298 | ), 1299 | tenant_name=dict( 1300 | required=False 1301 | ), 1302 | project_name=dict( 1303 | required=False 1304 | ), 1305 | domain_name=dict( 1306 | required=False 1307 | ), 1308 | role_name=dict( 1309 | required=False 1310 | ), 1311 | service_name=dict( 1312 | required=False 1313 | ), 1314 | region_name=dict( 1315 | required=False 1316 | ), 1317 | description=dict( 1318 | required=False 1319 | ), 1320 | email=dict( 1321 | required=False 1322 | ), 1323 | service_type=dict( 1324 | required=False 1325 | ), 1326 | endpoint_list=dict( 1327 | required=False, 1328 | type='list' 1329 | ), 1330 | command=dict( 1331 | required=True, 1332 | choices=COMMAND_MAP.keys() 1333 | ), 1334 | insecure=dict( 1335 | default=False, 1336 | required=False, 1337 | type='bool' 1338 | ), 1339 | return_code=dict( 1340 | type='str', 1341 | default='0' 1342 | ), 1343 | group_name=dict( 1344 | type='str', 1345 | required=False 1346 | ), 1347 | idp_remote_ids=dict( 1348 | type='list', 1349 | required=False, 1350 | ), 1351 | idp_name=dict( 1352 | type='str', 1353 | required=False, 1354 | ), 1355 | idp_enabled=dict( 1356 | type='bool', 1357 | default=True, 1358 | required=False, 1359 | ), 1360 | sp_name=dict( 1361 | type='str', 1362 | required=False, 1363 | ), 1364 | sp_auth_url=dict( 1365 | type='str', 1366 | required=False, 1367 | ), 1368 | sp_url=dict( 1369 | type='str', 1370 | required=False, 1371 | ), 1372 | sp_enabled=dict( 1373 | type='bool', 1374 | default=True, 1375 | required=False, 1376 | ), 1377 | protocol_name=dict( 1378 | type='str', 1379 | required=False, 1380 | ), 1381 | mapping_name=dict( 1382 | type='str', 1383 | required=False, 1384 | ), 1385 | mapping_rules=dict( 1386 | type='list', 1387 | required=False, 1388 | ), 1389 | domain_enabled=dict( 1390 | type='bool', 1391 | required=False, 1392 | default=True 1393 | ), 1394 | state=dict( 1395 | choices=['present', 'absent', 'update'], 1396 | required=False, 1397 | default='present' 1398 | ) 1399 | ), 1400 | supports_check_mode=False, 1401 | mutually_exclusive=[ 1402 | ['token', 'login_user'], 1403 | ['token', 'login_password'], 1404 | ['token', 'login_tenant_name'] 1405 | ] 1406 | ) 1407 | 1408 | km = ManageKeystone(module=module) 1409 | if not keystoneclient_found: 1410 | km.failure( 1411 | error='python-keystoneclient is missing', 1412 | rc=2, 1413 | msg='keystone client was not importable, is it installed?' 1414 | ) 1415 | 1416 | return_code = module.params.get('return_code', '').split(',') 1417 | module.params['return_code'] = return_code 1418 | km.command_router() 1419 | 1420 | 1421 | # import module snippets 1422 | from ansible.module_utils.basic import * # NOQA 1423 | if __name__ == '__main__': 1424 | main() 1425 | --------------------------------------------------------------------------------