You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by GitBox <gi...@apache.org> on 2020/04/04 19:13:07 UTC

[GitHub] [libcloud] Kami commented on a change in pull request #1442: Kamatera compute driver

Kami commented on a change in pull request #1442: Kamatera compute driver
URL: https://github.com/apache/libcloud/pull/1442#discussion_r403505758
 
 

 ##########
 File path: libcloud/compute/drivers/kamatera.py
 ##########
 @@ -0,0 +1,639 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Kamatera node driver
+"""
+import json
+import datetime
+import time
+
+from libcloud.utils.py3 import basestring
+from libcloud.compute.base import NodeDriver, NodeLocation, NodeSize
+from libcloud.compute.base import NodeImage, Node, NodeState
+from libcloud.compute.types import Provider
+from libcloud.common.base import ConnectionUserAndKey, JsonResponse
+
+
+class KamateraResponse(JsonResponse):
+    """
+    Response class for KamateraDriver
+    """
+
+    def parse_error(self):
+        data = self.parse_body()
+        if 'message' in data:
+            return data['message']
+        else:
+            return json.dumps(data)
+
+
+class KamateraConnection(ConnectionUserAndKey):
+    """
+    Connection class for KamateraDriver
+    """
+
+    host = 'cloudcli.cloudwm.com'
+    responseCls = KamateraResponse
+
+    def add_default_headers(self, headers):
+        """Adds headers that are needed for all requests"""
+        headers['AuthClientId'] = self.user_id
+        headers['AuthSecret'] = self.key
+        headers['Accept'] = 'application/json'
+        headers['Content-Type'] = 'application/json'
+        return headers
+
+
+class KamateraNodeDriver(NodeDriver):
+    """
+    Kamatera node driver
+
+    :keyword    key: API Client ID, required for authentication
+    :type       key: ``str``
+
+    :keyword    secret: API Secret, required for authentcaiont
+    :type       secret: ``str``
+    """
+
+    type = Provider.KAMATERA
+    name = 'Kamatera'
+    website = 'https://www.kamatera.com/'
+    connectionCls = KamateraConnection
+    features = {'create_node': ['password', 'generates_password', 'ssh_key']}
+
+    EX_BILLINGCYCLE_HOURLY = 'hourly'
+    EX_BILLINGCYCLE_MONTHLY = 'monthly'
+
+    def list_locations(self):
+        """
+        List available locations for deployment
+
+        :rtype: ``list`` of :class:`NodeLocation`
+        """
+        response = self.connection.request('service/server?datacenter=1')
+        return [self.ex_get_location(
+            datacenter['id'], datacenter['subCategory'], datacenter['name']
+        ) for datacenter in response.object]
+
+    def list_sizes(self, location):
+        """
+        List predefined sizes for the given location.
+
+        :param location: Location of the deployment.
+        :type location: :class:`.NodeLocation`
+
+        @inherits: :class:`NodeDriver.list_sizes`
+        """
+        response = self.connection.request(
+            'service/server?sizes=1&datacenter=%s' % location.id)
+        return [self.ex_get_size(
+            size['ramMB'],
+            size['diskSizeGB'],
+            size['cpuType'],
+            size['cpuCores'],
+            extraDiskSizesGB=[],
+            monthlyTrafficPackage=size['monthlyTrafficPackage'],
+            id=size['id']
+        ) for size in response.object]
+
+    def list_images(self, location):
+        """
+        List available disk images.
+
+        :param location: Location of the deployement.
+                         Available disk images depend on location.
+        :type location: :class:`.NodeLocation`
+
+        :rtype: ``list`` of :class:`NodeImage`
+        """
+        response = self.connection.request(
+            'service/server?images=1&datacenter=%s' % location.id)
+        images = []
+        for image in response.object:
+            extra = self._copy_dict(('datacenter', 'os', 'code',
+                                     'osDiskSizeGB', 'ramMBMin'), image)
+            images.append(self.ex_get_image(image['name'],
+                                            image['id'], extra))
+        return images
+
+    def create_node(self, name, size, image, location, auth=None,
+                    ex_networks=None, ex_dailybackup=False,
+                    ex_managed=False, ex_billingcycle=EX_BILLINGCYCLE_HOURLY,
+                    ex_poweronaftercreate=True,
+                    ex_wait=True):
+        """
+        Creates a Kamatera node.
+
+        If auth is not given then password will be generated.
+
+        :param name:   String with a name for this new node (required)
+        :type name:   ``str``
+
+        :param size:   The size of resources allocated to this node (required)
+        :type size:   :class:`.NodeSize`
+
+        :param image:  OS Image to boot on node. (required)
+        :type image:  :class:`.NodeImage`
+
+        :param location: Which data center to create a node in. (required)
+        :type location: :class:`.NodeLocation`
+
+        :param auth:   Authentication information for the node (optional)
+        :type auth:   :class:`.NodeAuthSSHKey` or :class:`.NodeAuthPassword`
+
+        :param ex_networks:   Network configurations (optional)
+        :type ex_networks:   ``list`` of ``dict``
+
+        :param ex_dailybackup:   Whether to create daily backups (optional)
+        :type ex_dailybackup:    ``bool``
+
+        :param ex_managed:   Whether to provide managed support (optional)
+        :type ex_managed:    ``bool``
+
+        :param ex_billingcycle:   billing cycle (hourly / monthly) (optional)
+        :type ex_billingcycle:    ``str``
+
+        :param ex_poweronaftercreate:   power on after creation (optional)
+        :type ex_poweronaftercreate:    ``bool``
+
+        :param ex_wait:   wait for server to be running (optional)
+        :type ex_wait:    ``bool``
+
+        :return: The newly created node.
+        :rtype: :class:`.Node`
+        """
+        password = None
+        pubkey = None
+        generate_password = False
+        if isinstance(auth, basestring):
+            password = auth
+        else:
+            auth_obj = self._get_and_check_auth(auth)
+            if getattr(auth_obj, 'generated', False):
+                password = '__generate__'
+                generate_password = True
+            elif hasattr(auth_obj, 'password'):
+                password = auth_obj.password
+                generate_password = False
+            elif hasattr(auth_obj, 'pubkey'):
+                pubkey = auth_obj.pubkey
+        if not ex_networks:
+            ex_networks = [{'name': 'wan', 'ip': 'auto'}]
+        request_data = {
+            "name": name,
+            "password": password or '',
+            "passwordValidate": password or '',
+            'ssh-key': pubkey or '',
+            "datacenter": location.id,
+            "image": image.id,
+            "cpu": '%s%s' % (size.extra['cpuCores'], size.extra['cpuType']),
+            "ram": size.ram,
+            "disk": ' '.join([
+                'size=%d' % disksize for disksize
+                in [size.disk] + size.extra['extraDiskSizesGB']]),
+            "dailybackup": 'yes' if ex_dailybackup else 'no',
+            "managed": 'yes' if ex_managed else 'no',
+            "network": ' '.join([','.join([
+                '%s=%s' % (k, v) for k, v
+                in network.items()]) for network in ex_networks]),
+            "quantity": 1,
+            "billingcycle": ex_billingcycle,
+            "monthlypackage": size.extra['monthlyTrafficPackage'] or '',
+            "poweronaftercreate": 'yes' if ex_poweronaftercreate else 'no'
+        }
+        response = self.connection.request('service/server', method='POST',
+                                           data=json.dumps(request_data))
+        if generate_password:
+            command_ids = response.object['commandIds']
+            generated_password = response.object['password']
+        else:
+            command_ids = response.object
+            generated_password = None
+        if len(command_ids) != 1:
+            raise RuntimeError('invalid response')
+        node = self.ex_get_node(
+            name=name, size=size, image=image, location=location,
+            dailybackup=ex_dailybackup, managed=ex_managed,
+            billingcycle=ex_billingcycle,
+            generated_password=generated_password,
+            create_command_id=command_ids[0],
+            poweronaftercreate=ex_poweronaftercreate)
+        if ex_wait:
+            if (
+                'create_command_id' not in node.extra
+                or node.state != NodeState.UNKNOWN
+            ):
+                raise ValueError('invalid node for updating create status')
+            command = self.ex_wait_command(node.extra['create_command_id'])
+            node.extra['create_log'] = command.get('log')
+            if node.extra.get('poweronaftercreate'):
+                node.state = NodeState.RUNNING
+            else:
+                node.state = NodeState.STOPPED
+            if command.get('completed'):
+                node.created_at = datetime.datetime.strptime(
+                    command['completed'], '%Y-%m-%d %H:%M:%S')
+            name_lines = [line for line
+                          in node.extra['create_log'].split("\n")
+                          if line.startswith('Name: ')]
+            if len(name_lines) != 1:
+                raise RuntimeError('Invalid node create log response')
+            node.name = name_lines[0].replace('Name: ', '')
+            response = self.connection.request(
+                '/service/server/info', method='POST',
+                data=json.dumps({'name': node.name}))
+            self._update_node_from_server_info(node, response.object[0])
+        return node
+
+    def list_nodes(self, ex_name_regex=None, ex_full_details=False,
+                   ex_id=None):
+        """
+        List nodes
+
+        :param ex_name_regex:   Regular expression to match node names
+                                if set returns full node details (optional)
+        :type ex_name_regex:    ``str``
+
+        :param ex_full_details:   Whether to return full node details
+                                  takes longer to complete (optional)
+        :type ex_full_details:    ``bool``
+
+        :return: List of node objects
+        :rtype: ``list`` of :class:`Node`
+        """
+        if ex_name_regex or ex_full_details or ex_id:
+            request_data = {}
+            if ex_id:
+                request_data['id'] = ex_id
+            else:
+                if not ex_name_regex:
+                    ex_name_regex = '.*'
+                request_data['name'] = ex_name_regex
+            response = self.connection.request(
+                '/service/server/info', method='POST',
+                data=json.dumps(request_data))
+            return [self._update_node_from_server_info(
+                self.ex_get_node(), server) for server in response.object]
+        else:
+            response = self.connection.request('/service/servers')
+            return [
+                self.ex_get_node(
+                    id=server['id'], name=server['name'],
+                    state=(
+                        NodeState.RUNNING if server['power'] == 'on'
+                        else NodeState.STOPPED),
+                    location=self.ex_get_location(server['datacenter'])
+                ) for server in response.object]
+
+    def reboot_node(self, node, ex_wait=True):
+        """
+        Reboot the given node
+
+        :param node:     the node to reboot
+        :type node: :class:`Node`
+
+        :param ex_wait:     wait for reboot to complete (optional)
+        :type ex_wait:  ``bool``
+
+        :rtype: ``bool``
+        """
+        return self.ex_node_operation(node, 'reboot', ex_wait)
+
+    def destroy_node(self, node, ex_wait=True):
+        """
+        Destroy the given node
+
+        :param node:     the node to destroy
+        :type node: :class:`Node`
+
+        :param ex_wait:     wait for destroy to complete (optional)
+        :type ex_wait:      ``bool``
+
+        :rtype: ``bool``
+        """
+        return self.ex_node_operation(node, 'terminate', ex_wait)
+
+    def stop_node(self, node, ex_wait=True):
+        """
+        Stop the given node
+
+        :param node:     the node to stop
+        :type node: :class:`Node`
+
+        :param ex_wait:     wait for stop to complete (optional)
+        :type ex_wait:      ``bool``
+
+        :rtype: ``bool``
+        """
+        return self.ex_node_operation(node, 'poweroff', ex_wait)
+
+    def start_node(self, node, ex_wait=True):
+        """
+        Start the given node
+
+        :param node:     the node to start
+        :type node: :class:`Node`
+
+        :param ex_wait:     wait for start to complete (optional)
+        :type ex_wait:      ``bool``
+
+        :rtype: ``bool``
+        """
+        return self.ex_node_operation(node, 'poweron', ex_wait)
+
+    def ex_node_operation(self, node, operation, wait=True):
+        """
+        Run custom operations on the node
+
+        :param node:     the node to run operation on
+        :type node: :class:`Node`
+
+        :param operation:   the operation to run
+        :type operation:   ``str``
+
+        :param ex_wait:     wait for destroy to complete (optional)
+        :type ex_wait:      ``bool``
+
+        :rtype: ``bool``
+        """
+        if node.id:
+            request_data = {'id': node.id}
+        elif node.name:
+            request_data = {'name': node.name}
+        else:
+            raise ValueError('Invalid node for %s node operation: '
+                             'missing id / name' % operation)
+        if operation == 'terminate':
+            request_data['force'] = True
+        command_id = self.connection.request(
+            '/service/server/%s' % operation, method='POST',
+            data=json.dumps(request_data)).object[0]
+        if wait:
+            self.ex_wait_command(command_id)
+        else:
+            node.extra['%s_command_id' % operation] = command_id
+        return True
+
+    def ex_get_location(self, id, name=None, country=None):
+        """
+        Get a NodeLocation object to use for other methods
+
+        :param id:     Location ID - uppercase letters code (required)
+        :type id:      ``str``
+
+        :param name:     Location Name (optional)
+        :type name:      ``str``
+
+        :param name:     Location country (optional)
+        :type name:      ``str``
+
+        :rtype: :class:`.NodeLocation`
+        """
+        return NodeLocation(id=id, name=name, country=country, driver=self)
+
+    def ex_get_size(self, ramMB, diskSizeGB, cpuType, cpuCores,
 
 Review comment:
   We tend to use underscore separated variable names, but since this is an extension method, this notation is also acceptable if it matches the API parameters.

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services