1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Make coding more python3-ish 5from __future__ import (absolute_import, division, print_function) 6__metaclass__ = type 7 8""" 9(c) 2017, Milan Ilic <milani@nordeus.com> 10 11This file is part of Ansible 12 13Ansible is free software: you can redistribute it and/or modify 14it under the terms of the GNU General Public License as published by 15the Free Software Foundation, either version 3 of the License, or 16(at your option) any later version. 17 18Ansible is distributed in the hope that it will be useful, 19but WITHOUT ANY WARRANTY; without even the implied warranty of 20MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21GNU General Public License for more details. 22 23You should have received a copy of the GNU General Public License 24along with Ansible. If not, see <http://www.gnu.org/licenses/>. 25""" 26 27ANSIBLE_METADATA = {'status': ['preview'], 28 'supported_by': 'community', 29 'metadata_version': '1.1'} 30 31DOCUMENTATION = ''' 32--- 33module: one_service 34short_description: Deploy and manage OpenNebula services 35description: 36 - Manage OpenNebula services 37version_added: "2.6" 38options: 39 api_url: 40 description: 41 - URL of the OpenNebula OneFlow API server. 42 - It is recommended to use HTTPS so that the username/password are not transferred over the network unencrypted. 43 - If not set then the value of the ONEFLOW_URL environment variable is used. 44 api_username: 45 description: 46 - Name of the user to login into the OpenNebula OneFlow API server. If not set then the value of the C(ONEFLOW_USERNAME) environment variable is used. 47 api_password: 48 description: 49 - Password of the user to login into OpenNebula OneFlow API server. If not set then the value of the C(ONEFLOW_PASSWORD) environment variable is used. 50 template_name: 51 description: 52 - Name of service template to use to create a new instance of a service 53 template_id: 54 description: 55 - ID of a service template to use to create a new instance of a service 56 service_id: 57 description: 58 - ID of a service instance that you would like to manage 59 service_name: 60 description: 61 - Name of a service instance that you would like to manage 62 unique: 63 description: 64 - Setting C(unique=yes) will make sure that there is only one service instance running with a name set with C(service_name) when 65 - instantiating a service from a template specified with C(template_id)/C(template_name). Check examples below. 66 type: bool 67 default: no 68 state: 69 description: 70 - C(present) - instantiate a service from a template specified with C(template_id)/C(template_name). 71 - C(absent) - terminate an instance of a service specified with C(service_id)/C(service_name). 72 choices: ["present", "absent"] 73 default: present 74 mode: 75 description: 76 - Set permission mode of a service instance in octet format, e.g. C(600) to give owner C(use) and C(manage) and nothing to group and others. 77 owner_id: 78 description: 79 - ID of the user which will be set as the owner of the service 80 group_id: 81 description: 82 - ID of the group which will be set as the group of the service 83 wait: 84 description: 85 - Wait for the instance to reach RUNNING state after DEPLOYING or COOLDOWN state after SCALING 86 type: bool 87 default: no 88 wait_timeout: 89 description: 90 - How long before wait gives up, in seconds 91 default: 300 92 custom_attrs: 93 description: 94 - Dictionary of key/value custom attributes which will be used when instantiating a new service. 95 default: {} 96 role: 97 description: 98 - Name of the role whose cardinality should be changed 99 cardinality: 100 description: 101 - Number of VMs for the specified role 102 force: 103 description: 104 - Force the new cardinality even if it is outside the limits 105 type: bool 106 default: no 107author: 108 - "Milan Ilic (@ilicmilan)" 109''' 110 111EXAMPLES = ''' 112# Instantiate a new service 113- one_service: 114 template_id: 90 115 register: result 116 117# Print service properties 118- debug: 119 msg: result 120 121# Instantiate a new service with specified service_name, service group and mode 122- one_service: 123 template_name: 'app1_template' 124 service_name: 'app1' 125 group_id: 1 126 mode: '660' 127 128# Instantiate a new service with template_id and pass custom_attrs dict 129- one_service: 130 template_id: 90 131 custom_attrs: 132 public_network_id: 21 133 private_network_id: 26 134 135# Instantiate a new service 'foo' if the service doesn't already exist, otherwise do nothing 136- one_service: 137 template_id: 53 138 service_name: 'foo' 139 unique: yes 140 141# Delete a service by ID 142- one_service: 143 service_id: 153 144 state: absent 145 146# Get service info 147- one_service: 148 service_id: 153 149 register: service_info 150 151# Change service owner, group and mode 152- one_service: 153 service_name: 'app2' 154 owner_id: 34 155 group_id: 113 156 mode: '600' 157 158# Instantiate service and wait for it to become RUNNING 159- one_service: 160 template_id: 43 161 service_name: 'foo1' 162 163# Wait service to become RUNNING 164- one_service: 165 service_id: 112 166 wait: yes 167 168# Change role cardinality 169- one_service: 170 service_id: 153 171 role: bar 172 cardinality: 5 173 174# Change role cardinality and wait for it to be applied 175- one_service: 176 service_id: 112 177 role: foo 178 cardinality: 7 179 wait: yes 180''' 181 182RETURN = ''' 183service_id: 184 description: service id 185 type: int 186 returned: success 187 sample: 153 188service_name: 189 description: service name 190 type: str 191 returned: success 192 sample: app1 193group_id: 194 description: service's group id 195 type: int 196 returned: success 197 sample: 1 198group_name: 199 description: service's group name 200 type: str 201 returned: success 202 sample: one-users 203owner_id: 204 description: service's owner id 205 type: int 206 returned: success 207 sample: 143 208owner_name: 209 description: service's owner name 210 type: str 211 returned: success 212 sample: ansible-test 213state: 214 description: state of service instance 215 type: str 216 returned: success 217 sample: RUNNING 218mode: 219 description: service's mode 220 type: int 221 returned: success 222 sample: 660 223roles: 224 description: list of dictionaries of roles, each role is described by name, cardinality, state and nodes ids 225 type: list 226 returned: success 227 sample: '[{"cardinality": 1,"name": "foo","state": "RUNNING","ids": [ 123, 456 ]}, 228 {"cardinality": 2,"name": "bar","state": "RUNNING", "ids": [ 452, 567, 746 ]}]' 229''' 230 231import os 232import sys 233from ansible.module_utils.basic import AnsibleModule 234from ansible.module_utils.urls import open_url 235 236STATES = ("PENDING", "DEPLOYING", "RUNNING", "UNDEPLOYING", "WARNING", "DONE", 237 "FAILED_UNDEPLOYING", "FAILED_DEPLOYING", "SCALING", "FAILED_SCALING", "COOLDOWN") 238 239 240def get_all_templates(module, auth): 241 try: 242 all_templates = open_url(url=(auth.url + "/service_template"), method="GET", force_basic_auth=True, url_username=auth.user, url_password=auth.password) 243 except Exception as e: 244 module.fail_json(msg=str(e)) 245 246 return module.from_json(all_templates.read()) 247 248 249def get_template(module, auth, pred): 250 all_templates_dict = get_all_templates(module, auth) 251 252 found = 0 253 found_template = None 254 template_name = '' 255 256 if "DOCUMENT_POOL" in all_templates_dict and "DOCUMENT" in all_templates_dict["DOCUMENT_POOL"]: 257 for template in all_templates_dict["DOCUMENT_POOL"]["DOCUMENT"]: 258 if pred(template): 259 found = found + 1 260 found_template = template 261 template_name = template["NAME"] 262 263 if found <= 0: 264 return None 265 elif found > 1: 266 module.fail_json(msg="There is no template with unique name: " + template_name) 267 else: 268 return found_template 269 270 271def get_all_services(module, auth): 272 try: 273 response = open_url(auth.url + "/service", method="GET", force_basic_auth=True, url_username=auth.user, url_password=auth.password) 274 except Exception as e: 275 module.fail_json(msg=str(e)) 276 277 return module.from_json(response.read()) 278 279 280def get_service(module, auth, pred): 281 all_services_dict = get_all_services(module, auth) 282 283 found = 0 284 found_service = None 285 service_name = '' 286 287 if "DOCUMENT_POOL" in all_services_dict and "DOCUMENT" in all_services_dict["DOCUMENT_POOL"]: 288 for service in all_services_dict["DOCUMENT_POOL"]["DOCUMENT"]: 289 if pred(service): 290 found = found + 1 291 found_service = service 292 service_name = service["NAME"] 293 294 # fail if there are more services with same name 295 if found > 1: 296 module.fail_json(msg="There are multiple services with a name: '" + 297 service_name + "'. You have to use a unique service name or use 'service_id' instead.") 298 elif found <= 0: 299 return None 300 else: 301 return found_service 302 303 304def get_service_by_id(module, auth, service_id): 305 return get_service(module, auth, lambda service: (int(service["ID"]) == int(service_id))) if service_id else None 306 307 308def get_service_by_name(module, auth, service_name): 309 return get_service(module, auth, lambda service: (service["NAME"] == service_name)) 310 311 312def get_service_info(module, auth, service): 313 314 result = { 315 "service_id": int(service["ID"]), 316 "service_name": service["NAME"], 317 "group_id": int(service["GID"]), 318 "group_name": service["GNAME"], 319 "owner_id": int(service["UID"]), 320 "owner_name": service["UNAME"], 321 "state": STATES[service["TEMPLATE"]["BODY"]["state"]] 322 } 323 324 roles_status = service["TEMPLATE"]["BODY"]["roles"] 325 roles = [] 326 for role in roles_status: 327 nodes_ids = [] 328 if "nodes" in role: 329 for node in role["nodes"]: 330 nodes_ids.append(node["deploy_id"]) 331 roles.append({"name": role["name"], "cardinality": role["cardinality"], "state": STATES[int(role["state"])], "ids": nodes_ids}) 332 333 result["roles"] = roles 334 result["mode"] = int(parse_service_permissions(service)) 335 336 return result 337 338 339def create_service(module, auth, template_id, service_name, custom_attrs, unique, wait, wait_timeout): 340 # make sure that the values in custom_attrs dict are strings 341 custom_attrs_with_str = dict((k, str(v)) for k, v in custom_attrs.items()) 342 343 data = { 344 "action": { 345 "perform": "instantiate", 346 "params": { 347 "merge_template": { 348 "custom_attrs_values": custom_attrs_with_str, 349 "name": service_name 350 } 351 } 352 } 353 } 354 355 try: 356 response = open_url(auth.url + "/service_template/" + str(template_id) + "/action", method="POST", 357 data=module.jsonify(data), force_basic_auth=True, url_username=auth.user, url_password=auth.password) 358 except Exception as e: 359 module.fail_json(msg=str(e)) 360 361 service_result = module.from_json(response.read())["DOCUMENT"] 362 363 return service_result 364 365 366def wait_for_service_to_become_ready(module, auth, service_id, wait_timeout): 367 import time 368 start_time = time.time() 369 370 while (time.time() - start_time) < wait_timeout: 371 try: 372 status_result = open_url(auth.url + "/service/" + str(service_id), method="GET", 373 force_basic_auth=True, url_username=auth.user, url_password=auth.password) 374 except Exception as e: 375 module.fail_json(msg="Request for service status has failed. Error message: " + str(e)) 376 377 status_result = module.from_json(status_result.read()) 378 service_state = status_result["DOCUMENT"]["TEMPLATE"]["BODY"]["state"] 379 380 if service_state in [STATES.index("RUNNING"), STATES.index("COOLDOWN")]: 381 return status_result["DOCUMENT"] 382 elif service_state not in [STATES.index("PENDING"), STATES.index("DEPLOYING"), STATES.index("SCALING")]: 383 log_message = '' 384 for log_info in status_result["DOCUMENT"]["TEMPLATE"]["BODY"]["log"]: 385 if log_info["severity"] == "E": 386 log_message = log_message + log_info["message"] 387 break 388 389 module.fail_json(msg="Deploying is unsuccessful. Service state: " + STATES[service_state] + ". Error message: " + log_message) 390 391 time.sleep(1) 392 393 module.fail_json(msg="Wait timeout has expired") 394 395 396def change_service_permissions(module, auth, service_id, permissions): 397 398 data = { 399 "action": { 400 "perform": "chmod", 401 "params": {"octet": permissions} 402 } 403 } 404 405 try: 406 status_result = open_url(auth.url + "/service/" + str(service_id) + "/action", method="POST", force_basic_auth=True, 407 url_username=auth.user, url_password=auth.password, data=module.jsonify(data)) 408 except Exception as e: 409 module.fail_json(msg=str(e)) 410 411 412def change_service_owner(module, auth, service_id, owner_id): 413 data = { 414 "action": { 415 "perform": "chown", 416 "params": {"owner_id": owner_id} 417 } 418 } 419 420 try: 421 status_result = open_url(auth.url + "/service/" + str(service_id) + "/action", method="POST", force_basic_auth=True, 422 url_username=auth.user, url_password=auth.password, data=module.jsonify(data)) 423 except Exception as e: 424 module.fail_json(msg=str(e)) 425 426 427def change_service_group(module, auth, service_id, group_id): 428 429 data = { 430 "action": { 431 "perform": "chgrp", 432 "params": {"group_id": group_id} 433 } 434 } 435 436 try: 437 status_result = open_url(auth.url + "/service/" + str(service_id) + "/action", method="POST", force_basic_auth=True, 438 url_username=auth.user, url_password=auth.password, data=module.jsonify(data)) 439 except Exception as e: 440 module.fail_json(msg=str(e)) 441 442 443def change_role_cardinality(module, auth, service_id, role, cardinality, force): 444 445 data = { 446 "cardinality": cardinality, 447 "force": force 448 } 449 450 try: 451 status_result = open_url(auth.url + "/service/" + str(service_id) + "/role/" + role, method="PUT", 452 force_basic_auth=True, url_username=auth.user, url_password=auth.password, data=module.jsonify(data)) 453 except Exception as e: 454 module.fail_json(msg=str(e)) 455 456 if status_result.getcode() != 204: 457 module.fail_json(msg="Failed to change cardinality for role: " + role + ". Return code: " + str(status_result.getcode())) 458 459 460def check_change_service_owner(module, service, owner_id): 461 old_owner_id = int(service["UID"]) 462 463 return old_owner_id != owner_id 464 465 466def check_change_service_group(module, service, group_id): 467 old_group_id = int(service["GID"]) 468 469 return old_group_id != group_id 470 471 472def parse_service_permissions(service): 473 perm_dict = service["PERMISSIONS"] 474 ''' 475 This is the structure of the 'PERMISSIONS' dictionary: 476 477 "PERMISSIONS": { 478 "OWNER_U": "1", 479 "OWNER_M": "1", 480 "OWNER_A": "0", 481 "GROUP_U": "0", 482 "GROUP_M": "0", 483 "GROUP_A": "0", 484 "OTHER_U": "0", 485 "OTHER_M": "0", 486 "OTHER_A": "0" 487 } 488 ''' 489 490 owner_octal = int(perm_dict["OWNER_U"]) * 4 + int(perm_dict["OWNER_M"]) * 2 + int(perm_dict["OWNER_A"]) 491 group_octal = int(perm_dict["GROUP_U"]) * 4 + int(perm_dict["GROUP_M"]) * 2 + int(perm_dict["GROUP_A"]) 492 other_octal = int(perm_dict["OTHER_U"]) * 4 + int(perm_dict["OTHER_M"]) * 2 + int(perm_dict["OTHER_A"]) 493 494 permissions = str(owner_octal) + str(group_octal) + str(other_octal) 495 496 return permissions 497 498 499def check_change_service_permissions(module, service, permissions): 500 old_permissions = parse_service_permissions(service) 501 502 return old_permissions != permissions 503 504 505def check_change_role_cardinality(module, service, role_name, cardinality): 506 roles_list = service["TEMPLATE"]["BODY"]["roles"] 507 508 for role in roles_list: 509 if role["name"] == role_name: 510 return int(role["cardinality"]) != cardinality 511 512 module.fail_json(msg="There is no role with name: " + role_name) 513 514 515def create_service_and_operation(module, auth, template_id, service_name, owner_id, group_id, permissions, custom_attrs, unique, wait, wait_timeout): 516 if not service_name: 517 service_name = '' 518 changed = False 519 service = None 520 521 if unique: 522 service = get_service_by_name(module, auth, service_name) 523 524 if not service: 525 if not module.check_mode: 526 service = create_service(module, auth, template_id, service_name, custom_attrs, unique, wait, wait_timeout) 527 changed = True 528 529 # if check_mode=true and there would be changes, service doesn't exist and we can not get it 530 if module.check_mode and changed: 531 return {"changed": True} 532 533 result = service_operation(module, auth, owner_id=owner_id, group_id=group_id, wait=wait, 534 wait_timeout=wait_timeout, permissions=permissions, service=service) 535 536 if result["changed"]: 537 changed = True 538 539 result["changed"] = changed 540 541 return result 542 543 544def service_operation(module, auth, service_id=None, owner_id=None, group_id=None, permissions=None, 545 role=None, cardinality=None, force=None, wait=False, wait_timeout=None, service=None): 546 547 changed = False 548 549 if not service: 550 service = get_service_by_id(module, auth, service_id) 551 else: 552 service_id = service["ID"] 553 554 if not service: 555 module.fail_json(msg="There is no service with id: " + str(service_id)) 556 557 if owner_id: 558 if check_change_service_owner(module, service, owner_id): 559 if not module.check_mode: 560 change_service_owner(module, auth, service_id, owner_id) 561 changed = True 562 if group_id: 563 if check_change_service_group(module, service, group_id): 564 if not module.check_mode: 565 change_service_group(module, auth, service_id, group_id) 566 changed = True 567 if permissions: 568 if check_change_service_permissions(module, service, permissions): 569 if not module.check_mode: 570 change_service_permissions(module, auth, service_id, permissions) 571 changed = True 572 573 if role: 574 if check_change_role_cardinality(module, service, role, cardinality): 575 if not module.check_mode: 576 change_role_cardinality(module, auth, service_id, role, cardinality, force) 577 changed = True 578 579 if wait and not module.check_mode: 580 service = wait_for_service_to_become_ready(module, auth, service_id, wait_timeout) 581 582 # if something has changed, fetch service info again 583 if changed: 584 service = get_service_by_id(module, auth, service_id) 585 586 service_info = get_service_info(module, auth, service) 587 service_info["changed"] = changed 588 589 return service_info 590 591 592def delete_service(module, auth, service_id): 593 service = get_service_by_id(module, auth, service_id) 594 if not service: 595 return {"changed": False} 596 597 service_info = get_service_info(module, auth, service) 598 599 service_info["changed"] = True 600 601 if module.check_mode: 602 return service_info 603 604 try: 605 result = open_url(auth.url + '/service/' + str(service_id), method="DELETE", force_basic_auth=True, url_username=auth.user, url_password=auth.password) 606 except Exception as e: 607 module.fail_json(msg="Service deletion has failed. Error message: " + str(e)) 608 609 return service_info 610 611 612def get_template_by_name(module, auth, template_name): 613 return get_template(module, auth, lambda template: (template["NAME"] == template_name)) 614 615 616def get_template_by_id(module, auth, template_id): 617 return get_template(module, auth, lambda template: (int(template["ID"]) == int(template_id))) if template_id else None 618 619 620def get_template_id(module, auth, requested_id, requested_name): 621 template = get_template_by_id(module, auth, requested_id) if requested_id else get_template_by_name(module, auth, requested_name) 622 623 if template: 624 return template["ID"] 625 626 return None 627 628 629def get_service_id_by_name(module, auth, service_name): 630 service = get_service_by_name(module, auth, service_name) 631 632 if service: 633 return service["ID"] 634 635 return None 636 637 638def get_connection_info(module): 639 640 url = module.params.get('api_url') 641 username = module.params.get('api_username') 642 password = module.params.get('api_password') 643 644 if not url: 645 url = os.environ.get('ONEFLOW_URL') 646 647 if not username: 648 username = os.environ.get('ONEFLOW_USERNAME') 649 650 if not password: 651 password = os.environ.get('ONEFLOW_PASSWORD') 652 653 if not(url and username and password): 654 module.fail_json(msg="One or more connection parameters (api_url, api_username, api_password) were not specified") 655 from collections import namedtuple 656 657 auth_params = namedtuple('auth', ('url', 'user', 'password')) 658 659 return auth_params(url=url, user=username, password=password) 660 661 662def main(): 663 fields = { 664 "api_url": {"required": False, "type": "str"}, 665 "api_username": {"required": False, "type": "str"}, 666 "api_password": {"required": False, "type": "str", "no_log": True}, 667 "service_name": {"required": False, "type": "str"}, 668 "service_id": {"required": False, "type": "int"}, 669 "template_name": {"required": False, "type": "str"}, 670 "template_id": {"required": False, "type": "int"}, 671 "state": { 672 "default": "present", 673 "choices": ['present', 'absent'], 674 "type": "str" 675 }, 676 "mode": {"required": False, "type": "str"}, 677 "owner_id": {"required": False, "type": "int"}, 678 "group_id": {"required": False, "type": "int"}, 679 "unique": {"default": False, "type": "bool"}, 680 "wait": {"default": False, "type": "bool"}, 681 "wait_timeout": {"default": 300, "type": "int"}, 682 "custom_attrs": {"default": {}, "type": "dict"}, 683 "role": {"required": False, "type": "str"}, 684 "cardinality": {"required": False, "type": "int"}, 685 "force": {"default": False, "type": "bool"} 686 } 687 688 module = AnsibleModule(argument_spec=fields, 689 mutually_exclusive=[ 690 ['template_id', 'template_name', 'service_id'], 691 ['service_id', 'service_name'], 692 ['template_id', 'template_name', 'role'], 693 ['template_id', 'template_name', 'cardinality'], 694 ['service_id', 'custom_attrs'] 695 ], 696 required_together=[['role', 'cardinality']], 697 supports_check_mode=True) 698 699 auth = get_connection_info(module) 700 params = module.params 701 service_name = params.get('service_name') 702 service_id = params.get('service_id') 703 704 requested_template_id = params.get('template_id') 705 requested_template_name = params.get('template_name') 706 state = params.get('state') 707 permissions = params.get('mode') 708 owner_id = params.get('owner_id') 709 group_id = params.get('group_id') 710 unique = params.get('unique') 711 wait = params.get('wait') 712 wait_timeout = params.get('wait_timeout') 713 custom_attrs = params.get('custom_attrs') 714 role = params.get('role') 715 cardinality = params.get('cardinality') 716 force = params.get('force') 717 718 template_id = None 719 720 if requested_template_id or requested_template_name: 721 template_id = get_template_id(module, auth, requested_template_id, requested_template_name) 722 if not template_id: 723 if requested_template_id: 724 module.fail_json(msg="There is no template with template_id: " + str(requested_template_id)) 725 elif requested_template_name: 726 module.fail_json(msg="There is no template with name: " + requested_template_name) 727 728 if unique and not service_name: 729 module.fail_json(msg="You cannot use unique without passing service_name!") 730 731 if template_id and state == 'absent': 732 module.fail_json(msg="State absent is not valid for template") 733 734 if template_id and state == 'present': # Instantiate a service 735 result = create_service_and_operation(module, auth, template_id, service_name, owner_id, 736 group_id, permissions, custom_attrs, unique, wait, wait_timeout) 737 else: 738 if not (service_id or service_name): 739 module.fail_json(msg="To manage the service at least the service id or service name should be specified!") 740 if custom_attrs: 741 module.fail_json(msg="You can only set custom_attrs when instantiate service!") 742 743 if not service_id: 744 service_id = get_service_id_by_name(module, auth, service_name) 745 # The task should be failed when we want to manage a non-existent service identified by its name 746 if not service_id and state == 'present': 747 module.fail_json(msg="There is no service with name: " + service_name) 748 749 if state == 'absent': 750 result = delete_service(module, auth, service_id) 751 else: 752 result = service_operation(module, auth, service_id, owner_id, group_id, permissions, role, cardinality, force, wait, wait_timeout) 753 754 module.exit_json(**result) 755 756 757if __name__ == '__main__': 758 main() 759