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