~ubuntu-branches/ubuntu/trusty/ceilometer/trusty-proposed

« back to all changes in this revision

Viewing changes to ceilometer/api/controllers/v2.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short, James Page, Chuck Short
  • Date: 2014-01-23 15:08:11 UTC
  • mfrom: (1.1.11)
  • Revision ID: package-import@ubuntu.com-20140123150811-1zaismsuyh1hcl8y
Tags: 2014.1~b2-0ubuntu1
[ James Page ]
* d/control: Add python-jsonpath-rw to BD's.
* d/p/fix-setup-requirements.patch: Bump WebOb to support < 1.4.
 (LP: #1261101)

[ Chuck Short ]
* New upstream version.
* debian/control, debian/ceilometer-common.install: Split out
  ceilometer-alarm-evaluator and ceilometer-alarm-notifier into their
  own packages. (LP: #1250002)
* debian/ceilometer-agent-central.logrotate,
  debian/ceilometer-agent-compute.logrotate,
  debian/ceilometer-api.logrotate,
  debian/ceilometer-collector.logrotate: Add logrotate files, 
  thanks to Ahmed Rahal. (LP: #1224223)
* Fix typos in upstart files.

Show diffs side-by-side

added added

removed removed

Lines of Context:
26
26
import base64
27
27
import copy
28
28
import datetime
 
29
import functools
29
30
import inspect
30
31
import json
31
32
import uuid
179
180
 
180
181
 
181
182
class Query(_Base):
182
 
    """Sample query filter.
 
183
    """Query filter.
183
184
    """
184
185
 
 
186
    # The data types supported by the query.
 
187
    _supported_types = ['integer', 'float', 'string', 'boolean']
 
188
 
 
189
    # Functions to convert the data field to the correct type.
 
190
    _type_converters = {'integer': int,
 
191
                        'float': float,
 
192
                        'boolean': strutils.bool_from_string,
 
193
                        'string': six.text_type,
 
194
                        'datetime': timeutils.parse_isotime}
 
195
 
185
196
    _op = None  # provide a default
186
197
 
187
198
    def get_op(self):
249
260
                            ' automatically') % (self.value)
250
261
                    LOG.debug(msg)
251
262
            else:
252
 
                if type == 'integer':
253
 
                    converted_value = int(self.value)
254
 
                elif type == 'float':
255
 
                    converted_value = float(self.value)
256
 
                elif type == 'boolean':
257
 
                    converted_value = strutils.bool_from_string(self.value)
258
 
                elif type == 'string':
259
 
                    converted_value = self.value
260
 
                else:
261
 
                    # For now, this method only support integer, float,
262
 
                    # boolean and and string as the metadata type. A TypeError
263
 
                    # will be raised for any other type.
 
263
                if type not in self._supported_types:
 
264
                    # Types must be explicitly declared so the
 
265
                    # correct type converter may be used. Subclasses
 
266
                    # of Query may define _supported_types and
 
267
                    # _type_converters to define their own types.
264
268
                    raise TypeError()
 
269
                converted_value = self._type_converters[type](self.value)
265
270
        except ValueError:
266
 
            msg = _('Failed to convert the metadata value %(value)s'
 
271
            msg = _('Failed to convert the value %(value)s'
267
272
                    ' to the expected data type %(type)s.') % \
268
273
                {'value': self.value, 'type': type}
269
274
            raise ClientSideError(msg)
270
275
        except TypeError:
271
 
            msg = _('The data type %s is not supported. The supported'
272
 
                    ' data type list is: integer, float, boolean and'
273
 
                    ' string.') % (type)
 
276
            msg = _('The data type %(type)s is not supported. The supported'
 
277
                    ' data type list is: %(supported)s') % \
 
278
                {'type': type, 'supported': self._supported_types}
274
279
            raise ClientSideError(msg)
275
280
        except Exception:
276
281
            msg = _('Unexpected exception converting %(value)s to'
527
532
                  notification, notify.INFO, payload)
528
533
 
529
534
 
530
 
class Sample(_Base):
 
535
class OldSample(_Base):
531
536
    """A single measurement for a given meter and resource.
 
537
 
 
538
    This class is deprecated in favor of Sample.
532
539
    """
533
540
 
534
541
    source = wtypes.text
576
583
        if timestamp and isinstance(timestamp, basestring):
577
584
            timestamp = timeutils.parse_isotime(timestamp)
578
585
 
579
 
        super(Sample, self).__init__(counter_volume=counter_volume,
580
 
                                     resource_metadata=resource_metadata,
581
 
                                     timestamp=timestamp, **kwds)
 
586
        super(OldSample, self).__init__(counter_volume=counter_volume,
 
587
                                        resource_metadata=resource_metadata,
 
588
                                        timestamp=timestamp, **kwds)
582
589
 
583
590
        if self.resource_metadata in (wtypes.Unset, None):
584
591
            self.resource_metadata = {}
706
713
        pecan.request.context['meter_id'] = meter_id
707
714
        self._id = meter_id
708
715
 
709
 
    @wsme_pecan.wsexpose([Sample], [Query], int)
 
716
    @wsme_pecan.wsexpose([OldSample], [Query], int)
710
717
    def get_all(self, q=[], limit=None):
711
718
        """Return samples for the meter.
712
719
 
714
721
        :param limit: Maximum number of samples to return.
715
722
        """
716
723
        if limit and limit < 0:
717
 
            raise ValueError("Limit must be positive")
 
724
            raise ClientSideError(_("Limit must be positive"))
718
725
        kwargs = _query_to_kwargs(q, storage.SampleFilter.__init__)
719
726
        kwargs['meter'] = self._id
720
727
        f = storage.SampleFilter(**kwargs)
721
 
        return [Sample.from_db_model(e)
 
728
        return [OldSample.from_db_model(e)
722
729
                for e in pecan.request.storage_conn.get_samples(f, limit=limit)
723
730
                ]
724
731
 
725
 
    @wsme_pecan.wsexpose([Sample], body=[Sample])
 
732
    @wsme_pecan.wsexpose([OldSample], body=[OldSample])
726
733
    def post(self, samples):
727
734
        """Post a list of new Samples to Ceilometer.
728
735
 
884
891
                for m in pecan.request.storage_conn.get_meters(**kwargs)]
885
892
 
886
893
 
 
894
class Sample(_Base):
 
895
    """One measurement."""
 
896
 
 
897
    id = wtypes.text
 
898
    "The unique identifier for the sample."
 
899
 
 
900
    meter = wtypes.text
 
901
    "The meter name this sample is for."
 
902
 
 
903
    type = wtypes.Enum(str, *sample.TYPES)
 
904
    "The meter type (see :ref:`measurements`)"
 
905
 
 
906
    unit = wtypes.text
 
907
    "The unit of measure."
 
908
 
 
909
    volume = float
 
910
    "The metered value."
 
911
 
 
912
    user_id = wtypes.text
 
913
    "The user this sample was taken for."
 
914
 
 
915
    project_id = wtypes.text
 
916
    "The project this sample was taken for."
 
917
 
 
918
    resource_id = wtypes.text
 
919
    "The :class:`Resource` this sample was taken for."
 
920
 
 
921
    source = wtypes.text
 
922
    "The source that identifies where the sample comes from."
 
923
 
 
924
    timestamp = datetime.datetime
 
925
    "When the sample has been generated."
 
926
 
 
927
    metadata = {wtypes.text: wtypes.text}
 
928
    "Arbitrary metadata associated with the sample."
 
929
 
 
930
    @classmethod
 
931
    def from_db_model(cls, m):
 
932
        return cls(id=m.message_id,
 
933
                   meter=m.counter_name,
 
934
                   type=m.counter_type,
 
935
                   unit=m.counter_unit,
 
936
                   volume=m.counter_volume,
 
937
                   user_id=m.user_id,
 
938
                   project_id=m.project_id,
 
939
                   resource_id=m.resource_id,
 
940
                   timestamp=m.timestamp,
 
941
                   metadata=_flatten_metadata(m.resource_metadata))
 
942
 
 
943
    @classmethod
 
944
    def sample(cls):
 
945
        return cls(id=str(uuid.uuid1()),
 
946
                   meter='instance',
 
947
                   type='gauge',
 
948
                   unit='instance',
 
949
                   resource_id='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36',
 
950
                   project_id='35b17138-b364-4e6a-a131-8f3099c5be68',
 
951
                   user_id='efd87807-12d2-4b38-9c70-5f5c2ac427ff',
 
952
                   timestamp=timeutils.utcnow(),
 
953
                   source='openstack',
 
954
                   metadata={'name1': 'value1',
 
955
                             'name2': 'value2'},
 
956
                   )
 
957
 
 
958
 
 
959
class SamplesController(rest.RestController):
 
960
    """Controller managing the samples."""
 
961
 
 
962
    @wsme_pecan.wsexpose([Sample], [Query], int)
 
963
    def get_all(self, q=[], limit=None):
 
964
        """Return all known samples, based on the data recorded so far.
 
965
 
 
966
        :param q: Filter rules for the samples to be returned.
 
967
        :param limit: Maximum number of samples to be returned.
 
968
        """
 
969
        if limit and limit < 0:
 
970
            raise ClientSideError(_("Limit must be positive"))
 
971
        kwargs = _query_to_kwargs(q, storage.SampleFilter.__init__)
 
972
        f = storage.SampleFilter(**kwargs)
 
973
        return map(Sample.from_db_model,
 
974
                   pecan.request.storage_conn.get_samples(f, limit=limit))
 
975
 
 
976
    @wsme_pecan.wsexpose(Sample, wtypes.text)
 
977
    def get_one(self, sample_id):
 
978
        """Return a sample
 
979
 
 
980
        :param sample_id: the id of the sample
 
981
        """
 
982
        f = storage.SampleFilter(message_id=sample_id)
 
983
 
 
984
        samples = list(pecan.request.storage_conn.get_samples(f))
 
985
        if len(samples) < 1:
 
986
            raise EntityNotFound(_('Sample'), sample_id)
 
987
 
 
988
        return Sample.from_db_model(samples[0])
 
989
 
 
990
 
887
991
class Resource(_Base):
888
992
    """An externally defined object for which samples have been received.
889
993
    """
897
1001
    user_id = wtypes.text
898
1002
    "The ID of the user who created the resource or updated it last"
899
1003
 
900
 
    timestamp = datetime.datetime
901
 
    "UTC date and time of the last update to any meter for the resource"
 
1004
    first_sample_timestamp = datetime.datetime
 
1005
    "UTC date & time of the first sample associated with the resource"
 
1006
 
 
1007
    last_sample_timestamp = datetime.datetime
 
1008
    "UTC date & time of the last sample associated with the resource"
902
1009
 
903
1010
    metadata = {wtypes.text: wtypes.text}
904
1011
    "Arbitrary metadata associated with the resource"
1157
1264
 
1158
1265
    @staticmethod
1159
1266
    def validate(alarm):
1160
 
        if (alarm.threshold_rule == wtypes.Unset
1161
 
                and alarm.combination_rule == wtypes.Unset):
 
1267
        if (alarm.threshold_rule in (wtypes.Unset, None)
 
1268
                and alarm.combination_rule in (wtypes.Unset, None)):
1162
1269
            error = _("either threshold_rule or combination_rule "
1163
1270
                      "must be set")
1164
1271
            raise ClientSideError(error)
1279
1386
        auth_project = acl.get_limited_to_project(pecan.request.headers)
1280
1387
        alarms = list(self.conn.get_alarms(alarm_id=self._id,
1281
1388
                                           project=auth_project))
1282
 
        if len(alarms) < 1:
 
1389
        if not alarms:
1283
1390
            raise EntityNotFound(_('Alarm'), self._id)
1284
1391
        return alarms[0]
1285
1392
 
1481
1588
        # make sure alarms are unique by name per project.
1482
1589
        alarms = list(conn.get_alarms(name=data.name,
1483
1590
                                      project=data.project_id))
1484
 
        if len(alarms) > 0:
 
1591
        if alarms:
1485
1592
            raise ClientSideError(_("Alarm with that name exists"))
1486
1593
 
1487
1594
        try:
1506
1613
                for m in pecan.request.storage_conn.get_alarms(**kwargs)]
1507
1614
 
1508
1615
 
 
1616
class TraitDescription(_Base):
 
1617
    """A description of a trait, with no associated value."""
 
1618
 
 
1619
    type = wtypes.text
 
1620
    "the data type, defaults to string"
 
1621
 
 
1622
    name = wtypes.text
 
1623
    "the name of the trait"
 
1624
 
 
1625
    @classmethod
 
1626
    def sample(cls):
 
1627
        return cls(name='service',
 
1628
                   type='string'
 
1629
                   )
 
1630
 
 
1631
 
 
1632
class EventQuery(Query):
 
1633
    """Query arguments for Event Queries."""
 
1634
 
 
1635
    _supported_types = ['integer', 'float', 'string', 'datetime']
 
1636
 
 
1637
    type = wsme.wsattr(wtypes.text, default='string')
 
1638
    "the type of the trait filter, defaults to string"
 
1639
 
 
1640
    def __repr__(self):
 
1641
        # for logging calls
 
1642
        return '<EventQuery %r %s %r %s>' % (self.field,
 
1643
                                             self.op,
 
1644
                                             self._get_value_as_type(),
 
1645
                                             self.type)
 
1646
 
 
1647
 
 
1648
class Trait(_Base):
 
1649
    """A Trait associated with an event."""
 
1650
 
 
1651
    name = wtypes.text
 
1652
    "The name of the trait"
 
1653
 
 
1654
    value = wtypes.text
 
1655
    "the value of the trait"
 
1656
 
 
1657
    type = wtypes.text
 
1658
    "the type of the trait (string, integer, float or datetime)"
 
1659
 
 
1660
    @classmethod
 
1661
    def sample(cls):
 
1662
        return cls(name='service',
 
1663
                   type='string',
 
1664
                   value='compute.hostname'
 
1665
                   )
 
1666
 
 
1667
 
 
1668
class Event(_Base):
 
1669
    """A System event."""
 
1670
 
 
1671
    message_id = wtypes.text
 
1672
    "The message ID for the notification"
 
1673
 
 
1674
    event_type = wtypes.text
 
1675
    "The type of the event"
 
1676
 
 
1677
    _traits = None
 
1678
 
 
1679
    def get_traits(self):
 
1680
        return self._traits
 
1681
 
 
1682
    @staticmethod
 
1683
    def _convert_storage_trait(t):
 
1684
        """Helper method to convert a storage model into an API trait
 
1685
        instance. If an API trait instance is passed in, just return it.
 
1686
        """
 
1687
        if isinstance(t, Trait):
 
1688
            return t
 
1689
        value = (six.text_type(t.value)
 
1690
                 if not t.dtype == storage.models.Trait.DATETIME_TYPE
 
1691
                 else t.value.isoformat())
 
1692
        type = storage.models.Trait.get_name_by_type(t.dtype)
 
1693
        return Trait(name=t.name, type=type, value=value)
 
1694
 
 
1695
    def set_traits(self, traits):
 
1696
        self._traits = map(self._convert_storage_trait, traits)
 
1697
 
 
1698
    traits = wsme.wsproperty(wtypes.ArrayType(Trait),
 
1699
                             get_traits,
 
1700
                             set_traits)
 
1701
    "Event specific properties"
 
1702
 
 
1703
    generated = datetime.datetime
 
1704
    "The time the event occurred"
 
1705
 
 
1706
    @classmethod
 
1707
    def sample(cls):
 
1708
        return cls(
 
1709
            event_type='compute.instance.update',
 
1710
            generated='2013-11-11T20:00:00',
 
1711
            message_id='94834db1-8f1b-404d-b2ec-c35901f1b7f0',
 
1712
            traits={
 
1713
                'request_id': 'req-4e2d67b8-31a4-48af-bb2f-9df72a353a72',
 
1714
                'service': 'conductor.tem-devstack-01',
 
1715
                'tenant_id': '7f13f2b17917463b9ee21aa92c4b36d6'
 
1716
            }
 
1717
        )
 
1718
 
 
1719
 
 
1720
def requires_admin(func):
 
1721
 
 
1722
    @functools.wraps(func)
 
1723
    def wrapped(*args, **kwargs):
 
1724
        usr_limit, proj_limit = acl.get_limited_to(pecan.request.headers)
 
1725
        # If User and Project are None, you have full access.
 
1726
        if usr_limit and proj_limit:
 
1727
            raise ClientSideError(_("Not Authorized"), status_code=403)
 
1728
        return func(*args, **kwargs)
 
1729
 
 
1730
    return wrapped
 
1731
 
 
1732
 
 
1733
def _event_query_to_event_filter(q):
 
1734
    evt_model_filter = {
 
1735
        'event_type': None,
 
1736
        'message_id': None,
 
1737
        'start_time': None,
 
1738
        'end_time': None
 
1739
    }
 
1740
    traits_filter = []
 
1741
 
 
1742
    for i in q:
 
1743
        # FIXME(herndon): Support for operators other than
 
1744
        # 'eq' will come later.
 
1745
        if i.op != 'eq':
 
1746
            error = _("operator %s not supported") % i.op
 
1747
            raise ClientSideError(error)
 
1748
        if i.field in evt_model_filter:
 
1749
            evt_model_filter[i.field] = i.value
 
1750
        else:
 
1751
            traits_filter.append({"key": i.field,
 
1752
                                  i.type: i._get_value_as_type()})
 
1753
    return storage.EventFilter(traits_filter=traits_filter, **evt_model_filter)
 
1754
 
 
1755
 
 
1756
class TraitsController(rest.RestController):
 
1757
    """Works on Event Traits."""
 
1758
 
 
1759
    @requires_admin
 
1760
    @wsme_pecan.wsexpose([Trait], wtypes.text, wtypes.text)
 
1761
    def get_one(self, event_type, trait_name):
 
1762
        """Return all instances of a trait for an event type.
 
1763
 
 
1764
        :param event_type: Event type to filter traits by
 
1765
        :param trait_name: Trait to return values for
 
1766
        """
 
1767
        LOG.debug(_("Getting traits for %s") % event_type)
 
1768
        return [Trait(name=t.name, type=t.get_type_name(), value=t.value)
 
1769
                for t in pecan.request.storage_conn
 
1770
                .get_traits(event_type, trait_name)]
 
1771
 
 
1772
    @requires_admin
 
1773
    @wsme_pecan.wsexpose([TraitDescription], wtypes.text)
 
1774
    def get_all(self, event_type):
 
1775
        """Return all trait names for an event type.
 
1776
 
 
1777
        :param event_type: Event type to filter traits by
 
1778
        """
 
1779
        get_trait_name = storage.models.Trait.get_name_by_type
 
1780
        return [TraitDescription(name=t['name'],
 
1781
                                 type=get_trait_name(t['data_type']))
 
1782
                for t in pecan.request.storage_conn
 
1783
                .get_trait_types(event_type)]
 
1784
 
 
1785
 
 
1786
class EventTypesController(rest.RestController):
 
1787
    """Works on Event Types in the system."""
 
1788
 
 
1789
    traits = TraitsController()
 
1790
 
 
1791
    # FIXME(herndon): due to a bug in pecan, making this method
 
1792
    # get_all instead of get will hide the traits subcontroller.
 
1793
    # https://bugs.launchpad.net/pecan/+bug/1262277
 
1794
    @requires_admin
 
1795
    @wsme_pecan.wsexpose([unicode])
 
1796
    def get(self):
 
1797
        """Get all event types.
 
1798
        """
 
1799
        return list(pecan.request.storage_conn.get_event_types())
 
1800
 
 
1801
 
 
1802
class EventsController(rest.RestController):
 
1803
    """Works on Events."""
 
1804
 
 
1805
    @requires_admin
 
1806
    @wsme_pecan.wsexpose([Event], [EventQuery])
 
1807
    def get_all(self, q=[]):
 
1808
        """Return all events matching the query filters.
 
1809
 
 
1810
        :param q: Filter arguments for which Events to return
 
1811
        """
 
1812
        event_filter = _event_query_to_event_filter(q)
 
1813
        return [Event(message_id=event.message_id,
 
1814
                      event_type=event.event_type,
 
1815
                      generated=event.generated,
 
1816
                      traits=event.traits)
 
1817
                for event in
 
1818
                pecan.request.storage_conn.get_events(event_filter)]
 
1819
 
 
1820
    @requires_admin
 
1821
    @wsme_pecan.wsexpose(Event, wtypes.text)
 
1822
    def get_one(self, message_id):
 
1823
        """Return a single event with the given message id.
 
1824
 
 
1825
        :param message_id: Message ID of the Event to be returned
 
1826
        """
 
1827
        event_filter = storage.EventFilter(message_id=message_id)
 
1828
        events = pecan.request.storage_conn.get_events(event_filter)
 
1829
        if not events:
 
1830
            raise EntityNotFound(_("Event"), message_id)
 
1831
 
 
1832
        if len(events) > 1:
 
1833
            LOG.error(_("More than one event with "
 
1834
                        "id %s returned from storage driver") % message_id)
 
1835
 
 
1836
        event = events[0]
 
1837
 
 
1838
        return Event(message_id=event.message_id,
 
1839
                     event_type=event.event_type,
 
1840
                     generated=event.generated,
 
1841
                     traits=event.traits)
 
1842
 
 
1843
 
1509
1844
class V2Controller(object):
1510
1845
    """Version 2 API controller root."""
1511
1846
 
1512
1847
    resources = ResourcesController()
1513
1848
    meters = MetersController()
 
1849
    samples = SamplesController()
1514
1850
    alarms = AlarmsController()
 
1851
    event_types = EventTypesController()
 
1852
    events = EventsController()