mirror of
https://github.com/kemko/nomad.git
synced 2026-01-04 17:35:43 +03:00
[ui, mirage] Evaluation mocks (#12471)
* Linear and Branching mock evaluations * De-comment * test-trigger * Making evaluation trees dynamic * Reinstated job relationship on eval mock * Dasherize job prefix back to normal * Handle bug where UUIDKey is not present on job * Appending node to eval * Job ID as a passed property * Remove unused import * Branching evals set up as generatable
This commit is contained in:
@@ -7,6 +7,6 @@ import { computed } from '@ember/object';
|
||||
// short: shortUUIDProperty('id') // 123456
|
||||
export default function shortUUIDProperty(uuidKey) {
|
||||
return computed(uuidKey, function () {
|
||||
return this.get(uuidKey).split('-')[0];
|
||||
return this.get(uuidKey)?.split('-')[0];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { generateDiff } from './factories/job-version';
|
||||
import { generateTaskGroupFailures } from './factories/evaluation';
|
||||
import { copy } from 'ember-copy';
|
||||
import formatHost from 'nomad-ui/utils/format-host';
|
||||
import { generateAcceptanceTestEvalMock } from './utils';
|
||||
|
||||
export function findLeader(schema) {
|
||||
const agent = schema.agents.first();
|
||||
@@ -263,19 +262,9 @@ export default function () {
|
||||
});
|
||||
|
||||
this.get('/evaluations');
|
||||
this.get(
|
||||
'/evaluation/:id',
|
||||
function ({ evaluations }, { params, queryParams }) {
|
||||
const showRelated = queryParams.related;
|
||||
|
||||
if (showRelated) {
|
||||
// we are dealing with a "related" request - return the mock
|
||||
return generateAcceptanceTestEvalMock(params.id);
|
||||
}
|
||||
|
||||
return evaluations.find(params.id);
|
||||
}
|
||||
);
|
||||
this.get('/evaluation/:id', function ({ evaluations }, { params }) {
|
||||
return evaluations.find(params.id);
|
||||
});
|
||||
|
||||
this.get('/deployment/allocations/:id', function (schema, { params }) {
|
||||
const job = schema.jobs.find(schema.deployments.find(params.id).jobId);
|
||||
|
||||
@@ -31,9 +31,17 @@ const generateCountMap = (keysCount, list) => () => {
|
||||
};
|
||||
|
||||
const generateNodesAvailable = generateCountMap(5, DATACENTERS);
|
||||
const generateClassFiltered = generateCountMap(3, provide(10, faker.hacker.abbreviation));
|
||||
const generateClassFiltered = generateCountMap(
|
||||
3,
|
||||
provide(10, faker.hacker.abbreviation)
|
||||
);
|
||||
const generateClassExhausted = generateClassFiltered;
|
||||
const generateDimensionExhausted = generateCountMap(1, ['cpu', 'mem', 'disk', 'iops']);
|
||||
const generateDimensionExhausted = generateCountMap(1, [
|
||||
'cpu',
|
||||
'mem',
|
||||
'disk',
|
||||
'iops',
|
||||
]);
|
||||
const generateQuotaExhausted = generateDimensionExhausted;
|
||||
const generateScores = generateCountMap(1, ['binpack', 'job-anti-affinity']);
|
||||
const generateConstraintFiltered = generateCountMap(2, [
|
||||
@@ -59,7 +67,9 @@ export default Factory.extend({
|
||||
|
||||
createIndex: () => faker.random.number({ min: 10, max: 2000 }),
|
||||
createTime() {
|
||||
return faker.date.past(2 / 365, new Date(this.modifyTime / 1000000)) * 1000000;
|
||||
return (
|
||||
faker.date.past(2 / 365, new Date(this.modifyTime / 1000000)) * 1000000
|
||||
);
|
||||
},
|
||||
|
||||
waitUntil: null,
|
||||
@@ -68,14 +78,22 @@ export default Factory.extend({
|
||||
status: 'blocked',
|
||||
afterCreate(evaluation, server) {
|
||||
assignJob(evaluation, server);
|
||||
const taskGroups = server.db.taskGroups.where({ jobId: evaluation.jobId });
|
||||
const taskGroups = server.db.taskGroups.where({
|
||||
jobId: evaluation.jobId,
|
||||
});
|
||||
|
||||
const taskGroupNames = taskGroups.mapBy('name');
|
||||
const failedTaskGroupsCount = faker.random.number({ min: 1, max: taskGroupNames.length });
|
||||
const failedTaskGroupsCount = faker.random.number({
|
||||
min: 1,
|
||||
max: taskGroupNames.length,
|
||||
});
|
||||
const failedTaskGroupNames = [];
|
||||
for (let i = 0; i < failedTaskGroupsCount; i++) {
|
||||
failedTaskGroupNames.push(
|
||||
...taskGroupNames.splice(faker.random.number(taskGroupNames.length - 1), 1)
|
||||
...taskGroupNames.splice(
|
||||
faker.random.number(taskGroupNames.length - 1),
|
||||
1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +121,9 @@ function assignJob(evaluation, server) {
|
||||
server.db.jobs.length
|
||||
);
|
||||
|
||||
const job = evaluation.jobId ? server.db.jobs.find(evaluation.jobId) : pickOne(server.db.jobs);
|
||||
const job = evaluation.jobId
|
||||
? server.db.jobs.find(evaluation.jobId)
|
||||
: pickOne(server.db.jobs);
|
||||
evaluation.update({
|
||||
jobId: job.id,
|
||||
});
|
||||
@@ -115,12 +135,18 @@ export function generateTaskGroupFailures() {
|
||||
NodesEvaluated: faker.random.number({ min: 1, max: 100 }),
|
||||
NodesExhausted: faker.random.number({ min: 1, max: 100 }),
|
||||
|
||||
NodesAvailable: faker.random.number(10) >= 7 ? generateNodesAvailable() : null,
|
||||
ClassFiltered: faker.random.number(10) >= 7 ? generateClassFiltered() : null,
|
||||
ConstraintFiltered: faker.random.number(10) >= 7 ? generateConstraintFiltered() : null,
|
||||
ClassExhausted: faker.random.number(10) >= 7 ? generateClassExhausted() : null,
|
||||
DimensionExhausted: faker.random.number(10) >= 7 ? generateDimensionExhausted() : null,
|
||||
QuotaExhausted: faker.random.number(10) >= 7 ? generateQuotaExhausted() : null,
|
||||
NodesAvailable:
|
||||
faker.random.number(10) >= 7 ? generateNodesAvailable() : null,
|
||||
ClassFiltered:
|
||||
faker.random.number(10) >= 7 ? generateClassFiltered() : null,
|
||||
ConstraintFiltered:
|
||||
faker.random.number(10) >= 7 ? generateConstraintFiltered() : null,
|
||||
ClassExhausted:
|
||||
faker.random.number(10) >= 7 ? generateClassExhausted() : null,
|
||||
DimensionExhausted:
|
||||
faker.random.number(10) >= 7 ? generateDimensionExhausted() : null,
|
||||
QuotaExhausted:
|
||||
faker.random.number(10) >= 7 ? generateQuotaExhausted() : null,
|
||||
Scores: faker.random.number(10) >= 7 ? generateScores() : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ export default Factory.extend({
|
||||
|
||||
if (!job.shallow) {
|
||||
const knownEvaluationProperties = {
|
||||
job,
|
||||
jobId: job.id,
|
||||
namespace: job.namespace,
|
||||
};
|
||||
server.createList(
|
||||
|
||||
3
ui/mirage/models/evaluation-stub.js
Normal file
3
ui/mirage/models/evaluation-stub.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Model } from 'ember-cli-mirage';
|
||||
|
||||
export default Model.extend({});
|
||||
5
ui/mirage/models/evaluation.js
Normal file
5
ui/mirage/models/evaluation.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Model, hasMany, belongsTo } from 'ember-cli-mirage';
|
||||
|
||||
export default Model.extend({
|
||||
relatedEvals: hasMany('evaluation-stub'),
|
||||
});
|
||||
@@ -20,9 +20,11 @@ const allScenarios = {
|
||||
...sysbatchScenarios,
|
||||
};
|
||||
|
||||
const scenario = getScenarioQueryParameter() || getConfigValue('mirageScenario', 'emptyCluster');
|
||||
const scenario =
|
||||
getScenarioQueryParameter() ||
|
||||
getConfigValue('mirageScenario', 'emptyCluster');
|
||||
|
||||
export default function(server) {
|
||||
export default function (server) {
|
||||
const activeScenario = allScenarios[scenario];
|
||||
if (!activeScenario) {
|
||||
throw new Error(
|
||||
@@ -44,14 +46,74 @@ function smallCluster(server) {
|
||||
server.create('feature', { name: 'Dynamic Application Sizing' });
|
||||
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
||||
server.createList('node', 5);
|
||||
server.createList('job', 5, { createRecommendations: true });
|
||||
server.createList('job', 1, { createRecommendations: true });
|
||||
server.createList('allocFile', 5);
|
||||
server.create('allocFile', 'dir', { depth: 2 });
|
||||
server.createList('csi-plugin', 2);
|
||||
|
||||
// #region evaluations
|
||||
|
||||
// Branching: a single eval that relates to N-1 mutually-unrelated evals
|
||||
const NUM_BRANCHING_EVALUATIONS = 3;
|
||||
Array(NUM_BRANCHING_EVALUATIONS)
|
||||
.fill()
|
||||
.map((_, i) => {
|
||||
return {
|
||||
evaluation: server.create('evaluation', {
|
||||
id: `branching_${i}`,
|
||||
previousEval: i > 0 ? `branching_0` : '',
|
||||
jobID: pickOne(server.db.jobs).id,
|
||||
}),
|
||||
|
||||
evaluationStub: server.create('evaluation-stub', {
|
||||
id: `branching_${i}`,
|
||||
previousEval: i > 0 ? `branching_0` : '',
|
||||
status: 'failed',
|
||||
}),
|
||||
};
|
||||
})
|
||||
.map((x, i, all) => {
|
||||
x.evaluation.update({
|
||||
relatedEvals:
|
||||
i === 0
|
||||
? all.filter((_, j) => j !== 0).map((e) => e.evaluation)
|
||||
: all.filter((_, j) => j !== i).map((e) => e.evaluation),
|
||||
});
|
||||
return x;
|
||||
});
|
||||
|
||||
// Linear: a long line of N related evaluations
|
||||
const NUM_LINEAR_EVALUATIONS = 20;
|
||||
Array(NUM_LINEAR_EVALUATIONS)
|
||||
.fill()
|
||||
.map((_, i) => {
|
||||
return {
|
||||
evaluation: server.create('evaluation', {
|
||||
id: `linear_${i}`,
|
||||
previousEval: i > 0 ? `linear_${i - 1}` : '',
|
||||
jobID: pickOne(server.db.jobs).id,
|
||||
}),
|
||||
|
||||
evaluationStub: server.create('evaluation-stub', {
|
||||
id: `linear_${i}`,
|
||||
previousEval: i > 0 ? `linear_${i - 1}` : '',
|
||||
nextEval: `linear_${i + 1}`,
|
||||
status: 'failed',
|
||||
}),
|
||||
};
|
||||
})
|
||||
.map((x, i, all) => {
|
||||
x.evaluation.update({
|
||||
relatedEvals: all.filter((_, j) => i !== j).map((e) => e.evaluation),
|
||||
});
|
||||
return x;
|
||||
});
|
||||
|
||||
// #endregion evaluations
|
||||
|
||||
const csiAllocations = server.createList('allocation', 5);
|
||||
const volumes = server.schema.csiVolumes.all().models;
|
||||
csiAllocations.forEach(alloc => {
|
||||
csiAllocations.forEach((alloc) => {
|
||||
const volume = pickOne(volumes);
|
||||
volume.writeAllocs.add(alloc);
|
||||
volume.readAllocs.add(alloc);
|
||||
@@ -120,7 +182,11 @@ function everyFeature(server) {
|
||||
namespaceId: 'default',
|
||||
createAllocations: false,
|
||||
});
|
||||
server.create('job', { type: 'batch', failedPlacements: true, namespaceId: 'default' });
|
||||
server.create('job', {
|
||||
type: 'batch',
|
||||
failedPlacements: true,
|
||||
namespaceId: 'default',
|
||||
});
|
||||
server.create('job', { type: 'system', namespaceId: 'default' });
|
||||
server.create('job', 'periodic', { namespaceId: 'default' });
|
||||
server.create('job', 'parameterized', { namespaceId: 'default' });
|
||||
@@ -147,15 +213,17 @@ function createNamespaces(server) {
|
||||
}
|
||||
|
||||
function createRegions(server) {
|
||||
['americas', 'europe', 'asia', 'some-long-name-just-to-test'].forEach(id => {
|
||||
server.create('region', { id });
|
||||
});
|
||||
['americas', 'europe', 'asia', 'some-long-name-just-to-test'].forEach(
|
||||
(id) => {
|
||||
server.create('region', { id });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
function logTokens(server) {
|
||||
console.log('TOKENS:');
|
||||
server.db.tokens.forEach(token => {
|
||||
server.db.tokens.forEach((token) => {
|
||||
console.log(`
|
||||
Name: ${token.name}
|
||||
Secret: ${token.secretId}
|
||||
|
||||
6
ui/mirage/serializers/evaluation.js
Normal file
6
ui/mirage/serializers/evaluation.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
embed: true,
|
||||
include: ['relatedEvals'],
|
||||
});
|
||||
@@ -26,6 +26,7 @@ export function arrToObj(prop, alias = '') {
|
||||
}
|
||||
|
||||
export const generateAcceptanceTestEvalMock = (id) => {
|
||||
|
||||
return {
|
||||
CreateIndex: 20,
|
||||
CreateTime: 1647899150314738000,
|
||||
@@ -267,224 +268,3 @@ export const generateAcceptanceTestEvalMock = (id) => {
|
||||
Wait: 20000000000,
|
||||
};
|
||||
};
|
||||
|
||||
export const MOCK_EVALUATION = {
|
||||
CreateIndex: 20,
|
||||
CreateTime: 1647899150314738000,
|
||||
ID: 'fede162c-26a6-c108-178b-1c140f9f5680',
|
||||
JobID: 'example',
|
||||
JobModifyIndex: 10,
|
||||
ModifyIndex: 31,
|
||||
ModifyTime: 1647899318007569000,
|
||||
Namespace: 'default',
|
||||
NextEval: 'fd1cd898-d655-c7e4-17f6-a1a2e98b18ef',
|
||||
PreviousEval: 'd8a5c14f-120a-3d83-6305-90927356dd6c',
|
||||
Priority: 50,
|
||||
RelatedEvals: [
|
||||
{
|
||||
BlockedEval: '',
|
||||
CreateIndex: 31,
|
||||
CreateTime: 1647899318007563000,
|
||||
DeploymentID: '',
|
||||
ID: 'fd1cd898-d655-c7e4-17f6-a1a2e98b18ef',
|
||||
JobID: 'example',
|
||||
ModifyIndex: 44,
|
||||
ModifyTime: 1647899591412413000,
|
||||
Namespace: 'default',
|
||||
NextEval: 'cac7dfa0-b79b-ee55-c86a-0ca89dffb9e1',
|
||||
NodeID: '',
|
||||
PreviousEval: 'fede162c-26a6-c108-178b-1c140f9f5680',
|
||||
Priority: 50,
|
||||
Status: 'failed',
|
||||
StatusDescription: 'evaluation reached delivery limit (3)',
|
||||
TriggeredBy: 'failed-follow-up',
|
||||
Type: 'service',
|
||||
WaitUntil: null,
|
||||
},
|
||||
{
|
||||
BlockedEval: '',
|
||||
CreateIndex: 10,
|
||||
CreateTime: 1647899129298997000,
|
||||
DeploymentID: '',
|
||||
ID: 'd8a5c14f-120a-3d83-6305-90927356dd6c',
|
||||
JobID: 'example',
|
||||
ModifyIndex: 20,
|
||||
ModifyTime: 1647899150314745000,
|
||||
Namespace: 'default',
|
||||
NextEval: 'fede162c-26a6-c108-178b-1c140f9f5680',
|
||||
NodeID: '',
|
||||
PreviousEval: '',
|
||||
Priority: 50,
|
||||
Status: 'failed',
|
||||
StatusDescription: 'evaluation reached delivery limit (3)',
|
||||
TriggeredBy: 'job-register',
|
||||
Type: 'service',
|
||||
WaitUntil: null,
|
||||
},
|
||||
{
|
||||
BlockedEval: '',
|
||||
CreateIndex: 44,
|
||||
CreateTime: 1647899591412410000,
|
||||
DeploymentID: '',
|
||||
ID: 'cac7dfa0-b79b-ee55-c86a-0ca89dffb9e1',
|
||||
JobID: 'example',
|
||||
ModifyIndex: 53,
|
||||
ModifyTime: 1647899729480596000,
|
||||
Namespace: 'default',
|
||||
NextEval: 'e49bf53c-da6a-c869-8317-f2089682f503',
|
||||
NodeID: '',
|
||||
PreviousEval: 'fd1cd898-d655-c7e4-17f6-a1a2e98b18ef',
|
||||
Priority: 50,
|
||||
Status: 'failed',
|
||||
StatusDescription: 'evaluation reached delivery limit (3)',
|
||||
TriggeredBy: 'failed-follow-up',
|
||||
Type: 'service',
|
||||
WaitUntil: null,
|
||||
},
|
||||
{
|
||||
BlockedEval: '',
|
||||
CreateIndex: 53,
|
||||
CreateTime: 1647899729480592000,
|
||||
DeploymentID: '',
|
||||
ID: 'e49bf53c-da6a-c869-8317-f2089682f503',
|
||||
JobID: 'example',
|
||||
ModifyIndex: 64,
|
||||
ModifyTime: 1647899881302731000,
|
||||
Namespace: 'default',
|
||||
NextEval: 'a8d29cfc-517c-2e4c-9722-b47e84152c64',
|
||||
NodeID: '',
|
||||
PreviousEval: 'cac7dfa0-b79b-ee55-c86a-0ca89dffb9e1',
|
||||
Priority: 50,
|
||||
Status: 'failed',
|
||||
StatusDescription: 'evaluation reached delivery limit (3)',
|
||||
TriggeredBy: 'failed-follow-up',
|
||||
Type: 'service',
|
||||
WaitUntil: null,
|
||||
},
|
||||
{
|
||||
BlockedEval: '',
|
||||
CreateIndex: 64,
|
||||
CreateTime: 1647899881302723000,
|
||||
DeploymentID: '',
|
||||
ID: 'a8d29cfc-517c-2e4c-9722-b47e84152c64',
|
||||
JobID: 'example',
|
||||
ModifyIndex: 81,
|
||||
ModifyTime: 1647900212725381000,
|
||||
Namespace: 'default',
|
||||
NextEval: 'b37d06e4-4eb4-b29d-3b4a-b0c7bf2528ad',
|
||||
NodeID: '',
|
||||
PreviousEval: 'e49bf53c-da6a-c869-8317-f2089682f503',
|
||||
Priority: 50,
|
||||
Status: 'failed',
|
||||
StatusDescription: 'evaluation reached delivery limit (3)',
|
||||
TriggeredBy: 'failed-follow-up',
|
||||
Type: 'service',
|
||||
WaitUntil: null,
|
||||
},
|
||||
{
|
||||
BlockedEval: '',
|
||||
CreateIndex: 81,
|
||||
CreateTime: 1647900212725376000,
|
||||
DeploymentID: '',
|
||||
ID: 'b37d06e4-4eb4-b29d-3b4a-b0c7bf2528ad',
|
||||
JobID: 'example',
|
||||
ModifyIndex: 97,
|
||||
ModifyTime: 1647900516944239000,
|
||||
Namespace: 'default',
|
||||
NextEval: 'd7c50aa5-5bf1-5119-d7e7-0d0ae5381856',
|
||||
NodeID: '',
|
||||
PreviousEval: 'a8d29cfc-517c-2e4c-9722-b47e84152c64',
|
||||
Priority: 50,
|
||||
Status: 'failed',
|
||||
StatusDescription: 'evaluation reached delivery limit (3)',
|
||||
TriggeredBy: 'failed-follow-up',
|
||||
Type: 'service',
|
||||
WaitUntil: null,
|
||||
},
|
||||
{
|
||||
BlockedEval: '',
|
||||
CreateIndex: 97,
|
||||
CreateTime: 1647900516944236000,
|
||||
DeploymentID: '',
|
||||
ID: 'd7c50aa5-5bf1-5119-d7e7-0d0ae5381856',
|
||||
JobID: 'example',
|
||||
ModifyIndex: 114,
|
||||
ModifyTime: 1647900825385587000,
|
||||
Namespace: 'default',
|
||||
NextEval: 'ea2239aa-26d6-8874-8c56-e1600585772b',
|
||||
NodeID: '',
|
||||
PreviousEval: 'b37d06e4-4eb4-b29d-3b4a-b0c7bf2528ad',
|
||||
Priority: 50,
|
||||
Status: 'failed',
|
||||
StatusDescription: 'evaluation reached delivery limit (3)',
|
||||
TriggeredBy: 'failed-follow-up',
|
||||
Type: 'service',
|
||||
WaitUntil: null,
|
||||
},
|
||||
{
|
||||
BlockedEval: '',
|
||||
CreateIndex: 114,
|
||||
CreateTime: 1647900825385584000,
|
||||
DeploymentID: '',
|
||||
ID: 'ea2239aa-26d6-8874-8c56-e1600585772b',
|
||||
JobID: 'example',
|
||||
ModifyIndex: 128,
|
||||
ModifyTime: 1647900979511304000,
|
||||
Namespace: 'default',
|
||||
NextEval: '25a2dd19-8d22-d1dd-280a-79860c9b8bdb',
|
||||
NodeID: '',
|
||||
PreviousEval: 'd7c50aa5-5bf1-5119-d7e7-0d0ae5381856',
|
||||
Priority: 50,
|
||||
Status: 'failed',
|
||||
StatusDescription: 'evaluation reached delivery limit (3)',
|
||||
TriggeredBy: 'failed-follow-up',
|
||||
Type: 'service',
|
||||
WaitUntil: null,
|
||||
},
|
||||
{
|
||||
BlockedEval: '',
|
||||
CreateIndex: 128,
|
||||
CreateTime: 1647900979511301000,
|
||||
DeploymentID: '',
|
||||
ID: '25a2dd19-8d22-d1dd-280a-79860c9b8bdb',
|
||||
JobID: 'example',
|
||||
ModifyIndex: 136,
|
||||
ModifyTime: 1647901211369652000,
|
||||
Namespace: 'default',
|
||||
NextEval: '1fded690-20ad-6afa-3b89-59e319dfce18',
|
||||
NodeID: '',
|
||||
PreviousEval: 'ea2239aa-26d6-8874-8c56-e1600585772b',
|
||||
Priority: 50,
|
||||
Status: 'failed',
|
||||
StatusDescription: 'evaluation reached delivery limit (3)',
|
||||
TriggeredBy: 'failed-follow-up',
|
||||
Type: 'service',
|
||||
WaitUntil: null,
|
||||
},
|
||||
{
|
||||
BlockedEval: '',
|
||||
CreateIndex: 136,
|
||||
CreateTime: 1647901211369648000,
|
||||
DeploymentID: '',
|
||||
ID: '1fded690-20ad-6afa-3b89-59e319dfce18',
|
||||
JobID: 'example',
|
||||
ModifyIndex: 136,
|
||||
ModifyTime: 1647901211369648000,
|
||||
Namespace: 'default',
|
||||
NextEval: '',
|
||||
NodeID: '',
|
||||
PreviousEval: '25a2dd19-8d22-d1dd-280a-79860c9b8bdb',
|
||||
Priority: 50,
|
||||
Status: 'pending',
|
||||
StatusDescription: '',
|
||||
TriggeredBy: 'failed-follow-up',
|
||||
Type: 'service',
|
||||
WaitUntil: null,
|
||||
},
|
||||
],
|
||||
Status: 'failed',
|
||||
StatusDescription: 'evaluation reached delivery limit (3)',
|
||||
TriggeredBy: 'failed-follow-up',
|
||||
Type: 'service',
|
||||
Wait: 20000000000,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user