├── LICENSE ├── README.md └── rancher.py /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ansible-rancher-module 2 | Module to manage Rancher through Ansible 3 | 4 | You can configure the stacks, the environments and the catalogs. 5 | 6 | ```yaml 7 | - name: 'Rancher module' 8 | hosts: localhost 9 | connection: local 10 | gather_facts: no 11 | tasks: 12 | - name: 'Setup Rancher' 13 | rancher: 14 | url: 'https://rancher.url/' 15 | access_key: 'ACCESS_KEY' 16 | secret_key: 'SECRET_KEY' 17 | catalogs: 18 | - url: 'https://.../rancher-catalog.git' 19 | name: 'MyCatalog' 20 | branch: 'master' 21 | environments: 22 | - name: Production 23 | stacks: 24 | - name: Janitor 25 | catalog_entry: 'MyCatalog:Janitor' 26 | answers: 27 | - name: 'FREQUENCY' 28 | value: '9200' 29 | - name: 'Display Registration Token' 30 | debug: 31 | msg: "Rancher Production Token is {{ rancher.envs.Production.token }}" 32 | ``` 33 | 34 | This playbook will : 35 | 36 | - Add or update a catalog named MyCatalog with repository (https://.../rancher-catalog.git) on master branch 37 | - Add a Production environment if it doesn't exist 38 | - Add or update a Janitor stack based on the Janitor item of the catalog MyCatalog, with variable FREQUENCY set to 9200 39 | - Gather fact in rancher.envs with registration token 40 | 41 | **Parameters** 42 | 43 | | Name | Required | Value | Default 44 | |--------|-----|-------|----| 45 | | url | * | Rancher URL 46 | | access_key | * | API Access Key 47 | | secret_key | * | API Secret Key 48 | | catalogs | | List of catalogs to add 49 | | environments | | List of environments to add 50 | | clean_envs | | Keep only specified environments | False 51 | | clean_catalogs | | Keep only specified catalogs | False 52 | | clean_stacks | | Keep only specified environments | False 53 | 54 | **catalog** 55 | 56 | ``` 57 | url: 'https://.../rancher-catalog.git' 58 | name: 'MyCatalog' 59 | branch: 'master' 60 | ``` 61 | 62 | *branch* is by default master 63 | 64 | *name* is used for matching catalog 65 | 66 | **environments** 67 | 68 | ``` 69 | name: Default 70 | stacks: 71 | - ... 72 | ``` 73 | *name* is the main key 74 | 75 | *stacks* list of stack 76 | 77 | **stack** 78 | 79 | ``` 80 | name: Janitor 81 | catalog_entry: 'Arken:Janitor' 82 | answers: 83 | - name: 'FREQUENCY' 84 | value: '9200' 85 | ``` 86 | 87 | *name* of the stack 88 | 89 | *catalog_entry* or (*rancher_compose* and *docker_compose*) to specify the stack 90 | *catalog_entry* use CatalogName:TemplateKey format, it will load the rancher_compose and docker_compose from the catalog. Or you can directly define your composes. You can find the templateKey when browsing to the item : https://rancherurl/env/1a5/catalog/Arken:infra\*datadog?catalogId=Arken, the key is infra\*datadog 91 | 92 | *answers* the different values to use for rancher compose, list of (*name*, *value*) 93 | 94 | -------------------------------------------------------------------------------- /rancher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from ansible.module_utils.basic import AnsibleModule 3 | import gdapi 4 | import json 5 | import urllib2, base64 6 | from time import sleep 7 | 8 | DOCUMENTATION = ''' 9 | --- 10 | module: rancher 11 | short_description: Rancher module to configure stacks / environments / catalogs 12 | description: 13 | - With this module you can either update your catalogs or environments and define stacks on an environment 14 | version_added: "1.0" 15 | author: "Remi Cattiau, @loopingz" 16 | notes: 17 | - Other things consumers of your module should know 18 | - Additional setting requirements 19 | requirements: 20 | - gdapi 21 | options: 22 | url: 23 | description: 24 | - Rancher URL 25 | required: true 26 | access_key: 27 | description: 28 | - Rancher Account Access Key 29 | required: true 30 | secret_key: 31 | description: 32 | - Rancher Account Secret Key 33 | required: true 34 | catalogs: 35 | description: 36 | - List of catalogs you want to install: list of map(url,name,[branch]) 37 | required: false 38 | clean_catalogs: 39 | description: 40 | - Remove any catalogs that are not specified ( default=False ) 41 | required: false 42 | environments: 43 | description: 44 | - List of environments you want to have: list of map(name,stacks) 45 | - stacks are list of map(name,catalog_entry|(docker_compose,rancher_compose),[answers]) 46 | - answers are a list of map(name,value) 47 | - catalog_entry is format CatalogName:TemplateName[:Version] 48 | required: false 49 | clean_envs: 50 | description: 51 | - Remove any envs that are not specified ( default=False ) 52 | required: false 53 | clean_stacks: 54 | description: 55 | - Remove any stacks that are not specified ( default=False ) 56 | required: false 57 | github_user: 58 | description: 59 | - Github user to authenticate calls 60 | required: false 61 | github_password: 62 | description: 63 | - Github password to authenticate calls 64 | clean_hosts: 65 | description: 66 | - Remove any hosts that are marked as disconected in Rancher( default=False ) 67 | required: false 68 | ''' 69 | 70 | 71 | class ProjectAutomationClient(gdapi.Client): 72 | 73 | def __init__(self, url, root_client, project_id, *args, **kwargs): 74 | self._project_id = project_id 75 | self._root_client = root_client 76 | self._init = False 77 | if url[-1] != '/': 78 | url += '/' 79 | url += 'v2-beta/projects/' + project_id 80 | super(ProjectAutomationClient, self).__init__(url=url, *args, **kwargs) 81 | 82 | def _load_schemas(self, *args, **kwargs): 83 | if not self._init: 84 | return 85 | super(ProjectAutomationClient, self)._load_schemas(*args, **kwargs) 86 | 87 | def __enter__(self): 88 | # Creating key for environment 89 | key_name = 'io-deploy-automation' 90 | self._key = self._root_client.create_api_key({'accountId': self._project_id, 'name': key_name}) 91 | self._access_key = self._key.publicValue 92 | self._secret_key = self._key.secretValue 93 | self._auth = (self._access_key, self._secret_key) 94 | self._init = True 95 | while True: 96 | try: 97 | self._load_schemas() 98 | except gdapi.ApiError: 99 | # Wait until key is active 100 | sleep(1) 101 | continue 102 | break 103 | return self 104 | 105 | def __exit__(self, exc_type, exc_val, exc_tb): 106 | # Desactivate environment API key 107 | self._key = self._root_client._post(self._key.links.self + "?action=deactivate") 108 | # Remove environment API key 109 | while True: 110 | try: 111 | self._root_client.delete(self._key) 112 | except gdapi.ApiError: 113 | # Wait until key is deactived 114 | sleep(1) 115 | continue 116 | break 117 | 118 | 119 | class RancherAnsibleModule(AnsibleModule): 120 | def __init__(self, *args, **kwargs): 121 | self._output = [] 122 | self._facts = dict() 123 | self._account_client = None 124 | self._catalog_client = None 125 | super(RancherAnsibleModule, self).__init__(*args, **kwargs) 126 | 127 | def list_catalog(self, name): 128 | return self._catalog_client.list_template(catalogId=name).data 129 | 130 | def get_catalog_template(self, catalog, template): 131 | for entry in self.list_catalog(catalog): 132 | if entry.folderName == template: 133 | return entry 134 | return None 135 | 136 | def init_stack(self, stack, stack_params): 137 | stack['definition'] = dict() 138 | stack['definition']['name'] = stack['name'] 139 | stack['definition']['type'] = 'stack' 140 | 141 | # Init the default values 142 | if 'startOnCreate' in stack: 143 | stack['definition']['startOnCreate'] = stack['startOnCreate'] 144 | else: 145 | stack['definition']['startOnCreate'] = True 146 | stack['definition']['environment'] = dict() 147 | # Inject any additional environment 148 | if 'answers' in stack: 149 | for answer in stack['answers']: 150 | stack['definition']['environment'][answer['name']] = answer['value'] 151 | if answer['name'] == stack_params['name']: 152 | stack['definition']['environment'][answer['name']] = stack_params['value'] 153 | 154 | if 'catalog_entry' in stack: 155 | # Load catalog 156 | infos = stack['catalog_entry'].split(':') 157 | if len(infos) < 2 or len(infos) > 3: 158 | raise Exception('Catalog entry format is CatalogName:TemplateName[:Version] \'%s\' is invalid' %\ 159 | (stack['catalog_entry'],)) 160 | catalog_template = self.get_catalog_template(infos[0], infos[1]) 161 | if catalog_template is None: 162 | raise Exception('Catalog entry not found: %s' % (stack['catalog_entry'],)) 163 | if len(infos) == 2: 164 | infos.append(catalog_template.defaultVersion) 165 | url = catalog_template.versionLinks[infos[2]] 166 | template = self._catalog_client.by_id_template(url[url.rindex('/') + 1:]) 167 | stack['definition']['rancherCompose'] = template.files['rancher-compose.yml'] 168 | stack['definition']['dockerCompose'] = template.files['docker-compose.yml'] 169 | 170 | if infos[1].startswith('infra*'): 171 | stack['definition']['system'] = True 172 | elif not ('docker_compose' in stack and 'rancher_compose' in stack): 173 | # Check arguments failed 174 | raise Exception('Stack need to have either catalog_entry or docker_compose and rancher_compose') 175 | else: 176 | # Use compose information 177 | stack['definition']['rancherCompose'] = stack['rancher_compose'] 178 | stack['definition']['dockerCompose'] = stack['docker_compose'] 179 | 180 | def equals_stack(self, expected_stack, stack): 181 | if not (expected_stack['definition']['dockerCompose'] == stack.dockerCompose and \ 182 | expected_stack['definition']['rancherCompose'] == stack.rancherCompose): 183 | return False 184 | for key, value in expected_stack['definition']['environment'].items(): 185 | if stack.environment[key] != value: 186 | return False 187 | return True 188 | 189 | def check_stacks(self, env, expected_env): 190 | with ProjectAutomationClient(self.params['url'], self._account_client, env.id) as client: 191 | stacks = client.list_stack().data 192 | adds = [] 193 | updates = [] 194 | names = [] 195 | result = False 196 | stack_parameters = expected_env['stack_parameters'] 197 | for expected_stack in expected_env['stacks']: 198 | names.append(expected_stack['name']) 199 | self.init_stack(expected_stack, stack_parameters) 200 | found = False 201 | for stack in stacks: 202 | if stack.name == expected_stack['name']: 203 | found = True 204 | # Check modification 205 | if not self.equals_stack(expected_stack, stack): 206 | expected_stack['current'] = stack.links.self 207 | updates.append(expected_stack) 208 | break 209 | if found: 210 | continue 211 | adds.append(expected_stack) 212 | removes = [] 213 | if self.params['clean_stacks']: 214 | for stack in stacks: 215 | if not stack.name in names: 216 | removes.append(stack) 217 | ''' 218 | for item in catalog: 219 | if item.name in launch: 220 | print "Launching stack " + item.name 221 | launch_stack(item, catalog_client, client, launch[item.name], stacks) 222 | print client.list_registration_token().data[0].token 223 | ''' 224 | result |= (len(removes) + len(adds) + len(updates)) != 0 225 | # Push all registration_token 226 | # client.list_registration_token().data[0].token 227 | # Check mode don't actually update stuffs 228 | if self.check_mode: 229 | return result 230 | # Launch update process 231 | for stack in updates: 232 | client._post(stack['current'] + "?action=upgrade", stack['definition']) 233 | for stack in updates: 234 | current = client._get(stack['current']) 235 | while current.state == 'upgrading': 236 | sleep(1) 237 | current = client.reload(current) 238 | if current.state == 'upgraded': 239 | client._post(stack['current'] + "?action=finishupgrade") 240 | else: 241 | raise Exception("Cant update the stack " + stack['current']) 242 | 243 | # Adding stack 244 | for stack in adds: 245 | client.create_stack(stack['definition']) 246 | return result 247 | 248 | def get_environment_token(self, env, iter=0): 249 | tokens = self._account_client._get(env.links['registrationTokens']).data 250 | 251 | if len(tokens) > 0: 252 | return tokens[0].token 253 | else: 254 | if (iter > 3): 255 | raise Exception('Cannot create environment token') 256 | self._account_client._post(env.links['registrationTokens'][:-1]) 257 | sleep(1) 258 | return self.get_environment_token(env, iter=iter+1) 259 | 260 | def need_members_update(self, current, expected): 261 | if current is None: 262 | return True 263 | members = dict() 264 | for member in current: 265 | key = member['externalId']+member['externalIdType'] 266 | members[key] = member.role 267 | for member in expected: 268 | key = member['externalId'] + member['externalIdType'] 269 | # New member or new role 270 | if key not in members or members[key] != member['role']: 271 | return True 272 | # Remove this member from the list 273 | del members[key] 274 | return len(members) > 0 275 | 276 | 277 | def remove_disconnected_hosts(self): 278 | envs = self._account_client.list_project(all=True).data 279 | for env in envs: 280 | with ProjectAutomationClient(self.params['url'], self._account_client, env.id) as client: 281 | #get hosts for each env 282 | hosts = client._get(env.links['hosts']).data 283 | for host in hosts: 284 | if host.state == 'disconnected': 285 | client._post(host.links.self + "?action=deactivate") 286 | for host in hosts: 287 | host = client._get(host.links.self) 288 | if host.state == 'inactive': 289 | client._delete(host.links.self) 290 | 291 | 292 | 293 | def check_environments(self, expected_envs): 294 | # Get environments 295 | envs = self._account_client.list_project(all=True).data 296 | adds = [] 297 | updates = [] 298 | names = [] 299 | result = False 300 | for expected_env in expected_envs: 301 | names.append(expected_env['name']) 302 | # Generate the members for the project 303 | members = [] 304 | if 'members' in expected_env: 305 | for member in expected_env['members']: 306 | info = self.load_user(member) 307 | info['role'] = member['role'] 308 | info['type'] = 'projectMember' 309 | members.append(info) 310 | expected_env['compute_members'] = members 311 | found = False 312 | for env in envs: 313 | if env.name == expected_env['name']: 314 | if self.need_members_update(env.members, members): 315 | expected_env['current'] = env 316 | updates.append(expected_env) 317 | found = True 318 | # Check environment 319 | if 'stacks' in expected_env: 320 | result |= self.check_stacks(env, expected_env) 321 | break 322 | if found: 323 | continue 324 | adds.append(expected_env) 325 | removes = [] 326 | self._facts["envs"] = dict() 327 | # If in clean mode remove the unwanted environments 328 | for env in envs: 329 | if not env.name in names: 330 | if self.params['clean_envs']: 331 | removes.append(env) 332 | continue 333 | self._facts["envs"][env.name] = dict() 334 | self._facts["envs"][env.name]["token"] = self.get_environment_token(env) 335 | self._facts["envs"][env.name]["id"] = env.id 336 | result |= (len(removes) + len(adds)) != 0 337 | # Check mode don't actually update stuffs 338 | if self.check_mode: 339 | return result 340 | # Create missing environment 341 | for env in adds: 342 | created_env = self._account_client.create_project({'name': env['name'], 'members': env['compute_members']}) 343 | # Add the stack now 344 | if 'stacks' in env: 345 | self.check_stacks(created_env, env) 346 | self._facts["envs"][created_env.name] = dict() 347 | self._facts["envs"][created_env.name]["token"] = self.get_environment_token(created_env) 348 | self._facts["envs"][env.name]["id"] = env.id 349 | # Handle update of member for an environment 350 | for env in updates: 351 | self._account_client.action(env['current'], 'setmembers', members=env['compute_members']) 352 | del env['current'] 353 | # Remove additional environment 354 | for env in removes: 355 | self._account_client.delete(env) 356 | return result 357 | 358 | def check_catalogs(self, expected_catalogs): 359 | # Get catalog settings 360 | catalog_url_id = 'catalog.url' 361 | setting = self._account_client.by_id_setting(catalog_url_id) 362 | if setting.value[0] != '{': 363 | # Default value for catalogs is not JSON 364 | # u'community=https://git.rancher.io/community-catalog.git,library=https://git.rancher.io/rancher-catalog.git' 365 | catalogs = dict() 366 | for ctl in [ctl.split('=') for ctl in setting.value.split(',')]: 367 | catalogs[ctl[0]] = {'url': ctl[1]} 368 | else: 369 | catalogs = json.loads(setting.value)['catalogs'] 370 | urls = [] 371 | adds = [] 372 | # Verify if expected catalogs already exists or need updates 373 | for expected_catalog in expected_catalogs: 374 | urls.append(expected_catalog['url']) 375 | # By default use master branch 376 | if not 'branch' in expected_catalog: 377 | expected_catalog['branch'] = 'master' 378 | found = False 379 | for key, catalog in catalogs.iteritems(): 380 | if expected_catalog['url'] == catalog['url'] or expected_catalog['name'] == key: 381 | found = True 382 | if expected_catalog['url'] == catalog['url'] and expected_catalog['name'] == key \ 383 | and expected_catalog['branch'] == catalog['branch']: 384 | break 385 | updates.append(expected_catalog) 386 | break 387 | if found: 388 | continue 389 | adds.append(expected_catalog) 390 | 391 | removes = [] 392 | # If in clean mode remove the unwanted catalogs 393 | if self.params['clean_catalogs']: 394 | for key, catalog in catalogs.iteritems(): 395 | if not catalog[u'url'] in urls: 396 | removes.append(catalog) 397 | # It will reinit the catalogs 398 | catalogs = dict() 399 | result = (len(removes) + len(adds) + len(updates)) != 0 400 | self._catalogs = catalogs 401 | # Check mode don't actually update stuffs 402 | if self.check_mode: 403 | return result 404 | # Update the settings according to the catalog requested 405 | for catalog in adds: 406 | catalogs[catalog['name']] = {'url': catalog['url'], 'branch': catalog['branch']} 407 | for catalog in updates: 408 | if catalog['name'] in catalogs: 409 | catalogs[catalog['name']]['url'] = catalog['url'] 410 | catalogs[catalog['name']]['branch'] = catalog['branch'] 411 | else: 412 | catalogs[catalog['name']] = {'url': catalog['url'], 'branch': catalog['branch']} 413 | setting['value'] = json.dumps({'catalogs': catalogs}) 414 | self._account_client.update_by_id_setting(catalog_url_id, setting) 415 | self._catalogs = catalogs 416 | return result 417 | 418 | def log(self, msg, *args, **kwargs): 419 | self._output.append(msg % kwargs) 420 | super(RancherAnsibleModule, self).log(msg, *args, **kwargs) 421 | 422 | def process(self): 423 | try: 424 | changed = False 425 | if self.params['create_keys'] is not None and self.params['access_key'] is None: 426 | if self.check_mode: 427 | self.exit_json(changed=True) 428 | return 429 | client = gdapi.Client(url=self.params['url'] + 'v2-beta/') 430 | name = 'Ansible Key' 431 | description = 'Created by Ansible Rancher module' 432 | if self.params['create_keys'] != "True": 433 | try: 434 | params = eval(self.params['create_keys']) 435 | if 'name' in params: 436 | name = params['name'] 437 | if 'description' in params: 438 | description = params['description'] 439 | except: 440 | self.log('exception') 441 | pass 442 | main_key = client.create('apiKey', name=name, description=description) 443 | self.params['access_key'] = main_key['publicValue'] 444 | self.params['secret_key'] = main_key['secretValue'] 445 | self._facts['api_key'] = {'secret_key': self.params['secret_key'], 'access_key': self.params['access_key']} 446 | self._account_client = gdapi.Client(url=self.params['url'] + 'v2-beta/', 447 | access_key=self.params['access_key'], 448 | secret_key=self.params['secret_key']) 449 | self._catalog_client = gdapi.Client(url=self.params['url'] + 'v1-catalog/', 450 | access_key=self.params['access_key'], 451 | secret_key=self.params['secret_key']) 452 | if self.params['catalogs'] is not None: 453 | changed |= self.check_catalogs(self.params['catalogs']) 454 | if self.params['environments'] is not None: 455 | ## not that nice, but no point in updating the envs when cleaning 456 | changed |= self.check_environments(self.params['environments']) 457 | if self.params['clean_hosts']: 458 | self.remove_disconnected_hosts() 459 | if self.params['setup_auth'] is not None: 460 | self.setup_auth(self._account_client, self.params['setup_auth']) 461 | self.exit_json(changed=changed, ansible_facts={self.params['var']: self._facts}, output=self._output) 462 | except urllib2.HTTPError as e: 463 | error_message = e.read() 464 | self.fail_json(msg=error_message) 465 | except Exception as e: 466 | self.fail_json(msg=e.message) 467 | 468 | 469 | def load_user(self, user): 470 | if user['type'] == 'github': 471 | return self.get_github_identity(user['id']) 472 | return None 473 | 474 | def setup_auth(self, client, params): 475 | if self.check_mode: 476 | return 477 | if params['type'] == 'github': 478 | cfg = dict() 479 | cfg['type'] = 'config' 480 | cfg['provider'] = 'githubconfig' 481 | if 'disable' not in params: 482 | params['disable'] = False 483 | cfg['enabled'] = not params['disable'] 484 | if 'access_mode' not in params: 485 | params['access_mode'] = 'required' 486 | cfg['accessMode'] = params['access_mode'] 487 | if 'users' not in params: 488 | params['users'] = [] 489 | cfg['allowedIdentities'] = [self.get_github_identity(user) for user in params['users']] 490 | cfg['githubConfig'] = {'hostname': '', 'type': 'githubconfig', 'scheme': 'https', 491 | 'clientId': params['client_id'], 492 | 'clientSecret': params['client_secret']} 493 | try: 494 | client._post(self.params['url'] + 'v1-auth/config', data=cfg) 495 | except AttributeError: 496 | # Cannot parse the None result 497 | pass 498 | 499 | def get_github_identity(self, name): 500 | request = urllib2.Request("https://api.github.com/users/" + name) 501 | 502 | if self.params['github_user'] != '' and self.params['github_password'] != '': 503 | base64string = base64.encodestring('%s:%s' % (self.params['github_user'], self.params['github_password'])).replace('\n', '') 504 | request.add_header("Authorization", "Basic %s" % base64string) 505 | 506 | user = json.loads(urllib2.urlopen(request).read()) 507 | res = dict() 508 | res['externalId'] = str(user['id']) 509 | if user['type'] == 'User': 510 | res['externalIdType'] = 'github_user' 511 | else: 512 | res['externalIdType'] = 'github_org' 513 | res['type'] = 'identity' 514 | res['id'] = res['externalIdType'] + ':' + res['externalId'] 515 | return res 516 | 517 | 518 | def main(): 519 | RancherAnsibleModule( 520 | argument_spec=dict( 521 | url=dict(required=True), 522 | create_keys=dict(default=False), 523 | setup_auth=dict(type='dict', required=False), 524 | var=dict(required=False, default="rancher"), 525 | access_key=dict(required=False), 526 | secret_key=dict(required=False), 527 | clean_envs=dict(default=False,type='bool'), 528 | clean_catalogs=dict(default=False, type='bool'), 529 | clean_stacks=dict(default=False, type='bool'), 530 | clean_hosts=dict(default=False, type='bool'), 531 | catalogs=dict(type='list'), 532 | environments=dict(required=False, type='list'), 533 | github_user=dict(required=False), 534 | github_password=dict(required=False), 535 | ), 536 | supports_check_mode=True 537 | ).process() 538 | 539 | if __name__ == '__main__': 540 | main() --------------------------------------------------------------------------------