github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/ui/tests/acceptance/client-detail-test.js (about) 1 import { currentURL, waitUntil } from '@ember/test-helpers'; 2 import { assign } from '@ember/polyfills'; 3 import { module, test } from 'qunit'; 4 import { setupApplicationTest } from 'ember-qunit'; 5 import { setupMirage } from 'ember-cli-mirage/test-support'; 6 import { formatBytes } from 'nomad-ui/helpers/format-bytes'; 7 import moment from 'moment'; 8 import ClientDetail from 'nomad-ui/tests/pages/clients/detail'; 9 import Clients from 'nomad-ui/tests/pages/clients/list'; 10 import Jobs from 'nomad-ui/tests/pages/jobs/list'; 11 12 let node; 13 let managementToken; 14 let clientToken; 15 16 const wasPreemptedFilter = allocation => !!allocation.preemptedByAllocation; 17 18 module('Acceptance | client detail', function(hooks) { 19 setupApplicationTest(hooks); 20 setupMirage(hooks); 21 22 hooks.beforeEach(function() { 23 server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' }); 24 node = server.db.nodes[0]; 25 26 managementToken = server.create('token'); 27 clientToken = server.create('token'); 28 29 window.localStorage.nomadTokenSecret = managementToken.secretId; 30 31 // Related models 32 server.create('agent'); 33 server.create('job', { createAllocations: false }); 34 server.createList('allocation', 3); 35 server.create('allocation', 'preempted'); 36 37 // Force all allocations into the running state so now allocation rows are missing 38 // CPU/Mem runtime metrics 39 server.schema.allocations.all().models.forEach(allocation => { 40 allocation.update({ clientStatus: 'running' }); 41 }); 42 }); 43 44 test('/clients/:id should have a breadcrumb trail linking back to clients', async function(assert) { 45 await ClientDetail.visit({ id: node.id }); 46 47 assert.equal(document.title, `Client ${node.name} - Nomad`); 48 49 assert.equal( 50 ClientDetail.breadcrumbFor('clients.index').text, 51 'Clients', 52 'First breadcrumb says clients' 53 ); 54 assert.equal( 55 ClientDetail.breadcrumbFor('clients.client').text, 56 node.id.split('-')[0], 57 'Second breadcrumb says the node short id' 58 ); 59 await ClientDetail.breadcrumbFor('clients.index').visit(); 60 assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients'); 61 }); 62 63 test('/clients/:id should list immediate details for the node in the title', async function(assert) { 64 node = server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible', drain: false }); 65 66 await ClientDetail.visit({ id: node.id }); 67 68 assert.ok(ClientDetail.title.includes(node.name), 'Title includes name'); 69 assert.ok(ClientDetail.clientId.includes(node.id), 'Title includes id'); 70 assert.equal( 71 ClientDetail.statusLight.objectAt(0).id, 72 node.status, 73 'Title includes status light' 74 ); 75 }); 76 77 test('/clients/:id should list additional detail for the node below the title', async function(assert) { 78 await ClientDetail.visit({ id: node.id }); 79 80 assert.ok( 81 ClientDetail.statusDefinition.includes(node.status), 82 'Status is in additional details' 83 ); 84 assert.ok( 85 ClientDetail.statusDecorationClass.includes(`node-${node.status}`), 86 'Status is decorated with a status class' 87 ); 88 assert.ok( 89 ClientDetail.addressDefinition.includes(node.httpAddr), 90 'Address is in additional details' 91 ); 92 assert.ok( 93 ClientDetail.datacenterDefinition.includes(node.datacenter), 94 'Datacenter is in additional details' 95 ); 96 }); 97 98 test('/clients/:id should include resource utilization graphs', async function(assert) { 99 await ClientDetail.visit({ id: node.id }); 100 101 assert.equal(ClientDetail.resourceCharts.length, 2, 'Two resource utilization graphs'); 102 assert.equal(ClientDetail.resourceCharts.objectAt(0).name, 'CPU', 'First chart is CPU'); 103 assert.equal(ClientDetail.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory'); 104 }); 105 106 test('/clients/:id should list all allocations on the node', async function(assert) { 107 const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length; 108 109 await ClientDetail.visit({ id: node.id }); 110 111 assert.equal( 112 ClientDetail.allocations.length, 113 allocationsCount, 114 `Allocations table lists all ${allocationsCount} associated allocations` 115 ); 116 }); 117 118 test('each allocation should have high-level details for the allocation', async function(assert) { 119 const allocation = server.db.allocations 120 .where({ nodeId: node.id }) 121 .sortBy('modifyIndex') 122 .reverse()[0]; 123 124 const allocStats = server.db.clientAllocationStats.find(allocation.id); 125 const taskGroup = server.db.taskGroups.findBy({ 126 name: allocation.taskGroup, 127 jobId: allocation.jobId, 128 }); 129 130 const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); 131 const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0); 132 const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0); 133 134 await ClientDetail.visit({ id: node.id }); 135 136 const allocationRow = ClientDetail.allocations.objectAt(0); 137 138 assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short ID'); 139 assert.equal( 140 allocationRow.createTime, 141 moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'), 142 'Allocation create time' 143 ); 144 assert.equal( 145 allocationRow.modifyTime, 146 moment(allocation.modifyTime / 1000000).fromNow(), 147 'Allocation modify time' 148 ); 149 assert.equal(allocationRow.status, allocation.clientStatus, 'Client status'); 150 assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name'); 151 assert.ok(allocationRow.taskGroup, 'Task group name'); 152 assert.ok(allocationRow.jobVersion, 'Job Version'); 153 assert.equal(allocationRow.volume, 'Yes', 'Volume'); 154 assert.equal( 155 allocationRow.cpu, 156 Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed, 157 'CPU %' 158 ); 159 assert.equal( 160 allocationRow.cpuTooltip, 161 `${Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks)} / ${cpuUsed} MHz`, 162 'Detailed CPU information is in a tooltip' 163 ); 164 assert.equal( 165 allocationRow.mem, 166 allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed, 167 'Memory used' 168 ); 169 assert.equal( 170 allocationRow.memTooltip, 171 `${formatBytes([allocStats.resourceUsage.MemoryStats.RSS])} / ${memoryUsed} MiB`, 172 'Detailed memory information is in a tooltip' 173 ); 174 }); 175 176 test('each allocation should show job information even if the job is incomplete and already in the store', async function(assert) { 177 // First, visit clients to load the allocations for each visible node. 178 // Don't load the job belongsTo of the allocation! Leave it unfulfilled. 179 180 await Clients.visit(); 181 182 // Then, visit jobs to load all jobs, which should implicitly fulfill 183 // the job belongsTo of each allocation pointed at each job. 184 185 await Jobs.visit(); 186 187 // Finally, visit a node to assert that the job name and task group name are 188 // present. This will require reloading the job, since task groups aren't a 189 // part of the jobs list response. 190 191 await ClientDetail.visit({ id: node.id }); 192 193 const allocationRow = ClientDetail.allocations.objectAt(0); 194 const allocation = server.db.allocations 195 .where({ nodeId: node.id }) 196 .sortBy('modifyIndex') 197 .reverse()[0]; 198 199 assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name'); 200 assert.ok(allocationRow.taskGroup.includes(allocation.taskGroup), 'Task group name'); 201 }); 202 203 test('each allocation should link to the allocation detail page', async function(assert) { 204 const allocation = server.db.allocations 205 .where({ nodeId: node.id }) 206 .sortBy('modifyIndex') 207 .reverse()[0]; 208 209 await ClientDetail.visit({ id: node.id }); 210 await ClientDetail.allocations.objectAt(0).visit(); 211 212 assert.equal( 213 currentURL(), 214 `/allocations/${allocation.id}`, 215 'Allocation rows link to allocation detail pages' 216 ); 217 }); 218 219 test('each allocation should link to the job the allocation belongs to', async function(assert) { 220 await ClientDetail.visit({ id: node.id }); 221 222 const allocation = server.db.allocations.where({ nodeId: node.id })[0]; 223 const job = server.db.jobs.find(allocation.jobId); 224 225 await ClientDetail.allocations.objectAt(0).visitJob(); 226 227 assert.equal( 228 currentURL(), 229 `/jobs/${job.id}`, 230 'Allocation rows link to the job detail page for the allocation' 231 ); 232 }); 233 234 test('the allocation section should show the count of preempted allocations on the client', async function(assert) { 235 const allocations = server.db.allocations.where({ nodeId: node.id }); 236 237 await ClientDetail.visit({ id: node.id }); 238 239 assert.equal( 240 ClientDetail.allocationFilter.allCount, 241 allocations.length, 242 'All filter/badge shows all allocations count' 243 ); 244 assert.ok( 245 ClientDetail.allocationFilter.preemptionsCount.startsWith( 246 allocations.filter(wasPreemptedFilter).length 247 ), 248 'Preemptions filter/badge shows preempted allocations count' 249 ); 250 }); 251 252 test('clicking the preemption badge filters the allocations table and sets a query param', async function(assert) { 253 const allocations = server.db.allocations.where({ nodeId: node.id }); 254 255 await ClientDetail.visit({ id: node.id }); 256 await ClientDetail.allocationFilter.preemptions(); 257 258 assert.equal( 259 ClientDetail.allocations.length, 260 allocations.filter(wasPreemptedFilter).length, 261 'Only preempted allocations are shown' 262 ); 263 assert.equal( 264 currentURL(), 265 `/clients/${node.id}?preemptions=true`, 266 'Filter is persisted in the URL' 267 ); 268 }); 269 270 test('clicking the total allocations badge resets the filter and removes the query param', async function(assert) { 271 const allocations = server.db.allocations.where({ nodeId: node.id }); 272 273 await ClientDetail.visit({ id: node.id }); 274 await ClientDetail.allocationFilter.preemptions(); 275 await ClientDetail.allocationFilter.all(); 276 277 assert.equal(ClientDetail.allocations.length, allocations.length, 'All allocations are shown'); 278 assert.equal(currentURL(), `/clients/${node.id}`, 'Filter is persisted in the URL'); 279 }); 280 281 test('navigating directly to the client detail page with the preemption query param set will filter the allocations table', async function(assert) { 282 const allocations = server.db.allocations.where({ nodeId: node.id }); 283 284 await ClientDetail.visit({ id: node.id, preemptions: true }); 285 286 assert.equal( 287 ClientDetail.allocations.length, 288 allocations.filter(wasPreemptedFilter).length, 289 'Only preempted allocations are shown' 290 ); 291 }); 292 293 test('/clients/:id should list all attributes for the node', async function(assert) { 294 await ClientDetail.visit({ id: node.id }); 295 296 assert.ok(ClientDetail.attributesTable, 'Attributes table is on the page'); 297 }); 298 299 test('/clients/:id lists all meta attributes', async function(assert) { 300 node = server.create('node', 'forceIPv4', 'withMeta'); 301 302 await ClientDetail.visit({ id: node.id }); 303 304 assert.ok(ClientDetail.metaTable, 'Meta attributes table is on the page'); 305 assert.notOk(ClientDetail.emptyMetaMessage, 'Meta attributes is not empty'); 306 307 const firstMetaKey = Object.keys(node.meta)[0]; 308 const firstMetaAttribute = ClientDetail.metaAttributes.objectAt(0); 309 assert.equal( 310 firstMetaAttribute.key, 311 firstMetaKey, 312 'Meta attributes for the node are bound to the attributes table' 313 ); 314 assert.equal( 315 firstMetaAttribute.value, 316 node.meta[firstMetaKey], 317 'Meta attributes for the node are bound to the attributes table' 318 ); 319 }); 320 321 test('/clients/:id shows an empty message when there is no meta data', async function(assert) { 322 await ClientDetail.visit({ id: node.id }); 323 324 assert.notOk(ClientDetail.metaTable, 'Meta attributes table is not on the page'); 325 assert.ok(ClientDetail.emptyMetaMessage, 'Meta attributes is empty'); 326 }); 327 328 test('when the node is not found, an error message is shown, but the URL persists', async function(assert) { 329 await ClientDetail.visit({ id: 'not-a-real-node' }); 330 331 assert.equal( 332 server.pretender.handledRequests 333 .filter(request => !request.url.includes('policy')) 334 .findBy('status', 404).url, 335 '/v1/node/not-a-real-node', 336 'A request to the nonexistent node is made' 337 ); 338 assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists'); 339 assert.ok(ClientDetail.error.isShown, 'Error message is shown'); 340 assert.equal(ClientDetail.error.title, 'Not Found', 'Error message is for 404'); 341 }); 342 343 test('/clients/:id shows the recent events list', async function(assert) { 344 await ClientDetail.visit({ id: node.id }); 345 346 assert.ok(ClientDetail.hasEvents, 'Client events section exists'); 347 }); 348 349 test('each node event shows basic node event information', async function(assert) { 350 const event = server.db.nodeEvents 351 .where({ nodeId: node.id }) 352 .sortBy('time') 353 .reverse()[0]; 354 355 await ClientDetail.visit({ id: node.id }); 356 357 const eventRow = ClientDetail.events.objectAt(0); 358 assert.equal( 359 eventRow.time, 360 moment(event.time).format("MMM DD, 'YY HH:mm:ss ZZ"), 361 'Event timestamp' 362 ); 363 assert.equal(eventRow.subsystem, event.subsystem, 'Event subsystem'); 364 assert.equal(eventRow.message, event.message, 'Event message'); 365 }); 366 367 test('/clients/:id shows the driver status of every driver for the node', async function(assert) { 368 // Set the drivers up so health and detection is well tested 369 const nodeDrivers = node.drivers; 370 const undetectedDriver = 'raw_exec'; 371 372 Object.values(nodeDrivers).forEach(driver => { 373 driver.Detected = true; 374 }); 375 376 nodeDrivers[undetectedDriver].Detected = false; 377 node.drivers = nodeDrivers; 378 379 const drivers = Object.keys(node.drivers) 380 .map(driverName => assign({ Name: driverName }, node.drivers[driverName])) 381 .sortBy('Name'); 382 383 assert.ok(drivers.length > 0, 'Node has drivers'); 384 385 await ClientDetail.visit({ id: node.id }); 386 387 drivers.forEach((driver, index) => { 388 const driverHead = ClientDetail.driverHeads.objectAt(index); 389 390 assert.equal(driverHead.name, driver.Name, `${driver.Name}: Name is correct`); 391 assert.equal( 392 driverHead.detected, 393 driver.Detected ? 'Yes' : 'No', 394 `${driver.Name}: Detection is correct` 395 ); 396 assert.equal( 397 driverHead.lastUpdated, 398 moment(driver.UpdateTime).fromNow(), 399 `${driver.Name}: Last updated shows time since now` 400 ); 401 402 if (driver.Name === undetectedDriver) { 403 assert.notOk( 404 driverHead.healthIsShown, 405 `${driver.Name}: No health for the undetected driver` 406 ); 407 } else { 408 assert.equal( 409 driverHead.health, 410 driver.Healthy ? 'Healthy' : 'Unhealthy', 411 `${driver.Name}: Health is correct` 412 ); 413 assert.ok( 414 driverHead.healthClass.includes(driver.Healthy ? 'running' : 'failed'), 415 `${driver.Name}: Swatch with correct class is shown` 416 ); 417 } 418 }); 419 }); 420 421 test('each driver can be opened to see a message and attributes', async function(assert) { 422 // Only detected drivers can be expanded 423 const nodeDrivers = node.drivers; 424 Object.values(nodeDrivers).forEach(driver => { 425 driver.Detected = true; 426 }); 427 node.drivers = nodeDrivers; 428 429 const driver = Object.keys(node.drivers) 430 .map(driverName => assign({ Name: driverName }, node.drivers[driverName])) 431 .sortBy('Name')[0]; 432 433 await ClientDetail.visit({ id: node.id }); 434 const driverHead = ClientDetail.driverHeads.objectAt(0); 435 const driverBody = ClientDetail.driverBodies.objectAt(0); 436 437 assert.notOk(driverBody.descriptionIsShown, 'Driver health description is not shown'); 438 assert.notOk(driverBody.attributesAreShown, 'Driver attributes section is not shown'); 439 440 await driverHead.toggle(); 441 assert.equal( 442 driverBody.description, 443 driver.HealthDescription, 444 'Driver health description is now shown' 445 ); 446 assert.ok(driverBody.attributesAreShown, 'Driver attributes section is now shown'); 447 }); 448 449 test('the status light indicates when the node is ineligible for scheduling', async function(assert) { 450 node = server.create('node', { 451 drain: false, 452 schedulingEligibility: 'ineligible', 453 }); 454 455 await ClientDetail.visit({ id: node.id }); 456 457 assert.equal( 458 ClientDetail.statusLight.objectAt(0).id, 459 'ineligible', 460 'Title status light is in the ineligible state' 461 ); 462 }); 463 464 test('when the node has a drain strategy with a positive deadline, the drain stategy section prints the duration', async function(assert) { 465 const deadline = 5400000000000; // 1.5 hours in nanoseconds 466 const forceDeadline = moment().add(1, 'd'); 467 468 node = server.create('node', { 469 drain: true, 470 schedulingEligibility: 'ineligible', 471 drainStrategy: { 472 Deadline: deadline, 473 ForceDeadline: forceDeadline.toISOString(), 474 IgnoreSystemJobs: false, 475 }, 476 }); 477 478 await ClientDetail.visit({ id: node.id }); 479 480 assert.ok( 481 ClientDetail.drainDetails.deadline.includes(forceDeadline.fromNow(true)), 482 'Deadline is shown in a human formatted way' 483 ); 484 485 assert.equal( 486 ClientDetail.drainDetails.deadlineTooltip, 487 forceDeadline.format("MMM DD, 'YY HH:mm:ss ZZ"), 488 'The tooltip for deadline shows the force deadline as an absolute date' 489 ); 490 491 assert.ok( 492 ClientDetail.drainDetails.drainSystemJobsText.endsWith('Yes'), 493 'Drain System Jobs state is shown' 494 ); 495 }); 496 497 test('when the node has a drain stategy with no deadline, the drain stategy section mentions that and omits the force deadline', async function(assert) { 498 const deadline = 0; 499 500 node = server.create('node', { 501 drain: true, 502 schedulingEligibility: 'ineligible', 503 drainStrategy: { 504 Deadline: deadline, 505 ForceDeadline: '0001-01-01T00:00:00Z', // null as a date 506 IgnoreSystemJobs: true, 507 }, 508 }); 509 510 await ClientDetail.visit({ id: node.id }); 511 512 assert.notOk(ClientDetail.drainDetails.durationIsShown, 'Duration is omitted'); 513 514 assert.ok( 515 ClientDetail.drainDetails.deadline.includes('No deadline'), 516 'The value for Deadline is "no deadline"' 517 ); 518 519 assert.ok( 520 ClientDetail.drainDetails.drainSystemJobsText.endsWith('No'), 521 'Drain System Jobs state is shown' 522 ); 523 }); 524 525 test('when the node has a drain stategy with a negative deadline, the drain strategy section shows the force badge', async function(assert) { 526 const deadline = -1; 527 528 node = server.create('node', { 529 drain: true, 530 schedulingEligibility: 'ineligible', 531 drainStrategy: { 532 Deadline: deadline, 533 ForceDeadline: '0001-01-01T00:00:00Z', // null as a date 534 IgnoreSystemJobs: false, 535 }, 536 }); 537 538 await ClientDetail.visit({ id: node.id }); 539 540 assert.ok( 541 ClientDetail.drainDetails.forceDrainText.endsWith('Yes'), 542 'Forced Drain is described' 543 ); 544 545 assert.ok(ClientDetail.drainDetails.duration.includes('--'), 'Duration is shown but unset'); 546 547 assert.ok(ClientDetail.drainDetails.deadline.includes('--'), 'Deadline is shown but unset'); 548 549 assert.ok( 550 ClientDetail.drainDetails.drainSystemJobsText.endsWith('Yes'), 551 'Drain System Jobs state is shown' 552 ); 553 }); 554 555 test('toggling node eligibility disables the toggle and sends the correct POST request', async function(assert) { 556 node = server.create('node', { 557 drain: false, 558 schedulingEligibility: 'eligible', 559 }); 560 561 server.pretender.post('/v1/node/:id/eligibility', () => [200, {}, ''], true); 562 563 await ClientDetail.visit({ id: node.id }); 564 assert.ok(ClientDetail.eligibilityToggle.isActive); 565 566 ClientDetail.eligibilityToggle.toggle(); 567 await waitUntil(() => server.pretender.handledRequests.findBy('method', 'POST')); 568 569 assert.ok(ClientDetail.eligibilityToggle.isDisabled); 570 server.pretender.resolve(server.pretender.requestReferences[0].request); 571 572 assert.notOk(ClientDetail.eligibilityToggle.isActive); 573 assert.notOk(ClientDetail.eligibilityToggle.isDisabled); 574 575 const request = server.pretender.handledRequests.findBy('method', 'POST'); 576 assert.equal(request.url, `/v1/node/${node.id}/eligibility`); 577 assert.deepEqual(JSON.parse(request.requestBody), { 578 NodeID: node.id, 579 Eligibility: 'ineligible', 580 }); 581 582 ClientDetail.eligibilityToggle.toggle(); 583 await waitUntil(() => server.pretender.handledRequests.filterBy('method', 'POST').length === 2); 584 server.pretender.resolve(server.pretender.requestReferences[0].request); 585 586 assert.ok(ClientDetail.eligibilityToggle.isActive); 587 const request2 = server.pretender.handledRequests.filterBy('method', 'POST')[1]; 588 589 assert.equal(request2.url, `/v1/node/${node.id}/eligibility`); 590 assert.deepEqual(JSON.parse(request2.requestBody), { 591 NodeID: node.id, 592 Eligibility: 'eligible', 593 }); 594 }); 595 596 test('starting a drain sends the correct POST request', async function(assert) { 597 let request; 598 599 node = server.create('node', { 600 drain: false, 601 schedulingEligibility: 'eligible', 602 }); 603 604 await ClientDetail.visit({ id: node.id }); 605 await ClientDetail.drainPopover.toggle(); 606 await ClientDetail.drainPopover.submit(); 607 608 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 609 610 assert.equal(request.url, `/v1/node/${node.id}/drain`); 611 assert.deepEqual( 612 JSON.parse(request.requestBody), 613 { 614 NodeID: node.id, 615 DrainSpec: { 616 Deadline: 0, 617 IgnoreSystemJobs: false, 618 }, 619 }, 620 'Drain with default settings' 621 ); 622 623 await ClientDetail.drainPopover.toggle(); 624 await ClientDetail.drainPopover.deadlineToggle.toggle(); 625 await ClientDetail.drainPopover.submit(); 626 627 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 628 629 assert.deepEqual( 630 JSON.parse(request.requestBody), 631 { 632 NodeID: node.id, 633 DrainSpec: { 634 Deadline: 60 * 60 * 1000 * 1000000, 635 IgnoreSystemJobs: false, 636 }, 637 }, 638 'Drain with deadline toggled' 639 ); 640 641 await ClientDetail.drainPopover.toggle(); 642 await ClientDetail.drainPopover.deadlineOptions.open(); 643 await ClientDetail.drainPopover.deadlineOptions.options[1].choose(); 644 await ClientDetail.drainPopover.submit(); 645 646 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 647 648 assert.deepEqual( 649 JSON.parse(request.requestBody), 650 { 651 NodeID: node.id, 652 DrainSpec: { 653 Deadline: 4 * 60 * 60 * 1000 * 1000000, 654 IgnoreSystemJobs: false, 655 }, 656 }, 657 'Drain with non-default preset deadline set' 658 ); 659 660 await ClientDetail.drainPopover.toggle(); 661 await ClientDetail.drainPopover.deadlineOptions.open(); 662 const optionsCount = ClientDetail.drainPopover.deadlineOptions.options.length; 663 await ClientDetail.drainPopover.deadlineOptions.options.objectAt(optionsCount - 1).choose(); 664 await ClientDetail.drainPopover.setCustomDeadline('1h40m20s'); 665 await ClientDetail.drainPopover.submit(); 666 667 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 668 669 assert.deepEqual( 670 JSON.parse(request.requestBody), 671 { 672 NodeID: node.id, 673 DrainSpec: { 674 Deadline: ((1 * 60 + 40) * 60 + 20) * 1000 * 1000000, 675 IgnoreSystemJobs: false, 676 }, 677 }, 678 'Drain with custom deadline set' 679 ); 680 681 await ClientDetail.drainPopover.toggle(); 682 await ClientDetail.drainPopover.deadlineToggle.toggle(); 683 await ClientDetail.drainPopover.forceDrainToggle.toggle(); 684 await ClientDetail.drainPopover.submit(); 685 686 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 687 688 assert.deepEqual( 689 JSON.parse(request.requestBody), 690 { 691 NodeID: node.id, 692 DrainSpec: { 693 Deadline: -1, 694 IgnoreSystemJobs: false, 695 }, 696 }, 697 'Drain with force set' 698 ); 699 700 await ClientDetail.drainPopover.toggle(); 701 await ClientDetail.drainPopover.systemJobsToggle.toggle(); 702 await ClientDetail.drainPopover.submit(); 703 704 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 705 706 assert.deepEqual( 707 JSON.parse(request.requestBody), 708 { 709 NodeID: node.id, 710 DrainSpec: { 711 Deadline: -1, 712 IgnoreSystemJobs: true, 713 }, 714 }, 715 'Drain system jobs unset' 716 ); 717 }); 718 719 test('the drain popover cancel button closes the popover', async function(assert) { 720 node = server.create('node', { 721 drain: false, 722 schedulingEligibility: 'eligible', 723 }); 724 725 await ClientDetail.visit({ id: node.id }); 726 assert.notOk(ClientDetail.drainPopover.isOpen); 727 728 await ClientDetail.drainPopover.toggle(); 729 assert.ok(ClientDetail.drainPopover.isOpen); 730 731 await ClientDetail.drainPopover.cancel(); 732 assert.notOk(ClientDetail.drainPopover.isOpen); 733 assert.equal(server.pretender.handledRequests.filterBy('method', 'POST'), 0); 734 }); 735 736 test('toggling eligibility is disabled while a drain is active', async function(assert) { 737 node = server.create('node', { 738 drain: true, 739 schedulingEligibility: 'ineligible', 740 }); 741 742 await ClientDetail.visit({ id: node.id }); 743 assert.ok(ClientDetail.eligibilityToggle.isDisabled); 744 }); 745 746 test('stopping a drain sends the correct POST request', async function(assert) { 747 node = server.create('node', { 748 drain: true, 749 schedulingEligibility: 'ineligible', 750 }); 751 752 await ClientDetail.visit({ id: node.id }); 753 assert.ok(ClientDetail.stopDrainIsPresent); 754 755 await ClientDetail.stopDrain.idle(); 756 await ClientDetail.stopDrain.confirm(); 757 758 const request = server.pretender.handledRequests.findBy('method', 'POST'); 759 assert.equal(request.url, `/v1/node/${node.id}/drain`); 760 assert.deepEqual(JSON.parse(request.requestBody), { 761 NodeID: node.id, 762 DrainSpec: null, 763 }); 764 }); 765 766 test('when a drain is active, the "drain" popover is labeled as the "update" popover', async function(assert) { 767 node = server.create('node', { 768 drain: true, 769 schedulingEligibility: 'ineligible', 770 }); 771 772 await ClientDetail.visit({ id: node.id }); 773 assert.equal(ClientDetail.drainPopover.label, 'Update Drain'); 774 }); 775 776 test('forcing a drain sends the correct POST request', async function(assert) { 777 node = server.create('node', { 778 drain: true, 779 schedulingEligibility: 'ineligible', 780 drainStrategy: { 781 Deadline: 0, 782 IgnoreSystemJobs: true, 783 }, 784 }); 785 786 await ClientDetail.visit({ id: node.id }); 787 await ClientDetail.drainDetails.force.idle(); 788 await ClientDetail.drainDetails.force.confirm(); 789 790 const request = server.pretender.handledRequests.findBy('method', 'POST'); 791 assert.equal(request.url, `/v1/node/${node.id}/drain`); 792 assert.deepEqual(JSON.parse(request.requestBody), { 793 NodeID: node.id, 794 DrainSpec: { 795 Deadline: -1, 796 IgnoreSystemJobs: true, 797 }, 798 }); 799 }); 800 801 test('when stopping a drain fails, an error is shown', async function(assert) { 802 node = server.create('node', { 803 drain: true, 804 schedulingEligibility: 'ineligible', 805 }); 806 807 server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']); 808 809 await ClientDetail.visit({ id: node.id }); 810 await ClientDetail.stopDrain.idle(); 811 await ClientDetail.stopDrain.confirm(); 812 813 assert.ok(ClientDetail.stopDrainError.isPresent); 814 assert.ok(ClientDetail.stopDrainError.title.includes('Stop Drain Error')); 815 816 await ClientDetail.stopDrainError.dismiss(); 817 assert.notOk(ClientDetail.stopDrainError.isPresent); 818 }); 819 820 test('when starting a drain fails, an error message is shown', async function(assert) { 821 node = server.create('node', { 822 drain: false, 823 schedulingEligibility: 'eligible', 824 }); 825 826 server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']); 827 828 await ClientDetail.visit({ id: node.id }); 829 await ClientDetail.drainPopover.toggle(); 830 await ClientDetail.drainPopover.submit(); 831 832 assert.ok(ClientDetail.drainError.isPresent); 833 assert.ok(ClientDetail.drainError.title.includes('Drain Error')); 834 835 await ClientDetail.drainError.dismiss(); 836 assert.notOk(ClientDetail.drainError.isPresent); 837 }); 838 839 test('when updating a drain fails, an error message is shown', async function(assert) { 840 node = server.create('node', { 841 drain: true, 842 schedulingEligibility: 'ineligible', 843 }); 844 845 server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']); 846 847 await ClientDetail.visit({ id: node.id }); 848 await ClientDetail.drainPopover.toggle(); 849 await ClientDetail.drainPopover.submit(); 850 851 assert.ok(ClientDetail.drainError.isPresent); 852 assert.ok(ClientDetail.drainError.title.includes('Drain Error')); 853 854 await ClientDetail.drainError.dismiss(); 855 assert.notOk(ClientDetail.drainError.isPresent); 856 }); 857 858 test('when toggling eligibility fails, an error message is shown', async function(assert) { 859 node = server.create('node', { 860 drain: false, 861 schedulingEligibility: 'eligible', 862 }); 863 864 server.pretender.post('/v1/node/:id/eligibility', () => [500, {}, '']); 865 866 await ClientDetail.visit({ id: node.id }); 867 await ClientDetail.eligibilityToggle.toggle(); 868 869 assert.ok(ClientDetail.eligibilityError.isPresent); 870 assert.ok(ClientDetail.eligibilityError.title.includes('Eligibility Error')); 871 872 await ClientDetail.eligibilityError.dismiss(); 873 assert.notOk(ClientDetail.eligibilityError.isPresent); 874 }); 875 876 test('when navigating away from a client that has an error message to another client, the error is not shown', async function(assert) { 877 node = server.create('node', { 878 drain: false, 879 schedulingEligibility: 'eligible', 880 }); 881 882 const node2 = server.create('node'); 883 884 server.pretender.post('/v1/node/:id/eligibility', () => [500, {}, '']); 885 886 await ClientDetail.visit({ id: node.id }); 887 await ClientDetail.eligibilityToggle.toggle(); 888 889 assert.ok(ClientDetail.eligibilityError.isPresent); 890 assert.ok(ClientDetail.eligibilityError.title.includes('Eligibility Error')); 891 892 await ClientDetail.visit({ id: node2.id }); 893 894 assert.notOk(ClientDetail.eligibilityError.isPresent); 895 }); 896 897 test('toggling eligibility and node drain are disabled when the active ACL token does not permit node write', async function(assert) { 898 window.localStorage.nomadTokenSecret = clientToken.secretId; 899 900 await ClientDetail.visit({ id: node.id }); 901 assert.ok(ClientDetail.eligibilityToggle.isDisabled); 902 assert.ok(ClientDetail.drainPopover.isDisabled); 903 }); 904 905 test('the host volumes table lists all host volumes in alphabetical order by name', async function(assert) { 906 await ClientDetail.visit({ id: node.id }); 907 908 const sortedHostVolumes = Object.keys(node.hostVolumes) 909 .map(key => node.hostVolumes[key]) 910 .sortBy('Name'); 911 912 assert.ok(ClientDetail.hasHostVolumes); 913 assert.equal(ClientDetail.hostVolumes.length, Object.keys(node.hostVolumes).length); 914 915 ClientDetail.hostVolumes.forEach((volume, index) => { 916 assert.equal(volume.name, sortedHostVolumes[index].Name); 917 }); 918 }); 919 920 test('each host volume row contains information about the host volume', async function(assert) { 921 await ClientDetail.visit({ id: node.id }); 922 923 const sortedHostVolumes = Object.keys(node.hostVolumes) 924 .map(key => node.hostVolumes[key]) 925 .sortBy('Name'); 926 927 ClientDetail.hostVolumes[0].as(volume => { 928 const volumeRow = sortedHostVolumes[0]; 929 assert.equal(volume.name, volumeRow.Name); 930 assert.equal(volume.path, volumeRow.Path); 931 assert.equal(volume.permissions, volumeRow.ReadOnly ? 'Read' : 'Read/Write'); 932 }); 933 }); 934 935 test('the host volumes table is not shown if the client has no host volumes', async function(assert) { 936 node = server.create('node', 'noHostVolumes'); 937 938 await ClientDetail.visit({ id: node.id }); 939 940 assert.notOk(ClientDetail.hasHostVolumes); 941 }); 942 }); 943 944 module('Acceptance | client detail (multi-namespace)', function(hooks) { 945 setupApplicationTest(hooks); 946 setupMirage(hooks); 947 948 hooks.beforeEach(function() { 949 server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' }); 950 node = server.db.nodes[0]; 951 952 // Related models 953 server.create('namespace'); 954 server.create('namespace', { id: 'other-namespace' }); 955 956 server.create('agent'); 957 958 // Make a job for each namespace, but have both scheduled on the same node 959 server.create('job', { id: 'job-1', namespaceId: 'default', createAllocations: false }); 960 server.createList('allocation', 3, { nodeId: node.id, clientStatus: 'running' }); 961 962 server.create('job', { id: 'job-2', namespaceId: 'other-namespace', createAllocations: false }); 963 server.createList('allocation', 3, { 964 nodeId: node.id, 965 jobId: 'job-2', 966 clientStatus: 'running', 967 }); 968 }); 969 970 test('when the node has allocations on different namespaces, the associated jobs are fetched correctly', async function(assert) { 971 window.localStorage.nomadActiveNamespace = 'other-namespace'; 972 973 await ClientDetail.visit({ id: node.id }); 974 975 assert.equal( 976 ClientDetail.allocations.length, 977 server.db.allocations.length, 978 'All allocations are scheduled on this node' 979 ); 980 assert.ok( 981 server.pretender.handledRequests.findBy('url', '/v1/job/job-1'), 982 'Job One fetched correctly' 983 ); 984 assert.ok( 985 server.pretender.handledRequests.findBy('url', '/v1/job/job-2?namespace=other-namespace'), 986 'Job Two fetched correctly' 987 ); 988 }); 989 });