Merge lp:~justin-fathomdb/nova/schedule-compute-near-volume into lp:~hudson-openstack/nova/trunk

Proposed by justinsb
Status: Work in progress
Proposed branch: lp:~justin-fathomdb/nova/schedule-compute-near-volume
Merge into: lp:~hudson-openstack/nova/trunk
Prerequisite: lp:~justin-fathomdb/nova/constraint-scheduler
Diff against target: 700 lines (+437/-30)
9 files modified
nova/db/sqlalchemy/models.py (+3/-0)
nova/scheduler/constraint.py (+68/-2)
nova/scheduler/constraint_lib.py (+31/-22)
nova/scheduler/datastore.py (+32/-1)
nova/scheduler/driver.py (+39/-2)
nova/scheduler/topology.py (+121/-0)
nova/tests/test_constraintlib.py (+2/-1)
nova/tests/test_scheduler.py (+52/-2)
nova/tests/test_topology.py (+89/-0)
To merge this branch: bzr merge lp:~justin-fathomdb/nova/schedule-compute-near-volume
Reviewer Review Type Date Requested Status
Ed Leafe Pending
Sandy Walsh Pending
Nova Core security contacts Pending
Review via email: mp+52520@code.launchpad.net

Description of the change

The first new pluggable scheduler policy: support allocation of machines in a requested 'location'.

The 'location' is like eday's DNS-style zone names. Because that isn't implemented yet, we instead infer the location based on the reversed host DNS name (for now). Also, because we don't know how we're going to pass this information, we instead pass it in the only collection we've got: the metadata.

To post a comment you must log in.
Revision history for this message
Todd Willey (xtoddx) wrote :

Probably shouldn't pass context as {} in the test case, as it will raise warnings if it actually gets used.

Revision history for this message
Christopher MacGown (0x44) wrote :

Should _build_topology, Topology, NamedTopology, and TopologyNode have docstrings?

Revision history for this message
justinsb (justin-fathomdb) wrote :

Moving to WIP - we're going to discuss at Design Summit

Unmerged revisions

734. By justinsb

Pep8ulous

733. By justinsb

Got tests passing; the problem was how to handle ties (i.e. when the weakest constraint had a draw)

732. By justinsb

Changed to using the more pythonic __str__ instead of the to_s hack

731. By justinsb

Merged in tests

730. By justinsb

Fix misnamed method service_get_all_by_topic_location

729. By justinsb

Fix superclass call

728. By justinsb

Plug in proximity based scheduler based on metadata

727. By justinsb

Merged with trunk

726. By justinsb

Initial implementation of proximity scheduler constraint

725. By justinsb

Added simple topology mapper

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/db/sqlalchemy/models.py'
2--- nova/db/sqlalchemy/models.py 2011-03-03 19:13:15 +0000
3+++ nova/db/sqlalchemy/models.py 2011-03-08 07:15:21 +0000
4@@ -99,6 +99,9 @@
5 local.update(joined)
6 return local.iteritems()
7
8+ def __repr__(self):
9+ return "%s" % dict(self)
10+
11
12 class Service(BASE, NovaBase):
13 """Represents a running service on a host."""
14
15=== modified file 'nova/scheduler/constraint.py'
16--- nova/scheduler/constraint.py 2011-03-08 07:15:20 +0000
17+++ nova/scheduler/constraint.py 2011-03-08 07:15:21 +0000
18@@ -23,6 +23,7 @@
19 from nova import db
20 from nova import flags
21 from nova import log as logging
22+from nova.scheduler import datastore
23 from nova.scheduler import driver
24 from nova.scheduler import constraint_lib
25
26@@ -44,13 +45,20 @@
27 def datastore(self):
28 return self.scheduler().datastore()
29
30+ def topology(self):
31+ context = self.request_context()
32+ return self.scheduler().topology(context)
33+
34 def selectivity(self):
35 # We default to an intermediate value here
36 return 0.75
37
38- def to_s(self):
39+ def __str__(self):
40 return self.__class__.__name__
41
42+ def __repr__(self):
43+ return self.__str__()
44+
45
46 class InstanceConstraintFavorLeastLoaded(SchedulerConstraint):
47 def __init__(self):
48@@ -71,7 +79,9 @@
49 return self.db_results
50
51 def score_item(self, candidate):
52- return candidate.utilization_score()
53+ score = candidate.utilization_score()
54+ self.trace_score(candidate, score)
55+ return score
56
57 def get_candidate_iterator(self):
58 """Returns an iterator over all items in best-to-least-good order"""
59@@ -85,6 +95,46 @@
60 self.trace(_("skip %s: capacity") % host)
61
62
63+class InstanceConstraintPlaceNearLocation(SchedulerConstraint):
64+ def __str__(self):
65+ return "InstanceConstraintPlaceNearLocation:%s" % self.target_location
66+
67+ def __init__(self, target_location):
68+ super(InstanceConstraintPlaceNearLocation, self).__init__()
69+ self.target_location = target_location
70+
71+ def _score_service(self, service):
72+ location = datastore.get_service_location(service)
73+ distance = self.topology().distance(self.target_location, location)
74+ score = 1 / float(1 + distance) # Map to (0, 1]
75+ LOG.debug("%s scores %s" % (location, score))
76+ return score
77+
78+ def score_item(self, candidate):
79+ score = self._score_service(candidate.model)
80+ self.trace_score(candidate, score)
81+ return score
82+
83+ def get_candidate_iterator(self):
84+ """Returns an iterator over all items in best-to-least-good order"""
85+ topo = self.topology()
86+ for location in topo.find_by_distance_from(self.target_location):
87+ services = self.datastore().service_get_all_by_topic_location(
88+ self.request_context(),
89+ 'compute',
90+ location.key)
91+ LOG.debug(_("Found services at %(location)s => %(services)s")
92+ % locals())
93+ for service in services:
94+ score = self._score_service(service)
95+ LOG.debug("%s scores %s" % (service.host, score))
96+ yield constraint_lib.Candidate(service.id, score)
97+
98+ def selectivity(self):
99+ # TODO(justinsb): We need to systematize this a bit better...
100+ return 0.1 # We expect to be very selective
101+
102+
103 class VolumeConstraintFavorLeastGigabytes(SchedulerConstraint):
104 def __init__(self):
105 super(VolumeConstraintFavorLeastGigabytes, self).__init__()
106@@ -169,6 +219,22 @@
107 'scheduler': self})
108 solver.add_constraint(InstanceConstraintFavorLeastLoaded())
109
110+ # NOTE(justinsb): Once we reach a decision on where this metadata
111+ # should be, then use that location. For now, use 'metadata'
112+ if instance_ref.get('metadata'):
113+ for item in instance_ref['metadata']:
114+ key = item['key']
115+ value = item['value']
116+
117+ if key == 'openstack:near':
118+ topo = self.topology(request_context)
119+ node = topo.get_node_by_key(value, create=True)
120+ constraint = InstanceConstraintPlaceNearLocation(node)
121+ solver.add_constraint(constraint)
122+
123+ LOG.debug("Metadata: %s" % instance_ref.get('metadata'))
124+ LOG.debug("Constraints: %s" % solver.constraints)
125+
126 for service in solver.get_solutions_iterator():
127 if self.service_is_up(service.model):
128 host = service.model['host']
129
130=== modified file 'nova/scheduler/constraint_lib.py'
131--- nova/scheduler/constraint_lib.py 2011-03-08 07:15:20 +0000
132+++ nova/scheduler/constraint_lib.py 2011-03-08 07:15:21 +0000
133@@ -34,7 +34,7 @@
134 self.id = id
135 self.score = score
136
137- def to_s(self):
138+ def __str__(self):
139 return "%s:%s" % (self.id, self.score)
140
141
142@@ -42,8 +42,8 @@
143 def __init__(self):
144 self.solver = None
145
146- def to_s(self):
147- return "%s" % self
148+ def __str__(self):
149+ return "%s" % self.__class__.__name__
150
151 def score_item(self, candidate):
152 """Scores the 'goodness' of a candidate.
153@@ -86,6 +86,9 @@
154 def trace(self, message):
155 self.solver.trace(message)
156
157+ def trace_score(self, candidate, score):
158+ self.trace("%s scored %s => %s" % (self, candidate, score))
159+
160 def cache_item(self, item):
161 item_id = item.id
162 self.solver.cache_item(item, item_id)
163@@ -117,11 +120,8 @@
164 self.current = None
165 self.is_done = True
166
167- def to_s(self):
168- if self.current:
169- return "%s %s" % (self.constraint.to_s(), self.current.to_s())
170- else:
171- return "%s -" % (self.constraint.to_s())
172+ def __str__(self):
173+ return "%s %s" % (self.constraint, self.current)
174
175
176 class Solver(object):
177@@ -194,7 +194,7 @@
178 # to be the worst, we don't need to query (see code below)
179 # If we'd be querying a REST service or DB, this is important
180 self.trace(_("Won't query constraint - not the worst: %s") %
181- (constraint.to_s()))
182+ (constraint))
183 query_values = False
184
185 state = ConstraintIteratorState(constraint,
186@@ -206,40 +206,49 @@
187 worst = state
188
189 self.trace(_("Using lowest-scoring criteria: %s") %
190- (worst.constraint.to_s()))
191+ (worst.constraint))
192
193 while not worst.is_done:
194- self.trace(_("Candidate: %s") % (worst.to_s()))
195-
196- # Query other constraints to get the score for the candidate
197- item_score = worst.current.score
198+ self.trace(_("Candidate: %s") % (worst))
199+
200+ # Loop over other constraints to get the score for the candidate
201+ # We break ties by choosing the candidate with the sum of scores
202+ min_score = worst.current.score
203+ tie_break_score = 0
204+
205 item_id = worst.current.id
206 item = self.lookup_item(item_id)
207 for constraint_state in states:
208 score = constraint_state.constraint.score_item(item)
209- item_score = min(item_score, score)
210+ min_score = min(min_score, score)
211+ tie_break_score += score
212
213- heapq.heappush(queue, (-item_score, item))
214+ # Heapify is really nice and will look at tie_break_score
215+ # if min_score is the same
216+ heapq.heappush(queue, (-min_score, -tie_break_score, item))
217
218 # Every future item will have a score <= worst.current.score
219 while True:
220- (head_score, head_item) = queue[0]
221+ (head_score, tie_break_score, head_item) = queue[0]
222 head_score = -head_score
223+ #tie_break_score = -tie_break_score
224 if head_score <= worst.current.score:
225 break
226
227 heapq.heappop(queue)
228- self.trace(_("Yielding: %s") % (head_item.to_s()))
229+ self.trace(_("Yielding: %s") % (head_item))
230 yield head_item
231
232 # Advance the iterator
233 worst.advance()
234 if not worst.is_done:
235- self.trace(_("Advanced: %s") % (worst.to_s()))
236+ self.trace(_("Advanced: %s") % (worst))
237
238- self.trace(_("Reached end of: %s") % (worst.constraint.to_s()))
239+ self.trace(_("Reached end of: %s") % (worst.constraint))
240
241 while queue:
242- (head_score, head_item) = heapq.heappop(queue)
243- self.trace(_("Yielding: %s") % (head_item.to_s()))
244+ (head_score, tie_break_score, head_item) = heapq.heappop(queue)
245+ #head_score = -head_score
246+ #tie_break_score = -tie_break_score
247+ self.trace(_("Yielding: %s") % (head_item))
248 yield head_item
249
250=== modified file 'nova/scheduler/datastore.py'
251--- nova/scheduler/datastore.py 2011-03-08 07:15:20 +0000
252+++ nova/scheduler/datastore.py 2011-03-08 07:15:21 +0000
253@@ -32,6 +32,20 @@
254 FLAGS = flags.FLAGS
255
256
257+def get_service_location(service):
258+ #TODO(justinsb): Implement configurable location
259+ #TODO(justinsb): Move this to service class
260+ location = None # service.location
261+
262+ if not location:
263+ # Fall back to using the DNS name / IP for "poor man's" topology
264+ location = service.host
265+ components = location.split('.')
266+ components.reverse()
267+ location = '.'.join(components)
268+ return location
269+
270+
271 class SchedulerAbstractHostModel(object):
272 def __init__(self, id, model, used, capacity):
273 self.id = id
274@@ -39,9 +53,12 @@
275 self.used = used
276 self.capacity = capacity
277
278- def to_s(self):
279+ def __str__(self):
280 return '%s %s/%s' % (self.id, self.used, self.capacity)
281
282+ def __repr__(self):
283+ return "Host:%s" % self
284+
285 def utilization_score(self):
286 # The best machine is one that is unused
287 utilization = float(self.used) / float(self.capacity)
288@@ -94,6 +111,17 @@
289 def service_get_all_by_topic(self, context, topic):
290 return db.service_get_all_by_topic(context, topic)
291
292+ def service_get_all_by_topic_location(self, context, topic, location):
293+ #TODO(justinsb): We need a call like this once we've agreed it
294+ #return db.service_get_all_by_topic_location(context, topic, location)
295+
296+ services = []
297+ for service in db.service_get_all_by_topic(context, topic):
298+ if location == get_service_location(service):
299+ services.append(service)
300+
301+ return services
302+
303 def get_volume_hosts_sorted(self, context):
304 results = []
305 db_results = db.service_get_all_volume_sorted(context)
306@@ -146,3 +174,6 @@
307
308 def record_network_scheduled(self, context, host):
309 pass
310+
311+ def service_get_all(self, context):
312+ return db.service_get_all(context)
313
314=== modified file 'nova/scheduler/driver.py'
315--- nova/scheduler/driver.py 2011-03-08 07:15:20 +0000
316+++ nova/scheduler/driver.py 2011-03-08 07:15:21 +0000
317@@ -26,9 +26,12 @@
318
319 from nova import exception
320 from nova import flags
321+from nova import log as logging
322 from nova.scheduler import datastore
323-
324-
325+from nova.scheduler import topology
326+
327+
328+LOG = logging.getLogger('nova.scheduler.driver')
329 FLAGS = flags.FLAGS
330 flags.DEFINE_integer('service_down_time', 60,
331 'maximum time since last checkin for up service')
332@@ -49,6 +52,7 @@
333 def __init__(self):
334 super(Scheduler, self).__init__()
335 self._datastore = None
336+ self._topology = None
337
338 @staticmethod
339 def service_is_up(service):
340@@ -74,3 +78,36 @@
341 if not self._datastore:
342 self._datastore = datastore.SchedulerDataStore()
343 return self._datastore
344+
345+ def _build_topology(self, context):
346+ #TODO(justinsb): Refresh?
347+ topo = topology.NamedTopology()
348+
349+ nodes = {}
350+
351+ services = self.datastore().service_get_all(context)
352+ for service in services:
353+ #TODO(justinsb): Implement configurable location
354+ location = None # service.location
355+
356+ if not location:
357+ # Fall back to using the DNS name / IP for topology
358+ location = service.host
359+ components = location.split('.')
360+ components.reverse()
361+ location = '.'.join(components)
362+ LOG.debug("Rewrote %s to %s" % (service.host, location))
363+
364+ node = nodes.get(location)
365+ if not node:
366+ node = topology.TopologyNode(location)
367+ topo.add(node)
368+ nodes[location] = node
369+
370+ return topo
371+
372+ def topology(self, context):
373+ """Returns the associated SchedulerDataStore"""
374+ if not self._topology:
375+ self._topology = self._build_topology(context)
376+ return self._topology
377
378=== added file 'nova/scheduler/topology.py'
379--- nova/scheduler/topology.py 1970-01-01 00:00:00 +0000
380+++ nova/scheduler/topology.py 2011-03-08 07:15:21 +0000
381@@ -0,0 +1,121 @@
382+# vim: tabstop=4 shiftwidth=4 softtabstop=4
383+
384+# Copyright (c) 2011 Justin Santa Barbara
385+#
386+# All Rights Reserved.
387+#
388+# Licensed under the Apache License, Version 2.0 (the "License"); you may
389+# not use this file except in compliance with the License. You may obtain
390+# a copy of the License at
391+#
392+# http://www.apache.org/licenses/LICENSE-2.0
393+#
394+# Unless required by applicable law or agreed to in writing, software
395+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
396+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
397+# License for the specific language governing permissions and limitations
398+# under the License.
399+
400+"""
401+Topology mapping to expose e.g. same machine, same rack, same room etc
402+"""
403+
404+from nova import flags
405+from nova import log as logging
406+
407+
408+LOG = logging.getLogger('nova.scheduler.constraint')
409+FLAGS = flags.FLAGS
410+
411+
412+class TopologyNode(object):
413+ def __str__(self):
414+ return self.key
415+
416+ def __repr__(self):
417+ return "TopologyNode:%s" % self.key
418+
419+ def __init__(self, key):
420+ self.key = key
421+
422+
423+class Topology(object):
424+ def distance(self, a, b):
425+ """Returns the distance from TopologyNode a to TopologyNode b"""
426+ raise NotImplementedError()
427+
428+ def find_by_distance_from(self, a):
429+ """Returns all the topology nodes ordered with the closest to a first.
430+
431+ This may (should?) return an iterator"""
432+ raise NotImplementedError()
433+
434+
435+class NamedTopology(Topology):
436+ """A named toplogy uses DNS-like naming e.g. machine3.rack2.roomb.nyc.us
437+
438+ We assume that the distance between two items is:
439+ (D - the length of the suffix path they share)
440+ where D is the maximum distance.
441+
442+ Trick: We assume all names are reversed, so
443+ machine3.rack2.roomb.us => us.roomb.rack2.machine3
444+ Prefix matching is much more natural, in particular for databases
445+
446+ This is really eday's concept - all credit to him!
447+ """
448+ def __init__(self):
449+ #TODO(justinsb): As we aim for scale, we should switch to a graph
450+ self.nodes = []
451+ self.nodes_by_key = {}
452+ self.max_distance = 0
453+
454+ def add(self, node):
455+ # Pre-compute the components
456+ node.components = node.key.split('.')
457+ self.nodes.append(node)
458+ self.nodes_by_key[node.key] = node
459+ self.max_distance = max(self.max_distance, len(node.components))
460+
461+ def distance(self, a, b):
462+ # This is an inefficient implementation
463+ if not isinstance(a, TopologyNode):
464+ a = self.get_node_by_key(a)
465+
466+ if not isinstance(b, TopologyNode):
467+ b = self.get_node_by_key(b)
468+
469+ if not a or not b:
470+ return self.max_distance + 1
471+
472+ a_components = a.components
473+ b_components = b.components
474+
475+ min_length = min(len(a_components), len(b_components))
476+ d = 0
477+ while True:
478+ if d >= min_length:
479+ break
480+ if a_components[d] != b_components[d]:
481+ break
482+ d = d + 1
483+
484+ d = self.max_distance - d
485+ return d
486+
487+ def find_by_distance_from(self, a):
488+ # This is an inefficient implementation
489+ proximities = [(node, self.distance(a, node))
490+ for node in self.nodes]
491+
492+ proximities.sort(key=lambda p: p[1])
493+
494+ ret = [p[0] for p in proximities]
495+ return ret
496+
497+ def get_node_by_key(self, key, create=False):
498+ node = self.nodes_by_key.get(key)
499+ if create and not node:
500+ node = TopologyNode(key)
501+ self.add(node)
502+ return node
503
504=== modified file 'nova/tests/test_constraintlib.py'
505--- nova/tests/test_constraintlib.py 2011-03-08 07:15:20 +0000
506+++ nova/tests/test_constraintlib.py 2011-03-08 07:15:21 +0000
507@@ -119,7 +119,8 @@
508 def _do_random_test(self, rnd, item_count, constraints_count):
509 all_items = []
510
511- solver = constraint_lib.Solver(None, None)
512+ context = {}
513+ solver = constraint_lib.Solver(context)
514
515 for j in range(item_count):
516 item = {}
517
518=== modified file 'nova/tests/test_scheduler.py'
519--- nova/tests/test_scheduler.py 2011-03-08 07:15:20 +0000
520+++ nova/tests/test_scheduler.py 2011-03-08 07:15:21 +0000
521@@ -30,6 +30,7 @@
522 from nova import rpc
523 from nova import utils
524 from nova.auth import manager as auth_manager
525+from nova.db.sqlalchemy import models
526 from nova.scheduler import manager
527 from nova.scheduler import driver
528
529@@ -84,7 +85,7 @@
530 self.flags(scheduler_driver='nova.scheduler.zone.ZoneScheduler')
531
532 def _create_service_model(self, **kwargs):
533- service = db.sqlalchemy.models.Service()
534+ service = models.Service()
535 service.host = kwargs['host']
536 service.disabled = False
537 service.deleted = False
538@@ -153,7 +154,7 @@
539 def tearDown(self):
540 self.manager.delete_user(self.user)
541 self.manager.delete_project(self.project)
542- super(SimpleDriverTestCase, self).tearDown()
543+ super(_SchedulerBaseTestCase, self).tearDown()
544
545 def _create_instance(self, **kwargs):
546 """Create a test instance"""
547@@ -167,6 +168,12 @@
548 inst['ami_launch_index'] = 0
549 inst['vcpus'] = 1
550 inst['availability_zone'] = kwargs.get('availability_zone', None)
551+ if kwargs.get('metadata'):
552+ metadata_list = []
553+ for k, v in kwargs['metadata'].items():
554+ metadata_list.append(models.InstanceMetadata(key=k, value=v))
555+ inst['metadata'] = metadata_list
556+
557 return db.instance_create(self.context, inst)['id']
558
559 def _create_volume(self):
560@@ -335,3 +342,46 @@
561 def __init__(self, *args, **kwargs):
562 self.scheduler_driver = 'nova.scheduler.constraint.ConstraintScheduler'
563 super(ConstraintDriverTestCase, self).__init__(*args, **kwargs)
564+
565+ def test_create_compute_in_location(self):
566+ """Ensures that location requests behave as we expect"""
567+
568+ # Set up two cabinets each with two machines
569+ for machine_number in [1, 2]:
570+ for cabinet_number in [1, 2]:
571+ host = ('machine%s.cab%s.openstack' %
572+ (cabinet_number, machine_number))
573+ _service = self.start_service('compute', host=host)
574+
575+ # Should allocate onto requested machine
576+ metadata = {'openstack:near': 'openstack.cab1.machine1'}
577+ instance_id = self._create_instance(metadata=metadata)
578+ host1 = self.scheduler.driver.schedule_run_instance(self.context,
579+ instance_id)
580+ self.assertEqual('machine1.cab1.openstack', host1)
581+
582+ # Requested machine doesn't exist; should match onto cab1
583+ # and then choose the less-loaded machine2
584+ metadata = {'openstack:near': 'openstack.cab1.machineX'}
585+ instance_id = self._create_instance(metadata=metadata)
586+ host2 = self.scheduler.driver.schedule_run_instance(self.context,
587+ instance_id)
588+ self.assertEqual('machine2.cab1.openstack', host2)
589+
590+ # Will match cab2 but not a machine, neither machine should have any
591+ # load, so will go onto either machine in cab2
592+ metadata = {'openstack:near': 'openstack.cab2.machineX'}
593+ instance_id = self._create_instance(metadata=metadata)
594+ host3 = self.scheduler.driver.schedule_run_instance(self.context,
595+ instance_id)
596+ # Should allocate onto either machine in cab2
597+ self.assertTrue(host3.endswith('.cab2.openstack'))
598+
599+ # Should be no-location match.
600+ # Should go onto the only machine with no servers
601+ metadata = {'openstack:near': 'openstack.cabX.machine1'}
602+ instance_id = self._create_instance(metadata=metadata)
603+ host4 = self.scheduler.driver.schedule_run_instance(self.context,
604+ instance_id)
605+ self.assertTrue(host4.endswith('.cab2.openstack'))
606+ self.assertNotEqual(host3, host4) # It should be on the other server
607
608=== added file 'nova/tests/test_topology.py'
609--- nova/tests/test_topology.py 1970-01-01 00:00:00 +0000
610+++ nova/tests/test_topology.py 2011-03-08 07:15:21 +0000
611@@ -0,0 +1,89 @@
612+# vim: tabstop=4 shiftwidth=4 softtabstop=4
613+
614+# Copyright 2011 Justin Santa Barbara
615+# All Rights Reserved.
616+#
617+# Licensed under the Apache License, Version 2.0 (the "License"); you may
618+# not use this file except in compliance with the License. You may obtain
619+# a copy of the License at
620+#
621+# http://www.apache.org/licenses/LICENSE-2.0
622+#
623+# Unless required by applicable law or agreed to in writing, software
624+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
625+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
626+# License for the specific language governing permissions and limitations
627+# under the License.
628+"""
629+Tests For Topology
630+"""
631+
632+from nova import test
633+from nova.scheduler import topology
634+
635+
636+class NamedTopologyTestCase(test.TestCase):
637+ """Test case for named topology"""
638+ def _build_simple_topology(self):
639+ t = topology.NamedTopology()
640+ t.add(topology.TopologyNode('room1.rack1.machine1'))
641+ t.add(topology.TopologyNode('room1.rack1.machine2'))
642+ t.add(topology.TopologyNode('room1.rack2.machine1'))
643+ t.add(topology.TopologyNode('room1.rack2.machine2'))
644+ t.add(topology.TopologyNode('room2.rack1.machine1'))
645+ t.add(topology.TopologyNode('room2.rack1.machine2'))
646+ t.add(topology.TopologyNode('room2.rack2.machine1'))
647+ t.add(topology.TopologyNode('room2.rack2.machine2'))
648+ self.topology = t
649+
650+ def _distance(self, key1, key2):
651+ node1 = self.topology.get_node_by_key(key1)
652+ node2 = self.topology.get_node_by_key(key2)
653+ return self.topology.distance(node1, node2)
654+
655+ def _keys_by_distance_from(self, key1):
656+ node1 = self.topology.get_node_by_key(key1)
657+ nodes = self.topology.find_by_distance_from(node1)
658+ return [node.key for node in nodes]
659+
660+ def test_distances(self):
661+ self._build_simple_topology()
662+
663+ self.assertEquals(0, self._distance('room1.rack1.machine1',
664+ 'room1.rack1.machine1'))
665+
666+ self.assertEquals(1, self._distance('room1.rack1.machine1',
667+ 'room1.rack1.machine2'))
668+
669+ self.assertEquals(2, self._distance('room1.rack1.machine1',
670+ 'room1.rack2.machine1'))
671+
672+ self.assertEquals(3, self._distance('room1.rack1.machine1',
673+ 'room2.rack1.machine1'))
674+
675+ def test_order_by_distance(self):
676+ self._build_simple_topology()
677+
678+ keys = self._keys_by_distance_from('room1.rack1.machine1')
679+ # We assume a stable sort...
680+ self.assertEquals(keys, ['room1.rack1.machine1',
681+ 'room1.rack1.machine2',
682+ 'room1.rack2.machine1',
683+ 'room1.rack2.machine2',
684+ 'room2.rack1.machine1',
685+ 'room2.rack1.machine2',
686+ 'room2.rack2.machine1',
687+ 'room2.rack2.machine2',
688+ ])
689+
690+ keys = self._keys_by_distance_from('room2.rack2.machine2')
691+ # We assume a stable sort...
692+ self.assertEquals(keys, ['room2.rack2.machine2',
693+ 'room2.rack2.machine1',
694+ 'room2.rack1.machine1',
695+ 'room2.rack1.machine2',
696+ 'room1.rack1.machine1',
697+ 'room1.rack1.machine2',
698+ 'room1.rack2.machine1',
699+ 'room1.rack2.machine2',
700+ ])