Repository navigation
Expand file tree
/
Copy pathmock-backends.js
More file actions
296 lines (279 loc) · 15.3 KB
/
Copy pathmock-backends.js
File metadata and controls
296 lines (279 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// Local development on SAMPLE data, without Elasticsearch or Globus: `npm run dev:mock`, then
// open http://localhost:8080. For the real data use `npm run dev` (dev/real-backends.js).
// Preloaded before server.js; stubs the config files, Elasticsearch (in-memory data below),
// the Globus login (signs you in as "Test User") and moves port 80 to 8080.
// http://localhost:8082 shows the in-memory data, e.g. what the subscriptions page saved.
const fs = require('fs');
const http = require('http');
const Module = require('module');
const PORT = 8080;
const FAKE_GLOBUS_PORT = 8081;
const config = {
TITLE: 'AAAS',
ES_HOST: { node: 'http://fake-es' },
PREFERENCES: {
vacation: { name: 'Vacation mode', type: 'boolean', default_value: false },
mail_interval: { name: 'Mail interval (hours)', type: 'number', default_value: 6 },
prefered_mail: { name: 'Preferred email', type: 'email', default_value: '' },
},
TOPOLOGY_FIELDS: ['category', 'subcategory', 'event'],
REQUIRED_ALARM_FIELDS: ['body'],
OPTIONAL_ALARM_FIELDS: ['tags', 'source', 'level'],
REQUIRED_ALARM_TOPOLOGY_FIELDS: ['description', 'template'],
REQUIRED_HEARTBEAT_FIELDS: [],
OPTIONAL_HEARTBEAT_FIELDS: ['source', 'tags'],
REQUIRED_HEARTBEAT_TOPOLOGY_FIELDS: ['description', 'template', 'interval', 'min_expected'],
};
const globus = {
CLIENT_ID: 'cid',
CLIENT_SECRET: 'secret',
REDIRECT_LINK: `http://localhost:${PORT}/authcallback`,
AUTHORIZE_URI: `http://localhost:${FAKE_GLOBUS_PORT}/authorize`,
TOKEN_URI: 'https://auth.globus.org/v2/oauth2/token',
LOGOUT_URI: 'https://auth.globus.org/v2/web/logout',
};
const origRead = fs.readFileSync;
fs.readFileSync = function patched(p, ...rest) {
if (p === '/etc/aaasf/config.json') return JSON.stringify(config);
if (p === '/etc/aaasf/globus-config.json') return JSON.stringify(globus);
return origRead.call(this, p, ...rest);
};
const origListen = http.Server.prototype.listen;
http.Server.prototype.listen = function patched(port, ...rest) {
return origListen.call(this, port === 80 ? PORT : port, ...rest);
};
// ---- fake data -----------------------------------------------------------
const net = (subcategory, event, description) => ({ category: 'Networking', subcategory, event, description, template: 'x' });
const categories = [
net('Sites', 'bandwidth decreased', 'Bandwidth decreased for a site compared to the past 21 days, measured with perfSONAR throughput tests.'),
net('Sites', 'bandwidth decreased from/to multiple sites', 'Bandwidth decreased between a site and several other sites at the same time.'),
net('Sites', 'high packet loss', 'Packet loss above 2% for a source-destination pair over the past 24 hours.'),
net('Sites', 'high packet loss on multiple links', 'Packet loss above 2% on several links involving the same site.'),
net('Sites', 'firewall issue', 'Throughput tests succeed but latency tests fail, which points to a firewall.'),
net('Infrastructure', 'complete packet loss', '100% packet loss on a link.'),
net('Infrastructure', 'large clock correction', 'perfSONAR host clock drifted by more than 100 ms.'),
net('Infrastructure', 'hosts not found', 'perfSONAR hosts registered in CRIC that do not report measurements.'),
net('Infrastructure', 'unresolvable host', 'perfSONAR hosts whose names do not resolve.'),
net('RENs', 'path changed', 'The route between two sites changed compared to its usual path.'),
net('RENs', 'ASN path anomalies', 'An unusual autonomous system appears on a path.'),
{
category: 'Networking', subcategory: 'Network', event: 'suspected path element', description: 'A network element suspected of degrading several paths.', template: 'x',
},
{ category: 'Analytics', subcategory: 'Frontier', event: 'Failed queries', description: 'Code running every 1h at UC k8s cluster. Checks all servers for rejected queries and DB disconnections.', template: 'x' },
{ category: 'Analytics', subcategory: 'Frontier', event: 'Bad SQL queries', description: 'Queries that took too long or returned too much data.', template: 'x' },
{ category: 'Analytics', subcategory: 'Elasticsearch', event: 'cluster status', description: 'Elasticsearch cluster is not green.', template: 'x' },
{ category: 'Virtual Placement', subcategory: 'XCache', event: 'dead server', description: 'An XCache server stopped responding.', template: 'x' },
];
const hbTopology = [
{ category: 'SLATE', subcategory: 'Squid', event: 'server down', description: 'This alarm gets generated if a squid instance does not send at least 4 heartbeats in last 60 seconds.', template: 'x', interval: 3600, min_expected: 4 },
{ category: 'Virtual Placement', subcategory: 'XCache', event: 'heartbeat missing', description: 'XCache node stopped sending heartbeats.', template: 'x', interval: 3600, min_expected: 2 },
];
const psNetsites = ['MWT2_UC', 'MWT2_IU', 'AGLT2_UM', 'AGLT2_MSU', 'BNL-ATLAS-LHCOPNE', 'CERN-PROD-LHCOPNE', 'FZK-LCG2-LHCOPNE',
'IN2P3-CC-LHCOPNE', 'INFN-T1-LHCOPNE', 'NDGF-T1-LHCOPNE', 'RAL-LCG2-LHCOPN', 'TRIUMF-LCG2-LHCOPNE', 'PIC-LHCOPNE', 'KIT-LHCONE',
'JINR-T1-LHCOPNE', 'UKI-SCOTGRID-GLASGOW', 'UKI-NORTHGRID-LANCS-HEP', 'UKI-NORTHGRID-MAN-HEP subnets', 'GLOW-LHCONE', 'INFN-ROMA1'];
const psHosts = ['perfsonar01.cern.ch', 'perfsonar02.cern.ch', 'psmsu01.aglt2.org', 'psum01.aglt2.org', 'uct2-net1.mwt2.org',
'iut2-net1.iu.edu', 'lhcmon.bnl.gov', 'perfsonar-de-kit.gridka.de', 'ccperfsonar1.in2p3.fr', 'ps-latency.clumeq.mcgill.ca'];
// ps_trace: a year of traceroutes
const aggValues = {
src_netsite: psNetsites,
dest_netsite: psNetsites.slice(5),
src_host: psHosts,
dest_host: psHosts.slice(3),
// networks on traceroute paths; 0 marks an unknown hop
asns: [513, 2200, 39590, 786, 3, 0],
};
// aaas_alarms: the tags each event's alarms carried over the past week, and those of its latest
// alarms (`latest`, the same when missing)
const eventTags = [
{ category: 'Networking', subcategory: 'Sites', event: 'high packet loss', tags: ['MWT2', 'AGLT2', 'BNL-ATLAS', 'MWT2_UC'] },
{ category: 'Networking', subcategory: 'Sites', event: 'bandwidth decreased', tags: ['BNL-ATLAS', 'PIC-LHCOPNE'] },
{ category: 'Networking', subcategory: 'RENs', event: 'path changed', tags: ['MWT2', 'AGLT2', 'CERN-PROD', 'KIT-LHCONE'] },
{ category: 'Networking', subcategory: 'Network', event: 'suspected path element', tags: ['AS2200', 'AS513', 'CERN-PROD-LHCOPNE', 'UKI-NORTHGRID-MAN-HEP_SUBNETS'] },
// no alarms this week, but its latest ones were tagged with hosts: suggests hosts
{ category: 'Networking', subcategory: 'Infrastructure', event: 'unresolvable host', tags: [], latest: ['ps-latency.clumeq.mcgill.ca', 'bdw.scinet.utoronto.ca'] },
// tagged with both sites and hosts: suggests both lists
{ category: 'Networking', subcategory: 'Infrastructure', event: 'hosts not found', tags: ['AGLT2_UM', 'psum01.aglt2.org'] },
{ category: 'Analytics', subcategory: 'Frontier', event: 'Failed queries', tags: ['atlasfrontier2-dev.cern.ch'] },
{ category: 'Analytics', subcategory: 'Frontier', event: 'Bad SQL queries', tags: [] },
{ category: 'Analytics', subcategory: 'Elasticsearch', event: 'cluster status', tags: ['red', 'yellow'] },
{ category: 'Virtual Placement', subcategory: 'XCache', event: 'dead server', tags: ['BNL', 'MWT2', 'RHUL', 'University of Nowhere'] },
];
// ps_asns_1: network owner by ASN
const asnOwners = {
513: 'CERN, CH', 2200: 'FR-RENATER Reseau National de telecommunications pour la Tec', 39590: 'NDGF High energy physicists community in the nordics., SE', 786: 'JANET Jisc Services Limited, GB', 3: 'MIT-GATEWAYS, US',
};
// events whose alarms carry a severity `level`; one has only raised major alarms so far
const levelData = [
{ category: 'Networking', subcategory: 'Sites', event: 'high packet loss', levels: ['minor', 'major'] },
{ category: 'Networking', subcategory: 'RENs', event: 'path changed', levels: ['major'] },
];
// recent alarms for the "Ongoing issues" panel, covering each filtering rule
const minutesAgo = (m) => Date.now() - m * 60000;
const netAlarm = (subcategory, event, tags, level, m) => ({
category: 'Networking', subcategory, event, tags, level, created_at: minutesAgo(m),
});
const recentAlarms = [
...[20, 80, 140, 300, 600].map((m) => netAlarm('Sites', 'high packet loss', ['MWT2', 'BNL-ATLAS'], 'major', m)),
// the test user only wants 'major' for this event, so these are left out
...[30, 90].map((m) => netAlarm('Sites', 'high packet loss', ['AGLT2'], 'minor', m)),
...[45, 200, 400].map((m) => netAlarm('RENs', 'path changed', ['MWT2', 'AGLT2'], 'major', m)),
netAlarm('RENs', 'path changed', ['CERN-PROD'], 'major', 50),
// comma-separated subscription tags never match in alerts.py, so this is left out too
netAlarm('Sites', 'bandwidth decreased', ['BNL-ATLAS'], undefined, 70),
// heartbeat alarms have no tags and can't be muted per site
{
category: 'SLATE', subcategory: 'Squid', event: 'server down', created_at: minutesAgo(15), source: { site: 'MWT2' },
},
];
const users = {
'user-1': {
username: 'test-user',
affiliation: 'University of Chicago',
user: 'Test User',
email: 'test.user@example.org',
preferences: { vacation: false, mail_interval: 6 },
subscriptions: [
{
category: 'Networking', subcategory: 'Sites', event: 'high packet loss', tags: 'MWT2 AGLT2', levels: 'major',
},
{ category: 'Networking', subcategory: 'Sites', event: 'bandwidth decreased', tags: 'BNL-ATLAS,SWT2_CPB' },
{ category: 'Networking', subcategory: 'RENs', event: 'path changed', tags: '*' },
{ category: 'SLATE', subcategory: 'Squid', event: 'server down', tags: '*' },
{ category: 'Old', subcategory: 'Gone', event: 'deleted event', tags: '*' },
],
acknowledgements: [
{
category: 'Networking', subcategory: 'RENs', event: 'path changed', tag: 'CERN-PROD', created_at: minutesAgo(60), until: minutesAgo(-2 * 24 * 60),
},
// muted, but no matching alarms in the last 24 h
{
category: 'Networking', subcategory: 'Sites', event: 'high packet loss', tag: 'AGLT2', created_at: minutesAgo(600), until: minutesAgo(-5 * 24 * 60),
},
// already expired: must not be shown
{
category: 'Networking', subcategory: 'RENs', event: 'path changed', tag: 'MWT2', created_at: minutesAgo(9000), until: minutesAgo(30),
},
],
},
};
global.FAKE = { users, writes: [] };
// shown as a badge on every page (see views/layout.pug)
process.env.AAAS_DEV_BANNER = 'Sample data · npm run dev:mock';
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
const hits = (arr) => ({ hits: { total: { value: arr.length }, hits: arr.map((s, i) => ({ _id: String(i), _source: s })) } });
class FakeES {
async search(p) {
if (p.index === 'aaas_users') {
if (p.query.match && p.query.match._id) {
const u = users[p.query.match._id];
return u ? { hits: { total: { value: 1 }, hits: [{ _id: p.query.match._id, _source: JSON.parse(JSON.stringify(u)) }] } } : hits([]);
}
return { hits: { total: { value: Object.keys(users).length }, hits: Object.entries(users).map(([id, s]) => ({ _id: id, _source: s })) } };
}
if (p.index === 'aaas_categories') return hits(categories);
if (p.index === 'aaas_heartbeats_topology') return hits(hbTopology);
if (p.index === 'aaas_alarms' && p.aggs && p.aggs.category) {
// nested category > subcategory > event > level aggregation (loadAlarmLevels)
const group = (items, field) => [...new Set(items.map((i) => i[field]))].map((key) => ({ key, items: items.filter((i) => i[field] === key) }));
const aggregations = {
category: {
buckets: group(levelData, 'category').map((c) => ({
key: c.key,
subcategory: {
buckets: group(c.items, 'subcategory').map((s) => ({
key: s.key,
event: {
buckets: s.items.map((e) => ({ key: e.event, level: { buckets: e.levels.map((key) => ({ key, doc_count: 1 })) } })),
},
})),
},
})),
},
};
return { hits: { total: { value: 0 }, hits: [] }, aggregations };
}
if (p.index === 'aaas_alarms' && p.aggs && p.aggs.events) {
// category|subcategory|event > the past week's tags and the latest alarms (loadEventTags)
const buckets = eventTags.map((e) => ({
key: [e.category, e.subcategory, e.event],
week: { tags: { buckets: e.tags.map((key) => ({ key, doc_count: 1 })) } },
latest: { hits: { hits: (e.latest || e.tags).map((tag) => ({ _source: { tags: [tag] } })) } },
}));
return { hits: { total: { value: 0 }, hits: [] }, aggregations: { events: { buckets } } };
}
if (p.index === 'aaas_alarms' && !p.aggs) return hits(recentAlarms);
if (p.index === 'ps_trace' && p.aggs) {
await delay(400);
global.FAKE.lastQuery = global.FAKE.lastQuery || {};
global.FAKE.lastQuery[p.index] = p.query;
const aggregations = {};
Object.entries(p.aggs).forEach(([name, a]) => {
aggregations[name] = { buckets: (aggValues[a.terms.field] || []).map((key) => ({ key, doc_count: 1 })) };
});
global.FAKE.aggFields = Object.values(p.aggs).map((a) => a.terms.field);
return { hits: { total: { value: 0 }, hits: [] }, aggregations };
}
return hits([]);
}
async fieldCaps(p) {
const kw = { keyword: { type: 'keyword', aggregatable: true } };
const text = { text: { type: 'text', aggregatable: false } };
return {
indices: ['aaas_alarms'],
fields: {
tags: text,
'tags.keyword': kw,
category: kw,
subcategory: kw,
event: kw,
level: kw,
},
};
}
async mget(p) {
return { docs: p.ids.map((id) => (asnOwners[id] ? { _id: id, found: true, _source: { owner: asnOwners[id] } } : { _id: id, found: false })) };
}
async index(p) {
if (p.index === 'aaas_users') users[p.id] = p.body;
return { result: 'created' };
}
async update(p) {
global.FAKE.writes.push({ id: p.id, doc: p.body.doc });
Object.assign(users[p.id], p.body.doc);
return { result: 'updated' };
}
async deleteByQuery() { return { deleted: 0 }; }
async updateByQuery() { return {}; }
}
const fakeAxios = {
async post(url) {
if (url.startsWith(globus.TOKEN_URI)) return { status: 200, data: { access_token: 'tok' } };
if (url.includes('userinfo')) {
return {
status: 200,
data: {
sub: 'user-1', name: 'Test User', email: 'test.user@example.org', preferred_username: 'test-user', organization: 'University of Chicago',
},
};
}
throw new Error(`unexpected POST ${url}`);
},
async get() { return { status: 200 }; },
};
const origLoad = Module._load;
Module._load = function patched(request, ...rest) {
if (request === '@elastic/elasticsearch') return { Client: FakeES };
if (request === 'axios') return { default: fakeAxios };
return origLoad.call(this, request, ...rest);
};
// fake Globus authorize page: bounce straight back with a code
http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
res.writeHead(302, { Location: `${u.searchParams.get('redirect_uri')}?code=abc&state=${u.searchParams.get('state')}` });
res.end();
}).listen(FAKE_GLOBUS_PORT);
// expose the in-memory data for inspection
http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(global.FAKE));
}).listen(8082);