You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by an...@apache.org on 2016/10/08 02:30:37 UTC
[06/27] libcloud git commit: initial push
initial push
Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/8495ea71
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/8495ea71
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/8495ea71
Branch: refs/heads/trunk
Commit: 8495ea71744394528f30e3e297ea41a2e22574bb
Parents: a37a02a
Author: Mario Loria <ma...@arroyonetworks.com>
Authored: Tue Sep 27 23:12:46 2016 -0400
Committer: Anthony Shaw <an...@apache.org>
Committed: Sat Oct 8 13:29:22 2016 +1100
----------------------------------------------------------------------
libcloud/container/drivers/rancher.py | 648 +++++++++++++++++++++++++++++
libcloud/container/providers.py | 2 +
libcloud/container/types.py | 1 +
3 files changed, 651 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/libcloud/blob/8495ea71/libcloud/container/drivers/rancher.py
----------------------------------------------------------------------
diff --git a/libcloud/container/drivers/rancher.py b/libcloud/container/drivers/rancher.py
new file mode 100644
index 0000000..5229442
--- /dev/null
+++ b/libcloud/container/drivers/rancher.py
@@ -0,0 +1,648 @@
+import base64
+import datetime
+import shlex
+import re
+
+try:
+ import simplejson as json
+except:
+ import json
+
+from libcloud.utils.py3 import httplib
+from libcloud.utils.py3 import b
+
+from libcloud.common.base import JsonResponse, ConnectionUserAndKey
+from libcloud.common.types import InvalidCredsError
+
+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]
+
+
+class RancherResponse(JsonResponse):
+
+ def parse_body(self):
+ if len(self.body) == 0 and not self.parse_zero_length_body:
+ return self.body
+ valid_content_types = ['application/json',
+ 'application/json; charset=utf-8']
+ content_type = self.headers.get('content-type')
+ if content_type in valid_content_types:
+ return json.loads(self.body)
+
+ def parse_error(self):
+ if self.status == 401:
+ raise InvalidCredsError('Invalid credentials')
+ return self.body
+
+ 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 "%s %s" % (self.code, self.message)
+
+ def __repr__(self):
+ return "RancherException %s %s" % (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('%s:%s' % (self.user_id, self.key)))
+ headers['Authorization'] = 'Basic %s' % (user_b64.decode('utf-8'))
+ return headers
+
+
+class RancherContainerDriver(ContainerDriver):
+
+ type = Provider.RANCHER
+ name = 'Rancher'
+ website = 'http://rancher.com'
+ connectionCls = RancherConnection
+ # Holding off on this for now. Only Environment API interaction enabled.
+ supports_clusters = False
+ # As in the /v1/
+ version = '1'
+
+ def __init__(self, key, secret, secure=False, host='localhost',
+ port=80, key_file=None, cert_file=None):
+ """
+ :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. Note: Some providers
+ only support HTTPS, and it is on by default.
+ :type secure: ``bool``
+
+ :param host: Override hostname used for connections.
+ :type host: ``str``
+
+ :param port: Override port used for connections.
+ :type port: ``int``
+
+ :param key_file: Path to private key for TLS connection (optional)
+ :type key_file: ``str``
+
+ :param cert_file: Path to public key for TLS connection (optional)
+ :type cert_file: ``str``
+
+ :return: ``None``
+ """
+ super(RancherContainerDriver, self).__init__(key=key, secret=secret,
+ secure=secure, host=host,
+ port=port,
+ key_file=key_file,
+ cert_file=cert_file)
+ if host.startswith('https://'):
+ secure = True
+
+ # strip the prefix
+ prefixes = ['http://', 'https://']
+ for prefix in prefixes:
+ if host.startswith(prefix):
+ host = host.strip(prefix)
+ """
+ if key_file or cert_file:
+ # docker tls authentication-
+ # https://docs.docker.com/articles/https/
+ # We pass two files, a key_file with the
+ # private key and cert_file with the certificate
+ # libcloud will handle them through LibcloudHTTPSConnection
+ if not (key_file and cert_file):
+ raise Exception(
+ 'Needs both private key file and '
+ 'certificate file for tls authentication')
+ self.connection.key_file = key_file
+ self.connection.cert_file = cert_file
+ self.connection.secure = True
+ else:
+ self.connection.secure = secure
+ """
+
+ self.connection.host = host
+ self.connection.port = port
+
+ # We only support environment api keys, meaning none of this:
+ # self.baseuri = "/v%s/projects/%s" % (self.version, project_id)
+ self.baseuri = "/v%s" % self.version
+
+ def ex_list_environments(self):
+ """
+ List the deployed container images
+ :param image: Filter to containers with a certain image
+ :type image: :class:`libcloud.container.base.ContainerImage`
+ :param all: Show all container (including stopped ones)
+ :type all: ``bool``
+ :rtype: ``list`` of :class:`libcloud.container.base.Container`
+ """
+
+ result = self.connection.request("%s/environments" % self.baseuri).object
+ return result['data']
+
+ def ex_deploy_environment(self, name, description=None, dockercompose=None,
+ environment=None, externalid=None, outputs=None,
+ previousenvironment=None, previousexternalid=None,
+ ranchercompose=None, start=True):
+
+ payload = {
+ "description": description,
+ "dockerCompose": dockercompose,
+ "environment": environment,
+ "externalId": externalid,
+ "name": name,
+ "outputs": outputs,
+ "previousEnvironment": previousenvironment,
+ "previousExternalId": previousexternalid,
+ "rancherCompose": ranchercompose,
+ "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_environment(self, env_id):
+ """
+ Get a service by ID
+ :param env_id: The environment to be obtained.
+ :return: The API dict object returned for the new service.
+ """
+ result = self.connection.request("%s/environments/%s" %
+ (self.baseuri, env_id)).object
+
+ return result
+
+ def ex_destroy_environment(self, env_id):
+
+ result = self.connection.request('%s/environments/%s' % (
+ self.baseuri, env_id), method='DELETE')
+ if result.status in VALID_RESPONSE_CODES:
+ return result.status in VALID_RESPONSE_CODES
+ else:
+ raise RancherException(result.status,
+ 'failed to destroy environment')
+
+ def ex_activate_environment(self, env_id):
+
+ result = self.connection.request('%s/environments/%s?action=activateservices' %
+ (self.baseuri, env_id),
+ method='POST')
+ if result.status in VALID_RESPONSE_CODES:
+ return result.status in VALID_RESPONSE_CODES
+ else:
+ raise RancherException(result.status,
+ 'failed to activate environment')
+
+ def ex_deactivate_environment(self, env_id):
+
+ result = self.connection.request('%s/environments/%s?action=deactivateservices' %
+ (self.baseuri, env_id),
+ method='POST')
+ if result.status in VALID_RESPONSE_CODES:
+ return result.status in VALID_RESPONSE_CODES
+ else:
+ raise RancherException(result.status,
+ 'failed to deactivate environment')
+
+ def build_payload(self, image, start=True, name=None, image_type="docker",
+ blkiodeviceoptions=None, build=None,
+ capadd=None,
+ capdrop=None, command=None,
+ count=None, cpuset=None, cpushares=None,
+ datavolumemounts=None, datavolumes=None,
+ datavolumesfrom=None, description=None, devices=None,
+ dns=None, dnssearch=None, domainname=None,
+ entrypoint=None, environment=None, expose=None,
+ extrahosts=None, healthcheck=None, hostname=None,
+ instancelinks=None, labels=None, logconfig=None,
+ lxcconf=None, memory=None, memoryswap=None,
+ networkcontainerrid=None, networkids=None,
+ networkmode=None, pidmode=None, ports=None,
+ privileged=None, publishallports=None,
+ readonly=None, registrycredentialid=None,
+ requestedhostid=None, restartpolicy=None,
+ securityopt=None,
+ stdinopen=None, tty=None, user=None,
+ volumedriver=None, workingdir=None):
+
+ if command is not None:
+ command = shlex.split(str(command))
+
+ if image.version is not None:
+ imageuuid = image_type + ':' + image.path + ':' + image.version
+ else:
+ imageuuid = image_type + ':' + image.path
+
+ payload = {
+ "blkioDeviceOptions": blkiodeviceoptions,
+ "build": build,
+ "capAdd": capadd,
+ "capDrop": capdrop,
+ "command": command,
+ "count": count,
+ "cpuSet": cpuset,
+ "cpuShares": cpushares,
+ "dataVolumeMounts": datavolumemounts,
+ "dataVolumes": datavolumes,
+ "dataVolumesFrom": datavolumesfrom,
+ "description": description,
+ "devices": devices,
+ "dns": dns,
+ "dnsSearch": dnssearch,
+ "domainName": domainname,
+ "entryPoint": entrypoint,
+ "environment": environment,
+ "expose": expose,
+ "extraHosts": extrahosts,
+ "healthCheck": healthcheck,
+ "hostname": hostname,
+ "imageUuid": imageuuid,
+ "instanceLinks": instancelinks,
+ "labels": labels,
+ "logConfig": logconfig,
+ "lxcConf": lxcconf,
+ "memory": memory,
+ "memorySwap": memoryswap,
+ "name": name,
+ "networkContainerId": networkcontainerrid,
+ "networkIds": networkids,
+ "networkMode": networkmode,
+ "pidMode": pidmode,
+ "ports": ports,
+ "privileged": privileged,
+ "publishAllPorts": publishallports,
+ "readOnly": readonly,
+ "registryCredentialId": registrycredentialid,
+ "requestedHostId": requestedhostid,
+ "restartPolicy": restartpolicy,
+ "securityOpt": securityopt,
+ "startOnCreate": start,
+ "stdinOpen": stdinopen,
+ "tty": tty,
+ "user": user,
+ "volumeDriver": volumedriver,
+ "workingdir": workingdir
+ }
+
+ return payload
+
+ def ex_list_services(self):
+ """
+ List the deployed container images
+ :param image: Filter to containers with a certain image
+ :type image: :class:`libcloud.container.base.ContainerImage`
+ :param all: Show all container (including stopped ones)
+ :type all: ``bool``
+ :rtype: ``list`` of :class:`libcloud.container.base.Container`
+ """
+
+ result = self.connection.request("%s/services" % self.baseuri).object
+ return result['data']
+
+ def ex_deploy_service(self, name, image, environmentid, start=True,
+ assignserviceipaddress=None, service_description=None,
+ externalid=None, metadata=None, retainip=None,
+ scale=None, scalepolicy=None,
+ secondarylaunchconfigs=None, selectorcontainer=None,
+ selectorlink=None,
+ vip=None, datavolumesfromlaunchconfigs=None,
+ disks=None, kind=None, memorymb=None,
+ networklaunchconfig=None, requsetedipaddress=None,
+ userdata=None, vcpu=None, **kwargs):
+ """
+ Deploy a Rancher Service under a stack.
+ :param name: Name of the service.
+ :param image: ContainerImage object of image to utilize.
+ :param environmentid: The environment/stack ID this service is tied to.
+ :param kwargs: The Launchconfig provided as top-level options,
+ similar to deploy_container.
+ :return: The API object returned on proper service creation.
+ """
+
+ service_specific_container_config = {
+ "dataVolumesFromLaunchConfigs": datavolumesfromlaunchconfigs,
+ "disks": disks,
+ "kind": kind,
+ "memoryMb": memorymb,
+ "networkLaunchConfig": networklaunchconfig,
+ "requestedIpAddress": requsetedipaddress,
+ "userdata": userdata,
+ "vcpu": vcpu
+ }
+
+ # Build the de-facto container payload
+ # Note that we don't need to remove "name" since its None by default.
+ launchconfig = self.build_payload(image, start, **kwargs)
+ # Add in extra service configuration
+ launchconfig.update(service_specific_container_config)
+ launchconfig = json.dumps({k: v for k, v in launchconfig.items()
+ if v is not None})
+ service_payload = {
+ "assignServiceIpAddress": assignserviceipaddress,
+ "description": service_description,
+ "environmentId": environmentid,
+ "externalId": externalid,
+ "launchConfig": json.loads(launchconfig),
+ "metadata": metadata,
+ "name": name,
+ "retainIp": retainip,
+ "scale": scale,
+ "scalePolicy": scalepolicy,
+ "secondaryLaunchConfigs": secondarylaunchconfigs,
+ "selectorContainer": selectorcontainer,
+ "selectorLink": selectorlink,
+ "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 to be obtained.
+ :return: The API dict object returned for the new service.
+ """
+ result = self.connection.request("%s/services/%s" %
+ (self.baseuri, service_id)).object
+
+ return result
+
+ def ex_destroy_service(self, service_id):
+ """
+ Delete a service.
+ :param service_id: The service to be destroyed
+ :return: True if the destroy was successful, False otherwise.
+ :rtype: ``bool``
+ """
+ result = self.connection.request('%s/services/%s' % (self.baseuri,
+ service_id), method='DELETE')
+ if result.status in VALID_RESPONSE_CODES:
+ return result.status in VALID_RESPONSE_CODES
+ else:
+ raise RancherException(result.status,
+ 'failed to destroy service')
+
+ def ex_activate_service(self, service_id):
+
+ result = self.connection.request('%s/services/%s?action=activate' %
+ (self.baseuri, service_id),
+ method='POST')
+ if result.status in VALID_RESPONSE_CODES:
+ return result.status in VALID_RESPONSE_CODES
+ else:
+ raise RancherException(result.status,
+ 'failed to activate service')
+
+ def ex_deactivate_service(self, service_id):
+
+ result = self.connection.request('%s/services/%s?action=deactivate' %
+ (self.baseuri, service_id),
+ method='POST')
+ if result.status in VALID_RESPONSE_CODES:
+ return result.status in VALID_RESPONSE_CODES
+ else:
+ raise RancherException(result.status,
+ 'failed to deactivate service')
+
+ def list_containers(self, image=None, all=True):
+ """
+ List the deployed container images
+ :param image: Filter to containers with a certain image
+ :type image: :class:`libcloud.container.base.ContainerImage`
+ :param all: Show all container (including stopped ones)
+ :type all: ``bool``
+ :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,
+ **kwargs):
+ """
+ Deploy an installed container image
+ For details on the additional parameters see : http://bit.ly/1PjMVKV
+ :param name: The name of the new container
+ :type name: ``str``
+ :param image: The container image to deploy
+ :type image: :class:`libcloud.container.base.ContainerImage`
+ :param parameters: Container Image parameters
+ :type parameters: ``str``
+ :param start: Start the container on deployment
+ :type start: ``bool``
+ :rtype: :class:`Container`
+ """
+ payload = self.build_payload(name, image, start, **kwargs)
+ data = json.dumps({k: v for k, v in payload.items() if v is not None})
+
+ result = self.connection.request('%s/containers' % self.baseuri,
+ data=data, method='POST').object
+
+ return self._to_container(result)
+
+ def get_container(self, id):
+ """
+ Get a container by ID
+ :param id: The ID of the container to get
+ :type id: ``str``
+ :rtype: :class:`libcloud.container.base.Container`
+ """
+ result = self.connection.request("%s/containers/%s" %
+ (self.baseuri, id)).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('%s/containers/%s?action=stop' %
+ (self.baseuri, container.id),
+ method='POST')
+ if result.status in VALID_RESPONSE_CODES:
+ return self.get_container(container.id)
+ else:
+ raise RancherException(result.status,
+ 'failed to stop container')
+
+ 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')
+ if result.status in VALID_RESPONSE_CODES:
+ return self.get_container(container.id)
+ else:
+ raise RancherException(result.status,
+ 'failed to destroy container')
+
+ 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!!
+
+ For a imageuuid:
+ docker:<hostname>:<port>/<namespace>/<imagename>:<version>
+
+ The following applies:
+ id = <imagename>
+ name = <imagename>
+ path = <hostname>:<port>/<namespace>/<imagename>
+ version = <version>
+
+ :param imageUuid: A valid Rancher image string
+ i.e. `docker:rlister/hastebin:8.0`
+ :return: Proper ContainerImage object.
+ """
+ # 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 _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']
+ 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 'removed' or 'purged' in rancher_state:
+ # A Removed container is purged after x amt of time.
+ # Both of these render the container dead (can't be started later)
+ state = ContainerState.TERMINATED
+ elif rancher_state.endswith('ing'):
+ # Best we can do for current actions
+ state = ContainerState.PENDING
+ else:
+ state = ContainerState.UNKNOWN
+
+ # Includes the most used items atm. Eventually, everything ;)
+ extra = {
+ "state": rancher_state,
+ "command": data['command'],
+ "created": data['created'],
+ "dataVolumes": data['dataVolumes'],
+ "dns": data['dns'],
+ "dnsSearch": data['dnsSearch'],
+ "domainName": data['domainName'],
+ "entryPoint": data['entryPoint'],
+ "environment": data['environment'],
+ "expose": data['expose'],
+ "healthState": data['healthState'],
+ "hostId": data['hostId'],
+ "hostname": data['hostname'],
+ "labels": data['labels'],
+ "networkMode": data['networkMode'],
+ "ports": data['ports'],
+ "primaryIpAddress": data['primaryIpAddress'],
+ "privileged": data['privileged'],
+ "restartPolicy": data['restartPolicy'],
+ "stdinOpen": data['stdinOpen'],
+ "tty": data['tty'],
+ "uuid": data['uuid'],
+ "workingDir": data['workingDir']
+ }
+
+ 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)
+
+
+def ts_to_str(timestamp):
+ """
+ Return a timestamp as a nicely formated datetime string.
+ """
+ date = datetime.datetime.fromtimestamp(timestamp)
+ date_string = date.strftime("%d/%m/%Y %H:%M %Z")
+ return date_string
http://git-wip-us.apache.org/repos/asf/libcloud/blob/8495ea71/libcloud/container/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/container/providers.py b/libcloud/container/providers.py
index 16ab58c..a823382 100644
--- a/libcloud/container/providers.py
+++ b/libcloud/container/providers.py
@@ -28,6 +28,8 @@ DRIVERS = {
('libcloud.container.drivers.ecs', 'ElasticContainerDriver'),
Provider.KUBERNETES:
('libcloud.container.drivers.kubernetes', 'KubernetesContainerDriver'),
+ Provider.RANCHER:
+ ('libcloud.container.drivers.rancher', 'RancherContainerDriver'),
}
http://git-wip-us.apache.org/repos/asf/libcloud/blob/8495ea71/libcloud/container/types.py
----------------------------------------------------------------------
diff --git a/libcloud/container/types.py b/libcloud/container/types.py
index bddca7f..b89fdfd 100644
--- a/libcloud/container/types.py
+++ b/libcloud/container/types.py
@@ -51,6 +51,7 @@ class Provider(object):
ECS = 'ecs'
JOYENT = 'joyent'
KUBERNETES = 'kubernetes'
+ RANCHER = 'rancher'
class ContainerState(Type):