summaryrefslogtreecommitdiff
path: root/lib/ansible/module_utils/opennebula.py
blob: a520e32187f452e017052925e12895bba1fe9b1d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#
# Copyright 2018 www.privaz.io Valletech AB
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)


import time
import ssl
from os import environ
from ansible.module_utils.six import string_types
from ansible.module_utils.basic import AnsibleModule


HAS_PYONE = True

try:
    from pyone import OneException
    from pyone.server import OneServer
except ImportError:
    OneException = Exception
    HAS_PYONE = False


class OpenNebulaModule:
    """
    Base class for all OpenNebula Ansible Modules.
    This is basically a wrapper of the common arguments, the pyone client and
    some utility methods.
    """

    common_args = dict(
        api_url=dict(type='str', aliases=['api_endpoint'], default=environ.get("ONE_URL")),
        api_username=dict(type='str', default=environ.get("ONE_USERNAME")),
        api_password=dict(type='str', no_log=True, aliases=['api_token'], default=environ.get("ONE_PASSWORD")),
        validate_certs=dict(default=True, type='bool'),
        wait_timeout=dict(type='int', default=300),
    )

    def __init__(self, argument_spec, supports_check_mode=False, mutually_exclusive=None):

        module_args = OpenNebulaModule.common_args
        module_args.update(argument_spec)

        self.module = AnsibleModule(argument_spec=module_args,
                                    supports_check_mode=supports_check_mode,
                                    mutually_exclusive=mutually_exclusive)
        self.result = dict(changed=False,
                           original_message='',
                           message='')
        self.one = self.create_one_client()

        self.resolved_parameters = self.resolve_parameters()

    def create_one_client(self):
        """
        Creates an XMLPRC client to OpenNebula.

        Returns: the new xmlrpc client.

        """

        # context required for not validating SSL, old python versions won't validate anyway.
        if hasattr(ssl, '_create_unverified_context'):
            no_ssl_validation_context = ssl._create_unverified_context()
        else:
            no_ssl_validation_context = None

        # Check if the module can run
        if not HAS_PYONE:
            self.fail("pyone is required for this module")

        if self.module.params.get("api_url"):
            url = self.module.params.get("api_url")
        else:
            self.fail("Either api_url or the environment variable ONE_URL must be provided")

        if self.module.params.get("api_username"):
            username = self.module.params.get("api_username")
        else:
            self.fail("Either api_username or the environment vairable ONE_USERNAME must be provided")

        if self.module.params.get("api_password"):
            password = self.module.params.get("api_password")
        else:
            self.fail("Either api_password or the environment vairable ONE_PASSWORD must be provided")

        session = "%s:%s" % (username, password)

        if not self.module.params.get("validate_certs") and "PYTHONHTTPSVERIFY" not in environ:
            return OneServer(url, session=session, context=no_ssl_validation_context)
        else:
            return OneServer(url, session)

    def close_one_client(self):
        """
        Close the pyone session.
        """
        self.one.server_close()

    def fail(self, msg):
        """
        Utility failure method, will ensure pyone is properly closed before failing.
        Args:
            msg: human readable failure reason.
        """
        if hasattr(self, 'one'):
            self.close_one_client()
        self.module.fail_json(msg=msg)

    def exit(self):
        """
        Utility exit method, will ensure pyone is properly closed before exiting.

        """
        if hasattr(self, 'one'):
            self.close_one_client()
        self.module.exit_json(**self.result)

    def resolve_parameters(self):
        """
        This method resolves parameters provided by a secondary ID to the primary ID.
        For example if cluster_name is present, cluster_id will be introduced by performing
        the required resolution

        Returns: a copy of the parameters that includes the resolved parameters.

        """

        resolved_params = dict(self.module.params)

        if 'cluster_name' in self.module.params:
            clusters = self.one.clusterpool.info()
            for cluster in clusters.CLUSTER:
                if cluster.NAME == self.module.params.get('cluster_name'):
                    resolved_params['cluster_id'] = cluster.ID

        return resolved_params

    def is_parameter(self, name):
        """
        Utility method to check if a parameter was provided or is resolved
        Args:
            name: the parameter to check
        """
        if name in self.resolved_parameters:
            return self.get_parameter(name) is not None
        else:
            return False

    def get_parameter(self, name):
        """
        Utility method for accessing parameters that includes resolved ID
        parameters from provided Name parameters.
        """
        return self.resolved_parameters.get(name)

    def get_host_by_name(self, name):
        '''
        Returns a host given its name.
        Args:
            name: the name of the host

        Returns: the host object or None if the host is absent.

        '''
        hosts = self.one.hostpool.info()
        for h in hosts.HOST:
            if h.NAME == name:
                return h
        return None

    def get_cluster_by_name(self, name):
        """
        Returns a cluster given its name.
        Args:
            name: the name of the cluster

        Returns: the cluster object or None if the host is absent.
        """

        clusters = self.one.clusterpool.info()
        for c in clusters.CLUSTER:
            if c.NAME == name:
                return c
        return None

    def get_template_by_name(self, name):
        '''
        Returns a template given its name.
        Args:
            name: the name of the template

        Returns: the template object or None if the host is absent.

        '''
        templates = self.one.templatepool.info()
        for t in templates.TEMPLATE:
            if t.NAME == name:
                return t
        return None

    def cast_template(self, template):
        """
        OpenNebula handles all template elements as strings
        At some point there is a cast being performed on types provided by the user
        This function mimics that transformation so that required template updates are detected properly
        additionally an array will be converted to a comma separated list,
        which works for labels and hopefully for something more.

        Args:
            template: the template to transform

        Returns: the transformed template with data casts applied.
        """

        # TODO: check formally available data types in templates
        # TODO: some arrays might be converted to space separated

        for key in template:
            value = template[key]
            if isinstance(value, dict):
                self.cast_template(template[key])
            elif isinstance(value, list):
                template[key] = ', '.join(value)
            elif not isinstance(value, string_types):
                template[key] = str(value)

    def requires_template_update(self, current, desired):
        """
        This function will help decide if a template update is required or not
        If a desired key is missing from the current dictionary an update is required
        If the intersection of both dictionaries is not deep equal, an update is required
        Args:
            current: current template as a dictionary
            desired: desired template as a dictionary

        Returns: True if a template update is required
        """

        if not desired:
            return False

        self.cast_template(desired)
        intersection = dict()
        for dkey in desired.keys():
            if dkey in current.keys():
                intersection[dkey] = current[dkey]
            else:
                return True
        return not (desired == intersection)

    def wait_for_state(self, element_name, state, state_name, target_states,
                       invalid_states=None, transition_states=None,
                       wait_timeout=None):
        """
        Args:
            element_name: the name of the object we are waiting for: HOST, VM, etc.
            state: lambda that returns the current state, will be queried until target state is reached
            state_name: lambda that returns the readable form of a given state
            target_states: states expected to be reached
            invalid_states: if any of this states is reached, fail
            transition_states: when used, these are the valid states during the transition.
            wait_timeout: timeout period in seconds. Defaults to the provided parameter.
        """

        if not wait_timeout:
            wait_timeout = self.module.params.get("wait_timeout")

        start_time = time.time()

        while (time.time() - start_time) < wait_timeout:
            current_state = state()

            if current_state in invalid_states:
                self.fail('invalid %s state %s' % (element_name, state_name(current_state)))

            if transition_states:
                if current_state not in transition_states:
                    self.fail('invalid %s transition state %s' % (element_name, state_name(current_state)))

            if current_state in target_states:
                return True

            time.sleep(self.one.server_retry_interval())

        self.fail(msg="Wait timeout has expired!")

    def run_module(self):
        """
        trigger the start of the execution of the module.
        Returns:

        """
        try:
            self.run(self.one, self.module, self.result)
        except OneException as e:
            self.fail(msg="OpenNebula Exception: %s" % e)

    def run(self, one, module, result):
        """
        to be implemented by subclass with the actual module actions.
        Args:
            one: the OpenNebula XMLRPC client
            module: the Ansible Module object
            result: the Ansible result
        """
        raise NotImplementedError("Method requires implementation")