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
17import re
18
19from libcloud.compute.base import Node, NodeDriver, NodeLocation
20from libcloud.compute.base import NodeSize, NodeImage
21from libcloud.compute.base import KeyPair
22from libcloud.common.maxihost import MaxihostConnection
23from libcloud.compute.types import Provider, NodeState
24from libcloud.common.exceptions import BaseHTTPError
25from libcloud.utils.py3 import httplib
26
27
28__all__ = [
29    "MaxihostNodeDriver"
30]
31
32
33class MaxihostNodeDriver(NodeDriver):
34    """
35    Base Maxihost node driver.
36    """
37
38    connectionCls = MaxihostConnection
39    type = Provider.MAXIHOST
40    name = 'Maxihost'
41    website = 'https://www.maxihost.com/'
42
43    def create_node(self, name, size, image, location,
44                    ex_ssh_key_ids=None):
45        """
46        Create a node.
47
48        :return: The newly created node.
49        :rtype: :class:`Node`
50        """
51        attr = {'hostname': name, 'plan': size.id,
52                'operating_system': image.id,
53                'facility': location.id.lower(), 'billing_cycle': 'monthly'}
54
55        if ex_ssh_key_ids:
56            attr['ssh_keys'] = ex_ssh_key_ids
57
58        try:
59            res = self.connection.request('/devices',
60                                          params=attr, method='POST')
61        except BaseHTTPError as exc:
62            error_message = exc.message.get('error_messages', '')
63            raise ValueError('Failed to create node: %s' % (error_message))
64
65        return self._to_node(res.object['devices'][0])
66
67    def start_node(self, node):
68        """
69        Start a node.
70        """
71        params = {"type": "power_on"}
72        res = self.connection.request('/devices/%s/actions' % node.id,
73                                      params=params, method='PUT')
74
75        return res.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
76
77    def stop_node(self, node):
78        """
79        Stop a node.
80        """
81        params = {"type": "power_off"}
82        res = self.connection.request('/devices/%s/actions' % node.id,
83                                      params=params, method='PUT')
84
85        return res.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
86
87    def destroy_node(self, node):
88        """
89        Destroy a node.
90        """
91        res = self.connection.request('/devices/%s' % node.id,
92                                      method='DELETE')
93
94        return res.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
95
96    def reboot_node(self, node):
97        """
98        Reboot a node.
99        """
100        params = {"type": "power_cycle"}
101        res = self.connection.request('/devices/%s/actions' % node.id,
102                                      params=params, method='PUT')
103
104        return res.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
105
106    def list_nodes(self):
107        """
108        List nodes
109
110        :rtype: ``list`` of :class:`MaxihostNode`
111        """
112        response = self.connection.request('/devices')
113        nodes = [self._to_node(host)
114                 for host in response.object['devices']]
115        return nodes
116
117    def _to_node(self, data):
118        extra = {}
119        private_ips = []
120        public_ips = []
121        for ip in data['ips']:
122            if 'Private' in ip['ip_description']:
123                private_ips.append(ip['ip_address'])
124            else:
125                public_ips.append(ip['ip_address'])
126
127        if data['power_status']:
128            state = NodeState.RUNNING
129        else:
130            state = NodeState.STOPPED
131
132        for key in data:
133            extra[key] = data[key]
134
135        node = Node(id=data['id'], name=data['description'], state=state,
136                    private_ips=private_ips, public_ips=public_ips,
137                    driver=self, extra=extra)
138        return node
139
140    def list_locations(self, ex_available=True):
141        """
142        List locations
143
144        If ex_available is True, show only locations which are available
145        """
146        locations = []
147        data = self.connection.request('/regions')
148        for location in data.object['regions']:
149            if ex_available:
150                if location.get('available'):
151                    locations.append(self._to_location(location))
152            else:
153                locations.append(self._to_location(location))
154        return locations
155
156    def _to_location(self, data):
157        name = data.get('location').get('city', '')
158        country = data.get('location').get('country', '')
159        return NodeLocation(id=data['slug'], name=name, country=country,
160                            driver=self)
161
162    def list_sizes(self):
163        """
164        List sizes
165        """
166        sizes = []
167        data = self.connection.request('/plans')
168        for size in data.object['servers']:
169            sizes.append(self._to_size(size))
170        return sizes
171
172    def _to_size(self, data):
173        extra = {'specs': data['specs'],
174                 'regions': data['regions'],
175                 'pricing': data['pricing']}
176        ram = data['specs']['memory']['total']
177        ram = re.sub("[^0-9]", "", ram)
178        return NodeSize(id=data['slug'], name=data['name'], ram=int(ram),
179                        disk=None, bandwidth=None,
180                        price=data['pricing']['usd_month'],
181                        driver=self, extra=extra)
182
183    def list_images(self):
184        """
185        List images
186        """
187        images = []
188        data = self.connection.request('/plans/operating-systems')
189        for image in data.object['operating-systems']:
190            images.append(self._to_image(image))
191        return images
192
193    def _to_image(self, data):
194        extra = {'operating_system': data['operating_system'],
195                 'distro': data['distro'],
196                 'version': data['version'],
197                 'pricing': data['pricing']}
198        return NodeImage(id=data['slug'], name=data['name'], driver=self,
199                         extra=extra)
200
201    def list_key_pairs(self):
202        """
203        List all the available SSH keys.
204
205        :return: Available SSH keys.
206        :rtype: ``list`` of :class:`KeyPair`
207        """
208        data = self.connection.request('/account/keys')
209        return list(map(self._to_key_pair, data.object['ssh_keys']))
210
211    def create_key_pair(self, name, public_key):
212        """
213        Create a new SSH key.
214
215        :param      name: Key name (required)
216        :type       name: ``str``
217
218        :param      public_key: base64 encoded public key string (required)
219        :type       public_key: ``str``
220        """
221        attr = {'name': name, 'public_key': public_key}
222        res = self.connection.request('/account/keys', method='POST',
223                                      data=json.dumps(attr))
224
225        data = res.object['ssh_key']
226
227        return self._to_key_pair(data=data)
228
229    def _to_key_pair(self, data):
230        extra = {'id': data['id']}
231        return KeyPair(name=data['name'],
232                       fingerprint=data['fingerprint'],
233                       public_key=data['public_key'],
234                       private_key=None,
235                       driver=self,
236                       extra=extra)
237
238    def ex_start_node(self, node):
239        # NOTE: This method is here for backward compatibility reasons after
240        # this method was promoted to be part of the standard compute API in
241        # Libcloud v2.7.0
242        return self.start_node(node=node)
243
244    def ex_stop_node(self, node):
245        # NOTE: This method is here for backward compatibility reasons after
246        # this method was promoted to be part of the standard compute API in
247        # Libcloud v2.7.0
248        return self.stop_node(node=node)
249