]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/Parser.js
lxc: Add `Disconnect` option for network interfaces
[pve-manager.git] / www / manager6 / Parser.js
1 // Some configuration values are complex strings - so we need parsers/generators for them.
2 Ext.define('PVE.Parser', {
3 statics: {
4
5 // this class only contains static functions
6
7 printACME: function(value) {
8 if (Ext.isArray(value.domains)) {
9 value.domains = value.domains.join(';');
10 }
11 return PVE.Parser.printPropertyString(value);
12 },
13
14 parseACME: function(value) {
15 if (!value) {
16 return {};
17 }
18
19 let res = {};
20 try {
21 value.split(',').forEach(property => {
22 let [k, v] = property.split('=', 2);
23 if (Ext.isDefined(v)) {
24 res[k] = v;
25 } else {
26 throw `Failed to parse key-value pair: ${property}`;
27 }
28 });
29 } catch (err) {
30 console.warn(err);
31 return undefined;
32 }
33
34 if (res.domains !== undefined) {
35 res.domains = res.domains.split(/;/);
36 }
37
38 return res;
39 },
40
41 parseBoolean: function(value, default_value) {
42 if (!Ext.isDefined(value)) {
43 return default_value;
44 }
45 value = value.toLowerCase();
46 return value === '1' ||
47 value === 'on' ||
48 value === 'yes' ||
49 value === 'true';
50 },
51
52 parsePropertyString: function(value, defaultKey) {
53 let res = {};
54
55 if (typeof value !== 'string' || value === '') {
56 return res;
57 }
58
59 try {
60 value.split(',').forEach(property => {
61 let [k, v] = property.split('=', 2);
62 if (Ext.isDefined(v)) {
63 res[k] = v;
64 } else if (Ext.isDefined(defaultKey)) {
65 if (Ext.isDefined(res[defaultKey])) {
66 throw 'defaultKey may be only defined once in propertyString';
67 }
68 res[defaultKey] = k; // k ist the value in this case
69 } else {
70 throw `Failed to parse key-value pair: ${property}`;
71 }
72 });
73 } catch (err) {
74 console.warn(err);
75 return undefined;
76 }
77
78 return res;
79 },
80
81 printPropertyString: function(data, defaultKey) {
82 var stringparts = [],
83 gotDefaultKeyVal = false,
84 defaultKeyVal;
85
86 Ext.Object.each(data, function(key, value) {
87 if (defaultKey !== undefined && key === defaultKey) {
88 gotDefaultKeyVal = true;
89 defaultKeyVal = value;
90 } else if (value !== '') {
91 stringparts.push(key + '=' + value);
92 }
93 });
94
95 stringparts = stringparts.sort();
96 if (gotDefaultKeyVal) {
97 stringparts.unshift(defaultKeyVal);
98 }
99
100 return stringparts.join(',');
101 },
102
103 parseQemuNetwork: function(key, value) {
104 if (!(key && value)) {
105 return undefined;
106 }
107
108 let res = {},
109 errors = false;
110 Ext.Array.each(value.split(','), function(p) {
111 if (!p || p.match(/^\s*$/)) {
112 return undefined; // continue
113 }
114
115 let match_res;
116
117 if ((match_res = p.match(/^(ne2k_pci|e1000|e1000-82540em|e1000-82544gc|e1000-82545em|vmxnet3|rtl8139|pcnet|virtio|ne2k_isa|i82551|i82557b|i82559er)(=([0-9a-f]{2}(:[0-9a-f]{2}){5}))?$/i)) !== null) {
118 res.model = match_res[1].toLowerCase();
119 if (match_res[3]) {
120 res.macaddr = match_res[3];
121 }
122 } else if ((match_res = p.match(/^bridge=(\S+)$/)) !== null) {
123 res.bridge = match_res[1];
124 } else if ((match_res = p.match(/^rate=(\d+(\.\d+)?)$/)) !== null) {
125 res.rate = match_res[1];
126 } else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) {
127 res.tag = match_res[1];
128 } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
129 res.firewall = match_res[1];
130 } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
131 res.disconnect = match_res[1];
132 } else if ((match_res = p.match(/^queues=(\d+)$/)) !== null) {
133 res.queues = match_res[1];
134 } else if ((match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null) {
135 res.trunks = match_res[1];
136 } else if ((match_res = p.match(/^mtu=(\d+)$/)) !== null) {
137 res.mtu = match_res[1];
138 } else {
139 errors = true;
140 return false; // break
141 }
142 return undefined; // continue
143 });
144
145 if (errors || !res.model) {
146 return undefined;
147 }
148
149 return res;
150 },
151
152 printQemuNetwork: function(net) {
153 var netstr = net.model;
154 if (net.macaddr) {
155 netstr += "=" + net.macaddr;
156 }
157 if (net.bridge) {
158 netstr += ",bridge=" + net.bridge;
159 if (net.tag) {
160 netstr += ",tag=" + net.tag;
161 }
162 if (net.firewall) {
163 netstr += ",firewall=" + net.firewall;
164 }
165 }
166 if (net.rate) {
167 netstr += ",rate=" + net.rate;
168 }
169 if (net.queues) {
170 netstr += ",queues=" + net.queues;
171 }
172 if (net.disconnect) {
173 netstr += ",link_down=" + net.disconnect;
174 }
175 if (net.trunks) {
176 netstr += ",trunks=" + net.trunks;
177 }
178 if (net.mtu) {
179 netstr += ",mtu=" + net.mtu;
180 }
181 return netstr;
182 },
183
184 parseQemuDrive: function(key, value) {
185 if (!(key && value)) {
186 return undefined;
187 }
188
189 const [, bus, index] = key.match(/^([a-z]+)(\d+)$/);
190 if (!bus) {
191 return undefined;
192 }
193 let res = {
194 'interface': bus,
195 index,
196 };
197
198 var errors = false;
199 Ext.Array.each(value.split(','), function(p) {
200 if (!p || p.match(/^\s*$/)) {
201 return undefined; // continue
202 }
203 let match = p.match(/^([a-z_]+)=(\S+)$/);
204 if (!match) {
205 if (!p.match(/[=]/)) {
206 res.file = p;
207 return undefined; // continue
208 }
209 errors = true;
210 return false; // break
211 }
212 let [, k, v] = match;
213 if (k === 'volume') {
214 k = 'file';
215 }
216
217 if (Ext.isDefined(res[k])) {
218 errors = true;
219 return false; // break
220 }
221
222 if (k === 'cache' && v === 'off') {
223 v = 'none';
224 }
225
226 res[k] = v;
227
228 return undefined; // continue
229 });
230
231 if (errors || !res.file) {
232 return undefined;
233 }
234
235 return res;
236 },
237
238 printQemuDrive: function(drive) {
239 var drivestr = drive.file;
240
241 Ext.Object.each(drive, function(key, value) {
242 if (!Ext.isDefined(value) || key === 'file' ||
243 key === 'index' || key === 'interface') {
244 return; // continue
245 }
246 drivestr += ',' + key + '=' + value;
247 });
248
249 return drivestr;
250 },
251
252 parseIPConfig: function(key, value) {
253 if (!(key && value)) {
254 return undefined; // continue
255 }
256
257 let res = {};
258 try {
259 value.split(',').forEach(p => {
260 if (!p || p.match(/^\s*$/)) {
261 return; // continue
262 }
263
264 const match = p.match(/^(ip|gw|ip6|gw6)=(\S+)$/);
265 if (!match) {
266 throw `could not parse as IP config: ${p}`;
267 }
268 let [, k, v] = match;
269 res[k] = v;
270 });
271 } catch (err) {
272 console.warn(err);
273 return undefined; // continue
274 }
275
276 return res;
277 },
278
279 printIPConfig: function(cfg) {
280 return Object.entries(cfg)
281 .filter(([k, v]) => v && k.match(/^(ip|gw|ip6|gw6)$/))
282 .map(([k, v]) => `${k}=${v}`)
283 .join(',');
284 },
285
286 parseLxcNetwork: function(value) {
287 if (!value) {
288 return undefined;
289 }
290
291 let data = {};
292 value.split(',').forEach(p => {
293 if (!p || p.match(/^\s*$/)) {
294 return; // continue
295 }
296 let match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|tag|rate)=(\S+)$/);
297 if (match_res) {
298 data[match_res[1]] = match_res[2];
299 } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
300 data.firewall = PVE.Parser.parseBoolean(match_res[1]);
301 } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
302 data.link_down = PVE.Parser.parseBoolean(match_res[1]);
303 } else if (!p.match(/^type=\S+$/)) {
304 console.warn(`could not parse LXC network string ${p}`);
305 }
306 });
307
308 return data;
309 },
310
311 printLxcNetwork: function(config) {
312 let knownKeys = {
313 bridge: 1,
314 firewall: 1,
315 gw6: 1,
316 gw: 1,
317 hwaddr: 1,
318 ip6: 1,
319 ip: 1,
320 mtu: 1,
321 name: 1,
322 rate: 1,
323 tag: 1,
324 link_down: 1,
325 };
326 return Object.entries(config)
327 .filter(([k, v]) => v !== undefined && v !== '' && knownKeys[k])
328 .map(([k, v]) => `${k}=${v}`)
329 .join(',');
330 },
331
332 parseLxcMountPoint: function(value) {
333 if (!value) {
334 return undefined;
335 }
336
337 let res = {};
338 let errors = false;
339 Ext.Array.each(value.split(','), function(p) {
340 if (!p || p.match(/^\s*$/)) {
341 return undefined; // continue
342 }
343 let match = p.match(/^([a-z_]+)=(.+)$/);
344 if (!match) {
345 if (!p.match(/[=]/)) {
346 res.file = p;
347 return undefined; // continue
348 }
349 errors = true;
350 return false; // break
351 }
352 let [, k, v] = match;
353 if (k === 'volume') {
354 k = 'file';
355 }
356
357 if (Ext.isDefined(res[k])) {
358 errors = true;
359 return false; // break
360 }
361
362 res[k] = v;
363
364 return undefined;
365 });
366
367 if (errors || !res.file) {
368 return undefined;
369 }
370
371 const match = res.file.match(/^([a-z][a-z0-9\-_.]*[a-z0-9]):/i);
372 if (match) {
373 res.storage = match[1];
374 res.type = 'volume';
375 } else if (res.file.match(/^\/dev\//)) {
376 res.type = 'device';
377 } else {
378 res.type = 'bind';
379 }
380
381 return res;
382 },
383
384 printLxcMountPoint: function(mp) {
385 let drivestr = mp.file;
386 for (const [key, value] of Object.entries(mp)) {
387 if (!Ext.isDefined(value) || key === 'file' || key === 'type' || key === 'storage') {
388 continue;
389 }
390 drivestr += `,${key}=${value}`;
391 }
392 return drivestr;
393 },
394
395 parseStartup: function(value) {
396 if (value === undefined) {
397 return undefined;
398 }
399
400 let res = {};
401 try {
402 value.split(',').forEach(p => {
403 if (!p || p.match(/^\s*$/)) {
404 return; // continue
405 }
406
407 let match_res;
408 if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) {
409 res.order = match_res[2];
410 } else if ((match_res = p.match(/^up=(\d+)$/)) !== null) {
411 res.up = match_res[1];
412 } else if ((match_res = p.match(/^down=(\d+)$/)) !== null) {
413 res.down = match_res[1];
414 } else {
415 throw `could not parse startup config ${p}`;
416 }
417 });
418 } catch (err) {
419 console.warn(err);
420 return undefined;
421 }
422
423 return res;
424 },
425
426 printStartup: function(startup) {
427 let arr = [];
428 if (startup.order !== undefined && startup.order !== '') {
429 arr.push('order=' + startup.order);
430 }
431 if (startup.up !== undefined && startup.up !== '') {
432 arr.push('up=' + startup.up);
433 }
434 if (startup.down !== undefined && startup.down !== '') {
435 arr.push('down=' + startup.down);
436 }
437
438 return arr.join(',');
439 },
440
441 parseQemuSmbios1: function(value) {
442 let res = value.split(',').reduce((acc, currentValue) => {
443 const [k, v] = currentValue.split(/[=](.+)/);
444 acc[k] = v;
445 return acc;
446 }, {});
447
448 if (PVE.Parser.parseBoolean(res.base64, false)) {
449 for (const [k, v] of Object.entries(res)) {
450 if (k !== 'uuid') {
451 res[k] = Ext.util.Base64.decode(v);
452 }
453 }
454 }
455
456 return res;
457 },
458
459 printQemuSmbios1: function(data) {
460 let base64 = false;
461 let datastr = Object.entries(data)
462 .map(([key, value]) => {
463 if (value === '') {
464 return undefined;
465 }
466 if (key !== 'uuid') {
467 base64 = true; // smbios values can be arbitrary, so encode and mark config as such
468 value = Ext.util.Base64.encode(value);
469 }
470 return `${key}=${value}`;
471 })
472 .filter(v => v !== undefined)
473 .join(',');
474
475 if (base64) {
476 datastr += ',base64=1';
477 }
478 return datastr;
479 },
480
481 parseTfaConfig: function(value) {
482 let res = {};
483 value.split(',').forEach(p => {
484 const [k, v] = p.split('=', 2);
485 res[k] = v;
486 });
487
488 return res;
489 },
490
491 parseTfaType: function(value) {
492 let match;
493 if (!value || !value.length) {
494 return undefined;
495 } else if (value === 'x!oath') {
496 return 'totp';
497 } else if ((match = value.match(/^x!(.+)$/)) !== null) {
498 return match[1];
499 } else {
500 return 1;
501 }
502 },
503
504 parseQemuCpu: function(value) {
505 if (!value) {
506 return {};
507 }
508
509 let res = {};
510 let errors = false;
511 Ext.Array.each(value.split(','), function(p) {
512 if (!p || p.match(/^\s*$/)) {
513 return undefined; // continue
514 }
515
516 if (!p.match(/[=]/)) {
517 if (Ext.isDefined(res.cpu)) {
518 errors = true;
519 return false; // break
520 }
521 res.cputype = p;
522 return undefined; // continue
523 }
524
525 let match = p.match(/^([a-z_]+)=(\S+)$/);
526 if (!match || Ext.isDefined(res[match[1]])) {
527 errors = true;
528 return false; // break
529 }
530
531 let [, k, v] = match;
532 res[k] = v;
533
534 return undefined;
535 });
536
537 if (errors || !res.cputype) {
538 return undefined;
539 }
540
541 return res;
542 },
543
544 printQemuCpu: function(cpu) {
545 let cpustr = cpu.cputype;
546 let optstr = '';
547
548 Ext.Object.each(cpu, function(key, value) {
549 if (!Ext.isDefined(value) || key === 'cputype') {
550 return; // continue
551 }
552 optstr += ',' + key + '=' + value;
553 });
554
555 if (!cpustr) {
556 if (optstr) {
557 return 'kvm64' + optstr;
558 } else {
559 return undefined;
560 }
561 }
562
563 return cpustr + optstr;
564 },
565
566 parseSSHKey: function(key) {
567 // |--- options can have quotes--| type key comment
568 let keyre = /^(?:((?:[^\s"]|"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/;
569 let typere = /^(?:(?:sk-)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)(?:@(?:[a-z0-9_-]+\.)+[a-z]{2,})?)$/;
570
571 let m = key.match(keyre);
572 if (!m || m.length < 3 || !m[2]) { // [2] is always either type or key
573 return null;
574 }
575 if (m[1] && m[1].match(typere)) {
576 return {
577 type: m[1],
578 key: m[2],
579 comment: m[3],
580 };
581 }
582 if (m[2].match(typere)) {
583 return {
584 options: m[1],
585 type: m[2],
586 key: m[3],
587 comment: m[4],
588 };
589 }
590 return null;
591 },
592
593 parseACMEPluginData: function(data) {
594 let res = {};
595 let extradata = [];
596 data.split('\n').forEach((line) => {
597 // capture everything after the first = as value
598 let [key, value] = line.split(/[=](.+)/);
599 if (value !== undefined) {
600 res[key] = value;
601 } else {
602 extradata.push(line);
603 }
604 });
605 return [res, extradata];
606 },
607 },
608 });