beeps | 7d8273b | 2013-11-06 09:44:34 -0800 | [diff] [blame] | 1 | #!/usr/bin/python |
beeps | 7d8273b | 2013-11-06 09:44:34 -0800 | [diff] [blame] | 2 | |
Richard Barnette | dd0ec81 | 2016-07-13 11:40:54 -0700 | [diff] [blame] | 3 | #pylint: disable=missing-docstring |
| 4 | |
Justin Giorgi | 67ad67d | 2016-06-29 14:41:04 -0700 | [diff] [blame] | 5 | import unittest |
| 6 | |
beeps | 7d8273b | 2013-11-06 09:44:34 -0800 | [diff] [blame] | 7 | import common |
| 8 | from autotest_lib.frontend import setup_django_environment |
| 9 | from autotest_lib.frontend.afe import frontend_test_utils |
beeps | 7d8273b | 2013-11-06 09:44:34 -0800 | [diff] [blame] | 10 | from autotest_lib.frontend.afe import models |
beeps | 7d8273b | 2013-11-06 09:44:34 -0800 | [diff] [blame] | 11 | from autotest_lib.scheduler import monitor_db_unittest |
| 12 | from autotest_lib.scheduler import scheduler_models |
| 13 | |
| 14 | _DEBUG = False |
| 15 | |
| 16 | |
| 17 | class AtomicGroupTest(monitor_db_unittest.DispatcherSchedulingTest): |
| 18 | |
| 19 | def test_atomic_group_hosts_blocked_from_non_atomic_jobs(self): |
| 20 | # Create a job scheduled to run on label6. |
| 21 | self._create_job(metahosts=[self.label6.id]) |
| 22 | self._run_scheduler() |
| 23 | # label6 only has hosts that are in atomic groups associated with it, |
| 24 | # there should be no scheduling. |
| 25 | self._check_for_extra_schedulings() |
| 26 | |
| 27 | |
| 28 | def test_atomic_group_hosts_blocked_from_non_atomic_jobs_explicit(self): |
| 29 | # Create a job scheduled to run on label5. This is an atomic group |
| 30 | # label but this job does not request atomic group scheduling. |
| 31 | self._create_job(metahosts=[self.label5.id]) |
| 32 | self._run_scheduler() |
| 33 | # label6 only has hosts that are in atomic groups associated with it, |
| 34 | # there should be no scheduling. |
| 35 | self._check_for_extra_schedulings() |
| 36 | |
| 37 | |
| 38 | def test_atomic_group_scheduling_basics(self): |
| 39 | # Create jobs scheduled to run on an atomic group. |
| 40 | job_a = self._create_job(synchronous=True, metahosts=[self.label4.id], |
| 41 | atomic_group=1) |
| 42 | job_b = self._create_job(synchronous=True, metahosts=[self.label5.id], |
| 43 | atomic_group=1) |
| 44 | self._run_scheduler() |
| 45 | # atomic_group.max_number_of_machines was 2 so we should run on 2. |
| 46 | self._assert_job_scheduled_on_number_of(job_a.id, (5, 6, 7), 2) |
| 47 | self._assert_job_scheduled_on(job_b.id, 8) # label5 |
| 48 | self._assert_job_scheduled_on(job_b.id, 9) # label5 |
| 49 | self._check_for_extra_schedulings() |
| 50 | |
| 51 | # The three host label4 atomic group still has one host available. |
| 52 | # That means a job with a synch_count of 1 asking to be scheduled on |
| 53 | # the atomic group can still use the final machine. |
| 54 | # |
| 55 | # This may seem like a somewhat odd use case. It allows the use of an |
| 56 | # atomic group as a set of machines to run smaller jobs within (a set |
| 57 | # of hosts configured for use in network tests with eachother perhaps?) |
| 58 | onehost_job = self._create_job(atomic_group=1) |
| 59 | self._run_scheduler() |
| 60 | self._assert_job_scheduled_on_number_of(onehost_job.id, (5, 6, 7), 1) |
| 61 | self._check_for_extra_schedulings() |
| 62 | |
| 63 | # No more atomic groups have hosts available, no more jobs should |
| 64 | # be scheduled. |
| 65 | self._create_job(atomic_group=1) |
| 66 | self._run_scheduler() |
| 67 | self._check_for_extra_schedulings() |
| 68 | |
| 69 | |
| 70 | def test_atomic_group_scheduling_obeys_acls(self): |
| 71 | # Request scheduling on a specific atomic label but be denied by ACLs. |
| 72 | self._do_query('DELETE FROM afe_acl_groups_hosts ' |
| 73 | 'WHERE host_id in (8,9)') |
| 74 | job = self._create_job(metahosts=[self.label5.id], atomic_group=1) |
| 75 | self._run_scheduler() |
| 76 | self._check_for_extra_schedulings() |
| 77 | |
| 78 | |
| 79 | def test_atomic_group_scheduling_dependency_label_exclude(self): |
| 80 | # A dependency label that matches no hosts in the atomic group. |
| 81 | job_a = self._create_job(atomic_group=1) |
| 82 | job_a.dependency_labels.add(self.label3) |
| 83 | self._run_scheduler() |
| 84 | self._check_for_extra_schedulings() |
| 85 | |
| 86 | |
| 87 | def test_atomic_group_scheduling_metahost_dependency_label_exclude(self): |
| 88 | # A metahost and dependency label that excludes too many hosts. |
| 89 | job_b = self._create_job(synchronous=True, metahosts=[self.label4.id], |
| 90 | atomic_group=1) |
| 91 | job_b.dependency_labels.add(self.label7) |
| 92 | self._run_scheduler() |
| 93 | self._check_for_extra_schedulings() |
| 94 | |
| 95 | |
| 96 | def test_atomic_group_scheduling_dependency_label_match(self): |
| 97 | # A dependency label that exists on enough atomic group hosts in only |
| 98 | # one of the two atomic group labels. |
| 99 | job_c = self._create_job(synchronous=True, atomic_group=1) |
| 100 | job_c.dependency_labels.add(self.label7) |
| 101 | self._run_scheduler() |
| 102 | self._assert_job_scheduled_on_number_of(job_c.id, (8, 9), 2) |
| 103 | self._check_for_extra_schedulings() |
| 104 | |
| 105 | |
| 106 | def test_atomic_group_scheduling_no_metahost(self): |
| 107 | # Force it to schedule on the other group for a reliable test. |
| 108 | self._do_query('UPDATE afe_hosts SET invalid=1 WHERE id=9') |
| 109 | # An atomic job without a metahost. |
| 110 | job = self._create_job(synchronous=True, atomic_group=1) |
| 111 | self._run_scheduler() |
| 112 | self._assert_job_scheduled_on_number_of(job.id, (5, 6, 7), 2) |
| 113 | self._check_for_extra_schedulings() |
| 114 | |
| 115 | |
| 116 | def test_atomic_group_scheduling_partial_group(self): |
| 117 | # Make one host in labels[3] unavailable so that there are only two |
| 118 | # hosts left in the group. |
| 119 | self._do_query('UPDATE afe_hosts SET status="Repair Failed" WHERE id=5') |
| 120 | job = self._create_job(synchronous=True, metahosts=[self.label4.id], |
| 121 | atomic_group=1) |
| 122 | self._run_scheduler() |
| 123 | # Verify that it was scheduled on the 2 ready hosts in that group. |
| 124 | self._assert_job_scheduled_on(job.id, 6) |
| 125 | self._assert_job_scheduled_on(job.id, 7) |
| 126 | self._check_for_extra_schedulings() |
| 127 | |
| 128 | |
| 129 | def test_atomic_group_scheduling_not_enough_available(self): |
| 130 | # Mark some hosts in each atomic group label as not usable. |
| 131 | # One host running, another invalid in the first group label. |
| 132 | self._do_query('UPDATE afe_hosts SET status="Running" WHERE id=5') |
| 133 | self._do_query('UPDATE afe_hosts SET invalid=1 WHERE id=6') |
| 134 | # One host invalid in the second group label. |
| 135 | self._do_query('UPDATE afe_hosts SET invalid=1 WHERE id=9') |
| 136 | # Nothing to schedule when no group label has enough (2) good hosts.. |
| 137 | self._create_job(atomic_group=1, synchronous=True) |
| 138 | self._run_scheduler() |
| 139 | # There are not enough hosts in either atomic group, |
| 140 | # No more scheduling should occur. |
| 141 | self._check_for_extra_schedulings() |
| 142 | |
| 143 | # Now create an atomic job that has a synch count of 1. It should |
| 144 | # schedule on exactly one of the hosts. |
| 145 | onehost_job = self._create_job(atomic_group=1) |
| 146 | self._run_scheduler() |
| 147 | self._assert_job_scheduled_on_number_of(onehost_job.id, (7, 8), 1) |
| 148 | |
| 149 | |
| 150 | def test_atomic_group_scheduling_no_valid_hosts(self): |
| 151 | self._do_query('UPDATE afe_hosts SET invalid=1 WHERE id in (8,9)') |
| 152 | self._create_job(synchronous=True, metahosts=[self.label5.id], |
| 153 | atomic_group=1) |
| 154 | self._run_scheduler() |
| 155 | # no hosts in the selected group and label are valid. no schedulings. |
| 156 | self._check_for_extra_schedulings() |
| 157 | |
| 158 | |
| 159 | def test_atomic_group_scheduling_metahost_works(self): |
| 160 | # Test that atomic group scheduling also obeys metahosts. |
| 161 | self._create_job(metahosts=[0], atomic_group=1) |
| 162 | self._run_scheduler() |
| 163 | # There are no atomic group hosts that also have that metahost. |
| 164 | self._check_for_extra_schedulings() |
| 165 | |
| 166 | job_b = self._create_job(metahosts=[self.label5.id], atomic_group=1) |
| 167 | self._run_scheduler() |
| 168 | self._assert_job_scheduled_on(job_b.id, 8) |
| 169 | self._assert_job_scheduled_on(job_b.id, 9) |
| 170 | self._check_for_extra_schedulings() |
| 171 | |
| 172 | |
| 173 | def test_atomic_group_skips_ineligible_hosts(self): |
| 174 | # Test hosts marked ineligible for this job are not eligible. |
| 175 | # How would this ever happen anyways? |
| 176 | job = self._create_job(metahosts=[self.label4.id], atomic_group=1) |
| 177 | models.IneligibleHostQueue.objects.create(job=job, host_id=5) |
| 178 | models.IneligibleHostQueue.objects.create(job=job, host_id=6) |
| 179 | models.IneligibleHostQueue.objects.create(job=job, host_id=7) |
| 180 | self._run_scheduler() |
| 181 | # No scheduling should occur as all desired hosts were ineligible. |
| 182 | self._check_for_extra_schedulings() |
| 183 | |
| 184 | |
| 185 | def test_atomic_group_scheduling_fail(self): |
| 186 | # If synch_count is > the atomic group number of machines, the job |
| 187 | # should be aborted immediately. |
| 188 | model_job = self._create_job(synchronous=True, atomic_group=1) |
| 189 | model_job.synch_count = 4 |
| 190 | model_job.save() |
| 191 | job = scheduler_models.Job(id=model_job.id) |
| 192 | self._run_scheduler() |
| 193 | self._check_for_extra_schedulings() |
Richard Barnette | dd0ec81 | 2016-07-13 11:40:54 -0700 | [diff] [blame] | 194 | queue_entries = scheduler_models.HostQueueEntry.fetch( |
| 195 | where='job_id=%d' % job.id) |
beeps | 7d8273b | 2013-11-06 09:44:34 -0800 | [diff] [blame] | 196 | self.assertEqual(1, len(queue_entries)) |
| 197 | self.assertEqual(queue_entries[0].status, |
| 198 | models.HostQueueEntry.Status.ABORTED) |
| 199 | |
| 200 | |
| 201 | def test_atomic_group_no_labels_no_scheduling(self): |
| 202 | # Never schedule on atomic groups marked invalid. |
| 203 | job = self._create_job(metahosts=[self.label5.id], synchronous=True, |
| 204 | atomic_group=1) |
| 205 | # Deleting an atomic group via the frontend marks it invalid and |
| 206 | # removes all label references to the group. The job now references |
| 207 | # an invalid atomic group with no labels associated with it. |
| 208 | self.label5.atomic_group.invalid = True |
| 209 | self.label5.atomic_group.save() |
| 210 | self.label5.atomic_group = None |
| 211 | self.label5.save() |
| 212 | |
| 213 | self._run_scheduler() |
| 214 | self._check_for_extra_schedulings() |
| 215 | |
| 216 | |
| 217 | def test_schedule_directly_on_atomic_group_host_fail(self): |
| 218 | # Scheduling a job directly on hosts in an atomic group must |
| 219 | # fail to avoid users inadvertently holding up the use of an |
| 220 | # entire atomic group by using the machines individually. |
| 221 | job = self._create_job(hosts=[5]) |
| 222 | self._run_scheduler() |
| 223 | self._check_for_extra_schedulings() |
| 224 | |
| 225 | |
| 226 | def test_schedule_directly_on_atomic_group_host(self): |
| 227 | # Scheduling a job directly on one host in an atomic group will |
| 228 | # work when the atomic group is listed on the HQE in addition |
| 229 | # to the host (assuming the sync count is 1). |
| 230 | job = self._create_job(hosts=[5], atomic_group=1) |
| 231 | self._run_scheduler() |
| 232 | self._assert_job_scheduled_on(job.id, 5) |
| 233 | self._check_for_extra_schedulings() |
| 234 | |
| 235 | |
| 236 | def test_schedule_directly_on_atomic_group_hosts_sync2(self): |
| 237 | job = self._create_job(hosts=[5,8], atomic_group=1, synchronous=True) |
| 238 | self._run_scheduler() |
| 239 | self._assert_job_scheduled_on(job.id, 5) |
| 240 | self._assert_job_scheduled_on(job.id, 8) |
| 241 | self._check_for_extra_schedulings() |
| 242 | |
| 243 | |
| 244 | def test_schedule_directly_on_atomic_group_hosts_wrong_group(self): |
| 245 | job = self._create_job(hosts=[5,8], atomic_group=2, synchronous=True) |
| 246 | self._run_scheduler() |
| 247 | self._check_for_extra_schedulings() |
| 248 | |
| 249 | |
| 250 | # TODO(gps): These should probably live in their own TestCase class |
| 251 | # specific to testing HostScheduler methods directly. It was convenient |
| 252 | # to put it here for now to share existing test environment setup code. |
| 253 | def test_HostScheduler_check_atomic_group_labels(self): |
| 254 | normal_job = self._create_job(metahosts=[0]) |
| 255 | atomic_job = self._create_job(atomic_group=1) |
| 256 | # Indirectly initialize the internal state of the host scheduler. |
| 257 | self._dispatcher._refresh_pending_queue_entries() |
| 258 | |
| 259 | atomic_hqe = scheduler_models.HostQueueEntry.fetch(where='job_id=%d' % |
| 260 | atomic_job.id)[0] |
| 261 | normal_hqe = scheduler_models.HostQueueEntry.fetch(where='job_id=%d' % |
| 262 | normal_job.id)[0] |
| 263 | |
| 264 | host_scheduler = self._dispatcher._host_scheduler |
| 265 | self.assertTrue(host_scheduler._check_atomic_group_labels( |
| 266 | [self.label4.id], atomic_hqe)) |
| 267 | self.assertFalse(host_scheduler._check_atomic_group_labels( |
| 268 | [self.label4.id], normal_hqe)) |
| 269 | self.assertFalse(host_scheduler._check_atomic_group_labels( |
| 270 | [self.label5.id, self.label6.id, self.label7.id], normal_hqe)) |
| 271 | self.assertTrue(host_scheduler._check_atomic_group_labels( |
| 272 | [self.label4.id, self.label6.id], atomic_hqe)) |
| 273 | self.assertTrue(host_scheduler._check_atomic_group_labels( |
| 274 | [self.label4.id, self.label5.id], |
| 275 | atomic_hqe)) |
| 276 | |
| 277 | |
| 278 | class OnlyIfNeededTest(monitor_db_unittest.DispatcherSchedulingTest): |
| 279 | |
| 280 | def _setup_test_only_if_needed_labels(self): |
| 281 | # apply only_if_needed label3 to host1 |
| 282 | models.Host.smart_get('host1').labels.add(self.label3) |
| 283 | return self._create_job_simple([1], use_metahost=True) |
| 284 | |
| 285 | |
| 286 | def test_only_if_needed_labels_avoids_host(self): |
| 287 | job = self._setup_test_only_if_needed_labels() |
| 288 | # if the job doesn't depend on label3, there should be no scheduling |
| 289 | self._run_scheduler() |
| 290 | self._check_for_extra_schedulings() |
| 291 | |
| 292 | |
| 293 | def test_only_if_needed_labels_schedules(self): |
| 294 | job = self._setup_test_only_if_needed_labels() |
| 295 | job.dependency_labels.add(self.label3) |
| 296 | self._run_scheduler() |
| 297 | self._assert_job_scheduled_on(1, 1) |
| 298 | self._check_for_extra_schedulings() |
| 299 | |
| 300 | |
| 301 | def test_only_if_needed_labels_via_metahost(self): |
| 302 | job = self._setup_test_only_if_needed_labels() |
| 303 | job.dependency_labels.add(self.label3) |
| 304 | # should also work if the metahost is the only_if_needed label |
| 305 | self._do_query('DELETE FROM afe_jobs_dependency_labels') |
| 306 | self._create_job(metahosts=[3]) |
| 307 | self._run_scheduler() |
| 308 | self._assert_job_scheduled_on(2, 1) |
| 309 | self._check_for_extra_schedulings() |
| 310 | |
| 311 | |
| 312 | def test_metahosts_obey_blocks(self): |
| 313 | """ |
| 314 | Metahosts can't get scheduled on hosts already scheduled for |
| 315 | that job. |
| 316 | """ |
| 317 | self._create_job(metahosts=[1], hosts=[1]) |
| 318 | # make the nonmetahost entry complete, so the metahost can try |
| 319 | # to get scheduled |
| 320 | self._update_hqe(set='complete = 1', where='host_id=1') |
| 321 | self._run_scheduler() |
| 322 | self._check_for_extra_schedulings() |
| 323 | |
| 324 | |
Justin Giorgi | 67ad67d | 2016-06-29 14:41:04 -0700 | [diff] [blame] | 325 | if __name__ == '__main__': |
| 326 | unittest.main() |
| 327 | |