コード例 #1
0
    def load(self, response):
        """The load method parses the raw JSON response from the server.

        Most models are not returned in the main response body, but in a key
        such as 'Clusters', defined by the 'data_key' attribute on the class.
        Also, related objects are often returned and can be used to pre-cache
        related model objects without having to contact the server again.  This
        method handles all of those cases.

        Also, if a request has triggered a background operation, the request
        details are returned in a 'Requests' section. We need to store that
        request object so we can poll it until completion.
        """
        if 'Requests' in response and 'Requests' != self.data_key:
            from ambariclient.models import Request
            self.request = Request(self.cluster.requests,
                                   href=response.get('href'),
                                   data=response['Requests'])
        else:
            if 'href' in response:
                self._href = response.pop('href')
            if self.data_key and self.data_key in response:
                self._data.update(response.pop(self.data_key))
                # preload related object collections, if received
                for rel in [
                        x for x in self.relationships
                        if x in response and response[x]
                ]:
                    rel_class = self.relationships[rel]
                    collection = rel_class.collection_class(self.client,
                                                            rel_class,
                                                            parent=self)
                    self._relationship_cache[rel] = collection(response[rel])
            elif not self.data_key:
                self._data.update(response)
コード例 #2
0
    def load(self, response):
        """Parse the GET response for the collection.

        The response from a GET request against that url should look like:
            { 'items': [ item1, item2, ... ] }

        While each of the item objects is usually a subset of the information
        for each model, it generally includes the URL needed to load the full
        data in an 'href' key.  This information is used to lazy-load the
        details on the model, when needed.

        In some rare cases, a collection can have an asynchronous request
        triggered.  For those cases, we handle it here.
        """
        if 'Requests' in response:
            from ambariclient.models import Request
            self.request = Request(self.parent.cluster.requests,
                                   href=response.get('href'),
                                   data=response['Requests'])
        if 'items' in response:
            self._models = []
            for item in response['items']:
                model = self.model_class(self, href=item.get('href'))
                model.load(item)
                self._models.append(model)
コード例 #3
0
    def load(self, response):
        """The load method parses the raw JSON response from the server.

        Most models are not returned in the main response body, but in a key
        such as 'Clusters', defined by the 'data_key' attribute on the class.
        Also, related objects are often returned and can be used to pre-cache
        related model objects without having to contact the server again.  This
        method handles all of those cases.

        Also, if a request has triggered a background operation, the request
        details are returned in a 'Requests' section. We need to store that
        request object so we can poll it until completion.
        """
        if 'Requests' in response and 'Requests' != self.data_key:
            from ambariclient.models import Request
            self.request = Request(self.cluster.requests,
                                   href=response.get('href'),
                                   data=response['Requests'])
        else:
            if 'href' in response:
                self._href = response.pop('href')
            if self.data_key and self.data_key in response:
                self._data.update(response.pop(self.data_key))
                # preload related object collections, if received
                for rel in [x for x in self.relationships if x in response and response[x]]:
                    rel_class = self.relationships[rel]
                    collection = rel_class.collection_class(
                        self.client, rel_class, parent=self
                    )
                    self._relationship_cache[rel] = collection(response[rel])
            elif not self.data_key:
                self._data.update(response)
コード例 #4
0
    def load(self, response):
        """Parse the GET response for the collection.

        The response from a GET request against that url should look like:
            { 'items': [ item1, item2, ... ] }

        While each of the item objects is usually a subset of the information
        for each model, it generally includes the URL needed to load the full
        data in an 'href' key.  This information is used to lazy-load the
        details on the model, when needed.

        In some rare cases, a collection can have an asynchronous request
        triggered.  For those cases, we handle it here.
        """
        if 'Requests' in response:
            from ambariclient.models import Request
            self.request = Request(self.parent.cluster.requests,
                                   href=response.get('href'),
                                   data=response['Requests'])
        if 'items' in response:
            self._models = []
            for item in response['items']:
                model = self.model_class(
                    self,
                    href=item.get('href')
                )
                model.load(item)
                self._models.append(model)
コード例 #5
0
class QueryableModel(Model):
    """A queryable model is a model that is backed by a URL.

    Most resources in the Ambari API are directly accessible via a URL, and this
    class serves as a base class for all of them.  Things like a Host, Cluster,
    etc, that map to a URL all stem from here.

    There are some nice convenience methods like create(), update(), and
    delete().  Unlike some ORMs, there's no way to modify values by updating
    attributes directly and then calling save() or something to send those to
    the server.  You must call update() with the keyword arguments of the fields
    you wish to update.  I've always found that allowing for attribute updates
    is problematic as some users expect that the update will happen immediately,
    when in reality they still have to call another method like save() to make
    those changes permanent. I might recant if enough people request the addition
    of attribute setters.

    All of the data in these objects is lazy-loaded.  It will only do the API
    request at a point where it needs to in order to proceed.  These cases are:

        * accessing an attribute that isn't already loaded
        * accessing a relationship
        * calling 'inflate()' directly
        * calling wait()

    If you hit a situation where you want to force an already-loaded object to
    get the latest data from the server, the refresh() method will do that for
    you.
    """

    collection_class = QueryableModelCollection
    path = None
    data_key = None
    relationships = {}

    def __init__(self, *args, **kwargs):
        self.request = None
        if 'href' in kwargs:
            self._href = kwargs.pop('href')
        else:
            self._href = None
        self._is_inflating = False
        super(QueryableModel, self).__init__(*args, **kwargs)

    @property
    def url(self):
        """Gets the url for the resource this model represents.

        It will just use the 'href' passed in to the constructor if that exists.
        Otherwise, it will generated it based on the collection's url and the
        model's identifier.
        """
        if self._href is not None:
            return self._href
        if self.identifier:
            return '/'.join([self.parent.url, self.identifier])
        raise exceptions.ClientError("Not able to determine object URL")

    def inflate(self):
        """Load the resource from the server, if not already loaded."""
        if not self._is_inflated:
            if self._is_inflating:
                # catch infinite recursion when attempting to inflate
                # an object that doesn't have enough data to inflate
                msg = ("There is not enough data to inflate this object.  "
                       "Need either an href: {} or a {}: {}")
                msg = msg.format(self._href, self.primary_key, self._data.get(self.primary_key))
                raise exceptions.ClientError(msg)

            self._is_inflating = True
            self.load(self.client.get(self.url))
            self._is_inflated = True
            self._is_inflating = False
        return self

    def _generate_input_dict(self, **kwargs):
        if self.data_key:
            data = { self.data_key: {}}
            for field in kwargs:
                if field in self.fields:
                    data[self.data_key][field] = kwargs[field]
                else:
                    data[field] = kwargs[field]
            return data
        else:
            return kwargs

    @events.evented
    def load(self, response):
        """The load method parses the raw JSON response from the server.

        Most models are not returned in the main response body, but in a key
        such as 'Clusters', defined by the 'data_key' attribute on the class.
        Also, related objects are often returned and can be used to pre-cache
        related model objects without having to contact the server again.  This
        method handles all of those cases.

        Also, if a request has triggered a background operation, the request
        details are returned in a 'Requests' section. We need to store that
        request object so we can poll it until completion.
        """
        if 'Requests' in response and 'Requests' != self.data_key:
            from ambariclient.models import Request
            self.request = Request(self.cluster.requests,
                                   href=response.get('href'),
                                   data=response['Requests'])
        else:
            if 'href' in response:
                self._href = response.pop('href')
            if self.data_key and self.data_key in response:
                self._data.update(response.pop(self.data_key))
                # preload related object collections, if received
                for rel in [x for x in self.relationships if x in response and response[x]]:
                    rel_class = self.relationships[rel]
                    collection = rel_class.collection_class(
                        self.client, rel_class, parent=self
                    )
                    self._relationship_cache[rel] = collection(response[rel])
            elif not self.data_key:
                self._data.update(response)

    @events.evented
    def create(self, **kwargs):
        """Create a new instance of this resource type.

        As a general rule, the identifier should have been provided, but in
        some subclasses the identifier is server-side-generated.  Those classes
        have to overload this method to deal with that scenario.
        """
        if self.primary_key in kwargs:
            del kwargs[self.primary_key]
        data = self._generate_input_dict(**kwargs)
        self.load(self.client.post(self.url, data=data))
        return self

    @events.evented
    def update(self, **kwargs):
        """Update a resource by passing in modifications via keyword arguments.

        For example:

            model.update(a='b', b='c')

        is generally converted to:

            PUT model.url { model.data_key: {'a': 'b', 'b': 'c' } }

        If the request body doesn't follow that pattern, you'll need to overload
        this method to handle your particular case.
        """
        data = self._generate_input_dict(**kwargs)
        self.load(self.client.put(self.url, data=data))
        return self

    @events.evented
    def delete(self):
        """Delete a resource by issuing a DELETE http request against it."""
        self.load(self.client.delete(self.url))
        self.parent.remove(self)
        return

    @events.evented
    def wait(self, **kwargs):
        """Wait until any pending asynchronous requests are finished."""
        if self.request:
            self.request.wait(**kwargs)
            self.request = None
        return self.inflate()
コード例 #6
0
class QueryableModelCollection(ModelCollection):
    """A collection of QueryableModel objects.

    These collections are backed by a url that can be used to load and/or
    reload the collection from the server.  For the most part, they are
    lazy-loaded on demand when you attempt to access members of the collection,
    but they can be preloaded with data by passing in a list of dictionaries.
    This comes in handy because the Ambari API often returns related objects
    when you do a GET call on a specific resource.  So for example:

    client.clusters(cluster_name).hosts

    Will call GET /clusters/<cluster_name>
    Which returns all of the basic host information that then pre-populates
    the hosts collection and avoids having to query the server for that data
    when you act on the host objects it contains.
    """
    def __init__(self, *args, **kwargs):
        super(QueryableModelCollection, self).__init__(*args, **kwargs)
        self.request = None
        self._filter = {}

    def __call__(self, *args, **kwargs):
        if len(args) == 1:
            if isinstance(args[0], list):
                # allow for passing in a list of ids and filtering the set
                items = args[0]
            else:
                identifier = str(args[0])
                return self.model_class(self, href='/'.join([self.url, identifier]),
                                        data={self.model_class.primary_key: identifier})
        else:
            items = args

        if len(items) > 0:
            self._models = []
            self._is_inflated = True
            for item in items:
                if isinstance(item, dict):
                    # we're preloading this object from existing response data
                    model = self.model_class(self, href=item['href'])
                    model.load(item)
                else:
                    # we only have the primary id, so create an deflated model
                    model = self.model_class(self, href='/'.join([self.url, item]),
                                             data={self.model_class.primary_key: item})
                self._models.append(model)
            return self

        self._is_inflated = False
        self._filter = {}
        self._models = []
        if kwargs:
            prefix = self.model_class.data_key
            for (key, value) in kwargs.iteritems():
                key = '/'.join([prefix, key])
                if not isinstance(value, six.string_types):
                    value = json.dumps(value)
                self._filter[key] = value

        return self

    @property
    def url(self):
        """The url for this collection."""
        if self.parent is None:
            # TODO: differing API Versions?
            pieces = [self.client.base_url, 'api', 'v1']
        else:
            pieces = [self.parent.url]

        pieces.append(self.model_class.path)

        return '/'.join(pieces)

    def inflate(self):
        """Load the collection from the server, if necessary."""
        if not self._is_inflated:
            self.check_version()
            self.load(self.client.get(self.url, params=self._filter))

        self._is_inflated = True
        return self

    @events.evented
    def load(self, response):
        """Parse the GET response for the collection.

        The response from a GET request against that url should look like:
            { 'items': [ item1, item2, ... ] }

        While each of the item objects is usually a subset of the information
        for each model, it generally includes the URL needed to load the full
        data in an 'href' key.  This information is used to lazy-load the
        details on the model, when needed.

        In some rare cases, a collection can have an asynchronous request
        triggered.  For those cases, we handle it here.
        """
        if 'Requests' in response:
            from ambariclient.models import Request
            self.request = Request(self.parent.cluster.requests,
                                   href=response.get('href'),
                                   data=response['Requests'])
        if 'items' in response:
            self._models = []
            for item in response['items']:
                model = self.model_class(
                    self,
                    href=item.get('href')
                )
                model.load(item)
                self._models.append(model)

    def create(self, *args, **kwargs):
        """Add a resource to this collection."""
        href = self.url
        if len(args) == 1:
            kwargs[self.model_class.primary_key] = args[0]
            href = '/'.join([href, args[0]])

        model = self.model_class(self, href=href, data=kwargs)
        model.create(**kwargs)
        self._models.append(model)
        return model

    def update(self, **kwargs):
        """Update all resources in this collection."""
        self.inflate()
        for model in self._models:
            model.update(**kwargs)
        return self

    def delete(self):
        """Delete all resources in this collection."""
        self.inflate()
        for model in self._models:
            model.delete()
        return

    @events.evented
    def wait(self, **kwargs):
        """Wait until any pending asynchronous requests are finished for this collection."""
        if self.request:
            self.request.wait(**kwargs)
            self.request = None
        return self.inflate()

    def check_version(self):
        if (self.model_class.min_version > OLDEST_SUPPORTED_VERSION
                and self.client.version < self.model_class.min_version):
            min_version = utils.version_str(self.model_class.min_version)
            curr_version = utils.version_str(self.client.version)
            raise exceptions.ClientError(message="Cannot access %s in version %s, it was added in "
                                                 "version %s" % (self.url, curr_version,
                                                                 min_version))
コード例 #7
0
class QueryableModel(Model):
    """A queryable model is a model that is backed by a URL.

    Most resources in the Ambari API are directly accessible via a URL, and this
    class serves as a base class for all of them.  Things like a Host, Cluster,
    etc, that map to a URL all stem from here.

    There are some nice convenience methods like create(), update(), and
    delete().  Unlike some ORMs, there's no way to modify values by updating
    attributes directly and then calling save() or something to send those to
    the server.  You must call update() with the keyword arguments of the fields
    you wish to update.  I've always found that allowing for attribute updates
    is problematic as some users expect that the update will happen immediately,
    when in reality they still have to call another method like save() to make
    those changes permanent. I might recant if enough people request the addition
    of attribute setters.

    All of the data in these objects is lazy-loaded.  It will only do the API
    request at a point where it needs to in order to proceed.  These cases are:

        * accessing an attribute that isn't already loaded
        * accessing a relationship
        * calling 'inflate()' directly
        * calling wait()

    If you hit a situation where you want to force an already-loaded object to
    get the latest data from the server, the refresh() method will do that for
    you.
    """

    collection_class = QueryableModelCollection
    use_key_prefix = True
    path = None
    data_key = None
    relationships = {}

    def __init__(self, *args, **kwargs):
        self.request = None
        if 'href' in kwargs:
            self._href = kwargs.pop('href')
        else:
            self._href = None
        self._is_inflating = False
        super(QueryableModel, self).__init__(*args, **kwargs)

    @property
    def url(self):
        """Gets the url for the resource this model represents.

        It will just use the 'href' passed in to the constructor if that exists.
        Otherwise, it will generated it based on the collection's url and the
        model's identifier.
        """
        if self._href is not None:
            return self._href
        if self.identifier:
            return '/'.join([self.parent.url, self.identifier])
        raise exceptions.ClientError("Not able to determine object URL")

    def inflate(self):
        """Load the resource from the server, if not already loaded."""
        if not self._is_inflated:
            if self._is_inflating:
                # catch infinite recursion when attempting to inflate
                # an object that doesn't have enough data to inflate
                msg = ("There is not enough data to inflate this object.  "
                       "Need either an href: {} or a {}: {}")
                msg = msg.format(self._href, self.primary_key,
                                 self._data.get(self.primary_key))
                raise exceptions.ClientError(msg)

            self._is_inflating = True
            self.load(self.client.get(self.url))
            self._is_inflated = True
            self._is_inflating = False
        return self

    def _generate_input_dict(self, **kwargs):
        if self.data_key:
            data = {self.data_key: {}}
            for field in kwargs:
                if field in self.fields:
                    data[self.data_key][field] = kwargs[field]
                else:
                    data[field] = kwargs[field]
            return data
        else:
            return kwargs

    @events.evented
    def load(self, response):
        """The load method parses the raw JSON response from the server.

        Most models are not returned in the main response body, but in a key
        such as 'Clusters', defined by the 'data_key' attribute on the class.
        Also, related objects are often returned and can be used to pre-cache
        related model objects without having to contact the server again.  This
        method handles all of those cases.

        Also, if a request has triggered a background operation, the request
        details are returned in a 'Requests' section. We need to store that
        request object so we can poll it until completion.
        """
        if 'Requests' in response and 'Requests' != self.data_key:
            from ambariclient.models import Request
            self.request = Request(self.cluster.requests,
                                   href=response.get('href'),
                                   data=response['Requests'])
        else:
            if 'href' in response:
                self._href = response.pop('href')
            if self.data_key and self.data_key in response:
                self._data.update(response.pop(self.data_key))
                # preload related object collections, if received
                for rel in [
                        x for x in self.relationships
                        if x in response and response[x]
                ]:
                    rel_class = self.relationships[rel]
                    collection = rel_class.collection_class(self.client,
                                                            rel_class,
                                                            parent=self)
                    self._relationship_cache[rel] = collection(response[rel])
            elif not self.data_key:
                self._data.update(response)

    @events.evented
    def create(self, **kwargs):
        """Create a new instance of this resource type.

        As a general rule, the identifier should have been provided, but in
        some subclasses the identifier is server-side-generated.  Those classes
        have to overload this method to deal with that scenario.
        """
        if self.primary_key in kwargs:
            del kwargs[self.primary_key]
        data = self._generate_input_dict(**kwargs)
        self.load(self.client.post(self.url, data=data))
        return self

    @events.evented
    def update(self, **kwargs):
        """Update a resource by passing in modifications via keyword arguments.

        For example:

            model.update(a='b', b='c')

        is generally converted to:

            PUT model.url { model.data_key: {'a': 'b', 'b': 'c' } }

        If the request body doesn't follow that pattern, you'll need to overload
        this method to handle your particular case.
        """
        data = self._generate_input_dict(**kwargs)
        self.load(self.client.put(self.url, data=data))
        return self

    @events.evented
    def delete(self):
        """Delete a resource by issuing a DELETE http request against it."""
        self.load(self.client.delete(self.url))
        self.parent.remove(self)
        return

    @events.evented
    def wait(self, **kwargs):
        """Wait until any pending asynchronous requests are finished."""
        if self.request:
            self.request.wait(**kwargs)
            self.request = None
        return self.inflate()
コード例 #8
0
class QueryableModelCollection(ModelCollection):
    """A collection of QueryableModel objects.

    These collections are backed by a url that can be used to load and/or
    reload the collection from the server.  For the most part, they are
    lazy-loaded on demand when you attempt to access members of the collection,
    but they can be preloaded with data by passing in a list of dictionaries.
    This comes in handy because the Ambari API often returns related objects
    when you do a GET call on a specific resource.  So for example:

    client.clusters(cluster_name).hosts

    Will call GET /clusters/<cluster_name>
    Which returns all of the basic host information that then pre-populates
    the hosts collection and avoids having to query the server for that data
    when you act on the host objects it contains.
    """
    def __init__(self, *args, **kwargs):
        super(QueryableModelCollection, self).__init__(*args, **kwargs)
        self.request = None
        self._filter = {}

    def __call__(self, *args, **kwargs):
        if len(args) == 1:
            if isinstance(args[0], list):
                # allow for passing in a list of ids and filtering the set
                items = args[0]
            else:
                identifier = str(args[0])
                return self.model_class(
                    self,
                    href='/'.join([self.url, identifier]),
                    data={self.model_class.primary_key: identifier})
        else:
            items = args

        if len(items) > 0:
            self._models = []
            self._is_inflated = True
            for item in items:
                if isinstance(item, dict):
                    # we're preloading this object from existing response data
                    model = self.model_class(self, href=item['href'])
                    model.load(item)
                else:
                    # we only have the primary id, so create an deflated model
                    model = self.model_class(
                        self,
                        href='/'.join([self.url, item]),
                        data={self.model_class.primary_key: item})
                self._models.append(model)
            return self

        self._is_inflated = False
        self._filter = {}
        self._models = []
        if kwargs:
            prefix = self.model_class.data_key
            for (key, value) in kwargs.iteritems():
                if self.model_class.use_key_prefix:
                    key = '/'.join([prefix, key])
                if not isinstance(value, six.string_types):
                    value = json.dumps(value)
                self._filter[key] = value

        return self

    @property
    def url(self):
        """The url for this collection."""
        if self.parent is None:
            # TODO: differing API Versions?
            pieces = [self.client.base_url, 'api', 'v1']
        else:
            pieces = [self.parent.url]

        pieces.append(self.model_class.path)

        return '/'.join(pieces)

    def inflate(self):
        """Load the collection from the server, if necessary."""
        if not self._is_inflated:
            self.check_version()
            self.load(self.client.get(self.url, params=self._filter))

        self._is_inflated = True
        return self

    @events.evented
    def load(self, response):
        """Parse the GET response for the collection.

        The response from a GET request against that url should look like:
            { 'items': [ item1, item2, ... ] }

        While each of the item objects is usually a subset of the information
        for each model, it generally includes the URL needed to load the full
        data in an 'href' key.  This information is used to lazy-load the
        details on the model, when needed.

        In some rare cases, a collection can have an asynchronous request
        triggered.  For those cases, we handle it here.
        """
        if 'Requests' in response:
            from ambariclient.models import Request
            self.request = Request(self.parent.cluster.requests,
                                   href=response.get('href'),
                                   data=response['Requests'])
        if 'items' in response:
            self._models = []
            for item in response['items']:
                model = self.model_class(self, href=item.get('href'))
                model.load(item)
                self._models.append(model)

    def create(self, *args, **kwargs):
        """Add a resource to this collection."""
        href = self.url
        if len(args) == 1:
            kwargs[self.model_class.primary_key] = args[0]
            href = '/'.join([href, args[0]])

        model = self.model_class(self, href=href, data=kwargs)
        model.create(**kwargs)
        self._models.append(model)
        return model

    def update(self, **kwargs):
        """Update all resources in this collection."""
        self.inflate()
        for model in self._models:
            model.update(**kwargs)
        return self

    def delete(self):
        """Delete all resources in this collection."""
        self.inflate()
        for model in self._models:
            model.delete()
        return

    @events.evented
    def wait(self, **kwargs):
        """Wait until any pending asynchronous requests are finished for this collection."""
        if self.request:
            self.request.wait(**kwargs)
            self.request = None
        return self.inflate()

    def check_version(self):
        if (self.model_class.min_version > OLDEST_SUPPORTED_VERSION
                and self.client.version < self.model_class.min_version):
            min_version = utils.version_str(self.model_class.min_version)
            curr_version = utils.version_str(self.client.version)
            raise exceptions.ClientError(
                message="Cannot access %s in version %s, it was added in "
                "version %s" % (self.url, curr_version, min_version))