]> git.proxmox.com Git - pve-manager.git/blobdiff - www/manager6/Parser.js
bump version to 8.2.7
[pve-manager.git] / www / manager6 / Parser.js
index df2be5dc9b1499fa74d3407d008923e3762e01f9..07eb9b1767e126f5a15b2ad0601bafade303812a 100644 (file)
@@ -1,34 +1,38 @@
-// Some configuration values are complex strings -
-// so we need parsers/generators for them.
-
-Ext.define('PVE.Parser', { statics: {
+// Some configuration values are complex strings - so we need parsers/generators for them.
+Ext.define('PVE.Parser', {
+ statics: {
 
     // this class only contains static functions
 
+    printACME: function(value) {
+       if (Ext.isArray(value.domains)) {
+           value.domains = value.domains.join(';');
+       }
+       return PVE.Parser.printPropertyString(value);
+    },
+
     parseACME: function(value) {
        if (!value) {
-           return;
+           return {};
        }
 
-       var res = {};
-       var errors = false;
-
-       Ext.Array.each(value.split(','), function(p) {
-           if (!p || p.match(/^\s*$/)) {
-               return; //continue
-           }
-
-           var match_res;
-           if ((match_res = p.match(/^(?:domains=)?((?:[a-zA-Z0-9\-\.]+[;, ]?)+)$/)) !== null) {
-               res.domains = match_res[1].split(/[;, ]/);
-           } else {
-               errors = true;
-               return false;
-           }
-       });
+       let res = {};
+       try {
+           value.split(',').forEach(property => {
+               let [k, v] = property.split('=', 2);
+               if (Ext.isDefined(v)) {
+                   res[k] = v;
+               } else {
+                   throw `Failed to parse key-value pair: ${property}`;
+               }
+           });
+       } catch (err) {
+           console.warn(err);
+           return undefined;
+       }
 
-       if (errors || !res) {
-           return;
+       if (res.domains !== undefined) {
+           res.domains = res.domains.split(/;/);
        }
 
        return res;
@@ -46,71 +50,78 @@ Ext.define('PVE.Parser', { statics: {
     },
 
     parsePropertyString: function(value, defaultKey) {
-       var res = {},
-       errors = false;
+       let res = {};
 
-       Ext.Array.each(value.split(','), function(p) {
-           var kv = p.split('=', 2);
-           if (Ext.isDefined(kv[1])) {
-               res[kv[0]] = kv[1];
-           } else if (Ext.isDefined(defaultKey)) {
-               if (Ext.isDefined(res[defaultKey])) {
-                   errors = true;
-                   return false; //break
-               }
-               res[defaultKey] = kv[0];
-           } else {
-               errors = true;
-               return false; // break
-           }
-       });
+       if (typeof value !== 'string' || value === '') {
+           return res;
+       }
 
-       if (errors) {
-           return;
+       try {
+           value.split(',').forEach(property => {
+               let [k, v] = property.split('=', 2);
+               if (Ext.isDefined(v)) {
+                   res[k] = v;
+               } else if (Ext.isDefined(defaultKey)) {
+                   if (Ext.isDefined(res[defaultKey])) {
+                       throw 'defaultKey may be only defined once in propertyString';
+                   }
+                   res[defaultKey] = k; // k is the value in this case
+               } else {
+                   throw `Failed to parse key-value pair: ${property}`;
+               }
+           });
+       } catch (err) {
+           console.warn(err);
+           return undefined;
        }
 
        return res;
     },
 
     printPropertyString: function(data, defaultKey) {
-       var stringparts = [];
+       var stringparts = [],
+           gotDefaultKeyVal = false,
+           defaultKeyVal;
 
        Ext.Object.each(data, function(key, value) {
-           var keystring = '' ;
-           if (key === defaultKey) {
-               keystring = '';
-           } else {
-               keystring = key + '=';
+           if (defaultKey !== undefined && key === defaultKey) {
+               gotDefaultKeyVal = true;
+               defaultKeyVal = value;
+           } else if (value !== '') {
+               stringparts.push(key + '=' + value);
            }
-           stringparts.push(keystring+value);
        });
 
+       stringparts = stringparts.sort();
+       if (gotDefaultKeyVal) {
+           stringparts.unshift(defaultKeyVal);
+       }
+
        return stringparts.join(',');
     },
 
     parseQemuNetwork: function(key, value) {
        if (!(key && value)) {
-           return;
+           return undefined;
        }
 
-       var res = {};
-
-       var errors = false;
+       let res = {},
+           errors = false;
        Ext.Array.each(value.split(','), function(p) {
            if (!p || p.match(/^\s*$/)) {
-               return; // continue
+               return undefined; // continue
            }
 
-           var match_res;
+           let match_res;
 
-           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) {
+           if ((match_res = p.match(/^(ne2k_pci|e1000e?|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) {
                res.model = match_res[1].toLowerCase();
                if (match_res[3]) {
                    res.macaddr = match_res[3];
                }
            } else if ((match_res = p.match(/^bridge=(\S+)$/)) !== null) {
                res.bridge = match_res[1];
-           } else if ((match_res = p.match(/^rate=(\d+(\.\d+)?)$/)) !== null) {
+           } else if ((match_res = p.match(/^rate=(\d+(\.\d+)?|\.\d+)$/)) !== null) {
                res.rate = match_res[1];
            } else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) {
                res.tag = match_res[1];
@@ -122,21 +133,23 @@ Ext.define('PVE.Parser', { statics: {
                res.queues = match_res[1];
            } else if ((match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null) {
                res.trunks = match_res[1];
+           } else if ((match_res = p.match(/^mtu=(\d+)$/)) !== null) {
+               res.mtu = match_res[1];
            } else {
                errors = true;
                return false; // break
            }
+           return undefined; // continue
        });
 
        if (errors || !res.model) {
-           return;
+           return undefined;
        }
 
        return res;
     },
 
     printQemuNetwork: function(net) {
-
        var netstr = net.model;
        if (net.macaddr) {
            netstr += "=" + net.macaddr;
@@ -162,38 +175,41 @@ Ext.define('PVE.Parser', { statics: {
        if (net.trunks) {
            netstr += ",trunks=" + net.trunks;
        }
+       if (net.mtu) {
+           netstr += ",mtu=" + net.mtu;
+       }
        return netstr;
     },
 
     parseQemuDrive: function(key, value) {
        if (!(key && value)) {
-           return;
+           return undefined;
        }
 
-       var res = {};
-
-       var match_res = key.match(/^([a-z]+)(\d+)$/);
-       if (!match_res) {
-           return;
+       const [, bus, index] = key.match(/^([a-z]+)(\d+)$/);
+       if (!bus) {
+           return undefined;
        }
-       res['interface'] = match_res[1];
-       res.index = match_res[2];
+       let res = {
+           'interface': bus,
+           index,
+       };
 
        var errors = false;
        Ext.Array.each(value.split(','), function(p) {
            if (!p || p.match(/^\s*$/)) {
-               return; // continue
+               return undefined; // continue
            }
-           var match_res = p.match(/^([a-z_]+)=(\S+)$/);
-           if (!match_res) {
-               if (!p.match(/\=/)) {
+           let match = p.match(/^([a-z_]+)=(\S+)$/);
+           if (!match) {
+               if (!p.match(/[=]/)) {
                    res.file = p;
-                   return; // continue
+                   return undefined; // continue
                }
                errors = true;
                return false; // break
            }
-           var k = match_res[1];
+           let [, k, v] = match;
            if (k === 'volume') {
                k = 'file';
            }
@@ -203,24 +219,23 @@ Ext.define('PVE.Parser', { statics: {
                return false; // break
            }
 
-           var v = match_res[2];
-
            if (k === 'cache' && v === 'off') {
                v = 'none';
            }
 
            res[k] = v;
+
+           return undefined; // continue
        });
 
        if (errors || !res.file) {
-           return;
+           return undefined;
        }
 
        return res;
     },
 
     printQemuDrive: function(drive) {
-
        var drivestr = drive.file;
 
        Ext.Object.each(drive, function(key, value) {
@@ -236,201 +251,105 @@ Ext.define('PVE.Parser', { statics: {
 
     parseIPConfig: function(key, value) {
        if (!(key && value)) {
-           return;
+           return undefined; // continue
        }
 
-       var res = {};
-
-       var errors = false;
-       Ext.Array.each(value.split(','), function(p) {
-           if (!p || p.match(/^\s*$/)) {
-               return; // continue
-           }
-
-           var match_res;
-           if ((match_res = p.match(/^ip=(\S+)$/)) !== null) {
-               res.ip = match_res[1];
-           } else if ((match_res = p.match(/^gw=(\S+)$/)) !== null) {
-               res.gw = match_res[1];
-           } else if ((match_res = p.match(/^ip6=(\S+)$/)) !== null) {
-               res.ip6 = match_res[1];
-           } else if ((match_res = p.match(/^gw6=(\S+)$/)) !== null) {
-               res.gw6 = match_res[1];
-           } else {
-               errors = true;
-               return false; // break
-           }
-       });
-
-       if (errors) {
-           return;
-       }
-
-       return res;
-    },
-
-    printIPConfig: function(cfg) {
-       var c = "";
-       var str = "";
-       if (cfg.ip) {
-           str += "ip=" + cfg.ip;
-           c = ",";
-       }
-       if (cfg.gw) {
-           str += c + "gw=" + cfg.gw;
-           c = ",";
-       }
-       if (cfg.ip6) {
-           str += c + "ip6=" + cfg.ip6;
-           c = ",";
-       }
-       if (cfg.gw6) {
-           str += c + "gw6=" + cfg.gw6;
-           c = ",";
-       }
-       return str;
-    },
-
-    parseOpenVZNetIf: function(value) {
-       if (!value) {
-           return;
-       }
-
-       var res = {};
-
-       var errors = false;
-       Ext.Array.each(value.split(';'), function(item) {
-           if (!item || item.match(/^\s*$/)) {
-               return; // continue
-           }
-
-           var data = {};
-           Ext.Array.each(item.split(','), function(p) {
+       let res = {};
+       try {
+           value.split(',').forEach(p => {
                if (!p || p.match(/^\s*$/)) {
                    return; // continue
                }
-               var match_res = p.match(/^(ifname|mac|bridge|host_ifname|host_mac|mac_filter)=(\S+)$/);
-               if (!match_res) {
-                   errors = true;
-                   return false; // break
-               }
-               if (match_res[1] === 'bridge'){
-                   var bridgevlanf = match_res[2];
-                   var bridge_res = bridgevlanf.match(/^(vmbr(\d+))(v(\d+))?(f)?$/);
-                   if (!bridge_res) {
-                       errors = true;
-                       return false; // break
-                   }
-                   data.bridge = bridge_res[1];
-                   data.tag = bridge_res[4];
-                   /*jslint confusion: true*/
-                   data.firewall = bridge_res[5] ? 1 : 0;
-                   /*jslint confusion: false*/
-               } else {
-                   data[match_res[1]] = match_res[2];
+
+               const match = p.match(/^(ip|gw|ip6|gw6)=(\S+)$/);
+               if (!match) {
+                   throw `could not parse as IP config: ${p}`;
                }
+               let [, k, v] = match;
+               res[k] = v;
            });
+       } catch (err) {
+           console.warn(err);
+           return undefined; // continue
+       }
 
-           if (errors || !data.ifname) {
-               errors = true;
-               return false; // break
-           }
-
-           data.raw = item;
-
-           res[data.ifname] = data;
-       });
-
-       return errors ? undefined: res;
+       return res;
     },
 
-    printOpenVZNetIf: function(netif) {
-       var netarray = [];
-
-       Ext.Object.each(netif, function(iface, data) {
-           var tmparray = [];
-           Ext.Array.each(['ifname', 'mac', 'bridge', 'host_ifname' , 'host_mac', 'mac_filter', 'tag', 'firewall'], function(key) {
-               var value = data[key];
-               if (key === 'bridge'){
-                   if(data.tag){
-                       value = value + 'v' + data.tag;
-                   }
-                   if (data.firewall){
-                       value = value + 'f';
-                   }
-               }
-               if (value) {
-                   tmparray.push(key + '=' + value);
-               }
-
-           });
-           netarray.push(tmparray.join(','));
-       });
-
-       return netarray.join(';');
+    printIPConfig: function(cfg) {
+       return Object.entries(cfg)
+           .filter(([k, v]) => v && k.match(/^(ip|gw|ip6|gw6)$/))
+           .map(([k, v]) => `${k}=${v}`)
+           .join(',');
     },
 
     parseLxcNetwork: function(value) {
        if (!value) {
-           return;
+           return undefined;
        }
 
-       var data = {};
-       Ext.Array.each(value.split(','), function(p) {
+       let data = {};
+       value.split(',').forEach(p => {
            if (!p || p.match(/^\s*$/)) {
                return; // continue
            }
-           var match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|firewall|tag|rate)=(\S+)$/);
-           if (!match_res) {
-               // todo: simply ignore errors ?
-               return; // continue
+           let match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|tag|rate)=(\S+)$/);
+           if (match_res) {
+               data[match_res[1]] = match_res[2];
+           } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
+               data.firewall = PVE.Parser.parseBoolean(match_res[1]);
+           } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
+               data.link_down = PVE.Parser.parseBoolean(match_res[1]);
+           } else if (!p.match(/^type=\S+$/)) {
+               console.warn(`could not parse LXC network string ${p}`);
            }
-           data[match_res[1]] = match_res[2];
        });
 
        return data;
     },
 
-    printLxcNetwork: function(data) {
-       var tmparray = [];
-       Ext.Array.each(['bridge', 'hwaddr', 'mtu', 'name', 'ip',
-                       'gw', 'ip6', 'gw6', 'firewall', 'tag'], function(key) {
-               var value = data[key];
-               if (value) {
-                   tmparray.push(key + '=' + value);
-               }
-       });
-
-       /*jslint confusion: true*/
-       if (data.rate > 0) {
-           tmparray.push('rate=' + data.rate);
-       }
-       /*jslint confusion: false*/
-       return tmparray.join(',');
+    printLxcNetwork: function(config) {
+       let knownKeys = {
+           bridge: 1,
+           firewall: 1,
+           gw6: 1,
+           gw: 1,
+           hwaddr: 1,
+           ip6: 1,
+           ip: 1,
+           mtu: 1,
+           name: 1,
+           rate: 1,
+           tag: 1,
+           link_down: 1,
+       };
+       return Object.entries(config)
+           .filter(([k, v]) => v !== undefined && v !== '' && knownKeys[k])
+           .map(([k, v]) => `${k}=${v}`)
+           .join(',');
     },
 
     parseLxcMountPoint: function(value) {
        if (!value) {
-           return;
+           return undefined;
        }
 
-       var res = {};
-
-       var errors = false;
+       let res = {};
+       let errors = false;
        Ext.Array.each(value.split(','), function(p) {
            if (!p || p.match(/^\s*$/)) {
-               return; // continue
+               return undefined; // continue
            }
-           var match_res = p.match(/^([a-z_]+)=(\S+)$/);
-           if (!match_res) {
-               if (!p.match(/\=/)) {
+           let match = p.match(/^([a-z_]+)=(.+)$/);
+           if (!match) {
+               if (!p.match(/[=]/)) {
                    res.file = p;
-                   return; // continue
+                   return undefined; // continue
                }
                errors = true;
                return false; // break
            }
-           var k = match_res[1];
+           let [, k, v] = match;
            if (k === 'volume') {
                k = 'file';
            }
@@ -440,18 +359,18 @@ Ext.define('PVE.Parser', { statics: {
                return false; // break
            }
 
-           var v = match_res[2];
-
            res[k] = v;
+
+           return undefined;
        });
 
        if (errors || !res.file) {
-           return;
+           return undefined;
        }
 
-       var m = res.file.match(/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):/i);
-       if (m) {
-           res.storage = m[1];
+       const match = res.file.match(/^([a-z][a-z0-9\-_.]*[a-z0-9]):/i);
+       if (match) {
+           res.storage = match[1];
            res.type = 'volume';
        } else if (res.file.match(/^\/dev\//)) {
            res.type = 'device';
@@ -463,55 +382,49 @@ Ext.define('PVE.Parser', { statics: {
     },
 
     printLxcMountPoint: function(mp) {
-       var drivestr = mp.file;
-
-       Ext.Object.each(mp, function(key, value) {
-           if (!Ext.isDefined(value) || key === 'file' ||
-               key === 'type' || key === 'storage') {
-               return; // continue
+       let drivestr = mp.file;
+       for (const [key, value] of Object.entries(mp)) {
+           if (!Ext.isDefined(value) || key === 'file' || key === 'type' || key === 'storage') {
+               continue;
            }
-           drivestr += ',' + key + '=' + value;
-       });
-
+           drivestr += `,${key}=${value}`;
+       }
        return drivestr;
     },
 
     parseStartup: function(value) {
        if (value === undefined) {
-           return;
+           return undefined;
        }
 
-       var res = {};
-
-       var errors = false;
-       Ext.Array.each(value.split(','), function(p) {
-           if (!p || p.match(/^\s*$/)) {
-               return; // continue
-           }
-
-           var match_res;
-
-           if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) {
-               res.order = match_res[2];
-           } else if ((match_res = p.match(/^up=(\d+)$/)) !== null) {
-               res.up = match_res[1];
-           } else if ((match_res = p.match(/^down=(\d+)$/)) !== null) {
-                res.down = match_res[1];
-           } else {
-               errors = true;
-               return false; // break
-           }
-       });
+       let res = {};
+       try {
+           value.split(',').forEach(p => {
+               if (!p || p.match(/^\s*$/)) {
+                   return; // continue
+               }
 
-       if (errors) {
-           return;
+               let match_res;
+               if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) {
+                   res.order = match_res[2];
+               } else if ((match_res = p.match(/^up=(\d+)$/)) !== null) {
+                   res.up = match_res[1];
+               } else if ((match_res = p.match(/^down=(\d+)$/)) !== null) {
+                   res.down = match_res[1];
+               } else {
+                   throw `could not parse startup config ${p}`;
+               }
+           });
+       } catch (err) {
+           console.warn(err);
+           return undefined;
        }
 
        return res;
     },
 
     printStartup: function(startup) {
-       var arr = [];
+       let arr = [];
        if (startup.order !== undefined && startup.order !== '') {
            arr.push('order=' + startup.order);
        }
@@ -526,86 +439,111 @@ Ext.define('PVE.Parser', { statics: {
     },
 
     parseQemuSmbios1: function(value) {
-       var res = {};
-
-       Ext.Array.each(value.split(','), function(p) {
-           var kva = p.split('=', 2);
-           res[kva[0]] = kva[1];
-       });
+       let res = value.split(',').reduce((acc, currentValue) => {
+           const [k, v] = currentValue.split(/[=](.+)/);
+           acc[k] = v;
+           return acc;
+       }, {});
+
+       if (PVE.Parser.parseBoolean(res.base64, false)) {
+           for (const [k, v] of Object.entries(res)) {
+               if (k !== 'uuid') {
+                   res[k] = Ext.util.Base64.decode(v);
+               }
+           }
+       }
 
        return res;
     },
 
     printQemuSmbios1: function(data) {
+       let base64 = false;
+       let datastr = Object.entries(data)
+           .map(([key, value]) => {
+               if (value === '') {
+                   return undefined;
+               }
+               if (key !== 'uuid') {
+                   base64 = true; // smbios values can be arbitrary, so encode and mark config as such
+                   value = Ext.util.Base64.encode(value);
+               }
+               return `${key}=${value}`;
+           })
+           .filter(v => v !== undefined)
+           .join(',');
 
-       var datastr = '';
-
-       Ext.Object.each(data, function(key, value) {
-           if (value === '') { return; }
-           datastr += (datastr !== '' ? ',' : '') + key + '=' + value;
-       });
-
+       if (base64) {
+           datastr += ',base64=1';
+       }
        return datastr;
     },
 
     parseTfaConfig: function(value) {
-       var res = {};
-
-       Ext.Array.each(value.split(','), function(p) {
-           var kva = p.split('=', 2);
-           res[kva[0]] = kva[1];
+       let res = {};
+       value.split(',').forEach(p => {
+           const [k, v] = p.split('=', 2);
+           res[k] = v;
        });
 
        return res;
     },
 
+    parseTfaType: function(value) {
+       let match;
+       if (!value || !value.length) {
+           return undefined;
+       } else if (value === 'x!oath') {
+           return 'totp';
+       } else if ((match = value.match(/^x!(.+)$/)) !== null) {
+           return match[1];
+       } else {
+           return 1;
+       }
+    },
+
     parseQemuCpu: function(value) {
        if (!value) {
            return {};
        }
 
-       var res = {};
-
-       var errors = false;
+       let res = {};
+       let errors = false;
        Ext.Array.each(value.split(','), function(p) {
            if (!p || p.match(/^\s*$/)) {
-               return; // continue
+               return undefined; // continue
            }
 
-           if (!p.match(/\=/)) {
+           if (!p.match(/[=]/)) {
                if (Ext.isDefined(res.cpu)) {
                    errors = true;
                    return false; // break
                }
                res.cputype = p;
-               return; // continue
+               return undefined; // continue
            }
 
-           var match_res = p.match(/^([a-z_]+)=(\S+)$/);
-           if (!match_res) {
+           let match = p.match(/^([a-z_]+)=(\S+)$/);
+           if (!match || Ext.isDefined(res[match[1]])) {
                errors = true;
                return false; // break
            }
 
-           var k = match_res[1];
-           if (Ext.isDefined(res[k])) {
-               errors = true;
-               return false; // break
-           }
+           let [, k, v] = match;
+           res[k] = v;
 
-           res[k] = match_res[2];
+           return undefined;
        });
 
        if (errors || !res.cputype) {
-           return;
+           return undefined;
        }
 
        return res;
     },
 
     printQemuCpu: function(cpu) {
-       var cpustr = cpu.cputype;
-       var optstr = '';
+       let cpustr = cpu.cputype;
+       let optstr = '';
 
        Ext.Object.each(cpu, function(key, value) {
            if (!Ext.isDefined(value) || key === 'cputype') {
@@ -617,8 +555,9 @@ Ext.define('PVE.Parser', { statics: {
        if (!cpustr) {
            if (optstr) {
                return 'kvm64' + optstr;
+           } else {
+               return undefined;
            }
-           return;
        }
 
        return cpustr + optstr;
@@ -626,21 +565,18 @@ Ext.define('PVE.Parser', { statics: {
 
     parseSSHKey: function(key) {
        //                |--- options can have quotes--|     type    key        comment
-       var keyre = /^(?:((?:[^\s"]|\"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/;
-       var typere = /^(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)$/;
+       let keyre = /^(?:((?:[^\s"]|"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/;
+       let typere = /^(?:(?:sk-)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)(?:@(?:[a-z0-9_-]+\.)+[a-z]{2,})?)$/;
 
-       var m = key.match(keyre);
-       if (!m) {
-           return null;
-       }
-       if (m.length < 3 || !m[2]) { // [2] is always either type or key
+       let m = key.match(keyre);
+       if (!m || m.length < 3 || !m[2]) { // [2] is always either type or key
            return null;
        }
        if (m[1] && m[1].match(typere)) {
            return {
                type: m[1],
                key: m[2],
-               comment: m[3]
+               comment: m[3],
            };
        }
        if (m[2].match(typere)) {
@@ -648,9 +584,29 @@ Ext.define('PVE.Parser', { statics: {
                options: m[1],
                type: m[2],
                key: m[3],
-               comment: m[4]
+               comment: m[4],
            };
        }
        return null;
-    }
-}});
+    },
+
+    parseACMEPluginData: function(data) {
+       let res = {};
+       let extradata = [];
+       data.split('\n').forEach((line) => {
+           // capture everything after the first = as value
+           let [key, value] = line.split(/[=](.+)/);
+           if (value !== undefined) {
+               res[key] = value;
+           } else {
+               extradata.push(line);
+           }
+       });
+       return [res, extradata];
+    },
+
+    filterPropertyStringList: function(list, filterFn, defaultKey) {
+       return list.filter((entry) => filterFn(PVE.Parser.parsePropertyString(entry, defaultKey)));
+    },
+},
+});