~ttx/nova/d4-merge

« back to all changes in this revision

Viewing changes to nova/scheduler/host_filter.py

  • Committer: Thierry Carrez
  • Date: 2011-08-23 12:23:07 UTC
  • mfrom: (1130.75.258 nova)
  • Revision ID: thierry@openstack.org-20110823122307-f0vtuyg1ikc14n87
Merge diablo-4 development from trunk (rev1479)

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
#    under the License.
15
15
 
16
16
"""
17
 
Host Filter is a mechanism for requesting instance resources.
18
 
Three filters are included: AllHosts, Flavor & JSON. AllHosts just
19
 
returns the full, unfiltered list of hosts. Flavor is a hard coded
20
 
matching mechanism based on flavor criteria and JSON is an ad-hoc
21
 
filter grammar.
22
 
 
23
 
Why JSON? The requests for instances may come in through the
24
 
REST interface from a user or a parent Zone.
25
 
Currently Flavors and/or InstanceTypes are used for
26
 
specifing the type of instance desired. Specific Nova users have
27
 
noted a need for a more expressive way of specifying instances.
28
 
Since we don't want to get into building full DSL this is a simple
29
 
form as an example of how this could be done. In reality, most
30
 
consumers will use the more rigid filters such as FlavorFilter.
31
 
 
32
 
Note: These are "required" capability filters. These capabilities
33
 
used must be present or the host will be excluded. The hosts
34
 
returned are then weighed by the Weighted Scheduler. Weights
35
 
can take the more esoteric factors into consideration (such as
36
 
server affinity and customer separation).
 
17
The Host Filter classes are a way to ensure that only hosts that are
 
18
appropriate are considered when creating a new instance. Hosts that are
 
19
either incompatible or insufficient to accept a newly-requested instance
 
20
are removed by Host Filter classes from consideration. Those that pass
 
21
the filter are then passed on for weighting or other process for ordering.
 
22
 
 
23
Filters are in the 'filters' directory that is off the 'scheduler'
 
24
directory of nova. Additional filters can be created and added to that
 
25
directory; be sure to add them to the filters/__init__.py file so that
 
26
they are part of the nova.schedulers.filters namespace.
37
27
"""
38
28
 
39
 
import json
 
29
import types
40
30
 
41
31
from nova import exception
42
32
from nova import flags
43
 
from nova import log as logging
44
 
from nova.scheduler import zone_aware_scheduler
45
 
from nova import utils
46
 
from nova.scheduler import zone_aware_scheduler
 
33
import nova.scheduler
47
34
 
48
 
LOG = logging.getLogger('nova.scheduler.host_filter')
49
35
 
50
36
FLAGS = flags.FLAGS
51
 
flags.DEFINE_string('default_host_filter',
52
 
                    'nova.scheduler.host_filter.AllHostsFilter',
53
 
                    'Which filter to use for filtering hosts.')
54
 
 
55
 
 
56
 
class HostFilter(object):
57
 
    """Base class for host filters."""
58
 
 
59
 
    def instance_type_to_filter(self, instance_type):
60
 
        """Convert instance_type into a filter for most common use-case."""
61
 
        raise NotImplementedError()
62
 
 
63
 
    def filter_hosts(self, zone_manager, query):
64
 
        """Return a list of hosts that fulfill the filter."""
65
 
        raise NotImplementedError()
66
 
 
67
 
    def _full_name(self):
68
 
        """module.classname of the filter."""
69
 
        return "%s.%s" % (self.__module__, self.__class__.__name__)
70
 
 
71
 
 
72
 
class AllHostsFilter(HostFilter):
73
 
    """ NOP host filter. Returns all hosts in ZoneManager.
74
 
    This essentially does what the old Scheduler+Chance used
75
 
    to give us.
76
 
    """
77
 
 
78
 
    def instance_type_to_filter(self, instance_type):
79
 
        """Return anything to prevent base-class from raising
80
 
        exception."""
81
 
        return (self._full_name(), instance_type)
82
 
 
83
 
    def filter_hosts(self, zone_manager, query):
84
 
        """Return a list of hosts from ZoneManager list."""
85
 
        return [(host, services)
86
 
               for host, services in zone_manager.service_states.iteritems()]
87
 
 
88
 
 
89
 
class InstanceTypeFilter(HostFilter):
90
 
    """HostFilter hard-coded to work with InstanceType records."""
91
 
 
92
 
    def instance_type_to_filter(self, instance_type):
93
 
        """Use instance_type to filter hosts."""
94
 
        return (self._full_name(), instance_type)
95
 
 
96
 
    def _satisfies_extra_specs(self, capabilities, instance_type):
97
 
        """Check that the capabilities provided by the compute service
98
 
        satisfy the extra specs associated with the instance type"""
99
 
 
100
 
        if 'extra_specs' not in instance_type:
101
 
            return True
102
 
 
103
 
        # Note(lorinh): For now, we are just checking exact matching on the
104
 
        # values. Later on, we  want to handle numerical
105
 
        # values so we can represent things like number of GPU cards
106
 
 
107
 
        try:
108
 
            for key, value in instance_type['extra_specs'].iteritems():
109
 
                if capabilities[key] != value:
110
 
                    return False
111
 
        except KeyError:
112
 
            return False
113
 
 
114
 
        return True
115
 
 
116
 
    def filter_hosts(self, zone_manager, query):
117
 
        """Return a list of hosts that can create instance_type."""
118
 
        instance_type = query
119
 
        selected_hosts = []
120
 
        for host, services in zone_manager.service_states.iteritems():
121
 
            capabilities = services.get('compute', {})
122
 
            host_ram_mb = capabilities['host_memory_free']
123
 
            disk_bytes = capabilities['disk_available']
124
 
            spec_ram = instance_type['memory_mb']
125
 
            spec_disk = instance_type['local_gb']
126
 
            extra_specs = instance_type['extra_specs']
127
 
 
128
 
            if host_ram_mb >= spec_ram and \
129
 
               disk_bytes >= spec_disk and \
130
 
               self._satisfies_extra_specs(capabilities, instance_type):
131
 
                selected_hosts.append((host, capabilities))
132
 
        return selected_hosts
133
 
 
134
 
#host entries (currently) are like:
135
 
#    {'host_name-description': 'Default install of XenServer',
136
 
#    'host_hostname': 'xs-mini',
137
 
#    'host_memory_total': 8244539392,
138
 
#    'host_memory_overhead': 184225792,
139
 
#    'host_memory_free': 3868327936,
140
 
#    'host_memory_free_computed': 3840843776,
141
 
#    'host_other_config': {},
142
 
#    'host_ip_address': '192.168.1.109',
143
 
#    'host_cpu_info': {},
144
 
#    'disk_available': 32954957824,
145
 
#    'disk_total': 50394562560,
146
 
#    'disk_used': 17439604736,
147
 
#    'host_uuid': 'cedb9b39-9388-41df-8891-c5c9a0c0fe5f',
148
 
#    'host_name_label': 'xs-mini'}
149
 
 
150
 
# instance_type table has:
151
 
#name = Column(String(255), unique=True)
152
 
#memory_mb = Column(Integer)
153
 
#vcpus = Column(Integer)
154
 
#local_gb = Column(Integer)
155
 
#flavorid = Column(Integer, unique=True)
156
 
#swap = Column(Integer, nullable=False, default=0)
157
 
#rxtx_quota = Column(Integer, nullable=False, default=0)
158
 
#rxtx_cap = Column(Integer, nullable=False, default=0)
159
 
 
160
 
 
161
 
class JsonFilter(HostFilter):
162
 
    """Host Filter to allow simple JSON-based grammar for
163
 
    selecting hosts.
164
 
    """
165
 
 
166
 
    def _equals(self, args):
167
 
        """First term is == all the other terms."""
168
 
        if len(args) < 2:
169
 
            return False
170
 
        lhs = args[0]
171
 
        for rhs in args[1:]:
172
 
            if lhs != rhs:
173
 
                return False
174
 
        return True
175
 
 
176
 
    def _less_than(self, args):
177
 
        """First term is < all the other terms."""
178
 
        if len(args) < 2:
179
 
            return False
180
 
        lhs = args[0]
181
 
        for rhs in args[1:]:
182
 
            if lhs >= rhs:
183
 
                return False
184
 
        return True
185
 
 
186
 
    def _greater_than(self, args):
187
 
        """First term is > all the other terms."""
188
 
        if len(args) < 2:
189
 
            return False
190
 
        lhs = args[0]
191
 
        for rhs in args[1:]:
192
 
            if lhs <= rhs:
193
 
                return False
194
 
        return True
195
 
 
196
 
    def _in(self, args):
197
 
        """First term is in set of remaining terms"""
198
 
        if len(args) < 2:
199
 
            return False
200
 
        return args[0] in args[1:]
201
 
 
202
 
    def _less_than_equal(self, args):
203
 
        """First term is <= all the other terms."""
204
 
        if len(args) < 2:
205
 
            return False
206
 
        lhs = args[0]
207
 
        for rhs in args[1:]:
208
 
            if lhs > rhs:
209
 
                return False
210
 
        return True
211
 
 
212
 
    def _greater_than_equal(self, args):
213
 
        """First term is >= all the other terms."""
214
 
        if len(args) < 2:
215
 
            return False
216
 
        lhs = args[0]
217
 
        for rhs in args[1:]:
218
 
            if lhs < rhs:
219
 
                return False
220
 
        return True
221
 
 
222
 
    def _not(self, args):
223
 
        """Flip each of the arguments."""
224
 
        if len(args) == 0:
225
 
            return False
226
 
        return [not arg for arg in args]
227
 
 
228
 
    def _or(self, args):
229
 
        """True if any arg is True."""
230
 
        return True in args
231
 
 
232
 
    def _and(self, args):
233
 
        """True if all args are True."""
234
 
        return False not in args
235
 
 
236
 
    commands = {
237
 
        '=': _equals,
238
 
        '<': _less_than,
239
 
        '>': _greater_than,
240
 
        'in': _in,
241
 
        '<=': _less_than_equal,
242
 
        '>=': _greater_than_equal,
243
 
        'not': _not,
244
 
        'or': _or,
245
 
        'and': _and,
246
 
    }
247
 
 
248
 
    def instance_type_to_filter(self, instance_type):
249
 
        """Convert instance_type into JSON filter object."""
250
 
        required_ram = instance_type['memory_mb']
251
 
        required_disk = instance_type['local_gb']
252
 
        query = ['and',
253
 
                    ['>=', '$compute.host_memory_free', required_ram],
254
 
                    ['>=', '$compute.disk_available', required_disk]]
255
 
        return (self._full_name(), json.dumps(query))
256
 
 
257
 
    def _parse_string(self, string, host, services):
258
 
        """Strings prefixed with $ are capability lookups in the
259
 
        form '$service.capability[.subcap*]'
260
 
        """
261
 
        if not string:
262
 
            return None
263
 
        if string[0] != '$':
264
 
            return string
265
 
 
266
 
        path = string[1:].split('.')
267
 
        for item in path:
268
 
            services = services.get(item, None)
269
 
            if not services:
270
 
                return None
271
 
        return services
272
 
 
273
 
    def _process_filter(self, zone_manager, query, host, services):
274
 
        """Recursively parse the query structure."""
275
 
        if len(query) == 0:
276
 
            return True
277
 
        cmd = query[0]
278
 
        method = self.commands[cmd]  # Let exception fly.
279
 
        cooked_args = []
280
 
        for arg in query[1:]:
281
 
            if isinstance(arg, list):
282
 
                arg = self._process_filter(zone_manager, arg, host, services)
283
 
            elif isinstance(arg, basestring):
284
 
                arg = self._parse_string(arg, host, services)
285
 
            if arg != None:
286
 
                cooked_args.append(arg)
287
 
        result = method(self, cooked_args)
288
 
        return result
289
 
 
290
 
    def filter_hosts(self, zone_manager, query):
291
 
        """Return a list of hosts that can fulfill filter."""
292
 
        expanded = json.loads(query)
293
 
        hosts = []
294
 
        for host, services in zone_manager.service_states.iteritems():
295
 
            r = self._process_filter(zone_manager, expanded, host, services)
296
 
            if isinstance(r, list):
297
 
                r = True in r
298
 
            if r:
299
 
                hosts.append((host, services))
300
 
        return hosts
301
 
 
302
 
 
303
 
FILTERS = [AllHostsFilter, InstanceTypeFilter, JsonFilter]
 
37
 
 
38
 
 
39
def _get_filters():
 
40
    # Imported here to avoid circular imports
 
41
    from nova.scheduler import filters
 
42
 
 
43
    def get_itm(nm):
 
44
        return getattr(filters, nm)
 
45
 
 
46
    return [get_itm(itm) for itm in dir(filters)
 
47
            if (type(get_itm(itm)) is types.TypeType)
 
48
            and issubclass(get_itm(itm), filters.AbstractHostFilter)
 
49
            and get_itm(itm) is not filters.AbstractHostFilter]
304
50
 
305
51
 
306
52
def choose_host_filter(filter_name=None):
309
55
    function checks the filter name against a predefined set
310
56
    of acceptable filters.
311
57
    """
312
 
 
313
58
    if not filter_name:
314
59
        filter_name = FLAGS.default_host_filter
315
 
    for filter_class in FILTERS:
316
 
        host_match = "%s.%s" % (filter_class.__module__, filter_class.__name__)
317
 
        if host_match == filter_name:
 
60
    for filter_class in _get_filters():
 
61
        if filter_class.__name__ == filter_name:
318
62
            return filter_class()
319
63
    raise exception.SchedulerHostFilterNotFound(filter_name=filter_name)
320
 
 
321
 
 
322
 
class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler):
323
 
    """The HostFilterScheduler uses the HostFilter to filter
324
 
    hosts for weighing. The particular filter used may be passed in
325
 
    as an argument or the default will be used.
326
 
 
327
 
    request_spec = {'filter': <Filter name>,
328
 
                    'instance_type': <InstanceType dict>}
329
 
    """
330
 
 
331
 
    def filter_hosts(self, topic, request_spec, hosts=None):
332
 
        """Filter the full host list (from the ZoneManager)"""
333
 
 
334
 
        filter_name = request_spec.get('filter', None)
335
 
        host_filter = choose_host_filter(filter_name)
336
 
 
337
 
        # TODO(sandy): We're only using InstanceType-based specs
338
 
        # currently. Later we'll need to snoop for more detailed
339
 
        # host filter requests.
340
 
        instance_type = request_spec['instance_type']
341
 
        name, query = host_filter.instance_type_to_filter(instance_type)
342
 
        return host_filter.filter_hosts(self.zone_manager, query)
343
 
 
344
 
    def weigh_hosts(self, topic, request_spec, hosts):
345
 
        """Derived classes must override this method and return
346
 
        a lists of hosts in [{weight, hostname}] format.
347
 
        """
348
 
        return [dict(weight=1, hostname=hostname, capabilities=caps)
349
 
                for hostname, caps in hosts]