text: # 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.
  """
  A driver for cloudscale.ch.
  """
  
  import json
  
  from libcloud.utils.py3 import httplib
  
  from libcloud.common.base import ConnectionKey, JsonResponse
  from libcloud.compute.types import Provider, NodeState
  from libcloud.common.types import InvalidCredsError
  from libcloud.compute.base import NodeDriver
  from libcloud.compute.base import Node, NodeImage, NodeSize
  
  
  class CloudscaleResponse(JsonResponse):
      valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED,
                              httplib.NO_CONTENT]
  
      def parse_error(self):
          body = self.parse_body()
          if self.status == httplib.UNAUTHORIZED:
              raise InvalidCredsError(body['detail'])
          else:
              # We are taking the first issue here. There might be multiple ones,
              # but that doesn't really matter. It's nicer if the error is just
              # one error (because it's a Python API and there's only one
              # exception.
              return next(iter(body.values()))
  
      def success(self):
          return self.status in self.valid_response_codes
  
  
  class CloudscaleConnection(ConnectionKey):
      """
      Connection class for the cloudscale.ch driver.
      """
      host = 'api.cloudscale.ch'
      responseCls = CloudscaleResponse
  
      def add_default_headers(self, headers):
          """
          Add headers that are necessary for every request
  
          This method adds ``token`` to the request.
          """
          headers['Authorization'] = 'Bearer %s' % (self.key)
          headers['Content-Type'] = 'application/json'
          return headers
  
  
  class CloudscaleNodeDriver(NodeDriver):
      """
      Cloudscale's node driver.
      """
  
      connectionCls = CloudscaleConnection
  
      type = Provider.CLOUDSCALE
      name = 'Cloudscale'
      website = 'https://www.cloudscale.ch'
  
      NODE_STATE_MAP = dict(
          changing=NodeState.PENDING,
          running=NodeState.RUNNING,
          stopped=NodeState.STOPPED,
          paused=NodeState.PAUSED,
      )
  
      def __init__(self, key, **kwargs):
          super(CloudscaleNodeDriver, self).__init__(key, **kwargs)
  
      def list_nodes(self):
          """
          List all your existing compute nodes.
          """
          return self._list_resources('/v1/servers', self._to_node)
  
      def list_sizes(self):
          """
          Lists all available sizes. On cloudscale these are known as flavors.
          """
          return self._list_resources('/v1/flavors', self._to_size)
  
      def list_images(self):
          """
          List all images.
  
          Images are identified by slugs on cloudscale.ch. This means that minor
          version upgrades (e.g. Ubuntu 16.04.1 to Ubuntu 16.04.2) will be
          possible within the same id ``ubuntu-16.04``.
          """
          return self._list_resources('/v1/images', self._to_image)
  
      def create_node(self, name, size, image, location=None,
                      ex_create_attr=None):
          """
          Create a node.
  
          The `ex_create_attr` parameter can include the following dictionary
          key and value pairs:
  
          * `ssh_keys`: ``list`` of ``str`` ssh public keys
          * `volume_size_gb`: ``int`` defaults to 10.
          * `bulk_volume_size_gb`: defaults to None.
          * `use_public_network`: ``bool`` defaults to True
          * `use_private_network`: ``bool`` defaults to False
          * `use_ipv6`: ``bool`` defaults to True
          * `anti_affinity_with`: ``uuid`` of a server to create an anti-affinity
            group with that server or add it to the same group as that server.
          * `user_data`: ``str`` for optional cloud-config data
  
          :keyword ex_create_attr: A dictionary of optional attributes for
                                   droplet creation
          :type ex_create_attr: ``dict``
  
          :return: The newly created node.
          :rtype: :class:`Node`
          """
          ex_create_attr = ex_create_attr or {}
          attr = dict(ex_create_attr)
          attr.update(
              name=name,
              image=image.id,
              flavor=size.id,
          )
          result = self.connection.request(
              '/v1/servers',
              data=json.dumps(attr),
              method='POST'
          )
          return self._to_node(result.object)
  
      def reboot_node(self, node):
          """
          Reboot a node. It's also possible to use ``node.reboot()``.
          """
          return self._action(node, 'reboot')
  
      def start_node(self, node):
          """
          Start a node. This is only possible if the node is stopped.
          """
          return self._action(node, 'start')
  
      def stop_node(self, node):
          """
          Stop a specific node. Similar to ``shutdown -h now``. This is only
          possible if the node is running.
          """
          return self._action(node, 'stop')
  
      def ex_start_node(self, node):
          # NOTE: This method is here for backward compatibility reasons after
          # this method was promoted to be part of the standard compute API in
          # Libcloud v2.7.0
          return self.start_node(node=node)
  
      def ex_stop_node(self, node):
          # NOTE: This method is here for backward compatibility reasons after
          # this method was promoted to be part of the standard compute API in
          # Libcloud v2.7.0
          return self.stop_node(node=node)
  
      def ex_node_by_uuid(self, uuid):
          """
          :param str ex_user_data: A valid uuid that references your exisiting
              cloudscale.ch server.
          :type       ex_user_data:  ``str``
  
          :return: The server node you asked for.
          :rtype: :class:`Node`
          """
          res = self.connection.request(self._get_server_url(uuid))
          return self._to_node(res.object)
  
      def destroy_node(self, node):
          """
          Delete a node. It's also possible to use ``node.destroy()``.
          This will irreversibly delete the cloudscale.ch server and all its
          volumes. So please be cautious.
          """
          res = self.connection.request(
              self._get_server_url(node.id),
              method='DELETE'
          )
          return res.status == httplib.NO_CONTENT
  
      def _get_server_url(self, uuid):
          return '/v1/servers/%s' % uuid
  
      def _action(self, node, action_name):
          response = self.connection.request(
              self._get_server_url(node.id) + '/' + action_name,
              method='POST'
          )
          return response.status == httplib.OK
  
      def _list_resources(self, url, tranform_func):
          data = self.connection.request(url, method='GET').object
          return [tranform_func(obj) for obj in data]
  
      def _to_node(self, data):
          state = self.NODE_STATE_MAP.get(data['status'], NodeState.UNKNOWN)
          extra_keys_exclude = ['uuid', 'name', 'status', 'flavor', 'image']
          extra = {}
          for k, v in data.items():
              if k not in extra_keys_exclude:
                  extra[k] = v
  
          public_ips = []
          private_ips = []
          for interface in data['interfaces']:
              if interface['type'] == 'public':
                  ips = public_ips
              else:
                  ips = private_ips
              for address_obj in interface['addresses']:
                  ips.append(address_obj['address'])
  
          return Node(
              id=data['uuid'],
              name=data['name'],
              state=state,
              public_ips=public_ips,
              private_ips=private_ips,
              extra=extra,
              driver=self,
              image=self._to_image(data['image']),
              size=self._to_size(data['flavor']),
          )
  
      def _to_size(self, data):
          extra = {'vcpu_count': data['vcpu_count']}
          ram = data['memory_gb'] * 1024
  
          return NodeSize(id=data['slug'], name=data['name'],
                          ram=ram, disk=10,
                          bandwidth=0, price=0,
                          extra=extra, driver=self)
  
      def _to_image(self, data):
          extra = {'operating_system': data['operating_system']}
          return NodeImage(id=data['slug'], name=data['name'], extra=extra,
                           driver=self)
  