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

from libcloud.utils.py3 import b, httplib, urlparse
from libcloud.common.base import JsonResponse, ConnectionUserAndKey
from libcloud.container.base import Container, ContainerImage, ContainerDriver
from libcloud.container.types import ContainerState
from libcloud.container.providers import Provider

try:
    import simplejson as json
except Exception:
    import json


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


[docs]class RancherResponse(JsonResponse):
[docs] def parse_error(self): parsed = super().parse_error() if "fieldName" in parsed: return "Field {} is {}: {} - {}".format( parsed["fieldName"], parsed["code"], parsed["message"], parsed["detail"], ) else: return "{} - {}".format(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 "{} {}".format(self.code, self.message) def __repr__(self): return "RancherException {} {}".format(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("{}:{}".format(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().__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({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("{}/environments/{}".format(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( "{}/environments?{}".format(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( "{}/environments/{}".format(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( "{}/environments/{}?action=activateservices".format(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( "{}/environments/{}?action=deactivateservices".format(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({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("{}/services/{}".format(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("{}/services?{}".format(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( "{}/services/{}".format(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( "{}/services/{}?action=activate".format(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( "{}/services/{}?action=deactivate".format(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("{}/containers/{}".format(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( "{}/containers/{}?action=start".format(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( "{}/containers/{}?action=stop".format(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( "{}/containers?{}".format(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( "{}/containers/{}".format(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, )