# Copyright 2012-2014 Ravello Systems, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, print_function

import sys
import base64
import socket
import logging
import time
import json
import random
import requests
import urllib

# Python 2.x / 3.x module name differences
try:
    from urllib import parse as urlparse
except ImportError:
    import urlparse

pyver = sys.version_info[:2]
if pyver not in [(2, 6), (2, 7)] and pyver < (3, 3):
    raise ImportError('Python 2.6, 2.7 or 3.3+ is required')


__all__ = ['random_luid', 'update_luids', 'application_state', 'new_name',
           'RavelloError', 'RavelloClient']

http_methods = {'POST': requests.post, 'GET': requests.get, 'PUT': requests.put, 'DELETE': requests.delete}
DEFAULT_HTTPS_PORT = 443
DEFAULT_HTTP_PORT = 80

def random_luid():
    """Return a new random local ID."""
    return random.randint(0, 1 << 63)


def update_luids(obj):
    """Update the locally unique IDs in *obj*.

    The object must be a dict, or a list of dicts.

    This replaces all "id" keys (directly or indirectly) below *obj* with an
    new random ID generated by :func:`random_luid`. This function is useful
    when adding VMs images to a new or existing application. Every entity in
    an application's design must have a unique local ID. When you're adding
    multiple VMs based on the same image, the IDs are copied and you need to
    use this function to ensure the VMs have unique local IDs again.
    """
    if isinstance(obj, list):
        return [update_luids(elem) for elem in obj]
    elif isinstance(obj, dict):
        for key, value in obj.items():
            if key == 'id':
                obj['id'] = random_luid()
            elif isinstance(value, (dict, list)):
                update_luids(value)
    else:
        return obj


def application_state(app):
    """Return the consolidated state for application *app*.

    The *app* parameter must be a dict as returned by
    :meth:`~RavelloClient.get_application`.

    The consolidated state for an application is the set of distinct states
    for its VMs. As special cases, None is returned if there are no VMs, and
    the single state is returned if there is exactly one state.
    """
    states = list(set((vm['state'] for vm in app.get('deployment', {}).get('vms', []))))
    return states if len(states) > 1 else states[0] if len(states) == 1 else None


def new_name(existing, prefix):
    """Return a name that is not in *existing*.

    The *existing* parameter must be a sequence of strings, or dicts with a
    "name" key. It the latter case, it is typically a list returned by one of
    the "get all" functions like :meth:`RavelloClient.get_applications` or
    :meth:~RavelloClient.get_blueprints`.

    The unique name is generated by appending a number to *prefix*.
    """
    names = set()
    for name in existing:
        if isinstance(name, dict):
            names.add(name['name'])
        else:
            names.add(name)
    for i in range(len(names)+1):
        name = '{0}{1}'.format(prefix, i)
        if name not in names:
            break
    return name


def urlsplit2(url, default_scheme='http'):
    """Like :func:`urllib.parse.urlsplit`, but fills in default values for
    *scheme* (based on *default_scheme*), *port* (depending on scheme), and
    *path* (defaults to "/").
    """
    if '://' not in url:
        url = '{0}://{1}'.format(default_scheme, url)
    result = urlparse.urlsplit(url)
    updates = {}
    if result.port is None:
        port = DEFAULT_HTTPS_PORT if result.scheme == 'https' else DEFAULT_HTTP_PORT
        updates['netloc'] = '{0}:{1}'.format(result.hostname, port)
    if not result.path:
        updates['path'] = '/'
    if updates:
        result = result._replace(**updates)
    return result


def _idempotent(method):
    """Return whether *method* is idempotent."""
    return method in ('GET', 'HEAD', 'PUT')


def _match_filter(obj, flt):
    """Match the object *obj* with filter *flt*."""

    if isinstance(obj, list):
        return [ob for ob in obj if _match_filter(ob, flt)]

    if callable(flt):
        return flt(obj)
    elif not isinstance(flt, dict):
        raise TypeError('expecting a callable or a dict')

    for fkey, fval in flt.items():
        obval = obj.get(fkey)
        if obval is None:
            return False
        elif isinstance(fval, dict):
            if not isinstance(obval, dict) or not _match_filter(obval, fval):
                return False
        elif callable(fval):
            return fval(obval)
        elif fval != obval:
            return False
    return True


class RavelloError(Exception):
    """Exception used by :class:`RavelloClient`."""


class RavelloClient(object):
    """A client for the Ravello API.

    The client is a thin wrapper around the Ravello RESTful API. The client
    manages a single HTTPS connection, and implements login, redirect and retry
    functionality. A single generic :meth:`request` method is provided to issue
    API requests.

    On top of this, most existing RESTful API calls are mapped as methods on
    this class. These mapped methods are simple wrappers around the generic
    :meth:`request` method. Some general comments on this mapping:

    * The calls are named "<method>_<resource>", for example
      ":meth:`create_keypair`" and ":meth:`get_blueprints`". A method is always
      an English verb, while a resource is can be a singular or plural Englush
      noun.
    * The standard methods are "get", "create", "update" and "delete". Not all
      methods are defined for all resources, and some resources have additional
      methods.
    * The available resources are "application", "blueprint", "image",
      "keypair" and "vm". The plural versions of these exist as well.
    * There is no client-side object model. The return value from any API call
      is simply the parsed JSON response.
    * Resources are returned as a dict or a list of dicts. A dict always
      represents a single object, and its key/value pairs correspond to the
      object's attributes. Lists always represents multiple objects.
    * Objects are identifed by a numeric ID, which is always the key "id".
    * All methods that accept an object ID either accept this parameter
      as a simple Python int, or alternatively as a dict with an "id" key
      containing the ID. In the latter case, the dict is typically returned
      previsouly by another API call.
    * HTTP response codes in the 4xx or 5xx range are considered errors, and
      are turned into :class:`RavelloError` exceptions (except for 404 which
      results in a response of ``None``).
    """

    default_url = 'https://cloud.ravellosystems.com/api/v1'
    default_timeout = 60
    default_retries = 3
    default_redirects = 3

    def __init__(self, username=None, password=None, url=None, timeout=None, retries=None, proxy_url=None, eph_token=None, identity_domain=None):
        """Create a new client.

        The *username* and *password* parameters specify the credentials to use
        when connecting to the API. When the organization of the user has an identity domain,
        the user must specify it or include it in the username: <identity_domain>/<username>.
        When the organization doesnt have an identity domain use only the username.
        The *timeout* and *retries* parameters
        specify the default network system call time timeout and maximum number
        of retries respectively.
        *proxy_url* should be used when an HTTP proxy is in place.
        *eph_token* is ephemeral access token to be used instead of username/password.
        """
        self._identity_domain = identity_domain
        self._username = username
        self._password = password
        self.timeout = timeout if timeout is not None else self.default_timeout
        self.retries = retries if retries is not None else self.default_retries
        self.redirects = self.default_redirects
        self._logger = logging.getLogger('ravello')
        self._autologin = True
        self._connection = None
        self._user_info = None
        self._set_url(url or self.default_url)
        self._proxies = {}
        if proxy_url is not None:
            self._proxies = {"http": proxy_url, "https": proxy_url}
        self._eph_token = eph_token

    @property
    def url(self):
        """The parsed URL of the API endpoint, which is a
        :class:`urllib.parse.SplitResult` instance."""
        return self._url

    @property
    def connected(self):
        """Whether or not the client is connected to the API."""
        return self._connection is not None

    @property
    def have_credentials(self):
        """Whether or not credentials are available."""
        return self._username is not None and self._password is not None

    @property
    def have_eph_access_token(self):
        """whether or not ephemeral access token is available."""
        return self._eph_token is not None

    @property
    def logged_in(self):
        """Whether or not the client is logged in."""
        return self._connection is not None

    @property
    def user_info(self):
        """Return information about the current logged-in user."""
        return self._user_info

    def _set_url(self, url):
        if self.connected:
            raise RuntimeError('cannot change URL when connected')
        self.default_url = url
        self._url = urlsplit2(url)

    def connect(self, url=None, proxy_url=None, eph_token=None):
        """Connect to the API.

        It is not mandatory to call this method. If this method is not called,
        the client will automatically connect when required.
        """
        if url is not None:
            self._set_url(url)
        if proxy_url is not None:
            self._proxies = {"http": proxy_url, "https": proxy_url}
        if eph_token is not None:
            self._eph_token = eph_token
        if self._connection is not None:
            self._connection.proxies = self._proxies

    def login(self, username=None, password=None, identity_domain=None):
        """Login to the API.

        This method performs a login to the API, and store the resulting
        authentication cookie in memory.

        It is not mandatory to call this method. If this method is not called,
        the client will automatically login when required.

        When the organization of the user has an identity domain,
        the user must specify it or include it in the username: <identity_domain>/<username>.
        When the organization doesnt have an identity domain use only the username.
        """
        if self.logged_in:
            raise RuntimeError('already logged in')
        if username is not None:
            self._username = username
        if identity_domain is not None:
            self._identity_domain = identity_domain
        if password is not None:
            self._password = password
        self._login()

    def _login(self):
        if not self.have_credentials and not self.have_eph_access_token:
            raise RuntimeError('no credentials or ephemeral access token set')
        self._connection = requests.Session()
        self._connection.proxies = self._proxies
        self._connection.stream = True
        self._connection.redirects = self.redirects
        self._autologin = False
        if self.have_credentials:
            self._logger.debug('performing a username/password login')
            if self._identity_domain is not None:
                auth = '{0}:{1}'.format(self._identity_domain + "/" + self._username, self._password)
            else:
                auth = '{0}:{1}'.format(self._username, self._password)
            auth = base64.b64encode(auth.encode('ascii')).decode('ascii')
            headers = [('Authorization', 'Basic {0}'.format(auth))]
            response = self._request('POST', '/login', b'', headers)
            self._user_info = response
        else:
            self._logger.debug('using ephemeral access based session')
        self._autologin = True

    def logout(self):
        """Logout from the API.
        This invalidates the authentication cookie in case of username/password authentication,
        and in any case will force close the connection.
        """
        if self.logged_in:
            self.request('POST', '/logout')
        self._connection = None

    def close(self):
        """Close the connection to the API."""
        if not self.connected:
            return
        self._connection = None

    # The request() method is the main function. All other methods are a small
    # shim on top of this.

    def request(self, method, path, entity=None, headers=None):
        """Issues a request to the API.

        The parsed entity is returned, or a :class:`RavelloError` exception is
        raised on error.

        This method can be used in case a certain API call has not yet been
        added as a method.
        """
        body = json.dumps(entity).encode('utf-8') if entity is not None else b''
        headers = headers if headers is not None else []
        response = self._request(method, path, body, headers)
        return response.entity

    def _request(self, method, path, body=b'', headers=None):
        rpath = self._url.path + path
        abpath = self.default_url + path
        hdict = {'Accept': 'application/json'}
        if self._eph_token is not None:
            hdict['X-Ephemeral-Token-Authorization'] = self._eph_token
        if body:
            hdict['Content-Type'] = 'application/json;charset=utf-8'
        if isinstance(headers, dict):
            hdict.update(headers)
        elif isinstance(headers, list):
            for key, value in headers:
                hdict[key] = value
        retries = 0
        while retries < self.retries:
            if not self.logged_in and (self.have_credentials or self.have_eph_access_token) and self._autologin:
                self._login()
            try:
                self._logger.debug('request: {0} {1}'.format(method, rpath))
                req = requests.Request(method, abpath, data=body, headers=hdict, cookies=self._connection.cookies).prepare()
                response = self._connection.send(req, timeout=self.timeout)
                status = response.status_code
                ctype = response.headers.get('Content-Type')
                if ctype == 'application/json':
                    entity = response.json()
                elif ctype == 'text/plain':
                    entity = response.text
                else:
                    entity = None
                self._logger.debug('response: {0} ({1})'.format(status, ctype))
                if 200 <= status < 299:
                    if isinstance(entity, dict) and entity.get('id'):
                        if response.headers.get('Content-Location'):
                            href = urlsplit2(response.headers.get('Content-Location')).path
                        elif response.headers.get('Location'):
                            href = urlsplit2(response.headers.get('Location')).path
                        elif method == 'POST':
                            # missing Location header e.g. with /pubkeys
                            href = '{0}/{1}'.format(abpath, entity['id'])
                        else:
                            href = abpath
                        entity['_href'] = href[len(self._url.path):]
                    elif isinstance(entity, list):
                        for elem in entity:
                            if 'id' in elem:
                                elem['_href'] = '{0}/{1}'.format(path, elem['id'])
                elif 300 <= status < 399:
                    loc = response.headers.get('Location')
                    if loc is None:
                        raise RavelloError('no location for {0} response'.format(status))
                    if loc.startswith('/'):
                        rpath = loc
                    else:
                        url = urlsplit2(loc)
                        if url.netloc != self._url.netloc:
                            raise RavelloError('will not chase referral to {0}'.format(loc))
                        rpath = url.path
                elif status == 401:
                    if path == '/login':
                        self.close()
                        response.raise_for_status()
                    elif self._autologin:
                        self._login()
                        retries += 1
                        continue
                elif status == 404:
                    entity = None
                else:
                    response.raise_for_status()
                response.entity = entity
            except (requests.exceptions.Timeout, ValueError) as e:
                self._logger.debug('error: {0!s}'.format(e))
                self.close()
                if not _idempotent(method):
                    self._logger.debug('not retrying {0} request'.format(method))
                    raise e
                retries += 1
                continue
            break
        if retries == self.retries:
            raise RavelloError('maximum number of retries reached')
        return response

    def reload(self, obj):
        """Reload the object *obj*.

        The object must have been returned by the API, and must be a dict with
        an ``"_href"`` key.
        """
        href = obj.get('_href')
        if href is None:
            raise RuntimeError('obj must have an "_href" key')
        return self.request('GET', href)

    def wait_for(self, obj, cond, timeout=None):
        """Wait for a condition on *obj* to become true.

        The object *obj* must be reloadable. See :meth:`reload` for more
        details.

        The condition *cond* must be a dict or a callable. If it is a dict, it
        lists the keys and values that the object must have. If it is a
        callable, it will be called with the object as an argument, and it
        should return True or False.

        The *timeout* argument specifies the total time to wait. If not
        specified, it will default to the system call timeout passed to the
        constructor.

        If the condition does not become true before the timeout, a
        :class:`RavelloError` exception is raised.
        """
        end_time = time.time() + timeout
        while end_time > time.time():
            obj = self.reload(obj)
            if _match_filter(obj, cond):
                break
            time.sleep(5)
        if end_time < time.time():
            raise RavelloError('timeout waiting for condition')

    # Mapped API calls below

    def get_application_by_name(self, app_name, aspect=None):
      
        criteria = dict()
        criteria['type'] = 'COMPLEX'
        criteria['operator'] = 'And'
        criteria['criteria'] = [1]
        
        criterion = dict()
        criterion['type'] = 'SIMPLE'
        criterion['operator'] = 'Equals'
        criterion['propertyName'] = 'name'
        criterion['operand'] = app_name
        criteria['criteria'][0] = criterion
         
        apps = self.request('POST', '/applications/filter',criteria)
        if len(apps) == 0:
            raise RavelloError('app "{0}" not found'.format(app_name))
        if len(apps) > 1:
            raise RavelloError('multiple apps for name "{0}" found'.format(app_name))
        app = apps[0]
        if aspect is not 'properties':
            app = self.get_application(app,aspect)
        return app
    
    def get_application(self, app, aspect=None):
        """Return the application with ID *app*, or None if it does not exist.

        The *aspect* parameter can be used to return the application only with
        the specified aspect (e.g., design, deployment, properties).
        """
        if isinstance(app, dict): app = app['id']
        if aspect is not None:
            app = '{0};{1}'.format(app, aspect)
        return self.request('GET', '/applications/{0}'.format(app))

    def get_applications(self, filter=None):
        """Return a list with all applications.
        The *filter* argument can be used to return only a subset of the
        applications. See the description of the *cond* argument to
        :meth:`wait_for`.
        """
        apps = self.request('GET', '/applications')
        if filter is not None:
            apps = _match_filter(apps, filter)
        return apps

    def create_application(self, app):
        """Create a new application.

        The *app* parameter must be a dict describing the application to
        create.

        The new application is returned.
        """
        return self.request('POST', '/applications', app)

    def update_application(self, app):
        """Update an existing application.

        The *app* parameter must be the updated application. The way to update
        an application (or any other resource) is to first retrieve it, make
        the updates client-side, and then use this method to make the update.

        The updated application is returned.
        """
        return self.request('PUT', '/applications/{0}'.format(app['id']), app)

    def update_vm(self, app, vm):
        """Update an existing vm.

        The *app* parameter is the app containing the vm to update.

        The *vm* parameter must be the updated vm. The way to update
        a vm (or any other resource) is to first retrieve it, make
        the updates client-side, and then use this method to make the update.

        The updated vm is returned.
        """
        return self.request('PUT', '/applications/{0}/vms/{1}'.format(app['id'], vm['id']), entity=vm)

    def delete_application(self, app):
        """Delete an application with ID *app*."""
        if isinstance(app, dict): app = app['id']
        self.request('DELETE', '/applications/{0}'.format(app))

    def publish_application(self, app, req={"optimizationLevel":"COST_OPTIMIZED"}):
        """Publish the application with ID *app*.

        The *req* parameter, if provided, must be a dict with publish
        parameters.
        """
        if isinstance(app, dict):
            app = app['id']
        self.request('POST', '/applications/{0}/publish'.format(app), req)

    def start_application(self, app, req=None):
        """Start the application with ID *app*.

        The *req* parameter, if provided, must be a dict with start
        parameters.
        """
        if isinstance(app, dict): app = app['id']
        self.request('POST', '/applications/{0}/start'.format(app), req)

    def stop_application(self, app, req=None):
        """Stop the application with ID *app*.

        The *req* parameter, if provided, must be a dict with stop
        parameters.
        """
        if isinstance(app, dict): app = app['id']
        self.request('POST', '/applications/{0}/stop'.format(app), req)

    def restart_application(self, app, req=None):
        """Restart the application with ID *app*.

        The *req* parameter, if provided, must be a dict with restart
        parameters.
        """
        if isinstance(app, dict): app = app['id']
        self.request('POST', '/applications/{0}/restart'.format(app), req)

    def publish_application_updates(self, app, autostart=True):
        """Publish updates for the application with ID *app*."""
        if isinstance(app, dict): app = app['id']
        url = '/applications/{0}/publishUpdates'.format(app)
        if not autostart:
            url += '?startAllDraftVms=false'
        self.request('POST', url)

    def set_application_expiration(self, app, req):
        """Set the expiration for the application with ID *app*.

        The *req* parameter must be a dict describing the new expiration.
        """
        if isinstance(app, dict): app = app['id']
        self.request('POST', '/applications/{0}/setExpiration'.format(app), req)

    def get_application_publish_locations(self, app, req=None):
        """Get a list of locations where *app* can be published."""
        if isinstance(app, dict): app = app['id']
        url = '/applications/{0}/publishLocations'.format(app)
        return self.request('GET', url, req)

    def get_blueprint_publish_locations(self, bp, req=None):
        """Get a list of locations where *bp* can be published."""
        if isinstance(bp, dict): bp = bp['id']
        url = '/blueprints/{0}/publishLocations'.format(bp)
        return self.request('GET', url, req)

    def get_vm(self, app, vm, aspect=None):
        """Return the vm with ID *vm* in the appplication with ID *app*,
        or None if it does not exist.

        The *aspect* parameter (design, deployment) can be used to return
        the vm as designed or as deployed in the cloud.
        """
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        if aspect is not None:
            app = '{0};{1}'.format(app, aspect)
        return self.request('GET', '/applications/{0}/vms/{1}'.format(app, vm))

    def get_vms(self, app, filter=None, level='design'):
        """Return a list with all vms (for a given app).

        The *filter* argument can be used to return only a subset of the
        applications. See the description of the *cond* argument to
        :meth:`wait_for`.
        """
        if isinstance(app, dict): app = app['id']
        apps = self.request('GET', '/applications/{0};{1}/vms'.format(app,level))
        if filter is not None:
            apps = _match_filter(apps, filter)
        return apps

    def start_vm(self, app, vm):
        """Start the VM with ID *vm* in the application with ID *app*."""
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        self.request('POST', '/applications/{0}/vms/{1}/start'.format(app, vm))

    def stop_vm(self, app, vm):
        """Stop the VM with ID *vm* in the application with ID *app*."""
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        self.request('POST', '/applications/{0}/vms/{1}/stop'.format(app, vm))

    def poweroff_vm(self, app, vm):
        """Power off the VM with ID *vm* in the application with ID *app*."""
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        self.request('POST', '/applications/{0}/vms/{1}/poweroff'.format(app, vm))

    def restart_vm(self, app, vm):
        """Restart the VM with ID *vm* in the application with ID *app*."""
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        self.request('POST', '/applications/{0}/vms/{1}/restart'.format(app, vm))

    def redeploy_vm(self, app, vm):
        """Redeploy the VM with ID *vm* in the application with ID *app*."""
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        self.request('POST', '/applications/{0}/vms/{1}/redeploy'.format(app, vm))

    def repair_vm(self, app, vm):
        """Repair the VM with ID *vm* in the application with ID *app*."""
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        self.request('POST', '/applications/{0}/vms/{1}/repair'.format(app, vm))

    def reset_disks_vm(self, app, vm):
        """Resets each disk of the VM with ID *vm* in the application with ID *app*."""
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        self.request('POST', '/applications/{0}/vms/{1}/resetDisks'.format(app, vm))

    def get_vnc_url(self, app, vm):
        """Get the VNC URL for the VM with ID *vm* in the application with ID *app*."""
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        headers = [('Accept', 'text/plain')]
        url = self.request('GET', '/applications/{0}/vms/{1}/vncUrl'.format(app, vm),
                           headers=headers)
        if hasattr(url, "decode"):
            return url.decode('iso-8859-1')
        return url

    def get_detailed_charges_for_application(self, app, mode = 'deployment', deployment_options = {}):
        """Get the detailed hourly charges for an application.
        *app* is the application to get the charges for
        *mode* optional parameter, either 'design' or 'deployment' (default value)
        *deployment_options* optional parameter, should be non empty if and only if *mode* is being used, is a dict 
        with the deployment optimizationLevel when querying for a design pricing.
        See the REST API docs for details on possible values.
        """
        if isinstance(app, dict): app = app['id']
        if mode =='design' and deployment_options == {}:
            raise RavelloError("Cannot query for detailed application charges with mode=design and no deployment_options")
        return self.request('POST', '/applications/{0}/calcPrice;{1}'.format(app, mode), deployment_options)

    def get_vm_fqdn(self, app, vm):
        """Get the FQDN for a deployed VM
        *app* is the applicaiton/application-id of the VM
        *vm* is the VM/VM-id we're querying for
        """
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        return self.request('GET', '/applications/{0}/vms/{1}/fqdn;deployment'.format(app,vm))

    def get_vm_state(self, app, vm):
        """Get the state of a deployed VM (e.g. STARTED / STOPPED / ERROR / ....)
        *app* is the applicaiton/application-id of the VM
        *vm* is the VM/VM-id we're querying for
        """
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        return self.request('GET', '/applications/{0}/vms/{1}/state;deployment'.format(app,vm))

    def get_vm_public_ips(self, app, vm):
        """Get the list of a VM's public IPs
        *app* is the applicaiton/application-id of the VM
        *vm* is the VM/VM-id we're querying for
        """
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        return self.request('GET', '/applications/{0}/vms/{1}/publicIps;deployment'.format(app,vm))

    def is_application_published(self, app):
        """Is the application *app* published or draft?"""
        if isinstance(app, dict): app = app['id']
        return self.request('GET', '/applications/{0}/isPublished'.format(app))

    def add_library_vm_to_application(self, app, library_vm_id):
        """Add a VM from the library to an existing application design (note that you will still need to publish the update)
        *app* the application (object or ID) to add the library VM to
        *library_vm_id* the ID of the Library VM to add to the application
        """
        if isinstance(app, dict): app = app['id']
        return self.request('POST', '/applications/{0}/vms'.format(app), {'baseVmId':library_vm_id})

    def delete_vm_from_application(self, app, vm):
        """Deletes a single VM from an existing application's design (note that you will still need to publish the update)
        *app* the application (object or ID) to delete the library VM from
        *vm* the VM to delete from the application
        """
        if isinstance(app, dict): app = app['id']
        if isinstance(vm, dict): vm = vm['id']
        return self.request('DELETE', '/applications/{0}/vms/{1}'.format(app, vm))

    def get_blueprint(self, bp):
        """Return the blueprint with ID *bp*, or None if it does not exist."""
        if isinstance(bp, dict): bp = bp['id']
        return self.request('GET', '/blueprints/{0}'.format(bp))

    def get_blueprints(self, filter=None):
        """Return a list with all blueprints.

        The *filter* argument can be used to return only a subset of the
        applications. See the description of the *cond* argument to
        :meth:`wait_for`.
        """
        bps = self.request('GET', '/blueprints')
        if filter is not None:
            bps = _match_filter(bps, filter)
        return bps

    def create_blueprint(self, bp):
        """Create a new blueprint.

        The *bp* parameter must be a dict describing the blueprint to
        create.

        The new blueprint is returned.
        """
        return self.request('POST', '/blueprints', bp)

    def delete_blueprint(self, bp):
        """Delete the blueprint with ID *bp*."""
        if isinstance(bp, dict): bp = bp['id']
        self.request('DELETE', '/blueprints/{0}'.format(bp))

    def get_detailed_charges_for_blueprint(self, bp, deployment_options = {}):
        """Estimate the detailed charges for an application deployed from this blueprint.
        *bp* is the blueprint to get the charges for
        *deployment_options* required parameter, is a dict 
        with the deployment optimizationLevel when querying for a design pricing.
        See the REST API docs for details on possible values.
        """
        if isinstance(bp, dict): bp = bp['id']
        if 'optimizationLevel' not in deployment_options:
            raise RavelloError("Cannot query for detailed blueprint charges with no optimizationLevel specified in deployment_options")
        return self.request('POST', '/blueprints/{0}/calcPrice'.format(bp), deployment_options)

    def get_image(self, img):
        """Return the image with ID *img*, or None if it does not exist."""
        if isinstance(img, dict): img = img['id']
        return self.request('GET', '/images/{0}'.format(img))

    def get_images(self, filter=None):
        """Return a list with all images.

        The *filter* argument can be used to return only a subset of the
        images. See the description of the *cond* argument to
        :meth:`wait_for`.
        """
        imgs = self.request('GET', '/images')
        if filter is not None:
            imgs = _match_filter(imgs, filter)
        return imgs

    def create_image(self, image):
        """Create a new image.

        The *image* parameter must be a dict describing the image to create.

        The new image is returned.
        """
        return self.request('POST', '/images', image)

    def update_image(self, img):
        """Update an existing image.

        The *img* parameter must be the updated image.  The updated image is
        returned.
        """
        return self.request('PUT', '/images/{0}'.format(img['id']), img)

    def delete_image(self, img):
        """Delete the image with ID *img*."""
        if isinstance(img, dict): img = img['id']
        self.request('DELETE', '/images/{0}'.format(img))

    def get_diskimage(self, img):
        """Return the disk image with ID *img*, or None if it does not exist."""
        if isinstance(img, dict): img = img['id']
        return self.request('GET', '/diskImages/{0}'.format(img))

    def get_diskimages(self, filter=None):
        """Return a list with all disk images.

        The *filter* argument can be used to return only a subset of the
        disk images. See the description of the *cond* argument to
        :meth:`wait_for`.
        """
        imgs = self.request('GET', '/diskImages')
        if filter is not None:
            imgs = _match_filter(imgs, filter)
        return imgs

    def create_diskimage(self, img):
        """Create a new disk image.

        The *img* parameter must be a dict describing the disk image to create.

        The new disk image is returned.
        """
        return self.request('POST', '/diskImages', img)

    def update_diskimage(self, img):
        """Update an existing image.

        The *img* parameter must be the updated image.  The updated disk image
        is returned.
        """
        return self.request('PUT', '/diskImages/{0}'.format(img['id']), img)

    def delete_diskimage(self, img):
        """Delete the image with ID *img*."""
        if isinstance(img, dict): img = img['id']
        self.request('DELETE', '/diskImages/{0}'.format(img))

    def get_keypair(self, kp):
        """Return the keypair with ID *kp*, or None if it does not exist."""
        if isinstance(kp, dict): kp = kp['id']
        return self.request('GET', '/keypairs/{0}'.format(kp))

    def get_keypairs(self, filter=None):
        """Return a list with all keypairs.

        The *filter* argument can be used to return only a subset of the
        keypairs.  See the description of the *cond* argument to
        :meth:`wait_for`.
        """
        kps = self.request('GET', '/keypairs')
        if filter is not None:
            kps = _match_filter(kps, filter)
        return kps

    def create_keypair(self, kp):
        """Create a new keypair.

        The *kp* parameter must be a dict describing the keypair to create.

        The new blueprint is returned.
        """
        return self.request('POST', '/keypairs', kp)

    def update_keypair(self, kp):
        """Update an existing keypair.

        The *kp* parameter must be the updated keypair. The updated keypair is
        returned.
        """
        return self.request('PUT', '/keypairs/{0}'.format(kp['id']), kp)

    def delete_keypair(self, kp):
        """Delete the keypair with ID *kp*."""
        if isinstance(kp, dict): kp = kp['id']
        self.request('DELETE', '/keypairs/{0}'.format(kp))

    def generate_keypair(self):
        """Generate a new keypair and return it."""
        return self.request('POST', '/keypairs/generate')

    def get_user(self, user):
        """Return the user with ID *user*, or None if it does not exist."""
        if isinstance(user, dict): user = user['id']
        return self.request('GET', '/users/{0}'.format(user))

    def get_users(self, filter=None):
        """Return a list with all users.

        The *filter* argument can be used to return only a subset of the
        users. See the description of the *cond* argument to :meth:`wait_for`.
        """
        users = self.request('GET', '/users')
        if filter is not None:
            users = _match_filter(users, filter)
        return users

    def invite_user(self, user):
        """Invite a new user.

        The *user* parameter must be a dict describing the user to invite.

        The new user is returned.
        """
        return self.request('POST', '/users/invite', user)

    def update_user(self, user, userId):
        """Update an existing user.

        The *user* parameter must be the updated user. The way to update a
        user (or any other resource) is to first retrieve it, make the
        updates client-side, and then use this method to make the update.
        In this case, note however that you can only provide email, name,
        roles, and surname (and email cannot be changed).

        The updated user is returned.
        """
        return self.request('PUT', '/users/{0}'.format(userId), user)

    def delete_user(self, user):
        """Delete a user with ID *user*."""
        if isinstance(user, dict): user = user['id']
        self.request('DELETE', '/users/{0}'.format(user))

    def changepw_user(self, passwords, user):
        """Change the password of a user with ID *user*.

        The *passwords* parameter must be a dict describing the existing
        and new passwords.
        """
        return self.request('PUT', '/users/{0}/changepw'.format(user), passwords)

    def get_billing(self, filter=None):
        """Return a list with all applications' charges incurred since
        beginning of the month.

        The *filter* argument can be used to return only a subset of the
        applications. See the description of the *cond* argument to
        :meth:`wait_for`.
        """
        billing = self.request('GET', '/billing')
        if filter is not None:
            billing = _match_filter(billing, filter)
        return billing

    def get_billing_for_month(self, year, month):
        """Return a list with all applications' charges incurred during the
        specified month and year.
        """
        return self.request('GET', '/billing?year={0}&month={1}'.format(year, month))

    def get_events(self):
        """Return a list of all possible event names."""
        return self.request('GET', '/events')

    def get_alerts(self):
        """Return a list of all alerts that user is registered to.

        If user is an administrator, list contains all alerts that the
        organization is registered too.
        """
        return self.request('GET', '/userAlerts')

    def create_alert(self, eventName, userId=None):
        """Registers a user to an alert.

        User must be an administrator to specify a *userId*.
        """
        req = {'eventName': eventName}
        if isinstance(userId, int): req['userId'] = userId
        return self.request('POST', '/userAlerts', req)

    def delete_alert(self, alertId):
        """Delete a specific userAlert.

        Specifiy an *alertId* to unregister a user from it.
        """
        return self.request('DELETE', '/userAlerts/{0}'.format(alertId))

    def search_notifications(self, query):
        """Return list of notifications regarding given criteria.

        The *query* parameter must be a dict describing the notifications to
        match. Technically, all 4 of the following params are optional:
        appId, notificationLevel, maxResults, dateRange
        """
        return self.request('POST', '/notifications/search', query)

    def get_organization(self, org=None):
        """Return the authenticated user organization's details.

        The *org* parameter can be used to instead return details according to
        organization ID.
        """
        if isinstance(org, dict): org = org['id']
        if org is None:
            org = ''
        else:
            org = 's/{0}'.format(org)
        return self.request('GET', '/organization{0}'.format(org))

    def update_organization(self, org):
        """Update an organization's details.

        The *org* parameter must be the updated organization. The way to update
        an organization (or any other resource) is to first retrieve it, make
        the updates client-side, and then use this method to make the update.

        The updated organization is returned.
        """
        return self.request('PUT', '/organizations/{0}'.format(org['id']), org)

    def get_permgroup(self, pg):
        """Return the permission group with ID *pg*, or None if it does not exist."""
        if isinstance(pg, dict): pg = pg['id']
        return self.request('GET', '/permissionsGroups/{0}'.format(pg))

    def get_permgroups(self, filter=None):
        """Return a list with all permission groups.

        The *filter* argument can be used to return only a subset of the
        permission groups. See the description of the *cond* argument to
        :meth:`wait_for`.
        """
        pgs = self.request('GET', '/permissionsGroups')
        if filter is not None:
            pgs = _match_filter(pgs, filter)
        return pgs

    def create_permgroup(self, pg):
        """Create a new permission group.

        The *pg* parameter must be a dict describing the permission group
        to create.

        The new permission group is returned.
        """
        return self.request('POST', '/permissionsGroups', pg)

    def update_permgroup(self, pg):
        """Update an existing permission group.

        The *pg* parameter must be the updated permission group. The way to
        update a permission group (or any other resource) is to first retrieve
        it, make the updates client-side, and then use this method to make the
        update.

        The updated permission group is returned.
        """
        return self.request('PUT', '/permissionsGroups/{0}'.format(pg['id']), pg)

    def delete_permgroup(self, pg):
        """Delete a permission group with ID *pg*."""
        if isinstance(pg, dict): pg = pg['id']
        self.request('DELETE', '/permissionsGroups/{0}'.format(pg))

    def get_users_in_permgroup(self, pg):
        """List all of the users in a permission group."""
        if isinstance(pg, dict): pg = pg['id']
        return self.request('GET', '/permissionsGroups/{0}/users'.format(pg))

    def add_user_to_permgroup(self, pg, user):
        """Add a user to a permission group.

        The *user* parameter must be a valid user id.
        """
        if isinstance(pg, dict): pg = pg['id']
        req = {'userId': user}
        return self.request('POST', '/permissionsGroups/{0}/users'.format(pg), req)

    def del_user_from_permgroup(self, pg, user):
        """Delete a user from a permission group.

        The *user* parameter must be a valid user id.
        """
        if isinstance(pg, dict): pg = pg['id']
        return self.request('DELETE', '/permissionsGroups/{0}/users/{1}'.format(pg, user))

    def get_permgroup_descriptors(self):
        """Return a list of resource permission descriptors."""
        return self.request('GET', '/permissionsGroups/describe')

    def create_elastic_ip(self, locationType, locationName, name=None, description=None):
        """Creates elastic Ip. Returns the ip"""
        req = {'name': name,
               'description': description,
               'locationType': locationType,
               'locationName': locationName
               }

        return self.request('POST', '/elasticIps/', req)

    def delete_elastic_ip(self, ip):
        """
        :param ip: The ip to delete. In string format.
        """
        return self.request('DELETE', '/elasticIps/{0}/'.format(ip))

    def get_elastic_ips(self):
        """
        :return: all the elastic ips
        """
        return self.request('GET', '/elasticIps')

    def get_elastic_ip_locations(self):
        """
        :return: all the possible locations for elastic ip
        """
        return self.request('GET', '/elasticIps/locations/')

    def create_application_task(self, application, task_details):
        """Create and Schedule a new application task.
        
        The *task_details* parameter is a dict describing the task to schedule
        """
        if isinstance(application, dict): application = application['id']
        return self.request('POST', '/applications/{0}/tasks'.format(application), task_details)

    def update_application_task(self, application, task, task_details):
        """Update an already scheduled application task.
        
        The *task* parameter is the ID of the task to update
        The *task_details* parameter is a dict describing the task to schedule
        """
        if isinstance(application, dict): application = application['id']
        if isinstance(task, dict): task = task['id']
        
        return self.request('PUT', '/applications/{0}/tasks/{1}'.format(application ,task), task_details)

    def get_application_tasks(self, application):
        """Return a list of the application's scheduled tasks"""
        if isinstance(application, dict): application = application['id']
        return self.request('GET', '/applications/{0}/tasks'.format(application))

    def get_application_task(self, application, task):
        """Return a specific application's scheduled task"""
        if isinstance(application, dict): application = application['id']
        if isinstance(task, dict): task = task['id']
        return self.request('GET', '/applications/{0}/tasks/{1}'.format(application, task))

    def delete_application_task(self, application, task):
        """Delete a specific application's scheduled task"""
        if isinstance(application, dict): application = application['id']
        if isinstance(task, dict): task = task['id']
        return self.request('DELETE', '/applications/{0}/tasks/{1}'.format(application, task))

    def delete_application_tasks(self, application):
        """Delete all scheduled tasks of an application"""
        if isinstance(application, dict): application = application['id']
        return self.request('DELETE', '/applications/{0}/tasks'.format(application))

    def get_ephemeral_access_tokens(self):
        """Return a list of all ephemeral access tokens"""
        return self.request('GET', '/ephemeralAccessTokens')

    def get_ephemeral_access_token(self, token):
        """Return a specific ephemeral access token"""
        if isinstance(token, dict): token = token['id']
        return self.request('GET', '/ephemeralAccessTokens/{0}'.format(token))

    def create_ephemeral_access_token(self, token_details):
        """Creates a new ephemeral access token.
        The *token_details* parameter is a dict describing the ephemeral access token to create.
        This parameter has a mandatory field named *name*, as well as optional fields:
        - expirationTime - in millis since 1.1.1970 UTC
        - description - text
        - permissions - a list of permissions associated with the eph access token
        """
        return self.request('POST', '/ephemeralAccessTokens', token_details)

    def update_ephemeral_access_token(self, token, token_details):
        """Updates an existing ephemeral access token.
        The *token* parameter is the ID of the token to update
        The *token_details* parameter is a dict describing the updated token details
        """
        if isinstance(token, dict): token = token['id']
        return self.request('PUT', '/ephemeralAccessTokens/{0}'.format(token), token_details)

    def delete_ephemeral_access_token(self, token):
        """Deletes an existing ephemeral access token.
        The *token* parameter is the ID of the token to delete
        """
        if isinstance(token, dict): token = token['id']
        return self.request('DELETE', '/ephemeralAccessTokens/{0}'.format(token))

    def get_community(self, community):
        """Retrieves an existing community.
        The *community* parameter is the ID of the community to retrieve
        """
        if isinstance(community, dict): community = community['id']
        return self.request('GET', '/communities/{0}'.format(community))

    def get_communities(self):
        """Retrieves all communities."""
        return self.request('GET', '/communities')

    def get_cost_buckets(self, permissions="execute,update", skip_deleted=False):
        """Retrieves all cost buckets
        *permissions* - Possible values: execute, create, read, update, delete, share or ephemeral_access, or any combination of the above, comma separated. The returned list will be only buckets with user's authentication plus defined permissions.
        *skip_deleted* - If False, list contains also deleted cost buckets.
        """
        skip_deleted_str=str(skip_deleted).lower()
        return self.request('GET', '/costBuckets?permissions={0}&skipDeleted={1}'.format(permissions,skip_deleted_str))

    def get_cost_bucket(self, cost_bucket):
        """Retrieves an existing cost bucket.
        The *cost_bucket* parameter is the ID of the cost bucket to retrieve
        """
        if isinstance(cost_bucket, dict): cost_bucket = cost_bucket['id']
        return self.request('GET', '/costBuckets/{0}'.format(cost_bucket))

    def create_cost_bucket(self, cost_bucket_details):
        """Creates a new cost bucket.
        The *cost_bucket_details* parameter is a dict describing the cost bucket to create.
        This parameter has a mandatory field named *name*, as well as optional fields:
        - description - The description of the cost bucket
        - parentId - The ID of the cost bucket parent, which is also a cost bucket. If this parameter is missing, the default parentId, "Organization", is set.
        """
        return self.request('POST', '/costBuckets', cost_bucket_details)

    def update_cost_bucket(self, cost_bucket, cost_bucket_details):
        """Updates an existing cost bucket.
        The *cost_bucket* parameter is the ID of the cost bucket to update
        The *cost_bucket_details* parameter is a dict describing the cost bucket details
        """
        if isinstance(cost_bucket, dict): cost_bucket = cost_bucket['id']
        return self.request('PUT', '/costBuckets/{0}'.format(cost_bucket), cost_bucket_details)

    def associate_resource_to_cost_bucket(self, cost_bucket, resource_details):
        """Associate Billed Resource to a Different Cost Bucket
        The *cost_bucket* parameter is the ID of the cost bucket the resource will be associated to
        The *resource_details* parameter is a dict describing the resource to associate details
        """
        if isinstance(cost_bucket, dict): cost_bucket = cost_bucket['id']
        return self.request('PUT', '/costBuckets/{0}/associateResource'.format(cost_bucket), resource_details)

    def describe_cost_bucket(self):
        """Describe Cost Bucket
        """
        return self.request('GET', '/costBuckets/describe')

    def get_cost_alert_definition(self, cost_alert_definition):
        """Returns a single cost alert definition according to its ID.
        The *cost_alert_definition* parameter is the ID of the cost alert definition to retrieve
        """
        if isinstance(cost_alert_definition, dict): cost_alert_definition = cost_alert_definition['id']
        return self.request('GET', '/costAlertDefinitions/{0}'.format(cost_alert_definition))

    def get_cost_alert_definitions(self, cost_bucket):
        """Retrieves all the cost alert definitions for an existing cost bucket.
        The *cost_bucket* parameter is the ID of the cost bucket to retrieve
        """
        if isinstance(cost_bucket, dict): cost_bucket = cost_bucket['id']
        return self.request('GET', '/costBuckets/{0}/costAlertDefinitions'.format(cost_bucket))

    def create_cost_alert_definition(self, cost_alert_definition_details):
        """Creates a new cost alert definition. In addition to permission to create cost alert definitions,
         the user must also have READ permission on the aggregation parent, and READ permission on Billing Info.
        The *cost_alert_definition_details* parameter is a dict describing the cost alert definition to create.
        This parameter has several mandatory fields: 
        - aggregationTimeUnit - The duration of the aggregation. Possible values: daily, weekly, monthly, yearly, all_times.
        - costLimit - The limit in dollars.
        - aggregationParent - Possible values: "cost_bucket" or "application". If "cost_bucket" is sent, an alert is sent when the quota for this bucket is exceeded. If "application" is sent, an alert is sent when the quota is exceeded for this application.
        - parentId - The ID of the cost_bucket / application in which the alert is defined.
        This parameter has several optional fields:
        - description - The description of the cost bucket
        - warningThreshold - The threshold for warning, as a percentage.
        - userIds - List of IDs of the users that will receive the messages when the configured quota is exceeded.
        """
        return self.request('POST', '/costAlertDefinitions', cost_alert_definition_details)

    def update_cost_alert_definition(self, cost_alert_definition, cost_alert_definition_details):
        """Updates a specific cost alert definition. The user should have the following permissions in order to complete this operation: UPDATE permission on cost alert definitions, READ permission on the aggregation parent (the cost bucket or application's on which the alert is set) and READ permission on Billing Info.
        The *cost_alert_definition* parameter is the ID of the cost alert definition to update
        The *cost_alert_definition_details* parameter is a dict describing the cost alert definition to update.
        """
        if isinstance(cost_alert_definition, dict): cost_alert_definition = cost_alert_definition['id']
        return self.request('PUT', '/costAlertDefinitions/{0}'.format(cost_alert_definition), cost_alert_definition_details)

    def delete_cost_alert_definition(self, cost_alert_definition):
        """Deletes a cost alert definition. The user should have the following permissions in order to complete this operation: DELETE permission on cost alert definitions, READ permission on the aggregation parent (the cost bucket or application's on which the alert is set) and READ permission on Billing Info.
        The *cost_alert_definition* parameter is the ID of the cost alert definition to delete
        """
        if isinstance(cost_alert_definition, dict): cost_alert_definition = cost_alert_definition['id']
        return self.request('DELETE', '/costAlertDefinitions/{0}'.format(cost_alert_definition))

    def get_users_of_cost_alert_definition(self, cost_alert_definition):
        """Returns list of all the recipients of a specific cost alert definition.
        The *cost_alert_definition* parameter is the ID of the cost alert definition to retrieve
        """
        if isinstance(cost_alert_definition, dict): cost_alert_definition = cost_alert_definition['id']
        return self.request('GET', '/costAlertDefinitions/{0}/users'.format(cost_alert_definition))

    def add_user_to_cost_alert_definition(self, cost_alert_definition, user):
        """Adds a recipient to the cost alert definition. Required permissions: READ permission on the user, and UPDATE permission on the Cost Alert Definition.
        The *cost_alert_definition* parameter is the ID of the cost alert definition to add the user to
        The *user* parameter is the ID of the user to add to the cost alert definition
        """
        if isinstance(cost_alert_definition, dict): cost_alert_definition = cost_alert_definition['id']
        if isinstance(user, dict): user = user['id']
        return self.request('POST', '/costAlertDefinitions/{0}/users/{1}'.format(cost_alert_definition, user))

    def remove_user_from_cost_alert_definition(self, cost_alert_definition, user):
        """Deletes related user from cost alert definition. Required permissions: READ permission on the user, and UPDATE permission on the Cost Alert Definition.
        The *cost_alert_definition* parameter is the ID of the cost alert definition to remove the user from
        The *user* parameter is the ID of the user to remove from the cost alert definition
        """
        if isinstance(cost_alert_definition, dict): cost_alert_definition = cost_alert_definition['id']
        if isinstance(user, dict): user = user['id']
        return self.request('DELETE', '/costAlertDefinitions/{0}/users/{1}'.format(cost_alert_definition, user))

    def get_shares(self, request=None):
        """Get List of Shares.
        Returns a list of share records, optional filters could be used.
        - sharingUserId	- The ID of the sharing user.
        - targetEmail - The email of the user whom we share the resource with.
        - sharedResourceType - Could be one of the following: BLUEPRINT, LIBRARY_VM, DISK_IMAGE.
        - sharedResourceId - The resource ID.
        """

        if isinstance(request, dict):
            query = urllib.urlencode(request)
            path = '/shares?{0}'.format(query)
        else:
            path = '/shares'
        return self.request('GET', path)

    def share_resource(self, share_details):
        """Share Specific Resource.
        The *share_details* parameter is a dict, describing the share to create.
        All fields are mandatory:
        - targetEmail - The email address of the user the share the resource with.
        - sharedResourceType - Should be one of the following: BLUEPRINT, LIBRARY_VM, DISK_IMAGE
        - sharedResourceId - The resource ID
        """
        return self.request('POST', '/shares', share_details)

    def delete_share(self, share):
        """Delete Share Data by ID.
        Unshare specific resource.
        """
        if isinstance(share, dict): share = share['id']
        return self.request('DELETE', '/shares/{0}'.format(share))
