Source code for libcloud.container.drivers.rancher

# 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.

import base64

try:
    import simplejson as json
except Exception:
    import json

from libcloud.utils.py3 import httplib, urlparse
from libcloud.utils.py3 import b

from libcloud.common.base import JsonResponse, ConnectionUserAndKey

from libcloud.container.base import Container, ContainerDriver, ContainerImage

from libcloud.container.providers import Provider
from libcloud.container.types import ContainerState

VALID_RESPONSE_CODES = [
    httplib.OK,
    httplib.ACCEPTED,
    httplib.CREATED,
    httplib.NO_CONTENT,
]


[docs]class RancherResponse(JsonResponse):
[docs] def parse_error(self): parsed = super(RancherResponse, self).parse_error() if "fieldName" in parsed: return "Field %s is %s: %s - %s" % ( parsed["fieldName"], parsed["code"], parsed["message"], parsed["detail"], ) else: return "%s - %s" % (parsed["message"], parsed["detail"])
[docs] def success(self): return self.status in VALID_RESPONSE_CODES
[docs]class RancherException(Exception): def __init__(self, code, message): self.code = code self.message = message self.args = (code, message) def __str__(self): return "%s %s" % (self.code, self.message) def __repr__(self): return "RancherException %s %s" % (self.code, self.message)
[docs]class RancherConnection(ConnectionUserAndKey): responseCls = RancherResponse timeout = 30
[docs] def add_default_headers(self, headers): """ Add parameters that are necessary for every request If user and password are specified, include a base http auth header """ headers["Content-Type"] = "application/json" headers["Accept"] = "application/json" if self.key and self.user_id: user_b64 = base64.b64encode(b("%s:%s" % (self.user_id, self.key))) headers["Authorization"] = "Basic %s" % (user_b64.decode("utf-8")) return headers
[docs]class RancherContainerDriver(ContainerDriver): """ Driver for Rancher by Rancher Labs. This driver is capable of interacting with the Version 1 API of Rancher. It currently does NOT support the Version 2 API. Example: >>> from libcloud.container.providers import get_driver >>> from libcloud.container.types import Provider >>> driver = get_driver(Provider.RANCHER) >>> connection = driver(key="ACCESS_KEY_HERE", secret="SECRET_KEY_HERE", host="172.30.0.100", port=8080) >>> image = ContainerImage("hastebin", "hastebin", "rlister/hastebin", "latest", driver=None) >>> newcontainer = connection.deploy_container("myawesomepastebin", image, environment={"STORAGE_TYPE": "file"}) :ivar baseuri: The URL base path to the API. :type baseuri: ``str`` """ type = Provider.RANCHER name = "Rancher" website = "http://rancher.com" connectionCls = RancherConnection # Holding off on cluster support for now. # Only Environment API interaction enabled. supports_clusters = False # As in the /v1/ version = "1" def __init__(self, key, secret, secure=True, host="localhost", port=443): """ Creates a new Rancher Container driver. :param key: API key or username to used (required) :type key: ``str`` :param secret: Secret password to be used (required) :type secret: ``str`` :param secure: Whether to use HTTPS or HTTP. :type secure: ``bool`` :param host: Override hostname used for connections. This can also be a full URL string, including scheme, port, and base path. :type host: ``str`` :param port: Override port used for connections. :type port: ``int`` :return: A newly initialized driver instance. """ # Parse the Given Host if "://" not in host and not host.startswith("//"): host = "//" + host parsed = urlparse.urlparse(host) super(RancherContainerDriver, self).__init__( key=key, secret=secret, secure=False if parsed.scheme == "http" else secure, host=parsed.hostname, port=parsed.port if parsed.port else port, ) self.baseuri = parsed.path if parsed.path else "/v%s" % self.version
[docs] def ex_list_stacks(self): """ List all Rancher Stacks http://docs.rancher.com/rancher/v1.2/en/api/api-resources/environment/ :rtype: ``list`` of ``dict`` """ result = self.connection.request("%s/environments" % self.baseuri).object return result["data"]
[docs] def ex_deploy_stack( self, name, description=None, docker_compose=None, environment=None, external_id=None, rancher_compose=None, start=True, ): """ Deploy a new stack. http://docs.rancher.com/rancher/v1.2/en/api/api-resources/environment/#create :param name: The desired name of the stack. (required) :type name: ``str`` :param description: A desired description for the stack. :type description: ``str`` :param docker_compose: The Docker Compose configuration to use. :type docker_compose: ``str`` :param environment: Environment K/V specific to this stack. :type environment: ``dict`` :param external_id: The externalId of the stack. :type external_id: ``str`` :param rancher_compose: The Rancher Compose configuration for this env. :type rancher_compose: ``str`` :param start: Whether to start this stack on creation. :type start: ``bool`` :return: The newly created stack. :rtype: ``dict`` """ payload = { "description": description, "dockerCompose": docker_compose, "environment": environment, "externalId": external_id, "name": name, "rancherCompose": rancher_compose, "startOnCreate": start, } data = json.dumps(dict((k, v) for (k, v) in payload.items() if v is not None)) result = self.connection.request( "%s/environments" % self.baseuri, data=data, method="POST" ).object return result
[docs] def ex_get_stack(self, env_id): """ Get a stack by ID :param env_id: The stack to be obtained. :type env_id: ``str`` :rtype: ``dict`` """ result = self.connection.request( "%s/environments/%s" % (self.baseuri, env_id) ).object return result
[docs] def ex_search_stacks(self, search_params): """ Search for stacks matching certain filters i.e. ``{ "name": "awesomestack"}`` :param search_params: A collection of search parameters to use. :type search_params: ``dict`` :rtype: ``list`` """ search_list = [] for f, v in search_params.items(): search_list.append(f + "=" + v) search_items = "&".join(search_list) result = self.connection.request( "%s/environments?%s" % (self.baseuri, search_items) ).object return result["data"]
[docs] def ex_destroy_stack(self, env_id): """ Destroy a stack by ID http://docs.rancher.com/rancher/v1.2/en/api/api-resources/environment/#delete :param env_id: The stack to be destroyed. :type env_id: ``str`` :return: True if destroy was successful, False otherwise. :rtype: ``bool`` """ result = self.connection.request( "%s/environments/%s" % (self.baseuri, env_id), method="DELETE" ) return result.status in VALID_RESPONSE_CODES
[docs] def ex_activate_stack(self, env_id): """ Activate Services for a stack. http://docs.rancher.com/rancher/v1.2/en/api/api-resources/environment/#activateservices :param env_id: The stack to activate services for. :type env_id: ``str`` :return: True if activate was successful, False otherwise. :rtype: ``bool`` """ result = self.connection.request( "%s/environments/%s?action=activateservices" % (self.baseuri, env_id), method="POST", ) return result.status in VALID_RESPONSE_CODES
[docs] def ex_deactivate_stack(self, env_id): """ Deactivate Services for a stack. http://docs.rancher.com/rancher/v1.2/en/api/api-resources/environment/#deactivateservices :param env_id: The stack to deactivate services for. :type env_id: ``str`` :return: True if deactivate was successful, False otherwise. :rtype: ``bool`` """ result = self.connection.request( "%s/environments/%s?action=deactivateservices" % (self.baseuri, env_id), method="POST", ) return result.status in VALID_RESPONSE_CODES
[docs] def ex_list_services(self): """ List all Rancher Services http://docs.rancher.com/rancher/v1.2/en/api/api-resources/service/ :rtype: ``list`` of ``dict`` """ result = self.connection.request("%s/services" % self.baseuri).object return result["data"]
[docs] def ex_deploy_service( self, name, image, environment_id, start=True, assign_service_ip_address=None, service_description=None, external_id=None, metadata=None, retain_ip=None, scale=None, scale_policy=None, secondary_launch_configs=None, selector_container=None, selector_link=None, vip=None, **launch_conf, ): """ Deploy a Rancher Service under a stack. http://docs.rancher.com/rancher/v1.2/en/api/api-resources/service/#create *Any further configuration passed applies to the ``launchConfig``* :param name: The desired name of the service. (required) :type name: ``str`` :param image: The Image object to deploy. (required) :type image: :class:`libcloud.container.base.ContainerImage` :param environment_id: The stack ID this service is tied to. (required) :type environment_id: ``str`` :param start: Whether to start the service on creation. :type start: ``bool`` :param assign_service_ip_address: The IP address to assign the service. :type assign_service_ip_address: ``bool`` :param service_description: The service description. :type service_description: ``str`` :param external_id: The externalId for this service. :type external_id: ``str`` :param metadata: K/V Metadata for this service. :type metadata: ``dict`` :param retain_ip: Whether this service should retain its IP. :type retain_ip: ``bool`` :param scale: The scale of containers in this service. :type scale: ``int`` :param scale_policy: The scaling policy for this service. :type scale_policy: ``dict`` :param secondary_launch_configs: Secondary container launch configs. :type secondary_launch_configs: ``list`` :param selector_container: The selectorContainer for this service. :type selector_container: ``str`` :param selector_link: The selectorLink for this service. :type selector_link: ``type`` :param vip: The VIP to assign to this service. :type vip: ``str`` :return: The newly created service. :rtype: ``dict`` """ launch_conf["imageUuid"] = (self._degen_image(image),) service_payload = { "assignServiceIpAddress": assign_service_ip_address, "description": service_description, "environmentId": environment_id, "externalId": external_id, "launchConfig": launch_conf, "metadata": metadata, "name": name, "retainIp": retain_ip, "scale": scale, "scalePolicy": scale_policy, "secondary_launch_configs": secondary_launch_configs, "selectorContainer": selector_container, "selectorLink": selector_link, "startOnCreate": start, "vip": vip, } data = json.dumps( dict((k, v) for (k, v) in service_payload.items() if v is not None) ) result = self.connection.request( "%s/services" % self.baseuri, data=data, method="POST" ).object return result
[docs] def ex_get_service(self, service_id): """ Get a service by ID :param service_id: The service_id to be obtained. :type service_id: ``str`` :rtype: ``dict`` """ result = self.connection.request( "%s/services/%s" % (self.baseuri, service_id) ).object return result
[docs] def ex_search_services(self, search_params): """ Search for services matching certain filters i.e. ``{ "name": "awesomesause", "environmentId": "1e2"}`` :param search_params: A collection of search parameters to use. :type search_params: ``dict`` :rtype: ``list`` """ search_list = [] for f, v in search_params.items(): search_list.append(f + "=" + v) search_items = "&".join(search_list) result = self.connection.request( "%s/services?%s" % (self.baseuri, search_items) ).object return result["data"]
[docs] def ex_destroy_service(self, service_id): """ Destroy a service by ID http://docs.rancher.com/rancher/v1.2/en/api/api-resources/service/#delete :param service_id: The service to be destroyed. :type service_id: ``str`` :return: True if destroy was successful, False otherwise. :rtype: ``bool`` """ result = self.connection.request( "%s/services/%s" % (self.baseuri, service_id), method="DELETE" ) return result.status in VALID_RESPONSE_CODES
[docs] def ex_activate_service(self, service_id): """ Activate a service. http://docs.rancher.com/rancher/v1.2/en/api/api-resources/service/#activate :param service_id: The service to activate services for. :type service_id: ``str`` :return: True if activate was successful, False otherwise. :rtype: ``bool`` """ result = self.connection.request( "%s/services/%s?action=activate" % (self.baseuri, service_id), method="POST" ) return result.status in VALID_RESPONSE_CODES
[docs] def ex_deactivate_service(self, service_id): """ Deactivate a service. http://docs.rancher.com/rancher/v1.2/en/api/api-resources/service/#deactivate :param service_id: The service to deactivate services for. :type service_id: ``str`` :return: True if deactivate was successful, False otherwise. :rtype: ``bool`` """ result = self.connection.request( "%s/services/%s?action=deactivate" % (self.baseuri, service_id), method="POST", ) return result.status in VALID_RESPONSE_CODES
[docs] def list_containers(self): """ List the deployed containers. http://docs.rancher.com/rancher/v1.2/en/api/api-resources/container/ :rtype: ``list`` of :class:`libcloud.container.base.Container` """ result = self.connection.request("%s/containers" % self.baseuri).object containers = [self._to_container(value) for value in result["data"]] return containers
[docs] def deploy_container(self, name, image, parameters=None, start=True, **config): """ Deploy a new container. http://docs.rancher.com/rancher/v1.2/en/api/api-resources/container/#create **The following is the Image format used for ``ContainerImage``** *For a ``imageuuid``*: - ``docker:<hostname>:<port>/<namespace>/<imagename>:<version>`` *The following applies*: - ``id`` = ``<imagename>`` - ``name`` = ``<imagename>`` - ``path`` = ``<hostname>:<port>/<namespace>/<imagename>`` - ``version`` = ``<version>`` *Any extra configuration can also be passed i.e. "environment"* :param name: The desired name of the container. (required) :type name: ``str`` :param image: The Image object to deploy. (required) :type image: :class:`libcloud.container.base.ContainerImage` :param parameters: Container Image parameters (unused) :type parameters: ``str`` :param start: Whether to start the container on creation(startOnCreate) :type start: ``bool`` :rtype: :class:`Container` """ payload = { "name": name, "imageUuid": self._degen_image(image), "startOnCreate": start, } config.update(payload) data = json.dumps(config) result = self.connection.request( "%s/containers" % self.baseuri, data=data, method="POST" ).object return self._to_container(result)
[docs] def get_container(self, con_id): """ Get a container by ID :param con_id: The ID of the container to get :type con_id: ``str`` :rtype: :class:`libcloud.container.base.Container` """ result = self.connection.request( "%s/containers/%s" % (self.baseuri, con_id) ).object return self._to_container(result)
[docs] def start_container(self, container): """ Start a container :param container: The container to be started :type container: :class:`libcloud.container.base.Container` :return: The container refreshed with current data :rtype: :class:`libcloud.container.base.Container` """ result = self.connection.request( "%s/containers/%s?action=start" % (self.baseuri, container.id), method="POST", ).object return self._to_container(result)
[docs] def stop_container(self, container): """ Stop a container :param container: The container to be stopped :type container: :class:`libcloud.container.base.Container` :return: The container refreshed with current data :rtype: :class:`libcloud.container.base.Container` """ result = self.connection.request( "%s/containers/%s?action=stop" % (self.baseuri, container.id), method="POST" ).object return self._to_container(result)
[docs] def ex_search_containers(self, search_params): """ Search for containers matching certain filters i.e. ``{ "imageUuid": "docker:mysql", "state": "running"}`` :param search_params: A collection of search parameters to use. :type search_params: ``dict`` :rtype: ``list`` """ search_list = [] for f, v in search_params.items(): search_list.append(f + "=" + v) search_items = "&".join(search_list) result = self.connection.request( "%s/containers?%s" % (self.baseuri, search_items) ).object return result["data"]
[docs] def destroy_container(self, container): """ Remove a container :param container: The container to be destroyed :type container: :class:`libcloud.container.base.Container` :return: True if the destroy was successful, False otherwise. :rtype: ``bool`` """ result = self.connection.request( "%s/containers/%s" % (self.baseuri, container.id), method="DELETE" ).object return self._to_container(result)
def _gen_image(self, imageuuid): """ This function converts a valid Rancher ``imageUuid`` string to a valid image object. Only supports docker based images hence `docker:` must prefix!! Please see the deploy_container() for details on the format. :param imageuuid: A valid Rancher image string i.e. ``docker:rlister/hastebin:8.0`` :type imageuuid: ``str`` :return: Converted ContainerImage object. :rtype: :class:`libcloud.container.base.ContainerImage` """ # Obtain just the name(:version) for parsing if "/" not in imageuuid: # String looks like `docker:mysql:8.0` image_name_version = imageuuid.partition(":")[2] else: # String looks like `docker:oracle/mysql:8.0` image_name_version = imageuuid.rpartition("/")[2] # Parse based on ':' if ":" in image_name_version: version = image_name_version.partition(":")[2] id = image_name_version.partition(":")[0] name = id else: version = "latest" id = image_name_version name = id # Get our path based on if there was a version if version != "latest": path = imageuuid.partition(":")[2].rpartition(":")[0] else: path = imageuuid.partition(":")[2] return ContainerImage( id=id, name=name, path=path, version=version, driver=self.connection.driver, extra={"imageUuid": imageuuid}, ) def _degen_image(self, image): """ Take in an image object to break down into an ``imageUuid`` :param image: :return: """ # Only supporting docker atm image_type = "docker" if image.version is not None: return image_type + ":" + image.path + ":" + image.version else: return image_type + ":" + image.path def _to_container(self, data): """ Convert container in proper Container instance object ** Updating is NOT supported!! :param data: API data about container i.e. result.object :return: Proper Container object: see http://libcloud.readthedocs.io/en/latest/container/api.html """ rancher_state = data["state"] # A Removed container is purged after x amt of time. # Both of these render the container dead (can't be started later) terminate_condition = ["removed", "purged"] if "running" in rancher_state: state = ContainerState.RUNNING elif "stopped" in rancher_state: state = ContainerState.STOPPED elif "restarting" in rancher_state: state = ContainerState.REBOOTING elif "error" in rancher_state: state = ContainerState.ERROR elif any(x in rancher_state for x in terminate_condition): state = ContainerState.TERMINATED elif data["transitioning"] == "yes": # Best we can do for current actions state = ContainerState.PENDING else: state = ContainerState.UNKNOWN # Everything contained in the json response is dumped in extra extra = data return Container( id=data["id"], name=data["name"], image=self._gen_image(data["imageUuid"]), ip_addresses=[data["primaryIpAddress"]], state=state, driver=self.connection.driver, extra=extra, )