Add utility class for definition-based data queries
[ganeti-local] / lib / query.py
1 #
2 #
3
4 # Copyright (C) 2010 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Module for query operations"""
23
24 import operator
25 import re
26
27 from ganeti import constants
28 from ganeti import errors
29 from ganeti import utils
30 from ganeti import compat
31 from ganeti import objects
32 from ganeti import ht
33
34
35 FIELD_NAME_RE = re.compile(r"^[a-z0-9/._]+$")
36 TITLE_RE = re.compile(r"^[^\s]+$")
37
38 #: Verification function for each field type
39 _VERIFY_FN = {
40   constants.QFT_UNKNOWN: ht.TNone,
41   constants.QFT_TEXT: ht.TString,
42   constants.QFT_BOOL: ht.TBool,
43   constants.QFT_NUMBER: ht.TInt,
44   constants.QFT_UNIT: ht.TInt,
45   constants.QFT_TIMESTAMP: ht.TOr(ht.TInt, ht.TFloat),
46   constants.QFT_OTHER: lambda _: True,
47   }
48
49
50 def _GetUnknownField(ctx, item): # pylint: disable-msg=W0613
51   """Gets the contents of an unknown field.
52
53   """
54   return (constants.QRFS_UNKNOWN, None)
55
56
57 def _GetQueryFields(fielddefs, selected):
58   """Calculates the internal list of selected fields.
59
60   Unknown fields are returned as L{constants.QFT_UNKNOWN}.
61
62   @type fielddefs: dict
63   @param fielddefs: Field definitions
64   @type selected: list of strings
65   @param selected: List of selected fields
66
67   """
68   result = []
69
70   for name in selected:
71     try:
72       fdef = fielddefs[name]
73     except KeyError:
74       fdef = (_MakeField(name, name, constants.QFT_UNKNOWN),
75               None, _GetUnknownField)
76
77     assert len(fdef) == 3
78
79     result.append(fdef)
80
81   return result
82
83
84 def GetAllFields(fielddefs):
85   """Extract L{objects.QueryFieldDefinition} from field definitions.
86
87   @rtype: list of L{objects.QueryFieldDefinition}
88
89   """
90   return [fdef for (fdef, _, _) in fielddefs]
91
92
93 class Query:
94   def __init__(self, fieldlist, selected):
95     """Initializes this class.
96
97     The field definition is a dictionary with the field's name as a key and a
98     tuple containing, in order, the field definition object
99     (L{objects.QueryFieldDefinition}, the data kind to help calling code
100     collect data and a retrieval function. The retrieval function is called
101     with two parameters, in order, the data container and the item in container
102     (see L{Query.Query}).
103
104     Users of this class can call L{RequestedData} before preparing the data
105     container to determine what data is needed.
106
107     @type fieldlist: dictionary
108     @param fieldlist: Field definitions
109     @type selected: list of strings
110     @param selected: List of selected fields
111
112     """
113     self._fields = _GetQueryFields(fieldlist, selected)
114
115   def RequestedData(self):
116     """Gets requested kinds of data.
117
118     @rtype: frozenset
119
120     """
121     return frozenset(datakind
122                      for (_, datakind, _) in self._fields
123                      if datakind is not None)
124
125   def GetFields(self):
126     """Returns the list of fields for this query.
127
128     Includes unknown fields.
129
130     @rtype: List of L{objects.QueryFieldDefinition}
131
132     """
133     return GetAllFields(self._fields)
134
135   def Query(self, ctx):
136     """Execute a query.
137
138     @param ctx: Data container passed to field retrieval functions, must
139       support iteration using C{__iter__}
140
141     """
142     result = [[fn(ctx, item) for (_, _, fn) in self._fields]
143               for item in ctx]
144
145     # Verify result
146     if __debug__:
147       for (idx, row) in enumerate(result):
148         assert _VerifyResultRow(self._fields, row), \
149                ("Inconsistent result for fields %s in row %s: %r" %
150                 (self._fields, idx, row))
151
152     return result
153
154   def OldStyleQuery(self, ctx):
155     """Query with "old" query result format.
156
157     See L{Query.Query} for arguments.
158
159     """
160     unknown = set(fdef.name
161                   for (fdef, _, _) in self._fields
162                   if fdef.kind == constants.QFT_UNKNOWN)
163     if unknown:
164       raise errors.OpPrereqError("Unknown output fields selected: %s" %
165                                  (utils.CommaJoin(unknown), ),
166                                  errors.ECODE_INVAL)
167
168     return [[value for (_, value) in row]
169             for row in self.Query(ctx)]
170
171
172 def _VerifyResultRow(fields, row):
173   """Verifies the contents of a query result row.
174
175   @type fields: list
176   @param fields: Field definitions for result
177   @type row: list of tuples
178   @param row: Row data
179
180   """
181   return (len(row) == len(fields) and
182           compat.all((status == constants.QRFS_NORMAL and
183                       _VERIFY_FN[fdef.kind](value)) or
184                      # Value for an abnormal status must be None
185                      (status != constants.QRFS_NORMAL and value is None)
186                      for ((status, value), (fdef, _, _)) in zip(row, fields)))
187
188
189 def _PrepareFieldList(fields):
190   """Prepares field list for use by L{Query}.
191
192   Converts the list to a dictionary and does some verification.
193
194   @type fields: list of tuples; (L{objects.QueryFieldDefinition}, data kind,
195     retrieval function)
196   @param fields: List of fields
197   @rtype: dict
198   @return: Field dictionary for L{Query}
199
200   """
201   assert len(set(fdef.title.lower()
202                  for (fdef, _, _) in fields)) == len(fields), \
203          "Duplicate title found"
204
205   result = {}
206
207   for field in fields:
208     (fdef, _, fn) = field
209
210     assert fdef.name and fdef.title, "Name and title are required"
211     assert FIELD_NAME_RE.match(fdef.name)
212     assert TITLE_RE.match(fdef.title)
213     assert callable(fn)
214     assert fdef.name not in result, "Duplicate field name found"
215
216     result[fdef.name] = field
217
218   assert len(result) == len(fields)
219   assert compat.all(name == fdef.name
220                     for (name, (fdef, _, _)) in result.items())
221
222   return result
223
224
225 def _MakeField(name, title, kind):
226   """Wrapper for creating L{objects.QueryFieldDefinition} instances.
227
228   @param name: Field name as a regular expression
229   @param title: Human-readable title
230   @param kind: Field type
231
232   """
233   return objects.QueryFieldDefinition(name=name, title=title, kind=kind)