diff --git a/.gitignore b/.gitignore index ba74660..7d0d1f4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ __pycache__/ *.py[cod] -# C extensions -*.so # Distribution / packaging .Python @@ -22,6 +20,7 @@ var/ *.egg-info/ .installed.cfg *.egg +venv/ # PyInstaller # Usually these files are written by a python script from a template @@ -55,3 +54,8 @@ docs/_build/ # PyBuilder target/ + +# IDE +.idea/ + +venv/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0fd86d8 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +test:prepare + venv/bin/python setup.py test + +clean: + rm -rf venv build dist *.egg-info + find . -name '*.pyc' -delete + +prepare:clean + set -ex + virtualenv venv + venv/bin/pip install mock + venv/bin/pip install -r requirements.txt + venv/bin/python setup.py install diff --git a/README.md b/README.md index 846cdca..5a82ce2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,80 @@ # python-docker-cloud + Python library for Docker Cloud + +## Installation + +In order to install the Docker Cloud Python library, you can use pip install: + + pip install python-dockercloud + +It will install a Python module called dockercloud which you can use to interface with the API. + +## Authorization + +The authentication can be configured in the following ways: + +* Manually set it in your Python initialization code: + + import dockercloud + dockercloud.user = "username" + dockercloud.apikey = "apikey" + +* Login with docker cli, and the library will read the configfile automatically: + + $ docker login + +* Set the environment variables DOCKERCLOUD_USER and DOCKERCLOUD_APIKEY: + + export DOCKERCLOUD_USER=username + export DOCKERCLOUD_APIKEY=apikey + +## Errors + +Errors in the HTTP API will be returned with status codes in the 4xx and 5xx ranges. + +The Python library will detect this status codes and raise ApiError exceptions with the error message, which should be handled by the calling application accordingly. + + +## Quick examples + +### Services + + >>> import dockercloud + >>> dockercloud.Service.list() + [, ] + >>> service = dockercloud.Service.fetch("fee900c6-97da-46b3-a21c-e2b50ed07015") + + >>> service.name + "my-python-app" + >>> service = dockercloud.Service.create(image="dockercloud/hello-world", name="my-new- app", target_num_containers=2) + >>> service.save() + True + >>> service.target_num_containers = 3 + >>> service.save() + True + >>> service.stop() + True + >>> service.start() + True + >>> service.delete() + True + +### Containers + + >>> import dockercloud + >>> dockercloud.Container.list() + [, ] + >>> container = dockercloud.Container.fetch("7d6696b7-fbaf-471d-8e6b-ce7052586c24") + + >>> container.public_dns = "my-web-app.example.com" + >>> container.save() + True + >>> container.stop() + True + >>> container.start() + True + >>> container.logs() + "2014-03-24 23:58:08,973 CRIT Supervisor running as root (no user in config file) [...]" + >>> container.delete() + True diff --git a/dockercloud/__init__.py b/dockercloud/__init__.py new file mode 100644 index 0000000..70092b3 --- /dev/null +++ b/dockercloud/__init__.py @@ -0,0 +1,49 @@ +import base64 +import logging +import os + +import requests +from future.standard_library import install_aliases + +install_aliases() + +from dockercloud.api import auth +from dockercloud.api.service import Service +from dockercloud.api.container import Container +from dockercloud.api.repository import Repository +from dockercloud.api.node import Node +from dockercloud.api.action import Action +from dockercloud.api.nodecluster import NodeCluster +from dockercloud.api.nodetype import NodeType +from dockercloud.api.nodeprovider import Provider +from dockercloud.api.noderegion import Region +from dockercloud.api.tag import Tag +from dockercloud.api.trigger import Trigger +from dockercloud.api.stack import Stack +from dockercloud.api.exceptions import ApiError, AuthError, ObjectNotFound, NonUniqueIdentifier +from dockercloud.api.utils import Utils +from dockercloud.api.events import Events +from dockercloud.api.nodeaz import AZ + +__version__ = '1.0.0' + +dockercloud_auth = os.environ.get('DOCKERCLOUD_AUTH') +basic_auth = auth.load_from_file("~/.docker/config.json") + +if os.environ.get('DOCKERCLOUD_USER') and os.environ.get('DOCKERCLOUD_PASS'): + basic_auth = base64.b64encode("%s:%s" % (os.environ.get('DOCKERCLOUD_USER'), os.environ.get('DOCKERCLOUD_PASS'))) +if os.environ.get('DOCKERCLOUD_USER') and os.environ.get('DOCKERCLOUD_APIKEY'): + basic_auth = base64.b64encode("%s:%s" % (os.environ.get('DOCKERCLOUD_USER'), os.environ.get('DOCKERCLOUD_APIKEY'))) + +rest_host = os.environ.get("DOCKERCLOUD_REST_HOST") or 'https://cloud.docker.com/' +stream_host = os.environ.get("DOCKERCLOUD_STREAM_HOST") or 'wss://ws.cloud.docker.com/' + +user_agent = None + +logging.basicConfig() +logger = logging.getLogger("python-dockercloud") + +try: + requests.packages.urllib3.disable_warnings() +except: + pass diff --git a/dockercloud/api/__init__.py b/dockercloud/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dockercloud/api/action.py b/dockercloud/api/action.py new file mode 100644 index 0000000..71a3ed8 --- /dev/null +++ b/dockercloud/api/action.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import + +from .base import Immutable, StreamingLog + + +class Action(Immutable): + subsystem = 'audit' + endpoint = "/action" + + @classmethod + def _pk_key(cls): + return 'uuid' + + def logs(self, tail, follow, log_handler=StreamingLog.default_log_handler): + logs = StreamingLog(self.subsystem, self.endpoint, self.pk, tail, follow) + logs.on_message(log_handler) + logs.run_forever() + + def cancel(self): + return self._perform_action("cancel") + + def retry(self): + return self._perform_action("retry") diff --git a/dockercloud/api/auth.py b/dockercloud/api/auth.py new file mode 100644 index 0000000..5c44e9c --- /dev/null +++ b/dockercloud/api/auth.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import + +import base64 +import json +import os + +from requests.auth import HTTPBasicAuth + +import dockercloud +from .http import send_request + + +def authenticate(username, password): + verify_credential(username, password) + dockercloud.basic_auth = base64.b64encode("%s:%s" % (username, password)) + + +def verify_credential(username, password): + auth = HTTPBasicAuth(username, password) + send_request("GET", "/auth", auth=auth) + + +def is_authenticated(): + try: + dockercloud.basic_auth = base64.b64encode("%s:%s" % (dockercloud.user, dockercloud.password)) + except: + pass + + try: + dockercloud.basic_auth = base64.b64encode("%s:%s" % (dockercloud.user, dockercloud.apikey)) + except: + pass + + return dockercloud.dockercloud_auth or dockercloud.basic_auth + + +def logout(): + dockercloud.dockercloud_auth = None + dockercloud.basic_auth = None + + +def load_from_file(f="~/.docker/config.json"): + try: + with open(os.path.expanduser(f)) as config_file: + data = json.load(config_file) + + return data.get("auths", {}).get("https://index.docker.io/v1/", {}).get("auth", None) + except Exception: + return None + + +def get_auth_header(): + try: + dockercloud.basic_auth = base64.b64encode("%s:%s" % (dockercloud.user, dockercloud.password)) + except: + pass + + try: + dockercloud.basic_auth = base64.b64encode("%s:%s" % (dockercloud.user, dockercloud.apikey)) + except: + pass + + if dockercloud.dockercloud_auth: + return {'Authorization': dockercloud.dockercloud_auth} + if dockercloud.basic_auth: + return {'Authorization': 'Basic %s' % dockercloud.basic_auth} + return {} diff --git a/dockercloud/api/base.py b/dockercloud/api/base.py new file mode 100644 index 0000000..b375ba4 --- /dev/null +++ b/dockercloud/api/base.py @@ -0,0 +1,351 @@ +from __future__ import absolute_import, print_function + +import json as json_parser +import logging +import urllib + +import websocket + +import dockercloud +from .exceptions import ApiError, AuthError +from .http import send_request + +logger = logging.getLogger("python-dockercloud") + + +class BasicObject(object): + _api_version = 'v1' + + def __init__(self, **kwargs): + pass + + +class Restful(BasicObject): + _detail_uri = None + + def __init__(self, **kwargs): + """Simply reflect all the values in kwargs""" + for k, v in list(kwargs.items()): + setattr(self, k, v) + + def __addchanges__(self, name): + changed_attrs = self.__getchanges__() + if not name in changed_attrs: + changed_attrs.append(name) + self.__setchanges__(changed_attrs) + + def __setattr__(self, name, value): + """Keeps track of what attributes have been set""" + current_value = getattr(self, name, None) + if value != current_value: + self.__addchanges__(name) + super(Restful, self).__setattr__(name, value) + + def __getchanges__(self): + """Internal. Convenience method to get the changed attrs list""" + return getattr(self, '__changedattrs__', []) + + def __setchanges__(self, val): + """Internal. Convenience method to set the changed attrs list""" + # Use the super implementation to prevent infinite recursion + super(Restful, self).__setattr__('__changedattrs__', val) + + def _loaddict(self, dict): + """Internal. Sets the model attributes to the dictionary values passed""" + endpoint = getattr(self, 'endpoint', None) + subsystem = getattr(self, 'subsystem', None) + assert endpoint, "Endpoint not specified for %s" % self.__class__.__name__ + assert subsystem, "Subsystem not specified for %s" % self.__class__.__name__ + for k, v in list(dict.items()): + setattr(self, k, v) + self._detail_uri = "/".join(["api", subsystem, self._api_version, endpoint.strip("/"), self.pk]) + self.__setchanges__([]) + + @property + def pk(self): + """Returns the primary key for the object. + + :returns: str -- the primary key for the object + """ + return getattr(self, self._pk_key(), None) + + @classmethod + def _pk_key(cls): + """Internal. Returns the attribute name that acts as primary key of the model. Can be overridden by subclasses. + + :returns: str -- the name of the primary key attribute for the model + """ + return 'uuid' + + @property + def is_dirty(self): + """Returns whether or not the object has unsaved changes + + :returns: bool -- whether or not the object has unsaved changes + """ + return len(self.__getchanges__()) > 0 + + def _perform_action(self, action, params=None, data={}): + """Internal. Performs the specified action on the object remotely""" + success = False + if not self._detail_uri: + raise ApiError("You must save the object before performing this operation") + path = "/".join([self._detail_uri.rstrip("/"), action.lstrip("/")]) + json = send_request("POST", path, params=params, data=data) + if json: + self._loaddict(json) + success = True + return success + + def _expand_attribute(self, attribute): + """Internal. Expands the given attribute from remote information""" + if not self._detail_uri: + raise ApiError("You must save the object before performing this operation") + path = "/".join([self._detail_uri, attribute]) + json = send_request("GET", path) + if json: + return json[attribute] + return None + + def get_all_attributes(self): + """Returns a dict with all object attributes + + :returns: dict -- all object attributes as a dict + """ + attributes = {} + for attr in [attr for attr in vars(self) if not attr.startswith('_')]: + attributes[attr] = getattr(self, attr, None) + return attributes + + +class Immutable(Restful): + @classmethod + def fetch(cls, pk): + instance = None + endpoint = getattr(cls, 'endpoint', None) + subsystem = getattr(cls, 'subsystem', None) + assert endpoint, "Endpoint not specified for %s" % cls.__name__ + assert subsystem, "Subsystem not specified for %s" % cls.__name__ + detail_uri = "/".join(["api", subsystem, cls._api_version, endpoint.strip("/"), pk]) + json = send_request('GET', detail_uri) + if json: + instance = cls() + instance._loaddict(json) + return instance + + @classmethod + def list(cls, limit=None, **kwargs): + restful = [] + endpoint = getattr(cls, 'endpoint', None) + subsystem = getattr(cls, 'subsystem', None) + assert endpoint, "Endpoint not specified for %s" % cls.__name__ + assert subsystem, "Subsystem not specified for %s" % cls.__name__ + + detail_uri = "/".join(["api", subsystem, cls._api_version, endpoint.strip("/")]) + objects = [] + while True: + if limit and len(objects) >= limit: + break + json = send_request('GET', detail_uri, params=kwargs) + objs = json.get('objects', []) + meta = json.get('meta', {}) + next_url = meta.get('next', '') + offset = meta.get('offset', 0) + api_limit = meta.get('limit', 0) + objects.extend(objs) + if next_url: + kwargs['offset'] = offset + api_limit + kwargs['limit'] = api_limit + else: + break + if limit: + objects = objects[:limit] + for obj in objects: + instance = cls() + instance._loaddict(obj) + restful.append(instance) + return restful + + def refresh(self, force=False): + success = False + if self.is_dirty and not force: + # We have local non-committed changes - rejecting the refresh + success = False + elif not self._detail_uri: + raise ApiError("You must save the object before performing this operation") + else: + json = send_request("GET", self._detail_uri) + if json: + self._loaddict(json) + success = True + return success + + +class Mutable(Immutable): + @classmethod + def create(cls, **kwargs): + """Returns a new instance of the model (without saving it) with the attributes specified in ``kwargs`` + + :returns: RESTModel -- a new local instance of the model + """ + return cls(**kwargs) + + def delete(self): + if not self._detail_uri: + raise ApiError("You must save the object before performing this operation") + action = "DELETE" + url = self._detail_uri + json = send_request(action, url) + if json: + self._loaddict(json) + else: + # Object deleted successfully and nothing came back - deleting PK reference. + self._detail_uri = None + # setattr(self, self._pk_key(), None) -- doesn't work + self.__setchanges__([]) + return True + + def save(self): + success = False + if not self.is_dirty: + # No changes + success = True + else: + cls = self.__class__ + endpoint = getattr(cls, 'endpoint', None) + subsystem = getattr(cls, 'subsystem', None) + assert endpoint, "Endpoint not specified for %s" % self.__class__.__name__ + assert subsystem, "Subsystem not specified for %s" % self.__class__.__name__ + # Figure out whether we should do a create or update + if not self._detail_uri: + action = "POST" + path = "/".join(["api", subsystem, self._api_version, endpoint.lstrip("/")]) + else: + action = "PATCH" + path = self._detail_uri + # Construct the necessary params + params = {} + for attr in self.__getchanges__(): + value = getattr(self, attr, None) + params[attr] = value + # Construct the json body + payload = None + if params: + payload = json_parser.dumps(params) + if not payload: + payload = json_parser.dumps({}) + # Make the request + success = False + json = send_request(action, path, data=payload) + if json: + self._loaddict(json) + success = True + return success + + +class Taggable(BasicObject): + pass + + +class Triggerable(BasicObject): + pass + + +class StreamingAPI(BasicObject): + def __init__(self, url): + self._ws_init(url) + + def _ws_init(self, url): + self.url = url + + user_agent = 'python-dockercloud/%s' % dockercloud.__version__ + if dockercloud.user_agent: + user_agent = "%s %s" % (dockercloud.user_agent, user_agent) + header = {'User-Agent': user_agent} + header.update(dockercloud.auth.get_auth_header()) + self.header = [": ".join([key, value]) for key, value in header.items()] + logger.info("websocket: %s %s" % (self.url, self.header)) + self.open_handler = None + self.message_handler = None + self.error_handler = None + self.close_handler = None + self.auth_error = False + + def _on_open(self, ws): + if self.open_handler: + self.open_handler() + + def _on_message(self, ws, message): + if self.message_handler: + self.message_handler(message) + + def _on_error(self, ws, error): + if self.error_handler: + self.error_handler(error) + + def _on_close(self, ws): + if self.close_handler: + self.close_handler() + + def on_open(self, handler): + self.open_handler = handler + + def on_message(self, handler): + self.message_handler = handler + + def on_error(self, handler): + self.error_handler = handler + + def on_close(self, handler): + self.close_handler = handler + + def run_forever(self, *args, **kwargs): + while True: + if getattr(self, "auth_error", False): + raise AuthError("Not authorized") + ws = websocket.WebSocketApp(self.url, header=self.header, + on_open=self._on_open, + on_message=self._on_message, + on_error=self._on_error, + on_close=self._on_close) + ws.run_forever(ping_interval=5, ping_timeout=5, *args, **kwargs) + + +class StreamingLog(StreamingAPI): + def __init__(self, subsystem, resource, uuid, tail, follow): + endpoint = "%s/%s/logs/?follow=%s" % (resource, uuid, str(follow).lower()) + if tail: + endpoint = "%s&tail=%d" % (endpoint, tail) + url = "/".join([dockercloud.stream_host.rstrip("/"), "api", subsystem, self._api_version, endpoint.lstrip("/")]) + super(self.__class__, self).__init__(url) + + @staticmethod + def default_log_handler(message): + print(message) + + def run_forever(self, *args, **kwargs): + ws = websocket.WebSocketApp(self.url, header=self.header, + on_open=self._on_open, + on_message=self._on_message, + on_error=self._on_error, + on_close=self._on_close) + ws.run_forever(ping_interval=5, ping_timeout=5, *args, **kwargs) + + +class Exec(StreamingAPI): + def __init__(self, uuid, cmd='sh'): + endpoint = "container/%s/exec/?command=%s" % (uuid, urllib.quote_plus(cmd)) + url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "app", self._api_version, endpoint.lstrip("/")]) + super(self.__class__, self).__init__(url) + + @staticmethod + def default_message_handler(message): + print(message) + + def run_forever(self, *args, **kwargs): + ws = websocket.WebSocketApp(self.url, header=self.header, + on_open=self._on_open, + on_message=self._on_message, + on_error=self._on_error, + on_close=self._on_close) + ws.run_forever(ping_interval=5, ping_timeout=5, *args, **kwargs) diff --git a/dockercloud/api/container.py b/dockercloud/api/container.py new file mode 100644 index 0000000..2e3cf05 --- /dev/null +++ b/dockercloud/api/container.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import + +from .base import Mutable, StreamingLog, Exec + + +class Container(Mutable): + subsystem = "app" + endpoint = "/container" + + def save(self): + raise AttributeError("'save' is not supported in 'Container' object. " + "Please use the related 'Service' object instead.") + + def start(self): + return self._perform_action("start") + + def stop(self): + return self._perform_action("stop") + + def redeploy(self, reuse_volumes=True): + params = {'reuse_volumes': reuse_volumes} + return self._perform_action("redeploy", params=params) + + def logs(self, tail, follow, log_handler=StreamingLog.default_log_handler): + logs = StreamingLog(self.subsystem, self.endpoint, self.pk, tail, follow) + logs.on_message(log_handler) + logs.run_forever() + + def execute(self, cmd, handler=Exec.default_message_handler): + if hasattr(self, "uuid"): + exec_obj = Exec(self.uuid, cmd) + exec_obj.on_message(handler) + exec_obj.run_forever() diff --git a/dockercloud/api/events.py b/dockercloud/api/events.py new file mode 100644 index 0000000..3f34870 --- /dev/null +++ b/dockercloud/api/events.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import + +import json + +import websocket + +import dockercloud +from .base import StreamingAPI +from .exceptions import AuthError + + +class Events(StreamingAPI): + def __init__(self): + endpoint = "events" + url = "/".join([dockercloud.stream_host.rstrip("/"), "api", "audit", self._api_version, endpoint.lstrip("/")]) + super(self.__class__, self).__init__(url) + + def _on_message(self, ws, message): + try: + event = json.loads(message) + except ValueError: + return + + if event.get("type") == "error" and event.get("data", {}).get("errorMessage") == "UNAUTHORIZED": + self.auth_error = True + raise AuthError("Not authorized") + if event.get("type") == "auth": + return + + if self.message_handler: + self.message_handler(event) + + def run_forever(self, *args, **kwargs): + while True: + if self.auth_error: + raise AuthError("Not authorized") + ws = websocket.WebSocketApp(self.url, header=self.header, + on_open=self._on_open, + on_message=self._on_message, + on_error=self._on_error, + on_close=self._on_close) + ws.run_forever(ping_interval=5, ping_timeout=5, *args, **kwargs) diff --git a/dockercloud/api/exceptions.py b/dockercloud/api/exceptions.py new file mode 100644 index 0000000..127d795 --- /dev/null +++ b/dockercloud/api/exceptions.py @@ -0,0 +1,16 @@ +class ApiError(Exception): + """An error status code was returned when querying the HTTP API""" + pass + + +class AuthError(ApiError): + """An 401 Unauthorized status code was returned when querying the API""" + pass + + +class NonUniqueIdentifier(ApiError): + pass + + +class ObjectNotFound(ApiError): + pass diff --git a/dockercloud/api/http.py b/dockercloud/api/http.py new file mode 100644 index 0000000..46fd3f0 --- /dev/null +++ b/dockercloud/api/http.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import + +import logging + +from requests import Request, Session +from requests import utils +from urllib.parse import urljoin + +import dockercloud +from .exceptions import ApiError, AuthError + +logger = logging.getLogger("python-dockercloud") + + +def send_request(method, path, inject_header=True, **kwargs): + json = None + url = urljoin(dockercloud.rest_host.rstrip("/"), path.strip("/").encode("ascii", "ignore")) + if not url.endswith("/"): + url = "%s/" % url + user_agent = 'python-dockercloud/%s' % dockercloud.__version__ + if dockercloud.user_agent: + user_agent = "%s %s" % (dockercloud.user_agent, user_agent) + + # construct headers + headers = {'Content-Type': 'application/json', 'User-Agent': user_agent} + headers.update(dockercloud.auth.get_auth_header()) + logger.info("Request: %s, %s, %s, %s" % (method, url, headers, kwargs)) + + # construct request + s = Session() + req = Request(method, url, headers=headers, **kwargs) + + # get environment proxies + env_proxies = utils.get_environ_proxies(url) or {} + kw_args = {'proxies': env_proxies} + + # make the request + response = s.send(req.prepare(), **kw_args) + status_code = getattr(response, 'status_code', None) + logger.info("Response: Status %s, %s, %s" % (str(status_code), response.headers, response.text)) + + # handle the response + if not status_code: + # Most likely network trouble + raise ApiError("No Response (%s %s)" % (method, url)) + elif 200 <= status_code <= 299: + # Success + if status_code != 204: + # Try to parse the response. + try: + json = response.json() + if response.headers and inject_header: + json["dockercloud_action_uri"] = response.headers.get("X-DockerCloud-Action-URI", "") + except TypeError: + raise ApiError("JSON Parse Error (%s %s). Response: %s" % (method, url, response.text)) + else: + json = None + else: + # Server returned an error + if status_code == 401: + raise AuthError("Not authorized") + else: + raise ApiError("Status %s (%s %s). Response: %s" % (str(status_code), method, url, response.text)) + return json diff --git a/dockercloud/api/node.py b/dockercloud/api/node.py new file mode 100644 index 0000000..31f80ff --- /dev/null +++ b/dockercloud/api/node.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import + +from .base import Mutable, Taggable + + +class Node(Mutable, Taggable): + subsystem = "infra" + endpoint = "/node" + + def save(self): + if not self._detail_uri: + raise AttributeError("Adding a new node is not supported via 'save' method") + super(Node, self).save() + + def deploy(self, tag=None): + return self._perform_action("deploy") + + def upgrade_docker(self): + return self._perform_action("docker-upgrade") diff --git a/dockercloud/api/nodeaz.py b/dockercloud/api/nodeaz.py new file mode 100644 index 0000000..abc4d5f --- /dev/null +++ b/dockercloud/api/nodeaz.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from .base import Immutable + + +class AZ(Immutable): + subsystem = "infra" + endpoint = "/az" + + @classmethod + def _pk_key(cls): + return 'name' diff --git a/dockercloud/api/nodecluster.py b/dockercloud/api/nodecluster.py new file mode 100644 index 0000000..2443b40 --- /dev/null +++ b/dockercloud/api/nodecluster.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import + +from .base import Mutable, Taggable +from .noderegion import Region +from .nodetype import NodeType + + +class NodeCluster(Mutable, Taggable): + subsystem = "infra" + endpoint = "/nodecluster" + + def deploy(self, tag=None): + return self._perform_action("deploy") + + @classmethod + def create(cls, **kwargs): + for key, value in kwargs.items(): + if key == "node_type" and isinstance(value, NodeType): + kwargs[key] = getattr(value, "resource_uri", "") + if key == "region" and isinstance(value, Region): + kwargs[key] = getattr(value, "resource_uri", "") + return cls(**kwargs) diff --git a/dockercloud/api/nodeprovider.py b/dockercloud/api/nodeprovider.py new file mode 100644 index 0000000..3d31f06 --- /dev/null +++ b/dockercloud/api/nodeprovider.py @@ -0,0 +1,18 @@ +from __future__ import absolute_import + +from .base import Immutable + + +class Provider(Immutable): + subsystem = "infra" + endpoint = "/provider" + + @classmethod + def _pk_key(cls): + return 'name' + + def delete(self): + raise AttributeError("'delete' is not supported in 'Provider'") + + def save(self): + raise AttributeError("'save' is not supported in 'Provider'") diff --git a/dockercloud/api/noderegion.py b/dockercloud/api/noderegion.py new file mode 100644 index 0000000..4d10bed --- /dev/null +++ b/dockercloud/api/noderegion.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from .base import Immutable + + +class Region(Immutable): + subsystem = "infra" + endpoint = "/region" + + @classmethod + def _pk_key(cls): + return 'name' diff --git a/dockercloud/api/nodetype.py b/dockercloud/api/nodetype.py new file mode 100644 index 0000000..5274aaf --- /dev/null +++ b/dockercloud/api/nodetype.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from .base import Immutable + + +class NodeType(Immutable): + subsystem = "infra" + endpoint = "/nodetype" + + @classmethod + def _pk_key(cls): + return 'name' diff --git a/dockercloud/api/repository.py b/dockercloud/api/repository.py new file mode 100644 index 0000000..7dacaab --- /dev/null +++ b/dockercloud/api/repository.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from .base import Mutable, Taggable + + +class Repository(Mutable, Taggable): + subsystem = "repo" + endpoint = "/repository" + + @classmethod + def _pk_key(cls): + return 'name' diff --git a/dockercloud/api/service.py b/dockercloud/api/service.py new file mode 100644 index 0000000..6d67b05 --- /dev/null +++ b/dockercloud/api/service.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import + +from .base import Mutable, Taggable, Triggerable, StreamingLog + + +class Service(Mutable, Taggable, Triggerable): + subsystem = "app" + endpoint = "/service" + + def start(self): + return self._perform_action("start") + + def stop(self): + return self._perform_action("stop") + + def redeploy(self, reuse_volumes=True): + params = {'reuse_volumes': reuse_volumes} + return self._perform_action("redeploy", params=params) + + def scale(self): + return self._perform_action("scale") + + def logs(self, tail, follow, log_handler=StreamingLog.default_log_handler): + logs = StreamingLog(self.subsystem, self.endpoint, self.pk, tail, follow) + logs.on_message(log_handler) + logs.run_forever() diff --git a/dockercloud/api/stack.py b/dockercloud/api/stack.py new file mode 100644 index 0000000..331b690 --- /dev/null +++ b/dockercloud/api/stack.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import + +from .base import Mutable +from .exceptions import ApiError +from .http import send_request + + +class Stack(Mutable): + subsystem = "app" + endpoint = "/stack" + + def start(self): + return self._perform_action("start") + + def stop(self): + return self._perform_action("stop") + + def redeploy(self, reuse_volumes=True): + params = {'reuse_volumes': reuse_volumes} + return self._perform_action("redeploy", params=params) + + def export(self): + if not self._detail_uri: + raise ApiError("You must save the object before performing this operation") + url = "/".join([self._detail_uri, "export"]) + return send_request("GET", url, inject_header=False) diff --git a/dockercloud/api/tag.py b/dockercloud/api/tag.py new file mode 100644 index 0000000..10d5354 --- /dev/null +++ b/dockercloud/api/tag.py @@ -0,0 +1,78 @@ +from __future__ import absolute_import + +from .base import Taggable, BasicObject +from .exceptions import ApiError + + +class Tag(BasicObject): + def __init__(self): + self.tags = [] + + def add(self, tagname): + if isinstance(tagname, list): + for t in tagname: + self.taggable.tags.append({"name": t}) + else: + self.taggable.tags.append({"name": tagname}) + + self.taggable.__addchanges__('tags') + + @classmethod + def create(cls, **kwargs): + return cls(**kwargs) + + def remove(self, tagname): + if not self.taggable: + raise ApiError("You must initialize the tag object before performing this operation") + + _tags = [] + tagnames = [] + if isinstance(tagname, list): + for n in tagname: + tagnames.append(n) + else: + tagnames.append(tagname) + + for t in self.taggable.tags: + for tagname in tagnames: + if t.get("name", "") == tagname: + _tags.append(t) + + if _tags: + for _tag in _tags: + self.taggable.tags.remove(_tag) + self.taggable.__addchanges__('tags') + + def delete(self, tagname): + if not self.taggable: + raise ApiError("You must initialize the tag object before performing this operation") + + if self.taggable.is_dirty: + raise ApiError("You must save the tab object before performing this operation") + + self.remove(tagname) + return self.save() + + @classmethod + def fetch(cls, taggable): + if not isinstance(taggable, Taggable): + raise ApiError("The object does not support tag") + if not taggable._detail_uri: + raise ApiError("You must save the taggable object before performing this operation") + + tag = cls() + tag.taggable = taggable + + return tag + + def list(self, **kwargs): + if not self.taggable: + raise ApiError("You must initialize the tag object before performing this operation") + + return self.taggable.tags + + def save(self): + if not self.taggable: + raise ApiError("You must initialize the tag object before performing this operation") + + return self.taggable.save() diff --git a/dockercloud/api/trigger.py b/dockercloud/api/trigger.py new file mode 100644 index 0000000..1c5aa73 --- /dev/null +++ b/dockercloud/api/trigger.py @@ -0,0 +1,102 @@ +import json as json_parser + +from .base import Triggerable, BasicObject +from .exceptions import ApiError +from .http import send_request + + +class Trigger(BasicObject): + def __init__(self): + self.trigger = None + + def add(self, name=None, operation=None): + + if self.trigger is not None: + raise ApiError("You must save the object before performing this operation") + + trigger = {} + if name: + trigger['name'] = name + if operation: + trigger['operation'] = operation + self.trigger = trigger + + @classmethod + def create(cls, **kwargs): + """Returns a new instance of the model (without saving it) with the attributes specified in ``kwargs`` + + :returns: trigger -- a new local instance of the Trigger + """ + return cls(**kwargs) + + def delete(self, uuid): + if not self.endpoint: + raise ApiError("You must initialize the Trigger object before performing this operation") + + action = "DELETE" + url = "/".join([self.endpoint, uuid]) + send_request(action, url) + return True + + @classmethod + def fetch(cls, triggerable): + if not isinstance(triggerable, Triggerable): + raise ApiError("The object does not support trigger") + + if not triggerable._detail_uri: + raise ApiError("You must save the triggerable object before performing this operation") + + trigger = cls() + trigger.endpoint = "/".join([triggerable._detail_uri, "trigger"]) + handlers = [] + for t in trigger.list(): + triggername = t.get("name", "") + if triggername: + handlers.append({"name": triggername}) + return trigger + + def list(self, **kwargs): + if not self.endpoint: + raise ApiError("You must initialize the Trigger object before performing this operation") + + objects = [] + while True: + json = send_request('GET', self.endpoint, params=kwargs) + objs = json.get('objects', []) + meta = json.get('meta', {}) + next_url = meta.get('next', '') + offset = meta.get('offset', 0) + limit = meta.get('limit', 0) + objects.extend(objs) + if next_url: + kwargs['offset'] = offset + limit + kwargs['limit'] = limit + else: + break + + return objects + + def save(self): + if not self.endpoint: + raise ApiError("You must initialize the Trigger object before performing this operation") + + if self.trigger is None: + return True + + json = send_request("POST", self.endpoint, data=json_parser.dumps(self.trigger)) + if json: + self.clear() + self.clear() + return True + + def call(self, uuid): + if not self.endpoint: + raise ApiError("You must initialize the Trigger object before performing this operation") + + json = send_request("POST", "/".join([self.endpoint, uuid + "/call"])) + if json: + return True + return False + + def clear(self): + self.trigger = None diff --git a/dockercloud/api/utils.py b/dockercloud/api/utils.py new file mode 100644 index 0000000..8719aa2 --- /dev/null +++ b/dockercloud/api/utils.py @@ -0,0 +1,193 @@ +from __future__ import absolute_import + +import re + +from .action import Action +from .container import Container +from .exceptions import ApiError, ObjectNotFound, NonUniqueIdentifier +from .node import Node +from .nodecluster import NodeCluster +from .service import Service +from .stack import Stack + + +def is_uuid4(identifier): + uuid4_regexp = re.compile('^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}', re.I) + match = uuid4_regexp.match(identifier) + return bool(match) + + +class Utils: + @staticmethod + def fetch_by_resource_uri(uri): + if not isinstance(uri, basestring): + raise ApiError("Uri format is invalid") + terms = uri.strip("/").split("/") + if len(terms) < 2: + raise ApiError("Uri format is invalid") + + id = terms[-1] + resource_type = terms[-2] + + if resource_type.lower() == "container": + return Container.fetch(id) + elif resource_type.lower() == "service": + return Service.fetch(id) + elif resource_type.lower() == "stack": + return Stack.fetch(id) + elif resource_type.lower() == "node": + return Node.fetch(id) + elif resource_type.lower() == "nodecluster": + return NodeCluster.fetch(id) + elif resource_type.lower() == "action": + return Action.fetch(id) + else: + raise ApiError( + "Unsupported resource type. Only support: action, container, node, nodecluster, service, stack") + + @staticmethod + def fetch_remote_container(identifier, raise_exceptions=True): + try: + if is_uuid4(identifier): + try: + return Container.fetch(identifier) + except Exception: + raise ObjectNotFound("Cannot find a container with the identifier '%s'" % identifier) + else: + if "." in identifier: + terms = identifier.split(".", 2) + objects_same_identifier = Container.list(name=terms[0], service__stack__name=terms[1]) + else: + objects_same_identifier = Container.list(uuid__startswith=identifier) or \ + Container.list(name=identifier) + + if len(objects_same_identifier) == 1: + uuid = objects_same_identifier[0].uuid + return Container.fetch(uuid) + elif len(objects_same_identifier) == 0: + raise ObjectNotFound("Cannot find a container with the identifier '%s'" % identifier) + raise NonUniqueIdentifier("More than one container has the same identifier, please use the long uuid") + + except (NonUniqueIdentifier, ObjectNotFound) as e: + if not raise_exceptions: + return e + raise e + + @staticmethod + def fetch_remote_service(identifier, raise_exceptions=True): + try: + if is_uuid4(identifier): + try: + return Service.fetch(identifier) + except Exception: + raise ObjectNotFound("Cannot find a service with the identifier '%s'" % identifier) + else: + if "." in identifier: + terms = identifier.split(".", 2) + objects_same_identifier = Service.list(name=terms[0], stack__name=terms[1]) + else: + objects_same_identifier = Service.list(uuid__startswith=identifier) or \ + Service.list(name=identifier) + + if len(objects_same_identifier) == 1: + uuid = objects_same_identifier[0].uuid + return Service.fetch(uuid) + elif len(objects_same_identifier) == 0: + raise ObjectNotFound("Cannot find a service with the identifier '%s'" % identifier) + raise NonUniqueIdentifier("More than one service has the same identifier, please use the long uuid") + except (NonUniqueIdentifier, ObjectNotFound) as e: + if not raise_exceptions: + return e + raise e + + @staticmethod + def fetch_remote_stack(identifier, raise_exceptions=True): + try: + if is_uuid4(identifier): + try: + return Stack.fetch(identifier) + except Exception: + raise ObjectNotFound("Cannot find a stack with the identifier '%s'" % identifier) + else: + objects_same_identifier = Stack.list(uuid__startswith=identifier) or \ + Stack.list(name=identifier) + if len(objects_same_identifier) == 1: + uuid = objects_same_identifier[0].uuid + return Stack.fetch(uuid) + elif len(objects_same_identifier) == 0: + raise ObjectNotFound("Cannot find a stack with the identifier '%s'" % identifier) + raise NonUniqueIdentifier("More than one stack has the same identifier, please use the long uuid") + + except (NonUniqueIdentifier, ObjectNotFound) as e: + if not raise_exceptions: + return e + raise e + + @staticmethod + def fetch_remote_node(identifier, raise_exceptions=True): + try: + if is_uuid4(identifier): + try: + return Node.fetch(identifier) + except Exception: + raise ObjectNotFound("Cannot find a node with the identifier '%s'" % identifier) + else: + objects_same_identifier = Node.list(uuid__startswith=identifier) + if len(objects_same_identifier) == 1: + uuid = objects_same_identifier[0].uuid + return Node.fetch(uuid) + elif len(objects_same_identifier) == 0: + raise ObjectNotFound("Cannot find a node with the identifier '%s'" % identifier) + raise NonUniqueIdentifier("More than one node has the same identifier, please use the long uuid") + + except (NonUniqueIdentifier, ObjectNotFound) as e: + if not raise_exceptions: + return e + raise e + + @staticmethod + def fetch_remote_nodecluster(identifier, raise_exceptions=True): + try: + if is_uuid4(identifier): + try: + return NodeCluster.fetch(identifier) + except Exception: + raise ObjectNotFound("Cannot find a node cluster with the identifier '%s'" % identifier) + else: + objects_same_identifier = NodeCluster.list(uuid__startswith=identifier) or \ + NodeCluster.list(name=identifier) + if len(objects_same_identifier) == 1: + uuid = objects_same_identifier[0].uuid + return NodeCluster.fetch(uuid) + elif len(objects_same_identifier) == 0: + raise ObjectNotFound("Cannot find a node cluster with the identifier '%s'" % identifier) + raise NonUniqueIdentifier( + "More than one node cluster has the same identifier, please use the long uuid") + + except (NonUniqueIdentifier, ObjectNotFound) as e: + if not raise_exceptions: + return e + raise e + + @staticmethod + def fetch_remote_action(identifier, raise_exceptions=True): + try: + if is_uuid4(identifier): + try: + return Action.fetch(identifier) + except Exception: + raise ObjectNotFound("Cannot find an action with the identifier '%s'" % identifier) + else: + objects_same_identifier = Action.list(uuid__startswith=identifier) + if len(objects_same_identifier) == 1: + uuid = objects_same_identifier[0].uuid + return Action.fetch(uuid) + elif len(objects_same_identifier) == 0: + raise ObjectNotFound("Cannot find an action cluster with the identifier '%s'" % identifier) + raise NonUniqueIdentifier( + "More than one action has the same identifier, please use the long uuid") + + except (NonUniqueIdentifier, ObjectNotFound) as e: + if not raise_exceptions: + return e + raise e diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..72c0961 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +backports.ssl-match-hostname==3.4.0.2 +future==0.15.0 +requests==2.7.0 +six==1.9.0 +websocket-client==0.32.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a833d51 --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +import codecs +import os +import re + +from setuptools import setup, find_packages + + +def read(*parts): + path = os.path.join(os.path.dirname(__file__), *parts) + with codecs.open(path, encoding='utf-8') as fobj: + return fobj.read() + + +def find_version(*file_paths): + version_file = read(*file_paths) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError('Unable to find version string.') + + +with open('requirements.txt') as f: + install_requires = f.read().splitlines() + +setup( + name="python-dockercloud", + version=find_version('dockercloud', '__init__.py'), + packages=find_packages(), + install_requires=install_requires, + provides=['docker'], + include_package_data=True, + author="Docker, Inc.", + author_email="info@docker.com", + description="Python Library for Dockercloud", + license="Apache v2", + keywords="docker cloud", + url="http://cloud.docker.com/", + test_suite="tests", +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..9d73dfb --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,12 @@ +__author__ = 'fermayo' + +# Python 3.4.2 includes mock in-box, prefer that version +# For other versions, patch it up to use the external mock library +try: + from unittest import mock +except ImportError: + import sys + + sys.modules['unittest'] = __import__('unittest') + sys.modules['unittest.mock'] = __import__('mock') + setattr(sys.modules['unittest'], 'mock', __import__('mock')) diff --git a/tests/fake_api.py b/tests/fake_api.py new file mode 100644 index 0000000..d1d03ac --- /dev/null +++ b/tests/fake_api.py @@ -0,0 +1,276 @@ +import datetime +import json + +import requests + +FAKE_USER = 'fake_user' +FAKE_PASSWORD = 'fake_password' +FAKE_DOCKERCLOUD_AUTH = 'fake_auth' +FAKE_BASIC_AUTH = 'ZmFrZV91c2VyOmZha2VfcGFzc3dvcmQ=' +FAKE_APIKEY_AUTH = 'ZmFrZV90dXR1bV91c2VyOmZha2VfdHV0dW1fYXBpa2V5' +FAKE_APIKEY = 'fake_apikey' +FAKE_EMAIL = 'fake@docker.com' +FAKE_UUID = 'b0374cc2-4003-4270-b131-25fc494ea2be' +FAKE_UUIDS = ['b0374cc2-4003-4270-b131-25fc494ea2be', 'd89fc6f9-d7ec-4602-be94-429c65d6657d', + 'aeaa0b9f-a878-488a-b4a5-a5b54264edd7'] + + +def response(status_code=200, content='', headers=None, reason=None, elapsed=0, + request=None): + res = requests.Response() + res.status_code = status_code + content = json.dumps(content).encode('ascii') + res._content = content + res.headers = requests.structures.CaseInsensitiveDict(headers or {}) + res.reason = reason + res.elapsed = datetime.timedelta(elapsed) + res.request = request + return res + + +def fake_resp(fake_api_call): + status_code, content = fake_api_call() + return response(status_code=status_code, content=content) + + +def fake_auth(): + status_code = 200 + resp = '{"meta": {"limit": 25, "next": null, "offset": 0, "previous": null, "total_count": 1},' \ + '"objects": [{"key": "%s", "username": "%s"}]}' % (FAKE_APIKEY, FAKE_USER) + return status_code, json.loads(resp) + + +def fake_action_list(): + status_code = 200 + resp = '{"meta": {"limit": 25, "next": null, "offset": 0, "previous": null, "total_count": 3}, ' \ + '"objects": [{"action": "Node Cluster Create", "end_date": "Mon, 29 Sep 2014 15:40:59 +0000", "ip": "207.41.188.212", "location": "New York, United States", "method": "POST", "object": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/", "path": "/api/v1/nodecluster/", "resource_uri": "/api/v1/action/7f62b667-2693-420a-ad2e-41cda5605322/", "start_date": "Mon, 29 Sep 2014 15:40:59 +0000", "state": "Success", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "uuid": "7f62b667-2693-420a-ad2e-41cda5605322"}, ' \ + '{"action": "Node Cluster Deploy", "end_date": "Mon, 29 Sep 2014 15:41:01 +0000", "ip": "207.41.188.212", "location": "New York, United States", "method": "POST", "object": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/", "path": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/deploy/", "resource_uri": "/api/v1/action/db69b048-3bab-4a2e-bcbd-91265edf1a31/", "start_date": "Mon, 29 Sep 2014 15:41:00 +0000", "state": "Failed", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "uuid": "db69b048-3bab-4a2e-bcbd-91265edf1a31"}, ' \ + '{"action": "Node Deploy", "end_date": "Mon, 29 Sep 2014 15:41:16 +0000", "ip": "207.41.188.212", "location": "New York, United States", "method": "POST", "object": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "path": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/deploy/", "resource_uri": "/api/v1/action/ce9ae16b-88fa-4be6-b12e-fc970b8d2445/", "start_date": "Mon, 29 Sep 2014 15:41:16 +0000", "state": "Failed", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "uuid": "ce9ae16b-88fa-4be6-b12e-fc970b8d2445"}]}' + return status_code, json.loads(resp) + + +def fake_action_fetch(): + status_code = 200 + resp = '{"action": "Node Cluster Create", "end_date": "Mon, 29 Sep 2014 15:40:59 +0000", "ip": "207.41.188.212", "location": "New York, United States", "logs": "", "method": "POST", "object": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/", "path": "/api/v1/nodecluster/", "resource_uri": "/api/v1/action/7f62b667-2693-420a-ad2e-41cda5605322/", "start_date": "Mon, 29 Sep 2014 15:40:59 +0000", "state": "Success", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "uuid": "7f62b667-2693-420a-ad2e-41cda5605322"}' + return status_code, json.loads(resp) + + +def fake_provider_list(): + status_code = 200 + resp = '{"meta": {"limit": 25, "next": null, "offset": 0, "previous": null, "total_count": 1}, "objects": [{"available": true, "label": "Digital Ocean", "name": "digitalocean", "regions": ["/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/ams3/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/sgp1/"], "resource_uri": "/api/v1/provider/digitalocean/"}]}' + return status_code, json.loads(resp) + + +def fake_provider_fetch(): + status_code = 200 + resp = '{"available": true, "label": "Digital Ocean", "name": "digitalocean", "regions": ["/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/ams3/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/sgp1/"], "resource_uri": "/api/v1/provider/digitalocean/"}' + return status_code, json.loads(resp) + + +def fake_region_list(): + status_code = 200 + resp = '{"meta": {"limit": 25, "next": null, "offset": 0, "previous": null, "total_count": 8}, ' \ + '"objects": [{"availability_zones": [], "available": true, "label": "Amsterdam 1", "name": "ams1", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/"], "resource_uri": "/api/v1/region/digitalocean/ams1/"}, ' \ + '{"availability_zones": [], "available": true, "label": "San Francisco 1", "name": "sfo1", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/sfo1/"}, ' \ + '{"availability_zones": [], "available": true, "label": "New York 2", "name": "nyc2", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/nyc2/"}, ' \ + '{"availability_zones": [], "available": true, "label": "Amsterdam 2", "name": "ams2", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/ams2/"}, ' \ + '{"availability_zones": [], "available": true, "label": "Singapore 1", "name": "sgp1", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/sgp1/"}, ' \ + '{"availability_zones": [], "available": true, "label": "London 1", "name": "lon1", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/lon1/"}, ' \ + '{"availability_zones": [], "available": true, "label": "New York 3", "name": "nyc3", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/nyc3/"}, ' \ + '{"availability_zones": [], "available": true, "label": "Amsterdam 3", "name": "ams3", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/ams3/"}]}' + return status_code, json.loads(resp) + + +def fake_region_fetch(): + status_code = 200 + resp = '{"availability_zones": [], "available": true, "label": "Amsterdam 1", "name": "ams1", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/"], "provider": "/api/v1/provider/digitalocean/", "resource_uri": "/api/v1/region/digitalocean/ams1/"}' + return status_code, json.loads(resp) + + +def fake_nodetype_list(): + status_code = 200 + resp = '{"meta": {"limit": 25, "next": null, "offset": 0, "previous": null, "total_count": 9}, ' \ + '"objects": [{"availability_zones": [], "available": true, "label": "512MB", "name": "512mb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/512mb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "1GB", "name": "1gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/1gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "2GB", "name": "2gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/2gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "4GB", "name": "4gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/4gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "8GB", "name": "8gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/8gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "16GB", "name": "16gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/16gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "32GB", "name": "32gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/32gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "48GB", "name": "48gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/48gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "64GB", "name": "64gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/64gb/"}]}' + return status_code, json.loads(resp) + + +def fake_nodetype_fetch(): + status_code = 200 + resp = '{"availability_zones": [], "available": true, "label": "8GB", "name": "8gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/8gb/"}' + return status_code, json.loads(resp) + + +def fake_nodeclster_list(): + status_code = 200 + resp = '{"meta": {"limit": 25, "next": null, "offset": 0, "previous": null, "total_count": 2}, ' \ + '"objects": [{"current_num_nodes": 1, "deployed_datetime": "Mon, 29 Sep 2014 22:29:03 +0000", "destroyed_datetime": null, "name": "test", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "region": "/api/v1/region/digitalocean/sfo1/", "resource_uri": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/", "state": "Deployed", "target_num_nodes": 1, "uuid": "a02c3763-e639-46fc-a6db-587f4dbb5444"}, ' \ + '{"current_num_nodes": 1, "deployed_datetime": null, "destroyed_datetime": null, "name": "test2", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "state": "Deploying", "target_num_nodes": 1, "uuid": "b616a720-6684-42c6-83bb-4d298b11b3f3"}]}' + return status_code, json.loads(resp) + + +def fake_nodecluster_fetch(): + status_code = 200 + resp = '{"actions": ["/api/v1/action/bf02b00a-e2fc-4098-8b69-1424b659ef4a/", "/api/v1/action/f8dce6d4-5c41-46a9-9754-baa8f3cdf031/"], "current_num_nodes": 1, "deployed_datetime": null, "destroyed_datetime": null, "name": "test2", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "nodes": ["/api/v1/node/43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456/"], "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "state": "Init", "target_num_nodes": 1, "uuid": "b616a720-6684-42c6-83bb-4d298b11b3f3"}' + return status_code, json.loads(resp) + + +def fake_nodecluster_save(): + status_code = 201 + resp = '{"actions": ["/api/v1/action/f47e26a6-c60c-416f-a0a9-ddf14e3aae83/"], "current_num_nodes": 1, "deployed_datetime": null, "destroyed_datetime": null, "name": "my_cluster", "node_type": "/api/v1/nodetype/digitalocean/1gb/", "nodes": ["/api/v1/node/2cfe7823-f551-4c7b-a82c-f6ab31d7ca25/"], "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/nodecluster/e7915a74-618b-4908-9189-dce965465702/", "state": "Init", "target_num_nodes": 1, "uuid": "e7915a74-618b-4908-9189-dce965465702"}' + return status_code, json.loads(resp) + + +def fake_nodecluster_deploy(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/f47e26a6-c60c-416f-a0a9-ddf14e3aae83/", "/api/v1/action/d110016e-e65d-4ce7-9f11-50c6302494a6/"], "current_num_nodes": 1, "deployed_datetime": null, "destroyed_datetime": null, "name": "my_cluster", "node_type": "/api/v1/nodetype/digitalocean/1gb/", "nodes": ["/api/v1/node/2cfe7823-f551-4c7b-a82c-f6ab31d7ca25/"], "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/nodecluster/e7915a74-618b-4908-9189-dce965465702/", "state": "Deploying", "target_num_nodes": 1, "uuid": "e7915a74-618b-4908-9189-dce965465702"}' + return status_code, json.loads(resp) + + +def fake_nodecluster_delete(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/f47e26a6-c60c-416f-a0a9-ddf14e3aae83/", "/api/v1/action/d110016e-e65d-4ce7-9f11-50c6302494a6/", "/api/v1/action/e33b4bb1-192b-46a6-a1ba-eadfc494c2dd/"], "current_num_nodes": 1, "deployed_datetime": "Mon, 29 Sep 2014 23:45:45 +0000", "destroyed_datetime": null, "name": "my_cluster", "node_type": "/api/v1/nodetype/digitalocean/1gb/", "nodes": ["/api/v1/node/2cfe7823-f551-4c7b-a82c-f6ab31d7ca25/"], "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/nodecluster/e7915a74-618b-4908-9189-dce965465702/", "state": "Terminating", "target_num_nodes": 0, "uuid": "e7915a74-618b-4908-9189-dce965465702"}' + return status_code, json.loads(resp) + + +def fake_node_list(): + status_code = 200 + resp = '{"meta": {"limit": 25, "next": null, "offset": 0, "previous": null, "total_count": 2}, ' \ + '"objects": [{"deployed_datetime": "Mon, 29 Sep 2014 22:29:03 +0000", "destroyed_datetime": null, "docker_execdriver": "native-0.2", "docker_graphdriver": "aufs", "docker_version": "1.2.0", "external_fqdn": "fa9df19a-tifayuki.node.docker.io", "last_seen": "Tue, 30 Sep 2014 15:27:05 +0000", "node_cluster": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "public_ip": "198.199.97.190", "region": "/api/v1/region/digitalocean/sfo1/", "resource_uri": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "state": "Deployed", "uuid": "fa9df19a-162b-45b4-bb5a-152dfd1b133f"}, ' \ + '{"deployed_datetime": "Mon, 29 Sep 2014 22:59:47 +0000", "destroyed_datetime": null, "docker_execdriver": "native-0.2", "docker_graphdriver": "aufs", "docker_version": "1.2.0", "external_fqdn": "43b5ebaf-tifayuki.node.docker.io", "last_seen": "Tue, 30 Sep 2014 15:27:06 +0000", "node_cluster": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "public_ip": "178.62.20.100", "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/node/43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456/", "state": "Deployed", "uuid": "43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456"}]}' + return status_code, json.loads(resp) + + +def fake_node_fetch(): + status_code = 200 + resp = '{"actions": ["/api/v1/action/8f5b893b-826e-40b7-bb8b-2d96301425f2/"], "deployed_datetime": "Mon, 29 Sep 2014 22:59:47 +0000", "destroyed_datetime": null, "docker_execdriver": "native-0.2", "docker_graphdriver": "aufs", "docker_version": "1.2.0", "external_fqdn": "43b5ebaf-tifayuki.node.docker.io", "last_seen": "Tue, 30 Sep 2014 15:30:06 +0000", "node_cluster": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "public_ip": "178.62.20.100", "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/node/43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456/", "state": "Deployed", "uuid": "43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456"}' + return status_code, json.loads(resp) + + +def fake_node_delete(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/8f5b893b-826e-40b7-bb8b-2d96301425f2/", "/api/v1/action/83c66611-ea5d-45d7-a97d-914029a90524/"], "deployed_datetime": "Mon, 29 Sep 2014 22:59:47 +0000", "destroyed_datetime": null, "docker_execdriver": "native-0.2", "docker_graphdriver": "aufs", "docker_version": "1.2.0", "external_fqdn": "43b5ebaf-tifayuki.node.docker.io", "last_seen": "Tue, 30 Sep 2014 15:38:12 +0000", "node_cluster": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "public_ip": "178.62.20.100", "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/node/43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456/", "state": "Terminating", "uuid": "43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456"}' + return status_code, json.loads(resp) + + +def fake_node_deploy(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/8f5b893b-826e-40b7-bb8b-2d96301425f2/", "/api/v1/action/83c66611-ea5d-45d7-a97d-914029a90524/"], "deployed_datetime": "Mon, 29 Sep 2014 22:59:47 +0000", "destroyed_datetime": null, "docker_execdriver": "native-0.2", "docker_graphdriver": "aufs", "docker_version": "1.2.0", "external_fqdn": "43b5ebaf-tifayuki.node.docker.io", "last_seen": "Tue, 30 Sep 2014 15:38:12 +0000", "node_cluster": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "public_ip": "178.62.20.100", "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/node/43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456/", "state": "Starting", "uuid": "43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456"}' + return status_code, json.loads(resp) + + +def fake_image_list(): + status_code = 200 + resp = '{"meta": {"limit": 25, "next": null, "offset": 0, "previous": null, "total_count": 1}, "objects": [{"base_image": false, "categories": [], "cluster_aware": true, "description": "", "docker_registry": "/api/v1/registry/docker.com/", "image_url": "", "imagetag_set": ["/api/v1/image/docker.com/tifayuki/mongodb/tag/latest/"], "is_private_image": true, "name": "docker.com/tifayuki/mongodb", "public_url": "", "resource_uri": "/api/v1/image/docker.com/tifayuki/mongodb/", "starred": false}]}' + return status_code, json.loads(resp) + + +def fake_image_fetch(): + status_code = 200 + resp = '{"base_image": false, "categories": [], "cluster_aware": true, "description": "", "docker_registry": {"created": true, "host": "registry.hub.docker.com", "id": 5, "image_url": "/_static/assets/images/dockerregistries/docker.png", "is_ssl": true, "is_tutum_registry": false, "modified": true, "name": "Docker.io", "resource_uri": "/api/v1/registry/registry.hub.docker.com/", "uuid": "d533039e-c44c-4cdc-951b-e0e03b8410c6"}, "image_url": "", "imagetag_set": [{"full_name": "tifayuki/cadvisor:latest", "image": {"author": "fake ", "docker_id": "9e2907ef52bf811b4da100f50ba8f0908ccc610c7054bd69087f0a9f4703efdd", "entrypoint": "", "image_creation": "Fri, 15 Aug 2014 15:19:04 +0000", "imageenvvar_set": [{"key": "CADVISOR_TAG", "value": "0.2.2"}, {"key": "DB_NAME", "value": "cadvisor"}, {"key": "DB_PASS", "value": "root"}, {"key": "DB_USER", "value": "root"}, {"key": "HOME", "value": "/"}, {"key": "PATH", "value": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}], "imageport_set": [], "run_command": "/run.sh"}, "image_info": "/api/v1/image/tifayuki/cadvisor/", "name": "latest", "resource_uri": "/api/v1/image/tifayuki/cadvisor/tag/latest/"}], "is_private_image": true, "name": "tifayuki/cadvisor", "public_url": "https://registry.hub.docker.com/u/tifayuki/cadvisor/", "resource_uri": "/api/v1/image/tifayuki/cadvisor/", "starred": false}' + return status_code, json.loads(resp) + + +def fake_image_save(): + status_code = 202 + resp = '{"base_image": false, "categories": [], "cluster_aware": true, "description": "description", "docker_registry": {"created": true, "host": "registry.hub.docker.com", "id": 5, "image_url": "/_static/assets/images/dockerregistries/docker.png", "is_ssl": true, "is_tutum_registry": false, "modified": true, "name": "Docker.io", "resource_uri": "/api/v1/registry/registry.hub.docker.com/", "uuid": "d533039e-c44c-4cdc-951b-e0e03b8410c6"}, "image_url": "", "imagetag_set": [{"full_name": "tifayuki/cadvisor:latest", "image": {"author": "fake ", "docker_id": "9e2907ef52bf811b4da100f50ba8f0908ccc610c7054bd69087f0a9f4703efdd", "entrypoint": "", "image_creation": "Fri, 15 Aug 2014 15:19:04 +0000", "imageenvvar_set": [{"key": "CADVISOR_TAG", "value": "0.2.2"}, {"key": "DB_NAME", "value": "cadvisor"}, {"key": "DB_PASS", "value": "root"}, {"key": "DB_USER", "value": "root"}, {"key": "HOME", "value": "/"}, {"key": "PATH", "value": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}], "imageport_set": [], "run_command": "/run.sh"}, "image_info": "/api/v1/image/tifayuki/cadvisor/", "name": "latest", "resource_uri": "/api/v1/image/tifayuki/cadvisor/tag/latest/"}], "is_private_image": true, "name": "tifayuki/cadvisor", "public_url": "https://registry.hub.docker.com/u/tifayuki/cadvisor/", "resource_uri": "/api/v1/image/tifayuki/cadvisor/", "starred": false}' + return status_code, json.loads(resp) + + +def fake_image_delete(): + status_code = 204 + resp = '' + return status_code, resp + + +def fake_service_list(): + status_code = 200 + resp = '{"meta": {"limit": 25, "next": null, "offset": 0, "previous": null, "total_count": 1}, ' \ + '"objects": [{"autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_ports": [{"endpoint_uri": null, "inner_port": 80, "outer_port": null, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}], "cpu_shares": null, "current_num_containers": 3, "deployed_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/hello-world:latest", "image_tag": "/api/v1/image/tutum/hello-world/tag/latest/", "memory": null, "memory_swap": null, "name": "hello-world", "resource_uri": "/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/", "run_command": "/run.sh", "running_num_containers": 3, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "state": "Running", "stopped_datetime": null, "stopped_num_containers": 0, "target_num_containers": 3, "unique_name": "hello-world", "uuid": "a2ac25c9-7cfe-4a1b-9d97-66de23642ee8"}]}' + return status_code, json.loads(resp) + + +def fake_service_fetch(): + status_code = 200 + resp = '{"actions": ["/api/v1/action/e3ee01df-9f2f-4720-a114-ea1a236d47d2/", "/api/v1/action/c58213ab-8d5c-4a6d-b4c3-bd7157242dc2/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 80, "outer_port": null, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}], "containers": ["/api/v1/container/cff4dfa7-28a5-4599-a3f9-c7dc39353c11/", "/api/v1/container/4d966087-5169-4a0b-a2f0-78bbb878d872/", "/api/v1/container/0c84cd78-c239-40ad-939e-dbbc372ae345/"], "cpu_shares": null, "current_num_containers": 3, "deployed_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/hello-world:latest", "image_tag": "/api/v1/image/tutum/hello-world/tag/latest/", "link_variables": {"HELLO_WORLD_1_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_1_ENV_HOME": "/", "HELLO_WORLD_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_1_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_1_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_1_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_2_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_2_ENV_HOME": "/", "HELLO_WORLD_2_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_2_PORT": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP_ADDR": "hello-world-2.a2f5a2e9-tifayuki.node.docker.io", "HELLO_WORLD_2_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_2_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_3_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_3_ENV_HOME": "/", "HELLO_WORLD_3_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_3_PORT": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP_ADDR": "hello-world-3.5067d4f4-tifayuki.node.docker.io", "HELLO_WORLD_3_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_3_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_ENV_HOME": "/", "HELLO_WORLD_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "hello-world", "resource_uri": "/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/", "roles": [], "run_command": "/run.sh", "running_num_containers": 3, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "state": "Running", "stopped_datetime": null, "stopped_num_containers": 0, "target_num_containers": 3, "unique_name": "hello-world", "uuid": "a2ac25c9-7cfe-4a1b-9d97-66de23642ee8"}' + return status_code, json.loads(resp) + + +def fake_service_save(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/e3ee01df-9f2f-4720-a114-ea1a236d47d2/", "/api/v1/action/c58213ab-8d5c-4a6d-b4c3-bd7157242dc2/", "/api/v1/action/b3adfa79-dbd2-41a4-8c71-5e242ecce9bb/", "/api/v1/action/f82e25e7-d550-454e-a897-8599f2f530e5/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 80, "outer_port": null, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}], "containers": ["/api/v1/container/cff4dfa7-28a5-4599-a3f9-c7dc39353c11/", "/api/v1/container/4d966087-5169-4a0b-a2f0-78bbb878d872/", "/api/v1/container/0c84cd78-c239-40ad-939e-dbbc372ae345/"], "cpu_shares": null, "current_num_containers": 3, "deployed_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/hello-world:latest", "image_tag": "/api/v1/image/tutum/hello-world/tag/latest/", "link_variables": {"HELLO_WORLD_1_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_1_ENV_HOME": "/", "HELLO_WORLD_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_1_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_1_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_1_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_2_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_2_ENV_HOME": "/", "HELLO_WORLD_2_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_2_PORT": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP_ADDR": "hello-world-2.a2f5a2e9-tifayuki.node.docker.io", "HELLO_WORLD_2_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_2_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_3_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_3_ENV_HOME": "/", "HELLO_WORLD_3_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_3_PORT": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP_ADDR": "hello-world-3.5067d4f4-tifayuki.node.docker.io", "HELLO_WORLD_3_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_3_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_ENV_HOME": "/", "HELLO_WORLD_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "hello-world", "resource_uri": "/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/", "roles": [], "run_command": "/run.sh", "running_num_containers": 3, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "state": "Scaling", "stopped_datetime": null, "stopped_num_containers": 0, "target_num_containers": 5, "unique_name": "hello-world", "uuid": "a2ac25c9-7cfe-4a1b-9d97-66de23642ee8", "web_public_dns": ""}' + return status_code, json.loads(resp) + + +def fake_service_delete(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/e3ee01df-9f2f-4720-a114-ea1a236d47d2/", "/api/v1/action/c58213ab-8d5c-4a6d-b4c3-bd7157242dc2/", "/api/v1/action/b3adfa79-dbd2-41a4-8c71-5e242ecce9bb/", "/api/v1/action/f82e25e7-d550-454e-a897-8599f2f530e5/", "/api/v1/action/381c53a0-bf18-4a01-a1b0-be87051c35e8/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 80, "outer_port": null, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}], "containers": ["/api/v1/container/cff4dfa7-28a5-4599-a3f9-c7dc39353c11/", "/api/v1/container/4d966087-5169-4a0b-a2f0-78bbb878d872/", "/api/v1/container/0c84cd78-c239-40ad-939e-dbbc372ae345/", "/api/v1/container/6e74df59-83ee-4351-8ba9-3d26e0d64c34/", "/api/v1/container/7bbff9f0-af41-408f-9dd0-213cd67e4aa2/"], "cpu_shares": null, "current_num_containers": 5, "deployed_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/hello-world:latest", "image_tag": "/api/v1/image/tutum/hello-world/tag/latest/", "link_variables": {"HELLO_WORLD_1_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_1_ENV_HOME": "/", "HELLO_WORLD_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_1_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_1_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_1_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_2_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_2_ENV_HOME": "/", "HELLO_WORLD_2_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_2_PORT": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP_ADDR": "hello-world-2.a2f5a2e9-tifayuki.node.docker.io", "HELLO_WORLD_2_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_2_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_3_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_3_ENV_HOME": "/", "HELLO_WORLD_3_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_3_PORT": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP_ADDR": "hello-world-3.5067d4f4-tifayuki.node.docker.io", "HELLO_WORLD_3_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_3_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_4_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_4_ENV_HOME": "/", "HELLO_WORLD_4_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_4_PORT": "tcp://hello-world-4.5067d4f4-tifayuki.node.docker.io:49156", "HELLO_WORLD_4_PORT_80_TCP": "tcp://hello-world-4.5067d4f4-tifayuki.node.docker.io:49156", "HELLO_WORLD_4_PORT_80_TCP_ADDR": "hello-world-4.5067d4f4-tifayuki.node.docker.io", "HELLO_WORLD_4_PORT_80_TCP_PORT": "49156", "HELLO_WORLD_4_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_5_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_5_ENV_HOME": "/", "HELLO_WORLD_5_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_5_PORT": "tcp://hello-world-5.fa9df19a-tifayuki.node.docker.io:49157", "HELLO_WORLD_5_PORT_80_TCP": "tcp://hello-world-5.fa9df19a-tifayuki.node.docker.io:49157", "HELLO_WORLD_5_PORT_80_TCP_ADDR": "hello-world-5.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_5_PORT_80_TCP_PORT": "49157", "HELLO_WORLD_5_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_ENV_HOME": "/", "HELLO_WORLD_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-t* Connection #0 to host dashboard.docker.com left intactifayuki.node.docker.io", "HELLO_WORLD_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "hello-world", "resource_uri": "/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/", "roles": [], "run_command": "/run.sh", "running_num_containers": 3, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 22:57:36 +0000", "state": "Terminating", "stopped_datetime": null, "stopped_num_containers": 0, "target_num_containers": 0, "unique_name": "hello-world", "uuid": "a2ac25c9-7cfe-4a1b-9d97-66de23642ee8"}' + return status_code, json.loads(resp) + + +def fake_service_start(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/a8aaf64b-3186-41e7-9256-6b3d69786036/", "/api/v1/action/2481790d-a860-4bb4-95d2-5676ae2d6748/", "/api/v1/action/799a0d06-efae-4bf1-b063-7141f722cbb1/", "/api/v1/action/ef6b9f59-1edb-44c7-bcc6-1b80f737e4b0/", "/api/v1/action/99b7ac29-d16d-448f-bac5-54dbd5dd3b7b/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 3306, "outer_port": null, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "containers": ["/api/v1/container/2a1c4057-7753-4393-98c6-35699c198e08/", "/api/v1/container/54ead360-698f-4354-96f7-538f686cdd69/"], "cpu_shares": null, "current_num_containers": 2, "deployed_datetime": "Tue, 30 Sep 2014 22:44:44 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/mysql:latest", "image_tag": "/api/v1/image/tutum/mysql/tag/latest/", "link_variables": {"MYSQL_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "mysql", "resource_uri": "/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/", "roles": [], "run_command": "/run.sh", "running_num_containers": 0, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 22:44:44 +0000", "state": "Starting", "stopped_datetime": "Tue, 30 Sep 2014 23:09:09 +0000", "stopped_num_containers": 2, "target_num_containers": 2, "unique_name": "mysql", "uuid": "5ecde92d-498b-4bbb-b773-a998e5e421dc"}' + return status_code, json.loads(resp) + + +def fake_service_stop(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/a8aaf64b-3186-41e7-9256-6b3d69786036/", "/api/v1/action/2481790d-a860-4bb4-95d2-5676ae2d6748/", "/api/v1/action/799a0d06-efae-4bf1-b063-7141f722cbb1/", "/api/v1/action/ef6b9f59-1edb-44c7-bcc6-1b80f737e4b0/", "/api/v1/action/99b7ac29-d16d-448f-bac5-54dbd5dd3b7b/", "/api/v1/action/98816bfc-4fa7-4697-bea1-d926de69b48f/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 3306, "outer_port": null, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "containers": ["/api/v1/container/2a1c4057-7753-4393-98c6-35699c198e08/", "/api/v1/container/54ead360-698f-4354-96f7-538f686cdd69/"], "cpu_shares": null, "current_num_containers": 2, "deployed_datetime": "Tue, 30 Sep 2014 22:44:44 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/mysql:latest", "image_tag": "/api/v1/image/tutum/mysql/tag/latest/", "link_variables": {"MYSQL_1_ENV_DEBIAN_FRONTEND": "noninteractive", "MYSQL_1_ENV_MYSQL_PASS": "**Random**", "MYSQL_1_ENV_MYSQL_USER": "admin", "MYSQL_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "MYSQL_1_ENV_REPLICATION_MASTER": "**False**", "MYSQL_1_ENV_REPLICATION_PASS": "replica", "MYSQL_1_ENV_REPLICATION_SLAVE": "**False**", "MYSQL_1_ENV_REPLICATION_USER": "replica", "MYSQL_1_PORT": "tcp://mysql-1.fa9df19a-tifayuki.node.docker.io:49156", "MYSQL_1_PORT_3306_TCP": "tcp://mysql-1.fa9df19a-tifayuki.node.docker.io:49156", "MYSQL_1_PORT_3306_TCP_ADDR": "mysql-1.fa9df19a-tifayuki.node.docker.io", "MYSQL_1_PORT_3306_TCP_PORT": "49156", "MYSQL_1_PORT_3306_TCP_PROTO": "tcp", "MYSQL_2_ENV_DEBIAN_FRONTEND": "noninteractive", "MYSQL_2_ENV_MYSQL_PASS": "**Random**", "MYSQL_2_ENV_MYSQL_USER": "admin", "MYSQL_2_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "MYSQL_2_ENV_REPLICATION_MASTER": "**False**", "MYSQL_2_ENV_REPLICATION_PASS": "replica", "MYSQL_2_ENV_REPLICATION_SLAVE": "**False**", "MYSQL_2_ENV_REPLICATION_USER": "replica", "MYSQL_2_PORT": "tcp://mysql-2.a2f5a2e9-tifayuki.node.docker.io:49156", "MYSQL_2_PORT_3306_TCP": "tcp://mysql-2.a2f5a2e9-tifayuki.node.docker.io:49156", "MYSQL_2_PORT_3306_TCP_ADDR": "mysql-2.a2f5a2e9-tifayuki.node.docker.io", "MYSQL_2_PORT_3306_TCP_PORT": "49156", "MYSQL_2_PORT_3306_TCP_PROTO": "tcp", "MYSQL_ENV_DEBIAN_FRONTEND": "noninteractive", "MYSQL_ENV_MYSQL_PASS": "**Random**", "MYSQL_ENV_MYSQL_USER": "admin", "MYSQL_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "MYSQL_ENV_REPLICATION_MASTER": "**False**", "MYSQL_ENV_REPLICATION_PASS": "replica", "MYSQL_ENV_REPLICATION_SLAVE": "**False**", "MYSQL_ENV_REPLICATION_USER": "replica", "MYSQL_PORT": "tcp://mysql-1.fa9df19a-tifayuki.node.docker.io:49156", "MYSQL_PORT_3306_TCP": "tcp://mysql-1.fa9df19a-tifayuki.node.docker.io:49156", "MYSQL_PORT_3306_TCP_ADDR": "mysql-1.fa9df19a-tifayuki.node.docker.io", "MYSQL_PORT_3306_TCP_PORT": "49156", "MYSQL_PORT_3306_TCP_PROTO": "tcp", "MYSQL_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "mysql", "resource_uri": "/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/", "roles": [], "run_command": "/run.sh", "running_num_containers": 1, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 23:50:59 +0000", "state": "Stopping", "stopped_datetime": "Tue, 30 Sep 2014 23:09:09 +0000", "stopped_num_containers": 0, "target_num_containers": 2, "unique_name": "mysql", "uuid": "5ecde92d-498b-4bbb-b773-a998e5e421dc"}' + return status_code, json.loads(resp) + + +def fake_service_redeploy(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/a8aaf64b-3186-41e7-9256-6b3d69786036/", "/api/v1/action/2481790d-a860-4bb4-95d2-5676ae2d6748/", "/api/v1/action/799a0d06-efae-4bf1-b063-7141f722cbb1/", "/api/v1/action/ef6b9f59-1edb-44c7-bcc6-1b80f737e4b0/", "/api/v1/action/99b7ac29-d16d-448f-bac5-54dbd5dd3b7b/", "/api/v1/action/98816bfc-4fa7-4697-bea1-d926de69b48f/", "/api/v1/action/285e250f-7429-4f0a-a252-5594a05c8020/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 3306, "outer_port": null, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "containers": ["/api/v1/container/2a1c4057-7753-4393-98c6-35699c198e08/", "/api/v1/container/54ead360-698f-4354-96f7-538f686cdd69/"], "cpu_shares": null, "current_num_containers": 2, "deployed_datetime": "Tue, 30 Sep 2014 22:44:44 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/mysql:latest", "image_tag": "/api/v1/image/tutum/mysql/tag/latest/", "link_variables": {"MYSQL_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "mysql", "resource_uri": "/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/", "roles": [], "run_command": "/run.sh", "running_num_containers": 0, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 23:50:59 +0000", "state": "Redeploying", "stopped_datetime": "Tue, 30 Sep 2014 23:51:52 +0000", "stopped_num_containers": 0, "target_num_containers": 2, "unique_name": "mysql", "uuid": "5ecde92d-498b-4bbb-b773-a998e5e421dc"}' + return status_code, json.loads(resp) + + +def fake_service_logs(): + status_code = 200 + resp = r'{"logs": "[mysql-1] 2014-09-30T22:44:31.385643366Z => An empty or uninitialized MySQL volume is detected in /var/lib/mysql\n[mysql-1] 2014-09-30T22:44:31.386200239Z => Installing MySQL ...\n"}' + return status_code, json.loads(resp) + + +def fake_container_list(): + status_code = 200 + resp = '{"meta": {"limit": 25, "next": null, "offset": 0, "previous": null, "total_count": 2}, ' \ + '"objects": [{"autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_ports": [{"endpoint_uri": "mysql://mysql-1.fa9df19a-tifayuki.node.docker.io:49159/", "inner_port": 3306, "outer_port": 49159, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:44:32 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": null, "exit_code_msg": null, "image_name": "tutum/mysql:latest", "image_tag": "/api/v1/image/tutum/mysql/tag/latest/", "is_dead_backend": null, "memory": null, "memory_swap": null, "name": "mysql", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "mysql-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/567f1ff8-57bd-4689-a732-1e1705bc5082/", "run_command": "/run.sh", "service": "/api/v1/service/326a2daf-2069-4cd4-9e44-08faa068a62f/", "started_datetime": "Wed, 1 Oct 2014 14:44:32 +0000", "state": "Running", "stopped_datetime": null, "unique_name": "mysql-1", "uuid": "567f1ff8-57bd-4689-a732-1e1705bc5082"}, {"autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_ports": [{"endpoint_uri": "http://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160/", "inner_port": 80, "outer_port": 49160, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}, {"endpoint_uri": "mysql://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161/", "inner_port": 3306, "outer_port": 49161, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": null, "exit_code_msg": null, "image_name": "tutum/wordpress:latest", "image_tag": "/api/v1/image/tutum/wordpress/tag/latest/", "is_dead_backend": null, "memory": null, "memory_swap": null, "name": "wordpress", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d/", "run_command": "/run.sh", "service": "/api/v1/service/81bbc30a-35de-4f5e-840d-87bc2573d818/", "started_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "state": "Running", "stopped_datetime": null, "unique_name": "wordpress-1", "uuid": "52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d"}]}' + return status_code, json.loads(resp) + + +def fake_container_fetch(): + status_code = 200 + resp = '{"actions": ["/api/v1/action/62b1f681-5c0a-4b4e-8bc7-570f62020711/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": "http://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160/", "inner_port": 80, "outer_port": 49160, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}, {"endpoint_uri": "mysql://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161/", "inner_port": 3306, "outer_port": 49161, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": null, "exit_code_msg": null, "image_name": "tutum/wordpress:latest", "image_tag": "/api/v1/image/tutum/wordpress/tag/latest/", "is_dead_backend": null, "link_variables": {"WORDPRESS_1_ENV_HOME": "/", "WORDPRESS_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_1_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_1_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_1_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_1_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_3306_TCP_PORT": "49161", "WORDPRESS_1_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_1_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_1_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_80_TCP_PORT": "49160", "WORDPRESS_1_PORT_80_TCP_PROTO": "tcp", "WORDPRESS_ENV_HOME": "/", "WORDPRESS_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_3306_TCP_PORT": "49161", "WORDPRESS_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_80_TCP_PORT": "49160", "WORDPRESS_PORT_80_TCP_PROTO": "tcp"}, "memory": null, "memory_swap": null, "name": "wordpress", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d/", "roles": [], "run_command": "/run.sh", "service": "/api/v1/service/81bbc30a-35de-4f5e-840d-87bc2573d818/", "started_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "state": "Running", "stopped_datetime": null, "unique_name": "wordpress-1", "uuid": "52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d"}' + return status_code, json.loads(resp) + + +def fake_container_delete(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/62b1f681-5c0a-4b4e-8bc7-570f62020711/", "/api/v1/action/5c435933-a1d9-449e-acb6-fbe7b5904b26/", "/api/v1/action/2508d37c-57a8-4ef1-88ff-3b5e20f88b7f/", "/api/v1/action/0438248f-b05c-41f2-b715-40a788735deb/", "/api/v1/action/6a5b82a0-b14a-4c05-94d0-117d730cb647/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": "http://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160/", "inner_port": 80, "outer_port": 49160, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}, {"endpoint_uri": "mysql://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161/", "inner_port": 3306, "outer_port": 49161, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": 0, "exit_code_msg": "Exit code 0 (Success)", "image_name": "tutum/wordpress:latest", "image_tag": "/api/v1/image/tutum/wordpress/tag/latest/", "is_dead_backend": null, "link_variables": {"WORDPRESS_1_ENV_HOME": "/", "WORDPRESS_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_1_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_1_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_1_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_1_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_3306_TCP_PORT": "49161", "WORDPRESS_1_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_1_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_1_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_80_TCP_PORT": "49160", "WORDPRESS_1_PORT_80_TCP_PROTO": "tcp", "WORDPRESS_ENV_HOME": "/", "WORDPRESS_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_3306_TCP_PORT": "49161", "WORDPRESS_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_80_TCP_PORT": "49160", "WORDPRESS_PORT_80_TCP_PROTO": "tcp"}, "memory": null, "memory_swap": null, "name": "wordpress", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d/", "roles": [], "run_command": "/run.sh", "service": "/api/v1/service/81bbc30a-35de-4f5e-840d-87bc2573d818/", "started_datetime": "Wed, 1 Oct 2014 15:22:51 +0000", "state": "Terminating", "stopped_datetime": "Wed, 1 Oct 2014 15:20:58 +0000", "unique_name": "wordpress-1", "uuid": "52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d"}' + return status_code, json.loads(resp) + + +def fake_container_start(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/62b1f681-5c0a-4b4e-8bc7-570f62020711/", "/api/v1/action/5c435933-a1d9-449e-acb6-fbe7b5904b26/", "/api/v1/action/2508d37c-57a8-4ef1-88ff-3b5e20f88b7f/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": "http://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160/", "inner_port": 80, "outer_port": 49160, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}, {"endpoint_uri": "mysql://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161/", "inner_port": 3306, "outer_port": 49161, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": 0, "exit_code_msg": "Exit code 0 (Success)", "image_name": "tutum/wordpress:latest", "image_tag": "/api/v1/image/tutum/wordpress/tag/latest/", "is_dead_backend": null, "link_variables": {"WORDPRESS_1_ENV_HOME": "/", "WORDPRESS_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_1_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_1_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_1_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_1_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_3306_TCP_PORT": "49161", "WORDPRESS_1_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_1_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_1_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_80_TCP_PORT": "49160", "WORDPRESS_1_PORT_80_TCP_PROTO": "tcp", "WORDPRESS_ENV_HOME": "/", "WORDPRESS_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_3306_TCP_PORT": "49161", "WORDPRESS_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_80_TCP_PORT": "49160", "WORDPRESS_PORT_80_TCP_PROTO": "tcp"}, "memory": null, "memory_swap": null, "name": "wordpress", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d/", "roles": [], "run_command": "/run.sh", "service": "/api/v1/service/81bbc30a-35de-4f5e-840d-87bc2573d818/", "started_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "state": "Starting", "stopped_datetime": "Wed, 1 Oct 2014 15:20:58 +0000", "unique_name": "wordpress-1", "uuid": "52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d"}' + return status_code, json.loads(resp) + + +def fake_container_stop(): + status_code = 202 + resp = '{"actions": ["/api/v1/action/62b1f681-5c0a-4b4e-8bc7-570f62020711/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": "http://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160/", "inner_port": 80, "outer_port": 49160, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}, {"endpoint_uri": "mysql://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161/", "inner_port": 3306, "outer_port": 49161, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": null, "exit_code_msg": null, "image_name": "tutum/wordpress:latest", "image_tag": "/api/v1/image/tutum/wordpress/tag/latest/", "is_dead_backend": null, "link_variables": {"WORDPRESS_1_ENV_HOME": "/", "WORDPRESS_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_1_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_1_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_1_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_1_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_3306_TCP_PORT": "49161", "WORDPRESS_1_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_1_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_1_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_80_TCP_PORT": "49160", "WORDPRESS_1_PORT_80_TCP_PROTO": "tcp", "WORDPRESS_ENV_HOME": "/", "WORDPRESS_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_3306_TCP_PORT": "49161", "WORDPRESS_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_80_TCP_PORT": "49160", "WORDPRESS_PORT_80_TCP_PROTO": "tcp"}, "memory": null, "memory_swap": null, "name": "wordpress", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d/", "roles": [], "run_command": "/run.sh", "service": "/api/v1/service/81bbc30a-35de-4f5e-840d-87bc2573d818/", "started_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "state": "Stopping", "stopped_datetime": null, "unique_name": "wordpress-1", "uuid": "52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d"}' + return status_code, json.loads(resp) + + +def fake_container_logs(): + status_code = 200 + resp = r'{"logs": "2014-10-01T14:54:23.173185119Z => An empty or uninitialized MySQL volume is detected in /var/lib/mysql\n2014-10-01T14:54:23.173350403Z => Installing MySQL ...\n2014-10-01T14:54:23.467005403Z => Done!\n"}' + return status_code, json.loads(resp) diff --git a/tests/test_action.py b/tests/test_action.py new file mode 100644 index 0000000..f5893a7 --- /dev/null +++ b/tests/test_action.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import + +import unittest + +import unittest.mock as mock + +import dockercloud +from .fake_api import * + + +class ActionTestCase(unittest.TestCase): + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_action_list(self, mock_send): + attributes = json.loads( + '[{"action": "Node Cluster Create", "end_date": "Mon, 29 Sep 2014 15:40:59 +0000", "ip": "207.41.188.212", "location": "New York, United States", "method": "POST", "object": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/", "path": "/api/v1/nodecluster/", "resource_uri": "/api/v1/action/7f62b667-2693-420a-ad2e-41cda5605322/", "start_date": "Mon, 29 Sep 2014 15:40:59 +0000", "state": "Success", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "uuid": "7f62b667-2693-420a-ad2e-41cda5605322"}, ' \ + '{"action": "Node Cluster Deploy", "end_date": "Mon, 29 Sep 2014 15:41:01 +0000", "ip": "207.41.188.212", "location": "New York, United States", "method": "POST", "object": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/", "path": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/deploy/", "resource_uri": "/api/v1/action/db69b048-3bab-4a2e-bcbd-91265edf1a31/", "start_date": "Mon, 29 Sep 2014 15:41:00 +0000", "state": "Failed", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "uuid": "db69b048-3bab-4a2e-bcbd-91265edf1a31"}, ' \ + '{"action": "Node Deploy", "end_date": "Mon, 29 Sep 2014 15:41:16 +0000", "ip": "207.41.188.212", "location": "New York, United States", "method": "POST", "object": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "path": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/deploy/", "resource_uri": "/api/v1/action/ce9ae16b-88fa-4be6-b12e-fc970b8d2445/", "start_date": "Mon, 29 Sep 2014 15:41:16 +0000", "state": "Failed", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "uuid": "ce9ae16b-88fa-4be6-b12e-fc970b8d2445"}]' + ) + mock_send.return_value = fake_resp(fake_action_list) + actions = dockercloud.Action.list() + for i in range(0, len(actions)): + result = json.loads(json.dumps(actions[i].get_all_attributes())) + target = json.loads(json.dumps(attributes[i])) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_action_fetch(self, mock_send): + attribute = json.loads( + '{"action": "Node Cluster Create", "end_date": "Mon, 29 Sep 2014 15:40:59 +0000", "ip": "207.41.188.212", "location": "New York, United States", "logs": "", "method":"POST", "object": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/", "path": "/api/v1/nodecluster/", "resource_uri": "/api/v1/action/7f62b667-2693-420a-ad2e-41cda5605322/", "start_date": "Mon, 29 Sep 2014 15:40:59 +0000", "state": "Success", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "uuid": "7f62b667-2693-420a-ad2e-41cda5605322"}' + ) + mock_send.return_value = fake_resp(fake_action_fetch) + action = dockercloud.Action.fetch("7f62b667-2693-420a-ad2e-41cda5605322") + result = json.loads(json.dumps(action.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..dd81a01 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,89 @@ +from __future__ import absolute_import + +import os +import tempfile +import unittest + +import unittest.mock as mock + +import dockercloud +from .fake_api import * + + +class AuthTestCase(unittest.TestCase): + def setUp(self): + self.dockercloud_auth = dockercloud.dockercloud_auth + self.basic_auth = dockercloud.basic_auth + + def tearDown(self): + dockercloud.dockercloud_auth = self.dockercloud_auth + dockercloud.basic_auth = self.basic_auth + + @mock.patch('dockercloud.api.auth.verify_credential') + def test_auth_authenticate(self, mock_verify_credential): + dockercloud.auth.authenticate(FAKE_USER, FAKE_PASSWORD) + mock_verify_credential.assert_called_with(FAKE_USER, FAKE_PASSWORD) + self.assertEqual(dockercloud.basic_auth, FAKE_BASIC_AUTH) + self.tearDown() + + def test_auth_is_authenticated(self): + dockercloud.dockercloud_auth = FAKE_DOCKERCLOUD_AUTH + dockercloud.basic_auth = FAKE_BASIC_AUTH + self.assertTrue(dockercloud.auth.is_authenticated()) + + dockercloud.dockercloud_auth = None + dockercloud.basic_auth = FAKE_BASIC_AUTH + self.assertTrue(dockercloud.auth.is_authenticated()) + + dockercloud.dockercloud_auth = FAKE_DOCKERCLOUD_AUTH + dockercloud.basic_auth = None + dockercloud.apikey_auth = None + self.assertTrue(dockercloud.auth.is_authenticated()) + + dockercloud.dockercloud_auth = None + dockercloud.basic_auth = None + self.assertFalse(dockercloud.auth.is_authenticated()) + + def test_auth_logout(self): + dockercloud.dockercloud_auth = FAKE_DOCKERCLOUD_AUTH + dockercloud.basic_auth = FAKE_BASIC_AUTH + dockercloud.auth.logout() + self.assertIsNone(dockercloud.dockercloud_auth) + self.assertIsNone(dockercloud.basic_auth) + + def test_auth_load_from_file(self): + temp = tempfile.NamedTemporaryFile('w', delete=False) + with temp as f: + f.write('''{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "%s", + "email": "tifayuki@gmail.com" + } + } +}''' % FAKE_BASIC_AUTH) + basic_auth = dockercloud.auth.load_from_file(f.name) + self.assertEqual(basic_auth, FAKE_BASIC_AUTH) + os.remove(temp.name) + + def test_auth_load_from_file_with_exception(self): + basic_auth = dockercloud.auth.load_from_file('abc') + self.assertIsNone(basic_auth) + + def test_auth_get_auth_header(self): + dockercloud.dockercloud_auth = FAKE_DOCKERCLOUD_AUTH + dockercloud.basic_auth = FAKE_BASIC_AUTH + self.assertEqual({'Authorization': FAKE_DOCKERCLOUD_AUTH}, dockercloud.auth.get_auth_header()) + + print "====================" + dockercloud.dockercloud_auth = None + dockercloud.basic_auth = FAKE_BASIC_AUTH + self.assertEqual({'Authorization': 'Basic %s' % (FAKE_BASIC_AUTH)}, dockercloud.auth.get_auth_header()) + + dockercloud.dockercloud_auth = FAKE_DOCKERCLOUD_AUTH + dockercloud.basic_auth = None + self.assertEqual({'Authorization': FAKE_DOCKERCLOUD_AUTH}, dockercloud.auth.get_auth_header()) + + dockercloud.dockercloud_auth = None + dockercloud.basic_auth = None + self.assertEqual({}, dockercloud.auth.get_auth_header()) diff --git a/tests/test_base.py b/tests/test_base.py new file mode 100644 index 0000000..d2be83a --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,252 @@ +from __future__ import absolute_import + +import json +import unittest + +import unittest.mock as mock + +import dockercloud +from dockercloud.api.base import Restful, Mutable, Immutable + + +class RestfulTestCase(unittest.TestCase): + def setUp(self): + self.pk = Restful.pk + Restful.pk = 'uuid' + + def tearDown(self): + Restful.pk = self.pk + + def test_restful_init(self): + model = Restful(key1='value1', key2='value2') + self.assertEqual('value1', model.key1) + self.assertEqual('value2', model.key2) + + def test_restful_setattr(self): + model = Restful() + + setattr(model, 'key', 'value') + self.assertEqual('value', model.key) + self.assertEqual(['key'], model.__changedattrs__) + + setattr(model, 'key', 'other value') + self.assertEqual('other value', model.key) + self.assertEqual(['key'], model.__changedattrs__) + + setattr(model, 'another_key', 'another_value') + self.assertEqual('another_value', model.another_key) + self.assertEqual(['key', 'another_key'], model.__changedattrs__) + + def test_restful_getchanges(self): + model = Restful() + self.assertEqual([], model.__getchanges__()) + + model.__changedattrs__ = ['dockercloud'] + self.assertEqual(['dockercloud'], model.__getchanges__()) + + def test_restful_setchanges(self): + model = Restful() + model.__setchanges__('abc') + self.assertEqual('abc', model.__changedattrs__) + + model.__setchanges__(None) + self.assertIsNone(model.__changedattrs__) + + def test_restful_loaddict(self): + model = Restful() + self.assertRaises(AssertionError, model._loaddict, {'key': 'value'}) + + model.endpoint = 'endpoint' + model.subsystem = "subsystem" + model._loaddict({'key': 'value'}) + self.assertEqual('value', model.key) + self.assertEqual("/".join(["api", model.subsystem, model._api_version, model.endpoint.lstrip("/"), model.pk]), + model._detail_uri) + self.assertEqual([], model.__getchanges__()) + + def test_restful_pk(self): + model = Restful() + self.assertEqual(model.__class__._pk_key(), model.pk) + + def test_restful_is_dirty(self): + model = Restful() + self.assertFalse(model.is_dirty) + + model.key = 'value' + self.assertTrue(model.is_dirty) + + @mock.patch('dockercloud.api.base.send_request') + def test_restful_perform_action(self, mock_send_request): + try: + model = Restful() + self.assertRaises(dockercloud.ApiError, model._perform_action, 'action') + + model.endpoint = 'fake' + model.subsystem = "subsystem" + model._detail_uri = "/".join( + ["api", model.subsystem, model._api_version, model.endpoint.lstrip("/"), model.pk]) + mock_send_request.side_effect = [{'key': 'value'}, None] + self.assertTrue(model._perform_action('action', params={'k': 'v'}, data={'key': 'value'})) + self.assertEqual('value', model.key) + mock_send_request.assert_called_with('POST', "/".join([model._detail_uri, "action"]), data={'key': 'value'}, + params={'k': 'v'}) + + self.assertFalse(model._perform_action('action', {'key': 'value'})) + + finally: + if hasattr(Restful, 'endpoint'): + delattr(Restful, 'endpoint') + + @mock.patch('dockercloud.api.base.send_request') + def test_restful_expand_attribute(self, mock_send_request): + model = Restful() + self.assertRaises(dockercloud.ApiError, model._expand_attribute, 'attribute') + + model._detail_uri = 'fake/uuid' + mock_send_request.side_effect = [{'key': 'value'}, None] + self.assertEqual('value', model._expand_attribute('key')) + + self.assertIsNone(model._expand_attribute('key')) + + def test_restful_get_all_attributes(self): + model = Restful() + model.key = 'value' + self.assertDictEqual({'key': 'value'}, model.get_all_attributes()) + + +class ImmutableTestCase(unittest.TestCase): + def setUp(self): + self.pk = Immutable.pk + Immutable.pk = 'uuid' + + def tearDown(self): + Immutable.pk = self.pk + + @mock.patch('dockercloud.api.base.send_request') + def test_immutable_fetch(self, mock_send_request): + self.assertRaises(AssertionError, Immutable.fetch, 'uuid') + + try: + ret_json = {"key": "value"} + mock_send_request.return_value = ret_json + Immutable.endpoint = 'endpoint' + Immutable.subsystem = "subsystem" + model = Immutable.fetch('uuid') + mock_send_request.assert_called_with('GET', 'api/subsystem/%s/endpoint/uuid' % Immutable._api_version) + self.assertIsInstance(model, Immutable) + self.assertEqual('value', model.key) + finally: + if hasattr(Immutable, 'endpoint'): + delattr(Immutable, 'endpoint') + + @mock.patch('dockercloud.api.base.send_request') + def test_immutable_list(self, mock_send_request): + self.assertRaises(AssertionError, Immutable.list) + try: + kwargs = {'key': 'value'} + ret_json = {"meta": {"limit": 25, "next": None, "offset": 0, "previous": None, "total_count": 1}, + "objects": [{"key": "value1"}, {"key": "value2"}]} + mock_send_request.return_value = ret_json + Immutable.endpoint = 'fake' + models = Immutable.list(**kwargs) + mock_send_request.assert_called_with('GET', 'api/subsystem/%s/fake' % Immutable._api_version, params=kwargs) + self.assertEqual(2, len(models)) + self.assertIsInstance(models[0], Immutable) + self.assertEqual('value1', models[0].key) + self.assertIsInstance(models[1], Immutable) + self.assertEqual('value2', models[1].key) + finally: + if hasattr(Immutable, 'endpoint'): + delattr(Immutable, 'endpoint') + + @mock.patch('dockercloud.api.base.send_request') + def test_immutable_refresh(self, mock_send_request): + try: + model = Immutable() + model.key = 'value' + self.assertFalse(model.refresh(force=False)) + + self.assertRaises(dockercloud.ApiError, model.refresh, force=True) + + Immutable.endpoint = 'endpoint' + Immutable.subsystem = 'subsystem' + model._detail_uri = 'api/subsystem/%s/endpoint/uuid' % Immutable._api_version + mock_send_request.side_effect = [{'newkey': 'newvalue'}, None] + self.assertTrue(model.refresh(force=True)) + self.assertEqual('newvalue', model.newkey) + mock_send_request.assert_called_with('GET', model._detail_uri) + + self.assertFalse(model.refresh(force=True)) + mock_send_request.assert_called_with('GET', model._detail_uri) + finally: + if hasattr(Immutable, 'endpoint'): + delattr(Immutable, 'endpoint') + + +class MutableTestCase(unittest.TestCase): + def setUp(self): + self.pk = Mutable.pk + Mutable.pk = 'uuid' + + def tearDown(self): + Mutable.pk = self.pk + + def test_mutable_create(self): + self.assertIsInstance(Mutable.create(), Mutable) + + @mock.patch('dockercloud.api.base.send_request') + def test_mutable_delete(self, mock_send_request): + try: + model = Mutable() + self.assertRaises(dockercloud.ApiError, model.delete) + + Mutable.endpoint = 'fake' + model._detail_uri = 'fake/uuid' + mock_send_request.side_effect = [{'key': 'value'}, None] + self.assertTrue(model.delete()) + self.assertEqual('value', model.key) + mock_send_request.assert_called_with('DELETE', 'fake/uuid') + + self.assertTrue(model.delete()) + self.assertIsNone(model._detail_uri) + self.assertFalse(model.is_dirty) + finally: + if hasattr(Mutable, 'endpoint'): + delattr(Mutable, 'endpoint') + + @mock.patch('dockercloud.api.base.send_request') + def test_mutable_save(self, mock_send_request): + try: + self.assertTrue(Mutable().save()) + + model = Mutable() + model.key = 'value' + self.assertRaises(AssertionError, model.save) + + Mutable.endpoint = 'endpoint' + Mutable.subsystem = 'subsystem' + mock_send_request.return_value = None + result = model.save() + mock_send_request.assert_called_with('POST', 'api/subsystem/%s/endpoint' % Mutable._api_version, + data=json.dumps({'key': 'value'})) + self.assertFalse(result) + + mock_send_request.return_value = {'newkey': 'newvalue'} + result = model.save() + mock_send_request.assert_called_with('POST', 'api/subsystem/%s/endpoint' % Mutable._api_version, + data=json.dumps({'key': 'value'})) + self.assertTrue(result) + self.assertEqual('newvalue', model.newkey) + + model.key = 'another value' + mock_send_request.return_value = {'newkey2': 'newvalue2'} + model._detail_uri = 'api/subsystem/%s/endpoint/uuid' % Immutable._api_version + result = model.save() + mock_send_request.assert_called_with('PATCH', 'api/subsystem/%s/endpoint/uuid' % Mutable._api_version, + data=json.dumps({'key': 'another value'})) + self.assertTrue(result) + self.assertEqual('another value', model.key) + self.assertEqual('newvalue2', model.newkey2) + finally: + if hasattr(Mutable, 'endpoint'): + delattr(Mutable, 'endpoint') diff --git a/tests/test_container.py b/tests/test_container.py new file mode 100644 index 0000000..9fa54a3 --- /dev/null +++ b/tests/test_container.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import + +import unittest + +import unittest.mock as mock + +import dockercloud +from .fake_api import * + + +class ContainerTestCase(unittest.TestCase): + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_container_list(self, mock_send): + attributes = json.loads( + '[{"autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_ports": [{"endpoint_uri": "mysql://mysql-1.fa9df19a-tifayuki.node.docker.io:49159/", "inner_port": 3306, "outer_port": 49159, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:44:32 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": null, "exit_code_msg": null, "image_name": "tutum/mysql:latest", "image_tag": "/api/v1/image/tutum/mysql/tag/latest/", "is_dead_backend": null, "memory": null, "memory_swap": null, "name": "mysql", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "mysql-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/567f1ff8-57bd-4689-a732-1e1705bc5082/", "run_command": "/run.sh", "service": "/api/v1/service/326a2daf-2069-4cd4-9e44-08faa068a62f/", "started_datetime": "Wed, 1 Oct 2014 14:44:32 +0000", "state": "Running", "stopped_datetime": null, "unique_name": "mysql-1", "uuid": "567f1ff8-57bd-4689-a732-1e1705bc5082"}, {"autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_ports": [{"endpoint_uri": "http://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160/", "inner_port": 80, "outer_port": 49160, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}, {"endpoint_uri": "mysql://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161/", "inner_port": 3306, "outer_port": 49161, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": null, "exit_code_msg": null, "image_name": "tutum/wordpress:latest", "image_tag": "/api/v1/image/tutum/wordpress/tag/latest/", "is_dead_backend": null, "memory": null, "memory_swap": null, "name": "wordpress", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d/", "run_command": "/run.sh", "service": "/api/v1/service/81bbc30a-35de-4f5e-840d-87bc2573d818/", "started_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "state": "Running", "stopped_datetime": null, "unique_name": "wordpress-1", "uuid": "52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d"}]' + ) + mock_send.return_value = fake_resp(fake_container_list) + containers = dockercloud.Container.list() + for i in range(0, len(containers)): + result = json.loads(json.dumps(containers[i].get_all_attributes())) + target = json.loads(json.dumps(attributes[i])) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_container_fetch(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/62b1f681-5c0a-4b4e-8bc7-570f62020711/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": "http://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160/", "inner_port": 80, "outer_port": 49160, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}, {"endpoint_uri": "mysql://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161/", "inner_port": 3306, "outer_port": 49161, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": null, "exit_code_msg": null, "image_name": "tutum/wordpress:latest", "image_tag": "/api/v1/image/tutum/wordpress/tag/latest/", "is_dead_backend": null, "link_variables": {"WORDPRESS_1_ENV_HOME": "/", "WORDPRESS_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_1_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_1_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_1_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_1_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_3306_TCP_PORT": "49161", "WORDPRESS_1_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_1_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_1_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_80_TCP_PORT": "49160", "WORDPRESS_1_PORT_80_TCP_PROTO": "tcp", "WORDPRESS_ENV_HOME": "/", "WORDPRESS_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_3306_TCP_PORT": "49161", "WORDPRESS_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_80_TCP_PORT": "49160", "WORDPRESS_PORT_80_TCP_PROTO": "tcp"}, "memory": null, "memory_swap": null, "name": "wordpress", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d/", "roles": [], "run_command": "/run.sh", "service": "/api/v1/service/81bbc30a-35de-4f5e-840d-87bc2573d818/", "started_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "state": "Running", "stopped_datetime": null, "unique_name": "wordpress-1", "uuid": "52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d"}' + ) + mock_send.return_value = fake_resp(fake_container_fetch) + container = dockercloud.Container.fetch('52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d') + self.assertTrue(container.start()) + result = json.loads(json.dumps(container.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_container_delete(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/62b1f681-5c0a-4b4e-8bc7-570f62020711/", "/api/v1/action/5c435933-a1d9-449e-acb6-fbe7b5904b26/", "/api/v1/action/2508d37c-57a8-4ef1-88ff-3b5e20f88b7f/", "/api/v1/action/0438248f-b05c-41f2-b715-40a788735deb/", "/api/v1/action/6a5b82a0-b14a-4c05-94d0-117d730cb647/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": "http://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160/", "inner_port": 80, "outer_port": 49160, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}, {"endpoint_uri": "mysql://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161/", "inner_port": 3306, "outer_port": 49161, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": 0, "exit_code_msg": "Exit code 0 (Success)", "image_name": "tutum/wordpress:latest", "image_tag": "/api/v1/image/tutum/wordpress/tag/latest/", "is_dead_backend": null, "link_variables": {"WORDPRESS_1_ENV_HOME": "/", "WORDPRESS_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_1_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_1_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_1_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_1_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_3306_TCP_PORT": "49161", "WORDPRESS_1_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_1_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_1_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_80_TCP_PORT": "49160", "WORDPRESS_1_PORT_80_TCP_PROTO": "tcp", "WORDPRESS_ENV_HOME": "/", "WORDPRESS_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_3306_TCP_PORT": "49161", "WORDPRESS_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_80_TCP_PORT": "49160", "WORDPRESS_PORT_80_TCP_PROTO": "tcp"}, "memory": null, "memory_swap": null, "name": "wordpress", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d/", "roles": [], "run_command": "/run.sh", "service": "/api/v1/service/81bbc30a-35de-4f5e-840d-87bc2573d818/", "started_datetime": "Wed, 1 Oct 2014 15:22:51 +0000", "state": "Terminating", "stopped_datetime": "Wed, 1 Oct 2014 15:20:58 +0000", "unique_name": "wordpress-1", "uuid": "52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d"}' + ) + mock_send.side_effect = [fake_resp(fake_container_fetch), fake_resp(fake_container_delete)] + container = dockercloud.Container.fetch('52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d') + self.assertTrue(container.delete()) + result = json.loads(json.dumps(container.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_container_start(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/62b1f681-5c0a-4b4e-8bc7-570f62020711/", "/api/v1/action/5c435933-a1d9-449e-acb6-fbe7b5904b26/", "/api/v1/action/2508d37c-57a8-4ef1-88ff-3b5e20f88b7f/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": "http://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160/", "inner_port": 80, "outer_port": 49160, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}, {"endpoint_uri": "mysql://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161/", "inner_port": 3306, "outer_port": 49161, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": 0, "exit_code_msg": "Exit code 0 (Success)", "image_name": "tutum/wordpress:latest", "image_tag": "/api/v1/image/tutum/wordpress/tag/latest/", "is_dead_backend": null, "link_variables": {"WORDPRESS_1_ENV_HOME": "/", "WORDPRESS_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_1_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_1_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_1_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_1_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_3306_TCP_PORT": "49161", "WORDPRESS_1_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_1_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_1_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_80_TCP_PORT": "49160", "WORDPRESS_1_PORT_80_TCP_PROTO": "tcp", "WORDPRESS_ENV_HOME": "/", "WORDPRESS_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_3306_TCP_PORT": "49161", "WORDPRESS_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_80_TCP_PORT": "49160", "WORDPRESS_PORT_80_TCP_PROTO": "tcp"}, "memory": null, "memory_swap": null, "name": "wordpress", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d/", "roles": [], "run_command": "/run.sh", "service": "/api/v1/service/81bbc30a-35de-4f5e-840d-87bc2573d818/", "started_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "state": "Starting", "stopped_datetime": "Wed, 1 Oct 2014 15:20:58 +0000", "unique_name": "wordpress-1", "uuid": "52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d"}' + ) + mock_send.side_effect = [fake_resp(fake_container_fetch), fake_resp(fake_container_start)] + container = dockercloud.Container.fetch('52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d') + self.assertTrue(container.start()) + result = json.loads(json.dumps(container.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_container_stop(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/62b1f681-5c0a-4b4e-8bc7-570f62020711/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": "http://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160/", "inner_port": 80, "outer_port": 49160, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}, {"endpoint_uri": "mysql://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161/", "inner_port": 3306, "outer_port": 49161, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "cpu_shares": null, "deployed_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "destroyed_datetime": null, "entrypoint": "", "exit_code": null, "exit_code_msg": null, "image_name": "tutum/wordpress:latest", "image_tag": "/api/v1/image/tutum/wordpress/tag/latest/", "is_dead_backend": null, "link_variables": {"WORDPRESS_1_ENV_HOME": "/", "WORDPRESS_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_1_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_1_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_1_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_1_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_3306_TCP_PORT": "49161", "WORDPRESS_1_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_1_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_1_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_1_PORT_80_TCP_PORT": "49160", "WORDPRESS_1_PORT_80_TCP_PROTO": "tcp", "WORDPRESS_ENV_HOME": "/", "WORDPRESS_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "WORDPRESS_ENV_PHP_POST_MAX_SIZE": "10M", "WORDPRESS_ENV_PHP_UPLOAD_MAX_FILESIZE": "10M", "WORDPRESS_PORT_3306_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49161", "WORDPRESS_PORT_3306_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_3306_TCP_PORT": "49161", "WORDPRESS_PORT_3306_TCP_PROTO": "tcp", "WORDPRESS_PORT_80_TCP": "tcp://wordpress-1.fa9df19a-tifayuki.node.docker.io:49160", "WORDPRESS_PORT_80_TCP_ADDR": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "WORDPRESS_PORT_80_TCP_PORT": "49160", "WORDPRESS_PORT_80_TCP_PROTO": "tcp"}, "memory": null, "memory_swap": null, "name": "wordpress", "node": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "public_dns": "wordpress-1.fa9df19a-tifayuki.node.docker.io", "resource_uri": "/api/v1/container/52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d/", "roles": [], "run_command": "/run.sh", "service": "/api/v1/service/81bbc30a-35de-4f5e-840d-87bc2573d818/", "started_datetime": "Wed, 1 Oct 2014 14:54:23 +0000", "state": "Stopping", "stopped_datetime": null, "unique_name": "wordpress-1", "uuid": "52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d"}' + ) + mock_send.side_effect = [fake_resp(fake_container_fetch), fake_resp(fake_container_stop)] + container = dockercloud.Container.fetch('52fffbca-88b2-4eac-a66d-8f8ca7e3ff2d') + self.assertTrue(container.stop()) + result = json.loads(json.dumps(container.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) diff --git a/tests/test_http.py b/tests/test_http.py new file mode 100644 index 0000000..88802a4 --- /dev/null +++ b/tests/test_http.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import + +import unittest + +import requests +import unittest.mock as mock + +import dockercloud +from dockercloud.api.base import send_request +from .fake_api import fake_resp + + +class SendRequestTestCase(unittest.TestCase): + @mock.patch('dockercloud.api.http.Request', return_value=requests.Request('GET', 'http://fake.com')) + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_http_send_request(self, mock_send, mock_Request): + json_obj = {'key': 'value'} + mock_send.return_value = fake_resp(lambda: (None, json_obj)) + self.assertRaises(dockercloud.ApiError, send_request, 'METHOD', 'path', data='data') + headers = {'Content-Type': 'application/json', 'User-Agent': 'python-dockercloud/%s' % dockercloud.__version__} + headers.update(dockercloud.auth.get_auth_header()) + + mock_send.return_value = fake_resp(lambda: (200, json_obj)) + self.assertEqual(json_obj, send_request('METHOD', 'path')) + + mock_send.return_value = fake_resp(lambda: (204, json_obj)) + self.assertIsNone(send_request('METHOD', 'path')) + + mock_send.return_value = fake_resp(lambda: (401, json_obj)) + self.assertRaises(dockercloud.AuthError, send_request, 'METHOD', 'path') + + mock_send.return_value = fake_resp(lambda: (500, json_obj)) + self.assertRaises(dockercloud.ApiError, send_request, 'METHOD', 'path') diff --git a/tests/test_node.py b/tests/test_node.py new file mode 100644 index 0000000..6afa961 --- /dev/null +++ b/tests/test_node.py @@ -0,0 +1,61 @@ +from __future__ import absolute_import + +import unittest + +import unittest.mock as mock + +import dockercloud +from .fake_api import * + + +class NodeTestCase(unittest.TestCase): + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_node_list(self, mock_send): + attributes = json.loads( + '[{"deployed_datetime": "Mon, 29 Sep 2014 22:29:03 +0000", "destroyed_datetime": null, "docker_execdriver": "native-0.2", "docker_graphdriver": "aufs", "docker_version": "1.2.0", "external_fqdn": "fa9df19a-tifayuki.node.docker.io", "last_seen": "Tue, 30 Sep 2014 15:27:05 +0000", "node_cluster": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "public_ip": "198.199.97.190", "region": "/api/v1/region/digitalocean/sfo1/", "resource_uri": "/api/v1/node/fa9df19a-162b-45b4-bb5a-152dfd1b133f/", "state": "Deployed", "uuid": "fa9df19a-162b-45b4-bb5a-152dfd1b133f"}, ' \ + '{"deployed_datetime": "Mon, 29 Sep 2014 22:59:47 +0000", "destroyed_datetime": null, "docker_execdriver": "native-0.2", "docker_graphdriver": "aufs", "docker_version": "1.2.0", "external_fqdn": "43b5ebaf-tifayuki.node.docker.io", "last_seen": "Tue, 30 Sep 2014 15:27:06 +0000", "node_cluster": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "public_ip": "178.62.20.100", "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/node/43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456/", "state": "Deployed", "uuid": "43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456"}]' + ) + mock_send.return_value = fake_resp(fake_node_list) + nodes = dockercloud.Node.list() + for i in range(0, len(nodes)): + result = json.loads(json.dumps(nodes[i].get_all_attributes())) + target = json.loads(json.dumps(attributes[i])) + self.assertDictEqual(target, result) + + def test_node_save(self): + self.assertRaises(AttributeError, dockercloud.Node().save) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_node_fetch(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/8f5b893b-826e-40b7-bb8b-2d96301425f2/"], "deployed_datetime": "Mon, 29 Sep 2014 22:59:47 +0000", "destroyed_datetime": null, "docker_execdriver": "native-0.2", "docker_graphdriver": "aufs", "docker_version": "1.2.0", "external_fqdn": "43b5ebaf-tifayuki.node.docker.io", "last_seen": "Tue, 30 Sep 2014 15:30:06 +0000", "node_cluster": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "public_ip": "178.62.20.100", "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/node/43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456/", "state": "Deployed", "uuid": "43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456"}' + ) + mock_send.return_value = fake_resp(fake_node_fetch) + node = dockercloud.Node.fetch('43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456') + result = json.loads(json.dumps(node.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_node_delete(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/8f5b893b-826e-40b7-bb8b-2d96301425f2/", "/api/v1/action/83c66611-ea5d-45d7-a97d-914029a90524/"], "deployed_datetime": "Mon, 29 Sep 2014 22:59:47 +0000", "destroyed_datetime": null, "docker_execdriver": "native-0.2", "docker_graphdriver": "aufs", "docker_version": "1.2.0", "external_fqdn": "43b5ebaf-tifayuki.node.docker.io", "last_seen": "Tue, 30 Sep 2014 15:38:12 +0000", "node_cluster": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "public_ip": "178.62.20.100", "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/node/43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456/", "state": "Terminating", "uuid": "43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456"}' + ) + mock_send.return_value = fake_resp(fake_node_delete) + node = dockercloud.Node.fetch('43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456') + self.assertTrue(node.delete()) + result = json.loads(json.dumps(node.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_node_deploy(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/8f5b893b-826e-40b7-bb8b-2d96301425f2/", "/api/v1/action/83c66611-ea5d-45d7-a97d-914029a90524/"], "deployed_datetime": "Mon, 29 Sep 2014 22:59:47 +0000", "destroyed_datetime": null, "docker_execdriver": "native-0.2", "docker_graphdriver": "aufs", "docker_version": "1.2.0", "external_fqdn": "43b5ebaf-tifayuki.node.docker.io", "last_seen": "Tue, 30 Sep 2014 15:38:12 +0000", "node_cluster": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "public_ip": "178.62.20.100", "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/node/43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456/", "state": "Starting", "uuid": "43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456"}' + ) + mock_send.return_value = fake_resp(fake_node_deploy) + node = dockercloud.Node.fetch('43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456') + self.assertTrue(node.deploy()) + result = json.loads(json.dumps(node.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) diff --git a/tests/test_nodecluster.py b/tests/test_nodecluster.py new file mode 100644 index 0000000..20548d7 --- /dev/null +++ b/tests/test_nodecluster.py @@ -0,0 +1,73 @@ +from __future__ import absolute_import + +import unittest + +import unittest.mock as mock + +import dockercloud +from .fake_api import * + + +class NodeClusterTestCase(unittest.TestCase): + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_nodecluster_list(self, mock_send): + attributes = json.loads( + '[{"current_num_nodes": 1, "deployed_datetime": "Mon, 29 Sep 2014 22:29:03 +0000", "destroyed_datetime": null, "name": "test", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "region": "/api/v1/region/digitalocean/sfo1/", "resource_uri": "/api/v1/nodecluster/a02c3763-e639-46fc-a6db-587f4dbb5444/", "state": "Deployed", "target_num_nodes": 1, "uuid": "a02c3763-e639-46fc-a6db-587f4dbb5444"}, ' + '{"current_num_nodes": 1, "deployed_datetime": null, "destroyed_datetime": null, "name": "test2", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "state": "Deploying", "target_num_nodes": 1, "uuid": "b616a720-6684-42c6-83bb-4d298b11b3f3"}]' + ) + mock_send.return_value = fake_resp(fake_nodeclster_list) + clusters = dockercloud.NodeCluster.list() + for i in range(0, len(clusters)): + result = json.loads(json.dumps(clusters[i].get_all_attributes())) + target = json.loads(json.dumps(attributes[i])) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_nodecluster_fetch(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/bf02b00a-e2fc-4098-8b69-1424b659ef4a/", "/api/v1/action/f8dce6d4-5c41-46a9-9754-baa8f3cdf031/"], "current_num_nodes": 1, "deployed_datetime": null, "destroyed_datetime": null, "name": "test2", "node_type": "/api/v1/nodetype/digitalocean/512mb/", "nodes": ["/api/v1/node/43b5ebaf-5b9c-4ed3-a1e5-3d91cea70456/"], "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/nodecluster/b616a720-6684-42c6-83bb-4d298b11b3f3/", "state": "Init", "target_num_nodes": 1, "uuid": "b616a720-6684-42c6-83bb-4d298b11b3f3"}' + ) + mock_send.return_value = fake_resp(fake_nodecluster_fetch) + cluster = dockercloud.NodeCluster.fetch('b616a720-6684-42c6-83bb-4d298b11b3f3') + result = json.loads(json.dumps(cluster.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_nodecluster_save(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/f47e26a6-c60c-416f-a0a9-ddf14e3aae83/"], "current_num_nodes": 1, "deployed_datetime": null, "destroyed_datetime": null, "name": "my_cluster", "node_type": "/api/v1/nodetype/digitalocean/1gb/", "nodes": ["/api/v1/node/2cfe7823-f551-4c7b-a82c-f6ab31d7ca25/"], "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/nodecluster/e7915a74-618b-4908-9189-dce965465702/", "state": "Init", "target_num_nodes": 1, "uuid": "e7915a74-618b-4908-9189-dce965465702"}' + ) + mock_send.return_value = fake_resp(fake_nodecluster_save) + cluster = dockercloud.NodeCluster.create(name="my_cluster", region="/api/v1/region/digitalocean/lon1/", + node_type="/api/v1/nodetype/digitalocean/1gb/") + self.assertTrue(cluster.save()) + result = json.loads(json.dumps(cluster.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_nodecluster_deploy(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/f47e26a6-c60c-416f-a0a9-ddf14e3aae83/", "/api/v1/action/d110016e-e65d-4ce7-9f11-50c6302494a6/"], "current_num_nodes": 1, "deployed_datetime": null, "destroyed_datetime": null, "name": "my_cluster", "node_type": "/api/v1/nodetype/digitalocean/1gb/", "nodes": ["/api/v1/node/2cfe7823-f551-4c7b-a82c-f6ab31d7ca25/"], "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/nodecluster/e7915a74-618b-4908-9189-dce965465702/", "state": "Deploying", "target_num_nodes": 1, "uuid": "e7915a74-618b-4908-9189-dce965465702"}' + ) + mock_send.side_effect = [fake_resp(fake_nodecluster_save), fake_resp(fake_nodecluster_deploy)] + cluster = dockercloud.NodeCluster.create(name="my_cluster", region="/api/v1/region/digitalocean/lon1/", + node_type="/api/v1/nodetype/digitalocean/1gb/") + cluster.save() + self.assertTrue(cluster.deploy()) + result = json.loads(json.dumps(cluster.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_nodecluster_delete(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/f47e26a6-c60c-416f-a0a9-ddf14e3aae83/", "/api/v1/action/d110016e-e65d-4ce7-9f11-50c6302494a6/", "/api/v1/action/e33b4bb1-192b-46a6-a1ba-eadfc494c2dd/"], "current_num_nodes": 1, "deployed_datetime": "Mon, 29 Sep 2014 23:45:45 +0000", "destroyed_datetime": null, "name": "my_cluster", "node_type": "/api/v1/nodetype/digitalocean/1gb/", "nodes": ["/api/v1/node/2cfe7823-f551-4c7b-a82c-f6ab31d7ca25/"], "region": "/api/v1/region/digitalocean/lon1/", "resource_uri": "/api/v1/nodecluster/e7915a74-618b-4908-9189-dce965465702/", "state": "Terminating", "target_num_nodes": 0, "uuid": "e7915a74-618b-4908-9189-dce965465702"}' + ) + mock_send.return_value = fake_resp(fake_nodecluster_delete) + cluster = dockercloud.NodeCluster.fetch('e7915a74-618b-4908-9189-dce965465702') + self.assertTrue(cluster.delete()) + result = json.loads(json.dumps(cluster.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) diff --git a/tests/test_nodeprovider.py b/tests/test_nodeprovider.py new file mode 100644 index 0000000..357b752 --- /dev/null +++ b/tests/test_nodeprovider.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import + +import unittest + +import unittest.mock as mock + +import dockercloud +from .fake_api import * + + +class ProviderTestCase(unittest.TestCase): + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_provider_list(self, mock_send): + attributes = json.loads( + '[{"available": true, "label": "Digital Ocean", "name": "digitalocean", "regions": ["/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/ams3/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/sgp1/"], "resource_uri": "/api/v1/provider/digitalocean/"}]' + ) + mock_send.return_value = fake_resp(fake_provider_list) + providers = dockercloud.Provider.list() + for i in range(0, len(providers)): + result = json.loads(json.dumps(providers[i].get_all_attributes())) + target = json.loads(json.dumps(attributes[i])) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_provider_fetch(self, mock_send): + attribute = json.loads( + '{"available": true, "label": "Digital Ocean", "name": "digitalocean", "regions": ["/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/ams3/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/sgp1/"], "resource_uri": "/api/v1/provider/digitalocean/"}' + ) + mock_send.return_value = fake_resp(fake_provider_fetch) + provider = dockercloud.Provider.fetch("digitalocean") + result = json.loads(json.dumps(provider.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) diff --git a/tests/test_noderegion.py b/tests/test_noderegion.py new file mode 100644 index 0000000..bc40a7e --- /dev/null +++ b/tests/test_noderegion.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import + +import unittest + +import unittest.mock as mock + +import dockercloud +from .fake_api import * + + +class RegionTestCase(unittest.TestCase): + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_region_list(self, mock_send): + attrributes = json.loads( + '[{"availability_zones": [], "available": true, "label": "Amsterdam 1", "name": "ams1", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/"], "resource_uri": "/api/v1/region/digitalocean/ams1/"}, ' \ + '{"availability_zones": [], "available": true, "label": "San Francisco 1", "name": "sfo1", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/sfo1/"}, ' \ + '{"availability_zones": [], "available": true, "label": "New York 2", "name": "nyc2", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/nyc2/"}, ' \ + '{"availability_zones": [], "available": true, "label": "Amsterdam 2", "name": "ams2", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/ams2/"}, ' \ + '{"availability_zones": [], "available": true, "label": "Singapore 1", "name": "sgp1", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/sgp1/"}, ' \ + '{"availability_zones": [], "available": true, "label": "London 1", "name": "lon1", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/lon1/"}, ' \ + '{"availability_zones": [], "available": true, "label": "New York 3", "name": "nyc3", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/nyc3/"}, ' \ + '{"availability_zones": [], "available": true, "label": "Amsterdam 3", "name": "ams3", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/", "/api/v1/nodetype/digitalocean/32gb/", "/api/v1/nodetype/digitalocean/48gb/", "/api/v1/nodetype/digitalocean/64gb/"], "resource_uri": "/api/v1/region/digitalocean/ams3/"}]' + ) + + mock_send.return_value = fake_resp(fake_region_list) + regions = dockercloud.Region.list() + for i in range(0, len(regions)): + result = json.loads(json.dumps(regions[i].get_all_attributes())) + target = json.loads(json.dumps(attrributes[i])) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_region_fet(self, mock_send): + attribute = json.loads( + '{"availability_zones": [], "available": true, "label": "Amsterdam 1", "name": "ams1", "node_types": ["/api/v1/nodetype/digitalocean/512mb/", "/api/v1/nodetype/digitalocean/1gb/", "/api/v1/nodetype/digitalocean/2gb/", "/api/v1/nodetype/digitalocean/4gb/", "/api/v1/nodetype/digitalocean/8gb/", "/api/v1/nodetype/digitalocean/16gb/"], "provider": "/api/v1/provider/digitalocean/", "resource_uri": "/api/v1/region/digitalocean/ams1/"}' + ) + mock_send.return_value = fake_resp(fake_region_fetch) + region = dockercloud.Region.fetch('digitalocean/asm1') + result = json.loads(json.dumps(region.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) diff --git a/tests/test_nodetype.py b/tests/test_nodetype.py new file mode 100644 index 0000000..1cfc4f3 --- /dev/null +++ b/tests/test_nodetype.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import + +import unittest + +import unittest.mock as mock + +import dockercloud +from .fake_api import * + + +class NodeTypeTestCase(unittest.TestCase): + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_nodetype_list(self, mock_send): + attributes = json.loads( + '[{"availability_zones": [], "available": true, "label": "512MB", "name": "512mb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/512mb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "1GB", "name": "1gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/1gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "2GB", "name": "2gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/2gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "4GB", "name": "4gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/4gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "8GB", "name": "8gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/8gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "16GB", "name": "16gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/16gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "32GB", "name": "32gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/32gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "48GB", "name": "48gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/48gb/"}, ' \ + '{"availability_zones": [], "available": true, "label": "64GB", "name": "64gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/64gb/"}]' + ) + mock_send.return_value = fake_resp(fake_nodetype_list) + nodetypes = dockercloud.NodeType.list() + for i in range(0, len(nodetypes)): + result = json.loads(json.dumps(nodetypes[i].get_all_attributes())) + target = json.loads(json.dumps(attributes[i])) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_nodetype_fetch(self, mock_send): + attribute = json.loads( + '{"availability_zones": [], "available": true, "label": "8GB", "name": "8gb", "provider": "/api/v1/provider/digitalocean/", "regions": ["/api/v1/region/digitalocean/nyc1/", "/api/v1/region/digitalocean/ams1/", "/api/v1/region/digitalocean/sfo1/", "/api/v1/region/digitalocean/nyc2/", "/api/v1/region/digitalocean/ams2/", "/api/v1/region/digitalocean/sgp1/", "/api/v1/region/digitalocean/lon1/", "/api/v1/region/digitalocean/nyc3/", "/api/v1/region/digitalocean/ams3/"], "resource_uri": "/api/v1/nodetype/digitalocean/8gb/"}' + ) + mock_send.return_value = fake_resp(fake_nodetype_fetch) + nodetype = dockercloud.NodeType.fetch('digitalocean/8gb') + result = json.loads(json.dumps(nodetype.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) diff --git a/tests/test_repository.py b/tests/test_repository.py new file mode 100644 index 0000000..82cb209 --- /dev/null +++ b/tests/test_repository.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import + +import unittest + +import unittest.mock as mock + +import dockercloud +from .fake_api import * + + +class ImageTestCase(unittest.TestCase): + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_image_list(self, mock_send): + attributes = json.loads( + '[{"base_image": false, "categories": [], "cluster_aware": true, "description": "", "docker_registry": "/api/v1/registry/docker.com/", "image_url": "", "imagetag_set": ["/api/v1/image/docker.com/tifayuki/mongodb/tag/latest/"], "is_private_image": true, "name": "docker.com/tifayuki/mongodb", "public_url": "", "resource_uri": "/api/v1/image/docker.com/tifayuki/mongodb/", "starred": false}]' + ) + mock_send.return_value = fake_resp(fake_image_list) + images = dockercloud.Repository.list() + for i in range(0, len(images)): + result = json.loads(json.dumps(images[i].get_all_attributes())) + target = json.loads(json.dumps(attributes[i])) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_image_fetch(self, mock_send): + attribute = json.loads( + '{"base_image": false, "categories": [], "cluster_aware": true, "description": "", "docker_registry": {"created": true, "host": "registry.hub.docker.com", "id": 5, "image_url": "/_static/assets/images/dockerregistries/docker.png", "is_ssl": true, "is_tutum_registry": false, "modified": true, "name": "Docker.io", "resource_uri": "/api/v1/registry/registry.hub.docker.com/", "uuid": "d533039e-c44c-4cdc-951b-e0e03b8410c6"}, "image_url": "", "imagetag_set": [{"full_name": "tifayuki/cadvisor:latest", "image": {"author": "fake ", "docker_id": "9e2907ef52bf811b4da100f50ba8f0908ccc610c7054bd69087f0a9f4703efdd", "entrypoint": "", "image_creation": "Fri, 15 Aug 2014 15:19:04 +0000", "imageenvvar_set": [{"key": "CADVISOR_TAG", "value": "0.2.2"}, {"key": "DB_NAME", "value": "cadvisor"}, {"key": "DB_PASS", "value": "root"}, {"key": "DB_USER", "value": "root"}, {"key": "HOME", "value": "/"}, {"key": "PATH", "value": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}], "imageport_set": [], "run_command": "/run.sh"}, "image_info": "/api/v1/image/tifayuki/cadvisor/", "name": "latest", "resource_uri": "/api/v1/image/tifayuki/cadvisor/tag/latest/"}], "is_private_image": true, "name": "tifayuki/cadvisor", "public_url": "https://registry.hub.docker.com/u/tifayuki/cadvisor/", "resource_uri": "/api/v1/image/tifayuki/cadvisor/", "starred": false}' + ) + mock_send.return_value = fake_resp(fake_image_fetch) + image = dockercloud.Repository.fetch('tifayuki/cadvisor') + result = json.loads(json.dumps(image.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_image_save(self, mock_send): + attribute = json.loads( + '{"base_image": false, "categories": [], "cluster_aware": true, "description": "description", "docker_registry": {"created": true, "host": "registry.hub.docker.com", "id": 5, "image_url": "/_static/assets/images/dockerregistries/docker.png", "is_ssl": true, "is_tutum_registry": false, "modified": true, "name": "Docker.io", "resource_uri": "/api/v1/registry/registry.hub.docker.com/", "uuid": "d533039e-c44c-4cdc-951b-e0e03b8410c6"}, "image_url": "", "imagetag_set": [{"full_name": "tifayuki/cadvisor:latest", "image": {"author": "fake ", "docker_id": "9e2907ef52bf811b4da100f50ba8f0908ccc610c7054bd69087f0a9f4703efdd", "entrypoint": "", "image_creation": "Fri, 15 Aug 2014 15:19:04 +0000", "imageenvvar_set": [{"key": "CADVISOR_TAG", "value": "0.2.2"}, {"key": "DB_NAME", "value": "cadvisor"}, {"key": "DB_PASS", "value": "root"}, {"key": "DB_USER", "value": "root"}, {"key": "HOME", "value": "/"}, {"key": "PATH", "value": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}], "imageport_set": [], "run_command": "/run.sh"}, "image_info": "/api/v1/image/tifayuki/cadvisor/", "name": "latest", "resource_uri": "/api/v1/image/tifayuki/cadvisor/tag/latest/"}], "is_private_image": true, "name": "tifayuki/cadvisor", "public_url": "https://registry.hub.docker.com/u/tifayuki/cadvisor/", "resource_uri": "/api/v1/image/tifayuki/cadvisor/", "starred": false}' + ) + mock_send.return_value = fake_resp(fake_image_save) + image = dockercloud.Repository.fetch('tifayuki/cadvisor') + image.description = 'descripiton' + self.assertTrue(image.save()) + result = json.loads(json.dumps(image.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_image_delete(self, mock_send): + mock_send.side_effect = [fake_resp(fake_image_fetch), fake_resp(fake_image_delete)] + image = dockercloud.Repository.fetch('tifayuki/cadvisor') + self.assertTrue(image.delete()) diff --git a/tests/test_service.py b/tests/test_service.py new file mode 100644 index 0000000..e210625 --- /dev/null +++ b/tests/test_service.py @@ -0,0 +1,93 @@ +from __future__ import absolute_import + +import unittest + +import unittest.mock as mock + +import dockercloud +from .fake_api import * + + +class ServiceTestCase(unittest.TestCase): + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_service_list(self, mock_send): + attributes = json.loads( + '[{"autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_ports": [{"endpoint_uri": null, "inner_port": 80, "outer_port": null, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}], "cpu_shares": null, "current_num_containers": 3, "deployed_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/hello-world:latest", "image_tag": "/api/v1/image/tutum/hello-world/tag/latest/", "memory": null, "memory_swap": null, "name": "hello-world", "resource_uri": "/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/", "run_command": "/run.sh", "running_num_containers": 3, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "state": "Running", "stopped_datetime": null, "stopped_num_containers": 0, "target_num_containers": 3, "unique_name": "hello-world", "uuid": "a2ac25c9-7cfe-4a1b-9d97-66de23642ee8"}]' + ) + mock_send.return_value = fake_resp(fake_service_list) + services = dockercloud.Service.list() + for i in range(0, len(services)): + result = json.loads(json.dumps(services[i].get_all_attributes())) + target = json.loads(json.dumps(attributes[i])) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_service_fetch(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/e3ee01df-9f2f-4720-a114-ea1a236d47d2/", "/api/v1/action/c58213ab-8d5c-4a6d-b4c3-bd7157242dc2/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 80, "outer_port": null, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}], "containers": ["/api/v1/container/cff4dfa7-28a5-4599-a3f9-c7dc39353c11/", "/api/v1/container/4d966087-5169-4a0b-a2f0-78bbb878d872/", "/api/v1/container/0c84cd78-c239-40ad-939e-dbbc372ae345/"], "cpu_shares": null, "current_num_containers": 3, "deployed_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/hello-world:latest", "image_tag": "/api/v1/image/tutum/hello-world/tag/latest/", "link_variables": {"HELLO_WORLD_1_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_1_ENV_HOME": "/", "HELLO_WORLD_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_1_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_1_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_1_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_2_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_2_ENV_HOME": "/", "HELLO_WORLD_2_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_2_PORT": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP_ADDR": "hello-world-2.a2f5a2e9-tifayuki.node.docker.io", "HELLO_WORLD_2_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_2_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_3_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_3_ENV_HOME": "/", "HELLO_WORLD_3_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_3_PORT": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP_ADDR": "hello-world-3.5067d4f4-tifayuki.node.docker.io", "HELLO_WORLD_3_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_3_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_ENV_HOME": "/", "HELLO_WORLD_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "hello-world", "resource_uri": "/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/", "roles": [], "run_command": "/run.sh", "running_num_containers": 3, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "state": "Running", "stopped_datetime": null, "stopped_num_containers": 0, "target_num_containers": 3, "unique_name": "hello-world", "uuid": "a2ac25c9-7cfe-4a1b-9d97-66de23642ee8"}') + mock_send.return_value = fake_resp(fake_service_fetch) + service = dockercloud.Service.fetch('a2ac25c9-7cfe-4a1b-9d97-66de23642ee8') + result = json.loads(json.dumps(service.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_service_save(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/e3ee01df-9f2f-4720-a114-ea1a236d47d2/", "/api/v1/action/c58213ab-8d5c-4a6d-b4c3-bd7157242dc2/", "/api/v1/action/b3adfa79-dbd2-41a4-8c71-5e242ecce9bb/", "/api/v1/action/f82e25e7-d550-454e-a897-8599f2f530e5/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 80, "outer_port": null, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}], "containers": ["/api/v1/container/cff4dfa7-28a5-4599-a3f9-c7dc39353c11/", "/api/v1/container/4d966087-5169-4a0b-a2f0-78bbb878d872/", "/api/v1/container/0c84cd78-c239-40ad-939e-dbbc372ae345/"], "cpu_shares": null, "current_num_containers": 3, "deployed_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/hello-world:latest", "image_tag": "/api/v1/image/tutum/hello-world/tag/latest/", "link_variables": {"HELLO_WORLD_1_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_1_ENV_HOME": "/", "HELLO_WORLD_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_1_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_1_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_1_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_2_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_2_ENV_HOME": "/", "HELLO_WORLD_2_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_2_PORT": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP_ADDR": "hello-world-2.a2f5a2e9-tifayuki.node.docker.io", "HELLO_WORLD_2_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_2_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_3_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_3_ENV_HOME": "/", "HELLO_WORLD_3_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_3_PORT": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP_ADDR": "hello-world-3.5067d4f4-tifayuki.node.docker.io", "HELLO_WORLD_3_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_3_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_ENV_HOME": "/", "HELLO_WORLD_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "hello-world", "resource_uri": "/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/", "roles": [], "run_command": "/run.sh", "running_num_containers": 3, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "state": "Scaling", "stopped_datetime": null, "stopped_num_containers": 0, "target_num_containers": 5, "unique_name": "hello-world", "uuid": "a2ac25c9-7cfe-4a1b-9d97-66de23642ee8", "web_public_dns": ""}' + ) + mock_send.side_effect = [fake_resp(fake_service_fetch), fake_resp(fake_service_save)] + service = dockercloud.Service.fetch('a2ac25c9-7cfe-4a1b-9d97-66de23642ee8') + service.target_num_containers = 5 + self.assertTrue(service.save()) + result = json.loads(json.dumps(service.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_service_delete(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/e3ee01df-9f2f-4720-a114-ea1a236d47d2/", "/api/v1/action/c58213ab-8d5c-4a6d-b4c3-bd7157242dc2/", "/api/v1/action/b3adfa79-dbd2-41a4-8c71-5e242ecce9bb/", "/api/v1/action/f82e25e7-d550-454e-a897-8599f2f530e5/", "/api/v1/action/381c53a0-bf18-4a01-a1b0-be87051c35e8/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 80, "outer_port": null, "port_name": "http", "protocol": "tcp", "uri_protocol": "http"}], "containers": ["/api/v1/container/cff4dfa7-28a5-4599-a3f9-c7dc39353c11/", "/api/v1/container/4d966087-5169-4a0b-a2f0-78bbb878d872/", "/api/v1/container/0c84cd78-c239-40ad-939e-dbbc372ae345/", "/api/v1/container/6e74df59-83ee-4351-8ba9-3d26e0d64c34/", "/api/v1/container/7bbff9f0-af41-408f-9dd0-213cd67e4aa2/"], "cpu_shares": null, "current_num_containers": 5, "deployed_datetime": "Tue, 30 Sep 2014 16:07:36 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/hello-world:latest", "image_tag": "/api/v1/image/tutum/hello-world/tag/latest/", "link_variables": {"HELLO_WORLD_1_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_1_ENV_HOME": "/", "HELLO_WORLD_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_1_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_1_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_1_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_1_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_2_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_2_ENV_HOME": "/", "HELLO_WORLD_2_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_2_PORT": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP": "tcp://hello-world-2.a2f5a2e9-tifayuki.node.docker.io:49155", "HELLO_WORLD_2_PORT_80_TCP_ADDR": "hello-world-2.a2f5a2e9-tifayuki.node.docker.io", "HELLO_WORLD_2_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_2_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_3_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_3_ENV_HOME": "/", "HELLO_WORLD_3_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_3_PORT": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP": "tcp://hello-world-3.5067d4f4-tifayuki.node.docker.io:49155", "HELLO_WORLD_3_PORT_80_TCP_ADDR": "hello-world-3.5067d4f4-tifayuki.node.docker.io", "HELLO_WORLD_3_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_3_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_4_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_4_ENV_HOME": "/", "HELLO_WORLD_4_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_4_PORT": "tcp://hello-world-4.5067d4f4-tifayuki.node.docker.io:49156", "HELLO_WORLD_4_PORT_80_TCP": "tcp://hello-world-4.5067d4f4-tifayuki.node.docker.io:49156", "HELLO_WORLD_4_PORT_80_TCP_ADDR": "hello-world-4.5067d4f4-tifayuki.node.docker.io", "HELLO_WORLD_4_PORT_80_TCP_PORT": "49156", "HELLO_WORLD_4_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_5_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_5_ENV_HOME": "/", "HELLO_WORLD_5_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_5_PORT": "tcp://hello-world-5.fa9df19a-tifayuki.node.docker.io:49157", "HELLO_WORLD_5_PORT_80_TCP": "tcp://hello-world-5.fa9df19a-tifayuki.node.docker.io:49157", "HELLO_WORLD_5_PORT_80_TCP_ADDR": "hello-world-5.fa9df19a-tifayuki.node.docker.io", "HELLO_WORLD_5_PORT_80_TCP_PORT": "49157", "HELLO_WORLD_5_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_ENV_DEBIAN_FRONTEND": "noninteractive", "HELLO_WORLD_ENV_HOME": "/", "HELLO_WORLD_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HELLO_WORLD_PORT": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP": "tcp://hello-world-1.fa9df19a-tifayuki.node.docker.io:49155", "HELLO_WORLD_PORT_80_TCP_ADDR": "hello-world-1.fa9df19a-t* Connection #0 to host dashboard.docker.com left intactifayuki.node.docker.io", "HELLO_WORLD_PORT_80_TCP_PORT": "49155", "HELLO_WORLD_PORT_80_TCP_PROTO": "tcp", "HELLO_WORLD_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "hello-world", "resource_uri": "/api/v1/service/a2ac25c9-7cfe-4a1b-9d97-66de23642ee8/", "roles": [], "run_command": "/run.sh", "running_num_containers": 3, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 22:57:36 +0000", "state": "Terminating", "stopped_datetime": null, "stopped_num_containers": 0, "target_num_containers": 0, "unique_name": "hello-world", "uuid": "a2ac25c9-7cfe-4a1b-9d97-66de23642ee8"}' + ) + mock_send.side_effect = [fake_resp(fake_service_fetch), fake_resp(fake_service_delete)] + service = dockercloud.Service.fetch('a2ac25c9-7cfe-4a1b-9d97-66de23642ee8') + self.assertTrue(service.delete()) + result = json.loads(json.dumps(service.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_service_start(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/a8aaf64b-3186-41e7-9256-6b3d69786036/", "/api/v1/action/2481790d-a860-4bb4-95d2-5676ae2d6748/", "/api/v1/action/799a0d06-efae-4bf1-b063-7141f722cbb1/", "/api/v1/action/ef6b9f59-1edb-44c7-bcc6-1b80f737e4b0/", "/api/v1/action/99b7ac29-d16d-448f-bac5-54dbd5dd3b7b/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 3306, "outer_port": null, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "containers": ["/api/v1/container/2a1c4057-7753-4393-98c6-35699c198e08/", "/api/v1/container/54ead360-698f-4354-96f7-538f686cdd69/"], "cpu_shares": null, "current_num_containers": 2, "deployed_datetime": "Tue, 30 Sep 2014 22:44:44 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/mysql:latest", "image_tag": "/api/v1/image/tutum/mysql/tag/latest/", "link_variables": {"MYSQL_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "mysql", "resource_uri": "/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/", "roles": [], "run_command": "/run.sh", "running_num_containers": 0, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 22:44:44 +0000", "state": "Starting", "stopped_datetime": "Tue, 30 Sep 2014 23:09:09 +0000", "stopped_num_containers": 2, "target_num_containers": 2, "unique_name": "mysql", "uuid": "5ecde92d-498b-4bbb-b773-a998e5e421dc"}' + ) + mock_send.side_effect = [fake_resp(fake_service_fetch), fake_resp(fake_service_start)] + service = dockercloud.Service.fetch('a2ac25c9-7cfe-4a1b-9d97-66de23642ee8') + self.assertTrue(service.start()) + result = json.loads(json.dumps(service.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_service_stop(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/a8aaf64b-3186-41e7-9256-6b3d69786036/", "/api/v1/action/2481790d-a860-4bb4-95d2-5676ae2d6748/", "/api/v1/action/799a0d06-efae-4bf1-b063-7141f722cbb1/", "/api/v1/action/ef6b9f59-1edb-44c7-bcc6-1b80f737e4b0/", "/api/v1/action/99b7ac29-d16d-448f-bac5-54dbd5dd3b7b/", "/api/v1/action/98816bfc-4fa7-4697-bea1-d926de69b48f/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 3306, "outer_port": null, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "containers": ["/api/v1/container/2a1c4057-7753-4393-98c6-35699c198e08/", "/api/v1/container/54ead360-698f-4354-96f7-538f686cdd69/"], "cpu_shares": null, "current_num_containers": 2, "deployed_datetime": "Tue, 30 Sep 2014 22:44:44 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/mysql:latest", "image_tag": "/api/v1/image/tutum/mysql/tag/latest/", "link_variables": {"MYSQL_1_ENV_DEBIAN_FRONTEND": "noninteractive", "MYSQL_1_ENV_MYSQL_PASS": "**Random**", "MYSQL_1_ENV_MYSQL_USER": "admin", "MYSQL_1_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "MYSQL_1_ENV_REPLICATION_MASTER": "**False**", "MYSQL_1_ENV_REPLICATION_PASS": "replica", "MYSQL_1_ENV_REPLICATION_SLAVE": "**False**", "MYSQL_1_ENV_REPLICATION_USER": "replica", "MYSQL_1_PORT": "tcp://mysql-1.fa9df19a-tifayuki.node.docker.io:49156", "MYSQL_1_PORT_3306_TCP": "tcp://mysql-1.fa9df19a-tifayuki.node.docker.io:49156", "MYSQL_1_PORT_3306_TCP_ADDR": "mysql-1.fa9df19a-tifayuki.node.docker.io", "MYSQL_1_PORT_3306_TCP_PORT": "49156", "MYSQL_1_PORT_3306_TCP_PROTO": "tcp", "MYSQL_2_ENV_DEBIAN_FRONTEND": "noninteractive", "MYSQL_2_ENV_MYSQL_PASS": "**Random**", "MYSQL_2_ENV_MYSQL_USER": "admin", "MYSQL_2_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "MYSQL_2_ENV_REPLICATION_MASTER": "**False**", "MYSQL_2_ENV_REPLICATION_PASS": "replica", "MYSQL_2_ENV_REPLICATION_SLAVE": "**False**", "MYSQL_2_ENV_REPLICATION_USER": "replica", "MYSQL_2_PORT": "tcp://mysql-2.a2f5a2e9-tifayuki.node.docker.io:49156", "MYSQL_2_PORT_3306_TCP": "tcp://mysql-2.a2f5a2e9-tifayuki.node.docker.io:49156", "MYSQL_2_PORT_3306_TCP_ADDR": "mysql-2.a2f5a2e9-tifayuki.node.docker.io", "MYSQL_2_PORT_3306_TCP_PORT": "49156", "MYSQL_2_PORT_3306_TCP_PROTO": "tcp", "MYSQL_ENV_DEBIAN_FRONTEND": "noninteractive", "MYSQL_ENV_MYSQL_PASS": "**Random**", "MYSQL_ENV_MYSQL_USER": "admin", "MYSQL_ENV_PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "MYSQL_ENV_REPLICATION_MASTER": "**False**", "MYSQL_ENV_REPLICATION_PASS": "replica", "MYSQL_ENV_REPLICATION_SLAVE": "**False**", "MYSQL_ENV_REPLICATION_USER": "replica", "MYSQL_PORT": "tcp://mysql-1.fa9df19a-tifayuki.node.docker.io:49156", "MYSQL_PORT_3306_TCP": "tcp://mysql-1.fa9df19a-tifayuki.node.docker.io:49156", "MYSQL_PORT_3306_TCP_ADDR": "mysql-1.fa9df19a-tifayuki.node.docker.io", "MYSQL_PORT_3306_TCP_PORT": "49156", "MYSQL_PORT_3306_TCP_PROTO": "tcp", "MYSQL_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "mysql", "resource_uri": "/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/", "roles": [], "run_command": "/run.sh", "running_num_containers": 1, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 23:50:59 +0000", "state": "Stopping", "stopped_datetime": "Tue, 30 Sep 2014 23:09:09 +0000", "stopped_num_containers": 0, "target_num_containers": 2, "unique_name": "mysql", "uuid": "5ecde92d-498b-4bbb-b773-a998e5e421dc"}' + ) + mock_send.side_effect = [fake_resp(fake_service_fetch), fake_resp(fake_service_stop)] + service = dockercloud.Service.fetch('a2ac25c9-7cfe-4a1b-9d97-66de23642ee8') + self.assertTrue(service.stop()) + result = json.loads(json.dumps(service.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) + + @mock.patch.object(dockercloud.api.http.Session, 'send') + def test_service_redeploy(self, mock_send): + attribute = json.loads( + '{"actions": ["/api/v1/action/a8aaf64b-3186-41e7-9256-6b3d69786036/", "/api/v1/action/2481790d-a860-4bb4-95d2-5676ae2d6748/", "/api/v1/action/799a0d06-efae-4bf1-b063-7141f722cbb1/", "/api/v1/action/ef6b9f59-1edb-44c7-bcc6-1b80f737e4b0/", "/api/v1/action/99b7ac29-d16d-448f-bac5-54dbd5dd3b7b/", "/api/v1/action/98816bfc-4fa7-4697-bea1-d926de69b48f/", "/api/v1/action/285e250f-7429-4f0a-a252-5594a05c8020/"], "autodestroy": "OFF", "autoreplace": "OFF", "autorestart": "OFF", "container_envvars": [], "container_ports": [{"endpoint_uri": null, "inner_port": 3306, "outer_port": null, "port_name": "mysql", "protocol": "tcp", "uri_protocol": "mysql"}], "containers": ["/api/v1/container/2a1c4057-7753-4393-98c6-35699c198e08/", "/api/v1/container/54ead360-698f-4354-96f7-538f686cdd69/"], "cpu_shares": null, "current_num_containers": 2, "deployed_datetime": "Tue, 30 Sep 2014 22:44:44 +0000", "destroyed_datetime": null, "entrypoint": "", "image_name": "tutum/mysql:latest", "image_tag": "/api/v1/image/tutum/mysql/tag/latest/", "link_variables": {"MYSQL_TUTUM_API_URL": "https://dashboard.docker.com/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/"}, "linked_from_service": [], "linked_to_service": [], "memory": null, "memory_swap": null, "name": "mysql", "resource_uri": "/api/v1/service/5ecde92d-498b-4bbb-b773-a998e5e421dc/", "roles": [], "run_command": "/run.sh", "running_num_containers": 0, "sequential_deployment": false, "started_datetime": "Tue, 30 Sep 2014 23:50:59 +0000", "state": "Redeploying", "stopped_datetime": "Tue, 30 Sep 2014 23:51:52 +0000", "stopped_num_containers": 0, "target_num_containers": 2, "unique_name": "mysql", "uuid": "5ecde92d-498b-4bbb-b773-a998e5e421dc"}' + ) + mock_send.side_effect = [fake_resp(fake_service_fetch), fake_resp(fake_service_redeploy)] + service = dockercloud.Service.fetch('a2ac25c9-7cfe-4a1b-9d97-66de23642ee8') + self.assertTrue(service.redeploy()) + result = json.loads(json.dumps(service.get_all_attributes())) + target = json.loads(json.dumps(attribute)) + self.assertDictEqual(target, result) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..f962b6f --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,221 @@ +import unittest + +import unittest.mock as mock + +import dockercloud +from dockercloud.api.exceptions import ObjectNotFound, ApiError, NonUniqueIdentifier + + +class FetchRemoteObjectTestCase(unittest.TestCase): + @mock.patch('dockercloud.Container.list') + @mock.patch('dockercloud.Container.fetch') + def test_fetch_remote_container(self, mock_fetch, mock_list): + # test container exist queried with uuid4 + mock_fetch.return_value = 'returned' + self.assertEqual(dockercloud.Utils.fetch_remote_container('7A4CFE51-03BB-42D6-825E-3B533888D8CD', True), + 'returned') + self.assertEqual(dockercloud.Utils.fetch_remote_container('7A4CFE51-03BB-42D6-825E-3B533888D8CD', False), + 'returned') + + # test container doesn't exist queried with uuid4 + mock_fetch.side_effect = ObjectNotFound + self.assertRaises(ObjectNotFound, dockercloud.Utils.fetch_remote_container, + '7A4CFE51-03BB-42D6-825E-3B533888D8CD', + True) + self.assertIsInstance(dockercloud.Utils.fetch_remote_container('7A4CFE51-03BB-42D6-825E-3B533888D8CD', False), + ObjectNotFound) + + # test unique container found queried with short uuid + container = dockercloud.Container.create() + container.uuid = 'uuid' + mock_list.side_effect = [[container], []] + mock_fetch.side_effect = [container] + self.assertEquals(dockercloud.Utils.fetch_remote_container('shortuuid', True), container) + mock_list.side_effect = [[container], []] + mock_fetch.side_effect = [container] + self.assertEquals(dockercloud.Utils.fetch_remote_container('shortuuid', False), container) + + # test unique container found queried with name + mock_list.side_effect = [[], [container]] + mock_fetch.side_effect = [container] + self.assertEquals(dockercloud.Utils.fetch_remote_container('name', True), container) + mock_list.side_effect = [[], [container]] + mock_fetch.side_effect = [container] + self.assertEquals(dockercloud.Utils.fetch_remote_container('name', False), container) + + # test no container found + mock_list.side_effect = [[], []] + self.assertRaises(ObjectNotFound, dockercloud.Utils.fetch_remote_container, 'uuid_or_name', True) + mock_list.side_effect = [[], []] + self.assertIsInstance(dockercloud.Utils.fetch_remote_container('uuid_or_name', False), ObjectNotFound) + + # test multi-container found + mock_list.side_effect = [['container1', 'container2'], []] + self.assertRaises(NonUniqueIdentifier, dockercloud.Utils.fetch_remote_container, 'uuid_or_name', True) + mock_list.side_effect = [['container1', 'container2'], []] + self.assertIsInstance(dockercloud.Utils.fetch_remote_container('uuid_or_name', False), NonUniqueIdentifier) + mock_list.side_effect = [[], ['container1', 'container2']] + self.assertRaises(NonUniqueIdentifier, dockercloud.Utils.fetch_remote_container, 'uuid_or_name', True) + mock_list.side_effect = [[], ['container1', 'container2']] + self.assertIsInstance(dockercloud.Utils.fetch_remote_container('uuid_or_name', False), NonUniqueIdentifier) + + # test api error + mock_list.side_effect = [ApiError, ApiError] + self.assertRaises(ApiError, dockercloud.Utils.fetch_remote_container, 'uuid_or_name', True) + self.assertRaises(ApiError, dockercloud.Utils.fetch_remote_container, 'uuid_or_name', False) + + @mock.patch('dockercloud.Service.list') + @mock.patch('dockercloud.Service.fetch') + def test_fetch_remote_service(self, mock_fetch, mock_list): + # test cluster exist queried with uuid4 + mock_fetch.return_value = 'returned' + self.assertEqual(dockercloud.Utils.fetch_remote_service('7A4CFE51-03BB-42D6-825E-3B533888D8CD', True), + 'returned') + self.assertEqual(dockercloud.Utils.fetch_remote_service('7A4CFE51-03BB-42D6-825E-3B533888D8CD', False), + 'returned') + + # test cluster doesn't exist queried with uuid4 + mock_fetch.side_effect = ObjectNotFound + self.assertRaises(ObjectNotFound, dockercloud.Utils.fetch_remote_service, + '7A4CFE51-03BB-42D6-825E-3B533888D8CD', + True) + self.assertIsInstance(dockercloud.Utils.fetch_remote_service('7A4CFE51-03BB-42D6-825E-3B533888D8CD', False), + ObjectNotFound) + + # test unique cluster found queried with short uuid + service = dockercloud.Service.create() + service.uuid = 'uuid' + mock_list.side_effect = [[service], []] + mock_fetch.side_effect = [service] + self.assertEquals(dockercloud.Utils.fetch_remote_service('shortuuid', True), service) + mock_list.side_effect = [[service], []] + mock_fetch.side_effect = [service] + self.assertEquals(dockercloud.Utils.fetch_remote_service('shortuuid', False), service) + + # test unique cluster found queried with name + mock_list.side_effect = [[], [service]] + mock_fetch.side_effect = [service] + self.assertEquals(dockercloud.Utils.fetch_remote_service('name', True), service) + mock_list.side_effect = [[], [service]] + mock_fetch.side_effect = [service] + self.assertEquals(dockercloud.Utils.fetch_remote_service('name', False), service) + + # test no cluster found + mock_list.side_effect = [[], []] + self.assertRaises(ObjectNotFound, dockercloud.Utils.fetch_remote_service, 'uuid_or_name', True) + mock_list.side_effect = [[], []] + self.assertIsInstance(dockercloud.Utils.fetch_remote_service('uuid_or_name', False), ObjectNotFound) + + # test multi-cluster found + mock_list.side_effect = [['cluster1', 'cluster2'], []] + self.assertRaises(NonUniqueIdentifier, dockercloud.Utils.fetch_remote_service, 'uuid_or_name', True) + mock_list.side_effect = [['cluster1', 'cluster2'], []] + self.assertIsInstance(dockercloud.Utils.fetch_remote_service('uuid_or_name', False), NonUniqueIdentifier) + mock_list.side_effect = [[], ['cluster1', 'cluster2']] + self.assertRaises(NonUniqueIdentifier, dockercloud.Utils.fetch_remote_service, 'uuid_or_name', True) + mock_list.side_effect = [[], ['cluster1', 'cluster2']] + self.assertIsInstance(dockercloud.Utils.fetch_remote_service('uuid_or_name', False), NonUniqueIdentifier) + + # test api error + mock_list.side_effect = [ApiError, ApiError] + self.assertRaises(ApiError, dockercloud.Utils.fetch_remote_service, 'uuid_or_name', True) + self.assertRaises(ApiError, dockercloud.Utils.fetch_remote_service, 'uuid_or_name', False) + + @mock.patch('dockercloud.Node.list') + @mock.patch('dockercloud.Node.fetch') + def test_fetch_remote_node(self, mock_fetch, mock_list): + # test node exist queried with uuid4 + mock_fetch.return_value = 'returned' + self.assertEqual(dockercloud.Utils.fetch_remote_node('7A4CFE51-03BB-42D6-825E-3B533888D8CD', True), 'returned') + self.assertEqual(dockercloud.Utils.fetch_remote_node('7A4CFE51-03BB-42D6-825E-3B533888D8CD', False), 'returned') + + # test node doesn't exist queried with uuid4 + mock_fetch.side_effect = ObjectNotFound + self.assertRaises(ObjectNotFound, dockercloud.Utils.fetch_remote_node, '7A4CFE51-03BB-42D6-825E-3B533888D8CD', + True) + self.assertIsInstance(dockercloud.Utils.fetch_remote_node('7A4CFE51-03BB-42D6-825E-3B533888D8CD', False), + ObjectNotFound) + + # test unique node found queried with short uuid + node = dockercloud.Node.create() + node.uuid = 'uuid' + mock_list.side_effect = [[node]] + mock_fetch.side_effect = [node] + self.assertEquals(dockercloud.Utils.fetch_remote_node('uuid', True), node) + mock_list.side_effect = [[node]] + mock_fetch.side_effect = [node] + self.assertEquals(dockercloud.Utils.fetch_remote_node('uuid', False), node) + + # test no node found + mock_list.side_effect = [[]] + self.assertRaises(ObjectNotFound, dockercloud.Utils.fetch_remote_node, 'uuid', True) + mock_list.side_effect = [[]] + self.assertIsInstance(dockercloud.Utils.fetch_remote_node('uuid', False), ObjectNotFound) + + # test multi-node found + mock_list.side_effect = [['node1', 'node2']] + self.assertRaises(NonUniqueIdentifier, dockercloud.Utils.fetch_remote_node, 'uuid', True) + mock_list.side_effect = [['node1', 'node2']] + self.assertIsInstance(dockercloud.Utils.fetch_remote_node('uuid', False), NonUniqueIdentifier) + + # test api error + mock_list.side_effect = [ApiError, ApiError] + self.assertRaises(ApiError, dockercloud.Utils.fetch_remote_node, 'uuid', True) + self.assertRaises(ApiError, dockercloud.Utils.fetch_remote_node, 'uuid', False) + + @mock.patch('dockercloud.NodeCluster.list') + @mock.patch('dockercloud.NodeCluster.fetch') + def test_fetch_remote_nodecluster(self, mock_fetch, mock_list): + # test nodecluster exist queried with uuid4 + mock_fetch.return_value = 'returned' + self.assertEqual(dockercloud.Utils.fetch_remote_nodecluster('7A4CFE51-03BB-42D6-825E-3B533888D8CD', True), + 'returned') + self.assertEqual(dockercloud.Utils.fetch_remote_nodecluster('7A4CFE51-03BB-42D6-825E-3B533888D8CD', False), + 'returned') + + # test nodecluster doesn't exist queried with uuid4 + mock_fetch.side_effect = ObjectNotFound + self.assertRaises(ObjectNotFound, dockercloud.Utils.fetch_remote_nodecluster, + '7A4CFE51-03BB-42D6-825E-3B533888D8CD', + True) + self.assertIsInstance(dockercloud.Utils.fetch_remote_nodecluster('7A4CFE51-03BB-42D6-825E-3B533888D8CD', False), + ObjectNotFound) + + # test unique nodecluster found queried with short uuid + nodecluster = dockercloud.NodeCluster.create() + nodecluster.uuid = 'uuid' + mock_list.side_effect = [[nodecluster], []] + mock_fetch.side_effect = [nodecluster] + self.assertEquals(dockercloud.Utils.fetch_remote_nodecluster('shortuuid', True), nodecluster) + mock_list.side_effect = [[nodecluster], []] + mock_fetch.side_effect = [nodecluster] + self.assertEquals(dockercloud.Utils.fetch_remote_nodecluster('shortuuid', False), nodecluster) + + # test unique nodecluster found queried with name + mock_list.side_effect = [[], [nodecluster]] + mock_fetch.side_effect = [nodecluster] + self.assertEquals(dockercloud.Utils.fetch_remote_nodecluster('name', True), nodecluster) + mock_list.side_effect = [[], [nodecluster]] + mock_fetch.side_effect = [nodecluster] + self.assertEquals(dockercloud.Utils.fetch_remote_nodecluster('name', False), nodecluster) + + # test no nodecluster found + mock_list.side_effect = [[], []] + self.assertRaises(ObjectNotFound, dockercloud.Utils.fetch_remote_nodecluster, 'uuid_or_name', True) + mock_list.side_effect = [[], []] + self.assertIsInstance(dockercloud.Utils.fetch_remote_nodecluster('uuid_or_name', False), ObjectNotFound) + + # test multi-nodecluster found + mock_list.side_effect = [['nodecluster1', 'nodecluster2'], []] + self.assertRaises(NonUniqueIdentifier, dockercloud.Utils.fetch_remote_nodecluster, 'uuid_or_name', True) + mock_list.side_effect = [['nodecluster1', 'nodecluster2'], []] + self.assertIsInstance(dockercloud.Utils.fetch_remote_nodecluster('uuid_or_name', False), NonUniqueIdentifier) + mock_list.side_effect = [[], ['nodecluster1', 'nodecluster2']] + self.assertRaises(NonUniqueIdentifier, dockercloud.Utils.fetch_remote_nodecluster, 'uuid_or_name', True) + mock_list.side_effect = [[], ['nodecluster1', 'nodecluster2']] + self.assertIsInstance(dockercloud.Utils.fetch_remote_nodecluster('uuid_or_name', False), NonUniqueIdentifier) + + # test api error + mock_list.side_effect = [ApiError, ApiError] + self.assertRaises(ApiError, dockercloud.Utils.fetch_remote_nodecluster, 'uuid_or_name', True) + self.assertRaises(ApiError, dockercloud.Utils.fetch_remote_nodecluster, 'uuid_or_name', False)