github.com/zhizhiboom/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/ui/tests/acceptance/client-detail-test.js (about)

     1  import { assign } from '@ember/polyfills';
     2  import { currentURL } from 'ember-native-dom-helpers';
     3  import { test } from 'qunit';
     4  import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
     5  import { formatBytes } from 'nomad-ui/helpers/format-bytes';
     6  import formatDuration from 'nomad-ui/utils/format-duration';
     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  
    14  moduleForAcceptance('Acceptance | client detail', {
    15    beforeEach() {
    16      server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' });
    17      node = server.db.nodes[0];
    18  
    19      // Related models
    20      server.create('agent');
    21      server.create('job', { createAllocations: false });
    22      server.createList('allocation', 3, { nodeId: node.id });
    23    },
    24  });
    25  
    26  test('/clients/:id should have a breadcrumb trail linking back to clients', function(assert) {
    27    ClientDetail.visit({ id: node.id });
    28  
    29    andThen(() => {
    30      assert.equal(
    31        ClientDetail.breadcrumbFor('clients.index').text,
    32        'Clients',
    33        'First breadcrumb says clients'
    34      );
    35      assert.equal(
    36        ClientDetail.breadcrumbFor('clients.client').text,
    37        node.id.split('-')[0],
    38        'Second breadcrumb says the node short id'
    39      );
    40    });
    41  
    42    andThen(() => {
    43      ClientDetail.breadcrumbFor('clients.index').visit();
    44    });
    45  
    46    andThen(() => {
    47      assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients');
    48    });
    49  });
    50  
    51  test('/clients/:id should list immediate details for the node in the title', function(assert) {
    52    ClientDetail.visit({ id: node.id });
    53  
    54    andThen(() => {
    55      assert.ok(ClientDetail.title.includes(node.name), 'Title includes name');
    56      assert.ok(ClientDetail.title.includes(node.id), 'Title includes id');
    57      assert.equal(
    58        ClientDetail.statusLight.objectAt(0).id,
    59        node.status,
    60        'Title includes status light'
    61      );
    62    });
    63  });
    64  
    65  test('/clients/:id should list additional detail for the node below the title', function(assert) {
    66    ClientDetail.visit({ id: node.id });
    67  
    68    andThen(() => {
    69      assert.ok(
    70        ClientDetail.statusDefinition.includes(node.status),
    71        'Status is in additional details'
    72      );
    73      assert.ok(
    74        ClientDetail.statusDecorationClass.includes(`node-${node.status}`),
    75        'Status is decorated with a status class'
    76      );
    77      assert.ok(
    78        ClientDetail.addressDefinition.includes(node.httpAddr),
    79        'Address is in additional details'
    80      );
    81      assert.ok(
    82        ClientDetail.drainingDefinition.includes(node.drain + ''),
    83        'Drain status is in additional details'
    84      );
    85      assert.ok(
    86        ClientDetail.eligibilityDefinition.includes(node.schedulingEligibility),
    87        'Scheduling eligibility is in additional details'
    88      );
    89      assert.ok(
    90        ClientDetail.datacenterDefinition.includes(node.datacenter),
    91        'Datacenter is in additional details'
    92      );
    93    });
    94  });
    95  
    96  test('/clients/:id should list all allocations on the node', function(assert) {
    97    const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length;
    98  
    99    ClientDetail.visit({ id: node.id });
   100  
   101    andThen(() => {
   102      assert.equal(
   103        ClientDetail.allocations.length,
   104        allocationsCount,
   105        `Allocations table lists all ${allocationsCount} associated allocations`
   106      );
   107    });
   108  });
   109  
   110  test('each allocation should have high-level details for the allocation', function(assert) {
   111    const allocation = server.db.allocations
   112      .where({ nodeId: node.id })
   113      .sortBy('modifyIndex')
   114      .reverse()[0];
   115  
   116    const allocStats = server.db.clientAllocationStats.find(allocation.id);
   117    const taskGroup = server.db.taskGroups.findBy({
   118      name: allocation.taskGroup,
   119      jobId: allocation.jobId,
   120    });
   121  
   122    const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id));
   123    const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0);
   124    const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0);
   125  
   126    ClientDetail.visit({ id: node.id });
   127  
   128    andThen(() => {
   129      const allocationRow = ClientDetail.allocations.objectAt(0);
   130  
   131      assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short ID');
   132      assert.equal(
   133        allocationRow.createTime,
   134        moment(allocation.createTime / 1000000).format('MM/DD HH:mm:ss'),
   135        'Allocation create time'
   136      );
   137      assert.equal(
   138        allocationRow.modifyTime,
   139        moment(allocation.modifyTime / 1000000).fromNow(),
   140        'Allocation modify time'
   141      );
   142      assert.equal(allocationRow.status, allocation.clientStatus, 'Client status');
   143      assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name');
   144      assert.ok(allocationRow.taskGroup, 'Task group name');
   145      assert.ok(allocationRow.jobVersion, 'Job Version');
   146      assert.equal(
   147        allocationRow.cpu,
   148        Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed,
   149        'CPU %'
   150      );
   151      assert.equal(
   152        allocationRow.cpuTooltip,
   153        `${Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks)} / ${cpuUsed} MHz`,
   154        'Detailed CPU information is in a tooltip'
   155      );
   156      assert.equal(
   157        allocationRow.mem,
   158        allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed,
   159        'Memory used'
   160      );
   161      assert.equal(
   162        allocationRow.memTooltip,
   163        `${formatBytes([allocStats.resourceUsage.MemoryStats.RSS])} / ${memoryUsed} MiB`,
   164        'Detailed memory information is in a tooltip'
   165      );
   166    });
   167  });
   168  
   169  test('each allocation should show job information even if the job is incomplete and already in the store', function(assert) {
   170    // First, visit clients to load the allocations for each visible node.
   171    // Don't load the job belongsTo of the allocation! Leave it unfulfilled.
   172  
   173    Clients.visit();
   174  
   175    // Then, visit jobs to load all jobs, which should implicitly fulfill
   176    // the job belongsTo of each allocation pointed at each job.
   177  
   178    Jobs.visit();
   179  
   180    // Finally, visit a node to assert that the job name and task group name are
   181    // present. This will require reloading the job, since task groups aren't a
   182    // part of the jobs list response.
   183  
   184    ClientDetail.visit({ id: node.id });
   185  
   186    andThen(() => {
   187      const allocationRow = ClientDetail.allocations.objectAt(0);
   188      const allocation = server.db.allocations
   189        .where({ nodeId: node.id })
   190        .sortBy('modifyIndex')
   191        .reverse()[0];
   192  
   193      assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name');
   194      assert.ok(allocationRow.taskGroup.includes(allocation.taskGroup), 'Task group name');
   195    });
   196  });
   197  
   198  test('each allocation should link to the allocation detail page', function(assert) {
   199    const allocation = server.db.allocations
   200      .where({ nodeId: node.id })
   201      .sortBy('modifyIndex')
   202      .reverse()[0];
   203  
   204    ClientDetail.visit({ id: node.id });
   205  
   206    andThen(() => {
   207      ClientDetail.allocations.objectAt(0).visit();
   208    });
   209  
   210    andThen(() => {
   211      assert.equal(
   212        currentURL(),
   213        `/allocations/${allocation.id}`,
   214        'Allocation rows link to allocation detail pages'
   215      );
   216    });
   217  });
   218  
   219  test('each allocation should link to the job the allocation belongs to', function(assert) {
   220    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    andThen(() => {
   226      ClientDetail.allocations.objectAt(0).visitJob();
   227    });
   228  
   229    andThen(() => {
   230      assert.equal(
   231        currentURL(),
   232        `/jobs/${job.id}`,
   233        'Allocation rows link to the job detail page for the allocation'
   234      );
   235    });
   236  });
   237  
   238  test('/clients/:id should list all attributes for the node', function(assert) {
   239    ClientDetail.visit({ id: node.id });
   240  
   241    andThen(() => {
   242      assert.ok(ClientDetail.attributesTable, 'Attributes table is on the page');
   243    });
   244  });
   245  
   246  test('/clients/:id lists all meta attributes', function(assert) {
   247    node = server.create('node', 'forceIPv4', 'withMeta');
   248  
   249    ClientDetail.visit({ id: node.id });
   250  
   251    andThen(() => {
   252      assert.ok(ClientDetail.metaTable, 'Meta attributes table is on the page');
   253      assert.notOk(ClientDetail.emptyMetaMessage, 'Meta attributes is not empty');
   254  
   255      const firstMetaKey = Object.keys(node.meta)[0];
   256      const firstMetaAttribute = ClientDetail.metaAttributes.objectAt(0);
   257      assert.equal(
   258        firstMetaAttribute.key,
   259        firstMetaKey,
   260        'Meta attributes for the node are bound to the attributes table'
   261      );
   262      assert.equal(
   263        firstMetaAttribute.value,
   264        node.meta[firstMetaKey],
   265        'Meta attributes for the node are bound to the attributes table'
   266      );
   267    });
   268  });
   269  
   270  test('/clients/:id shows an empty message when there is no meta data', function(assert) {
   271    ClientDetail.visit({ id: node.id });
   272  
   273    andThen(() => {
   274      assert.notOk(ClientDetail.metaTable, 'Meta attributes table is not on the page');
   275      assert.ok(ClientDetail.emptyMetaMessage, 'Meta attributes is empty');
   276    });
   277  });
   278  
   279  test('when the node is not found, an error message is shown, but the URL persists', function(assert) {
   280    ClientDetail.visit({ id: 'not-a-real-node' });
   281  
   282    andThen(() => {
   283      assert.equal(
   284        server.pretender.handledRequests.findBy('status', 404).url,
   285        '/v1/node/not-a-real-node',
   286        'A request to the nonexistent node is made'
   287      );
   288      assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists');
   289      assert.ok(ClientDetail.error.isShown, 'Error message is shown');
   290      assert.equal(ClientDetail.error.title, 'Not Found', 'Error message is for 404');
   291    });
   292  });
   293  
   294  test('/clients/:id shows the recent events list', function(assert) {
   295    ClientDetail.visit({ id: node.id });
   296  
   297    andThen(() => {
   298      assert.ok(ClientDetail.hasEvents, 'Client events section exists');
   299    });
   300  });
   301  
   302  test('each node event shows basic node event information', function(assert) {
   303    const event = server.db.nodeEvents
   304      .where({ nodeId: node.id })
   305      .sortBy('time')
   306      .reverse()[0];
   307  
   308    ClientDetail.visit({ id: node.id });
   309  
   310    andThen(() => {
   311      const eventRow = ClientDetail.events.objectAt(0);
   312      assert.equal(eventRow.time, moment(event.time).format('MM/DD/YY HH:mm:ss'), 'Event timestamp');
   313      assert.equal(eventRow.subsystem, event.subsystem, 'Event subsystem');
   314      assert.equal(eventRow.message, event.message, 'Event message');
   315    });
   316  });
   317  
   318  test('/clients/:id shows the driver status of every driver for the node', function(assert) {
   319    // Set the drivers up so health and detection is well tested
   320    const nodeDrivers = node.drivers;
   321    const undetectedDriver = 'raw_exec';
   322  
   323    Object.values(nodeDrivers).forEach(driver => {
   324      driver.Detected = true;
   325    });
   326  
   327    nodeDrivers[undetectedDriver].Detected = false;
   328    node.drivers = nodeDrivers;
   329  
   330    const drivers = Object.keys(node.drivers)
   331      .map(driverName => assign({ Name: driverName }, node.drivers[driverName]))
   332      .sortBy('Name');
   333  
   334    assert.ok(drivers.length > 0, 'Node has drivers');
   335  
   336    ClientDetail.visit({ id: node.id });
   337  
   338    andThen(() => {
   339      drivers.forEach((driver, index) => {
   340        const driverHead = ClientDetail.driverHeads.objectAt(index);
   341  
   342        assert.equal(driverHead.name, driver.Name, `${driver.Name}: Name is correct`);
   343        assert.equal(
   344          driverHead.detected,
   345          driver.Detected ? 'Yes' : 'No',
   346          `${driver.Name}: Detection is correct`
   347        );
   348        assert.equal(
   349          driverHead.lastUpdated,
   350          moment(driver.UpdateTime).fromNow(),
   351          `${driver.Name}: Last updated shows time since now`
   352        );
   353  
   354        if (driver.Name === undetectedDriver) {
   355          assert.notOk(
   356            driverHead.healthIsShown,
   357            `${driver.Name}: No health for the undetected driver`
   358          );
   359        } else {
   360          assert.equal(
   361            driverHead.health,
   362            driver.Healthy ? 'Healthy' : 'Unhealthy',
   363            `${driver.Name}: Health is correct`
   364          );
   365          assert.ok(
   366            driverHead.healthClass.includes(driver.Healthy ? 'running' : 'failed'),
   367            `${driver.Name}: Swatch with correct class is shown`
   368          );
   369        }
   370      });
   371    });
   372  });
   373  
   374  test('each driver can be opened to see a message and attributes', function(assert) {
   375    // Only detected drivers can be expanded
   376    const nodeDrivers = node.drivers;
   377    Object.values(nodeDrivers).forEach(driver => {
   378      driver.Detected = true;
   379    });
   380    node.drivers = nodeDrivers;
   381  
   382    const driver = Object.keys(node.drivers)
   383      .map(driverName => assign({ Name: driverName }, node.drivers[driverName]))
   384      .sortBy('Name')[0];
   385  
   386    ClientDetail.visit({ id: node.id });
   387    const driverHead = ClientDetail.driverHeads.objectAt(0);
   388    const driverBody = ClientDetail.driverBodies.objectAt(0);
   389  
   390    andThen(() => {
   391      assert.notOk(driverBody.descriptionIsShown, 'Driver health description is not shown');
   392      assert.notOk(driverBody.attributesAreShown, 'Driver attributes section is not shown');
   393      driverHead.toggle();
   394    });
   395  
   396    andThen(() => {
   397      assert.equal(
   398        driverBody.description,
   399        driver.HealthDescription,
   400        'Driver health description is now shown'
   401      );
   402      assert.ok(driverBody.attributesAreShown, 'Driver attributes section is now shown');
   403    });
   404  });
   405  
   406  test('the status light indicates when the node is ineligible for scheduling', function(assert) {
   407    node = server.create('node', {
   408      schedulingEligibility: 'ineligible',
   409    });
   410  
   411    ClientDetail.visit({ id: node.id });
   412  
   413    andThen(() => {
   414      assert.equal(
   415        ClientDetail.statusLight.objectAt(0).id,
   416        'ineligible',
   417        'Title status light is in the ineligible state'
   418      );
   419    });
   420  });
   421  
   422  test('when the node has a drain strategy with a positive deadline, the drain stategy section prints the duration', function(assert) {
   423    const deadline = 5400000000000; // 1.5 hours in nanoseconds
   424    const forceDeadline = moment().add(1, 'd');
   425  
   426    node = server.create('node', {
   427      drain: true,
   428      schedulingEligibility: 'ineligible',
   429      drainStrategy: {
   430        Deadline: deadline,
   431        ForceDeadline: forceDeadline.toISOString(),
   432        IgnoreSystemJobs: false,
   433      },
   434    });
   435  
   436    ClientDetail.visit({ id: node.id });
   437  
   438    andThen(() => {
   439      assert.ok(
   440        ClientDetail.drain.deadline.includes(formatDuration(deadline)),
   441        'Deadline is shown in a human formatted way'
   442      );
   443  
   444      assert.ok(
   445        ClientDetail.drain.forcedDeadline.includes(forceDeadline.format('MM/DD/YY HH:mm:ss')),
   446        'Force deadline is shown as an absolute date'
   447      );
   448  
   449      assert.ok(
   450        ClientDetail.drain.forcedDeadline.includes(forceDeadline.fromNow()),
   451        'Force deadline is shown as a relative date'
   452      );
   453  
   454      assert.ok(
   455        ClientDetail.drain.ignoreSystemJobs.endsWith('No'),
   456        'Ignore System Jobs state is shown'
   457      );
   458    });
   459  });
   460  
   461  test('when the node has a drain stategy with no deadline, the drain stategy section mentions that and omits the force deadline', function(assert) {
   462    const deadline = 0;
   463  
   464    node = server.create('node', {
   465      drain: true,
   466      schedulingEligibility: 'ineligible',
   467      drainStrategy: {
   468        Deadline: deadline,
   469        ForceDeadline: '0001-01-01T00:00:00Z', // null as a date
   470        IgnoreSystemJobs: true,
   471      },
   472    });
   473  
   474    ClientDetail.visit({ id: node.id });
   475  
   476    andThen(() => {
   477      assert.ok(
   478        ClientDetail.drain.deadline.includes('No deadline'),
   479        'The value for Deadline is "no deadline"'
   480      );
   481  
   482      assert.notOk(
   483        ClientDetail.drain.hasForcedDeadline,
   484        'Forced deadline is not shown since there is no forced deadline'
   485      );
   486  
   487      assert.ok(
   488        ClientDetail.drain.ignoreSystemJobs.endsWith('Yes'),
   489        'Ignore System Jobs state is shown'
   490      );
   491    });
   492  });
   493  
   494  test('when the node has a drain stategy with a negative deadline, the drain strategy section shows the force badge', function(assert) {
   495    const deadline = -1;
   496  
   497    node = server.create('node', {
   498      drain: true,
   499      schedulingEligibility: 'ineligible',
   500      drainStrategy: {
   501        Deadline: deadline,
   502        ForceDeadline: '0001-01-01T00:00:00Z', // null as a date
   503        IgnoreSystemJobs: false,
   504      },
   505    });
   506  
   507    ClientDetail.visit({ id: node.id });
   508  
   509    andThen(() => {
   510      assert.equal(ClientDetail.drain.badgeLabel, 'Forced Drain', 'Forced Drain badge is described');
   511      assert.ok(ClientDetail.drain.badgeIsDangerous, 'Forced Drain is shown in a red badge');
   512  
   513      assert.notOk(
   514        ClientDetail.drain.hasForcedDeadline,
   515        'Forced deadline is not shown since there is no forced deadline'
   516      );
   517  
   518      assert.ok(
   519        ClientDetail.drain.ignoreSystemJobs.endsWith('No'),
   520        'Ignore System Jobs state is shown'
   521      );
   522    });
   523  });
   524  
   525  moduleForAcceptance('Acceptance | client detail (multi-namespace)', {
   526    beforeEach() {
   527      server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' });
   528      node = server.db.nodes[0];
   529  
   530      // Related models
   531      server.create('namespace');
   532      server.create('namespace', { id: 'other-namespace' });
   533  
   534      server.create('agent');
   535  
   536      // Make a job for each namespace, but have both scheduled on the same node
   537      server.create('job', { id: 'job-1', namespaceId: 'default', createAllocations: false });
   538      server.createList('allocation', 3, { nodeId: node.id });
   539  
   540      server.create('job', { id: 'job-2', namespaceId: 'other-namespace', createAllocations: false });
   541      server.createList('allocation', 3, { nodeId: node.id, jobId: 'job-2' });
   542    },
   543  });
   544  
   545  test('when the node has allocations on different namespaces, the associated jobs are fetched correctly', function(assert) {
   546    window.localStorage.nomadActiveNamespace = 'other-namespace';
   547  
   548    ClientDetail.visit({ id: node.id });
   549  
   550    andThen(() => {
   551      assert.equal(
   552        ClientDetail.allocations.length,
   553        server.db.allocations.length,
   554        'All allocations are scheduled on this node'
   555      );
   556      assert.ok(
   557        server.pretender.handledRequests.findBy('url', '/v1/job/job-1'),
   558        'Job One fetched correctly'
   559      );
   560      assert.ok(
   561        server.pretender.handledRequests.findBy('url', '/v1/job/job-2?namespace=other-namespace'),
   562        'Job Two fetched correctly'
   563      );
   564    });
   565  });