blob: 8bf423dd1f2860a577b4c3958c717371edb7c196 [file] [log] [blame]
# 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,
]
class RancherResponse(JsonResponse):
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"])
def success(self):
return self.status in VALID_RESPONSE_CODES
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)
class RancherConnection(ConnectionUserAndKey):
responseCls = RancherResponse
timeout = 30
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
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
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"]
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
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
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"]
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
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
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
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"]
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
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
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"]
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
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
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
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
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)
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)
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)
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)
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"]
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,
)