1# Licensed to the Apache Software Foundation (ASF) under one or more
2# contributor license agreements.  See the NOTICE file distributed with
3# this work for additional information regarding copyright ownership.
4# The ASF licenses this file to You under the Apache License, Version 2.0
5# (the "License"); you may not use this file except in compliance with
6# the License.  You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import json
17
18from libcloud.utils.py3 import httplib
19from libcloud.utils.networking import is_private_subnet
20
21from libcloud.common.onapp import OnAppConnection
22from libcloud.compute.base import Node, NodeDriver, NodeImage, KeyPair
23from libcloud.compute.providers import Provider
24
25
26__all__ = [
27    "OnAppNodeDriver"
28]
29
30"""
31Define the extra dictionary for specific resources
32"""
33RESOURCE_EXTRA_ATTRIBUTES_MAP = {
34    "node": {
35        "add_to_marketplace": {
36            "key_name": "add_to_marketplace",
37            "transform_func": bool
38        },
39        "admin_note": {
40            "key_name": "admin_note",
41            "transform_func": str
42        },
43        "allow_resize_without_reboot": {
44            "key_name": "allow_resize_without_reboot",
45            "transform_func": bool
46        },
47        "allowed_hot_migrate": {
48            "key_name": "allowed_hot_migrate",
49            "transform_func": bool
50        },
51        "allowed_swap": {
52            "key_name": "allowed_swap",
53            "transform_func": bool
54        },
55        "booted": {
56            "key_name": "booted",
57            "transform_func": bool
58        },
59        "built": {
60            "key_name": "built",
61            "transform_func": bool
62        },
63        "cpu_priority": {
64            "key_name": "cpu_priority",
65            "transform_func": int
66        },
67        "cpu_shares": {
68            "key_name": "cpu_shares",
69            "transform_func": int
70        },
71        "cpu_sockets": {
72            "key_name": "cpu_sockets",
73            "transform_func": int
74        },
75        "cpu_threads": {
76            "key_name": "cpu_threads",
77            "transform_func": int
78        },
79        "cpu_units": {
80            "key_name": "cpu_units",
81            "transform_func": int
82        },
83        "cpus": {
84            "key_name": "cpus",
85            "transform_func": int
86        },
87        "created_at": {
88            "key_name": "created_at",
89            "transform_func": str
90        },
91        "customer_network_id": {
92            "key_name": "customer_network_id",
93            "transform_func": str
94        },
95        "deleted_at": {
96            "key_name": "deleted_at",
97            "transform_func": str
98        },
99        "edge_server_type": {
100            "key_name": "edge_server_type",
101            "transform_func": str
102        },
103        "enable_autoscale": {
104            "key_name": "enable_autoscale",
105            "transform_func": bool
106        },
107        "enable_monitis": {
108            "key_name": "enable_monitis",
109            "transform_func": bool
110        },
111        "firewall_notrack": {
112            "key_name": "firewall_notrack",
113            "transform_func": bool
114        },
115        "hostname": {
116            "key_name": "hostname",
117            "transform_func": str
118        },
119        "hypervisor_id": {
120            "key_name": "hypervisor_id",
121            "transform_func": int
122        },
123        "id": {
124            "key_name": "id",
125            "transform_func": int
126        },
127        "initial_root_password": {
128            "key_name": "initial_root_password",
129            "transform_func": str
130        },
131        "initial_root_password_encrypted": {
132            "key_name": "initial_root_password_encrypted",
133            "transform_func": bool
134        },
135        "local_remote_access_ip_address": {
136            "key_name": "local_remote_access_ip_address",
137            "transform_func": str
138        },
139        "local_remote_access_port": {
140            "key_name": "local_remote_access_port",
141            "transform_func": int
142        },
143        "locked": {
144            "key_name": "locked",
145            "transform_func": bool
146        },
147        "memory": {
148            "key_name": "memory",
149            "transform_func": int
150        },
151        "min_disk_size": {
152            "key_name": "min_disk_size",
153            "transform_func": int
154        },
155        "monthly_bandwidth_used": {
156            "key_name": "monthly_bandwidth_used",
157            "transform_func": int
158        },
159        "note": {
160            "key_name": "note",
161            "transform_func": str
162        },
163        "operating_system": {
164            "key_name": "operating_system",
165            "transform_func": str
166        },
167        "operating_system_distro": {
168            "key_name": "operating_system_distro",
169            "transform_func": str
170        },
171        "preferred_hvs": {
172            "key_name": "preferred_hvs",
173            "transform_func": list
174        },
175        "price_per_hour": {
176            "key_name": "price_per_hour",
177            "transform_func": float
178        },
179        "price_per_hour_powered_off": {
180            "key_name": "price_per_hour_powered_off",
181            "transform_func": float
182        },
183        "recovery_mode": {
184            "key_name": "recovery_mode",
185            "transform_func": bool
186        },
187        "remote_access_password": {
188            "key_name": "remote_access_password",
189            "transform_func": str
190        },
191        "service_password": {
192            "key_name": "service_password",
193            "transform_func": str
194        },
195        "state": {
196            "key_name": "state",
197            "transform_func": str
198        },
199        "storage_server_type": {
200            "key_name": "storage_server_type",
201            "transform_func": str
202        },
203        "strict_virtual_machine_id": {
204            "key_name": "strict_virtual_machine_id",
205            "transform_func": str
206        },
207        "support_incremental_backups": {
208            "key_name": "support_incremental_backups",
209            "transform_func": bool
210        },
211        "suspended": {
212            "key_name": "suspended",
213            "transform_func": bool
214        },
215        "template_id": {
216            "key_name": "template_id",
217            "transform_func": int
218        },
219        "template_label": {
220            "key_name": "template_label",
221            "transform_func": str
222        },
223        "total_disk_size": {
224            "key_name": "total_disk_size",
225            "transform_func": int
226        },
227        "updated_at": {
228            "key_name": "updated_at",
229            "transform_func": str
230        },
231        "user_id": {
232            "key_name": "user_id",
233            "transform_func": int
234        },
235        "vip": {
236            "key_name": "vip",
237            "transform_func": bool
238        },
239        "xen_id": {
240            "key_name": "xen_id",
241            "transform_func": int
242        }
243    }
244}
245
246
247class OnAppNodeDriver(NodeDriver):
248    """
249    Base OnApp node driver.
250    """
251
252    connectionCls = OnAppConnection
253    type = Provider.ONAPP
254    name = 'OnApp'
255    website = 'http://onapp.com/'
256
257    def create_node(self, name, ex_memory, ex_cpus, ex_cpu_shares,
258                    ex_hostname, ex_template_id, ex_primary_disk_size,
259                    ex_swap_disk_size, ex_required_virtual_machine_build=1,
260                    ex_required_ip_address_assignment=1, **kwargs):
261        """
262        Add a VS
263
264        :param  kwargs: All keyword arguments to create a VS
265        :type   kwargs: ``dict``
266
267        :rtype: :class:`OnAppNode`
268        """
269        server_params = dict(
270            label=name,
271            memory=ex_memory,
272            cpus=ex_cpus,
273            cpu_shares=ex_cpu_shares,
274            hostname=ex_hostname,
275            template_id=ex_template_id,
276            primary_disk_size=ex_primary_disk_size,
277            swap_disk_size=ex_swap_disk_size,
278            required_virtual_machine_build=ex_required_virtual_machine_build,
279            required_ip_address_assignment=ex_required_ip_address_assignment,
280            rate_limit=kwargs.get("rate_limit")
281        )
282
283        server_params.update(OnAppNodeDriver._create_args_to_params(**kwargs))
284        data = json.dumps({"virtual_machine": server_params})
285
286        response = self.connection.request(
287            "/virtual_machines.json",
288            data=data,
289            headers={
290                "Content-type": "application/json"},
291            method="POST")
292
293        return self._to_node(response.object["virtual_machine"])
294
295    def destroy_node(self,
296                     node,
297                     ex_convert_last_backup=0,
298                     ex_destroy_all_backups=0):
299        """
300        Delete a VS
301
302        :param node: OnApp node
303        :type  node: :class: `OnAppNode`
304
305        :param convert_last_backup: set 1 to convert the last VS's backup to
306                                    template, otherwise set 0
307        :type  convert_last_backup: ``int``
308
309        :param destroy_all_backups: set 1 to destroy all existing backups of
310                                    this VS, otherwise set 0
311        :type  destroy_all_backups: ``int``
312        """
313        server_params = {
314            "convert_last_backup": ex_convert_last_backup,
315            "destroy_all_backups": ex_destroy_all_backups
316        }
317        action = "/virtual_machines/{identifier}.json".format(
318            identifier=node.id)
319
320        self.connection.request(action, params=server_params, method="DELETE")
321        return True
322
323    def list_nodes(self):
324        """
325        List all VS
326
327        :rtype: ``list`` of :class:`OnAppNode`
328        """
329        response = self.connection.request("/virtual_machines.json")
330        nodes = []
331        for vm in response.object:
332            nodes.append(self._to_node(vm["virtual_machine"]))
333        return nodes
334
335    def list_images(self):
336        """
337        List all images
338
339        :rtype: ``list`` of :class:`NodeImage`
340        """
341        response = self.connection.request("/templates.json")
342        templates = []
343        for template in response.object:
344            templates.append(self._to_image(template["image_template"]))
345        return templates
346
347    def list_key_pairs(self):
348        """
349        List all the available key pair objects.
350
351        :rtype: ``list`` of :class:`.KeyPair` objects
352        """
353        user_id = self.connection.request('/profile.json').object['user']['id']
354        response = self.connection.request('/users/%s/ssh_keys.json' % user_id)
355        ssh_keys = []
356        for ssh_key in response.object:
357            ssh_keys.append(self._to_key_pair(ssh_key['ssh_key']))
358        return ssh_keys
359
360    def get_key_pair(self, name):
361        """
362        Retrieve a single key pair.
363
364        :param name: ID of the key pair to retrieve.
365        :type name: ``str``
366
367        :rtype: :class:`.KeyPair` object
368        """
369        user_id = self.connection.request('/profile.json').object['user']['id']
370        response = self.connection.request(
371            '/users/%s/ssh_keys/%s.json' % (user_id, name))
372        return self._to_key_pair(response.object['ssh_key'])
373
374    def import_key_pair_from_string(self, name, key_material):
375        """
376        Import a new public key from string.
377
378        :param name: Key pair name (unused).
379        :type name: ``str``
380
381        :param key_material: Public key material.
382        :type key_material: ``str``
383
384        :rtype: :class:`.KeyPair` object
385        """
386        data = json.dumps({'key': key_material})
387        user_id = self.connection.request('/profile.json').object['user']['id']
388        response = self.connection.request(
389            '/users/%s/ssh_keys.json' % user_id,
390            data=data,
391            headers={
392                "Content-type": "application/json"},
393            method="POST")
394        return self._to_key_pair(response.object['ssh_key'])
395
396    def delete_key_pair(self, key):
397        """
398        Delete an existing key pair.
399
400        :param key_pair: Key pair object.
401        :type key_pair: :class:`.KeyPair`
402
403        :return: True on success
404        :rtype: ``bool``
405        """
406        key_id = key.name
407        response = self.connection.request(
408            '/settings/ssh_keys/%s.json' % key_id,
409            method='DELETE')
410        return response.status == httplib.NO_CONTENT
411
412    #
413    # Helper methods
414    #
415
416    def _to_key_pair(self, data):
417        extra = {'created_at': data['created_at'],
418                 'updated_at': data['updated_at']}
419        return KeyPair(name=data['id'],
420                       fingerprint=None,
421                       public_key=data['key'],
422                       private_key=None,
423                       driver=self,
424                       extra=extra)
425
426    def _to_image(self, template):
427        extra = {'distribution': template['operating_system_distro'],
428                 'operating_system': template['operating_system'],
429                 'operating_system_arch': template['operating_system_arch'],
430                 'allow_resize_without_reboot':
431                 template['allow_resize_without_reboot'],
432                 'allowed_hot_migrate': template['allowed_hot_migrate'],
433                 'allowed_swap': template['allowed_swap'],
434                 'min_disk_size': template['min_disk_size'],
435                 'min_memory_size': template['min_memory_size'],
436                 'created_at': template['created_at']}
437        return NodeImage(id=template['id'], name=template['label'],
438                         driver=self, extra=extra)
439
440    def _to_node(self, data):
441        identifier = data["identifier"]
442        name = data["label"]
443        private_ips = []
444        public_ips = []
445        for ip in data["ip_addresses"]:
446            address = ip["ip_address"]['address']
447            if is_private_subnet(address):
448                private_ips.append(address)
449            else:
450                public_ips.append(address)
451
452        extra = OnAppNodeDriver._get_extra_dict(
453            data, RESOURCE_EXTRA_ATTRIBUTES_MAP["node"]
454        )
455        return Node(identifier,
456                    name,
457                    extra['state'],
458                    public_ips,
459                    private_ips,
460                    self,
461                    extra=extra)
462
463    @staticmethod
464    def _get_extra_dict(response, mapping):
465        """
466        Extract attributes from the element based on rules provided in the
467        mapping dictionary.
468
469        :param   response: The JSON response to parse the values from.
470        :type    response: ``dict``
471
472        :param   mapping: Dictionary with the extra layout
473        :type    mapping: ``dict``
474
475        :rtype:  ``dict``
476        """
477        extra = {}
478        for attribute, values in mapping.items():
479            transform_func = values["transform_func"]
480            value = response.get(values["key_name"])
481
482            extra[attribute] = transform_func(value) if value else None
483        return extra
484
485    @staticmethod
486    def _create_args_to_params(**kwargs):
487        """
488        Extract server params from keyword args to create a VS
489
490        :param   kwargs: keyword args
491        :return: ``dict``
492        """
493        params = [
494            "ex_cpu_sockets",
495            "ex_cpu_threads",
496            "ex_enable_autoscale",
497            "ex_data_store_group_primary_id",
498            "ex_data_store_group_swap_id",
499            "ex_hypervisor_group_id",
500            "ex_hypervisor_id",
501            "ex_initial_root_password",
502            "ex_note",
503            "ex_primary_disk_min_iops",
504            "ex_primary_network_id",
505            "ex_primary_network_group_id",
506            "ex_recipe_ids",
507            "ex_required_automatic_backup",
508            "ex_required_virtual_machine_startup",
509            "ex_required_virtual_machine_startup",
510            "ex_selected_ip_address_id",
511            "ex_swap_disk_min_iops",
512            "ex_type_of_format",
513            "ex_custom_recipe_variables",
514            "ex_licensing_key",
515            "ex_licensing_server_id",
516            "ex_licensing_type",
517        ]
518        server_params = {}
519
520        for p in params:
521            value = kwargs.get(p)
522            if value:
523                server_params[p[3:]] = value
524        return server_params
525