diff --git a/serverside/jsmod/6.0-4/charts.js b/serverside/jsmod/6.0-4/charts.js
new file mode 100644
index 0000000..713bec3
--- /dev/null
+++ b/serverside/jsmod/6.0-4/charts.js
@@ -0,0 +1,22013 @@
+Ext.define("Ext.draw.ContainerBase", {
+ extend: "Ext.panel.Panel",
+ requires: ["Ext.window.Window"],
+ previewTitleText: "Chart Preview",
+ previewAltText: "Chart preview",
+ layout: "container",
+ addElementListener: function() {
+ var b = this,
+ a = arguments;
+ if (b.rendered) {
+ b.el.on.apply(b.el, a)
+ } else {
+ b.on("render", function() {
+ b.el.on.apply(b.el, a)
+ })
+ }
+ },
+ removeElementListener: function() {
+ var b = this,
+ a = arguments;
+ if (b.rendered) {
+ b.el.un.apply(b.el, a)
+ }
+ },
+ afterRender: function() {
+ this.callParent(arguments);
+ this.initAnimator()
+ },
+ getItems: function() {
+ var b = this,
+ a = b.items;
+ if (!a || !a.isMixedCollection) {
+ b.initItems()
+ }
+ return b.items
+ },
+ onRender: function() {
+ this.callParent(arguments);
+ this.element = this.el;
+ this.innerElement = this.body
+ },
+ setItems: function(a) {
+ this.items = a;
+ return a
+ },
+ setSurfaceSize: function(b, a) {
+ this.resizeHandler({
+ width: b,
+ height: a
+ });
+ this.renderFrame()
+ },
+ onResize: function(c, a, b, e) {
+ var d = this;
+ d.callParent([c, a, b, e]);
+ d.setBodySize({
+ width: c,
+ height: a
+ })
+ },
+ preview: function() {
+ var a = this.getImage();
+ new Ext.window.Window({
+ title: this.previewTitleText,
+ closeable: true,
+ renderTo: Ext.getBody(),
+ autoShow: true,
+ maximizeable: true,
+ maximized: true,
+ border: true,
+ layout: {
+ type: "hbox",
+ pack: "center",
+ align: "middle"
+ },
+ items: {
+ xtype: "container",
+ items: {
+ xtype: "image",
+ mode: "img",
+ cls: Ext.baseCSSPrefix + "chart-image",
+ alt: this.previewAltText,
+ src: a.data,
+ listeners: {
+ afterrender: function() {
+ var e = this,
+ b = e.imgEl.dom,
+ d = a.type === "svg" ? 1 : (window.devicePixelRatio || 1),
+ c;
+ if (!b.naturalWidth || !b.naturalHeight) {
+ b.onload = function() {
+ var g = b.naturalWidth,
+ f = b.naturalHeight;
+ e.setWidth(Math.floor(g / d));
+ e.setHeight(Math.floor(f / d))
+ }
+ } else {
+ c = e.getSize();
+ e.setWidth(Math.floor(c.width / d));
+ e.setHeight(Math.floor(c.height / d))
+ }
+ }
+ }
+ }
+ }
+ })
+ },
+ privates: {
+ getTargetEl: function() {
+ return this.innerElement
+ },
+ reattachToBody: function() {
+ var a = this;
+ if (a.pendingDetachSize) {
+ a.onBodyResize()
+ }
+ a.pendingDetachSize = false;
+ a.callParent()
+ }
+ }
+});
+Ext.define("Ext.draw.SurfaceBase", {
+ extend: "Ext.Widget",
+ getOwnerBody: function() {
+ return this.ownerCt.body
+ },
+ destroy: function() {
+ var a = this;
+ if (a.hasListeners.destroy) {
+ a.fireEvent("destroy", a)
+ }
+ a.callParent()
+ }
+});
+Ext.define("Ext.draw.Color", {
+ statics: {
+ colorToHexRe: /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
+ rgbToHexRe: /\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
+ rgbaToHexRe: /\s*rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\.\d]+)\)/,
+ hexRe: /\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/,
+ NONE: "none",
+ RGBA_NONE: "rgba(0, 0, 0, 0)"
+ },
+ isColor: true,
+ lightnessFactor: 0.2,
+ constructor: function(d, b, a, c) {
+ this.setRGB(d, b, a, c)
+ },
+ setRGB: function(e, c, a, d) {
+ var b = this;
+ b.r = Math.min(255, Math.max(0, e));
+ b.g = Math.min(255, Math.max(0, c));
+ b.b = Math.min(255, Math.max(0, a));
+ if (d === undefined) {
+ b.a = 1
+ } else {
+ b.a = Math.min(1, Math.max(0, d))
+ }
+ },
+ getGrayscale: function() {
+ return this.r * 0.3 + this.g * 0.59 + this.b * 0.11
+ },
+ getHSL: function() {
+ var i = this,
+ a = i.r / 255,
+ f = i.g / 255,
+ j = i.b / 255,
+ k = Math.max(a, f, j),
+ d = Math.min(a, f, j),
+ m = k - d,
+ e, n = 0,
+ c = 0.5 * (k + d);
+ if (d !== k) {
+ n = (c <= 0.5) ? m / (k + d) : m / (2 - k - d);
+ if (a === k) {
+ e = 60 * (f - j) / m
+ } else {
+ if (f === k) {
+ e = 120 + 60 * (j - a) / m
+ } else {
+ e = 240 + 60 * (a - f) / m
+ }
+ }
+ if (e < 0) {
+ e += 360
+ }
+ if (e >= 360) {
+ e -= 360
+ }
+ }
+ return [e, n, c]
+ },
+ getHSV: function() {
+ var i = this,
+ a = i.r / 255,
+ f = i.g / 255,
+ j = i.b / 255,
+ k = Math.max(a, f, j),
+ d = Math.min(a, f, j),
+ c = k - d,
+ e, m = 0,
+ l = k;
+ if (d != k) {
+ m = l ? c / l : 0;
+ if (a === k) {
+ e = 60 * (f - j) / c
+ } else {
+ if (f === k) {
+ e = 60 * (j - a) / c + 120
+ } else {
+ e = 60 * (a - f) / c + 240
+ }
+ }
+ if (e < 0) {
+ e += 360
+ }
+ if (e >= 360) {
+ e -= 360
+ }
+ }
+ return [e, m, l]
+ },
+ setHSL: function(g, f, e) {
+ var i = this,
+ d = Math.abs,
+ j, b, a;
+ g = (g % 360 + 360) % 360;
+ f = f > 1 ? 1 : f < 0 ? 0 : f;
+ e = e > 1 ? 1 : e < 0 ? 0 : e;
+ if (f === 0 || g === null) {
+ e *= 255;
+ i.setRGB(e, e, e)
+ } else {
+ g /= 60;
+ j = f * (1 - d(2 * e - 1));
+ b = j * (1 - d(g % 2 - 1));
+ a = e - j / 2;
+ a *= 255;
+ j *= 255;
+ b *= 255;
+ switch (Math.floor(g)) {
+ case 0:
+ i.setRGB(j + a, b + a, a);
+ break;
+ case 1:
+ i.setRGB(b + a, j + a, a);
+ break;
+ case 2:
+ i.setRGB(a, j + a, b + a);
+ break;
+ case 3:
+ i.setRGB(a, b + a, j + a);
+ break;
+ case 4:
+ i.setRGB(b + a, a, j + a);
+ break;
+ case 5:
+ i.setRGB(j + a, a, b + a);
+ break
+ }
+ }
+ return i
+ },
+ setHSV: function(f, e, d) {
+ var g = this,
+ i, b, a;
+ f = (f % 360 + 360) % 360;
+ e = e > 1 ? 1 : e < 0 ? 0 : e;
+ d = d > 1 ? 1 : d < 0 ? 0 : d;
+ if (e === 0 || f === null) {
+ d *= 255;
+ g.setRGB(d, d, d)
+ } else {
+ f /= 60;
+ i = d * e;
+ b = i * (1 - Math.abs(f % 2 - 1));
+ a = d - i;
+ a *= 255;
+ i *= 255;
+ b *= 255;
+ switch (Math.floor(f)) {
+ case 0:
+ g.setRGB(i + a, b + a, a);
+ break;
+ case 1:
+ g.setRGB(b + a, i + a, a);
+ break;
+ case 2:
+ g.setRGB(a, i + a, b + a);
+ break;
+ case 3:
+ g.setRGB(a, b + a, i + a);
+ break;
+ case 4:
+ g.setRGB(b + a, a, i + a);
+ break;
+ case 5:
+ g.setRGB(i + a, a, b + a);
+ break
+ }
+ }
+ return g
+ },
+ createLighter: function(b) {
+ if (!b && b !== 0) {
+ b = this.lightnessFactor
+ }
+ var a = this.getHSL();
+ a[2] = Ext.Number.constrain(a[2] + b, 0, 1);
+ return Ext.draw.Color.fromHSL(a[0], a[1], a[2])
+ },
+ createDarker: function(a) {
+ if (!a && a !== 0) {
+ a = this.lightnessFactor
+ }
+ return this.createLighter(-a)
+ },
+ toString: function() {
+ var f = this,
+ c = Math.round;
+ if (f.a === 1) {
+ var e = c(f.r).toString(16),
+ d = c(f.g).toString(16),
+ a = c(f.b).toString(16);
+ e = (e.length === 1) ? "0" + e : e;
+ d = (d.length === 1) ? "0" + d : d;
+ a = (a.length === 1) ? "0" + a : a;
+ return ["#", e, d, a].join("")
+ } else {
+ return "rgba(" + [c(f.r), c(f.g), c(f.b), f.a === 0 ? 0 : f.a.toFixed(15)].join(", ") + ")"
+ }
+ },
+ toHex: function(b) {
+ if (Ext.isArray(b)) {
+ b = b[0]
+ }
+ if (!Ext.isString(b)) {
+ return ""
+ }
+ if (b.substr(0, 1) === "#") {
+ return b
+ }
+ var e = Ext.draw.Color.colorToHexRe.exec(b);
+ if (Ext.isArray(e)) {
+ var f = parseInt(e[2], 10),
+ d = parseInt(e[3], 10),
+ a = parseInt(e[4], 10),
+ c = a | (d << 8) | (f << 16);
+ return e[1] + "#" + ("000000" + c.toString(16)).slice(-6)
+ } else {
+ return ""
+ }
+ },
+ setFromString: function(j) {
+ var e, h, f, c, d = 1,
+ i = parseInt;
+ if (j === Ext.draw.Color.NONE) {
+ this.r = this.g = this.b = this.a = 0;
+ return this
+ }
+ if ((j.length === 4 || j.length === 7) && j.substr(0, 1) === "#") {
+ e = j.match(Ext.draw.Color.hexRe);
+ if (e) {
+ h = i(e[1], 16) >> 0;
+ f = i(e[2], 16) >> 0;
+ c = i(e[3], 16) >> 0;
+ if (j.length === 4) {
+ h += (h * 16);
+ f += (f * 16);
+ c += (c * 16)
+ }
+ }
+ } else {
+ if ((e = j.match(Ext.draw.Color.rgbToHexRe))) {
+ h = +e[1];
+ f = +e[2];
+ c = +e[3]
+ } else {
+ if ((e = j.match(Ext.draw.Color.rgbaToHexRe))) {
+ h = +e[1];
+ f = +e[2];
+ c = +e[3];
+ d = +e[4]
+ } else {
+ if (Ext.draw.Color.ColorList.hasOwnProperty(j.toLowerCase())) {
+ return this.setFromString(Ext.draw.Color.ColorList[j.toLowerCase()])
+ }
+ }
+ }
+ }
+ if (typeof h === "undefined") {
+ return this
+ }
+ this.r = h;
+ this.g = f;
+ this.b = c;
+ this.a = d;
+ return this
+ }
+}, function() {
+ var a = new this();
+ this.addStatics({
+ fly: function(f, e, c, d) {
+ switch (arguments.length) {
+ case 1:
+ a.setFromString(f);
+ break;
+ case 3:
+ case 4:
+ a.setRGB(f, e, c, d);
+ break;
+ default:
+ return null
+ }
+ return a
+ },
+ ColorList: {
+ aliceblue: "#f0f8ff",
+ antiquewhite: "#faebd7",
+ aqua: "#00ffff",
+ aquamarine: "#7fffd4",
+ azure: "#f0ffff",
+ beige: "#f5f5dc",
+ bisque: "#ffe4c4",
+ black: "#000000",
+ blanchedalmond: "#ffebcd",
+ blue: "#0000ff",
+ blueviolet: "#8a2be2",
+ brown: "#a52a2a",
+ burlywood: "#deb887",
+ cadetblue: "#5f9ea0",
+ chartreuse: "#7fff00",
+ chocolate: "#d2691e",
+ coral: "#ff7f50",
+ cornflowerblue: "#6495ed",
+ cornsilk: "#fff8dc",
+ crimson: "#dc143c",
+ cyan: "#00ffff",
+ darkblue: "#00008b",
+ darkcyan: "#008b8b",
+ darkgoldenrod: "#b8860b",
+ darkgray: "#a9a9a9",
+ darkgreen: "#006400",
+ darkkhaki: "#bdb76b",
+ darkmagenta: "#8b008b",
+ darkolivegreen: "#556b2f",
+ darkorange: "#ff8c00",
+ darkorchid: "#9932cc",
+ darkred: "#8b0000",
+ darksalmon: "#e9967a",
+ darkseagreen: "#8fbc8f",
+ darkslateblue: "#483d8b",
+ darkslategray: "#2f4f4f",
+ darkturquoise: "#00ced1",
+ darkviolet: "#9400d3",
+ deeppink: "#ff1493",
+ deepskyblue: "#00bfff",
+ dimgray: "#696969",
+ dodgerblue: "#1e90ff",
+ firebrick: "#b22222",
+ floralwhite: "#fffaf0",
+ forestgreen: "#228b22",
+ fuchsia: "#ff00ff",
+ gainsboro: "#dcdcdc",
+ ghostwhite: "#f8f8ff",
+ gold: "#ffd700",
+ goldenrod: "#daa520",
+ gray: "#808080",
+ green: "#008000",
+ greenyellow: "#adff2f",
+ honeydew: "#f0fff0",
+ hotpink: "#ff69b4",
+ indianred: "#cd5c5c",
+ indigo: "#4b0082",
+ ivory: "#fffff0",
+ khaki: "#f0e68c",
+ lavender: "#e6e6fa",
+ lavenderblush: "#fff0f5",
+ lawngreen: "#7cfc00",
+ lemonchiffon: "#fffacd",
+ lightblue: "#add8e6",
+ lightcoral: "#f08080",
+ lightcyan: "#e0ffff",
+ lightgoldenrodyellow: "#fafad2",
+ lightgray: "#d3d3d3",
+ lightgrey: "#d3d3d3",
+ lightgreen: "#90ee90",
+ lightpink: "#ffb6c1",
+ lightsalmon: "#ffa07a",
+ lightseagreen: "#20b2aa",
+ lightskyblue: "#87cefa",
+ lightslategray: "#778899",
+ lightsteelblue: "#b0c4de",
+ lightyellow: "#ffffe0",
+ lime: "#00ff00",
+ limegreen: "#32cd32",
+ linen: "#faf0e6",
+ magenta: "#ff00ff",
+ maroon: "#800000",
+ mediumaquamarine: "#66cdaa",
+ mediumblue: "#0000cd",
+ mediumorchid: "#ba55d3",
+ mediumpurple: "#9370d8",
+ mediumseagreen: "#3cb371",
+ mediumslateblue: "#7b68ee",
+ mediumspringgreen: "#00fa9a",
+ mediumturquoise: "#48d1cc",
+ mediumvioletred: "#c71585",
+ midnightblue: "#191970",
+ mintcream: "#f5fffa",
+ mistyrose: "#ffe4e1",
+ moccasin: "#ffe4b5",
+ navajowhite: "#ffdead",
+ navy: "#000080",
+ oldlace: "#fdf5e6",
+ olive: "#808000",
+ olivedrab: "#6b8e23",
+ orange: "#ffa500",
+ orangered: "#ff4500",
+ orchid: "#da70d6",
+ palegoldenrod: "#eee8aa",
+ palegreen: "#98fb98",
+ paleturquoise: "#afeeee",
+ palevioletred: "#d87093",
+ papayawhip: "#ffefd5",
+ peachpuff: "#ffdab9",
+ peru: "#cd853f",
+ pink: "#ffc0cb",
+ plum: "#dda0dd",
+ powderblue: "#b0e0e6",
+ purple: "#800080",
+ red: "#ff0000",
+ rosybrown: "#bc8f8f",
+ royalblue: "#4169e1",
+ saddlebrown: "#8b4513",
+ salmon: "#fa8072",
+ sandybrown: "#f4a460",
+ seagreen: "#2e8b57",
+ seashell: "#fff5ee",
+ sienna: "#a0522d",
+ silver: "#c0c0c0",
+ skyblue: "#87ceeb",
+ slateblue: "#6a5acd",
+ slategray: "#708090",
+ snow: "#fffafa",
+ springgreen: "#00ff7f",
+ steelblue: "#4682b4",
+ tan: "#d2b48c",
+ teal: "#008080",
+ thistle: "#d8bfd8",
+ tomato: "#ff6347",
+ turquoise: "#40e0d0",
+ violet: "#ee82ee",
+ wheat: "#f5deb3",
+ white: "#ffffff",
+ whitesmoke: "#f5f5f5",
+ yellow: "#ffff00",
+ yellowgreen: "#9acd32"
+ },
+ fromHSL: function(d, c, b) {
+ return (new this(0, 0, 0, 0)).setHSL(d, c, b)
+ },
+ fromHSV: function(d, c, b) {
+ return (new this(0, 0, 0, 0)).setHSL(d, c, b)
+ },
+ fromString: function(b) {
+ return (new this(0, 0, 0, 0)).setFromString(b)
+ },
+ create: function(b) {
+ if (b instanceof this) {
+ return b
+ } else {
+ if (Ext.isArray(b)) {
+ return new Ext.draw.Color(b[0], b[1], b[2], b[3])
+ } else {
+ if (Ext.isString(b)) {
+ return Ext.draw.Color.fromString(b)
+ } else {
+ if (arguments.length > 2) {
+ return new Ext.draw.Color(arguments[0], arguments[1], arguments[2], arguments[3])
+ } else {
+ return new Ext.draw.Color(0, 0, 0, 0)
+ }
+ }
+ }
+ }
+ }
+ })
+});
+Ext.define("Ext.draw.sprite.AnimationParser", function() {
+ function a(d, c, b) {
+ return d + (c - d) * b
+ }
+ return {
+ singleton: true,
+ attributeRe: /^url\(#([a-zA-Z\-]+)\)$/,
+ requires: ["Ext.draw.Color"],
+ color: {
+ parseInitial: function(c, b) {
+ if (Ext.isString(c)) {
+ c = Ext.draw.Color.create(c)
+ }
+ if (Ext.isString(b)) {
+ b = Ext.draw.Color.create(b)
+ }
+ if ((c instanceof Ext.draw.Color) && (b instanceof Ext.draw.Color)) {
+ return [
+ [c.r, c.g, c.b, c.a],
+ [b.r, b.g, b.b, b.a]
+ ]
+ } else {
+ return [c || b, b || c]
+ }
+ },
+ compute: function(d, c, b) {
+ if (!Ext.isArray(d) || !Ext.isArray(c)) {
+ return c || d
+ } else {
+ return [a(d[0], c[0], b), a(d[1], c[1], b), a(d[2], c[2], b), a(d[3], c[3], b)]
+ }
+ },
+ serve: function(c) {
+ var b = Ext.draw.Color.fly(c[0], c[1], c[2], c[3]);
+ return b.toString()
+ }
+ },
+ number: {
+ parse: function(b) {
+ return b === null ? null : +b
+ },
+ compute: function(d, c, b) {
+ if (!Ext.isNumber(d) || !Ext.isNumber(c)) {
+ return c || d
+ } else {
+ return a(d, c, b)
+ }
+ }
+ },
+ angle: {
+ parseInitial: function(c, b) {
+ if (b - c > Math.PI) {
+ b -= Math.PI * 2
+ } else {
+ if (b - c < -Math.PI) {
+ b += Math.PI * 2
+ }
+ }
+ return [c, b]
+ },
+ compute: function(d, c, b) {
+ if (!Ext.isNumber(d) || !Ext.isNumber(c)) {
+ return c || d
+ } else {
+ return a(d, c, b)
+ }
+ }
+ },
+ path: {
+ parseInitial: function(m, n) {
+ var c = m.toStripes(),
+ o = n.toStripes(),
+ e, d, k = c.length,
+ p = o.length,
+ h, f, b, g = o[p - 1],
+ l = [g[g.length - 2], g[g.length - 1]];
+ for (e = k; e < p; e++) {
+ c.push(c[k - 1].slice(0))
+ }
+ for (e = p; e < k; e++) {
+ o.push(l.slice(0))
+ }
+ b = c.length;
+ o.path = n;
+ o.temp = new Ext.draw.Path();
+ for (e = 0; e < b; e++) {
+ h = c[e];
+ f = o[e];
+ k = h.length;
+ p = f.length;
+ o.temp.commands.push("M");
+ for (d = p; d < k; d += 6) {
+ f.push(l[0], l[1], l[0], l[1], l[0], l[1])
+ }
+ g = o[o.length - 1];
+ l = [g[g.length - 2], g[g.length - 1]];
+ for (d = k; d < p; d += 6) {
+ h.push(l[0], l[1], l[0], l[1], l[0], l[1])
+ }
+ for (e = 0; e < f.length; e++) {
+ f[e] -= h[e]
+ }
+ for (e = 2; e < f.length; e += 6) {
+ o.temp.commands.push("C")
+ }
+ }
+ return [c, o]
+ },
+ compute: function(c, l, m) {
+ if (m >= 1) {
+ return l.path
+ }
+ var e = 0,
+ f = c.length,
+ d = 0,
+ b, k, h, n = l.temp.params,
+ g = 0;
+ for (; e < f; e++) {
+ k = c[e];
+ h = l[e];
+ b = k.length;
+ for (d = 0; d < b; d++) {
+ n[g++] = h[d] * m + k[d]
+ }
+ }
+ return l.temp
+ }
+ },
+ data: {
+ compute: function(h, j, k, g) {
+ var m = h.length - 1,
+ b = j.length - 1,
+ e = Math.max(m, b),
+ d, l, c;
+ if (!g || g === h) {
+ g = []
+ }
+ g.length = e + 1;
+ for (c = 0; c <= e; c++) {
+ d = h[Math.min(c, m)];
+ l = j[Math.min(c, b)];
+ if (Ext.isNumber(d)) {
+ if (!Ext.isNumber(l)) {
+ l = 0
+ }
+ g[c] = (l - d) * k + d
+ } else {
+ g[c] = l
+ }
+ }
+ return g
+ }
+ },
+ text: {
+ compute: function(d, c, b) {
+ return d.substr(0, Math.round(d.length * (1 - b))) + c.substr(Math.round(c.length * (1 - b)))
+ }
+ },
+ limited: "number",
+ limited01: "number"
+ }
+});
+(function() {
+ if (!Ext.global.Float32Array) {
+ var a = function(d) {
+ if (typeof d === "number") {
+ this.length = d
+ } else {
+ if ("length" in d) {
+ this.length = d.length;
+ for (var c = 0, b = d.length; c < b; c++) {
+ this[c] = +d[c]
+ }
+ }
+ }
+ };
+ a.prototype = [];
+ Ext.global.Float32Array = a
+ }
+})();
+Ext.define("Ext.draw.Draw", {
+ singleton: true,
+ radian: Math.PI / 180,
+ pi2: Math.PI * 2,
+ reflectFn: function(b) {
+ return b
+ },
+ rad: function(a) {
+ return (a % 360) * this.radian
+ },
+ degrees: function(a) {
+ return (a / this.radian) % 360
+ },
+ isBBoxIntersect: function(b, a, c) {
+ c = c || 0;
+ return (Math.max(b.x, a.x) - c > Math.min(b.x + b.width, a.x + a.width)) || (Math.max(b.y, a.y) - c > Math.min(b.y + b.height, a.y + a.height))
+ },
+ isPointInBBox: function(a, c, b) {
+ return !!b && a >= b.x && a <= (b.x + b.width) && c >= b.y && c <= (b.y + b.height)
+ },
+ spline: function(m) {
+ var e, c, k = m.length,
+ b, h, l, f, a = 0,
+ g = new Float32Array(m.length),
+ n = new Float32Array(m.length * 3 - 2);
+ g[0] = 0;
+ g[k - 1] = 0;
+ for (e = 1; e < k - 1; e++) {
+ g[e] = (m[e + 1] + m[e - 1] - 2 * m[e]) - g[e - 1];
+ a = 1 / (4 - a);
+ g[e] *= a
+ }
+ for (e = k - 2; e > 0; e--) {
+ a = 3.732050807568877 + 48.248711305964385 / (-13.928203230275537 + Math.pow(0.07179676972449123, e));
+ g[e] -= g[e + 1] * a
+ }
+ f = m[0];
+ b = f - g[0];
+ for (e = 0, c = 0; e < k - 1; c += 3) {
+ l = f;
+ h = b;
+ e++;
+ f = m[e];
+ b = f - g[e];
+ n[c] = l;
+ n[c + 1] = (b + 2 * h) / 3;
+ n[c + 2] = (b * 2 + h) / 3
+ }
+ n[c] = f;
+ return n
+ },
+ getAnchors: function(e, d, i, h, t, s, o) {
+ o = o || 4;
+ var n = Math.PI,
+ p = n / 2,
+ k = Math.abs,
+ a = Math.sin,
+ b = Math.cos,
+ f = Math.atan,
+ r, q, g, j, m, l, v, u, c;
+ r = (i - e) / o;
+ q = (t - i) / o;
+ if ((h >= d && h >= s) || (h <= d && h <= s)) {
+ g = j = p
+ } else {
+ g = f((i - e) / k(h - d));
+ if (d < h) {
+ g = n - g
+ }
+ j = f((t - i) / k(h - s));
+ if (s < h) {
+ j = n - j
+ }
+ }
+ c = p - ((g + j) % (n * 2)) / 2;
+ if (c > p) {
+ c -= n
+ }
+ g += c;
+ j += c;
+ m = i - r * a(g);
+ l = h + r * b(g);
+ v = i + q * a(j);
+ u = h + q * b(j);
+ if ((h > d && l < d) || (h < d && l > d)) {
+ m += k(d - l) * (m - i) / (l - h);
+ l = d
+ }
+ if ((h > s && u < s) || (h < s && u > s)) {
+ v -= k(s - u) * (v - i) / (u - h);
+ u = s
+ }
+ return {
+ x1: m,
+ y1: l,
+ x2: v,
+ y2: u
+ }
+ },
+ smooth: function(l, j, o) {
+ var k = l.length,
+ h, g, c, b, q, p, n, m, f = [],
+ e = [],
+ d, a;
+ for (d = 0; d < k - 1; d++) {
+ h = l[d];
+ g = j[d];
+ if (d === 0) {
+ n = h;
+ m = g;
+ f.push(n);
+ e.push(m);
+ if (k === 1) {
+ break
+ }
+ }
+ c = l[d + 1];
+ b = j[d + 1];
+ q = l[d + 2];
+ p = j[d + 2];
+ if (!Ext.isNumber(q + p)) {
+ f.push(n, c, c);
+ e.push(m, b, b);
+ break
+ }
+ a = this.getAnchors(h, g, c, b, q, p, o);
+ f.push(n, a.x1, c);
+ e.push(m, a.y1, b);
+ n = a.x2;
+ m = a.y2
+ }
+ return {
+ smoothX: f,
+ smoothY: e
+ }
+ },
+ beginUpdateIOS: Ext.os.is.iOS ? function() {
+ this.iosUpdateEl = Ext.getBody().createChild({
+ style: "position: absolute; top: 0px; bottom: 0px; left: 0px; right: 0px; background: rgba(0,0,0,0.001); z-index: 100000"
+ })
+ } : Ext.emptyFn,
+ endUpdateIOS: function() {
+ this.iosUpdateEl = Ext.destroy(this.iosUpdateEl)
+ }
+});
+Ext.define("Ext.draw.gradient.Gradient", {
+ requires: ["Ext.draw.Color"],
+ isGradient: true,
+ config: {
+ stops: []
+ },
+ applyStops: function(f) {
+ var e = [],
+ d = f.length,
+ c, b, a;
+ for (c = 0; c < d; c++) {
+ b = f[c];
+ a = b.color;
+ if (!(a && a.isColor)) {
+ a = Ext.draw.Color.fly(a || Ext.draw.Color.NONE)
+ }
+ e.push({
+ offset: Math.min(1, Math.max(0, "offset" in b ? b.offset : b.position || 0)),
+ color: a.toString()
+ })
+ }
+ e.sort(function(h, g) {
+ return h.offset - g.offset
+ });
+ return e
+ },
+ onClassExtended: function(a, b) {
+ if (!b.alias && b.type) {
+ b.alias = "gradient." + b.type
+ }
+ },
+ constructor: function(a) {
+ this.initConfig(a)
+ },
+ generateGradient: Ext.emptyFn
+});
+Ext.define("Ext.draw.gradient.GradientDefinition", {
+ singleton: true,
+ urlStringRe: /^url\(#([\w\-]+)\)$/,
+ gradients: {},
+ add: function(a) {
+ var b = this.gradients,
+ c, e, d;
+ for (c = 0, e = a.length; c < e; c++) {
+ d = a[c];
+ if (Ext.isString(d.id)) {
+ b[d.id] = d
+ }
+ }
+ },
+ get: function(d) {
+ var a = this.gradients,
+ b = d.match(this.urlStringRe),
+ c;
+ if (b && b[1] && (c = a[b[1]])) {
+ return c || d
+ }
+ return d
+ }
+});
+Ext.define("Ext.draw.sprite.AttributeParser", {
+ singleton: true,
+ attributeRe: /^url\(#([a-zA-Z\-]+)\)$/,
+ requires: ["Ext.draw.Color", "Ext.draw.gradient.GradientDefinition"],
+ "default": Ext.identityFn,
+ string: function(a) {
+ return String(a)
+ },
+ number: function(a) {
+ if (Ext.isNumber(+a)) {
+ return a
+ }
+ },
+ angle: function(a) {
+ if (Ext.isNumber(a)) {
+ a %= Math.PI * 2;
+ if (a < -Math.PI) {
+ a += Math.PI * 2
+ } else {
+ if (a >= Math.PI) {
+ a -= Math.PI * 2
+ }
+ }
+ return a
+ }
+ },
+ data: function(a) {
+ if (Ext.isArray(a)) {
+ return a.slice()
+ } else {
+ if (a instanceof Float32Array) {
+ return new Float32Array(a)
+ }
+ }
+ },
+ bool: function(a) {
+ return !!a
+ },
+ color: function(a) {
+ if (a instanceof Ext.draw.Color) {
+ return a.toString()
+ } else {
+ if (a instanceof Ext.draw.gradient.Gradient) {
+ return a
+ } else {
+ if (!a) {
+ return Ext.draw.Color.NONE
+ } else {
+ if (Ext.isString(a)) {
+ if (a.substr(0, 3) === "url") {
+ a = Ext.draw.gradient.GradientDefinition.get(a);
+ if (Ext.isString(a)) {
+ return a
+ }
+ } else {
+ return Ext.draw.Color.fly(a).toString()
+ }
+ }
+ }
+ }
+ }
+ if (a.type === "linear") {
+ return Ext.create("Ext.draw.gradient.Linear", a)
+ } else {
+ if (a.type === "radial") {
+ return Ext.create("Ext.draw.gradient.Radial", a)
+ } else {
+ if (a.type === "pattern") {
+ return Ext.create("Ext.draw.gradient.Pattern", a)
+ } else {
+ return Ext.draw.Color.NONE
+ }
+ }
+ }
+ },
+ limited: function(a, b) {
+ return function(c) {
+ c = +c;
+ return Ext.isNumber(c) ? Math.min(Math.max(c, a), b) : undefined
+ }
+ },
+ limited01: function(a) {
+ a = +a;
+ return Ext.isNumber(a) ? Math.min(Math.max(a, 0), 1) : undefined
+ },
+ enums: function() {
+ var d = {},
+ a = Array.prototype.slice.call(arguments, 0),
+ b, c;
+ for (b = 0, c = a.length; b < c; b++) {
+ d[a[b]] = true
+ }
+ return function(e) {
+ return e in d ? e : undefined
+ }
+ }
+});
+Ext.define("Ext.draw.sprite.AttributeDefinition", {
+ requires: ["Ext.draw.sprite.AttributeParser", "Ext.draw.sprite.AnimationParser"],
+ config: {
+ defaults: {
+ $value: {},
+ lazy: true
+ },
+ aliases: {},
+ animationProcessors: {},
+ processors: {
+ $value: {},
+ lazy: true
+ },
+ dirtyTriggers: {},
+ triggers: {},
+ updaters: {}
+ },
+ inheritableStatics: {
+ processorFactoryRe: /^(\w+)\(([\w\-,]*)\)$/
+ },
+ spriteClass: null,
+ constructor: function(a) {
+ var b = this;
+ b.initConfig(a)
+ },
+ applyDefaults: function(b, a) {
+ a = Ext.apply(a || {}, this.normalize(b));
+ return a
+ },
+ applyAliases: function(b, a) {
+ return Ext.apply(a || {}, b)
+ },
+ applyProcessors: function(e, i) {
+ this.getAnimationProcessors();
+ var j = i || {},
+ h = Ext.draw.sprite.AttributeParser,
+ a = this.self.processorFactoryRe,
+ g = {},
+ d, b, c, f;
+ for (b in e) {
+ f = e[b];
+ if (typeof f === "string") {
+ c = f.match(a);
+ if (c) {
+ f = h[c[1]].apply(h, c[2].split(","))
+ } else {
+ if (h[f]) {
+ g[b] = f;
+ d = true;
+ f = h[f]
+ }
+ }
+ }
+ j[b] = f
+ }
+ if (d) {
+ this.setAnimationProcessors(g)
+ }
+ return j
+ },
+ applyAnimationProcessors: function(c, a) {
+ var e = Ext.draw.sprite.AnimationParser,
+ b, d;
+ if (!a) {
+ a = {}
+ }
+ for (b in c) {
+ d = c[b];
+ if (d === "none") {
+ a[b] = null
+ } else {
+ if (Ext.isString(d) && !(b in a)) {
+ if (d in e) {
+ while (Ext.isString(e[d])) {
+ d = e[d]
+ }
+ a[b] = e[d]
+ }
+ } else {
+ if (Ext.isObject(d)) {
+ a[b] = d
+ }
+ }
+ }
+ }
+ return a
+ },
+ updateDirtyTriggers: function(a) {
+ this.setTriggers(a)
+ },
+ applyTriggers: function(b, c) {
+ if (!c) {
+ c = {}
+ }
+ for (var a in b) {
+ c[a] = b[a].split(",")
+ }
+ return c
+ },
+ applyUpdaters: function(b, a) {
+ return Ext.apply(a || {}, b)
+ },
+ batchedNormalize: function(f, n) {
+ if (!f) {
+ return {}
+ }
+ var j = this.getProcessors(),
+ d = this.getAliases(),
+ a = f.translation || f.translate,
+ o = {},
+ g, h, b, e, p, c, m, l, k;
+ if ("rotation" in f) {
+ p = f.rotation
+ } else {
+ p = ("rotate" in f) ? f.rotate : undefined
+ }
+ if ("scaling" in f) {
+ c = f.scaling
+ } else {
+ c = ("scale" in f) ? f.scale : undefined
+ }
+ if (typeof c !== "undefined") {
+ if (Ext.isNumber(c)) {
+ o.scalingX = c;
+ o.scalingY = c
+ } else {
+ if ("x" in c) {
+ o.scalingX = c.x
+ }
+ if ("y" in c) {
+ o.scalingY = c.y
+ }
+ if ("centerX" in c) {
+ o.scalingCenterX = c.centerX
+ }
+ if ("centerY" in c) {
+ o.scalingCenterY = c.centerY
+ }
+ }
+ }
+ if (typeof p !== "undefined") {
+ if (Ext.isNumber(p)) {
+ p = Ext.draw.Draw.rad(p);
+ o.rotationRads = p
+ } else {
+ if ("rads" in p) {
+ o.rotationRads = p.rads
+ } else {
+ if ("degrees" in p) {
+ if (Ext.isArray(p.degrees)) {
+ o.rotationRads = Ext.Array.map(p.degrees, function(i) {
+ return Ext.draw.Draw.rad(i)
+ })
+ } else {
+ o.rotationRads = Ext.draw.Draw.rad(p.degrees)
+ }
+ }
+ }
+ if ("centerX" in p) {
+ o.rotationCenterX = p.centerX
+ }
+ if ("centerY" in p) {
+ o.rotationCenterY = p.centerY
+ }
+ }
+ }
+ if (typeof a !== "undefined") {
+ if ("x" in a) {
+ o.translationX = a.x
+ }
+ if ("y" in a) {
+ o.translationY = a.y
+ }
+ }
+ if ("matrix" in f) {
+ m = Ext.draw.Matrix.create(f.matrix);
+ k = m.split();
+ o.matrix = m;
+ o.rotationRads = k.rotation;
+ o.rotationCenterX = 0;
+ o.rotationCenterY = 0;
+ o.scalingX = k.scaleX;
+ o.scalingY = k.scaleY;
+ o.scalingCenterX = 0;
+ o.scalingCenterY = 0;
+ o.translationX = k.translateX;
+ o.translationY = k.translateY
+ }
+ for (b in f) {
+ e = f[b];
+ if (typeof e === "undefined") {
+ continue
+ } else {
+ if (Ext.isArray(e)) {
+ if (b in d) {
+ b = d[b]
+ }
+ if (b in j) {
+ o[b] = [];
+ for (g = 0, h = e.length; g < h; g++) {
+ l = j[b].call(this, e[g]);
+ if (typeof l !== "undefined") {
+ o[b][g] = l
+ }
+ }
+ } else {
+ if (n) {
+ o[b] = e
+ }
+ }
+ } else {
+ if (b in d) {
+ b = d[b]
+ }
+ if (b in j) {
+ e = j[b].call(this, e);
+ if (typeof e !== "undefined") {
+ o[b] = e
+ }
+ } else {
+ if (n) {
+ o[b] = e
+ }
+ }
+ }
+ }
+ }
+ return o
+ },
+ normalize: function(i, j) {
+ if (!i) {
+ return {}
+ }
+ var f = this.getProcessors(),
+ d = this.getAliases(),
+ a = i.translation || i.translate,
+ k = {},
+ b, e, l, c, h, g;
+ if ("rotation" in i) {
+ l = i.rotation
+ } else {
+ l = ("rotate" in i) ? i.rotate : undefined
+ }
+ if ("scaling" in i) {
+ c = i.scaling
+ } else {
+ c = ("scale" in i) ? i.scale : undefined
+ }
+ if (a) {
+ if ("x" in a) {
+ k.translationX = a.x
+ }
+ if ("y" in a) {
+ k.translationY = a.y
+ }
+ }
+ if (typeof c !== "undefined") {
+ if (Ext.isNumber(c)) {
+ k.scalingX = c;
+ k.scalingY = c
+ } else {
+ if ("x" in c) {
+ k.scalingX = c.x
+ }
+ if ("y" in c) {
+ k.scalingY = c.y
+ }
+ if ("centerX" in c) {
+ k.scalingCenterX = c.centerX
+ }
+ if ("centerY" in c) {
+ k.scalingCenterY = c.centerY
+ }
+ }
+ }
+ if (typeof l !== "undefined") {
+ if (Ext.isNumber(l)) {
+ l = Ext.draw.Draw.rad(l);
+ k.rotationRads = l
+ } else {
+ if ("rads" in l) {
+ k.rotationRads = l.rads
+ } else {
+ if ("degrees" in l) {
+ k.rotationRads = Ext.draw.Draw.rad(l.degrees)
+ }
+ }
+ if ("centerX" in l) {
+ k.rotationCenterX = l.centerX
+ }
+ if ("centerY" in l) {
+ k.rotationCenterY = l.centerY
+ }
+ }
+ }
+ if ("matrix" in i) {
+ h = Ext.draw.Matrix.create(i.matrix);
+ g = h.split();
+ k.matrix = h;
+ k.rotationRads = g.rotation;
+ k.rotationCenterX = 0;
+ k.rotationCenterY = 0;
+ k.scalingX = g.scaleX;
+ k.scalingY = g.scaleY;
+ k.scalingCenterX = 0;
+ k.scalingCenterY = 0;
+ k.translationX = g.translateX;
+ k.translationY = g.translateY
+ }
+ for (b in i) {
+ e = i[b];
+ if (typeof e === "undefined") {
+ continue
+ }
+ if (b in d) {
+ b = d[b]
+ }
+ if (b in f) {
+ e = f[b].call(this, e);
+ if (typeof e !== "undefined") {
+ k[b] = e
+ }
+ } else {
+ if (j) {
+ k[b] = e
+ }
+ }
+ }
+ return k
+ },
+ setBypassingNormalization: function(a, c, b) {
+ return c.pushDown(a, b)
+ },
+ set: function(a, c, b) {
+ b = this.normalize(b);
+ return this.setBypassingNormalization(a, c, b)
+ }
+});
+Ext.define("Ext.draw.Matrix", {
+ isMatrix: true,
+ statics: {
+ createAffineMatrixFromTwoPair: function(h, t, g, s, k, o, i, j) {
+ var v = g - h,
+ u = s - t,
+ e = i - k,
+ q = j - o,
+ d = 1 / (v * v + u * u),
+ p = v * e + u * q,
+ n = e * u - v * q,
+ m = -p * h - n * t,
+ l = n * h - p * t;
+ return new this(p * d, -n * d, n * d, p * d, m * d + k, l * d + o)
+ },
+ createPanZoomFromTwoPair: function(q, e, p, c, h, s, n, g) {
+ if (arguments.length === 2) {
+ return this.createPanZoomFromTwoPair.apply(this, q.concat(e))
+ }
+ var k = p - q,
+ j = c - e,
+ d = (q + p) * 0.5,
+ b = (e + c) * 0.5,
+ o = n - h,
+ a = g - s,
+ f = (h + n) * 0.5,
+ l = (s + g) * 0.5,
+ m = k * k + j * j,
+ i = o * o + a * a,
+ t = Math.sqrt(i / m);
+ return new this(t, 0, 0, t, f - t * d, l - t * b)
+ },
+ fly: (function() {
+ var a = null,
+ b = function(c) {
+ a.elements = c;
+ return a
+ };
+ return function(c) {
+ if (!a) {
+ a = new Ext.draw.Matrix()
+ }
+ a.elements = c;
+ Ext.draw.Matrix.fly = b;
+ return a
+ }
+ })(),
+ create: function(a) {
+ if (a instanceof this) {
+ return a
+ }
+ return new this(a)
+ }
+ },
+ constructor: function(e, d, a, f, c, b) {
+ if (e && e.length === 6) {
+ this.elements = e.slice()
+ } else {
+ if (e !== undefined) {
+ this.elements = [e, d, a, f, c, b]
+ } else {
+ this.elements = [1, 0, 0, 1, 0, 0]
+ }
+ }
+ },
+ prepend: function(a, l, h, g, m, k) {
+ var b = this.elements,
+ d = b[0],
+ j = b[1],
+ e = b[2],
+ c = b[3],
+ i = b[4],
+ f = b[5];
+ b[0] = a * d + h * j;
+ b[1] = l * d + g * j;
+ b[2] = a * e + h * c;
+ b[3] = l * e + g * c;
+ b[4] = a * i + h * f + m;
+ b[5] = l * i + g * f + k;
+ return this
+ },
+ prependMatrix: function(a) {
+ return this.prepend.apply(this, a.elements)
+ },
+ append: function(a, l, h, g, m, k) {
+ var b = this.elements,
+ d = b[0],
+ j = b[1],
+ e = b[2],
+ c = b[3],
+ i = b[4],
+ f = b[5];
+ b[0] = a * d + l * e;
+ b[1] = a * j + l * c;
+ b[2] = h * d + g * e;
+ b[3] = h * j + g * c;
+ b[4] = m * d + k * e + i;
+ b[5] = m * j + k * c + f;
+ return this
+ },
+ appendMatrix: function(a) {
+ return this.append.apply(this, a.elements)
+ },
+ set: function(f, e, a, g, c, b) {
+ var d = this.elements;
+ d[0] = f;
+ d[1] = e;
+ d[2] = a;
+ d[3] = g;
+ d[4] = c;
+ d[5] = b;
+ return this
+ },
+ inverse: function(i) {
+ var g = this.elements,
+ o = g[0],
+ m = g[1],
+ l = g[2],
+ k = g[3],
+ j = g[4],
+ h = g[5],
+ n = 1 / (o * k - m * l);
+ o *= n;
+ m *= n;
+ l *= n;
+ k *= n;
+ if (i) {
+ i.set(k, -m, -l, o, l * h - k * j, m * j - o * h);
+ return i
+ } else {
+ return new Ext.draw.Matrix(k, -m, -l, o, l * h - k * j, m * j - o * h)
+ }
+ },
+ translate: function(a, c, b) {
+ if (b) {
+ return this.prepend(1, 0, 0, 1, a, c)
+ } else {
+ return this.append(1, 0, 0, 1, a, c)
+ }
+ },
+ scale: function(f, e, c, a, b) {
+ var d = this;
+ if (e == null) {
+ e = f
+ }
+ if (c === undefined) {
+ c = 0
+ }
+ if (a === undefined) {
+ a = 0
+ }
+ if (b) {
+ return d.prepend(f, 0, 0, e, c - c * f, a - a * e)
+ } else {
+ return d.append(f, 0, 0, e, c - c * f, a - a * e)
+ }
+ },
+ rotate: function(g, e, c, b) {
+ var d = this,
+ f = Math.cos(g),
+ a = Math.sin(g);
+ e = e || 0;
+ c = c || 0;
+ if (b) {
+ return d.prepend(f, a, -a, f, e - f * e + c * a, c - f * c - e * a)
+ } else {
+ return d.append(f, a, -a, f, e - f * e + c * a, c - f * c - e * a)
+ }
+ },
+ rotateFromVector: function(a, h, c) {
+ var e = this,
+ g = Math.sqrt(a * a + h * h),
+ f = a / g,
+ b = h / g;
+ if (c) {
+ return e.prepend(f, b, -b, f, 0, 0)
+ } else {
+ return e.append(f, b, -b, f, 0, 0)
+ }
+ },
+ clone: function() {
+ return new Ext.draw.Matrix(this.elements)
+ },
+ flipX: function() {
+ return this.append(-1, 0, 0, 1, 0, 0)
+ },
+ flipY: function() {
+ return this.append(1, 0, 0, -1, 0, 0)
+ },
+ skewX: function(a) {
+ return this.append(1, 0, Math.tan(a), 1, 0, 0)
+ },
+ skewY: function(a) {
+ return this.append(1, Math.tan(a), 0, 1, 0, 0)
+ },
+ shearX: function(a) {
+ return this.append(1, 0, a, 1, 0, 0)
+ },
+ shearY: function(a) {
+ return this.append(1, a, 0, 1, 0, 0)
+ },
+ reset: function() {
+ return this.set(1, 0, 0, 1, 0, 0)
+ },
+ precisionCompensate: function(j, g) {
+ var c = this.elements,
+ f = c[0],
+ e = c[1],
+ i = c[2],
+ h = c[3],
+ d = c[4],
+ b = c[5],
+ a = e * i - f * h;
+ g.b = j * e / f;
+ g.c = j * i / h;
+ g.d = j;
+ g.xx = f / j;
+ g.yy = h / j;
+ g.dx = (b * f * i - d * f * h) / a / j;
+ g.dy = (d * e * h - b * f * h) / a / j
+ },
+ precisionCompensateRect: function(j, g) {
+ var b = this.elements,
+ f = b[0],
+ e = b[1],
+ i = b[2],
+ h = b[3],
+ c = b[4],
+ a = b[5],
+ d = i / f;
+ g.b = j * e / f;
+ g.c = j * d;
+ g.d = j * h / f;
+ g.xx = f / j;
+ g.yy = f / j;
+ g.dx = (a * i - c * h) / (e * d - h) / j;
+ g.dy = -(a * f - c * e) / (e * d - h) / j
+ },
+ x: function(a, c) {
+ var b = this.elements;
+ return a * b[0] + c * b[2] + b[4]
+ },
+ y: function(a, c) {
+ var b = this.elements;
+ return a * b[1] + c * b[3] + b[5]
+ },
+ get: function(b, a) {
+ return +this.elements[b + a * 2].toFixed(4)
+ },
+ transformPoint: function(b) {
+ var c = this.elements,
+ a, d;
+ if (b.isPoint) {
+ a = b.x;
+ d = b.y
+ } else {
+ a = b[0];
+ d = b[1]
+ }
+ return [a * c[0] + d * c[2] + c[4], a * c[1] + d * c[3] + c[5]]
+ },
+ transformBBox: function(q, i, j) {
+ var b = this.elements,
+ d = q.x,
+ r = q.y,
+ g = q.width * 0.5,
+ o = q.height * 0.5,
+ a = b[0],
+ s = b[1],
+ n = b[2],
+ k = b[3],
+ e = d + g,
+ c = r + o,
+ p, f, m;
+ if (i) {
+ g -= i;
+ o -= i;
+ m = [Math.sqrt(b[0] * b[0] + b[2] * b[2]), Math.sqrt(b[1] * b[1] + b[3] * b[3])];
+ p = Math.abs(g * a) + Math.abs(o * n) + Math.abs(m[0] * i);
+ f = Math.abs(g * s) + Math.abs(o * k) + Math.abs(m[1] * i)
+ } else {
+ p = Math.abs(g * a) + Math.abs(o * n);
+ f = Math.abs(g * s) + Math.abs(o * k)
+ }
+ if (!j) {
+ j = {}
+ }
+ j.x = e * a + c * n + b[4] - p;
+ j.y = e * s + c * k + b[5] - f;
+ j.width = p + p;
+ j.height = f + f;
+ return j
+ },
+ transformList: function(e) {
+ var b = this.elements,
+ a = b[0],
+ h = b[2],
+ l = b[4],
+ k = b[1],
+ g = b[3],
+ j = b[5],
+ f = e.length,
+ c, d;
+ for (d = 0; d < f; d++) {
+ c = e[d];
+ e[d] = [c[0] * a + c[1] * h + l, c[0] * k + c[1] * g + j]
+ }
+ return e
+ },
+ isIdentity: function() {
+ var a = this.elements;
+ return a[0] === 1 && a[1] === 0 && a[2] === 0 && a[3] === 1 && a[4] === 0 && a[5] === 0
+ },
+ isEqual: function(a) {
+ var c = a && a.isMatrix ? a.elements : a,
+ b = this.elements;
+ return b[0] === c[0] && b[1] === c[1] && b[2] === c[2] && b[3] === c[3] && b[4] === c[4] && b[5] === c[5]
+ },
+ equals: function(a) {
+ return this.isEqual(a)
+ },
+ toArray: function() {
+ var a = this.elements;
+ return [a[0], a[2], a[4], a[1], a[3], a[5]]
+ },
+ toVerticalArray: function() {
+ return this.elements.slice()
+ },
+ toString: function() {
+ var a = this;
+ return [a.get(0, 0), a.get(0, 1), a.get(1, 0), a.get(1, 1), a.get(2, 0), a.get(2, 1)].join(",")
+ },
+ toContext: function(a) {
+ a.transform.apply(a, this.elements);
+ return this
+ },
+ toSvg: function() {
+ var a = this.elements;
+ return "matrix(" + a[0].toFixed(9) + "," + a[1].toFixed(9) + "," + a[2].toFixed(9) + "," + a[3].toFixed(9) + "," + a[4].toFixed(9) + "," + a[5].toFixed(9) + ")"
+ },
+ getScaleX: function() {
+ var a = this.elements;
+ return Math.sqrt(a[0] * a[0] + a[2] * a[2])
+ },
+ getScaleY: function() {
+ var a = this.elements;
+ return Math.sqrt(a[1] * a[1] + a[3] * a[3])
+ },
+ getXX: function() {
+ return this.elements[0]
+ },
+ getXY: function() {
+ return this.elements[1]
+ },
+ getYX: function() {
+ return this.elements[2]
+ },
+ getYY: function() {
+ return this.elements[3]
+ },
+ getDX: function() {
+ return this.elements[4]
+ },
+ getDY: function() {
+ return this.elements[5]
+ },
+ split: function() {
+ var b = this.elements,
+ d = b[0],
+ c = b[1],
+ e = b[3],
+ a = {
+ translateX: b[4],
+ translateY: b[5]
+ };
+ a.rotate = a.rotation = Math.atan2(c, d);
+ a.scaleX = d / Math.cos(a.rotate);
+ a.scaleY = e / d * a.scaleX;
+ return a
+ }
+}, function() {
+ function b(e, c, d) {
+ e[c] = {
+ get: function() {
+ return this.elements[d]
+ },
+ set: function(f) {
+ this.elements[d] = f
+ }
+ }
+ }
+ if (Object.defineProperties) {
+ var a = {};
+ b(a, "a", 0);
+ b(a, "b", 1);
+ b(a, "c", 2);
+ b(a, "d", 3);
+ b(a, "e", 4);
+ b(a, "f", 5);
+ Object.defineProperties(this.prototype, a)
+ }
+ this.prototype.multiply = this.prototype.appendMatrix
+});
+Ext.define("Ext.draw.modifier.Modifier", {
+ mixins: {
+ observable: "Ext.mixin.Observable"
+ },
+ config: {
+ previous: null,
+ next: null,
+ sprite: null
+ },
+ constructor: function(a) {
+ this.mixins.observable.constructor.call(this, a)
+ },
+ updateNext: function(a) {
+ if (a) {
+ a.setPrevious(this)
+ }
+ },
+ updatePrevious: function(a) {
+ if (a) {
+ a.setNext(this)
+ }
+ },
+ prepareAttributes: function(a) {
+ if (this._previous) {
+ this._previous.prepareAttributes(a)
+ }
+ },
+ popUp: function(a, b) {
+ if (this._next) {
+ this._next.popUp(a, b)
+ } else {
+ Ext.apply(a, b)
+ }
+ },
+ pushDown: function(a, c) {
+ if (this._previous) {
+ return this._previous.pushDown(a, c)
+ } else {
+ for (var b in c) {
+ if (c[b] === a[b]) {
+ delete c[b]
+ }
+ }
+ return c
+ }
+ }
+});
+Ext.define("Ext.draw.modifier.Target", {
+ requires: ["Ext.draw.Matrix"],
+ extend: "Ext.draw.modifier.Modifier",
+ alias: "modifier.target",
+ statics: {
+ uniqueId: 0
+ },
+ prepareAttributes: function(a) {
+ var b = this.getPrevious();
+ if (b) {
+ b.prepareAttributes(a)
+ }
+ a.attributeId = "attribute-" + Ext.draw.modifier.Target.uniqueId++;
+ if (!a.hasOwnProperty("canvasAttributes")) {
+ a.bbox = {
+ plain: {
+ dirty: true
+ },
+ transform: {
+ dirty: true
+ }
+ };
+ a.dirty = true;
+ a.pendingUpdaters = {};
+ a.canvasAttributes = {};
+ a.matrix = new Ext.draw.Matrix();
+ a.inverseMatrix = new Ext.draw.Matrix()
+ }
+ },
+ applyChanges: function(f, k) {
+ Ext.apply(f, k);
+ var l = this.getSprite(),
+ o = f.pendingUpdaters,
+ h = l.self.def.getTriggers(),
+ p, a, m, b, e, n, d, c, g;
+ for (b in k) {
+ e = true;
+ if ((p = h[b])) {
+ l.scheduleUpdaters(f, p, [b])
+ }
+ if (f.template && k.removeFromInstance && k.removeFromInstance[b]) {
+ delete f[b]
+ }
+ }
+ if (!e) {
+ return
+ }
+ if (o.canvas) {
+ n = o.canvas;
+ delete o.canvas;
+ for (d = 0, g = n.length; d < g; d++) {
+ b = n[d];
+ f.canvasAttributes[b] = f[b]
+ }
+ }
+ if (f.hasOwnProperty("children")) {
+ a = f.children;
+ for (d = 0, g = a.length; d < g; d++) {
+ m = a[d];
+ Ext.apply(m.pendingUpdaters, o);
+ if (n) {
+ for (c = 0; c < n.length; c++) {
+ b = n[c];
+ m.canvasAttributes[b] = m[b]
+ }
+ }
+ l.callUpdaters(m)
+ }
+ }
+ l.setDirty(true);
+ l.callUpdaters(f)
+ },
+ popUp: function(a, b) {
+ this.applyChanges(a, b)
+ },
+ pushDown: function(a, b) {
+ var c = this.getPrevious();
+ if (c) {
+ b = c.pushDown(a, b)
+ }
+ this.applyChanges(a, b);
+ return b
+ }
+});
+Ext.define("Ext.draw.TimingFunctions", function() {
+ var g = Math.pow,
+ j = Math.sin,
+ m = Math.cos,
+ l = Math.sqrt,
+ e = Math.PI,
+ b = ["quad", "cube", "quart", "quint"],
+ c = {
+ pow: function(o, i) {
+ return g(o, i || 6)
+ },
+ expo: function(i) {
+ return g(2, 8 * (i - 1))
+ },
+ circ: function(i) {
+ return 1 - l(1 - i * i)
+ },
+ sine: function(i) {
+ return 1 - j((1 - i) * e / 2)
+ },
+ back: function(i, o) {
+ o = o || 1.616;
+ return i * i * ((o + 1) * i - o)
+ },
+ bounce: function(q) {
+ for (var o = 0, i = 1; 1; o += i, i /= 2) {
+ if (q >= (7 - 4 * o) / 11) {
+ return i * i - g((11 - 6 * o - 11 * q) / 4, 2)
+ }
+ }
+ },
+ elastic: function(o, i) {
+ return g(2, 10 * --o) * m(20 * o * e * (i || 1) / 3)
+ }
+ },
+ k = {},
+ a, f, d;
+
+ function h(i) {
+ return function(o) {
+ return g(o, i)
+ }
+ }
+
+ function n(i, o) {
+ k[i + "In"] = function(p) {
+ return o(p)
+ };
+ k[i + "Out"] = function(p) {
+ return 1 - o(1 - p)
+ };
+ k[i + "InOut"] = function(p) {
+ return (p <= 0.5) ? o(2 * p) / 2 : (2 - o(2 * (1 - p))) / 2
+ }
+ }
+ for (d = 0, f = b.length; d < f; ++d) {
+ c[b[d]] = h(d + 2)
+ }
+ for (a in c) {
+ n(a, c[a])
+ }
+ k.linear = Ext.identityFn;
+ k.easeIn = k.quadIn;
+ k.easeOut = k.quadOut;
+ k.easeInOut = k.quadInOut;
+ return {
+ singleton: true,
+ easingMap: k
+ }
+}, function(a) {
+ Ext.apply(a, a.easingMap)
+});
+Ext.define("Ext.draw.Animator", {
+ uses: ["Ext.draw.Draw"],
+ singleton: true,
+ frameCallbacks: {},
+ frameCallbackId: 0,
+ scheduled: 0,
+ frameStartTimeOffset: Ext.now(),
+ animations: [],
+ running: false,
+ animationTime: function() {
+ return Ext.AnimationQueue.frameStartTime - this.frameStartTimeOffset
+ },
+ add: function(b) {
+ var a = this;
+ if (!a.contains(b)) {
+ a.animations.push(b);
+ a.ignite();
+ if ("fireEvent" in b) {
+ b.fireEvent("animationstart", b)
+ }
+ }
+ },
+ remove: function(d) {
+ var c = this,
+ e = c.animations,
+ b = 0,
+ a = e.length;
+ for (; b < a; ++b) {
+ if (e[b] === d) {
+ e.splice(b, 1);
+ if ("fireEvent" in d) {
+ d.fireEvent("animationend", d)
+ }
+ return
+ }
+ }
+ },
+ contains: function(a) {
+ return Ext.Array.indexOf(this.animations, a) > -1
+ },
+ empty: function() {
+ return this.animations.length === 0
+ },
+ step: function(d) {
+ var c = this,
+ f = c.animations,
+ e, a = 0,
+ b = f.length;
+ for (; a < b; a++) {
+ e = f[a];
+ e.step(d);
+ if (!e.animating) {
+ f.splice(a, 1);
+ a--;
+ b--;
+ if (e.fireEvent) {
+ e.fireEvent("animationend", e)
+ }
+ }
+ }
+ },
+ schedule: function(c, a) {
+ a = a || this;
+ var b = "frameCallback" + (this.frameCallbackId++);
+ if (Ext.isString(c)) {
+ c = a[c]
+ }
+ Ext.draw.Animator.frameCallbacks[b] = {
+ fn: c,
+ scope: a,
+ once: true
+ };
+ this.scheduled++;
+ Ext.draw.Animator.ignite();
+ return b
+ },
+ scheduleIf: function(e, b) {
+ b = b || this;
+ var c = Ext.draw.Animator.frameCallbacks,
+ a, d;
+ if (Ext.isString(e)) {
+ e = b[e]
+ }
+ for (d in c) {
+ a = c[d];
+ if (a.once && a.fn === e && a.scope === b) {
+ return null
+ }
+ }
+ return this.schedule(e, b)
+ },
+ cancel: function(a) {
+ if (Ext.draw.Animator.frameCallbacks[a] && Ext.draw.Animator.frameCallbacks[a].once) {
+ this.scheduled--;
+ delete Ext.draw.Animator.frameCallbacks[a]
+ }
+ },
+ addFrameCallback: function(c, a) {
+ a = a || this;
+ if (Ext.isString(c)) {
+ c = a[c]
+ }
+ var b = "frameCallback" + (this.frameCallbackId++);
+ Ext.draw.Animator.frameCallbacks[b] = {
+ fn: c,
+ scope: a
+ };
+ return b
+ },
+ removeFrameCallback: function(a) {
+ delete Ext.draw.Animator.frameCallbacks[a]
+ },
+ fireFrameCallbacks: function() {
+ var c = this.frameCallbacks,
+ d, b, a;
+ for (d in c) {
+ a = c[d];
+ b = a.fn;
+ if (Ext.isString(b)) {
+ b = a.scope[b]
+ }
+ b.call(a.scope);
+ if (c[d] && a.once) {
+ this.scheduled--;
+ delete c[d]
+ }
+ }
+ },
+ handleFrame: function() {
+ this.step(this.animationTime());
+ this.fireFrameCallbacks();
+ if (!this.scheduled && this.empty()) {
+ Ext.AnimationQueue.stop(this.handleFrame, this);
+ this.running = false;
+ Ext.draw.Draw.endUpdateIOS()
+ }
+ },
+ ignite: function() {
+ if (!this.running) {
+ this.running = true;
+ Ext.AnimationQueue.start(this.handleFrame, this);
+ Ext.draw.Draw.beginUpdateIOS()
+ }
+ }
+});
+Ext.define("Ext.draw.modifier.Animation", {
+ requires: ["Ext.draw.TimingFunctions", "Ext.draw.Animator"],
+ extend: "Ext.draw.modifier.Modifier",
+ alias: "modifier.animation",
+ config: {
+ easing: Ext.identityFn,
+ duration: 0,
+ customEasings: {},
+ customDurations: {},
+ customDuration: null
+ },
+ constructor: function(a) {
+ var b = this;
+ b.anyAnimation = b.anySpecialAnimations = false;
+ b.animating = 0;
+ b.animatingPool = [];
+ b.callParent([a])
+ },
+ prepareAttributes: function(a) {
+ if (!a.hasOwnProperty("timers")) {
+ a.animating = false;
+ a.timers = {};
+ a.animationOriginal = Ext.Object.chain(a);
+ a.animationOriginal.prototype = a
+ }
+ if (this._previous) {
+ this._previous.prepareAttributes(a.animationOriginal)
+ }
+ },
+ updateSprite: function(a) {
+ this.setConfig(a.config.fx)
+ },
+ updateDuration: function(a) {
+ this.anyAnimation = a > 0
+ },
+ applyEasing: function(a) {
+ if (typeof a === "string") {
+ a = Ext.draw.TimingFunctions.easingMap[a]
+ }
+ return a
+ },
+ applyCustomEasings: function(a, e) {
+ e = e || {};
+ var g, d, b, h, c, f;
+ for (d in a) {
+ g = true;
+ h = a[d];
+ b = d.split(",");
+ if (typeof h === "string") {
+ h = Ext.draw.TimingFunctions.easingMap[h]
+ }
+ for (c = 0, f = b.length; c < f; c++) {
+ e[b[c]] = h
+ }
+ }
+ if (g) {
+ this.anySpecialAnimations = g
+ }
+ return e
+ },
+ setEasingOn: function(a, e) {
+ a = Ext.Array.from(a).slice();
+ var c = {},
+ d = a.length,
+ b = 0;
+ for (; b < d; b++) {
+ c[a[b]] = e
+ }
+ this.setCustomEasings(c)
+ },
+ clearEasingOn: function(a) {
+ a = Ext.Array.from(a, true);
+ var b = 0,
+ c = a.length;
+ for (; b < c; b++) {
+ delete this._customEasings[a[b]]
+ }
+ },
+ applyCustomDurations: function(g, h) {
+ h = h || {};
+ var e, c, f, a, b, d;
+ for (c in g) {
+ e = true;
+ f = g[c];
+ a = c.split(",");
+ for (b = 0, d = a.length; b < d; b++) {
+ h[a[b]] = f
+ }
+ }
+ if (e) {
+ this.anySpecialAnimations = e
+ }
+ return h
+ },
+ applyCustomDuration: function(a, b) {
+ if (a) {
+ this.getCustomDurations();
+ this.setCustomDurations(a)
+ }
+ },
+ setDurationOn: function(b, e) {
+ b = Ext.Array.from(b).slice();
+ var a = {},
+ c = 0,
+ d = b.length;
+ for (; c < d; c++) {
+ a[b[c]] = e
+ }
+ this.setCustomDurations(a)
+ },
+ clearDurationOn: function(a) {
+ a = Ext.Array.from(a, true);
+ var b = 0,
+ c = a.length;
+ for (; b < c; b++) {
+ delete this._customDurations[a[b]]
+ }
+ },
+ setAnimating: function(a, b) {
+ var e = this,
+ d = e.animatingPool;
+ if (a.animating !== b) {
+ a.animating = b;
+ if (b) {
+ d.push(a);
+ if (e.animating === 0) {
+ Ext.draw.Animator.add(e)
+ }
+ e.animating++
+ } else {
+ for (var c = d.length; c--;) {
+ if (d[c] === a) {
+ d.splice(c, 1)
+ }
+ }
+ e.animating = d.length
+ }
+ }
+ },
+ setAttrs: function(r, t) {
+ var s = this,
+ m = r.timers,
+ h = s._sprite.self.def._animationProcessors,
+ f = s._easing,
+ e = s._duration,
+ j = s._customDurations,
+ i = s._customEasings,
+ g = s.anySpecialAnimations,
+ n = s.anyAnimation || g,
+ o = r.animationOriginal,
+ d = false,
+ k, u, l, p, c, q, a;
+ if (!n) {
+ for (u in t) {
+ if (r[u] === t[u]) {
+ delete t[u]
+ } else {
+ r[u] = t[u]
+ }
+ delete o[u];
+ delete m[u]
+ }
+ return t
+ } else {
+ for (u in t) {
+ l = t[u];
+ p = r[u];
+ if (l !== p && p !== undefined && p !== null && (c = h[u])) {
+ q = f;
+ a = e;
+ if (g) {
+ if (u in i) {
+ q = i[u]
+ }
+ if (u in j) {
+ a = j[u]
+ }
+ }
+ if (p && p.isGradient || l && l.isGradient) {
+ a = 0
+ }
+ if (a) {
+ if (!m[u]) {
+ m[u] = {}
+ }
+ k = m[u];
+ k.start = 0;
+ k.easing = q;
+ k.duration = a;
+ k.compute = c.compute;
+ k.serve = c.serve || Ext.identityFn;
+ k.remove = t.removeFromInstance && t.removeFromInstance[u];
+ if (c.parseInitial) {
+ var b = c.parseInitial(p, l);
+ k.source = b[0];
+ k.target = b[1]
+ } else {
+ if (c.parse) {
+ k.source = c.parse(p);
+ k.target = c.parse(l)
+ } else {
+ k.source = p;
+ k.target = l
+ }
+ }
+ o[u] = l;
+ delete t[u];
+ d = true;
+ continue
+ } else {
+ delete o[u]
+ }
+ } else {
+ delete o[u]
+ }
+ delete m[u]
+ }
+ }
+ if (d && !r.animating) {
+ s.setAnimating(r, true)
+ }
+ return t
+ },
+ updateAttributes: function(g) {
+ if (!g.animating) {
+ return {}
+ }
+ var h = {},
+ e = false,
+ d = g.timers,
+ f = g.animationOriginal,
+ c = Ext.draw.Animator.animationTime(),
+ a, b, i;
+ if (g.lastUpdate === c) {
+ return null
+ }
+ for (a in d) {
+ b = d[a];
+ if (!b.start) {
+ b.start = c;
+ i = 0
+ } else {
+ i = (c - b.start) / b.duration
+ }
+ if (i >= 1) {
+ h[a] = f[a];
+ delete f[a];
+ if (d[a].remove) {
+ h.removeFromInstance = h.removeFromInstance || {};
+ h.removeFromInstance[a] = true
+ }
+ delete d[a]
+ } else {
+ h[a] = b.serve(b.compute(b.source, b.target, b.easing(i), g[a]));
+ e = true
+ }
+ }
+ g.lastUpdate = c;
+ this.setAnimating(g, e);
+ return h
+ },
+ pushDown: function(a, b) {
+ b = this.callParent([a.animationOriginal, b]);
+ return this.setAttrs(a, b)
+ },
+ popUp: function(a, b) {
+ a = a.prototype;
+ b = this.setAttrs(a, b);
+ if (this._next) {
+ return this._next.popUp(a, b)
+ } else {
+ return Ext.apply(a, b)
+ }
+ },
+ step: function(g) {
+ var f = this,
+ c = f.animatingPool.slice(),
+ e = c.length,
+ b = 0,
+ a, d;
+ for (; b < e; b++) {
+ a = c[b];
+ d = f.updateAttributes(a);
+ if (d && f._next) {
+ f._next.popUp(a, d)
+ }
+ }
+ },
+ stop: function() {
+ this.step();
+ var d = this,
+ b = d.animatingPool,
+ a, c;
+ for (a = 0, c = b.length; a < c; a++) {
+ b[a].animating = false
+ }
+ d.animatingPool.length = 0;
+ d.animating = 0;
+ Ext.draw.Animator.remove(d)
+ },
+ destroy: function() {
+ this.animatingPool.length = 0;
+ this.animating = 0;
+ this.callParent()
+ }
+});
+Ext.define("Ext.draw.modifier.Highlight", {
+ extend: "Ext.draw.modifier.Modifier",
+ alias: "modifier.highlight",
+ config: {
+ enabled: false,
+ highlightStyle: null
+ },
+ preFx: true,
+ applyHighlightStyle: function(b, a) {
+ a = a || {};
+ if (this.getSprite()) {
+ Ext.apply(a, this.getSprite().self.def.normalize(b))
+ } else {
+ Ext.apply(a, b)
+ }
+ return a
+ },
+ prepareAttributes: function(a) {
+ if (!a.hasOwnProperty("highlightOriginal")) {
+ a.highlighted = false;
+ a.highlightOriginal = Ext.Object.chain(a);
+ a.highlightOriginal.prototype = a;
+ a.highlightOriginal.removeFromInstance = {}
+ }
+ if (this._previous) {
+ this._previous.prepareAttributes(a.highlightOriginal)
+ }
+ },
+ updateSprite: function(b, a) {
+ if (b) {
+ if (this.getHighlightStyle()) {
+ this._highlightStyle = b.self.def.normalize(this.getHighlightStyle())
+ }
+ this.setHighlightStyle(b.config.highlight)
+ }
+ b.self.def.setConfig({
+ defaults: {
+ highlighted: false
+ },
+ processors: {
+ highlighted: "bool"
+ }
+ });
+ this.setSprite(b)
+ },
+ filterChanges: function(a, d) {
+ var e = this,
+ f = a.highlightOriginal,
+ c = e.getHighlightStyle(),
+ b;
+ if (a.highlighted) {
+ for (b in d) {
+ if (c.hasOwnProperty(b)) {
+ f[b] = d[b];
+ delete d[b]
+ }
+ }
+ }
+ for (b in d) {
+ if (b !== "highlighted" && f[b] === d[b]) {
+ delete d[b]
+ }
+ }
+ return d
+ },
+ pushDown: function(e, g) {
+ var f = this.getHighlightStyle(),
+ c = e.highlightOriginal,
+ i = c.removeFromInstance,
+ d, a, h, b;
+ if (g.hasOwnProperty("highlighted")) {
+ d = g.highlighted;
+ delete g.highlighted;
+ if (this._previous) {
+ g = this._previous.pushDown(c, g)
+ }
+ g = this.filterChanges(e, g);
+ if (d !== e.highlighted) {
+ if (d) {
+ for (a in f) {
+ if (a in g) {
+ c[a] = g[a]
+ } else {
+ h = e.template && e.template.ownAttr;
+ if (h && !e.prototype.hasOwnProperty(a)) {
+ i[a] = true;
+ c[a] = h.animationOriginal[a]
+ } else {
+ b = c.timers[a];
+ if (b && b.remove) {
+ i[a] = true
+ }
+ c[a] = e[a]
+ }
+ }
+ if (c[a] !== f[a]) {
+ g[a] = f[a]
+ }
+ }
+ } else {
+ for (a in f) {
+ if (!(a in g)) {
+ g[a] = c[a]
+ }
+ delete c[a]
+ }
+ g.removeFromInstance = g.removeFromInstance || {};
+ Ext.apply(g.removeFromInstance, i);
+ c.removeFromInstance = {}
+ }
+ g.highlighted = d
+ }
+ } else {
+ if (this._previous) {
+ g = this._previous.pushDown(c, g)
+ }
+ g = this.filterChanges(e, g)
+ }
+ return g
+ },
+ popUp: function(a, b) {
+ b = this.filterChanges(a, b);
+ Ext.draw.modifier.Modifier.prototype.popUp.call(this, a, b)
+ }
+});
+Ext.define("Ext.draw.sprite.Sprite", {
+ alias: "sprite.sprite",
+ mixins: {
+ observable: "Ext.mixin.Observable"
+ },
+ requires: ["Ext.draw.Draw", "Ext.draw.gradient.Gradient", "Ext.draw.sprite.AttributeDefinition", "Ext.draw.modifier.Target", "Ext.draw.modifier.Animation", "Ext.draw.modifier.Highlight"],
+ isSprite: true,
+ statics: {
+ defaultHitTestOptions: {
+ fill: true,
+ stroke: true
+ }
+ },
+ inheritableStatics: {
+ def: {
+ processors: {
+ strokeStyle: "color",
+ fillStyle: "color",
+ strokeOpacity: "limited01",
+ fillOpacity: "limited01",
+ lineWidth: "number",
+ lineCap: "enums(butt,round,square)",
+ lineJoin: "enums(round,bevel,miter)",
+ lineDash: "data",
+ lineDashOffset: "number",
+ miterLimit: "number",
+ shadowColor: "color",
+ shadowOffsetX: "number",
+ shadowOffsetY: "number",
+ shadowBlur: "number",
+ globalAlpha: "limited01",
+ globalCompositeOperation: "enums(source-over,destination-over,source-in,destination-in,source-out,destination-out,source-atop,destination-atop,lighter,xor,copy)",
+ hidden: "bool",
+ transformFillStroke: "bool",
+ zIndex: "number",
+ translationX: "number",
+ translationY: "number",
+ rotationRads: "number",
+ rotationCenterX: "number",
+ rotationCenterY: "number",
+ scalingX: "number",
+ scalingY: "number",
+ scalingCenterX: "number",
+ scalingCenterY: "number",
+ constrainGradients: "bool"
+ },
+ aliases: {
+ stroke: "strokeStyle",
+ fill: "fillStyle",
+ color: "fillStyle",
+ "stroke-width": "lineWidth",
+ "stroke-linecap": "lineCap",
+ "stroke-linejoin": "lineJoin",
+ "stroke-miterlimit": "miterLimit",
+ "text-anchor": "textAlign",
+ opacity: "globalAlpha",
+ translateX: "translationX",
+ translateY: "translationY",
+ rotateRads: "rotationRads",
+ rotateCenterX: "rotationCenterX",
+ rotateCenterY: "rotationCenterY",
+ scaleX: "scalingX",
+ scaleY: "scalingY",
+ scaleCenterX: "scalingCenterX",
+ scaleCenterY: "scalingCenterY"
+ },
+ defaults: {
+ hidden: false,
+ zIndex: 0,
+ strokeStyle: "none",
+ fillStyle: "none",
+ lineWidth: 1,
+ lineDash: [],
+ lineDashOffset: 0,
+ lineCap: "butt",
+ lineJoin: "miter",
+ miterLimit: 10,
+ shadowColor: "none",
+ shadowOffsetX: 0,
+ shadowOffsetY: 0,
+ shadowBlur: 0,
+ globalAlpha: 1,
+ strokeOpacity: 1,
+ fillOpacity: 1,
+ transformFillStroke: false,
+ translationX: 0,
+ translationY: 0,
+ rotationRads: 0,
+ rotationCenterX: null,
+ rotationCenterY: null,
+ scalingX: 1,
+ scalingY: 1,
+ scalingCenterX: null,
+ scalingCenterY: null,
+ constrainGradients: false
+ },
+ triggers: {
+ zIndex: "zIndex",
+ globalAlpha: "canvas",
+ globalCompositeOperation: "canvas",
+ transformFillStroke: "canvas",
+ strokeStyle: "canvas",
+ fillStyle: "canvas",
+ strokeOpacity: "canvas",
+ fillOpacity: "canvas",
+ lineWidth: "canvas",
+ lineCap: "canvas",
+ lineJoin: "canvas",
+ lineDash: "canvas",
+ lineDashOffset: "canvas",
+ miterLimit: "canvas",
+ shadowColor: "canvas",
+ shadowOffsetX: "canvas",
+ shadowOffsetY: "canvas",
+ shadowBlur: "canvas",
+ translationX: "transform",
+ translationY: "transform",
+ rotationRads: "transform",
+ rotationCenterX: "transform",
+ rotationCenterY: "transform",
+ scalingX: "transform",
+ scalingY: "transform",
+ scalingCenterX: "transform",
+ scalingCenterY: "transform",
+ constrainGradients: "canvas"
+ },
+ updaters: {
+ bbox: "bboxUpdater",
+ zIndex: function(a) {
+ a.dirtyZIndex = true
+ },
+ transform: function(a) {
+ a.dirtyTransform = true;
+ a.bbox.transform.dirty = true
+ }
+ }
+ }
+ },
+ config: {
+ parent: null,
+ surface: null
+ },
+ onClassExtended: function(d, c) {
+ var b = d.superclass.self.def.initialConfig,
+ e = c.inheritableStatics && c.inheritableStatics.def,
+ a;
+ if (e) {
+ a = Ext.Object.merge({}, b, e);
+ d.def = new Ext.draw.sprite.AttributeDefinition(a);
+ delete c.inheritableStatics.def
+ } else {
+ d.def = new Ext.draw.sprite.AttributeDefinition(b)
+ }
+ d.def.spriteClass = d
+ },
+ constructor: function(b) {
+ var d = this,
+ c = d.self.def,
+ e = c.getDefaults(),
+ a;
+ b = Ext.isObject(b) ? b : {};
+ d.id = b.id || Ext.id(null, "ext-sprite-");
+ d.attr = {};
+ d.mixins.observable.constructor.apply(d, arguments);
+ a = Ext.Array.from(b.modifiers, true);
+ d.prepareModifiers(a);
+ d.initializeAttributes();
+ d.setAttributes(e, true);
+ d.setAttributes(b)
+ },
+ getDirty: function() {
+ return this.attr.dirty
+ },
+ setDirty: function(b) {
+ this.attr.dirty = b;
+ if (b) {
+ var a = this.getParent();
+ if (a) {
+ a.setDirty(true)
+ }
+ }
+ },
+ addModifier: function(a, b) {
+ var c = this;
+ if (!(a instanceof Ext.draw.modifier.Modifier)) {
+ a = Ext.factory(a, null, null, "modifier")
+ }
+ a.setSprite(c);
+ if (a.preFx || a.config && a.config.preFx) {
+ if (c.fx.getPrevious()) {
+ c.fx.getPrevious().setNext(a)
+ }
+ a.setNext(c.fx)
+ } else {
+ c.topModifier.getPrevious().setNext(a);
+ a.setNext(c.topModifier)
+ }
+ if (b) {
+ c.initializeAttributes()
+ }
+ return a
+ },
+ prepareModifiers: function(d) {
+ var c = this,
+ a, b;
+ c.topModifier = new Ext.draw.modifier.Target({
+ sprite: c
+ });
+ c.fx = new Ext.draw.modifier.Animation({
+ sprite: c
+ });
+ c.fx.setNext(c.topModifier);
+ for (a = 0, b = d.length; a < b; a++) {
+ c.addModifier(d[a], false)
+ }
+ },
+ getAnimation: function() {
+ return this.fx
+ },
+ setAnimation: function(a) {
+ this.fx.setConfig(a)
+ },
+ initializeAttributes: function() {
+ this.topModifier.prepareAttributes(this.attr)
+ },
+ callUpdaters: function(d) {
+ var e = this,
+ h = d.pendingUpdaters,
+ i = e.self.def.getUpdaters(),
+ c = false,
+ a = false,
+ b, g, f;
+ e.callUpdaters = Ext.emptyFn;
+ do {
+ c = false;
+ for (g in h) {
+ c = true;
+ b = h[g];
+ delete h[g];
+ f = i[g];
+ if (typeof f === "string") {
+ f = e[f]
+ }
+ if (f) {
+ f.call(e, d, b)
+ }
+ }
+ a = a || c
+ } while (c);
+ delete e.callUpdaters;
+ if (a) {
+ e.setDirty(true)
+ }
+ },
+ scheduleUpdaters: function(a, e, c) {
+ var f;
+ if (c) {
+ for (var b = 0, d = e.length; b < d; b++) {
+ f = e[b];
+ this.scheduleUpdater(a, f, c)
+ }
+ } else {
+ for (f in e) {
+ c = e[f];
+ this.scheduleUpdater(a, f, c)
+ }
+ }
+ },
+ scheduleUpdater: function(a, c, b) {
+ b = b || [];
+ var d = a.pendingUpdaters;
+ if (c in d) {
+ if (b.length) {
+ d[c] = Ext.Array.merge(d[c], b)
+ }
+ } else {
+ d[c] = b
+ }
+ },
+ setAttributes: function(d, g, c) {
+ var a = this.attr,
+ b, e, f;
+ if (g) {
+ if (c) {
+ this.topModifier.pushDown(a, d)
+ } else {
+ f = {};
+ for (b in d) {
+ e = d[b];
+ if (e !== a[b]) {
+ f[b] = e
+ }
+ }
+ this.topModifier.pushDown(a, f)
+ }
+ } else {
+ this.topModifier.pushDown(a, this.self.def.normalize(d))
+ }
+ },
+ setAttributesBypassingNormalization: function(b, a) {
+ return this.setAttributes(b, true, a)
+ },
+ bboxUpdater: function(b) {
+ var c = b.rotationRads !== 0,
+ a = b.scalingX !== 1 || b.scalingY !== 1,
+ d = b.rotationCenterX === null || b.rotationCenterY === null,
+ e = b.scalingCenterX === null || b.scalingCenterY === null;
+ b.bbox.plain.dirty = true;
+ b.bbox.transform.dirty = true;
+ if (c && d || a && e) {
+ this.scheduleUpdater(b, "transform")
+ }
+ },
+ getBBox: function(d) {
+ var e = this,
+ a = e.attr,
+ f = a.bbox,
+ c = f.plain,
+ b = f.transform;
+ if (c.dirty) {
+ e.updatePlainBBox(c);
+ c.dirty = false
+ }
+ if (!d) {
+ e.applyTransformations();
+ if (b.dirty) {
+ e.updateTransformedBBox(b, c);
+ b.dirty = false
+ }
+ return b
+ }
+ return c
+ },
+ updatePlainBBox: Ext.emptyFn,
+ updateTransformedBBox: function(a, b) {
+ this.attr.matrix.transformBBox(b, 0, a)
+ },
+ getBBoxCenter: function(a) {
+ var b = this.getBBox(a);
+ if (b) {
+ return [b.x + b.width * 0.5, b.y + b.height * 0.5]
+ } else {
+ return [0, 0]
+ }
+ },
+ hide: function() {
+ this.attr.hidden = true;
+ this.setDirty(true);
+ return this
+ },
+ show: function() {
+ this.attr.hidden = false;
+ this.setDirty(true);
+ return this
+ },
+ useAttributes: function(i, f) {
+ this.applyTransformations();
+ var d = this.attr,
+ h = d.canvasAttributes,
+ e = h.strokeStyle,
+ g = h.fillStyle,
+ b = h.lineDash,
+ c = h.lineDashOffset,
+ a;
+ if (e) {
+ if (e.isGradient) {
+ i.strokeStyle = "black";
+ i.strokeGradient = e
+ } else {
+ i.strokeGradient = false
+ }
+ }
+ if (g) {
+ if (g.isGradient) {
+ i.fillStyle = "black";
+ i.fillGradient = g
+ } else {
+ i.fillGradient = false
+ }
+ }
+ if (b) {
+ i.setLineDash(b)
+ }
+ if (Ext.isNumber(c + i.lineDashOffset)) {
+ i.lineDashOffset = c
+ }
+ for (a in h) {
+ if (h[a] !== undefined && h[a] !== i[a]) {
+ i[a] = h[a]
+ }
+ }
+ this.setGradientBBox(i, f)
+ },
+ setGradientBBox: function(b, c) {
+ var a = this.attr;
+ if (a.constrainGradients) {
+ b.setGradientBBox({
+ x: c[0],
+ y: c[1],
+ width: c[2],
+ height: c[3]
+ })
+ } else {
+ b.setGradientBBox(this.getBBox(a.transformFillStroke))
+ }
+ },
+ applyTransformations: function(b) {
+ if (!b && !this.attr.dirtyTransform) {
+ return
+ }
+ var r = this,
+ k = r.attr,
+ p = r.getBBoxCenter(true),
+ g = p[0],
+ f = p[1],
+ q = k.translationX,
+ o = k.translationY,
+ j = k.scalingX,
+ i = k.scalingY === null ? k.scalingX : k.scalingY,
+ m = k.scalingCenterX === null ? g : k.scalingCenterX,
+ l = k.scalingCenterY === null ? f : k.scalingCenterY,
+ s = k.rotationRads,
+ e = k.rotationCenterX === null ? g : k.rotationCenterX,
+ d = k.rotationCenterY === null ? f : k.rotationCenterY,
+ c = Math.cos(s),
+ a = Math.sin(s),
+ n, h;
+ if (j === 1 && i === 1) {
+ m = 0;
+ l = 0
+ }
+ if (s === 0) {
+ e = 0;
+ d = 0
+ }
+ n = m * (1 - j) - e;
+ h = l * (1 - i) - d;
+ k.matrix.elements = [c * j, a * j, -a * i, c * i, c * n - a * h + e + q, a * n + c * h + d + o];
+ k.matrix.inverse(k.inverseMatrix);
+ k.dirtyTransform = false;
+ k.bbox.transform.dirty = true
+ },
+ transform: function(b, c) {
+ var a = this.attr,
+ e = a.matrix,
+ d;
+ if (b && b.isMatrix) {
+ d = b.elements
+ } else {
+ d = b
+ }
+ e.prepend.apply(e, d.slice());
+ e.inverse(a.inverseMatrix);
+ if (c) {
+ this.updateTransformAttributes()
+ }
+ a.dirtyTransform = false;
+ a.bbox.transform.dirty = true;
+ this.setDirty(true);
+ return this
+ },
+ updateTransformAttributes: function() {
+ var a = this.attr,
+ b = a.matrix.split();
+ a.rotationRads = b.rotate;
+ a.rotationCenterX = 0;
+ a.rotationCenterY = 0;
+ a.scalingX = b.scaleX;
+ a.scalingY = b.scaleY;
+ a.scalingCenterX = 0;
+ a.scalingCenterY = 0;
+ a.translationX = b.translateX;
+ a.translationY = b.translateY
+ },
+ resetTransform: function(b) {
+ var a = this.attr;
+ a.matrix.reset();
+ a.inverseMatrix.reset();
+ if (!b) {
+ this.updateTransformAttributes()
+ }
+ a.dirtyTransform = false;
+ a.bbox.transform.dirty = true;
+ this.setDirty(true);
+ return this
+ },
+ setTransform: function(a, b) {
+ this.resetTransform(true);
+ this.transform.call(this, a, b);
+ return this
+ },
+ preRender: Ext.emptyFn,
+ render: Ext.emptyFn,
+ hitTest: function(b, c) {
+ if (this.isVisible()) {
+ var a = b[0],
+ f = b[1],
+ e = this.getBBox(),
+ d = e && a >= e.x && a <= (e.x + e.width) && f >= e.y && f <= (e.y + e.height);
+ if (d) {
+ return {
+ sprite: this
+ }
+ }
+ }
+ return null
+ },
+ isVisible: function() {
+ var e = this.attr,
+ f = this.getParent(),
+ g = f && (f.isSurface || f.isVisible()),
+ d = g && !e.hidden && e.globalAlpha,
+ b = Ext.draw.Color.NONE,
+ a = Ext.draw.Color.RGBA_NONE,
+ c = e.fillOpacity && e.fillStyle !== b && e.fillStyle !== a,
+ i = e.strokeOpacity && e.strokeStyle !== b && e.strokeStyle !== a,
+ h = d && (c || i);
+ return !!h
+ },
+ repaint: function() {
+ var a = this.getSurface();
+ if (a) {
+ a.renderFrame()
+ }
+ },
+ remove: function() {
+ var a = this.getSurface();
+ if (a && a.isSurface) {
+ return a.remove(this)
+ }
+ return null
+ },
+ destroy: function() {
+ var b = this,
+ a = b.topModifier,
+ c;
+ while (a) {
+ c = a;
+ a = a.getPrevious();
+ c.destroy()
+ }
+ delete b.attr;
+ b.remove();
+ if (b.fireEvent("beforedestroy", b) !== false) {
+ b.fireEvent("destroy", b)
+ }
+ b.callParent()
+ }
+}, function() {
+ this.def = new Ext.draw.sprite.AttributeDefinition(this.def);
+ this.def.spriteClass = this
+});
+Ext.define("Ext.draw.Path", {
+ requires: ["Ext.draw.Draw"],
+ statics: {
+ pathRe: /,?([achlmqrstvxz]),?/gi,
+ pathRe2: /-/gi,
+ pathSplitRe: /\s|,/g
+ },
+ svgString: "",
+ constructor: function(a) {
+ var b = this;
+ b.commands = [];
+ b.params = [];
+ b.cursor = null;
+ b.startX = 0;
+ b.startY = 0;
+ if (a) {
+ b.fromSvgString(a)
+ }
+ },
+ clear: function() {
+ var a = this;
+ a.params.length = 0;
+ a.commands.length = 0;
+ a.cursor = null;
+ a.startX = 0;
+ a.startY = 0;
+ a.dirt()
+ },
+ dirt: function() {
+ this.svgString = ""
+ },
+ moveTo: function(a, c) {
+ var b = this;
+ if (!b.cursor) {
+ b.cursor = [a, c]
+ }
+ b.params.push(a, c);
+ b.commands.push("M");
+ b.startX = a;
+ b.startY = c;
+ b.cursor[0] = a;
+ b.cursor[1] = c;
+ b.dirt()
+ },
+ lineTo: function(a, c) {
+ var b = this;
+ if (!b.cursor) {
+ b.cursor = [a, c];
+ b.params.push(a, c);
+ b.commands.push("M")
+ } else {
+ b.params.push(a, c);
+ b.commands.push("L")
+ }
+ b.cursor[0] = a;
+ b.cursor[1] = c;
+ b.dirt()
+ },
+ bezierCurveTo: function(c, e, b, d, a, g) {
+ var f = this;
+ if (!f.cursor) {
+ f.moveTo(c, e)
+ }
+ f.params.push(c, e, b, d, a, g);
+ f.commands.push("C");
+ f.cursor[0] = a;
+ f.cursor[1] = g;
+ f.dirt()
+ },
+ quadraticCurveTo: function(b, e, a, d) {
+ var c = this;
+ if (!c.cursor) {
+ c.moveTo(b, e)
+ }
+ c.bezierCurveTo((2 * b + c.cursor[0]) / 3, (2 * e + c.cursor[1]) / 3, (2 * b + a) / 3, (2 * e + d) / 3, a, d)
+ },
+ closePath: function() {
+ var a = this;
+ if (a.cursor) {
+ a.cursor = null;
+ a.commands.push("Z");
+ a.dirt()
+ }
+ },
+ arcTo: function(A, f, z, d, j, i, v) {
+ var E = this;
+ if (i === undefined) {
+ i = j
+ }
+ if (v === undefined) {
+ v = 0
+ }
+ if (!E.cursor) {
+ E.moveTo(A, f);
+ return
+ }
+ if (j === 0 || i === 0) {
+ E.lineTo(A, f);
+ return
+ }
+ z -= A;
+ d -= f;
+ var B = E.cursor[0] - A,
+ g = E.cursor[1] - f,
+ C = z * g - d * B,
+ b, a, l, r, k, q, x = Math.sqrt(B * B + g * g),
+ u = Math.sqrt(z * z + d * d),
+ t, e, c;
+ if (C === 0) {
+ E.lineTo(A, f);
+ return
+ }
+ if (i !== j) {
+ b = Math.cos(v);
+ a = Math.sin(v);
+ l = b / j;
+ r = a / i;
+ k = -a / j;
+ q = b / i;
+ var D = l * B + r * g;
+ g = k * B + q * g;
+ B = D;
+ D = l * z + r * d;
+ d = k * z + q * d;
+ z = D
+ } else {
+ B /= j;
+ g /= i;
+ z /= j;
+ d /= i
+ }
+ e = B * u + z * x;
+ c = g * u + d * x;
+ t = 1 / (Math.sin(Math.asin(Math.abs(C) / (x * u)) * 0.5) * Math.sqrt(e * e + c * c));
+ e *= t;
+ c *= t;
+ var o = (e * B + c * g) / (B * B + g * g),
+ m = (e * z + c * d) / (z * z + d * d);
+ var n = B * o - e,
+ p = g * o - c,
+ h = z * m - e,
+ y = d * m - c,
+ w = Math.atan2(p, n),
+ s = Math.atan2(y, h);
+ if (C > 0) {
+ if (s < w) {
+ s += Math.PI * 2
+ }
+ } else {
+ if (w < s) {
+ w += Math.PI * 2
+ }
+ }
+ if (i !== j) {
+ e = b * e * j - a * c * i + A;
+ c = a * c * i + b * c * i + f;
+ E.lineTo(b * j * n - a * i * p + e, a * j * n + b * i * p + c);
+ E.ellipse(e, c, j, i, v, w, s, C < 0)
+ } else {
+ e = e * j + A;
+ c = c * i + f;
+ E.lineTo(j * n + e, i * p + c);
+ E.ellipse(e, c, j, i, v, w, s, C < 0)
+ }
+ },
+ ellipse: function(h, f, c, a, q, n, d, e) {
+ var o = this,
+ g = o.params,
+ b = g.length,
+ m, l, k;
+ if (d - n >= Math.PI * 2) {
+ o.ellipse(h, f, c, a, q, n, n + Math.PI, e);
+ o.ellipse(h, f, c, a, q, n + Math.PI, d, e);
+ return
+ }
+ if (!e) {
+ if (d < n) {
+ d += Math.PI * 2
+ }
+ m = o.approximateArc(g, h, f, c, a, q, n, d)
+ } else {
+ if (n < d) {
+ n += Math.PI * 2
+ }
+ m = o.approximateArc(g, h, f, c, a, q, d, n);
+ for (l = b, k = g.length - 2; l < k; l += 2, k -= 2) {
+ var p = g[l];
+ g[l] = g[k];
+ g[k] = p;
+ p = g[l + 1];
+ g[l + 1] = g[k + 1];
+ g[k + 1] = p
+ }
+ }
+ if (!o.cursor) {
+ o.cursor = [g[g.length - 2], g[g.length - 1]];
+ o.commands.push("M")
+ } else {
+ o.cursor[0] = g[g.length - 2];
+ o.cursor[1] = g[g.length - 1];
+ o.commands.push("L")
+ }
+ for (l = 2; l < m; l += 6) {
+ o.commands.push("C")
+ }
+ o.dirt()
+ },
+ arc: function(b, f, a, d, c, e) {
+ this.ellipse(b, f, a, a, 0, d, c, e)
+ },
+ rect: function(b, e, c, a) {
+ if (c == 0 || a == 0) {
+ return
+ }
+ var d = this;
+ d.moveTo(b, e);
+ d.lineTo(b + c, e);
+ d.lineTo(b + c, e + a);
+ d.lineTo(b, e + a);
+ d.closePath()
+ },
+ approximateArc: function(s, i, f, o, n, d, x, v) {
+ var e = Math.cos(d),
+ z = Math.sin(d),
+ k = Math.cos(x),
+ l = Math.sin(x),
+ q = e * k * o - z * l * n,
+ y = -e * l * o - z * k * n,
+ p = z * k * o + e * l * n,
+ w = -z * l * o + e * k * n,
+ m = Math.PI / 2,
+ r = 2,
+ j = q,
+ u = y,
+ h = p,
+ t = w,
+ b = 0.547443256150549,
+ C, g, A, a, B, c;
+ v -= x;
+ if (v < 0) {
+ v += Math.PI * 2
+ }
+ s.push(q + i, p + f);
+ while (v >= m) {
+ s.push(j + u * b + i, h + t * b + f, j * b + u + i, h * b + t + f, u + i, t + f);
+ r += 6;
+ v -= m;
+ C = j;
+ j = u;
+ u = -C;
+ C = h;
+ h = t;
+ t = -C
+ }
+ if (v) {
+ g = (0.3294738052815987 + 0.012120855841304373 * v) * v;
+ A = Math.cos(v);
+ a = Math.sin(v);
+ B = A + g * a;
+ c = a - g * A;
+ s.push(j + u * g + i, h + t * g + f, j * B + u * c + i, h * B + t * c + f, j * A + u * a + i, h * A + t * a + f);
+ r += 6
+ }
+ return r
+ },
+ arcSvg: function(j, h, r, m, w, t, c) {
+ if (j < 0) {
+ j = -j
+ }
+ if (h < 0) {
+ h = -h
+ }
+ var x = this,
+ u = x.cursor[0],
+ f = x.cursor[1],
+ a = (u - t) / 2,
+ y = (f - c) / 2,
+ d = Math.cos(r),
+ s = Math.sin(r),
+ o = a * d + y * s,
+ v = -a * s + y * d,
+ i = o / j,
+ g = v / h,
+ p = i * i + g * g,
+ e = (u + t) * 0.5,
+ b = (f + c) * 0.5,
+ l = 0,
+ k = 0;
+ if (p >= 1) {
+ p = Math.sqrt(p);
+ j *= p;
+ h *= p
+ } else {
+ p = Math.sqrt(1 / p - 1);
+ if (m === w) {
+ p = -p
+ }
+ l = p * j * g;
+ k = -p * h * i;
+ e += d * l - s * k;
+ b += s * l + d * k
+ }
+ var q = Math.atan2((v - k) / h, (o - l) / j),
+ n = Math.atan2((-v - k) / h, (-o - l) / j) - q;
+ if (w) {
+ if (n <= 0) {
+ n += Math.PI * 2
+ }
+ } else {
+ if (n >= 0) {
+ n -= Math.PI * 2
+ }
+ }
+ x.ellipse(e, b, j, h, r, q, q + n, 1 - w)
+ },
+ fromSvgString: function(e) {
+ if (!e) {
+ return
+ }
+ var m = this,
+ h, l = {
+ a: 7,
+ c: 6,
+ h: 1,
+ l: 2,
+ m: 2,
+ q: 4,
+ s: 4,
+ t: 2,
+ v: 1,
+ z: 0,
+ A: 7,
+ C: 6,
+ H: 1,
+ L: 2,
+ M: 2,
+ Q: 4,
+ S: 4,
+ T: 2,
+ V: 1,
+ Z: 0
+ },
+ k = "",
+ g, f, c = 0,
+ b = 0,
+ d = false,
+ j, n, a;
+ if (Ext.isString(e)) {
+ h = e.replace(Ext.draw.Path.pathRe, " $1 ").replace(Ext.draw.Path.pathRe2, " -").split(Ext.draw.Path.pathSplitRe)
+ } else {
+ if (Ext.isArray(e)) {
+ h = e.join(",").split(Ext.draw.Path.pathSplitRe)
+ }
+ }
+ for (j = 0, n = 0; j < h.length; j++) {
+ if (h[j] !== "") {
+ h[n++] = h[j]
+ }
+ }
+ h.length = n;
+ m.clear();
+ for (j = 0; j < h.length;) {
+ k = d;
+ d = h[j];
+ a = (d.toUpperCase() !== d);
+ j++;
+ switch (d) {
+ case "M":
+ m.moveTo(c = +h[j], b = +h[j + 1]);
+ j += 2;
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.lineTo(c = +h[j], b = +h[j + 1]);
+ j += 2
+ }
+ break;
+ case "L":
+ m.lineTo(c = +h[j], b = +h[j + 1]);
+ j += 2;
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.lineTo(c = +h[j], b = +h[j + 1]);
+ j += 2
+ }
+ break;
+ case "A":
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.arcSvg(+h[j], +h[j + 1], +h[j + 2] * Math.PI / 180, +h[j + 3], +h[j + 4], c = +h[j + 5], b = +h[j + 6]);
+ j += 7
+ }
+ break;
+ case "C":
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.bezierCurveTo(+h[j], +h[j + 1], g = +h[j + 2], f = +h[j + 3], c = +h[j + 4], b = +h[j + 5]);
+ j += 6
+ }
+ break;
+ case "Z":
+ m.closePath();
+ break;
+ case "m":
+ m.moveTo(c += +h[j], b += +h[j + 1]);
+ j += 2;
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.lineTo(c += +h[j], b += +h[j + 1]);
+ j += 2
+ }
+ break;
+ case "l":
+ m.lineTo(c += +h[j], b += +h[j + 1]);
+ j += 2;
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.lineTo(c += +h[j], b += +h[j + 1]);
+ j += 2
+ }
+ break;
+ case "a":
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.arcSvg(+h[j], +h[j + 1], +h[j + 2] * Math.PI / 180, +h[j + 3], +h[j + 4], c += +h[j + 5], b += +h[j + 6]);
+ j += 7
+ }
+ break;
+ case "c":
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.bezierCurveTo(c + (+h[j]), b + (+h[j + 1]), g = c + (+h[j + 2]), f = b + (+h[j + 3]), c += +h[j + 4], b += +h[j + 5]);
+ j += 6
+ }
+ break;
+ case "z":
+ m.closePath();
+ break;
+ case "s":
+ if (!(k === "c" || k === "C" || k === "s" || k === "S")) {
+ g = c;
+ f = b
+ }
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.bezierCurveTo(c + c - g, b + b - f, g = c + (+h[j]), f = b + (+h[j + 1]), c += +h[j + 2], b += +h[j + 3]);
+ j += 4
+ }
+ break;
+ case "S":
+ if (!(k === "c" || k === "C" || k === "s" || k === "S")) {
+ g = c;
+ f = b
+ }
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.bezierCurveTo(c + c - g, b + b - f, g = +h[j], f = +h[j + 1], c = (+h[j + 2]), b = (+h[j + 3]));
+ j += 4
+ }
+ break;
+ case "q":
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.quadraticCurveTo(g = c + (+h[j]), f = b + (+h[j + 1]), c += +h[j + 2], b += +h[j + 3]);
+ j += 4
+ }
+ break;
+ case "Q":
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.quadraticCurveTo(g = +h[j], f = +h[j + 1], c = +h[j + 2], b = +h[j + 3]);
+ j += 4
+ }
+ break;
+ case "t":
+ if (!(k === "q" || k === "Q" || k === "t" || k === "T")) {
+ g = c;
+ f = b
+ }
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.quadraticCurveTo(g = c + c - g, f = b + b - f, c += +h[j + 1], b += +h[j + 2]);
+ j += 2
+ }
+ break;
+ case "T":
+ if (!(k === "q" || k === "Q" || k === "t" || k === "T")) {
+ g = c;
+ f = b
+ }
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.quadraticCurveTo(g = c + c - g, f = b + b - f, c = (+h[j + 1]), b = (+h[j + 2]));
+ j += 2
+ }
+ break;
+ case "h":
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.lineTo(c += +h[j], b);
+ j++
+ }
+ break;
+ case "H":
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.lineTo(c = +h[j], b);
+ j++
+ }
+ break;
+ case "v":
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.lineTo(c, b += +h[j]);
+ j++
+ }
+ break;
+ case "V":
+ while (j < n && !l.hasOwnProperty(h[j])) {
+ m.lineTo(c, b = +h[j]);
+ j++
+ }
+ break
+ }
+ }
+ },
+ clone: function() {
+ var a = this,
+ b = new Ext.draw.Path();
+ b.params = a.params.slice(0);
+ b.commands = a.commands.slice(0);
+ b.cursor = a.cursor ? a.cursor.slice(0) : null;
+ b.startX = a.startX;
+ b.startY = a.startY;
+ b.svgString = a.svgString;
+ return b
+ },
+ transform: function(j) {
+ if (j.isIdentity()) {
+ return
+ }
+ var a = j.getXX(),
+ f = j.getYX(),
+ m = j.getDX(),
+ l = j.getXY(),
+ e = j.getYY(),
+ k = j.getDY(),
+ b = this.params,
+ c = 0,
+ d = b.length,
+ h, g;
+ for (; c < d; c += 2) {
+ h = b[c];
+ g = b[c + 1];
+ b[c] = h * a + g * f + m;
+ b[c + 1] = h * l + g * e + k
+ }
+ this.dirt()
+ },
+ getDimension: function(f) {
+ if (!f) {
+ f = {}
+ }
+ if (!this.commands || !this.commands.length) {
+ f.x = 0;
+ f.y = 0;
+ f.width = 0;
+ f.height = 0;
+ return f
+ }
+ f.left = Infinity;
+ f.top = Infinity;
+ f.right = -Infinity;
+ f.bottom = -Infinity;
+ var d = 0,
+ c = 0,
+ b = this.commands,
+ g = this.params,
+ e = b.length,
+ a, h;
+ for (; d < e; d++) {
+ switch (b[d]) {
+ case "M":
+ case "L":
+ a = g[c];
+ h = g[c + 1];
+ f.left = Math.min(a, f.left);
+ f.top = Math.min(h, f.top);
+ f.right = Math.max(a, f.right);
+ f.bottom = Math.max(h, f.bottom);
+ c += 2;
+ break;
+ case "C":
+ this.expandDimension(f, a, h, g[c], g[c + 1], g[c + 2], g[c + 3], a = g[c + 4], h = g[c + 5]);
+ c += 6;
+ break
+ }
+ }
+ f.x = f.left;
+ f.y = f.top;
+ f.width = f.right - f.left;
+ f.height = f.bottom - f.top;
+ return f
+ },
+ getDimensionWithTransform: function(n, f) {
+ if (!this.commands || !this.commands.length) {
+ if (!f) {
+ f = {}
+ }
+ f.x = 0;
+ f.y = 0;
+ f.width = 0;
+ f.height = 0;
+ return f
+ }
+ f.left = Infinity;
+ f.top = Infinity;
+ f.right = -Infinity;
+ f.bottom = -Infinity;
+ var a = n.getXX(),
+ k = n.getYX(),
+ q = n.getDX(),
+ p = n.getXY(),
+ h = n.getYY(),
+ o = n.getDY(),
+ e = 0,
+ d = 0,
+ b = this.commands,
+ c = this.params,
+ g = b.length,
+ m, l;
+ for (; e < g; e++) {
+ switch (b[e]) {
+ case "M":
+ case "L":
+ m = c[d] * a + c[d + 1] * k + q;
+ l = c[d] * p + c[d + 1] * h + o;
+ f.left = Math.min(m, f.left);
+ f.top = Math.min(l, f.top);
+ f.right = Math.max(m, f.right);
+ f.bottom = Math.max(l, f.bottom);
+ d += 2;
+ break;
+ case "C":
+ this.expandDimension(f, m, l, c[d] * a + c[d + 1] * k + q, c[d] * p + c[d + 1] * h + o, c[d + 2] * a + c[d + 3] * k + q, c[d + 2] * p + c[d + 3] * h + o, m = c[d + 4] * a + c[d + 5] * k + q, l = c[d + 4] * p + c[d + 5] * h + o);
+ d += 6;
+ break
+ }
+ }
+ if (!f) {
+ f = {}
+ }
+ f.x = f.left;
+ f.y = f.top;
+ f.width = f.right - f.left;
+ f.height = f.bottom - f.top;
+ return f
+ },
+ expandDimension: function(i, d, p, k, g, j, e, c, o) {
+ var m = this,
+ f = i.left,
+ a = i.right,
+ q = i.top,
+ n = i.bottom,
+ h = m.dim || (m.dim = []);
+ m.curveDimension(d, k, j, c, h);
+ f = Math.min(f, h[0]);
+ a = Math.max(a, h[1]);
+ m.curveDimension(p, g, e, o, h);
+ q = Math.min(q, h[0]);
+ n = Math.max(n, h[1]);
+ i.left = f;
+ i.right = a;
+ i.top = q;
+ i.bottom = n
+ },
+ curveDimension: function(p, n, k, j, h) {
+ var i = 3 * (-p + 3 * (n - k) + j),
+ g = 6 * (p - 2 * n + k),
+ f = -3 * (p - n),
+ o, m, e = Math.min(p, j),
+ l = Math.max(p, j),
+ q;
+ if (i === 0) {
+ if (g === 0) {
+ h[0] = e;
+ h[1] = l;
+ return
+ } else {
+ o = -f / g;
+ if (0 < o && o < 1) {
+ m = this.interpolate(p, n, k, j, o);
+ e = Math.min(e, m);
+ l = Math.max(l, m)
+ }
+ }
+ } else {
+ q = g * g - 4 * i * f;
+ if (q >= 0) {
+ q = Math.sqrt(q);
+ o = (q - g) / 2 / i;
+ if (0 < o && o < 1) {
+ m = this.interpolate(p, n, k, j, o);
+ e = Math.min(e, m);
+ l = Math.max(l, m)
+ }
+ if (q > 0) {
+ o -= q / i;
+ if (0 < o && o < 1) {
+ m = this.interpolate(p, n, k, j, o);
+ e = Math.min(e, m);
+ l = Math.max(l, m)
+ }
+ }
+ }
+ }
+ h[0] = e;
+ h[1] = l
+ },
+ interpolate: function(f, e, j, i, g) {
+ if (g === 0) {
+ return f
+ }
+ if (g === 1) {
+ return i
+ }
+ var h = (1 - g) / g;
+ return g * g * g * (i + h * (3 * j + h * (3 * e + h * f)))
+ },
+ fromStripes: function(g) {
+ var e = this,
+ c = 0,
+ d = g.length,
+ b, a, f;
+ e.clear();
+ for (; c < d; c++) {
+ f = g[c];
+ e.params.push.apply(e.params, f);
+ e.commands.push("M");
+ for (b = 2, a = f.length; b < a; b += 6) {
+ e.commands.push("C")
+ }
+ }
+ if (!e.cursor) {
+ e.cursor = []
+ }
+ e.cursor[0] = e.params[e.params.length - 2];
+ e.cursor[1] = e.params[e.params.length - 1];
+ e.dirt()
+ },
+ toStripes: function(k) {
+ var o = k || [],
+ p, n, m, b, a, h, g, f, e, c = this.commands,
+ d = this.params,
+ l = c.length;
+ for (f = 0, e = 0; f < l; f++) {
+ switch (c[f]) {
+ case "M":
+ p = [h = b = d[e++], g = a = d[e++]];
+ o.push(p);
+ break;
+ case "L":
+ n = d[e++];
+ m = d[e++];
+ p.push((b + b + n) / 3, (a + a + m) / 3, (b + n + n) / 3, (a + m + m) / 3, b = n, a = m);
+ break;
+ case "C":
+ p.push(d[e++], d[e++], d[e++], d[e++], b = d[e++], a = d[e++]);
+ break;
+ case "Z":
+ n = h;
+ m = g;
+ p.push((b + b + n) / 3, (a + a + m) / 3, (b + n + n) / 3, (a + m + m) / 3, b = n, a = m);
+ break
+ }
+ }
+ return o
+ },
+ updateSvgString: function() {
+ var b = [],
+ a = this.commands,
+ f = this.params,
+ e = a.length,
+ d = 0,
+ c = 0;
+ for (; d < e; d++) {
+ switch (a[d]) {
+ case "M":
+ b.push("M" + f[c] + "," + f[c + 1]);
+ c += 2;
+ break;
+ case "L":
+ b.push("L" + f[c] + "," + f[c + 1]);
+ c += 2;
+ break;
+ case "C":
+ b.push("C" + f[c] + "," + f[c + 1] + " " + f[c + 2] + "," + f[c + 3] + " " + f[c + 4] + "," + f[c + 5]);
+ c += 6;
+ break;
+ case "Z":
+ b.push("Z");
+ break
+ }
+ }
+ this.svgString = b.join("")
+ },
+ toString: function() {
+ if (!this.svgString) {
+ this.updateSvgString()
+ }
+ return this.svgString
+ }
+});
+Ext.define("Ext.draw.overrides.Path", {
+ override: "Ext.draw.Path",
+ rayOrigin: {
+ x: -10000,
+ y: -10000
+ },
+ isPointInPath: function(o, n) {
+ var m = this,
+ c = m.commands,
+ q = Ext.draw.PathUtil,
+ p = m.rayOrigin,
+ f = m.params,
+ l = c.length,
+ e = null,
+ d = null,
+ b = 0,
+ a = 0,
+ k = 0,
+ h, g;
+ for (h = 0, g = 0; h < l; h++) {
+ switch (c[h]) {
+ case "M":
+ if (e !== null) {
+ if (q.linesIntersection(e, d, b, a, p.x, p.y, o, n)) {
+ k += 1
+ }
+ }
+ e = b = f[g];
+ d = a = f[g + 1];
+ g += 2;
+ break;
+ case "L":
+ if (q.linesIntersection(b, a, f[g], f[g + 1], p.x, p.y, o, n)) {
+ k += 1
+ }
+ b = f[g];
+ a = f[g + 1];
+ g += 2;
+ break;
+ case "C":
+ k += q.cubicLineIntersections(b, f[g], f[g + 2], f[g + 4], a, f[g + 1], f[g + 3], f[g + 5], p.x, p.y, o, n).length;
+ b = f[g + 4];
+ a = f[g + 5];
+ g += 6;
+ break;
+ case "Z":
+ if (e !== null) {
+ if (q.linesIntersection(e, d, b, a, p.x, p.y, o, n)) {
+ k += 1
+ }
+ }
+ break
+ }
+ }
+ return k % 2 === 1
+ },
+ isPointOnPath: function(n, m) {
+ var l = this,
+ c = l.commands,
+ o = Ext.draw.PathUtil,
+ f = l.params,
+ k = c.length,
+ e = null,
+ d = null,
+ b = 0,
+ a = 0,
+ h, g;
+ for (h = 0, g = 0; h < k; h++) {
+ switch (c[h]) {
+ case "M":
+ if (e !== null) {
+ if (o.pointOnLine(e, d, b, a, n, m)) {
+ return true
+ }
+ }
+ e = b = f[g];
+ d = a = f[g + 1];
+ g += 2;
+ break;
+ case "L":
+ if (o.pointOnLine(b, a, f[g], f[g + 1], n, m)) {
+ return true
+ }
+ b = f[g];
+ a = f[g + 1];
+ g += 2;
+ break;
+ case "C":
+ if (o.pointOnCubic(b, f[g], f[g + 2], f[g + 4], a, f[g + 1], f[g + 3], f[g + 5], n, m)) {
+ return true
+ }
+ b = f[g + 4];
+ a = f[g + 5];
+ g += 6;
+ break;
+ case "Z":
+ if (e !== null) {
+ if (o.pointOnLine(e, d, b, a, n, m)) {
+ return true
+ }
+ }
+ break
+ }
+ }
+ return false
+ },
+ getSegmentIntersections: function(t, d, s, c, r, b, o, a) {
+ var w = this,
+ g = arguments.length,
+ v = Ext.draw.PathUtil,
+ f = w.commands,
+ u = w.params,
+ k = f.length,
+ m = null,
+ l = null,
+ h = 0,
+ e = 0,
+ x = [],
+ q, n, p;
+ for (q = 0, n = 0; q < k; q++) {
+ switch (f[q]) {
+ case "M":
+ if (m !== null) {
+ switch (g) {
+ case 4:
+ p = v.linesIntersection(m, l, h, e, t, d, s, c);
+ if (p) {
+ x.push(p)
+ }
+ break;
+ case 8:
+ p = v.cubicLineIntersections(t, s, r, o, d, c, b, a, m, l, h, e);
+ x.push.apply(x, p);
+ break
+ }
+ }
+ m = h = u[n];
+ l = e = u[n + 1];
+ n += 2;
+ break;
+ case "L":
+ switch (g) {
+ case 4:
+ p = v.linesIntersection(h, e, u[n], u[n + 1], t, d, s, c);
+ if (p) {
+ x.push(p)
+ }
+ break;
+ case 8:
+ p = v.cubicLineIntersections(t, s, r, o, d, c, b, a, h, e, u[n], u[n + 1]);
+ x.push.apply(x, p);
+ break
+ }
+ h = u[n];
+ e = u[n + 1];
+ n += 2;
+ break;
+ case "C":
+ switch (g) {
+ case 4:
+ p = v.cubicLineIntersections(h, u[n], u[n + 2], u[n + 4], e, u[n + 1], u[n + 3], u[n + 5], t, d, s, c);
+ x.push.apply(x, p);
+ break;
+ case 8:
+ p = v.cubicsIntersections(h, u[n], u[n + 2], u[n + 4], e, u[n + 1], u[n + 3], u[n + 5], t, s, r, o, d, c, b, a);
+ x.push.apply(x, p);
+ break
+ }
+ h = u[n + 4];
+ e = u[n + 5];
+ n += 6;
+ break;
+ case "Z":
+ if (m !== null) {
+ switch (g) {
+ case 4:
+ p = v.linesIntersection(m, l, h, e, t, d, s, c);
+ if (p) {
+ x.push(p)
+ }
+ break;
+ case 8:
+ p = v.cubicLineIntersections(t, s, r, o, d, c, b, a, m, l, h, e);
+ x.push.apply(x, p);
+ break
+ }
+ }
+ break
+ }
+ }
+ return x
+ },
+ getIntersections: function(o) {
+ var m = this,
+ c = m.commands,
+ g = m.params,
+ l = c.length,
+ f = null,
+ e = null,
+ b = 0,
+ a = 0,
+ d = [],
+ k, h, n;
+ for (k = 0, h = 0; k < l; k++) {
+ switch (c[k]) {
+ case "M":
+ if (f !== null) {
+ n = o.getSegmentIntersections.call(o, f, e, b, a);
+ d.push.apply(d, n)
+ }
+ f = b = g[h];
+ e = a = g[h + 1];
+ h += 2;
+ break;
+ case "L":
+ n = o.getSegmentIntersections.call(o, b, a, g[h], g[h + 1]);
+ d.push.apply(d, n);
+ b = g[h];
+ a = g[h + 1];
+ h += 2;
+ break;
+ case "C":
+ n = o.getSegmentIntersections.call(o, b, a, g[h], g[h + 1], g[h + 2], g[h + 3], g[h + 4], g[h + 5]);
+ d.push.apply(d, n);
+ b = g[h + 4];
+ a = g[h + 5];
+ h += 6;
+ break;
+ case "Z":
+ if (f !== null) {
+ n = o.getSegmentIntersections.call(o, f, e, b, a);
+ d.push.apply(d, n)
+ }
+ break
+ }
+ }
+ return d
+ }
+});
+Ext.define("Ext.draw.sprite.Path", {
+ extend: "Ext.draw.sprite.Sprite",
+ requires: ["Ext.draw.Draw", "Ext.draw.Path"],
+ alias: ["sprite.path", "Ext.draw.Sprite"],
+ type: "path",
+ isPath: true,
+ inheritableStatics: {
+ def: {
+ processors: {
+ path: function(b, a) {
+ if (!(b instanceof Ext.draw.Path)) {
+ b = new Ext.draw.Path(b)
+ }
+ return b
+ }
+ },
+ aliases: {
+ d: "path"
+ },
+ triggers: {
+ path: "bbox"
+ },
+ updaters: {
+ path: function(a) {
+ var b = a.path;
+ if (!b || b.bindAttr !== a) {
+ b = new Ext.draw.Path();
+ b.bindAttr = a;
+ a.path = b
+ }
+ b.clear();
+ this.updatePath(b, a);
+ this.scheduleUpdater(a, "bbox", ["path"])
+ }
+ }
+ }
+ },
+ updatePlainBBox: function(a) {
+ if (this.attr.path) {
+ this.attr.path.getDimension(a)
+ }
+ },
+ updateTransformedBBox: function(a) {
+ if (this.attr.path) {
+ this.attr.path.getDimensionWithTransform(this.attr.matrix, a)
+ }
+ },
+ render: function(b, c) {
+ var d = this.attr.matrix,
+ a = this.attr;
+ if (!a.path || a.path.params.length === 0) {
+ return
+ }
+ d.toContext(c);
+ c.appendPath(a.path);
+ c.fillStroke(a)
+ },
+ updatePath: function(b, a) {}
+});
+Ext.define("Ext.draw.overrides.sprite.Path", {
+ override: "Ext.draw.sprite.Path",
+ requires: ["Ext.draw.Color"],
+ isPointInPath: function(c, g) {
+ var b = this.attr;
+ if (b.fillStyle === Ext.draw.Color.RGBA_NONE) {
+ return this.isPointOnPath(c, g)
+ }
+ var e = b.path,
+ d = b.matrix,
+ f, a;
+ if (!d.isIdentity()) {
+ f = e.params.slice(0);
+ e.transform(b.matrix)
+ }
+ a = e.isPointInPath(c, g);
+ if (f) {
+ e.params = f
+ }
+ return a
+ },
+ isPointOnPath: function(c, g) {
+ var b = this.attr,
+ e = b.path,
+ d = b.matrix,
+ f, a;
+ if (!d.isIdentity()) {
+ f = e.params.slice(0);
+ e.transform(b.matrix)
+ }
+ a = e.isPointOnPath(c, g);
+ if (f) {
+ e.params = f
+ }
+ return a
+ },
+ hitTest: function(i, l) {
+ var e = this,
+ c = e.attr,
+ k = c.path,
+ g = c.matrix,
+ h = i[0],
+ f = i[1],
+ d = e.callParent([i, l]),
+ j = null,
+ a, b;
+ if (!d) {
+ return j
+ }
+ l = l || Ext.draw.sprite.Sprite.defaultHitTestOptions;
+ if (!g.isIdentity()) {
+ a = k.params.slice(0);
+ k.transform(c.matrix)
+ }
+ if (l.fill && l.stroke) {
+ b = c.fillStyle !== Ext.draw.Color.NONE && c.fillStyle !== Ext.draw.Color.RGBA_NONE;
+ if (b) {
+ if (k.isPointInPath(h, f)) {
+ j = {
+ sprite: e
+ }
+ }
+ } else {
+ if (k.isPointInPath(h, f) || k.isPointOnPath(h, f)) {
+ j = {
+ sprite: e
+ }
+ }
+ }
+ } else {
+ if (l.stroke && !l.fill) {
+ if (k.isPointOnPath(h, f)) {
+ j = {
+ sprite: e
+ }
+ }
+ } else {
+ if (l.fill && !l.stroke) {
+ if (k.isPointInPath(h, f)) {
+ j = {
+ sprite: e
+ }
+ }
+ }
+ }
+ }
+ if (a) {
+ k.params = a
+ }
+ return j
+ },
+ getIntersections: function(j) {
+ if (!(j.isSprite && j.isPath)) {
+ return []
+ }
+ var e = this.attr,
+ d = j.attr,
+ i = e.path,
+ h = d.path,
+ g = e.matrix,
+ a = d.matrix,
+ c, f, b;
+ if (!g.isIdentity()) {
+ c = i.params.slice(0);
+ i.transform(e.matrix)
+ }
+ if (!a.isIdentity()) {
+ f = h.params.slice(0);
+ h.transform(d.matrix)
+ }
+ b = i.getIntersections(h);
+ if (c) {
+ i.params = c
+ }
+ if (f) {
+ h.params = f
+ }
+ return b
+ }
+});
+Ext.define("Ext.draw.sprite.Circle", {
+ extend: "Ext.draw.sprite.Path",
+ alias: "sprite.circle",
+ type: "circle",
+ inheritableStatics: {
+ def: {
+ processors: {
+ cx: "number",
+ cy: "number",
+ r: "number"
+ },
+ aliases: {
+ radius: "r",
+ x: "cx",
+ y: "cy",
+ centerX: "cx",
+ centerY: "cy"
+ },
+ defaults: {
+ cx: 0,
+ cy: 0,
+ r: 4
+ },
+ triggers: {
+ cx: "path",
+ cy: "path",
+ r: "path"
+ }
+ }
+ },
+ updatePlainBBox: function(c) {
+ var b = this.attr,
+ a = b.cx,
+ e = b.cy,
+ d = b.r;
+ c.x = a - d;
+ c.y = e - d;
+ c.width = d + d;
+ c.height = d + d
+ },
+ updateTransformedBBox: function(d) {
+ var g = this.attr,
+ f = g.cx,
+ e = g.cy,
+ a = g.r,
+ h = g.matrix,
+ j = h.getScaleX(),
+ i = h.getScaleY(),
+ c, b;
+ c = j * a;
+ b = i * a;
+ d.x = h.x(f, e) - c;
+ d.y = h.y(f, e) - b;
+ d.width = c + c;
+ d.height = b + b
+ },
+ updatePath: function(b, a) {
+ b.arc(a.cx, a.cy, a.r, 0, Math.PI * 2, false)
+ }
+});
+Ext.define("Ext.draw.sprite.Arc", {
+ extend: "Ext.draw.sprite.Circle",
+ alias: "sprite.arc",
+ type: "arc",
+ inheritableStatics: {
+ def: {
+ processors: {
+ startAngle: "number",
+ endAngle: "number",
+ anticlockwise: "bool"
+ },
+ aliases: {
+ from: "startAngle",
+ to: "endAngle",
+ start: "startAngle",
+ end: "endAngle"
+ },
+ defaults: {
+ startAngle: 0,
+ endAngle: Math.PI * 2,
+ anticlockwise: false
+ },
+ triggers: {
+ startAngle: "path",
+ endAngle: "path",
+ anticlockwise: "path"
+ }
+ }
+ },
+ updatePath: function(b, a) {
+ b.arc(a.cx, a.cy, a.r, a.startAngle, a.endAngle, a.anticlockwise)
+ }
+});
+Ext.define("Ext.draw.sprite.Arrow", {
+ extend: "Ext.draw.sprite.Path",
+ alias: "sprite.arrow",
+ inheritableStatics: {
+ def: {
+ processors: {
+ x: "number",
+ y: "number",
+ size: "number"
+ },
+ defaults: {
+ x: 0,
+ y: 0,
+ size: 4
+ },
+ triggers: {
+ x: "path",
+ y: "path",
+ size: "path"
+ }
+ }
+ },
+ updatePath: function(d, b) {
+ var c = b.size * 1.5,
+ a = b.x - b.lineWidth / 2,
+ e = b.y;
+ d.fromSvgString("M".concat(a - c * 0.7, ",", e - c * 0.4, "l", [c * 0.6, 0, 0, -c * 0.4, c, c * 0.8, -c, c * 0.8, 0, -c * 0.4, -c * 0.6, 0], "z"))
+ }
+});
+Ext.define("Ext.draw.sprite.Composite", {
+ extend: "Ext.draw.sprite.Sprite",
+ alias: "sprite.composite",
+ type: "composite",
+ isComposite: true,
+ config: {
+ sprites: []
+ },
+ constructor: function() {
+ this.sprites = [];
+ this.sprites.map = {};
+ this.callParent(arguments)
+ },
+ add: function(c) {
+ if (!c) {
+ return null
+ }
+ if (!c.isSprite) {
+ c = Ext.create("sprite." + c.type, c);
+ c.setParent(this);
+ c.setSurface(this.getSurface())
+ }
+ var d = this,
+ a = d.attr,
+ b = c.applyTransformations;
+ c.applyTransformations = function() {
+ if (c.attr.dirtyTransform) {
+ a.dirtyTransform = true;
+ a.bbox.plain.dirty = true;
+ a.bbox.transform.dirty = true
+ }
+ b.call(c)
+ };
+ d.sprites.push(c);
+ d.sprites.map[c.id] = c.getId();
+ a.bbox.plain.dirty = true;
+ a.bbox.transform.dirty = true;
+ return c
+ },
+ updateSurface: function(a) {
+ for (var b = 0, c = this.sprites.length; b < c; b++) {
+ this.sprites[b].setSurface(a)
+ }
+ },
+ addAll: function(b) {
+ if (b.isSprite || b.type) {
+ this.add(b)
+ } else {
+ if (Ext.isArray(b)) {
+ var a = 0;
+ while (a < b.length) {
+ this.add(b[a++])
+ }
+ }
+ }
+ },
+ updatePlainBBox: function(g) {
+ var e = this,
+ b = Infinity,
+ h = -Infinity,
+ f = Infinity,
+ a = -Infinity,
+ j, k, c, d;
+ for (c = 0, d = e.sprites.length; c < d; c++) {
+ j = e.sprites[c];
+ j.applyTransformations();
+ k = j.getBBox();
+ if (b > k.x) {
+ b = k.x
+ }
+ if (h < k.x + k.width) {
+ h = k.x + k.width
+ }
+ if (f > k.y) {
+ f = k.y
+ }
+ if (a < k.y + k.height) {
+ a = k.y + k.height
+ }
+ }
+ g.x = b;
+ g.y = f;
+ g.width = h - b;
+ g.height = a - f
+ },
+ render: function(a, b, f) {
+ var d = this.attr.matrix,
+ c, e;
+ d.toContext(b);
+ for (c = 0, e = this.sprites.length; c < e; c++) {
+ a.renderSprite(this.sprites[c], f)
+ }
+ },
+ destroy: function() {
+ var c = this,
+ d = c.sprites,
+ b = d.length,
+ a;
+ c.callParent();
+ for (a = 0; a < b; a++) {
+ d[a].destroy()
+ }
+ d.length = 0
+ }
+});
+Ext.define("Ext.draw.sprite.Cross", {
+ extend: "Ext.draw.sprite.Path",
+ alias: "sprite.cross",
+ inheritableStatics: {
+ def: {
+ processors: {
+ x: "number",
+ y: "number",
+ size: "number"
+ },
+ defaults: {
+ x: 0,
+ y: 0,
+ size: 4
+ },
+ triggers: {
+ x: "path",
+ y: "path",
+ size: "path"
+ }
+ }
+ },
+ updatePath: function(d, b) {
+ var c = b.size / 1.7,
+ a = b.x - b.lineWidth / 2,
+ e = b.y;
+ d.fromSvgString("M".concat(a - c, ",", e, "l", [-c, -c, c, -c, c, c, c, -c, c, c, -c, c, c, c, -c, c, -c, -c, -c, c, -c, -c, "z"]))
+ }
+});
+Ext.define("Ext.draw.sprite.Diamond", {
+ extend: "Ext.draw.sprite.Path",
+ alias: "sprite.diamond",
+ inheritableStatics: {
+ def: {
+ processors: {
+ x: "number",
+ y: "number",
+ size: "number"
+ },
+ defaults: {
+ x: 0,
+ y: 0,
+ size: 4
+ },
+ triggers: {
+ x: "path",
+ y: "path",
+ size: "path"
+ }
+ }
+ },
+ updatePath: function(d, b) {
+ var c = b.size * 1.25,
+ a = b.x - b.lineWidth / 2,
+ e = b.y;
+ d.fromSvgString(["M", a, e - c, "l", c, c, -c, c, -c, -c, c, -c, "z"])
+ }
+});
+Ext.define("Ext.draw.sprite.Ellipse", {
+ extend: "Ext.draw.sprite.Path",
+ alias: "sprite.ellipse",
+ type: "ellipse",
+ inheritableStatics: {
+ def: {
+ processors: {
+ cx: "number",
+ cy: "number",
+ rx: "number",
+ ry: "number",
+ axisRotation: "number"
+ },
+ aliases: {
+ radius: "r",
+ x: "cx",
+ y: "cy",
+ centerX: "cx",
+ centerY: "cy",
+ radiusX: "rx",
+ radiusY: "ry"
+ },
+ defaults: {
+ cx: 0,
+ cy: 0,
+ rx: 1,
+ ry: 1,
+ axisRotation: 0
+ },
+ triggers: {
+ cx: "path",
+ cy: "path",
+ rx: "path",
+ ry: "path",
+ axisRotation: "path"
+ }
+ }
+ },
+ updatePlainBBox: function(c) {
+ var b = this.attr,
+ a = b.cx,
+ f = b.cy,
+ e = b.rx,
+ d = b.ry;
+ c.x = a - e;
+ c.y = f - d;
+ c.width = e + e;
+ c.height = d + d
+ },
+ updateTransformedBBox: function(d) {
+ var i = this.attr,
+ f = i.cx,
+ e = i.cy,
+ c = i.rx,
+ b = i.ry,
+ l = b / c,
+ m = i.matrix.clone(),
+ a, q, k, j, p, o, n, g;
+ m.append(1, 0, 0, l, 0, e * (1 - l));
+ a = m.getXX();
+ k = m.getYX();
+ p = m.getDX();
+ q = m.getXY();
+ j = m.getYY();
+ o = m.getDY();
+ n = Math.sqrt(a * a + k * k) * c;
+ g = Math.sqrt(q * q + j * j) * c;
+ d.x = f * a + e * k + p - n;
+ d.y = f * q + e * j + o - g;
+ d.width = n + n;
+ d.height = g + g
+ },
+ updatePath: function(b, a) {
+ b.ellipse(a.cx, a.cy, a.rx, a.ry, a.axisRotation, 0, Math.PI * 2, false)
+ }
+});
+Ext.define("Ext.draw.sprite.EllipticalArc", {
+ extend: "Ext.draw.sprite.Ellipse",
+ alias: "sprite.ellipticalArc",
+ type: "ellipticalArc",
+ inheritableStatics: {
+ def: {
+ processors: {
+ startAngle: "number",
+ endAngle: "number",
+ anticlockwise: "bool"
+ },
+ aliases: {
+ from: "startAngle",
+ to: "endAngle",
+ start: "startAngle",
+ end: "endAngle"
+ },
+ defaults: {
+ startAngle: 0,
+ endAngle: Math.PI * 2,
+ anticlockwise: false
+ },
+ triggers: {
+ startAngle: "path",
+ endAngle: "path",
+ anticlockwise: "path"
+ }
+ }
+ },
+ updatePath: function(b, a) {
+ b.ellipse(a.cx, a.cy, a.rx, a.ry, a.axisRotation, a.startAngle, a.endAngle, a.anticlockwise)
+ }
+});
+Ext.define("Ext.draw.sprite.Rect", {
+ extend: "Ext.draw.sprite.Path",
+ alias: "sprite.rect",
+ type: "rect",
+ inheritableStatics: {
+ def: {
+ processors: {
+ x: "number",
+ y: "number",
+ width: "number",
+ height: "number",
+ radius: "number"
+ },
+ aliases: {},
+ triggers: {
+ x: "path",
+ y: "path",
+ width: "path",
+ height: "path",
+ radius: "path"
+ },
+ defaults: {
+ x: 0,
+ y: 0,
+ width: 8,
+ height: 8,
+ radius: 0
+ }
+ }
+ },
+ updatePlainBBox: function(b) {
+ var a = this.attr;
+ b.x = a.x;
+ b.y = a.y;
+ b.width = a.width;
+ b.height = a.height
+ },
+ updateTransformedBBox: function(a, b) {
+ this.attr.matrix.transformBBox(b, this.attr.radius, a)
+ },
+ updatePath: function(f, d) {
+ var c = d.x,
+ g = d.y,
+ e = d.width,
+ b = d.height,
+ a = Math.min(d.radius, Math.abs(d.height) * 0.5, Math.abs(d.width) * 0.5);
+ if (a === 0) {
+ f.rect(c, g, e, b)
+ } else {
+ f.moveTo(c + a, g);
+ f.arcTo(c + e, g, c + e, g + b, a);
+ f.arcTo(c + e, g + b, c, g + b, a);
+ f.arcTo(c, g + b, c, g, a);
+ f.arcTo(c, g, c + a, g, a)
+ }
+ }
+});
+Ext.define("Ext.draw.sprite.Image", {
+ extend: "Ext.draw.sprite.Rect",
+ alias: "sprite.image",
+ type: "image",
+ statics: {
+ imageLoaders: {}
+ },
+ inheritableStatics: {
+ def: {
+ processors: {
+ src: "string"
+ },
+ defaults: {
+ src: "",
+ width: null,
+ height: null
+ }
+ }
+ },
+ render: function(c, o) {
+ var j = this,
+ h = j.attr,
+ n = h.matrix,
+ a = h.src,
+ l = h.x,
+ k = h.y,
+ b = h.width,
+ m = h.height,
+ g = Ext.draw.sprite.Image.imageLoaders[a],
+ f, d, e;
+ if (g && g.done) {
+ n.toContext(o);
+ d = g.image;
+ o.drawImage(d, l, k, b || (d.naturalWidth || d.width) / c.devicePixelRatio, m || (d.naturalHeight || d.height) / c.devicePixelRatio)
+ } else {
+ if (!g) {
+ f = new Image();
+ g = Ext.draw.sprite.Image.imageLoaders[a] = {
+ image: f,
+ done: false,
+ pendingSprites: [j],
+ pendingSurfaces: [c]
+ };
+ f.width = b;
+ f.height = m;
+ f.onload = function() {
+ if (!g.done) {
+ g.done = true;
+ for (e = 0; e < g.pendingSprites.length; e++) {
+ g.pendingSprites[e].setDirty(true)
+ }
+ for (e in g.pendingSurfaces) {
+ g.pendingSurfaces[e].renderFrame()
+ }
+ }
+ };
+ f.src = a
+ } else {
+ Ext.Array.include(g.pendingSprites, j);
+ Ext.Array.include(g.pendingSurfaces, c)
+ }
+ }
+ }
+});
+Ext.define("Ext.draw.sprite.Instancing", {
+ extend: "Ext.draw.sprite.Sprite",
+ alias: "sprite.instancing",
+ type: "instancing",
+ isInstancing: true,
+ config: {
+ template: null
+ },
+ instances: null,
+ applyTemplate: function(a) {
+ if (!a.isSprite) {
+ if (!a.xclass && !a.type) {
+ a.type = "circle"
+ }
+ a = Ext.create(a.xclass || "sprite." + a.type, a)
+ }
+ a.setParent(this);
+ return a
+ },
+ updateTemplate: function(a, b) {
+ if (b) {
+ delete b.ownAttr
+ }
+ a.setSurface(this.getSurface());
+ a.ownAttr = a.attr;
+ this.clearAll()
+ },
+ updateSurface: function(a) {
+ var b = this.getTemplate();
+ if (b) {
+ b.setSurface(a)
+ }
+ },
+ get: function(a) {
+ return this.instances[a]
+ },
+ getCount: function() {
+ return this.instances.length
+ },
+ clearAll: function() {
+ var a = this.getTemplate();
+ a.attr.children = this.instances = [];
+ this.position = 0
+ },
+ createInstance: function(d, f, c) {
+ var e = this.getTemplate(),
+ b = e.attr,
+ a = Ext.Object.chain(b);
+ e.topModifier.prepareAttributes(a);
+ e.attr = a;
+ e.setAttributes(d, f, c);
+ a.template = e;
+ this.instances.push(a);
+ e.attr = b;
+ this.position++;
+ return a
+ },
+ getBBox: function() {
+ return null
+ },
+ getBBoxFor: function(b, d) {
+ var c = this.getTemplate(),
+ a = c.attr,
+ e;
+ c.attr = this.instances[b];
+ e = c.getBBox(d);
+ c.attr = a;
+ return e
+ },
+ isVisible: function() {
+ var b = this.attr,
+ c = this.getParent(),
+ a;
+ a = c && c.isSurface && !b.hidden && b.globalAlpha;
+ return !!a
+ },
+ isInstanceVisible: function(c) {
+ var e = this,
+ d = e.getTemplate(),
+ b = d.attr,
+ f = e.instances,
+ a = false;
+ if (!Ext.isNumber(c) || c < 0 || c >= f.length || !e.isVisible()) {
+ return a
+ }
+ d.attr = f[c];
+ a = d.isVisible(point, options);
+ d.attr = b;
+ return a
+ },
+ render: function(b, l, d, h) {
+ var g = this,
+ j = g.getTemplate(),
+ k = g.attr.matrix,
+ c = j.attr,
+ a = g.instances,
+ e, f = g.position;
+ k.toContext(l);
+ j.preRender(b, l, d, h);
+ j.useAttributes(l, h);
+ for (e = 0; e < f; e++) {
+ if (a[e].dirtyZIndex) {
+ break
+ }
+ }
+ for (e = 0; e < f; e++) {
+ if (a[e].hidden) {
+ continue
+ }
+ l.save();
+ j.attr = a[e];
+ j.useAttributes(l, h);
+ j.render(b, l, d, h);
+ l.restore()
+ }
+ j.attr = c
+ },
+ setAttributesFor: function(c, e, f) {
+ var d = this.getTemplate(),
+ b = d.attr,
+ a = this.instances[c];
+ if (!a) {
+ return
+ }
+ d.attr = a;
+ if (f) {
+ e = Ext.apply({}, e)
+ } else {
+ e = d.self.def.normalize(e)
+ }
+ d.topModifier.pushDown(a, e);
+ d.attr = b
+ },
+ destroy: function() {
+ var b = this,
+ a = b.getTemplate();
+ b.instances = null;
+ if (a) {
+ a.destroy()
+ }
+ b.callParent()
+ }
+});
+Ext.define("Ext.draw.overrides.sprite.Instancing", {
+ override: "Ext.draw.sprite.Instancing",
+ hitTest: function(f, j) {
+ var e = this,
+ g = e.getTemplate(),
+ b = g.attr,
+ a = e.instances,
+ d = a.length,
+ c = 0,
+ h = null;
+ if (!e.isVisible()) {
+ return h
+ }
+ for (; c < d; c++) {
+ g.attr = a[c];
+ h = g.hitTest(f, j);
+ if (h) {
+ h.isInstance = true;
+ h.template = h.sprite;
+ h.sprite = this;
+ h.instance = a[c];
+ h.index = c;
+ return h
+ }
+ }
+ g.attr = b;
+ return h
+ }
+});
+Ext.define("Ext.draw.sprite.Line", {
+ extend: "Ext.draw.sprite.Sprite",
+ alias: "sprite.line",
+ type: "line",
+ inheritableStatics: {
+ def: {
+ processors: {
+ fromX: "number",
+ fromY: "number",
+ toX: "number",
+ toY: "number"
+ },
+ defaults: {
+ fromX: 0,
+ fromY: 0,
+ toX: 1,
+ toY: 1,
+ strokeStyle: "black"
+ },
+ aliases: {
+ x1: "fromX",
+ y1: "fromY",
+ x2: "toX",
+ y2: "toY"
+ }
+ }
+ },
+ updateLineBBox: function(b, i, s, g, r, f) {
+ var o = this.attr,
+ q = o.matrix,
+ h = o.lineWidth / 2,
+ m, l, d, c, k, j, n;
+ if (i) {
+ n = q.transformPoint([s, g]);
+ s = n[0];
+ g = n[1];
+ n = q.transformPoint([r, f]);
+ r = n[0];
+ f = n[1]
+ }
+ m = Math.min(s, r);
+ d = Math.max(s, r);
+ l = Math.min(g, f);
+ c = Math.max(g, f);
+ var t = Math.atan2(d - m, c - l),
+ a = Math.sin(t),
+ e = Math.cos(t),
+ k = h * e,
+ j = h * a;
+ m -= k;
+ l -= j;
+ d += k;
+ c += j;
+ b.x = m;
+ b.y = l;
+ b.width = d - m;
+ b.height = c - l
+ },
+ updatePlainBBox: function(b) {
+ var a = this.attr;
+ this.updateLineBBox(b, false, a.fromX, a.fromY, a.toX, a.toY)
+ },
+ updateTransformedBBox: function(b, c) {
+ var a = this.attr;
+ this.updateLineBBox(b, true, a.fromX, a.fromY, a.toX, a.toY)
+ },
+ render: function(b, c) {
+ var a = this.attr,
+ d = this.attr.matrix;
+ d.toContext(c);
+ c.beginPath();
+ c.moveTo(a.fromX, a.fromY);
+ c.lineTo(a.toX, a.toY);
+ c.stroke()
+ }
+});
+Ext.define("Ext.draw.sprite.Plus", {
+ extend: "Ext.draw.sprite.Path",
+ alias: "sprite.plus",
+ inheritableStatics: {
+ def: {
+ processors: {
+ x: "number",
+ y: "number",
+ size: "number"
+ },
+ defaults: {
+ x: 0,
+ y: 0,
+ size: 4
+ },
+ triggers: {
+ x: "path",
+ y: "path",
+ size: "path"
+ }
+ }
+ },
+ updatePath: function(d, b) {
+ var c = b.size / 1.3,
+ a = b.x - b.lineWidth / 2,
+ e = b.y;
+ d.fromSvgString("M".concat(a - c / 2, ",", e - c / 2, "l", [0, -c, c, 0, 0, c, c, 0, 0, c, -c, 0, 0, c, -c, 0, 0, -c, -c, 0, 0, -c, "z"]))
+ }
+});
+Ext.define("Ext.draw.sprite.Sector", {
+ extend: "Ext.draw.sprite.Path",
+ alias: "sprite.sector",
+ type: "sector",
+ inheritableStatics: {
+ def: {
+ processors: {
+ centerX: "number",
+ centerY: "number",
+ startAngle: "number",
+ endAngle: "number",
+ startRho: "number",
+ endRho: "number",
+ margin: "number"
+ },
+ aliases: {
+ rho: "endRho"
+ },
+ triggers: {
+ centerX: "path,bbox",
+ centerY: "path,bbox",
+ startAngle: "path,bbox",
+ endAngle: "path,bbox",
+ startRho: "path,bbox",
+ endRho: "path,bbox",
+ margin: "path,bbox"
+ },
+ defaults: {
+ centerX: 0,
+ centerY: 0,
+ startAngle: 0,
+ endAngle: 0,
+ startRho: 0,
+ endRho: 150,
+ margin: 0,
+ path: "M 0,0"
+ }
+ }
+ },
+ getMidAngle: function() {
+ return this.midAngle || 0
+ },
+ updatePath: function(j, h) {
+ var g = Math.min(h.startAngle, h.endAngle),
+ c = Math.max(h.startAngle, h.endAngle),
+ b = this.midAngle = (g + c) * 0.5,
+ d = h.margin,
+ f = h.centerX,
+ e = h.centerY,
+ i = Math.min(h.startRho, h.endRho),
+ a = Math.max(h.startRho, h.endRho);
+ if (d) {
+ f += d * Math.cos(b);
+ e += d * Math.sin(b)
+ }
+ j.moveTo(f + i * Math.cos(g), e + i * Math.sin(g));
+ j.lineTo(f + a * Math.cos(g), e + a * Math.sin(g));
+ j.arc(f, e, a, g, c, false);
+ j.lineTo(f + i * Math.cos(c), e + i * Math.sin(c));
+ j.arc(f, e, i, c, g, true)
+ }
+});
+Ext.define("Ext.draw.sprite.Square", {
+ extend: "Ext.draw.sprite.Rect",
+ alias: "sprite.square",
+ inheritableStatics: {
+ def: {
+ processors: {
+ size: "number"
+ },
+ defaults: {
+ size: 4
+ },
+ triggers: {
+ size: "size"
+ },
+ updaters: {
+ size: function(a) {
+ var c = a.size,
+ b = a.lineWidth / 2;
+ this.setAttributes({
+ x: a.x - c - b,
+ y: a.y - c,
+ height: 2 * c,
+ width: 2 * c
+ })
+ }
+ }
+ }
+ }
+});
+Ext.define("Ext.draw.TextMeasurer", {
+ singleton: true,
+ requires: ["Ext.util.TextMetrics"],
+ measureDiv: null,
+ measureCache: {},
+ precise: Ext.isIE8,
+ measureDivTpl: {
+ tag: "div",
+ style: {
+ overflow: "hidden",
+ position: "relative",
+ "float": "left",
+ width: 0,
+ height: 0
+ },
+ children: {
+ tag: "div",
+ style: {
+ display: "block",
+ position: "absolute",
+ x: -100000,
+ y: -100000,
+ padding: 0,
+ margin: 0,
+ "z-index": -100000,
+ "white-space": "nowrap"
+ }
+ }
+ },
+ actualMeasureText: function(g, b) {
+ var e = Ext.draw.TextMeasurer,
+ f = e.measureDiv,
+ a = 100000,
+ c;
+ if (!f) {
+ var d = Ext.Element.create({
+ style: {
+ overflow: "hidden",
+ position: "relative",
+ "float": "left",
+ width: 0,
+ height: 0
+ }
+ });
+ e.measureDiv = f = Ext.Element.create({
+ style: {
+ position: "absolute",
+ x: a,
+ y: a,
+ "z-index": -a,
+ "white-space": "nowrap",
+ display: "block",
+ padding: 0,
+ margin: 0
+ }
+ });
+ Ext.getBody().appendChild(d);
+ d.appendChild(f)
+ }
+ if (b) {
+ f.setStyle({
+ font: b,
+ lineHeight: "normal"
+ })
+ }
+ f.setText("(" + g + ")");
+ c = f.getSize();
+ f.setText("()");
+ c.width -= f.getSize().width;
+ return c
+ },
+ measureTextSingleLine: function(h, d) {
+ if (this.precise) {
+ return this.preciseMeasureTextSingleLine(h, d)
+ }
+ h = h.toString();
+ var a = this.measureCache,
+ g = h.split(""),
+ c = 0,
+ j = 0,
+ l, b, e, f, k;
+ if (!a[d]) {
+ a[d] = {}
+ }
+ a = a[d];
+ if (a[h]) {
+ return a[h]
+ }
+ for (e = 0, f = g.length; e < f; e++) {
+ b = g[e];
+ if (!(l = a[b])) {
+ k = this.actualMeasureText(b, d);
+ l = a[b] = k
+ }
+ c += l.width;
+ j = Math.max(j, l.height)
+ }
+ return a[h] = {
+ width: c,
+ height: j
+ }
+ },
+ preciseMeasureTextSingleLine: function(c, a) {
+ c = c.toString();
+ var b = this.measureDiv || (this.measureDiv = Ext.getBody().createChild(this.measureDivTpl).down("div"));
+ b.setStyle({
+ font: a || ""
+ });
+ return Ext.util.TextMetrics.measure(b, c)
+ },
+ measureText: function(e, b) {
+ var h = e.split("\n"),
+ d = h.length,
+ f = 0,
+ a = 0,
+ j, c, g;
+ if (d === 1) {
+ return this.measureTextSingleLine(e, b)
+ }
+ g = [];
+ for (c = 0; c < d; c++) {
+ j = this.measureTextSingleLine(h[c], b);
+ g.push(j);
+ f += j.height;
+ a = Math.max(a, j.width)
+ }
+ return {
+ width: a,
+ height: f,
+ sizes: g
+ }
+ }
+});
+Ext.define("Ext.draw.sprite.Text", function() {
+ var d = {
+ "xx-small": true,
+ "x-small": true,
+ small: true,
+ medium: true,
+ large: true,
+ "x-large": true,
+ "xx-large": true
+ };
+ var b = {
+ normal: true,
+ bold: true,
+ bolder: true,
+ lighter: true,
+ 100: true,
+ 200: true,
+ 300: true,
+ 400: true,
+ 500: true,
+ 600: true,
+ 700: true,
+ 800: true,
+ 900: true
+ };
+ var a = {
+ start: "start",
+ left: "start",
+ center: "center",
+ middle: "center",
+ end: "end",
+ right: "end"
+ };
+ var c = {
+ top: "top",
+ hanging: "hanging",
+ middle: "middle",
+ center: "middle",
+ alphabetic: "alphabetic",
+ ideographic: "ideographic",
+ bottom: "bottom"
+ };
+ return {
+ extend: "Ext.draw.sprite.Sprite",
+ requires: ["Ext.draw.TextMeasurer", "Ext.draw.Color"],
+ alias: "sprite.text",
+ type: "text",
+ lineBreakRe: /\r?\n/g,
+ inheritableStatics: {
+ def: {
+ animationProcessors: {
+ text: "text"
+ },
+ processors: {
+ x: "number",
+ y: "number",
+ text: "string",
+ fontSize: function(e) {
+ if (Ext.isNumber(+e)) {
+ return e + "px"
+ } else {
+ if (e.match(Ext.dom.Element.unitRe)) {
+ return e
+ } else {
+ if (e in d) {
+ return e
+ }
+ }
+ }
+ },
+ fontStyle: "enums(,italic,oblique)",
+ fontVariant: "enums(,small-caps)",
+ fontWeight: function(e) {
+ if (e in b) {
+ return String(e)
+ } else {
+ return ""
+ }
+ },
+ fontFamily: "string",
+ textAlign: function(e) {
+ return a[e] || "center"
+ },
+ textBaseline: function(e) {
+ return c[e] || "alphabetic"
+ },
+ font: "string"
+ },
+ aliases: {
+ "font-size": "fontSize",
+ "font-family": "fontFamily",
+ "font-weight": "fontWeight",
+ "font-variant": "fontVariant",
+ "text-anchor": "textAlign"
+ },
+ defaults: {
+ fontStyle: "",
+ fontVariant: "",
+ fontWeight: "",
+ fontSize: "10px",
+ fontFamily: "sans-serif",
+ font: "10px sans-serif",
+ textBaseline: "alphabetic",
+ textAlign: "start",
+ strokeStyle: "rgba(0, 0, 0, 0)",
+ fillStyle: "#000",
+ x: 0,
+ y: 0,
+ text: ""
+ },
+ triggers: {
+ fontStyle: "fontX,bbox",
+ fontVariant: "fontX,bbox",
+ fontWeight: "fontX,bbox",
+ fontSize: "fontX,bbox",
+ fontFamily: "fontX,bbox",
+ font: "font,bbox,canvas",
+ textBaseline: "bbox",
+ textAlign: "bbox",
+ x: "bbox",
+ y: "bbox",
+ text: "bbox"
+ },
+ updaters: {
+ fontX: "makeFontShorthand",
+ font: "parseFontShorthand"
+ }
+ }
+ },
+ constructor: function(e) {
+ if (e && e.font) {
+ e = Ext.clone(e);
+ for (var f in e) {
+ if (f !== "font" && f.indexOf("font") === 0) {
+ delete e[f]
+ }
+ }
+ }
+ Ext.draw.sprite.Sprite.prototype.constructor.call(this, e)
+ },
+ fontValuesMap: {
+ italic: "fontStyle",
+ oblique: "fontStyle",
+ "small-caps": "fontVariant",
+ bold: "fontWeight",
+ bolder: "fontWeight",
+ lighter: "fontWeight",
+ "100": "fontWeight",
+ "200": "fontWeight",
+ "300": "fontWeight",
+ "400": "fontWeight",
+ "500": "fontWeight",
+ "600": "fontWeight",
+ "700": "fontWeight",
+ "800": "fontWeight",
+ "900": "fontWeight",
+ "xx-small": "fontSize",
+ "x-small": "fontSize",
+ small: "fontSize",
+ medium: "fontSize",
+ large: "fontSize",
+ "x-large": "fontSize",
+ "xx-large": "fontSize"
+ },
+ makeFontShorthand: function(e) {
+ var f = [];
+ if (e.fontStyle) {
+ f.push(e.fontStyle)
+ }
+ if (e.fontVariant) {
+ f.push(e.fontVariant)
+ }
+ if (e.fontWeight) {
+ f.push(e.fontWeight)
+ }
+ if (e.fontSize) {
+ f.push(e.fontSize)
+ }
+ if (e.fontFamily) {
+ f.push(e.fontFamily)
+ }
+ this.setAttributes({
+ font: f.join(" ")
+ }, true)
+ },
+ parseFontShorthand: function(j) {
+ var m = j.font,
+ k = m.length,
+ l = {},
+ n = this.fontValuesMap,
+ e = 0,
+ i, g, f, h;
+ while (e < k && i !== -1) {
+ i = m.indexOf(" ", e);
+ if (i < 0) {
+ f = m.substr(e)
+ } else {
+ if (i > e) {
+ f = m.substr(e, i - e)
+ } else {
+ continue
+ }
+ }
+ g = f.indexOf("/");
+ if (g > 0) {
+ f = f.substr(0, g)
+ } else {
+ if (g === 0) {
+ continue
+ }
+ }
+ if (f !== "normal" && f !== "inherit") {
+ h = n[f];
+ if (h) {
+ l[h] = f
+ } else {
+ if (f.match(Ext.dom.Element.unitRe)) {
+ l.fontSize = f
+ } else {
+ l.fontFamily = m.substr(e);
+ break
+ }
+ }
+ }
+ e = i + 1
+ }
+ if (!l.fontStyle) {
+ l.fontStyle = ""
+ }
+ if (!l.fontVariant) {
+ l.fontVariant = ""
+ }
+ if (!l.fontWeight) {
+ l.fontWeight = ""
+ }
+ this.setAttributes(l, true)
+ },
+ fontProperties: {
+ fontStyle: true,
+ fontVariant: true,
+ fontWeight: true,
+ fontSize: true,
+ fontFamily: true
+ },
+ setAttributes: function(g, i, e) {
+ var f, h;
+ if (g && g.font) {
+ h = {};
+ for (f in g) {
+ if (!(f in this.fontProperties)) {
+ h[f] = g[f]
+ }
+ }
+ g = h
+ }
+ this.callParent([g, i, e])
+ },
+ getBBox: function(g) {
+ var h = this,
+ f = h.attr.bbox.plain,
+ e = h.getSurface();
+ if (f.dirty) {
+ h.updatePlainBBox(f);
+ f.dirty = false
+ }
+ if (e.getInherited().rtl && e.getFlipRtlText()) {
+ h.updatePlainBBox(f, true)
+ }
+ return h.callParent([g])
+ },
+ rtlAlignments: {
+ start: "end",
+ center: "center",
+ end: "start"
+ },
+ updatePlainBBox: function(k, B) {
+ var C = this,
+ w = C.attr,
+ o = w.x,
+ n = w.y,
+ q = [],
+ t = w.font,
+ r = w.text,
+ s = w.textBaseline,
+ l = w.textAlign,
+ u = (B && C.oldSize) ? C.oldSize : (C.oldSize = Ext.draw.TextMeasurer.measureText(r, t)),
+ z = C.getSurface(),
+ p = z.getInherited().rtl,
+ v = p && z.getFlipRtlText(),
+ h = z.getRect(),
+ f = u.sizes,
+ g = u.height,
+ j = u.width,
+ m = f ? f.length : 0,
+ e, A = 0;
+ switch (s) {
+ case "hanging":
+ case "top":
+ break;
+ case "ideographic":
+ case "bottom":
+ n -= g;
+ break;
+ case "alphabetic":
+ n -= g * 0.8;
+ break;
+ case "middle":
+ n -= g * 0.5;
+ break
+ }
+ if (v) {
+ o = h[2] - h[0] - o;
+ l = C.rtlAlignments[l]
+ }
+ switch (l) {
+ case "start":
+ if (p) {
+ for (; A < m; A++) {
+ e = f[A].width;
+ q.push(-(j - e))
+ }
+ }
+ break;
+ case "end":
+ o -= j;
+ if (p) {
+ break
+ }
+ for (; A < m; A++) {
+ e = f[A].width;
+ q.push(j - e)
+ }
+ break;
+ case "center":
+ o -= j * 0.5;
+ for (; A < m; A++) {
+ e = f[A].width;
+ q.push((p ? -1 : 1) * (j - e) * 0.5)
+ }
+ break
+ }
+ w.textAlignOffsets = q;
+ k.x = o;
+ k.y = n;
+ k.width = j;
+ k.height = g
+ },
+ setText: function(e) {
+ this.setAttributes({
+ text: e
+ }, true)
+ },
+ render: function(e, q, k) {
+ var h = this,
+ g = h.attr,
+ p = Ext.draw.Matrix.fly(g.matrix.elements.slice(0)),
+ o = h.getBBox(true),
+ s = g.textAlignOffsets,
+ m = Ext.draw.Color.RGBA_NONE,
+ l, j, f, r, n;
+ if (g.text.length === 0) {
+ return
+ }
+ r = g.text.split(h.lineBreakRe);
+ n = o.height / r.length;
+ l = g.bbox.plain.x;
+ j = g.bbox.plain.y + n * 0.78;
+ p.toContext(q);
+ if (e.getInherited().rtl) {
+ l += g.bbox.plain.width
+ }
+ for (f = 0; f < r.length; f++) {
+ if (q.fillStyle !== m) {
+ q.fillText(r[f], l + (s[f] || 0), j + n * f)
+ }
+ if (q.strokeStyle !== m) {
+ q.strokeText(r[f], l + (s[f] || 0), j + n * f)
+ }
+ }
+ }
+ }
+});
+Ext.define("Ext.draw.sprite.Tick", {
+ extend: "Ext.draw.sprite.Line",
+ alias: "sprite.tick",
+ inheritableStatics: {
+ def: {
+ processors: {
+ x: "number",
+ y: "number",
+ size: "number"
+ },
+ defaults: {
+ x: 0,
+ y: 0,
+ size: 4
+ },
+ triggers: {
+ x: "tick",
+ y: "tick",
+ size: "tick"
+ },
+ updaters: {
+ tick: function(b) {
+ var d = b.size * 1.5,
+ c = b.lineWidth / 2,
+ a = b.x,
+ e = b.y;
+ this.setAttributes({
+ fromX: a - c,
+ fromY: e - d,
+ toX: a - c,
+ toY: e + d
+ })
+ }
+ }
+ }
+ }
+});
+Ext.define("Ext.draw.sprite.Triangle", {
+ extend: "Ext.draw.sprite.Path",
+ alias: "sprite.triangle",
+ inheritableStatics: {
+ def: {
+ processors: {
+ x: "number",
+ y: "number",
+ size: "number"
+ },
+ defaults: {
+ x: 0,
+ y: 0,
+ size: 4
+ },
+ triggers: {
+ x: "path",
+ y: "path",
+ size: "path"
+ }
+ }
+ },
+ updatePath: function(d, b) {
+ var c = b.size * 2.2,
+ a = b.x,
+ e = b.y;
+ d.fromSvgString("M".concat(a, ",", e, "m0-", c * 0.58, "l", c * 0.5, ",", c * 0.87, "-", c, ",0z"))
+ }
+});
+Ext.define("Ext.draw.gradient.Linear", {
+ extend: "Ext.draw.gradient.Gradient",
+ requires: ["Ext.draw.Color"],
+ type: "linear",
+ config: {
+ degrees: 0,
+ radians: 0
+ },
+ applyRadians: function(b, a) {
+ if (Ext.isNumber(b)) {
+ return b
+ }
+ return a
+ },
+ applyDegrees: function(b, a) {
+ if (Ext.isNumber(b)) {
+ return b
+ }
+ return a
+ },
+ updateRadians: function(a) {
+ this.setDegrees(Ext.draw.Draw.degrees(a))
+ },
+ updateDegrees: function(a) {
+ this.setRadians(Ext.draw.Draw.rad(a))
+ },
+ generateGradient: function(q, o) {
+ var c = this.getRadians(),
+ p = Math.cos(c),
+ j = Math.sin(c),
+ m = o.width,
+ f = o.height,
+ d = o.x + m * 0.5,
+ b = o.y + f * 0.5,
+ n = this.getStops(),
+ g = n.length,
+ k, a, e;
+ if (Ext.isNumber(d + b) && f > 0 && m > 0) {
+ a = (Math.sqrt(f * f + m * m) * Math.abs(Math.cos(c - Math.atan(f / m)))) / 2;
+ k = q.createLinearGradient(d + p * a, b + j * a, d - p * a, b - j * a);
+ for (e = 0; e < g; e++) {
+ k.addColorStop(n[e].offset, n[e].color)
+ }
+ return k
+ }
+ return Ext.draw.Color.NONE
+ }
+});
+Ext.define("Ext.draw.gradient.Radial", {
+ extend: "Ext.draw.gradient.Gradient",
+ type: "radial",
+ config: {
+ start: {
+ x: 0,
+ y: 0,
+ r: 0
+ },
+ end: {
+ x: 0,
+ y: 0,
+ r: 1
+ }
+ },
+ applyStart: function(a, b) {
+ if (!b) {
+ return a
+ }
+ var c = {
+ x: b.x,
+ y: b.y,
+ r: b.r
+ };
+ if ("x" in a) {
+ c.x = a.x
+ } else {
+ if ("centerX" in a) {
+ c.x = a.centerX
+ }
+ }
+ if ("y" in a) {
+ c.y = a.y
+ } else {
+ if ("centerY" in a) {
+ c.y = a.centerY
+ }
+ }
+ if ("r" in a) {
+ c.r = a.r
+ } else {
+ if ("radius" in a) {
+ c.r = a.radius
+ }
+ }
+ return c
+ },
+ applyEnd: function(b, a) {
+ if (!a) {
+ return b
+ }
+ var c = {
+ x: a.x,
+ y: a.y,
+ r: a.r
+ };
+ if ("x" in b) {
+ c.x = b.x
+ } else {
+ if ("centerX" in b) {
+ c.x = b.centerX
+ }
+ }
+ if ("y" in b) {
+ c.y = b.y
+ } else {
+ if ("centerY" in b) {
+ c.y = b.centerY
+ }
+ }
+ if ("r" in b) {
+ c.r = b.r
+ } else {
+ if ("radius" in b) {
+ c.r = b.radius
+ }
+ }
+ return c
+ },
+ generateGradient: function(n, m) {
+ var a = this.getStart(),
+ b = this.getEnd(),
+ k = m.width * 0.5,
+ d = m.height * 0.5,
+ j = m.x + k,
+ f = m.y + d,
+ g = n.createRadialGradient(j + a.x * k, f + a.y * d, a.r * Math.max(k, d), j + b.x * k, f + b.y * d, b.r * Math.max(k, d)),
+ l = this.getStops(),
+ e = l.length,
+ c;
+ for (c = 0; c < e; c++) {
+ g.addColorStop(l[c].offset, l[c].color)
+ }
+ return g
+ }
+});
+Ext.define("Ext.draw.Surface", {
+ extend: "Ext.draw.SurfaceBase",
+ xtype: "surface",
+ requires: ["Ext.draw.sprite.*", "Ext.draw.gradient.*", "Ext.draw.sprite.AttributeDefinition", "Ext.draw.Matrix", "Ext.draw.Draw"],
+ uses: ["Ext.draw.engine.Canvas"],
+ devicePixelRatio: window.devicePixelRatio || window.screen.deviceXDPI / window.screen.logicalXDPI,
+ deprecated: {
+ "5.1.0": {
+ statics: {
+ methods: {
+ stableSort: function(a) {
+ return Ext.Array.sort(a, function(d, c) {
+ return d.attr.zIndex - c.attr.zIndex
+ })
+ }
+ }
+ }
+ }
+ },
+ config: {
+ cls: Ext.baseCSSPrefix + "surface",
+ rect: null,
+ background: null,
+ items: [],
+ dirty: false,
+ flipRtlText: false
+ },
+ isSurface: true,
+ isPendingRenderFrame: false,
+ dirtyPredecessorCount: 0,
+ constructor: function(a) {
+ var b = this;
+ b.predecessors = [];
+ b.successors = [];
+ b.map = {};
+ b.callParent([a]);
+ b.matrix = new Ext.draw.Matrix();
+ b.inverseMatrix = b.matrix.inverse()
+ },
+ roundPixel: function(a) {
+ return Math.round(this.devicePixelRatio * a) / this.devicePixelRatio
+ },
+ waitFor: function(a) {
+ var b = this,
+ c = b.predecessors;
+ if (!Ext.Array.contains(c, a)) {
+ c.push(a);
+ a.successors.push(b);
+ if (a.getDirty()) {
+ b.dirtyPredecessorCount++
+ }
+ }
+ },
+ updateDirty: function(d) {
+ var c = this.successors,
+ e = c.length,
+ b = 0,
+ a;
+ for (; b < e; b++) {
+ a = c[b];
+ if (d) {
+ a.dirtyPredecessorCount++;
+ a.setDirty(true)
+ } else {
+ a.dirtyPredecessorCount--;
+ if (a.dirtyPredecessorCount === 0 && a.isPendingRenderFrame) {
+ a.renderFrame()
+ }
+ }
+ }
+ },
+ applyBackground: function(a, b) {
+ this.setDirty(true);
+ if (Ext.isString(a)) {
+ a = {
+ fillStyle: a
+ }
+ }
+ return Ext.factory(a, Ext.draw.sprite.Rect, b)
+ },
+ applyRect: function(a, b) {
+ if (b && a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]) {
+ return
+ }
+ if (Ext.isArray(a)) {
+ return [a[0], a[1], a[2], a[3]]
+ } else {
+ if (Ext.isObject(a)) {
+ return [a.x || a.left, a.y || a.top, a.width || (a.right - a.left), a.height || (a.bottom - a.top)]
+ }
+ }
+ },
+ updateRect: function(i) {
+ var h = this,
+ c = i[0],
+ f = i[1],
+ g = c + i[2],
+ a = f + i[3],
+ e = h.getBackground(),
+ d = h.element;
+ d.setLocalXY(Math.floor(c), Math.floor(f));
+ d.setSize(Math.ceil(g - Math.floor(c)), Math.ceil(a - Math.floor(f)));
+ if (e) {
+ e.setAttributes({
+ x: 0,
+ y: 0,
+ width: Math.ceil(g - Math.floor(c)),
+ height: Math.ceil(a - Math.floor(f))
+ })
+ }
+ h.setDirty(true)
+ },
+ resetTransform: function() {
+ this.matrix.set(1, 0, 0, 1, 0, 0);
+ this.inverseMatrix.set(1, 0, 0, 1, 0, 0);
+ this.setDirty(true)
+ },
+ get: function(a) {
+ return this.map[a] || this.getItems()[a]
+ },
+ add: function() {
+ var g = this,
+ e = Array.prototype.slice.call(arguments),
+ j = Ext.isArray(e[0]),
+ a = g.map,
+ c = [],
+ f, k, h, b, d;
+ f = Ext.Array.clean(j ? e[0] : e);
+ if (!f.length) {
+ return c
+ }
+ for (b = 0, d = f.length; b < d; b++) {
+ k = f[b];
+ h = null;
+ if (k.isSprite && !a[k.getId()]) {
+ h = k
+ } else {
+ if (!a[k.id]) {
+ h = this.createItem(k)
+ }
+ }
+ if (h) {
+ a[h.getId()] = h;
+ c.push(h);
+ h.setParent(g);
+ h.setSurface(g);
+ g.onAdd(h)
+ }
+ }
+ f = g.getItems();
+ if (f) {
+ f.push.apply(f, c)
+ }
+ g.dirtyZIndex = true;
+ g.setDirty(true);
+ if (!j && c.length === 1) {
+ return c[0]
+ } else {
+ return c
+ }
+ },
+ onAdd: Ext.emptyFn,
+ remove: function(a, c) {
+ var b = this,
+ e, d;
+ if (a) {
+ if (a.charAt) {
+ a = b.map[a]
+ }
+ if (!a || !a.isSprite) {
+ return null
+ }
+ if (a.isDestroyed || a.isDestroying) {
+ return a
+ }
+ e = a.getId();
+ d = b.map[e];
+ delete b.map[e];
+ if (c) {
+ a.destroy()
+ }
+ if (!d) {
+ return a
+ }
+ a.setParent(null);
+ a.setSurface(null);
+ Ext.Array.remove(b.getItems(), a);
+ b.dirtyZIndex = true;
+ b.setDirty(true)
+ }
+ return a || null
+ },
+ removeAll: function(d) {
+ var a = this.getItems(),
+ b = a.length - 1,
+ c;
+ if (d) {
+ for (; b >= 0; b--) {
+ a[b].destroy()
+ }
+ } else {
+ for (; b >= 0; b--) {
+ c = a[b];
+ c.setParent(null);
+ c.setSurface(null)
+ }
+ }
+ a.length = 0;
+ this.map = {};
+ this.dirtyZIndex = true
+ },
+ applyItems: function(a) {
+ if (this.getItems()) {
+ this.removeAll(true)
+ }
+ return Ext.Array.from(this.add(a))
+ },
+ createItem: function(a) {
+ return Ext.create(a.xclass || "sprite." + a.type, a)
+ },
+ getBBox: function(f, b) {
+ var f = Ext.Array.from(f),
+ c = Infinity,
+ h = -Infinity,
+ g = Infinity,
+ a = -Infinity,
+ j, k, d, e;
+ for (d = 0, e = f.length; d < e; d++) {
+ j = f[d];
+ k = j.getBBox(b);
+ if (c > k.x) {
+ c = k.x
+ }
+ if (h < k.x + k.width) {
+ h = k.x + k.width
+ }
+ if (g > k.y) {
+ g = k.y
+ }
+ if (a < k.y + k.height) {
+ a = k.y + k.height
+ }
+ }
+ return {
+ x: c,
+ y: g,
+ width: h - c,
+ height: a - g
+ }
+ },
+ emptyRect: [0, 0, 0, 0],
+ getEventXY: function(d) {
+ var g = this,
+ f = g.getInherited().rtl,
+ c = d.getXY(),
+ a = g.getOwnerBody(),
+ i = a.getXY(),
+ h = g.getRect() || g.emptyRect,
+ j = [],
+ b;
+ if (f) {
+ b = a.getWidth();
+ j[0] = i[0] - c[0] - h[0] + b
+ } else {
+ j[0] = c[0] - i[0] - h[0]
+ }
+ j[1] = c[1] - i[1] - h[1];
+ return j
+ },
+ clear: Ext.emptyFn,
+ orderByZIndex: function() {
+ var d = this,
+ a = d.getItems(),
+ e = false,
+ b, c;
+ if (d.getDirty()) {
+ for (b = 0, c = a.length; b < c; b++) {
+ if (a[b].attr.dirtyZIndex) {
+ e = true;
+ break
+ }
+ }
+ if (e) {
+ Ext.Array.sort(a, function(g, f) {
+ return g.attr.zIndex - f.attr.zIndex
+ });
+ this.setDirty(true)
+ }
+ for (b = 0, c = a.length; b < c; b++) {
+ a[b].attr.dirtyZIndex = false
+ }
+ }
+ },
+ repaint: function() {
+ var a = this;
+ a.repaint = Ext.emptyFn;
+ Ext.defer(function() {
+ delete a.repaint;
+ a.element.repaint()
+ }, 1)
+ },
+ renderFrame: function() {
+ var g = this;
+ if (!g.element) {
+ return
+ }
+ if (g.dirtyPredecessorCount > 0) {
+ g.isPendingRenderFrame = true;
+ return
+ }
+ var f = g.getRect(),
+ c = g.getBackground(),
+ a = g.getItems(),
+ e, b, d;
+ if (!f) {
+ return
+ }
+ g.orderByZIndex();
+ if (g.getDirty()) {
+ g.clear();
+ g.clearTransform();
+ if (c) {
+ g.renderSprite(c)
+ }
+ for (b = 0, d = a.length; b < d; b++) {
+ e = a[b];
+ if (g.renderSprite(e) === false) {
+ return
+ }
+ e.attr.textPositionCount = g.textPosition
+ }
+ g.setDirty(false)
+ }
+ },
+ renderSprite: Ext.emptyFn,
+ clearTransform: Ext.emptyFn,
+ destroy: function() {
+ var a = this;
+ a.removeAll(true);
+ a.predecessors = null;
+ a.successors = null;
+ a.callParent()
+ }
+});
+Ext.define("Ext.draw.overrides.Surface", {
+ override: "Ext.draw.Surface",
+ hitTest: function(b, c) {
+ var f = this,
+ g = f.getItems(),
+ e, d, a;
+ c = c || Ext.draw.sprite.Sprite.defaultHitTestOptions;
+ for (e = g.length - 1; e >= 0; e--) {
+ d = g[e];
+ if (d.hitTest) {
+ a = d.hitTest(b, c);
+ if (a) {
+ return a
+ }
+ }
+ }
+ return null
+ },
+ hitTestEvent: function(b, a) {
+ var c = this.getEventXY(b);
+ return this.hitTest(c, a)
+ }
+});
+Ext.define("Ext.draw.engine.SvgContext", {
+ requires: ["Ext.draw.Color"],
+ toSave: ["strokeOpacity", "strokeStyle", "fillOpacity", "fillStyle", "globalAlpha", "lineWidth", "lineCap", "lineJoin", "lineDash", "lineDashOffset", "miterLimit", "shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor", "globalCompositeOperation", "position", "fillGradient", "strokeGradient"],
+ strokeOpacity: 1,
+ strokeStyle: "none",
+ fillOpacity: 1,
+ fillStyle: "none",
+ lineDash: [],
+ lineDashOffset: 0,
+ globalAlpha: 1,
+ lineWidth: 1,
+ lineCap: "butt",
+ lineJoin: "miter",
+ miterLimit: 10,
+ shadowOffsetX: 0,
+ shadowOffsetY: 0,
+ shadowBlur: 0,
+ shadowColor: "none",
+ globalCompositeOperation: "src",
+ urlStringRe: /^url\(#([\w\-]+)\)$/,
+ constructor: function(a) {
+ this.surface = a;
+ this.state = [];
+ this.matrix = new Ext.draw.Matrix();
+ this.path = null;
+ this.clear()
+ },
+ clear: function() {
+ this.group = this.surface.mainGroup;
+ this.position = 0;
+ this.path = null
+ },
+ getElement: function(a) {
+ return this.surface.getSvgElement(this.group, a, this.position++)
+ },
+ removeElement: function(d) {
+ var d = Ext.fly(d),
+ h, g, b, f, a, e, c;
+ if (!d) {
+ return
+ }
+ if (d.dom.tagName === "g") {
+ a = d.dom.gradients;
+ for (c in a) {
+ a[c].destroy()
+ }
+ } else {
+ h = d.getAttribute("fill");
+ g = d.getAttribute("stroke");
+ b = h && h.match(this.urlStringRe);
+ f = g && g.match(this.urlStringRe);
+ if (b && b[1]) {
+ e = Ext.fly(b[1]);
+ if (e) {
+ e.destroy()
+ }
+ }
+ if (f && f[1]) {
+ e = Ext.fly(f[1]);
+ if (e) {
+ e.destroy()
+ }
+ }
+ }
+ d.destroy()
+ },
+ save: function() {
+ var c = this.toSave,
+ e = {},
+ d = this.getElement("g"),
+ b, a;
+ for (a = 0; a < c.length; a++) {
+ b = c[a];
+ if (b in this) {
+ e[b] = this[b]
+ }
+ }
+ this.position = 0;
+ e.matrix = this.matrix.clone();
+ this.state.push(e);
+ this.group = d;
+ return d
+ },
+ restore: function() {
+ var d = this.toSave,
+ e = this.state.pop(),
+ c = this.group.dom.childNodes,
+ b, a;
+ while (c.length > this.position) {
+ this.removeElement(c[c.length - 1])
+ }
+ for (a = 0; a < d.length; a++) {
+ b = d[a];
+ if (b in e) {
+ this[b] = e[b]
+ } else {
+ delete this[b]
+ }
+ }
+ this.setTransform.apply(this, e.matrix.elements);
+ this.group = this.group.getParent()
+ },
+ transform: function(f, b, e, g, d, c) {
+ if (this.path) {
+ var a = Ext.draw.Matrix.fly([f, b, e, g, d, c]).inverse();
+ this.path.transform(a)
+ }
+ this.matrix.append(f, b, e, g, d, c)
+ },
+ setTransform: function(e, a, d, f, c, b) {
+ if (this.path) {
+ this.path.transform(this.matrix)
+ }
+ this.matrix.reset();
+ this.transform(e, a, d, f, c, b)
+ },
+ scale: function(a, b) {
+ this.transform(a, 0, 0, b, 0, 0)
+ },
+ rotate: function(d) {
+ var c = Math.cos(d),
+ a = Math.sin(d),
+ b = -Math.sin(d),
+ e = Math.cos(d);
+ this.transform(c, a, b, e, 0, 0)
+ },
+ translate: function(a, b) {
+ this.transform(1, 0, 0, 1, a, b)
+ },
+ setGradientBBox: function(a) {
+ this.bbox = a
+ },
+ beginPath: function() {
+ this.path = new Ext.draw.Path()
+ },
+ moveTo: function(a, b) {
+ if (!this.path) {
+ this.beginPath()
+ }
+ this.path.moveTo(a, b);
+ this.path.element = null
+ },
+ lineTo: function(a, b) {
+ if (!this.path) {
+ this.beginPath()
+ }
+ this.path.lineTo(a, b);
+ this.path.element = null
+ },
+ rect: function(b, d, c, a) {
+ this.moveTo(b, d);
+ this.lineTo(b + c, d);
+ this.lineTo(b + c, d + a);
+ this.lineTo(b, d + a);
+ this.closePath()
+ },
+ strokeRect: function(b, d, c, a) {
+ this.beginPath();
+ this.rect(b, d, c, a);
+ this.stroke()
+ },
+ fillRect: function(b, d, c, a) {
+ this.beginPath();
+ this.rect(b, d, c, a);
+ this.fill()
+ },
+ closePath: function() {
+ if (!this.path) {
+ this.beginPath()
+ }
+ this.path.closePath();
+ this.path.element = null
+ },
+ arcSvg: function(d, a, f, g, c, b, e) {
+ if (!this.path) {
+ this.beginPath()
+ }
+ this.path.arcSvg(d, a, f, g, c, b, e);
+ this.path.element = null
+ },
+ arc: function(b, f, a, d, c, e) {
+ if (!this.path) {
+ this.beginPath()
+ }
+ this.path.arc(b, f, a, d, c, e);
+ this.path.element = null
+ },
+ ellipse: function(a, h, g, f, d, c, b, e) {
+ if (!this.path) {
+ this.beginPath()
+ }
+ this.path.ellipse(a, h, g, f, d, c, b, e);
+ this.path.element = null
+ },
+ arcTo: function(b, e, a, d, g, f, c) {
+ if (!this.path) {
+ this.beginPath()
+ }
+ this.path.arcTo(b, e, a, d, g, f, c);
+ this.path.element = null
+ },
+ bezierCurveTo: function(d, f, b, e, a, c) {
+ if (!this.path) {
+ this.beginPath()
+ }
+ this.path.bezierCurveTo(d, f, b, e, a, c);
+ this.path.element = null
+ },
+ strokeText: function(d, a, e) {
+ d = String(d);
+ if (this.strokeStyle) {
+ var b = this.getElement("text"),
+ c = this.surface.getSvgElement(b, "tspan", 0);
+ this.surface.setElementAttributes(b, {
+ x: a,
+ y: e,
+ transform: this.matrix.toSvg(),
+ stroke: this.strokeStyle,
+ fill: "none",
+ opacity: this.globalAlpha,
+ "stroke-opacity": this.strokeOpacity,
+ style: "font: " + this.font,
+ "stroke-dasharray": this.lineDash.join(","),
+ "stroke-dashoffset": this.lineDashOffset
+ });
+ if (this.lineDash.length) {
+ this.surface.setElementAttributes(b, {
+ "stroke-dasharray": this.lineDash.join(","),
+ "stroke-dashoffset": this.lineDashOffset
+ })
+ }
+ if (c.dom.firstChild) {
+ c.dom.removeChild(c.dom.firstChild)
+ }
+ this.surface.setElementAttributes(c, {
+ "alignment-baseline": "alphabetic"
+ });
+ c.dom.appendChild(document.createTextNode(Ext.String.htmlDecode(d)))
+ }
+ },
+ fillText: function(d, a, e) {
+ d = String(d);
+ if (this.fillStyle) {
+ var b = this.getElement("text"),
+ c = this.surface.getSvgElement(b, "tspan", 0);
+ this.surface.setElementAttributes(b, {
+ x: a,
+ y: e,
+ transform: this.matrix.toSvg(),
+ fill: this.fillStyle,
+ opacity: this.globalAlpha,
+ "fill-opacity": this.fillOpacity,
+ style: "font: " + this.font
+ });
+ if (c.dom.firstChild) {
+ c.dom.removeChild(c.dom.firstChild)
+ }
+ this.surface.setElementAttributes(c, {
+ "alignment-baseline": "alphabetic"
+ });
+ c.dom.appendChild(document.createTextNode(Ext.String.htmlDecode(d)))
+ }
+ },
+ drawImage: function(c, k, i, l, e, p, n, a, g) {
+ var f = this,
+ d = f.getElement("image"),
+ j = k,
+ h = i,
+ b = typeof l === "undefined" ? c.width : l,
+ m = typeof e === "undefined" ? c.height : e,
+ o = null;
+ if (typeof g !== "undefined") {
+ o = k + " " + i + " " + l + " " + e;
+ j = p;
+ h = n;
+ b = a;
+ m = g
+ }
+ d.dom.setAttributeNS("http://www.w3.org/1999/xlink", "href", c.src);
+ f.surface.setElementAttributes(d, {
+ viewBox: o,
+ x: j,
+ y: h,
+ width: b,
+ height: m,
+ opacity: f.globalAlpha,
+ transform: f.matrix.toSvg()
+ })
+ },
+ fill: function() {
+ if (!this.path) {
+ return
+ }
+ if (this.fillStyle) {
+ var c, a = this.fillGradient,
+ d = this.bbox,
+ b = this.path.element;
+ if (!b) {
+ c = this.path.toString();
+ b = this.path.element = this.getElement("path");
+ this.surface.setElementAttributes(b, {
+ d: c,
+ transform: this.matrix.toSvg()
+ })
+ }
+ this.surface.setElementAttributes(b, {
+ fill: a && d ? a.generateGradient(this, d) : this.fillStyle,
+ "fill-opacity": this.fillOpacity * this.globalAlpha
+ })
+ }
+ },
+ stroke: function() {
+ if (!this.path) {
+ return
+ }
+ if (this.strokeStyle) {
+ var c, b = this.strokeGradient,
+ d = this.bbox,
+ a = this.path.element;
+ if (!a || !this.path.svgString) {
+ c = this.path.toString();
+ if (!c) {
+ return
+ }
+ a = this.path.element = this.getElement("path");
+ this.surface.setElementAttributes(a, {
+ fill: "none",
+ d: c,
+ transform: this.matrix.toSvg()
+ })
+ }
+ this.surface.setElementAttributes(a, {
+ stroke: b && d ? b.generateGradient(this, d) : this.strokeStyle,
+ "stroke-linecap": this.lineCap,
+ "stroke-linejoin": this.lineJoin,
+ "stroke-width": this.lineWidth,
+ "stroke-opacity": this.strokeOpacity * this.globalAlpha,
+ "stroke-dasharray": this.lineDash.join(","),
+ "stroke-dashoffset": this.lineDashOffset
+ });
+ if (this.lineDash.length) {
+ this.surface.setElementAttributes(a, {
+ "stroke-dasharray": this.lineDash.join(","),
+ "stroke-dashoffset": this.lineDashOffset
+ })
+ }
+ }
+ },
+ fillStroke: function(a, e) {
+ var b = this,
+ d = b.fillStyle,
+ g = b.strokeStyle,
+ c = b.fillOpacity,
+ f = b.strokeOpacity;
+ if (e === undefined) {
+ e = a.transformFillStroke
+ }
+ if (!e) {
+ a.inverseMatrix.toContext(b)
+ }
+ if (d && c !== 0) {
+ b.fill()
+ }
+ if (g && f !== 0) {
+ b.stroke()
+ }
+ },
+ appendPath: function(a) {
+ this.path = a.clone()
+ },
+ setLineDash: function(a) {
+ this.lineDash = a
+ },
+ getLineDash: function() {
+ return this.lineDash
+ },
+ createLinearGradient: function(d, g, b, e) {
+ var f = this,
+ c = f.surface.getNextDef("linearGradient"),
+ a = f.group.dom.gradients || (f.group.dom.gradients = {}),
+ h;
+ f.surface.setElementAttributes(c, {
+ x1: d,
+ y1: g,
+ x2: b,
+ y2: e,
+ gradientUnits: "userSpaceOnUse"
+ });
+ h = new Ext.draw.engine.SvgContext.Gradient(f, f.surface, c);
+ a[c.dom.id] = h;
+ return h
+ },
+ createRadialGradient: function(b, j, d, a, i, c) {
+ var g = this,
+ e = g.surface.getNextDef("radialGradient"),
+ f = g.group.dom.gradients || (g.group.dom.gradients = {}),
+ h;
+ g.surface.setElementAttributes(e, {
+ fx: b,
+ fy: j,
+ cx: a,
+ cy: i,
+ r: c,
+ gradientUnits: "userSpaceOnUse"
+ });
+ h = new Ext.draw.engine.SvgContext.Gradient(g, g.surface, e, d / c);
+ f[e.dom.id] = h;
+ return h
+ }
+});
+Ext.define("Ext.draw.engine.SvgContext.Gradient", {
+ statics: {
+ map: {}
+ },
+ constructor: function(c, a, d, b) {
+ var f = this.statics().map,
+ e;
+ e = f[d.dom.id];
+ if (e) {
+ e.element = null
+ }
+ f[d.dom.id] = this;
+ this.ctx = c;
+ this.surface = a;
+ this.element = d;
+ this.position = 0;
+ this.compression = b || 0
+ },
+ addColorStop: function(d, b) {
+ var c = this.surface.getSvgElement(this.element, "stop", this.position++),
+ a = this.compression;
+ this.surface.setElementAttributes(c, {
+ offset: (((1 - a) * d + a) * 100).toFixed(2) + "%",
+ "stop-color": b,
+ "stop-opacity": Ext.draw.Color.fly(b).a.toFixed(15)
+ })
+ },
+ toString: function() {
+ var a = this.element.dom.childNodes;
+ while (a.length > this.position) {
+ Ext.fly(a[a.length - 1]).destroy()
+ }
+ return "url(#" + this.element.getId() + ")"
+ },
+ destroy: function() {
+ var b = this.statics().map,
+ a = this.element;
+ if (a && a.dom) {
+ delete b[a.dom.id];
+ a.destroy()
+ }
+ this.callParent()
+ }
+});
+Ext.define("Ext.draw.engine.Svg", {
+ extend: "Ext.draw.Surface",
+ requires: ["Ext.draw.engine.SvgContext"],
+ statics: {
+ BBoxTextCache: {}
+ },
+ config: {
+ highPrecision: false
+ },
+ getElementConfig: function() {
+ return {
+ reference: "element",
+ style: {
+ position: "absolute"
+ },
+ children: [{
+ reference: "innerElement",
+ style: {
+ width: "100%",
+ height: "100%",
+ position: "relative"
+ },
+ children: [{
+ tag: "svg",
+ reference: "svgElement",
+ namespace: "http://www.w3.org/2000/svg",
+ width: "100%",
+ height: "100%",
+ version: 1.1
+ }]
+ }]
+ }
+ },
+ constructor: function(a) {
+ var b = this;
+ b.callParent([a]);
+ b.mainGroup = b.createSvgNode("g");
+ b.defElement = b.createSvgNode("defs");
+ b.svgElement.appendChild(b.mainGroup);
+ b.svgElement.appendChild(b.defElement);
+ b.ctx = new Ext.draw.engine.SvgContext(b)
+ },
+ createSvgNode: function(a) {
+ var b = document.createElementNS("http://www.w3.org/2000/svg", a);
+ return Ext.get(b)
+ },
+ getSvgElement: function(d, b, a) {
+ var c;
+ if (d.dom.childNodes.length > a) {
+ c = d.dom.childNodes[a];
+ if (c.tagName === b) {
+ return Ext.get(c)
+ } else {
+ Ext.destroy(c)
+ }
+ }
+ c = Ext.get(this.createSvgNode(b));
+ if (a === 0) {
+ d.insertFirst(c)
+ } else {
+ c.insertAfter(Ext.fly(d.dom.childNodes[a - 1]))
+ }
+ c.cache = {};
+ return c
+ },
+ setElementAttributes: function(d, b) {
+ var f = d.dom,
+ a = d.cache,
+ c, e;
+ for (c in b) {
+ e = b[c];
+ if (a[c] !== e) {
+ a[c] = e;
+ f.setAttribute(c, e)
+ }
+ }
+ },
+ getNextDef: function(a) {
+ return this.getSvgElement(this.defElement, a, this.defPosition++)
+ },
+ clearTransform: function() {
+ var a = this;
+ a.mainGroup.set({
+ transform: a.matrix.toSvg()
+ })
+ },
+ clear: function() {
+ this.ctx.clear();
+ this.defPosition = 0
+ },
+ renderSprite: function(b) {
+ var d = this,
+ c = d.getRect(),
+ a = d.ctx;
+ if (b.attr.hidden || b.attr.globalAlpha === 0) {
+ a.save();
+ a.restore();
+ return
+ }
+ b.element = a.save();
+ b.preRender(this);
+ b.useAttributes(a, c);
+ if (false === b.render(this, a, [0, 0, c[2], c[3]])) {
+ return false
+ }
+ b.setDirty(false);
+ a.restore()
+ },
+ flatten: function(e, b) {
+ var c = '',
+ f = Ext.getClassName(this),
+ a, g, d;
+ c += '";
+ return {
+ data: "data:image/svg+xml;utf8," + encodeURIComponent(c),
+ type: "svg"
+ }
+ },
+ serializeNode: function(d) {
+ var b = "",
+ c, f, a, e;
+ if (d.nodeType === document.TEXT_NODE) {
+ return d.nodeValue
+ }
+ b += "<" + d.nodeName;
+ if (d.attributes.length) {
+ for (c = 0, f = d.attributes.length; c < f; c++) {
+ a = d.attributes[c];
+ b += " " + a.name + '="' + a.value + '"'
+ }
+ }
+ b += ">";
+ if (d.childNodes && d.childNodes.length) {
+ for (c = 0, f = d.childNodes.length; c < f; c++) {
+ e = d.childNodes[c];
+ b += this.serializeNode(e)
+ }
+ }
+ b += "" + d.nodeName + ">";
+ return b
+ },
+ destroy: function() {
+ var a = this;
+ a.ctx.destroy();
+ a.mainGroup.destroy();
+ delete a.mainGroup;
+ delete a.ctx;
+ a.callParent()
+ },
+ remove: function(a, b) {
+ if (a && a.element) {
+ if (this.ctx) {
+ this.ctx.removeElement(a.element)
+ } else {
+ a.element.destroy()
+ }
+ a.element = null
+ }
+ this.callParent(arguments)
+ }
+});
+Ext.draw || (Ext.draw = {});
+Ext.draw.engine || (Ext.draw.engine = {});
+Ext.draw.engine.excanvas = true;
+if (!document.createElement("canvas").getContext) {
+ (function() {
+ var ab = Math;
+ var n = ab.round;
+ var l = ab.sin;
+ var A = ab.cos;
+ var H = ab.abs;
+ var N = ab.sqrt;
+ var d = 10;
+ var f = d / 2;
+ var z = +navigator.userAgent.match(/MSIE ([\d.]+)?/)[1];
+
+ function y() {
+ return this.context_ || (this.context_ = new D(this))
+ }
+ var t = Array.prototype.slice;
+
+ function g(j, m, p) {
+ var i = t.call(arguments, 2);
+ return function() {
+ return j.apply(m, i.concat(t.call(arguments)))
+ }
+ }
+
+ function af(i) {
+ return String(i).replace(/&/g, "&").replace(/"/g, """)
+ }
+
+ function Y(m, j, i) {
+ Ext.onReady(function() {
+ if (!m.namespaces[j]) {
+ m.namespaces.add(j, i, "#default#VML")
+ }
+ })
+ }
+
+ function R(j) {
+ Y(j, "g_vml_", "urn:schemas-microsoft-com:vml");
+ Y(j, "g_o_", "urn:schemas-microsoft-com:office:office");
+ if (!j.styleSheets.ex_canvas_) {
+ var i = j.createStyleSheet();
+ i.owningElement.id = "ex_canvas_";
+ i.cssText = "canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"
+ }
+ }
+ R(document);
+ var e = {
+ init: function(i) {
+ var j = i || document;
+ j.createElement("canvas");
+ j.attachEvent("onreadystatechange", g(this.init_, this, j))
+ },
+ init_: function(p) {
+ var m = p.getElementsByTagName("canvas");
+ for (var j = 0; j < m.length; j++) {
+ this.initElement(m[j])
+ }
+ },
+ initElement: function(j) {
+ if (!j.getContext) {
+ j.getContext = y;
+ R(j.ownerDocument);
+ j.innerHTML = "";
+ j.attachEvent("onpropertychange", x);
+ j.attachEvent("onresize", W);
+ var i = j.attributes;
+ if (i.width && i.width.specified) {
+ j.style.width = i.width.nodeValue + "px"
+ } else {
+ j.width = j.clientWidth
+ }
+ if (i.height && i.height.specified) {
+ j.style.height = i.height.nodeValue + "px"
+ } else {
+ j.height = j.clientHeight
+ }
+ }
+ return j
+ }
+ };
+
+ function x(j) {
+ var i = j.srcElement;
+ switch (j.propertyName) {
+ case "width":
+ i.getContext().clearRect();
+ i.style.width = i.attributes.width.nodeValue + "px";
+ i.firstChild.style.width = i.clientWidth + "px";
+ break;
+ case "height":
+ i.getContext().clearRect();
+ i.style.height = i.attributes.height.nodeValue + "px";
+ i.firstChild.style.height = i.clientHeight + "px";
+ break
+ }
+ }
+
+ function W(j) {
+ var i = j.srcElement;
+ if (i.firstChild) {
+ i.firstChild.style.width = i.clientWidth + "px";
+ i.firstChild.style.height = i.clientHeight + "px"
+ }
+ }
+ e.init();
+ var k = [];
+ for (var ae = 0; ae < 16; ae++) {
+ for (var ad = 0; ad < 16; ad++) {
+ k[ae * 16 + ad] = ae.toString(16) + ad.toString(16)
+ }
+ }
+
+ function B() {
+ return [
+ [1, 0, 0],
+ [0, 1, 0],
+ [0, 0, 1]
+ ]
+ }
+
+ function J(p, m) {
+ var j = B();
+ for (var i = 0; i < 3; i++) {
+ for (var ah = 0; ah < 3; ah++) {
+ var Z = 0;
+ for (var ag = 0; ag < 3; ag++) {
+ Z += p[i][ag] * m[ag][ah]
+ }
+ j[i][ah] = Z
+ }
+ }
+ return j
+ }
+
+ function v(j, i) {
+ i.fillStyle = j.fillStyle;
+ i.lineCap = j.lineCap;
+ i.lineJoin = j.lineJoin;
+ i.lineDash = j.lineDash;
+ i.lineWidth = j.lineWidth;
+ i.miterLimit = j.miterLimit;
+ i.shadowBlur = j.shadowBlur;
+ i.shadowColor = j.shadowColor;
+ i.shadowOffsetX = j.shadowOffsetX;
+ i.shadowOffsetY = j.shadowOffsetY;
+ i.strokeStyle = j.strokeStyle;
+ i.globalAlpha = j.globalAlpha;
+ i.font = j.font;
+ i.textAlign = j.textAlign;
+ i.textBaseline = j.textBaseline;
+ i.arcScaleX_ = j.arcScaleX_;
+ i.arcScaleY_ = j.arcScaleY_;
+ i.lineScale_ = j.lineScale_
+ }
+ var b = {
+ aliceblue: "#F0F8FF",
+ antiquewhite: "#FAEBD7",
+ aquamarine: "#7FFFD4",
+ azure: "#F0FFFF",
+ beige: "#F5F5DC",
+ bisque: "#FFE4C4",
+ black: "#000000",
+ blanchedalmond: "#FFEBCD",
+ blueviolet: "#8A2BE2",
+ brown: "#A52A2A",
+ burlywood: "#DEB887",
+ cadetblue: "#5F9EA0",
+ chartreuse: "#7FFF00",
+ chocolate: "#D2691E",
+ coral: "#FF7F50",
+ cornflowerblue: "#6495ED",
+ cornsilk: "#FFF8DC",
+ crimson: "#DC143C",
+ cyan: "#00FFFF",
+ darkblue: "#00008B",
+ darkcyan: "#008B8B",
+ darkgoldenrod: "#B8860B",
+ darkgray: "#A9A9A9",
+ darkgreen: "#006400",
+ darkgrey: "#A9A9A9",
+ darkkhaki: "#BDB76B",
+ darkmagenta: "#8B008B",
+ darkolivegreen: "#556B2F",
+ darkorange: "#FF8C00",
+ darkorchid: "#9932CC",
+ darkred: "#8B0000",
+ darksalmon: "#E9967A",
+ darkseagreen: "#8FBC8F",
+ darkslateblue: "#483D8B",
+ darkslategray: "#2F4F4F",
+ darkslategrey: "#2F4F4F",
+ darkturquoise: "#00CED1",
+ darkviolet: "#9400D3",
+ deeppink: "#FF1493",
+ deepskyblue: "#00BFFF",
+ dimgray: "#696969",
+ dimgrey: "#696969",
+ dodgerblue: "#1E90FF",
+ firebrick: "#B22222",
+ floralwhite: "#FFFAF0",
+ forestgreen: "#228B22",
+ gainsboro: "#DCDCDC",
+ ghostwhite: "#F8F8FF",
+ gold: "#FFD700",
+ goldenrod: "#DAA520",
+ grey: "#808080",
+ greenyellow: "#ADFF2F",
+ honeydew: "#F0FFF0",
+ hotpink: "#FF69B4",
+ indianred: "#CD5C5C",
+ indigo: "#4B0082",
+ ivory: "#FFFFF0",
+ khaki: "#F0E68C",
+ lavender: "#E6E6FA",
+ lavenderblush: "#FFF0F5",
+ lawngreen: "#7CFC00",
+ lemonchiffon: "#FFFACD",
+ lightblue: "#ADD8E6",
+ lightcoral: "#F08080",
+ lightcyan: "#E0FFFF",
+ lightgoldenrodyellow: "#FAFAD2",
+ lightgreen: "#90EE90",
+ lightgrey: "#D3D3D3",
+ lightpink: "#FFB6C1",
+ lightsalmon: "#FFA07A",
+ lightseagreen: "#20B2AA",
+ lightskyblue: "#87CEFA",
+ lightslategray: "#778899",
+ lightslategrey: "#778899",
+ lightsteelblue: "#B0C4DE",
+ lightyellow: "#FFFFE0",
+ limegreen: "#32CD32",
+ linen: "#FAF0E6",
+ magenta: "#FF00FF",
+ mediumaquamarine: "#66CDAA",
+ mediumblue: "#0000CD",
+ mediumorchid: "#BA55D3",
+ mediumpurple: "#9370DB",
+ mediumseagreen: "#3CB371",
+ mediumslateblue: "#7B68EE",
+ mediumspringgreen: "#00FA9A",
+ mediumturquoise: "#48D1CC",
+ mediumvioletred: "#C71585",
+ midnightblue: "#191970",
+ mintcream: "#F5FFFA",
+ mistyrose: "#FFE4E1",
+ moccasin: "#FFE4B5",
+ navajowhite: "#FFDEAD",
+ oldlace: "#FDF5E6",
+ olivedrab: "#6B8E23",
+ orange: "#FFA500",
+ orangered: "#FF4500",
+ orchid: "#DA70D6",
+ palegoldenrod: "#EEE8AA",
+ palegreen: "#98FB98",
+ paleturquoise: "#AFEEEE",
+ palevioletred: "#DB7093",
+ papayawhip: "#FFEFD5",
+ peachpuff: "#FFDAB9",
+ peru: "#CD853F",
+ pink: "#FFC0CB",
+ plum: "#DDA0DD",
+ powderblue: "#B0E0E6",
+ rosybrown: "#BC8F8F",
+ royalblue: "#4169E1",
+ saddlebrown: "#8B4513",
+ salmon: "#FA8072",
+ sandybrown: "#F4A460",
+ seagreen: "#2E8B57",
+ seashell: "#FFF5EE",
+ sienna: "#A0522D",
+ skyblue: "#87CEEB",
+ slateblue: "#6A5ACD",
+ slategray: "#708090",
+ slategrey: "#708090",
+ snow: "#FFFAFA",
+ springgreen: "#00FF7F",
+ steelblue: "#4682B4",
+ tan: "#D2B48C",
+ thistle: "#D8BFD8",
+ tomato: "#FF6347",
+ turquoise: "#40E0D0",
+ violet: "#EE82EE",
+ wheat: "#F5DEB3",
+ whitesmoke: "#F5F5F5",
+ yellowgreen: "#9ACD32"
+ };
+
+ function M(j) {
+ var p = j.indexOf("(", 3);
+ var i = j.indexOf(")", p + 1);
+ var m = j.substring(p + 1, i).split(",");
+ if (m.length != 4 || j.charAt(3) != "a") {
+ m[3] = 1
+ }
+ return m
+ }
+
+ function c(i) {
+ return parseFloat(i) / 100
+ }
+
+ function r(j, m, i) {
+ return Math.min(i, Math.max(m, j))
+ }
+
+ function I(ag) {
+ var i, ai, aj, ah, ak, Z;
+ ah = parseFloat(ag[0]) / 360 % 360;
+ if (ah < 0) {
+ ah++
+ }
+ ak = r(c(ag[1]), 0, 1);
+ Z = r(c(ag[2]), 0, 1);
+ if (ak == 0) {
+ i = ai = aj = Z
+ } else {
+ var j = Z < 0.5 ? Z * (1 + ak) : Z + ak - Z * ak;
+ var m = 2 * Z - j;
+ i = a(m, j, ah + 1 / 3);
+ ai = a(m, j, ah);
+ aj = a(m, j, ah - 1 / 3)
+ }
+ return "#" + k[Math.floor(i * 255)] + k[Math.floor(ai * 255)] + k[Math.floor(aj * 255)]
+ }
+
+ function a(j, i, m) {
+ if (m < 0) {
+ m++
+ }
+ if (m > 1) {
+ m--
+ }
+ if (6 * m < 1) {
+ return j + (i - j) * 6 * m
+ } else {
+ if (2 * m < 1) {
+ return i
+ } else {
+ if (3 * m < 2) {
+ return j + (i - j) * (2 / 3 - m) * 6
+ } else {
+ return j
+ }
+ }
+ }
+ }
+ var C = {};
+
+ function F(j) {
+ if (j in C) {
+ return C[j]
+ }
+ var ag, Z = 1;
+ j = String(j);
+ if (j.charAt(0) == "#") {
+ ag = j
+ } else {
+ if (/^rgb/.test(j)) {
+ var p = M(j);
+ var ag = "#",
+ ah;
+ for (var m = 0; m < 3; m++) {
+ if (p[m].indexOf("%") != -1) {
+ ah = Math.floor(c(p[m]) * 255)
+ } else {
+ ah = +p[m]
+ }
+ ag += k[r(ah, 0, 255)]
+ }
+ Z = +p[3]
+ } else {
+ if (/^hsl/.test(j)) {
+ var p = M(j);
+ ag = I(p);
+ Z = p[3]
+ } else {
+ ag = b[j] || j
+ }
+ }
+ }
+ return C[j] = {
+ color: ag,
+ alpha: Z
+ }
+ }
+ var o = {
+ style: "normal",
+ variant: "normal",
+ weight: "normal",
+ size: 10,
+ family: "sans-serif"
+ };
+ var L = {};
+
+ function E(i) {
+ if (L[i]) {
+ return L[i]
+ }
+ var p = document.createElement("div");
+ var m = p.style;
+ try {
+ m.font = i
+ } catch (j) {}
+ return L[i] = {
+ style: m.fontStyle || o.style,
+ variant: m.fontVariant || o.variant,
+ weight: m.fontWeight || o.weight,
+ size: m.fontSize || o.size,
+ family: m.fontFamily || o.family
+ }
+ }
+
+ function u(m, j) {
+ var i = {};
+ for (var ah in m) {
+ i[ah] = m[ah]
+ }
+ var ag = parseFloat(j.currentStyle.fontSize),
+ Z = parseFloat(m.size);
+ if (typeof m.size == "number") {
+ i.size = m.size
+ } else {
+ if (m.size.indexOf("px") != -1) {
+ i.size = Z
+ } else {
+ if (m.size.indexOf("em") != -1) {
+ i.size = ag * Z
+ } else {
+ if (m.size.indexOf("%") != -1) {
+ i.size = (ag / 100) * Z
+ } else {
+ if (m.size.indexOf("pt") != -1) {
+ i.size = Z / 0.75
+ } else {
+ i.size = ag
+ }
+ }
+ }
+ }
+ }
+ i.size *= 0.981;
+ return i
+ }
+
+ function ac(i) {
+ return i.style + " " + i.variant + " " + i.weight + " " + i.size + "px " + i.family
+ }
+ var s = {
+ butt: "flat",
+ round: "round"
+ };
+
+ function S(i) {
+ return s[i] || "square"
+ }
+
+ function D(i) {
+ this.m_ = B();
+ this.mStack_ = [];
+ this.aStack_ = [];
+ this.currentPath_ = [];
+ this.strokeStyle = "#000";
+ this.fillStyle = "#000";
+ this.lineWidth = 1;
+ this.lineJoin = "miter";
+ this.lineDash = [];
+ this.lineCap = "butt";
+ this.miterLimit = d * 1;
+ this.globalAlpha = 1;
+ this.font = "10px sans-serif";
+ this.textAlign = "left";
+ this.textBaseline = "alphabetic";
+ this.canvas = i;
+ var m = "width:" + i.clientWidth + "px;height:" + i.clientHeight + "px;overflow:hidden;position:absolute";
+ var j = i.ownerDocument.createElement("div");
+ j.style.cssText = m;
+ i.appendChild(j);
+ var p = j.cloneNode(false);
+ p.style.backgroundColor = "red";
+ p.style.filter = "alpha(opacity=0)";
+ i.appendChild(p);
+ this.element_ = j;
+ this.arcScaleX_ = 1;
+ this.arcScaleY_ = 1;
+ this.lineScale_ = 1
+ }
+ var q = D.prototype;
+ q.clearRect = function() {
+ if (this.textMeasureEl_) {
+ this.textMeasureEl_.removeNode(true);
+ this.textMeasureEl_ = null
+ }
+ this.element_.innerHTML = ""
+ };
+ q.beginPath = function() {
+ this.currentPath_ = []
+ };
+ q.moveTo = function(j, i) {
+ var m = V(this, j, i);
+ this.currentPath_.push({
+ type: "moveTo",
+ x: m.x,
+ y: m.y
+ });
+ this.currentX_ = m.x;
+ this.currentY_ = m.y
+ };
+ q.lineTo = function(j, i) {
+ var m = V(this, j, i);
+ this.currentPath_.push({
+ type: "lineTo",
+ x: m.x,
+ y: m.y
+ });
+ this.currentX_ = m.x;
+ this.currentY_ = m.y
+ };
+ q.bezierCurveTo = function(m, j, ak, aj, ai, ag) {
+ var i = V(this, ai, ag);
+ var ah = V(this, m, j);
+ var Z = V(this, ak, aj);
+ K(this, ah, Z, i)
+ };
+
+ function K(i, Z, m, j) {
+ i.currentPath_.push({
+ type: "bezierCurveTo",
+ cp1x: Z.x,
+ cp1y: Z.y,
+ cp2x: m.x,
+ cp2y: m.y,
+ x: j.x,
+ y: j.y
+ });
+ i.currentX_ = j.x;
+ i.currentY_ = j.y
+ }
+ q.quadraticCurveTo = function(ai, m, j, i) {
+ var ah = V(this, ai, m);
+ var ag = V(this, j, i);
+ var aj = {
+ x: this.currentX_ + 2 / 3 * (ah.x - this.currentX_),
+ y: this.currentY_ + 2 / 3 * (ah.y - this.currentY_)
+ };
+ var Z = {
+ x: aj.x + (ag.x - this.currentX_) / 3,
+ y: aj.y + (ag.y - this.currentY_) / 3
+ };
+ K(this, aj, Z, ag)
+ };
+ q.arc = function(al, aj, ak, ag, j, m) {
+ ak *= d;
+ var ap = m ? "at" : "wa";
+ var am = al + A(ag) * ak - f;
+ var ao = aj + l(ag) * ak - f;
+ var i = al + A(j) * ak - f;
+ var an = aj + l(j) * ak - f;
+ if (am == i && !m) {
+ am += 0.125
+ }
+ var Z = V(this, al, aj);
+ var ai = V(this, am, ao);
+ var ah = V(this, i, an);
+ this.currentPath_.push({
+ type: ap,
+ x: Z.x,
+ y: Z.y,
+ radius: ak,
+ xStart: ai.x,
+ yStart: ai.y,
+ xEnd: ah.x,
+ yEnd: ah.y
+ })
+ };
+ q.rect = function(m, j, i, p) {
+ this.moveTo(m, j);
+ this.lineTo(m + i, j);
+ this.lineTo(m + i, j + p);
+ this.lineTo(m, j + p);
+ this.closePath()
+ };
+ q.strokeRect = function(m, j, i, p) {
+ var Z = this.currentPath_;
+ this.beginPath();
+ this.moveTo(m, j);
+ this.lineTo(m + i, j);
+ this.lineTo(m + i, j + p);
+ this.lineTo(m, j + p);
+ this.closePath();
+ this.stroke();
+ this.currentPath_ = Z
+ };
+ q.fillRect = function(m, j, i, p) {
+ var Z = this.currentPath_;
+ this.beginPath();
+ this.moveTo(m, j);
+ this.lineTo(m + i, j);
+ this.lineTo(m + i, j + p);
+ this.lineTo(m, j + p);
+ this.closePath();
+ this.fill();
+ this.currentPath_ = Z
+ };
+ q.createLinearGradient = function(j, p, i, m) {
+ var Z = new U("gradient");
+ Z.x0_ = j;
+ Z.y0_ = p;
+ Z.x1_ = i;
+ Z.y1_ = m;
+ return Z
+ };
+ q.createRadialGradient = function(p, ag, m, j, Z, i) {
+ var ah = new U("gradientradial");
+ ah.x0_ = p;
+ ah.y0_ = ag;
+ ah.r0_ = m;
+ ah.x1_ = j;
+ ah.y1_ = Z;
+ ah.r1_ = i;
+ return ah
+ };
+ q.drawImage = function(an, j) {
+ var ah, Z, aj, ar, al, ak, ao, av;
+ var ai = an.runtimeStyle.width;
+ var am = an.runtimeStyle.height;
+ an.runtimeStyle.width = "auto";
+ an.runtimeStyle.height = "auto";
+ var ag = an.width;
+ var aq = an.height;
+ an.runtimeStyle.width = ai;
+ an.runtimeStyle.height = am;
+ if (arguments.length == 3) {
+ ah = arguments[1];
+ Z = arguments[2];
+ al = ak = 0;
+ ao = aj = ag;
+ av = ar = aq
+ } else {
+ if (arguments.length == 5) {
+ ah = arguments[1];
+ Z = arguments[2];
+ aj = arguments[3];
+ ar = arguments[4];
+ al = ak = 0;
+ ao = ag;
+ av = aq
+ } else {
+ if (arguments.length == 9) {
+ al = arguments[1];
+ ak = arguments[2];
+ ao = arguments[3];
+ av = arguments[4];
+ ah = arguments[5];
+ Z = arguments[6];
+ aj = arguments[7];
+ ar = arguments[8]
+ } else {
+ throw Error("Invalid number of arguments")
+ }
+ }
+ }
+ var au = V(this, ah, Z);
+ var at = [];
+ var i = 10;
+ var p = 10;
+ var ap = this.m_;
+ at.push("
=1){return l.path}var e=0,f=c.length,d=0,b,k,h,n=l.temp.params,g=0;for(;e ' +
+ Ext.String.format(gettext('{0} is not initialized.'), 'Ceph') + ' '+
+ gettext('You need to create a initial config once.') + ' ' +
+ Ext.String.format(gettext('{0} is not installed on this node.'), 'Ceph') + ' ' + Ext.htmlEncode(errors[name]) + ' ' + Ext.htmlEncode(msg) + ' "Ceph is a unified, distributed storage system designed for excellent performance, reliability and scalability." Ceph is currently not installed on this node, click on the next button below to start the installation.'+
+ ' This wizard will guide you through the necessary steps, after the initial installation you will be offered to create an initial configuration.'+
+ ' The configuration step is only needed once per cluster and will be skipped if a config is already present. Please take a look at our documentation, by clicking the help button below, before starting the installation, '+
+ 'if you want to gain deeper knowledge about Ceph visit ceph.com. The basic installation and configuration is completed, depending on your setup some of the following steps are required to start using Ceph: To learn more click on the help button below. ' + gettext('Note: Rollback stops CT') + ' ' + Ext.htmlEncode(msg) + ' ' +
+ Ext.String.format(gettext('{0} is not initialized.'), 'Ceph') + ' '+
+ gettext('You need to create a initial config once.') + ' ' +
+ Ext.String.format(gettext('{0} is not installed on this node.'), 'Ceph') + ' ' + Ext.htmlEncode(errors[name]) + ' ' + Ext.htmlEncode(msg) + ' "Ceph is a unified, distributed storage system designed for excellent performance, reliability and scalability." Ceph is currently not installed on this node, click on the next button below to start the installation.'+
+ ' This wizard will guide you through the necessary steps, after the initial installation you will be offered to create an initial configuration.'+
+ ' The configuration step is only needed once per cluster and will be skipped if a config is already present. Please take a look at our documentation, by clicking the help button below, before starting the installation, '+
+ 'if you want to gain deeper knowledge about Ceph visit ceph.com. The basic installation and configuration is completed, depending on your setup some of the following steps are required to start using Ceph: To learn more click on the help button below. ' + gettext('Note: Rollback stops CT') + ' ' + Ext.htmlEncode(msg) + 's)){v-=k(s-u)*(v-i)/(u-h);u=s}return{x1:m,y1:l,x2:v,y2:u}},smooth:function(l,j,o){var k=l.length,h,g,c,b,q,p,n,m,f=[],e=[],d,a;for(d=0;dl&&vf){m.map["time_"+b[2]]=[f,l]}}},"double":function(h,u,j,a,t,b,c){var e=0,k,f=1,n,d,v,g,s,l,m,r,q,p,o;while(u>e+1){k=e;e=u;f+=f;for(n=k;n
=0){r=d(r);q=(r-i)/2/j;if(0
0){q-=r/j;if(0
n[1]||x[1]
=0&&i<=1&&g>=0&&g<=1){return[k+i*(j-k),p+i*(o-p)]}return null},pointOnLine:function(j,m,h,l,g,n){var k,i;if(a(h-j)1){return false}return a(m+k*(l-m)-n)<4},pointOnCubic:function(w,u,s,r,l,k,h,g,p,o){var C=this,B=C.bezierCoeffs(w,u,s,r),A=C.bezierCoeffs(l,k,h,g),z,v,n,m,q;B[3]-=p;A[3]-=o;n=C.cubicRoots(B);m=C.cubicRoots(A);for(z=0;z
";
+ Ext.Object.each(result.errors, function(prop, desc) {
+ msg += "
" + Ext.htmlEncode(prop) + ": " +
+ Ext.htmlEncode(desc);
+ });
+ }
+ }
+
+ return msg;
+ },
+
+ // Ext.Ajax.request
+ API2Request: function(reqOpts) {
+
+ var newopts = Ext.apply({
+ waitMsg: gettext('Please wait...')
+ }, reqOpts);
+
+ if (!newopts.url.match(/^\/api2/)) {
+ newopts.url = '/api2/extjs' + newopts.url;
+ }
+ delete newopts.callback;
+
+ var createWrapper = function(successFn, callbackFn, failureFn) {
+ Ext.apply(newopts, {
+ success: function(response, options) {
+ if (options.waitMsgTarget) {
+ if (Proxmox.Utils.toolkit === 'touch') {
+ options.waitMsgTarget.setMasked(false);
+ } else {
+ options.waitMsgTarget.setLoading(false);
+ }
+ }
+ var result = Ext.decode(response.responseText);
+ response.result = result;
+ if (!result.success) {
+ response.htmlStatus = Proxmox.Utils.extractRequestError(result, true);
+ Ext.callback(callbackFn, options.scope, [options, false, response]);
+ Ext.callback(failureFn, options.scope, [response, options]);
+ return;
+ }
+ Ext.callback(callbackFn, options.scope, [options, true, response]);
+ Ext.callback(successFn, options.scope, [response, options]);
+ },
+ failure: function(response, options) {
+ if (options.waitMsgTarget) {
+ if (Proxmox.Utils.toolkit === 'touch') {
+ options.waitMsgTarget.setMasked(false);
+ } else {
+ options.waitMsgTarget.setLoading(false);
+ }
+ }
+ response.result = {};
+ try {
+ response.result = Ext.decode(response.responseText);
+ } catch(e) {}
+ var msg = gettext('Connection error') + ' - server offline?';
+ if (response.aborted) {
+ msg = gettext('Connection error') + ' - aborted.';
+ } else if (response.timedout) {
+ msg = gettext('Connection error') + ' - Timeout.';
+ } else if (response.status && response.statusText) {
+ msg = gettext('Connection error') + ' ' + response.status + ': ' + response.statusText;
+ }
+ response.htmlStatus = msg;
+ Ext.callback(callbackFn, options.scope, [options, false, response]);
+ Ext.callback(failureFn, options.scope, [response, options]);
+ }
+ });
+ };
+
+ createWrapper(reqOpts.success, reqOpts.callback, reqOpts.failure);
+
+ var target = newopts.waitMsgTarget;
+ if (target) {
+ if (Proxmox.Utils.toolkit === 'touch') {
+ target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg} );
+ } else {
+ // Note: ExtJS bug - this does not work when component is not rendered
+ target.setLoading(newopts.waitMsg);
+ }
+ }
+ Ext.Ajax.request(newopts);
+ },
+
+ checked_command: function(orig_cmd) {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/localhost/subscription',
+ method: 'GET',
+ //waitMsgTarget: me,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, opts) {
+ var data = response.result.data;
+
+ if (data.status !== 'Active') {
+ Ext.Msg.show({
+ title: gettext('No valid subscription'),
+ icon: Ext.Msg.WARNING,
+ msg: Proxmox.Utils.getNoSubKeyHtml(data.url),
+ buttons: Ext.Msg.OK,
+ callback: function(btn) {
+ if (btn !== 'ok') {
+ return;
+ }
+ orig_cmd();
+ }
+ });
+ } else {
+ orig_cmd();
+ }
+ }
+ });
+ },
+
+ assemble_field_data: function(values, data) {
+ if (Ext.isObject(data)) {
+ Ext.Object.each(data, function(name, val) {
+ if (values.hasOwnProperty(name)) {
+ var bucket = values[name];
+ if (!Ext.isArray(bucket)) {
+ bucket = values[name] = [bucket];
+ }
+ if (Ext.isArray(val)) {
+ values[name] = bucket.concat(val);
+ } else {
+ bucket.push(val);
+ }
+ } else {
+ values[name] = val;
+ }
+ });
+ }
+ },
+
+ dialog_title: function(subject, create, isAdd) {
+ if (create) {
+ if (isAdd) {
+ return gettext('Add') + ': ' + subject;
+ } else {
+ return gettext('Create') + ': ' + subject;
+ }
+ } else {
+ return gettext('Edit') + ': ' + subject;
+ }
+ },
+
+ network_iface_types: {
+ eth: gettext("Network Device"),
+ bridge: 'Linux Bridge',
+ bond: 'Linux Bond',
+ vlan: 'Linux VLAN',
+ OVSBridge: 'OVS Bridge',
+ OVSBond: 'OVS Bond',
+ OVSPort: 'OVS Port',
+ OVSIntPort: 'OVS IntPort'
+ },
+
+ render_network_iface_type: function(value) {
+ return Proxmox.Utils.network_iface_types[value] ||
+ Proxmox.Utils.unknownText;
+ },
+
+ task_desc_table: {
+ acmenewcert: [ 'SRV', gettext('Order Certificate') ],
+ acmeregister: [ 'ACME Account', gettext('Register') ],
+ acmedeactivate: [ 'ACME Account', gettext('Deactivate') ],
+ acmeupdate: [ 'ACME Account', gettext('Update') ],
+ acmerefresh: [ 'ACME Account', gettext('Refresh') ],
+ acmerenew: [ 'SRV', gettext('Renew Certificate') ],
+ acmerevoke: [ 'SRV', gettext('Revoke Certificate') ],
+ 'move_volume': [ 'CT', gettext('Move Volume') ],
+ clustercreate: [ '', gettext('Create Cluster') ],
+ clusterjoin: [ '', gettext('Join Cluster') ],
+ diskinit: [ 'Disk', gettext('Initialize Disk with GPT') ],
+ vncproxy: [ 'VM/CT', gettext('Console') ],
+ spiceproxy: [ 'VM/CT', gettext('Console') + ' (Spice)' ],
+ vncshell: [ '', gettext('Shell') ],
+ spiceshell: [ '', gettext('Shell') + ' (Spice)' ],
+ qmsnapshot: [ 'VM', gettext('Snapshot') ],
+ qmrollback: [ 'VM', gettext('Rollback') ],
+ qmdelsnapshot: [ 'VM', gettext('Delete Snapshot') ],
+ qmcreate: [ 'VM', gettext('Create') ],
+ qmrestore: [ 'VM', gettext('Restore') ],
+ qmdestroy: [ 'VM', gettext('Destroy') ],
+ qmigrate: [ 'VM', gettext('Migrate') ],
+ qmclone: [ 'VM', gettext('Clone') ],
+ qmmove: [ 'VM', gettext('Move disk') ],
+ qmtemplate: [ 'VM', gettext('Convert to template') ],
+ qmstart: [ 'VM', gettext('Start') ],
+ qmstop: [ 'VM', gettext('Stop') ],
+ qmreset: [ 'VM', gettext('Reset') ],
+ qmshutdown: [ 'VM', gettext('Shutdown') ],
+ qmsuspend: [ 'VM', gettext('Hibernate') ],
+ qmpause: [ 'VM', gettext('Pause') ],
+ qmresume: [ 'VM', gettext('Resume') ],
+ qmconfig: [ 'VM', gettext('Configure') ],
+ vzsnapshot: [ 'CT', gettext('Snapshot') ],
+ vzrollback: [ 'CT', gettext('Rollback') ],
+ vzdelsnapshot: [ 'CT', gettext('Delete Snapshot') ],
+ vzcreate: ['CT', gettext('Create') ],
+ vzrestore: ['CT', gettext('Restore') ],
+ vzdestroy: ['CT', gettext('Destroy') ],
+ vzmigrate: [ 'CT', gettext('Migrate') ],
+ vzclone: [ 'CT', gettext('Clone') ],
+ vztemplate: [ 'CT', gettext('Convert to template') ],
+ vzstart: ['CT', gettext('Start') ],
+ vzstop: ['CT', gettext('Stop') ],
+ vzmount: ['CT', gettext('Mount') ],
+ vzumount: ['CT', gettext('Unmount') ],
+ vzshutdown: ['CT', gettext('Shutdown') ],
+ vzsuspend: [ 'CT', gettext('Suspend') ],
+ vzresume: [ 'CT', gettext('Resume') ],
+ hamigrate: [ 'HA', gettext('Migrate') ],
+ hastart: [ 'HA', gettext('Start') ],
+ hastop: [ 'HA', gettext('Stop') ],
+ srvstart: ['SRV', gettext('Start') ],
+ srvstop: ['SRV', gettext('Stop') ],
+ srvrestart: ['SRV', gettext('Restart') ],
+ srvreload: ['SRV', gettext('Reload') ],
+ cephcreatemgr: ['Ceph Manager', gettext('Create') ],
+ cephdestroymgr: ['Ceph Manager', gettext('Destroy') ],
+ cephcreatemon: ['Ceph Monitor', gettext('Create') ],
+ cephdestroymon: ['Ceph Monitor', gettext('Destroy') ],
+ cephcreateosd: ['Ceph OSD', gettext('Create') ],
+ cephdestroyosd: ['Ceph OSD', gettext('Destroy') ],
+ cephcreatepool: ['Ceph Pool', gettext('Create') ],
+ cephdestroypool: ['Ceph Pool', gettext('Destroy') ],
+ cephfscreate: ['CephFS', gettext('Create') ],
+ cephcreatemds: ['Ceph Metadata Server', gettext('Create') ],
+ cephdestroymds: ['Ceph Metadata Server', gettext('Destroy') ],
+ imgcopy: ['', gettext('Copy data') ],
+ imgdel: ['', gettext('Erase data') ],
+ unknownimgdel: ['', gettext('Destroy image from unknown guest') ],
+ download: ['', gettext('Download') ],
+ vzdump: ['VM/CT', gettext('Backup') ],
+ aptupdate: ['', gettext('Update package database') ],
+ startall: [ '', gettext('Start all VMs and Containers') ],
+ stopall: [ '', gettext('Stop all VMs and Containers') ],
+ migrateall: [ '', gettext('Migrate all VMs and Containers') ],
+ dircreate: [ gettext('Directory Storage'), gettext('Create') ],
+ lvmcreate: [ gettext('LVM Storage'), gettext('Create') ],
+ lvmthincreate: [ gettext('LVM-Thin Storage'), gettext('Create') ],
+ zfscreate: [ gettext('ZFS Storage'), gettext('Create') ]
+ },
+
+ format_task_description: function(type, id) {
+ var farray = Proxmox.Utils.task_desc_table[type];
+ var text;
+ if (!farray) {
+ text = type;
+ if (id) {
+ type += ' ' + id;
+ }
+ return text;
+ }
+ var prefix = farray[0];
+ text = farray[1];
+ if (prefix) {
+ return prefix + ' ' + id + ' - ' + text;
+ }
+ return text;
+ },
+
+ format_size: function(size) {
+ /*jslint confusion: true */
+
+ var units = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'];
+ var num = 0;
+
+ while (size >= 1024 && ((num++)+1) < units.length) {
+ size = size / 1024;
+ }
+
+ return size.toFixed((num > 0)?2:0) + " " + units[num] + "B";
+ },
+
+ render_upid: function(value, metaData, record) {
+ var type = record.data.type;
+ var id = record.data.id;
+
+ return Proxmox.Utils.format_task_description(type, id);
+ },
+
+ render_uptime: function(value) {
+
+ var uptime = value;
+
+ if (uptime === undefined) {
+ return '';
+ }
+
+ if (uptime <= 0) {
+ return '-';
+ }
+
+ return Proxmox.Utils.format_duration_long(uptime);
+ },
+
+ parse_task_upid: function(upid) {
+ var task = {};
+
+ var res = upid.match(/^UPID:(\S+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/);
+ if (!res) {
+ throw "unable to parse upid '" + upid + "'";
+ }
+ task.node = res[1];
+ task.pid = parseInt(res[2], 16);
+ task.pstart = parseInt(res[3], 16);
+ task.starttime = parseInt(res[4], 16);
+ task.type = res[5];
+ task.id = res[6];
+ task.user = res[7];
+
+ task.desc = Proxmox.Utils.format_task_description(task.type, task.id);
+
+ return task;
+ },
+
+ render_timestamp: function(value, metaData, record, rowIndex, colIndex, store) {
+ var servertime = new Date(value * 1000);
+ return Ext.Date.format(servertime, 'Y-m-d H:i:s');
+ },
+
+ get_help_info: function(section) {
+ var helpMap;
+ if (typeof proxmoxOnlineHelpInfo !== 'undefined') {
+ helpMap = proxmoxOnlineHelpInfo;
+ } else if (typeof pveOnlineHelpInfo !== 'undefined') {
+ // be backward compatible with older pve-doc-generators
+ helpMap = pveOnlineHelpInfo;
+ } else {
+ throw "no global OnlineHelpInfo map declared";
+ }
+
+ return helpMap[section];
+ },
+
+ get_help_link: function(section) {
+ var info = Proxmox.Utils.get_help_info(section);
+ if (!info) {
+ return;
+ }
+
+ return window.location.origin + info.link;
+ },
+
+ openXtermJsViewer: function(vmtype, vmid, nodename, vmname, cmd) {
+ var url = Ext.Object.toQueryString({
+ console: vmtype, // kvm, lxc, upgrade or shell
+ xtermjs: 1,
+ vmid: vmid,
+ vmname: vmname,
+ node: nodename,
+ cmd: cmd,
+
+ });
+ var nw = window.open("?" + url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,resizable=yes,width=800,height=420');
+ if (nw) {
+ nw.focus();
+ }
+ }
+
+},
+
+ singleton: true,
+ constructor: function() {
+ var me = this;
+ Ext.apply(me, me.utilities);
+
+ var IPV4_OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
+ var IPV4_REGEXP = "(?:(?:" + IPV4_OCTET + "\\.){3}" + IPV4_OCTET + ")";
+ var IPV6_H16 = "(?:[0-9a-fA-F]{1,4})";
+ var IPV6_LS32 = "(?:(?:" + IPV6_H16 + ":" + IPV6_H16 + ")|" + IPV4_REGEXP + ")";
+ var IPV4_CIDR_MASK = "([0-9]{1,2})";
+ var IPV6_CIDR_MASK = "([0-9]{1,3})";
+
+
+ me.IP4_match = new RegExp("^(?:" + IPV4_REGEXP + ")$");
+ me.IP4_cidr_match = new RegExp("^(?:" + IPV4_REGEXP + ")\/" + IPV4_CIDR_MASK + "$");
+
+ var IPV6_REGEXP = "(?:" +
+ "(?:(?:" + "(?:" + IPV6_H16 + ":){6})" + IPV6_LS32 + ")|" +
+ "(?:(?:" + "::" + "(?:" + IPV6_H16 + ":){5})" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){4})" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,1}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){3})" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,2}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){2})" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,3}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){1})" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,4}" + IPV6_H16 + ")?::" + ")" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,5}" + IPV6_H16 + ")?::" + ")" + IPV6_H16 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,7}" + IPV6_H16 + ")?::" + ")" + ")" +
+ ")";
+
+ me.IP6_match = new RegExp("^(?:" + IPV6_REGEXP + ")$");
+ me.IP6_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + ")\/" + IPV6_CIDR_MASK + "$");
+ me.IP6_bracket_match = new RegExp("^\\[(" + IPV6_REGEXP + ")\\]");
+
+ me.IP64_match = new RegExp("^(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + ")$");
+ me.IP64_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + "\/" + IPV6_CIDR_MASK + ")|(?:" + IPV4_REGEXP + "\/" + IPV4_CIDR_MASK + ")$");
+
+ var DnsName_REGEXP = "(?:(([a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)\\.)*([A-Za-z0-9]([A-Za-z0-9\\-]*[A-Za-z0-9])?))";
+ me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$");
+
+ me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(:\\d+)?$");
+ me.HostPortBrackets_match = new RegExp("^\\[(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](:\\d+)?$");
+ me.IP6_dotnotation_match = new RegExp("^" + IPV6_REGEXP + "(\\.\\d+)?$");
+ }
+});
+// ExtJS related things
+
+ // do not send '_dc' parameter
+Ext.Ajax.disableCaching = false;
+
+// custom Vtypes
+Ext.apply(Ext.form.field.VTypes, {
+ IPAddress: function(v) {
+ return Proxmox.Utils.IP4_match.test(v);
+ },
+ IPAddressText: gettext('Example') + ': 192.168.1.1',
+ IPAddressMask: /[\d\.]/i,
+
+ IPCIDRAddress: function(v) {
+ var result = Proxmox.Utils.IP4_cidr_match.exec(v);
+ // limits according to JSON Schema see
+ // pve-common/src/PVE/JSONSchema.pm
+ return (result !== null && result[1] >= 8 && result[1] <= 32);
+ },
+ IPCIDRAddressText: gettext('Example') + ': 192.168.1.1/24' + "
" + gettext('Valid CIDR Range') + ': 8-32',
+ IPCIDRAddressMask: /[\d\.\/]/i,
+
+ IP6Address: function(v) {
+ return Proxmox.Utils.IP6_match.test(v);
+ },
+ IP6AddressText: gettext('Example') + ': 2001:DB8::42',
+ IP6AddressMask: /[A-Fa-f0-9:]/,
+
+ IP6CIDRAddress: function(v) {
+ var result = Proxmox.Utils.IP6_cidr_match.exec(v);
+ // limits according to JSON Schema see
+ // pve-common/src/PVE/JSONSchema.pm
+ return (result !== null && result[1] >= 8 && result[1] <= 128);
+ },
+ IP6CIDRAddressText: gettext('Example') + ': 2001:DB8::42/64' + "
" + gettext('Valid CIDR Range') + ': 8-128',
+ IP6CIDRAddressMask: /[A-Fa-f0-9:\/]/,
+
+ IP6PrefixLength: function(v) {
+ return v >= 0 && v <= 128;
+ },
+ IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128',
+ IP6PrefixLengthMask: /[0-9]/,
+
+ IP64Address: function(v) {
+ return Proxmox.Utils.IP64_match.test(v);
+ },
+ IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
+ IP64AddressMask: /[A-Fa-f0-9\.:]/,
+
+ IP64CIDRAddress: function(v) {
+ var result = Proxmox.Utils.IP64_cidr_match.exec(v);
+ if (result === null) {
+ return false;
+ }
+ if (result[1] !== undefined) {
+ return result[1] >= 8 && result[1] <= 128;
+ } else if (result[2] !== undefined) {
+ return result[2] >= 8 && result[2] <= 32;
+ } else {
+ return false;
+ }
+ },
+ IP64CIDRAddressText: gettext('Example') + ': 192.168.1.1/24 2001:DB8::42/64',
+ IP64CIDRAddressMask: /[A-Fa-f0-9\.:\/]/,
+
+ MacAddress: function(v) {
+ return (/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/).test(v);
+ },
+ MacAddressMask: /[a-fA-F0-9:]/,
+ MacAddressText: gettext('Example') + ': 01:23:45:67:89:ab',
+
+ MacPrefix: function(v) {
+ return (/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i).test(v);
+ },
+ MacPrefixMask: /[a-fA-F0-9:]/,
+ MacPrefixText: gettext('Example') + ': 02:8f - ' + gettext('only unicast addresses are allowed'),
+
+ BridgeName: function(v) {
+ return (/^vmbr\d{1,4}$/).test(v);
+ },
+ BridgeNameText: gettext('Format') + ': vmbrN, where 0 <= N <= 9999',
+
+ BondName: function(v) {
+ return (/^bond\d{1,4}$/).test(v);
+ },
+ BondNameText: gettext('Format') + ': bondN, where 0 <= N <= 9999',
+
+ InterfaceName: function(v) {
+ return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
+ },
+ InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'" + "
" +
+ gettext("Minimum characters") + ": 2" + "
" +
+ gettext("Maximum characters") + ": 21" + "
" +
+ gettext("Must start with") + ": 'a-z'",
+
+ StorageId: function(v) {
+ return (/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i).test(v);
+ },
+ StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'" + "
" +
+ gettext("Minimum characters") + ": 2" + "
" +
+ gettext("Must start with") + ": 'A-Z', 'a-z'
" +
+ gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'
",
+
+ ConfigId: function(v) {
+ return (/^[a-z][a-z0-9\_]+$/i).test(v);
+ },
+ ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'" + "
" +
+ gettext("Minimum characters") + ": 2" + "
" +
+ gettext("Must start with") + ": " + gettext("letter"),
+
+ HttpProxy: function(v) {
+ return (/^http:\/\/.*$/).test(v);
+ },
+ HttpProxyText: gettext('Example') + ": http://username:password@host:port/",
+
+ DnsName: function(v) {
+ return Proxmox.Utils.DnsName_match.test(v);
+ },
+ DnsNameText: gettext('This is not a valid DNS name'),
+
+ // workaround for https://www.sencha.com/forum/showthread.php?302150
+ proxmoxMail: function(v) {
+ return (/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/).test(v);
+ },
+ proxmoxMailText: gettext('Example') + ": user@example.com",
+
+ DnsOrIp: function(v) {
+ if (!Proxmox.Utils.DnsName_match.test(v) &&
+ !Proxmox.Utils.IP64_match.test(v)) {
+ return false;
+ }
+
+ return true;
+ },
+ DnsOrIpText: gettext('Not a valid DNS name or IP address.'),
+
+ HostList: function(v) {
+ var list = v.split(/[\ \,\;]+/);
+ var i;
+ for (i = 0; i < list.length; i++) {
+ if (list[i] == "") {
+ continue;
+ }
+
+ if (!Proxmox.Utils.HostPort_match.test(list[i]) &&
+ !Proxmox.Utils.HostPortBrackets_match.test(list[i]) &&
+ !Proxmox.Utils.IP6_dotnotation_match.test(list[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+ HostListText: gettext('Not a valid list of hosts'),
+
+ password: function(val, field) {
+ if (field.initialPassField) {
+ var pwd = field.up('form').down(
+ '[name=' + field.initialPassField + ']');
+ return (val == pwd.getValue());
+ }
+ return true;
+ },
+
+ passwordText: gettext('Passwords do not match')
+});
+
+// Firefox 52+ Touchscreen bug
+// see https://www.sencha.com/forum/showthread.php?336762-Examples-don-t-work-in-Firefox-52-touchscreen/page2
+// and https://bugzilla.proxmox.com/show_bug.cgi?id=1223
+Ext.define('EXTJS_23846.Element', {
+ override: 'Ext.dom.Element'
+}, function(Element) {
+ var supports = Ext.supports,
+ proto = Element.prototype,
+ eventMap = proto.eventMap,
+ additiveEvents = proto.additiveEvents;
+
+ if (Ext.os.is.Desktop && supports.TouchEvents && !supports.PointerEvents) {
+ eventMap.touchstart = 'mousedown';
+ eventMap.touchmove = 'mousemove';
+ eventMap.touchend = 'mouseup';
+ eventMap.touchcancel = 'mouseup';
+
+ additiveEvents.mousedown = 'mousedown';
+ additiveEvents.mousemove = 'mousemove';
+ additiveEvents.mouseup = 'mouseup';
+ additiveEvents.touchstart = 'touchstart';
+ additiveEvents.touchmove = 'touchmove';
+ additiveEvents.touchend = 'touchend';
+ additiveEvents.touchcancel = 'touchcancel';
+
+ additiveEvents.pointerdown = 'mousedown';
+ additiveEvents.pointermove = 'mousemove';
+ additiveEvents.pointerup = 'mouseup';
+ additiveEvents.pointercancel = 'mouseup';
+ }
+});
+
+Ext.define('EXTJS_23846.Gesture', {
+ override: 'Ext.event.publisher.Gesture'
+}, function(Gesture) {
+ var me = Gesture.instance;
+
+ if (Ext.supports.TouchEvents && !Ext.isWebKit && Ext.os.is.Desktop) {
+ me.handledDomEvents.push('mousedown', 'mousemove', 'mouseup');
+ me.registerEvents();
+ }
+});
+
+Ext.define('EXTJS_18900.Pie', {
+ override: 'Ext.chart.series.Pie',
+
+ // from 6.0.2
+ betweenAngle: function (x, a, b) {
+ var pp = Math.PI * 2,
+ offset = this.rotationOffset;
+
+ if (a === b) {
+ return false;
+ }
+
+ if (!this.getClockwise()) {
+ x *= -1;
+ a *= -1;
+ b *= -1;
+ a -= offset;
+ b -= offset;
+ } else {
+ a += offset;
+ b += offset;
+ }
+
+ x -= a;
+ b -= a;
+
+ // Normalize, so that both x and b are in the [0,360) interval.
+ x %= pp;
+ b %= pp;
+ x += pp;
+ b += pp;
+ x %= pp;
+ b %= pp;
+
+ // Because 360 * n angles will be normalized to 0,
+ // we need to treat b === 0 as a special case.
+ return x < b || b === 0;
+ },
+});
+
+// we always want the number in x.y format and never in, e.g., x,y
+Ext.define('PVE.form.field.Number', {
+ override: 'Ext.form.field.Number',
+ submitLocaleSeparator: false
+});
+
+// ExtJs 5-6 has an issue with caching
+// see https://www.sencha.com/forum/showthread.php?308989
+Ext.define('Proxmox.UnderlayPool', {
+ override: 'Ext.dom.UnderlayPool',
+
+ checkOut: function () {
+ var cache = this.cache,
+ len = cache.length,
+ el;
+
+ // do cleanup because some of the objects might have been destroyed
+ while (len--) {
+ if (cache[len].destroyed) {
+ cache.splice(len, 1);
+ }
+ }
+ // end do cleanup
+
+ el = cache.shift();
+
+ if (!el) {
+ el = Ext.Element.create(this.elementConfig);
+ el.setVisibilityMode(2);
+ //
'), first - 1, total);
+ me.running = false;
+ if (me.requested) {
+ me.requested = false;
+ view.loadTask.delay(200);
+ }
+ },
+ failure: function(response) {
+ if (view.failCallback) {
+ view.failCallback(response);
+ } else {
+ var msg = response.htmlStatus;
+ Proxmox.Utils.setErrorMask(me, msg);
+ }
+ me.running = false;
+ if (me.requested) {
+ me.requested = false;
+ view.loadTask.delay(200);
+ }
+ }
+ });
+ },
+
+ onScroll: function(x, y) {
+ var me = this;
+ var view = me.getView();
+ var viewModel = me.getViewModel();
+
+ var lineHeight = view.lineHeight;
+ var line = view.getScrollY()/lineHeight;
+ var start = viewModel.get('params.start');
+ var limit = viewModel.get('params.limit');
+ var viewLines = view.getHeight()/lineHeight;
+
+ var viewStart = Math.max(parseInt(line - 1 - view.viewBuffer, 10), 0);
+ var viewEnd = parseInt(line + viewLines + 1 + view.viewBuffer, 10);
+
+ if (viewStart < start || viewEnd > (start+limit)) {
+ viewModel.set('params.start',
+ Math.max(parseInt(line - limit/2 + 10, 10), 0));
+ view.loadTask.delay(200);
+ }
+ },
+
+ init: function(view) {
+ var me = this;
+
+ if (!view.url) {
+ throw "no url specified";
+ }
+
+ var viewModel = this.getViewModel();
+ var since = new Date();
+ since.setDate(since.getDate() - 3);
+ viewModel.set('until', new Date());
+ viewModel.set('since', since);
+ viewModel.set('params.limit', view.pageSize);
+ viewModel.set('hide_timespan', !view.log_select_timespan);
+ me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
+
+ view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
+
+ me.updateParams();
+ view.task = Ext.TaskManager.start({
+ run: function() {
+ if (!view.isVisible() || !view.scrollToEnd) {
+ return;
+ }
+
+ if (me.scrollPosBottom() <= 1) {
+ view.loadTask.delay(200);
+ }
+ },
+ interval: 1000
+ });
+ }
+ },
+
+ onDestroy: function() {
+ var me = this;
+ me.loadTask.cancel();
+ Ext.TaskManager.stop(me.task);
+ },
+
+ // for user to initiate a load from outside
+ requestUpdate: function() {
+ var me = this;
+ me.loadTask.delay(200);
+ },
+
+ viewModel: {
+ data: {
+ until: null,
+ since: null,
+ hide_timespan: false,
+ data: {
+ start: 0,
+ total: 0,
+ textlen: 0
+ },
+ params: {
+ start: 0,
+ limit: 500,
+ }
+ }
+ },
+
+ layout: 'auto',
+ bodyPadding: 5,
+ scrollable: {
+ x: 'auto',
+ y: 'auto',
+ listeners: {
+ // we have to have this here, since we cannot listen to events
+ // of the scroller in the viewcontroller (extjs bug?), nor does
+ // the panel have a 'scroll' event'
+ scroll: {
+ fn: function(scroller, x, y) {
+ var controller = this.component.getController();
+ if (controller) { // on destroy, controller can be gone
+ controller.onScroll(x,y);
+ }
+ },
+ buffer: 200
+ },
+ }
+ },
+
+ tbar: {
+ bind: {
+ hidden: '{hide_timespan}'
+ },
+ items: [
+ '->',
+ 'Since: ',
+ {
+ xtype: 'datefield',
+ name: 'since_date',
+ reference: 'since',
+ format: 'Y-m-d',
+ bind: {
+ value: '{since}',
+ maxValue: '{until}'
+ }
+ },
+ 'Until: ',
+ {
+ xtype: 'datefield',
+ name: 'until_date',
+ reference: 'until',
+ format: 'Y-m-d',
+ bind: {
+ value: '{until}',
+ minValue: '{since}'
+ }
+ },
+ {
+ xtype: 'button',
+ text: 'Update',
+ handler: 'updateParams'
+ }
+ ],
+ },
+
+ items: [
+ {
+ xtype: 'box',
+ reference: 'content',
+ style: {
+ font: 'normal 11px tahoma, arial, verdana, sans-serif',
+ 'white-space': 'pre'
+ },
+ }
+ ]
+});
+/*
+ * Display log entries in a panel with scrollbar
+ * The log entries are automatically refreshed via a background task,
+ * with newest entries comming at the bottom
+ */
+Ext.define('Proxmox.panel.JournalView', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'proxmoxJournalView',
+
+ numEntries: 500,
+ lineHeight: 16,
+
+ scrollToEnd: true,
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ updateParams: function() {
+ var me = this;
+ var viewModel = me.getViewModel();
+ var since = viewModel.get('since');
+ var until = viewModel.get('until');
+
+ since.setHours(0, 0, 0, 0);
+ until.setHours(0, 0, 0, 0);
+ until.setDate(until.getDate()+1);
+
+ me.getView().loadTask.delay(200, undefined, undefined, [
+ false,
+ false,
+ Ext.Date.format(since, "U"),
+ Ext.Date.format(until, "U")
+ ]);
+ },
+
+ scrollPosBottom: function() {
+ var view = this.getView();
+ var pos = view.getScrollY();
+ var maxPos = view.getScrollable().getMaxPosition().y;
+ return maxPos - pos;
+ },
+
+ scrollPosTop: function() {
+ var view = this.getView();
+ return view.getScrollY();
+ },
+
+ updateScroll: function(livemode, num, scrollPos, scrollPosTop) {
+ var me = this;
+ var view = me.getView();
+
+ if (!livemode) {
+ setTimeout(function() { view.scrollTo(0, 0); }, 10);
+ } else if (view.scrollToEnd && scrollPos <= 0) {
+ setTimeout(function() { view.scrollTo(0, Infinity); }, 10);
+ } else if (!view.scrollToEnd && scrollPosTop < 20*view.lineHeight) {
+ setTimeout(function() { view.scrollTo(0, num*view.lineHeight + scrollPosTop); }, 10);
+ }
+ },
+
+ updateView: function(lines, livemode, top) {
+ var me = this;
+ var view = me.getView();
+ var viewmodel = me.getViewModel();
+ if (viewmodel.get('livemode') !== livemode) {
+ return; // we switched mode, do not update the content
+ }
+ var contentEl = me.lookup('content');
+
+ // save old scrollpositions
+ var scrollPos = me.scrollPosBottom();
+ var scrollPosTop = me.scrollPosTop();
+
+ var newend = lines.shift();
+ var newstart = lines.pop();
+
+ var num = lines.length;
+ var text = lines.map(Ext.htmlEncode).join('
');
+
+ if (!livemode) {
+ if (num) {
+ view.content = text;
+ } else {
+ view.content = 'nothing logged or no timespan selected';
+ }
+ } else {
+ // update content
+ if (top && num) {
+ view.content = view.content ? text + '
' + view.content : text;
+ } else if (!top && num) {
+ view.content = view.content ? view.content + '
' + text : text;
+ }
+
+ // update cursors
+ if (!top || !view.startcursor) {
+ view.startcursor = newstart;
+ }
+
+ if (top || !view.endcursor) {
+ view.endcursor = newend;
+ }
+ }
+
+ contentEl.update(view.content);
+
+ me.updateScroll(livemode, num, scrollPos, scrollPosTop);
+ },
+
+ doLoad: function(livemode, top, since, until) {
+ var me = this;
+ if (me.running) {
+ me.requested = true;
+ return;
+ }
+ me.running = true;
+ var view = me.getView();
+ var params = {
+ lastentries: view.numEntries || 500,
+ };
+ if (livemode) {
+ if (!top && view.startcursor) {
+ params = {
+ startcursor: view.startcursor
+ };
+ } else if (view.endcursor) {
+ params.endcursor = view.endcursor;
+ }
+ } else {
+ params = {
+ since: since,
+ until: until
+ };
+ }
+ Proxmox.Utils.API2Request({
+ url: view.url,
+ params: params,
+ waitMsgTarget: (!livemode) ? view : undefined,
+ method: 'GET',
+ success: function(response) {
+ Proxmox.Utils.setErrorMask(me, false);
+ var lines = response.result.data;
+ me.updateView(lines, livemode, top);
+ me.running = false;
+ if (me.requested) {
+ me.requested = false;
+ view.loadTask.delay(200);
+ }
+ },
+ failure: function(response) {
+ var msg = response.htmlStatus;
+ Proxmox.Utils.setErrorMask(me, msg);
+ me.running = false;
+ if (me.requested) {
+ me.requested = false;
+ view.loadTask.delay(200);
+ }
+ }
+ });
+ },
+
+ onScroll: function(x, y) {
+ var me = this;
+ var view = me.getView();
+ var viewmodel = me.getViewModel();
+ var livemode = viewmodel.get('livemode');
+ if (!livemode) {
+ return;
+ }
+
+ if (me.scrollPosTop() < 20*view.lineHeight) {
+ view.scrollToEnd = false;
+ view.loadTask.delay(200, undefined, undefined, [true, true]);
+ } else if (me.scrollPosBottom() <= 1) {
+ view.scrollToEnd = true;
+ }
+ },
+
+ init: function(view) {
+ var me = this;
+
+ if (!view.url) {
+ throw "no url specified";
+ }
+
+ var viewmodel = me.getViewModel();
+ var viewModel = this.getViewModel();
+ var since = new Date();
+ since.setDate(since.getDate() - 3);
+ viewModel.set('until', new Date());
+ viewModel.set('since', since);
+ me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
+
+ view.loadTask = new Ext.util.DelayedTask(me.doLoad, me, [true, false]);
+
+ me.updateParams();
+ view.task = Ext.TaskManager.start({
+ run: function() {
+ if (!view.isVisible() || !view.scrollToEnd || !viewmodel.get('livemode')) {
+ return;
+ }
+
+ if (me.scrollPosBottom() <= 1) {
+ view.loadTask.delay(200, undefined, undefined, [true, false]);
+ }
+ },
+ interval: 1000
+ });
+ },
+
+ onLiveMode: function() {
+ var me = this;
+ var view = me.getView();
+ delete view.startcursor;
+ delete view.endcursor;
+ delete view.content;
+ me.getViewModel().set('livemode', true);
+ view.scrollToEnd = true;
+ me.updateView([], true, false);
+ },
+
+ onTimespan: function() {
+ var me = this;
+ me.getViewModel().set('livemode', false);
+ me.updateView([], false);
+ }
+ },
+
+ onDestroy: function() {
+ var me = this;
+ me.loadTask.cancel();
+ Ext.TaskManager.stop(me.task);
+ delete me.content;
+ },
+
+ // for user to initiate a load from outside
+ requestUpdate: function() {
+ var me = this;
+ me.loadTask.delay(200);
+ },
+
+ viewModel: {
+ data: {
+ livemode: true,
+ until: null,
+ since: null
+ }
+ },
+
+ layout: 'auto',
+ bodyPadding: 5,
+ scrollable: {
+ x: 'auto',
+ y: 'auto',
+ listeners: {
+ // we have to have this here, since we cannot listen to events
+ // of the scroller in the viewcontroller (extjs bug?), nor does
+ // the panel have a 'scroll' event'
+ scroll: {
+ fn: function(scroller, x, y) {
+ var controller = this.component.getController();
+ if (controller) { // on destroy, controller can be gone
+ controller.onScroll(x,y);
+ }
+ },
+ buffer: 200
+ },
+ }
+ },
+
+ tbar: {
+
+ items: [
+ '->',
+ {
+ xtype: 'segmentedbutton',
+ items: [
+ {
+ text: gettext('Live Mode'),
+ bind: {
+ pressed: '{livemode}'
+ },
+ handler: 'onLiveMode',
+ },
+ {
+ text: gettext('Select Timespan'),
+ bind: {
+ pressed: '{!livemode}'
+ },
+ handler: 'onTimespan',
+ }
+ ]
+ },
+ {
+ xtype: 'box',
+ bind: { disabled: '{livemode}' },
+ autoEl: { cn: gettext('Since') + ':' }
+ },
+ {
+ xtype: 'datefield',
+ name: 'since_date',
+ reference: 'since',
+ format: 'Y-m-d',
+ bind: {
+ disabled: '{livemode}',
+ value: '{since}',
+ maxValue: '{until}'
+ }
+ },
+ {
+ xtype: 'box',
+ bind: { disabled: '{livemode}' },
+ autoEl: { cn: gettext('Until') + ':' }
+ },
+ {
+ xtype: 'datefield',
+ name: 'until_date',
+ reference: 'until',
+ format: 'Y-m-d',
+ bind: {
+ disabled: '{livemode}',
+ value: '{until}',
+ minValue: '{since}'
+ }
+ },
+ {
+ xtype: 'button',
+ text: 'Update',
+ reference: 'updateBtn',
+ handler: 'updateParams',
+ bind: {
+ disabled: '{livemode}'
+ }
+ }
+ ]
+ },
+
+ items: [
+ {
+ xtype: 'box',
+ reference: 'content',
+ style: {
+ font: 'normal 11px tahoma, arial, verdana, sans-serif',
+ 'white-space': 'pre'
+ },
+ }
+ ]
+});
+Ext.define('Proxmox.widget.RRDChart', {
+ extend: 'Ext.chart.CartesianChart',
+ alias: 'widget.proxmoxRRDChart',
+
+ unit: undefined, // bytes, bytespersecond, percent
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ convertToUnits: function(value) {
+ var units = ['', 'k','M','G','T', 'P'];
+ var si = 0;
+ while(value >= 1000 && si < (units.length -1)){
+ value = value / 1000;
+ si++;
+ }
+
+ // javascript floating point weirdness
+ value = Ext.Number.correctFloat(value);
+
+ // limit to 2 decimal points
+ value = Ext.util.Format.number(value, "0.##");
+
+ return value.toString() + " " + units[si];
+ },
+
+ leftAxisRenderer: function(axis, label, layoutContext) {
+ var me = this;
+
+ return me.convertToUnits(label);
+ },
+
+ onSeriesTooltipRender: function(tooltip, record, item) {
+ var me = this.getView();
+
+ var suffix = '';
+
+ if (me.unit === 'percent') {
+ suffix = '%';
+ } else if (me.unit === 'bytes') {
+ suffix = 'B';
+ } else if (me.unit === 'bytespersecond') {
+ suffix = 'B/s';
+ }
+
+ var prefix = item.field;
+ if (me.fieldTitles && me.fieldTitles[me.fields.indexOf(item.field)]) {
+ prefix = me.fieldTitles[me.fields.indexOf(item.field)];
+ }
+ tooltip.setHtml(prefix + ': ' + this.convertToUnits(record.get(item.field)) + suffix +
+ '
' + new Date(record.get('time')));
+ },
+
+ onAfterAnimation: function(chart, eopts) {
+ // if the undobuton is disabled,
+ // disable our tool
+
+ var ourUndoZoomButton = chart.tools[0];
+ var undoButton = chart.interactions[0].getUndoButton();
+ ourUndoZoomButton.setDisabled(undoButton.isDisabled());
+ }
+ },
+
+ width: 770,
+ height: 300,
+ animation: false,
+ interactions: [{
+ type: 'crosszoom'
+ }],
+ axes: [{
+ type: 'numeric',
+ position: 'left',
+ grid: true,
+ renderer: 'leftAxisRenderer',
+ //renderer: function(axis, label) { return label; },
+ minimum: 0
+ }, {
+ type: 'time',
+ position: 'bottom',
+ grid: true,
+ fields: ['time']
+ }],
+ legend: {
+ docked: 'bottom'
+ },
+ listeners: {
+ animationend: 'onAfterAnimation'
+ },
+
+
+ initComponent: function() {
+ var me = this;
+ var series = {};
+
+ if (!me.store) {
+ throw "cannot work without store";
+ }
+
+ if (!me.fields) {
+ throw "cannot work without fields";
+ }
+
+ me.callParent();
+
+ // add correct label for left axis
+ var axisTitle = "";
+ if (me.unit === 'percent') {
+ axisTitle = "%";
+ } else if (me.unit === 'bytes') {
+ axisTitle = "Bytes";
+ } else if (me.unit === 'bytespersecond') {
+ axisTitle = "Bytes/s";
+ } else if (me.fieldTitles && me.fieldTitles.length === 1) {
+ axisTitle = me.fieldTitles[0];
+ } else if (me.fields.length === 1) {
+ axisTitle = me.fields[0];
+ }
+
+ me.axes[0].setTitle(axisTitle);
+
+ if (!me.noTool) {
+ me.addTool([{
+ type: 'minus',
+ disabled: true,
+ tooltip: gettext('Undo Zoom'),
+ handler: function(){
+ var undoButton = me.interactions[0].getUndoButton();
+ if (undoButton.handler) {
+ undoButton.handler();
+ }
+ }
+ },{
+ type: 'restore',
+ tooltip: gettext('Toggle Legend'),
+ handler: function(){
+ if (me.legend) {
+ me.legend.setVisible(!me.legend.isVisible());
+ }
+ }
+ }]);
+ }
+
+ // add a series for each field we get
+ me.fields.forEach(function(item, index){
+ var title = item;
+ if (me.fieldTitles && me.fieldTitles[index]) {
+ title = me.fieldTitles[index];
+ }
+ me.addSeries(Ext.apply(
+ {
+ type: 'line',
+ xField: 'time',
+ yField: item,
+ title: title,
+ fill: true,
+ style: {
+ lineWidth: 1.5,
+ opacity: 0.60
+ },
+ marker: {
+ opacity: 0,
+ scaling: 0.01,
+ fx: {
+ duration: 200,
+ easing: 'easeOut'
+ }
+ },
+ highlightCfg: {
+ opacity: 1,
+ scaling: 1.5
+ },
+ tooltip: {
+ trackMouse: true,
+ renderer: 'onSeriesTooltipRender'
+ }
+ },
+ me.seriesConfig
+ ));
+ });
+
+ // enable animation after the store is loaded
+ me.store.onAfter('load', function() {
+ me.setAnimation(true);
+ }, this, {single: true});
+ }
+});
+Ext.define('Proxmox.panel.GaugeWidget', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.proxmoxGauge',
+
+ defaults: {
+ style: {
+ 'text-align':'center'
+ }
+ },
+ items: [
+ {
+ xtype: 'box',
+ itemId: 'title',
+ data: {
+ title: ''
+ },
+ tpl: '{title}
'
+ },
+ {
+ xtype: 'polar',
+ height: 120,
+ border: false,
+ itemId: 'chart',
+ series: [{
+ type: 'gauge',
+ value: 0,
+ colors: ['#f5f5f5'],
+ sectors: [0],
+ donut: 90,
+ needleLength: 100,
+ totalAngle: Math.PI
+ }],
+ sprites: [{
+ id: 'valueSprite',
+ type: 'text',
+ text: '',
+ textAlign: 'center',
+ textBaseline: 'bottom',
+ x: 125,
+ y: 110,
+ fontSize: 30
+ }]
+ },
+ {
+ xtype: 'box',
+ itemId: 'text'
+ }
+ ],
+
+ header: false,
+ border: false,
+
+ warningThreshold: 0.6,
+ criticalThreshold: 0.9,
+ warningColor: '#fc0',
+ criticalColor: '#FF6C59',
+ defaultColor: '#7289DA',
+ backgroundColor: '#2C2F33',
+
+ initialValue: 0,
+
+
+ updateValue: function(value, text) {
+ var me = this;
+ var color = me.defaultColor;
+ var attr = {};
+
+ if (value >= me.criticalThreshold) {
+ color = me.criticalColor;
+ } else if (value >= me.warningThreshold) {
+ color = me.warningColor;
+ }
+
+ me.chart.series[0].setColors([color, me.backgroundColor]);
+ me.chart.series[0].setValue(value*100);
+
+ me.valueSprite.setText(' '+(value*100).toFixed(0) + '%');
+ attr.x = me.chart.getWidth()/2;
+ attr.y = me.chart.getHeight()-20;
+ if (me.spriteFontSize) {
+ attr.fontSize = me.spriteFontSize;
+ }
+ me.valueSprite.setAttributes(attr, true);
+
+ if (text !== undefined) {
+ me.text.setHtml(text);
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+
+ if (me.title) {
+ me.getComponent('title').update({title: me.title});
+ }
+ me.text = me.getComponent('text');
+ me.chart = me.getComponent('chart');
+ me.valueSprite = me.chart.getSurface('chart').get('valueSprite');
+ }
+});
+// fixme: how can we avoid those lint errors?
+/*jslint confusion: true */
+Ext.define('Proxmox.window.Edit', {
+ extend: 'Ext.window.Window',
+ alias: 'widget.proxmoxWindowEdit',
+
+ // autoLoad trigger a load() after component creation
+ autoLoad: false,
+
+ resizable: false,
+
+ // use this tio atimatically generate a title like
+ // Create: " + Ext.htmlEncode(changes) + "
");
+ changeitem.setHidden(false);
+ }
+ }
+ });
+ };
+
+ var run_editor = function() {
+ var grid = me.down('gridpanel');
+ var sm = grid.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iface: rec.data.iface,
+ iftype: rec.data.type
+ });
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ var edit_btn = new Ext.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ handler: run_editor
+ });
+
+ var del_btn = new Ext.Button({
+ text: gettext('Remove'),
+ disabled: true,
+ handler: function(){
+ var grid = me.down('gridpanel');
+ var sm = grid.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var iface = rec.data.iface;
+
+ Proxmox.Utils.API2Request({
+ url: baseUrl + '/' + iface,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ callback: function() {
+ reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ }
+ });
+
+ var set_button_status = function() {
+ var grid = me.down('gridpanel');
+ var sm = grid.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ edit_btn.setDisabled(!rec);
+ del_btn.setDisabled(!rec);
+ };
+
+ var render_ports = function(value, metaData, record) {
+ if (value === 'bridge') {
+ return record.data.bridge_ports;
+ } else if (value === 'bond') {
+ return record.data.slaves;
+ } else if (value === 'OVSBridge') {
+ return record.data.ovs_ports;
+ } else if (value === 'OVSBond') {
+ return record.data.ovs_bonds;
+ }
+ };
+
+ var find_next_iface_id = function(prefix) {
+ var next;
+ for (next = 0; next <= 9999; next++) {
+ if (!store.getById(prefix + next.toString())) {
+ break;
+ }
+ }
+ return prefix + next.toString();
+ };
+
+ var menu_items = [];
+
+ if (me.types.indexOf('bridge') !== -1) {
+ menu_items.push({
+ text: Proxmox.Utils.render_network_iface_type('bridge'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'bridge',
+ iface_default: find_next_iface_id('vmbr')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ });
+ }
+
+ if (me.types.indexOf('bond') !== -1) {
+ menu_items.push({
+ text: Proxmox.Utils.render_network_iface_type('bond'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'bond',
+ iface_default: find_next_iface_id('bond')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ });
+ }
+
+ if (me.types.indexOf('ovs') !== -1) {
+ if (menu_items.length > 0) {
+ menu_items.push({ xtype: 'menuseparator' });
+ }
+
+ menu_items.push(
+ {
+ text: Proxmox.Utils.render_network_iface_type('OVSBridge'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'OVSBridge',
+ iface_default: find_next_iface_id('vmbr')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: Proxmox.Utils.render_network_iface_type('OVSBond'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'OVSBond',
+ iface_default: find_next_iface_id('bond')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'OVSIntPort'
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ }
+ );
+ }
+
+ var renderer_generator = function(fieldname) {
+ return function(val, metaData, rec) {
+ var tmp = [];
+ if (rec.data[fieldname]) {
+ tmp.push(rec.data[fieldname]);
+ }
+ if (rec.data[fieldname + '6']) {
+ tmp.push(rec.data[fieldname + '6']);
+ }
+ return tmp.join('
') || '';
+ };
+ };
+
+ Ext.apply(me, {
+ layout: 'border',
+ tbar: [
+ {
+ text: gettext('Create'),
+ menu: {
+ plain: true,
+ items: menu_items
+ }
+ }, ' ',
+ {
+ text: gettext('Revert'),
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: baseUrl,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ callback: function() {
+ reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ }
+ },
+ edit_btn,
+ del_btn
+ ],
+ items: [
+ {
+ xtype: 'gridpanel',
+ stateful: true,
+ stateId: 'grid-node-network',
+ store: store,
+ region: 'center',
+ border: false,
+ columns: [
+ {
+ header: gettext('Name'),
+ sortable: true,
+ dataIndex: 'iface'
+ },
+ {
+ header: gettext('Type'),
+ sortable: true,
+ width: 120,
+ renderer: Proxmox.Utils.render_network_iface_type,
+ dataIndex: 'type'
+ },
+ {
+ xtype: 'booleancolumn',
+ header: gettext('Active'),
+ width: 80,
+ sortable: true,
+ dataIndex: 'active',
+ trueText: Proxmox.Utils.yesText,
+ falseText: Proxmox.Utils.noText,
+ undefinedText: Proxmox.Utils.noText,
+ },
+ {
+ xtype: 'booleancolumn',
+ header: gettext('Autostart'),
+ width: 80,
+ sortable: true,
+ dataIndex: 'autostart',
+ trueText: Proxmox.Utils.yesText,
+ falseText: Proxmox.Utils.noText,
+ undefinedText: Proxmox.Utils.noText
+ },
+ {
+ xtype: 'booleancolumn',
+ header: gettext('VLAN aware'),
+ width: 80,
+ sortable: true,
+ dataIndex: 'bridge_vlan_aware',
+ trueText: Proxmox.Utils.yesText,
+ falseText: Proxmox.Utils.noText,
+ undefinedText: Proxmox.Utils.noText
+ },
+ {
+ header: gettext('Ports/Slaves'),
+ dataIndex: 'type',
+ renderer: render_ports
+ },
+ {
+ header: gettext('Bond Mode'),
+ dataIndex: 'bond_mode',
+ renderer: Proxmox.Utils.render_bond_mode,
+ },
+ {
+ header: gettext('Hash Policy'),
+ hidden: true,
+ dataIndex: 'bond_xmit_hash_policy',
+ },
+ {
+ header: gettext('IP address'),
+ sortable: true,
+ width: 120,
+ hidden: true,
+ dataIndex: 'address',
+ renderer: renderer_generator('address'),
+ },
+ {
+ header: gettext('Subnet mask'),
+ width: 120,
+ sortable: true,
+ hidden: true,
+ dataIndex: 'netmask',
+ renderer: renderer_generator('netmask'),
+ },
+ {
+ header: gettext('CIDR'),
+ width: 120,
+ sortable: true,
+ dataIndex: 'cidr',
+ renderer: renderer_generator('cidr'),
+ },
+ {
+ header: gettext('Gateway'),
+ width: 120,
+ sortable: true,
+ dataIndex: 'gateway',
+ renderer: renderer_generator('gateway'),
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comments',
+ flex: 1,
+ renderer: Ext.String.htmlEncode
+ }
+ ],
+ listeners: {
+ selectionchange: set_button_status,
+ itemdblclick: run_editor
+ }
+ },
+ {
+ border: false,
+ region: 'south',
+ autoScroll: true,
+ hidden: true,
+ itemId: 'changes',
+ tbar: [
+ gettext('Pending changes') + ' (' +
+ gettext('Please reboot to activate changes') + ')'
+ ],
+ split: true,
+ bodyPadding: 5,
+ flex: 0.6,
+ html: gettext("No changes")
+ }
+ ],
+ });
+
+ me.callParent();
+ reload();
+ }
+});
+Ext.define('Proxmox.node.DNSEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.proxmoxNodeDNSEdit'],
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.items = [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Search domain'),
+ name: 'search',
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('DNS server') + " 1",
+ vtype: 'IP64Address',
+ skipEmptyText: true,
+ name: 'dns1'
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('DNS server') + " 2",
+ vtype: 'IP64Address',
+ skipEmptyText: true,
+ name: 'dns2'
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('DNS server') + " 3",
+ vtype: 'IP64Address',
+ skipEmptyText: true,
+ name: 'dns3'
+ }
+ ];
+
+ Ext.applyIf(me, {
+ subject: gettext('DNS'),
+ url: "/api2/extjs/nodes/" + me.nodename + "/dns",
+ fieldDefaults: {
+ labelWidth: 120
+ }
+ });
+
+ me.callParent();
+
+ me.load();
+ }
+});
+Ext.define('Proxmox.node.HostsView', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'proxmoxNodeHostsView',
+
+ reload: function() {
+ var me = this;
+ me.store.load();
+ },
+
+ tbar: [
+ {
+ text: gettext('Save'),
+ disabled: true,
+ itemId: 'savebtn',
+ handler: function() {
+ var me = this.up('panel');
+ Proxmox.Utils.API2Request({
+ params: {
+ digest: me.digest,
+ data: me.down('#hostsfield').getValue()
+ },
+ method: 'POST',
+ url: '/nodes/' + me.nodename + '/hosts',
+ waitMsgTarget: me,
+ success: function(response, opts) {
+ me.reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ }
+ },
+ {
+ text: gettext('Revert'),
+ disabled: true,
+ itemId: 'resetbtn',
+ handler: function() {
+ var me = this.up('panel');
+ me.down('#hostsfield').reset();
+ }
+ }
+ ],
+
+ layout: 'fit',
+
+ items: [
+ {
+ xtype: 'textarea',
+ itemId: 'hostsfield',
+ fieldStyle: {
+ 'font-family': 'monospace',
+ 'white-space': 'pre'
+ },
+ listeners: {
+ dirtychange: function(ta, dirty) {
+ var me = this.up('panel');
+ me.down('#savebtn').setDisabled(!dirty);
+ me.down('#resetbtn').setDisabled(!dirty);
+ }
+ }
+ }
+ ],
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.store = Ext.create('Ext.data.Store', {
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/nodes/" + me.nodename + "/hosts",
+ }
+ });
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, me.store);
+
+ me.mon(me.store, 'load', function(store, records, success) {
+ if (!success || records.length < 1) {
+ return;
+ }
+ me.digest = records[0].data.digest;
+ var data = records[0].data.data;
+ me.down('#hostsfield').setValue(data);
+ me.down('#hostsfield').resetOriginalValue();
+ });
+
+ me.reload();
+ }
+});
+Ext.define('Proxmox.node.DNSView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.proxmoxNodeDNSView'],
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var run_editor = function() {
+ var win = Ext.create('Proxmox.node.DNSEdit', {
+ nodename: me.nodename
+ });
+ win.show();
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + me.nodename + "/dns",
+ cwidth1: 130,
+ interval: 1000,
+ run_editor: run_editor,
+ rows: {
+ search: {
+ header: 'Search domain',
+ required: true,
+ renderer: Ext.htmlEncode
+ },
+ dns1: {
+ header: gettext('DNS server') + " 1",
+ required: true,
+ renderer: Ext.htmlEncode
+ },
+ dns2: {
+ header: gettext('DNS server') + " 2",
+ renderer: Ext.htmlEncode
+ },
+ dns3: {
+ header: gettext('DNS server') + " 3",
+ renderer: Ext.htmlEncode
+ }
+ },
+ tbar: [
+ {
+ text: gettext("Edit"),
+ handler: run_editor
+ }
+ ],
+ listeners: {
+ itemdblclick: run_editor
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ }
+});
+Ext.define('Proxmox.node.Tasks', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.proxmoxNodeTasks'],
+ stateful: true,
+ stateId: 'grid-node-tasks',
+ loadMask: true,
+ sortableColumns: false,
+ vmidFilter: 0,
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var store = Ext.create('Ext.data.BufferedStore', {
+ pageSize: 500,
+ autoLoad: true,
+ remoteFilter: true,
+ model: 'proxmox-tasks',
+ proxy: {
+ type: 'proxmox',
+ startParam: 'start',
+ limitParam: 'limit',
+ url: "/api2/json/nodes/" + me.nodename + "/tasks"
+ }
+ });
+
+ var userfilter = '';
+ var filter_errors = 0;
+
+ var updateProxyParams = function() {
+ var params = {
+ errors: filter_errors
+ };
+ if (userfilter) {
+ params.userfilter = userfilter;
+ }
+ if (me.vmidFilter) {
+ params.vmid = me.vmidFilter;
+ }
+ store.proxy.extraParams = params;
+ };
+
+ updateProxyParams();
+
+ var reload_task = Ext.create('Ext.util.DelayedTask',function() {
+ updateProxyParams();
+ store.reload();
+ });
+
+ var run_task_viewer = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: rec.data.upid
+ });
+ win.show();
+ };
+
+ var view_btn = new Ext.Button({
+ text: gettext('View'),
+ disabled: true,
+ handler: run_task_viewer
+ });
+
+ Proxmox.Utils.monStoreErrors(me, store, true);
+
+ Ext.apply(me, {
+ store: store,
+ viewConfig: {
+ trackOver: false,
+ stripeRows: false, // does not work with getRowClass()
+
+ getRowClass: function(record, index) {
+ var status = record.get('status');
+
+ if (status && status != 'OK') {
+ return "proxmox-invalid-row";
+ }
+ }
+ },
+ tbar: [
+ view_btn, '->', gettext('User name') +':', ' ',
+ {
+ xtype: 'textfield',
+ width: 200,
+ value: userfilter,
+ enableKeyEvents: true,
+ listeners: {
+ keyup: function(field, e) {
+ userfilter = field.getValue();
+ reload_task.delay(500);
+ }
+ }
+ }, ' ', gettext('Only Errors') + ':', ' ',
+ {
+ xtype: 'checkbox',
+ hideLabel: true,
+ checked: filter_errors,
+ listeners: {
+ change: function(field, checked) {
+ filter_errors = checked ? 1 : 0;
+ reload_task.delay(10);
+ }
+ }
+ }, ' '
+ ],
+ columns: [
+ {
+ header: gettext("Start Time"),
+ dataIndex: 'starttime',
+ width: 100,
+ renderer: function(value) {
+ return Ext.Date.format(value, "M d H:i:s");
+ }
+ },
+ {
+ header: gettext("End Time"),
+ dataIndex: 'endtime',
+ width: 100,
+ renderer: function(value, metaData, record) {
+ return Ext.Date.format(value,"M d H:i:s");
+ }
+ },
+ {
+ header: gettext("Node"),
+ dataIndex: 'node',
+ width: 100
+ },
+ {
+ header: gettext("User name"),
+ dataIndex: 'user',
+ width: 150
+ },
+ {
+ header: gettext("Description"),
+ dataIndex: 'upid',
+ flex: 1,
+ renderer: Proxmox.Utils.render_upid
+ },
+ {
+ header: gettext("Status"),
+ dataIndex: 'status',
+ width: 200,
+ renderer: function(value, metaData, record) {
+ if (value == 'OK') {
+ return 'OK';
+ }
+ // metaData.attr = 'style="color:red;"';
+ return "ERROR: " + value;
+ }
+ }
+ ],
+ listeners: {
+ itemdblclick: run_task_viewer,
+ selectionchange: function(v, selections) {
+ view_btn.setDisabled(!(selections && selections[0]));
+ },
+ show: function() { reload_task.delay(10); },
+ destroy: function() { reload_task.cancel(); }
+ }
+ });
+
+ me.callParent();
+
+ }
+});
+Ext.define('proxmox-services', {
+ extend: 'Ext.data.Model',
+ fields: [ 'service', 'name', 'desc', 'state' ],
+ idProperty: 'service'
+});
+
+Ext.define('Proxmox.node.ServiceView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.proxmoxNodeServiceView'],
+
+ startOnlyServices: {},
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var rstore = Ext.create('Proxmox.data.UpdateStore', {
+ interval: 1000,
+ storeid: 'proxmox-services' + me.nodename,
+ model: 'proxmox-services',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/nodes/" + me.nodename + "/services"
+ }
+ });
+
+ var store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: rstore,
+ sortAfterUpdate: true,
+ sorters: [
+ {
+ property : 'name',
+ direction: 'ASC'
+ }
+ ]
+ });
+
+ var view_service_log = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ var win = Ext.create('Ext.window.Window', {
+ title: gettext('Syslog') + ': ' + rec.data.service,
+ modal: true,
+ width: 800,
+ height: 400,
+ layout: 'fit',
+ items: {
+ xtype: 'proxmoxLogView',
+ url: "/api2/extjs/nodes/" + me.nodename + "/syslog?service=" +
+ rec.data.service,
+ log_select_timespan: 1
+ }
+ });
+ win.show();
+ };
+
+ var service_cmd = function(cmd) {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + me.nodename + "/services/" + rec.data.service + "/" + cmd,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ me.loading = true;
+ },
+ success: function(response, opts) {
+ rstore.startUpdate();
+ var upid = response.result.data;
+
+ var win = Ext.create('Proxmox.window.TaskProgress', {
+ upid: upid
+ });
+ win.show();
+ }
+ });
+ };
+
+ var start_btn = new Ext.Button({
+ text: gettext('Start'),
+ disabled: true,
+ handler: function(){
+ service_cmd("start");
+ }
+ });
+
+ var stop_btn = new Ext.Button({
+ text: gettext('Stop'),
+ disabled: true,
+ handler: function(){
+ service_cmd("stop");
+ }
+ });
+
+ var restart_btn = new Ext.Button({
+ text: gettext('Restart'),
+ disabled: true,
+ handler: function(){
+ service_cmd("restart");
+ }
+ });
+
+ var syslog_btn = new Ext.Button({
+ text: gettext('Syslog'),
+ disabled: true,
+ handler: view_service_log
+ });
+
+ var set_button_status = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ if (!rec) {
+ start_btn.disable();
+ stop_btn.disable();
+ restart_btn.disable();
+ syslog_btn.disable();
+ return;
+ }
+ var service = rec.data.service;
+ var state = rec.data.state;
+
+ syslog_btn.enable();
+
+ if (me.startOnlyServices[service]) {
+ if (state == 'running') {
+ start_btn.disable();
+ restart_btn.enable();
+ } else {
+ start_btn.enable();
+ restart_btn.disable();
+ }
+ stop_btn.disable();
+ } else {
+ if (state == 'running') {
+ start_btn.disable();
+ restart_btn.enable();
+ stop_btn.enable();
+ } else {
+ start_btn.enable();
+ restart_btn.disable();
+ stop_btn.disable();
+ }
+ }
+ };
+
+ me.mon(store, 'refresh', set_button_status);
+
+ Proxmox.Utils.monStoreErrors(me, rstore);
+
+ Ext.apply(me, {
+ store: store,
+ stateful: false,
+ tbar: [ start_btn, stop_btn, restart_btn, syslog_btn ],
+ columns: [
+ {
+ header: gettext('Name'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'name'
+ },
+ {
+ header: gettext('Status'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'state'
+ },
+ {
+ header: gettext('Description'),
+ renderer: Ext.String.htmlEncode,
+ dataIndex: 'desc',
+ flex: 2
+ }
+ ],
+ listeners: {
+ selectionchange: set_button_status,
+ itemdblclick: view_service_log,
+ activate: rstore.startUpdate,
+ destroy: rstore.stopUpdate
+ }
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('Proxmox.node.TimeEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.proxmoxNodeTimeEdit'],
+
+ subject: gettext('Time zone'),
+
+ width: 400,
+
+ autoLoad: true,
+
+ fieldDefaults: {
+ labelWidth: 70
+ },
+
+ items: {
+ xtype: 'combo',
+ fieldLabel: gettext('Time zone'),
+ name: 'timezone',
+ queryMode: 'local',
+ store: Ext.create('Proxmox.data.TimezoneStore'),
+ displayField: 'zone',
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+ allowBlank: false
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+ me.url = "/api2/extjs/nodes/" + me.nodename + "/time";
+
+ me.callParent();
+ }
+});
+Ext.define('Proxmox.node.TimeView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.proxmoxNodeTimeView'],
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var tzoffset = (new Date()).getTimezoneOffset()*60000;
+ var renderlocaltime = function(value) {
+ var servertime = new Date((value * 1000) + tzoffset);
+ return Ext.Date.format(servertime, 'Y-m-d H:i:s');
+ };
+
+ var run_editor = function() {
+ var win = Ext.create('Proxmox.node.TimeEdit', {
+ nodename: me.nodename
+ });
+ win.show();
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + me.nodename + "/time",
+ cwidth1: 150,
+ interval: 1000,
+ run_editor: run_editor,
+ rows: {
+ timezone: {
+ header: gettext('Time zone'),
+ required: true
+ },
+ localtime: {
+ header: gettext('Server time'),
+ required: true,
+ renderer: renderlocaltime
+ }
+ },
+ tbar: [
+ {
+ text: gettext("Edit"),
+ handler: run_editor
+ }
+ ],
+ listeners: {
+ itemdblclick: run_editor
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ }
+});
diff --git a/serverside/jsmod/6.0-4/proxmoxlib.js.original b/serverside/jsmod/6.0-4/proxmoxlib.js.original
new file mode 100644
index 0000000..e4e71b7
--- /dev/null
+++ b/serverside/jsmod/6.0-4/proxmoxlib.js.original
@@ -0,0 +1,7357 @@
+// 2.0-5
+Ext.ns('Proxmox');
+Ext.ns('Proxmox.Setup');
+
+if (!Ext.isDefined(Proxmox.Setup.auth_cookie_name)) {
+ throw "Proxmox library not initialized";
+}
+
+// avoid errors related to Accessible Rich Internet Applications
+// (access for people with disabilities)
+// TODO reenable after all components are upgraded
+Ext.enableAria = false;
+Ext.enableAriaButtons = false;
+Ext.enableAriaPanels = false;
+
+// avoid errors when running without development tools
+if (!Ext.isDefined(Ext.global.console)) {
+ var console = {
+ dir: function() {},
+ log: function() {}
+ };
+}
+
+Ext.Ajax.defaultHeaders = {
+ 'Accept': 'application/json'
+};
+
+Ext.Ajax.on('beforerequest', function(conn, options) {
+ if (Proxmox.CSRFPreventionToken) {
+ if (!options.headers) {
+ options.headers = {};
+ }
+ options.headers.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
+ }
+});
+
+Ext.define('Proxmox.Utils', { utilities: {
+
+ // this singleton contains miscellaneous utilities
+
+ yesText: gettext('Yes'),
+ noText: gettext('No'),
+ enabledText: gettext('Enabled'),
+ disabledText: gettext('Disabled'),
+ noneText: gettext('none'),
+ errorText: gettext('Error'),
+ unknownText: gettext('Unknown'),
+ defaultText: gettext('Default'),
+ daysText: gettext('days'),
+ dayText: gettext('day'),
+ runningText: gettext('running'),
+ stoppedText: gettext('stopped'),
+ neverText: gettext('never'),
+ totalText: gettext('Total'),
+ usedText: gettext('Used'),
+ directoryText: gettext('Directory'),
+ stateText: gettext('State'),
+ groupText: gettext('Group'),
+
+ language_map: {
+ zh_CN: 'Chinese (Simplified)',
+ zh_TW: 'Chinese (Traditional)',
+ ca: 'Catalan',
+ da: 'Danish',
+ en: 'English',
+ eu: 'Euskera (Basque)',
+ fr: 'French',
+ de: 'German',
+ it: 'Italian',
+ es: 'Spanish',
+ ja: 'Japanese',
+ nb: 'Norwegian (Bokmal)',
+ nn: 'Norwegian (Nynorsk)',
+ fa: 'Persian (Farsi)',
+ pl: 'Polish',
+ pt_BR: 'Portuguese (Brazil)',
+ ru: 'Russian',
+ sl: 'Slovenian',
+ sv: 'Swedish',
+ tr: 'Turkish'
+ },
+
+ render_language: function (value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText + ' (English)';
+ }
+ var text = Proxmox.Utils.language_map[value];
+ if (text) {
+ return text + ' (' + value + ')';
+ }
+ return value;
+ },
+
+ language_array: function() {
+ var data = [['__default__', Proxmox.Utils.render_language('')]];
+ Ext.Object.each(Proxmox.Utils.language_map, function(key, value) {
+ data.push([key, Proxmox.Utils.render_language(value)]);
+ });
+
+ return data;
+ },
+
+ bond_mode_gettext_map: {
+ '802.3ad': 'LACP (802.3ad)',
+ 'lacp-balance-slb': 'LACP (balance-slb)',
+ 'lacp-balance-tcp': 'LACP (balance-tcp)',
+ },
+
+ render_bond_mode: value => Proxmox.Utils.bond_mode_gettext_map[value] || value || '',
+
+ bond_mode_array: function(modes) {
+ return modes.map(mode => [mode, Proxmox.Utils.render_bond_mode(mode)]);
+ },
+
+ getNoSubKeyHtml: function(url) {
+ // url http://www.proxmox.com/products/proxmox-ve/subscription-service-plans
+ return Ext.String.format('You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.', url || 'https://www.proxmox.com');
+ },
+
+ format_boolean_with_default: function(value) {
+ if (Ext.isDefined(value) && value !== '__default__') {
+ return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
+ }
+ return Proxmox.Utils.defaultText;
+ },
+
+ format_boolean: function(value) {
+ return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
+ },
+
+ format_neg_boolean: function(value) {
+ return !value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
+ },
+
+ format_enabled_toggle: function(value) {
+ return value ? Proxmox.Utils.enabledText : Proxmox.Utils.disabledText;
+ },
+
+ format_expire: function(date) {
+ if (!date) {
+ return Proxmox.Utils.neverText;
+ }
+ return Ext.Date.format(date, "Y-m-d");
+ },
+
+ format_duration_long: function(ut) {
+
+ var days = Math.floor(ut / 86400);
+ ut -= days*86400;
+ var hours = Math.floor(ut / 3600);
+ ut -= hours*3600;
+ var mins = Math.floor(ut / 60);
+ ut -= mins*60;
+
+ var hours_str = '00' + hours.toString();
+ hours_str = hours_str.substr(hours_str.length - 2);
+ var mins_str = "00" + mins.toString();
+ mins_str = mins_str.substr(mins_str.length - 2);
+ var ut_str = "00" + ut.toString();
+ ut_str = ut_str.substr(ut_str.length - 2);
+
+ if (days) {
+ var ds = days > 1 ? Proxmox.Utils.daysText : Proxmox.Utils.dayText;
+ return days.toString() + ' ' + ds + ' ' +
+ hours_str + ':' + mins_str + ':' + ut_str;
+ } else {
+ return hours_str + ':' + mins_str + ':' + ut_str;
+ }
+ },
+
+ format_subscription_level: function(level) {
+ if (level === 'c') {
+ return 'Community';
+ } else if (level === 'b') {
+ return 'Basic';
+ } else if (level === 's') {
+ return 'Standard';
+ } else if (level === 'p') {
+ return 'Premium';
+ } else {
+ return Proxmox.Utils.noneText;
+ }
+ },
+
+ compute_min_label_width: function(text, width) {
+
+ if (width === undefined) { width = 100; }
+
+ var tm = new Ext.util.TextMetrics();
+ var min = tm.getWidth(text + ':');
+
+ return min < width ? width : min;
+ },
+
+ setAuthData: function(data) {
+ Proxmox.CSRFPreventionToken = data.CSRFPreventionToken;
+ Proxmox.UserName = data.username;
+ Proxmox.LoggedOut = data.LoggedOut;
+ // creates a session cookie (expire = null)
+ // that way the cookie gets deleted after the browser window is closed
+ Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true);
+ },
+
+ authOK: function() {
+ if (Proxmox.LoggedOut) {
+ return undefined;
+ }
+ return (Proxmox.UserName !== '') && Ext.util.Cookies.get(Proxmox.Setup.auth_cookie_name);
+ },
+
+ authClear: function() {
+ if (Proxmox.LoggedOut) {
+ return undefined;
+ }
+ Ext.util.Cookies.clear(Proxmox.Setup.auth_cookie_name);
+ },
+
+ // comp.setLoading() is buggy in ExtJS 4.0.7, so we
+ // use el.mask() instead
+ setErrorMask: function(comp, msg) {
+ var el = comp.el;
+ if (!el) {
+ return;
+ }
+ if (!msg) {
+ el.unmask();
+ } else {
+ if (msg === true) {
+ el.mask(gettext("Loading..."));
+ } else {
+ el.mask(msg);
+ }
+ }
+ },
+
+ monStoreErrors: function(me, store, clearMaskBeforeLoad) {
+ if (clearMaskBeforeLoad) {
+ me.mon(store, 'beforeload', function(s, operation, eOpts) {
+ Proxmox.Utils.setErrorMask(me, false);
+ });
+ } else {
+ me.mon(store, 'beforeload', function(s, operation, eOpts) {
+ if (!me.loadCount) {
+ me.loadCount = 0; // make sure it is numeric
+ Proxmox.Utils.setErrorMask(me, true);
+ }
+ });
+ }
+
+ // only works with 'proxmox' proxy
+ me.mon(store.proxy, 'afterload', function(proxy, request, success) {
+ me.loadCount++;
+
+ if (success) {
+ Proxmox.Utils.setErrorMask(me, false);
+ return;
+ }
+
+ var msg;
+ /*jslint nomen: true */
+ var operation = request._operation;
+ var error = operation.getError();
+ if (error.statusText) {
+ msg = error.statusText + ' (' + error.status + ')';
+ } else {
+ msg = gettext('Connection error');
+ }
+ Proxmox.Utils.setErrorMask(me, msg);
+ });
+ },
+
+ extractRequestError: function(result, verbose) {
+ var msg = gettext('Successful');
+
+ if (!result.success) {
+ msg = gettext("Unknown error");
+ if (result.message) {
+ msg = result.message;
+ if (result.status) {
+ msg += ' (' + result.status + ')';
+ }
+ }
+ if (verbose && Ext.isObject(result.errors)) {
+ msg += "
";
+ Ext.Object.each(result.errors, function(prop, desc) {
+ msg += "
" + Ext.htmlEncode(prop) + ": " +
+ Ext.htmlEncode(desc);
+ });
+ }
+ }
+
+ return msg;
+ },
+
+ // Ext.Ajax.request
+ API2Request: function(reqOpts) {
+
+ var newopts = Ext.apply({
+ waitMsg: gettext('Please wait...')
+ }, reqOpts);
+
+ if (!newopts.url.match(/^\/api2/)) {
+ newopts.url = '/api2/extjs' + newopts.url;
+ }
+ delete newopts.callback;
+
+ var createWrapper = function(successFn, callbackFn, failureFn) {
+ Ext.apply(newopts, {
+ success: function(response, options) {
+ if (options.waitMsgTarget) {
+ if (Proxmox.Utils.toolkit === 'touch') {
+ options.waitMsgTarget.setMasked(false);
+ } else {
+ options.waitMsgTarget.setLoading(false);
+ }
+ }
+ var result = Ext.decode(response.responseText);
+ response.result = result;
+ if (!result.success) {
+ response.htmlStatus = Proxmox.Utils.extractRequestError(result, true);
+ Ext.callback(callbackFn, options.scope, [options, false, response]);
+ Ext.callback(failureFn, options.scope, [response, options]);
+ return;
+ }
+ Ext.callback(callbackFn, options.scope, [options, true, response]);
+ Ext.callback(successFn, options.scope, [response, options]);
+ },
+ failure: function(response, options) {
+ if (options.waitMsgTarget) {
+ if (Proxmox.Utils.toolkit === 'touch') {
+ options.waitMsgTarget.setMasked(false);
+ } else {
+ options.waitMsgTarget.setLoading(false);
+ }
+ }
+ response.result = {};
+ try {
+ response.result = Ext.decode(response.responseText);
+ } catch(e) {}
+ var msg = gettext('Connection error') + ' - server offline?';
+ if (response.aborted) {
+ msg = gettext('Connection error') + ' - aborted.';
+ } else if (response.timedout) {
+ msg = gettext('Connection error') + ' - Timeout.';
+ } else if (response.status && response.statusText) {
+ msg = gettext('Connection error') + ' ' + response.status + ': ' + response.statusText;
+ }
+ response.htmlStatus = msg;
+ Ext.callback(callbackFn, options.scope, [options, false, response]);
+ Ext.callback(failureFn, options.scope, [response, options]);
+ }
+ });
+ };
+
+ createWrapper(reqOpts.success, reqOpts.callback, reqOpts.failure);
+
+ var target = newopts.waitMsgTarget;
+ if (target) {
+ if (Proxmox.Utils.toolkit === 'touch') {
+ target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg} );
+ } else {
+ // Note: ExtJS bug - this does not work when component is not rendered
+ target.setLoading(newopts.waitMsg);
+ }
+ }
+ Ext.Ajax.request(newopts);
+ },
+
+ checked_command: function(orig_cmd) {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/localhost/subscription',
+ method: 'GET',
+ //waitMsgTarget: me,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, opts) {
+ var data = response.result.data;
+
+ if (data.status !== 'Active') {
+ Ext.Msg.show({
+ title: gettext('No valid subscription'),
+ icon: Ext.Msg.WARNING,
+ msg: Proxmox.Utils.getNoSubKeyHtml(data.url),
+ buttons: Ext.Msg.OK,
+ callback: function(btn) {
+ if (btn !== 'ok') {
+ return;
+ }
+ orig_cmd();
+ }
+ });
+ } else {
+ orig_cmd();
+ }
+ }
+ });
+ },
+
+ assemble_field_data: function(values, data) {
+ if (Ext.isObject(data)) {
+ Ext.Object.each(data, function(name, val) {
+ if (values.hasOwnProperty(name)) {
+ var bucket = values[name];
+ if (!Ext.isArray(bucket)) {
+ bucket = values[name] = [bucket];
+ }
+ if (Ext.isArray(val)) {
+ values[name] = bucket.concat(val);
+ } else {
+ bucket.push(val);
+ }
+ } else {
+ values[name] = val;
+ }
+ });
+ }
+ },
+
+ dialog_title: function(subject, create, isAdd) {
+ if (create) {
+ if (isAdd) {
+ return gettext('Add') + ': ' + subject;
+ } else {
+ return gettext('Create') + ': ' + subject;
+ }
+ } else {
+ return gettext('Edit') + ': ' + subject;
+ }
+ },
+
+ network_iface_types: {
+ eth: gettext("Network Device"),
+ bridge: 'Linux Bridge',
+ bond: 'Linux Bond',
+ vlan: 'Linux VLAN',
+ OVSBridge: 'OVS Bridge',
+ OVSBond: 'OVS Bond',
+ OVSPort: 'OVS Port',
+ OVSIntPort: 'OVS IntPort'
+ },
+
+ render_network_iface_type: function(value) {
+ return Proxmox.Utils.network_iface_types[value] ||
+ Proxmox.Utils.unknownText;
+ },
+
+ task_desc_table: {
+ acmenewcert: [ 'SRV', gettext('Order Certificate') ],
+ acmeregister: [ 'ACME Account', gettext('Register') ],
+ acmedeactivate: [ 'ACME Account', gettext('Deactivate') ],
+ acmeupdate: [ 'ACME Account', gettext('Update') ],
+ acmerefresh: [ 'ACME Account', gettext('Refresh') ],
+ acmerenew: [ 'SRV', gettext('Renew Certificate') ],
+ acmerevoke: [ 'SRV', gettext('Revoke Certificate') ],
+ 'move_volume': [ 'CT', gettext('Move Volume') ],
+ clustercreate: [ '', gettext('Create Cluster') ],
+ clusterjoin: [ '', gettext('Join Cluster') ],
+ diskinit: [ 'Disk', gettext('Initialize Disk with GPT') ],
+ vncproxy: [ 'VM/CT', gettext('Console') ],
+ spiceproxy: [ 'VM/CT', gettext('Console') + ' (Spice)' ],
+ vncshell: [ '', gettext('Shell') ],
+ spiceshell: [ '', gettext('Shell') + ' (Spice)' ],
+ qmsnapshot: [ 'VM', gettext('Snapshot') ],
+ qmrollback: [ 'VM', gettext('Rollback') ],
+ qmdelsnapshot: [ 'VM', gettext('Delete Snapshot') ],
+ qmcreate: [ 'VM', gettext('Create') ],
+ qmrestore: [ 'VM', gettext('Restore') ],
+ qmdestroy: [ 'VM', gettext('Destroy') ],
+ qmigrate: [ 'VM', gettext('Migrate') ],
+ qmclone: [ 'VM', gettext('Clone') ],
+ qmmove: [ 'VM', gettext('Move disk') ],
+ qmtemplate: [ 'VM', gettext('Convert to template') ],
+ qmstart: [ 'VM', gettext('Start') ],
+ qmstop: [ 'VM', gettext('Stop') ],
+ qmreset: [ 'VM', gettext('Reset') ],
+ qmshutdown: [ 'VM', gettext('Shutdown') ],
+ qmsuspend: [ 'VM', gettext('Hibernate') ],
+ qmpause: [ 'VM', gettext('Pause') ],
+ qmresume: [ 'VM', gettext('Resume') ],
+ qmconfig: [ 'VM', gettext('Configure') ],
+ vzsnapshot: [ 'CT', gettext('Snapshot') ],
+ vzrollback: [ 'CT', gettext('Rollback') ],
+ vzdelsnapshot: [ 'CT', gettext('Delete Snapshot') ],
+ vzcreate: ['CT', gettext('Create') ],
+ vzrestore: ['CT', gettext('Restore') ],
+ vzdestroy: ['CT', gettext('Destroy') ],
+ vzmigrate: [ 'CT', gettext('Migrate') ],
+ vzclone: [ 'CT', gettext('Clone') ],
+ vztemplate: [ 'CT', gettext('Convert to template') ],
+ vzstart: ['CT', gettext('Start') ],
+ vzstop: ['CT', gettext('Stop') ],
+ vzmount: ['CT', gettext('Mount') ],
+ vzumount: ['CT', gettext('Unmount') ],
+ vzshutdown: ['CT', gettext('Shutdown') ],
+ vzsuspend: [ 'CT', gettext('Suspend') ],
+ vzresume: [ 'CT', gettext('Resume') ],
+ hamigrate: [ 'HA', gettext('Migrate') ],
+ hastart: [ 'HA', gettext('Start') ],
+ hastop: [ 'HA', gettext('Stop') ],
+ srvstart: ['SRV', gettext('Start') ],
+ srvstop: ['SRV', gettext('Stop') ],
+ srvrestart: ['SRV', gettext('Restart') ],
+ srvreload: ['SRV', gettext('Reload') ],
+ cephcreatemgr: ['Ceph Manager', gettext('Create') ],
+ cephdestroymgr: ['Ceph Manager', gettext('Destroy') ],
+ cephcreatemon: ['Ceph Monitor', gettext('Create') ],
+ cephdestroymon: ['Ceph Monitor', gettext('Destroy') ],
+ cephcreateosd: ['Ceph OSD', gettext('Create') ],
+ cephdestroyosd: ['Ceph OSD', gettext('Destroy') ],
+ cephcreatepool: ['Ceph Pool', gettext('Create') ],
+ cephdestroypool: ['Ceph Pool', gettext('Destroy') ],
+ cephfscreate: ['CephFS', gettext('Create') ],
+ cephcreatemds: ['Ceph Metadata Server', gettext('Create') ],
+ cephdestroymds: ['Ceph Metadata Server', gettext('Destroy') ],
+ imgcopy: ['', gettext('Copy data') ],
+ imgdel: ['', gettext('Erase data') ],
+ unknownimgdel: ['', gettext('Destroy image from unknown guest') ],
+ download: ['', gettext('Download') ],
+ vzdump: ['VM/CT', gettext('Backup') ],
+ aptupdate: ['', gettext('Update package database') ],
+ startall: [ '', gettext('Start all VMs and Containers') ],
+ stopall: [ '', gettext('Stop all VMs and Containers') ],
+ migrateall: [ '', gettext('Migrate all VMs and Containers') ],
+ dircreate: [ gettext('Directory Storage'), gettext('Create') ],
+ lvmcreate: [ gettext('LVM Storage'), gettext('Create') ],
+ lvmthincreate: [ gettext('LVM-Thin Storage'), gettext('Create') ],
+ zfscreate: [ gettext('ZFS Storage'), gettext('Create') ]
+ },
+
+ format_task_description: function(type, id) {
+ var farray = Proxmox.Utils.task_desc_table[type];
+ var text;
+ if (!farray) {
+ text = type;
+ if (id) {
+ type += ' ' + id;
+ }
+ return text;
+ }
+ var prefix = farray[0];
+ text = farray[1];
+ if (prefix) {
+ return prefix + ' ' + id + ' - ' + text;
+ }
+ return text;
+ },
+
+ format_size: function(size) {
+ /*jslint confusion: true */
+
+ var units = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'];
+ var num = 0;
+
+ while (size >= 1024 && ((num++)+1) < units.length) {
+ size = size / 1024;
+ }
+
+ return size.toFixed((num > 0)?2:0) + " " + units[num] + "B";
+ },
+
+ render_upid: function(value, metaData, record) {
+ var type = record.data.type;
+ var id = record.data.id;
+
+ return Proxmox.Utils.format_task_description(type, id);
+ },
+
+ render_uptime: function(value) {
+
+ var uptime = value;
+
+ if (uptime === undefined) {
+ return '';
+ }
+
+ if (uptime <= 0) {
+ return '-';
+ }
+
+ return Proxmox.Utils.format_duration_long(uptime);
+ },
+
+ parse_task_upid: function(upid) {
+ var task = {};
+
+ var res = upid.match(/^UPID:(\S+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/);
+ if (!res) {
+ throw "unable to parse upid '" + upid + "'";
+ }
+ task.node = res[1];
+ task.pid = parseInt(res[2], 16);
+ task.pstart = parseInt(res[3], 16);
+ task.starttime = parseInt(res[4], 16);
+ task.type = res[5];
+ task.id = res[6];
+ task.user = res[7];
+
+ task.desc = Proxmox.Utils.format_task_description(task.type, task.id);
+
+ return task;
+ },
+
+ render_timestamp: function(value, metaData, record, rowIndex, colIndex, store) {
+ var servertime = new Date(value * 1000);
+ return Ext.Date.format(servertime, 'Y-m-d H:i:s');
+ },
+
+ get_help_info: function(section) {
+ var helpMap;
+ if (typeof proxmoxOnlineHelpInfo !== 'undefined') {
+ helpMap = proxmoxOnlineHelpInfo;
+ } else if (typeof pveOnlineHelpInfo !== 'undefined') {
+ // be backward compatible with older pve-doc-generators
+ helpMap = pveOnlineHelpInfo;
+ } else {
+ throw "no global OnlineHelpInfo map declared";
+ }
+
+ return helpMap[section];
+ },
+
+ get_help_link: function(section) {
+ var info = Proxmox.Utils.get_help_info(section);
+ if (!info) {
+ return;
+ }
+
+ return window.location.origin + info.link;
+ },
+
+ openXtermJsViewer: function(vmtype, vmid, nodename, vmname, cmd) {
+ var url = Ext.Object.toQueryString({
+ console: vmtype, // kvm, lxc, upgrade or shell
+ xtermjs: 1,
+ vmid: vmid,
+ vmname: vmname,
+ node: nodename,
+ cmd: cmd,
+
+ });
+ var nw = window.open("?" + url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,resizable=yes,width=800,height=420');
+ if (nw) {
+ nw.focus();
+ }
+ }
+
+},
+
+ singleton: true,
+ constructor: function() {
+ var me = this;
+ Ext.apply(me, me.utilities);
+
+ var IPV4_OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
+ var IPV4_REGEXP = "(?:(?:" + IPV4_OCTET + "\\.){3}" + IPV4_OCTET + ")";
+ var IPV6_H16 = "(?:[0-9a-fA-F]{1,4})";
+ var IPV6_LS32 = "(?:(?:" + IPV6_H16 + ":" + IPV6_H16 + ")|" + IPV4_REGEXP + ")";
+ var IPV4_CIDR_MASK = "([0-9]{1,2})";
+ var IPV6_CIDR_MASK = "([0-9]{1,3})";
+
+
+ me.IP4_match = new RegExp("^(?:" + IPV4_REGEXP + ")$");
+ me.IP4_cidr_match = new RegExp("^(?:" + IPV4_REGEXP + ")\/" + IPV4_CIDR_MASK + "$");
+
+ var IPV6_REGEXP = "(?:" +
+ "(?:(?:" + "(?:" + IPV6_H16 + ":){6})" + IPV6_LS32 + ")|" +
+ "(?:(?:" + "::" + "(?:" + IPV6_H16 + ":){5})" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){4})" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,1}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){3})" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,2}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){2})" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,3}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){1})" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,4}" + IPV6_H16 + ")?::" + ")" + IPV6_LS32 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,5}" + IPV6_H16 + ")?::" + ")" + IPV6_H16 + ")|" +
+ "(?:(?:(?:(?:" + IPV6_H16 + ":){0,7}" + IPV6_H16 + ")?::" + ")" + ")" +
+ ")";
+
+ me.IP6_match = new RegExp("^(?:" + IPV6_REGEXP + ")$");
+ me.IP6_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + ")\/" + IPV6_CIDR_MASK + "$");
+ me.IP6_bracket_match = new RegExp("^\\[(" + IPV6_REGEXP + ")\\]");
+
+ me.IP64_match = new RegExp("^(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + ")$");
+ me.IP64_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + "\/" + IPV6_CIDR_MASK + ")|(?:" + IPV4_REGEXP + "\/" + IPV4_CIDR_MASK + ")$");
+
+ var DnsName_REGEXP = "(?:(([a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)\\.)*([A-Za-z0-9]([A-Za-z0-9\\-]*[A-Za-z0-9])?))";
+ me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$");
+
+ me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(:\\d+)?$");
+ me.HostPortBrackets_match = new RegExp("^\\[(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](:\\d+)?$");
+ me.IP6_dotnotation_match = new RegExp("^" + IPV6_REGEXP + "(\\.\\d+)?$");
+ }
+});
+// ExtJS related things
+
+ // do not send '_dc' parameter
+Ext.Ajax.disableCaching = false;
+
+// custom Vtypes
+Ext.apply(Ext.form.field.VTypes, {
+ IPAddress: function(v) {
+ return Proxmox.Utils.IP4_match.test(v);
+ },
+ IPAddressText: gettext('Example') + ': 192.168.1.1',
+ IPAddressMask: /[\d\.]/i,
+
+ IPCIDRAddress: function(v) {
+ var result = Proxmox.Utils.IP4_cidr_match.exec(v);
+ // limits according to JSON Schema see
+ // pve-common/src/PVE/JSONSchema.pm
+ return (result !== null && result[1] >= 8 && result[1] <= 32);
+ },
+ IPCIDRAddressText: gettext('Example') + ': 192.168.1.1/24' + "
" + gettext('Valid CIDR Range') + ': 8-32',
+ IPCIDRAddressMask: /[\d\.\/]/i,
+
+ IP6Address: function(v) {
+ return Proxmox.Utils.IP6_match.test(v);
+ },
+ IP6AddressText: gettext('Example') + ': 2001:DB8::42',
+ IP6AddressMask: /[A-Fa-f0-9:]/,
+
+ IP6CIDRAddress: function(v) {
+ var result = Proxmox.Utils.IP6_cidr_match.exec(v);
+ // limits according to JSON Schema see
+ // pve-common/src/PVE/JSONSchema.pm
+ return (result !== null && result[1] >= 8 && result[1] <= 128);
+ },
+ IP6CIDRAddressText: gettext('Example') + ': 2001:DB8::42/64' + "
" + gettext('Valid CIDR Range') + ': 8-128',
+ IP6CIDRAddressMask: /[A-Fa-f0-9:\/]/,
+
+ IP6PrefixLength: function(v) {
+ return v >= 0 && v <= 128;
+ },
+ IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128',
+ IP6PrefixLengthMask: /[0-9]/,
+
+ IP64Address: function(v) {
+ return Proxmox.Utils.IP64_match.test(v);
+ },
+ IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
+ IP64AddressMask: /[A-Fa-f0-9\.:]/,
+
+ IP64CIDRAddress: function(v) {
+ var result = Proxmox.Utils.IP64_cidr_match.exec(v);
+ if (result === null) {
+ return false;
+ }
+ if (result[1] !== undefined) {
+ return result[1] >= 8 && result[1] <= 128;
+ } else if (result[2] !== undefined) {
+ return result[2] >= 8 && result[2] <= 32;
+ } else {
+ return false;
+ }
+ },
+ IP64CIDRAddressText: gettext('Example') + ': 192.168.1.1/24 2001:DB8::42/64',
+ IP64CIDRAddressMask: /[A-Fa-f0-9\.:\/]/,
+
+ MacAddress: function(v) {
+ return (/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/).test(v);
+ },
+ MacAddressMask: /[a-fA-F0-9:]/,
+ MacAddressText: gettext('Example') + ': 01:23:45:67:89:ab',
+
+ MacPrefix: function(v) {
+ return (/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i).test(v);
+ },
+ MacPrefixMask: /[a-fA-F0-9:]/,
+ MacPrefixText: gettext('Example') + ': 02:8f - ' + gettext('only unicast addresses are allowed'),
+
+ BridgeName: function(v) {
+ return (/^vmbr\d{1,4}$/).test(v);
+ },
+ BridgeNameText: gettext('Format') + ': vmbrN, where 0 <= N <= 9999',
+
+ BondName: function(v) {
+ return (/^bond\d{1,4}$/).test(v);
+ },
+ BondNameText: gettext('Format') + ': bondN, where 0 <= N <= 9999',
+
+ InterfaceName: function(v) {
+ return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
+ },
+ InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'" + "
" +
+ gettext("Minimum characters") + ": 2" + "
" +
+ gettext("Maximum characters") + ": 21" + "
" +
+ gettext("Must start with") + ": 'a-z'",
+
+ StorageId: function(v) {
+ return (/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i).test(v);
+ },
+ StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'" + "
" +
+ gettext("Minimum characters") + ": 2" + "
" +
+ gettext("Must start with") + ": 'A-Z', 'a-z'
" +
+ gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'
",
+
+ ConfigId: function(v) {
+ return (/^[a-z][a-z0-9\_]+$/i).test(v);
+ },
+ ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'" + "
" +
+ gettext("Minimum characters") + ": 2" + "
" +
+ gettext("Must start with") + ": " + gettext("letter"),
+
+ HttpProxy: function(v) {
+ return (/^http:\/\/.*$/).test(v);
+ },
+ HttpProxyText: gettext('Example') + ": http://username:password@host:port/",
+
+ DnsName: function(v) {
+ return Proxmox.Utils.DnsName_match.test(v);
+ },
+ DnsNameText: gettext('This is not a valid DNS name'),
+
+ // workaround for https://www.sencha.com/forum/showthread.php?302150
+ proxmoxMail: function(v) {
+ return (/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/).test(v);
+ },
+ proxmoxMailText: gettext('Example') + ": user@example.com",
+
+ DnsOrIp: function(v) {
+ if (!Proxmox.Utils.DnsName_match.test(v) &&
+ !Proxmox.Utils.IP64_match.test(v)) {
+ return false;
+ }
+
+ return true;
+ },
+ DnsOrIpText: gettext('Not a valid DNS name or IP address.'),
+
+ HostList: function(v) {
+ var list = v.split(/[\ \,\;]+/);
+ var i;
+ for (i = 0; i < list.length; i++) {
+ if (list[i] == "") {
+ continue;
+ }
+
+ if (!Proxmox.Utils.HostPort_match.test(list[i]) &&
+ !Proxmox.Utils.HostPortBrackets_match.test(list[i]) &&
+ !Proxmox.Utils.IP6_dotnotation_match.test(list[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+ HostListText: gettext('Not a valid list of hosts'),
+
+ password: function(val, field) {
+ if (field.initialPassField) {
+ var pwd = field.up('form').down(
+ '[name=' + field.initialPassField + ']');
+ return (val == pwd.getValue());
+ }
+ return true;
+ },
+
+ passwordText: gettext('Passwords do not match')
+});
+
+// Firefox 52+ Touchscreen bug
+// see https://www.sencha.com/forum/showthread.php?336762-Examples-don-t-work-in-Firefox-52-touchscreen/page2
+// and https://bugzilla.proxmox.com/show_bug.cgi?id=1223
+Ext.define('EXTJS_23846.Element', {
+ override: 'Ext.dom.Element'
+}, function(Element) {
+ var supports = Ext.supports,
+ proto = Element.prototype,
+ eventMap = proto.eventMap,
+ additiveEvents = proto.additiveEvents;
+
+ if (Ext.os.is.Desktop && supports.TouchEvents && !supports.PointerEvents) {
+ eventMap.touchstart = 'mousedown';
+ eventMap.touchmove = 'mousemove';
+ eventMap.touchend = 'mouseup';
+ eventMap.touchcancel = 'mouseup';
+
+ additiveEvents.mousedown = 'mousedown';
+ additiveEvents.mousemove = 'mousemove';
+ additiveEvents.mouseup = 'mouseup';
+ additiveEvents.touchstart = 'touchstart';
+ additiveEvents.touchmove = 'touchmove';
+ additiveEvents.touchend = 'touchend';
+ additiveEvents.touchcancel = 'touchcancel';
+
+ additiveEvents.pointerdown = 'mousedown';
+ additiveEvents.pointermove = 'mousemove';
+ additiveEvents.pointerup = 'mouseup';
+ additiveEvents.pointercancel = 'mouseup';
+ }
+});
+
+Ext.define('EXTJS_23846.Gesture', {
+ override: 'Ext.event.publisher.Gesture'
+}, function(Gesture) {
+ var me = Gesture.instance;
+
+ if (Ext.supports.TouchEvents && !Ext.isWebKit && Ext.os.is.Desktop) {
+ me.handledDomEvents.push('mousedown', 'mousemove', 'mouseup');
+ me.registerEvents();
+ }
+});
+
+Ext.define('EXTJS_18900.Pie', {
+ override: 'Ext.chart.series.Pie',
+
+ // from 6.0.2
+ betweenAngle: function (x, a, b) {
+ var pp = Math.PI * 2,
+ offset = this.rotationOffset;
+
+ if (a === b) {
+ return false;
+ }
+
+ if (!this.getClockwise()) {
+ x *= -1;
+ a *= -1;
+ b *= -1;
+ a -= offset;
+ b -= offset;
+ } else {
+ a += offset;
+ b += offset;
+ }
+
+ x -= a;
+ b -= a;
+
+ // Normalize, so that both x and b are in the [0,360) interval.
+ x %= pp;
+ b %= pp;
+ x += pp;
+ b += pp;
+ x %= pp;
+ b %= pp;
+
+ // Because 360 * n angles will be normalized to 0,
+ // we need to treat b === 0 as a special case.
+ return x < b || b === 0;
+ },
+});
+
+// we always want the number in x.y format and never in, e.g., x,y
+Ext.define('PVE.form.field.Number', {
+ override: 'Ext.form.field.Number',
+ submitLocaleSeparator: false
+});
+
+// ExtJs 5-6 has an issue with caching
+// see https://www.sencha.com/forum/showthread.php?308989
+Ext.define('Proxmox.UnderlayPool', {
+ override: 'Ext.dom.UnderlayPool',
+
+ checkOut: function () {
+ var cache = this.cache,
+ len = cache.length,
+ el;
+
+ // do cleanup because some of the objects might have been destroyed
+ while (len--) {
+ if (cache[len].destroyed) {
+ cache.splice(len, 1);
+ }
+ }
+ // end do cleanup
+
+ el = cache.shift();
+
+ if (!el) {
+ el = Ext.Element.create(this.elementConfig);
+ el.setVisibilityMode(2);
+ //
'), first - 1, total);
+ me.running = false;
+ if (me.requested) {
+ me.requested = false;
+ view.loadTask.delay(200);
+ }
+ },
+ failure: function(response) {
+ if (view.failCallback) {
+ view.failCallback(response);
+ } else {
+ var msg = response.htmlStatus;
+ Proxmox.Utils.setErrorMask(me, msg);
+ }
+ me.running = false;
+ if (me.requested) {
+ me.requested = false;
+ view.loadTask.delay(200);
+ }
+ }
+ });
+ },
+
+ onScroll: function(x, y) {
+ var me = this;
+ var view = me.getView();
+ var viewModel = me.getViewModel();
+
+ var lineHeight = view.lineHeight;
+ var line = view.getScrollY()/lineHeight;
+ var start = viewModel.get('params.start');
+ var limit = viewModel.get('params.limit');
+ var viewLines = view.getHeight()/lineHeight;
+
+ var viewStart = Math.max(parseInt(line - 1 - view.viewBuffer, 10), 0);
+ var viewEnd = parseInt(line + viewLines + 1 + view.viewBuffer, 10);
+
+ if (viewStart < start || viewEnd > (start+limit)) {
+ viewModel.set('params.start',
+ Math.max(parseInt(line - limit/2 + 10, 10), 0));
+ view.loadTask.delay(200);
+ }
+ },
+
+ init: function(view) {
+ var me = this;
+
+ if (!view.url) {
+ throw "no url specified";
+ }
+
+ var viewModel = this.getViewModel();
+ var since = new Date();
+ since.setDate(since.getDate() - 3);
+ viewModel.set('until', new Date());
+ viewModel.set('since', since);
+ viewModel.set('params.limit', view.pageSize);
+ viewModel.set('hide_timespan', !view.log_select_timespan);
+ me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
+
+ view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
+
+ me.updateParams();
+ view.task = Ext.TaskManager.start({
+ run: function() {
+ if (!view.isVisible() || !view.scrollToEnd) {
+ return;
+ }
+
+ if (me.scrollPosBottom() <= 1) {
+ view.loadTask.delay(200);
+ }
+ },
+ interval: 1000
+ });
+ }
+ },
+
+ onDestroy: function() {
+ var me = this;
+ me.loadTask.cancel();
+ Ext.TaskManager.stop(me.task);
+ },
+
+ // for user to initiate a load from outside
+ requestUpdate: function() {
+ var me = this;
+ me.loadTask.delay(200);
+ },
+
+ viewModel: {
+ data: {
+ until: null,
+ since: null,
+ hide_timespan: false,
+ data: {
+ start: 0,
+ total: 0,
+ textlen: 0
+ },
+ params: {
+ start: 0,
+ limit: 500,
+ }
+ }
+ },
+
+ layout: 'auto',
+ bodyPadding: 5,
+ scrollable: {
+ x: 'auto',
+ y: 'auto',
+ listeners: {
+ // we have to have this here, since we cannot listen to events
+ // of the scroller in the viewcontroller (extjs bug?), nor does
+ // the panel have a 'scroll' event'
+ scroll: {
+ fn: function(scroller, x, y) {
+ var controller = this.component.getController();
+ if (controller) { // on destroy, controller can be gone
+ controller.onScroll(x,y);
+ }
+ },
+ buffer: 200
+ },
+ }
+ },
+
+ tbar: {
+ bind: {
+ hidden: '{hide_timespan}'
+ },
+ items: [
+ '->',
+ 'Since: ',
+ {
+ xtype: 'datefield',
+ name: 'since_date',
+ reference: 'since',
+ format: 'Y-m-d',
+ bind: {
+ value: '{since}',
+ maxValue: '{until}'
+ }
+ },
+ 'Until: ',
+ {
+ xtype: 'datefield',
+ name: 'until_date',
+ reference: 'until',
+ format: 'Y-m-d',
+ bind: {
+ value: '{until}',
+ minValue: '{since}'
+ }
+ },
+ {
+ xtype: 'button',
+ text: 'Update',
+ handler: 'updateParams'
+ }
+ ],
+ },
+
+ items: [
+ {
+ xtype: 'box',
+ reference: 'content',
+ style: {
+ font: 'normal 11px tahoma, arial, verdana, sans-serif',
+ 'white-space': 'pre'
+ },
+ }
+ ]
+});
+/*
+ * Display log entries in a panel with scrollbar
+ * The log entries are automatically refreshed via a background task,
+ * with newest entries comming at the bottom
+ */
+Ext.define('Proxmox.panel.JournalView', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'proxmoxJournalView',
+
+ numEntries: 500,
+ lineHeight: 16,
+
+ scrollToEnd: true,
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ updateParams: function() {
+ var me = this;
+ var viewModel = me.getViewModel();
+ var since = viewModel.get('since');
+ var until = viewModel.get('until');
+
+ since.setHours(0, 0, 0, 0);
+ until.setHours(0, 0, 0, 0);
+ until.setDate(until.getDate()+1);
+
+ me.getView().loadTask.delay(200, undefined, undefined, [
+ false,
+ false,
+ Ext.Date.format(since, "U"),
+ Ext.Date.format(until, "U")
+ ]);
+ },
+
+ scrollPosBottom: function() {
+ var view = this.getView();
+ var pos = view.getScrollY();
+ var maxPos = view.getScrollable().getMaxPosition().y;
+ return maxPos - pos;
+ },
+
+ scrollPosTop: function() {
+ var view = this.getView();
+ return view.getScrollY();
+ },
+
+ updateScroll: function(livemode, num, scrollPos, scrollPosTop) {
+ var me = this;
+ var view = me.getView();
+
+ if (!livemode) {
+ setTimeout(function() { view.scrollTo(0, 0); }, 10);
+ } else if (view.scrollToEnd && scrollPos <= 0) {
+ setTimeout(function() { view.scrollTo(0, Infinity); }, 10);
+ } else if (!view.scrollToEnd && scrollPosTop < 20*view.lineHeight) {
+ setTimeout(function() { view.scrollTo(0, num*view.lineHeight + scrollPosTop); }, 10);
+ }
+ },
+
+ updateView: function(lines, livemode, top) {
+ var me = this;
+ var view = me.getView();
+ var viewmodel = me.getViewModel();
+ if (viewmodel.get('livemode') !== livemode) {
+ return; // we switched mode, do not update the content
+ }
+ var contentEl = me.lookup('content');
+
+ // save old scrollpositions
+ var scrollPos = me.scrollPosBottom();
+ var scrollPosTop = me.scrollPosTop();
+
+ var newend = lines.shift();
+ var newstart = lines.pop();
+
+ var num = lines.length;
+ var text = lines.map(Ext.htmlEncode).join('
');
+
+ if (!livemode) {
+ if (num) {
+ view.content = text;
+ } else {
+ view.content = 'nothing logged or no timespan selected';
+ }
+ } else {
+ // update content
+ if (top && num) {
+ view.content = view.content ? text + '
' + view.content : text;
+ } else if (!top && num) {
+ view.content = view.content ? view.content + '
' + text : text;
+ }
+
+ // update cursors
+ if (!top || !view.startcursor) {
+ view.startcursor = newstart;
+ }
+
+ if (top || !view.endcursor) {
+ view.endcursor = newend;
+ }
+ }
+
+ contentEl.update(view.content);
+
+ me.updateScroll(livemode, num, scrollPos, scrollPosTop);
+ },
+
+ doLoad: function(livemode, top, since, until) {
+ var me = this;
+ if (me.running) {
+ me.requested = true;
+ return;
+ }
+ me.running = true;
+ var view = me.getView();
+ var params = {
+ lastentries: view.numEntries || 500,
+ };
+ if (livemode) {
+ if (!top && view.startcursor) {
+ params = {
+ startcursor: view.startcursor
+ };
+ } else if (view.endcursor) {
+ params.endcursor = view.endcursor;
+ }
+ } else {
+ params = {
+ since: since,
+ until: until
+ };
+ }
+ Proxmox.Utils.API2Request({
+ url: view.url,
+ params: params,
+ waitMsgTarget: (!livemode) ? view : undefined,
+ method: 'GET',
+ success: function(response) {
+ Proxmox.Utils.setErrorMask(me, false);
+ var lines = response.result.data;
+ me.updateView(lines, livemode, top);
+ me.running = false;
+ if (me.requested) {
+ me.requested = false;
+ view.loadTask.delay(200);
+ }
+ },
+ failure: function(response) {
+ var msg = response.htmlStatus;
+ Proxmox.Utils.setErrorMask(me, msg);
+ me.running = false;
+ if (me.requested) {
+ me.requested = false;
+ view.loadTask.delay(200);
+ }
+ }
+ });
+ },
+
+ onScroll: function(x, y) {
+ var me = this;
+ var view = me.getView();
+ var viewmodel = me.getViewModel();
+ var livemode = viewmodel.get('livemode');
+ if (!livemode) {
+ return;
+ }
+
+ if (me.scrollPosTop() < 20*view.lineHeight) {
+ view.scrollToEnd = false;
+ view.loadTask.delay(200, undefined, undefined, [true, true]);
+ } else if (me.scrollPosBottom() <= 1) {
+ view.scrollToEnd = true;
+ }
+ },
+
+ init: function(view) {
+ var me = this;
+
+ if (!view.url) {
+ throw "no url specified";
+ }
+
+ var viewmodel = me.getViewModel();
+ var viewModel = this.getViewModel();
+ var since = new Date();
+ since.setDate(since.getDate() - 3);
+ viewModel.set('until', new Date());
+ viewModel.set('since', since);
+ me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
+
+ view.loadTask = new Ext.util.DelayedTask(me.doLoad, me, [true, false]);
+
+ me.updateParams();
+ view.task = Ext.TaskManager.start({
+ run: function() {
+ if (!view.isVisible() || !view.scrollToEnd || !viewmodel.get('livemode')) {
+ return;
+ }
+
+ if (me.scrollPosBottom() <= 1) {
+ view.loadTask.delay(200, undefined, undefined, [true, false]);
+ }
+ },
+ interval: 1000
+ });
+ },
+
+ onLiveMode: function() {
+ var me = this;
+ var view = me.getView();
+ delete view.startcursor;
+ delete view.endcursor;
+ delete view.content;
+ me.getViewModel().set('livemode', true);
+ view.scrollToEnd = true;
+ me.updateView([], true, false);
+ },
+
+ onTimespan: function() {
+ var me = this;
+ me.getViewModel().set('livemode', false);
+ me.updateView([], false);
+ }
+ },
+
+ onDestroy: function() {
+ var me = this;
+ me.loadTask.cancel();
+ Ext.TaskManager.stop(me.task);
+ delete me.content;
+ },
+
+ // for user to initiate a load from outside
+ requestUpdate: function() {
+ var me = this;
+ me.loadTask.delay(200);
+ },
+
+ viewModel: {
+ data: {
+ livemode: true,
+ until: null,
+ since: null
+ }
+ },
+
+ layout: 'auto',
+ bodyPadding: 5,
+ scrollable: {
+ x: 'auto',
+ y: 'auto',
+ listeners: {
+ // we have to have this here, since we cannot listen to events
+ // of the scroller in the viewcontroller (extjs bug?), nor does
+ // the panel have a 'scroll' event'
+ scroll: {
+ fn: function(scroller, x, y) {
+ var controller = this.component.getController();
+ if (controller) { // on destroy, controller can be gone
+ controller.onScroll(x,y);
+ }
+ },
+ buffer: 200
+ },
+ }
+ },
+
+ tbar: {
+
+ items: [
+ '->',
+ {
+ xtype: 'segmentedbutton',
+ items: [
+ {
+ text: gettext('Live Mode'),
+ bind: {
+ pressed: '{livemode}'
+ },
+ handler: 'onLiveMode',
+ },
+ {
+ text: gettext('Select Timespan'),
+ bind: {
+ pressed: '{!livemode}'
+ },
+ handler: 'onTimespan',
+ }
+ ]
+ },
+ {
+ xtype: 'box',
+ bind: { disabled: '{livemode}' },
+ autoEl: { cn: gettext('Since') + ':' }
+ },
+ {
+ xtype: 'datefield',
+ name: 'since_date',
+ reference: 'since',
+ format: 'Y-m-d',
+ bind: {
+ disabled: '{livemode}',
+ value: '{since}',
+ maxValue: '{until}'
+ }
+ },
+ {
+ xtype: 'box',
+ bind: { disabled: '{livemode}' },
+ autoEl: { cn: gettext('Until') + ':' }
+ },
+ {
+ xtype: 'datefield',
+ name: 'until_date',
+ reference: 'until',
+ format: 'Y-m-d',
+ bind: {
+ disabled: '{livemode}',
+ value: '{until}',
+ minValue: '{since}'
+ }
+ },
+ {
+ xtype: 'button',
+ text: 'Update',
+ reference: 'updateBtn',
+ handler: 'updateParams',
+ bind: {
+ disabled: '{livemode}'
+ }
+ }
+ ]
+ },
+
+ items: [
+ {
+ xtype: 'box',
+ reference: 'content',
+ style: {
+ font: 'normal 11px tahoma, arial, verdana, sans-serif',
+ 'white-space': 'pre'
+ },
+ }
+ ]
+});
+Ext.define('Proxmox.widget.RRDChart', {
+ extend: 'Ext.chart.CartesianChart',
+ alias: 'widget.proxmoxRRDChart',
+
+ unit: undefined, // bytes, bytespersecond, percent
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ convertToUnits: function(value) {
+ var units = ['', 'k','M','G','T', 'P'];
+ var si = 0;
+ while(value >= 1000 && si < (units.length -1)){
+ value = value / 1000;
+ si++;
+ }
+
+ // javascript floating point weirdness
+ value = Ext.Number.correctFloat(value);
+
+ // limit to 2 decimal points
+ value = Ext.util.Format.number(value, "0.##");
+
+ return value.toString() + " " + units[si];
+ },
+
+ leftAxisRenderer: function(axis, label, layoutContext) {
+ var me = this;
+
+ return me.convertToUnits(label);
+ },
+
+ onSeriesTooltipRender: function(tooltip, record, item) {
+ var me = this.getView();
+
+ var suffix = '';
+
+ if (me.unit === 'percent') {
+ suffix = '%';
+ } else if (me.unit === 'bytes') {
+ suffix = 'B';
+ } else if (me.unit === 'bytespersecond') {
+ suffix = 'B/s';
+ }
+
+ var prefix = item.field;
+ if (me.fieldTitles && me.fieldTitles[me.fields.indexOf(item.field)]) {
+ prefix = me.fieldTitles[me.fields.indexOf(item.field)];
+ }
+ tooltip.setHtml(prefix + ': ' + this.convertToUnits(record.get(item.field)) + suffix +
+ '
' + new Date(record.get('time')));
+ },
+
+ onAfterAnimation: function(chart, eopts) {
+ // if the undobuton is disabled,
+ // disable our tool
+
+ var ourUndoZoomButton = chart.tools[0];
+ var undoButton = chart.interactions[0].getUndoButton();
+ ourUndoZoomButton.setDisabled(undoButton.isDisabled());
+ }
+ },
+
+ width: 770,
+ height: 300,
+ animation: false,
+ interactions: [{
+ type: 'crosszoom'
+ }],
+ axes: [{
+ type: 'numeric',
+ position: 'left',
+ grid: true,
+ renderer: 'leftAxisRenderer',
+ //renderer: function(axis, label) { return label; },
+ minimum: 0
+ }, {
+ type: 'time',
+ position: 'bottom',
+ grid: true,
+ fields: ['time']
+ }],
+ legend: {
+ docked: 'bottom'
+ },
+ listeners: {
+ animationend: 'onAfterAnimation'
+ },
+
+
+ initComponent: function() {
+ var me = this;
+ var series = {};
+
+ if (!me.store) {
+ throw "cannot work without store";
+ }
+
+ if (!me.fields) {
+ throw "cannot work without fields";
+ }
+
+ me.callParent();
+
+ // add correct label for left axis
+ var axisTitle = "";
+ if (me.unit === 'percent') {
+ axisTitle = "%";
+ } else if (me.unit === 'bytes') {
+ axisTitle = "Bytes";
+ } else if (me.unit === 'bytespersecond') {
+ axisTitle = "Bytes/s";
+ } else if (me.fieldTitles && me.fieldTitles.length === 1) {
+ axisTitle = me.fieldTitles[0];
+ } else if (me.fields.length === 1) {
+ axisTitle = me.fields[0];
+ }
+
+ me.axes[0].setTitle(axisTitle);
+
+ if (!me.noTool) {
+ me.addTool([{
+ type: 'minus',
+ disabled: true,
+ tooltip: gettext('Undo Zoom'),
+ handler: function(){
+ var undoButton = me.interactions[0].getUndoButton();
+ if (undoButton.handler) {
+ undoButton.handler();
+ }
+ }
+ },{
+ type: 'restore',
+ tooltip: gettext('Toggle Legend'),
+ handler: function(){
+ if (me.legend) {
+ me.legend.setVisible(!me.legend.isVisible());
+ }
+ }
+ }]);
+ }
+
+ // add a series for each field we get
+ me.fields.forEach(function(item, index){
+ var title = item;
+ if (me.fieldTitles && me.fieldTitles[index]) {
+ title = me.fieldTitles[index];
+ }
+ me.addSeries(Ext.apply(
+ {
+ type: 'line',
+ xField: 'time',
+ yField: item,
+ title: title,
+ fill: true,
+ style: {
+ lineWidth: 1.5,
+ opacity: 0.60
+ },
+ marker: {
+ opacity: 0,
+ scaling: 0.01,
+ fx: {
+ duration: 200,
+ easing: 'easeOut'
+ }
+ },
+ highlightCfg: {
+ opacity: 1,
+ scaling: 1.5
+ },
+ tooltip: {
+ trackMouse: true,
+ renderer: 'onSeriesTooltipRender'
+ }
+ },
+ me.seriesConfig
+ ));
+ });
+
+ // enable animation after the store is loaded
+ me.store.onAfter('load', function() {
+ me.setAnimation(true);
+ }, this, {single: true});
+ }
+});
+Ext.define('Proxmox.panel.GaugeWidget', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.proxmoxGauge',
+
+ defaults: {
+ style: {
+ 'text-align':'center'
+ }
+ },
+ items: [
+ {
+ xtype: 'box',
+ itemId: 'title',
+ data: {
+ title: ''
+ },
+ tpl: '{title}
'
+ },
+ {
+ xtype: 'polar',
+ height: 120,
+ border: false,
+ itemId: 'chart',
+ series: [{
+ type: 'gauge',
+ value: 0,
+ colors: ['#f5f5f5'],
+ sectors: [0],
+ donut: 90,
+ needleLength: 100,
+ totalAngle: Math.PI
+ }],
+ sprites: [{
+ id: 'valueSprite',
+ type: 'text',
+ text: '',
+ textAlign: 'center',
+ textBaseline: 'bottom',
+ x: 125,
+ y: 110,
+ fontSize: 30
+ }]
+ },
+ {
+ xtype: 'box',
+ itemId: 'text'
+ }
+ ],
+
+ header: false,
+ border: false,
+
+ warningThreshold: 0.6,
+ criticalThreshold: 0.9,
+ warningColor: '#fc0',
+ criticalColor: '#FF6C59',
+ defaultColor: '#c2ddf2',
+ backgroundColor: '#f5f5f5',
+
+ initialValue: 0,
+
+
+ updateValue: function(value, text) {
+ var me = this;
+ var color = me.defaultColor;
+ var attr = {};
+
+ if (value >= me.criticalThreshold) {
+ color = me.criticalColor;
+ } else if (value >= me.warningThreshold) {
+ color = me.warningColor;
+ }
+
+ me.chart.series[0].setColors([color, me.backgroundColor]);
+ me.chart.series[0].setValue(value*100);
+
+ me.valueSprite.setText(' '+(value*100).toFixed(0) + '%');
+ attr.x = me.chart.getWidth()/2;
+ attr.y = me.chart.getHeight()-20;
+ if (me.spriteFontSize) {
+ attr.fontSize = me.spriteFontSize;
+ }
+ me.valueSprite.setAttributes(attr, true);
+
+ if (text !== undefined) {
+ me.text.setHtml(text);
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+
+ if (me.title) {
+ me.getComponent('title').update({title: me.title});
+ }
+ me.text = me.getComponent('text');
+ me.chart = me.getComponent('chart');
+ me.valueSprite = me.chart.getSurface('chart').get('valueSprite');
+ }
+});
+// fixme: how can we avoid those lint errors?
+/*jslint confusion: true */
+Ext.define('Proxmox.window.Edit', {
+ extend: 'Ext.window.Window',
+ alias: 'widget.proxmoxWindowEdit',
+
+ // autoLoad trigger a load() after component creation
+ autoLoad: false,
+
+ resizable: false,
+
+ // use this tio atimatically generate a title like
+ // Create: " + Ext.htmlEncode(changes) + "
");
+ changeitem.setHidden(false);
+ }
+ }
+ });
+ };
+
+ var run_editor = function() {
+ var grid = me.down('gridpanel');
+ var sm = grid.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iface: rec.data.iface,
+ iftype: rec.data.type
+ });
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ var edit_btn = new Ext.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ handler: run_editor
+ });
+
+ var del_btn = new Ext.Button({
+ text: gettext('Remove'),
+ disabled: true,
+ handler: function(){
+ var grid = me.down('gridpanel');
+ var sm = grid.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var iface = rec.data.iface;
+
+ Proxmox.Utils.API2Request({
+ url: baseUrl + '/' + iface,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ callback: function() {
+ reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ }
+ });
+
+ var set_button_status = function() {
+ var grid = me.down('gridpanel');
+ var sm = grid.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ edit_btn.setDisabled(!rec);
+ del_btn.setDisabled(!rec);
+ };
+
+ var render_ports = function(value, metaData, record) {
+ if (value === 'bridge') {
+ return record.data.bridge_ports;
+ } else if (value === 'bond') {
+ return record.data.slaves;
+ } else if (value === 'OVSBridge') {
+ return record.data.ovs_ports;
+ } else if (value === 'OVSBond') {
+ return record.data.ovs_bonds;
+ }
+ };
+
+ var find_next_iface_id = function(prefix) {
+ var next;
+ for (next = 0; next <= 9999; next++) {
+ if (!store.getById(prefix + next.toString())) {
+ break;
+ }
+ }
+ return prefix + next.toString();
+ };
+
+ var menu_items = [];
+
+ if (me.types.indexOf('bridge') !== -1) {
+ menu_items.push({
+ text: Proxmox.Utils.render_network_iface_type('bridge'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'bridge',
+ iface_default: find_next_iface_id('vmbr')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ });
+ }
+
+ if (me.types.indexOf('bond') !== -1) {
+ menu_items.push({
+ text: Proxmox.Utils.render_network_iface_type('bond'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'bond',
+ iface_default: find_next_iface_id('bond')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ });
+ }
+
+ if (me.types.indexOf('ovs') !== -1) {
+ if (menu_items.length > 0) {
+ menu_items.push({ xtype: 'menuseparator' });
+ }
+
+ menu_items.push(
+ {
+ text: Proxmox.Utils.render_network_iface_type('OVSBridge'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'OVSBridge',
+ iface_default: find_next_iface_id('vmbr')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: Proxmox.Utils.render_network_iface_type('OVSBond'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'OVSBond',
+ iface_default: find_next_iface_id('bond')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'OVSIntPort'
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ }
+ );
+ }
+
+ var renderer_generator = function(fieldname) {
+ return function(val, metaData, rec) {
+ var tmp = [];
+ if (rec.data[fieldname]) {
+ tmp.push(rec.data[fieldname]);
+ }
+ if (rec.data[fieldname + '6']) {
+ tmp.push(rec.data[fieldname + '6']);
+ }
+ return tmp.join('
') || '';
+ };
+ };
+
+ Ext.apply(me, {
+ layout: 'border',
+ tbar: [
+ {
+ text: gettext('Create'),
+ menu: {
+ plain: true,
+ items: menu_items
+ }
+ }, ' ',
+ {
+ text: gettext('Revert'),
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: baseUrl,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ callback: function() {
+ reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ }
+ },
+ edit_btn,
+ del_btn
+ ],
+ items: [
+ {
+ xtype: 'gridpanel',
+ stateful: true,
+ stateId: 'grid-node-network',
+ store: store,
+ region: 'center',
+ border: false,
+ columns: [
+ {
+ header: gettext('Name'),
+ sortable: true,
+ dataIndex: 'iface'
+ },
+ {
+ header: gettext('Type'),
+ sortable: true,
+ width: 120,
+ renderer: Proxmox.Utils.render_network_iface_type,
+ dataIndex: 'type'
+ },
+ {
+ xtype: 'booleancolumn',
+ header: gettext('Active'),
+ width: 80,
+ sortable: true,
+ dataIndex: 'active',
+ trueText: Proxmox.Utils.yesText,
+ falseText: Proxmox.Utils.noText,
+ undefinedText: Proxmox.Utils.noText,
+ },
+ {
+ xtype: 'booleancolumn',
+ header: gettext('Autostart'),
+ width: 80,
+ sortable: true,
+ dataIndex: 'autostart',
+ trueText: Proxmox.Utils.yesText,
+ falseText: Proxmox.Utils.noText,
+ undefinedText: Proxmox.Utils.noText
+ },
+ {
+ xtype: 'booleancolumn',
+ header: gettext('VLAN aware'),
+ width: 80,
+ sortable: true,
+ dataIndex: 'bridge_vlan_aware',
+ trueText: Proxmox.Utils.yesText,
+ falseText: Proxmox.Utils.noText,
+ undefinedText: Proxmox.Utils.noText
+ },
+ {
+ header: gettext('Ports/Slaves'),
+ dataIndex: 'type',
+ renderer: render_ports
+ },
+ {
+ header: gettext('Bond Mode'),
+ dataIndex: 'bond_mode',
+ renderer: Proxmox.Utils.render_bond_mode,
+ },
+ {
+ header: gettext('Hash Policy'),
+ hidden: true,
+ dataIndex: 'bond_xmit_hash_policy',
+ },
+ {
+ header: gettext('IP address'),
+ sortable: true,
+ width: 120,
+ hidden: true,
+ dataIndex: 'address',
+ renderer: renderer_generator('address'),
+ },
+ {
+ header: gettext('Subnet mask'),
+ width: 120,
+ sortable: true,
+ hidden: true,
+ dataIndex: 'netmask',
+ renderer: renderer_generator('netmask'),
+ },
+ {
+ header: gettext('CIDR'),
+ width: 120,
+ sortable: true,
+ dataIndex: 'cidr',
+ renderer: renderer_generator('cidr'),
+ },
+ {
+ header: gettext('Gateway'),
+ width: 120,
+ sortable: true,
+ dataIndex: 'gateway',
+ renderer: renderer_generator('gateway'),
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comments',
+ flex: 1,
+ renderer: Ext.String.htmlEncode
+ }
+ ],
+ listeners: {
+ selectionchange: set_button_status,
+ itemdblclick: run_editor
+ }
+ },
+ {
+ border: false,
+ region: 'south',
+ autoScroll: true,
+ hidden: true,
+ itemId: 'changes',
+ tbar: [
+ gettext('Pending changes') + ' (' +
+ gettext('Please reboot to activate changes') + ')'
+ ],
+ split: true,
+ bodyPadding: 5,
+ flex: 0.6,
+ html: gettext("No changes")
+ }
+ ],
+ });
+
+ me.callParent();
+ reload();
+ }
+});
+Ext.define('Proxmox.node.DNSEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.proxmoxNodeDNSEdit'],
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.items = [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Search domain'),
+ name: 'search',
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('DNS server') + " 1",
+ vtype: 'IP64Address',
+ skipEmptyText: true,
+ name: 'dns1'
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('DNS server') + " 2",
+ vtype: 'IP64Address',
+ skipEmptyText: true,
+ name: 'dns2'
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('DNS server') + " 3",
+ vtype: 'IP64Address',
+ skipEmptyText: true,
+ name: 'dns3'
+ }
+ ];
+
+ Ext.applyIf(me, {
+ subject: gettext('DNS'),
+ url: "/api2/extjs/nodes/" + me.nodename + "/dns",
+ fieldDefaults: {
+ labelWidth: 120
+ }
+ });
+
+ me.callParent();
+
+ me.load();
+ }
+});
+Ext.define('Proxmox.node.HostsView', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'proxmoxNodeHostsView',
+
+ reload: function() {
+ var me = this;
+ me.store.load();
+ },
+
+ tbar: [
+ {
+ text: gettext('Save'),
+ disabled: true,
+ itemId: 'savebtn',
+ handler: function() {
+ var me = this.up('panel');
+ Proxmox.Utils.API2Request({
+ params: {
+ digest: me.digest,
+ data: me.down('#hostsfield').getValue()
+ },
+ method: 'POST',
+ url: '/nodes/' + me.nodename + '/hosts',
+ waitMsgTarget: me,
+ success: function(response, opts) {
+ me.reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ }
+ },
+ {
+ text: gettext('Revert'),
+ disabled: true,
+ itemId: 'resetbtn',
+ handler: function() {
+ var me = this.up('panel');
+ me.down('#hostsfield').reset();
+ }
+ }
+ ],
+
+ layout: 'fit',
+
+ items: [
+ {
+ xtype: 'textarea',
+ itemId: 'hostsfield',
+ fieldStyle: {
+ 'font-family': 'monospace',
+ 'white-space': 'pre'
+ },
+ listeners: {
+ dirtychange: function(ta, dirty) {
+ var me = this.up('panel');
+ me.down('#savebtn').setDisabled(!dirty);
+ me.down('#resetbtn').setDisabled(!dirty);
+ }
+ }
+ }
+ ],
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.store = Ext.create('Ext.data.Store', {
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/nodes/" + me.nodename + "/hosts",
+ }
+ });
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, me.store);
+
+ me.mon(me.store, 'load', function(store, records, success) {
+ if (!success || records.length < 1) {
+ return;
+ }
+ me.digest = records[0].data.digest;
+ var data = records[0].data.data;
+ me.down('#hostsfield').setValue(data);
+ me.down('#hostsfield').resetOriginalValue();
+ });
+
+ me.reload();
+ }
+});
+Ext.define('Proxmox.node.DNSView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.proxmoxNodeDNSView'],
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var run_editor = function() {
+ var win = Ext.create('Proxmox.node.DNSEdit', {
+ nodename: me.nodename
+ });
+ win.show();
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + me.nodename + "/dns",
+ cwidth1: 130,
+ interval: 1000,
+ run_editor: run_editor,
+ rows: {
+ search: {
+ header: 'Search domain',
+ required: true,
+ renderer: Ext.htmlEncode
+ },
+ dns1: {
+ header: gettext('DNS server') + " 1",
+ required: true,
+ renderer: Ext.htmlEncode
+ },
+ dns2: {
+ header: gettext('DNS server') + " 2",
+ renderer: Ext.htmlEncode
+ },
+ dns3: {
+ header: gettext('DNS server') + " 3",
+ renderer: Ext.htmlEncode
+ }
+ },
+ tbar: [
+ {
+ text: gettext("Edit"),
+ handler: run_editor
+ }
+ ],
+ listeners: {
+ itemdblclick: run_editor
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ }
+});
+Ext.define('Proxmox.node.Tasks', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.proxmoxNodeTasks'],
+ stateful: true,
+ stateId: 'grid-node-tasks',
+ loadMask: true,
+ sortableColumns: false,
+ vmidFilter: 0,
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var store = Ext.create('Ext.data.BufferedStore', {
+ pageSize: 500,
+ autoLoad: true,
+ remoteFilter: true,
+ model: 'proxmox-tasks',
+ proxy: {
+ type: 'proxmox',
+ startParam: 'start',
+ limitParam: 'limit',
+ url: "/api2/json/nodes/" + me.nodename + "/tasks"
+ }
+ });
+
+ var userfilter = '';
+ var filter_errors = 0;
+
+ var updateProxyParams = function() {
+ var params = {
+ errors: filter_errors
+ };
+ if (userfilter) {
+ params.userfilter = userfilter;
+ }
+ if (me.vmidFilter) {
+ params.vmid = me.vmidFilter;
+ }
+ store.proxy.extraParams = params;
+ };
+
+ updateProxyParams();
+
+ var reload_task = Ext.create('Ext.util.DelayedTask',function() {
+ updateProxyParams();
+ store.reload();
+ });
+
+ var run_task_viewer = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: rec.data.upid
+ });
+ win.show();
+ };
+
+ var view_btn = new Ext.Button({
+ text: gettext('View'),
+ disabled: true,
+ handler: run_task_viewer
+ });
+
+ Proxmox.Utils.monStoreErrors(me, store, true);
+
+ Ext.apply(me, {
+ store: store,
+ viewConfig: {
+ trackOver: false,
+ stripeRows: false, // does not work with getRowClass()
+
+ getRowClass: function(record, index) {
+ var status = record.get('status');
+
+ if (status && status != 'OK') {
+ return "proxmox-invalid-row";
+ }
+ }
+ },
+ tbar: [
+ view_btn, '->', gettext('User name') +':', ' ',
+ {
+ xtype: 'textfield',
+ width: 200,
+ value: userfilter,
+ enableKeyEvents: true,
+ listeners: {
+ keyup: function(field, e) {
+ userfilter = field.getValue();
+ reload_task.delay(500);
+ }
+ }
+ }, ' ', gettext('Only Errors') + ':', ' ',
+ {
+ xtype: 'checkbox',
+ hideLabel: true,
+ checked: filter_errors,
+ listeners: {
+ change: function(field, checked) {
+ filter_errors = checked ? 1 : 0;
+ reload_task.delay(10);
+ }
+ }
+ }, ' '
+ ],
+ columns: [
+ {
+ header: gettext("Start Time"),
+ dataIndex: 'starttime',
+ width: 100,
+ renderer: function(value) {
+ return Ext.Date.format(value, "M d H:i:s");
+ }
+ },
+ {
+ header: gettext("End Time"),
+ dataIndex: 'endtime',
+ width: 100,
+ renderer: function(value, metaData, record) {
+ return Ext.Date.format(value,"M d H:i:s");
+ }
+ },
+ {
+ header: gettext("Node"),
+ dataIndex: 'node',
+ width: 100
+ },
+ {
+ header: gettext("User name"),
+ dataIndex: 'user',
+ width: 150
+ },
+ {
+ header: gettext("Description"),
+ dataIndex: 'upid',
+ flex: 1,
+ renderer: Proxmox.Utils.render_upid
+ },
+ {
+ header: gettext("Status"),
+ dataIndex: 'status',
+ width: 200,
+ renderer: function(value, metaData, record) {
+ if (value == 'OK') {
+ return 'OK';
+ }
+ // metaData.attr = 'style="color:red;"';
+ return "ERROR: " + value;
+ }
+ }
+ ],
+ listeners: {
+ itemdblclick: run_task_viewer,
+ selectionchange: function(v, selections) {
+ view_btn.setDisabled(!(selections && selections[0]));
+ },
+ show: function() { reload_task.delay(10); },
+ destroy: function() { reload_task.cancel(); }
+ }
+ });
+
+ me.callParent();
+
+ }
+});
+Ext.define('proxmox-services', {
+ extend: 'Ext.data.Model',
+ fields: [ 'service', 'name', 'desc', 'state' ],
+ idProperty: 'service'
+});
+
+Ext.define('Proxmox.node.ServiceView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.proxmoxNodeServiceView'],
+
+ startOnlyServices: {},
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var rstore = Ext.create('Proxmox.data.UpdateStore', {
+ interval: 1000,
+ storeid: 'proxmox-services' + me.nodename,
+ model: 'proxmox-services',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/nodes/" + me.nodename + "/services"
+ }
+ });
+
+ var store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: rstore,
+ sortAfterUpdate: true,
+ sorters: [
+ {
+ property : 'name',
+ direction: 'ASC'
+ }
+ ]
+ });
+
+ var view_service_log = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ var win = Ext.create('Ext.window.Window', {
+ title: gettext('Syslog') + ': ' + rec.data.service,
+ modal: true,
+ width: 800,
+ height: 400,
+ layout: 'fit',
+ items: {
+ xtype: 'proxmoxLogView',
+ url: "/api2/extjs/nodes/" + me.nodename + "/syslog?service=" +
+ rec.data.service,
+ log_select_timespan: 1
+ }
+ });
+ win.show();
+ };
+
+ var service_cmd = function(cmd) {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + me.nodename + "/services/" + rec.data.service + "/" + cmd,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ me.loading = true;
+ },
+ success: function(response, opts) {
+ rstore.startUpdate();
+ var upid = response.result.data;
+
+ var win = Ext.create('Proxmox.window.TaskProgress', {
+ upid: upid
+ });
+ win.show();
+ }
+ });
+ };
+
+ var start_btn = new Ext.Button({
+ text: gettext('Start'),
+ disabled: true,
+ handler: function(){
+ service_cmd("start");
+ }
+ });
+
+ var stop_btn = new Ext.Button({
+ text: gettext('Stop'),
+ disabled: true,
+ handler: function(){
+ service_cmd("stop");
+ }
+ });
+
+ var restart_btn = new Ext.Button({
+ text: gettext('Restart'),
+ disabled: true,
+ handler: function(){
+ service_cmd("restart");
+ }
+ });
+
+ var syslog_btn = new Ext.Button({
+ text: gettext('Syslog'),
+ disabled: true,
+ handler: view_service_log
+ });
+
+ var set_button_status = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ if (!rec) {
+ start_btn.disable();
+ stop_btn.disable();
+ restart_btn.disable();
+ syslog_btn.disable();
+ return;
+ }
+ var service = rec.data.service;
+ var state = rec.data.state;
+
+ syslog_btn.enable();
+
+ if (me.startOnlyServices[service]) {
+ if (state == 'running') {
+ start_btn.disable();
+ restart_btn.enable();
+ } else {
+ start_btn.enable();
+ restart_btn.disable();
+ }
+ stop_btn.disable();
+ } else {
+ if (state == 'running') {
+ start_btn.disable();
+ restart_btn.enable();
+ stop_btn.enable();
+ } else {
+ start_btn.enable();
+ restart_btn.disable();
+ stop_btn.disable();
+ }
+ }
+ };
+
+ me.mon(store, 'refresh', set_button_status);
+
+ Proxmox.Utils.monStoreErrors(me, rstore);
+
+ Ext.apply(me, {
+ store: store,
+ stateful: false,
+ tbar: [ start_btn, stop_btn, restart_btn, syslog_btn ],
+ columns: [
+ {
+ header: gettext('Name'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'name'
+ },
+ {
+ header: gettext('Status'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'state'
+ },
+ {
+ header: gettext('Description'),
+ renderer: Ext.String.htmlEncode,
+ dataIndex: 'desc',
+ flex: 2
+ }
+ ],
+ listeners: {
+ selectionchange: set_button_status,
+ itemdblclick: view_service_log,
+ activate: rstore.startUpdate,
+ destroy: rstore.stopUpdate
+ }
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('Proxmox.node.TimeEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.proxmoxNodeTimeEdit'],
+
+ subject: gettext('Time zone'),
+
+ width: 400,
+
+ autoLoad: true,
+
+ fieldDefaults: {
+ labelWidth: 70
+ },
+
+ items: {
+ xtype: 'combo',
+ fieldLabel: gettext('Time zone'),
+ name: 'timezone',
+ queryMode: 'local',
+ store: Ext.create('Proxmox.data.TimezoneStore'),
+ displayField: 'zone',
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+ allowBlank: false
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+ me.url = "/api2/extjs/nodes/" + me.nodename + "/time";
+
+ me.callParent();
+ }
+});
+Ext.define('Proxmox.node.TimeView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.proxmoxNodeTimeView'],
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var tzoffset = (new Date()).getTimezoneOffset()*60000;
+ var renderlocaltime = function(value) {
+ var servertime = new Date((value * 1000) + tzoffset);
+ return Ext.Date.format(servertime, 'Y-m-d H:i:s');
+ };
+
+ var run_editor = function() {
+ var win = Ext.create('Proxmox.node.TimeEdit', {
+ nodename: me.nodename
+ });
+ win.show();
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + me.nodename + "/time",
+ cwidth1: 150,
+ interval: 1000,
+ run_editor: run_editor,
+ rows: {
+ timezone: {
+ header: gettext('Time zone'),
+ required: true
+ },
+ localtime: {
+ header: gettext('Server time'),
+ required: true,
+ renderer: renderlocaltime
+ }
+ },
+ tbar: [
+ {
+ text: gettext("Edit"),
+ handler: run_editor
+ }
+ ],
+ listeners: {
+ itemdblclick: run_editor
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ }
+});
diff --git a/serverside/jsmod/6.0-4/pvemanagerlib.js b/serverside/jsmod/6.0-4/pvemanagerlib.js
new file mode 100644
index 0000000..add3e49
--- /dev/null
+++ b/serverside/jsmod/6.0-4/pvemanagerlib.js
@@ -0,0 +1,39779 @@
+var pveOnlineHelpInfo = {
+ "ceph_rados_block_devices" : {
+ "link" : "/pve-docs/chapter-pvesm.html#ceph_rados_block_devices",
+ "title" : "Ceph RADOS Block Devices (RBD)"
+ },
+ "chapter_ha_manager" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#chapter_ha_manager",
+ "title" : "High Availability"
+ },
+ "chapter_lvm" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#chapter_lvm",
+ "title" : "Logical Volume Manager (LVM)"
+ },
+ "chapter_pct" : {
+ "link" : "/pve-docs/chapter-pct.html#chapter_pct",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "chapter_pve_firewall" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#chapter_pve_firewall",
+ "title" : "Proxmox VE Firewall"
+ },
+ "chapter_pveceph" : {
+ "link" : "/pve-docs/chapter-pveceph.html#chapter_pveceph",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "chapter_pvecm" : {
+ "link" : "/pve-docs/chapter-pvecm.html#chapter_pvecm",
+ "title" : "Cluster Manager"
+ },
+ "chapter_pvesr" : {
+ "link" : "/pve-docs/chapter-pvesr.html#chapter_pvesr",
+ "title" : "Storage Replication"
+ },
+ "chapter_storage" : {
+ "link" : "/pve-docs/chapter-pvesm.html#chapter_storage",
+ "title" : "Proxmox VE Storage"
+ },
+ "chapter_system_administration" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#chapter_system_administration",
+ "title" : "Host System Administration"
+ },
+ "chapter_user_management" : {
+ "link" : "/pve-docs/chapter-pveum.html#chapter_user_management",
+ "title" : "User Management"
+ },
+ "chapter_virtual_machines" : {
+ "link" : "/pve-docs/chapter-qm.html#chapter_virtual_machines",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "chapter_vzdump" : {
+ "link" : "/pve-docs/chapter-vzdump.html#chapter_vzdump",
+ "title" : "Backup and Restore"
+ },
+ "chapter_zfs" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#chapter_zfs",
+ "title" : "ZFS on Linux"
+ },
+ "datacenter_configuration_file" : {
+ "link" : "/pve-docs/pve-admin-guide.html#datacenter_configuration_file",
+ "title" : "Datacenter Configuration"
+ },
+ "getting_help" : {
+ "link" : "/pve-docs/pve-admin-guide.html#getting_help",
+ "title" : "Getting Help"
+ },
+ "gui_my_settings" : {
+ "link" : "/pve-docs/chapter-pve-gui.html#gui_my_settings",
+ "subtitle" : "My Settings",
+ "title" : "Graphical User Interface"
+ },
+ "ha_manager_fencing" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_fencing",
+ "subtitle" : "Fencing",
+ "title" : "High Availability"
+ },
+ "ha_manager_groups" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_groups",
+ "subtitle" : "Groups",
+ "title" : "High Availability"
+ },
+ "ha_manager_resource_config" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resource_config",
+ "subtitle" : "Resources",
+ "title" : "High Availability"
+ },
+ "ha_manager_resources" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resources",
+ "subtitle" : "Resources",
+ "title" : "High Availability"
+ },
+ "pct_configuration" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_configuration",
+ "subtitle" : "Configuration",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_container_images" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_container_images",
+ "subtitle" : "Container Images",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_container_network" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_container_network",
+ "subtitle" : "Network",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_container_storage" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_container_storage",
+ "subtitle" : "Container Storage",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_cpu" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_cpu",
+ "subtitle" : "CPU",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_general" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_general",
+ "subtitle" : "General Settings",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_memory" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_memory",
+ "subtitle" : "Memory",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_migration" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_migration",
+ "subtitle" : "Migration",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_options" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_options",
+ "subtitle" : "Options",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_snapshots" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_snapshots",
+ "subtitle" : "Snapshots",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_startup_and_shutdown" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_startup_and_shutdown",
+ "subtitle" : "Automatic Start and Shutdown of Containers",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pve_admin_guide" : {
+ "link" : "/pve-docs/pve-admin-guide.html",
+ "title" : "Proxmox VE Administration Guide"
+ },
+ "pve_ceph_install" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_install",
+ "subtitle" : "Installation of Ceph Packages",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "pve_ceph_osds" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_osds",
+ "subtitle" : "Creating Ceph OSDs",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "pve_ceph_pools" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_pools",
+ "subtitle" : "Creating Ceph Pools",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "pve_documentation_index" : {
+ "link" : "/pve-docs/index.html",
+ "title" : "Proxmox VE Documentation Index"
+ },
+ "pve_firewall_cluster_wide_setup" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_cluster_wide_setup",
+ "subtitle" : "Cluster Wide Setup",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_host_specific_configuration" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_host_specific_configuration",
+ "subtitle" : "Host Specific Configuration",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_ip_aliases" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_aliases",
+ "subtitle" : "IP Aliases",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_ip_sets" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_sets",
+ "subtitle" : "IP Sets",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_vm_container_configuration" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_vm_container_configuration",
+ "subtitle" : "VM/Container Configuration",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_service_daemons" : {
+ "link" : "/pve-docs/index.html#_service_daemons",
+ "title" : "Service Daemons"
+ },
+ "pveceph_fs" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs",
+ "subtitle" : "CephFS",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "pveceph_fs_create" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_create",
+ "subtitle" : "Create a CephFS",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "pvecm_create_cluster" : {
+ "link" : "/pve-docs/chapter-pvecm.html#pvecm_create_cluster",
+ "subtitle" : "Create the Cluster",
+ "title" : "Cluster Manager"
+ },
+ "pvesr_schedule_time_format" : {
+ "link" : "/pve-docs/chapter-pvesr.html#pvesr_schedule_time_format",
+ "subtitle" : "Schedule Format",
+ "title" : "Storage Replication"
+ },
+ "pveum_authentication_realms" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_authentication_realms",
+ "subtitle" : "Authentication Realms",
+ "title" : "User Management"
+ },
+ "pveum_groups" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_groups",
+ "subtitle" : "Groups",
+ "title" : "User Management"
+ },
+ "pveum_permission_management" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_permission_management",
+ "subtitle" : "Permission Management",
+ "title" : "User Management"
+ },
+ "pveum_pools" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_pools",
+ "subtitle" : "Pools",
+ "title" : "User Management"
+ },
+ "pveum_roles" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_roles",
+ "subtitle" : "Roles",
+ "title" : "User Management"
+ },
+ "pveum_tfa_auth" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_tfa_auth",
+ "subtitle" : "Two factor authentication",
+ "title" : "User Management"
+ },
+ "pveum_users" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_users",
+ "subtitle" : "Users",
+ "title" : "User Management"
+ },
+ "qm_bios_and_uefi" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_bios_and_uefi",
+ "subtitle" : "BIOS and UEFI",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_cloud_init" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_cloud_init",
+ "title" : "Cloud-Init Support"
+ },
+ "qm_copy_and_clone" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_copy_and_clone",
+ "subtitle" : "Copies and Clones",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_cpu" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_cpu",
+ "subtitle" : "CPU",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_general_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_general_settings",
+ "subtitle" : "General Settings",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_hard_disk" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_hard_disk",
+ "subtitle" : "Hard Disk",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_memory" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_memory",
+ "subtitle" : "Memory",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_migration" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_migration",
+ "subtitle" : "Migration",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_network_device" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_network_device",
+ "subtitle" : "Network Device",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_options" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_options",
+ "subtitle" : "Options",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_os_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_os_settings",
+ "subtitle" : "OS Settings",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_pci_passthrough" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_pci_passthrough",
+ "title" : "PCI(e) Passthrough"
+ },
+ "qm_startup_and_shutdown" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_startup_and_shutdown",
+ "subtitle" : "Automatic Start and Shutdown of Virtual Machines",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_system_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_system_settings",
+ "subtitle" : "System Settings",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_usb_passthrough" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_usb_passthrough",
+ "subtitle" : "USB Passthrough",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_virtual_machines_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_virtual_machines_settings",
+ "subtitle" : "Virtual Machines Settings",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "storage_cephfs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_cephfs",
+ "title" : "Ceph Filesystem (CephFS)"
+ },
+ "storage_cifs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_cifs",
+ "title" : "CIFS Backend"
+ },
+ "storage_directory" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_directory",
+ "title" : "Directory Backend"
+ },
+ "storage_glusterfs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_glusterfs",
+ "title" : "GlusterFS Backend"
+ },
+ "storage_lvm" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_lvm",
+ "title" : "LVM Backend"
+ },
+ "storage_lvmthin" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_lvmthin",
+ "title" : "LVM thin Backend"
+ },
+ "storage_nfs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_nfs",
+ "title" : "NFS Backend"
+ },
+ "storage_open_iscsi" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_open_iscsi",
+ "title" : "Open-iSCSI initiator"
+ },
+ "storage_zfspool" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_zfspool",
+ "title" : "Local ZFS Pool Backend"
+ },
+ "sysadmin_certificate_management" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certificate_management",
+ "title" : "Certificate Management"
+ },
+ "sysadmin_network_configuration" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_network_configuration",
+ "title" : "Network Configuration"
+ }
+};
+Ext.ns('PVE');
+
+// avoid errors related to Accessible Rich Internet Applications
+// (access for people with disabilities)
+// TODO reenable after all components are upgraded
+Ext.enableAria = false;
+Ext.enableAriaButtons = false;
+Ext.enableAriaPanels = false;
+
+// avoid errors when running without development tools
+if (!Ext.isDefined(Ext.global.console)) {
+ var console = {
+ log: function() {}
+ };
+}
+console.log("Starting PVE Manager");
+
+Ext.Ajax.defaultHeaders = {
+ 'Accept': 'application/json'
+};
+
+/*jslint confusion: true */
+Ext.define('PVE.Utils', { utilities: {
+
+ // this singleton contains miscellaneous utilities
+
+ toolkit: undefined, // (extjs|touch), set inside Toolkit.js
+
+ bus_match: /^(ide|sata|virtio|scsi)\d+$/,
+
+ log_severity_hash: {
+ 0: "panic",
+ 1: "alert",
+ 2: "critical",
+ 3: "error",
+ 4: "warning",
+ 5: "notice",
+ 6: "info",
+ 7: "debug"
+ },
+
+ support_level_hash: {
+ 'c': gettext('Community'),
+ 'b': gettext('Basic'),
+ 's': gettext('Standard'),
+ 'p': gettext('Premium')
+ },
+
+ noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.',
+
+ kvm_ostypes: {
+ 'Linux': [
+ { desc: '5.x - 2.6 Kernel', val: 'l26' },
+ { desc: '2.4 Kernel', val: 'l24' }
+ ],
+ 'Microsoft Windows': [
+ { desc: '10/2016', val: 'win10' },
+ { desc: '8.x/2012/2012r2', val: 'win8' },
+ { desc: '7/2008r2', val: 'win7' },
+ { desc: 'Vista/2008', val: 'w2k8' },
+ { desc: 'XP/2003', val: 'wxp' },
+ { desc: '2000', val: 'w2k' }
+ ],
+ 'Solaris Kernel': [
+ { desc: '-', val: 'solaris'}
+ ],
+ 'Other': [
+ { desc: '-', val: 'other'}
+ ]
+ },
+
+ get_health_icon: function(state, circle) {
+ if (circle === undefined) {
+ circle = false;
+ }
+
+ if (state === undefined) {
+ state = 'uknown';
+ }
+
+ var icon = 'faded fa-question';
+ switch(state) {
+ case 'good':
+ icon = 'good fa-check';
+ break;
+ case 'old':
+ icon = 'warning fa-refresh';
+ break;
+ case 'warning':
+ icon = 'warning fa-exclamation';
+ break;
+ case 'critical':
+ icon = 'critical fa-times';
+ break;
+ default: break;
+ }
+
+ if (circle) {
+ icon += '-circle';
+ }
+
+ return icon;
+ },
+
+ parse_ceph_version: function(service) {
+ if (service.ceph_version_short) {
+ return service.ceph_version_short;
+ }
+
+ if (service.ceph_version) {
+ var match = service.ceph_version.match(/version (\d+(\.\d+)*)/);
+ if (match) {
+ return match[1];
+ }
+ }
+
+ return undefined;
+ },
+
+ compare_ceph_versions: function(a, b) {
+ if (a === b) {
+ return 0;
+ }
+ let avers = a.toString().split('.');
+ let bvers = b.toString().split('.');
+
+ while (true) {
+ let av = avers.shift();
+ let bv = bvers.shift();
+
+ if (av === undefined && bv === undefined) {
+ return 0;
+ } else if (av === undefined) {
+ return -1;
+ } else if (bv === undefined) {
+ return 1;
+ } else {
+ let diff = parseInt(av, 10) - parseInt(bv, 10);
+ if (diff != 0) return diff;
+ // else we need to look at the next parts
+ }
+ }
+
+ },
+
+ get_ceph_icon_html: function(health, fw) {
+ var state = PVE.Utils.map_ceph_health[health];
+ var cls = PVE.Utils.get_health_icon(state);
+ if (fw) {
+ cls += ' fa-fw';
+ }
+ return " ";
+ },
+
+ map_ceph_health: {
+ 'HEALTH_OK':'good',
+ 'HEALTH_OLD':'old',
+ 'HEALTH_WARN':'warning',
+ 'HEALTH_ERR':'critical'
+ },
+
+ render_ceph_health: function(healthObj) {
+ var state = {
+ iconCls: PVE.Utils.get_health_icon(),
+ text: ''
+ };
+
+ if (!healthObj || !healthObj.status) {
+ return state;
+ }
+
+ var health = PVE.Utils.map_ceph_health[healthObj.status];
+
+ state.iconCls = PVE.Utils.get_health_icon(health, true);
+ state.text = healthObj.status;
+
+ return state;
+ },
+
+ render_zfs_health: function(value) {
+ if (typeof value == 'undefined'){
+ return "";
+ }
+ var iconCls = 'question-circle';
+ switch (value) {
+ case 'AVAIL':
+ case 'ONLINE':
+ iconCls = 'check-circle good';
+ break;
+ case 'REMOVED':
+ case 'DEGRADED':
+ iconCls = 'exclamation-circle warning';
+ break;
+ case 'UNAVAIL':
+ case 'FAULTED':
+ case 'OFFLINE':
+ iconCls = 'times-circle critical';
+ break;
+ default: //unknown
+ }
+
+ return ' ' + value;
+
+ },
+
+ get_kvm_osinfo: function(value) {
+ var info = { base: 'Other' }; // default
+ if (value) {
+ Ext.each(Object.keys(PVE.Utils.kvm_ostypes), function(k) {
+ Ext.each(PVE.Utils.kvm_ostypes[k], function(e) {
+ if (e.val === value) {
+ info = { desc: e.desc, base: k };
+ }
+ });
+ });
+ }
+ return info;
+ },
+
+ render_kvm_ostype: function (value) {
+ var osinfo = PVE.Utils.get_kvm_osinfo(value);
+ if (osinfo.desc && osinfo.desc !== '-') {
+ return osinfo.base + ' ' + osinfo.desc;
+ } else {
+ return osinfo.base;
+ }
+ },
+
+ render_hotplug_features: function (value) {
+ var fa = [];
+
+ if (!value || (value === '0')) {
+ return gettext('Disabled');
+ }
+
+ if (value === '1') {
+ value = 'disk,network,usb';
+ }
+
+ Ext.each(value.split(','), function(el) {
+ if (el === 'disk') {
+ fa.push(gettext('Disk'));
+ } else if (el === 'network') {
+ fa.push(gettext('Network'));
+ } else if (el === 'usb') {
+ fa.push('USB');
+ } else if (el === 'memory') {
+ fa.push(gettext('Memory'));
+ } else if (el === 'cpu') {
+ fa.push(gettext('CPU'));
+ } else {
+ fa.push(el);
+ }
+ });
+
+ return fa.join(', ');
+ },
+
+ render_qga_features: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')';
+ }
+ var props = PVE.Parser.parsePropertyString(value, 'enabled');
+ if (!PVE.Parser.parseBoolean(props.enabled)) {
+ return Proxmox.Utils.disabledText;
+ }
+
+ delete props.enabled;
+ var agentstring = Proxmox.Utils.enabledText;
+
+ Ext.Object.each(props, function(key, value) {
+ var keystring = '' ;
+ agentstring += ', ' + key + ': ';
+
+ if (PVE.Parser.parseBoolean(value)) {
+ agentstring += Proxmox.Utils.enabledText;
+ } else {
+ agentstring += Proxmox.Utils.disabledText;
+ }
+ });
+
+ return agentstring;
+ },
+
+ render_qemu_machine: function(value) {
+ return value || (Proxmox.Utils.defaultText + ' (i440fx)');
+ },
+
+ render_qemu_bios: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText + ' (SeaBIOS)';
+ } else if (value === 'seabios') {
+ return "SeaBIOS";
+ } else if (value === 'ovmf') {
+ return "OVMF (UEFI)";
+ } else {
+ return value;
+ }
+ },
+
+ render_dc_ha_opts: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText;
+ } else {
+ return PVE.Parser.printPropertyString(value);
+ }
+ },
+ render_as_property_string: function(value) {
+ return (!value) ? Proxmox.Utils.defaultText
+ : PVE.Parser.printPropertyString(value);
+ },
+
+ render_scsihw: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText + ' (LSI 53C895A)';
+ } else if (value === 'lsi') {
+ return 'LSI 53C895A';
+ } else if (value === 'lsi53c810') {
+ return 'LSI 53C810';
+ } else if (value === 'megasas') {
+ return 'MegaRAID SAS 8708EM2';
+ } else if (value === 'virtio-scsi-pci') {
+ return 'VirtIO SCSI';
+ } else if (value === 'virtio-scsi-single') {
+ return 'VirtIO SCSI single';
+ } else if (value === 'pvscsi') {
+ return 'VMware PVSCSI';
+ } else {
+ return value;
+ }
+ },
+
+ // fixme: auto-generate this
+ // for now, please keep in sync with PVE::Tools::kvmkeymaps
+ kvm_keymaps: {
+ //ar: 'Arabic',
+ da: 'Danish',
+ de: 'German',
+ 'de-ch': 'German (Swiss)',
+ 'en-gb': 'English (UK)',
+ 'en-us': 'English (USA)',
+ es: 'Spanish',
+ //et: 'Estonia',
+ fi: 'Finnish',
+ //fo: 'Faroe Islands',
+ fr: 'French',
+ 'fr-be': 'French (Belgium)',
+ 'fr-ca': 'French (Canada)',
+ 'fr-ch': 'French (Swiss)',
+ //hr: 'Croatia',
+ hu: 'Hungarian',
+ is: 'Icelandic',
+ it: 'Italian',
+ ja: 'Japanese',
+ lt: 'Lithuanian',
+ //lv: 'Latvian',
+ mk: 'Macedonian',
+ nl: 'Dutch',
+ //'nl-be': 'Dutch (Belgium)',
+ no: 'Norwegian',
+ pl: 'Polish',
+ pt: 'Portuguese',
+ 'pt-br': 'Portuguese (Brazil)',
+ //ru: 'Russian',
+ sl: 'Slovenian',
+ sv: 'Swedish',
+ //th: 'Thai',
+ tr: 'Turkish'
+ },
+
+ kvm_vga_drivers: {
+ std: gettext('Standard VGA'),
+ vmware: gettext('VMware compatible'),
+ qxl: 'SPICE',
+ qxl2: 'SPICE dual monitor',
+ qxl3: 'SPICE three monitors',
+ qxl4: 'SPICE four monitors',
+ serial0: gettext('Serial terminal') + ' 0',
+ serial1: gettext('Serial terminal') + ' 1',
+ serial2: gettext('Serial terminal') + ' 2',
+ serial3: gettext('Serial terminal') + ' 3',
+ virtio: 'VirtIO-GPU',
+ none: Proxmox.Utils.noneText
+ },
+
+ render_kvm_language: function (value) {
+ if (!value || value === '__default__') {
+ return Proxmox.Utils.defaultText;
+ }
+ var text = PVE.Utils.kvm_keymaps[value];
+ if (text) {
+ return text + ' (' + value + ')';
+ }
+ return value;
+ },
+
+ kvm_keymap_array: function() {
+ var data = [['__default__', PVE.Utils.render_kvm_language('')]];
+ Ext.Object.each(PVE.Utils.kvm_keymaps, function(key, value) {
+ data.push([key, PVE.Utils.render_kvm_language(value)]);
+ });
+
+ return data;
+ },
+
+ console_map: {
+ '__default__': Proxmox.Utils.defaultText + ' (xterm.js)',
+ 'vv': 'SPICE (remote-viewer)',
+ 'html5': 'HTML5 (noVNC)',
+ 'xtermjs': 'xterm.js'
+ },
+
+ render_console_viewer: function(value) {
+ value = value || '__default__';
+ if (PVE.Utils.console_map[value]) {
+ return PVE.Utils.console_map[value];
+ }
+ return value;
+ },
+
+ console_viewer_array: function() {
+ return Ext.Array.map(Object.keys(PVE.Utils.console_map), function(v) {
+ return [v, PVE.Utils.render_console_viewer(v)];
+ });
+ },
+
+ render_kvm_vga_driver: function (value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText;
+ }
+ var vga = PVE.Parser.parsePropertyString(value, 'type');
+ var text = PVE.Utils.kvm_vga_drivers[vga.type];
+ if (!vga.type) {
+ text = Proxmox.Utils.defaultText;
+ }
+ if (text) {
+ return text + ' (' + value + ')';
+ }
+ return value;
+ },
+
+ kvm_vga_driver_array: function() {
+ var data = [['__default__', PVE.Utils.render_kvm_vga_driver('')]];
+ Ext.Object.each(PVE.Utils.kvm_vga_drivers, function(key, value) {
+ data.push([key, PVE.Utils.render_kvm_vga_driver(value)]);
+ });
+
+ return data;
+ },
+
+ render_kvm_startup: function(value) {
+ var startup = PVE.Parser.parseStartup(value);
+
+ var res = 'order=';
+ if (startup.order === undefined) {
+ res += 'any';
+ } else {
+ res += startup.order;
+ }
+ if (startup.up !== undefined) {
+ res += ',up=' + startup.up;
+ }
+ if (startup.down !== undefined) {
+ res += ',down=' + startup.down;
+ }
+
+ return res;
+ },
+
+ extractFormActionError: function(action) {
+ var msg;
+ switch (action.failureType) {
+ case Ext.form.action.Action.CLIENT_INVALID:
+ msg = gettext('Form fields may not be submitted with invalid values');
+ break;
+ case Ext.form.action.Action.CONNECT_FAILURE:
+ msg = gettext('Connection error');
+ var resp = action.response;
+ if (resp.status && resp.statusText) {
+ msg += " " + resp.status + ": " + resp.statusText;
+ }
+ break;
+ case Ext.form.action.Action.LOAD_FAILURE:
+ case Ext.form.action.Action.SERVER_INVALID:
+ msg = Proxmox.Utils.extractRequestError(action.result, true);
+ break;
+ }
+ return msg;
+ },
+
+ format_duration_short: function(ut) {
+
+ if (ut < 60) {
+ return ut.toFixed(1) + 's';
+ }
+
+ if (ut < 3600) {
+ var mins = ut / 60;
+ return mins.toFixed(1) + 'm';
+ }
+
+ if (ut < 86400) {
+ var hours = ut / 3600;
+ return hours.toFixed(1) + 'h';
+ }
+
+ var days = ut / 86400;
+ return days.toFixed(1) + 'd';
+ },
+
+ contentTypes: {
+ 'images': gettext('Disk image'),
+ 'backup': gettext('VZDump backup file'),
+ 'vztmpl': gettext('Container template'),
+ 'iso': gettext('ISO image'),
+ 'rootdir': gettext('Container'),
+ 'snippets': gettext('Snippets')
+ },
+
+ storageSchema: {
+ dir: {
+ name: Proxmox.Utils.directoryText,
+ ipanel: 'DirInputPanel',
+ faIcon: 'folder'
+ },
+ lvm: {
+ name: 'LVM',
+ ipanel: 'LVMInputPanel',
+ faIcon: 'folder'
+ },
+ lvmthin: {
+ name: 'LVM-Thin',
+ ipanel: 'LvmThinInputPanel',
+ faIcon: 'folder'
+ },
+ nfs: {
+ name: 'NFS',
+ ipanel: 'NFSInputPanel',
+ faIcon: 'building'
+ },
+ cifs: {
+ name: 'CIFS',
+ ipanel: 'CIFSInputPanel',
+ faIcon: 'building'
+ },
+ glusterfs: {
+ name: 'GlusterFS',
+ ipanel: 'GlusterFsInputPanel',
+ faIcon: 'building'
+ },
+ iscsi: {
+ name: 'iSCSI',
+ ipanel: 'IScsiInputPanel',
+ faIcon: 'building'
+ },
+ cephfs: {
+ name: 'CephFS',
+ ipanel: 'CephFSInputPanel',
+ faIcon: 'building'
+ },
+ pvecephfs: {
+ name: 'CephFS (PVE)',
+ ipanel: 'CephFSInputPanel',
+ hideAdd: true,
+ faIcon: 'building'
+ },
+ rbd: {
+ name: 'RBD',
+ ipanel: 'RBDInputPanel',
+ faIcon: 'building'
+ },
+ pveceph: {
+ name: 'RBD (PVE)',
+ ipanel: 'RBDInputPanel',
+ hideAdd: true,
+ faIcon: 'building'
+ },
+ zfs: {
+ name: 'ZFS over iSCSI',
+ ipanel: 'ZFSInputPanel',
+ faIcon: 'building'
+ },
+ zfspool: {
+ name: 'ZFS',
+ ipanel: 'ZFSPoolInputPanel',
+ faIcon: 'folder'
+ },
+ drbd: {
+ name: 'DRBD',
+ hideAdd: true
+ }
+ },
+
+ format_storage_type: function(value, md, record) {
+ if (value === 'rbd') {
+ value = (!record || record.get('monhost') ? 'rbd' : 'pveceph');
+ } else if (value === 'cephfs') {
+ value = (!record || record.get('monhost') ? 'cephfs' : 'pvecephfs');
+ }
+
+ var schema = PVE.Utils.storageSchema[value];
+ if (schema) {
+ return schema.name;
+ }
+ return Proxmox.Utils.unknownText;
+ },
+
+ format_ha: function(value) {
+ var text = Proxmox.Utils.noneText;
+
+ if (value.managed) {
+ text = value.state || Proxmox.Utils.noneText;
+
+ text += ', ' + Proxmox.Utils.groupText + ': ';
+ text += value.group || Proxmox.Utils.noneText;
+ }
+
+ return text;
+ },
+
+ format_content_types: function(value) {
+ return value.split(',').sort().map(function(ct) {
+ return PVE.Utils.contentTypes[ct] || ct;
+ }).join(', ');
+ },
+
+ render_storage_content: function(value, metaData, record) {
+ var data = record.data;
+ if (Ext.isNumber(data.channel) &&
+ Ext.isNumber(data.id) &&
+ Ext.isNumber(data.lun)) {
+ return "CH " +
+ Ext.String.leftPad(data.channel,2, '0') +
+ " ID " + data.id + " LUN " + data.lun;
+ }
+ return data.volid.replace(/^.*:(.*\/)?/,'');
+ },
+
+ render_serverity: function (value) {
+ return PVE.Utils.log_severity_hash[value] || value;
+ },
+
+ render_cpu: function(value, metaData, record, rowIndex, colIndex, store) {
+
+ if (!(record.data.uptime && Ext.isNumeric(value))) {
+ return '';
+ }
+
+ var maxcpu = record.data.maxcpu || 1;
+
+ if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
+ return '';
+ }
+
+ var per = value * 100;
+
+ return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU');
+ },
+
+ render_size: function(value, metaData, record, rowIndex, colIndex, store) {
+ /*jslint confusion: true */
+
+ if (!Ext.isNumeric(value)) {
+ return '';
+ }
+
+ return Proxmox.Utils.format_size(value);
+ },
+
+ render_bandwidth: function(value) {
+ if (!Ext.isNumeric(value)) {
+ return '';
+ }
+
+ return Proxmox.Utils.format_size(value) + '/s';
+ },
+
+ render_timestamp_human_readable: function(value) {
+ return Ext.Date.format(new Date(value * 1000), 'l d F Y H:i:s');
+ },
+
+ render_duration: function(value) {
+ if (value === undefined) {
+ return '-';
+ }
+ return PVE.Utils.format_duration_short(value);
+ },
+
+ calculate_mem_usage: function(data) {
+ if (!Ext.isNumeric(data.mem) ||
+ data.maxmem === 0 ||
+ data.uptime < 1) {
+ return -1;
+ }
+
+ return (data.mem / data.maxmem);
+ },
+
+ render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
+ if (!Ext.isNumeric(value) || value === -1) {
+ return '';
+ }
+ if (value > 1 ) {
+ // we got no percentage but bytes
+ var mem = value;
+ var maxmem = record.data.maxmem;
+ if (!record.data.uptime ||
+ maxmem === 0 ||
+ !Ext.isNumeric(mem)) {
+ return '';
+ }
+
+ return ((mem*100)/maxmem).toFixed(1) + " %";
+ }
+ return (value*100).toFixed(1) + " %";
+ },
+
+ render_mem_usage: function(value, metaData, record, rowIndex, colIndex, store) {
+
+ var mem = value;
+ var maxmem = record.data.maxmem;
+
+ if (!record.data.uptime) {
+ return '';
+ }
+
+ if (!(Ext.isNumeric(mem) && maxmem)) {
+ return '';
+ }
+
+ return PVE.Utils.render_size(value);
+ },
+
+ calculate_disk_usage: function(data) {
+
+ if (!Ext.isNumeric(data.disk) ||
+ data.type === 'qemu' ||
+ (data.type === 'lxc' && data.uptime === 0) ||
+ data.maxdisk === 0) {
+ return -1;
+ }
+
+ return (data.disk / data.maxdisk);
+ },
+
+ render_disk_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
+ if (!Ext.isNumeric(value) || value === -1) {
+ return '';
+ }
+
+ return (value * 100).toFixed(1) + " %";
+ },
+
+ render_disk_usage: function(value, metaData, record, rowIndex, colIndex, store) {
+
+ var disk = value;
+ var maxdisk = record.data.maxdisk;
+ var type = record.data.type;
+
+ if (!Ext.isNumeric(disk) ||
+ type === 'qemu' ||
+ maxdisk === 0 ||
+ (type === 'lxc' && record.data.uptime === 0)) {
+ return '';
+ }
+
+ return PVE.Utils.render_size(value);
+ },
+
+ get_object_icon_class: function(type, record) {
+ var status = '';
+ var objType = type;
+
+ if (type === 'type') {
+ // for folder view
+ objType = record.groupbyid;
+ } else if (record.template) {
+ // templates
+ objType = 'template';
+ status = type;
+ } else {
+ // everything else
+ status = record.status + ' ha-' + record.hastate;
+ }
+
+ if (record.lock) {
+ status += ' locked lock-' + record.lock;
+ }
+
+ var defaults = PVE.tree.ResourceTree.typeDefaults[objType];
+ if (defaults && defaults.iconCls) {
+ var retVal = defaults.iconCls + ' ' + status;
+ return retVal;
+ }
+
+ return '';
+ },
+
+ render_resource_type: function(value, metaData, record, rowIndex, colIndex, store) {
+
+ var cls = PVE.Utils.get_object_icon_class(value,record.data);
+
+ var fa = ' ';
+ return fa + value;
+ },
+
+ render_support_level: function(value, metaData, record) {
+ return PVE.Utils.support_level_hash[value] || '-';
+ },
+
+ render_upid: function(value, metaData, record) {
+ var type = record.data.type;
+ var id = record.data.id;
+
+ return Proxmox.Utils.format_task_description(type, id);
+ },
+
+ /* render functions for new status panel */
+
+ render_usage: function(val) {
+ return (val*100).toFixed(2) + '%';
+ },
+
+ render_cpu_usage: function(val, max) {
+ return Ext.String.format(gettext('{0}% of {1}') +
+ ' ' + gettext('CPU(s)'), (val*100).toFixed(2), max);
+ },
+
+ render_size_usage: function(val, max) {
+ if (max === 0) {
+ return gettext('N/A');
+ }
+ return (val*100/max).toFixed(2) + '% '+ '(' +
+ Ext.String.format(gettext('{0} of {1}'),
+ PVE.Utils.render_size(val), PVE.Utils.render_size(max)) + ')';
+ },
+
+ /* this is different for nodes */
+ render_node_cpu_usage: function(value, record) {
+ return PVE.Utils.render_cpu_usage(value, record.cpus);
+ },
+
+ /* this is different for nodes */
+ render_node_size_usage: function(record) {
+ return PVE.Utils.render_size_usage(record.used, record.total);
+ },
+
+ render_optional_url: function(value) {
+ var match;
+ if (value && (match = value.match(/^https?:\/\//)) !== null) {
+ return '' + value + '';
+ }
+ return value;
+ },
+
+ render_san: function(value) {
+ var names = [];
+ if (Ext.isArray(value)) {
+ value.forEach(function(val) {
+ if (!Ext.isNumber(val)) {
+ names.push(val);
+ }
+ });
+ return names.join('
');
+ }
+ return value;
+ },
+
+ render_full_name: function(firstname, metaData, record) {
+ var first = firstname || '';
+ var last = record.data.lastname || '';
+ return Ext.htmlEncode(first + " " + last);
+ },
+
+ render_u2f_error: function(error) {
+ var ErrorNames = {
+ '1': gettext('Other Error'),
+ '2': gettext('Bad Request'),
+ '3': gettext('Configuration Unsupported'),
+ '4': gettext('Device Ineligible'),
+ '5': gettext('Timeout')
+ };
+ return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText;
+ },
+
+ windowHostname: function() {
+ return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match,
+ function(m, addr, offset, original) { return addr; });
+ },
+
+ openDefaultConsoleWindow: function(consoles, vmtype, vmid, nodename, vmname, cmd) {
+ var dv = PVE.Utils.defaultViewer(consoles);
+ PVE.Utils.openConsoleWindow(dv, vmtype, vmid, nodename, vmname, cmd);
+ },
+
+ openConsoleWindow: function(viewer, vmtype, vmid, nodename, vmname, cmd) {
+ // kvm, lxc, shell, upgrade
+
+ if (vmid == undefined && (vmtype === 'kvm' || vmtype === 'lxc')) {
+ throw "missing vmid";
+ }
+
+ if (!nodename) {
+ throw "no nodename specified";
+ }
+
+ if (viewer === 'html5') {
+ PVE.Utils.openVNCViewer(vmtype, vmid, nodename, vmname, cmd);
+ } else if (viewer === 'xtermjs') {
+ Proxmox.Utils.openXtermJsViewer(vmtype, vmid, nodename, vmname, cmd);
+ } else if (viewer === 'vv') {
+ var url;
+ var params = { proxy: PVE.Utils.windowHostname() };
+ if (vmtype === 'kvm') {
+ url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy';
+ PVE.Utils.openSpiceViewer(url, params);
+ } else if (vmtype === 'lxc') {
+ url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy';
+ PVE.Utils.openSpiceViewer(url, params);
+ } else if (vmtype === 'shell') {
+ url = '/nodes/' + nodename + '/spiceshell';
+ PVE.Utils.openSpiceViewer(url, params);
+ } else if (vmtype === 'upgrade') {
+ url = '/nodes/' + nodename + '/spiceshell';
+ params.upgrade = 1;
+ PVE.Utils.openSpiceViewer(url, params);
+ } else if (vmtype === 'cmd') {
+ url = '/nodes/' + nodename + '/spiceshell';
+ params.cmd = cmd;
+ PVE.Utils.openSpiceViewer(url, params);
+ }
+ } else {
+ throw "unknown viewer type";
+ }
+ },
+
+ defaultViewer: function(consoles) {
+
+ var allowSpice, allowXtermjs;
+
+ if (consoles === true) {
+ allowSpice = true;
+ allowXtermjs = true;
+ } else if (typeof consoles === 'object') {
+ allowSpice = consoles.spice;
+ allowXtermjs = !!consoles.xtermjs;
+ }
+ var dv = PVE.VersionInfo.console || 'xtermjs';
+ if (dv === 'vv' && !allowSpice) {
+ dv = (allowXtermjs) ? 'xtermjs' : 'html5';
+ } else if (dv === 'xtermjs' && !allowXtermjs) {
+ dv = (allowSpice) ? 'vv' : 'html5';
+ }
+
+ return dv;
+ },
+
+ openVNCViewer: function(vmtype, vmid, nodename, vmname, cmd) {
+ var url = Ext.Object.toQueryString({
+ console: vmtype, // kvm, lxc, upgrade or shell
+ novnc: 1,
+ vmid: vmid,
+ vmname: vmname,
+ node: nodename,
+ resize: 'off',
+ cmd: cmd
+ });
+ var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427");
+ if (nw) {
+ nw.focus();
+ }
+ },
+
+ openSpiceViewer: function(url, params){
+
+ var downloadWithName = function(uri, name) {
+ var link = Ext.DomHelper.append(document.body, {
+ tag: 'a',
+ href: uri,
+ css : 'display:none;visibility:hidden;height:0px;'
+ });
+
+ // Note: we need to tell android the correct file name extension
+ // but we do not set 'download' tag for other environments, because
+ // It can have strange side effects (additional user prompt on firefox)
+ var andriod = navigator.userAgent.match(/Android/i) ? true : false;
+ if (andriod) {
+ link.download = name;
+ }
+
+ if (link.fireEvent) {
+ link.fireEvent('onclick');
+ } else {
+ var evt = document.createEvent("MouseEvents");
+ evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+ link.dispatchEvent(evt);
+ }
+ };
+
+ Proxmox.Utils.API2Request({
+ url: url,
+ params: params,
+ method: 'POST',
+ failure: function(response, opts){
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, opts){
+ var raw = "[virt-viewer]\n";
+ Ext.Object.each(response.result.data, function(k, v) {
+ raw += k + "=" + v + "\n";
+ });
+ var url = 'data:application/x-virt-viewer;charset=UTF-8,' +
+ encodeURIComponent(raw);
+
+ downloadWithName(url, "pve-spice.vv");
+ }
+ });
+ },
+
+ openTreeConsole: function(tree, record, item, index, e) {
+ e.stopEvent();
+ var nodename = record.data.node;
+ var vmid = record.data.vmid;
+ var vmname = record.data.name;
+ if (record.data.type === 'qemu' && !record.data.template) {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, opts) {
+ let conf = response.result.data;
+ var consoles = {
+ spice: !!conf.spice,
+ xtermjs: !!conf.serial,
+ };
+ PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname);
+ }
+ });
+ } else if (record.data.type === 'lxc' && !record.data.template) {
+ PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname);
+ }
+ },
+
+ // test automation helper
+ call_menu_handler: function(menu, text) {
+
+ var list = menu.query('menuitem');
+
+ Ext.Array.each(list, function(item) {
+ if (item.text === text) {
+ if (item.handler) {
+ item.handler();
+ return 1;
+ } else {
+ return undefined;
+ }
+ }
+ });
+ },
+
+ createCmdMenu: function(v, record, item, index, event) {
+ event.stopEvent();
+ if (!(v instanceof Ext.tree.View)) {
+ v.select(record);
+ }
+ var menu;
+ var template = !!record.data.template;
+ var type = record.data.type;
+
+ if (template) {
+ if (type === 'qemu' || type == 'lxc') {
+ menu = Ext.create('PVE.menu.TemplateMenu', {
+ pveSelNode: record
+ });
+ }
+ } else if (type === 'qemu' ||
+ type === 'lxc' ||
+ type === 'node') {
+ menu = Ext.create('PVE.' + type + '.CmdMenu', {
+ pveSelNode: record,
+ nodename: record.data.node
+ });
+ } else {
+ return;
+ }
+
+ menu.showAt(event.getXY());
+ return menu;
+ },
+
+ // helper for deleting field which are set to there default values
+ delete_if_default: function(values, fieldname, default_val, create) {
+ if (values[fieldname] === '' || values[fieldname] === default_val) {
+ if (!create) {
+ if (values['delete']) {
+ values['delete'] += ',' + fieldname;
+ } else {
+ values['delete'] = fieldname;
+ }
+ }
+
+ delete values[fieldname];
+ }
+ },
+
+ loadSSHKeyFromFile: function(file, callback) {
+ // ssh-keygen produces 740 bytes for an average 4096 bit rsa key, with
+ // a user@host comment, 1420 for 8192 bits; current max is 16kbit
+ // assume: 740*8 for max. 32kbit (5920 byte file)
+ // round upwards to nearest nice number => 8192 bytes, leaves lots of comment space
+ if (file.size > 8192) {
+ Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
+ return;
+ }
+ /*global
+ FileReader
+ */
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ callback(evt.target.result);
+ };
+ reader.readAsText(file);
+ },
+
+ bus_counts: { ide: 4, sata: 6, scsi: 16, virtio: 16 },
+
+ // types is either undefined (all busses), an array of busses, or a single bus
+ forEachBus: function(types, func) {
+ var busses = Object.keys(PVE.Utils.bus_counts);
+ var i, j, count, cont;
+
+ if (Ext.isArray(types)) {
+ busses = types;
+ } else if (Ext.isDefined(types)) {
+ busses = [ types ];
+ }
+
+ // check if we only have valid busses
+ for (i = 0; i < busses.length; i++) {
+ if (!PVE.Utils.bus_counts[busses[i]]) {
+ throw "invalid bus: '" + busses[i] + "'";
+ }
+ }
+
+ for (i = 0; i < busses.length; i++) {
+ count = PVE.Utils.bus_counts[busses[i]];
+ for (j = 0; j < count; j++) {
+ cont = func(busses[i], j);
+ if (!cont && cont !== undefined) {
+ return;
+ }
+ }
+ }
+ },
+
+ mp_counts: { mps: 256, unused: 256 },
+
+ forEachMP: function(func, includeUnused) {
+ var i, cont;
+ for (i = 0; i < PVE.Utils.mp_counts.mps; i++) {
+ cont = func('mp', i);
+ if (!cont && cont !== undefined) {
+ return;
+ }
+ }
+
+ if (!includeUnused) {
+ return;
+ }
+
+ for (i = 0; i < PVE.Utils.mp_counts.unused; i++) {
+ cont = func('unused', i);
+ if (!cont && cont !== undefined) {
+ return;
+ }
+ }
+ },
+
+ cleanEmptyObjectKeys: function (obj) {
+ var propName;
+ for (propName in obj) {
+ if (obj.hasOwnProperty(propName)) {
+ if (obj[propName] === null || obj[propName] === undefined) {
+ delete obj[propName];
+ }
+ }
+ }
+ },
+
+ handleStoreErrorOrMask: function(me, store, regex, callback) {
+
+ me.mon(store, 'load', function (proxy, response, success, operation) {
+
+ if (success) {
+ Proxmox.Utils.setErrorMask(me, false);
+ return;
+ }
+ var msg;
+
+ if (operation.error.statusText) {
+ if (operation.error.statusText.match(regex)) {
+ callback(me, operation.error);
+ return;
+ } else {
+ msg = operation.error.statusText + ' (' + operation.error.status + ')';
+ }
+ } else {
+ msg = gettext('Connection error');
+ }
+ Proxmox.Utils.setErrorMask(me, msg);
+ });
+ },
+
+ showCephInstallOrMask: function(container, msg, nodename, callback){
+ var regex = new RegExp("not (installed|initialized)", "i");
+ if (msg.match(regex)) {
+ if (Proxmox.UserName === 'root@pam') {
+ container.el.mask();
+ if (!container.down('pveCephInstallWindow')){
+ var isInstalled = msg.match(/not initialized/i) ? true : false;
+ var win = Ext.create('PVE.ceph.Install', {
+ nodename: nodename
+ });
+ win.getViewModel().set('isInstalled', isInstalled);
+ container.add(win);
+ win.show();
+ callback(win);
+ }
+ } else {
+ container.mask(Ext.String.format(gettext('{0} not installed.') +
+ ' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+},
+
+ singleton: true,
+ constructor: function() {
+ var me = this;
+ Ext.apply(me, me.utilities);
+ }
+
+});
+
+// ExtJS related things
+
+Proxmox.Utils.toolkit = 'extjs';
+
+// custom PVE specific VTypes
+Ext.apply(Ext.form.field.VTypes, {
+
+ QemuStartDate: function(v) {
+ return (/^(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)$/).test(v);
+ },
+ QemuStartDateText: gettext('Format') + ': "now" or "2006-06-17T16:01:21" or "2006-06-17"',
+ IP64AddressList: function(v) {
+ var list = v.split(/[\ \,\;]+/);
+ var i;
+ for (i = 0; i < list.length; i++) {
+ if (list[i] == '') {
+ continue;
+ }
+
+ if (!Proxmox.Utils.IP64_match.test(list[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+ IP64AddressListText: gettext('Example') + ': 192.168.1.1,192.168.1.2',
+ IP64AddressListMask: /[A-Fa-f0-9\,\:\.\;\ ]/
+});
+
+Ext.define('PVE.form.field.Display', {
+ override: 'Ext.form.field.Display',
+
+ setSubmitValue: function(value) {
+ // do nothing, this is only to allow generalized bindings for the:
+ // `me.isCreate ? 'textfield' : 'displayfield'` cases we have.
+ }
+});
+// Some configuration values are complex strings -
+// so we need parsers/generators for them.
+
+Ext.define('PVE.Parser', { statics: {
+
+ // this class only contains static functions
+
+ parseACME: function(value) {
+ if (!value) {
+ 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;
+ }
+ });
+
+ if (errors || !res) {
+ return;
+ }
+
+ return res;
+ },
+
+ parseBoolean: function(value, default_value) {
+ if (!Ext.isDefined(value)) {
+ return default_value;
+ }
+ value = value.toLowerCase();
+ return value === '1' ||
+ value === 'on' ||
+ value === 'yes' ||
+ value === 'true';
+ },
+
+ parsePropertyString: function(value, defaultKey) {
+ var res = {},
+ error;
+
+ 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])) {
+ error = 'defaultKey may be only defined once in propertyString';
+ return false; // break
+ }
+ res[defaultKey] = kv[0];
+ } else {
+ error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
+ return false; // break
+ }
+ });
+
+ if (error !== undefined) {
+ console.error(error);
+ return;
+ }
+
+ return res;
+ },
+
+ printPropertyString: function(data, defaultKey) {
+ var stringparts = [],
+ gotDefaultKeyVal = false,
+ defaultKeyVal;
+
+ Ext.Object.each(data, function(key, value) {
+ if (defaultKey !== undefined && key === defaultKey) {
+ gotDefaultKeyVal = true;
+ defaultKeyVal = value;
+ } else {
+ stringparts.push(key + '=' + value);
+ }
+ });
+
+ stringparts = stringparts.sort();
+ if (gotDefaultKeyVal) {
+ stringparts.unshift(defaultKeyVal);
+ }
+
+ return stringparts.join(',');
+ },
+
+ parseQemuNetwork: function(key, value) {
+ if (!(key && value)) {
+ 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(/^(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) {
+ 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) {
+ res.rate = match_res[1];
+ } else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) {
+ res.tag = match_res[1];
+ } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
+ res.firewall = match_res[1];
+ } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
+ res.disconnect = match_res[1];
+ } else if ((match_res = p.match(/^queues=(\d+)$/)) !== null) {
+ res.queues = match_res[1];
+ } else if ((match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null) {
+ res.trunks = match_res[1];
+ } else {
+ errors = true;
+ return false; // break
+ }
+ });
+
+ if (errors || !res.model) {
+ return;
+ }
+
+ return res;
+ },
+
+ printQemuNetwork: function(net) {
+
+ var netstr = net.model;
+ if (net.macaddr) {
+ netstr += "=" + net.macaddr;
+ }
+ if (net.bridge) {
+ netstr += ",bridge=" + net.bridge;
+ if (net.tag) {
+ netstr += ",tag=" + net.tag;
+ }
+ if (net.firewall) {
+ netstr += ",firewall=" + net.firewall;
+ }
+ }
+ if (net.rate) {
+ netstr += ",rate=" + net.rate;
+ }
+ if (net.queues) {
+ netstr += ",queues=" + net.queues;
+ }
+ if (net.disconnect) {
+ netstr += ",link_down=" + net.disconnect;
+ }
+ if (net.trunks) {
+ netstr += ",trunks=" + net.trunks;
+ }
+ return netstr;
+ },
+
+ parseQemuDrive: function(key, value) {
+ if (!(key && value)) {
+ return;
+ }
+
+ var res = {};
+
+ var match_res = key.match(/^([a-z]+)(\d+)$/);
+ if (!match_res) {
+ return;
+ }
+ res['interface'] = match_res[1];
+ res.index = match_res[2];
+
+ var errors = false;
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+ var match_res = p.match(/^([a-z_]+)=(\S+)$/);
+ if (!match_res) {
+ if (!p.match(/\=/)) {
+ res.file = p;
+ return; // continue
+ }
+ errors = true;
+ return false; // break
+ }
+ var k = match_res[1];
+ if (k === 'volume') {
+ k = 'file';
+ }
+
+ if (Ext.isDefined(res[k])) {
+ errors = true;
+ return false; // break
+ }
+
+ var v = match_res[2];
+
+ if (k === 'cache' && v === 'off') {
+ v = 'none';
+ }
+
+ res[k] = v;
+ });
+
+ if (errors || !res.file) {
+ return;
+ }
+
+ return res;
+ },
+
+ printQemuDrive: function(drive) {
+
+ var drivestr = drive.file;
+
+ Ext.Object.each(drive, function(key, value) {
+ if (!Ext.isDefined(value) || key === 'file' ||
+ key === 'index' || key === 'interface') {
+ return; // continue
+ }
+ drivestr += ',' + key + '=' + value;
+ });
+
+ return drivestr;
+ },
+
+ parseIPConfig: function(key, value) {
+ if (!(key && value)) {
+ 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(/^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) {
+ 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];
+ }
+ });
+
+ if (errors || !data.ifname) {
+ errors = true;
+ return false; // break
+ }
+
+ data.raw = item;
+
+ res[data.ifname] = data;
+ });
+
+ return errors ? undefined: 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(';');
+ },
+
+ parseLxcNetwork: function(value) {
+ if (!value) {
+ return;
+ }
+
+ var data = {};
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+ var 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 {
+ // todo: simply ignore errors ?
+ return; // continue
+ }
+ });
+
+ 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(',');
+ },
+
+ parseLxcMountPoint: function(value) {
+ if (!value) {
+ return;
+ }
+
+ var res = {};
+
+ var errors = false;
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+ var match_res = p.match(/^([a-z_]+)=(.+)$/);
+ if (!match_res) {
+ if (!p.match(/\=/)) {
+ res.file = p;
+ return; // continue
+ }
+ errors = true;
+ return false; // break
+ }
+ var k = match_res[1];
+ if (k === 'volume') {
+ k = 'file';
+ }
+
+ if (Ext.isDefined(res[k])) {
+ errors = true;
+ return false; // break
+ }
+
+ var v = match_res[2];
+
+ res[k] = v;
+ });
+
+ if (errors || !res.file) {
+ return;
+ }
+
+ var m = res.file.match(/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):/i);
+ if (m) {
+ res.storage = m[1];
+ res.type = 'volume';
+ } else if (res.file.match(/^\/dev\//)) {
+ res.type = 'device';
+ } else {
+ res.type = 'bind';
+ }
+
+ return res;
+ },
+
+ 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
+ }
+ drivestr += ',' + key + '=' + value;
+ });
+
+ return drivestr;
+ },
+
+ parseStartup: function(value) {
+ if (value === undefined) {
+ 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(/^(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
+ }
+ });
+
+ if (errors) {
+ return;
+ }
+
+ return res;
+ },
+
+ printStartup: function(startup) {
+ var arr = [];
+ if (startup.order !== undefined && startup.order !== '') {
+ arr.push('order=' + startup.order);
+ }
+ if (startup.up !== undefined && startup.up !== '') {
+ arr.push('up=' + startup.up);
+ }
+ if (startup.down !== undefined && startup.down !== '') {
+ arr.push('down=' + startup.down);
+ }
+
+ return arr.join(',');
+ },
+
+ parseQemuSmbios1: function(value) {
+ var res = value.split(',').reduce(function (accumulator, currentValue) {
+ var splitted = currentValue.split(new RegExp("=(.+)"));
+ accumulator[splitted[0]] = splitted[1];
+ return accumulator;
+ }, {});
+
+ if (PVE.Parser.parseBoolean(res.base64, false)) {
+ Ext.Object.each(res, function(key, value) {
+ if (key === 'uuid') { return; }
+ res[key] = Ext.util.Base64.decode(value);
+ });
+ }
+
+ return res;
+ },
+
+ printQemuSmbios1: function(data) {
+
+ var datastr = '';
+ var base64 = false;
+ Ext.Object.each(data, function(key, value) {
+ if (value === '') { return; }
+ if (key === 'uuid') {
+ datastr += (datastr !== '' ? ',' : '') + key + '=' + value;
+ } else {
+ // values should be base64 encoded from now on, mark config strings correspondingly
+ if (!base64) {
+ base64 = true;
+ datastr += (datastr !== '' ? ',' : '') + 'base64=1';
+ }
+ datastr += (datastr !== '' ? ',' : '') + key + '=' + Ext.util.Base64.encode(value);
+ }
+ });
+
+ return datastr;
+ },
+
+ parseTfaConfig: function(value) {
+ var res = {};
+
+ Ext.Array.each(value.split(','), function(p) {
+ var kva = p.split('=', 2);
+ res[kva[0]] = kva[1];
+ });
+
+ return res;
+ },
+
+ parseTfaType: function(value) {
+ /*jslint confusion: true*/
+ var match;
+ if (!value || !value.length) {
+ return undefined;
+ } else if (value === 'x!oath') {
+ return 'totp';
+ } else if (!!(match = value.match(/^x!(.+)$/))) {
+ return match[1];
+ } else {
+ return 1;
+ }
+ },
+
+ parseQemuCpu: function(value) {
+ if (!value) {
+ return {};
+ }
+
+ var res = {};
+
+ var errors = false;
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+
+ if (!p.match(/\=/)) {
+ if (Ext.isDefined(res.cpu)) {
+ errors = true;
+ return false; // break
+ }
+ res.cputype = p;
+ return; // continue
+ }
+
+ var match_res = p.match(/^([a-z_]+)=(\S+)$/);
+ if (!match_res) {
+ errors = true;
+ return false; // break
+ }
+
+ var k = match_res[1];
+ if (Ext.isDefined(res[k])) {
+ errors = true;
+ return false; // break
+ }
+
+ res[k] = match_res[2];
+ });
+
+ if (errors || !res.cputype) {
+ return;
+ }
+
+ return res;
+ },
+
+ printQemuCpu: function(cpu) {
+ var cpustr = cpu.cputype;
+ var optstr = '';
+
+ Ext.Object.each(cpu, function(key, value) {
+ if (!Ext.isDefined(value) || key === 'cputype') {
+ return; // continue
+ }
+ optstr += ',' + key + '=' + value;
+ });
+
+ if (!cpustr) {
+ if (optstr) {
+ return 'kvm64' + optstr;
+ }
+ return;
+ }
+
+ return cpustr + optstr;
+ },
+
+ 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+)$/;
+
+ var m = key.match(keyre);
+ if (!m) {
+ return null;
+ }
+ if (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]
+ };
+ }
+ if (m[2].match(typere)) {
+ return {
+ options: m[1],
+ type: m[2],
+ key: m[3],
+ comment: m[4]
+ };
+ }
+ return null;
+ }
+}});
+/* This state provider keeps part of the state inside
+ * the browser history.
+ *
+ * We compress (shorten) url using dictionary based compression
+ * i.e. use column separated list instead of url encoded hash:
+ * #v\d* version/format
+ * := indicates string values
+ * :\d+ lookup value in dictionary hash
+ * #v1:=value1:5:=value2:=value3:...
+*/
+
+Ext.define('PVE.StateProvider', {
+ extend: 'Ext.state.LocalStorageProvider',
+
+ // private
+ setHV: function(name, newvalue, fireEvents) {
+ var me = this;
+
+ var changes = false;
+ var oldtext = Ext.encode(me.UIState[name]);
+ var newtext = Ext.encode(newvalue);
+ if (newtext != oldtext) {
+ changes = true;
+ me.UIState[name] = newvalue;
+ //console.log("changed old " + name + " " + oldtext);
+ //console.log("changed new " + name + " " + newtext);
+ if (fireEvents) {
+ me.fireEvent("statechange", me, name, { value: newvalue });
+ }
+ }
+ return changes;
+ },
+
+ // private
+ hslist: [
+ // order is important for notifications
+ // [ name, default ]
+ ['view', 'server'],
+ ['rid', 'root'],
+ ['ltab', 'tasks'],
+ ['nodetab', ''],
+ ['storagetab', ''],
+ ['pooltab', ''],
+ ['kvmtab', ''],
+ ['lxctab', ''],
+ ['dctab', '']
+ ],
+
+ hprefix: 'v1',
+
+ compDict: {
+ cloudinit: 52,
+ replication: 51,
+ system: 50,
+ monitor: 49,
+ 'ha-fencing': 48,
+ 'ha-groups': 47,
+ 'ha-resources': 46,
+ 'ceph-log': 45,
+ 'ceph-crushmap':44,
+ 'ceph-pools': 43,
+ 'ceph-osdtree': 42,
+ 'ceph-disklist': 41,
+ 'ceph-monlist': 40,
+ 'ceph-config': 39,
+ ceph: 38,
+ 'firewall-fwlog': 37,
+ 'firewall-options': 36,
+ 'firewall-ipset': 35,
+ 'firewall-aliases': 34,
+ 'firewall-sg': 33,
+ firewall: 32,
+ apt: 31,
+ members: 30,
+ snapshot: 29,
+ ha: 28,
+ support: 27,
+ pools: 26,
+ syslog: 25,
+ ubc: 24,
+ initlog: 23,
+ openvz: 22,
+ backup: 21,
+ resources: 20,
+ content: 19,
+ root: 18,
+ domains: 17,
+ roles: 16,
+ groups: 15,
+ users: 14,
+ time: 13,
+ dns: 12,
+ network: 11,
+ services: 10,
+ options: 9,
+ console: 8,
+ hardware: 7,
+ permissions: 6,
+ summary: 5,
+ tasks: 4,
+ clog: 3,
+ storage: 2,
+ folder: 1,
+ server: 0
+ },
+
+ decodeHToken: function(token) {
+ var me = this;
+
+ var state = {};
+ if (!token) {
+ Ext.Array.each(me.hslist, function(rec) {
+ state[rec[0]] = rec[1];
+ });
+ return state;
+ }
+
+ // return Ext.urlDecode(token);
+
+ var items = token.split(':');
+ var prefix = items.shift();
+
+ if (prefix != me.hprefix) {
+ return me.decodeHToken();
+ }
+
+ Ext.Array.each(me.hslist, function(rec) {
+ var value = items.shift();
+ if (value) {
+ if (value[0] === '=') {
+ value = decodeURIComponent(value.slice(1));
+ } else {
+ Ext.Object.each(me.compDict, function(key, cv) {
+ if (value == cv) {
+ value = key;
+ return false;
+ }
+ });
+ }
+ }
+ state[rec[0]] = value;
+ });
+
+ return state;
+ },
+
+ encodeHToken: function(state) {
+ var me = this;
+
+ // return Ext.urlEncode(state);
+
+ var ctoken = me.hprefix;
+ Ext.Array.each(me.hslist, function(rec) {
+ var value = state[rec[0]];
+ if (!Ext.isDefined(value)) {
+ value = rec[1];
+ }
+ value = encodeURIComponent(value);
+ if (!value) {
+ ctoken += ':';
+ } else {
+ var comp = me.compDict[value];
+ if (Ext.isDefined(comp)) {
+ ctoken += ":" + comp;
+ } else {
+ ctoken += ":=" + value;
+ }
+ }
+ });
+
+ return ctoken;
+ },
+
+ constructor: function(config){
+ var me = this;
+
+ me.callParent([config]);
+
+ me.UIState = me.decodeHToken(); // set default
+
+ var history_change_cb = function(token) {
+ //console.log("HC " + token);
+ if (!token) {
+ var res = window.confirm(gettext('Are you sure you want to navigate away from this page?'));
+ if (res){
+ // process text value and close...
+ Ext.History.back();
+ } else {
+ Ext.History.forward();
+ }
+ return;
+ }
+
+ var newstate = me.decodeHToken(token);
+ Ext.Array.each(me.hslist, function(rec) {
+ if (typeof newstate[rec[0]] == "undefined") {
+ return;
+ }
+ me.setHV(rec[0], newstate[rec[0]], true);
+ });
+ };
+
+ var start_token = Ext.History.getToken();
+ if (start_token) {
+ history_change_cb(start_token);
+ } else {
+ var htext = me.encodeHToken(me.UIState);
+ Ext.History.add(htext);
+ }
+
+ Ext.History.on('change', history_change_cb);
+ },
+
+ get: function(name, defaultValue){
+ /*jslint confusion: true */
+ var me = this;
+ var data;
+
+ if (typeof me.UIState[name] != "undefined") {
+ data = { value: me.UIState[name] };
+ } else {
+ data = me.callParent(arguments);
+ if (!data && name === 'GuiCap') {
+ data = { vms: {}, storage: {}, access: {}, nodes: {}, dc: {} };
+ }
+ }
+
+ //console.log("GET " + name + " " + Ext.encode(data));
+ return data;
+ },
+
+ clear: function(name){
+ var me = this;
+
+ if (typeof me.UIState[name] != "undefined") {
+ me.UIState[name] = null;
+ }
+
+ me.callParent(arguments);
+ },
+
+ set: function(name, value, fireevent){
+ var me = this;
+
+ //console.log("SET " + name + " " + Ext.encode(value));
+ if (typeof me.UIState[name] != "undefined") {
+ var newvalue = value ? value.value : null;
+ if (me.setHV(name, newvalue, fireevent)) {
+ var htext = me.encodeHToken(me.UIState);
+ Ext.History.add(htext);
+ }
+ } else {
+ me.callParent(arguments);
+ }
+ }
+});
+Ext.define('PVE.menu.Item', {
+ extend: 'Ext.menu.Item',
+ alias: 'widget.pveMenuItem',
+
+ // set to wrap the handler callback in a confirm dialog showing this text
+ confirmMsg: false,
+
+ // set to focus 'No' instead of 'Yes' button and show a warning symbol
+ dangerous: false,
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.handler) {
+ me.setHandler(me.handler, me.scope);
+ }
+
+ me.callParent();
+ },
+
+ setHandler: function(fn, scope) {
+ var me = this;
+ me.scope = scope;
+ me.handler = function(button, e) {
+ var rec, msg;
+ if (me.confirmMsg) {
+ msg = me.confirmMsg;
+ Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
+ msg: msg,
+ buttons: Ext.Msg.YESNO,
+ defaultFocus: me.dangerous ? 'no' : 'yes',
+ callback: function(btn) {
+ if (btn === 'yes') {
+ Ext.callback(fn, me.scope, [me, e], 0, me);
+ }
+ }
+ });
+ } else {
+ Ext.callback(fn, me.scope, [me, e], 0, me);
+ }
+ };
+ }
+});
+Ext.define('PVE.menu.TemplateMenu', {
+ extend: 'Ext.menu.Menu',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var guestType = me.pveSelNode.data.type;
+ if (guestType !== 'qemu' && guestType != 'lxc') {
+ throw "invalid guest type";
+ }
+
+ var vmname = me.pveSelNode.data.name;
+
+ var template = me.pveSelNode.data.template;
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + nodename + '/' + guestType + '/' + vmid + "/status/" + cmd,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ };
+
+ me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + vmid;
+
+ me.items = [
+ {
+ text: gettext('Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ handler: function() {
+ var win = Ext.create('PVE.window.Migrate', {
+ vmtype: guestType,
+ nodename: nodename,
+ vmid: vmid
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ handler: function() {
+ var win = Ext.create('PVE.window.Clone', {
+ nodename: nodename,
+ guestType: guestType,
+ vmid: vmid,
+ isTemplate: template
+ });
+ win.show();
+ }
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.button.ConsoleButton', {
+ extend: 'Ext.button.Split',
+ alias: 'widget.pveConsoleButton',
+
+ consoleType: 'shell', // one of 'shell', 'kvm', 'lxc', 'upgrade', 'cmd'
+
+ cmd: undefined,
+
+ consoleName: undefined,
+
+ iconCls: 'fa fa-terminal',
+
+ enableSpice: true,
+ enableXtermjs: true,
+
+ nodename: undefined,
+
+ vmid: 0,
+
+ text: gettext('Console'),
+
+ setEnableSpice: function(enable){
+ var me = this;
+
+ me.enableSpice = enable;
+ me.down('#spicemenu').setDisabled(!enable);
+ },
+
+ setEnableXtermJS: function(enable){
+ var me = this;
+
+ me.enableXtermjs = enable;
+ me.down('#xtermjs').setDisabled(!enable);
+ },
+
+ handler: function() {
+ var me = this;
+ var consoles = {
+ spice: me.enableSpice,
+ xtermjs: me.enableXtermjs
+ };
+ PVE.Utils.openDefaultConsoleWindow(consoles, me.consoleType, me.vmid,
+ me.nodename, me.consoleName, me.cmd);
+ },
+
+ menu: [
+ {
+ xtype:'menuitem',
+ text: 'noVNC',
+ iconCls: 'pve-itype-icon-novnc',
+ type: 'html5',
+ handler: function(button) {
+ var me = this.up('button');
+ PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
+ }
+ },
+ {
+ xterm: 'menuitem',
+ itemId: 'spicemenu',
+ text: 'SPICE',
+ type: 'vv',
+ iconCls: 'pve-itype-icon-virt-viewer',
+ handler: function(button) {
+ var me = this.up('button');
+ PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
+ }
+ },
+ {
+ text: 'xterm.js',
+ itemId: 'xtermjs',
+ iconCls: 'pve-itype-icon-xtermjs',
+ type: 'xtermjs',
+ handler: function(button) {
+ var me = this.up('button');
+ PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
+ }
+ }
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.callParent();
+ }
+});
+/* Button features:
+ * - observe selection changes to enable/disable the button using enableFn()
+ * - pop up confirmation dialog using confirmMsg()
+ *
+ * does this for the button and every menu item
+ */
+Ext.define('PVE.button.Split', {
+ extend: 'Ext.button.Split',
+ alias: 'widget.pveSplitButton',
+
+ // the selection model to observe
+ selModel: undefined,
+
+ // if 'false' handler will not be called (button disabled)
+ enableFn: function(record) { },
+
+ // function(record) or text
+ confirmMsg: false,
+
+ // take special care in confirm box (select no as default).
+ dangerous: false,
+
+ handlerWrapper: function(button, event) {
+ var me = this;
+ var rec, msg;
+ if (me.selModel) {
+ rec = me.selModel.getSelection()[0];
+ if (!rec || (me.enableFn(rec) === false)) {
+ return;
+ }
+ }
+
+ if (me.confirmMsg) {
+ msg = me.confirmMsg;
+ // confirMsg can be boolean or function
+ /*jslint confusion: true*/
+ if (Ext.isFunction(me.confirmMsg)) {
+ msg = me.confirmMsg(rec);
+ }
+ /*jslint confusion: false*/
+ Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
+ msg: msg,
+ buttons: Ext.Msg.YESNO,
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ me.realHandler(button, event, rec);
+ }
+ });
+ } else {
+ me.realHandler(button, event, rec);
+ }
+ },
+
+ initComponent: function() {
+ /*jslint confusion: true */
+
+ var me = this;
+
+ if (me.handler) {
+ me.realHandler = me.handler;
+ me.handler = me.handlerWrapper;
+ }
+
+ if (me.menu && me.menu.items) {
+ me.menu.items.forEach(function(item) {
+ if (item.handler) {
+ item.realHandler = item.handler;
+ item.handler = me.handlerWrapper;
+ }
+
+ if (item.selModel) {
+ me.mon(item.selModel, "selectionchange", function() {
+ var rec = item.selModel.getSelection()[0];
+ if (!rec || (item.enableFn(rec) === false )) {
+ item.setDisabled(true);
+ } else {
+ item.setDisabled(false);
+ }
+ });
+ }
+ });
+ }
+
+ me.callParent();
+
+ if (me.selModel) {
+
+ me.mon(me.selModel, "selectionchange", function() {
+ var rec = me.selModel.getSelection()[0];
+ if (!rec || (me.enableFn(rec) === false)) {
+ me.setDisabled(true);
+ } else {
+ me.setDisabled(false);
+ }
+ });
+ }
+ }
+});
+Ext.define('PVE.controller.StorageEdit', {
+ extend: 'Ext.app.ViewController',
+ alias: 'controller.storageEdit',
+ control: {
+ 'field[name=content]': {
+ change: function(field, value) {
+ var hasBackups = Ext.Array.contains(value, 'backup');
+ var maxfiles = this.lookupReference('maxfiles');
+ if (!maxfiles) {
+ return;
+ }
+
+ if (!hasBackups) {
+ // clear values which will never be submitted
+ maxfiles.reset();
+ }
+ maxfiles.setDisabled(!hasBackups);
+ }
+ }
+ }
+});
+Ext.define('PVE.qemu.CmdMenu', {
+ extend: 'Ext.menu.Menu',
+
+ showSeparator: false,
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var vmname = me.pveSelNode.data.name;
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + nodename + '/qemu/' + vmid + "/status/" + cmd,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ };
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var running = false;
+ var stopped = true;
+ var suspended = false;
+ var standalone = PVE.data.ResourceStore.getNodes().length < 2;
+
+ switch (me.pveSelNode.data.status) {
+ case 'running':
+ running = true;
+ stopped = false;
+ break;
+ case 'suspended':
+ stopped = false;
+ suspended = true;
+ break;
+ case 'paused':
+ stopped = false;
+ suspended = true;
+ break;
+ default: break;
+ }
+
+ me.title = "VM " + vmid;
+
+ me.items = [
+ {
+ text: gettext('Start'),
+ iconCls: 'fa fa-fw fa-play',
+ hidden: running || suspended,
+ disabled: running || suspended,
+ handler: function() {
+ vm_command('start');
+ }
+ },
+ {
+ text: gettext('Pause'),
+ iconCls: 'fa fa-fw fa-pause',
+ hidden: stopped || suspended,
+ disabled: stopped || suspended,
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('qmpause', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ vm_command('suspend');
+ });
+ }
+ },
+ {
+ text: gettext('Hibernate'),
+ iconCls: 'fa fa-fw fa-download',
+ hidden: stopped || suspended,
+ disabled: stopped || suspended,
+ tooltip: gettext('Suspend to disk'),
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('qmsuspend', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ vm_command('suspend', { todisk: 1 });
+ });
+ }
+ },
+ {
+ text: gettext('Resume'),
+ iconCls: 'fa fa-fw fa-play',
+ hidden: !suspended,
+ handler: function() {
+ vm_command('resume');
+ }
+ },
+ {
+ text: gettext('Shutdown'),
+ iconCls: 'fa fa-fw fa-power-off',
+ disabled: stopped || suspended,
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('qmshutdown', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ vm_command('shutdown');
+ });
+ }
+ },
+ {
+ text: gettext('Stop'),
+ iconCls: 'fa fa-fw fa-stop',
+ disabled: stopped,
+ tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('qmstop', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ vm_command("stop");
+ });
+ }
+ },
+ {
+ xtype: 'menuseparator',
+ hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone']
+ },
+ {
+ text: gettext('Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ hidden: standalone || !caps.vms['VM.Migrate'],
+ handler: function() {
+ var win = Ext.create('PVE.window.Migrate', {
+ vmtype: 'qemu',
+ nodename: nodename,
+ vmid: vmid
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: !caps.vms['VM.Clone'],
+ handler: function() {
+ PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'qemu');
+ }
+ },
+ {
+ text: gettext('Convert to template'),
+ iconCls: 'fa fa-fw fa-file-o',
+ hidden: !caps.vms['VM.Allocate'],
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('qmtemplate', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/qemu/' + vmid + '/template',
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ });
+ }
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Console'),
+ iconCls: 'fa fa-fw fa-terminal',
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, opts) {
+ var allowSpice = response.result.data.spice;
+ var allowXtermjs = response.result.data.serial;
+ var consoles = {
+ spice: allowSpice,
+ xtermjs: allowXtermjs
+ };
+ PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname);
+ }
+ });
+ }
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.lxc.CmdMenu', {
+ extend: 'Ext.menu.Menu',
+
+ showSeparator: false,
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no CT ID specified";
+ }
+ var vmname = me.pveSelNode.data.name;
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + nodename + '/lxc/' + vmid + "/status/" + cmd,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ };
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var running = false;
+ var stopped = true;
+ var suspended = false;
+ var standalone = PVE.data.ResourceStore.getNodes().length < 2;
+
+ switch (me.pveSelNode.data.status) {
+ case 'running':
+ running = true;
+ stopped = false;
+ break;
+ case 'paused':
+ stopped = false;
+ suspended = true;
+ break;
+ default: break;
+ }
+
+ me.title = 'CT ' + vmid;
+
+ me.items = [
+ {
+ text: gettext('Start'),
+ iconCls: 'fa fa-fw fa-play',
+ disabled: running,
+ handler: function() {
+ vm_command('start');
+ }
+ },
+// {
+// text: gettext('Suspend'),
+// iconCls: 'fa fa-fw fa-pause',
+// hidde: suspended,
+// disabled: stopped || suspended,
+// handler: function() {
+// var msg = Proxmox.Utils.format_task_description('vzsuspend', vmid);
+// Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+// if (btn !== 'yes') {
+// return;
+// }
+//
+// vm_command('suspend');
+// });
+// }
+// },
+// {
+// text: gettext('Resume'),
+// iconCls: 'fa fa-fw fa-play',
+// hidden: !suspended,
+// handler: function() {
+// vm_command('resume');
+// }
+// },
+ {
+ text: gettext('Shutdown'),
+ iconCls: 'fa fa-fw fa-power-off',
+ disabled: stopped || suspended,
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('vzshutdown', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ vm_command('shutdown');
+ });
+ }
+ },
+ {
+ text: gettext('Stop'),
+ iconCls: 'fa fa-fw fa-stop',
+ disabled: stopped,
+ tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('vzstop', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ vm_command("stop");
+ });
+ }
+ },
+ {
+ xtype: 'menuseparator',
+ hidden: standalone || !caps.vms['VM.Migrate']
+ },
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: !caps.vms['VM.Clone'],
+ handler: function() {
+ PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'lxc');
+ }
+ },
+ {
+ text: gettext('Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ hidden: standalone || !caps.vms['VM.Migrate'],
+ handler: function() {
+ var win = Ext.create('PVE.window.Migrate', {
+ vmtype: 'lxc',
+ nodename: nodename,
+ vmid: vmid
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Convert to template'),
+ iconCls: 'fa fa-fw fa-file-o',
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('vztemplate', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/lxc/' + vmid + '/template',
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ });
+ }
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Console'),
+ iconCls: 'fa fa-fw fa-terminal',
+ handler: function() {
+ PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname);
+ }
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.node.CmdMenu', {
+ extend: 'Ext.menu.Menu',
+ xtype: 'nodeCmdMenu',
+
+ showSeparator: false,
+
+ items: [
+ {
+ text: gettext('Create VM'),
+ itemId: 'createvm',
+ iconCls: 'fa fa-desktop',
+ handler: function() {
+ var me = this.up('menu');
+ var wiz = Ext.create('PVE.qemu.CreateWizard', {
+ nodename: me.nodename
+ });
+ wiz.show();
+ }
+ },
+ {
+ text: gettext('Create CT'),
+ itemId: 'createct',
+ iconCls: 'fa fa-cube',
+ handler: function() {
+ var me = this.up('menu');
+ var wiz = Ext.create('PVE.lxc.CreateWizard', {
+ nodename: me.nodename
+ });
+ wiz.show();
+ }
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Bulk Start'),
+ itemId: 'bulkstart',
+ iconCls: 'fa fa-fw fa-play',
+ handler: function() {
+ var me = this.up('menu');
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: me.nodename,
+ title: gettext('Bulk Start'),
+ btnText: gettext('Start'),
+ action: 'startall'
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Bulk Stop'),
+ itemId: 'bulkstop',
+ iconCls: 'fa fa-fw fa-stop',
+ handler: function() {
+ var me = this.up('menu');
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: me.nodename,
+ title: gettext('Bulk Stop'),
+ btnText: gettext('Stop'),
+ action: 'stopall'
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Bulk Migrate'),
+ itemId: 'bulkmigrate',
+ iconCls: 'fa fa-fw fa-send-o',
+ handler: function() {
+ var me = this.up('menu');
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: me.nodename,
+ title: gettext('Bulk Migrate'),
+ btnText: gettext('Migrate'),
+ action: 'migrateall'
+ });
+ win.show();
+ }
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Shell'),
+ itemId: 'shell',
+ iconCls: 'fa fa-fw fa-terminal',
+ handler: function() {
+ var me = this.up('menu');
+ PVE.Utils.openDefaultConsoleWindow(true, 'shell', undefined, me.nodename, undefined);
+ }
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Wake-on-LAN'),
+ itemId: 'wakeonlan',
+ iconCls: 'fa fa-fw fa-power-off',
+ handler: function() {
+ var me = this.up('menu');
+ Proxmox.Utils.API2Request({
+ param: {},
+ url: '/nodes/' + me.nodename + '/wakeonlan',
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, opts) {
+ Ext.Msg.show({
+ title: 'Success',
+ icon: Ext.Msg.INFO,
+ msg: Ext.String.format(gettext("Wake on LAN packet send for '{0}': '{1}'"), me.nodename, response.result.data)
+ });
+ }
+ });
+ }
+ }
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw 'no nodename specified';
+ }
+
+ me.title = gettext('Node') + " '" + me.nodename + "'";
+ me.callParent();
+
+ var caps = Ext.state.Manager.get('GuiCap');
+ // disable not allowed options
+ if (!caps.vms['VM.Allocate']) {
+ me.getComponent('createct').setDisabled(true);
+ me.getComponent('createvm').setDisabled(true);
+ }
+
+ if (!caps.nodes['Sys.PowerMgmt']) {
+ me.getComponent('bulkstart').setDisabled(true);
+ me.getComponent('bulkstop').setDisabled(true);
+ me.getComponent('bulkmigrate').setDisabled(true);
+ me.getComponent('wakeonlan').setDisabled(true);
+ }
+
+ if (!caps.nodes['Sys.Console']) {
+ me.getComponent('shell').setDisabled(true);
+ }
+
+ if (me.pveSelNode.data.running) {
+ me.getComponent('wakeonlan').setDisabled(true);
+ }
+ }
+});
+Ext.define('PVE.noVncConsole', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNoVncConsole',
+
+ nodename: undefined,
+
+ vmid: undefined,
+
+ cmd: undefined,
+
+ consoleType: undefined, // lxc, kvm, shell, cmd
+
+ layout: 'fit',
+
+ xtermjs: false,
+
+ border: false,
+
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.consoleType) {
+ throw "no console type specified";
+ }
+
+ if (!me.vmid && me.consoleType !== 'shell' && me.consoleType !== 'cmd') {
+ throw "no VM ID specified";
+ }
+
+ // always use same iframe, to avoid running several noVnc clients
+ // at same time (to avoid performance problems)
+ var box = Ext.create('Ext.ux.IFrame', { itemid : "vncconsole" });
+
+ var type = me.xtermjs ? 'xtermjs' : 'novnc';
+ Ext.apply(me, {
+ items: box,
+ listeners: {
+ activate: function() {
+ var queryDict = {
+ console: me.consoleType, // kvm, lxc, upgrade or shell
+ vmid: me.vmid,
+ node: me.nodename,
+ cmd: me.cmd,
+ resize: 'scale'
+ };
+ queryDict[type] = 1;
+ PVE.Utils.cleanEmptyObjectKeys(queryDict);
+ var url = '/?' + Ext.Object.toQueryString(queryDict);
+ box.load(url);
+ }
+ }
+ });
+
+ me.callParent();
+
+ me.on('afterrender', function() {
+ me.focus();
+ });
+ }
+});
+
+Ext.define('PVE.data.PermPathStore', {
+ extend: 'Ext.data.Store',
+ alias: 'store.pvePermPath',
+ fields: [ 'value' ],
+ autoLoad: false,
+ data: [
+ {'value': '/'},
+ {'value': '/access'},
+ {'value': '/nodes'},
+ {'value': '/pool'},
+ {'value': '/storage'},
+ {'value': '/vms'}
+ ],
+
+ constructor: function(config) {
+ var me = this;
+
+ config = config || {};
+
+ me.callParent([config]);
+
+ me.suspendEvents();
+ PVE.data.ResourceStore.each(function(record) {
+ switch (record.get('type')) {
+ case 'node':
+ me.add({value: '/nodes/' + record.get('text')});
+ break;
+
+ case 'qemu':
+ me.add({value: '/vms/' + record.get('vmid')});
+ break;
+
+ case 'lxc':
+ me.add({value: '/vms/' + record.get('vmid')});
+ break;
+
+ case 'storage':
+ me.add({value: '/storage/' + record.get('storage')});
+ break;
+ case 'pool':
+ me.add({value: '/pool/' + record.get('pool')});
+ break;
+ }
+ });
+ me.resumeEvents();
+
+ me.fireEvent('refresh', me);
+ me.fireEvent('datachanged', me);
+
+ me.sort({
+ property: 'value',
+ direction: 'ASC'
+ });
+ }
+});
+Ext.define('PVE.data.ResourceStore', {
+ extend: 'Proxmox.data.UpdateStore',
+ singleton: true,
+
+ findVMID: function(vmid) {
+ var me = this, i;
+
+ return (me.findExact('vmid', parseInt(vmid, 10)) >= 0);
+ },
+
+ // returns the cached data from all nodes
+ getNodes: function() {
+ var me = this;
+
+ var nodes = [];
+ me.each(function(record) {
+ if (record.get('type') == "node") {
+ nodes.push( record.getData() );
+ }
+ });
+
+ return nodes;
+ },
+
+ storageIsShared: function(storage_path) {
+ var me = this;
+
+ var index = me.findExact('id', storage_path);
+
+ return me.getAt(index).data.shared;
+ },
+
+ guestNode: function(vmid) {
+ var me = this;
+
+ var index = me.findExact('vmid', parseInt(vmid, 10));
+
+ return me.getAt(index).data.node;
+ },
+
+ constructor: function(config) {
+ // fixme: how to avoid those warnings
+ /*jslint confusion: true */
+
+ var me = this;
+
+ config = config || {};
+
+ var field_defaults = {
+ type: {
+ header: gettext('Type'),
+ type: 'string',
+ renderer: PVE.Utils.render_resource_type,
+ sortable: true,
+ hideable: false,
+ width: 100
+ },
+ id: {
+ header: 'ID',
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 80
+ },
+ running: {
+ header: gettext('Online'),
+ type: 'boolean',
+ renderer: Proxmox.Utils.format_boolean,
+ hidden: true,
+ convert: function(value, record) {
+ var info = record.data;
+ return (Ext.isNumeric(info.uptime) && (info.uptime > 0));
+ }
+ },
+ text: {
+ header: gettext('Description'),
+ type: 'string',
+ sortable: true,
+ width: 200,
+ convert: function(value, record) {
+ var info = record.data;
+ var text;
+
+ if (value) {
+ return value;
+ }
+
+ if (Ext.isNumeric(info.vmid) && info.vmid > 0) {
+ text = String(info.vmid);
+ if (info.name) {
+ text += " (" + info.name + ')';
+ }
+ } else { // node, pool, storage
+ text = info[info.type] || info.id;
+ if (info.node && info.type !== 'node') {
+ text += " (" + info.node + ")";
+ }
+ }
+
+ return text;
+ }
+ },
+ vmid: {
+ header: 'VMID',
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ width: 80
+ },
+ name: {
+ header: gettext('Name'),
+ hidden: true,
+ sortable: true,
+ type: 'string'
+ },
+ disk: {
+ header: gettext('Disk usage'),
+ type: 'integer',
+ renderer: PVE.Utils.render_disk_usage,
+ sortable: true,
+ width: 100,
+ hidden: true
+ },
+ diskuse: {
+ header: gettext('Disk usage') + " %",
+ type: 'number',
+ sortable: true,
+ renderer: PVE.Utils.render_disk_usage_percent,
+ width: 100,
+ calculate: PVE.Utils.calculate_disk_usage,
+ sortType: 'asFloat'
+ },
+ maxdisk: {
+ header: gettext('Disk size'),
+ type: 'integer',
+ renderer: PVE.Utils.render_size,
+ sortable: true,
+ hidden: true,
+ width: 100
+ },
+ mem: {
+ header: gettext('Memory usage'),
+ type: 'integer',
+ renderer: PVE.Utils.render_mem_usage,
+ sortable: true,
+ hidden: true,
+ width: 100
+ },
+ memuse: {
+ header: gettext('Memory usage') + " %",
+ type: 'number',
+ renderer: PVE.Utils.render_mem_usage_percent,
+ calculate: PVE.Utils.calculate_mem_usage,
+ sortType: 'asFloat',
+ sortable: true,
+ width: 100
+ },
+ maxmem: {
+ header: gettext('Memory size'),
+ type: 'integer',
+ renderer: PVE.Utils.render_size,
+ hidden: true,
+ sortable: true,
+ width: 100
+ },
+ cpu: {
+ header: gettext('CPU usage'),
+ type: 'float',
+ renderer: PVE.Utils.render_cpu,
+ sortable: true,
+ width: 100
+ },
+ maxcpu: {
+ header: gettext('maxcpu'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ width: 60
+ },
+ diskread: {
+ header: gettext('Total Disk Read'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100
+ },
+ diskwrite: {
+ header: gettext('Total Disk Write'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100
+ },
+ netin: {
+ header: gettext('Total NetIn'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100
+ },
+ netout: {
+ header: gettext('Total NetOut'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100
+ },
+ template: {
+ header: gettext('Template'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ width: 60
+ },
+ uptime: {
+ header: gettext('Uptime'),
+ type: 'integer',
+ renderer: Proxmox.Utils.render_uptime,
+ sortable: true,
+ width: 110
+ },
+ node: {
+ header: gettext('Node'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110
+ },
+ storage: {
+ header: gettext('Storage'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110
+ },
+ pool: {
+ header: gettext('Pool'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110
+ },
+ hastate: {
+ header: gettext('HA State'),
+ type: 'string',
+ defaultValue: 'unmanaged',
+ hidden: true,
+ sortable: true
+ },
+ status: {
+ header: gettext('Status'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110
+ },
+ lock: {
+ header: gettext('Lock'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110
+ }
+ };
+
+ var fields = [];
+ var fieldNames = [];
+ Ext.Object.each(field_defaults, function(key, value) {
+ var field = {name: key, type: value.type};
+ if (Ext.isDefined(value.convert)) {
+ field.convert = value.convert;
+ }
+
+ if (Ext.isDefined(value.calculate)) {
+ field.calculate = value.calculate;
+ }
+
+ if (Ext.isDefined(value.defaultValue)) {
+ field.defaultValue = value.defaultValue;
+ }
+
+ fields.push(field);
+ fieldNames.push(key);
+ });
+
+ Ext.define('PVEResources', {
+ extend: "Ext.data.Model",
+ fields: fields,
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/resources'
+ }
+ });
+
+ Ext.define('PVETree', {
+ extend: "Ext.data.Model",
+ fields: fields,
+ proxy: { type: 'memory' }
+ });
+
+ Ext.apply(config, {
+ storeid: 'PVEResources',
+ model: 'PVEResources',
+ defaultColumns: function() {
+ var res = [];
+ Ext.Object.each(field_defaults, function(field, info) {
+ var fi = Ext.apply({ dataIndex: field }, info);
+ res.push(fi);
+ });
+ return res;
+ },
+ fieldNames: fieldNames
+ });
+
+ me.callParent([config]);
+ }
+});
+Ext.define('pve-domains', {
+ extend: "Ext.data.Model",
+ fields: [
+ 'realm', 'type', 'comment', 'default', 'tfa',
+ {
+ name: 'descr',
+ // Note: We use this in the RealmComboBox.js (see Bug #125)
+ convert: function(value, record) {
+ if (value) {
+ return value;
+ }
+
+ var info = record.data;
+ // return realm if there is no comment
+ var text = info.comment || info.realm;
+
+ if (info.tfa) {
+ text += " (+ " + info.tfa + ")";
+ }
+
+ return Ext.String.htmlEncode(text);
+ }
+ }
+ ],
+ idProperty: 'realm',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/domains"
+ }
+});
+Ext.define('pve-rrd-node', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name:'cpu',
+ // percentage
+ convert: function(value) {
+ return value*100;
+ }
+ },
+ {
+ name:'iowait',
+ // percentage
+ convert: function(value) {
+ return value*100;
+ }
+ },
+ 'loadavg',
+ 'maxcpu',
+ 'memtotal',
+ 'memused',
+ 'netin',
+ 'netout',
+ 'roottotal',
+ 'rootused',
+ 'swaptotal',
+ 'swapused',
+ { type: 'date', dateFormat: 'timestamp', name: 'time' }
+ ]
+});
+
+Ext.define('pve-rrd-guest', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name:'cpu',
+ // percentage
+ convert: function(value) {
+ return value*100;
+ }
+ },
+ 'maxcpu',
+ 'netin',
+ 'netout',
+ 'mem',
+ 'maxmem',
+ 'disk',
+ 'maxdisk',
+ 'diskread',
+ 'diskwrite',
+ { type: 'date', dateFormat: 'timestamp', name: 'time' }
+ ]
+});
+
+Ext.define('pve-rrd-storage', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'used',
+ 'total',
+ { type: 'date', dateFormat: 'timestamp', name: 'time' }
+ ]
+});
+Ext.define('PVE.form.VlanField', {
+ extend: 'Ext.form.field.Number',
+ alias: ['widget.pveVlanField'],
+
+ deleteEmpty: false,
+
+ emptyText: 'no VLAN',
+
+ fieldLabel: gettext('VLAN Tag'),
+
+ allowBlank: true,
+
+ getSubmitData: function() {
+ var me = this,
+ data = null,
+ val;
+ if (!me.disabled && me.submitValue) {
+ val = me.getSubmitValue();
+ if (val) {
+ data = {};
+ data[me.getName()] = val;
+ } else if (me.deleteEmpty) {
+ data = {};
+ data['delete'] = me.getName();
+ }
+ }
+ return data;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ Ext.apply(me, {
+ minValue: 1,
+ maxValue: 4094
+ });
+
+ me.callParent();
+ }
+});
+// boolean type including 'Default' (delete property from file)
+Ext.define('PVE.form.Boolean', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.booleanfield'],
+ comboItems: [
+ ['__default__', gettext('Default')],
+ [1, gettext('Yes')],
+ [0, gettext('No')]
+ ]
+});
+Ext.define('PVE.form.CompressionSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveCompressionSelector'],
+ comboItems: [
+ ['0', Proxmox.Utils.noneText],
+ ['lzo', 'LZO (' + gettext('fast') + ')'],
+ ['gzip', 'GZIP (' + gettext('good') + ')']
+ ]
+});
+Ext.define('PVE.form.PoolSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pvePoolSelector'],
+
+ allowBlank: false,
+ valueField: 'poolid',
+ displayField: 'poolid',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-pools',
+ sorters: 'poolid'
+ });
+
+ Ext.apply(me, {
+ store: store,
+ autoSelect: false,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Pool'),
+ sortable: true,
+ dataIndex: 'poolid',
+ flex: 1
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+
+}, function() {
+
+ Ext.define('pve-pools', {
+ extend: 'Ext.data.Model',
+ fields: [ 'poolid', 'comment' ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/pools"
+ },
+ idProperty: 'poolid'
+ });
+
+});
+Ext.define('PVE.form.PrivilegesSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ xtype: 'pvePrivilegesSelector',
+
+ multiSelect: true,
+
+ initComponent: function() {
+ var me = this;
+
+ // So me.store is available.
+ me.callParent();
+
+ Proxmox.Utils.API2Request({
+ url: '/access/roles/Administrator',
+ method: 'GET',
+ success: function(response, options) {
+ var data = [], key;
+ /*jslint forin: true */
+ for (key in response.result.data) {
+ data.push([key, key]);
+ }
+ /*jslint forin: false */
+
+ me.store.setData(data);
+
+ me.store.sort({
+ property: 'key',
+ direction: 'ASC'
+ });
+ },
+
+ failure: function (response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ }
+});
+Ext.define('pve-groups', {
+ extend: 'Ext.data.Model',
+ fields: [ 'groupid', 'comment' ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/groups"
+ },
+ idProperty: 'groupid'
+});
+
+Ext.define('PVE.form.GroupSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ xtype: 'pveGroupSelector',
+
+ allowBlank: false,
+ autoSelect: false,
+ valueField: 'groupid',
+ displayField: 'groupid',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Group'),
+ sortable: true,
+ dataIndex: 'groupid',
+ flex: 1
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ ]
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-groups',
+ sorters: [{
+ property: 'groupid'
+ }]
+ });
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+});
+Ext.define('PVE.form.UserSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveUserSelector'],
+
+ allowBlank: false,
+ autoSelect: false,
+ valueField: 'userid',
+ displayField: 'userid',
+
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-users',
+ sorters: [{
+ property: 'userid'
+ }]
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('User'),
+ sortable: true,
+ dataIndex: 'userid',
+ flex: 1
+ },
+ {
+ header: gettext('Name'),
+ sortable: true,
+ renderer: PVE.Utils.render_full_name,
+ dataIndex: 'firstname',
+ flex: 1
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+
+ store.load({ params: { enabled: 1 }});
+ }
+
+}, function() {
+
+ Ext.define('pve-users', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'userid', 'firstname', 'lastname' , 'email', 'comment',
+ { type: 'boolean', name: 'enable' },
+ { type: 'date', dateFormat: 'timestamp', name: 'expire' }
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/users"
+ },
+ idProperty: 'userid'
+ });
+
+});
+
+
+Ext.define('PVE.form.RoleSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveRoleSelector'],
+
+ allowBlank: false,
+ autoSelect: false,
+ valueField: 'roleid',
+ displayField: 'roleid',
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-roles',
+ sorters: [{
+ property: 'roleid'
+ }]
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Role'),
+ sortable: true,
+ dataIndex: 'roleid',
+ flex: 1
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+
+}, function() {
+
+ Ext.define('pve-roles', {
+ extend: 'Ext.data.Model',
+ fields: [ 'roleid', 'privs' ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/roles"
+ },
+ idProperty: 'roleid'
+ });
+
+});
+Ext.define('PVE.form.GuestIDSelector', {
+ extend: 'Ext.form.field.Number',
+ alias: 'widget.pveGuestIDSelector',
+
+ allowBlank: false,
+
+ minValue: 100,
+
+ maxValue: 999999999,
+
+ validateExists: undefined,
+
+ loadNextFreeID: false,
+
+ guestType: undefined,
+
+ validator: function(value) {
+ var me = this;
+
+ if (!Ext.isNumeric(value) ||
+ value < me.minValue ||
+ value > me.maxValue) {
+ // check is done by ExtJS
+ return true;
+ }
+
+ if (me.validateExists === true && !me.exists) {
+ return me.unknownID;
+ }
+
+ if (me.validateExists === false && me.exists) {
+ return me.inUseID;
+ }
+
+ return true;
+ },
+
+ initComponent: function() {
+ var me = this;
+ var label = '{0} ID';
+ var unknownID = gettext('This {0} ID does not exists');
+ var inUseID = gettext('This {0} ID is already in use');
+ var type = 'CT/VM';
+
+ if (me.guestType === 'lxc') {
+ type = 'CT';
+ } else if (me.guestType === 'qemu') {
+ type = 'VM';
+ }
+
+ me.label = Ext.String.format(label, type);
+ me.unknownID = Ext.String.format(unknownID, type);
+ me.inUseID = Ext.String.format(inUseID, type);
+
+ Ext.apply(me, {
+ fieldLabel: me.label,
+ listeners: {
+ 'change': function(field, newValue, oldValue) {
+ if (!Ext.isDefined(me.validateExists)) {
+ return;
+ }
+ Proxmox.Utils.API2Request({
+ params: { vmid: newValue },
+ url: '/cluster/nextid',
+ method: 'GET',
+ success: function(response, opts) {
+ me.exists = false;
+ me.validate();
+ },
+ failure: function(response, opts) {
+ me.exists = true;
+ me.validate();
+ }
+ });
+ }
+ }
+ });
+
+ me.callParent();
+
+ if (me.loadNextFreeID) {
+ Proxmox.Utils.API2Request({
+ url: '/cluster/nextid',
+ method: 'GET',
+ success: function(response, opts) {
+ me.setRawValue(response.result.data);
+ }
+ });
+ }
+ }
+});
+Ext.define('PVE.form.MemoryField', {
+ extend: 'Ext.form.field.Number',
+ alias: 'widget.pveMemoryField',
+
+ allowBlank: false,
+
+ hotplug: false,
+
+ minValue: 32,
+
+ maxValue: 4178944,
+
+ step: 32,
+
+ value: '512', // qm default
+
+ allowDecimals: false,
+
+ allowExponential: false,
+
+ computeUpDown: function(value) {
+ var me = this;
+
+ if (!me.hotplug) {
+ return { up: value + me.step, down: value - me.step };
+ }
+
+ var dimm_size = 512;
+ var prev_dimm_size = 0;
+ var min_size = 1024;
+ var current_size = min_size;
+ var value_up = min_size;
+ var value_down = min_size;
+ var value_start = min_size;
+
+ var i, j;
+ for (j = 0; j < 9; j++) {
+ for (i = 0; i < 32; i++) {
+ if ((value >= current_size) && (value < (current_size + dimm_size))) {
+ value_start = current_size;
+ value_up = current_size + dimm_size;
+ value_down = current_size - ((i === 0) ? prev_dimm_size : dimm_size);
+ }
+ current_size += dimm_size;
+ }
+ prev_dimm_size = dimm_size;
+ dimm_size = dimm_size*2;
+ }
+
+ return { up: value_up, down: value_down, start: value_start };
+ },
+
+ onSpinUp: function() {
+ var me = this;
+ if (!me.readOnly) {
+ var res = me.computeUpDown(me.getValue());
+ me.setValue(Ext.Number.constrain(res.up, me.minValue, me.maxValue));
+ }
+ },
+
+ onSpinDown: function() {
+ var me = this;
+ if (!me.readOnly) {
+ var res = me.computeUpDown(me.getValue());
+ me.setValue(Ext.Number.constrain(res.down, me.minValue, me.maxValue));
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.hotplug) {
+ me.minValue = 1024;
+
+ me.on('blur', function(field) {
+ var value = me.getValue();
+ var res = me.computeUpDown(value);
+ if (value === res.start || value === res.up || value === res.down) {
+ return;
+ }
+ field.setValue(res.up);
+ });
+ }
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.form.NetworkCardSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: 'widget.pveNetworkCardSelector',
+ comboItems: [
+ ['e1000', 'Intel E1000'],
+ ['virtio', 'VirtIO (' + gettext('paravirtualized') + ')'],
+ ['rtl8139', 'Realtek RTL8139'],
+ ['vmxnet3', 'VMware vmxnet3']
+ ]
+});
+Ext.define('PVE.form.DiskFormatSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: 'widget.pveDiskFormatSelector',
+ comboItems: [
+ ['raw', gettext('Raw disk image') + ' (raw)'],
+ ['qcow2', gettext('QEMU image format') + ' (qcow2)'],
+ ['vmdk', gettext('VMware image format') + ' (vmdk)']
+ ]
+});
+Ext.define('PVE.form.DiskSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ xtype: 'pveDiskSelector',
+
+ // can be
+ // undefined: all
+ // unused: only unused
+ // journal_disk: all disks with gpt
+ diskType: undefined,
+
+ valueField: 'devpath',
+ displayField: 'devpath',
+ emptyText: gettext('No Disks unused'),
+ listConfig: {
+ width: 600,
+ columns: [
+ {
+ header: gettext('Device'),
+ flex: 3,
+ sortable: true,
+ dataIndex: 'devpath'
+ },
+ {
+ header: gettext('Size'),
+ flex: 2,
+ sortable: false,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'size'
+ },
+ {
+ header: gettext('Serial'),
+ flex: 5,
+ sortable: true,
+ dataIndex: 'serial'
+ }
+ ]
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ filterOnLoad: true,
+ model: 'pve-disk-list',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/nodes/" + nodename + "/disks/list",
+ extraParams: { type: me.diskType }
+ },
+ sorters: [
+ {
+ property : 'devpath',
+ direction: 'ASC'
+ }
+ ]
+ });
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+}, function() {
+
+ Ext.define('pve-disk-list', {
+ extend: 'Ext.data.Model',
+ fields: [ 'devpath', 'used', { name: 'size', type: 'number'},
+ {name: 'osdid', type: 'number'},
+ 'vendor', 'model', 'serial'],
+ idProperty: 'devpath'
+ });
+});
+Ext.define('PVE.form.BusTypeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: 'widget.pveBusSelector',
+
+ noVirtIO: false,
+
+ initComponent: function() {
+ var me = this;
+
+ me.comboItems = [['ide', 'IDE'], ['sata', 'SATA']];
+
+ if (!me.noVirtIO) {
+ me.comboItems.push(['virtio', 'VirtIO Block']);
+ }
+
+ me.comboItems.push(['scsi', 'SCSI']);
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.form.ControllerSelector', {
+ extend: 'Ext.form.FieldContainer',
+ alias: 'widget.pveControllerSelector',
+
+ statics: {
+ maxIds: {
+ ide: 3,
+ sata: 5,
+ virtio: 15,
+ scsi: 13
+ }
+ },
+
+ noVirtIO: false,
+
+ vmconfig: {}, // used to check for existing devices
+
+ sortByPreviousUsage: function(vmconfig, controllerList) {
+
+ var usedControllers = Ext.clone(PVE.form.ControllerSelector.maxIds);
+
+ var type;
+ for (type in usedControllers) {
+ if(usedControllers.hasOwnProperty(type)) {
+ usedControllers[type] = 0;
+ }
+ }
+
+ var property;
+ for (property in vmconfig) {
+ if (vmconfig.hasOwnProperty(property)) {
+ if (property.match(PVE.Utils.bus_match) && !vmconfig[property].match(/media=cdrom/)) {
+ var foundController = property.match(PVE.Utils.bus_match)[1];
+ usedControllers[foundController]++;
+ }
+ }
+ }
+
+ var vmDefaults = PVE.qemu.OSDefaults[vmconfig.ostype];
+
+ var sortPriority = vmDefaults && vmDefaults.busPriority
+ ? vmDefaults.busPriority : PVE.qemu.OSDefaults.generic;
+
+ var sortedList = Ext.clone(controllerList);
+ sortedList.sort(function(a,b) {
+ if (usedControllers[b] == usedControllers[a]) {
+ return sortPriority[b] - sortPriority[a];
+ }
+ return usedControllers[b] - usedControllers[a];
+ });
+
+ return sortedList;
+ },
+
+ setVMConfig: function(vmconfig, autoSelect) {
+ var me = this;
+
+ me.vmconfig = Ext.apply({}, vmconfig);
+
+ var clist = ['ide', 'virtio', 'scsi', 'sata'];
+ var bussel = me.down('field[name=controller]');
+ var deviceid = me.down('field[name=deviceid]');
+
+ if (autoSelect === 'cdrom') {
+ clist = ['ide', 'scsi', 'sata'];
+ if (!Ext.isDefined(me.vmconfig.ide2)) {
+ bussel.setValue('ide');
+ deviceid.setValue(2);
+ return;
+ }
+ } else {
+ // in most cases we want to add a disk to the same controller
+ // we previously used
+ clist = me.sortByPreviousUsage(me.vmconfig, clist);
+ }
+
+ Ext.Array.each(clist, function(controller) {
+ var confid, i;
+ bussel.setValue(controller);
+ for (i = 0; i <= PVE.form.ControllerSelector.maxIds[controller]; i++) {
+ confid = controller + i.toString();
+ if (!Ext.isDefined(me.vmconfig[confid])) {
+ deviceid.setValue(i);
+ return false; // break
+ }
+ }
+ });
+ deviceid.validate();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ Ext.apply(me, {
+ fieldLabel: gettext('Bus/Device'),
+ layout: 'hbox',
+ defaults: {
+ hideLabel: true
+ },
+ items: [
+ {
+ xtype: 'pveBusSelector',
+ name: 'controller',
+ value: PVE.qemu.OSDefaults.generic.busType,
+ noVirtIO: me.noVirtIO,
+ allowBlank: false,
+ flex: 2,
+ listeners: {
+ change: function(t, value) {
+ if (!value) {
+ return;
+ }
+ var field = me.down('field[name=deviceid]');
+ field.setMaxValue(PVE.form.ControllerSelector.maxIds[value]);
+ field.validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'deviceid',
+ minValue: 0,
+ maxValue: PVE.form.ControllerSelector.maxIds.ide,
+ value: '0',
+ flex: 1,
+ allowBlank: false,
+ validator: function(value) {
+ /*jslint confusion: true */
+ if (!me.rendered) {
+ return;
+ }
+ var field = me.down('field[name=controller]');
+ var controller = field.getValue();
+ var confid = controller + value;
+ if (Ext.isDefined(me.vmconfig[confid])) {
+ return "This device is already in use.";
+ }
+ return true;
+ }
+ }
+ ]
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.form.EmailNotificationSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveEmailNotificationSelector'],
+ comboItems: [
+ ['always', gettext('Always')],
+ ['failure', gettext('On failure only')]
+ ]
+});
+/*global Proxmox*/
+Ext.define('PVE.form.RealmComboBox', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: ['widget.pveRealmComboBox'],
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ view.store.on('load', this.onLoad, view);
+ },
+
+ onLoad: function(store, records, success) {
+ if (!success) {
+ return;
+ }
+ var me = this;
+ var val = me.getValue();
+ if (!val || !me.store.findRecord('realm', val)) {
+ var def = 'pam';
+ Ext.each(records, function(rec) {
+ if (rec.data && rec.data['default']) {
+ def = rec.data.realm;
+ }
+ });
+ me.setValue(def);
+ }
+ }
+ },
+
+ fieldLabel: gettext('Realm'),
+ name: 'realm',
+ queryMode: 'local',
+ allowBlank: false,
+ editable: false,
+ forceSelection: true,
+ autoSelect: false,
+ triggerAction: 'all',
+ valueField: 'realm',
+ displayField: 'descr',
+ getState: function() {
+ return { value: this.getValue() };
+ },
+ applyState : function(state) {
+ if (state && state.value) {
+ this.setValue(state.value);
+ }
+ },
+ stateEvents: [ 'select' ],
+ stateful: true, // last chosen auth realm is saved between page reloads
+ id: 'pveloginrealm', // We need stable ids when using stateful, not autogenerated
+ stateID: 'pveloginrealm',
+
+ needOTP: function(realm) {
+ var me = this;
+ // use exact match
+ var rec = me.store.findRecord('realm', realm, 0, false, false, true);
+ return rec && rec.data && rec.data.tfa ? rec.data.tfa : undefined;
+ },
+
+ store: {
+ model: 'pve-domains',
+ autoLoad: true
+ }
+});
+/*
+ * Top left combobox, used to select a view of the underneath RessourceTree
+ */
+Ext.define('PVE.form.ViewSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: ['widget.pveViewSelector'],
+
+ editable: false,
+ allowBlank: false,
+ forceSelection: true,
+ autoSelect: false,
+ valueField: 'key',
+ displayField: 'value',
+ hideLabel: true,
+ queryMode: 'local',
+
+ initComponent: function() {
+ var me = this;
+
+ var default_views = {
+ server: {
+ text: gettext('Server View'),
+ groups: ['node']
+ },
+ folder: {
+ text: gettext('Folder View'),
+ groups: ['type']
+ },
+ storage: {
+ text: gettext('Storage View'),
+ groups: ['node'],
+ filterfn: function(node) {
+ return node.data.type === 'storage' || node.data.type === 'node';
+ }
+ },
+ pool: {
+ text: gettext('Pool View'),
+ groups: ['pool'],
+ // Pool View only lists VMs and Containers
+ filterfn: function(node) {
+ return node.data.type === 'qemu' || node.data.type === 'lxc' || node.data.type === 'openvz' ||
+ node.data.type === 'pool';
+ }
+ }
+ };
+
+ var groupdef = [];
+ Ext.Object.each(default_views, function(viewname, value) {
+ groupdef.push([viewname, value.text]);
+ });
+
+ var store = Ext.create('Ext.data.Store', {
+ model: 'KeyValue',
+ proxy: {
+ type: 'memory',
+ reader: 'array'
+ },
+ data: groupdef,
+ autoload: true
+ });
+
+ Ext.apply(me, {
+ store: store,
+ value: groupdef[0][0],
+ getViewFilter: function() {
+ var view = me.getValue();
+ return Ext.apply({ id: view }, default_views[view] || default_views.server);
+ },
+
+ getState: function() {
+ return { value: me.getValue() };
+ },
+
+ applyState : function(state, doSelect) {
+ var view = me.getValue();
+ if (state && state.value && (view != state.value)) {
+ var record = store.findRecord('key', state.value);
+ if (record) {
+ me.setValue(state.value, true);
+ if (doSelect) {
+ me.fireEvent('select', me, [record]);
+ }
+ }
+ }
+ },
+ stateEvents: [ 'select' ],
+ stateful: true,
+ stateId: 'pveview',
+ id: 'view'
+ });
+
+ me.callParent();
+
+ var statechange = function(sp, key, value) {
+ if (key === me.id) {
+ me.applyState(value, true);
+ }
+ };
+
+ var sp = Ext.state.Manager.getProvider();
+ me.mon(sp, 'statechange', statechange, me);
+ }
+});
+Ext.define('PVE.form.NodeSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveNodeSelector'],
+
+ // invalidate nodes which are offline
+ onlineValidator: false,
+
+ selectCurNode: false,
+
+ // do not allow those nodes (array)
+ disallowedNodes: undefined,
+
+ // only allow those nodes (array)
+ allowedNodes: undefined,
+ // set default value to empty array, else it inits it with
+ // null and after the store load it is an empty array,
+ // triggering dirtychange
+ value: [],
+ valueField: 'node',
+ displayField: 'node',
+ store: {
+ fields: [ 'node', 'cpu', 'maxcpu', 'mem', 'maxmem', 'uptime' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes'
+ },
+ sorters: [
+ {
+ property : 'node',
+ direction: 'ASC'
+ },
+ {
+ property : 'mem',
+ direction: 'DESC'
+ }
+ ]
+ },
+
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Node'),
+ dataIndex: 'node',
+ sortable: true,
+ hideable: false,
+ flex: 1
+ },
+ {
+ header: gettext('Memory usage') + " %",
+ renderer: PVE.Utils.render_mem_usage_percent,
+ sortable: true,
+ width: 100,
+ dataIndex: 'mem'
+ },
+ {
+ header: gettext('CPU usage'),
+ renderer: PVE.Utils.render_cpu,
+ sortable: true,
+ width: 100,
+ dataIndex: 'cpu'
+ }
+ ]
+ },
+
+ validator: function(value) {
+ /*jslint confusion: true */
+ var me = this;
+ if (!me.onlineValidator || (me.allowBlank && !value)) {
+ return true;
+ }
+
+ var offline = [];
+ var notAllowed = [];
+
+ Ext.Array.each(value.split(/\s*,\s*/), function(node) {
+ var rec = me.store.findRecord(me.valueField, node);
+ if (!(rec && rec.data) || rec.data.status !== 'online') {
+ offline.push(node);
+ } else if (me.allowedNodes && !Ext.Array.contains(me.allowedNodes, node)) {
+ notAllowed.push(node);
+ }
+ });
+
+ if (value && notAllowed.length !== 0) {
+ return "Node " + notAllowed.join(', ') + " is not allowed for this action!";
+ }
+
+ if (value && offline.length !== 0) {
+ return "Node " + offline.join(', ') + " seems to be offline!";
+ }
+ return true;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.selectCurNode && PVE.curSelectedNode && PVE.curSelectedNode.data.node) {
+ me.preferredValue = PVE.curSelectedNode.data.node;
+ }
+
+ me.callParent();
+ me.getStore().load();
+
+ // filter out disallowed nodes
+ me.getStore().addFilter(new Ext.util.Filter({
+ filterFn: function(item) {
+ if (Ext.isArray(me.disallowedNodes)) {
+ return !Ext.Array.contains(me.disallowedNodes, item.data.node);
+ } else {
+ return true;
+ }
+ }
+ }));
+
+ me.mon(me.getStore(), 'load', function(){
+ me.isValid();
+ });
+ }
+});
+Ext.define('PVE.form.FileSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: 'widget.pveFileSelector',
+
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+
+ listeners: {
+ afterrender: function() {
+ var me = this;
+ if (!me.disabled) {
+ me.setStorage(me.storage, me.nodename);
+ }
+ }
+ },
+
+ setStorage: function(storage, nodename) {
+ var me = this;
+
+ var change = false;
+ if (storage && (me.storage !== storage)) {
+ me.storage = storage;
+ change = true;
+ }
+
+ if (nodename && (me.nodename !== nodename)) {
+ me.nodename = nodename;
+ change = true;
+ }
+
+ if (!(me.storage && me.nodename && change)) {
+ return;
+ }
+
+ var url = '/api2/json/nodes/' + me.nodename + '/storage/' + me.storage + '/content';
+ if (me.storageContent) {
+ url += '?content=' + me.storageContent;
+ }
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: url
+ });
+
+ me.store.removeAll();
+ me.store.load();
+ },
+
+ setNodename: function(nodename) {
+ this.setStorage(undefined, nodename);
+ },
+
+ store: {
+ model: 'pve-storage-content'
+ },
+
+ allowBlank: false,
+ autoSelect: false,
+ valueField: 'volid',
+ displayField: 'text',
+
+ listConfig: {
+ width: 600,
+ columns: [
+ {
+ header: gettext('Name'),
+ dataIndex: 'text',
+ hideable: false,
+ flex: 1
+ },
+ {
+ header: gettext('Format'),
+ width: 60,
+ dataIndex: 'format'
+ },
+ {
+ header: gettext('Size'),
+ width: 100,
+ dataIndex: 'size',
+ renderer: Proxmox.Utils.format_size
+ }
+ ]
+ }
+});
+Ext.define('PVE.form.StorageSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: 'widget.pveStorageSelector',
+
+ allowBlank: false,
+ valueField: 'storage',
+ displayField: 'storage',
+ listConfig: {
+ width: 450,
+ columns: [
+ {
+ header: gettext('Name'),
+ dataIndex: 'storage',
+ hideable: false,
+ flex: 1
+ },
+ {
+ header: gettext('Type'),
+ width: 75,
+ dataIndex: 'type'
+ },
+ {
+ header: gettext('Avail'),
+ width: 90,
+ dataIndex: 'avail',
+ renderer: Proxmox.Utils.format_size
+ },
+ {
+ header: gettext('Capacity'),
+ width: 90,
+ dataIndex: 'total',
+ renderer: Proxmox.Utils.format_size
+ }
+ ]
+ },
+
+ reloadStorageList: function() {
+ var me = this;
+ if (!me.nodename) {
+ return;
+ }
+
+ var params = {
+ format: 1
+ };
+ var url = '/api2/json/nodes/' + me.nodename + '/storage';
+ if (me.storageContent) {
+ params.content = me.storageContent;
+ }
+ if (me.targetNode) {
+ params.target = me.targetNode;
+ params.enabled = 1; // skip disabled storages
+ }
+ me.store.setProxy({
+ type: 'proxmox',
+ url: url,
+ extraParams: params
+ });
+
+ me.store.load();
+
+ },
+
+ setTargetNode: function(targetNode) {
+ var me = this;
+
+ if (!targetNode || (me.targetNode === targetNode)) {
+ return;
+ }
+
+ me.targetNode = targetNode;
+
+ me.reloadStorageList();
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || (me.nodename === nodename)) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ me.reloadStorageList();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename;
+ me.nodename = undefined;
+
+ var store = Ext.create('Ext.data.Store', {
+ model: 'pve-storage-status',
+ sorters: {
+ property: 'storage',
+ order: 'DESC'
+ }
+ });
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+
+ if (nodename) {
+ me.setNodename(nodename);
+ }
+ }
+}, function() {
+
+ Ext.define('pve-storage-status', {
+ extend: 'Ext.data.Model',
+ fields: [ 'storage', 'active', 'type', 'avail', 'total' ],
+ idProperty: 'storage'
+ });
+
+});
+Ext.define('PVE.form.DiskStorageSelector', {
+ extend: 'Ext.container.Container',
+ alias: 'widget.pveDiskStorageSelector',
+
+ layout: 'fit',
+ defaults: {
+ margin: '0 0 5 0'
+ },
+
+ // the fieldLabel for the storageselector
+ storageLabel: gettext('Storage'),
+
+ // the content to show (e.g., images or rootdir)
+ storageContent: undefined,
+
+ // if true, selects the first available storage
+ autoSelect: false,
+
+ allowBlank: false,
+ emptyText: '',
+
+ // hides the selection field
+ // this is always hidden on creation,
+ // and only shown when the storage needs a selection and
+ // hideSelection is not true
+ hideSelection: undefined,
+
+ // hides the size field (e.g, for the efi disk dialog)
+ hideSize: false,
+
+ // sets the initial size value
+ // string because else we get a type confusion
+ defaultSize: '32',
+
+ changeStorage: function(f, value) {
+ var me = this;
+ var formatsel = me.getComponent('diskformat');
+ var hdfilesel = me.getComponent('hdimage');
+ var hdsizesel = me.getComponent('disksize');
+
+ // initial store load, and reset/deletion of the storage
+ if (!value) {
+ hdfilesel.setDisabled(true);
+ hdfilesel.setVisible(false);
+
+ formatsel.setDisabled(true);
+ return;
+ }
+
+ var rec = f.store.getById(value);
+ // if the storage is not defined, or valid,
+ // we cannot know what to enable/disable
+ if (!rec) {
+ return;
+ }
+
+ var selectformat = false;
+ if (rec.data.format) {
+ var format = rec.data.format[0]; // 0 is the formats, 1 the default in the backend
+ delete format.subvol; // we never need subvol in the gui
+ selectformat = (Ext.Object.getSize(format) > 1);
+ }
+
+ var select = !!rec.data.select_existing && !me.hideSelection;
+
+ formatsel.setDisabled(!selectformat);
+ formatsel.setValue(selectformat ? 'qcow2' : 'raw');
+
+ hdfilesel.setDisabled(!select);
+ hdfilesel.setVisible(select);
+ if (select) {
+ hdfilesel.setStorage(value);
+ }
+
+ hdsizesel.setDisabled(select || me.hideSize);
+ hdsizesel.setVisible(!select && !me.hideSize);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ var hdstorage = me.getComponent('hdstorage');
+ var hdfilesel = me.getComponent('hdimage');
+
+ hdstorage.setNodename(nodename);
+ hdfilesel.setNodename(nodename);
+ },
+
+ setDisabled: function(value) {
+ var me = this;
+ var hdstorage = me.getComponent('hdstorage');
+
+ // reset on disable
+ if (value) {
+ hdstorage.setValue();
+ }
+ hdstorage.setDisabled(value);
+
+ // disabling does not always fire this event and we do not need
+ // the value of the validity
+ hdstorage.fireEvent('validitychange');
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: 'pveStorageSelector',
+ itemId: 'hdstorage',
+ name: 'hdstorage',
+ reference: 'hdstorage',
+ fieldLabel: me.storageLabel,
+ nodename: me.nodename,
+ storageContent: me.storageContent,
+ disabled: me.disabled,
+ autoSelect: me.autoSelect,
+ allowBlank: me.allowBlank,
+ emptyText: me.emptyText,
+ listeners: {
+ change: {
+ fn: me.changeStorage,
+ scope: me
+ }
+ }
+ },
+ {
+ xtype: 'pveFileSelector',
+ name: 'hdimage',
+ reference: 'hdimage',
+ itemId: 'hdimage',
+ fieldLabel: gettext('Disk image'),
+ nodename: me.nodename,
+ disabled: true,
+ hidden: true
+ },
+ {
+ xtype: 'numberfield',
+ itemId: 'disksize',
+ reference: 'disksize',
+ name: 'disksize',
+ fieldLabel: gettext('Disk size') + ' (GiB)',
+ hidden: me.hideSize,
+ disabled: me.hideSize,
+ minValue: 0.001,
+ maxValue: 128*1024,
+ decimalPrecision: 3,
+ value: me.defaultSize,
+ allowBlank: false
+ },
+ {
+ xtype: 'pveDiskFormatSelector',
+ itemId: 'diskformat',
+ reference: 'diskformat',
+ name: 'diskformat',
+ fieldLabel: gettext('Format'),
+ nodename: me.nodename,
+ disabled: true,
+ hidden: me.storageContent === 'rootdir',
+ value: 'qcow2',
+ allowBlank: false
+ }
+ ];
+
+ // use it to disable the children but not ourself
+ me.disabled = false;
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.form.BridgeSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.PVE.form.BridgeSelector'],
+
+ bridgeType: 'any_bridge', // bridge, OVSBridge or any_bridge
+
+ store: {
+ fields: [ 'iface', 'active', 'type' ],
+ filterOnLoad: true,
+ sorters: [
+ {
+ property : 'iface',
+ direction: 'ASC'
+ }
+ ]
+ },
+ valueField: 'iface',
+ displayField: 'iface',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Bridge'),
+ dataIndex: 'iface',
+ hideable: false,
+ width: 100
+ },
+ {
+ header: gettext('Active'),
+ width: 60,
+ dataIndex: 'active',
+ renderer: Proxmox.Utils.format_boolean
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comments',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ ]
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || (me.nodename === nodename)) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/network?type=' +
+ me.bridgeType
+ });
+
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename;
+ me.nodename = undefined;
+
+ me.callParent();
+
+ me.setNodename(nodename);
+ }
+});
+
+Ext.define('PVE.form.PCISelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ xtype: 'pvePCISelector',
+
+ store: {
+ fields: [ 'id','vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev' ],
+ filterOnLoad: true,
+ sorters: [
+ {
+ property : 'id',
+ direction: 'ASC'
+ }
+ ]
+ },
+
+ autoSelect: false,
+ valueField: 'id',
+ displayField: 'id',
+
+ // can contain a load callback for the store
+ // useful to determine the state of the IOMMU
+ onLoadCallBack: undefined,
+
+ listConfig: {
+ width: 800,
+ columns: [
+ {
+ header: 'ID',
+ dataIndex: 'id',
+ width: 80
+ },
+ {
+ header: gettext('IOMMU Group'),
+ dataIndex: 'iommugroup',
+ width: 50
+ },
+ {
+ header: gettext('Vendor'),
+ dataIndex: 'vendor_name',
+ flex: 2
+ },
+ {
+ header: gettext('Device'),
+ dataIndex: 'device_name',
+ flex: 6
+ },
+ {
+ header: gettext('Mediated Devices'),
+ dataIndex: 'mdev',
+ flex: 1,
+ renderer: function(val) {
+ return Proxmox.Utils.format_boolean(!!val);
+ }
+ }
+ ]
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || (me.nodename === nodename)) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/hardware/pci'
+ });
+
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename;
+ me.nodename = undefined;
+
+ me.callParent();
+
+ if (me.onLoadCallBack !== undefined) {
+ me.mon(me.getStore(), 'load', me.onLoadCallBack);
+ }
+
+ me.setNodename(nodename);
+ }
+});
+
+Ext.define('PVE.form.MDevSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ xtype: 'pveMDevSelector',
+
+ store: {
+ fields: [ 'type','available', 'description' ],
+ filterOnLoad: true,
+ sorters: [
+ {
+ property : 'type',
+ direction: 'ASC'
+ }
+ ]
+ },
+ autoSelect: false,
+ valueField: 'type',
+ displayField: 'type',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ flex: 1
+ },
+ {
+ header: gettext('Available'),
+ dataIndex: 'available',
+ width: 80
+ },
+ {
+ header: gettext('Description'),
+ dataIndex: 'description',
+ flex: 1,
+ renderer: function(value) {
+ if (!value) {
+ return '';
+ }
+
+ return value.split('\n').join('
');
+ }
+ }
+ ]
+ },
+
+ setPciID: function(pciid, force) {
+ var me = this;
+
+ if (!force && (!pciid || (me.pciid === pciid))) {
+ return;
+ }
+
+ me.pciid = pciid;
+ me.updateProxy();
+ },
+
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || (me.nodename === nodename)) {
+ return;
+ }
+
+ me.nodename = nodename;
+ me.updateProxy();
+ },
+
+ updateProxy: function() {
+ var me = this;
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/hardware/pci/' + me.pciid + '/mdev'
+ });
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw 'no node name specified';
+ }
+
+ me.callParent();
+
+ if (me.pciid) {
+ me.setPciID(me.pciid, true);
+ }
+ }
+});
+
+Ext.define('PVE.form.SecurityGroupsSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveSecurityGroupsSelector'],
+
+ valueField: 'group',
+ displayField: 'group',
+ initComponent: function() {
+ var me = this;
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ fields: [ 'group', 'comment' ],
+ idProperty: 'group',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/firewall/groups"
+ },
+ sorters: {
+ property: 'group',
+ order: 'DESC'
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Security Group'),
+ dataIndex: 'group',
+ hideable: false,
+ width: 100
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.form.IPRefSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveIPRefSelector'],
+
+ base_url: undefined,
+
+ preferredValue: '', // hack: else Form sets dirty flag?
+
+ ref_type: undefined, // undefined = any [undefined, 'ipset' or 'alias']
+
+ valueField: 'ref',
+ displayField: 'ref',
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.base_url) {
+ throw "no base_url specified";
+ }
+
+ var url = "/api2/json" + me.base_url;
+ if (me.ref_type) {
+ url += "?type=" + me.ref_type;
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ fields: [ 'type', 'name', 'ref', 'comment' ],
+ idProperty: 'ref',
+ proxy: {
+ type: 'proxmox',
+ url: url
+ },
+ sorters: {
+ property: 'ref',
+ order: 'DESC'
+ }
+ });
+
+ var disable_query_for_ips = function(f, value) {
+ if (value === null ||
+ value.match(/^\d/)) { // IP address starts with \d
+ f.queryDelay = 9999999999; // hack: disable with long delay
+ } else {
+ f.queryDelay = 10;
+ }
+ };
+
+ var columns = [];
+
+ if (!me.ref_type) {
+ columns.push({
+ header: gettext('Type'),
+ dataIndex: 'type',
+ hideable: false,
+ width: 60
+ });
+ }
+
+ columns.push(
+ {
+ header: gettext('Name'),
+ dataIndex: 'ref',
+ hideable: false,
+ width: 140
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ );
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: { columns: columns }
+ });
+
+ me.on('change', disable_query_for_ips);
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.form.IPProtocolSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveIPProtocolSelector'],
+ valueField: 'p',
+ displayField: 'p',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Protocol'),
+ dataIndex: 'p',
+ hideable: false,
+ sortable: false,
+ width: 100
+ },
+ {
+ header: gettext('Number'),
+ dataIndex: 'n',
+ hideable: false,
+ sortable: false,
+ width: 50
+ },
+ {
+ header: gettext('Description'),
+ dataIndex: 'd',
+ hideable: false,
+ sortable: false,
+ flex: 1
+ }
+ ]
+ },
+ store: {
+ fields: [ 'p', 'd', 'n'],
+ data: [
+ { p: 'tcp', n: 6, d: 'Transmission Control Protocol' },
+ { p: 'udp', n: 17, d: 'User Datagram Protocol' },
+ { p: 'icmp', n: 1, d: 'Internet Control Message Protocol' },
+ { p: 'igmp', n: 2, d: 'Internet Group Management' },
+ { p: 'ggp', n: 3, d: 'gateway-gateway protocol' },
+ { p: 'ipencap', n: 4, d: 'IP encapsulated in IP' },
+ { p: 'st', n: 5, d: 'ST datagram mode' },
+ { p: 'egp', n: 8, d: 'exterior gateway protocol' },
+ { p: 'igp', n: 9, d: 'any private interior gateway (Cisco)' },
+ { p: 'pup', n: 12, d: 'PARC universal packet protocol' },
+ { p: 'hmp', n: 20, d: 'host monitoring protocol' },
+ { p: 'xns-idp', n: 22, d: 'Xerox NS IDP' },
+ { p: 'rdp', n: 27, d: '"reliable datagram" protocol' },
+ { p: 'iso-tp4', n: 29, d: 'ISO Transport Protocol class 4 [RFC905]' },
+ { p: 'dccp', n: 33, d: 'Datagram Congestion Control Prot. [RFC4340]' },
+ { p: 'xtp', n: 36, d: 'Xpress Transfer Protocol' },
+ { p: 'ddp', n: 37, d: 'Datagram Delivery Protocol' },
+ { p: 'idpr-cmtp', n: 38, d: 'IDPR Control Message Transport' },
+ { p: 'ipv6', n: 41, d: 'Internet Protocol, version 6' },
+ { p: 'ipv6-route', n: 43, d: 'Routing Header for IPv6' },
+ { p: 'ipv6-frag', n: 44, d: 'Fragment Header for IPv6' },
+ { p: 'idrp', n: 45, d: 'Inter-Domain Routing Protocol' },
+ { p: 'rsvp', n: 46, d: 'Reservation Protocol' },
+ { p: 'gre', n: 47, d: 'General Routing Encapsulation' },
+ { p: 'esp', n: 50, d: 'Encap Security Payload [RFC2406]' },
+ { p: 'ah', n: 51, d: 'Authentication Header [RFC2402]' },
+ { p: 'skip', n: 57, d: 'SKIP' },
+ { p: 'ipv6-icmp', n: 58, d: 'ICMP for IPv6' },
+ { p: 'ipv6-nonxt', n: 59, d: 'No Next Header for IPv6' },
+ { p: 'ipv6-opts', n: 60, d: 'Destination Options for IPv6' },
+ { p: 'vmtp', n: 81, d: 'Versatile Message Transport' },
+ { p: 'eigrp', n: 88, d: 'Enhanced Interior Routing Protocol (Cisco)' },
+ { p: 'ospf', n: 89, d: 'Open Shortest Path First IGP' },
+ { p: 'ax.25', n: 93, d: 'AX.25 frames' },
+ { p: 'ipip', n: 94, d: 'IP-within-IP Encapsulation Protocol' },
+ { p: 'etherip', n: 97, d: 'Ethernet-within-IP Encapsulation [RFC3378]' },
+ { p: 'encap', n: 98, d: 'Yet Another IP encapsulation [RFC1241]' },
+ { p: 'pim', n: 103, d: 'Protocol Independent Multicast' },
+ { p: 'ipcomp', n: 108, d: 'IP Payload Compression Protocol' },
+ { p: 'vrrp', n: 112, d: 'Virtual Router Redundancy Protocol [RFC5798]' },
+ { p: 'l2tp', n: 115, d: 'Layer Two Tunneling Protocol [RFC2661]' },
+ { p: 'isis', n: 124, d: 'IS-IS over IPv4' },
+ { p: 'sctp', n: 132, d: 'Stream Control Transmission Protocol' },
+ { p: 'fc', n: 133, d: 'Fibre Channel' },
+ { p: 'mobility-header', n: 135, d: 'Mobility Support for IPv6 [RFC3775]' },
+ { p: 'udplite', n: 136, d: 'UDP-Lite [RFC3828]' },
+ { p: 'mpls-in-ip', n: 137, d: 'MPLS-in-IP [RFC4023]' },
+ { p: 'hip', n: 139, d: 'Host Identity Protocol' },
+ { p: 'shim6', n: 140, d: 'Shim6 Protocol [RFC5533]' },
+ { p: 'wesp', n: 141, d: 'Wrapped Encapsulating Security Payload' },
+ { p: 'rohc', n: 142, d: 'Robust Header Compression' }
+ ]
+ }
+});
+Ext.define('PVE.form.CPUModelSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.CPUModelSelector'],
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + ' (kvm64)'],
+ ['486', '486'],
+ ['athlon', 'athlon'],
+ ['core2duo', 'core2duo'],
+ ['coreduo', 'coreduo'],
+ ['kvm32', 'kvm32'],
+ ['kvm64', 'kvm64'],
+ ['pentium', 'pentium'],
+ ['pentium2', 'pentium2'],
+ ['pentium3', 'pentium3'],
+ ['phenom', 'phenom'],
+ ['qemu32', 'qemu32'],
+ ['qemu64', 'qemu64'],
+ ['Conroe', 'Conroe'],
+ ['Penryn', 'Penryn'],
+ ['Nehalem', 'Nehalem'],
+ ['Westmere', 'Westmere'],
+ ['SandyBridge', 'SandyBridge'],
+ ['IvyBridge', 'IvyBridge'],
+ ['Haswell', 'Haswell'],
+ ['Haswell-noTSX','Haswell-noTSX'],
+ ['Broadwell', 'Broadwell'],
+ ['Broadwell-noTSX','Broadwell-noTSX'],
+ ['Skylake-Client','Skylake-Client'],
+ ['Opteron_G1', 'Opteron_G1'],
+ ['Opteron_G2', 'Opteron_G2'],
+ ['Opteron_G3', 'Opteron_G3'],
+ ['Opteron_G4', 'Opteron_G4'],
+ ['Opteron_G5', 'Opteron_G5'],
+ ['EPYC', 'EPYC'],
+ ['host', 'host']
+
+ ]
+});
+Ext.define('PVE.form.VNCKeyboardSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.VNCKeyboardSelector'],
+ comboItems: PVE.Utils.kvm_keymap_array()
+});
+Ext.define('PVE.form.CacheTypeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.CacheTypeSelector'],
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + " (" + gettext('No cache') + ")"],
+ ['directsync', 'Direct sync'],
+ ['writethrough', 'Write through'],
+ ['writeback', 'Write back'],
+ ['unsafe', 'Write back (' + gettext('unsafe') + ')'],
+ ['none', gettext('No cache')]
+ ]
+});
+Ext.define('PVE.form.SnapshotSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.PVE.form.SnapshotSelector'],
+
+ valueField: 'name',
+ displayField: 'name',
+
+ loadStore: function(nodename, vmid) {
+ var me = this;
+
+ if (!nodename) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ if (!vmid) {
+ return;
+ }
+
+ me.vmid = vmid;
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid +'/snapshot'
+ });
+
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.guestType) {
+ throw "no guest type specified";
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: [ 'name'],
+ filterOnLoad: true
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Snapshot'),
+ dataIndex: 'name',
+ hideable: false,
+ flex: 1
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+
+ me.loadStore(me.nodename, me.vmid);
+ }
+});
+Ext.define('PVE.form.ContentTypeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveContentTypeSelector'],
+
+ cts: undefined,
+
+ initComponent: function() {
+ var me = this;
+
+ me.comboItems = [];
+
+ if (me.cts === undefined) {
+ me.cts = ['images', 'iso', 'vztmpl', 'backup', 'rootdir', 'snippets'];
+ }
+
+ Ext.Array.each(me.cts, function(ct) {
+ me.comboItems.push([ct, PVE.Utils.format_content_types(ct)]);
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.form.HotplugFeatureSelector', {
+ extend: 'Ext.form.CheckboxGroup',
+ alias: 'widget.pveHotplugFeatureSelector',
+
+ columns: 1,
+ vertical: true,
+
+ defaults: {
+ name: 'hotplug',
+ submitValue: false
+ },
+ items: [
+ {
+ boxLabel: gettext('Disk'),
+ inputValue: 'disk',
+ checked: true
+ },
+ {
+ boxLabel: gettext('Network'),
+ inputValue: 'network',
+ checked: true
+ },
+ {
+ boxLabel: 'USB',
+ inputValue: 'usb',
+ checked: true
+ },
+ {
+ boxLabel: gettext('Memory'),
+ inputValue: 'memory'
+ },
+ {
+ boxLabel: gettext('CPU'),
+ inputValue: 'cpu'
+ }
+ ],
+
+ setValue: function(value) {
+ var me = this;
+ var newVal = [];
+ if (value === '1') {
+ newVal = ['disk', 'network', 'usb'];
+ } else if (value !== '0') {
+ newVal = value.split(',');
+ }
+ me.callParent([{ hotplug: newVal }]);
+ },
+
+ // override framework function to
+ // assemble the hotplug value
+ getSubmitData: function() {
+ var me = this,
+ boxes = me.getBoxes(),
+ data = [];
+ Ext.Array.forEach(boxes, function(box){
+ if (box.getValue()) {
+ data.push(box.inputValue);
+ }
+ });
+
+ /* because above is hotplug an array */
+ /*jslint confusion: true*/
+ if (data.length === 0) {
+ return { 'hotplug':'0' };
+ } else {
+ return { 'hotplug': data.join(',') };
+ }
+ }
+
+});
+Ext.define('PVE.form.AgentFeatureSelector', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: ['widget.pveAgentFeatureSelector'],
+
+ initComponent: function() {
+ var me = this;
+ me.items= [
+ {
+ xtype: 'proxmoxcheckbox',
+ boxLabel: gettext('Qemu Agent'),
+ name: 'enabled',
+ uncheckedValue: 0,
+ listeners: {
+ change: function(f, value, old) {
+ var gtcb = me.down('proxmoxcheckbox[name=fstrim_cloned_disks]');
+ if (value) {
+ gtcb.setDisabled(false);
+ } else {
+ gtcb.setDisabled(true);
+ }
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ boxLabel: gettext('Run guest-trim after clone disk'),
+ name: 'fstrim_cloned_disks',
+ disabled: true
+ }
+ ];
+ me.callParent();
+ },
+
+ onGetValues: function(values) {
+ var agentstr = PVE.Parser.printPropertyString(values, 'enabled');
+ return { agent: agentstr };
+ },
+
+ setValues: function(values) {
+ var agent = values.agent || '';
+ var res = PVE.Parser.parsePropertyString(agent, 'enabled');
+ this.callParent([res]);
+ }
+});
+Ext.define('PVE.form.iScsiProviderSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveiScsiProviderSelector'],
+ comboItems: [
+ ['comstar', 'Comstar'],
+ [ 'istgt', 'istgt'],
+ [ 'iet', 'IET'],
+ [ 'LIO', 'LIO']
+ ]
+});
+Ext.define('PVE.form.DayOfWeekSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveDayOfWeekSelector'],
+ comboItems:[],
+ initComponent: function(){
+ var me = this;
+ me.comboItems = [
+ ['mon', Ext.util.Format.htmlDecode(Ext.Date.dayNames[1])],
+ ['tue', Ext.util.Format.htmlDecode(Ext.Date.dayNames[2])],
+ ['wed', Ext.util.Format.htmlDecode(Ext.Date.dayNames[3])],
+ ['thu', Ext.util.Format.htmlDecode(Ext.Date.dayNames[4])],
+ ['fri', Ext.util.Format.htmlDecode(Ext.Date.dayNames[5])],
+ ['sat', Ext.util.Format.htmlDecode(Ext.Date.dayNames[6])],
+ ['sun', Ext.util.Format.htmlDecode(Ext.Date.dayNames[0])]
+ ];
+ this.callParent();
+ }
+});
+Ext.define('PVE.form.BackupModeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveBackupModeSelector'],
+ comboItems: [
+ ['snapshot', gettext('Snapshot')],
+ ['suspend', gettext('Suspend')],
+ ['stop', gettext('Stop')]
+ ]
+});
+Ext.define('PVE.form.ScsiHwSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveScsiHwSelector'],
+ comboItems: [
+ ['__default__', PVE.Utils.render_scsihw('')],
+ ['lsi', PVE.Utils.render_scsihw('lsi')],
+ ['lsi53c810', PVE.Utils.render_scsihw('lsi53c810')],
+ ['megasas', PVE.Utils.render_scsihw('megasas')],
+ ['virtio-scsi-pci', PVE.Utils.render_scsihw('virtio-scsi-pci')],
+ ['virtio-scsi-single', PVE.Utils.render_scsihw('virtio-scsi-single')],
+ ['pvscsi', PVE.Utils.render_scsihw('pvscsi')]
+ ]
+});
+Ext.define('PVE.form.FirewallPolicySelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveFirewallPolicySelector'],
+ comboItems: [
+ ['ACCEPT', 'ACCEPT'],
+ ['REJECT', 'REJECT'],
+ [ 'DROP', 'DROP']
+ ]
+});
+/*
+ * This is a global search field
+ * it loads the /cluster/resources on focus
+ * and displays the result in a floating grid
+ *
+ * it filters and sorts the objects by the algorithm in
+ * the customFilter function
+ *
+ * also it does accept key up/down and enter for input
+ * and it opens to ctrl+shift+f and ctrl+space
+ */
+Ext.define('PVE.form.GlobalSearchField', {
+ extend: 'Ext.form.field.Text',
+ alias: 'widget.pveGlobalSearchField',
+
+ emptyText: gettext('Search'),
+ enableKeyEvents: true,
+ selectOnFocus: true,
+ padding: '0 5 0 5',
+
+ grid: {
+ xtype: 'gridpanel',
+ focusOnToFront: false,
+ floating: true,
+ emptyText: Proxmox.Utils.noneText,
+ width: 600,
+ height: 400,
+ scrollable: {
+ xtype: 'scroller',
+ y: true,
+ x:false
+ },
+ store: {
+ model: 'PVEResources',
+ proxy:{
+ type: 'proxmox',
+ url: '/api2/extjs/cluster/resources'
+ }
+ },
+ plugins: {
+ ptype: 'bufferedrenderer',
+ trailingBufferZone: 20,
+ leadingBufferZone: 20
+ },
+
+ hideMe: function() {
+ var me = this;
+ if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) {
+ return;
+ }
+ me.hasFocus = false;
+ if (!me.textfield.hasFocus) {
+ me.hide();
+ }
+ },
+
+ setFocus: function() {
+ var me = this;
+ me.hasFocus = true;
+ },
+
+ listeners: {
+ rowclick: function(grid, record) {
+ var me = this;
+ me.textfield.selectAndHide(record.id);
+ },
+ itemcontextmenu: function(v, record, item, index, event) {
+ var me = this;
+ me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event);
+ },
+ /* because of lint */
+ focusleave: {
+ fn: 'hideMe'
+ },
+ focusenter: 'setFocus'
+ },
+
+ columns: [
+ {
+ text: gettext('Type'),
+ dataIndex: 'type',
+ width: 100,
+ renderer: PVE.Utils.render_resource_type
+ },
+ {
+ text: gettext('Description'),
+ flex: 1,
+ dataIndex: 'text'
+ },
+ {
+ text: gettext('Node'),
+ dataIndex: 'node'
+ },
+ {
+ text: gettext('Pool'),
+ dataIndex: 'pool'
+ }
+ ]
+ },
+
+ customFilter: function(item) {
+ var me = this;
+ var match = 0;
+ var fieldArr = [];
+ var i,j, fields;
+
+ // different types of objects have different fields to search
+ // for example, a node will never have a pool and vice versa
+ switch (item.data.type) {
+ case 'pool': fieldArr = ['type', 'pool', 'text']; break;
+ case 'node': fieldArr = ['type', 'node', 'text']; break;
+ case 'storage': fieldArr = ['type', 'pool', 'node', 'storage']; break;
+ default: fieldArr = ['name', 'type', 'node', 'pool', 'vmid'];
+ }
+ if (me.filterVal === '') {
+ item.data.relevance = 0;
+ return true;
+ }
+
+ // all text is case insensitive and each word is
+ // searched alone
+ // for every partial match, the row gets
+ // 1 match point, for every exact match
+ // it gets 2 points
+ //
+ // results gets sorted by points (descending)
+ fields = me.filterVal.split(/\s+/);
+ for(i = 0; i < fieldArr.length; i++) {
+ var v = item.data[fieldArr[i]];
+ if (v !== undefined) {
+ v = v.toString().toLowerCase();
+ for(j = 0; j < fields.length; j++) {
+ if (v.indexOf(fields[j]) !== -1) {
+ match++;
+ if(v === fields[j]) {
+ match++;
+ }
+ }
+ }
+ }
+ }
+ // give the row the 'relevance' value
+ item.data.relevance = match;
+ return (match > 0);
+ },
+
+ updateFilter: function(field, newValue, oldValue) {
+ var me = this;
+ // parse input and filter store,
+ // show grid
+ me.grid.store.filterVal = newValue.toLowerCase().trim();
+ me.grid.store.clearFilter(true);
+ me.grid.store.filterBy(me.customFilter);
+ me.grid.getSelectionModel().select(0);
+ },
+
+ selectAndHide: function(id) {
+ var me = this;
+ me.tree.selectById(id);
+ me.grid.hide();
+ me.setValue('');
+ me.blur();
+ },
+
+ onKey: function(field, e) {
+ var me = this;
+ var key = e.getKey();
+
+ switch(key) {
+ case Ext.event.Event.ENTER:
+ // go to first entry if there is one
+ if (me.grid.store.getCount() > 0) {
+ me.selectAndHide(me.grid.getSelection()[0].data.id);
+ }
+ break;
+ case Ext.event.Event.UP:
+ me.grid.getSelectionModel().selectPrevious();
+ break;
+ case Ext.event.Event.DOWN:
+ me.grid.getSelectionModel().selectNext();
+ break;
+ case Ext.event.Event.ESC:
+ me.grid.hide();
+ me.blur();
+ break;
+ }
+ },
+
+ loadValues: function(field) {
+ var me = this;
+ var records = [];
+
+ me.hasFocus = true;
+ me.grid.textfield = me;
+ me.grid.store.load();
+ me.grid.showBy(me, 'tl-bl');
+ },
+
+ hideGrid: function() {
+ var me = this;
+
+ me.hasFocus = false;
+ if (!me.grid.hasFocus) {
+ me.grid.hide();
+ }
+ },
+
+ listeners: {
+ change: {
+ fn: 'updateFilter',
+ buffer: 250
+ },
+ specialkey: 'onKey',
+ focusenter: 'loadValues',
+ focusleave: {
+ fn: 'hideGrid',
+ delay: 100
+ }
+ },
+
+ toggleFocus: function() {
+ var me = this;
+ if (!me.hasFocus) {
+ me.focus();
+ } else {
+ me.blur();
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.tree) {
+ throw "no tree given";
+ }
+
+ me.grid = Ext.create(me.grid);
+
+ me.callParent();
+
+ /*jslint confusion: true*/
+ /*because shift is also a function*/
+ // bind ctrl+shift+f and ctrl+space
+ // to open/close the search
+ me.keymap = new Ext.KeyMap({
+ target: Ext.get(document),
+ binding: [{
+ key:'F',
+ ctrl: true,
+ shift: true,
+ fn: me.toggleFocus,
+ scope: me
+ },{
+ key:' ',
+ ctrl: true,
+ fn: me.toggleFocus,
+ scope: me
+ }]
+ });
+
+ // always select first item and
+ // sort by relevance after load
+ me.mon(me.grid.store, 'load', function() {
+ me.grid.getSelectionModel().select(0);
+ me.grid.store.sort({
+ property: 'relevance',
+ direction: 'DESC'
+ });
+ });
+ }
+
+});
+Ext.define('PVE.form.QemuBiosSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveQemuBiosSelector'],
+
+ initComponent: function() {
+ var me = this;
+
+ me.comboItems = [
+ ['__default__', PVE.Utils.render_qemu_bios('')],
+ ['seabios', PVE.Utils.render_qemu_bios('seabios')],
+ ['ovmf', PVE.Utils.render_qemu_bios('ovmf')]
+ ];
+
+ me.callParent();
+ }
+});
+/*jslint confusion: true*/
+/* filter is a javascript builtin, but extjs calls it also filter */
+Ext.define('PVE.form.VMSelector', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.vmselector',
+
+ mixins: {
+ field: 'Ext.form.field.Field'
+ },
+
+ allowBlank: true,
+ selectAll: false,
+ isFormField: true,
+
+ plugins: 'gridfilters',
+
+ store: {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ filters: [{
+ property: 'type',
+ value: /lxc|qemu/
+ }]
+ },
+ columns: [
+ {
+ header: 'ID',
+ dataIndex: 'vmid',
+ width: 80,
+ filter: {
+ type: 'number'
+ }
+ },
+ {
+ header: gettext('Node'),
+ dataIndex: 'node'
+ },
+ {
+ header: gettext('Status'),
+ dataIndex: 'status',
+ filter: {
+ type: 'list'
+ }
+ },
+ {
+ header: gettext('Name'),
+ dataIndex: 'name',
+ flex: 1,
+ filter: {
+ type: 'string'
+ }
+ },
+ {
+ header: gettext('Pool'),
+ dataIndex: 'pool',
+ filter: {
+ type: 'list'
+ }
+ },
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ width: 120,
+ renderer: function(value) {
+ if (value === 'qemu') {
+ return gettext('Virtual Machine');
+ } else if (value === 'lxc') {
+ return gettext('LXC Container');
+ }
+
+ return '';
+ },
+ filter: {
+ type: 'list',
+ store: {
+ data: [
+ {id: 'qemu', text: gettext('Virtual Machine')},
+ {id: 'lxc', text: gettext('LXC Container')}
+ ],
+ // due to EXTJS-18711
+ // we have to do a static list via a store
+ // but to avoid creating an object,
+ // we have to have a pseudo un function
+ un: function(){}
+ }
+ }
+ },
+ {
+ header: 'HA ' + gettext('Status'),
+ dataIndex: 'hastate',
+ flex: 1,
+ filter: {
+ type: 'list'
+ }
+ }
+ ],
+
+ selModel: {
+ selType: 'checkboxmodel',
+ mode: 'SIMPLE'
+ },
+
+ checkChangeEvents: [
+ 'selectionchange',
+ 'change'
+ ],
+
+ listeners: {
+ selectionchange: function() {
+ // to trigger validity and error checks
+ this.checkChange();
+ }
+ },
+
+ getValue: function() {
+ var me = this;
+ var sm = me.getSelectionModel();
+ var selection = sm.getSelection();
+ var values = [];
+ var store = me.getStore();
+ selection.forEach(function(item) {
+ // only add if not filtered
+ if (store.findExact('vmid', item.data.vmid) !== -1) {
+ values.push(item.data.vmid);
+ }
+ });
+ return values;
+ },
+
+ setValue: function(value) {
+ console.log(value);
+ var me = this;
+ var sm = me.getSelectionModel();
+ if (!Ext.isArray(value)) {
+ value = value.split(',');
+ }
+ var selection = [];
+ var store = me.getStore();
+
+ value.forEach(function(item) {
+ var rec = store.findRecord('vmid',item, 0, false, true, true);
+ console.log(store);
+
+ if (rec) {
+ console.log(rec);
+ selection.push(rec);
+ }
+ });
+
+ sm.select(selection);
+
+ return me.mixins.field.setValue.call(me, value);
+ },
+
+ getErrors: function(value) {
+ var me = this;
+ if (me.allowBlank === false &&
+ me.getSelectionModel().getCount() === 0) {
+ me.addBodyCls(['x-form-trigger-wrap-default','x-form-trigger-wrap-invalid']);
+ return [gettext('No VM selected')];
+ }
+
+ me.removeBodyCls(['x-form-trigger-wrap-default','x-form-trigger-wrap-invalid']);
+ return [];
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+
+ if (me.nodename) {
+ me.store.filters.add({
+ property: 'node',
+ exactMatch: true,
+ value: me.nodename
+ });
+ }
+
+ // only show the relevant guests by default
+ if (me.action) {
+ var statusfilter = '';
+ switch (me.action) {
+ case 'startall':
+ statusfilter = 'stopped';
+ break;
+ case 'stopall':
+ statusfilter = 'running';
+ break;
+ }
+ if (statusfilter !== '') {
+ me.store.filters.add({
+ property: 'template',
+ value: 0
+ },{
+ id: 'x-gridfilter-status',
+ operator: 'in',
+ property: 'status',
+ value: [statusfilter]
+ });
+ }
+ }
+
+ var store = me.getStore();
+ var sm = me.getSelectionModel();
+
+ if (me.selectAll) {
+ me.mon(store,'load', function(){
+ me.getSelectionModel().selectAll(false);
+ });
+ }
+ }
+});
+
+
+Ext.define('PVE.form.VMComboSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: 'widget.vmComboSelector',
+
+ valueField: 'vmid',
+ displayField: 'vmid',
+
+ autoSelect: false,
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+
+ store: {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ filters: [{
+ property: 'type',
+ value: /lxc|qemu/
+ }]
+ },
+
+ listConfig: {
+ width: 600,
+ plugins: 'gridfilters',
+ columns: [
+ {
+ header: 'ID',
+ dataIndex: 'vmid',
+ width: 80,
+ filter: {
+ type: 'number'
+ }
+ },
+ {
+ header: gettext('Name'),
+ dataIndex: 'name',
+ flex: 1,
+ filter: {
+ type: 'string'
+ }
+ },
+ {
+ header: gettext('Node'),
+ dataIndex: 'node'
+ },
+ {
+ header: gettext('Status'),
+ dataIndex: 'status',
+ filter: {
+ type: 'list'
+ }
+ },
+ {
+ header: gettext('Pool'),
+ dataIndex: 'pool',
+ hidden: true,
+ filter: {
+ type: 'list'
+ }
+ },
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ width: 120,
+ renderer: function(value) {
+ if (value === 'qemu') {
+ return gettext('Virtual Machine');
+ } else if (value === 'lxc') {
+ return gettext('LXC Container');
+ }
+
+ return '';
+ },
+ filter: {
+ type: 'list',
+ store: {
+ data: [
+ {id: 'qemu', text: gettext('Virtual Machine')},
+ {id: 'lxc', text: gettext('LXC Container')}
+ ],
+ un: function(){} // due to EXTJS-18711
+ }
+ }
+ },
+ {
+ header: 'HA ' + gettext('Status'),
+ dataIndex: 'hastate',
+ hidden: true,
+ flex: 1,
+ filter: {
+ type: 'list'
+ }
+ }
+ ]
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.form.VMCPUFlagSelector', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.vmcpuflagselector',
+
+ mixins: {
+ field: 'Ext.form.field.Field'
+ },
+
+ disableSelection: true,
+ columnLines: false,
+ selectable: false,
+ hideHeaders: true,
+
+ scrollable: 'y',
+ height: 200,
+
+ unkownFlags: [],
+
+ store: {
+ type: 'store',
+ fields: ['flag', { name: 'state', defaultValue: '=' }, 'desc'],
+ data: [
+ // FIXME: let qemu-server host this and autogenerate or get from API call??
+ { flag: 'md-clear', desc: 'Required to let the guest OS know if MDS is mitigated correctly' },
+ { flag: 'pcid', desc: 'Meltdown fix cost reduction on Westmere, Sandy-, and IvyBridge Intel CPUs' },
+ { flag: 'spec-ctrl', desc: 'Allows improved Spectre mitigation with Intel CPUs' },
+ { flag: 'ssbd', desc: 'Protection for "Speculative Store Bypass" for Intel models' },
+ { flag: 'ibpb', desc: 'Allows improved Spectre mitigation with AMD CPUs' },
+ { flag: 'virt-ssbd', desc: 'Basis for "Speculative Store Bypass" protection for AMD models' },
+ { flag: 'amd-ssbd', desc: 'Improves Spectre mitigation performance with AMD CPUs, best used with "virt-ssbd"' },
+ { flag: 'amd-no-ssb', desc: 'Notifies guest OS that host is not vulnerable for Spectre on AMD CPUs' },
+ { flag: 'pdpe1gb', desc: 'Allow guest OS to use 1GB size pages, if host HW supports it' }
+ ],
+ listeners: {
+ update: function() {
+ this.commitChanges();
+ }
+ }
+ },
+
+ getValue: function() {
+ var me = this;
+ var store = me.getStore();
+ var flags = '';
+
+ // ExtJS does not has a nice getAllRecords interface for stores :/
+ store.queryBy(Ext.returnTrue).each(function(rec) {
+ var s = rec.get('state');
+ if (s && s !== '=') {
+ var f = rec.get('flag');
+ if (flags === '') {
+ flags = s + f;
+ } else {
+ flags += ';' + s + f;
+ }
+ }
+ });
+
+ flags += me.unkownFlags.join(';');
+
+ return flags;
+ },
+
+ setValue: function(value) {
+ var me = this;
+ var store = me.getStore();
+
+ me.value = value || '';
+
+ me.unkownFlags = [];
+
+ me.getStore().queryBy(Ext.returnTrue).each(function(rec) {
+ rec.set('state', '=');
+ });
+
+ var flags = value ? value.split(';') : [];
+ flags.forEach(function(flag) {
+ var sign = flag.substr(0, 1);
+ flag = flag.substr(1);
+
+ var rec = store.findRecord('flag', flag);
+ if (rec !== null) {
+ rec.set('state', sign);
+ } else {
+ me.unkownFlags.push(flag);
+ }
+ });
+ store.reload();
+
+ var res = me.mixins.field.setValue.call(me, value);
+
+ return res;
+ },
+ columns: [
+ {
+ dataIndex: 'state',
+ renderer: function(v) {
+ switch(v) {
+ case '=': return 'Default';
+ case '-': return 'Off';
+ case '+': return 'On';
+ default: return 'Unknown';
+ }
+ },
+ width: 65
+ },
+ {
+ xtype: 'widgetcolumn',
+ dataIndex: 'state',
+ width: 95,
+ onWidgetAttach: function (column, widget, record) {
+ var val = record.get('state') || '=';
+ widget.down('[inputValue=' + val + ']').setValue(true);
+ // TODO: disable if selected CPU model and flag are incompatible
+ },
+ widget: {
+ xtype: 'radiogroup',
+ hideLabel: true,
+ layout: 'hbox',
+ validateOnChange: false,
+ value: '=',
+ listeners: {
+ change: function(f, value) {
+ var v = Object.values(value)[0];
+ f.getWidgetRecord().set('state', v);
+
+ var view = this.up('grid');
+ view.dirty = view.getValue() !== view.originalValue;
+ view.checkDirty();
+ //view.checkChange();
+ }
+ },
+ items: [
+ {
+ boxLabel: '-',
+ boxLabelAlign: 'before',
+ inputValue: '-'
+ },
+ {
+ checked: true,
+ inputValue: '='
+ },
+ {
+ boxLabel: '+',
+ inputValue: '+'
+ }
+ ]
+ }
+ },
+ {
+ dataIndex: 'flag',
+ width: 100
+ },
+ {
+ dataIndex: 'desc',
+ cellWrap: true,
+ flex: 1
+ }
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ // static class store, thus gets not recreated, so ensure defaults are set!
+ me.getStore().data.forEach(function(v) {
+ v.state = '=';
+ });
+
+ me.value = me.originalValue = '';
+
+ me.callParent(arguments);
+ }
+});
+Ext.define('PVE.form.USBSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveUSBSelector'],
+ allowBlank: false,
+ autoSelect: false,
+ displayField: 'usbid',
+ valueField: 'usbid',
+ editable: true,
+
+ getUSBValue: function() {
+ var me = this;
+ var rec = me.store.findRecord('usbid', me.value);
+ var val = 'host='+ me.value;
+ if (rec && rec.data.speed === "5000") {
+ val = 'host=' + me.value + ",usb3=1";
+ }
+ return val;
+ },
+
+ validator: function(value) {
+ var me = this;
+ if (me.type === 'device') {
+ return (/^[a-f0-9]{4}\:[a-f0-9]{4}$/i).test(value);
+ } else if (me.type === 'port') {
+ return (/^[0-9]+\-[0-9]+(\.[0-9]+)*$/).test(value);
+ }
+ return false;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+
+ if (!nodename) {
+ throw "no nodename specified";
+ }
+
+ if (me.type !== 'device' && me.type !== 'port') {
+ throw "no valid type specified";
+ }
+
+ var store = new Ext.data.Store({
+ model: 'pve-usb-' + me.type,
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/nodes/" + nodename + "/scan/usb"
+ },
+ filters: [
+ function (item) {
+ return !!item.data.usbpath && !!item.data.prodid && item.data['class'] != 9;
+ }
+ ]
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: (me.type === 'device')?gettext('Device'):gettext('Port'),
+ sortable: true,
+ dataIndex: 'usbid',
+ width: 80
+ },
+ {
+ header: gettext('Manufacturer'),
+ sortable: true,
+ dataIndex: 'manufacturer',
+ width: 100
+ },
+ {
+ header: gettext('Product'),
+ sortable: true,
+ dataIndex: 'product',
+ flex: 1
+ },
+ {
+ header: gettext('Speed'),
+ width: 70,
+ sortable: true,
+ dataIndex: 'speed',
+ renderer: function(value) {
+ if (value === "5000") {
+ return "USB 3.0";
+ } else if (value === "480") {
+ return "USB 2.0";
+ } else {
+ return "USB 1.x";
+ }
+ }
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+
+}, function() {
+
+ Ext.define('pve-usb-device', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name: 'usbid',
+ convert: function(val, data) {
+ if (val) {
+ return val;
+ }
+ return data.get('vendid') + ':' + data.get('prodid');
+ }
+ },
+ 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath',
+ { name: 'port' , type: 'number' },
+ { name: 'level' , type: 'number' },
+ { name: 'class' , type: 'number' },
+ { name: 'devnum' , type: 'number' },
+ { name: 'busnum' , type: 'number' }
+ ]
+ });
+
+ Ext.define('pve-usb-port', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name: 'usbid',
+ convert: function(val,data) {
+ if (val) {
+ return val;
+ }
+ return data.get('busnum') + '-' + data.get('usbpath');
+ }
+ },
+ 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath',
+ { name: 'port' , type: 'number' },
+ { name: 'level' , type: 'number' },
+ { name: 'class' , type: 'number' },
+ { name: 'devnum' , type: 'number' },
+ { name: 'busnum' , type: 'number' }
+ ]
+ });
+});
+Ext.define('PVE.form.CalendarEvent', {
+ extend: 'Ext.form.field.ComboBox',
+ xtype: 'pveCalendarEvent',
+
+ editable: true,
+
+ valueField: 'value',
+ displayField: 'text',
+ queryMode: 'local',
+
+ store: {
+ field: [ 'value', 'text'],
+ data: [
+ { value: '*/30', text: Ext.String.format(gettext("Every {0} minutes"), 30) },
+ { value: '*/2:00', text: gettext("Every two hours")},
+ { value: '2,22:30', text: gettext("Every day") + " 02:30, 22:30"},
+ { value: 'mon..fri', text: gettext("Monday to Friday") + " 00:00"},
+ { value: 'mon..fri */1:00', text: gettext("Monday to Friday") + ': ' + gettext("hourly")},
+ { value: 'sun 01:00', text: gettext("Sunday") + " 01:00"}
+ ]
+ },
+
+ tpl: [
+ '
'
+ ],
+
+ displayTpl: [
+ '{title}:
'
+ },
+ {
+ flex: 1,
+ xtype: 'cartesian',
+ height: '100%',
+ itemId: 'chart',
+ border: false,
+ axes: [
+ {
+ type: 'numeric',
+ position: 'left',
+ hidden: true,
+ minimum: 0
+ },
+ {
+ type: 'numeric',
+ position: 'bottom',
+ hidden: true
+ }
+ ],
+
+ store: {
+ data: {}
+ },
+
+ sprites: [{
+ id: 'valueSprite',
+ type: 'text',
+ text: '0 B/s',
+ textAlign: 'end',
+ textBaseline: 'middle',
+ fontSize: 14
+ }],
+
+ series: [{
+ type: 'line',
+ xField: 'time',
+ yField: 'val',
+ fill: 'true',
+ colors: ['#cfcfcf'],
+ tooltip: {
+ trackMouse: true,
+ renderer: function( tooltip, record, ctx) {
+ var me = this.getChart();
+ var date = new Date(record.data.time);
+ var value = me.up().renderer(record.data.val);
+ tooltip.setHtml(
+ me.up().title + ': ' + value + '
' +
+ Ext.Date.format(date, 'H:i:s')
+ );
+ }
+ },
+ style: {
+ lineWidth: 1.5,
+ opacity: 0.60
+ },
+ marker: {
+ opacity: 0,
+ scaling: 0.01,
+ fx: {
+ duration: 200,
+ easing: 'easeOut'
+ }
+ },
+ highlightCfg: {
+ opacity: 1,
+ scaling: 1.5
+ }
+ }]
+ }
+ ],
+
+ // the renderer for the tooltip and last value,
+ // default just the value
+ renderer: Ext.identityFn,
+
+ // show the last x seconds
+ // default is 5 minutes
+ timeFrame: 5*60,
+
+ addDataPoint: function(value, time) {
+ var me = this.chart;
+ var panel = me.up();
+ var now = new Date();
+ var begin = new Date(now.getTime() - (1000*panel.timeFrame));
+
+ me.store.add({
+ time: time || now.getTime(),
+ val: value || 0
+ });
+
+ // delete all old records when we have 20 times more datapoints
+ // than seconds in our timeframe (so even a subsecond graph does
+ // not trigger this often)
+ //
+ // records in the store do not take much space, but like this,
+ // we prevent a memory leak when someone has the site open for a long time
+ // with minimal graphical glitches
+ if (me.store.count() > panel.timeFrame * 20) {
+ var oldData = me.store.getData().createFiltered(function(item) {
+ return item.data.time < begin.getTime();
+ });
+
+ me.store.remove(oldData.getRange());
+ }
+
+ me.timeaxis.setMinimum(begin.getTime());
+ me.timeaxis.setMaximum(now.getTime());
+ me.valuesprite.setText(panel.renderer(value || 0).toString());
+ me.valuesprite.setAttributes({
+ x: me.getWidth() - 15,
+ y: me.getHeight()/2
+ }, true);
+ me.redraw();
+ },
+
+ setTitle: function(title) {
+ this.title = title;
+ var me = this.getComponent('title');
+ me.update({title: title});
+ },
+
+ initComponent: function(){
+ var me = this;
+ me.callParent();
+
+ if (me.title) {
+ me.getComponent('title').update({title: me.title});
+ }
+ me.chart = me.getComponent('chart');
+ me.chart.timeaxis = me.chart.getAxes()[1];
+ me.chart.valuesprite = me.chart.getSurface('chart').get('valueSprite');
+ if (me.color) {
+ me.chart.series[0].setStyle({
+ fill: me.color,
+ stroke: me.color
+ });
+ }
+ }
+});
+Ext.define('PVE.widget.Info',{
+ extend: 'Ext.container.Container',
+ alias: 'widget.pveInfoWidget',
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+
+ value: 0,
+ maximum: 1,
+ printBar: true,
+ items: [
+ {
+ xtype: 'component',
+ itemId: 'label',
+ data: {
+ title: '',
+ usage: '',
+ iconCls: undefined
+ },
+ tpl: [
+ '{title}
',
+ '',
+ '
',
+ '{text}'
+ ],
+
+ updateHealth: function(data) {
+ var me = this;
+ me.update(Ext.apply(me.data, data));
+ },
+
+ initComponent: function(){
+ var me = this;
+
+ if (me.title) {
+ me.config.data.title = me.title;
+ }
+
+ me.callParent();
+ }
+
+});
+/*global u2f*/
+Ext.define('PVE.window.LoginWindow', {
+ extend: 'Ext.window.Window',
+
+ controller: {
+
+ xclass: 'Ext.app.ViewController',
+
+ onLogon: function() {
+ var me = this;
+
+ var form = this.lookupReference('loginForm');
+ var unField = this.lookupReference('usernameField');
+ var saveunField = this.lookupReference('saveunField');
+ var view = this.getView();
+
+ if (!form.isValid()) {
+ return;
+ }
+
+ view.el.mask(gettext('Please wait...'), 'x-mask-loading');
+
+ // set or clear username
+ var sp = Ext.state.Manager.getProvider();
+ if (saveunField.getValue() === true) {
+ sp.set(unField.getStateId(), unField.getValue());
+ } else {
+ sp.clear(unField.getStateId());
+ }
+ sp.set(saveunField.getStateId(), saveunField.getValue());
+
+ form.submit({
+ failure: function(f, resp){
+ me.failure(resp);
+ },
+ success: function(f, resp){
+ view.el.unmask();
+
+ var data = resp.result.data;
+ if (Ext.isDefined(data.NeedTFA)) {
+ // Store first factor login information first:
+ data.LoggedOut = true;
+ Proxmox.Utils.setAuthData(data);
+
+ if (Ext.isDefined(data.U2FChallenge)) {
+ me.perform_u2f(data);
+ } else {
+ me.perform_otp();
+ }
+ } else {
+ me.success(data);
+ }
+ }
+ });
+
+ },
+ failure: function(resp) {
+ var me = this;
+ var view = me.getView();
+ view.el.unmask();
+ var handler = function() {
+ var uf = me.lookupReference('usernameField');
+ uf.focus(true, true);
+ };
+
+ Ext.MessageBox.alert(gettext('Error'),
+ gettext("Login failed. Please try again"),
+ handler);
+ },
+ success: function(data) {
+ var me = this;
+ var view = me.getView();
+ var handler = view.handler || Ext.emptyFn;
+ handler.call(me, data);
+ view.close();
+ },
+
+ perform_otp: function() {
+ var me = this;
+ var win = Ext.create('PVE.window.TFALoginWindow', {
+ onLogin: function(value) {
+ me.finish_tfa(value);
+ },
+ onCancel: function() {
+ Proxmox.LoggedOut = false;
+ Proxmox.Utils.authClear();
+ me.getView().show();
+ }
+ });
+ win.show();
+ },
+
+ perform_u2f: function(data) {
+ var me = this;
+ // Show the message:
+ var msg = Ext.Msg.show({
+ title: 'U2F: '+gettext('Verification'),
+ message: gettext('Please press the button on your U2F Device'),
+ buttons: []
+ });
+ var chlg = data.U2FChallenge;
+ var key = {
+ version: chlg.version,
+ keyHandle: chlg.keyHandle
+ };
+ u2f.sign(chlg.appId, chlg.challenge, [key], function(res) {
+ msg.close();
+ if (res.errorCode) {
+ Proxmox.Utils.authClear();
+ Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode));
+ return;
+ }
+ delete res.errorCode;
+ me.finish_tfa(JSON.stringify(res));
+ });
+ },
+ finish_tfa: function(res) {
+ var me = this;
+ var view = me.getView();
+ view.el.mask(gettext('Please wait...'), 'x-mask-loading');
+ var params = { response: res };
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/access/tfa',
+ params: params,
+ method: 'POST',
+ timeout: 5000, // it'll delay both success & failure
+ success: function(resp, opts) {
+ view.el.unmask();
+ // Fill in what we copy over from the 1st factor:
+ var data = resp.result.data;
+ data.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
+ data.username = Proxmox.UserName;
+ // Finish logging in:
+ me.success(data);
+ },
+ failure: function(resp, opts) {
+ Proxmox.Utils.authClear();
+ me.failure(resp);
+ }
+ });
+ },
+
+ control: {
+ 'field[name=username]': {
+ specialkey: function(f, e) {
+ if (e.getKey() === e.ENTER) {
+ var pf = this.lookupReference('passwordField');
+ if (!pf.getValue()) {
+ pf.focus(false);
+ }
+ }
+ }
+ },
+ 'field[name=lang]': {
+ change: function(f, value) {
+ var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
+ Ext.util.Cookies.set('PVELangCookie', value, dt);
+ this.getView().mask(gettext('Please wait...'), 'x-mask-loading');
+ window.location.reload();
+ }
+ },
+ 'button[reference=loginButton]': {
+ click: 'onLogon'
+ },
+ '#': {
+ show: function() {
+ var sp = Ext.state.Manager.getProvider();
+ var checkboxField = this.lookupReference('saveunField');
+ var unField = this.lookupReference('usernameField');
+
+ var checked = sp.get(checkboxField.getStateId());
+ checkboxField.setValue(checked);
+
+ if(checked === true) {
+ var username = sp.get(unField.getStateId());
+ unField.setValue(username);
+ var pwField = this.lookupReference('passwordField');
+ pwField.focus();
+ }
+ }
+ }
+ }
+ },
+
+ width: 400,
+
+ modal: true,
+
+ border: false,
+
+ draggable: true,
+
+ closable: false,
+
+ resizable: false,
+
+ layout: 'auto',
+
+ title: gettext('Proxmox VE Login'),
+
+ defaultFocus: 'usernameField',
+
+ defaultButton: 'loginButton',
+
+ items: [{
+ xtype: 'form',
+ layout: 'form',
+ url: '/api2/extjs/access/ticket',
+ reference: 'loginForm',
+
+ fieldDefaults: {
+ labelAlign: 'right',
+ allowBlank: false
+ },
+
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('User name'),
+ name: 'username',
+ itemId: 'usernameField',
+ reference: 'usernameField',
+ stateId: 'login-username'
+ },
+ {
+ xtype: 'textfield',
+ inputType: 'password',
+ fieldLabel: gettext('Password'),
+ name: 'password',
+ reference: 'passwordField'
+ },
+ {
+ xtype: 'pveRealmComboBox',
+ name: 'realm'
+ },
+ {
+ xtype: 'proxmoxLanguageSelector',
+ fieldLabel: gettext('Language'),
+ value: Ext.util.Cookies.get('PVELangCookie') || Proxmox.defaultLang || 'en',
+ name: 'lang',
+ reference: 'langField',
+ submitValue: false
+ }
+ ],
+ buttons: [
+ {
+ xtype: 'checkbox',
+ fieldLabel: gettext('Save User name'),
+ name: 'saveusername',
+ reference: 'saveunField',
+ stateId: 'login-saveusername',
+ labelWidth: 'auto',
+ labelAlign: 'right',
+ submitValue: false
+ },
+ {
+ text: gettext('Login'),
+ reference: 'loginButton'
+ }
+ ]
+ }]
+ });
+Ext.define('PVE.window.TFALoginWindow', {
+ extend: 'Ext.window.Window',
+
+ modal: true,
+ resizable: false,
+ title: 'Two-Factor Authentication',
+ layout: 'form',
+ defaultButton: 'loginButton',
+ defaultFocus: 'otpField',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ login: function() {
+ var me = this;
+ var view = me.getView();
+ view.onLogin(me.lookup('otpField').getValue());
+ view.close();
+ },
+ cancel: function() {
+ var me = this;
+ var view = me.getView();
+ view.onCancel();
+ view.close();
+ }
+ },
+
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Please enter your OTP verification code:'),
+ name: 'otp',
+ itemId: 'otpField',
+ reference: 'otpField',
+ allowBlank: false
+ }
+ ],
+
+ buttons: [
+ {
+ text: gettext('Login'),
+ reference: 'loginButton',
+ handler: 'login'
+ },
+ {
+ text: gettext('Cancel'),
+ handler: 'cancel'
+ }
+ ]
+});
+Ext.define('PVE.window.Wizard', {
+ extend: 'Ext.window.Window',
+
+ activeTitle: '', // used for automated testing
+
+ width: 700,
+ height: 510,
+
+ modal: true,
+ border: false,
+
+ draggable: true,
+ closable: true,
+ resizable: false,
+
+ layout: 'border',
+
+ getValues: function(dirtyOnly) {
+ var me = this;
+
+ var values = {};
+
+ var form = me.down('form').getForm();
+
+ form.getFields().each(function(field) {
+ if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) {
+ Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
+ }
+ });
+
+ Ext.Array.each(me.query('inputpanel'), function(panel) {
+ Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly));
+ });
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var tabs = me.items || [];
+ delete me.items;
+
+ /*
+ * Items may have the following functions:
+ * validator(): per tab custom validation
+ * onSubmit(): submit handler
+ * onGetValues(): overwrite getValues results
+ */
+
+ Ext.Array.each(tabs, function(tab) {
+ tab.disabled = true;
+ });
+ tabs[0].disabled = false;
+
+ var maxidx = 0;
+ var curidx = 0;
+
+ var check_card = function(card) {
+ var valid = true;
+ var fields = card.query('field, fieldcontainer');
+ if (card.isXType('fieldcontainer')) {
+ fields.unshift(card);
+ }
+ Ext.Array.each(fields, function(field) {
+ // Note: not all fielcontainer have isValid()
+ if (Ext.isFunction(field.isValid) && !field.isValid()) {
+ valid = false;
+ }
+ });
+
+ if (Ext.isFunction(card.validator)) {
+ return card.validator();
+ }
+
+ return valid;
+ };
+
+ var disable_at = function(card) {
+ var tp = me.down('#wizcontent');
+ var idx = tp.items.indexOf(card);
+ for(;idx < tp.items.getCount();idx++) {
+ var nc = tp.items.getAt(idx);
+ if (nc) {
+ nc.disable();
+ }
+ }
+ };
+
+ var tabchange = function(tp, newcard, oldcard) {
+ if (newcard.onSubmit) {
+ me.down('#next').setVisible(false);
+ me.down('#submit').setVisible(true);
+ } else {
+ me.down('#next').setVisible(true);
+ me.down('#submit').setVisible(false);
+ }
+ var valid = check_card(newcard);
+ me.down('#next').setDisabled(!valid);
+ me.down('#submit').setDisabled(!valid);
+ me.down('#back').setDisabled(tp.items.indexOf(newcard) == 0);
+
+ var idx = tp.items.indexOf(newcard);
+ if (idx > maxidx) {
+ maxidx = idx;
+ }
+ curidx = idx;
+
+ var next = idx + 1;
+ var ntab = tp.items.getAt(next);
+ if (valid && ntab && !newcard.onSubmit) {
+ ntab.enable();
+ }
+ };
+
+ if (me.subject && !me.title) {
+ me.title = Proxmox.Utils.dialog_title(me.subject, true, false);
+ }
+
+ var sp = Ext.state.Manager.getProvider();
+ var advchecked = sp.get('proxmox-advanced-cb');
+
+ Ext.apply(me, {
+ items: [
+ {
+ xtype: 'form',
+ region: 'center',
+ layout: 'fit',
+ border: false,
+ margins: '5 5 0 5',
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: [{
+ itemId: 'wizcontent',
+ xtype: 'tabpanel',
+ activeItem: 0,
+ bodyPadding: 10,
+ listeners: {
+ afterrender: function(tp) {
+ var atab = this.getActiveTab();
+ tabchange(tp, atab);
+ },
+ tabchange: function(tp, newcard, oldcard) {
+ tabchange(tp, newcard, oldcard);
+ }
+ },
+ items: tabs
+ }]
+ }
+ ],
+ fbar: [
+ {
+ xtype: 'proxmoxHelpButton',
+ itemId: 'help'
+ },
+ '->',
+ {
+ xtype: 'proxmoxcheckbox',
+ boxLabelAlign: 'before',
+ boxLabel: gettext('Advanced'),
+ value: advchecked,
+ listeners: {
+ change: function(cb, val) {
+ var tp = me.down('#wizcontent');
+ tp.query('inputpanel').forEach(function(ip) {
+ ip.setAdvancedVisible(val);
+ });
+
+ sp.set('proxmox-advanced-cb', val);
+ }
+ }
+ },
+ {
+ text: gettext('Back'),
+ disabled: true,
+ itemId: 'back',
+ minWidth: 60,
+ handler: function() {
+ var tp = me.down('#wizcontent');
+ var atab = tp.getActiveTab();
+ var prev = tp.items.indexOf(atab) - 1;
+ if (prev < 0) {
+ return;
+ }
+ var ntab = tp.items.getAt(prev);
+ if (ntab) {
+ tp.setActiveTab(ntab);
+ }
+ }
+ },
+ {
+ text: gettext('Next'),
+ disabled: true,
+ itemId: 'next',
+ minWidth: 60,
+ handler: function() {
+
+ var form = me.down('form').getForm();
+
+ var tp = me.down('#wizcontent');
+ var atab = tp.getActiveTab();
+ if (!check_card(atab)) {
+ return;
+ }
+
+ var next = tp.items.indexOf(atab) + 1;
+ var ntab = tp.items.getAt(next);
+ if (ntab) {
+ ntab.enable();
+ tp.setActiveTab(ntab);
+ }
+
+ }
+ },
+ {
+ text: gettext('Finish'),
+ minWidth: 60,
+ hidden: true,
+ itemId: 'submit',
+ handler: function() {
+ var tp = me.down('#wizcontent');
+ var atab = tp.getActiveTab();
+ atab.onSubmit();
+ }
+ }
+ ]
+ });
+ me.callParent();
+
+ Ext.Array.each(me.query('inputpanel'), function(panel) {
+ panel.setAdvancedVisible(advchecked);
+ });
+
+ Ext.Array.each(me.query('field'), function(field) {
+ var validcheck = function() {
+ var tp = me.down('#wizcontent');
+
+ // check tabs from current to the last enabled for validity
+ // since we might have changed a validity on a later one
+ var i;
+ for (i = curidx; i <= maxidx && i < tp.items.getCount(); i++) {
+ var tab = tp.items.getAt(i);
+ var valid = check_card(tab);
+
+ // only set the buttons on the current panel
+ if (i === curidx) {
+ me.down('#next').setDisabled(!valid);
+ me.down('#submit').setDisabled(!valid);
+ }
+
+ // if a panel is invalid, then disable it and all following,
+ // else enable it and go to the next
+ var ntab = tp.items.getAt(i + 1);
+ if (!valid) {
+ disable_at(ntab);
+ return;
+ } else if (ntab && !tab.onSubmit) {
+ ntab.enable();
+ }
+ }
+ };
+ field.on('change', validcheck);
+ field.on('validitychange', validcheck);
+ });
+ }
+});
+Ext.define('PVE.window.NotesEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent : function() {
+ var me = this;
+
+ Ext.apply(me, {
+ title: gettext('Notes'),
+ width: 600,
+ height: '400px',
+ resizable: true,
+ layout: 'fit',
+ defaultButton: undefined,
+ items: {
+ xtype: 'textarea',
+ name: 'description',
+ height: '100%',
+ value: '',
+ hideLabel: true
+ }
+ });
+
+ me.callParent();
+
+ me.load();
+ }
+});
+Ext.define('PVE.window.Backup', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.vmtype) {
+ throw "no VM type specified";
+ }
+
+ var storagesel = Ext.create('PVE.form.StorageSelector', {
+ nodename: me.nodename,
+ name: 'storage',
+ value: me.storage,
+ fieldLabel: gettext('Storage'),
+ storageContent: 'backup',
+ allowBlank: false
+ });
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: [
+ storagesel,
+ {
+ xtype: 'pveBackupModeSelector',
+ fieldLabel: gettext('Mode'),
+ value: 'snapshot',
+ name: 'mode'
+ },
+ {
+ xtype: 'pveCompressionSelector',
+ name: 'compress',
+ value: 'lzo',
+ fieldLabel: gettext('Compression')
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Send email to'),
+ name: 'mailto',
+ emptyText: Proxmox.Utils.noneText
+ }
+ ]
+ });
+
+ var form = me.formPanel.getForm();
+
+ var submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Backup'),
+ handler: function(){
+ var storage = storagesel.getValue();
+ var values = form.getValues();
+ var params = {
+ storage: storage,
+ vmid: me.vmid,
+ mode: values.mode,
+ remove: 0
+ };
+
+ if ( values.mailto ) {
+ params.mailto = values.mailto;
+ }
+
+ if (values.compress) {
+ params.compress = values.compress;
+ }
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/vzdump',
+ params: params,
+ method: 'POST',
+ failure: function (response, opts) {
+ Ext.Msg.alert('Error',response.htmlStatus);
+ },
+ success: function(response, options) {
+ // close later so we reload the grid
+ // after the task has completed
+ me.hide();
+
+ var upid = response.result.data;
+
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid,
+ listeners: {
+ close: function() {
+ me.close();
+ }
+ }
+ });
+ win.show();
+ }
+ });
+ }
+ });
+
+ var helpBtn = Ext.create('Proxmox.button.Help', {
+ onlineHelp: 'chapter_vzdump',
+ listenToGlobalEvent: false,
+ hidden: false
+ });
+
+ var title = gettext('Backup') + " " +
+ ((me.vmtype === 'lxc') ? "CT" : "VM") +
+ " " + me.vmid;
+
+ Ext.apply(me, {
+ title: title,
+ width: 350,
+ modal: true,
+ layout: 'auto',
+ border: false,
+ items: [ me.formPanel ],
+ buttons: [ helpBtn, '->', submitBtn ]
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.window.Restore', {
+ extend: 'Ext.window.Window', // fixme: Proxmox.window.Edit?
+
+ resizable: false,
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.volid) {
+ throw "no volume ID specified";
+ }
+
+ if (!me.vmtype) {
+ throw "no vmtype specified";
+ }
+
+ var storagesel = Ext.create('PVE.form.StorageSelector', {
+ nodename: me.nodename,
+ name: 'storage',
+ value: '',
+ fieldLabel: gettext('Storage'),
+ storageContent: (me.vmtype === 'lxc') ? 'rootdir' : 'images',
+ allowBlank: true
+ });
+
+ var IDfield;
+ if (me.vmid) {
+ IDfield = Ext.create('Ext.form.field.Display', {
+ name: 'vmid',
+ value: me.vmid,
+ fieldLabel: (me.vmtype === 'lxc') ? 'CT' : 'VM'
+ });
+ } else {
+ IDfield = Ext.create('PVE.form.GuestIDSelector', {
+ name: 'vmid',
+ guestType: me.vmtype,
+ loadNextFreeID: true,
+ validateExists: false
+ });
+ }
+
+ var items = [
+ {
+ xtype: 'displayfield',
+ value: me.volidText || me.volid,
+ fieldLabel: gettext('Source')
+ },
+ storagesel,
+ IDfield,
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'bwlimit',
+ fieldLabel: gettext('Read Limit (MiB/s)'),
+ minValue: 0,
+ emptyText: gettext('Defaults to target storage restore limit'),
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext("Use '0' to disable all bandwidth limits.")
+ }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'unique',
+ fieldLabel: gettext('Unique'),
+ hidden: !!me.vmid,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Autogenerate unique properties, e.g., MAC addresses')
+ },
+ checked: false
+ }
+ ];
+
+ /*jslint confusion: true*/
+ if (me.vmtype === 'lxc') {
+ items.push({
+ xtype: 'proxmoxcheckbox',
+ name: 'unprivileged',
+ value: true,
+ fieldLabel: gettext('Unprivileged container')
+ });
+ }
+ /*jslint confusion: false*/
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: items
+ });
+
+ var form = me.formPanel.getForm();
+
+ var doRestore = function(url, params) {
+ Proxmox.Utils.API2Request({
+ url: url,
+ params: params,
+ method: 'POST',
+ waitMsgTarget: me,
+ failure: function (response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid
+ });
+ win.show();
+ me.close();
+ }
+ });
+ };
+
+ var submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Restore'),
+ handler: function(){
+ var storage = storagesel.getValue();
+ var values = form.getValues();
+
+ var params = {
+ storage: storage,
+ vmid: me.vmid || values.vmid,
+ force: me.vmid ? 1 : 0
+ };
+ if (values.unique) { params.unique = 1; }
+
+ if (values.bwlimit !== undefined) {
+ params.bwlimit = values.bwlimit * 1024;
+ }
+
+ var url;
+ var msg;
+ if (me.vmtype === 'lxc') {
+ url = '/nodes/' + me.nodename + '/lxc';
+ params.ostemplate = me.volid;
+ params.restore = 1;
+ if (values.unprivileged) { params.unprivileged = 1; }
+ msg = Proxmox.Utils.format_task_description('vzrestore', params.vmid);
+ } else if (me.vmtype === 'qemu') {
+ url = '/nodes/' + me.nodename + '/qemu';
+ params.archive = me.volid;
+ msg = Proxmox.Utils.format_task_description('qmrestore', params.vmid);
+ } else {
+ throw 'unknown VM type';
+ }
+
+ if (me.vmid) {
+ msg += '. ' + gettext('This will permanently erase current VM data.');
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ doRestore(url, params);
+ });
+ } else {
+ doRestore(url, params);
+ }
+ }
+ });
+
+ form.on('validitychange', function(f, valid) {
+ submitBtn.setDisabled(!valid);
+ });
+
+ var title = gettext('Restore') + ": " + (
+ (me.vmtype === 'lxc') ? 'CT' : 'VM');
+
+ if (me.vmid) {
+ title += " " + me.vmid;
+ }
+
+ Ext.apply(me, {
+ title: title,
+ width: 500,
+ modal: true,
+ layout: 'auto',
+ border: false,
+ items: [ me.formPanel ],
+ buttons: [ submitBtn ]
+ });
+
+ me.callParent();
+ }
+});
+/* Popup a message window
+ * where the user has to manually enter the resource ID
+ * to enable the destroy button
+ */
+Ext.define('PVE.window.SafeDestroy', {
+ extend: 'Ext.window.Window',
+ alias: 'widget.pveSafeDestroy',
+
+ title: gettext('Confirm'),
+ modal: true,
+ buttonAlign: 'center',
+ bodyPadding: 10,
+ width: 450,
+ layout: { type:'hbox' },
+ defaultFocus: 'confirmField',
+ showProgress: false,
+
+ config: {
+ item: {
+ id: undefined,
+ type: undefined
+ },
+ url: undefined,
+ params: {}
+ },
+
+ getParams: function() {
+ var me = this;
+ if (Ext.Object.isEmpty(me.params)) {
+ return '';
+ }
+ return '?' + Ext.Object.toQueryString(me.params);
+ },
+
+ controller: {
+
+ xclass: 'Ext.app.ViewController',
+
+ control: {
+ 'field[name=confirm]': {
+ change: function(f, value) {
+ var view = this.getView();
+ var removeButton = this.lookupReference('removeButton');
+ if (value === view.getItem().id.toString()) {
+ removeButton.enable();
+ } else {
+ removeButton.disable();
+ }
+ },
+ specialkey: function (field, event) {
+ var removeButton = this.lookupReference('removeButton');
+ if (!removeButton.isDisabled() && event.getKey() == event.ENTER) {
+ removeButton.fireEvent('click', removeButton, event);
+ }
+ }
+ },
+ 'button[reference=removeButton]': {
+ click: function() {
+ var view = this.getView();
+ Proxmox.Utils.API2Request({
+ url: view.getUrl() + view.getParams(),
+ method: 'DELETE',
+ waitMsgTarget: view,
+ failure: function(response, opts) {
+ view.close();
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ var hasProgressBar = view.showProgress &&
+ response.result.data ? true : false;
+
+ if (hasProgressBar) {
+ // stay around so we can trigger our close events
+ // when background action is completed
+ view.hide();
+
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', {
+ upid: upid,
+ listeners: {
+ destroy: function () {
+ view.close();
+ }
+ }
+ });
+ win.show();
+ } else {
+ view.close();
+ }
+ }
+ });
+ }
+ }
+ }
+ },
+
+ items: [
+ {
+ xtype: 'component',
+ cls: [ Ext.baseCSSPrefix + 'message-box-icon',
+ Ext.baseCSSPrefix + 'message-box-warning',
+ Ext.baseCSSPrefix + 'dlg-icon']
+ },
+ {
+ xtype: 'container',
+ flex: 1,
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+ items: [
+ {
+ xtype: 'component',
+ reference: 'messageCmp'
+ },
+ {
+ itemId: 'confirmField',
+ reference: 'confirmField',
+ xtype: 'textfield',
+ name: 'confirm',
+ labelWidth: 300,
+ hideTrigger: true,
+ allowBlank: false
+ }
+ ]
+ }
+ ],
+ buttons: [
+ {
+ reference: 'removeButton',
+ text: gettext('Remove'),
+ disabled: true
+ }
+ ],
+
+ initComponent : function() {
+ var me = this;
+
+ me.callParent();
+
+ var item = me.getItem();
+
+ if (!Ext.isDefined(item.id)) {
+ throw "no ID specified";
+ }
+
+ if (!Ext.isDefined(item.type)) {
+ throw "no VM type specified";
+ }
+
+ var messageCmp = me.lookupReference('messageCmp');
+ var msg;
+
+ if (item.type === 'VM') {
+ msg = Proxmox.Utils.format_task_description('qmdestroy', item.id);
+ } else if (item.type === 'CT') {
+ msg = Proxmox.Utils.format_task_description('vzdestroy', item.id);
+ } else if (item.type === 'CephPool') {
+ msg = Proxmox.Utils.format_task_description('cephdestroypool', item.id);
+ } else if (item.type === 'Image') {
+ msg = Proxmox.Utils.format_task_description('unknownimgdel', item.id);
+ } else {
+ throw "unknown item type specified";
+ }
+
+ messageCmp.setHtml(msg);
+
+ var confirmField = me.lookupReference('confirmField');
+ msg = gettext('Please enter the ID to confirm') +
+ ' (' + item.id + ')';
+ confirmField.setFieldLabel(msg);
+ }
+});
+Ext.define('PVE.window.BackupConfig', {
+ extend: 'Ext.window.Window',
+ title: gettext('Configuration'),
+ width: 600,
+ height: 400,
+ layout: 'fit',
+ modal: true,
+ items: {
+ xtype: 'component',
+ itemId: 'configtext',
+ autoScroll: true,
+ style: {
+ 'background-color': 'white',
+ 'white-space': 'pre',
+ 'font-family': 'monospace',
+ padding: '5px'
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.volume) {
+ throw "no volume specified";
+ }
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.callParent();
+
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + nodename + "/vzdump/extractconfig",
+ method: 'GET',
+ params: {
+ volume: me.volume
+ },
+ failure: function(response, opts) {
+ me.close();
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response,options) {
+ me.show();
+ me.down('#configtext').update(Ext.htmlEncode(response.result.data));
+ }
+ });
+ }
+});
+Ext.define('PVE.window.Settings', {
+ extend: 'Ext.window.Window',
+
+ width: '800px',
+ title: gettext('My Settings'),
+ iconCls: 'fa fa-gear',
+ modal: true,
+ bodyPadding: 10,
+ resizable: false,
+
+ buttons: [
+ {
+ xtype: 'proxmoxHelpButton',
+ onlineHelp: 'gui_my_settings',
+ hidden: false
+ },
+ '->',
+ {
+ text: gettext('Close'),
+ handler: function() {
+ this.up('window').close();
+ }
+ }
+ ],
+
+ layout: {
+ type: 'hbox',
+ align: 'top'
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ var me = this;
+ var sp = Ext.state.Manager.getProvider();
+
+ var username = sp.get('login-username') || Proxmox.Utils.noneText;
+ me.lookupReference('savedUserName').setValue(username);
+
+ var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
+ settings.forEach(function(setting) {
+ var val = localStorage.getItem('pve-xterm-' + setting);
+ if (val !== undefined && val !== null) {
+ var field = me.lookup(setting);
+ field.setValue(val);
+ field.resetOriginalValue();
+ }
+ });
+ },
+
+ set_button_status: function() {
+ var me = this;
+
+ var form = me.lookup('xtermform');
+ var valid = form.isValid();
+ var dirty = form.isDirty();
+
+ var hasvalues = false;
+ var values = form.getValues();
+ Ext.Object.eachValue(values, function(value) {
+ if (value) {
+ hasvalues = true;
+ return false;
+ }
+ });
+
+ me.lookup('xtermsave').setDisabled(!dirty || !valid);
+ me.lookup('xtermreset').setDisabled(!hasvalues);
+ },
+
+ control: {
+ '#xtermjs form': {
+ dirtychange: 'set_button_status',
+ validitychange: 'set_button_status'
+ },
+ '#xtermjs button': {
+ click: function(button) {
+ var me = this;
+ var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
+ settings.forEach(function(setting) {
+ var field = me.lookup(setting);
+ if (button.reference === 'xtermsave') {
+ var value = field.getValue();
+ if (value) {
+ localStorage.setItem('pve-xterm-' + setting, value);
+ } else {
+ localStorage.removeItem('pve-xterm-' + setting);
+ }
+ } else if (button.reference === 'xtermreset') {
+ field.setValue(undefined);
+ localStorage.removeItem('pve-xterm-' + setting);
+ }
+ field.resetOriginalValue();
+ });
+ me.set_button_status();
+ }
+ },
+ 'button[name=reset]': {
+ click: function () {
+ var blacklist = ['GuiCap', 'login-username', 'dash-storages'];
+ var sp = Ext.state.Manager.getProvider();
+ var state;
+ for (state in sp.state) {
+ if (sp.state.hasOwnProperty(state)) {
+ if (blacklist.indexOf(state) !== -1) {
+ continue;
+ }
+
+ sp.clear(state);
+ }
+ }
+
+ window.location.reload();
+ }
+ },
+ 'button[name=clear-username]': {
+ click: function () {
+ var me = this;
+ var usernamefield = me.lookupReference('savedUserName');
+ var sp = Ext.state.Manager.getProvider();
+
+ usernamefield.setValue(Proxmox.Utils.noneText);
+ sp.clear('login-username');
+ }
+ },
+ 'grid[reference=dashboard-storages]': {
+ selectionchange: function(grid, selected) {
+ var me = this;
+ var sp = Ext.state.Manager.getProvider();
+
+ // saves the selected storageids as
+ // "id1,id2,id3,..."
+ // or clears the variable
+ if (selected.length > 0) {
+ sp.set('dash-storages',
+ Ext.Array.pluck(selected, 'id').join(','));
+ } else {
+ sp.clear('dash-storages');
+ }
+ },
+ afterrender: function(grid) {
+ var me = grid;
+ var sp = Ext.state.Manager.getProvider();
+ var store = me.getStore();
+ var items = [];
+ me.suspendEvent('selectionchange');
+ var storages = sp.get('dash-storages') || '';
+ storages.split(',').forEach(function(storage){
+ // we have to get the records
+ // to be able to select them
+ if (storage !== '') {
+ var item = store.getById(storage);
+ if (item) {
+ items.push(item);
+ }
+ }
+ });
+ me.getSelectionModel().select(items);
+ me.resumeEvent('selectionchange');
+ }
+ }
+ }
+ },
+
+ items: [{
+ xtype: 'fieldset',
+ width: '50%',
+ title: gettext('Webinterface Settings'),
+ margin: '5',
+ layout: {
+ type: 'vbox',
+ align: 'left'
+ },
+ defaults: {
+ width: '100%',
+ margin: '0 0 10 0'
+ },
+ items: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Dashboard Storages'),
+ labelAlign: 'left',
+ labelWidth: '50%'
+ },
+ {
+ xtype: 'grid',
+ maxHeight: 150,
+ reference: 'dashboard-storages',
+ selModel: {
+ selType: 'checkboxmodel'
+ },
+ columns: [{
+ header: gettext('Name'),
+ dataIndex: 'storage',
+ flex: 1
+ },{
+ header: gettext('Node'),
+ dataIndex: 'node',
+ flex: 1
+ }],
+ store: {
+ type: 'diff',
+ field: ['type', 'storage', 'id', 'node'],
+ rstore: PVE.data.ResourceStore,
+ filters: [{
+ property: 'type',
+ value: 'storage'
+ }],
+ sorters: [ 'node','storage']
+ }
+ },
+ {
+ xtype: 'box',
+ autoEl: { tag: 'hr'}
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Saved User name'),
+ labelAlign: 'left',
+ labelWidth: '50%',
+ stateId: 'login-username',
+ reference: 'savedUserName',
+ value: ''
+ },
+ {
+ xtype: 'button',
+ cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+ text: gettext('Clear User name'),
+ width: 'auto',
+ name: 'clear-username'
+ },
+ {
+ xtype: 'box',
+ autoEl: { tag: 'hr'}
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Layout'),
+ labelAlign: 'left',
+ labelWidth: '50%'
+ },
+ {
+ xtype: 'button',
+ cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+ text: gettext('Reset Layout'),
+ width: 'auto',
+ name: 'reset'
+ }
+ ]
+ },{
+ xtype: 'fieldset',
+ itemId: 'xtermjs',
+ width: '50%',
+ margin: '5',
+ title: gettext('xterm.js Settings'),
+ items: [{
+ xtype: 'form',
+ reference: 'xtermform',
+ border: false,
+ layout: {
+ type: 'vbox',
+ algin: 'left'
+ },
+ defaults: {
+ width: '100%',
+ margin: '0 0 10 0'
+ },
+ items: [
+ {
+ xtype: 'textfield',
+ name: 'fontFamily',
+ reference: 'fontFamily',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Font-Family')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ emptyText: Proxmox.Utils.defaultText,
+ name: 'fontSize',
+ reference: 'fontSize',
+ minValue: 1,
+ fieldLabel: gettext('Font-Size')
+ },
+ {
+ xtype: 'numberfield',
+ name: 'letterSpacing',
+ reference: 'letterSpacing',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Letter Spacing')
+ },
+ {
+ xtype: 'numberfield',
+ name: 'lineHeight',
+ minValue: 0.1,
+ reference: 'lineHeight',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Line Height')
+ },
+ {
+ xtype: 'container',
+ layout: {
+ type: 'hbox',
+ pack: 'end'
+ },
+ items: [
+ {
+ xtype: 'button',
+ reference: 'xtermreset',
+ disabled: true,
+ text: gettext('Reset')
+ },
+ {
+ xtype: 'button',
+ reference: 'xtermsave',
+ disabled: true,
+ text: gettext('Save')
+ }
+ ]
+ }
+ ]
+ }]
+ }],
+
+ onShow: function() {
+ var me = this;
+ me.callParent();
+ }
+});
+Ext.define('PVE.panel.StartupInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ onlineHelp: 'qm_startup_and_shutdown',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var res = PVE.Parser.printStartup(values);
+
+ if (res === undefined || res === '') {
+ return { 'delete': 'startup' };
+ }
+
+ return { startup: res };
+ },
+
+ setStartup: function(value) {
+ var me = this;
+
+ var startup = PVE.Parser.parseStartup(value);
+ if (startup) {
+ me.setValues(startup);
+ }
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: 'textfield',
+ name: 'order',
+ defaultValue: '',
+ emptyText: 'any',
+ fieldLabel: gettext('Start/Shutdown order')
+ },
+ {
+ xtype: 'textfield',
+ name: 'up',
+ defaultValue: '',
+ emptyText: 'default',
+ fieldLabel: gettext('Startup delay')
+ },
+ {
+ xtype: 'textfield',
+ name: 'down',
+ defaultValue: '',
+ emptyText: 'default',
+ fieldLabel: gettext('Shutdown timeout')
+ }
+ ];
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.window.StartupEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveWindowStartupEdit',
+ onlineHelp: undefined,
+
+ initComponent : function() {
+
+ var me = this;
+ var ipanelConfig = me.onlineHelp ? {onlineHelp: me.onlineHelp} : {};
+ var ipanel = Ext.create('PVE.panel.StartupInputPanel', ipanelConfig);
+
+ Ext.applyIf(me, {
+ subject: gettext('Start/Shutdown order'),
+ fieldDefaults: {
+ labelWidth: 120
+ },
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var i, confid;
+ me.vmconfig = response.result.data;
+ ipanel.setStartup(me.vmconfig.startup);
+ }
+ });
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.ceph.Install', {
+ extend: 'Ext.window.Window',
+ xtype: 'pveCephInstallWindow',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ width: 220,
+ header: false,
+ resizable: false,
+ draggable: false,
+ modal: true,
+ nodename: undefined,
+ shadow: false,
+ border: false,
+ bodyBorder: false,
+ closable: false,
+ cls: 'install-mask',
+ bodyCls: 'install-mask',
+ layout: {
+ align: 'stretch',
+ pack: 'center',
+ type: 'vbox'
+ },
+ viewModel: {
+ data: {
+ cephVersion: 'nautilus',
+ isInstalled: false
+ },
+ formulas: {
+ buttonText: function (get){
+ if (get('isInstalled')) {
+ return gettext('Configure Ceph');
+ } else {
+ return gettext('Install Ceph-') + get('cephVersion');
+ }
+ },
+ windowText: function (get) {
+ if (get('isInstalled')) {
+ return '
' +
+ gettext('Would you like to install it now?') + '
Caution: This can reduce performance while it is running.",
+ buttons: Ext.Msg.YESNO,
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ doRequest();
+ }
+ });
+ } else {
+ doRequest();
+ }
+ },
+
+ create_osd: function() {
+ var me = this;
+ var vm = this.getViewModel();
+ Ext.create('PVE.CephCreateOsd', {
+ nodename: vm.get('nodename'),
+ taskDone: () => { me.reload(); }
+ }).show();
+ },
+
+ destroy_osd: function() {
+ var me = this;
+ var vm = this.getViewModel();
+ Ext.create('PVE.CephRemoveOsd', {
+ nodename: vm.get('osdhost'),
+ osdid: vm.get('osdid'),
+ taskDone: () => { me.reload(); }
+ }).show();
+ },
+
+ set_flag: function() {
+ var me = this;
+ var vm = this.getViewModel();
+ var flags = vm.get('flags');
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + vm.get('nodename') + "/ceph/flags/noout",
+ waitMsgTarget: me.getView(),
+ method: flags.includes('noout') ? 'DELETE' : 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: () => { me.reload(); }
+ });
+ },
+
+ service_cmd: function(comp) {
+ var me = this;
+ var vm = this.getViewModel();
+ var cmd = comp.cmd || comp;
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + vm.get('osdhost') + "/ceph/" + cmd,
+ params: { service: "osd." + vm.get('osdid') },
+ waitMsgTarget: me.getView(),
+ method: 'POST',
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', {
+ upid: upid,
+ taskDone: () => { me.reload(); }
+ });
+ win.show();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ },
+
+ set_selection_status: function(tp, selection) {
+ if (selection.length < 1) {
+ return;
+ }
+ var rec = selection[0];
+ var vm = this.getViewModel();
+
+ var isOsd = (rec.data.host && (rec.data.type === 'osd') && (rec.data.id >= 0));
+
+ vm.set('isOsd', isOsd);
+ vm.set('downOsd', isOsd && rec.data.status === 'down');
+ vm.set('upOsd', isOsd && rec.data.status !== 'down');
+ vm.set('inOsd', isOsd && rec.data.in);
+ vm.set('outOsd', isOsd && !rec.data.in);
+ vm.set('osdid', isOsd ? rec.data.id : undefined);
+ vm.set('osdhost', isOsd ? rec.data.host : undefined);
+ },
+
+ render_status: function(value, metaData, rec) {
+ if (!value) {
+ return value;
+ }
+ var inout = rec.data['in'] ? 'in' : 'out';
+ var updownicon = value === 'up' ? 'good fa-arrow-circle-up' :
+ 'critical fa-arrow-circle-down';
+
+ var inouticon = rec.data['in'] ? 'good fa-circle' :
+ 'warning fa-circle-o';
+
+ var text = value + ' / ' +
+ inout + ' ';
+
+ return text;
+ },
+
+ render_wal: function(value, metaData, rec) {
+ if (!value &&
+ rec.data.osdtype === 'bluestore' &&
+ rec.data.type === 'osd') {
+ return 'N/A';
+ }
+ return value;
+ },
+
+ render_version: function(value, metadata, rec) {
+ var vm = this.getViewModel();
+ var versions = vm.get('versions');
+ var icon = "";
+ var version = value || "";
+ if (value && value != vm.get('maxversion')) {
+ icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
+ }
+
+ if (!value && rec.data.type == 'host') {
+ version = versions[rec.data.name] || Proxmox.Utils.unknownText;
+ }
+
+ return icon + version;
+ },
+
+ render_osd_val: function(value, metaData, rec) {
+ return (rec.data.type === 'osd') ? value : '';
+ },
+ render_osd_weight: function(value, metaData, rec) {
+ if (rec.data.type !== 'osd') {
+ return '';
+ }
+ return Ext.util.Format.number(value, '0.00###');
+ },
+
+ render_osd_latency: function(value, metaData, rec) {
+ if (rec.data.type !== 'osd') {
+ return '';
+ }
+ let commit_ms = rec.data.commit_latency_ms,
+ apply_ms = rec.data.apply_latency_ms;
+ return apply_ms + ' / ' + commit_ms;
+ },
+
+ render_osd_size: function(value, metaData, rec) {
+ return this.render_osd_val(PVE.Utils.render_size(value), metaData, rec);
+ },
+
+ control: {
+ '#': {
+ selectionchange: 'set_selection_status'
+ }
+ },
+
+ init: function(view) {
+ var me = this;
+ var vm = this.getViewModel();
+
+ if (!view.pveSelNode.data.node) {
+ throw "no node name specified";
+ }
+
+ vm.set('nodename', view.pveSelNode.data.node);
+
+ me.callParent();
+ me.reload();
+ }
+ },
+
+ stateful: true,
+ stateId: 'grid-ceph-osd',
+ rootVisible: false,
+ useArrows: true,
+
+ columns: [
+ {
+ xtype: 'treecolumn',
+ text: 'Name',
+ dataIndex: 'name',
+ width: 150
+ },
+ {
+ text: 'Type',
+ dataIndex: 'type',
+ hidden: true,
+ align: 'right',
+ width: 75
+ },
+ {
+ text: gettext("Class"),
+ dataIndex: 'device_class',
+ align: 'right',
+ width: 75
+ },
+ {
+ text: "OSD Type",
+ dataIndex: 'osdtype',
+ align: 'right',
+ width: 100
+ },
+ {
+ text: "Bluestore Device",
+ dataIndex: 'blfsdev',
+ align: 'right',
+ width: 75,
+ hidden: true
+ },
+ {
+ text: "DB Device",
+ dataIndex: 'dbdev',
+ align: 'right',
+ width: 75,
+ hidden: true
+ },
+ {
+ text: "WAL Device",
+ dataIndex: 'waldev',
+ align: 'right',
+ renderer: 'render_wal',
+ width: 75,
+ hidden: true
+ },
+ {
+ text: 'Status',
+ dataIndex: 'status',
+ align: 'right',
+ renderer: 'render_status',
+ width: 120
+ },
+ {
+ text: gettext('Version'),
+ dataIndex: 'version',
+ align: 'right',
+ renderer: 'render_version'
+ },
+ {
+ text: 'weight',
+ dataIndex: 'crush_weight',
+ align: 'right',
+ renderer: 'render_osd_weight',
+ width: 90
+ },
+ {
+ text: 'reweight',
+ dataIndex: 'reweight',
+ align: 'right',
+ renderer: 'render_osd_weight',
+ width: 90
+ },
+ {
+ text: gettext('Used') + ' (%)',
+ dataIndex: 'percent_used',
+ align: 'right',
+ renderer: function(value, metaData, rec) {
+ if (rec.data.type !== 'osd') {
+ return '';
+ }
+ return Ext.util.Format.number(value, '0.00');
+ },
+ width: 100
+ },
+ {
+ text: gettext('Total'),
+ dataIndex: 'total_space',
+ align: 'right',
+ renderer: 'render_osd_size',
+ width: 100
+ },
+ {
+ text: 'Apply/Commit
Latency (ms)',
+ dataIndex: 'apply_latency_ms',
+ align: 'right',
+ renderer: 'render_osd_latency',
+ width: 120
+ }
+ ],
+
+
+ tbar: {
+ items: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: 'reload'
+ },
+ '-',
+ {
+ text: gettext('Create') + ': OSD',
+ handler: 'create_osd',
+ },
+ {
+ text: gettext('Set noout'),
+ itemId: 'nooutBtn',
+ handler: 'set_flag',
+ },
+ '->',
+ {
+ xtype: 'tbtext',
+ data: {
+ osd: undefined
+ },
+ bind: {
+ data: {
+ osd: "{osdid}"
+ }
+ },
+ tpl: [
+ '' + Ext.htmlEncode(record.data.detail) + '
'
+ ]
+ }]
+ });
+ win.show();
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ xtype: 'pveCephStatusDetail',
+ itemId: 'statusdetail',
+ plugins: 'responsive',
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: 1,
+ minHeight: 250
+ },
+ 'width >= 1900': {
+ columnWidth: 0.5,
+ minHeight: 300
+ }
+ },
+ title: gettext('Status')
+ },
+ {
+ title: gettext('Services'),
+ xtype: 'pveCephServices',
+ itemId: 'services',
+ plugins: 'responsive',
+ layout: {
+ type: 'hbox',
+ align: 'stretch'
+ },
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: 1,
+ minHeight: 200
+ },
+ 'width >= 1900': {
+ columnWidth: 0.5,
+ minHeight: 200
+ }
+ }
+ },
+ {
+ xtype: 'panel',
+ title: gettext('Performance'),
+ columnWidth: 1,
+ bodyPadding: 5,
+ layout: {
+ type: 'hbox',
+ align: 'center'
+ },
+ items: [
+ {
+ flex: 1,
+ xtype: 'proxmoxGauge',
+ itemId: 'space',
+ title: gettext('Usage')
+ },
+ {
+ flex: 2,
+ xtype: 'container',
+ defaults: {
+ padding: 0,
+ height: 100
+ },
+ items: [
+ {
+ itemId: 'reads',
+ xtype: 'pveRunningChart',
+ title: gettext('Reads'),
+ renderer: PVE.Utils.render_bandwidth
+ },
+ {
+ itemId: 'writes',
+ xtype: 'pveRunningChart',
+ title: gettext('Writes'),
+ renderer: PVE.Utils.render_bandwidth
+ },
+ {
+ itemId: 'iops',
+ xtype: 'pveRunningChart',
+ hidden: true,
+ title: 'IOPS', // do not localize
+ renderer: Ext.util.Format.numberRenderer('0,000')
+ },
+ {
+ itemId: 'readiops',
+ xtype: 'pveRunningChart',
+ hidden: true,
+ title: 'IOPS: ' + gettext('Reads'),
+ renderer: Ext.util.Format.numberRenderer('0,000')
+ },
+ {
+ itemId: 'writeiops',
+ xtype: 'pveRunningChart',
+ hidden: true,
+ title: 'IOPS: ' + gettext('Writes'),
+ renderer: Ext.util.Format.numberRenderer('0,000')
+ }
+ ]
+ }
+ ]
+ }
+ ],
+
+ generateCheckData: function(health) {
+ var result = [];
+ var checks = health.checks || {};
+ var keys = Ext.Object.getKeys(checks).sort();
+
+ Ext.Array.forEach(keys, function(key) {
+ var details = checks[key].detail || [];
+ result.push({
+ id: key,
+ summary: checks[key].summary.message,
+ detail: Ext.Array.reduce(
+ checks[key].detail,
+ function(first, second) {
+ return first + '\n' + second.message;
+ },
+ ''
+ ),
+ severity: checks[key].severity
+ });
+ });
+
+ return result;
+ },
+
+ updateAll: function(store, records, success) {
+ if (!success || records.length === 0) {
+ return;
+ }
+
+ var me = this;
+ var rec = records[0];
+ me.status = rec.data;
+
+ // add health panel
+ me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {}));
+ // add errors to gridstore
+ me.down('#warnings').getStore().loadRawData(me.generateCheckData(rec.data.health || {}), false);
+
+ // update services
+ me.getComponent('services').updateAll(me.metadata || {}, rec.data);
+
+ // update detailstatus panel
+ me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data);
+
+ // add performance data
+ var used = rec.data.pgmap.bytes_used;
+ var total = rec.data.pgmap.bytes_total;
+
+ var text = Ext.String.format(gettext('{0} of {1}'),
+ PVE.Utils.render_size(used),
+ PVE.Utils.render_size(total)
+ );
+
+ // update the usage widget
+ me.down('#space').updateValue(used/total, text);
+
+ // TODO: logic for jewel (iops split in read/write)
+
+ var iops = rec.data.pgmap.op_per_sec;
+ var readiops = rec.data.pgmap.read_op_per_sec;
+ var writeiops = rec.data.pgmap.write_op_per_sec;
+ var reads = rec.data.pgmap.read_bytes_sec || 0;
+ var writes = rec.data.pgmap.write_bytes_sec || 0;
+
+ if (iops !== undefined && me.version !== 'hammer') {
+ me.change_version('hammer');
+ } else if((readiops !== undefined || writeiops !== undefined) && me.version !== 'jewel') {
+ me.change_version('jewel');
+ }
+ // update the graphs
+ me.reads.addDataPoint(reads);
+ me.writes.addDataPoint(writes);
+ me.iops.addDataPoint(iops);
+ me.readiops.addDataPoint(readiops);
+ me.writeiops.addDataPoint(writeiops);
+ },
+
+ change_version: function(version) {
+ var me = this;
+ me.version = version;
+ me.sp.set('ceph-version', version);
+ me.iops.setVisible(version === 'hammer');
+ me.readiops.setVisible(version === 'jewel');
+ me.writeiops.setVisible(version === 'jewel');
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+
+ me.callParent();
+ var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph';
+ me.store = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'ceph-status-' + (nodename || 'cluster'),
+ interval: 5000,
+ proxy: {
+ type: 'proxmox',
+ url: baseurl + '/status'
+ }
+ });
+
+ me.metadatastore = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'ceph-metadata-' + (nodename || 'cluster'),
+ interval: 15*1000,
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/ceph/metadata'
+ }
+ });
+
+ // save references for the updatefunction
+ me.iops = me.down('#iops');
+ me.readiops = me.down('#readiops');
+ me.writeiops = me.down('#writeiops');
+ me.reads = me.down('#reads');
+ me.writes = me.down('#writes');
+
+ // get ceph version
+ me.sp = Ext.state.Manager.getProvider();
+ me.version = me.sp.get('ceph-version');
+ me.change_version(me.version);
+
+ var regex = new RegExp("not (installed|initialized)", "i");
+ PVE.Utils.handleStoreErrorOrMask(me, me.store, regex, function(me, error){
+ me.store.stopUpdate();
+ PVE.Utils.showCephInstallOrMask(me, error.statusText, (nodename || 'localhost'),
+ function(win){
+ me.mon(win, 'cephInstallWindowClosed', function(){
+ me.store.startUpdate();
+ });
+ }
+ );
+ });
+
+ me.mon(me.store, 'load', me.updateAll, me);
+ me.mon(me.metadatastore, 'load', function(store, records, success) {
+ if (!success || records.length < 1) {
+ return;
+ }
+ var rec = records[0];
+ me.metadata = rec.data;
+
+ // update services
+ me.getComponent('services').updateAll(rec.data, me.status || {});
+
+ // update detailstatus panel
+ me.getComponent('statusdetail').updateAll(rec.data, me.status || {});
+
+ }, me);
+
+ me.on('destroy', me.store.stopUpdate);
+ me.on('destroy', me.metadatastore.stopUpdate);
+ me.store.startUpdate();
+ me.metadatastore.startUpdate();
+ }
+
+});
+Ext.define('PVE.ceph.StatusDetail', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveCephStatusDetail',
+
+ layout: {
+ type: 'hbox',
+ align: 'stretch'
+ },
+
+ bodyPadding: '0 5',
+ defaults: {
+ xtype: 'box',
+ style: {
+ 'text-align':'center'
+ }
+ },
+
+ items: [{
+ flex: 1,
+ itemId: 'osds',
+ maxHeight: 250,
+ scrollable: true,
+ padding: '0 10 5 10',
+ data: {
+ total: 0,
+ upin: 0,
+ upout: 0,
+ downin: 0,
+ downout: 0,
+ oldosds: []
+ },
+ tpl: [
+ '' + 'OSDs' + '
',
+ '',
+ '
',
+ ' ',
+ '',
+ ' ',
+ gettext('In'),
+ ' ',
+ '',
+ gettext('Out'),
+ ' ',
+ '',
+ ' ',
+ '',
+ gettext('Up'),
+ ' ',
+ '{upin} ',
+ '{upout} ',
+ '',
+ ' ',
+ '',
+ gettext('Down'),
+ ' ',
+ '{downin} ',
+ '{downout} ',
+ '
',
+ '
",
+ '
',
+ '',
+ '
';
+ record.get('states').forEach(function(state) {
+ html += '
' +
+ state.state_name + ': ' + state.count.toString();
+ });
+ tooltip.setHtml(html);
+ }
+ },
+ subStyle: {
+ strokeStyle: false
+ }
+ }
+ ]
+ },
+ {
+ flex: 1.6,
+ itemId: 'pgs',
+ padding: '0 10',
+ maxHeight: 250,
+ scrollable: true,
+ data: {
+ states: []
+ },
+ tpl: [
+ '' + 'PGs' + '
',
+ '
',
+ '',
+ '
');
+
+ result.health = healthmap[result.health];
+
+ me[type][id] = result;
+ }
+ }
+
+ me.getComponent('mons').updateAll(Object.values(me.mon));
+ me.getComponent('mgrs').updateAll(Object.values(me.mgr));
+ me.getComponent('mdss').updateAll(Object.values(me.mds));
+ }
+});
+
+Ext.define('PVE.ceph.ServiceList', {
+ extend: 'Ext.container.Container',
+ xtype: 'pveCephServiceList',
+
+ style: {
+ 'text-align':'center'
+ },
+ defaults: {
+ xtype: 'box',
+ style: {
+ 'text-align':'center'
+ }
+ },
+
+ items: [
+ {
+ itemId: 'title',
+ data: {
+ title: ''
+ },
+ tpl: '{title}
'
+ }
+ ],
+
+ updateAll: function(list) {
+ var me = this;
+ me.suspendLayout = true;
+
+ var i;
+ list.sort(function(a,b) {
+ return a.id > b.id ? 1 : a.id < b.id ? -1 : 0;
+ });
+ var ids = {};
+ if (me.ids) {
+ me.ids.forEach(function(id) {
+ ids[id] = true;
+ });
+ }
+ for (i = 0; i < list.length; i++) {
+ var service = me.getComponent(list[i].id);
+ if (!service) {
+ // since services are already sorted, and
+ // we always have a sorted list
+ // we can add it at the service+1 position (because of the title)
+ service = me.insert(i+1, {
+ xtype: 'pveCephServiceWidget',
+ itemId: list[i].id
+ });
+ if (!me.ids) {
+ me.ids = [];
+ }
+ me.ids.push(list[i].id);
+ } else {
+ delete ids[list[i].id];
+ }
+ service.updateService(list[i].title, list[i].text, list[i].health);
+ }
+
+ Object.keys(ids).forEach(function(id) {
+ me.remove(id);
+ });
+ me.suspendLayout = false;
+ me.updateLayout();
+ },
+
+ initComponent: function() {
+ var me = this;
+ me.callParent();
+ me.getComponent('title').update({
+ title: me.title
+ });
+ }
+});
+
+/*jslint confusion: true*/
+Ext.define('PVE.ceph.ServiceWidget', {
+ extend: 'Ext.Component',
+ alias: 'widget.pveCephServiceWidget',
+
+ userCls: 'monitor inline-block',
+ data: {
+ title: '0',
+ health: 'HEALTH_ERR',
+ text: '',
+ iconCls: PVE.Utils.get_health_icon()
+ },
+
+ tpl: [
+ '{title}: ',
+ ''
+ ],
+
+ updateService: function(title, text, health) {
+ var me = this;
+
+ me.update(Ext.apply(me.data, {
+ health: health,
+ text: text,
+ title: title,
+ iconCls: PVE.Utils.get_health_icon(PVE.Utils.map_ceph_health[health])
+ }));
+
+ if (me.tooltip) {
+ me.tooltip.setHtml(text);
+ }
+ },
+
+ listeners: {
+ destroy: function() {
+ var me = this;
+ if (me.tooltip) {
+ me.tooltip.destroy();
+ delete me.tooltip;
+ }
+ },
+ mouseenter: {
+ element: 'el',
+ fn: function(events, element) {
+ var me = this.component;
+ if (!me) {
+ return;
+ }
+ if (!me.tooltip) {
+ me.tooltip = Ext.create('Ext.tip.ToolTip', {
+ target: me.el,
+ trackMouse: true,
+ dismissDelay: 0,
+ renderTo: Ext.getBody(),
+ html: me.data.text
+ });
+ }
+ me.tooltip.show();
+ }
+ },
+ mouseleave: {
+ element: 'el',
+ fn: function(events, element) {
+ var me = this.component;
+ if (me.tooltip) {
+ me.tooltip.destroy();
+ delete me.tooltip;
+ }
+ }
+ }
+ }
+});
+Ext.define('PVE.node.CephConfigDb', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveNodeCephConfigDb',
+
+ border: false,
+ store: {
+ proxy: {
+ type: 'proxmox'
+ }
+ },
+
+ columns: [
+ {
+ dataIndex: 'section',
+ text: 'WHO',
+ width: 100,
+ },
+ {
+ dataIndex: 'mask',
+ text: 'MASK',
+ hidden: true,
+ width: 80,
+ },
+ {
+ dataIndex: 'level',
+ hidden: true,
+ text: 'LEVEL',
+ },
+ {
+ dataIndex: 'name',
+ flex: 1,
+ text: 'OPTION',
+ },
+ {
+ dataIndex: 'value',
+ flex: 1,
+ text: 'VALUE',
+ },
+ {
+ dataIndex: 'can_update_at_runtime',
+ text: 'Runtime Updatable',
+ hidden: true,
+ width: 80,
+ renderer: Proxmox.Utils.format_boolean
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.store.proxy.url = '/api2/json/nodes/' + nodename + '/ceph/configdb';
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, me.getStore());
+ me.getStore().load();
+ }
+});
+Ext.define('PVE.node.CephConfig', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNodeCephConfig',
+
+ bodyStyle: 'white-space:pre',
+ bodyPadding: 5,
+ border: false,
+ scrollable: true,
+ load: function() {
+ var me = this;
+
+ Proxmox.Utils.API2Request({
+ url: me.url,
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ me.update(gettext('Error') + " " + response.htmlStatus);
+ var msg = response.htmlStatus;
+ PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node,
+ function(win){
+ me.mon(win, 'cephInstallWindowClosed', function(){
+ me.load();
+ });
+ }
+ );
+
+ },
+ success: function(response, opts) {
+ var data = response.result.data;
+ me.update(Ext.htmlEncode(data));
+ }
+ });
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.apply(me, {
+ url: '/nodes/' + nodename + '/ceph/config',
+ listeners: {
+ activate: function() {
+ me.load();
+ }
+ }
+ });
+
+ me.callParent();
+
+ me.load();
+ }
+});
+
+Ext.define('PVE.node.CephConfigCrush', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNodeCephConfigCrush',
+
+ onlineHelp: 'chapter_pveceph',
+
+ layout: 'border',
+ items: [{
+ title: gettext('Configuration'),
+ xtype: 'pveNodeCephConfig',
+ region: 'center'
+ },
+ {
+ title: 'Crush Map', // do not localize
+ xtype: 'pveNodeCephCrushMap',
+ region: 'east',
+ split: true,
+ width: '50%'
+ },
+ {
+ title: gettext('Configuration Database'),
+ xtype: 'pveNodeCephConfigDb',
+ region: 'south',
+ split: true,
+ weight: -30,
+ height: '50%'
+ }],
+
+ initComponent: function() {
+ var me = this;
+ me.defaults = {
+ pveSelNode: me.pveSelNode
+ };
+ me.callParent();
+ }
+});
+Ext.define('PVE.ceph.Log', {
+ extend: 'Proxmox.panel.LogView',
+ xtype: 'cephLogView',
+ nodename: undefined,
+ failCallback: function(response) {
+ var me = this;
+ var msg = response.htmlStatus;
+ var windowShow = PVE.Utils.showCephInstallOrMask(me, msg, me.nodename,
+ function(win){
+ me.mon(win, 'cephInstallWindowClosed', function(){
+ me.loadTask.delay(200);
+ });
+ }
+ );
+ if (!windowShow) {
+ Proxmox.Utils.setErrorMask(me, msg);
+ }
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.ceph.CephInstallWizard', {
+ extend: 'PVE.window.Wizard',
+ alias: 'widget.pveCephInstallWizard',
+ mixins: ['Proxmox.Mixin.CBind'],
+ resizable: false,
+ nodename: undefined,
+ viewModel: {
+ data: {
+ nodename: '',
+ configuration: true,
+ isInstalled: false
+ }
+ },
+ cbindData: {
+ nodename: undefined
+ },
+ title: gettext('Setup'),
+ navigateNext: function() {
+ var tp = this.down('#wizcontent');
+ var atab = tp.getActiveTab();
+
+ var next = tp.items.indexOf(atab) + 1;
+ var ntab = tp.items.getAt(next);
+ if (ntab) {
+ ntab.enable();
+ tp.setActiveTab(ntab);
+ }
+ },
+ setInitialTab: function (index) {
+ var tp = this.down('#wizcontent');
+ var initialTab = tp.items.getAt(index);
+ initialTab.enable();
+ tp.setActiveTab(initialTab);
+ },
+ onShow: function() {
+ this.callParent(arguments);
+ var isInstalled = this.getViewModel().get('isInstalled');
+ if (isInstalled) {
+ this.getViewModel().set('configuration', false);
+ this.setInitialTab(2);
+ }
+ },
+ items: [
+ {
+ title: gettext('Info'),
+ xtype: 'panel',
+ border: false,
+ bodyBorder: false,
+ onlineHelp: 'chapter_pveceph',
+ html: 'Ceph?
'+
+ '
'+
+ 'Installation successful!
'+
+ '
'+
+ '
');
+ }
+ return Proxmox.Utils.noneText;
+ }
+ }
+ };
+ /*jslint confusion: false*/
+
+ me.callParent();
+ me.mon(me.rstore, 'load', me.set_button_status, me);
+ me.rstore.startUpdate();
+ me.load_account();
+ }
+});
+Ext.define('PVE.node.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.node.Config',
+
+ onlineHelp: 'chapter_system_administration',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: "/api2/json/nodes/" + nodename + "/status",
+ interval: 1000
+ });
+
+ var node_command = function(cmd) {
+ Proxmox.Utils.API2Request({
+ params: { command: cmd },
+ url: '/nodes/' + nodename + '/status',
+ method: 'POST',
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ };
+
+ var actionBtn = Ext.create('Ext.Button', {
+ text: gettext('Bulk Actions'),
+ iconCls: 'fa fa-fw fa-ellipsis-v',
+ disabled: !caps.nodes['Sys.PowerMgmt'],
+ menu: new Ext.menu.Menu({
+ items: [
+ {
+ text: gettext('Bulk Start'),
+ iconCls: 'fa fa-fw fa-play',
+ handler: function() {
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: nodename,
+ title: gettext('Bulk Start'),
+ btnText: gettext('Start'),
+ action: 'startall'
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Bulk Stop'),
+ iconCls: 'fa fa-fw fa-stop',
+ handler: function() {
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: nodename,
+ title: gettext('Bulk Stop'),
+ btnText: gettext('Stop'),
+ action: 'stopall'
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Bulk Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ handler: function() {
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: nodename,
+ title: gettext('Bulk Migrate'),
+ btnText: gettext('Migrate'),
+ action: 'migrateall'
+ });
+ win.show();
+ }
+ }
+ ]
+ })
+ });
+
+ var restartBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('Reboot'),
+ disabled: !caps.nodes['Sys.PowerMgmt'],
+ dangerous: true,
+ confirmMsg: Ext.String.format(gettext("Reboot node '{0}'?"), nodename),
+ handler: function() {
+ node_command('reboot');
+ },
+ iconCls: 'fa fa-undo'
+ });
+
+ var shutdownBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('Shutdown'),
+ disabled: !caps.nodes['Sys.PowerMgmt'],
+ dangerous: true,
+ confirmMsg: Ext.String.format(gettext("Shutdown node '{0}'?"), nodename),
+ handler: function() {
+ node_command('shutdown');
+ },
+ iconCls: 'fa fa-power-off'
+ });
+
+ var shellBtn = Ext.create('PVE.button.ConsoleButton', {
+ disabled: !caps.nodes['Sys.Console'],
+ text: gettext('Shell'),
+ consoleType: 'shell',
+ nodename: nodename
+ });
+
+ me.items = [];
+
+ Ext.apply(me, {
+ title: gettext('Node') + " '" + nodename + "'",
+ hstateid: 'nodetab',
+ defaults: { statusStore: me.statusStore },
+ tbar: [ restartBtn, shutdownBtn, shellBtn, actionBtn]
+ });
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ title: gettext('Summary'),
+ iconCls: 'fa fa-book',
+ itemId: 'summary',
+ xtype: 'pveNodeSummary'
+ },
+ {
+ title: gettext('Notes'),
+ iconCls: 'fa fa-sticky-note-o',
+ itemId: 'notes',
+ xtype: 'pveNotesView'
+ }
+ );
+ }
+
+ if (caps.nodes['Sys.Console']) {
+ me.items.push(
+ {
+ title: gettext('Shell'),
+ iconCls: 'fa fa-terminal',
+ itemId: 'jsconsole',
+ xtype: 'pveNoVncConsole',
+ consoleType: 'shell',
+ xtermjs: true,
+ nodename: nodename
+ }
+ );
+ }
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ title: gettext('System'),
+ iconCls: 'fa fa-cogs',
+ itemId: 'services',
+ expandedOnInit: true,
+ startOnlyServices: {
+ 'pveproxy': true,
+ 'pvedaemon': true,
+ 'pve-cluster': true
+ },
+ nodename: nodename,
+ onlineHelp: 'pve_service_daemons',
+ xtype: 'proxmoxNodeServiceView'
+ },
+ {
+ title: gettext('Network'),
+ iconCls: 'fa fa-exchange',
+ itemId: 'network',
+ groups: ['services'],
+ nodename: nodename,
+ onlineHelp: 'sysadmin_network_configuration',
+ xtype: 'proxmoxNodeNetworkView'
+ },
+ {
+ title: gettext('Certificates'),
+ iconCls: 'fa fa-certificate',
+ itemId: 'certificates',
+ groups: ['services'],
+ nodename: nodename,
+ xtype: 'pveCertificatesView'
+ },
+ {
+ title: gettext('DNS'),
+ iconCls: 'fa fa-globe',
+ groups: ['services'],
+ itemId: 'dns',
+ nodename: nodename,
+ onlineHelp: 'sysadmin_network_configuration',
+ xtype: 'proxmoxNodeDNSView'
+ },
+ {
+ title: gettext('Hosts'),
+ iconCls: 'fa fa-globe',
+ groups: ['services'],
+ itemId: 'hosts',
+ nodename: nodename,
+ onlineHelp: 'sysadmin_network_configuration',
+ xtype: 'proxmoxNodeHostsView'
+ },
+ {
+ title: gettext('Time'),
+ itemId: 'time',
+ groups: ['services'],
+ nodename: nodename,
+ xtype: 'proxmoxNodeTimeView',
+ iconCls: 'fa fa-clock-o'
+ });
+ }
+
+ if (caps.nodes['Sys.Syslog']) {
+ me.items.push({
+ title: 'Syslog',
+ iconCls: 'fa fa-list',
+ groups: ['services'],
+ disabled: !caps.nodes['Sys.Syslog'],
+ itemId: 'syslog',
+ xtype: 'proxmoxJournalView',
+ url: "/api2/extjs/nodes/" + nodename + "/journal"
+ });
+
+ if (caps.nodes['Sys.Modify']) {
+ me.items.push({
+ title: gettext('Updates'),
+ iconCls: 'fa fa-refresh',
+ disabled: !caps.nodes['Sys.Console'],
+ // do we want to link to system updates instead?
+ itemId: 'apt',
+ xtype: 'proxmoxNodeAPT',
+ upgradeBtn: {
+ xtype: 'pveConsoleButton',
+ disabled: Proxmox.UserName !== 'root@pam',
+ text: gettext('Upgrade'),
+ consoleType: 'upgrade',
+ nodename: nodename
+ },
+ nodename: nodename
+ });
+ }
+ }
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ xtype: 'pveFirewallRules',
+ iconCls: 'fa fa-shield',
+ title: gettext('Firewall'),
+ allow_iface: true,
+ base_url: '/nodes/' + nodename + '/firewall/rules',
+ list_refs_url: '/cluster/firewall/refs',
+ itemId: 'firewall'
+ },
+ {
+ xtype: 'pveFirewallOptions',
+ title: gettext('Options'),
+ iconCls: 'fa fa-gear',
+ onlineHelp: 'pve_firewall_host_specific_configuration',
+ groups: ['firewall'],
+ base_url: '/nodes/' + nodename + '/firewall/options',
+ fwtype: 'node',
+ itemId: 'firewall-options'
+ });
+ }
+
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ title: gettext('Disks'),
+ itemId: 'storage',
+ expandedOnInit: true,
+ iconCls: 'fa fa-hdd-o',
+ xtype: 'pveNodeDiskList'
+ },
+ {
+ title: 'LVM',
+ itemId: 'lvm',
+ onlineHelp: 'chapter_lvm',
+ iconCls: 'fa fa-square',
+ groups: ['storage'],
+ xtype: 'pveLVMList'
+ },
+ {
+ title: 'LVM-Thin',
+ itemId: 'lvmthin',
+ onlineHelp: 'chapter_lvm',
+ iconCls: 'fa fa-square-o',
+ groups: ['storage'],
+ xtype: 'pveLVMThinList'
+ },
+ {
+ title: Proxmox.Utils.directoryText,
+ itemId: 'directory',
+ onlineHelp: 'chapter_storage',
+ iconCls: 'fa fa-folder',
+ groups: ['storage'],
+ xtype: 'pveDirectoryList'
+ },
+ {
+ title: 'ZFS',
+ itemId: 'zfs',
+ onlineHelp: 'chapter_zfs',
+ iconCls: 'fa fa-th-large',
+ groups: ['storage'],
+ xtype: 'pveZFSList'
+ },
+ {
+ title: 'Ceph',
+ itemId: 'ceph',
+ iconCls: 'fa fa-ceph',
+ xtype: 'pveNodeCephStatus'
+ },
+ {
+ xtype: 'pveReplicaView',
+ iconCls: 'fa fa-retweet',
+ title: gettext('Replication'),
+ itemId: 'replication'
+ },
+ {
+ xtype: 'pveNodeCephConfigCrush',
+ title: gettext('Configuration'),
+ iconCls: 'fa fa-gear',
+ groups: ['ceph'],
+ itemId: 'ceph-config'
+ },
+ {
+ xtype: 'pveNodeCephMonMgr',
+ title: gettext('Monitor'),
+ iconCls: 'fa fa-tv',
+ groups: ['ceph'],
+ itemId: 'ceph-monlist'
+ },
+ {
+ xtype: 'pveNodeCephOsdTree',
+ title: 'OSD',
+ iconCls: 'fa fa-hdd-o',
+ groups: ['ceph'],
+ itemId: 'ceph-osdtree'
+ },
+ {
+ xtype: 'pveNodeCephFSPanel',
+ title: 'CephFS',
+ iconCls: 'fa fa-folder',
+ groups: ['ceph'],
+ nodename: nodename,
+ itemId: 'ceph-cephfspanel'
+ },
+ {
+ xtype: 'pveNodeCephPoolList',
+ title: 'Pools',
+ iconCls: 'fa fa-sitemap',
+ groups: ['ceph'],
+ itemId: 'ceph-pools'
+ }
+ );
+ }
+
+ if (caps.nodes['Sys.Syslog']) {
+ me.items.push(
+ {
+ xtype: 'proxmoxLogView',
+ title: gettext('Log'),
+ iconCls: 'fa fa-list',
+ groups: ['firewall'],
+ onlineHelp: 'chapter_pve_firewall',
+ url: '/api2/extjs/nodes/' + nodename + '/firewall/log',
+ itemId: 'firewall-fwlog'
+ },
+ {
+ title: gettext('Log'),
+ itemId: 'ceph-log',
+ iconCls: 'fa fa-list',
+ groups: ['ceph'],
+ onlineHelp: 'chapter_pveceph',
+ xtype: 'cephLogView',
+ url: "/api2/extjs/nodes/" + nodename + "/ceph/log",
+ nodename: nodename
+ });
+ }
+
+ me.items.push(
+ {
+ title: gettext('Task History'),
+ iconCls: 'fa fa-list',
+ itemId: 'tasks',
+ nodename: nodename,
+ xtype: 'proxmoxNodeTasks'
+ },
+ {
+ title: gettext('Subscription'),
+ iconCls: 'fa fa-support',
+ itemId: 'support',
+ xtype: 'pveNodeSubscription',
+ nodename: nodename
+ }
+ );
+
+ me.callParent();
+
+ me.mon(me.statusStore, 'load', function(s, records, success) {
+ var uptimerec = s.data.get('uptime');
+ var powermgmt = uptimerec ? uptimerec.data.value : false;
+ if (!caps.nodes['Sys.PowerMgmt']) {
+ powermgmt = false;
+ }
+ restartBtn.setDisabled(!powermgmt);
+ shutdownBtn.setDisabled(!powermgmt);
+ shellBtn.setDisabled(!powermgmt);
+ });
+
+ me.on('afterrender', function() {
+ me.statusStore.startUpdate();
+ });
+
+ me.on('destroy', function() {
+ me.statusStore.stopUpdate();
+ });
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.window.Migrate', {
+ extend: 'Ext.window.Window',
+
+ vmtype: undefined,
+ nodename: undefined,
+ vmid: undefined,
+
+ viewModel: {
+ data: {
+ vmid: undefined,
+ nodename: undefined,
+ vmtype: undefined,
+ running: false,
+ qemu: {
+ onlineHelp: 'qm_migration',
+ commonName: 'VM'
+ },
+ lxc: {
+ onlineHelp: 'pct_migration',
+ commonName: 'CT'
+ },
+ migration: {
+ possible: true,
+ preconditions: [],
+ 'with-local-disks': 0,
+ mode: undefined,
+ allowedNodes: undefined
+ }
+
+ },
+
+ formulas: {
+ setMigrationMode: function(get) {
+ if (get('running')){
+ if (get('vmtype') === 'qemu') {
+ return gettext('Online');
+ } else {
+ return gettext('Restart Mode');
+ }
+ } else {
+ return gettext('Offline');
+ }
+ },
+ setStorageselectorHidden: function(get) {
+ if (get('migration.with-local-disks') && get('running')) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'panel[reference=formPanel]': {
+ validityChange: function(panel, isValid) {
+ this.getViewModel().set('migration.possible', isValid);
+ this.checkMigratePreconditions();
+ }
+ }
+ },
+
+ init: function(view) {
+ var me = this,
+ vm = view.getViewModel();
+
+ if (!view.nodename) {
+ throw "missing custom view config: nodename";
+ }
+ vm.set('nodename', view.nodename);
+
+ if (!view.vmid) {
+ throw "missing custom view config: vmid";
+ }
+ vm.set('vmid', view.vmid);
+
+ if (!view.vmtype) {
+ throw "missing custom view config: vmtype";
+ }
+ vm.set('vmtype', view.vmtype);
+
+
+ view.setTitle(
+ Ext.String.format('{0} {1}{2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid)
+ );
+ me.lookup('proxmoxHelpButton').setHelpConfig({
+ onlineHelp: vm.get(view.vmtype).onlineHelp
+ });
+ me.checkMigratePreconditions();
+ me.lookup('formPanel').isValid();
+
+ },
+
+ onTargetChange: function (nodeSelector) {
+ //Always display the storages of the currently seleceted migration target
+ this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value);
+ this.checkMigratePreconditions();
+ },
+
+ startMigration: function() {
+ var me = this,
+ view = me.getView(),
+ vm = me.getViewModel();
+
+ var values = me.lookup('formPanel').getValues();
+ var params = {
+ target: values.target
+ };
+
+ if (vm.get('migration.mode')) {
+ params[vm.get('migration.mode')] = 1;
+ }
+ if (vm.get('migration.with-local-disks')) {
+ params['with-local-disks'] = 1;
+ }
+ //only submit targetstorage if vm is running, storage migration to different storage is only possible online
+ if (vm.get('migration.with-local-disks') && vm.get('running')) {
+ params.targetstorage = values.targetstorage;
+ }
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
+ waitMsgTarget: view,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var extraTitle = Ext.String.format(' ({0} ---> {1})', vm.get('nodename'), params.target);
+
+ Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid,
+ extraTitle: extraTitle
+ }).show();
+
+ view.close();
+ }
+ });
+
+ },
+
+ checkMigratePreconditions: function() {
+ var me = this,
+ vm = me.getViewModel();
+
+
+ var vmrec = PVE.data.ResourceStore.findRecord('vmid', vm.get('vmid'),
+ 0, false, false, true);
+ if (vmrec && vmrec.data && vmrec.data.running) {
+ vm.set('running', true);
+ }
+
+ if (vm.get('vmtype') === 'qemu') {
+ me.checkQemuPreconditions();
+ } else {
+ me.checkLxcPreconditions();
+ }
+ me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
+
+ // Only allow nodes where the local storage is available in case of offline migration
+ // where storage migration is not possible
+ me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes');
+
+ me.lookup('formPanel').isValid();
+
+ },
+
+ checkQemuPreconditions: function() {
+ var me = this,
+ vm = me.getViewModel(),
+ migrateStats;
+
+ if (vm.get('running')) {
+ vm.set('migration.mode', 'online');
+ }
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
+ method: 'GET',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ migrateStats = response.result.data;
+ if (migrateStats.running) {
+ vm.set('running', true);
+ }
+ // Get migration object from viewmodel to prevent
+ // to many bind callbacks
+ var migration = vm.get('migration');
+ migration.preconditions = [];
+
+ if (migrateStats.allowed_nodes) {
+ migration.allowedNodes = migrateStats.allowed_nodes;
+ var target = me.lookup('pveNodeSelector').value;
+ if (target.length && !migrateStats.allowed_nodes.includes(target)) {
+ let disallowed = migrateStats.not_allowed_nodes[target];
+ let missing_storages = disallowed.unavailable_storages.join(', ');
+
+ migration.possible = false;
+ migration.preconditions.push({
+ text: 'Storage (' + missing_storages + ') not available on selected target. ' +
+ 'Start VM to use live storage migration or select other target node',
+ severity: 'error'
+ });
+ }
+ }
+
+ if (migrateStats.local_resources.length) {
+ migration.possible = false;
+ migration.preconditions.push({
+ text: 'Can\'t migrate VM with local resources: '+ migrateStats.local_resources.join(', '),
+ severity: 'error'
+ });
+ }
+
+ if (migrateStats.local_disks.length) {
+
+ migrateStats.local_disks.forEach(function (disk) {
+ if (disk.cdrom && disk.cdrom === 1) {
+ migration.possible = false;
+ migration.preconditions.push({
+ text: "Can't migrate VM with local CD/DVD",
+ severity: 'error'
+ });
+
+ } else if (!disk.referenced_in_config) {
+ migration.possible = false;
+ migration.preconditions.push({
+ text: 'Found not referenced/unused disk via storage: '+ disk.volid,
+ severity: 'error'
+ });
+ } else {
+ migration['with-local-disks'] = 1;
+ migration.preconditions.push({
+ text:'Migration with local disk might take long: ' + disk.volid
+ +' (' + PVE.Utils.render_size(disk.size) + ')',
+ severity: 'warning'
+ });
+ }
+ });
+
+ }
+
+ vm.set('migration', migration);
+
+ }
+ });
+ },
+ checkLxcPreconditions: function() {
+ var me = this,
+ vm = me.getViewModel();
+ if (vm.get('running')) {
+ vm.set('migration.mode', 'restart');
+ }
+ }
+
+
+ },
+
+ width: 600,
+ modal: true,
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+ border: false,
+ items: [
+ {
+ xtype: 'form',
+ reference: 'formPanel',
+ bodyPadding: 10,
+ border: false,
+ layout: {
+ type: 'column'
+ },
+ items: [
+ {
+ xtype: 'container',
+ columnWidth: 0.5,
+ items: [{
+ xtype: 'displayfield',
+ name: 'source',
+ fieldLabel: gettext('Source node'),
+ bind: {
+ value: '{nodename}'
+ }
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'migrationMode',
+ fieldLabel: gettext('Mode'),
+ bind: {
+ value: '{setMigrationMode}'
+ }
+ }]
+ },
+ {
+ xtype: 'container',
+ columnWidth: 0.5,
+ items: [{
+ xtype: 'pveNodeSelector',
+ reference: 'pveNodeSelector',
+ name: 'target',
+ fieldLabel: gettext('Target node'),
+ allowBlank: false,
+ disallowedNodes: undefined,
+ onlineValidator: true,
+ listeners: {
+ change: 'onTargetChange'
+ }
+ },
+ {
+ xtype: 'pveStorageSelector',
+ reference: 'pveDiskStorageSelector',
+ name: 'targetstorage',
+ fieldLabel: gettext('Target storage'),
+ storageContent: 'images',
+ bind: {
+ hidden: '{setStorageselectorHidden}'
+ }
+ }]
+ }
+ ]
+ },
+ {
+ xtype: 'gridpanel',
+ reference: 'preconditionGrid',
+ selectable: false,
+ flex: 1,
+ columns: [{
+ text: '',
+ dataIndex: 'severity',
+ renderer: function(v) {
+ switch (v) {
+ case 'warning':
+ return ' ';
+ case 'error':
+ return '';
+ default:
+ return v;
+ }
+ },
+ width: 35
+ },
+ {
+ text: 'Info',
+ dataIndex: 'text',
+ cellWrap: true,
+ flex: 1
+ }],
+ bind: {
+ hidden: '{!migration.preconditions.length}',
+ store: {
+ fields: ['severity','text'],
+ data: '{migration.preconditions}'
+ }
+ }
+ }
+
+ ],
+ buttons: [
+ {
+ xtype: 'proxmoxHelpButton',
+ reference: 'proxmoxHelpButton',
+ onlineHelp: 'pct_migration',
+ listenToGlobalEvent: false,
+ hidden: false
+ },
+ '->',
+ {
+ xtype: 'button',
+ reference: 'submitButton',
+ text: gettext('Migrate'),
+ handler: 'startMigration',
+ bind: {
+ disabled: '{!migration.possible}'
+ }
+ }
+ ]
+});
+Ext.define('PVE.window.BulkAction', {
+ extend: 'Ext.window.Window',
+
+ resizable: true,
+ width: 800,
+ modal: true,
+ layout: {
+ type: 'fit'
+ },
+ border: false,
+
+ // the action to be set
+ // currently there are
+ // startall
+ // migrateall
+ // stopall
+ action: undefined,
+
+ submit: function(params) {
+ var me = this;
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + me.nodename + '/' + me.action,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid
+ });
+ win.show();
+ me.hide();
+ win.on('destroy', function() {
+ me.close();
+ });
+ }
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.action) {
+ throw "no action specified";
+ }
+
+ if (!me.btnText) {
+ throw "no button text specified";
+ }
+
+ if (!me.title) {
+ throw "no title specified";
+ }
+
+ var items = [];
+
+ if (me.action === 'migrateall') {
+ /*jslint confusion: true*/
+ /*value is string and number*/
+ items.push(
+ {
+ xtype: 'pveNodeSelector',
+ name: 'target',
+ disallowedNodes: [me.nodename],
+ fieldLabel: gettext('Target node'),
+ allowBlank: false,
+ onlineValidator: true
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'maxworkers',
+ minValue: 1,
+ maxValue: 100,
+ value: 1,
+ fieldLabel: gettext('Parallel jobs'),
+ allowBlank: false
+ },
+ {
+ itemId: 'lxcwarning',
+ xtype: 'displayfield',
+ userCls: 'pve-hint',
+ value: 'Warning: Running CTs will be migrated in Restart Mode.',
+ hidden: true // only visible if running container chosen
+ }
+ );
+ /*jslint confusion: false*/
+ } else if (me.action === 'startall') {
+ items.push({
+ xtype: 'hiddenfield',
+ name: 'force',
+ value: 1
+ });
+ }
+
+ items.push({
+ xtype: 'vmselector',
+ itemId: 'vms',
+ name: 'vms',
+ flex: 1,
+ height: 300,
+ selectAll: true,
+ allowBlank: false,
+ nodename: me.nodename,
+ action: me.action,
+ listeners: {
+ selectionchange: function(vmselector, records) {
+ if (me.action == 'migrateall') {
+ var showWarning = records.some(function(item) {
+ return (item.data.type == 'lxc' &&
+ item.data.status == 'running');
+ });
+ me.down('#lxcwarning').setVisible(showWarning);
+ }
+ }
+ }
+ });
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+ fieldDefaults: {
+ labelWidth: 300,
+ anchor: '100%'
+ },
+ items: items
+ });
+
+ var form = me.formPanel.getForm();
+
+ var submitBtn = Ext.create('Ext.Button', {
+ text: me.btnText,
+ handler: function() {
+ form.isValid();
+ me.submit(form.getValues());
+ }
+ });
+
+ Ext.apply(me, {
+ items: [ me.formPanel ],
+ buttons: [ submitBtn ]
+ });
+
+ me.callParent();
+
+ form.on('validitychange', function() {
+ var valid = form.isValid();
+ submitBtn.setDisabled(!valid);
+ });
+ form.isValid();
+ }
+});
+Ext.define('PVE.window.Clone', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ isTemplate: false,
+
+ onlineHelp: 'qm_copy_and_clone',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'panel[reference=cloneform]': {
+ validitychange: 'disableSubmit'
+ }
+ },
+ disableSubmit: function(form) {
+ this.lookupReference('submitBtn').setDisabled(!form.isValid());
+ }
+ },
+
+ statics: {
+ // display a snapshot selector only if needed
+ wrap: function(nodename, vmid, isTemplate, guestType) {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/' + guestType + '/' + vmid +'/snapshot',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, opts) {
+ var snapshotList = response.result.data;
+ var hasSnapshots = snapshotList.length === 1 &&
+ snapshotList[0].name === 'current' ? false : true;
+
+ Ext.create('PVE.window.Clone', {
+ nodename: nodename,
+ guestType: guestType,
+ vmid: vmid,
+ isTemplate: isTemplate,
+ hasSnapshots: hasSnapshots
+ }).show();
+ }
+ });
+ }
+ },
+
+ create_clone: function(values) {
+ var me = this;
+
+ var params = { newid: values.newvmid };
+
+ if (values.snapname && values.snapname !== 'current') {
+ params.snapname = values.snapname;
+ }
+
+ if (values.pool) {
+ params.pool = values.pool;
+ }
+
+ if (values.name) {
+ if (me.guestType === 'lxc') {
+ params.hostname = values.name;
+ } else {
+ params.name = values.name;
+ }
+ }
+
+ if (values.target) {
+ params.target = values.target;
+ }
+
+ if (values.clonemode === 'copy') {
+ params.full = 1;
+ if (values.hdstorage) {
+ params.storage = values.hdstorage;
+ if (values.diskformat && me.guestType !== 'lxc') {
+ params.format = values.diskformat;
+ }
+ }
+ }
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/clone',
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ me.close();
+ }
+ });
+
+ },
+
+ // disable the Storage selector when clone mode is linked clone
+ updateVisibility: function() {
+ var me = this;
+ var clonemode = me.lookupReference('clonemodesel').getValue();
+ var disksel = me.lookup('diskselector');
+ disksel.setDisabled(clonemode === 'clone');
+ },
+
+ // add to the list of valid nodes each node where
+ // all the VM disks are available
+ verifyFeature: function() {
+ var me = this;
+
+ var snapname = me.lookupReference('snapshotsel').getValue();
+ var clonemode = me.lookupReference('clonemodesel').getValue();
+
+ var params = { feature: clonemode };
+ if (snapname !== 'current') {
+ params.snapname = snapname;
+ }
+
+ Proxmox.Utils.API2Request({
+ waitMsgTarget: me,
+ url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/feature',
+ params: params,
+ method: 'GET',
+ failure: function(response, opts) {
+ me.lookupReference('submitBtn').setDisabled(true);
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ var res = response.result.data;
+
+ me.lookupReference('targetsel').allowedNodes = res.nodes;
+ me.lookupReference('targetsel').validate();
+ }
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.snapname) {
+ me.snapname = 'current';
+ }
+
+ if (!me.guestType) {
+ throw "no Guest Type specified";
+ }
+
+ var titletext = me.guestType === 'lxc' ? 'CT' : 'VM';
+ if (me.isTemplate) {
+ titletext += ' Template';
+ }
+ me.title = "Clone " + titletext + " " + me.vmid;
+
+ var col1 = [];
+ var col2 = [];
+
+ col1.push({
+ xtype: 'pveNodeSelector',
+ name: 'target',
+ reference: 'targetsel',
+ fieldLabel: gettext('Target node'),
+ selectCurNode: true,
+ allowBlank: false,
+ onlineValidator: true,
+ listeners: {
+ change: function(f, value) {
+ me.lookupReference('hdstorage').setTargetNode(value);
+ }
+ }
+ });
+
+ var modelist = [['copy', gettext('Full Clone')]];
+ if (me.isTemplate) {
+ modelist.push(['clone', gettext('Linked Clone')]);
+ }
+
+ col1.push({
+ xtype: 'pveGuestIDSelector',
+ name: 'newvmid',
+ guestType: me.guestType,
+ value: '',
+ loadNextFreeID: true,
+ validateExists: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'name',
+ allowBlank: true,
+ fieldLabel: me.guestType === 'lxc' ? gettext('Hostname') : gettext('Name')
+ },
+ {
+ xtype: 'pvePoolSelector',
+ fieldLabel: gettext('Resource Pool'),
+ name: 'pool',
+ value: '',
+ allowBlank: true
+ }
+ );
+
+ col2.push({
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('Mode'),
+ name: 'clonemode',
+ reference: 'clonemodesel',
+ allowBlank: false,
+ hidden: !me.isTemplate,
+ value: me.isTemplate ? 'clone' : 'copy',
+ comboItems: modelist,
+ listeners: {
+ change: function(t, value) {
+ me.updateVisibility();
+ me.verifyFeature();
+ }
+ }
+ },
+ {
+ xtype: 'PVE.form.SnapshotSelector',
+ name: 'snapname',
+ reference: 'snapshotsel',
+ fieldLabel: gettext('Snapshot'),
+ nodename: me.nodename,
+ guestType: me.guestType,
+ vmid: me.vmid,
+ hidden: me.isTemplate || !me.hasSnapshots ? true : false,
+ disabled: false,
+ allowBlank: false,
+ value : me.snapname,
+ listeners: {
+ change: function(f, value) {
+ me.verifyFeature();
+ }
+ }
+ },
+ {
+ xtype: 'pveDiskStorageSelector',
+ reference: 'diskselector',
+ nodename: me.nodename,
+ autoSelect: false,
+ hideSize: true,
+ hideSelection: true,
+ storageLabel: gettext('Target Storage'),
+ allowBlank: true,
+ storageContent: me.guestType === 'qemu' ? 'images' : 'rootdir',
+ emptyText: gettext('Same as source'),
+ disabled: me.isTemplate ? true : false // because default mode is clone for templates
+ });
+
+ var formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ reference: 'cloneform',
+ border: false,
+ layout: 'column',
+ defaultType: 'container',
+ columns: 2,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: [
+ {
+ columnWidth: 0.5,
+ padding: '0 10 0 0',
+ layout: 'anchor',
+ items: col1
+ },
+ {
+ columnWidth: 0.5,
+ padding: '0 0 0 10',
+ layout: 'anchor',
+ items: col2
+ }
+ ]
+ });
+
+ Ext.apply(me, {
+ modal: true,
+ width: 600,
+ height: 250,
+ border: false,
+ layout: 'fit',
+ buttons: [ {
+ xtype: 'proxmoxHelpButton',
+ listenToGlobalEvent: false,
+ hidden: false,
+ onlineHelp: me.onlineHelp
+ },
+ '->',
+ {
+ reference: 'submitBtn',
+ text: gettext('Clone'),
+ disabled: true,
+ handler: function() {
+ var cloneForm = me.lookupReference('cloneform');
+ if (cloneForm.isValid()) {
+ me.create_clone(cloneForm.getValues());
+ }
+ }
+ } ],
+ items: [ formPanel ]
+ });
+
+ me.callParent();
+
+ me.verifyFeature();
+ }
+});
+Ext.define('PVE.qemu.Monitor', {
+ extend: 'Ext.panel.Panel',
+
+ alias: 'widget.pveQemuMonitor',
+
+ maxLines: 500,
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var history = [];
+ var histNum = -1;
+ var lines = [];
+
+ var textbox = Ext.createWidget('panel', {
+ region: 'center',
+ xtype: 'panel',
+ autoScroll: true,
+ border: true,
+ margins: '5 5 5 5',
+ bodyStyle: 'font-family: monospace;'
+ });
+
+ var scrollToEnd = function() {
+ var el = textbox.getTargetEl();
+ var dom = Ext.getDom(el);
+
+ var clientHeight = dom.clientHeight;
+ // BrowserBug: clientHeight reports 0 in IE9 StrictMode
+ // Instead we are using offsetHeight and hardcoding borders
+ if (Ext.isIE9 && Ext.isStrict) {
+ clientHeight = dom.offsetHeight + 2;
+ }
+ dom.scrollTop = dom.scrollHeight - clientHeight;
+ };
+
+ var refresh = function() {
+ textbox.update('' + lines.join('\n') + '
');
+ scrollToEnd();
+ };
+
+ var addLine = function(line) {
+ lines.push(line);
+ if (lines.length > me.maxLines) {
+ lines.shift();
+ }
+ };
+
+ var executeCmd = function(cmd) {
+ addLine("# " + Ext.htmlEncode(cmd));
+ if (cmd) {
+ history.unshift(cmd);
+ if (history.length > 20) {
+ history.splice(20);
+ }
+ }
+ histNum = -1;
+
+ refresh();
+ Proxmox.Utils.API2Request({
+ params: { command: cmd },
+ url: '/nodes/' + nodename + '/qemu/' + vmid + "/monitor",
+ method: 'POST',
+ waitMsgTarget: me,
+ success: function(response, opts) {
+ var res = response.result.data;
+ Ext.Array.each(res.split('\n'), function(line) {
+ addLine(Ext.htmlEncode(line));
+ });
+ refresh();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ };
+
+ Ext.apply(me, {
+ layout: { type: 'border' },
+ border: false,
+ items: [
+ textbox,
+ {
+ region: 'south',
+ margins:'0 5 5 5',
+ border: false,
+ xtype: 'textfield',
+ name: 'cmd',
+ value: '',
+ fieldStyle: 'font-family: monospace;',
+ allowBlank: true,
+ listeners: {
+ afterrender: function(f) {
+ f.focus(false);
+ addLine("Type 'help' for help.");
+ refresh();
+ },
+ specialkey: function(f, e) {
+ var key = e.getKey();
+ switch (key) {
+ case e.ENTER:
+ var cmd = f.getValue();
+ f.setValue('');
+ executeCmd(cmd);
+ break;
+ case e.PAGE_UP:
+ textbox.scrollBy(0, -0.9*textbox.getHeight(), false);
+ break;
+ case e.PAGE_DOWN:
+ textbox.scrollBy(0, 0.9*textbox.getHeight(), false);
+ break;
+ case e.UP:
+ if (histNum + 1 < history.length) {
+ f.setValue(history[++histNum]);
+ }
+ e.preventDefault();
+ break;
+ case e.DOWN:
+ if (histNum > 0) {
+ f.setValue(history[--histNum]);
+ }
+ e.preventDefault();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }
+ ],
+ listeners: {
+ show: function() {
+ var field = me.query('textfield[name="cmd"]')[0];
+ field.focus(false, true);
+ }
+ }
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.qemu.Summary', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveQemuSummary',
+
+ scrollable: true,
+ bodyPadding: 5,
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.workspace) {
+ throw "no workspace specified";
+ }
+
+ if (!me.statusStore) {
+ throw "no status storage specified";
+ }
+
+ var template = !!me.pveSelNode.data.template;
+ var rstore = me.statusStore;
+
+ var width = template ? 1 : 0.5;
+ var items = [
+ {
+ xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView',
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: width
+ },
+ 'width >= 1900': {
+ columnWidth: width / 2
+ }
+ },
+ itemId: 'gueststatus',
+ pveSelNode: me.pveSelNode,
+ rstore: rstore
+ },
+ {
+ xtype: 'pveNotesView',
+ maxHeight: 330,
+ itemId: 'notesview',
+ pveSelNode: me.pveSelNode,
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: width
+ },
+ 'width >= 1900': {
+ columnWidth: width / 2
+ }
+ }
+ }
+ ];
+
+ var rrdstore;
+ if (!template) {
+
+ rrdstore = Ext.create('Proxmox.data.RRDStore', {
+ rrdurl: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/rrddata",
+ model: 'pve-rrd-guest'
+ });
+
+ items.push(
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('CPU usage'),
+ pveSelNode: me.pveSelNode,
+ fields: ['cpu'],
+ fieldTitles: [gettext('CPU usage')],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Memory usage'),
+ pveSelNode: me.pveSelNode,
+ fields: ['maxmem', 'mem'],
+ fieldTitles: [gettext('Total'), gettext('RAM usage')],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Network traffic'),
+ pveSelNode: me.pveSelNode,
+ fields: ['netin','netout'],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Disk IO'),
+ pveSelNode: me.pveSelNode,
+ fields: ['diskread','diskwrite'],
+ store: rrdstore
+ }
+ );
+
+ }
+
+ Ext.apply(me, {
+ tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ],
+ items: [
+ {
+ xtype: 'container',
+ layout: {
+ type: 'column'
+ },
+ defaults: {
+ minHeight: 330,
+ padding: 5,
+ plugins: 'responsive',
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: 1
+ },
+ 'width >= 1900': {
+ columnWidth: 0.5
+ }
+ }
+ },
+ items: items
+ }
+ ]
+ });
+
+ me.callParent();
+ if (!template) {
+ rrdstore.startUpdate();
+ me.on('destroy', rrdstore.stopUpdate);
+ }
+ }
+});
+Ext.define('PVE.qemu.OSTypeInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuOSTypePanel',
+ onlineHelp: 'qm_os_settings',
+ insideWizard: false,
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'combobox[name=osbase]': {
+ change: 'onOSBaseChange'
+ },
+ 'combobox[name=ostype]': {
+ afterrender: 'onOSTypeChange',
+ change: 'onOSTypeChange'
+ }
+ },
+ onOSBaseChange: function(field, value) {
+ this.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]);
+ },
+ onOSTypeChange: function(field) {
+ var me = this, ostype = field.getValue();
+ if (!me.getView().insideWizard) {
+ return;
+ }
+ var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype);
+
+ me.setWidget('pveBusSelector', targetValues.busType);
+ me.setWidget('pveNetworkCardSelector', targetValues.networkCard);
+ var scsihw = targetValues.scsihw || '__default__';
+ this.getViewModel().set('current.scsihw', scsihw);
+ },
+ setWidget: function(widget, newValue) {
+ // changing a widget is safe only if ComponentQuery.query returns us
+ // a single value array
+ var widgets = Ext.ComponentQuery.query('pveQemuCreateWizard ' + widget);
+ if (widgets.length === 1) {
+ widgets[0].setValue(newValue);
+ } else {
+ throw 'non unique widget :' + widget + ' in Wizard';
+ }
+ }
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ /*jslint confusion: true */
+ me.items = [
+ {
+ xtype: 'displayfield',
+ value: gettext('Guest OS') + ':',
+ hidden: !me.insideWizard
+ },
+ {
+ xtype: 'combobox',
+ submitValue: false,
+ name: 'osbase',
+ fieldLabel: gettext('Type'),
+ editable: false,
+ queryMode: 'local',
+ value: 'Linux',
+ store: Object.keys(PVE.Utils.kvm_ostypes)
+ },
+ {
+ xtype: 'combobox',
+ name: 'ostype',
+ reference: 'ostype',
+ fieldLabel: gettext('Version'),
+ value: 'l26',
+ allowBlank : false,
+ editable: false,
+ queryMode: 'local',
+ valueField: 'val',
+ displayField: 'desc',
+ store: {
+ fields: ['desc', 'val'],
+ data: PVE.Utils.kvm_ostypes.Linux,
+ listeners: {
+ datachanged: function (store) {
+ var ostype = me.lookup('ostype');
+ var old_val = ostype.getValue();
+ if (!me.insideWizard && old_val && store.find('val', old_val) != -1) {
+ ostype.setValue(old_val);
+ } else {
+ ostype.setValue(store.getAt(0));
+ }
+ }
+ }
+ }
+ }
+ ];
+ /*jslint confusion: false */
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.OSTypeEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ subject: 'OS Type',
+
+ items: [{ xtype: 'pveQemuOSTypePanel' }],
+
+ initComponent : function() {
+ var me = this;
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var value = response.result.data.ostype || 'other';
+ var osinfo = PVE.Utils.get_kvm_osinfo(value);
+ me.setValues({ ostype: value, osbase: osinfo.base });
+ }
+ });
+ }
+});
+/*
+ * This class holds performance *recommended* settings for the PVE Qemu wizards
+ * the *mandatory* settings are set in the PVE::QemuServer
+ * config_to_command sub
+ * We store this here until we get the data from the API server
+*/
+
+// this is how you would add an hypothetic FreeBSD > 10 entry
+//
+//virtio-blk is stable but virtIO net still
+// problematic as of 10.3
+// see https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=165059
+// addOS({
+// parent: 'generic', // inherits defaults
+// pveOS: 'freebsd10', // must match a radiofield in OSTypeEdit.js
+// busType: 'virtio' // must match a pveBusController value
+// // networkCard muss match a pveNetworkCardSelector
+
+
+Ext.define('PVE.qemu.OSDefaults', {
+ singleton: true, // will also force creation when loaded
+
+ constructor: function() {
+ var me = this;
+
+ var addOS = function(settings) {
+ if (me.hasOwnProperty(settings.parent)) {
+ var child = Ext.clone(me[settings.parent]);
+ me[settings.pveOS] = Ext.apply(child, settings);
+
+ } else {
+ throw("Could not find your genitor");
+ }
+ };
+
+ // default values
+ me.generic = {
+ busType: 'ide',
+ networkCard: 'e1000',
+ busPriority: {
+ ide: 4,
+ sata: 3,
+ scsi: 2,
+ virtio: 1
+ },
+ scsihw: 'virtio-scsi-pci'
+ };
+
+ // virtio-net is in kernel since 2.6.25
+ // virtio-scsi since 3.2 but backported in RHEL with 2.6 kernel
+ addOS({
+ pveOS: 'l26',
+ parent : 'generic',
+ busType: 'scsi',
+ busPriority: {
+ scsi: 4,
+ virtio: 3,
+ sata: 2,
+ ide: 1
+ },
+ networkCard: 'virtio'
+ });
+
+ // recommandation from http://wiki.qemu.org/Windows2000
+ addOS({
+ pveOS: 'w2k',
+ parent : 'generic',
+ networkCard: 'rtl8139',
+ scsihw: ''
+ });
+ // https://pve.proxmox.com/wiki/Windows_XP_Guest_Notes
+ addOS({
+ pveOS: 'wxp',
+ parent : 'w2k'
+ });
+
+ me.getDefaults = function(ostype) {
+ if (PVE.qemu.OSDefaults[ostype]) {
+ return PVE.qemu.OSDefaults[ostype];
+ } else {
+ return PVE.qemu.OSDefaults.generic;
+ }
+ };
+ }
+});
+Ext.define('PVE.qemu.ProcessorInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuProcessorPanel',
+ onlineHelp: 'qm_cpu',
+
+ insideWizard: false,
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ updateCores: function() {
+ var me = this.getView();
+ var sockets = me.down('field[name=sockets]').getValue();
+ var cores = me.down('field[name=cores]').getValue();
+ me.down('field[name=totalcores]').setValue(sockets*cores);
+ var vcpus = me.down('field[name=vcpus]');
+ vcpus.setMaxValue(sockets*cores);
+ vcpus.setEmptyText(sockets*cores);
+ vcpus.validate();
+ },
+
+ control: {
+ 'field[name=sockets]': {
+ change: 'updateCores'
+ },
+ 'field[name=cores]': {
+ change: 'updateCores'
+ }
+ }
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (Array.isArray(values['delete'])) {
+ values['delete'] = values['delete'].join(',');
+ }
+
+ PVE.Utils.delete_if_default(values, 'cpulimit', '0', 0);
+ PVE.Utils.delete_if_default(values, 'cpuunits', '1024', 0);
+
+ // build the cpu options:
+ me.cpu.cputype = values.cputype;
+
+ if (values.flags) {
+ me.cpu.flags = values.flags;
+ } else {
+ delete me.cpu.flags;
+ }
+
+ delete values.cputype;
+ delete values.flags;
+ var cpustring = PVE.Parser.printQemuCpu(me.cpu);
+
+ // remove cputype delete request:
+ var del = values['delete'];
+ delete values['delete'];
+ if (del) {
+ del = del.split(',');
+ Ext.Array.remove(del, 'cputype');
+ } else {
+ del = [];
+ }
+
+ if (cpustring) {
+ values.cpu = cpustring;
+ } else {
+ del.push('cpu');
+ }
+
+ var delarr = del.join(',');
+ if (delarr) {
+ values['delete'] = delarr;
+ }
+
+ return values;
+ },
+
+ cpu: {},
+
+ column1: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'sockets',
+ minValue: 1,
+ maxValue: 4,
+ value: '1',
+ fieldLabel: gettext('Sockets'),
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'cores',
+ minValue: 1,
+ maxValue: 128,
+ value: '1',
+ fieldLabel: gettext('Cores'),
+ allowBlank: false
+ }
+ ],
+
+ column2: [
+ {
+ xtype: 'CPUModelSelector',
+ name: 'cputype',
+ value: '__default__',
+ fieldLabel: gettext('Type')
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Total cores'),
+ name: 'totalcores',
+ value: '1'
+ }
+ ],
+
+ advancedColumn1: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'vcpus',
+ minValue: 1,
+ maxValue: 1,
+ value: '',
+ fieldLabel: gettext('VCPUs'),
+ deleteEmpty: true,
+ allowBlank: true,
+ emptyText: '1'
+ },
+ {
+ xtype: 'numberfield',
+ name: 'cpulimit',
+ minValue: 0,
+ maxValue: 128, // api maximum
+ value: '',
+ step: 1,
+ fieldLabel: gettext('CPU limit'),
+ allowBlank: true,
+ emptyText: gettext('unlimited')
+ }
+ ],
+
+ advancedColumn2: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'cpuunits',
+ fieldLabel: gettext('CPU units'),
+ minValue: 8,
+ maxValue: 500000,
+ value: '1024',
+ deleteEmpty: true,
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Enable NUMA'),
+ name: 'numa',
+ uncheckedValue: 0
+ }
+ ],
+ advancedColumnB: [
+ {
+ xtype: 'label',
+ text: 'Extra CPU Flags:'
+ },
+ {
+ xtype: 'vmcpuflagselector',
+ name: 'flags'
+ }
+ ]
+});
+
+Ext.define('PVE.qemu.ProcessorEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ width: 700,
+
+ initComponent : function() {
+ var me = this;
+
+ var ipanel = Ext.create('PVE.qemu.ProcessorInputPanel');
+
+ Ext.apply(me, {
+ subject: gettext('Processors'),
+ items: ipanel
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data;
+ var value = data.cpu;
+ if (value) {
+ var cpu = PVE.Parser.parseQemuCpu(value);
+ ipanel.cpu = cpu;
+ data.cputype = cpu.cputype;
+ if (cpu.flags) {
+ data.flags = cpu.flags;
+ }
+ }
+ me.setValues(data);
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.BootOrderPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuBootOrderPanel',
+ vmconfig: {}, // store loaded vm config
+
+ bootdisk: undefined,
+ selection: [],
+ list: [],
+ comboboxes: [],
+
+ isBootDisk: function(value) {
+ return PVE.Utils.bus_match.test(value);
+ },
+
+ setVMConfig: function(vmconfig) {
+ var me = this;
+ me.vmconfig = vmconfig;
+ var order = me.vmconfig.boot || 'cdn';
+ me.bootdisk = me.vmconfig.bootdisk || undefined;
+
+ // get the first 3 characters
+ // ignore the rest (there should never be more than 3)
+ me.selection = order.split('').slice(0,3);
+
+ // build bootdev list
+ me.list = [];
+ Ext.Object.each(me.vmconfig, function(key, value) {
+ if (me.isBootDisk(key) &&
+ !(/media=cdrom/).test(value)) {
+ me.list.push([key, "Disk '" + key + "'"]);
+ }
+ });
+
+ me.list.push(['d', 'CD-ROM']);
+ me.list.push(['n', gettext('Network')]);
+ me.list.push(['__none__', Proxmox.Utils.noneText]);
+
+ me.recomputeList();
+
+ me.comboboxes.forEach(function(box) {
+ box.resetOriginalValue();
+ });
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+ var order = me.selection.join('');
+ var res = { boot: order };
+
+ if (me.bootdisk && order.indexOf('c') !== -1) {
+ res.bootdisk = me.bootdisk;
+ } else {
+ res['delete'] = 'bootdisk';
+ }
+
+ return res;
+ },
+
+ recomputeSelection: function(combobox, newVal, oldVal) {
+ var me = this.up('#inputpanel');
+ me.selection = [];
+ me.comboboxes.forEach(function(item) {
+ var val = item.getValue();
+
+ // when selecting an already selected item,
+ // switch it around
+ if ((val === newVal || (me.isBootDisk(val) && me.isBootDisk(newVal))) &&
+ item.name !== combobox.name &&
+ newVal !== '__none__') {
+ // swap items
+ val = oldVal;
+ }
+
+ // push 'c','d' or 'n' in the array
+ if (me.isBootDisk(val)) {
+ me.selection.push('c');
+ me.bootdisk = val;
+ } else if (val === 'd' ||
+ val === 'n') {
+ me.selection.push(val);
+ }
+ });
+
+ me.recomputeList();
+ },
+
+ recomputeList: function(){
+ var me = this;
+ // set the correct values in the kvcomboboxes
+ var cnt = 0;
+ me.comboboxes.forEach(function(item) {
+ if (cnt === 0) {
+ // never show 'none' on first combobox
+ item.store.loadData(me.list.slice(0, me.list.length-1));
+ } else {
+ item.store.loadData(me.list);
+ }
+ item.suspendEvent('change');
+ if (cnt < me.selection.length) {
+ item.setValue((me.selection[cnt] !== 'c')?me.selection[cnt]:me.bootdisk);
+ } else if (cnt === 0){
+ item.setValue('');
+ } else {
+ item.setValue('__none__');
+ }
+ cnt++;
+ item.resumeEvent('change');
+ item.validate();
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ // this has to be done here, because of
+ // the way our inputPanel class handles items
+ me.comboboxes = [
+ Ext.createWidget('proxmoxKVComboBox', {
+ fieldLabel: gettext('Boot device') + " 1",
+ labelWidth: 120,
+ name: 'bd1',
+ allowBlank: false,
+ listeners: {
+ change: me.recomputeSelection
+ }
+ }),
+ Ext.createWidget('proxmoxKVComboBox', {
+ fieldLabel: gettext('Boot device') + " 2",
+ labelWidth: 120,
+ name: 'bd2',
+ allowBlank: false,
+ listeners: {
+ change: me.recomputeSelection
+ }
+ }),
+ Ext.createWidget('proxmoxKVComboBox', {
+ fieldLabel: gettext('Boot device') + " 3",
+ labelWidth: 120,
+ name: 'bd3',
+ allowBlank: false,
+ listeners: {
+ change: me.recomputeSelection
+ }
+ })
+ ];
+ Ext.apply(me, { items: me.comboboxes });
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.BootOrderEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ items: [{
+ xtype: 'pveQemuBootOrderPanel',
+ itemId: 'inputpanel'
+ }],
+
+ subject: gettext('Boot Order'),
+
+ initComponent : function() {
+ var me = this;
+ me.callParent();
+ me.load({
+ success: function(response, options) {
+ me.down('#inputpanel').setVMConfig(response.result.data);
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.MemoryInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuMemoryPanel',
+ onlineHelp: 'qm_memory',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var res = {};
+
+ res.memory = values.memory;
+ res.balloon = values.balloon;
+
+ if (!values.ballooning) {
+ res.balloon = 0;
+ res['delete'] = 'shares';
+ } else if (values.memory === values.balloon) {
+ delete res.balloon;
+ res['delete'] = 'balloon,shares';
+ } else if (Ext.isDefined(values.shares) && (values.shares !== "")) {
+ res.shares = values.shares;
+ } else {
+ res['delete'] = "shares";
+ }
+
+ return res;
+ },
+
+ initComponent: function() {
+ var me = this;
+ var labelWidth = 160;
+
+ me.items= [
+ {
+ xtype: 'pveMemoryField',
+ labelWidth: labelWidth,
+ fieldLabel: gettext('Memory') + ' (MiB)',
+ name: 'memory',
+ minValue: 1,
+ step: 32,
+ hotplug: me.hotplug,
+ listeners: {
+ change: function(f, value, old) {
+ var bf = me.down('field[name=balloon]');
+ var balloon = bf.getValue();
+ bf.setMaxValue(value);
+ if (balloon === old) {
+ bf.setValue(value);
+ }
+ bf.validate();
+ }
+ }
+ }
+ ];
+
+ me.advancedItems= [
+ {
+ xtype: 'pveMemoryField',
+ name: 'balloon',
+ minValue: 1,
+ step: 32,
+ fieldLabel: gettext('Minimum memory') + ' (MiB)',
+ hotplug: me.hotplug,
+ labelWidth: labelWidth,
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ var memory = me.down('field[name=memory]').getValue();
+ var shares = me.down('field[name=shares]');
+ shares.setDisabled(value === memory);
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'shares',
+ disabled: true,
+ minValue: 0,
+ maxValue: 50000,
+ value: '',
+ step: 10,
+ fieldLabel: gettext('Shares'),
+ labelWidth: labelWidth,
+ allowBlank: true,
+ emptyText: Proxmox.Utils.defaultText + ' (1000)',
+ submitEmptyText: false
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ labelWidth: labelWidth,
+ value: '1',
+ name: 'ballooning',
+ fieldLabel: gettext('Ballooning Device'),
+ listeners: {
+ change: function(f, value) {
+ var bf = me.down('field[name=balloon]');
+ var shares = me.down('field[name=shares]');
+ var memory = me.down('field[name=memory]');
+ bf.setDisabled(!value);
+ shares.setDisabled(!value || (bf.getValue() === memory.getValue()));
+ }
+ }
+ }
+ ];
+
+ if (me.insideWizard) {
+ me.column1 = me.items;
+ me.items = undefined;
+ me.advancedColumn1 = me.advancedItems;
+ me.advancedItems = undefined;
+ }
+ me.callParent();
+ }
+
+});
+
+Ext.define('PVE.qemu.MemoryEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent: function() {
+ var me = this;
+
+ var memoryhotplug;
+ if(me.hotplug) {
+ Ext.each(me.hotplug.split(','), function(el) {
+ if (el === 'memory') {
+ memoryhotplug = 1;
+ }
+ });
+ }
+
+ var ipanel = Ext.create('PVE.qemu.MemoryInputPanel', {
+ hotplug: memoryhotplug
+ });
+
+ Ext.apply(me, {
+ subject: gettext('Memory'),
+ items: [ ipanel ],
+ // uncomment the following to use the async configiguration API
+ // backgroundDelay: 5,
+ width: 400
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data;
+
+ var values = {
+ ballooning: data.balloon === 0 ? '0' : '1',
+ shares: data.shares,
+ memory: data.memory || '512',
+ balloon: data.balloon > 0 ? data.balloon : (data.memory || '512')
+ };
+
+ ipanel.setValues(values);
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.NetworkInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuNetworkInputPanel',
+ onlineHelp: 'qm_network_device',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ me.network.model = values.model;
+ if (values.nonetwork) {
+ return {};
+ } else {
+ me.network.bridge = values.bridge;
+ me.network.tag = values.tag;
+ me.network.firewall = values.firewall;
+ }
+ me.network.macaddr = values.macaddr;
+ me.network.disconnect = values.disconnect;
+ me.network.queues = values.queues;
+
+ if (values.rate) {
+ me.network.rate = values.rate;
+ } else {
+ delete me.network.rate;
+ }
+
+ var params = {};
+
+ params[me.confid] = PVE.Parser.printQemuNetwork(me.network);
+
+ return params;
+ },
+
+ setNetwork: function(confid, data) {
+ var me = this;
+
+ me.confid = confid;
+
+ if (data) {
+ data.networkmode = data.bridge ? 'bridge' : 'nat';
+ } else {
+ data = {};
+ data.networkmode = 'bridge';
+ }
+ me.network = data;
+
+ me.setValues(me.network);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ me.bridgesel.setNodename(nodename);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.network = {};
+ me.confid = 'net0';
+
+ me.column1 = [];
+ me.column2 = [];
+
+ me.bridgesel = Ext.create('PVE.form.BridgeSelector', {
+ name: 'bridge',
+ fieldLabel: gettext('Bridge'),
+ nodename: me.nodename,
+ autoSelect: true,
+ allowBlank: false
+ });
+
+ me.column1 = [
+ me.bridgesel,
+ {
+ xtype: 'pveVlanField',
+ name: 'tag',
+ value: ''
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Firewall'),
+ name: 'firewall',
+ checked: (me.insideWizard || me.isCreate)
+ }
+ ];
+
+ me.advancedColumn1 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Disconnect'),
+ name: 'disconnect'
+ }
+ ];
+
+ if (me.insideWizard) {
+ me.column1.unshift({
+ xtype: 'checkbox',
+ name: 'nonetwork',
+ inputValue: 'none',
+ boxLabel: gettext('No network device'),
+ listeners: {
+ change: function(cb, value) {
+ var fields = [
+ 'disconnect',
+ 'bridge',
+ 'tag',
+ 'firewall',
+ 'model',
+ 'macaddr',
+ 'rate',
+ 'queues'
+ ];
+ fields.forEach(function(fieldname) {
+ me.down('field[name='+fieldname+']').setDisabled(value);
+ });
+ me.down('field[name=bridge]').validate();
+ }
+ }
+ });
+ me.column2.unshift({
+ xtype: 'displayfield'
+ });
+ }
+
+ me.column2.push(
+ {
+ xtype: 'pveNetworkCardSelector',
+ name: 'model',
+ fieldLabel: gettext('Model'),
+ value: PVE.qemu.OSDefaults.generic.networkCard,
+ allowBlank: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'macaddr',
+ fieldLabel: gettext('MAC address'),
+ vtype: 'MacAddress',
+ allowBlank: true,
+ emptyText: 'auto'
+ });
+ me.advancedColumn2 = [
+ {
+ xtype: 'numberfield',
+ name: 'rate',
+ fieldLabel: gettext('Rate limit') + ' (MB/s)',
+ minValue: 0,
+ maxValue: 10*1024,
+ value: '',
+ emptyText: 'unlimited',
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'queues',
+ fieldLabel: 'Multiqueue',
+ minValue: 1,
+ maxValue: 8,
+ value: '',
+ allowBlank: true
+ }
+ ];
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.NetworkEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ initComponent : function() {
+ /*jslint confusion: true */
+
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = me.confid ? false : true;
+
+ var ipanel = Ext.create('PVE.qemu.NetworkInputPanel', {
+ confid: me.confid,
+ nodename: nodename,
+ isCreate: me.isCreate
+ });
+
+ Ext.applyIf(me, {
+ subject: gettext('Network Device'),
+ items: ipanel
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var i, confid;
+ me.vmconfig = response.result.data;
+ if (!me.isCreate) {
+ var value = me.vmconfig[me.confid];
+ var network = PVE.Parser.parseQemuNetwork(me.confid, value);
+ if (!network) {
+ Ext.Msg.alert(gettext('Error'), 'Unable to parse network options');
+ me.close();
+ return;
+ }
+ ipanel.setNetwork(me.confid, network);
+ } else {
+ for (i = 0; i < 100; i++) {
+ confid = 'net' + i.toString();
+ if (!Ext.isDefined(me.vmconfig[confid])) {
+ me.confid = confid;
+ break;
+ }
+ }
+ ipanel.setNetwork(me.confid);
+ }
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.Smbios1InputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.PVE.qemu.Smbios1InputPanel',
+
+ insideWizard: false,
+
+ smbios1: {},
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var params = {
+ smbios1: PVE.Parser.printQemuSmbios1(values)
+ };
+
+ return params;
+ },
+
+ setSmbios1: function(data) {
+ var me = this;
+
+ me.smbios1 = data;
+
+ me.setValues(me.smbios1);
+ },
+
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: 'UUID',
+ regex: /^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$/,
+ name: 'uuid'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Manufacturer'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'manufacturer'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Product'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'product'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Version'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'version'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Serial'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'serial'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: 'SKU',
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'sku'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Family'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'family'
+ }
+ ]
+});
+
+Ext.define('PVE.qemu.Smbios1Edit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent : function() {
+ /*jslint confusion: true */
+
+ var me = this;
+
+ var ipanel = Ext.create('PVE.qemu.Smbios1InputPanel', {});
+
+ Ext.applyIf(me, {
+ subject: gettext('SMBIOS settings (type1)'),
+ width: 450,
+ items: ipanel
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var i, confid;
+ me.vmconfig = response.result.data;
+ var value = me.vmconfig.smbios1;
+ if (value) {
+ var data = PVE.Parser.parseQemuSmbios1(value);
+ if (!data) {
+ Ext.Msg.alert(gettext('Error'), 'Unable to parse smbios options');
+ me.close();
+ return;
+ }
+ ipanel.setSmbios1(data);
+ }
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.CDInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuCDInputPanel',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var confid = me.confid || (values.controller + values.deviceid);
+
+ me.drive.media = 'cdrom';
+ if (values.mediaType === 'iso') {
+ me.drive.file = values.cdimage;
+ } else if (values.mediaType === 'cdrom') {
+ me.drive.file = 'cdrom';
+ } else {
+ me.drive.file = 'none';
+ }
+
+ var params = {};
+
+ params[confid] = PVE.Parser.printQemuDrive(me.drive);
+
+ return params;
+ },
+
+ setVMConfig: function(vmconfig) {
+ var me = this;
+
+ if (me.bussel) {
+ me.bussel.setVMConfig(vmconfig, 'cdrom');
+ }
+ },
+
+ setDrive: function(drive) {
+ var me = this;
+
+ var values = {};
+ if (drive.file === 'cdrom') {
+ values.mediaType = 'cdrom';
+ } else if (drive.file === 'none') {
+ values.mediaType = 'none';
+ } else {
+ values.mediaType = 'iso';
+ var match = drive.file.match(/^([^:]+):/);
+ if (match) {
+ values.cdstorage = match[1];
+ values.cdimage = drive.file;
+ }
+ }
+
+ me.drive = drive;
+
+ me.setValues(values);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ me.cdstoragesel.setNodename(nodename);
+ me.cdfilesel.setStorage(undefined, nodename);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.drive = {};
+
+ var items = [];
+
+ if (!me.confid) {
+ me.bussel = Ext.create('PVE.form.ControllerSelector', {
+ noVirtIO: true
+ });
+ items.push(me.bussel);
+ }
+
+ items.push({
+ xtype: 'radiofield',
+ name: 'mediaType',
+ inputValue: 'iso',
+ boxLabel: gettext('Use CD/DVD disc image file (iso)'),
+ checked: true,
+ listeners: {
+ change: function(f, value) {
+ if (!me.rendered) {
+ return;
+ }
+ me.down('field[name=cdstorage]').setDisabled(!value);
+ me.down('field[name=cdimage]').setDisabled(!value);
+ me.down('field[name=cdimage]').validate();
+ }
+ }
+ });
+
+ me.cdfilesel = Ext.create('PVE.form.FileSelector', {
+ name: 'cdimage',
+ nodename: me.nodename,
+ storageContent: 'iso',
+ fieldLabel: gettext('ISO image'),
+ labelAlign: 'right',
+ allowBlank: false
+ });
+
+ me.cdstoragesel = Ext.create('PVE.form.StorageSelector', {
+ name: 'cdstorage',
+ nodename: me.nodename,
+ fieldLabel: gettext('Storage'),
+ labelAlign: 'right',
+ storageContent: 'iso',
+ allowBlank: false,
+ autoSelect: me.insideWizard,
+ listeners: {
+ change: function(f, value) {
+ me.cdfilesel.setStorage(value);
+ }
+ }
+ });
+
+ items.push(me.cdstoragesel);
+ items.push(me.cdfilesel);
+
+ items.push({
+ xtype: 'radiofield',
+ name: 'mediaType',
+ inputValue: 'cdrom',
+ boxLabel: gettext('Use physical CD/DVD Drive')
+ });
+
+ items.push({
+ xtype: 'radiofield',
+ name: 'mediaType',
+ inputValue: 'none',
+ boxLabel: gettext('Do not use any media')
+ });
+
+ me.items = items;
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.CDEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ width: 400,
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = me.confid ? false : true;
+
+ var ipanel = Ext.create('PVE.qemu.CDInputPanel', {
+ confid: me.confid,
+ nodename: nodename
+ });
+
+ Ext.applyIf(me, {
+ subject: 'CD/DVD Drive',
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ ipanel.setVMConfig(response.result.data);
+ if (me.confid) {
+ var value = response.result.data[me.confid];
+ var drive = PVE.Parser.parseQemuDrive(me.confid, value);
+ if (!drive) {
+ Ext.Msg.alert('Error', 'Unable to parse drive options');
+ me.close();
+ return;
+ }
+ ipanel.setDrive(drive);
+ }
+ }
+ });
+ }
+});
+/*jslint confusion: true */
+/* 'change' property is assigned a string and then a function */
+Ext.define('PVE.qemu.HDInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuHDInputPanel',
+ onlineHelp: 'qm_hard_disk',
+
+ insideWizard: false,
+
+ unused: false, // ADD usused disk imaged
+
+ vmconfig: {}, // used to select usused disks
+
+ controller: {
+
+ xclass: 'Ext.app.ViewController',
+
+ onControllerChange: function(field) {
+ var value = field.getValue();
+
+ var allowIOthread = value.match(/^(virtio|scsi)/);
+ this.lookup('iothread').setDisabled(!allowIOthread);
+ if (!allowIOthread) {
+ this.lookup('iothread').setValue(false);
+ }
+
+ var virtio = value.match(/^virtio/);
+ this.lookup('discard').setDisabled(virtio);
+ this.lookup('ssd').setDisabled(virtio);
+ if (virtio) {
+ this.lookup('discard').setValue(false);
+ this.lookup('ssd').setValue(false);
+ }
+
+ this.lookup('scsiController').setVisible(value.match(/^scsi/));
+ },
+
+ control: {
+ 'field[name=controller]': {
+ change: 'onControllerChange',
+ afterrender: 'onControllerChange'
+ },
+ 'field[name=iothread]' : {
+ change: function(f, value) {
+ if (!this.getView().insideWizard) {
+ return;
+ }
+ var vmScsiType = value ? 'virtio-scsi-single': 'virtio-scsi-pci';
+ this.lookupReference('scsiController').setValue(vmScsiType);
+ }
+ }
+ }
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var params = {};
+ var confid = me.confid || (values.controller + values.deviceid);
+
+ if (me.unused) {
+ me.drive.file = me.vmconfig[values.unusedId];
+ confid = values.controller + values.deviceid;
+ } else if (me.isCreate) {
+ if (values.hdimage) {
+ me.drive.file = values.hdimage;
+ } else {
+ me.drive.file = values.hdstorage + ":" + values.disksize;
+ }
+ me.drive.format = values.diskformat;
+ }
+
+ if (values.nobackup) {
+ me.drive.backup = 'no';
+ } else {
+ delete me.drive.backup;
+ }
+
+ if (values.noreplicate) {
+ me.drive.replicate = 'no';
+ } else {
+ delete me.drive.replicate;
+ }
+
+ if (values.discard) {
+ me.drive.discard = 'on';
+ } else {
+ delete me.drive.discard;
+ }
+
+ if (values.ssd) {
+ me.drive.ssd = 'on';
+ } else {
+ delete me.drive.ssd;
+ }
+
+ if (values.iothread) {
+ me.drive.iothread = 'on';
+ } else {
+ delete me.drive.iothread;
+ }
+
+ if (values.cache) {
+ me.drive.cache = values.cache;
+ } else {
+ delete me.drive.cache;
+ }
+
+ var names = ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'];
+ Ext.Array.each(names, function(name) {
+ if (values[name]) {
+ me.drive[name] = values[name];
+ } else {
+ delete me.drive[name];
+ }
+ var burst_name = name + '_max';
+ if (values[burst_name] && values[name]) {
+ me.drive[burst_name] = values[burst_name];
+ } else {
+ delete me.drive[burst_name];
+ }
+ });
+
+
+ params[confid] = PVE.Parser.printQemuDrive(me.drive);
+
+ return params;
+ },
+
+ setVMConfig: function(vmconfig) {
+ var me = this;
+
+ me.vmconfig = vmconfig;
+
+ if (me.bussel) {
+ me.bussel.setVMConfig(vmconfig);
+ me.scsiController.setValue(vmconfig.scsihw);
+ }
+ if (me.unusedDisks) {
+ var disklist = [];
+ Ext.Object.each(vmconfig, function(key, value) {
+ if (key.match(/^unused\d+$/)) {
+ disklist.push([key, value]);
+ }
+ });
+ me.unusedDisks.store.loadData(disklist);
+ me.unusedDisks.setValue(me.confid);
+ }
+ },
+
+ setDrive: function(drive) {
+ var me = this;
+
+ me.drive = drive;
+
+ var values = {};
+ var match = drive.file.match(/^([^:]+):/);
+ if (match) {
+ values.hdstorage = match[1];
+ }
+
+ values.hdimage = drive.file;
+ values.nobackup = !PVE.Parser.parseBoolean(drive.backup, 1);
+ values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1);
+ values.diskformat = drive.format || 'raw';
+ values.cache = drive.cache || '__default__';
+ values.discard = (drive.discard === 'on');
+ values.ssd = PVE.Parser.parseBoolean(drive.ssd);
+ values.iothread = PVE.Parser.parseBoolean(drive.iothread);
+
+ values.mbps_rd = drive.mbps_rd;
+ values.mbps_wr = drive.mbps_wr;
+ values.iops_rd = drive.iops_rd;
+ values.iops_wr = drive.iops_wr;
+ values.mbps_rd_max = drive.mbps_rd_max;
+ values.mbps_wr_max = drive.mbps_wr_max;
+ values.iops_rd_max = drive.iops_rd_max;
+ values.iops_wr_max = drive.iops_wr_max;
+
+ me.setValues(values);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ me.down('#hdstorage').setNodename(nodename);
+ me.down('#hdimage').setStorage(undefined, nodename);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ var labelWidth = 140;
+
+ me.drive = {};
+
+ me.column1 = [];
+ me.column2 = [];
+
+ me.advancedColumn1 = [];
+ me.advancedColumn2 = [];
+
+ if (!me.confid || me.unused) {
+ me.bussel = Ext.create('PVE.form.ControllerSelector', {
+ vmconfig: me.insideWizard ? {ide2: 'cdrom'} : {}
+ });
+ me.column1.push(me.bussel);
+
+ me.scsiController = Ext.create('Ext.form.field.Display', {
+ fieldLabel: gettext('SCSI Controller'),
+ reference: 'scsiController',
+ bind: me.insideWizard ? {
+ value: '{current.scsihw}'
+ } : undefined,
+ renderer: PVE.Utils.render_scsihw,
+ submitValue: false,
+ hidden: true
+ });
+ me.column1.push(me.scsiController);
+ }
+
+ if (me.unused) {
+ me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', {
+ name: 'unusedId',
+ fieldLabel: gettext('Disk image'),
+ matchFieldWidth: false,
+ listConfig: {
+ width: 350
+ },
+ data: [],
+ allowBlank: false
+ });
+ me.column1.push(me.unusedDisks);
+ } else if (me.isCreate) {
+ me.column1.push({
+ xtype: 'pveDiskStorageSelector',
+ storageContent: 'images',
+ name: 'disk',
+ nodename: me.nodename,
+ autoSelect: me.insideWizard
+ });
+ } else {
+ me.column1.push({
+ xtype: 'textfield',
+ disabled: true,
+ submitValue: false,
+ fieldLabel: gettext('Disk image'),
+ name: 'hdimage'
+ });
+ }
+
+ me.column2.push(
+ {
+ xtype: 'CacheTypeSelector',
+ name: 'cache',
+ value: '__default__',
+ fieldLabel: gettext('Cache')
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Discard'),
+ disabled: me.confid && me.confid.match(/^virtio/),
+ reference: 'discard',
+ name: 'discard'
+ }
+ );
+
+ me.advancedColumn1.push(
+ {
+ xtype: 'proxmoxcheckbox',
+ disabled: me.confid && me.confid.match(/^virtio/),
+ fieldLabel: gettext('SSD emulation'),
+ labelWidth: labelWidth,
+ name: 'ssd',
+ reference: 'ssd'
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ disabled: me.confid && !me.confid.match(/^(virtio|scsi)/),
+ fieldLabel: 'IO thread',
+ labelWidth: labelWidth,
+ reference: 'iothread',
+ name: 'iothread'
+ },
+ {
+ xtype: 'numberfield',
+ name: 'mbps_rd',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Read limit') + ' (MB/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited')
+ },
+ {
+ xtype: 'numberfield',
+ name: 'mbps_wr',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Write limit') + ' (MB/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_rd',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Read limit') + ' (ops/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_wr',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Write limit') + ' (ops/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited')
+ }
+ );
+
+ me.advancedColumn2.push(
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('No backup'),
+ labelWidth: labelWidth,
+ name: 'nobackup'
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Skip replication'),
+ labelWidth: labelWidth,
+ name: 'noreplicate'
+ },
+ {
+ xtype: 'numberfield',
+ name: 'mbps_rd_max',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Read max burst') + ' (MB)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default')
+ },
+ {
+ xtype: 'numberfield',
+ name: 'mbps_wr_max',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Write max burst') + ' (MB)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_rd_max',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Read max burst') + ' (ops)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_wr_max',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Write max burst') + ' (ops)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default')
+ }
+ );
+
+ me.callParent();
+ }
+});
+/*jslint confusion: false */
+
+Ext.define('PVE.qemu.HDEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ backgroundDelay: 5,
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var unused = me.confid && me.confid.match(/^unused\d+$/);
+
+ me.isCreate = me.confid ? unused : true;
+
+ var ipanel = Ext.create('PVE.qemu.HDInputPanel', {
+ confid: me.confid,
+ nodename: nodename,
+ unused: unused,
+ isCreate: me.isCreate
+ });
+
+ var subject;
+ if (unused) {
+ me.subject = gettext('Unused Disk');
+ } else if (me.isCreate) {
+ me.subject = gettext('Hard Disk');
+ } else {
+ me.subject = gettext('Hard Disk') + ' (' + me.confid + ')';
+ }
+
+ me.items = [ ipanel ];
+
+ me.callParent();
+ /*jslint confusion: true*/
+ /* 'data' is assigned an empty array in same file, and here we
+ * use it like an object
+ */
+ me.load({
+ success: function(response, options) {
+ ipanel.setVMConfig(response.result.data);
+ if (me.confid) {
+ var value = response.result.data[me.confid];
+ var drive = PVE.Parser.parseQemuDrive(me.confid, value);
+ if (!drive) {
+ Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options');
+ me.close();
+ return;
+ }
+ ipanel.setDrive(drive);
+ me.isValid(); // trigger validation
+ }
+ }
+ });
+ /*jslint confusion: false*/
+ }
+});
+Ext.define('PVE.window.HDResize', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ resize_disk: function(disk, size) {
+ var me = this;
+ var params = { disk: disk, size: '+' + size + 'G' };
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/resize',
+ waitMsgTarget: me,
+ method: 'PUT',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ me.close();
+ }
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ var items = [
+ {
+ xtype: 'displayfield',
+ name: 'disk',
+ value: me.disk,
+ fieldLabel: gettext('Disk'),
+ vtype: 'StorageId',
+ allowBlank: false
+ }
+ ];
+
+ me.hdsizesel = Ext.createWidget('numberfield', {
+ name: 'size',
+ minValue: 0,
+ maxValue: 128*1024,
+ decimalPrecision: 3,
+ value: '0',
+ fieldLabel: gettext('Size Increment') + ' (GiB)',
+ allowBlank: false
+ });
+
+ items.push(me.hdsizesel);
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 140,
+ anchor: '100%'
+ },
+ items: items
+ });
+
+ var form = me.formPanel.getForm();
+
+ var submitBtn;
+
+ me.title = gettext('Resize disk');
+ submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Resize disk'),
+ handler: function() {
+ if (form.isValid()) {
+ var values = form.getValues();
+ me.resize_disk(me.disk, values.size);
+ }
+ }
+ });
+
+ Ext.apply(me, {
+ modal: true,
+ width: 250,
+ height: 150,
+ border: false,
+ layout: 'fit',
+ buttons: [ submitBtn ],
+ items: [ me.formPanel ]
+ });
+
+
+ me.callParent();
+
+ if (!me.disk) {
+ return;
+ }
+
+ }
+});
+Ext.define('PVE.window.HDMove', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+
+ move_disk: function(disk, storage, format, delete_disk) {
+ var me = this;
+ var qemu = (me.type === 'qemu');
+ var params = {};
+ params.storage = storage;
+ params[qemu ? 'disk':'volume'] = disk;
+
+ if (format && qemu) {
+ params.format = format;
+ }
+
+ if (delete_disk) {
+ params['delete'] = 1;
+ }
+
+ var url = '/nodes/' + me.nodename + '/' + me.type + '/' + me.vmid + '/';
+ url += qemu ? 'move_disk' : 'move_volume';
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: url,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid
+ });
+ win.show();
+ win.on('destroy', function() { me.close(); });
+ }
+ });
+
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ var diskarray = [];
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.type) {
+ me.type = 'qemu';
+ }
+
+ var qemu = (me.type === 'qemu');
+
+ var items = [
+ {
+ xtype: 'displayfield',
+ name: qemu ? 'disk' : 'volume',
+ value: me.disk,
+ fieldLabel: qemu ? gettext('Disk') : gettext('Mount Point'),
+ vtype: 'StorageId',
+ allowBlank: false
+ }
+ ];
+
+ items.push({
+ xtype: 'pveDiskStorageSelector',
+ storageLabel: gettext('Target Storage'),
+ nodename: me.nodename,
+ storageContent: qemu ? 'images' : 'rootdir',
+ hideSize: true
+ });
+
+ items.push({
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Delete source'),
+ name: 'deleteDisk',
+ uncheckedValue: 0,
+ checked: false
+ });
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: items
+ });
+
+ var form = me.formPanel.getForm();
+
+ var submitBtn;
+
+ me.title = qemu ? gettext("Move disk") : gettext('Move Volume');
+ submitBtn = Ext.create('Ext.Button', {
+ text: me.title,
+ handler: function() {
+ if (form.isValid()) {
+ var values = form.getValues();
+ me.move_disk(me.disk, values.hdstorage, values.diskformat,
+ values.deleteDisk);
+ }
+ }
+ });
+
+ Ext.apply(me, {
+ modal: true,
+ width: 350,
+ border: false,
+ layout: 'fit',
+ buttons: [ submitBtn ],
+ items: [ me.formPanel ]
+ });
+
+
+ me.callParent();
+
+ me.mon(me.formPanel, 'validitychange', function(fp, isValid) {
+ submitBtn.setDisabled(!isValid);
+ });
+
+ me.formPanel.isValid();
+ }
+});
+Ext.define('PVE.qemu.EFIDiskInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveEFIDiskInputPanel',
+
+ insideWizard: false,
+
+ unused: false, // ADD usused disk imaged
+
+ vmconfig: {}, // used to select usused disks
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var confid = 'efidisk0';
+
+ if (values.hdimage) {
+ me.drive.file = values.hdimage;
+ } else {
+ // we use 1 here, because for efi the size gets overridden from the backend
+ me.drive.file = values.hdstorage + ":1";
+ }
+
+ me.drive.format = values.diskformat;
+ var params = {};
+ params[confid] = PVE.Parser.printQemuDrive(me.drive);
+ return params;
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ me.down('#hdstorage').setNodename(nodename);
+ me.down('#hdimage').setStorage(undefined, nodename);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.drive = {};
+
+ me.items= [];
+
+ me.items.push({
+ xtype: 'pveDiskStorageSelector',
+ name: 'efidisk0',
+ storageContent: 'images',
+ nodename: me.nodename,
+ hideSize: true
+ });
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.EFIDiskEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+ subject: gettext('EFI Disk'),
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.items = [{
+ xtype: 'pveEFIDiskInputPanel',
+ onlineHelp: 'qm_bios_and_uefi',
+ confid: me.confid,
+ nodename: nodename,
+ isCreate: true
+ }];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.qemu.DisplayInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveDisplayInputPanel',
+
+ onGetValues: function(values) {
+ var ret = PVE.Parser.printPropertyString(values, 'type');
+ if (ret === '') {
+ return {
+ 'delete': 'vga'
+ };
+ }
+ return {
+ vga: ret
+ };
+ },
+
+ items: [{
+ name: 'type',
+ xtype: 'proxmoxKVComboBox',
+ value: '__default__',
+ deleteEmpty: false,
+ fieldLabel: gettext('Graphic card'),
+ comboItems: PVE.Utils.kvm_vga_driver_array(),
+ validator: function() {
+ var v = this.getValue();
+ var cfg = this.up('proxmoxWindowEdit').vmconfig || {};
+
+ if (v.match(/^serial\d+$/) && (!cfg[v] || cfg[v] !== 'socket')) {
+ var fmt = gettext("Serial interface '{0}' is not correctly configured.");
+ return Ext.String.format(fmt, v);
+ }
+ return true;
+ },
+ listeners: {
+ change: function(cb, val) {
+ var me = this.up('panel');
+ if (!val) {
+ return;
+ }
+ var disable = false;
+ var emptyText = Proxmox.Utils.defaultText;
+ switch (val) {
+ case "cirrus":
+ emptyText = "4";
+ break;
+ case "std":
+ emptyText = "16";
+ break;
+ case "qxl":
+ case "qxl2":
+ case "qxl3":
+ case "qxl4":
+ emptyText = "16";
+ break;
+ case "vmware":
+ emptyText = "16";
+ break;
+ case "none":
+ case "serial0":
+ case "serial1":
+ case "serial2":
+ case "serial3":
+ emptyText = 'N/A';
+ disable = true;
+ break;
+ case "virtio":
+ emptyText = "256";
+ break;
+ default:
+ break;
+ }
+ var memoryfield = me.down('field[name=memory]');
+ memoryfield.setEmptyText(emptyText);
+ memoryfield.setDisabled(disable);
+ }
+ }
+ },{
+ xtype: 'proxmoxintegerfield',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Memory') + ' (MiB)',
+ minValue: 4,
+ maxValue: 512,
+ step: 4,
+ name: 'memory'
+ }]
+});
+
+Ext.define('PVE.qemu.DisplayEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ vmconfig: undefined,
+
+ subject: gettext('Display'),
+ width: 350,
+
+ items: [{
+ xtype: 'pveDisplayInputPanel'
+ }],
+
+ initComponent : function() {
+ var me = this;
+
+ me.callParent();
+
+ me.load({
+ success: function(response) {
+ me.vmconfig = response.result.data;
+ var vga = me.vmconfig.vga || '__default__';
+ me.setValues(PVE.Parser.parsePropertyString(vga, 'type'));
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.KeyboardEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent : function() {
+ var me = this;
+
+ Ext.applyIf(me, {
+ subject: gettext('Keyboard Layout'),
+ items: {
+ xtype: 'VNCKeyboardSelector',
+ name: 'keyboard',
+ value: '__default__',
+ fieldLabel: gettext('Keyboard Layout')
+ }
+ });
+
+ me.callParent();
+
+ me.load();
+ }
+});
+Ext.define('PVE.qemu.HardwareView', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ alias: ['widget.PVE.qemu.HardwareView'],
+
+ onlineHelp: 'qm_virtual_machines_settings',
+
+ renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
+ var me = this;
+ var rows = me.rows;
+ var rowdef = rows[key] || {};
+ var iconCls = rowdef.iconCls;
+ var icon = '';
+ var txt = (rowdef.header || key);
+
+ metaData.tdAttr = "valign=middle";
+
+ if (rowdef.tdCls) {
+ metaData.tdCls = rowdef.tdCls;
+ if (rowdef.tdCls == 'pve-itype-icon-storage') {
+ var value = me.getObjectValue(key, '', false);
+ if (value === '') {
+ value = me.getObjectValue(key, '', true);
+ }
+ if (value.match(/vm-.*-cloudinit/)) {
+ metaData.tdCls = 'pve-itype-icon-cloud';
+ return rowdef.cloudheader;
+ } else if (value.match(/media=cdrom/)) {
+ metaData.tdCls = 'pve-itype-icon-cdrom';
+ return rowdef.cdheader;
+ }
+ }
+ } else if (iconCls) {
+ icon = "";
+ metaData.tdCls += " pve-itype-fa";
+ }
+ return icon + txt;
+ },
+
+ initComponent : function() {
+ var me = this;
+ var i, confid;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+ var diskCap = caps.vms['VM.Config.Disk'];
+
+ /*jslint confusion: true */
+ var rows = {
+ memory: {
+ header: gettext('Memory'),
+ editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
+ never_delete: true,
+ defaultValue: '512',
+ tdCls: 'pve-itype-icon-memory',
+ group: 2,
+ multiKey: ['memory', 'balloon', 'shares'],
+ renderer: function(value, metaData, record, ri, ci, store, pending) {
+ var res = '';
+
+ var max = me.getObjectValue('memory', 512, pending);
+ var balloon = me.getObjectValue('balloon', undefined, pending);
+ var shares = me.getObjectValue('shares', undefined, pending);
+
+ res = Proxmox.Utils.format_size(max*1024*1024);
+
+ if (balloon !== undefined && balloon > 0) {
+ res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res;
+
+ if (shares) {
+ res += ' [shares=' + shares +']';
+ }
+ } else if (balloon === 0) {
+ res += ' [balloon=0]';
+ }
+ return res;
+ }
+ },
+ sockets: {
+ header: gettext('Processors'),
+ never_delete: true,
+ editor: (caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']) ?
+ 'PVE.qemu.ProcessorEdit' : undefined,
+ tdCls: 'pve-itype-icon-processor',
+ group: 3,
+ defaultValue: '1',
+ multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'],
+ renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
+
+ var sockets = me.getObjectValue('sockets', 1, pending);
+ var model = me.getObjectValue('cpu', undefined, pending);
+ var cores = me.getObjectValue('cores', 1, pending);
+ var numa = me.getObjectValue('numa', undefined, pending);
+ var vcpus = me.getObjectValue('vcpus', undefined, pending);
+ var cpulimit = me.getObjectValue('cpulimit', undefined, pending);
+ var cpuunits = me.getObjectValue('cpuunits', undefined, pending);
+
+ var res = Ext.String.format('{0} ({1} sockets, {2} cores)',
+ sockets*cores, sockets, cores);
+
+ if (model) {
+ res += ' [' + model + ']';
+ }
+
+ if (numa) {
+ res += ' [numa=' + numa +']';
+ }
+
+ if (vcpus) {
+ res += ' [vcpus=' + vcpus +']';
+ }
+
+ if (cpulimit) {
+ res += ' [cpulimit=' + cpulimit +']';
+ }
+
+ if (cpuunits) {
+ res += ' [cpuunits=' + cpuunits +']';
+ }
+
+ return res;
+ }
+ },
+ bios: {
+ header: 'BIOS',
+ group: 4,
+ never_delete: true,
+ editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
+ defaultValue: '',
+ iconCls: 'microchip',
+ renderer: PVE.Utils.render_qemu_bios
+ },
+ vga: {
+ header: gettext('Display'),
+ editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
+ never_delete: true,
+ tdCls: 'pve-itype-icon-display',
+ group:5,
+ defaultValue: '',
+ renderer: PVE.Utils.render_kvm_vga_driver
+ },
+ machine: {
+ header: gettext('Machine'),
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Machine'),
+ width: 350,
+ items: [{
+ xtype: 'proxmoxKVComboBox',
+ name: 'machine',
+ value: '__default__',
+ fieldLabel: gettext('Machine'),
+ comboItems: [
+ ['__default__', PVE.Utils.render_qemu_machine('')],
+ ['q35', 'q35']
+ ]
+ }]} : undefined,
+ iconCls: 'cogs',
+ never_delete: true,
+ group: 6,
+ defaultValue: '',
+ renderer: PVE.Utils.render_qemu_machine
+ },
+ scsihw: {
+ header: gettext('SCSI Controller'),
+ iconCls: 'database',
+ editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
+ renderer: PVE.Utils.render_scsihw,
+ group: 7,
+ never_delete: true,
+ defaultValue: ''
+ },
+ cores: {
+ visible: false
+ },
+ cpu: {
+ visible: false
+ },
+ numa: {
+ visible: false
+ },
+ balloon: {
+ visible: false
+ },
+ hotplug: {
+ visible: false
+ },
+ vcpus: {
+ visible: false
+ },
+ cpuunits: {
+ visible: false
+ },
+ cpulimit: {
+ visible: false
+ },
+ shares: {
+ visible: false
+ }
+ };
+ /*jslint confusion: false */
+
+ PVE.Utils.forEachBus(undefined, function(type, id) {
+ var confid = type + id;
+ rows[confid] = {
+ group: 10,
+ tdCls: 'pve-itype-icon-storage',
+ editor: 'PVE.qemu.HDEdit',
+ never_delete: caps.vms['VM.Config.Disk'] ? false : true,
+ header: gettext('Hard Disk') + ' (' + confid +')',
+ cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
+ cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')'
+ };
+ });
+ for (i = 0; i < 32; i++) {
+ confid = "net" + i.toString();
+ rows[confid] = {
+ group: 15,
+ order: i,
+ tdCls: 'pve-itype-icon-network',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
+ never_delete: caps.vms['VM.Config.Network'] ? false : true,
+ header: gettext('Network Device') + ' (' + confid +')'
+ };
+ }
+ rows.efidisk0 = {
+ group: 20,
+ tdCls: 'pve-itype-icon-storage',
+ editor: null,
+ never_delete: caps.vms['VM.Config.Disk'] ? false : true,
+ header: gettext('EFI Disk')
+ };
+ for (i = 0; i < 5; i++) {
+ confid = "usb" + i.toString();
+ rows[confid] = {
+ group: 25,
+ order: i,
+ tdCls: 'pve-itype-icon-usb',
+ editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
+ never_delete: caps.nodes['Sys.Console'] ? false : true,
+ header: gettext('USB Device') + ' (' + confid + ')'
+ };
+ }
+ for (i = 0; i < 4; i++) {
+ confid = "hostpci" + i.toString();
+ rows[confid] = {
+ group: 30,
+ order: i,
+ tdCls: 'pve-itype-icon-pci',
+ never_delete: caps.nodes['Sys.Console'] ? false : true,
+ editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
+ header: gettext('PCI Device') + ' (' + confid + ')'
+ };
+ }
+ for (i = 0; i < 4; i++) {
+ confid = "serial" + i.toString();
+ rows[confid] = {
+ group: 35,
+ order: i,
+ tdCls: 'pve-itype-icon-serial',
+ never_delete: caps.nodes['Sys.Console'] ? false : true,
+ header: gettext('Serial Port') + ' (' + confid + ')'
+ };
+ }
+ for (i = 0; i < 256; i++) {
+ rows["unused" + i.toString()] = {
+ group: 99,
+ order: i,
+ tdCls: 'pve-itype-icon-storage',
+ editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
+ header: gettext('Unused Disk') + ' ' + i.toString()
+ };
+ }
+
+ var sorterFn = function(rec1, rec2) {
+ var v1 = rec1.data.key;
+ var v2 = rec2.data.key;
+ var g1 = rows[v1].group || 0;
+ var g2 = rows[v2].group || 0;
+ var order1 = rows[v1].order || 0;
+ var order2 = rows[v2].order || 0;
+
+ if ((g1 - g2) !== 0) {
+ return g1 - g2;
+ }
+
+ if ((order1 - order2) !== 0) {
+ return order1 - order2;
+ }
+
+ if (v1 > v2) {
+ return 1;
+ } else if (v1 < v2) {
+ return -1;
+ } else {
+ return 0;
+ }
+ };
+
+ var reload = function() {
+ me.rstore.load();
+ };
+
+ var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var rowdef = rows[rec.data.key];
+ if (!rowdef.editor) {
+ return;
+ }
+
+ var editor = rowdef.editor;
+ if (rowdef.tdCls == 'pve-itype-icon-storage') {
+ var value = me.getObjectValue(rec.data.key, '', true);
+ if (value.match(/vm-.*-cloudinit/)) {
+ return;
+ } else if (value.match(/media=cdrom/)) {
+ editor = 'PVE.qemu.CDEdit';
+ } else if (!diskCap) {
+ return;
+ }
+ }
+
+ var win;
+
+ if (Ext.isString(editor)) {
+ win = Ext.create(editor, {
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs/' + baseurl
+ });
+ } else {
+ var config = Ext.apply({
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs/' + baseurl
+ }, rowdef.editor);
+ win = Ext.createWidget(rowdef.editor.xtype, config);
+ win.load();
+ }
+
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ var run_resize = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.HDResize', {
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid
+ });
+
+ win.show();
+
+ win.on('destroy', reload);
+ };
+
+ var run_move = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.HDMove', {
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid
+ });
+
+ win.show();
+
+ win.on('destroy', reload);
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ selModel: sm,
+ disabled: true,
+ handler: run_editor
+ });
+
+ var resize_btn = new Proxmox.button.Button({
+ text: gettext('Resize disk'),
+ selModel: sm,
+ disabled: true,
+ handler: run_resize
+ });
+
+ var move_btn = new Proxmox.button.Button({
+ text: gettext('Move disk'),
+ selModel: sm,
+ disabled: true,
+ handler: run_move
+ });
+
+ var remove_btn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ defaultText: gettext('Remove'),
+ altText: gettext('Detach'),
+ selModel: sm,
+ disabled: true,
+ dangerous: true,
+ RESTMethod: 'PUT',
+ confirmMsg: function(rec) {
+ var warn = gettext('Are you sure you want to remove entry {0}');
+ if (this.text === this.altText) {
+ warn = gettext('Are you sure you want to detach entry {0}');
+ }
+
+ var entry = rec.data.key;
+ var msg = Ext.String.format(warn, "'"
+ + me.renderKey(entry, {}, rec) + "'");
+
+ if (entry.match(/^unused\d+$/)) {
+ msg += " " + gettext('This will permanently erase all data.');
+ }
+
+ return msg;
+ },
+ handler: function(b, e, rec) {
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/' + baseurl,
+ waitMsgTarget: me,
+ method: b.RESTMethod,
+ params: {
+ 'delete': rec.data.key
+ },
+ callback: function() {
+ reload();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ if (b.RESTMethod === 'POST') {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', {
+ upid: upid,
+ listeners: {
+ destroy: function () {
+ me.reload();
+ }
+ }
+ });
+ win.show();
+ }
+ }
+ });
+ },
+ listeners: {
+ render: function(btn) {
+ // hack: calculate an optimal button width on first display
+ // to prevent the whole toolbar to move when we switch
+ // between the "Remove" and "Detach" labels
+ var def = btn.getSize().width;
+
+ btn.setText(btn.altText);
+ var alt = btn.getSize().width;
+
+ btn.setText(btn.defaultText);
+
+ var optimal = alt > def ? alt : def;
+ btn.setSize({ width: optimal });
+ }
+ }
+ });
+
+ var revert_btn = new Proxmox.button.Button({
+ text: gettext('Revert'),
+ selModel: sm,
+ disabled: true,
+ handler: function(b, e, rec) {
+ var rowdef = me.rows[rec.data.key] || {};
+ var keys = rowdef.multiKey || [ rec.data.key ];
+ var revert = keys.join(',');
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/' + baseurl,
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: {
+ 'revert': revert
+ },
+ callback: function() {
+ reload();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert('Error',response.htmlStatus);
+ }
+ });
+ }
+ });
+
+ var efidisk_menuitem = Ext.create('Ext.menu.Item',{
+ text: gettext('EFI Disk'),
+ iconCls: 'pve-itype-icon-storage',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function() {
+
+ var rstoredata = me.rstore.getData().map;
+ // check if ovmf is configured
+ if (rstoredata.bios && rstoredata.bios.data.value === 'ovmf') {
+ var win = Ext.create('PVE.qemu.EFIDiskEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ } else {
+ Ext.Msg.alert('Error',gettext('Please select OVMF(UEFI) as BIOS first.'));
+ }
+
+ }
+ });
+
+ var set_button_status = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ // disable button when we have an efidisk already
+ // disable is ok in this case, because you can instantly
+ // see that there is already one
+ efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined);
+ // en/disable usb add button
+ var usbcount = 0;
+ var pcicount = 0;
+ var hasCloudInit = false;
+ me.rstore.getData().items.forEach(function(item){
+ if (/^usb\d+/.test(item.id)) {
+ usbcount++;
+ } else if (/^hostpci\d+/.test(item.id)) {
+ pcicount++;
+ }
+ if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) {
+ hasCloudInit = true;
+ }
+ });
+
+ // heuristic only for disabling some stuff, the backend has the final word.
+ var noSysConsolePerm = !caps.nodes['Sys.Console'];
+
+ me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5));
+ me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4));
+ me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit);
+
+ if (!rec) {
+ remove_btn.disable();
+ edit_btn.disable();
+ resize_btn.disable();
+ move_btn.disable();
+ revert_btn.disable();
+ return;
+ }
+ var key = rec.data.key;
+ var value = rec.data.value;
+ var rowdef = rows[key];
+
+ var pending = rec.data['delete'] || me.hasPendingChanges(key);
+ var isCDRom = (value && !!value.toString().match(/media=cdrom/));
+ var isUnusedDisk = key.match(/^unused\d+/);
+ var isUsedDisk = !isUnusedDisk &&
+ rowdef.tdCls == 'pve-itype-icon-storage' &&
+ !isCDRom;
+
+ var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/));
+
+ var isEfi = (key === 'efidisk0');
+
+ remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true) || (isUnusedDisk && !diskCap));
+ remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText);
+ remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
+
+ edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit || (!isCDRom && !diskCap));
+
+ resize_btn.setDisabled(pending || !isUsedDisk || !diskCap);
+
+ move_btn.setDisabled(pending || !isUsedDisk || !diskCap);
+
+ revert_btn.setDisabled(!pending);
+
+ };
+
+ Ext.apply(me, {
+ url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending',
+ interval: 5000,
+ selModel: sm,
+ run_editor: run_editor,
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ items: [
+ {
+ text: gettext('Hard Disk'),
+ iconCls: 'pve-itype-icon-storage',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.HDEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: gettext('CD/DVD Drive'),
+ iconCls: 'pve-itype-icon-cdrom',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.CDEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: gettext('Network Device'),
+ iconCls: 'pve-itype-icon-network',
+ disabled: !caps.vms['VM.Config.Network'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.NetworkEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode,
+ isCreate: true
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ efidisk_menuitem,
+ {
+ text: gettext('USB Device'),
+ itemId: 'addusb',
+ iconCls: 'pve-itype-icon-usb',
+ disabled: !caps.nodes['Sys.Console'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.USBEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: gettext('PCI Device'),
+ itemId: 'addpci',
+ iconCls: 'pve-itype-icon-pci',
+ disabled: !caps.nodes['Sys.Console'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.PCIEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: gettext('Serial Port'),
+ itemId: 'addserial',
+ iconCls: 'pve-itype-icon-serial',
+ disabled: !caps.vms['VM.Config.Options'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.SerialEdit', {
+ url: '/api2/extjs/' + baseurl
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: gettext('CloudInit Drive'),
+ itemId: 'addci',
+ iconCls: 'pve-itype-icon-cloud',
+ disabled: !caps.nodes['Sys.Console'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.CIDriveEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ }
+ ]
+ })
+ },
+ remove_btn,
+ edit_btn,
+ resize_btn,
+ move_btn,
+ revert_btn
+ ],
+ rows: rows,
+ sorterFn: sorterFn,
+ listeners: {
+ itemdblclick: run_editor,
+ selectionchange: set_button_status
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+
+ me.mon(me.rstore, 'refresh', function() {
+ set_button_status();
+ });
+ }
+});
+Ext.define('PVE.qemu.ScsiHwEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent : function() {
+ var me = this;
+
+ Ext.applyIf(me, {
+ subject: gettext('SCSI Controller Type'),
+ items: {
+ xtype: 'pveScsiHwSelector',
+ name: 'scsihw',
+ value: '__default__',
+ fieldLabel: gettext('Type')
+ }
+ });
+
+ me.callParent();
+
+ me.load();
+ }
+});
+Ext.define('PVE.qemu.BiosEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveQemuBiosEdit',
+
+ initComponent : function() {
+ var me = this;
+
+ var EFIHint = Ext.createWidget({
+ xtype: 'displayfield', //submitValue is false, so we don't get submitted
+ userCls: 'pve-hint',
+ value: 'You need to add an EFI disk for storing the ' +
+ 'EFI settings. See the online help for details.',
+ hidden: true
+ });
+
+ Ext.applyIf(me, {
+ subject: 'BIOS',
+ items: [ {
+ xtype: 'pveQemuBiosSelector',
+ onlineHelp: 'qm_bios_and_uefi',
+ name: 'bios',
+ value: '__default__',
+ fieldLabel: 'BIOS',
+ listeners: {
+ 'change' : function(field, newValue) {
+ if (newValue == 'ovmf') {
+ Proxmox.Utils.API2Request({
+ url : me.url,
+ method : 'GET',
+ failure : function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success : function(response, opts) {
+ var vmConfig = response.result.data;
+ // there can be only one
+ if (!vmConfig.efidisk0) {
+ EFIHint.setVisible(true);
+ }
+ }
+ });
+ } else {
+ if (EFIHint.isVisible()) {
+ EFIHint.setVisible(false);
+ }
+ }
+ }
+ }
+ },
+ EFIHint
+ ] });
+
+ me.callParent();
+
+ me.load();
+
+ }
+});
+/*jslint confusion: true */
+Ext.define('PVE.qemu.Options', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ alias: ['widget.PVE.qemu.Options'],
+
+ onlineHelp: 'qm_options',
+
+ initComponent : function() {
+ var me = this;
+ var i;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var rows = {
+ name: {
+ required: true,
+ defaultValue: me.pveSelNode.data.name,
+ header: gettext('Name'),
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Name'),
+ items: {
+ xtype: 'inputpanel',
+ items:{
+ xtype: 'textfield',
+ name: 'name',
+ vtype: 'DnsName',
+ value: '',
+ fieldLabel: gettext('Name'),
+ allowBlank: true
+ },
+ onGetValues: function(values) {
+ var params = values;
+ if (values.name === undefined ||
+ values.name === null ||
+ values.name === '') {
+ params = { 'delete':'name'};
+ }
+ return params;
+ }
+ }
+ } : undefined
+ },
+ onboot: {
+ header: gettext('Start at boot'),
+ defaultValue: '',
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Start at boot'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'onboot',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Start at boot')
+ }
+ } : undefined
+ },
+ startup: {
+ header: gettext('Start/Shutdown order'),
+ defaultValue: '',
+ renderer: PVE.Utils.render_kvm_startup,
+ editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ?
+ {
+ xtype: 'pveWindowStartupEdit',
+ onlineHelp: 'qm_startup_and_shutdown'
+ } : undefined
+ },
+ ostype: {
+ header: gettext('OS Type'),
+ editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.OSTypeEdit' : undefined,
+ renderer: PVE.Utils.render_kvm_ostype,
+ defaultValue: 'other'
+ },
+ bootdisk: {
+ visible: false
+ },
+ boot: {
+ header: gettext('Boot Order'),
+ defaultValue: 'cdn',
+ editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.BootOrderEdit' : undefined,
+ multiKey: ['boot', 'bootdisk'],
+ renderer: function(order, metaData, record, rowIndex, colIndex, store, pending) {
+ var i;
+ var text = '';
+ var bootdisk = me.getObjectValue('bootdisk', undefined, pending);
+ order = order || 'cdn';
+ for (i = 0; i < order.length; i++) {
+ var sel = order.substring(i, i + 1);
+ if (text) {
+ text += ', ';
+ }
+ if (sel === 'c') {
+ if (bootdisk) {
+ text += "Disk '" + bootdisk + "'";
+ } else {
+ text += "Disk";
+ }
+ } else if (sel === 'n') {
+ text += 'Network';
+ } else if (sel === 'a') {
+ text += 'Floppy';
+ } else if (sel === 'd') {
+ text += 'CD-ROM';
+ } else {
+ text += sel;
+ }
+ }
+ return text;
+ }
+ },
+ tablet: {
+ header: gettext('Use tablet for pointer'),
+ defaultValue: true,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Use tablet for pointer'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'tablet',
+ checked: true,
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled')
+ }
+ } : undefined
+ },
+ hotplug: {
+ header: gettext('Hotplug'),
+ defaultValue: 'disk,network,usb',
+ renderer: PVE.Utils.render_hotplug_features,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Hotplug'),
+ items: {
+ xtype: 'pveHotplugFeatureSelector',
+ name: 'hotplug',
+ value: '',
+ multiSelect: true,
+ fieldLabel: gettext('Hotplug'),
+ allowBlank: true
+ }
+ } : undefined
+ },
+ acpi: {
+ header: gettext('ACPI support'),
+ defaultValue: true,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('ACPI support'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'acpi',
+ checked: true,
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled')
+ }
+ } : undefined
+ },
+ kvm: {
+ header: gettext('KVM hardware virtualization'),
+ defaultValue: true,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('KVM hardware virtualization'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'kvm',
+ checked: true,
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled')
+ }
+ } : undefined
+ },
+ freeze: {
+ header: gettext('Freeze CPU at startup'),
+ defaultValue: false,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.PowerMgmt'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Freeze CPU at startup'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'freeze',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ labelWidth: 140,
+ fieldLabel: gettext('Freeze CPU at startup')
+ }
+ } : undefined
+ },
+ localtime: {
+ header: gettext('Use local time for RTC'),
+ defaultValue: false,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Use local time for RTC'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'localtime',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ labelWidth: 140,
+ fieldLabel: gettext('Use local time for RTC')
+ }
+ } : undefined
+ },
+ startdate: {
+ header: gettext('RTC start date'),
+ defaultValue: 'now',
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('RTC start date'),
+ items: {
+ xtype: 'proxmoxtextfield',
+ name: 'startdate',
+ deleteEmpty: true,
+ value: 'now',
+ fieldLabel: gettext('RTC start date'),
+ vtype: 'QemuStartDate',
+ allowBlank: true
+ }
+ } : undefined
+ },
+ smbios1: {
+ header: gettext('SMBIOS settings (type1)'),
+ defaultValue: '',
+ renderer: Ext.String.htmlEncode,
+ editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.Smbios1Edit' : undefined
+ },
+ agent: {
+ header: gettext('Qemu Agent'),
+ defaultValue: false,
+ renderer: PVE.Utils.render_qga_features,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Qemu Agent'),
+ items: {
+ xtype: 'pveAgentFeatureSelector',
+ name: 'agent'
+ }
+ } : undefined
+ },
+ protection: {
+ header: gettext('Protection'),
+ defaultValue: false,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Protection'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'protection',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled')
+ }
+ } : undefined
+ },
+ hookscript: {
+ header: gettext('Hookscript')
+ }
+ };
+
+ var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
+
+ var edit_btn = new Ext.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ handler: function() { me.run_editor(); }
+ });
+
+ var revert_btn = new Proxmox.button.Button({
+ text: gettext('Revert'),
+ disabled: true,
+ handler: function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var rowdef = me.rows[rec.data.key] || {};
+ var keys = rowdef.multiKey || [ rec.data.key ];
+ var revert = keys.join(',');
+
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/' + baseurl,
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: {
+ 'revert': revert
+ },
+ callback: function() {
+ me.reload();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert('Error',response.htmlStatus);
+ }
+ });
+ }
+ });
+
+ var set_button_status = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ return;
+ }
+
+ var key = rec.data.key;
+ var pending = rec.data['delete'] || me.hasPendingChanges(key);
+ var rowdef = rows[key];
+
+ edit_btn.setDisabled(!rowdef.editor);
+ revert_btn.setDisabled(!pending);
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/pending",
+ interval: 5000,
+ cwidth1: 250,
+ tbar: [ edit_btn, revert_btn ],
+ rows: rows,
+ editorConfig: {
+ url: "/api2/extjs/" + baseurl
+ },
+ listeners: {
+ itemdblclick: me.run_editor,
+ selectionchange: set_button_status
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+
+ me.rstore.on('datachanged', function() {
+ set_button_status();
+ });
+ }
+});
+
+Ext.define('PVE.window.Snapshot', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ // needed for finding the reference to submitbutton
+ // because we do not have a controller
+ referenceHolder: true,
+ defaultButton: 'submitbutton',
+ defaultFocus: 'field',
+
+ take_snapshot: function(snapname, descr, vmstate) {
+ var me = this;
+ var params = { snapname: snapname, vmstate: vmstate ? 1 : 0 };
+ if (descr) {
+ params.description = descr;
+ }
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot",
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
+ win.show();
+ me.close();
+ }
+ });
+ },
+
+ update_snapshot: function(snapname, descr) {
+ var me = this;
+ Proxmox.Utils.API2Request({
+ params: { description: descr },
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" +
+ snapname + '/config',
+ waitMsgTarget: me,
+ method: 'PUT',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ me.close();
+ }
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ var summarystore = Ext.create('Ext.data.Store', {
+ model: 'KeyValue',
+ sorters: [
+ {
+ property : 'key',
+ direction: 'ASC'
+ }
+ ]
+ });
+
+ var items = [
+ {
+ xtype: me.snapname ? 'displayfield' : 'textfield',
+ name: 'snapname',
+ value: me.snapname,
+ fieldLabel: gettext('Name'),
+ vtype: 'ConfigId',
+ allowBlank: false
+ }
+ ];
+
+ if (me.snapname) {
+ items.push({
+ xtype: 'displayfield',
+ name: 'snaptime',
+ renderer: PVE.Utils.render_timestamp_human_readable,
+ fieldLabel: gettext('Timestamp')
+ });
+ } else {
+ items.push({
+ xtype: 'proxmoxcheckbox',
+ name: 'vmstate',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ checked: 1,
+ fieldLabel: gettext('Include RAM')
+ });
+ }
+
+ items.push({
+ xtype: 'textareafield',
+ grow: true,
+ name: 'description',
+ fieldLabel: gettext('Description')
+ });
+
+ if (me.snapname) {
+ items.push({
+ title: gettext('Settings'),
+ xtype: 'grid',
+ height: 200,
+ store: summarystore,
+ columns: [
+ {header: gettext('Key'), width: 150, dataIndex: 'key'},
+ {header: gettext('Value'), flex: 1, dataIndex: 'value'}
+ ]
+ });
+ }
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: items
+ });
+
+ var form = me.formPanel.getForm();
+
+ var submitBtn;
+
+ if (me.snapname) {
+ me.title = gettext('Edit') + ': ' + gettext('Snapshot');
+ submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Update'),
+ handler: function() {
+ if (form.isValid()) {
+ var values = form.getValues();
+ me.update_snapshot(me.snapname, values.description);
+ }
+ }
+ });
+ } else {
+ me.title ="VM " + me.vmid + ': ' + gettext('Take Snapshot');
+ submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Take Snapshot'),
+ reference: 'submitbutton',
+ handler: function() {
+ if (form.isValid()) {
+ var values = form.getValues();
+ me.take_snapshot(values.snapname, values.description, values.vmstate);
+ }
+ }
+ });
+ }
+
+ Ext.apply(me, {
+ modal: true,
+ width: 450,
+ border: false,
+ layout: 'fit',
+ buttons: [ submitBtn ],
+ items: [ me.formPanel ]
+ });
+
+ if (me.snapname) {
+ Ext.apply(me, {
+ width: 620,
+ height: 420
+ });
+ }
+
+ me.callParent();
+
+ if (!me.snapname) {
+ return;
+ }
+
+ // else load data
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" +
+ me.snapname + '/config',
+ waitMsgTarget: me,
+ method: 'GET',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ me.close();
+ },
+ success: function(response, options) {
+ var data = response.result.data;
+ var kvarray = [];
+ Ext.Object.each(data, function(key, value) {
+ if (key === 'description' || key === 'snaptime') {
+ return;
+ }
+ kvarray.push({ key: key, value: value });
+ });
+
+ summarystore.suspendEvents();
+ summarystore.add(kvarray);
+ summarystore.sort();
+ summarystore.resumeEvents();
+ summarystore.fireEvent('refresh', summarystore);
+
+ form.findField('snaptime').setValue(data.snaptime);
+ form.findField('description').setValue(data.description);
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.SnapshotTree', {
+ extend: 'Ext.tree.Panel',
+ alias: ['widget.pveQemuSnapshotTree'],
+
+ load_delay: 3000,
+
+ old_digest: 'invalid',
+
+ stateful: true,
+ stateId: 'grid-qemu-snapshots',
+
+ sorterFn: function(rec1, rec2) {
+ var v1 = rec1.data.snaptime;
+ var v2 = rec2.data.snaptime;
+
+ if (rec1.data.name === 'current') {
+ return 1;
+ }
+ if (rec2.data.name === 'current') {
+ return -1;
+ }
+
+ return (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0));
+ },
+
+ reload: function(repeat) {
+ var me = this;
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot',
+ method: 'GET',
+ failure: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, response.htmlStatus);
+ me.load_task.delay(me.load_delay);
+ },
+ success: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, false);
+ var digest = 'invalid';
+ var idhash = {};
+ var root = { name: '__root', expanded: true, children: [] };
+ Ext.Array.each(response.result.data, function(item) {
+ item.leaf = true;
+ item.children = [];
+ if (item.name === 'current') {
+ digest = item.digest + item.running;
+ if (item.running) {
+ item.iconCls = 'fa fa-fw fa-desktop x-fa-tree-running';
+ } else {
+ item.iconCls = 'fa fa-fw fa-desktop x-fa-tree';
+ }
+ } else {
+ item.iconCls = 'fa fa-fw fa-history x-fa-tree';
+ }
+ idhash[item.name] = item;
+ });
+
+ if (digest !== me.old_digest) {
+ me.old_digest = digest;
+
+ Ext.Array.each(response.result.data, function(item) {
+ if (item.parent && idhash[item.parent]) {
+ var parent_item = idhash[item.parent];
+ parent_item.children.push(item);
+ parent_item.leaf = false;
+ parent_item.expanded = true;
+ parent_item.expandable = false;
+ } else {
+ root.children.push(item);
+ }
+ });
+
+ me.setRootNode(root);
+ }
+
+ me.load_task.delay(me.load_delay);
+ }
+ });
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/feature',
+ params: { feature: 'snapshot' },
+ method: 'GET',
+ success: function(response, options) {
+ var res = response.result.data;
+ if (res.hasFeature) {
+ var snpBtns = Ext.ComponentQuery.query('#snapshotBtn');
+ snpBtns.forEach(function(item){
+ item.enable();
+ });
+ }
+ }
+ });
+
+
+ },
+
+ listeners: {
+ beforestatesave: function(grid, state, eopts) {
+ // extjs cannot serialize functions,
+ // so a the sorter with only the sorterFn will
+ // not be a valid sorter when restoring the state
+ delete state.storeState.sorters;
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.nodename = me.pveSelNode.data.node;
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.vmid = me.pveSelNode.data.vmid;
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ me.load_task = new Ext.util.DelayedTask(me.reload, me);
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var valid_snapshot = function(record) {
+ return record && record.data && record.data.name &&
+ record.data.name !== 'current';
+ };
+
+ var valid_snapshot_rollback = function(record) {
+ return record && record.data && record.data.name &&
+ record.data.name !== 'current' && !record.data.snapstate;
+ };
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (valid_snapshot(rec)) {
+ var win = Ext.create('PVE.window.Snapshot', {
+ snapname: rec.data.name,
+ nodename: me.nodename,
+ vmid: me.vmid
+ });
+ win.show();
+ me.mon(win, 'close', me.reload, me);
+ }
+ };
+
+ var editBtn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ enableFn: valid_snapshot,
+ handler: run_editor
+ });
+
+ var rollbackBtn = new Proxmox.button.Button({
+ text: gettext('Rollback'),
+ disabled: true,
+ selModel: sm,
+ enableFn: valid_snapshot_rollback,
+ confirmMsg: function(rec) {
+ return Proxmox.Utils.format_task_description('qmrollback', me.vmid) +
+ " '" + rec.data.name + "'";
+ },
+ handler: function(btn, event) {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ var snapname = rec.data.name;
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname + '/rollback',
+ method: 'POST',
+ waitMsgTarget: me,
+ callback: function() {
+ me.reload();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
+ win.show();
+ }
+ });
+ }
+ });
+
+ var removeBtn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ disabled: true,
+ selModel: sm,
+ confirmMsg: function(rec) {
+ var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
+ "'" + rec.data.name + "'");
+ return msg;
+ },
+ enableFn: valid_snapshot,
+ handler: function(btn, event) {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ var snapname = rec.data.name;
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ callback: function() {
+ me.reload();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
+ win.show();
+ }
+ });
+ }
+ });
+
+ var snapshotBtn = Ext.create('Ext.Button', {
+ itemId: 'snapshotBtn',
+ text: gettext('Take Snapshot'),
+ disabled: true,
+ handler: function() {
+ var win = Ext.create('PVE.window.Snapshot', {
+ nodename: me.nodename,
+ vmid: me.vmid
+ });
+ win.show();
+ }
+ });
+
+ Ext.apply(me, {
+ layout: 'fit',
+ rootVisible: false,
+ animate: false,
+ sortableColumns: false,
+ selModel: sm,
+ tbar: [ snapshotBtn, rollbackBtn, removeBtn, editBtn ],
+ fields: [
+ 'name', 'description', 'snapstate', 'vmstate', 'running',
+ { name: 'snaptime', type: 'date', dateFormat: 'timestamp' }
+ ],
+ columns: [
+ {
+ xtype: 'treecolumn',
+ text: gettext('Name'),
+ dataIndex: 'name',
+ width: 200,
+ renderer: function(value, metaData, record) {
+ if (value === 'current') {
+ return "NOW";
+ } else {
+ return value;
+ }
+ }
+ },
+ {
+ text: gettext('RAM'),
+ align: 'center',
+ resizable: false,
+ dataIndex: 'vmstate',
+ width: 50,
+ renderer: function(value, metaData, record) {
+ if (record.data.name !== 'current') {
+ return Proxmox.Utils.format_boolean(value);
+ }
+ }
+ },
+ {
+ text: gettext('Date') + "/" + gettext("Status"),
+ dataIndex: 'snaptime',
+ width: 150,
+ renderer: function(value, metaData, record) {
+ if (record.data.snapstate) {
+ return record.data.snapstate;
+ }
+ if (value) {
+ return Ext.Date.format(value,'Y-m-d H:i:s');
+ }
+ }
+ },
+ {
+ text: gettext('Description'),
+ dataIndex: 'description',
+ flex: 1,
+ renderer: function(value, metaData, record) {
+ if (record.data.name === 'current') {
+ return gettext("You are here!");
+ } else {
+ return Ext.String.htmlEncode(value);
+ }
+ }
+ }
+ ],
+ columnLines: true, // will work in 4.1?
+ listeners: {
+ activate: me.reload,
+ destroy: me.load_task.cancel,
+ itemdblclick: run_editor
+ }
+ });
+
+ me.callParent();
+
+ me.store.sorters.add(new Ext.util.Sorter({
+ sorterFn: me.sorterFn
+ }));
+ }
+});
+
+Ext.define('PVE.qemu.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.qemu.Config',
+
+ onlineHelp: 'chapter_virtual_machines',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var template = !!me.pveSelNode.data.template;
+
+ var running = !!me.pveSelNode.data.uptime;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var base_url = '/nodes/' + nodename + "/qemu/" + vmid;
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: '/api2/json' + base_url + '/status/current',
+ interval: 1000
+ });
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: base_url + '/status/' + cmd,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ };
+
+ var resumeBtn = Ext.create('Ext.Button', {
+ text: gettext('Resume'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ hidden: true,
+ handler: function() {
+ vm_command('resume');
+ },
+ iconCls: 'fa fa-play'
+ });
+
+ var startBtn = Ext.create('Ext.Button', {
+ text: gettext('Start'),
+ disabled: !caps.vms['VM.PowerMgmt'] || running,
+ hidden: template,
+ handler: function() {
+ vm_command('start');
+ },
+ iconCls: 'fa fa-play'
+ });
+
+ var migrateBtn = Ext.create('Ext.Button', {
+ text: gettext('Migrate'),
+ disabled: !caps.vms['VM.Migrate'],
+ hidden: PVE.data.ResourceStore.getNodes().length < 2,
+ handler: function() {
+ var win = Ext.create('PVE.window.Migrate', {
+ vmtype: 'qemu',
+ nodename: nodename,
+ vmid: vmid
+ });
+ win.show();
+ },
+ iconCls: 'fa fa-send-o'
+ });
+
+ var moreBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('More'),
+ menu: { items: [
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: caps.vms['VM.Clone'] ? false : true,
+ handler: function() {
+ PVE.window.Clone.wrap(nodename, vmid, template, 'qemu');
+ }
+ },
+ {
+ text: gettext('Convert to template'),
+ disabled: template,
+ xtype: 'pveMenuItem',
+ iconCls: 'fa fa-fw fa-file-o',
+ hidden: caps.vms['VM.Allocate'] ? false : true,
+ confirmMsg: Proxmox.Utils.format_task_description('qmtemplate', vmid),
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: base_url + '/template',
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ }
+ },
+ {
+ iconCls: 'fa fa-heartbeat ',
+ hidden: !caps.nodes['Sys.Console'],
+ text: gettext('Manage HA'),
+ handler: function() {
+ var ha = me.pveSelNode.data.hastate;
+ Ext.create('PVE.ha.VMResourceEdit', {
+ vmid: vmid,
+ isCreate: (!ha || ha === 'unmanaged')
+ }).show();
+ }
+ },
+ {
+ text: gettext('Remove'),
+ itemId: 'removeBtn',
+ disabled: !caps.vms['VM.Allocate'],
+ handler: function() {
+ Ext.create('PVE.window.SafeDestroy', {
+ url: base_url,
+ item: { type: 'VM', id: vmid }
+ }).show();
+ },
+ iconCls: 'fa fa-trash-o'
+ }
+ ]}
+ });
+
+ var shutdownBtn = Ext.create('PVE.button.Split', {
+ text: gettext('Shutdown'),
+ disabled: !caps.vms['VM.PowerMgmt'] || !running,
+ hidden: template,
+ confirmMsg: Proxmox.Utils.format_task_description('qmshutdown', vmid),
+ handler: function() {
+ vm_command('shutdown');
+ },
+ menu: {
+ items: [{
+ text: gettext('Pause'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ confirmMsg: Proxmox.Utils.format_task_description('qmpause', vmid),
+ handler: function() {
+ vm_command("suspend");
+ },
+ iconCls: 'fa fa-pause'
+ },{
+ text: gettext('Hibernate'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ confirmMsg: Proxmox.Utils.format_task_description('qmsuspend', vmid),
+ tooltip: gettext('Suspend to disk'),
+ handler: function() {
+ vm_command("suspend", { todisk: 1 });
+ },
+ iconCls: 'fa fa-download'
+ },{
+ text: gettext('Stop'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ dangerous: true,
+ tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
+ confirmMsg: Proxmox.Utils.format_task_description('qmstop', vmid),
+ handler: function() {
+ vm_command("stop", { timeout: 30 });
+ },
+ iconCls: 'fa fa-stop'
+ },{
+ text: gettext('Reset'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ confirmMsg: Proxmox.Utils.format_task_description('qmreset', vmid),
+ handler: function() {
+ vm_command("reset");
+ },
+ iconCls: 'fa fa-bolt'
+ }]
+ },
+ iconCls: 'fa fa-power-off'
+ });
+
+ var vm = me.pveSelNode.data;
+
+ var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
+ disabled: !caps.vms['VM.Console'],
+ hidden: template,
+ consoleType: 'kvm',
+ consoleName: vm.name,
+ nodename: nodename,
+ vmid: vmid
+ });
+
+ var statusTxt = Ext.create('Ext.toolbar.TextItem', {
+ data: {
+ lock: undefined
+ },
+ tpl: [
+ '
');
+ }
+ }
+ ]
+ }
+ ]
+});
+
+Ext.define('PVE.qemu.AgentIPView', {
+ extend: 'Ext.container.Container',
+ xtype: 'pveAgentIPView',
+
+ layout: {
+ type: 'hbox',
+ align: 'top'
+ },
+
+ nics: [],
+
+ items: [
+ {
+ xtype: 'box',
+ html: ' IPs'
+ },
+ {
+ xtype: 'container',
+ flex: 1,
+ layout: {
+ type: 'vbox',
+ align: 'right',
+ pack: 'end'
+ },
+ items: [
+ {
+ xtype: 'label',
+ flex: 1,
+ itemId: 'ipBox',
+ style: {
+ 'text-align': 'right'
+ }
+ },
+ {
+ xtype: 'button',
+ itemId: 'moreBtn',
+ hidden: true,
+ ui: 'default-toolbar',
+ handler: function(btn) {
+ var me = this.up('pveAgentIPView');
+
+ var win = Ext.create('PVE.window.IPInfo');
+ win.down('grid').getStore().setData(me.nics);
+ win.show();
+ },
+ text: gettext('More')
+ }
+ ]
+ }
+ ],
+
+ getDefaultIps: function(nics) {
+ var me = this;
+ var ips = [];
+ nics.forEach(function(nic) {
+ if (nic['hardware-address'] &&
+ nic['hardware-address'] != '00:00:00:00:00:00') {
+
+ var nic_ips = nic['ip-addresses'] || [];
+ nic_ips.forEach(function(ip) {
+ var p = ip['ip-address'];
+ // show 2 ips at maximum
+ if (ips.length < 2) {
+ ips.push(p);
+ }
+ });
+ }
+ });
+
+ return ips;
+ },
+
+ startIPStore: function(store, records, success) {
+ var me = this;
+ var agentRec = store.getById('agent');
+ /*jslint confusion: true*/
+ /* value is number and string */
+ me.agent = (agentRec && agentRec.data.value === 1);
+ me.running = (store.getById('status').data.value === 'running');
+ /*jslint confusion: false*/
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ if (!caps.vms['VM.Monitor']) {
+ var errorText = gettext("Requires '{0}' Privileges");
+ me.updateStatus(false, Ext.String.format(errorText, 'VM.Monitor'));
+ return;
+ }
+
+ if (me.agent && me.running && me.ipStore.isStopped) {
+ me.ipStore.startUpdate();
+ } else if (me.ipStore.isStopped) {
+ me.updateStatus();
+ }
+ },
+
+ updateStatus: function(unsuccessful, defaulttext) {
+ var me = this;
+ var text = defaulttext || gettext('No network information');
+ var more = false;
+ if (unsuccessful) {
+ text = gettext('Guest Agent not running');
+ } else if (me.agent && me.running) {
+ if (Ext.isArray(me.nics) && me.nics.length) {
+ more = true;
+ var ips = me.getDefaultIps(me.nics);
+ if (ips.length !== 0) {
+ text = ips.join('
');
+ }
+ } else if (me.nics && me.nics.error) {
+ var msg = gettext('Cannot get info from Guest Agent
Error: {0}');
+ text = Ext.String.format(text, me.nics.error.desc);
+ }
+ } else if (me.agent) {
+ text = gettext('Guest Agent not running');
+ } else {
+ text = gettext('No Guest Agent configured');
+ }
+
+ var ipBox = me.down('#ipBox');
+ ipBox.update(text);
+
+ var moreBtn = me.down('#moreBtn');
+ moreBtn.setVisible(more);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.rstore) {
+ throw 'rstore not given';
+ }
+
+ if (!me.pveSelNode) {
+ throw 'pveSelNode not given';
+ }
+
+ var nodename = me.pveSelNode.data.node;
+ var vmid = me.pveSelNode.data.vmid;
+
+ me.ipStore = Ext.create('Proxmox.data.UpdateStore', {
+ interval: 10000,
+ storeid: 'pve-qemu-agent-' + vmid,
+ method: 'POST',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + nodename + '/qemu/' + vmid + '/agent/network-get-interfaces'
+ }
+ });
+
+ me.callParent();
+
+ me.mon(me.ipStore, 'load', function(store, records, success) {
+ if (records && records.length) {
+ me.nics = records[0].data.result;
+ } else {
+ me.nics = undefined;
+ }
+ me.updateStatus(!success);
+ });
+
+ me.on('destroy', me.ipStore.stopUpdate);
+
+ // if we already have info about the vm, use it immediately
+ if (me.rstore.getCount()) {
+ me.startIPStore(me.rstore, me.rstore.getData(), false);
+ }
+
+ // check if the guest agent is there on every statusstore load
+ me.mon(me.rstore, 'load', me.startIPStore, me);
+ }
+});
+Ext.define('PVE.qemu.CloudInit', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ xtype: 'pveCiPanel',
+
+ onlineHelp: 'qm_cloud_init',
+
+ tbar: [
+ {
+ xtype: 'proxmoxButton',
+ disabled: true,
+ dangerous: true,
+ confirmMsg: function(rec) {
+ var me = this.up('grid');
+ var warn = gettext('Are you sure you want to remove entry {0}');
+
+ var entry = rec.data.key;
+ var msg = Ext.String.format(warn, "'"
+ + me.renderKey(entry, {}, rec) + "'");
+
+ return msg;
+ },
+ enableFn: function(record) {
+ var me = this.up('grid');
+ var caps = Ext.state.Manager.get('GuiCap');
+ if (me.rows[record.data.key].never_delete ||
+ !caps.vms['VM.Config.Network']) {
+ return false;
+ }
+
+ if (record.data.key === 'cipassword' && !record.data.value) {
+ return false;
+ }
+ return true;
+ },
+ handler: function() {
+ var me = this.up('grid');
+ var records = me.getSelection();
+ if (!records || !records.length) {
+ return;
+ }
+
+ var id = records[0].data.key;
+ var match = id.match(/^net(\d+)$/);
+ if (match) {
+ id = 'ipconfig' + match[1];
+ }
+
+ var params = {};
+ params['delete'] = id;
+ Proxmox.Utils.API2Request({
+ url: me.baseurl + '/config',
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: params,
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ callback: function() {
+ me.reload();
+ }
+ });
+ },
+ text: gettext('Remove')
+ },
+ {
+ xtype: 'proxmoxButton',
+ disabled: true,
+ handler: function() {
+ var me = this.up('grid');
+ me.run_editor();
+ },
+ text: gettext('Edit')
+ },
+ '-',
+ {
+ xtype: 'button',
+ itemId: 'savebtn',
+ text: gettext('Regenerate Image'),
+ handler: function() {
+ var me = this.up('grid');
+ var eject_params = {};
+ var insert_params = {};
+ var disk = PVE.Parser.parseQemuDrive(me.ciDriveId, me.ciDrive);
+ var storage = '';
+ var stormatch = disk.file.match(/^([^\:]+)\:/);
+ if (stormatch) {
+ storage = stormatch[1];
+ }
+ eject_params[me.ciDriveId] = 'none,media=cdrom';
+ insert_params[me.ciDriveId] = storage + ':cloudinit';
+
+ var failure = function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ };
+
+ Proxmox.Utils.API2Request({
+ url: me.baseurl + '/config',
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: eject_params,
+ failure: failure,
+ callback: function() {
+ Proxmox.Utils.API2Request({
+ url: me.baseurl + '/config',
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: insert_params,
+ failure: failure,
+ callback: function() {
+ me.reload();
+ }
+ });
+ }
+ });
+ }
+ }
+ ],
+
+ border: false,
+
+ set_button_status: function(rstore, records, success) {
+ if (!success || records.length < 1) {
+ return;
+ }
+ var me = this;
+ var found;
+ records.forEach(function(record) {
+ if (found) {
+ return;
+ }
+ var id = record.data.key;
+ var value = record.data.value;
+ var ciregex = new RegExp("vm-" + me.pveSelNode.data.vmid + "-cloudinit");
+ if (id.match(/^(ide|scsi|sata)\d+$/) && ciregex.test(value)) {
+ found = id;
+ me.ciDriveId = found;
+ me.ciDrive = value;
+ }
+ });
+
+ me.down('#savebtn').setDisabled(!found);
+ me.setDisabled(!found);
+ if (!found) {
+ me.getView().mask(gettext('No CloudInit Drive found'), ['pve-static-mask']);
+ } else {
+ me.getView().unmask();
+ }
+ },
+
+ renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
+ var me = this;
+ var rows = me.rows;
+ var rowdef = rows[key] || {};
+
+ var icon = "";
+ if (rowdef.iconCls) {
+ icon = ' ';
+ }
+ return icon + (rowdef.header || key);
+ },
+
+ listeners: {
+ activate: function () {
+ var me = this;
+ me.rstore.startUpdate();
+ },
+ itemdblclick: function() {
+ var me = this;
+ me.run_editor();
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+ var caps = Ext.state.Manager.get('GuiCap');
+ me.baseurl = '/api2/extjs/nodes/' + nodename + '/qemu/' + vmid;
+ me.url = me.baseurl + '/pending';
+ me.editorConfig.url = me.baseurl + '/config';
+ me.editorConfig.pveSelNode = me.pveSelNode;
+
+ /*jslint confusion: true*/
+ /* editor is string and object */
+ me.rows = {
+ ciuser: {
+ header: gettext('User'),
+ iconCls: 'fa fa-user',
+ never_delete: true,
+ defaultValue: '',
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('User'),
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ deleteEmpty: true,
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('User'),
+ name: 'ciuser'
+ }
+ ]
+ } : undefined,
+ renderer: function(value) {
+ return value || Proxmox.Utils.defaultText;
+ }
+ },
+ cipassword: {
+ header: gettext('Password'),
+ iconCls: 'fa fa-unlock',
+ defaultValue: '',
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Password'),
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ inputType: 'password',
+ deleteEmpty: true,
+ emptyText: Proxmox.Utils.noneText,
+ fieldLabel: gettext('Password'),
+ name: 'cipassword'
+ }
+ ]
+ } : undefined,
+ renderer: function(value) {
+ return value || Proxmox.Utils.noneText;
+ }
+ },
+ searchdomain: {
+ header: gettext('DNS domain'),
+ iconCls: 'fa fa-globe',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
+ never_delete: true,
+ defaultValue: gettext('use host settings')
+ },
+ nameserver: {
+ header: gettext('DNS servers'),
+ iconCls: 'fa fa-globe',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
+ never_delete: true,
+ defaultValue: gettext('use host settings')
+ },
+ sshkeys: {
+ header: gettext('SSH public key'),
+ iconCls: 'fa fa-key',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.SSHKeyEdit' : undefined,
+ never_delete: true,
+ renderer: function(value) {
+ value = decodeURIComponent(value);
+ var keys = value.split('\n');
+ var text = [];
+ keys.forEach(function(key) {
+ if (key.length) {
+ // First erase all quoted strings (eg. command="foo"
+ var v = key.replace(/"(?:\\.|[^"\\])*"/g, '');
+ // Now try to detect the comment:
+ var res = v.match(/^\s*(\S+\s+)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)\s+\S+\s+(.*?)\s*$/, '');
+ if (res) {
+ key = Ext.String.htmlEncode(res[2]);
+ if (res[1]) {
+ key += ' (' + gettext('with options') + ')';
+ }
+ text.push(key);
+ return;
+ }
+ // Most likely invalid at this point, so just stick to
+ // the old value.
+ text.push(Ext.String.htmlEncode(key));
+ }
+ });
+ if (text.length) {
+ return text.join('
');
+ } else {
+ return Proxmox.Utils.noneText;
+ }
+ },
+ defaultValue: ''
+ }
+ };
+ var i;
+ var ipconfig_renderer = function(value, md, record, ri, ci, store, pending) {
+ var id = record.data.key;
+ var match = id.match(/^net(\d+)$/);
+ var val = '';
+ if (match) {
+ val = me.getObjectValue('ipconfig'+match[1], '', pending);
+ }
+ return val;
+ };
+ for (i = 0; i < 32; i++) {
+ // we want to show an entry for every network device
+ // even if it is empty
+ me.rows['net' + i.toString()] = {
+ multiKey: ['ipconfig' + i.toString(), 'net' + i.toString()],
+ header: gettext('IP Config') + ' (net' + i.toString() +')',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.IPConfigEdit' : undefined,
+ iconCls: 'fa fa-exchange',
+ renderer: ipconfig_renderer
+ };
+ me.rows['ipconfig' + i.toString()] = {
+ visible: false
+ };
+ }
+ /*jslint confusion: false*/
+
+ PVE.Utils.forEachBus(['ide', 'scsi', 'sata'], function(type, id) {
+ me.rows[type+id] = {
+ visible: false
+ };
+ });
+ me.callParent();
+ me.mon(me.rstore, 'load', me.set_button_status, me);
+ }
+});
+Ext.define('PVE.qemu.CIDriveInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveCIDriveInputPanel',
+
+ insideWizard: false,
+
+ vmconfig: {}, // used to select usused disks
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var drive = {};
+ var params = {};
+ drive.file = values.hdstorage + ":cloudinit";
+ drive.format = values.diskformat;
+ params[values.controller + values.deviceid] = PVE.Parser.printQemuDrive(drive);
+ return params;
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ me.down('#hdstorage').setNodename(nodename);
+ me.down('#hdimage').setStorage(undefined, nodename);
+ },
+
+ setVMConfig: function(config) {
+ var me = this;
+ me.down('#drive').setVMConfig(config, 'cdrom');
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.drive = {};
+
+ me.items = [
+ {
+ xtype: 'pveControllerSelector',
+ noVirtIO: true,
+ itemId: 'drive',
+ fieldLabel: gettext('CloudInit Drive'),
+ name: 'drive'
+ },
+ {
+ xtype: 'pveDiskStorageSelector',
+ itemId: 'storselector',
+ storageContent: 'images',
+ nodename: me.nodename,
+ hideSize: true
+ }
+ ];
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.CIDriveEdit', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveCIDriveEdit',
+
+ isCreate: true,
+ subject: gettext('CloudInit Drive'),
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.items = [{
+ xtype: 'pveCIDriveInputPanel',
+ itemId: 'cipanel',
+ nodename: nodename
+ }];
+
+ me.callParent();
+
+ me.load({
+ success: function(response, opts) {
+ me.down('#cipanel').setVMConfig(response.result.data);
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.SSHKeyInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveQemuSSHKeyInputPanel',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+ if (values.sshkeys) {
+ values.sshkeys.trim();
+ }
+ if (!values.sshkeys.length) {
+ values = {};
+ values['delete'] = 'sshkeys';
+ return values;
+ } else {
+ values.sshkeys = encodeURIComponent(values.sshkeys);
+ }
+ return values;
+ },
+
+ items: [
+ {
+ xtype: 'textarea',
+ itemId: 'sshkeys',
+ name: 'sshkeys',
+ height: 250
+ },
+ {
+ xtype: 'filebutton',
+ itemId: 'filebutton',
+ name: 'file',
+ text: gettext('Load SSH Key File'),
+ fieldLabel: 'test',
+ listeners: {
+ change: function(btn, e, value) {
+ var me = this.up('inputpanel');
+ e = e.event;
+ Ext.Array.each(e.target.files, function(file) {
+ PVE.Utils.loadSSHKeyFromFile(file, function(res) {
+ var keysField = me.down('#sshkeys');
+ var old = keysField.getValue();
+ keysField.setValue(old + res);
+ });
+ });
+ btn.reset();
+ }
+ }
+ }
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+ if (!window.FileReader) {
+ me.down('#filebutton').setVisible(false);
+ }
+
+ }
+});
+
+Ext.define('PVE.qemu.SSHKeyEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ width: 800,
+
+ initComponent : function() {
+ var me = this;
+
+ var ipanel = Ext.create('PVE.qemu.SSHKeyInputPanel');
+
+ Ext.apply(me, {
+ subject: gettext('SSH Keys'),
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ if (!me.create) {
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data;
+ if (data.sshkeys) {
+ data.sshkeys = decodeURIComponent(data.sshkeys);
+ ipanel.setValues(data);
+ }
+ }
+ });
+ }
+ }
+});
+Ext.define('PVE.qemu.IPConfigPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveIPConfigPanel',
+
+ insideWizard: false,
+
+ vmconfig: {},
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (values.ipv4mode !== 'static') {
+ values.ip = values.ipv4mode;
+ }
+
+ if (values.ipv6mode !== 'static') {
+ values.ip6 = values.ipv6mode;
+ }
+
+ var params = {};
+
+ var cfg = PVE.Parser.printIPConfig(values);
+ if (cfg === '') {
+ params['delete'] = [me.confid];
+ } else {
+ params[me.confid] = cfg;
+ }
+ return params;
+ },
+
+ setVMConfig: function(config) {
+ var me = this;
+ me.vmconfig = config;
+ },
+
+ setIPConfig: function(confid, data) {
+ var me = this;
+
+ me.confid = confid;
+
+ if (data.ip === 'dhcp') {
+ data.ipv4mode = data.ip;
+ data.ip = '';
+ } else {
+ data.ipv4mode = 'static';
+ }
+ if (data.ip6 === 'dhcp' || data.ip6 === 'auto') {
+ data.ipv6mode = data.ip6;
+ data.ip6 = '';
+ } else {
+ data.ipv6mode = 'static';
+ }
+
+ me.ipconfig = data;
+ me.setValues(me.ipconfig);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.ipconfig = {};
+
+ me.column1 = [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Network Device'),
+ value: me.netid
+ },
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: gettext('IPv4') + ':'
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv4mode',
+ inputValue: 'static',
+ checked: false,
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip]').setDisabled(!value);
+ me.down('field[name=gw]').setDisabled(!value);
+ }
+ }
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('DHCP'),
+ name: 'ipv4mode',
+ inputValue: 'dhcp',
+ checked: false,
+ margin: '0 0 0 10'
+ }
+ ]
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip',
+ vtype: 'IPCIDRAddress',
+ value: '',
+ disabled: true,
+ fieldLabel: gettext('IPv4/CIDR')
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw',
+ value: '',
+ vtype: 'IPAddress',
+ disabled: true,
+ fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') +')'
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'displayfield'
+ },
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: gettext('IPv6') + ':'
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv6mode',
+ inputValue: 'static',
+ checked: false,
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip6]').setDisabled(!value);
+ me.down('field[name=gw6]').setDisabled(!value);
+ }
+ }
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('DHCP'),
+ name: 'ipv6mode',
+ inputValue: 'dhcp',
+ checked: false,
+ margin: '0 0 0 10'
+ }
+ ]
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip6',
+ value: '',
+ vtype: 'IP6CIDRAddress',
+ disabled: true,
+ fieldLabel: gettext('IPv6/CIDR')
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw6',
+ vtype: 'IP6Address',
+ value: '',
+ disabled: true,
+ fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')'
+ }
+ ];
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.IPConfigEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ initComponent : function() {
+ /*jslint confusion: true */
+
+ var me = this;
+
+ // convert confid from netX to ipconfigX
+ var match = me.confid.match(/^net(\d+)$/);
+ if (match) {
+ me.netid = me.confid;
+ me.confid = 'ipconfig' + match[1];
+ }
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = me.confid ? false : true;
+
+ var ipanel = Ext.create('PVE.qemu.IPConfigPanel', {
+ confid: me.confid,
+ netid: me.netid,
+ nodename: nodename
+ });
+
+ Ext.applyIf(me, {
+ subject: gettext('Network Config'),
+ items: ipanel
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ me.vmconfig = response.result.data;
+ var ipconfig = {};
+ var value = me.vmconfig[me.confid];
+ if (value) {
+ ipconfig = PVE.Parser.parseIPConfig(me.confid, value);
+ if (!ipconfig) {
+ Ext.Msg.alert(gettext('Error'), gettext('Unable to parse network configuration'));
+ me.close();
+ return;
+ }
+ }
+ ipanel.setIPConfig(me.confid, ipconfig);
+ ipanel.setVMConfig(me.vmconfig);
+ }
+ });
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.qemu.SystemInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveQemuSystemPanel',
+
+ onlineHelp: 'qm_system_settings',
+
+ viewModel: {
+ data: {
+ efi: false,
+ addefi: true
+ },
+
+ formulas: {
+ efidisk: function(get) {
+ return get('efi') && get('addefi');
+ }
+ }
+ },
+
+ onGetValues: function(values) {
+ if (values.vga && values.vga.substr(0,6) === 'serial') {
+ values['serial' + values.vga.substr(6,1)] = 'socket';
+ }
+
+ var efidrive = {};
+ if (values.hdimage) {
+ efidrive.file = values.hdimage;
+ } else if (values.hdstorage) {
+ efidrive.file = values.hdstorage + ":1";
+ }
+
+ if (values.diskformat) {
+ efidrive.format = values.diskformat;
+ }
+
+ delete values.hdimage;
+ delete values.hdstorage;
+ delete values.diskformat;
+
+ if (efidrive.file) {
+ values.efidisk0 = PVE.Parser.printQemuDrive(efidrive);
+ }
+
+ return values;
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ scsihwChange: function(field, value) {
+ var me = this;
+ if (me.getView().insideWizard) {
+ me.getViewModel().set('current.scsihw', value);
+ }
+ },
+
+ biosChange: function(field, value) {
+ var me = this;
+ if (me.getView().insideWizard) {
+ me.getViewModel().set('efi', value === 'ovmf');
+ }
+ },
+
+ control: {
+ 'pveScsiHwSelector': {
+ change: 'scsihwChange'
+ },
+ 'pveQemuBiosSelector': {
+ change: 'biosChange'
+ }
+ }
+ },
+
+ column1: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ value: '__default__',
+ deleteEmpty: false,
+ fieldLabel: gettext('Graphic card'),
+ name: 'vga',
+ comboItems: PVE.Utils.kvm_vga_driver_array()
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'agent',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Qemu Agent')
+ }
+ ],
+
+ column2: [
+ {
+ xtype: 'pveScsiHwSelector',
+ name: 'scsihw',
+ value: '__default__',
+ bind: {
+ value: '{current.scsihw}'
+ },
+ fieldLabel: gettext('SCSI Controller')
+ }
+ ],
+
+ advancedColumn1: [
+ {
+ xtype: 'pveQemuBiosSelector',
+ name: 'bios',
+ value: '__default__',
+ fieldLabel: 'BIOS'
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ bind: {
+ value: '{addefi}',
+ hidden: '{!efi}',
+ disabled: '{!efi}'
+ },
+ hidden: true,
+ submitValue: false,
+ disabled: true,
+ fieldLabel: gettext('Add EFI Disk')
+ },
+ {
+ xtype: 'pveDiskStorageSelector',
+ name: 'efidisk0',
+ storageContent: 'images',
+ bind: {
+ nodename: '{nodename}',
+ hidden: '{!efi}',
+ disabled: '{!efidisk}'
+ },
+ autoSelect: false,
+ disabled: true,
+ hidden: true,
+ hideSize: true
+ }
+ ],
+
+ advancedColumn2: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'machine',
+ value: '__default__',
+ fieldLabel: gettext('Machine'),
+ comboItems: [
+ ['__default__', PVE.Utils.render_qemu_machine('')],
+ ['q35', 'q35']
+ ]
+ }
+ ]
+
+});
+Ext.define('PVE.lxc.Summary', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveLxcSummary',
+
+ scrollable: true,
+ bodyPadding: 5,
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.workspace) {
+ throw "no workspace specified";
+ }
+
+ if (!me.statusStore) {
+ throw "no status storage specified";
+ }
+
+ var template = !!me.pveSelNode.data.template;
+ var rstore = me.statusStore;
+
+ var width = template ? 1 : 0.5;
+ var items = [
+ {
+ xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView',
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: width
+ },
+ 'width >= 1900': {
+ columnWidth: width / 2
+ }
+ },
+ itemId: 'gueststatus',
+ pveSelNode: me.pveSelNode,
+ rstore: rstore
+ },
+ {
+ xtype: 'pveNotesView',
+ maxHeight: 320,
+ itemId: 'notesview',
+ pveSelNode: me.pveSelNode,
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: width
+ },
+ 'width >= 1900': {
+ columnWidth: width / 2
+ }
+ }
+ }
+ ];
+
+ var rrdstore;
+ if (!template) {
+
+ rrdstore = Ext.create('Proxmox.data.RRDStore', {
+ rrdurl: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/rrddata",
+ model: 'pve-rrd-guest'
+ });
+
+ items.push(
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('CPU usage'),
+ pveSelNode: me.pveSelNode,
+ fields: ['cpu'],
+ fieldTitles: [gettext('CPU usage')],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Memory usage'),
+ pveSelNode: me.pveSelNode,
+ fields: ['maxmem', 'mem'],
+ fieldTitles: [gettext('Total'), gettext('RAM usage')],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Network traffic'),
+ pveSelNode: me.pveSelNode,
+ fields: ['netin','netout'],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Disk IO'),
+ pveSelNode: me.pveSelNode,
+ fields: ['diskread','diskwrite'],
+ store: rrdstore
+ }
+ );
+
+ }
+
+ Ext.apply(me, {
+ tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ],
+ items: [
+ {
+ xtype: 'container',
+ layout: {
+ type: 'column'
+ },
+ defaults: {
+ minHeight: 320,
+ padding: 5,
+ plugins: 'responsive',
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: 1
+ },
+ 'width >= 1900': {
+ columnWidth: 0.5
+ }
+ }
+ },
+ items: items
+ }
+ ]
+ });
+
+ me.callParent();
+ if (!template) {
+ rrdstore.startUpdate();
+ me.on('destroy', rrdstore.stopUpdate);
+ }
+ }
+});
+Ext.define('PVE.lxc.NetworkInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveLxcNetworkInputPanel',
+
+ insideWizard: false,
+
+ onlineHelp: 'pct_container_network',
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || (me.nodename === nodename)) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ var bridgesel = me.query("[isFormField][name=bridge]")[0];
+ bridgesel.setNodename(nodename);
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var id;
+ if (me.isCreate) {
+ id = values.id;
+ delete values.id;
+ } else {
+ id = me.ifname;
+ }
+
+ if (!id) {
+ return {};
+ }
+
+ var newdata = {};
+
+ if (values.ipv6mode !== 'static') {
+ values.ip6 = values.ipv6mode;
+ }
+ if (values.ipv4mode !== 'static') {
+ values.ip = values.ipv4mode;
+ }
+ newdata[id] = PVE.Parser.printLxcNetwork(values);
+ return newdata;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ var cdata = {};
+
+ if (me.insideWizard) {
+ me.ifname = 'net0';
+ cdata.name = 'eth0';
+ me.dataCache = {};
+ }
+ cdata.firewall = (me.insideWizard || me.isCreate);
+
+ if (!me.dataCache) {
+ throw "no dataCache specified";
+ }
+
+ if (!me.isCreate) {
+ if (!me.ifname) {
+ throw "no interface name specified";
+ }
+ if (!me.dataCache[me.ifname]) {
+ throw "no such interface '" + me.ifname + "'";
+ }
+
+ cdata = PVE.Parser.parseLxcNetwork(me.dataCache[me.ifname]);
+ }
+
+ var i;
+ for (i = 0; i < 10; i++) {
+ if (me.isCreate && !me.dataCache['net'+i.toString()]) {
+ me.ifname = 'net' + i.toString();
+ break;
+ }
+ }
+
+ var idselector = {
+ xtype: 'hidden',
+ name: 'id',
+ value: me.ifname
+ };
+
+ me.column1 = [
+ idselector,
+ {
+ xtype: 'textfield',
+ name: 'name',
+ fieldLabel: gettext('Name'),
+ emptyText: '(e.g., eth0)',
+ allowBlank: false,
+ value: cdata.name,
+ validator: function(value) {
+ var result = '';
+ Ext.Object.each(me.dataCache, function(key, netstr) {
+ if (!key.match(/^net\d+/) || key === me.ifname) {
+ return; // continue
+ }
+ var net = PVE.Parser.parseLxcNetwork(netstr);
+ if (net.name === value) {
+ result = "interface name already in use";
+ return false;
+ }
+ });
+ if (result !== '') {
+ return result;
+ }
+ // validator can return bool/string
+ /*jslint confusion:true*/
+ return true;
+ }
+ },
+ {
+ xtype: 'textfield',
+ name: 'hwaddr',
+ fieldLabel: gettext('MAC address'),
+ vtype: 'MacAddress',
+ value: cdata.hwaddr,
+ allowBlank: true,
+ emptyText: 'auto'
+ },
+ {
+ xtype: 'PVE.form.BridgeSelector',
+ name: 'bridge',
+ nodename: me.nodename,
+ fieldLabel: gettext('Bridge'),
+ value: cdata.bridge,
+ allowBlank: false
+ },
+ {
+ xtype: 'pveVlanField',
+ name: 'tag',
+ value: cdata.tag
+ },
+ {
+ xtype: 'numberfield',
+ name: 'rate',
+ fieldLabel: gettext('Rate limit') + ' (MB/s)',
+ minValue: 0,
+ maxValue: 10*1024,
+ value: cdata.rate,
+ emptyText: 'unlimited',
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Firewall'),
+ name: 'firewall',
+ value: cdata.firewall
+ }
+ ];
+
+ var dhcp4 = (cdata.ip === 'dhcp');
+ if (dhcp4) {
+ cdata.ip = '';
+ cdata.gw = '';
+ }
+
+ var auto6 = (cdata.ip6 === 'auto');
+ var dhcp6 = (cdata.ip6 === 'dhcp');
+ if (auto6 || dhcp6) {
+ cdata.ip6 = '';
+ cdata.gw6 = '';
+ }
+
+ me.column2 = [
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: 'IPv4:' // do not localize
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv4mode',
+ inputValue: 'static',
+ checked: !dhcp4,
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip]').setDisabled(!value);
+ me.down('field[name=gw]').setDisabled(!value);
+ }
+ }
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: 'DHCP', // do not localize
+ name: 'ipv4mode',
+ inputValue: 'dhcp',
+ checked: dhcp4,
+ margin: '0 0 0 10'
+ }
+ ]
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip',
+ vtype: 'IPCIDRAddress',
+ value: cdata.ip,
+ disabled: dhcp4,
+ fieldLabel: 'IPv4/CIDR' // do not localize
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw',
+ value: cdata.gw,
+ vtype: 'IPAddress',
+ disabled: dhcp4,
+ fieldLabel: gettext('Gateway') + ' (IPv4)',
+ margin: '0 0 3 0' // override bottom margin to account for the menuseparator
+ },
+ {
+ xtype: 'menuseparator',
+ height: '3',
+ margin: '0'
+ },
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: 'IPv6:' // do not localize
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv6mode',
+ inputValue: 'static',
+ checked: !(auto6 || dhcp6),
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip6]').setDisabled(!value);
+ me.down('field[name=gw6]').setDisabled(!value);
+ }
+ }
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: 'DHCP', // do not localize
+ name: 'ipv6mode',
+ inputValue: 'dhcp',
+ checked: dhcp6,
+ margin: '0 0 0 10'
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: 'SLAAC', // do not localize
+ name: 'ipv6mode',
+ inputValue: 'auto',
+ checked: auto6,
+ margin: '0 0 0 10'
+ }
+ ]
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip6',
+ value: cdata.ip6,
+ vtype: 'IP6CIDRAddress',
+ disabled: (dhcp6 || auto6),
+ fieldLabel: 'IPv6/CIDR' // do not localize
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw6',
+ vtype: 'IP6Address',
+ value: cdata.gw6,
+ disabled: (dhcp6 || auto6),
+ fieldLabel: gettext('Gateway') + ' (IPv6)'
+ }
+ ];
+
+ me.callParent();
+ }
+});
+
+
+Ext.define('PVE.lxc.NetworkEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.dataCache) {
+ throw "no dataCache specified";
+ }
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var ipanel = Ext.create('PVE.lxc.NetworkInputPanel', {
+ ifname: me.ifname,
+ nodename: me.nodename,
+ dataCache: me.dataCache,
+ isCreate: me.isCreate
+ });
+
+ Ext.apply(me, {
+ subject: gettext('Network Device') + ' (veth)',
+ digest: me.dataCache.digest,
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.lxc.NetworkView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveLxcNetworkView',
+
+ onlineHelp: 'pct_container_network',
+
+ dataCache: {}, // used to store result of last load
+
+ stateful: true,
+ stateId: 'grid-lxc-network',
+
+ load: function() {
+ var me = this;
+
+ Proxmox.Utils.setErrorMask(me, true);
+
+ Proxmox.Utils.API2Request({
+ url: me.url,
+ failure: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus);
+ },
+ success: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, false);
+ var result = Ext.decode(response.responseText);
+ var data = result.data || {};
+ me.dataCache = data;
+ var records = [];
+ Ext.Object.each(data, function(key, value) {
+ if (!key.match(/^net\d+/)) {
+ return; // continue
+ }
+ var net = PVE.Parser.parseLxcNetwork(value);
+ net.id = key;
+ records.push(net);
+ });
+ me.store.loadData(records);
+ me.down('button[name=addButton]').setDisabled((records.length >= 10));
+ }
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ me.url = '/nodes/' + nodename + '/lxc/' + vmid + '/config';
+
+ var store = new Ext.data.Store({
+ model: 'pve-lxc-network',
+ sorters: [
+ {
+ property : 'id',
+ direction: 'ASC'
+ }
+ ]
+ });
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var remove_btn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ return !!caps.vms['VM.Config.Network'];
+ },
+ confirmMsg: function (rec) {
+ return Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
+ "'" + rec.data.id + "'");
+ },
+ handler: function(btn, event, rec) {
+ Proxmox.Utils.API2Request({
+ url: me.url,
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: { 'delete': rec.data.id, digest: me.dataCache.digest },
+ callback: function() {
+ me.load();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ }
+ });
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ if (!caps.vms['VM.Config.Network']) {
+ return false;
+ }
+
+ var win = Ext.create('PVE.lxc.NetworkEdit', {
+ url: me.url,
+ nodename: nodename,
+ dataCache: me.dataCache,
+ ifname: rec.data.id
+ });
+ win.on('destroy', me.load, me);
+ win.show();
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ selModel: sm,
+ disabled: true,
+ enableFn: function(rec) {
+ if (!caps.vms['VM.Config.Network']) {
+ return false;
+ }
+ return true;
+ },
+ handler: run_editor
+ });
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: [
+ {
+ text: gettext('Add'),
+ name: 'addButton',
+ disabled: !caps.vms['VM.Config.Network'],
+ handler: function() {
+ var win = Ext.create('PVE.lxc.NetworkEdit', {
+ url: me.url,
+ nodename: nodename,
+ isCreate: true,
+ dataCache: me.dataCache
+ });
+ win.on('destroy', me.load, me);
+ win.show();
+ }
+ },
+ remove_btn,
+ edit_btn
+ ],
+ columns: [
+ {
+ header: 'ID',
+ width: 50,
+ dataIndex: 'id'
+ },
+ {
+ header: gettext('Name'),
+ width: 80,
+ dataIndex: 'name'
+ },
+ {
+ header: gettext('Bridge'),
+ width: 80,
+ dataIndex: 'bridge'
+ },
+ {
+ header: gettext('Firewall'),
+ width: 80,
+ dataIndex: 'firewall',
+ renderer: Proxmox.Utils.format_boolean
+ },
+ {
+ header: gettext('VLAN Tag'),
+ width: 80,
+ dataIndex: 'tag'
+ },
+ {
+ header: gettext('MAC address'),
+ width: 110,
+ dataIndex: 'hwaddr'
+ },
+ {
+ header: gettext('IP address'),
+ width: 150,
+ dataIndex: 'ip',
+ renderer: function(value, metaData, rec) {
+ if (rec.data.ip && rec.data.ip6) {
+ return rec.data.ip + "
" + rec.data.ip6;
+ } else if (rec.data.ip6) {
+ return rec.data.ip6;
+ } else {
+ return rec.data.ip;
+ }
+ }
+ },
+ {
+ header: gettext('Gateway'),
+ width: 150,
+ dataIndex: 'gw',
+ renderer: function(value, metaData, rec) {
+ if (rec.data.gw && rec.data.gw6) {
+ return rec.data.gw + "
" + rec.data.gw6;
+ } else if (rec.data.gw6) {
+ return rec.data.gw6;
+ } else {
+ return rec.data.gw;
+ }
+ }
+ }
+ ],
+ listeners: {
+ activate: me.load,
+ itemdblclick: run_editor
+ }
+ });
+
+ me.callParent();
+ }
+}, function() {
+
+ Ext.define('pve-lxc-network', {
+ extend: "Ext.data.Model",
+ proxy: { type: 'memory' },
+ fields: [ 'id', 'name', 'hwaddr', 'bridge',
+ 'ip', 'gw', 'ip6', 'gw6', 'tag', 'firewall' ]
+ });
+
+});
+
+/*jslint confusion: true */
+Ext.define('PVE.lxc.RessourceView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pveLxcRessourceView'],
+
+ onlineHelp: 'pct_configuration',
+
+ renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
+ var me = this;
+ var rowdef = me.rows[key] || {};
+
+ metaData.tdAttr = "valign=middle";
+ if (rowdef.tdCls) {
+ metaData.tdCls = rowdef.tdCls;
+ }
+ return rowdef.header || key;
+ },
+
+ initComponent : function() {
+ var me = this;
+ var i, confid;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+ var diskCap = caps.vms['VM.Config.Disk'];
+
+ var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined;
+
+ var rows = {
+ memory: {
+ header: gettext('Memory'),
+ editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
+ defaultValue: 512,
+ tdCls: 'pve-itype-icon-memory',
+ group: 1,
+ renderer: function(value) {
+ return Proxmox.Utils.format_size(value*1024*1024);
+ }
+ },
+ swap: {
+ header: gettext('Swap'),
+ editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
+ defaultValue: 512,
+ tdCls: 'pve-itype-icon-swap',
+ group: 2,
+ renderer: function(value) {
+ return Proxmox.Utils.format_size(value*1024*1024);
+ }
+ },
+ cores: {
+ header: gettext('Cores'),
+ editor: caps.vms['VM.Config.CPU'] ? 'PVE.lxc.CPUEdit' : undefined,
+ defaultValue: '',
+ tdCls: 'pve-itype-icon-processor',
+ group: 3,
+ renderer: function(value) {
+ var cpulimit = me.getObjectValue('cpulimit');
+ var cpuunits = me.getObjectValue('cpuunits');
+ var res;
+ if (value) {
+ res = value;
+ } else {
+ res = gettext('unlimited');
+ }
+
+ if (cpulimit) {
+ res += ' [cpulimit=' + cpulimit + ']';
+ }
+
+ if (cpuunits) {
+ res += ' [cpuunits=' + cpuunits + ']';
+ }
+ return res;
+ }
+ },
+ rootfs: {
+ header: gettext('Root Disk'),
+ defaultValue: Proxmox.Utils.noneText,
+ editor: mpeditor,
+ tdCls: 'pve-itype-icon-storage',
+ group: 4
+ },
+ cpulimit: {
+ visible: false
+ },
+ cpuunits: {
+ visible: false
+ },
+ unprivileged: {
+ visible: false
+ }
+ };
+
+ PVE.Utils.forEachMP(function(bus, i) {
+ confid = bus + i;
+ var group = 5;
+ var header;
+ if (bus === 'mp') {
+ header = gettext('Mount Point') + ' (' + confid + ')';
+ } else {
+ header = gettext('Unused Disk') + ' ' + i;
+ group += 1;
+ }
+ rows[confid] = {
+ group: group,
+ order: i,
+ tdCls: 'pve-itype-icon-storage',
+ editor: mpeditor,
+ header: header
+ };
+ }, true);
+
+ var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
+
+ me.selModel = Ext.create('Ext.selection.RowModel', {});
+
+ var run_resize = function() {
+ var rec = me.selModel.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.MPResize', {
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid
+ });
+
+ win.show();
+ };
+
+ var run_remove = function(b, e, rec) {
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/' + baseurl,
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: {
+ 'delete': rec.data.key
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ };
+
+ var run_move = function(b, e, rec) {
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.HDMove', {
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid,
+ type: 'lxc'
+ });
+
+ win.show();
+
+ win.on('destroy', me.reload, me);
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ selModel: me.selModel,
+ disabled: true,
+ enableFn: function(rec) {
+ if (!rec) {
+ return false;
+ }
+ var rowdef = rows[rec.data.key];
+ return !!rowdef.editor;
+ },
+ handler: function() { me.run_editor(); }
+ });
+
+ var resize_btn = new Proxmox.button.Button({
+ text: gettext('Resize disk'),
+ selModel: me.selModel,
+ disabled: true,
+ handler: run_resize
+ });
+
+ var remove_btn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ selModel: me.selModel,
+ disabled: true,
+ dangerous: true,
+ confirmMsg: function(rec) {
+ var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
+ "'" + me.renderKey(rec.data.key, {}, rec) + "'");
+ if (rec.data.key.match(/^unused\d+$/)) {
+ msg += " " + gettext('This will permanently erase all data.');
+ }
+
+ return msg;
+ },
+ handler: run_remove
+ });
+
+ var move_btn = new Proxmox.button.Button({
+ text: gettext('Move Volume'),
+ selModel: me.selModel,
+ disabled: true,
+ dangerous: true,
+ handler: run_move
+ });
+
+ var set_button_status = function() {
+ var rec = me.selModel.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ remove_btn.disable();
+ resize_btn.disable();
+ return;
+ }
+ var key = rec.data.key;
+ var value = rec.data.value;
+ var rowdef = rows[key];
+
+ var isDisk = (rowdef.tdCls == 'pve-itype-icon-storage');
+
+ var noedit = rec.data['delete'] || !rowdef.editor;
+ if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) {
+ var mp = PVE.Parser.parseLxcMountPoint(value);
+ if (mp.type !== 'volume') {
+ noedit = true;
+ }
+ }
+ edit_btn.setDisabled(noedit);
+
+ remove_btn.setDisabled(!isDisk || rec.data.key === 'rootfs' || !diskCap);
+ resize_btn.setDisabled(!isDisk || !diskCap);
+ move_btn.setDisabled(!isDisk || !diskCap);
+
+ };
+
+ var sorterFn = function(rec1, rec2) {
+ var v1 = rec1.data.key;
+ var v2 = rec2.data.key;
+ var g1 = rows[v1].group || 0;
+ var g2 = rows[v2].group || 0;
+ var order1 = rows[v1].order || 0;
+ var order2 = rows[v2].order || 0;
+
+ if ((g1 - g2) !== 0) {
+ return g1 - g2;
+ }
+
+ if ((order1 - order2) !== 0) {
+ return order1 - order2;
+ }
+
+ if (v1 > v2) {
+ return 1;
+ } else if (v1 < v2) {
+ return -1;
+ } else {
+ return 0;
+ }
+ };
+
+ Ext.apply(me, {
+ url: '/api2/json/' + baseurl,
+ selModel: me.selModel,
+ interval: 2000,
+ cwidth1: 170,
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ items: [
+ {
+ text: gettext('Mount Point'),
+ iconCls: 'pve-itype-icon-storage',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function() {
+ var win = Ext.create('PVE.lxc.MountPointEdit', {
+ url: '/api2/extjs/' + baseurl,
+ unprivileged: me.getObjectValue('unprivileged'),
+ pveSelNode: me.pveSelNode
+ });
+ win.show();
+ }
+ }
+ ]
+ })
+ },
+ edit_btn,
+ remove_btn,
+ resize_btn,
+ move_btn
+ ],
+ rows: rows,
+ sorterFn: sorterFn,
+ editorConfig: {
+ pveSelNode: me.pveSelNode,
+ url: '/api2/extjs/' + baseurl
+ },
+ listeners: {
+ itemdblclick: me.run_editor,
+ selectionchange: set_button_status
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+
+ Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') });
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.lxc.FeaturesInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveLxcFeaturesInputPanel',
+
+ // used to save the mounts fstypes until sending
+ mounts: [],
+
+ fstypes: ['nfs', 'cifs'],
+
+ viewModel: {
+ parent: null,
+ data: {
+ unprivileged: false
+ },
+ formulas: {
+ privilegedOnly: function(get) {
+ return (get('unprivileged') ? gettext('privileged only') : '');
+ },
+ unprivilegedOnly: function(get) {
+ return (!get('unprivileged') ? gettext('unprivileged only') : '');
+ }
+ }
+ },
+
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('keyctl'),
+ name: 'keyctl',
+ bind: {
+ disabled: '{!unprivileged}',
+ boxLabel: '{unprivilegedOnly}'
+ }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Nesting'),
+ name: 'nesting'
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'nfs',
+ fieldLabel: 'NFS',
+ bind: {
+ disabled: '{unprivileged}',
+ boxLabel: '{privilegedOnly}'
+ }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'cifs',
+ fieldLabel: 'CIFS',
+ bind: {
+ disabled: '{unprivileged}',
+ boxLabel: '{privilegedOnly}'
+ }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'fuse',
+ fieldLabel: 'FUSE'
+ }
+ ],
+
+ onGetValues: function(values) {
+ var me = this;
+ var mounts = me.mounts;
+ me.fstypes.forEach(function(fs) {
+ if (values[fs]) {
+ mounts.push(fs);
+ }
+ delete values[fs];
+ });
+
+ if (mounts.length) {
+ values.mount = mounts.join(';');
+ }
+
+ var featuresstring = PVE.Parser.printPropertyString(values, undefined);
+ if (featuresstring == '') {
+ return { 'delete': 'features' };
+ }
+ return { features: featuresstring };
+ },
+
+ setValues: function(values) {
+ var me = this;
+
+ me.viewModel.set({ unprivileged: values.unprivileged });
+
+ if (values.features) {
+ var res = PVE.Parser.parsePropertyString(values.features);
+ me.mounts = [];
+ if (res.mount) {
+ res.mount.split(/[; ]/).forEach(function(item) {
+ if (me.fstypes.indexOf(item) === -1) {
+ me.mounts.push(item);
+ } else {
+ res[item] = 1;
+ }
+ });
+ }
+ this.callParent([res]);
+ }
+ }
+});
+
+Ext.define('PVE.lxc.FeaturesEdit', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveLxcFeaturesEdit',
+
+ subject: gettext('Features'),
+
+ items: [{
+ xtype: 'pveLxcFeaturesInputPanel'
+ }],
+
+ initComponent : function() {
+ var me = this;
+
+ me.callParent();
+
+ me.load();
+ }
+});
+/*jslint confusion: true */
+Ext.define('PVE.lxc.Options', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pveLxcOptions'],
+
+ onlineHelp: 'pct_options',
+
+ initComponent : function() {
+ var me = this;
+ var i;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var rows = {
+ onboot: {
+ header: gettext('Start at boot'),
+ defaultValue: '',
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Start at boot'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'onboot',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ fieldLabel: gettext('Start at boot')
+ }
+ } : undefined
+ },
+ startup: {
+ header: gettext('Start/Shutdown order'),
+ defaultValue: '',
+ renderer: PVE.Utils.render_kvm_startup,
+ editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ?
+ {
+ xtype: 'pveWindowStartupEdit',
+ onlineHelp: 'pct_startup_and_shutdown'
+ } : undefined
+ },
+ ostype: {
+ header: gettext('OS Type'),
+ defaultValue: Proxmox.Utils.unknownText
+ },
+ arch: {
+ header: gettext('Architecture'),
+ defaultValue: Proxmox.Utils.unknownText
+ },
+ console: {
+ header: '/dev/console',
+ defaultValue: 1,
+ renderer: Proxmox.Utils.format_enabled_toggle,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: '/dev/console',
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'console',
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ checked: true,
+ fieldLabel: '/dev/console'
+ }
+ } : undefined
+ },
+ tty: {
+ header: gettext('TTY count'),
+ defaultValue: 2,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('TTY count'),
+ items: {
+ xtype: 'proxmoxintegerfield',
+ name: 'tty',
+ minValue: 0,
+ maxValue: 6,
+ value: 2,
+ fieldLabel: gettext('TTY count'),
+ emptyText: gettext('Default'),
+ deleteEmpty: true
+ }
+ } : undefined
+ },
+ cmode: {
+ header: gettext('Console mode'),
+ defaultValue: 'tty',
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Console mode'),
+ items: {
+ xtype: 'proxmoxKVComboBox',
+ name: 'cmode',
+ deleteEmpty: true,
+ value: '__default__',
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + " (tty)"],
+ ['tty', "/dev/tty[X]"],
+ ['console', "/dev/console"],
+ ['shell', "shell"]
+ ],
+ fieldLabel: gettext('Console mode')
+ }
+ } : undefined
+ },
+ protection: {
+ header: gettext('Protection'),
+ defaultValue: false,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Protection'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'protection',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled')
+ }
+ } : undefined
+ },
+ unprivileged: {
+ header: gettext('Unprivileged container'),
+ renderer: Proxmox.Utils.format_boolean,
+ defaultValue: 0
+ },
+ features: {
+ header: gettext('Features'),
+ defaultValue: Proxmox.Utils.noneText,
+ editor: Proxmox.UserName === 'root@pam' ?
+ 'PVE.lxc.FeaturesEdit' : undefined
+ },
+ hookscript: {
+ header: gettext('Hookscript')
+ }
+ };
+
+ var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ var rowdef = rows[rec.data.key];
+ return !!rowdef.editor;
+ },
+ handler: function() { me.run_editor(); }
+ });
+
+ Ext.apply(me, {
+ url: "/api2/json/" + baseurl,
+ selModel: sm,
+ interval: 5000,
+ tbar: [ edit_btn ],
+ rows: rows,
+ editorConfig: {
+ url: '/api2/extjs/' + baseurl
+ },
+ listeners: {
+ itemdblclick: me.run_editor
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+
+ }
+});
+
+Ext.define('PVE.lxc.DNSInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveLxcDNSInputPanel',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var deletes = [];
+ if (!values.searchdomain && !me.insideWizard) {
+ deletes.push('searchdomain');
+ }
+
+ if (values.nameserver) {
+ var list = values.nameserver.split(/[\ \,\;]+/);
+ values.nameserver = list.join(' ');
+ } else if(!me.insideWizard) {
+ deletes.push('nameserver');
+ }
+
+ if (deletes.length) {
+ values['delete'] = deletes.join(',');
+ }
+
+ return values;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ var items = [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'searchdomain',
+ skipEmptyText: true,
+ fieldLabel: gettext('DNS domain'),
+ emptyText: gettext('use host settings'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('DNS servers'),
+ vtype: 'IP64AddressList',
+ allowBlank: true,
+ emptyText: gettext('use host settings'),
+ name: 'nameserver',
+ itemId: 'nameserver'
+ }
+ ];
+
+ if (me.insideWizard) {
+ me.column1 = items;
+ } else {
+ me.items = items;
+ }
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.lxc.DNSEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent : function() {
+ var me = this;
+
+ var ipanel = Ext.create('PVE.lxc.DNSInputPanel');
+
+ Ext.apply(me, {
+ subject: gettext('Resources'),
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+
+ if (values.nameserver) {
+ values.nameserver.replace(/[,;]/, ' ');
+ values.nameserver.replace(/^\s+/, '');
+ }
+
+ ipanel.setValues(values);
+ }
+ });
+ }
+ }
+});
+
+/*jslint confusion: true */
+Ext.define('PVE.lxc.DNS', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pveLxcDNS'],
+
+ onlineHelp: 'pct_container_network',
+
+ initComponent : function() {
+ var me = this;
+ var i;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var rows = {
+ hostname: {
+ required: true,
+ defaultValue: me.pveSelNode.data.name,
+ header: gettext('Hostname'),
+ editor: caps.vms['VM.Config.Network'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Hostname'),
+ items: {
+ xtype: 'inputpanel',
+ items:{
+ fieldLabel: gettext('Hostname'),
+ xtype: 'textfield',
+ name: 'hostname',
+ vtype: 'DnsName',
+ allowBlank: true,
+ emptyText: 'CT' + vmid.toString()
+ },
+ onGetValues: function(values) {
+ var params = values;
+ if (values.hostname === undefined ||
+ values.hostname === null ||
+ values.hostname === '') {
+ params = { hostname: 'CT'+vmid.toString()};
+ }
+ return params;
+ }
+ }
+ } : undefined
+ },
+ searchdomain: {
+ header: gettext('DNS domain'),
+ defaultValue: '',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
+ renderer: function(value) {
+ return value || gettext('use host settings');
+ }
+ },
+ nameserver: {
+ header: gettext('DNS server'),
+ defaultValue: '',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
+ renderer: function(value) {
+ return value || gettext('use host settings');
+ }
+ }
+ };
+
+ var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
+
+ var reload = function() {
+ me.rstore.load();
+ };
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var rowdef = rows[rec.data.key];
+ if (!rowdef.editor) {
+ return;
+ }
+
+ var win;
+ if (Ext.isString(rowdef.editor)) {
+ win = Ext.create(rowdef.editor, {
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs/' + baseurl
+ });
+ } else {
+ var config = Ext.apply({
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs/' + baseurl
+ }, rowdef.editor);
+ win = Ext.createWidget(rowdef.editor.xtype, config);
+ win.load();
+ }
+ //win.load();
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ var rowdef = rows[rec.data.key];
+ return !!rowdef.editor;
+ },
+ handler: run_editor
+ });
+
+ var set_button_status = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ return;
+ }
+ var rowdef = rows[rec.data.key];
+ edit_btn.setDisabled(!rowdef.editor);
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/config",
+ selModel: sm,
+ cwidth1: 150,
+ run_editor: run_editor,
+ tbar: [ edit_btn ],
+ rows: rows,
+ listeners: {
+ itemdblclick: run_editor,
+ selectionchange: set_button_status,
+ activate: reload
+ }
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.lxc.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.lxc.Config',
+
+ onlineHelp: 'chapter_pct',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var template = !!me.pveSelNode.data.template;
+
+ var running = !!me.pveSelNode.data.uptime;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var base_url = '/nodes/' + nodename + '/lxc/' + vmid;
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: '/api2/json' + base_url + '/status/current',
+ interval: 1000
+ });
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: base_url + "/status/" + cmd,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ };
+
+ var startBtn = Ext.create('Ext.Button', {
+ text: gettext('Start'),
+ disabled: !caps.vms['VM.PowerMgmt'] || running,
+ hidden: template,
+ handler: function() {
+ vm_command('start');
+ },
+ iconCls: 'fa fa-play'
+ });
+
+ var stopBtn = Ext.create('Ext.menu.Item',{
+ text: gettext('Stop'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ confirmMsg: Proxmox.Utils.format_task_description('vzstop', vmid),
+ tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
+ dangerous: true,
+ handler: function() {
+ vm_command("stop");
+ },
+ iconCls: 'fa fa-stop'
+ });
+
+ var shutdownBtn = Ext.create('PVE.button.Split', {
+ text: gettext('Shutdown'),
+ disabled: !caps.vms['VM.PowerMgmt'] || !running,
+ hidden: template,
+ confirmMsg: Proxmox.Utils.format_task_description('vzshutdown', vmid),
+ handler: function() {
+ vm_command('shutdown');
+ },
+ menu: {
+ items:[stopBtn]
+ },
+ iconCls: 'fa fa-power-off'
+ });
+
+ var migrateBtn = Ext.create('Ext.Button', {
+ text: gettext('Migrate'),
+ disabled: !caps.vms['VM.Migrate'],
+ hidden: PVE.data.ResourceStore.getNodes().length < 2,
+ handler: function() {
+ var win = Ext.create('PVE.window.Migrate', {
+ vmtype: 'lxc',
+ nodename: nodename,
+ vmid: vmid
+ });
+ win.show();
+ },
+ iconCls: 'fa fa-send-o'
+ });
+
+ var moreBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('More'),
+ menu: { items: [
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: caps.vms['VM.Clone'] ? false : true,
+ handler: function() {
+ PVE.window.Clone.wrap(nodename, vmid, template, 'lxc');
+ }
+ },
+ {
+ text: gettext('Convert to template'),
+ disabled: template,
+ xtype: 'pveMenuItem',
+ iconCls: 'fa fa-fw fa-file-o',
+ hidden: caps.vms['VM.Allocate'] ? false : true,
+ confirmMsg: Proxmox.Utils.format_task_description('vztemplate', vmid),
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: base_url + '/template',
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ }
+ },
+ {
+ iconCls: 'fa fa-heartbeat ',
+ hidden: !caps.nodes['Sys.Console'],
+ text: gettext('Manage HA'),
+ handler: function() {
+ var ha = me.pveSelNode.data.hastate;
+ Ext.create('PVE.ha.VMResourceEdit', {
+ vmid: vmid,
+ guestType: 'ct',
+ isCreate: (!ha || ha === 'unmanaged')
+ }).show();
+ }
+ },
+ {
+ text: gettext('Remove'),
+ disabled: !caps.vms['VM.Allocate'],
+ itemId: 'removeBtn',
+ handler: function() {
+ Ext.create('PVE.window.SafeDestroy', {
+ url: base_url,
+ item: { type: 'CT', id: vmid }
+ }).show();
+ },
+ iconCls: 'fa fa-trash-o'
+ }
+ ]}
+ });
+
+ var vm = me.pveSelNode.data;
+
+ var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
+ disabled: !caps.vms['VM.Console'],
+ consoleType: 'lxc',
+ consoleName: vm.name,
+ hidden: template,
+ nodename: nodename,
+ vmid: vmid
+ });
+
+ var statusTxt = Ext.create('Ext.toolbar.TextItem', {
+ data: {
+ lock: undefined
+ },
+ tpl: [
+ '
' + gettext("You can delete the image from the guest's hardware pane");
+
+ Ext.Msg.show({
+ title: gettext('Cannot remove disk image.'),
+ icon: Ext.Msg.ERROR,
+ msg: msg
+ });
+ return;
+ }
+ }
+ var win = Ext.create('PVE.window.SafeDestroy', {
+ title: Ext.String.format(gettext("Destroy '{0}'"), rec.data.volid),
+ showProgress: true,
+ url: url,
+ item: { type: 'Image', id: vmid }
+ }).show();
+ win.on('destroy', function() {
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status'
+ });
+ reload();
+
+ });
+ }
+ });
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status'
+ });
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: [
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Restore'),
+ selModel: sm,
+ disabled: true,
+ enableFn: function(rec) {
+ return rec && rec.data.content === 'backup';
+ },
+ handler: function(b, e, rec) {
+ var vmtype;
+ if (rec.data.volid.match(/vzdump-qemu-/)) {
+ vmtype = 'qemu';
+ } else if (rec.data.volid.match(/vzdump-openvz-/) || rec.data.volid.match(/vzdump-lxc-/)) {
+ vmtype = 'lxc';
+ } else {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.Restore', {
+ nodename: nodename,
+ volid: rec.data.volid,
+ volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
+ vmtype: vmtype
+ });
+ win.show();
+ win.on('destroy', reload);
+ }
+ },
+ removeButton,
+ imageRemoveButton,
+ templateButton,
+ uploadButton,
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Show Configuration'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ return rec && rec.data.content === 'backup';
+ },
+ handler: function(b,e,rec) {
+ var win = Ext.create('PVE.window.BackupConfig', {
+ volume: rec.data.volid,
+ pveSelNode: me.pveSelNode
+ });
+
+ win.show();
+ }
+ },
+ '->',
+ gettext('Search') + ':', ' ',
+ {
+ xtype: 'textfield',
+ width: 200,
+ enableKeyEvents: true,
+ listeners: {
+ buffer: 500,
+ keyup: function(field) {
+ store.clearFilter(true);
+ store.filter([
+ {
+ property: 'text',
+ value: field.getValue(),
+ anyMatch: true,
+ caseSensitive: false
+ }
+ ]);
+ }
+ }
+ }
+ ],
+ columns: [
+ {
+ header: gettext('Name'),
+ flex: 1,
+ sortable: true,
+ renderer: PVE.Utils.render_storage_content,
+ dataIndex: 'text'
+ },
+ {
+ header: gettext('Format'),
+ width: 100,
+ dataIndex: 'format'
+ },
+ {
+ header: gettext('Type'),
+ width: 100,
+ dataIndex: 'content',
+ renderer: PVE.Utils.format_content_types
+ },
+ {
+ header: gettext('Size'),
+ width: 100,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'size'
+ }
+ ],
+ listeners: {
+ activate: reload
+ }
+ });
+
+ me.callParent();
+
+ // disable the buttons/restrict the upload window
+ // if templates or uploads are not allowed
+ me.mon(me.statusStore, 'load', function(s, records, success) {
+ var availcontent = [];
+ Ext.Array.each(records, function(item){
+ if (item.id === 'content') {
+ availcontent = item.data.value.split(',');
+ }
+ });
+ var templ = false;
+ var upload = false;
+ var cts = [];
+
+ Ext.Array.each(availcontent, function(content) {
+ if (content === 'vztmpl') {
+ templ = true;
+ cts.push('vztmpl');
+ } else if (content === 'iso') {
+ upload = true;
+ cts.push('iso');
+ }
+ });
+
+ if (templ !== upload) {
+ uploadButton.contents = cts;
+ }
+
+ templateButton.setDisabled(!templ);
+ uploadButton.setDisabled(!upload && !templ);
+ });
+ }
+}, function() {
+
+ Ext.define('pve-storage-content', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'volid', 'content', 'format', 'size', 'used', 'vmid',
+ 'channel', 'id', 'lun',
+ {
+ name: 'text',
+ convert: function(value, record) {
+ // check for volid, because if you click on a grouping header,
+ // it calls convert (but with an empty volid)
+ if (value || record.data.volid === null) {
+ return value;
+ }
+ return PVE.Utils.render_storage_content(value, {}, record);
+ }
+ }
+ ],
+ idProperty: 'volid'
+ });
+
+});
+Ext.define('PVE.storage.StatusView', {
+ extend: 'PVE.panel.StatusView',
+ alias: 'widget.pveStorageStatusView',
+
+ height: 230,
+ title: gettext('Status'),
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+
+ defaults: {
+ xtype: 'pveInfoWidget',
+ padding: '0 30 5 30'
+ },
+ items: [
+ {
+ xtype: 'box',
+ height: 30
+ },
+ {
+ itemId: 'enabled',
+ title: gettext('Enabled'),
+ printBar: false,
+ textField: 'disabled',
+ renderer: Proxmox.Utils.format_neg_boolean
+ },
+ {
+ itemId: 'active',
+ title: gettext('Active'),
+ printBar: false,
+ textField: 'active',
+ renderer: Proxmox.Utils.format_boolean
+ },
+ {
+ itemId: 'content',
+ title: gettext('Content'),
+ printBar: false,
+ textField: 'content',
+ renderer: PVE.Utils.format_content_types
+ },
+ {
+ itemId: 'type',
+ title: gettext('Type'),
+ printBar: false,
+ textField: 'type',
+ renderer: PVE.Utils.format_storage_type
+ },
+ {
+ xtype: 'box',
+ height: 10
+ },
+ {
+ itemId: 'usage',
+ title: gettext('Usage'),
+ valueField: 'used',
+ maxField: 'total'
+ }
+ ],
+
+ updateTitle: function() {
+ return;
+ }
+});
+Ext.define('PVE.storage.Summary', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveStorageSummary',
+ scrollable: true,
+ bodyPadding: 5,
+ tbar: [
+ '->',
+ {
+ xtype: 'proxmoxRRDTypeSelector'
+ }
+ ],
+ layout: {
+ type: 'column'
+ },
+ defaults: {
+ padding: 5,
+ columnWidth: 1
+ },
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var storage = me.pveSelNode.data.storage;
+ if (!storage) {
+ throw "no storage ID specified";
+ }
+
+ var rstore = Ext.create('Proxmox.data.ObjectStore', {
+ url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status",
+ interval: 1000
+ });
+
+ var rrdstore = Ext.create('Proxmox.data.RRDStore', {
+ rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata",
+ model: 'pve-rrd-storage'
+ });
+
+ Ext.apply(me, {
+ items: [
+ {
+ xtype: 'pveStorageStatusView',
+ pveSelNode: me.pveSelNode,
+ rstore: rstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Usage'),
+ fields: ['total','used'],
+ fieldTitles: ['Total Size', 'Used Size'],
+ store: rrdstore
+ }
+ ],
+ listeners: {
+ activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); },
+ destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); }
+ }
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.Browser', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.storage.Browser',
+
+ onlineHelp: 'chapter_storage',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var storeid = me.pveSelNode.data.storage;
+ if (!storeid) {
+ throw "no storage ID specified";
+ }
+
+
+ me.items = [
+ {
+ title: gettext('Summary'),
+ xtype: 'pveStorageSummary',
+ iconCls: 'fa fa-book',
+ itemId: 'summary'
+ }
+ ];
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ Ext.apply(me, {
+ title: Ext.String.format(gettext("Storage {0} on node {1}"),
+ "'" + storeid + "'", "'" + nodename + "'"),
+ hstateid: 'storagetab'
+ });
+
+ if (caps.storage['Datastore.Allocate'] ||
+ caps.storage['Datastore.AllocateSpace'] ||
+ caps.storage['Datastore.Audit']) {
+ me.items.push({
+ xtype: 'pveStorageContentView',
+ title: gettext('Content'),
+ iconCls: 'fa fa-th',
+ itemId: 'content'
+ });
+ }
+
+ if (caps.storage['Permissions.Modify']) {
+ me.items.push({
+ xtype: 'pveACLView',
+ title: gettext('Permissions'),
+ iconCls: 'fa fa-unlock',
+ itemId: 'permissions',
+ path: '/storage/' + storeid
+ });
+ }
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.DirInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_directory',
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'path',
+ value: '',
+ fieldLabel: gettext('Directory'),
+ allowBlank: false
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ name: 'content',
+ value: 'images',
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'shared',
+ uncheckedValue: 0,
+ fieldLabel: gettext('Shared')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Max Backups'),
+ disabled: true,
+ name: 'maxfiles',
+ reference: 'maxfiles',
+ minValue: 0,
+ maxValue: 365,
+ value: me.isCreate ? '1' : undefined,
+ allowBlank: false
+ }
+ ];
+
+ me.callParent();
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.storage.NFSScan', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveNFSScan',
+
+ queryParam: 'server',
+
+ valueField: 'path',
+ displayField: 'path',
+ matchFieldWidth: false,
+ listConfig: {
+ loadingText: gettext('Scanning...'),
+ width: 350
+ },
+ doRawQuery: function() {
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.nfsServer) {
+ me.store.removeAll();
+ }
+
+ me.allQuery = me.nfsServer;
+
+ me.callParent();
+ },
+
+ setServer: function(server) {
+ var me = this;
+
+ me.nfsServer = server;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: [ 'path', 'options' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/nfs'
+ }
+ });
+
+ store.sort('path', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.NFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_nfs',
+
+ options : [],
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var i;
+ var res = [];
+ for (i = 0; i < me.options.length; i++) {
+ var item = me.options[i];
+ if (!item.match(/^vers=(.*)$/)) {
+ res.push(item);
+ }
+ }
+ if (values.nfsversion && values.nfsversion !== '__default__') {
+ res.push('vers=' + values.nfsversion);
+ }
+ delete values.nfsversion;
+ values.options = res.join(',');
+ if (values.options === '') {
+ delete values.options;
+ if (!me.isCreate) {
+ values["delete"] = "options";
+ }
+ }
+
+ return me.callParent([values]);
+ },
+
+ setValues: function(values) {
+ var me = this;
+ if (values.options) {
+ var res = values.options;
+ me.options = values.options.split(',');
+ me.options.forEach(function(item) {
+ var match = item.match(/^vers=(.*)$/);
+ if (match) {
+ values.nfsversion = match[1];
+ }
+ });
+ }
+ return me.callParent([values]);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'server',
+ value: '',
+ fieldLabel: gettext('Server'),
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ var exportField = me.down('field[name=export]');
+ exportField.setServer(value);
+ exportField.setValue('');
+ }
+ }
+ }
+ },
+ {
+ xtype: me.isCreate ? 'pveNFSScan' : 'displayfield',
+ name: 'export',
+ value: '',
+ fieldLabel: 'Export',
+ allowBlank: false
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ name: 'content',
+ value: 'images',
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Max Backups'),
+ disabled: true,
+ name: 'maxfiles',
+ reference: 'maxfiles',
+ minValue: 0,
+ maxValue: 365,
+ value: me.isCreate ? '1' : undefined,
+ allowBlank: false
+ }
+ ];
+
+ me.advancedColumn1 = [
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('NFS Version'),
+ name: 'nfsversion',
+ value: '__default__',
+ deleteEmpty: false,
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText],
+ ['3', '3'],
+ ['4', '4'],
+ ['4.1', '4.1'],
+ ['4.2', '4.2']
+ ]
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.CIFSScan', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveCIFSScan',
+
+ queryParam: 'server',
+
+ valueField: 'share',
+ displayField: 'share',
+ matchFieldWidth: false,
+ listConfig: {
+ loadingText: gettext('Scanning...'),
+ width: 350
+ },
+ doRawQuery: function() {
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.cifsServer) {
+ me.store.removeAll();
+ }
+
+ var params = {};
+ if (me.cifsUsername && me.cifsPassword) {
+ params.username = me.cifsUsername;
+ params.password = me.cifsPassword;
+ }
+
+ if (me.cifsDomain) {
+ params.domain = me.cifsDomain;
+ }
+
+ me.store.getProxy().setExtraParams(params);
+ me.allQuery = me.cifsServer;
+
+ me.callParent();
+ },
+
+ setServer: function(server) {
+ this.cifsServer = server;
+ },
+
+ setUsername: function(username) {
+ this.cifsUsername = username;
+ },
+
+ setPassword: function(password) {
+ this.cifsPassword = password;
+ },
+
+ setDomain: function(domain) {
+ this.cifsDomain = domain;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: ['description', 'share'],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/cifs'
+ }
+ });
+ store.sort('share', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.CIFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_cifs',
+
+ initComponent : function() {
+ var me = this;
+
+ var passwordfield = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
+ inputType: 'password',
+ name: 'password',
+ value: me.isCreate ? '' : '********',
+ fieldLabel: gettext('Password'),
+ allowBlank: false,
+ disabled: me.isCreate,
+ minLength: 1,
+ listeners: {
+ change: function(f, value) {
+
+ if (me.isCreate) {
+ var exportField = me.down('field[name=share]');
+ exportField.setPassword(value);
+ }
+ }
+ }
+ });
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'server',
+ value: '',
+ fieldLabel: gettext('Server'),
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ var exportField = me.down('field[name=share]');
+ exportField.setServer(value);
+ }
+ }
+ }
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'username',
+ value: '',
+ fieldLabel: gettext('Username'),
+ emptyText: gettext('Guest user'),
+ allowBlank: true,
+ listeners: {
+ change: function(f, value) {
+ if (!me.isCreate) {
+ return;
+ }
+ var exportField = me.down('field[name=share]');
+ exportField.setUsername(value);
+
+ if (value == "") {
+ passwordfield.disable();
+ } else {
+ passwordfield.enable();
+ }
+ passwordfield.validate();
+ }
+ }
+ },
+ passwordfield,
+ {
+ xtype: me.isCreate ? 'pveCIFSScan' : 'displayfield',
+ name: 'share',
+ value: '',
+ fieldLabel: 'Share',
+ allowBlank: false
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Max Backups'),
+ name: 'maxfiles',
+ reference: 'maxfiles',
+ minValue: 0,
+ maxValue: 365,
+ value: me.isCreate ? '1' : undefined,
+ allowBlank: false
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ name: 'content',
+ value: 'images',
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'domain',
+ value: me.isCreate ? '' : undefined,
+ fieldLabel: gettext('Domain'),
+ allowBlank: true,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+
+ var exportField = me.down('field[name=share]');
+ exportField.setDomain(value);
+ }
+ }
+ }
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.GlusterFsScan', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveGlusterFsScan',
+
+ queryParam: 'server',
+
+ valueField: 'volname',
+ displayField: 'volname',
+ matchFieldWidth: false,
+ listConfig: {
+ loadingText: 'Scanning...',
+ width: 350
+ },
+ doRawQuery: function() {
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.glusterServer) {
+ me.store.removeAll();
+ }
+
+ me.allQuery = me.glusterServer;
+
+ me.callParent();
+ },
+
+ setServer: function(server) {
+ var me = this;
+
+ me.glusterServer = server;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: [ 'volname' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/glusterfs'
+ }
+ });
+
+ store.sort('volname', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.GlusterFsInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_glusterfs',
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'server',
+ value: '',
+ fieldLabel: gettext('Server'),
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ var volumeField = me.down('field[name=volume]');
+ volumeField.setServer(value);
+ volumeField.setValue('');
+ }
+ }
+ }
+ },
+ {
+ xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
+ name: 'server2',
+ value: '',
+ fieldLabel: gettext('Second Server'),
+ allowBlank: true
+ },
+ {
+ xtype: me.isCreate ? 'pveGlusterFsScan' : 'displayfield',
+ name: 'volume',
+ value: '',
+ fieldLabel: 'Volume name',
+ allowBlank: false
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'iso', 'backup', 'vztmpl', 'snippets'],
+ name: 'content',
+ value: 'images',
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Max Backups'),
+ disabled: true,
+ name: 'maxfiles',
+ reference: 'maxfiles',
+ minValue: 0,
+ maxValue: 365,
+ value: me.isCreate ? '1' : undefined,
+ allowBlank: false
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.IScsiScan', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveIScsiScan',
+
+ queryParam: 'portal',
+ valueField: 'target',
+ displayField: 'target',
+ matchFieldWidth: false,
+ listConfig: {
+ loadingText: gettext('Scanning...'),
+ width: 350
+ },
+ doRawQuery: function() {
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.portal) {
+ me.store.removeAll();
+ }
+
+ me.allQuery = me.portal;
+
+ me.callParent();
+ },
+
+ setPortal: function(portal) {
+ var me = this;
+
+ me.portal = portal;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: [ 'target', 'portal' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/iscsi'
+ }
+ });
+
+ store.sort('target', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.IScsiInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_open_iscsi',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ values.content = values.luns ? 'images' : 'none';
+ delete values.luns;
+
+ return me.callParent([values]);
+ },
+
+ setValues: function(values) {
+ values.luns = (values.content.indexOf('images') !== -1) ? true : false;
+ this.callParent([values]);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'portal',
+ value: '',
+ fieldLabel: 'Portal',
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ var exportField = me.down('field[name=target]');
+ exportField.setPortal(value);
+ exportField.setValue('');
+ }
+ }
+ }
+ },
+ {
+ readOnly: !me.isCreate,
+ xtype: me.isCreate ? 'pveIScsiScan' : 'displayfield',
+ name: 'target',
+ value: '',
+ fieldLabel: 'Target',
+ allowBlank: false
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'checkbox',
+ name: 'luns',
+ checked: true,
+ fieldLabel: gettext('Use LUNs directly')
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.VgSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveVgSelector',
+ valueField: 'vg',
+ displayField: 'vg',
+ queryMode: 'local',
+ editable: false,
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: {}, // true,
+ fields: [ 'vg', 'size', 'free' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/lvm'
+ }
+ });
+
+ store.sort('vg', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ loadingText: gettext('Scanning...')
+ }
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.BaseStorageSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveBaseStorageSelector',
+
+ existingGroupsText: gettext("Existing volume groups"),
+ queryMode: 'local',
+ editable: false,
+ value: '',
+ valueField: 'storage',
+ displayField: 'text',
+ initComponent : function() {
+ var me = this;
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: {
+ addRecords: true,
+ params: {
+ type: 'iscsi'
+ }
+ },
+ fields: [ 'storage', 'type', 'content',
+ {
+ name: 'text',
+ convert: function(value, record) {
+ if (record.data.storage) {
+ return record.data.storage + " (iSCSI)";
+ } else {
+ return me.existingGroupsText;
+ }
+ }
+ }],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/storage/'
+ }
+ });
+
+ store.loadData([{ storage: '' }], true);
+
+ store.sort('storage', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.LVMInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_lvm',
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [];
+
+ var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
+ name: 'vgname',
+ hidden: !!me.isCreate,
+ disabled: !!me.isCreate,
+ value: '',
+ fieldLabel: gettext('Volume group'),
+ allowBlank: false
+ });
+
+ if (me.isCreate) {
+ var vgField = Ext.create('PVE.storage.VgSelector', {
+ name: 'vgname',
+ fieldLabel: gettext('Volume group'),
+ allowBlank: false
+ });
+
+ var baseField = Ext.createWidget('pveFileSelector', {
+ name: 'base',
+ hidden: true,
+ disabled: true,
+ nodename: 'localhost',
+ storageContent: 'images',
+ fieldLabel: gettext('Base volume'),
+ allowBlank: false
+ });
+
+ me.column1.push({
+ xtype: 'pveBaseStorageSelector',
+ name: 'basesel',
+ fieldLabel: gettext('Base storage'),
+ submitValue: false,
+ listeners: {
+ change: function(f, value) {
+ if (value) {
+ vgnameField.setVisible(true);
+ vgnameField.setDisabled(false);
+ vgField.setVisible(false);
+ vgField.setDisabled(true);
+ baseField.setVisible(true);
+ baseField.setDisabled(false);
+ } else {
+ vgnameField.setVisible(false);
+ vgnameField.setDisabled(true);
+ vgField.setVisible(true);
+ vgField.setDisabled(false);
+ baseField.setVisible(false);
+ baseField.setDisabled(true);
+ }
+ baseField.setStorage(value);
+ }
+ }
+ });
+
+ me.column1.push(baseField);
+
+ me.column1.push(vgField);
+ }
+
+ me.column1.push(vgnameField);
+
+ // here value is an array,
+ // while before it was a string
+ /*jslint confusion: true*/
+ me.column1.push({
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images', 'rootdir'],
+ multiSelect: true,
+ allowBlank: false
+ });
+ /*jslint confusion: false*/
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'shared',
+ uncheckedValue: 0,
+ fieldLabel: gettext('Shared')
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.TPoolSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveTPSelector',
+
+ queryParam: 'vg',
+ valueField: 'lv',
+ displayField: 'lv',
+ editable: false,
+
+ doRawQuery: function() {
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.vg) {
+ me.store.removeAll();
+ }
+
+ me.allQuery = me.vg;
+
+ me.callParent();
+ },
+
+ setVG: function(myvg) {
+ var me = this;
+
+ me.vg = myvg;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: [ 'lv' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/lvmthin'
+ }
+ });
+
+ store.sort('lv', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ loadingText: gettext('Scanning...')
+ }
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.BaseVGSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveBaseVGSelector',
+
+ valueField: 'vg',
+ displayField: 'vg',
+ queryMode: 'local',
+ editable: false,
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: {},
+ fields: [ 'vg', 'size', 'free'],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/lvm'
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ loadingText: gettext('Scanning...')
+ }
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.LvmThinInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_lvmthin',
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [];
+
+ var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
+ name: 'vgname',
+ hidden: !!me.isCreate,
+ disabled: !!me.isCreate,
+ value: '',
+ fieldLabel: gettext('Volume group'),
+ allowBlank: false
+ });
+
+ var thinpoolField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
+ name: 'thinpool',
+ hidden: !!me.isCreate,
+ disabled: !!me.isCreate,
+ value: '',
+ fieldLabel: gettext('Thin Pool'),
+ allowBlank: false
+ });
+
+ if (me.isCreate) {
+ var vgField = Ext.create('PVE.storage.TPoolSelector', {
+ name: 'thinpool',
+ fieldLabel: gettext('Thin Pool'),
+ allowBlank: false
+ });
+
+ me.column1.push({
+ xtype: 'pveBaseVGSelector',
+ name: 'vgname',
+ fieldLabel: gettext('Volume group'),
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ vgField.setVG(value);
+ vgField.setValue('');
+ }
+ }
+ }
+ });
+
+ me.column1.push(vgField);
+ }
+
+ me.column1.push(vgnameField);
+
+ me.column1.push(thinpoolField);
+
+ // here value is an array,
+ // while before it was a string
+ /*jslint confusion: true*/
+ me.column1.push({
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images', 'rootdir'],
+ multiSelect: true,
+ allowBlank: false
+ });
+ /*jslint confusion: false*/
+
+ me.column2 = [];
+
+ me.callParent();
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.storage.CephFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+ controller: 'cephstorage',
+
+ onlineHelp: 'storage_cephfs',
+
+ viewModel: {
+ type: 'cephstorage'
+ },
+
+ setValues: function(values) {
+ if (values.monhost) {
+ this.viewModel.set('pveceph', false);
+ this.lookupReference('pvecephRef').setValue(false);
+ this.lookupReference('pvecephRef').resetOriginalValue();
+ }
+ this.callParent([values]);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+ me.type = 'cephfs';
+
+ me.column1 = [];
+
+ me.column1.push(
+ {
+ xtype: 'textfield',
+ name: 'monhost',
+ vtype: 'HostList',
+ value: '',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ hidden: '{pveceph}'
+ },
+ fieldLabel: 'Monitor(s)',
+ allowBlank: false
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'monhost',
+ bind: {
+ disabled: '{!pveceph}',
+ hidden: '{!pveceph}'
+ },
+ value: '',
+ fieldLabel: 'Monitor(s)'
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'username',
+ value: 'admin',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}'
+ },
+ fieldLabel: gettext('User name'),
+ allowBlank: true
+ }
+ );
+
+ me.column2 = [
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['backup', 'iso', 'vztmpl', 'snippets'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: 'backup',
+ multiSelect: true,
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Max Backups'),
+ name: 'maxfiles',
+ reference: 'maxfiles',
+ minValue: 0,
+ maxValue: 365,
+ value: me.isCreate ? '1' : undefined,
+ allowBlank: false
+ }
+ ];
+
+ me.columnB = [{
+ xtype: 'proxmoxcheckbox',
+ name: 'pveceph',
+ reference: 'pvecephRef',
+ bind : {
+ disabled: '{!pvecephPossible}',
+ value: '{pveceph}'
+ },
+ checked: true,
+ uncheckedValue: 0,
+ submitValue: false,
+ hidden: !me.isCreate,
+ boxLabel: gettext('Use Proxmox VE managed hyper-converged cephFS')
+ }];
+
+ me.callParent();
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.storage.Ceph.Model', {
+ extend: 'Ext.app.ViewModel',
+ alias: 'viewmodel.cephstorage',
+
+ data: {
+ pveceph: true,
+ pvecephPossible: true
+ }
+});
+
+Ext.define('PVE.storage.Ceph.Controller', {
+ extend: 'PVE.controller.StorageEdit',
+ alias: 'controller.cephstorage',
+
+ control: {
+ '#': {
+ afterrender: 'queryMonitors'
+ },
+ 'textfield[name=username]': {
+ disable: 'resetField'
+ },
+ 'displayfield[name=monhost]': {
+ enable: 'queryMonitors'
+ },
+ 'textfield[name=monhost]': {
+ disable: 'resetField',
+ enable: 'resetField'
+ }
+ },
+ resetField: function(field) {
+ field.reset();
+ },
+ queryMonitors: function(field, newVal, oldVal) {
+ // we get called with two signatures, the above one for a field
+ // change event and the afterrender from the view, this check only
+ // can be true for the field change one and omit the API request if
+ // pveceph got unchecked - as it's not needed there.
+ if (field && !newVal && oldVal) {
+ return;
+ }
+ var view = this.getView();
+ var vm = this.getViewModel();
+ if (!(view.isCreate || vm.get('pveceph'))) {
+ return; // only query on create or if editing a pveceph store
+ }
+
+ var monhostField = this.lookupReference('monhost');
+
+ Proxmox.Utils.API2Request({
+ url: '/api2/json/nodes/localhost/ceph/mon',
+ method: 'GET',
+ scope: this,
+ callback: function(options, success, response) {
+ var data = response.result.data;
+ if (response.status === 200) {
+ if (data.length > 0) {
+ var monhost = Ext.Array.pluck(data, 'name').sort().join(',');
+ monhostField.setValue(monhost);
+ monhostField.resetOriginalValue();
+ if (view.isCreate) {
+ vm.set('pvecephPossible', true);
+ }
+ } else {
+ vm.set('pveceph', false);
+ }
+ } else {
+ vm.set('pveceph', false);
+ vm.set('pvecephPossible', false);
+ }
+ }
+ });
+ }
+});
+
+Ext.define('PVE.storage.RBDInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+ controller: 'cephstorage',
+
+ onlineHelp: 'ceph_rados_block_devices',
+
+ viewModel: {
+ type: 'cephstorage'
+ },
+
+ setValues: function(values) {
+ if (values.monhost) {
+ this.viewModel.set('pveceph', false);
+ this.lookupReference('pvecephRef').setValue(false);
+ this.lookupReference('pvecephRef').resetOriginalValue();
+ }
+ this.callParent([values]);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+ me.type = 'rbd';
+
+ me.column1 = [];
+
+ if (me.isCreate) {
+ me.column1.push({
+ xtype: 'pveCephPoolSelector',
+ nodename: me.nodename,
+ name: 'pool',
+ bind: {
+ disabled: '{!pveceph}',
+ submitValue: '{pveceph}',
+ hidden: '{!pveceph}'
+ },
+ fieldLabel: gettext('Pool'),
+ allowBlank: false
+ },{
+ xtype: 'textfield',
+ name: 'pool',
+ value: 'rbd',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ hidden: '{pveceph}'
+ },
+ fieldLabel: gettext('Pool'),
+ allowBlank: false
+ });
+ } else {
+ me.column1.push({
+ xtype: 'displayfield',
+ nodename: me.nodename,
+ name: 'pool',
+ fieldLabel: gettext('Pool'),
+ allowBlank: false
+ });
+ }
+
+ me.column1.push(
+ {
+ xtype: 'textfield',
+ name: 'monhost',
+ vtype: 'HostList',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ hidden: '{pveceph}'
+ },
+ value: '',
+ fieldLabel: 'Monitor(s)',
+ allowBlank: false
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'monhost',
+ bind: {
+ disabled: '{!pveceph}',
+ hidden: '{!pveceph}'
+ },
+ value: '',
+ fieldLabel: 'Monitor(s)'
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'username',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}'
+ },
+ value: 'admin',
+ fieldLabel: gettext('User name'),
+ allowBlank: true
+ }
+ );
+
+ me.column2 = [
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images'],
+ multiSelect: true,
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'krbd',
+ uncheckedValue: 0,
+ fieldLabel: 'KRBD'
+ }
+ ];
+
+ me.columnB = [{
+ xtype: 'proxmoxcheckbox',
+ name: 'pveceph',
+ reference: 'pvecephRef',
+ bind : {
+ disabled: '{!pvecephPossible}',
+ value: '{pveceph}'
+ },
+ checked: true,
+ uncheckedValue: 0,
+ submitValue: false,
+ hidden: !me.isCreate,
+ boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool')
+ }];
+
+ me.callParent();
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.storage.ZFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ viewModel: {
+ parent: null,
+ data: {
+ isLIO: false,
+ isComstar: true,
+ hasWriteCacheOption: true
+ }
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'field[name=iscsiprovider]': {
+ change: 'changeISCSIProvider'
+ }
+ },
+ changeISCSIProvider: function(f, newVal, oldVal) {
+ var vm = this.getViewModel();
+ vm.set('isLIO', newVal === 'LIO');
+ vm.set('isComstar', newVal === 'comstar');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ }
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.content = 'images';
+ }
+
+ values.nowritecache = values.writecache ? 0 : 1;
+ delete values.writecache;
+
+ return me.callParent([values]);
+ },
+
+ setValues: function diff(values) {
+ values.writecache = values.nowritecache ? 0 : 1;
+ this.callParent([values]);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'portal',
+ value: '',
+ fieldLabel: gettext('Portal'),
+ allowBlank: false
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'pool',
+ value: '',
+ fieldLabel: gettext('Pool'),
+ allowBlank: false
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'blocksize',
+ value: '4k',
+ fieldLabel: gettext('Block Size'),
+ allowBlank: false
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'target',
+ value: '',
+ fieldLabel: gettext('Target'),
+ allowBlank: false
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'comstar_tg',
+ value: '',
+ fieldLabel: gettext('Target group'),
+ bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ allowBlank: true
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield',
+ name: 'iscsiprovider',
+ value: 'comstar',
+ fieldLabel: gettext('iSCSI Provider'),
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'sparse',
+ checked: false,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Thin provision')
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'writecache',
+ checked: true,
+ bind: me.isCreate ? { disabled: '{!hasWriteCacheOption}' } : { hidden: '{!hasWriteCacheOption}' },
+ uncheckedValue: 0,
+ fieldLabel: gettext('Write cache')
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'comstar_hg',
+ value: '',
+ bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ fieldLabel: gettext('Host group'),
+ allowBlank: true
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'lio_tpg',
+ value: '',
+ bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
+ allowBlank: false,
+ fieldLabel: gettext('Target portal group')
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.ZFSPoolSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveZFSPoolSelector',
+ valueField: 'pool',
+ displayField: 'pool',
+ queryMode: 'local',
+ editable: false,
+ listConfig: {
+ loadingText: gettext('Scanning...')
+ },
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: {}, // true,
+ fields: [ 'pool', 'size', 'free' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/zfs'
+ }
+ });
+
+ store.sort('pool', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.ZFSPoolInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_zfspool',
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [];
+
+ if (me.isCreate) {
+ me.column1.push(Ext.create('PVE.storage.ZFSPoolSelector', {
+ name: 'pool',
+ fieldLabel: gettext('ZFS Pool'),
+ allowBlank: false
+ }));
+ } else {
+ me.column1.push(Ext.createWidget('displayfield', {
+ name: 'pool',
+ value: '',
+ fieldLabel: gettext('ZFS Pool'),
+ allowBlank: false
+ }));
+ }
+
+ // value is an array,
+ // while before it was a string
+ /*jslint confusion: true*/
+ me.column1.push(
+ {xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images', 'rootdir'],
+ multiSelect: true,
+ allowBlank: false
+ });
+ /*jslint confusion: false*/
+ me.column2 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'sparse',
+ checked: false,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Thin provision')
+ },
+ {
+ xtype: 'textfield',
+ name: 'blocksize',
+ emptyText: '8k',
+ fieldLabel: gettext('Block Size'),
+ allowBlank: true
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.ha.StatusView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveHAStatusView'],
+
+ onlineHelp: 'chapter_ha_manager',
+
+ sortPriority: {
+ quorum: 1,
+ master: 2,
+ lrm: 3,
+ service: 4
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.rstore) {
+ throw "no rstore given";
+ }
+
+ Proxmox.Utils.monStoreErrors(me, me.rstore);
+
+ var store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: me.rstore,
+ sortAfterUpdate: true,
+ sorters: [{
+ sorterFn: function(rec1, rec2) {
+ var p1 = me.sortPriority[rec1.data.type];
+ var p2 = me.sortPriority[rec2.data.type];
+ return (p1 !== p2) ? ((p1 > p2) ? 1 : -1) : 0;
+ }
+ }],
+ filters: {
+ property: 'type',
+ value: 'service',
+ operator: '!='
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ stateful: false,
+ viewConfig: {
+ trackOver: false
+ },
+ columns: [
+ {
+ header: gettext('Type'),
+ width: 80,
+ dataIndex: 'type'
+ },
+ {
+ header: gettext('Status'),
+ width: 80,
+ flex: 1,
+ dataIndex: 'status'
+ }
+ ]
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+
+ }
+}, function() {
+
+ Ext.define('pve-ha-status', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'id', 'type', 'node', 'status', 'sid',
+ 'state', 'group', 'comment',
+ 'max_restart', 'max_relocate', 'type',
+ 'crm_state', 'request_state'
+ ],
+ idProperty: 'id'
+ });
+
+});
+Ext.define('PVE.ha.Status', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveHAStatus',
+
+ onlineHelp: 'chapter_ha_manager',
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.rstore = Ext.create('Proxmox.data.ObjectStore', {
+ interval: me.interval,
+ model: 'pve-ha-status',
+ storeid: 'pve-store-' + (++Ext.idSeed),
+ groupField: 'type',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/ha/status/current'
+ }
+ });
+
+ me.items = [{
+ xtype: 'pveHAStatusView',
+ title: gettext('Status'),
+ rstore: me.rstore,
+ border: 0,
+ collapsible: true,
+ padding: '0 0 20 0'
+ },{
+ xtype: 'pveHAResourcesView',
+ flex: 1,
+ collapsible: true,
+ title: gettext('Resources'),
+ border: 0,
+ rstore: me.rstore
+ }];
+
+ me.callParent();
+ me.on('activate', me.rstore.startUpdate);
+ }
+});
+Ext.define('PVE.ha.GroupSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveHAGroupSelector'],
+
+ value: [],
+ autoSelect: false,
+ valueField: 'group',
+ displayField: 'group',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Group'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'group'
+ },
+ {
+ header: gettext('Nodes'),
+ width: 100,
+ sortable: false,
+ dataIndex: 'nodes'
+ },
+ {
+ header: gettext('Comment'),
+ flex: 1,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode
+ }
+ ]
+ },
+ store: {
+ model: 'pve-ha-groups',
+ sorters: {
+ property: 'group',
+ order: 'DESC'
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+ me.callParent();
+ me.getStore().load();
+ }
+
+}, function() {
+
+ Ext.define('pve-ha-groups', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'group', 'type', 'digest', 'nodes', 'comment',
+ {
+ name : 'restricted',
+ type: 'boolean'
+ },
+ {
+ name : 'nofailback',
+ type: 'boolean'
+ }
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/ha/groups"
+ },
+ idProperty: 'group'
+ });
+});
+Ext.define('PVE.ha.VMResourceInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ onlineHelp: 'ha_manager_resource_config',
+ vmid: undefined,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (values.vmid) {
+ values.sid = values.vmid;
+ }
+ delete values.vmid;
+
+ PVE.Utils.delete_if_default(values, 'group', '', me.isCreate);
+ PVE.Utils.delete_if_default(values, 'max_restart', '1', me.isCreate);
+ PVE.Utils.delete_if_default(values, 'max_relocate', '1', me.isCreate);
+
+ return values;
+ },
+
+ initComponent : function() {
+ var me = this;
+ var MIN_QUORUM_VOTES = 3;
+
+ var disabledHint = Ext.createWidget({
+ xtype: 'displayfield', // won't get submitted by default
+ userCls: 'pve-hint',
+ value: 'Disabling the resource will stop the guest system. ' +
+ 'See the online help for details.',
+ hidden: true
+ });
+
+ var fewVotesHint = Ext.createWidget({
+ itemId: 'fewVotesHint',
+ xtype: 'displayfield',
+ userCls: 'pve-hint',
+ value: 'At least three quorum votes are recommended for reliable HA.',
+ hidden: true
+ });
+
+ Proxmox.Utils.API2Request({
+ url: '/cluster/config/nodes',
+ method: 'GET',
+ failure: function(response) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response) {
+ var nodes = response.result.data;
+ var votes = 0;
+ Ext.Array.forEach(nodes, function(node) {
+ var vote = parseInt(node.quorum_votes, 10); // parse as base 10
+ votes += vote || 0; // parseInt might return NaN, which is false
+ });
+
+ if (votes < MIN_QUORUM_VOTES) {
+ fewVotesHint.setVisible(true);
+ }
+ }
+ });
+
+ /*jslint confusion: true */
+ var vmidStore = (me.vmid) ? {} : {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ filters: [
+ {
+ property: 'type',
+ value: /lxc|qemu/
+ },
+ {
+ property: 'hastate',
+ value: /unmanaged/
+ }
+ ]
+ };
+
+ // value is a string above, but a number below
+ me.column1 = [
+ {
+ xtype: me.vmid ? 'displayfield' : 'vmComboSelector',
+ submitValue: me.isCreate,
+ name: 'vmid',
+ fieldLabel: (me.vmid && me.guestType === 'ct') ? 'CT' : 'VM',
+ value: me.vmid,
+ store: vmidStore,
+ validateExists: true
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'max_restart',
+ fieldLabel: gettext('Max. Restart'),
+ value: 1,
+ minValue: 0,
+ maxValue: 10,
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'max_relocate',
+ fieldLabel: gettext('Max. Relocate'),
+ value: 1,
+ minValue: 0,
+ maxValue: 10,
+ allowBlank: false
+ }
+ ];
+ /*jslint confusion: false */
+
+ me.column2 = [
+ {
+ xtype: 'pveHAGroupSelector',
+ name: 'group',
+ fieldLabel: gettext('Group')
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'state',
+ value: 'started',
+ fieldLabel: gettext('Request State'),
+ comboItems: [
+ ['started', 'started'],
+ ['stopped', 'stopped'],
+ ['ignored', 'ignored'],
+ ['disabled', 'disabled']
+ ],
+ listeners: {
+ 'change': function(field, newValue) {
+ if (newValue === 'disabled') {
+ disabledHint.setVisible(true);
+ }
+ else {
+ if (disabledHint.isVisible()) {
+ disabledHint.setVisible(false);
+ }
+ }
+ }
+ }
+ },
+ disabledHint
+ ];
+
+ me.columnB = [
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ fieldLabel: gettext('Comment')
+ },
+ fewVotesHint
+ ];
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.ha.VMResourceEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ vmid: undefined,
+ guestType: undefined,
+ isCreate: undefined,
+
+ initComponent : function() {
+ var me = this;
+
+ if (me.isCreate === undefined) {
+ me.isCreate = !me.vmid;
+ }
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs/cluster/ha/resources';
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs/cluster/ha/resources/' + me.vmid;
+ me.method = 'PUT';
+ }
+
+ var ipanel = Ext.create('PVE.ha.VMResourceInputPanel', {
+ isCreate: me.isCreate,
+ vmid: me.vmid,
+ guestType: me.guestType
+ });
+
+ Ext.apply(me, {
+ subject: gettext('Resource') + ': ' + gettext('Container') +
+ '/' + gettext('Virtual Machine'),
+ isAdd: true,
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+
+ var regex = /^(\S+):(\S+)$/;
+ var res = regex.exec(values.sid);
+
+ if (res[1] !== 'vm' && res[1] !== 'ct') {
+ throw "got unexpected resource type";
+ }
+
+ values.vmid = res[2];
+
+ ipanel.setValues(values);
+ }
+ });
+ }
+ }
+});
+Ext.define('PVE.ha.ResourcesView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveHAResourcesView'],
+
+ onlineHelp: 'ha_manager_resources',
+
+ stateful: true,
+ stateId: 'grid-ha-resources',
+
+ initComponent : function() {
+ var me = this;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ if (!me.rstore) {
+ throw "no store given";
+ }
+
+ Proxmox.Utils.monStoreErrors(me, me.rstore);
+
+ var store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: me.rstore,
+ filters: {
+ property: 'type',
+ value: 'service'
+ }
+ });
+
+ var reload = function() {
+ me.rstore.load();
+ };
+
+ var render_error = function(dataIndex, value, metaData, record) {
+ var errors = record.data.errors;
+ if (errors) {
+ var msg = errors[dataIndex];
+ if (msg) {
+ metaData.tdCls = 'proxmox-invalid-row';
+ var html = '' + gettext('Nodes') + '
',
+ '
',
+ '' + gettext("Virtual Machines") + '
',
+ '
',
+ '
',
+ '
',
+ '' + gettext("LXC Container") + '
',
+ '
',
+ '
',
+ '
',
+ 'No valid subscription
' + PVE.Utils.noSubKeyHtml,
+
+ communityHtml: 'Please use the public community forum for any questions.',
+
+ activeHtml: 'Please use our support portal for any questions. You can also use the public community forum to get additional information.',
+
+ bugzillaHtml: 'Bug Tracking
Our bug tracking system is available here.',
+
+ docuHtml: function() {
+ var me = this;
+ var guideUrl = window.location.origin + me.pveGuidePath;
+ var text = Ext.String.format('Documentation
'
+ + 'The official Proxmox VE Administration Guide'
+ + ' is included with this installation and can be browsed at '
+ + '{0}', guideUrl);
+ return text;
+ },
+
+ updateActive: function(data) {
+ var me = this;
+
+ var html = '' + data.productname + '
' + me.activeHtml;
+ html += '
' + me.docuHtml();
+ html += '
' + me.bugzillaHtml;
+
+ me.update(html);
+ },
+
+ updateCommunity: function(data) {
+ var me = this;
+
+ var html = '' + data.productname + '
' + me.communityHtml;
+ html += '
' + me.docuHtml();
+ html += '
' + me.bugzillaHtml;
+
+ me.update(html);
+ },
+
+ updateInactive: function(data) {
+ var me = this;
+ me.update(me.invalidHtml);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var reload = function() {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/localhost/subscription',
+ method: 'GET',
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ me.update('Unable to load subscription status' + ": " + response.htmlStatus);
+ },
+ success: function(response, opts) {
+ var data = response.result.data;
+
+ if (data.status === 'Active') {
+ if (data.level === 'c') {
+ me.updateCommunity(data);
+ } else {
+ me.updateActive(data);
+ }
+ } else {
+ me.updateInactive(data);
+ }
+ }
+ });
+ };
+
+ Ext.apply(me, {
+ autoScroll: true,
+ bodyStyle: 'padding:10px',
+ listeners: {
+ activate: reload
+ }
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('pve-security-groups', {
+ extend: 'Ext.data.Model',
+
+ fields: [ 'group', 'comment', 'digest' ],
+ idProperty: 'group'
+});
+
+Ext.define('PVE.SecurityGroupEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ base_url: "/cluster/firewall/groups",
+
+ allow_iface: false,
+
+ initComponent : function() {
+ var me = this;
+
+ me.isCreate = (me.group_name === undefined);
+
+ var subject;
+
+ me.url = '/api2/extjs' + me.base_url;
+ me.method = 'POST';
+
+ var items = [
+ {
+ xtype: 'textfield',
+ name: 'group',
+ value: me.group_name || '',
+ fieldLabel: gettext('Name'),
+ allowBlank: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ value: me.group_comment || '',
+ fieldLabel: gettext('Comment')
+ }
+ ];
+
+ if (me.isCreate) {
+ subject = gettext('Security Group');
+ } else {
+ subject = gettext('Security Group') + " '" + me.group_name + "'";
+ items.push({
+ xtype: 'hiddenfield',
+ name: 'rename',
+ value: me.group_name
+ });
+ }
+
+ var ipanel = Ext.create('Proxmox.panel.InputPanel', {
+ // InputPanel does not have a 'create' property, does it need a 'isCreate'
+ isCreate: me.isCreate,
+ items: items
+ });
+
+
+ Ext.apply(me, {
+ subject: subject,
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.SecurityGroupList', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveSecurityGroupList',
+
+ stateful: true,
+ stateId: 'grid-securitygroups',
+
+ rule_panel: undefined,
+
+ addBtn: undefined,
+ removeBtn: undefined,
+ editBtn: undefined,
+
+ base_url: "/cluster/firewall/groups",
+
+ initComponent: function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ if (me.rule_panel == undefined) {
+ throw "no rule panel specified";
+ }
+
+ if (me.base_url == undefined) {
+ throw "no base_url specified";
+ }
+
+ var store = new Ext.data.Store({
+ model: 'pve-security-groups',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json' + me.base_url
+ },
+ sorters: {
+ property: 'group',
+ order: 'DESC'
+ }
+ });
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var reload = function() {
+ var oldrec = sm.getSelection()[0];
+ store.load(function(records, operation, success) {
+ if (oldrec) {
+ var rec = store.findRecord('group', oldrec.data.group);
+ if (rec) {
+ sm.select(rec);
+ }
+ }
+ });
+ };
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ var win = Ext.create('PVE.SecurityGroupEdit', {
+ digest: rec.data.digest,
+ group_name: rec.data.group,
+ group_comment: rec.data.comment
+ });
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ me.editBtn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor
+ });
+
+ me.addBtn = new Proxmox.button.Button({
+ text: gettext('Create'),
+ handler: function() {
+ sm.deselectAll();
+ var win = Ext.create('PVE.SecurityGroupEdit', {});
+ win.show();
+ win.on('destroy', reload);
+ }
+ });
+
+ me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: me.base_url + '/',
+ enableFn: function(rec) {
+ return (rec && me.base_url);
+ },
+ callback: function() {
+ reload();
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ tbar: [ '' + gettext('Group') + ':', me.addBtn, me.removeBtn, me.editBtn ],
+ selModel: sm,
+ columns: [
+ { header: gettext('Group'), dataIndex: 'group', width: '100' },
+ { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 }
+ ],
+ listeners: {
+ itemdblclick: run_editor,
+ select: function(sm, rec) {
+ var url = '/cluster/firewall/groups/' + rec.data.group;
+ me.rule_panel.setBaseUrl(url);
+ },
+ deselect: function() {
+ me.rule_panel.setBaseUrl(undefined);
+ },
+ show: reload
+ }
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+});
+
+Ext.define('PVE.SecurityGroups', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveSecurityGroups',
+
+ title: 'Security Groups',
+
+ initComponent: function() {
+ var me = this;
+
+ var rule_panel = Ext.createWidget('pveFirewallRules', {
+ region: 'center',
+ allow_groups: false,
+ list_refs_url: '/cluster/firewall/refs',
+ tbar_prefix: '' + gettext('Rules') + ':',
+ border: false
+ });
+
+ var sglist = Ext.createWidget('pveSecurityGroupList', {
+ region: 'west',
+ rule_panel: rule_panel,
+ width: '25%',
+ border: false,
+ split: true
+ });
+
+
+ Ext.apply(me, {
+ layout: 'border',
+ items: [ sglist, rule_panel ],
+ listeners: {
+ show: function() {
+ sglist.fireEvent('show', sglist);
+ }
+ }
+ });
+
+ me.callParent();
+ }
+});
+/*
+ * Datacenter config panel, located in the center of the ViewPort after the Datacenter view is selected
+ */
+
+Ext.define('PVE.dc.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.dc.Config',
+
+ onlineHelp: 'pve_admin_guide',
+
+ initComponent: function() {
+ var me = this;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ me.items = [];
+
+ Ext.apply(me, {
+ title: gettext("Datacenter"),
+ hstateid: 'dctab'
+ });
+
+ if (caps.dc['Sys.Audit']) {
+ me.items.push({
+ title: gettext('Summary'),
+ xtype: 'pveDcSummary',
+ iconCls: 'fa fa-book',
+ itemId: 'summary'
+ },
+ {
+ title: gettext('Cluster'),
+ xtype: 'pveClusterAdministration',
+ iconCls: 'fa fa-server',
+ itemId: 'cluster'
+ },
+ {
+ title: 'Ceph',
+ itemId: 'ceph',
+ iconCls: 'fa fa-ceph',
+ xtype: 'pveNodeCephStatus'
+ },
+ {
+ xtype: 'pveDcOptionView',
+ title: gettext('Options'),
+ iconCls: 'fa fa-gear',
+ itemId: 'options'
+ });
+ }
+
+ if (caps.storage['Datastore.Allocate'] || caps.dc['Sys.Audit']) {
+ me.items.push({
+ xtype: 'pveStorageView',
+ title: gettext('Storage'),
+ iconCls: 'fa fa-database',
+ itemId: 'storage'
+ });
+ }
+
+ if (caps.dc['Sys.Audit']) {
+ me.items.push({
+ xtype: 'pveDcBackupView',
+ iconCls: 'fa fa-floppy-o',
+ title: gettext('Backup'),
+ itemId: 'backup'
+ },
+ {
+ xtype: 'pveReplicaView',
+ iconCls: 'fa fa-retweet',
+ title: gettext('Replication'),
+ itemId: 'replication'
+ },
+ {
+ xtype: 'pveACLView',
+ title: gettext('Permissions'),
+ iconCls: 'fa fa-unlock',
+ itemId: 'permissions',
+ expandedOnInit: true
+ });
+ }
+
+ me.items.push({
+ xtype: 'pveUserView',
+ groups: ['permissions'],
+ iconCls: 'fa fa-user',
+ title: gettext('Users'),
+ itemId: 'users'
+ });
+
+ if (caps.dc['Sys.Audit']) {
+ me.items.push({
+ xtype: 'pveGroupView',
+ title: gettext('Groups'),
+ iconCls: 'fa fa-users',
+ groups: ['permissions'],
+ itemId: 'groups'
+ },
+ {
+ xtype: 'pvePoolView',
+ title: gettext('Pools'),
+ iconCls: 'fa fa-tags',
+ groups: ['permissions'],
+ itemId: 'pools'
+ },
+ {
+ xtype: 'pveRoleView',
+ title: gettext('Roles'),
+ iconCls: 'fa fa-male',
+ groups: ['permissions'],
+ itemId: 'roles'
+ },
+ {
+ xtype: 'pveAuthView',
+ title: gettext('Authentication'),
+ groups: ['permissions'],
+ iconCls: 'fa fa-key',
+ itemId: 'domains'
+ },
+ {
+ xtype: 'pveHAStatus',
+ title: 'HA',
+ iconCls: 'fa fa-heartbeat',
+ itemId: 'ha'
+ },
+ {
+ title: gettext('Groups'),
+ groups: ['ha'],
+ xtype: 'pveHAGroupsView',
+ iconCls: 'fa fa-object-group',
+ itemId: 'ha-groups'
+ },
+ {
+ title: gettext('Fencing'),
+ groups: ['ha'],
+ iconCls: 'fa fa-bolt',
+ xtype: 'pveFencingView',
+ itemId: 'ha-fencing'
+ },
+ {
+ xtype: 'pveFirewallRules',
+ title: gettext('Firewall'),
+ allow_iface: true,
+ base_url: '/cluster/firewall/rules',
+ list_refs_url: '/cluster/firewall/refs',
+ iconCls: 'fa fa-shield',
+ itemId: 'firewall'
+ },
+ {
+ xtype: 'pveFirewallOptions',
+ title: gettext('Options'),
+ groups: ['firewall'],
+ iconCls: 'fa fa-gear',
+ base_url: '/cluster/firewall/options',
+ onlineHelp: 'pve_firewall_cluster_wide_setup',
+ fwtype: 'dc',
+ itemId: 'firewall-options'
+ },
+ {
+ xtype: 'pveSecurityGroups',
+ title: gettext('Security Group'),
+ groups: ['firewall'],
+ iconCls: 'fa fa-group',
+ itemId: 'firewall-sg'
+ },
+ {
+ xtype: 'pveFirewallAliases',
+ title: gettext('Alias'),
+ groups: ['firewall'],
+ iconCls: 'fa fa-external-link',
+ base_url: '/cluster/firewall/aliases',
+ itemId: 'firewall-aliases'
+ },
+ {
+ xtype: 'pveIPSet',
+ title: 'IPSet',
+ groups: ['firewall'],
+ iconCls: 'fa fa-list-ol',
+ base_url: '/cluster/firewall/ipset',
+ list_refs_url: '/cluster/firewall/refs',
+ itemId: 'firewall-ipset'
+ },
+ {
+ xtype: 'pveDcSupport',
+ title: gettext('Support'),
+ itemId: 'support',
+ iconCls: 'fa fa-comments-o'
+ });
+ }
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.dc.NodeView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveDcNodeView',
+
+ title: gettext('Nodes'),
+ disableSelection: true,
+ scrollable: true,
+
+ columns: [
+ {
+ header: gettext('Name'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'name'
+ },
+ {
+ header: 'ID',
+ width: 40,
+ sortable: true,
+ dataIndex: 'nodeid'
+ },
+ {
+ header: gettext('Online'),
+ width: 60,
+ sortable: true,
+ dataIndex: 'online',
+ renderer: function(value) {
+ var cls = (value)?'good':'critical';
+ return '';
+ }
+ },
+ {
+ header: gettext('Support'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'level',
+ renderer: PVE.Utils.render_support_level
+ },
+ {
+ header: gettext('Server Address'),
+ width: 115,
+ sortable: true,
+ dataIndex: 'ip'
+ },
+ {
+ header: gettext('CPU usage'),
+ sortable: true,
+ width: 110,
+ dataIndex: 'cpuusage',
+ tdCls: 'x-progressbar-default-cell',
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'pveProgressBar'
+ }
+ },
+ {
+ header: gettext('Memory usage'),
+ width: 110,
+ sortable: true,
+ tdCls: 'x-progressbar-default-cell',
+ dataIndex: 'memoryusage',
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'pveProgressBar'
+ }
+ },
+ {
+ header: gettext('Uptime'),
+ sortable: true,
+ dataIndex: 'uptime',
+ align: 'right',
+ renderer: Proxmox.Utils.render_uptime
+ }
+ ],
+
+ stateful: true,
+ stateId: 'grid-cluster-nodes',
+ tools: [
+ {
+ type: 'up',
+ handler: function(){
+ var me = this.up('grid');
+ var height = Math.max(me.getHeight()-50, 250);
+ me.setHeight(height);
+ }
+ },
+ {
+ type: 'down',
+ handler: function(){
+ var me = this.up('grid');
+ var height = me.getHeight()+50;
+ me.setHeight(height);
+ }
+ }
+ ]
+}, function() {
+
+ Ext.define('pve-dc-nodes', {
+ extend: 'Ext.data.Model',
+ fields: [ 'id', 'type', 'name', 'nodeid', 'ip', 'level', 'local', 'online'],
+ idProperty: 'id'
+ });
+
+});
+
+Ext.define('PVE.widget.ProgressBar',{
+ extend: 'Ext.Progress',
+ alias: 'widget.pveProgressBar',
+
+ animate: true,
+ textTpl: [
+ '{percent}%'
+ ],
+
+ setValue: function(value){
+ var me = this;
+ me.callParent([value]);
+
+ me.removeCls(['warning', 'critical']);
+
+ if (value > 0.89) {
+ me.addCls('critical');
+ } else if (value > 0.59) {
+ me.addCls('warning');
+ }
+ }
+});
+/*jslint confusion: true*/
+Ext.define('pve-cluster-nodes', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'node', { type: 'integer', name: 'nodeid' }, 'ring0_addr', 'ring1_addr',
+ { type: 'integer', name: 'quorum_votes' }
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/config/nodes"
+ },
+ idProperty: 'nodeid'
+});
+
+Ext.define('pve-cluster-info', {
+ extend: 'Ext.data.Model',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/config/join"
+ }
+});
+
+Ext.define('PVE.ClusterAdministration', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pveClusterAdministration',
+
+ title: gettext('Cluster Administration'),
+ onlineHelp: 'chapter_pvecm',
+
+ border: false,
+ defaults: { border: false },
+
+ viewModel: {
+ parent: null,
+ data: {
+ totem: {},
+ nodelist: [],
+ preferred_node: {
+ name: '',
+ fp: '',
+ addr: ''
+ },
+ isInCluster: false,
+ nodecount: 0
+ }
+ },
+
+ items: [
+ {
+ xtype: 'panel',
+ title: gettext('Cluster Information'),
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ view.store = Ext.create('Proxmox.data.UpdateStore', {
+ autoStart: true,
+ interval: 15 * 1000,
+ storeid: 'pve-cluster-info',
+ model: 'pve-cluster-info'
+ });
+ view.store.on('load', this.onLoad, this);
+ view.on('destroy', view.store.stopUpdate);
+ },
+
+ onLoad: function(store, records, success) {
+ var vm = this.getViewModel();
+ if (!success || !records || !records[0].data) {
+ vm.set('totem', {});
+ vm.set('isInCluster', false);
+ vm.set('nodelist', []);
+ vm.set('preferred_node', {
+ name: '',
+ addr: '',
+ fp: ''
+ });
+ return;
+ }
+ var data = records[0].data;
+ vm.set('totem', data.totem);
+ vm.set('isInCluster', !!data.totem.cluster_name);
+ vm.set('nodelist', data.nodelist);
+
+ var nodeinfo = Ext.Array.findBy(data.nodelist, function (el) {
+ return el.name === data.preferred_node;
+ });
+
+ vm.set('preferred_node', {
+ name: data.preferred_node,
+ addr: nodeinfo.pve_addr,
+ ring_addr: [ nodeinfo.ring0_addr, nodeinfo.ring1_addr ],
+ fp: nodeinfo.pve_fp
+ });
+ },
+
+ onCreate: function() {
+ var view = this.getView();
+ view.store.stopUpdate();
+ var win = Ext.create('PVE.ClusterCreateWindow', {
+ autoShow: true,
+ listeners: {
+ destroy: function() {
+ view.store.startUpdate();
+ }
+ }
+ });
+ },
+
+ onClusterInfo: function() {
+ var vm = this.getViewModel();
+ var win = Ext.create('PVE.ClusterInfoWindow', {
+ joinInfo: {
+ ipAddress: vm.get('preferred_node.addr'),
+ fingerprint: vm.get('preferred_node.fp'),
+ ring_addr: vm.get('preferred_node.ring_addr'),
+ totem: vm.get('totem')
+ }
+ });
+ win.show();
+ },
+
+ onJoin: function() {
+ var view = this.getView();
+ view.store.stopUpdate();
+ var win = Ext.create('PVE.ClusterJoinNodeWindow', {
+ autoShow: true,
+ listeners: {
+ destroy: function() {
+ view.store.startUpdate();
+ }
+ }
+ });
+ }
+ },
+ tbar: [
+ {
+ text: gettext('Create Cluster'),
+ reference: 'createButton',
+ handler: 'onCreate',
+ bind: {
+ disabled: '{isInCluster}'
+ }
+ },
+ {
+ text: gettext('Join Information'),
+ reference: 'addButton',
+ handler: 'onClusterInfo',
+ bind: {
+ disabled: '{!isInCluster}'
+ }
+ },
+ {
+ text: gettext('Join Cluster'),
+ reference: 'joinButton',
+ handler: 'onJoin',
+ bind: {
+ disabled: '{isInCluster}'
+ }
+ }
+ ],
+ layout: 'hbox',
+ bodyPadding: 5,
+ items: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Cluster Name'),
+ bind: {
+ value: '{totem.cluster_name}',
+ hidden: '{!isInCluster}'
+ },
+ flex: 1
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Config Version'),
+ bind: {
+ value: '{totem.config_version}',
+ hidden: '{!isInCluster}'
+ },
+ flex: 1
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Number of Nodes'),
+ labelWidth: 120,
+ bind: {
+ value: '{nodecount}',
+ hidden: '{!isInCluster}'
+ },
+ flex: 1
+ },
+ {
+ xtype: 'displayfield',
+ value: gettext('Standalone node - no cluster defined'),
+ bind: {
+ hidden: '{isInCluster}'
+ },
+ flex: 1
+ }
+ ]
+ },
+ {
+ xtype: 'grid',
+ title: gettext('Cluster Nodes'),
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ view.rstore = Ext.create('Proxmox.data.UpdateStore', {
+ autoLoad: true,
+ xtype: 'update',
+ interval: 5 * 1000,
+ autoStart: true,
+ storeid: 'pve-cluster-nodes',
+ model: 'pve-cluster-nodes'
+ });
+ view.setStore(Ext.create('Proxmox.data.DiffStore', {
+ rstore: view.rstore,
+ sorters: {
+ property: 'nodeid',
+ order: 'DESC'
+ }
+ }));
+ Proxmox.Utils.monStoreErrors(view, view.rstore);
+ view.rstore.on('load', this.onLoad, this);
+ view.on('destroy', view.rstore.stopUpdate);
+ },
+
+ onLoad: function(store, records, success) {
+ var vm = this.getViewModel();
+ if (!success || !records) {
+ vm.set('nodecount', 0);
+ return;
+ }
+ vm.set('nodecount', records.length);
+ }
+ },
+ columns: [
+ {
+ header: gettext('Nodename'),
+ flex: 2,
+ dataIndex: 'name'
+ },
+ {
+ header: gettext('ID'),
+ flex: 1,
+ dataIndex: 'nodeid'
+ },
+ {
+ header: gettext('Votes'),
+ flex: 1,
+ dataIndex: 'quorum_votes'
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 0),
+ flex: 2,
+ dataIndex: 'ring0_addr'
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 1),
+ flex: 2,
+ dataIndex: 'ring1_addr'
+ }
+ ]
+ }
+ ]
+});
+/*jslint confusion: true*/
+Ext.define('PVE.ClusterCreateWindow', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveClusterCreateWindow',
+
+ title: gettext('Create Cluster'),
+ width: 600,
+
+ method: 'POST',
+ url: '/cluster/config',
+
+ isCreate: true,
+ subject: gettext('Cluster'),
+ showTaskViewer: true,
+
+ onlineHelp: 'pvecm_create_cluster',
+
+ items: {
+ xtype: 'inputpanel',
+ items: [{
+ xtype: 'textfield',
+ fieldLabel: gettext('Cluster Name'),
+ allowBlank: false,
+ name: 'clustername'
+ },
+ {
+ xtype: 'proxmoxNetworkSelector',
+ fieldLabel: Ext.String.format(gettext('Link {0}'), 0),
+ emptyText: gettext("Optional, defaults to IP resolved by node's hostname"),
+ name: 'link0',
+ autoSelect: false,
+ valueField: 'address',
+ displayField: 'address',
+ skipEmptyText: true
+ }],
+ advancedItems: [{
+ xtype: 'proxmoxNetworkSelector',
+ fieldLabel: Ext.String.format(gettext('Link {0}'), 1),
+ emptyText: gettext("Optional second link for redundancy"),
+ name: 'link1',
+ autoSelect: false,
+ valueField: 'address',
+ displayField: 'address',
+ skipEmptyText: true
+ }]
+ }
+});
+
+Ext.define('PVE.ClusterInfoWindow', {
+ extend: 'Ext.window.Window',
+ xtype: 'pveClusterInfoWindow',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ width: 800,
+ modal: true,
+ resizable: false,
+ title: gettext('Cluster Join Information'),
+
+ joinInfo: {
+ ipAddress: undefined,
+ fingerprint: undefined,
+ totem: {}
+ },
+
+ items: [
+ {
+ xtype: 'component',
+ border: false,
+ padding: '10 10 10 10',
+ html: gettext("Copy the Join Information here and use it on the node you want to add.")
+ },
+ {
+ xtype: 'container',
+ layout: 'form',
+ border: false,
+ padding: '0 10 10 10',
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('IP Address'),
+ cbind: { value: '{joinInfo.ipAddress}' },
+ editable: false
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Fingerprint'),
+ cbind: { value: '{joinInfo.fingerprint}' },
+ editable: false
+ },
+ {
+ xtype: 'textarea',
+ inputId: 'pveSerializedClusterInfo',
+ fieldLabel: gettext('Join Information'),
+ grow: true,
+ cbind: { joinInfo: '{joinInfo}' },
+ editable: false,
+ listeners: {
+ afterrender: function(field) {
+ if (!field.joinInfo) {
+ return;
+ }
+ var jsons = Ext.JSON.encode(field.joinInfo);
+ var base64s = Ext.util.Base64.encode(jsons);
+ field.setValue(base64s);
+ }
+ }
+ }
+ ]
+ }
+ ],
+ dockedItems: [{
+ dock: 'bottom',
+ xtype: 'toolbar',
+ items: [{
+ xtype: 'button',
+ handler: function(b) {
+ var el = document.getElementById('pveSerializedClusterInfo');
+ el.select();
+ document.execCommand("copy");
+ },
+ text: gettext('Copy Information')
+ }]
+ }]
+});
+
+Ext.define('PVE.ClusterJoinNodeWindow', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveClusterJoinNodeWindow',
+
+ title: gettext('Cluster Join'),
+ width: 800,
+
+ method: 'POST',
+ url: '/cluster/config/join',
+
+ defaultFocus: 'textarea[name=serializedinfo]',
+ isCreate: true,
+ submitText: gettext('Join'),
+ showTaskViewer: true,
+
+ onlineHelp: 'chapter_pvecm',
+
+ viewModel: {
+ parent: null,
+ data: {
+ info: {
+ fp: '',
+ ip: '',
+ ring0Needed: false,
+ ring1Possible: false,
+ ring1Needed: false
+ }
+ },
+ formulas: {
+ ring0EmptyText: function(get) {
+ if (get('info.ring0Needed')) {
+ return gettext("Cannot use default address safely");
+ } else {
+ return gettext("Default: IP resolved by node's hostname");
+ }
+ }
+ }
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ '#': {
+ close: function() {
+ delete PVE.Utils.silenceAuthFailures;
+ }
+ },
+ 'proxmoxcheckbox[name=assistedEntry]': {
+ change: 'onInputTypeChange'
+ },
+ 'textarea[name=serializedinfo]': {
+ change: 'recomputeSerializedInfo',
+ enable: 'resetField'
+ },
+ 'proxmoxtextfield[name=ring1_addr]': {
+ enable: 'ring1Needed'
+ },
+ 'textfield': {
+ disable: 'resetField'
+ }
+ },
+ resetField: function(field) {
+ field.reset();
+ },
+ ring1Needed: function(f) {
+ var vm = this.getViewModel();
+ f.allowBlank = !vm.get('info.ring1Needed');
+ },
+ onInputTypeChange: function(field, assistedInput) {
+ var vm = this.getViewModel();
+ if (!assistedInput) {
+ vm.set('info.ring1Possible', true);
+ }
+ },
+ recomputeSerializedInfo: function(field, value) {
+ var vm = this.getViewModel();
+ var jsons = Ext.util.Base64.decode(value);
+ var joinInfo = Ext.JSON.decode(jsons, true);
+
+ var info = {
+ fp: '',
+ ring1Needed: false,
+ ring1Possible: false,
+ ip: ''
+ };
+
+ var totem = {};
+ if (!(joinInfo && joinInfo.totem)) {
+ field.valid = false;
+ } else {
+ var ring0Needed = false;
+ if (joinInfo.ring_addr !== undefined) {
+ ring0Needed = joinInfo.ring_addr[0] !== joinInfo.ipAddress;
+ }
+
+ info = {
+ ip: joinInfo.ipAddress,
+ fp: joinInfo.fingerprint,
+ ring0Needed: ring0Needed,
+ ring1Possible: !!joinInfo.totem['interface']['1'],
+ ring1Needed: !!joinInfo.totem['interface']['1']
+ };
+ totem = joinInfo.totem;
+ field.valid = true;
+ }
+
+ vm.set('info', info);
+ }
+ },
+
+ submit: function() {
+ // joining may produce temporarily auth failures, ignore as long the task runs
+ PVE.Utils.silenceAuthFailures = true;
+ this.callParent();
+ },
+
+ taskDone: function(success) {
+ delete PVE.Utils.silenceAuthFailures;
+ if (success) {
+ var txt = gettext('Cluster join task finished, node certificate may have changed, reload GUI!');
+ // ensure user cannot do harm
+ Ext.getBody().mask(txt, ['pve-static-mask']);
+ // TaskView may hide above mask, so tell him directly
+ Ext.Msg.show({
+ title: gettext('Join Task Finished'),
+ icon: Ext.Msg.INFO,
+ msg: txt
+ });
+ // reload always (if user wasn't faster), but wait a bit for pveproxy
+ Ext.defer(function() {
+ window.location.reload(true);
+ }, 5000);
+ }
+ },
+
+ items: [{
+ xtype: 'proxmoxcheckbox',
+ reference: 'assistedEntry',
+ name: 'assistedEntry',
+ submitValue: false,
+ value: true,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Select if join information should be extracted from pasted cluster information, deselect for manual entering')
+ },
+ boxLabel: gettext('Assisted join: Paste encoded cluster join information and enter password.')
+ },
+ {
+ xtype: 'textarea',
+ name: 'serializedinfo',
+ submitValue: false,
+ allowBlank: false,
+ fieldLabel: gettext('Information'),
+ emptyText: gettext('Paste encoded Cluster Information here'),
+ validator: function(val) {
+ return val === '' || this.valid ||
+ gettext('Does not seem like a valid encoded Cluster Information!');
+ },
+ bind: {
+ disabled: '{!assistedEntry.checked}',
+ hidden: '{!assistedEntry.checked}'
+ },
+ value: ''
+ },
+ {
+ xtype: 'inputpanel',
+ column1: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Peer Address'),
+ allowBlank: false,
+ bind: {
+ value: '{info.ip}',
+ readOnly: '{assistedEntry.checked}'
+ },
+ name: 'hostname'
+ },
+ {
+ xtype: 'textfield',
+ inputType: 'password',
+ emptyText: gettext("Peer's root password"),
+ fieldLabel: gettext('Password'),
+ allowBlank: false,
+ name: 'password'
+ }
+ ],
+ column2: [
+ {
+ xtype: 'proxmoxNetworkSelector',
+ fieldLabel: Ext.String.format(gettext('Link {0}'), 0),
+ bind: {
+ emptyText: '{ring0EmptyText}',
+ allowBlank: '{!info.ring0Needed}'
+ },
+ skipEmptyText: true,
+ autoSelect: false,
+ valueField: 'address',
+ displayField: 'address',
+ name: 'link0'
+ },
+ {
+ xtype: 'proxmoxNetworkSelector',
+ fieldLabel: Ext.String.format(gettext('Link {0}'), 1),
+ skipEmptyText: true,
+ autoSelect: false,
+ valueField: 'address',
+ displayField: 'address',
+ bind: {
+ disabled: '{!info.ring1Possible}',
+ allowBlank: '{!info.ring1Needed}',
+ },
+ name: 'link1'
+ }
+ ],
+ columnB: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Fingerprint'),
+ allowBlank: false,
+ bind: {
+ value: '{info.fp}',
+ readOnly: '{assistedEntry.checked}'
+ },
+ name: 'fingerprint'
+ }
+ ]
+ }]
+});
+/*
+ * Workspace base class
+ *
+ * popup login window when auth fails (call onLogin handler)
+ * update (re-login) ticket every 15 minutes
+ *
+ */
+
+Ext.define('PVE.Workspace', {
+ extend: 'Ext.container.Viewport',
+
+ title: 'Proxmox Virtual Environment',
+
+ loginData: null, // Data from last login call
+
+ onLogin: function(loginData) {},
+
+ // private
+ updateLoginData: function(loginData) {
+ var me = this;
+ me.loginData = loginData;
+ Proxmox.Utils.setAuthData(loginData);
+
+ var rt = me.down('pveResourceTree');
+ rt.setDatacenterText(loginData.clustername);
+
+ if (loginData.cap) {
+ Ext.state.Manager.set('GuiCap', loginData.cap);
+ }
+ me.response401count = 0;
+
+ me.onLogin(loginData);
+ },
+
+ // private
+ showLogin: function() {
+ var me = this;
+
+ Proxmox.Utils.authClear();
+ Proxmox.UserName = null;
+ me.loginData = null;
+
+ if (!me.login) {
+ me.login = Ext.create('PVE.window.LoginWindow', {
+ handler: function(data) {
+ me.login = null;
+ me.updateLoginData(data);
+ Proxmox.Utils.checked_command(function() {}); // display subscription status
+ }
+ });
+ }
+ me.onLogin(null);
+ me.login.show();
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ Ext.tip.QuickTipManager.init();
+
+ // fixme: what about other errors
+ Ext.Ajax.on('requestexception', function(conn, response, options) {
+ if (response.status == 401 && !PVE.Utils.silenceAuthFailures) { // auth failure
+ // don't immediately show as logged out to cope better with some big
+ // upgrades, which may temporarily produce a false positive 401 err
+ me.response401count++;
+ if (me.response401count > 5) {
+ me.showLogin();
+ }
+ }
+ });
+
+ me.callParent();
+
+ if (!Proxmox.Utils.authOK()) {
+ me.showLogin();
+ } else {
+ if (me.loginData) {
+ me.onLogin(me.loginData);
+ }
+ }
+
+ Ext.TaskManager.start({
+ run: function() {
+ var ticket = Proxmox.Utils.authOK();
+ if (!ticket || !Proxmox.UserName) {
+ return;
+ }
+
+ Ext.Ajax.request({
+ params: {
+ username: Proxmox.UserName,
+ password: ticket
+ },
+ url: '/api2/json/access/ticket',
+ method: 'POST',
+ success: function(response, opts) {
+ var obj = Ext.decode(response.responseText);
+ me.updateLoginData(obj.data);
+ }
+ });
+ },
+ interval: 15*60*1000
+ });
+
+ }
+});
+
+Ext.define('PVE.StdWorkspace', {
+ extend: 'PVE.Workspace',
+
+ alias: ['widget.pveStdWorkspace'],
+
+ // private
+ setContent: function(comp) {
+ var me = this;
+
+ var cont = me.child('#content');
+
+ var lay = cont.getLayout();
+
+ var cur = lay.getActiveItem();
+
+ if (comp) {
+ Proxmox.Utils.setErrorMask(cont, false);
+ comp.border = false;
+ cont.add(comp);
+ if (cur !== null && lay.getNext()) {
+ lay.next();
+ var task = Ext.create('Ext.util.DelayedTask', function(){
+ cont.remove(cur);
+ });
+ task.delay(10);
+ }
+ }
+ else {
+ // helper for cleaning the content when logging out
+ cont.removeAll();
+ }
+ },
+
+ selectById: function(nodeid) {
+ var me = this;
+ var tree = me.down('pveResourceTree');
+ tree.selectById(nodeid);
+ },
+
+ onLogin: function(loginData) {
+ var me = this;
+
+ me.updateUserInfo();
+
+ if (loginData) {
+ PVE.data.ResourceStore.startUpdate();
+
+ Proxmox.Utils.API2Request({
+ url: '/version',
+ method: 'GET',
+ success: function(response) {
+ PVE.VersionInfo = response.result.data;
+ me.updateVersionInfo();
+ }
+ });
+ }
+ },
+
+ updateUserInfo: function() {
+ var me = this;
+ var ui = me.query('#userinfo')[0];
+ ui.setText(Proxmox.UserName || '');
+ ui.updateLayout();
+ },
+
+ updateVersionInfo: function() {
+ var me = this;
+
+ var ui = me.query('#versioninfo')[0];
+
+ if (PVE.VersionInfo) {
+ var version = PVE.VersionInfo.version;
+ ui.update('Virtual Environment ' + version);
+ } else {
+ ui.update('Virtual Environment');
+ }
+ ui.updateLayout();
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ Ext.History.init();
+
+ var sprovider = Ext.create('PVE.StateProvider');
+ Ext.state.Manager.setProvider(sprovider);
+
+ var selview = Ext.create('PVE.form.ViewSelector');
+
+ var rtree = Ext.createWidget('pveResourceTree', {
+ viewFilter: selview.getViewFilter(),
+ flex: 1,
+ selModel: {
+ selType: 'treemodel',
+ listeners: {
+ selectionchange: function(sm, selected) {
+ if (selected.length > 0) {
+ var n = selected[0];
+ var tlckup = {
+ root: 'PVE.dc.Config',
+ node: 'PVE.node.Config',
+ qemu: 'PVE.qemu.Config',
+ lxc: 'PVE.lxc.Config',
+ storage: 'PVE.storage.Browser',
+ pool: 'pvePoolConfig'
+ };
+ var comp = {
+ xtype: tlckup[n.data.type || 'root'] ||
+ 'pvePanelConfig',
+ showSearch: (n.data.id === 'root') ||
+ Ext.isDefined(n.data.groupbyid),
+ pveSelNode: n,
+ workspace: me,
+ viewFilter: selview.getViewFilter()
+ };
+ PVE.curSelectedNode = n;
+ me.setContent(comp);
+ }
+ }
+ }
+ }
+ });
+
+ selview.on('select', function(combo, records) {
+ if (records) {
+ var view = combo.getViewFilter();
+ rtree.setViewFilter(view);
+ }
+ });
+
+ var caps = sprovider.get('GuiCap');
+
+ var createVM = Ext.createWidget('button', {
+ pack: 'end',
+ margin: '3 5 0 0',
+ baseCls: 'x-btn',
+ iconCls: 'fa fa-desktop',
+ text: gettext("Create VM"),
+ disabled: !caps.vms['VM.Allocate'],
+ handler: function() {
+ var wiz = Ext.create('PVE.qemu.CreateWizard', {});
+ wiz.show();
+ }
+ });
+
+ var createCT = Ext.createWidget('button', {
+ pack: 'end',
+ margin: '3 5 0 0',
+ baseCls: 'x-btn',
+ iconCls: 'fa fa-cube',
+ text: gettext("Create CT"),
+ disabled: !caps.vms['VM.Allocate'],
+ handler: function() {
+ var wiz = Ext.create('PVE.lxc.CreateWizard', {});
+ wiz.show();
+ }
+ });
+
+ sprovider.on('statechange', function(sp, key, value) {
+ if (key === 'GuiCap' && value) {
+ caps = value;
+ createVM.setDisabled(!caps.vms['VM.Allocate']);
+ createCT.setDisabled(!caps.vms['VM.Allocate']);
+ }
+ });
+
+ Ext.apply(me, {
+ layout: { type: 'border' },
+ border: false,
+ items: [
+ {
+ region: 'north',
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ baseCls: 'x-plain',
+ defaults: {
+ baseCls: 'x-plain'
+ },
+ border: false,
+ margin: '2 0 2 5',
+ items: [
+ {
+ html: '' +
+ ''
+ },
+ {
+ minWidth: 150,
+ id: 'versioninfo',
+ html: 'Virtual Environment'
+ },
+ {
+ xtype: 'pveGlobalSearchField',
+ tree: rtree
+ },
+ {
+ flex: 1
+ },
+ {
+ xtype: 'proxmoxHelpButton',
+ hidden: false,
+ baseCls: 'x-btn',
+ iconCls: 'fa fa-book x-btn-icon-el-default-toolbar-small ',
+ listenToGlobalEvent: false,
+ onlineHelp: 'pve_documentation_index',
+ text: gettext('Documentation'),
+ margin: '0 5 0 0'
+ },
+ createVM,
+ createCT,
+ {
+ pack: 'end',
+ margin: '0 5 0 0',
+ id: 'userinfo',
+ xtype: 'button',
+ baseCls: 'x-btn',
+ style: {
+ // proxmox dark grey p light grey as border
+ backgroundColor: '#464d4d',
+ borderColor: '#ABBABA'
+ },
+ iconCls: 'fa fa-user',
+ menu: [
+ {
+ iconCls: 'fa fa-gear',
+ text: gettext('My Settings'),
+ handler: function() {
+ var win = Ext.create('PVE.window.Settings');
+ win.show();
+ }
+ },
+ {
+ text: gettext('Password'),
+ iconCls: 'fa fa-fw fa-key',
+ handler: function() {
+ var win = Ext.create('Proxmox.window.PasswordEdit', {
+ userid: Proxmox.UserName
+ });
+ win.show();
+ }
+ },
+ {
+ text: 'TFA',
+ iconCls: 'fa fa-fw fa-lock',
+ handler: function(btn, event, rec) {
+ var win = Ext.create('PVE.window.TFAEdit',{
+ userid: Proxmox.UserName
+ });
+ win.show();
+ }
+ },
+ '-',
+ {
+ iconCls: 'fa fa-fw fa-sign-out',
+ text: gettext("Logout"),
+ handler: function() {
+ PVE.data.ResourceStore.loadData([], false);
+ me.showLogin();
+ me.setContent(null);
+ var rt = me.down('pveResourceTree');
+ rt.setDatacenterText(undefined);
+ rt.clearTree();
+
+ // empty the stores of the StatusPanel child items
+ var statusPanels = Ext.ComponentQuery.query('pveStatusPanel grid');
+ Ext.Array.forEach(statusPanels, function(comp) {
+ if (comp.getStore()) {
+ comp.getStore().loadData([], false);
+ }
+ });
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ region: 'center',
+ stateful: true,
+ stateId: 'pvecenter',
+ minWidth: 100,
+ minHeight: 100,
+ id: 'content',
+ xtype: 'container',
+ layout: { type: 'card' },
+ border: false,
+ margin: '0 5 0 0',
+ items: []
+ },
+ {
+ region: 'west',
+ stateful: true,
+ stateId: 'pvewest',
+ itemId: 'west',
+ xtype: 'container',
+ border: false,
+ layout: { type: 'vbox', align: 'stretch' },
+ margin: '0 0 0 5',
+ split: true,
+ width: 200,
+ items: [ selview, rtree ],
+ listeners: {
+ resize: function(panel, width, height) {
+ var viewWidth = me.getSize().width;
+ if (width > viewWidth - 100) {
+ panel.setWidth(viewWidth - 100);
+ }
+ }
+ }
+ },
+ {
+ xtype: 'pveStatusPanel',
+ stateful: true,
+ stateId: 'pvesouth',
+ itemId: 'south',
+ region: 'south',
+ margin:'0 5 5 5',
+ title: gettext('Logs'),
+ collapsible: true,
+ header: false,
+ height: 200,
+ split:true,
+ listeners: {
+ resize: function(panel, width, height) {
+ var viewHeight = me.getSize().height;
+ if (height > (viewHeight - 150)) {
+ panel.setHeight(viewHeight - 150);
+ }
+ }
+ }
+ }
+ ]
+ });
+
+ me.callParent();
+
+ me.updateUserInfo();
+
+ // on resize, center all modal windows
+ Ext.on('resize', function(){
+ var wins = Ext.ComponentQuery.query('window[modal]');
+ if (wins.length > 0) {
+ wins.forEach(function(win){
+ win.alignTo(me, 'c-c');
+ });
+ }
+ });
+ }
+});
+
diff --git a/serverside/jsmod/6.0-4/pvemanagerlib.js.original b/serverside/jsmod/6.0-4/pvemanagerlib.js.original
new file mode 100644
index 0000000..0ba8b5c
--- /dev/null
+++ b/serverside/jsmod/6.0-4/pvemanagerlib.js.original
@@ -0,0 +1,39779 @@
+var pveOnlineHelpInfo = {
+ "ceph_rados_block_devices" : {
+ "link" : "/pve-docs/chapter-pvesm.html#ceph_rados_block_devices",
+ "title" : "Ceph RADOS Block Devices (RBD)"
+ },
+ "chapter_ha_manager" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#chapter_ha_manager",
+ "title" : "High Availability"
+ },
+ "chapter_lvm" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#chapter_lvm",
+ "title" : "Logical Volume Manager (LVM)"
+ },
+ "chapter_pct" : {
+ "link" : "/pve-docs/chapter-pct.html#chapter_pct",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "chapter_pve_firewall" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#chapter_pve_firewall",
+ "title" : "Proxmox VE Firewall"
+ },
+ "chapter_pveceph" : {
+ "link" : "/pve-docs/chapter-pveceph.html#chapter_pveceph",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "chapter_pvecm" : {
+ "link" : "/pve-docs/chapter-pvecm.html#chapter_pvecm",
+ "title" : "Cluster Manager"
+ },
+ "chapter_pvesr" : {
+ "link" : "/pve-docs/chapter-pvesr.html#chapter_pvesr",
+ "title" : "Storage Replication"
+ },
+ "chapter_storage" : {
+ "link" : "/pve-docs/chapter-pvesm.html#chapter_storage",
+ "title" : "Proxmox VE Storage"
+ },
+ "chapter_system_administration" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#chapter_system_administration",
+ "title" : "Host System Administration"
+ },
+ "chapter_user_management" : {
+ "link" : "/pve-docs/chapter-pveum.html#chapter_user_management",
+ "title" : "User Management"
+ },
+ "chapter_virtual_machines" : {
+ "link" : "/pve-docs/chapter-qm.html#chapter_virtual_machines",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "chapter_vzdump" : {
+ "link" : "/pve-docs/chapter-vzdump.html#chapter_vzdump",
+ "title" : "Backup and Restore"
+ },
+ "chapter_zfs" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#chapter_zfs",
+ "title" : "ZFS on Linux"
+ },
+ "datacenter_configuration_file" : {
+ "link" : "/pve-docs/pve-admin-guide.html#datacenter_configuration_file",
+ "title" : "Datacenter Configuration"
+ },
+ "getting_help" : {
+ "link" : "/pve-docs/pve-admin-guide.html#getting_help",
+ "title" : "Getting Help"
+ },
+ "gui_my_settings" : {
+ "link" : "/pve-docs/chapter-pve-gui.html#gui_my_settings",
+ "subtitle" : "My Settings",
+ "title" : "Graphical User Interface"
+ },
+ "ha_manager_fencing" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_fencing",
+ "subtitle" : "Fencing",
+ "title" : "High Availability"
+ },
+ "ha_manager_groups" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_groups",
+ "subtitle" : "Groups",
+ "title" : "High Availability"
+ },
+ "ha_manager_resource_config" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resource_config",
+ "subtitle" : "Resources",
+ "title" : "High Availability"
+ },
+ "ha_manager_resources" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resources",
+ "subtitle" : "Resources",
+ "title" : "High Availability"
+ },
+ "pct_configuration" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_configuration",
+ "subtitle" : "Configuration",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_container_images" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_container_images",
+ "subtitle" : "Container Images",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_container_network" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_container_network",
+ "subtitle" : "Network",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_container_storage" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_container_storage",
+ "subtitle" : "Container Storage",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_cpu" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_cpu",
+ "subtitle" : "CPU",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_general" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_general",
+ "subtitle" : "General Settings",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_memory" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_memory",
+ "subtitle" : "Memory",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_migration" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_migration",
+ "subtitle" : "Migration",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_options" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_options",
+ "subtitle" : "Options",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_snapshots" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_snapshots",
+ "subtitle" : "Snapshots",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_startup_and_shutdown" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_startup_and_shutdown",
+ "subtitle" : "Automatic Start and Shutdown of Containers",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pve_admin_guide" : {
+ "link" : "/pve-docs/pve-admin-guide.html",
+ "title" : "Proxmox VE Administration Guide"
+ },
+ "pve_ceph_install" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_install",
+ "subtitle" : "Installation of Ceph Packages",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "pve_ceph_osds" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_osds",
+ "subtitle" : "Creating Ceph OSDs",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "pve_ceph_pools" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_pools",
+ "subtitle" : "Creating Ceph Pools",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "pve_documentation_index" : {
+ "link" : "/pve-docs/index.html",
+ "title" : "Proxmox VE Documentation Index"
+ },
+ "pve_firewall_cluster_wide_setup" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_cluster_wide_setup",
+ "subtitle" : "Cluster Wide Setup",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_host_specific_configuration" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_host_specific_configuration",
+ "subtitle" : "Host Specific Configuration",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_ip_aliases" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_aliases",
+ "subtitle" : "IP Aliases",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_ip_sets" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_sets",
+ "subtitle" : "IP Sets",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_vm_container_configuration" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_vm_container_configuration",
+ "subtitle" : "VM/Container Configuration",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_service_daemons" : {
+ "link" : "/pve-docs/index.html#_service_daemons",
+ "title" : "Service Daemons"
+ },
+ "pveceph_fs" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs",
+ "subtitle" : "CephFS",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "pveceph_fs_create" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_create",
+ "subtitle" : "Create a CephFS",
+ "title" : "Manage Ceph Services on Proxmox VE Nodes"
+ },
+ "pvecm_create_cluster" : {
+ "link" : "/pve-docs/chapter-pvecm.html#pvecm_create_cluster",
+ "subtitle" : "Create the Cluster",
+ "title" : "Cluster Manager"
+ },
+ "pvesr_schedule_time_format" : {
+ "link" : "/pve-docs/chapter-pvesr.html#pvesr_schedule_time_format",
+ "subtitle" : "Schedule Format",
+ "title" : "Storage Replication"
+ },
+ "pveum_authentication_realms" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_authentication_realms",
+ "subtitle" : "Authentication Realms",
+ "title" : "User Management"
+ },
+ "pveum_groups" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_groups",
+ "subtitle" : "Groups",
+ "title" : "User Management"
+ },
+ "pveum_permission_management" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_permission_management",
+ "subtitle" : "Permission Management",
+ "title" : "User Management"
+ },
+ "pveum_pools" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_pools",
+ "subtitle" : "Pools",
+ "title" : "User Management"
+ },
+ "pveum_roles" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_roles",
+ "subtitle" : "Roles",
+ "title" : "User Management"
+ },
+ "pveum_tfa_auth" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_tfa_auth",
+ "subtitle" : "Two factor authentication",
+ "title" : "User Management"
+ },
+ "pveum_users" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_users",
+ "subtitle" : "Users",
+ "title" : "User Management"
+ },
+ "qm_bios_and_uefi" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_bios_and_uefi",
+ "subtitle" : "BIOS and UEFI",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_cloud_init" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_cloud_init",
+ "title" : "Cloud-Init Support"
+ },
+ "qm_copy_and_clone" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_copy_and_clone",
+ "subtitle" : "Copies and Clones",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_cpu" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_cpu",
+ "subtitle" : "CPU",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_general_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_general_settings",
+ "subtitle" : "General Settings",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_hard_disk" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_hard_disk",
+ "subtitle" : "Hard Disk",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_memory" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_memory",
+ "subtitle" : "Memory",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_migration" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_migration",
+ "subtitle" : "Migration",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_network_device" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_network_device",
+ "subtitle" : "Network Device",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_options" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_options",
+ "subtitle" : "Options",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_os_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_os_settings",
+ "subtitle" : "OS Settings",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_pci_passthrough" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_pci_passthrough",
+ "title" : "PCI(e) Passthrough"
+ },
+ "qm_startup_and_shutdown" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_startup_and_shutdown",
+ "subtitle" : "Automatic Start and Shutdown of Virtual Machines",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_system_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_system_settings",
+ "subtitle" : "System Settings",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_usb_passthrough" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_usb_passthrough",
+ "subtitle" : "USB Passthrough",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "qm_virtual_machines_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_virtual_machines_settings",
+ "subtitle" : "Virtual Machines Settings",
+ "title" : "Qemu/KVM Virtual Machines"
+ },
+ "storage_cephfs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_cephfs",
+ "title" : "Ceph Filesystem (CephFS)"
+ },
+ "storage_cifs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_cifs",
+ "title" : "CIFS Backend"
+ },
+ "storage_directory" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_directory",
+ "title" : "Directory Backend"
+ },
+ "storage_glusterfs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_glusterfs",
+ "title" : "GlusterFS Backend"
+ },
+ "storage_lvm" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_lvm",
+ "title" : "LVM Backend"
+ },
+ "storage_lvmthin" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_lvmthin",
+ "title" : "LVM thin Backend"
+ },
+ "storage_nfs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_nfs",
+ "title" : "NFS Backend"
+ },
+ "storage_open_iscsi" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_open_iscsi",
+ "title" : "Open-iSCSI initiator"
+ },
+ "storage_zfspool" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_zfspool",
+ "title" : "Local ZFS Pool Backend"
+ },
+ "sysadmin_certificate_management" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certificate_management",
+ "title" : "Certificate Management"
+ },
+ "sysadmin_network_configuration" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_network_configuration",
+ "title" : "Network Configuration"
+ }
+};
+Ext.ns('PVE');
+
+// avoid errors related to Accessible Rich Internet Applications
+// (access for people with disabilities)
+// TODO reenable after all components are upgraded
+Ext.enableAria = false;
+Ext.enableAriaButtons = false;
+Ext.enableAriaPanels = false;
+
+// avoid errors when running without development tools
+if (!Ext.isDefined(Ext.global.console)) {
+ var console = {
+ log: function() {}
+ };
+}
+console.log("Starting PVE Manager");
+
+Ext.Ajax.defaultHeaders = {
+ 'Accept': 'application/json'
+};
+
+/*jslint confusion: true */
+Ext.define('PVE.Utils', { utilities: {
+
+ // this singleton contains miscellaneous utilities
+
+ toolkit: undefined, // (extjs|touch), set inside Toolkit.js
+
+ bus_match: /^(ide|sata|virtio|scsi)\d+$/,
+
+ log_severity_hash: {
+ 0: "panic",
+ 1: "alert",
+ 2: "critical",
+ 3: "error",
+ 4: "warning",
+ 5: "notice",
+ 6: "info",
+ 7: "debug"
+ },
+
+ support_level_hash: {
+ 'c': gettext('Community'),
+ 'b': gettext('Basic'),
+ 's': gettext('Standard'),
+ 'p': gettext('Premium')
+ },
+
+ noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.',
+
+ kvm_ostypes: {
+ 'Linux': [
+ { desc: '5.x - 2.6 Kernel', val: 'l26' },
+ { desc: '2.4 Kernel', val: 'l24' }
+ ],
+ 'Microsoft Windows': [
+ { desc: '10/2016', val: 'win10' },
+ { desc: '8.x/2012/2012r2', val: 'win8' },
+ { desc: '7/2008r2', val: 'win7' },
+ { desc: 'Vista/2008', val: 'w2k8' },
+ { desc: 'XP/2003', val: 'wxp' },
+ { desc: '2000', val: 'w2k' }
+ ],
+ 'Solaris Kernel': [
+ { desc: '-', val: 'solaris'}
+ ],
+ 'Other': [
+ { desc: '-', val: 'other'}
+ ]
+ },
+
+ get_health_icon: function(state, circle) {
+ if (circle === undefined) {
+ circle = false;
+ }
+
+ if (state === undefined) {
+ state = 'uknown';
+ }
+
+ var icon = 'faded fa-question';
+ switch(state) {
+ case 'good':
+ icon = 'good fa-check';
+ break;
+ case 'old':
+ icon = 'warning fa-refresh';
+ break;
+ case 'warning':
+ icon = 'warning fa-exclamation';
+ break;
+ case 'critical':
+ icon = 'critical fa-times';
+ break;
+ default: break;
+ }
+
+ if (circle) {
+ icon += '-circle';
+ }
+
+ return icon;
+ },
+
+ parse_ceph_version: function(service) {
+ if (service.ceph_version_short) {
+ return service.ceph_version_short;
+ }
+
+ if (service.ceph_version) {
+ var match = service.ceph_version.match(/version (\d+(\.\d+)*)/);
+ if (match) {
+ return match[1];
+ }
+ }
+
+ return undefined;
+ },
+
+ compare_ceph_versions: function(a, b) {
+ if (a === b) {
+ return 0;
+ }
+ let avers = a.toString().split('.');
+ let bvers = b.toString().split('.');
+
+ while (true) {
+ let av = avers.shift();
+ let bv = bvers.shift();
+
+ if (av === undefined && bv === undefined) {
+ return 0;
+ } else if (av === undefined) {
+ return -1;
+ } else if (bv === undefined) {
+ return 1;
+ } else {
+ let diff = parseInt(av, 10) - parseInt(bv, 10);
+ if (diff != 0) return diff;
+ // else we need to look at the next parts
+ }
+ }
+
+ },
+
+ get_ceph_icon_html: function(health, fw) {
+ var state = PVE.Utils.map_ceph_health[health];
+ var cls = PVE.Utils.get_health_icon(state);
+ if (fw) {
+ cls += ' fa-fw';
+ }
+ return " ";
+ },
+
+ map_ceph_health: {
+ 'HEALTH_OK':'good',
+ 'HEALTH_OLD':'old',
+ 'HEALTH_WARN':'warning',
+ 'HEALTH_ERR':'critical'
+ },
+
+ render_ceph_health: function(healthObj) {
+ var state = {
+ iconCls: PVE.Utils.get_health_icon(),
+ text: ''
+ };
+
+ if (!healthObj || !healthObj.status) {
+ return state;
+ }
+
+ var health = PVE.Utils.map_ceph_health[healthObj.status];
+
+ state.iconCls = PVE.Utils.get_health_icon(health, true);
+ state.text = healthObj.status;
+
+ return state;
+ },
+
+ render_zfs_health: function(value) {
+ if (typeof value == 'undefined'){
+ return "";
+ }
+ var iconCls = 'question-circle';
+ switch (value) {
+ case 'AVAIL':
+ case 'ONLINE':
+ iconCls = 'check-circle good';
+ break;
+ case 'REMOVED':
+ case 'DEGRADED':
+ iconCls = 'exclamation-circle warning';
+ break;
+ case 'UNAVAIL':
+ case 'FAULTED':
+ case 'OFFLINE':
+ iconCls = 'times-circle critical';
+ break;
+ default: //unknown
+ }
+
+ return ' ' + value;
+
+ },
+
+ get_kvm_osinfo: function(value) {
+ var info = { base: 'Other' }; // default
+ if (value) {
+ Ext.each(Object.keys(PVE.Utils.kvm_ostypes), function(k) {
+ Ext.each(PVE.Utils.kvm_ostypes[k], function(e) {
+ if (e.val === value) {
+ info = { desc: e.desc, base: k };
+ }
+ });
+ });
+ }
+ return info;
+ },
+
+ render_kvm_ostype: function (value) {
+ var osinfo = PVE.Utils.get_kvm_osinfo(value);
+ if (osinfo.desc && osinfo.desc !== '-') {
+ return osinfo.base + ' ' + osinfo.desc;
+ } else {
+ return osinfo.base;
+ }
+ },
+
+ render_hotplug_features: function (value) {
+ var fa = [];
+
+ if (!value || (value === '0')) {
+ return gettext('Disabled');
+ }
+
+ if (value === '1') {
+ value = 'disk,network,usb';
+ }
+
+ Ext.each(value.split(','), function(el) {
+ if (el === 'disk') {
+ fa.push(gettext('Disk'));
+ } else if (el === 'network') {
+ fa.push(gettext('Network'));
+ } else if (el === 'usb') {
+ fa.push('USB');
+ } else if (el === 'memory') {
+ fa.push(gettext('Memory'));
+ } else if (el === 'cpu') {
+ fa.push(gettext('CPU'));
+ } else {
+ fa.push(el);
+ }
+ });
+
+ return fa.join(', ');
+ },
+
+ render_qga_features: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')';
+ }
+ var props = PVE.Parser.parsePropertyString(value, 'enabled');
+ if (!PVE.Parser.parseBoolean(props.enabled)) {
+ return Proxmox.Utils.disabledText;
+ }
+
+ delete props.enabled;
+ var agentstring = Proxmox.Utils.enabledText;
+
+ Ext.Object.each(props, function(key, value) {
+ var keystring = '' ;
+ agentstring += ', ' + key + ': ';
+
+ if (PVE.Parser.parseBoolean(value)) {
+ agentstring += Proxmox.Utils.enabledText;
+ } else {
+ agentstring += Proxmox.Utils.disabledText;
+ }
+ });
+
+ return agentstring;
+ },
+
+ render_qemu_machine: function(value) {
+ return value || (Proxmox.Utils.defaultText + ' (i440fx)');
+ },
+
+ render_qemu_bios: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText + ' (SeaBIOS)';
+ } else if (value === 'seabios') {
+ return "SeaBIOS";
+ } else if (value === 'ovmf') {
+ return "OVMF (UEFI)";
+ } else {
+ return value;
+ }
+ },
+
+ render_dc_ha_opts: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText;
+ } else {
+ return PVE.Parser.printPropertyString(value);
+ }
+ },
+ render_as_property_string: function(value) {
+ return (!value) ? Proxmox.Utils.defaultText
+ : PVE.Parser.printPropertyString(value);
+ },
+
+ render_scsihw: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText + ' (LSI 53C895A)';
+ } else if (value === 'lsi') {
+ return 'LSI 53C895A';
+ } else if (value === 'lsi53c810') {
+ return 'LSI 53C810';
+ } else if (value === 'megasas') {
+ return 'MegaRAID SAS 8708EM2';
+ } else if (value === 'virtio-scsi-pci') {
+ return 'VirtIO SCSI';
+ } else if (value === 'virtio-scsi-single') {
+ return 'VirtIO SCSI single';
+ } else if (value === 'pvscsi') {
+ return 'VMware PVSCSI';
+ } else {
+ return value;
+ }
+ },
+
+ // fixme: auto-generate this
+ // for now, please keep in sync with PVE::Tools::kvmkeymaps
+ kvm_keymaps: {
+ //ar: 'Arabic',
+ da: 'Danish',
+ de: 'German',
+ 'de-ch': 'German (Swiss)',
+ 'en-gb': 'English (UK)',
+ 'en-us': 'English (USA)',
+ es: 'Spanish',
+ //et: 'Estonia',
+ fi: 'Finnish',
+ //fo: 'Faroe Islands',
+ fr: 'French',
+ 'fr-be': 'French (Belgium)',
+ 'fr-ca': 'French (Canada)',
+ 'fr-ch': 'French (Swiss)',
+ //hr: 'Croatia',
+ hu: 'Hungarian',
+ is: 'Icelandic',
+ it: 'Italian',
+ ja: 'Japanese',
+ lt: 'Lithuanian',
+ //lv: 'Latvian',
+ mk: 'Macedonian',
+ nl: 'Dutch',
+ //'nl-be': 'Dutch (Belgium)',
+ no: 'Norwegian',
+ pl: 'Polish',
+ pt: 'Portuguese',
+ 'pt-br': 'Portuguese (Brazil)',
+ //ru: 'Russian',
+ sl: 'Slovenian',
+ sv: 'Swedish',
+ //th: 'Thai',
+ tr: 'Turkish'
+ },
+
+ kvm_vga_drivers: {
+ std: gettext('Standard VGA'),
+ vmware: gettext('VMware compatible'),
+ qxl: 'SPICE',
+ qxl2: 'SPICE dual monitor',
+ qxl3: 'SPICE three monitors',
+ qxl4: 'SPICE four monitors',
+ serial0: gettext('Serial terminal') + ' 0',
+ serial1: gettext('Serial terminal') + ' 1',
+ serial2: gettext('Serial terminal') + ' 2',
+ serial3: gettext('Serial terminal') + ' 3',
+ virtio: 'VirtIO-GPU',
+ none: Proxmox.Utils.noneText
+ },
+
+ render_kvm_language: function (value) {
+ if (!value || value === '__default__') {
+ return Proxmox.Utils.defaultText;
+ }
+ var text = PVE.Utils.kvm_keymaps[value];
+ if (text) {
+ return text + ' (' + value + ')';
+ }
+ return value;
+ },
+
+ kvm_keymap_array: function() {
+ var data = [['__default__', PVE.Utils.render_kvm_language('')]];
+ Ext.Object.each(PVE.Utils.kvm_keymaps, function(key, value) {
+ data.push([key, PVE.Utils.render_kvm_language(value)]);
+ });
+
+ return data;
+ },
+
+ console_map: {
+ '__default__': Proxmox.Utils.defaultText + ' (xterm.js)',
+ 'vv': 'SPICE (remote-viewer)',
+ 'html5': 'HTML5 (noVNC)',
+ 'xtermjs': 'xterm.js'
+ },
+
+ render_console_viewer: function(value) {
+ value = value || '__default__';
+ if (PVE.Utils.console_map[value]) {
+ return PVE.Utils.console_map[value];
+ }
+ return value;
+ },
+
+ console_viewer_array: function() {
+ return Ext.Array.map(Object.keys(PVE.Utils.console_map), function(v) {
+ return [v, PVE.Utils.render_console_viewer(v)];
+ });
+ },
+
+ render_kvm_vga_driver: function (value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText;
+ }
+ var vga = PVE.Parser.parsePropertyString(value, 'type');
+ var text = PVE.Utils.kvm_vga_drivers[vga.type];
+ if (!vga.type) {
+ text = Proxmox.Utils.defaultText;
+ }
+ if (text) {
+ return text + ' (' + value + ')';
+ }
+ return value;
+ },
+
+ kvm_vga_driver_array: function() {
+ var data = [['__default__', PVE.Utils.render_kvm_vga_driver('')]];
+ Ext.Object.each(PVE.Utils.kvm_vga_drivers, function(key, value) {
+ data.push([key, PVE.Utils.render_kvm_vga_driver(value)]);
+ });
+
+ return data;
+ },
+
+ render_kvm_startup: function(value) {
+ var startup = PVE.Parser.parseStartup(value);
+
+ var res = 'order=';
+ if (startup.order === undefined) {
+ res += 'any';
+ } else {
+ res += startup.order;
+ }
+ if (startup.up !== undefined) {
+ res += ',up=' + startup.up;
+ }
+ if (startup.down !== undefined) {
+ res += ',down=' + startup.down;
+ }
+
+ return res;
+ },
+
+ extractFormActionError: function(action) {
+ var msg;
+ switch (action.failureType) {
+ case Ext.form.action.Action.CLIENT_INVALID:
+ msg = gettext('Form fields may not be submitted with invalid values');
+ break;
+ case Ext.form.action.Action.CONNECT_FAILURE:
+ msg = gettext('Connection error');
+ var resp = action.response;
+ if (resp.status && resp.statusText) {
+ msg += " " + resp.status + ": " + resp.statusText;
+ }
+ break;
+ case Ext.form.action.Action.LOAD_FAILURE:
+ case Ext.form.action.Action.SERVER_INVALID:
+ msg = Proxmox.Utils.extractRequestError(action.result, true);
+ break;
+ }
+ return msg;
+ },
+
+ format_duration_short: function(ut) {
+
+ if (ut < 60) {
+ return ut.toFixed(1) + 's';
+ }
+
+ if (ut < 3600) {
+ var mins = ut / 60;
+ return mins.toFixed(1) + 'm';
+ }
+
+ if (ut < 86400) {
+ var hours = ut / 3600;
+ return hours.toFixed(1) + 'h';
+ }
+
+ var days = ut / 86400;
+ return days.toFixed(1) + 'd';
+ },
+
+ contentTypes: {
+ 'images': gettext('Disk image'),
+ 'backup': gettext('VZDump backup file'),
+ 'vztmpl': gettext('Container template'),
+ 'iso': gettext('ISO image'),
+ 'rootdir': gettext('Container'),
+ 'snippets': gettext('Snippets')
+ },
+
+ storageSchema: {
+ dir: {
+ name: Proxmox.Utils.directoryText,
+ ipanel: 'DirInputPanel',
+ faIcon: 'folder'
+ },
+ lvm: {
+ name: 'LVM',
+ ipanel: 'LVMInputPanel',
+ faIcon: 'folder'
+ },
+ lvmthin: {
+ name: 'LVM-Thin',
+ ipanel: 'LvmThinInputPanel',
+ faIcon: 'folder'
+ },
+ nfs: {
+ name: 'NFS',
+ ipanel: 'NFSInputPanel',
+ faIcon: 'building'
+ },
+ cifs: {
+ name: 'CIFS',
+ ipanel: 'CIFSInputPanel',
+ faIcon: 'building'
+ },
+ glusterfs: {
+ name: 'GlusterFS',
+ ipanel: 'GlusterFsInputPanel',
+ faIcon: 'building'
+ },
+ iscsi: {
+ name: 'iSCSI',
+ ipanel: 'IScsiInputPanel',
+ faIcon: 'building'
+ },
+ cephfs: {
+ name: 'CephFS',
+ ipanel: 'CephFSInputPanel',
+ faIcon: 'building'
+ },
+ pvecephfs: {
+ name: 'CephFS (PVE)',
+ ipanel: 'CephFSInputPanel',
+ hideAdd: true,
+ faIcon: 'building'
+ },
+ rbd: {
+ name: 'RBD',
+ ipanel: 'RBDInputPanel',
+ faIcon: 'building'
+ },
+ pveceph: {
+ name: 'RBD (PVE)',
+ ipanel: 'RBDInputPanel',
+ hideAdd: true,
+ faIcon: 'building'
+ },
+ zfs: {
+ name: 'ZFS over iSCSI',
+ ipanel: 'ZFSInputPanel',
+ faIcon: 'building'
+ },
+ zfspool: {
+ name: 'ZFS',
+ ipanel: 'ZFSPoolInputPanel',
+ faIcon: 'folder'
+ },
+ drbd: {
+ name: 'DRBD',
+ hideAdd: true
+ }
+ },
+
+ format_storage_type: function(value, md, record) {
+ if (value === 'rbd') {
+ value = (!record || record.get('monhost') ? 'rbd' : 'pveceph');
+ } else if (value === 'cephfs') {
+ value = (!record || record.get('monhost') ? 'cephfs' : 'pvecephfs');
+ }
+
+ var schema = PVE.Utils.storageSchema[value];
+ if (schema) {
+ return schema.name;
+ }
+ return Proxmox.Utils.unknownText;
+ },
+
+ format_ha: function(value) {
+ var text = Proxmox.Utils.noneText;
+
+ if (value.managed) {
+ text = value.state || Proxmox.Utils.noneText;
+
+ text += ', ' + Proxmox.Utils.groupText + ': ';
+ text += value.group || Proxmox.Utils.noneText;
+ }
+
+ return text;
+ },
+
+ format_content_types: function(value) {
+ return value.split(',').sort().map(function(ct) {
+ return PVE.Utils.contentTypes[ct] || ct;
+ }).join(', ');
+ },
+
+ render_storage_content: function(value, metaData, record) {
+ var data = record.data;
+ if (Ext.isNumber(data.channel) &&
+ Ext.isNumber(data.id) &&
+ Ext.isNumber(data.lun)) {
+ return "CH " +
+ Ext.String.leftPad(data.channel,2, '0') +
+ " ID " + data.id + " LUN " + data.lun;
+ }
+ return data.volid.replace(/^.*:(.*\/)?/,'');
+ },
+
+ render_serverity: function (value) {
+ return PVE.Utils.log_severity_hash[value] || value;
+ },
+
+ render_cpu: function(value, metaData, record, rowIndex, colIndex, store) {
+
+ if (!(record.data.uptime && Ext.isNumeric(value))) {
+ return '';
+ }
+
+ var maxcpu = record.data.maxcpu || 1;
+
+ if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
+ return '';
+ }
+
+ var per = value * 100;
+
+ return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU');
+ },
+
+ render_size: function(value, metaData, record, rowIndex, colIndex, store) {
+ /*jslint confusion: true */
+
+ if (!Ext.isNumeric(value)) {
+ return '';
+ }
+
+ return Proxmox.Utils.format_size(value);
+ },
+
+ render_bandwidth: function(value) {
+ if (!Ext.isNumeric(value)) {
+ return '';
+ }
+
+ return Proxmox.Utils.format_size(value) + '/s';
+ },
+
+ render_timestamp_human_readable: function(value) {
+ return Ext.Date.format(new Date(value * 1000), 'l d F Y H:i:s');
+ },
+
+ render_duration: function(value) {
+ if (value === undefined) {
+ return '-';
+ }
+ return PVE.Utils.format_duration_short(value);
+ },
+
+ calculate_mem_usage: function(data) {
+ if (!Ext.isNumeric(data.mem) ||
+ data.maxmem === 0 ||
+ data.uptime < 1) {
+ return -1;
+ }
+
+ return (data.mem / data.maxmem);
+ },
+
+ render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
+ if (!Ext.isNumeric(value) || value === -1) {
+ return '';
+ }
+ if (value > 1 ) {
+ // we got no percentage but bytes
+ var mem = value;
+ var maxmem = record.data.maxmem;
+ if (!record.data.uptime ||
+ maxmem === 0 ||
+ !Ext.isNumeric(mem)) {
+ return '';
+ }
+
+ return ((mem*100)/maxmem).toFixed(1) + " %";
+ }
+ return (value*100).toFixed(1) + " %";
+ },
+
+ render_mem_usage: function(value, metaData, record, rowIndex, colIndex, store) {
+
+ var mem = value;
+ var maxmem = record.data.maxmem;
+
+ if (!record.data.uptime) {
+ return '';
+ }
+
+ if (!(Ext.isNumeric(mem) && maxmem)) {
+ return '';
+ }
+
+ return PVE.Utils.render_size(value);
+ },
+
+ calculate_disk_usage: function(data) {
+
+ if (!Ext.isNumeric(data.disk) ||
+ data.type === 'qemu' ||
+ (data.type === 'lxc' && data.uptime === 0) ||
+ data.maxdisk === 0) {
+ return -1;
+ }
+
+ return (data.disk / data.maxdisk);
+ },
+
+ render_disk_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
+ if (!Ext.isNumeric(value) || value === -1) {
+ return '';
+ }
+
+ return (value * 100).toFixed(1) + " %";
+ },
+
+ render_disk_usage: function(value, metaData, record, rowIndex, colIndex, store) {
+
+ var disk = value;
+ var maxdisk = record.data.maxdisk;
+ var type = record.data.type;
+
+ if (!Ext.isNumeric(disk) ||
+ type === 'qemu' ||
+ maxdisk === 0 ||
+ (type === 'lxc' && record.data.uptime === 0)) {
+ return '';
+ }
+
+ return PVE.Utils.render_size(value);
+ },
+
+ get_object_icon_class: function(type, record) {
+ var status = '';
+ var objType = type;
+
+ if (type === 'type') {
+ // for folder view
+ objType = record.groupbyid;
+ } else if (record.template) {
+ // templates
+ objType = 'template';
+ status = type;
+ } else {
+ // everything else
+ status = record.status + ' ha-' + record.hastate;
+ }
+
+ if (record.lock) {
+ status += ' locked lock-' + record.lock;
+ }
+
+ var defaults = PVE.tree.ResourceTree.typeDefaults[objType];
+ if (defaults && defaults.iconCls) {
+ var retVal = defaults.iconCls + ' ' + status;
+ return retVal;
+ }
+
+ return '';
+ },
+
+ render_resource_type: function(value, metaData, record, rowIndex, colIndex, store) {
+
+ var cls = PVE.Utils.get_object_icon_class(value,record.data);
+
+ var fa = ' ';
+ return fa + value;
+ },
+
+ render_support_level: function(value, metaData, record) {
+ return PVE.Utils.support_level_hash[value] || '-';
+ },
+
+ render_upid: function(value, metaData, record) {
+ var type = record.data.type;
+ var id = record.data.id;
+
+ return Proxmox.Utils.format_task_description(type, id);
+ },
+
+ /* render functions for new status panel */
+
+ render_usage: function(val) {
+ return (val*100).toFixed(2) + '%';
+ },
+
+ render_cpu_usage: function(val, max) {
+ return Ext.String.format(gettext('{0}% of {1}') +
+ ' ' + gettext('CPU(s)'), (val*100).toFixed(2), max);
+ },
+
+ render_size_usage: function(val, max) {
+ if (max === 0) {
+ return gettext('N/A');
+ }
+ return (val*100/max).toFixed(2) + '% '+ '(' +
+ Ext.String.format(gettext('{0} of {1}'),
+ PVE.Utils.render_size(val), PVE.Utils.render_size(max)) + ')';
+ },
+
+ /* this is different for nodes */
+ render_node_cpu_usage: function(value, record) {
+ return PVE.Utils.render_cpu_usage(value, record.cpus);
+ },
+
+ /* this is different for nodes */
+ render_node_size_usage: function(record) {
+ return PVE.Utils.render_size_usage(record.used, record.total);
+ },
+
+ render_optional_url: function(value) {
+ var match;
+ if (value && (match = value.match(/^https?:\/\//)) !== null) {
+ return '' + value + '';
+ }
+ return value;
+ },
+
+ render_san: function(value) {
+ var names = [];
+ if (Ext.isArray(value)) {
+ value.forEach(function(val) {
+ if (!Ext.isNumber(val)) {
+ names.push(val);
+ }
+ });
+ return names.join('
');
+ }
+ return value;
+ },
+
+ render_full_name: function(firstname, metaData, record) {
+ var first = firstname || '';
+ var last = record.data.lastname || '';
+ return Ext.htmlEncode(first + " " + last);
+ },
+
+ render_u2f_error: function(error) {
+ var ErrorNames = {
+ '1': gettext('Other Error'),
+ '2': gettext('Bad Request'),
+ '3': gettext('Configuration Unsupported'),
+ '4': gettext('Device Ineligible'),
+ '5': gettext('Timeout')
+ };
+ return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText;
+ },
+
+ windowHostname: function() {
+ return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match,
+ function(m, addr, offset, original) { return addr; });
+ },
+
+ openDefaultConsoleWindow: function(consoles, vmtype, vmid, nodename, vmname, cmd) {
+ var dv = PVE.Utils.defaultViewer(consoles);
+ PVE.Utils.openConsoleWindow(dv, vmtype, vmid, nodename, vmname, cmd);
+ },
+
+ openConsoleWindow: function(viewer, vmtype, vmid, nodename, vmname, cmd) {
+ // kvm, lxc, shell, upgrade
+
+ if (vmid == undefined && (vmtype === 'kvm' || vmtype === 'lxc')) {
+ throw "missing vmid";
+ }
+
+ if (!nodename) {
+ throw "no nodename specified";
+ }
+
+ if (viewer === 'html5') {
+ PVE.Utils.openVNCViewer(vmtype, vmid, nodename, vmname, cmd);
+ } else if (viewer === 'xtermjs') {
+ Proxmox.Utils.openXtermJsViewer(vmtype, vmid, nodename, vmname, cmd);
+ } else if (viewer === 'vv') {
+ var url;
+ var params = { proxy: PVE.Utils.windowHostname() };
+ if (vmtype === 'kvm') {
+ url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy';
+ PVE.Utils.openSpiceViewer(url, params);
+ } else if (vmtype === 'lxc') {
+ url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy';
+ PVE.Utils.openSpiceViewer(url, params);
+ } else if (vmtype === 'shell') {
+ url = '/nodes/' + nodename + '/spiceshell';
+ PVE.Utils.openSpiceViewer(url, params);
+ } else if (vmtype === 'upgrade') {
+ url = '/nodes/' + nodename + '/spiceshell';
+ params.upgrade = 1;
+ PVE.Utils.openSpiceViewer(url, params);
+ } else if (vmtype === 'cmd') {
+ url = '/nodes/' + nodename + '/spiceshell';
+ params.cmd = cmd;
+ PVE.Utils.openSpiceViewer(url, params);
+ }
+ } else {
+ throw "unknown viewer type";
+ }
+ },
+
+ defaultViewer: function(consoles) {
+
+ var allowSpice, allowXtermjs;
+
+ if (consoles === true) {
+ allowSpice = true;
+ allowXtermjs = true;
+ } else if (typeof consoles === 'object') {
+ allowSpice = consoles.spice;
+ allowXtermjs = !!consoles.xtermjs;
+ }
+ var dv = PVE.VersionInfo.console || 'xtermjs';
+ if (dv === 'vv' && !allowSpice) {
+ dv = (allowXtermjs) ? 'xtermjs' : 'html5';
+ } else if (dv === 'xtermjs' && !allowXtermjs) {
+ dv = (allowSpice) ? 'vv' : 'html5';
+ }
+
+ return dv;
+ },
+
+ openVNCViewer: function(vmtype, vmid, nodename, vmname, cmd) {
+ var url = Ext.Object.toQueryString({
+ console: vmtype, // kvm, lxc, upgrade or shell
+ novnc: 1,
+ vmid: vmid,
+ vmname: vmname,
+ node: nodename,
+ resize: 'off',
+ cmd: cmd
+ });
+ var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427");
+ if (nw) {
+ nw.focus();
+ }
+ },
+
+ openSpiceViewer: function(url, params){
+
+ var downloadWithName = function(uri, name) {
+ var link = Ext.DomHelper.append(document.body, {
+ tag: 'a',
+ href: uri,
+ css : 'display:none;visibility:hidden;height:0px;'
+ });
+
+ // Note: we need to tell android the correct file name extension
+ // but we do not set 'download' tag for other environments, because
+ // It can have strange side effects (additional user prompt on firefox)
+ var andriod = navigator.userAgent.match(/Android/i) ? true : false;
+ if (andriod) {
+ link.download = name;
+ }
+
+ if (link.fireEvent) {
+ link.fireEvent('onclick');
+ } else {
+ var evt = document.createEvent("MouseEvents");
+ evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+ link.dispatchEvent(evt);
+ }
+ };
+
+ Proxmox.Utils.API2Request({
+ url: url,
+ params: params,
+ method: 'POST',
+ failure: function(response, opts){
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, opts){
+ var raw = "[virt-viewer]\n";
+ Ext.Object.each(response.result.data, function(k, v) {
+ raw += k + "=" + v + "\n";
+ });
+ var url = 'data:application/x-virt-viewer;charset=UTF-8,' +
+ encodeURIComponent(raw);
+
+ downloadWithName(url, "pve-spice.vv");
+ }
+ });
+ },
+
+ openTreeConsole: function(tree, record, item, index, e) {
+ e.stopEvent();
+ var nodename = record.data.node;
+ var vmid = record.data.vmid;
+ var vmname = record.data.name;
+ if (record.data.type === 'qemu' && !record.data.template) {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, opts) {
+ let conf = response.result.data;
+ var consoles = {
+ spice: !!conf.spice,
+ xtermjs: !!conf.serial,
+ };
+ PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname);
+ }
+ });
+ } else if (record.data.type === 'lxc' && !record.data.template) {
+ PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname);
+ }
+ },
+
+ // test automation helper
+ call_menu_handler: function(menu, text) {
+
+ var list = menu.query('menuitem');
+
+ Ext.Array.each(list, function(item) {
+ if (item.text === text) {
+ if (item.handler) {
+ item.handler();
+ return 1;
+ } else {
+ return undefined;
+ }
+ }
+ });
+ },
+
+ createCmdMenu: function(v, record, item, index, event) {
+ event.stopEvent();
+ if (!(v instanceof Ext.tree.View)) {
+ v.select(record);
+ }
+ var menu;
+ var template = !!record.data.template;
+ var type = record.data.type;
+
+ if (template) {
+ if (type === 'qemu' || type == 'lxc') {
+ menu = Ext.create('PVE.menu.TemplateMenu', {
+ pveSelNode: record
+ });
+ }
+ } else if (type === 'qemu' ||
+ type === 'lxc' ||
+ type === 'node') {
+ menu = Ext.create('PVE.' + type + '.CmdMenu', {
+ pveSelNode: record,
+ nodename: record.data.node
+ });
+ } else {
+ return;
+ }
+
+ menu.showAt(event.getXY());
+ return menu;
+ },
+
+ // helper for deleting field which are set to there default values
+ delete_if_default: function(values, fieldname, default_val, create) {
+ if (values[fieldname] === '' || values[fieldname] === default_val) {
+ if (!create) {
+ if (values['delete']) {
+ values['delete'] += ',' + fieldname;
+ } else {
+ values['delete'] = fieldname;
+ }
+ }
+
+ delete values[fieldname];
+ }
+ },
+
+ loadSSHKeyFromFile: function(file, callback) {
+ // ssh-keygen produces 740 bytes for an average 4096 bit rsa key, with
+ // a user@host comment, 1420 for 8192 bits; current max is 16kbit
+ // assume: 740*8 for max. 32kbit (5920 byte file)
+ // round upwards to nearest nice number => 8192 bytes, leaves lots of comment space
+ if (file.size > 8192) {
+ Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
+ return;
+ }
+ /*global
+ FileReader
+ */
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ callback(evt.target.result);
+ };
+ reader.readAsText(file);
+ },
+
+ bus_counts: { ide: 4, sata: 6, scsi: 16, virtio: 16 },
+
+ // types is either undefined (all busses), an array of busses, or a single bus
+ forEachBus: function(types, func) {
+ var busses = Object.keys(PVE.Utils.bus_counts);
+ var i, j, count, cont;
+
+ if (Ext.isArray(types)) {
+ busses = types;
+ } else if (Ext.isDefined(types)) {
+ busses = [ types ];
+ }
+
+ // check if we only have valid busses
+ for (i = 0; i < busses.length; i++) {
+ if (!PVE.Utils.bus_counts[busses[i]]) {
+ throw "invalid bus: '" + busses[i] + "'";
+ }
+ }
+
+ for (i = 0; i < busses.length; i++) {
+ count = PVE.Utils.bus_counts[busses[i]];
+ for (j = 0; j < count; j++) {
+ cont = func(busses[i], j);
+ if (!cont && cont !== undefined) {
+ return;
+ }
+ }
+ }
+ },
+
+ mp_counts: { mps: 256, unused: 256 },
+
+ forEachMP: function(func, includeUnused) {
+ var i, cont;
+ for (i = 0; i < PVE.Utils.mp_counts.mps; i++) {
+ cont = func('mp', i);
+ if (!cont && cont !== undefined) {
+ return;
+ }
+ }
+
+ if (!includeUnused) {
+ return;
+ }
+
+ for (i = 0; i < PVE.Utils.mp_counts.unused; i++) {
+ cont = func('unused', i);
+ if (!cont && cont !== undefined) {
+ return;
+ }
+ }
+ },
+
+ cleanEmptyObjectKeys: function (obj) {
+ var propName;
+ for (propName in obj) {
+ if (obj.hasOwnProperty(propName)) {
+ if (obj[propName] === null || obj[propName] === undefined) {
+ delete obj[propName];
+ }
+ }
+ }
+ },
+
+ handleStoreErrorOrMask: function(me, store, regex, callback) {
+
+ me.mon(store, 'load', function (proxy, response, success, operation) {
+
+ if (success) {
+ Proxmox.Utils.setErrorMask(me, false);
+ return;
+ }
+ var msg;
+
+ if (operation.error.statusText) {
+ if (operation.error.statusText.match(regex)) {
+ callback(me, operation.error);
+ return;
+ } else {
+ msg = operation.error.statusText + ' (' + operation.error.status + ')';
+ }
+ } else {
+ msg = gettext('Connection error');
+ }
+ Proxmox.Utils.setErrorMask(me, msg);
+ });
+ },
+
+ showCephInstallOrMask: function(container, msg, nodename, callback){
+ var regex = new RegExp("not (installed|initialized)", "i");
+ if (msg.match(regex)) {
+ if (Proxmox.UserName === 'root@pam') {
+ container.el.mask();
+ if (!container.down('pveCephInstallWindow')){
+ var isInstalled = msg.match(/not initialized/i) ? true : false;
+ var win = Ext.create('PVE.ceph.Install', {
+ nodename: nodename
+ });
+ win.getViewModel().set('isInstalled', isInstalled);
+ container.add(win);
+ win.show();
+ callback(win);
+ }
+ } else {
+ container.mask(Ext.String.format(gettext('{0} not installed.') +
+ ' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+},
+
+ singleton: true,
+ constructor: function() {
+ var me = this;
+ Ext.apply(me, me.utilities);
+ }
+
+});
+
+// ExtJS related things
+
+Proxmox.Utils.toolkit = 'extjs';
+
+// custom PVE specific VTypes
+Ext.apply(Ext.form.field.VTypes, {
+
+ QemuStartDate: function(v) {
+ return (/^(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)$/).test(v);
+ },
+ QemuStartDateText: gettext('Format') + ': "now" or "2006-06-17T16:01:21" or "2006-06-17"',
+ IP64AddressList: function(v) {
+ var list = v.split(/[\ \,\;]+/);
+ var i;
+ for (i = 0; i < list.length; i++) {
+ if (list[i] == '') {
+ continue;
+ }
+
+ if (!Proxmox.Utils.IP64_match.test(list[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+ IP64AddressListText: gettext('Example') + ': 192.168.1.1,192.168.1.2',
+ IP64AddressListMask: /[A-Fa-f0-9\,\:\.\;\ ]/
+});
+
+Ext.define('PVE.form.field.Display', {
+ override: 'Ext.form.field.Display',
+
+ setSubmitValue: function(value) {
+ // do nothing, this is only to allow generalized bindings for the:
+ // `me.isCreate ? 'textfield' : 'displayfield'` cases we have.
+ }
+});
+// Some configuration values are complex strings -
+// so we need parsers/generators for them.
+
+Ext.define('PVE.Parser', { statics: {
+
+ // this class only contains static functions
+
+ parseACME: function(value) {
+ if (!value) {
+ 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;
+ }
+ });
+
+ if (errors || !res) {
+ return;
+ }
+
+ return res;
+ },
+
+ parseBoolean: function(value, default_value) {
+ if (!Ext.isDefined(value)) {
+ return default_value;
+ }
+ value = value.toLowerCase();
+ return value === '1' ||
+ value === 'on' ||
+ value === 'yes' ||
+ value === 'true';
+ },
+
+ parsePropertyString: function(value, defaultKey) {
+ var res = {},
+ error;
+
+ 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])) {
+ error = 'defaultKey may be only defined once in propertyString';
+ return false; // break
+ }
+ res[defaultKey] = kv[0];
+ } else {
+ error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
+ return false; // break
+ }
+ });
+
+ if (error !== undefined) {
+ console.error(error);
+ return;
+ }
+
+ return res;
+ },
+
+ printPropertyString: function(data, defaultKey) {
+ var stringparts = [],
+ gotDefaultKeyVal = false,
+ defaultKeyVal;
+
+ Ext.Object.each(data, function(key, value) {
+ if (defaultKey !== undefined && key === defaultKey) {
+ gotDefaultKeyVal = true;
+ defaultKeyVal = value;
+ } else {
+ stringparts.push(key + '=' + value);
+ }
+ });
+
+ stringparts = stringparts.sort();
+ if (gotDefaultKeyVal) {
+ stringparts.unshift(defaultKeyVal);
+ }
+
+ return stringparts.join(',');
+ },
+
+ parseQemuNetwork: function(key, value) {
+ if (!(key && value)) {
+ 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(/^(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) {
+ 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) {
+ res.rate = match_res[1];
+ } else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) {
+ res.tag = match_res[1];
+ } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
+ res.firewall = match_res[1];
+ } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
+ res.disconnect = match_res[1];
+ } else if ((match_res = p.match(/^queues=(\d+)$/)) !== null) {
+ res.queues = match_res[1];
+ } else if ((match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null) {
+ res.trunks = match_res[1];
+ } else {
+ errors = true;
+ return false; // break
+ }
+ });
+
+ if (errors || !res.model) {
+ return;
+ }
+
+ return res;
+ },
+
+ printQemuNetwork: function(net) {
+
+ var netstr = net.model;
+ if (net.macaddr) {
+ netstr += "=" + net.macaddr;
+ }
+ if (net.bridge) {
+ netstr += ",bridge=" + net.bridge;
+ if (net.tag) {
+ netstr += ",tag=" + net.tag;
+ }
+ if (net.firewall) {
+ netstr += ",firewall=" + net.firewall;
+ }
+ }
+ if (net.rate) {
+ netstr += ",rate=" + net.rate;
+ }
+ if (net.queues) {
+ netstr += ",queues=" + net.queues;
+ }
+ if (net.disconnect) {
+ netstr += ",link_down=" + net.disconnect;
+ }
+ if (net.trunks) {
+ netstr += ",trunks=" + net.trunks;
+ }
+ return netstr;
+ },
+
+ parseQemuDrive: function(key, value) {
+ if (!(key && value)) {
+ return;
+ }
+
+ var res = {};
+
+ var match_res = key.match(/^([a-z]+)(\d+)$/);
+ if (!match_res) {
+ return;
+ }
+ res['interface'] = match_res[1];
+ res.index = match_res[2];
+
+ var errors = false;
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+ var match_res = p.match(/^([a-z_]+)=(\S+)$/);
+ if (!match_res) {
+ if (!p.match(/\=/)) {
+ res.file = p;
+ return; // continue
+ }
+ errors = true;
+ return false; // break
+ }
+ var k = match_res[1];
+ if (k === 'volume') {
+ k = 'file';
+ }
+
+ if (Ext.isDefined(res[k])) {
+ errors = true;
+ return false; // break
+ }
+
+ var v = match_res[2];
+
+ if (k === 'cache' && v === 'off') {
+ v = 'none';
+ }
+
+ res[k] = v;
+ });
+
+ if (errors || !res.file) {
+ return;
+ }
+
+ return res;
+ },
+
+ printQemuDrive: function(drive) {
+
+ var drivestr = drive.file;
+
+ Ext.Object.each(drive, function(key, value) {
+ if (!Ext.isDefined(value) || key === 'file' ||
+ key === 'index' || key === 'interface') {
+ return; // continue
+ }
+ drivestr += ',' + key + '=' + value;
+ });
+
+ return drivestr;
+ },
+
+ parseIPConfig: function(key, value) {
+ if (!(key && value)) {
+ 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(/^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) {
+ 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];
+ }
+ });
+
+ if (errors || !data.ifname) {
+ errors = true;
+ return false; // break
+ }
+
+ data.raw = item;
+
+ res[data.ifname] = data;
+ });
+
+ return errors ? undefined: 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(';');
+ },
+
+ parseLxcNetwork: function(value) {
+ if (!value) {
+ return;
+ }
+
+ var data = {};
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+ var 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 {
+ // todo: simply ignore errors ?
+ return; // continue
+ }
+ });
+
+ 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(',');
+ },
+
+ parseLxcMountPoint: function(value) {
+ if (!value) {
+ return;
+ }
+
+ var res = {};
+
+ var errors = false;
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+ var match_res = p.match(/^([a-z_]+)=(.+)$/);
+ if (!match_res) {
+ if (!p.match(/\=/)) {
+ res.file = p;
+ return; // continue
+ }
+ errors = true;
+ return false; // break
+ }
+ var k = match_res[1];
+ if (k === 'volume') {
+ k = 'file';
+ }
+
+ if (Ext.isDefined(res[k])) {
+ errors = true;
+ return false; // break
+ }
+
+ var v = match_res[2];
+
+ res[k] = v;
+ });
+
+ if (errors || !res.file) {
+ return;
+ }
+
+ var m = res.file.match(/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):/i);
+ if (m) {
+ res.storage = m[1];
+ res.type = 'volume';
+ } else if (res.file.match(/^\/dev\//)) {
+ res.type = 'device';
+ } else {
+ res.type = 'bind';
+ }
+
+ return res;
+ },
+
+ 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
+ }
+ drivestr += ',' + key + '=' + value;
+ });
+
+ return drivestr;
+ },
+
+ parseStartup: function(value) {
+ if (value === undefined) {
+ 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(/^(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
+ }
+ });
+
+ if (errors) {
+ return;
+ }
+
+ return res;
+ },
+
+ printStartup: function(startup) {
+ var arr = [];
+ if (startup.order !== undefined && startup.order !== '') {
+ arr.push('order=' + startup.order);
+ }
+ if (startup.up !== undefined && startup.up !== '') {
+ arr.push('up=' + startup.up);
+ }
+ if (startup.down !== undefined && startup.down !== '') {
+ arr.push('down=' + startup.down);
+ }
+
+ return arr.join(',');
+ },
+
+ parseQemuSmbios1: function(value) {
+ var res = value.split(',').reduce(function (accumulator, currentValue) {
+ var splitted = currentValue.split(new RegExp("=(.+)"));
+ accumulator[splitted[0]] = splitted[1];
+ return accumulator;
+ }, {});
+
+ if (PVE.Parser.parseBoolean(res.base64, false)) {
+ Ext.Object.each(res, function(key, value) {
+ if (key === 'uuid') { return; }
+ res[key] = Ext.util.Base64.decode(value);
+ });
+ }
+
+ return res;
+ },
+
+ printQemuSmbios1: function(data) {
+
+ var datastr = '';
+ var base64 = false;
+ Ext.Object.each(data, function(key, value) {
+ if (value === '') { return; }
+ if (key === 'uuid') {
+ datastr += (datastr !== '' ? ',' : '') + key + '=' + value;
+ } else {
+ // values should be base64 encoded from now on, mark config strings correspondingly
+ if (!base64) {
+ base64 = true;
+ datastr += (datastr !== '' ? ',' : '') + 'base64=1';
+ }
+ datastr += (datastr !== '' ? ',' : '') + key + '=' + Ext.util.Base64.encode(value);
+ }
+ });
+
+ return datastr;
+ },
+
+ parseTfaConfig: function(value) {
+ var res = {};
+
+ Ext.Array.each(value.split(','), function(p) {
+ var kva = p.split('=', 2);
+ res[kva[0]] = kva[1];
+ });
+
+ return res;
+ },
+
+ parseTfaType: function(value) {
+ /*jslint confusion: true*/
+ var match;
+ if (!value || !value.length) {
+ return undefined;
+ } else if (value === 'x!oath') {
+ return 'totp';
+ } else if (!!(match = value.match(/^x!(.+)$/))) {
+ return match[1];
+ } else {
+ return 1;
+ }
+ },
+
+ parseQemuCpu: function(value) {
+ if (!value) {
+ return {};
+ }
+
+ var res = {};
+
+ var errors = false;
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+
+ if (!p.match(/\=/)) {
+ if (Ext.isDefined(res.cpu)) {
+ errors = true;
+ return false; // break
+ }
+ res.cputype = p;
+ return; // continue
+ }
+
+ var match_res = p.match(/^([a-z_]+)=(\S+)$/);
+ if (!match_res) {
+ errors = true;
+ return false; // break
+ }
+
+ var k = match_res[1];
+ if (Ext.isDefined(res[k])) {
+ errors = true;
+ return false; // break
+ }
+
+ res[k] = match_res[2];
+ });
+
+ if (errors || !res.cputype) {
+ return;
+ }
+
+ return res;
+ },
+
+ printQemuCpu: function(cpu) {
+ var cpustr = cpu.cputype;
+ var optstr = '';
+
+ Ext.Object.each(cpu, function(key, value) {
+ if (!Ext.isDefined(value) || key === 'cputype') {
+ return; // continue
+ }
+ optstr += ',' + key + '=' + value;
+ });
+
+ if (!cpustr) {
+ if (optstr) {
+ return 'kvm64' + optstr;
+ }
+ return;
+ }
+
+ return cpustr + optstr;
+ },
+
+ 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+)$/;
+
+ var m = key.match(keyre);
+ if (!m) {
+ return null;
+ }
+ if (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]
+ };
+ }
+ if (m[2].match(typere)) {
+ return {
+ options: m[1],
+ type: m[2],
+ key: m[3],
+ comment: m[4]
+ };
+ }
+ return null;
+ }
+}});
+/* This state provider keeps part of the state inside
+ * the browser history.
+ *
+ * We compress (shorten) url using dictionary based compression
+ * i.e. use column separated list instead of url encoded hash:
+ * #v\d* version/format
+ * := indicates string values
+ * :\d+ lookup value in dictionary hash
+ * #v1:=value1:5:=value2:=value3:...
+*/
+
+Ext.define('PVE.StateProvider', {
+ extend: 'Ext.state.LocalStorageProvider',
+
+ // private
+ setHV: function(name, newvalue, fireEvents) {
+ var me = this;
+
+ var changes = false;
+ var oldtext = Ext.encode(me.UIState[name]);
+ var newtext = Ext.encode(newvalue);
+ if (newtext != oldtext) {
+ changes = true;
+ me.UIState[name] = newvalue;
+ //console.log("changed old " + name + " " + oldtext);
+ //console.log("changed new " + name + " " + newtext);
+ if (fireEvents) {
+ me.fireEvent("statechange", me, name, { value: newvalue });
+ }
+ }
+ return changes;
+ },
+
+ // private
+ hslist: [
+ // order is important for notifications
+ // [ name, default ]
+ ['view', 'server'],
+ ['rid', 'root'],
+ ['ltab', 'tasks'],
+ ['nodetab', ''],
+ ['storagetab', ''],
+ ['pooltab', ''],
+ ['kvmtab', ''],
+ ['lxctab', ''],
+ ['dctab', '']
+ ],
+
+ hprefix: 'v1',
+
+ compDict: {
+ cloudinit: 52,
+ replication: 51,
+ system: 50,
+ monitor: 49,
+ 'ha-fencing': 48,
+ 'ha-groups': 47,
+ 'ha-resources': 46,
+ 'ceph-log': 45,
+ 'ceph-crushmap':44,
+ 'ceph-pools': 43,
+ 'ceph-osdtree': 42,
+ 'ceph-disklist': 41,
+ 'ceph-monlist': 40,
+ 'ceph-config': 39,
+ ceph: 38,
+ 'firewall-fwlog': 37,
+ 'firewall-options': 36,
+ 'firewall-ipset': 35,
+ 'firewall-aliases': 34,
+ 'firewall-sg': 33,
+ firewall: 32,
+ apt: 31,
+ members: 30,
+ snapshot: 29,
+ ha: 28,
+ support: 27,
+ pools: 26,
+ syslog: 25,
+ ubc: 24,
+ initlog: 23,
+ openvz: 22,
+ backup: 21,
+ resources: 20,
+ content: 19,
+ root: 18,
+ domains: 17,
+ roles: 16,
+ groups: 15,
+ users: 14,
+ time: 13,
+ dns: 12,
+ network: 11,
+ services: 10,
+ options: 9,
+ console: 8,
+ hardware: 7,
+ permissions: 6,
+ summary: 5,
+ tasks: 4,
+ clog: 3,
+ storage: 2,
+ folder: 1,
+ server: 0
+ },
+
+ decodeHToken: function(token) {
+ var me = this;
+
+ var state = {};
+ if (!token) {
+ Ext.Array.each(me.hslist, function(rec) {
+ state[rec[0]] = rec[1];
+ });
+ return state;
+ }
+
+ // return Ext.urlDecode(token);
+
+ var items = token.split(':');
+ var prefix = items.shift();
+
+ if (prefix != me.hprefix) {
+ return me.decodeHToken();
+ }
+
+ Ext.Array.each(me.hslist, function(rec) {
+ var value = items.shift();
+ if (value) {
+ if (value[0] === '=') {
+ value = decodeURIComponent(value.slice(1));
+ } else {
+ Ext.Object.each(me.compDict, function(key, cv) {
+ if (value == cv) {
+ value = key;
+ return false;
+ }
+ });
+ }
+ }
+ state[rec[0]] = value;
+ });
+
+ return state;
+ },
+
+ encodeHToken: function(state) {
+ var me = this;
+
+ // return Ext.urlEncode(state);
+
+ var ctoken = me.hprefix;
+ Ext.Array.each(me.hslist, function(rec) {
+ var value = state[rec[0]];
+ if (!Ext.isDefined(value)) {
+ value = rec[1];
+ }
+ value = encodeURIComponent(value);
+ if (!value) {
+ ctoken += ':';
+ } else {
+ var comp = me.compDict[value];
+ if (Ext.isDefined(comp)) {
+ ctoken += ":" + comp;
+ } else {
+ ctoken += ":=" + value;
+ }
+ }
+ });
+
+ return ctoken;
+ },
+
+ constructor: function(config){
+ var me = this;
+
+ me.callParent([config]);
+
+ me.UIState = me.decodeHToken(); // set default
+
+ var history_change_cb = function(token) {
+ //console.log("HC " + token);
+ if (!token) {
+ var res = window.confirm(gettext('Are you sure you want to navigate away from this page?'));
+ if (res){
+ // process text value and close...
+ Ext.History.back();
+ } else {
+ Ext.History.forward();
+ }
+ return;
+ }
+
+ var newstate = me.decodeHToken(token);
+ Ext.Array.each(me.hslist, function(rec) {
+ if (typeof newstate[rec[0]] == "undefined") {
+ return;
+ }
+ me.setHV(rec[0], newstate[rec[0]], true);
+ });
+ };
+
+ var start_token = Ext.History.getToken();
+ if (start_token) {
+ history_change_cb(start_token);
+ } else {
+ var htext = me.encodeHToken(me.UIState);
+ Ext.History.add(htext);
+ }
+
+ Ext.History.on('change', history_change_cb);
+ },
+
+ get: function(name, defaultValue){
+ /*jslint confusion: true */
+ var me = this;
+ var data;
+
+ if (typeof me.UIState[name] != "undefined") {
+ data = { value: me.UIState[name] };
+ } else {
+ data = me.callParent(arguments);
+ if (!data && name === 'GuiCap') {
+ data = { vms: {}, storage: {}, access: {}, nodes: {}, dc: {} };
+ }
+ }
+
+ //console.log("GET " + name + " " + Ext.encode(data));
+ return data;
+ },
+
+ clear: function(name){
+ var me = this;
+
+ if (typeof me.UIState[name] != "undefined") {
+ me.UIState[name] = null;
+ }
+
+ me.callParent(arguments);
+ },
+
+ set: function(name, value, fireevent){
+ var me = this;
+
+ //console.log("SET " + name + " " + Ext.encode(value));
+ if (typeof me.UIState[name] != "undefined") {
+ var newvalue = value ? value.value : null;
+ if (me.setHV(name, newvalue, fireevent)) {
+ var htext = me.encodeHToken(me.UIState);
+ Ext.History.add(htext);
+ }
+ } else {
+ me.callParent(arguments);
+ }
+ }
+});
+Ext.define('PVE.menu.Item', {
+ extend: 'Ext.menu.Item',
+ alias: 'widget.pveMenuItem',
+
+ // set to wrap the handler callback in a confirm dialog showing this text
+ confirmMsg: false,
+
+ // set to focus 'No' instead of 'Yes' button and show a warning symbol
+ dangerous: false,
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.handler) {
+ me.setHandler(me.handler, me.scope);
+ }
+
+ me.callParent();
+ },
+
+ setHandler: function(fn, scope) {
+ var me = this;
+ me.scope = scope;
+ me.handler = function(button, e) {
+ var rec, msg;
+ if (me.confirmMsg) {
+ msg = me.confirmMsg;
+ Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
+ msg: msg,
+ buttons: Ext.Msg.YESNO,
+ defaultFocus: me.dangerous ? 'no' : 'yes',
+ callback: function(btn) {
+ if (btn === 'yes') {
+ Ext.callback(fn, me.scope, [me, e], 0, me);
+ }
+ }
+ });
+ } else {
+ Ext.callback(fn, me.scope, [me, e], 0, me);
+ }
+ };
+ }
+});
+Ext.define('PVE.menu.TemplateMenu', {
+ extend: 'Ext.menu.Menu',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var guestType = me.pveSelNode.data.type;
+ if (guestType !== 'qemu' && guestType != 'lxc') {
+ throw "invalid guest type";
+ }
+
+ var vmname = me.pveSelNode.data.name;
+
+ var template = me.pveSelNode.data.template;
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + nodename + '/' + guestType + '/' + vmid + "/status/" + cmd,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ };
+
+ me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + vmid;
+
+ me.items = [
+ {
+ text: gettext('Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ handler: function() {
+ var win = Ext.create('PVE.window.Migrate', {
+ vmtype: guestType,
+ nodename: nodename,
+ vmid: vmid
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ handler: function() {
+ var win = Ext.create('PVE.window.Clone', {
+ nodename: nodename,
+ guestType: guestType,
+ vmid: vmid,
+ isTemplate: template
+ });
+ win.show();
+ }
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.button.ConsoleButton', {
+ extend: 'Ext.button.Split',
+ alias: 'widget.pveConsoleButton',
+
+ consoleType: 'shell', // one of 'shell', 'kvm', 'lxc', 'upgrade', 'cmd'
+
+ cmd: undefined,
+
+ consoleName: undefined,
+
+ iconCls: 'fa fa-terminal',
+
+ enableSpice: true,
+ enableXtermjs: true,
+
+ nodename: undefined,
+
+ vmid: 0,
+
+ text: gettext('Console'),
+
+ setEnableSpice: function(enable){
+ var me = this;
+
+ me.enableSpice = enable;
+ me.down('#spicemenu').setDisabled(!enable);
+ },
+
+ setEnableXtermJS: function(enable){
+ var me = this;
+
+ me.enableXtermjs = enable;
+ me.down('#xtermjs').setDisabled(!enable);
+ },
+
+ handler: function() {
+ var me = this;
+ var consoles = {
+ spice: me.enableSpice,
+ xtermjs: me.enableXtermjs
+ };
+ PVE.Utils.openDefaultConsoleWindow(consoles, me.consoleType, me.vmid,
+ me.nodename, me.consoleName, me.cmd);
+ },
+
+ menu: [
+ {
+ xtype:'menuitem',
+ text: 'noVNC',
+ iconCls: 'pve-itype-icon-novnc',
+ type: 'html5',
+ handler: function(button) {
+ var me = this.up('button');
+ PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
+ }
+ },
+ {
+ xterm: 'menuitem',
+ itemId: 'spicemenu',
+ text: 'SPICE',
+ type: 'vv',
+ iconCls: 'pve-itype-icon-virt-viewer',
+ handler: function(button) {
+ var me = this.up('button');
+ PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
+ }
+ },
+ {
+ text: 'xterm.js',
+ itemId: 'xtermjs',
+ iconCls: 'pve-itype-icon-xtermjs',
+ type: 'xtermjs',
+ handler: function(button) {
+ var me = this.up('button');
+ PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
+ }
+ }
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.callParent();
+ }
+});
+/* Button features:
+ * - observe selection changes to enable/disable the button using enableFn()
+ * - pop up confirmation dialog using confirmMsg()
+ *
+ * does this for the button and every menu item
+ */
+Ext.define('PVE.button.Split', {
+ extend: 'Ext.button.Split',
+ alias: 'widget.pveSplitButton',
+
+ // the selection model to observe
+ selModel: undefined,
+
+ // if 'false' handler will not be called (button disabled)
+ enableFn: function(record) { },
+
+ // function(record) or text
+ confirmMsg: false,
+
+ // take special care in confirm box (select no as default).
+ dangerous: false,
+
+ handlerWrapper: function(button, event) {
+ var me = this;
+ var rec, msg;
+ if (me.selModel) {
+ rec = me.selModel.getSelection()[0];
+ if (!rec || (me.enableFn(rec) === false)) {
+ return;
+ }
+ }
+
+ if (me.confirmMsg) {
+ msg = me.confirmMsg;
+ // confirMsg can be boolean or function
+ /*jslint confusion: true*/
+ if (Ext.isFunction(me.confirmMsg)) {
+ msg = me.confirmMsg(rec);
+ }
+ /*jslint confusion: false*/
+ Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
+ msg: msg,
+ buttons: Ext.Msg.YESNO,
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ me.realHandler(button, event, rec);
+ }
+ });
+ } else {
+ me.realHandler(button, event, rec);
+ }
+ },
+
+ initComponent: function() {
+ /*jslint confusion: true */
+
+ var me = this;
+
+ if (me.handler) {
+ me.realHandler = me.handler;
+ me.handler = me.handlerWrapper;
+ }
+
+ if (me.menu && me.menu.items) {
+ me.menu.items.forEach(function(item) {
+ if (item.handler) {
+ item.realHandler = item.handler;
+ item.handler = me.handlerWrapper;
+ }
+
+ if (item.selModel) {
+ me.mon(item.selModel, "selectionchange", function() {
+ var rec = item.selModel.getSelection()[0];
+ if (!rec || (item.enableFn(rec) === false )) {
+ item.setDisabled(true);
+ } else {
+ item.setDisabled(false);
+ }
+ });
+ }
+ });
+ }
+
+ me.callParent();
+
+ if (me.selModel) {
+
+ me.mon(me.selModel, "selectionchange", function() {
+ var rec = me.selModel.getSelection()[0];
+ if (!rec || (me.enableFn(rec) === false)) {
+ me.setDisabled(true);
+ } else {
+ me.setDisabled(false);
+ }
+ });
+ }
+ }
+});
+Ext.define('PVE.controller.StorageEdit', {
+ extend: 'Ext.app.ViewController',
+ alias: 'controller.storageEdit',
+ control: {
+ 'field[name=content]': {
+ change: function(field, value) {
+ var hasBackups = Ext.Array.contains(value, 'backup');
+ var maxfiles = this.lookupReference('maxfiles');
+ if (!maxfiles) {
+ return;
+ }
+
+ if (!hasBackups) {
+ // clear values which will never be submitted
+ maxfiles.reset();
+ }
+ maxfiles.setDisabled(!hasBackups);
+ }
+ }
+ }
+});
+Ext.define('PVE.qemu.CmdMenu', {
+ extend: 'Ext.menu.Menu',
+
+ showSeparator: false,
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var vmname = me.pveSelNode.data.name;
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + nodename + '/qemu/' + vmid + "/status/" + cmd,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ };
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var running = false;
+ var stopped = true;
+ var suspended = false;
+ var standalone = PVE.data.ResourceStore.getNodes().length < 2;
+
+ switch (me.pveSelNode.data.status) {
+ case 'running':
+ running = true;
+ stopped = false;
+ break;
+ case 'suspended':
+ stopped = false;
+ suspended = true;
+ break;
+ case 'paused':
+ stopped = false;
+ suspended = true;
+ break;
+ default: break;
+ }
+
+ me.title = "VM " + vmid;
+
+ me.items = [
+ {
+ text: gettext('Start'),
+ iconCls: 'fa fa-fw fa-play',
+ hidden: running || suspended,
+ disabled: running || suspended,
+ handler: function() {
+ vm_command('start');
+ }
+ },
+ {
+ text: gettext('Pause'),
+ iconCls: 'fa fa-fw fa-pause',
+ hidden: stopped || suspended,
+ disabled: stopped || suspended,
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('qmpause', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ vm_command('suspend');
+ });
+ }
+ },
+ {
+ text: gettext('Hibernate'),
+ iconCls: 'fa fa-fw fa-download',
+ hidden: stopped || suspended,
+ disabled: stopped || suspended,
+ tooltip: gettext('Suspend to disk'),
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('qmsuspend', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ vm_command('suspend', { todisk: 1 });
+ });
+ }
+ },
+ {
+ text: gettext('Resume'),
+ iconCls: 'fa fa-fw fa-play',
+ hidden: !suspended,
+ handler: function() {
+ vm_command('resume');
+ }
+ },
+ {
+ text: gettext('Shutdown'),
+ iconCls: 'fa fa-fw fa-power-off',
+ disabled: stopped || suspended,
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('qmshutdown', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ vm_command('shutdown');
+ });
+ }
+ },
+ {
+ text: gettext('Stop'),
+ iconCls: 'fa fa-fw fa-stop',
+ disabled: stopped,
+ tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('qmstop', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ vm_command("stop");
+ });
+ }
+ },
+ {
+ xtype: 'menuseparator',
+ hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone']
+ },
+ {
+ text: gettext('Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ hidden: standalone || !caps.vms['VM.Migrate'],
+ handler: function() {
+ var win = Ext.create('PVE.window.Migrate', {
+ vmtype: 'qemu',
+ nodename: nodename,
+ vmid: vmid
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: !caps.vms['VM.Clone'],
+ handler: function() {
+ PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'qemu');
+ }
+ },
+ {
+ text: gettext('Convert to template'),
+ iconCls: 'fa fa-fw fa-file-o',
+ hidden: !caps.vms['VM.Allocate'],
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('qmtemplate', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/qemu/' + vmid + '/template',
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ });
+ }
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Console'),
+ iconCls: 'fa fa-fw fa-terminal',
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, opts) {
+ var allowSpice = response.result.data.spice;
+ var allowXtermjs = response.result.data.serial;
+ var consoles = {
+ spice: allowSpice,
+ xtermjs: allowXtermjs
+ };
+ PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname);
+ }
+ });
+ }
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.lxc.CmdMenu', {
+ extend: 'Ext.menu.Menu',
+
+ showSeparator: false,
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no CT ID specified";
+ }
+ var vmname = me.pveSelNode.data.name;
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + nodename + '/lxc/' + vmid + "/status/" + cmd,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ };
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var running = false;
+ var stopped = true;
+ var suspended = false;
+ var standalone = PVE.data.ResourceStore.getNodes().length < 2;
+
+ switch (me.pveSelNode.data.status) {
+ case 'running':
+ running = true;
+ stopped = false;
+ break;
+ case 'paused':
+ stopped = false;
+ suspended = true;
+ break;
+ default: break;
+ }
+
+ me.title = 'CT ' + vmid;
+
+ me.items = [
+ {
+ text: gettext('Start'),
+ iconCls: 'fa fa-fw fa-play',
+ disabled: running,
+ handler: function() {
+ vm_command('start');
+ }
+ },
+// {
+// text: gettext('Suspend'),
+// iconCls: 'fa fa-fw fa-pause',
+// hidde: suspended,
+// disabled: stopped || suspended,
+// handler: function() {
+// var msg = Proxmox.Utils.format_task_description('vzsuspend', vmid);
+// Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+// if (btn !== 'yes') {
+// return;
+// }
+//
+// vm_command('suspend');
+// });
+// }
+// },
+// {
+// text: gettext('Resume'),
+// iconCls: 'fa fa-fw fa-play',
+// hidden: !suspended,
+// handler: function() {
+// vm_command('resume');
+// }
+// },
+ {
+ text: gettext('Shutdown'),
+ iconCls: 'fa fa-fw fa-power-off',
+ disabled: stopped || suspended,
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('vzshutdown', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ vm_command('shutdown');
+ });
+ }
+ },
+ {
+ text: gettext('Stop'),
+ iconCls: 'fa fa-fw fa-stop',
+ disabled: stopped,
+ tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('vzstop', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ vm_command("stop");
+ });
+ }
+ },
+ {
+ xtype: 'menuseparator',
+ hidden: standalone || !caps.vms['VM.Migrate']
+ },
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: !caps.vms['VM.Clone'],
+ handler: function() {
+ PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'lxc');
+ }
+ },
+ {
+ text: gettext('Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ hidden: standalone || !caps.vms['VM.Migrate'],
+ handler: function() {
+ var win = Ext.create('PVE.window.Migrate', {
+ vmtype: 'lxc',
+ nodename: nodename,
+ vmid: vmid
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Convert to template'),
+ iconCls: 'fa fa-fw fa-file-o',
+ handler: function() {
+ var msg = Proxmox.Utils.format_task_description('vztemplate', vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/lxc/' + vmid + '/template',
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ });
+ }
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Console'),
+ iconCls: 'fa fa-fw fa-terminal',
+ handler: function() {
+ PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname);
+ }
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.node.CmdMenu', {
+ extend: 'Ext.menu.Menu',
+ xtype: 'nodeCmdMenu',
+
+ showSeparator: false,
+
+ items: [
+ {
+ text: gettext('Create VM'),
+ itemId: 'createvm',
+ iconCls: 'fa fa-desktop',
+ handler: function() {
+ var me = this.up('menu');
+ var wiz = Ext.create('PVE.qemu.CreateWizard', {
+ nodename: me.nodename
+ });
+ wiz.show();
+ }
+ },
+ {
+ text: gettext('Create CT'),
+ itemId: 'createct',
+ iconCls: 'fa fa-cube',
+ handler: function() {
+ var me = this.up('menu');
+ var wiz = Ext.create('PVE.lxc.CreateWizard', {
+ nodename: me.nodename
+ });
+ wiz.show();
+ }
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Bulk Start'),
+ itemId: 'bulkstart',
+ iconCls: 'fa fa-fw fa-play',
+ handler: function() {
+ var me = this.up('menu');
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: me.nodename,
+ title: gettext('Bulk Start'),
+ btnText: gettext('Start'),
+ action: 'startall'
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Bulk Stop'),
+ itemId: 'bulkstop',
+ iconCls: 'fa fa-fw fa-stop',
+ handler: function() {
+ var me = this.up('menu');
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: me.nodename,
+ title: gettext('Bulk Stop'),
+ btnText: gettext('Stop'),
+ action: 'stopall'
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Bulk Migrate'),
+ itemId: 'bulkmigrate',
+ iconCls: 'fa fa-fw fa-send-o',
+ handler: function() {
+ var me = this.up('menu');
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: me.nodename,
+ title: gettext('Bulk Migrate'),
+ btnText: gettext('Migrate'),
+ action: 'migrateall'
+ });
+ win.show();
+ }
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Shell'),
+ itemId: 'shell',
+ iconCls: 'fa fa-fw fa-terminal',
+ handler: function() {
+ var me = this.up('menu');
+ PVE.Utils.openDefaultConsoleWindow(true, 'shell', undefined, me.nodename, undefined);
+ }
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Wake-on-LAN'),
+ itemId: 'wakeonlan',
+ iconCls: 'fa fa-fw fa-power-off',
+ handler: function() {
+ var me = this.up('menu');
+ Proxmox.Utils.API2Request({
+ param: {},
+ url: '/nodes/' + me.nodename + '/wakeonlan',
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, opts) {
+ Ext.Msg.show({
+ title: 'Success',
+ icon: Ext.Msg.INFO,
+ msg: Ext.String.format(gettext("Wake on LAN packet send for '{0}': '{1}'"), me.nodename, response.result.data)
+ });
+ }
+ });
+ }
+ }
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw 'no nodename specified';
+ }
+
+ me.title = gettext('Node') + " '" + me.nodename + "'";
+ me.callParent();
+
+ var caps = Ext.state.Manager.get('GuiCap');
+ // disable not allowed options
+ if (!caps.vms['VM.Allocate']) {
+ me.getComponent('createct').setDisabled(true);
+ me.getComponent('createvm').setDisabled(true);
+ }
+
+ if (!caps.nodes['Sys.PowerMgmt']) {
+ me.getComponent('bulkstart').setDisabled(true);
+ me.getComponent('bulkstop').setDisabled(true);
+ me.getComponent('bulkmigrate').setDisabled(true);
+ me.getComponent('wakeonlan').setDisabled(true);
+ }
+
+ if (!caps.nodes['Sys.Console']) {
+ me.getComponent('shell').setDisabled(true);
+ }
+
+ if (me.pveSelNode.data.running) {
+ me.getComponent('wakeonlan').setDisabled(true);
+ }
+ }
+});
+Ext.define('PVE.noVncConsole', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNoVncConsole',
+
+ nodename: undefined,
+
+ vmid: undefined,
+
+ cmd: undefined,
+
+ consoleType: undefined, // lxc, kvm, shell, cmd
+
+ layout: 'fit',
+
+ xtermjs: false,
+
+ border: false,
+
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.consoleType) {
+ throw "no console type specified";
+ }
+
+ if (!me.vmid && me.consoleType !== 'shell' && me.consoleType !== 'cmd') {
+ throw "no VM ID specified";
+ }
+
+ // always use same iframe, to avoid running several noVnc clients
+ // at same time (to avoid performance problems)
+ var box = Ext.create('Ext.ux.IFrame', { itemid : "vncconsole" });
+
+ var type = me.xtermjs ? 'xtermjs' : 'novnc';
+ Ext.apply(me, {
+ items: box,
+ listeners: {
+ activate: function() {
+ var queryDict = {
+ console: me.consoleType, // kvm, lxc, upgrade or shell
+ vmid: me.vmid,
+ node: me.nodename,
+ cmd: me.cmd,
+ resize: 'scale'
+ };
+ queryDict[type] = 1;
+ PVE.Utils.cleanEmptyObjectKeys(queryDict);
+ var url = '/?' + Ext.Object.toQueryString(queryDict);
+ box.load(url);
+ }
+ }
+ });
+
+ me.callParent();
+
+ me.on('afterrender', function() {
+ me.focus();
+ });
+ }
+});
+
+Ext.define('PVE.data.PermPathStore', {
+ extend: 'Ext.data.Store',
+ alias: 'store.pvePermPath',
+ fields: [ 'value' ],
+ autoLoad: false,
+ data: [
+ {'value': '/'},
+ {'value': '/access'},
+ {'value': '/nodes'},
+ {'value': '/pool'},
+ {'value': '/storage'},
+ {'value': '/vms'}
+ ],
+
+ constructor: function(config) {
+ var me = this;
+
+ config = config || {};
+
+ me.callParent([config]);
+
+ me.suspendEvents();
+ PVE.data.ResourceStore.each(function(record) {
+ switch (record.get('type')) {
+ case 'node':
+ me.add({value: '/nodes/' + record.get('text')});
+ break;
+
+ case 'qemu':
+ me.add({value: '/vms/' + record.get('vmid')});
+ break;
+
+ case 'lxc':
+ me.add({value: '/vms/' + record.get('vmid')});
+ break;
+
+ case 'storage':
+ me.add({value: '/storage/' + record.get('storage')});
+ break;
+ case 'pool':
+ me.add({value: '/pool/' + record.get('pool')});
+ break;
+ }
+ });
+ me.resumeEvents();
+
+ me.fireEvent('refresh', me);
+ me.fireEvent('datachanged', me);
+
+ me.sort({
+ property: 'value',
+ direction: 'ASC'
+ });
+ }
+});
+Ext.define('PVE.data.ResourceStore', {
+ extend: 'Proxmox.data.UpdateStore',
+ singleton: true,
+
+ findVMID: function(vmid) {
+ var me = this, i;
+
+ return (me.findExact('vmid', parseInt(vmid, 10)) >= 0);
+ },
+
+ // returns the cached data from all nodes
+ getNodes: function() {
+ var me = this;
+
+ var nodes = [];
+ me.each(function(record) {
+ if (record.get('type') == "node") {
+ nodes.push( record.getData() );
+ }
+ });
+
+ return nodes;
+ },
+
+ storageIsShared: function(storage_path) {
+ var me = this;
+
+ var index = me.findExact('id', storage_path);
+
+ return me.getAt(index).data.shared;
+ },
+
+ guestNode: function(vmid) {
+ var me = this;
+
+ var index = me.findExact('vmid', parseInt(vmid, 10));
+
+ return me.getAt(index).data.node;
+ },
+
+ constructor: function(config) {
+ // fixme: how to avoid those warnings
+ /*jslint confusion: true */
+
+ var me = this;
+
+ config = config || {};
+
+ var field_defaults = {
+ type: {
+ header: gettext('Type'),
+ type: 'string',
+ renderer: PVE.Utils.render_resource_type,
+ sortable: true,
+ hideable: false,
+ width: 100
+ },
+ id: {
+ header: 'ID',
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 80
+ },
+ running: {
+ header: gettext('Online'),
+ type: 'boolean',
+ renderer: Proxmox.Utils.format_boolean,
+ hidden: true,
+ convert: function(value, record) {
+ var info = record.data;
+ return (Ext.isNumeric(info.uptime) && (info.uptime > 0));
+ }
+ },
+ text: {
+ header: gettext('Description'),
+ type: 'string',
+ sortable: true,
+ width: 200,
+ convert: function(value, record) {
+ var info = record.data;
+ var text;
+
+ if (value) {
+ return value;
+ }
+
+ if (Ext.isNumeric(info.vmid) && info.vmid > 0) {
+ text = String(info.vmid);
+ if (info.name) {
+ text += " (" + info.name + ')';
+ }
+ } else { // node, pool, storage
+ text = info[info.type] || info.id;
+ if (info.node && info.type !== 'node') {
+ text += " (" + info.node + ")";
+ }
+ }
+
+ return text;
+ }
+ },
+ vmid: {
+ header: 'VMID',
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ width: 80
+ },
+ name: {
+ header: gettext('Name'),
+ hidden: true,
+ sortable: true,
+ type: 'string'
+ },
+ disk: {
+ header: gettext('Disk usage'),
+ type: 'integer',
+ renderer: PVE.Utils.render_disk_usage,
+ sortable: true,
+ width: 100,
+ hidden: true
+ },
+ diskuse: {
+ header: gettext('Disk usage') + " %",
+ type: 'number',
+ sortable: true,
+ renderer: PVE.Utils.render_disk_usage_percent,
+ width: 100,
+ calculate: PVE.Utils.calculate_disk_usage,
+ sortType: 'asFloat'
+ },
+ maxdisk: {
+ header: gettext('Disk size'),
+ type: 'integer',
+ renderer: PVE.Utils.render_size,
+ sortable: true,
+ hidden: true,
+ width: 100
+ },
+ mem: {
+ header: gettext('Memory usage'),
+ type: 'integer',
+ renderer: PVE.Utils.render_mem_usage,
+ sortable: true,
+ hidden: true,
+ width: 100
+ },
+ memuse: {
+ header: gettext('Memory usage') + " %",
+ type: 'number',
+ renderer: PVE.Utils.render_mem_usage_percent,
+ calculate: PVE.Utils.calculate_mem_usage,
+ sortType: 'asFloat',
+ sortable: true,
+ width: 100
+ },
+ maxmem: {
+ header: gettext('Memory size'),
+ type: 'integer',
+ renderer: PVE.Utils.render_size,
+ hidden: true,
+ sortable: true,
+ width: 100
+ },
+ cpu: {
+ header: gettext('CPU usage'),
+ type: 'float',
+ renderer: PVE.Utils.render_cpu,
+ sortable: true,
+ width: 100
+ },
+ maxcpu: {
+ header: gettext('maxcpu'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ width: 60
+ },
+ diskread: {
+ header: gettext('Total Disk Read'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100
+ },
+ diskwrite: {
+ header: gettext('Total Disk Write'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100
+ },
+ netin: {
+ header: gettext('Total NetIn'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100
+ },
+ netout: {
+ header: gettext('Total NetOut'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100
+ },
+ template: {
+ header: gettext('Template'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ width: 60
+ },
+ uptime: {
+ header: gettext('Uptime'),
+ type: 'integer',
+ renderer: Proxmox.Utils.render_uptime,
+ sortable: true,
+ width: 110
+ },
+ node: {
+ header: gettext('Node'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110
+ },
+ storage: {
+ header: gettext('Storage'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110
+ },
+ pool: {
+ header: gettext('Pool'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110
+ },
+ hastate: {
+ header: gettext('HA State'),
+ type: 'string',
+ defaultValue: 'unmanaged',
+ hidden: true,
+ sortable: true
+ },
+ status: {
+ header: gettext('Status'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110
+ },
+ lock: {
+ header: gettext('Lock'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110
+ }
+ };
+
+ var fields = [];
+ var fieldNames = [];
+ Ext.Object.each(field_defaults, function(key, value) {
+ var field = {name: key, type: value.type};
+ if (Ext.isDefined(value.convert)) {
+ field.convert = value.convert;
+ }
+
+ if (Ext.isDefined(value.calculate)) {
+ field.calculate = value.calculate;
+ }
+
+ if (Ext.isDefined(value.defaultValue)) {
+ field.defaultValue = value.defaultValue;
+ }
+
+ fields.push(field);
+ fieldNames.push(key);
+ });
+
+ Ext.define('PVEResources', {
+ extend: "Ext.data.Model",
+ fields: fields,
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/resources'
+ }
+ });
+
+ Ext.define('PVETree', {
+ extend: "Ext.data.Model",
+ fields: fields,
+ proxy: { type: 'memory' }
+ });
+
+ Ext.apply(config, {
+ storeid: 'PVEResources',
+ model: 'PVEResources',
+ defaultColumns: function() {
+ var res = [];
+ Ext.Object.each(field_defaults, function(field, info) {
+ var fi = Ext.apply({ dataIndex: field }, info);
+ res.push(fi);
+ });
+ return res;
+ },
+ fieldNames: fieldNames
+ });
+
+ me.callParent([config]);
+ }
+});
+Ext.define('pve-domains', {
+ extend: "Ext.data.Model",
+ fields: [
+ 'realm', 'type', 'comment', 'default', 'tfa',
+ {
+ name: 'descr',
+ // Note: We use this in the RealmComboBox.js (see Bug #125)
+ convert: function(value, record) {
+ if (value) {
+ return value;
+ }
+
+ var info = record.data;
+ // return realm if there is no comment
+ var text = info.comment || info.realm;
+
+ if (info.tfa) {
+ text += " (+ " + info.tfa + ")";
+ }
+
+ return Ext.String.htmlEncode(text);
+ }
+ }
+ ],
+ idProperty: 'realm',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/domains"
+ }
+});
+Ext.define('pve-rrd-node', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name:'cpu',
+ // percentage
+ convert: function(value) {
+ return value*100;
+ }
+ },
+ {
+ name:'iowait',
+ // percentage
+ convert: function(value) {
+ return value*100;
+ }
+ },
+ 'loadavg',
+ 'maxcpu',
+ 'memtotal',
+ 'memused',
+ 'netin',
+ 'netout',
+ 'roottotal',
+ 'rootused',
+ 'swaptotal',
+ 'swapused',
+ { type: 'date', dateFormat: 'timestamp', name: 'time' }
+ ]
+});
+
+Ext.define('pve-rrd-guest', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name:'cpu',
+ // percentage
+ convert: function(value) {
+ return value*100;
+ }
+ },
+ 'maxcpu',
+ 'netin',
+ 'netout',
+ 'mem',
+ 'maxmem',
+ 'disk',
+ 'maxdisk',
+ 'diskread',
+ 'diskwrite',
+ { type: 'date', dateFormat: 'timestamp', name: 'time' }
+ ]
+});
+
+Ext.define('pve-rrd-storage', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'used',
+ 'total',
+ { type: 'date', dateFormat: 'timestamp', name: 'time' }
+ ]
+});
+Ext.define('PVE.form.VlanField', {
+ extend: 'Ext.form.field.Number',
+ alias: ['widget.pveVlanField'],
+
+ deleteEmpty: false,
+
+ emptyText: 'no VLAN',
+
+ fieldLabel: gettext('VLAN Tag'),
+
+ allowBlank: true,
+
+ getSubmitData: function() {
+ var me = this,
+ data = null,
+ val;
+ if (!me.disabled && me.submitValue) {
+ val = me.getSubmitValue();
+ if (val) {
+ data = {};
+ data[me.getName()] = val;
+ } else if (me.deleteEmpty) {
+ data = {};
+ data['delete'] = me.getName();
+ }
+ }
+ return data;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ Ext.apply(me, {
+ minValue: 1,
+ maxValue: 4094
+ });
+
+ me.callParent();
+ }
+});
+// boolean type including 'Default' (delete property from file)
+Ext.define('PVE.form.Boolean', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.booleanfield'],
+ comboItems: [
+ ['__default__', gettext('Default')],
+ [1, gettext('Yes')],
+ [0, gettext('No')]
+ ]
+});
+Ext.define('PVE.form.CompressionSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveCompressionSelector'],
+ comboItems: [
+ ['0', Proxmox.Utils.noneText],
+ ['lzo', 'LZO (' + gettext('fast') + ')'],
+ ['gzip', 'GZIP (' + gettext('good') + ')']
+ ]
+});
+Ext.define('PVE.form.PoolSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pvePoolSelector'],
+
+ allowBlank: false,
+ valueField: 'poolid',
+ displayField: 'poolid',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-pools',
+ sorters: 'poolid'
+ });
+
+ Ext.apply(me, {
+ store: store,
+ autoSelect: false,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Pool'),
+ sortable: true,
+ dataIndex: 'poolid',
+ flex: 1
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+
+}, function() {
+
+ Ext.define('pve-pools', {
+ extend: 'Ext.data.Model',
+ fields: [ 'poolid', 'comment' ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/pools"
+ },
+ idProperty: 'poolid'
+ });
+
+});
+Ext.define('PVE.form.PrivilegesSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ xtype: 'pvePrivilegesSelector',
+
+ multiSelect: true,
+
+ initComponent: function() {
+ var me = this;
+
+ // So me.store is available.
+ me.callParent();
+
+ Proxmox.Utils.API2Request({
+ url: '/access/roles/Administrator',
+ method: 'GET',
+ success: function(response, options) {
+ var data = [], key;
+ /*jslint forin: true */
+ for (key in response.result.data) {
+ data.push([key, key]);
+ }
+ /*jslint forin: false */
+
+ me.store.setData(data);
+
+ me.store.sort({
+ property: 'key',
+ direction: 'ASC'
+ });
+ },
+
+ failure: function (response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ }
+});
+Ext.define('pve-groups', {
+ extend: 'Ext.data.Model',
+ fields: [ 'groupid', 'comment' ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/groups"
+ },
+ idProperty: 'groupid'
+});
+
+Ext.define('PVE.form.GroupSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ xtype: 'pveGroupSelector',
+
+ allowBlank: false,
+ autoSelect: false,
+ valueField: 'groupid',
+ displayField: 'groupid',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Group'),
+ sortable: true,
+ dataIndex: 'groupid',
+ flex: 1
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ ]
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-groups',
+ sorters: [{
+ property: 'groupid'
+ }]
+ });
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+});
+Ext.define('PVE.form.UserSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveUserSelector'],
+
+ allowBlank: false,
+ autoSelect: false,
+ valueField: 'userid',
+ displayField: 'userid',
+
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-users',
+ sorters: [{
+ property: 'userid'
+ }]
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('User'),
+ sortable: true,
+ dataIndex: 'userid',
+ flex: 1
+ },
+ {
+ header: gettext('Name'),
+ sortable: true,
+ renderer: PVE.Utils.render_full_name,
+ dataIndex: 'firstname',
+ flex: 1
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+
+ store.load({ params: { enabled: 1 }});
+ }
+
+}, function() {
+
+ Ext.define('pve-users', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'userid', 'firstname', 'lastname' , 'email', 'comment',
+ { type: 'boolean', name: 'enable' },
+ { type: 'date', dateFormat: 'timestamp', name: 'expire' }
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/users"
+ },
+ idProperty: 'userid'
+ });
+
+});
+
+
+Ext.define('PVE.form.RoleSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveRoleSelector'],
+
+ allowBlank: false,
+ autoSelect: false,
+ valueField: 'roleid',
+ displayField: 'roleid',
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-roles',
+ sorters: [{
+ property: 'roleid'
+ }]
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Role'),
+ sortable: true,
+ dataIndex: 'roleid',
+ flex: 1
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+
+}, function() {
+
+ Ext.define('pve-roles', {
+ extend: 'Ext.data.Model',
+ fields: [ 'roleid', 'privs' ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/roles"
+ },
+ idProperty: 'roleid'
+ });
+
+});
+Ext.define('PVE.form.GuestIDSelector', {
+ extend: 'Ext.form.field.Number',
+ alias: 'widget.pveGuestIDSelector',
+
+ allowBlank: false,
+
+ minValue: 100,
+
+ maxValue: 999999999,
+
+ validateExists: undefined,
+
+ loadNextFreeID: false,
+
+ guestType: undefined,
+
+ validator: function(value) {
+ var me = this;
+
+ if (!Ext.isNumeric(value) ||
+ value < me.minValue ||
+ value > me.maxValue) {
+ // check is done by ExtJS
+ return true;
+ }
+
+ if (me.validateExists === true && !me.exists) {
+ return me.unknownID;
+ }
+
+ if (me.validateExists === false && me.exists) {
+ return me.inUseID;
+ }
+
+ return true;
+ },
+
+ initComponent: function() {
+ var me = this;
+ var label = '{0} ID';
+ var unknownID = gettext('This {0} ID does not exists');
+ var inUseID = gettext('This {0} ID is already in use');
+ var type = 'CT/VM';
+
+ if (me.guestType === 'lxc') {
+ type = 'CT';
+ } else if (me.guestType === 'qemu') {
+ type = 'VM';
+ }
+
+ me.label = Ext.String.format(label, type);
+ me.unknownID = Ext.String.format(unknownID, type);
+ me.inUseID = Ext.String.format(inUseID, type);
+
+ Ext.apply(me, {
+ fieldLabel: me.label,
+ listeners: {
+ 'change': function(field, newValue, oldValue) {
+ if (!Ext.isDefined(me.validateExists)) {
+ return;
+ }
+ Proxmox.Utils.API2Request({
+ params: { vmid: newValue },
+ url: '/cluster/nextid',
+ method: 'GET',
+ success: function(response, opts) {
+ me.exists = false;
+ me.validate();
+ },
+ failure: function(response, opts) {
+ me.exists = true;
+ me.validate();
+ }
+ });
+ }
+ }
+ });
+
+ me.callParent();
+
+ if (me.loadNextFreeID) {
+ Proxmox.Utils.API2Request({
+ url: '/cluster/nextid',
+ method: 'GET',
+ success: function(response, opts) {
+ me.setRawValue(response.result.data);
+ }
+ });
+ }
+ }
+});
+Ext.define('PVE.form.MemoryField', {
+ extend: 'Ext.form.field.Number',
+ alias: 'widget.pveMemoryField',
+
+ allowBlank: false,
+
+ hotplug: false,
+
+ minValue: 32,
+
+ maxValue: 4178944,
+
+ step: 32,
+
+ value: '512', // qm default
+
+ allowDecimals: false,
+
+ allowExponential: false,
+
+ computeUpDown: function(value) {
+ var me = this;
+
+ if (!me.hotplug) {
+ return { up: value + me.step, down: value - me.step };
+ }
+
+ var dimm_size = 512;
+ var prev_dimm_size = 0;
+ var min_size = 1024;
+ var current_size = min_size;
+ var value_up = min_size;
+ var value_down = min_size;
+ var value_start = min_size;
+
+ var i, j;
+ for (j = 0; j < 9; j++) {
+ for (i = 0; i < 32; i++) {
+ if ((value >= current_size) && (value < (current_size + dimm_size))) {
+ value_start = current_size;
+ value_up = current_size + dimm_size;
+ value_down = current_size - ((i === 0) ? prev_dimm_size : dimm_size);
+ }
+ current_size += dimm_size;
+ }
+ prev_dimm_size = dimm_size;
+ dimm_size = dimm_size*2;
+ }
+
+ return { up: value_up, down: value_down, start: value_start };
+ },
+
+ onSpinUp: function() {
+ var me = this;
+ if (!me.readOnly) {
+ var res = me.computeUpDown(me.getValue());
+ me.setValue(Ext.Number.constrain(res.up, me.minValue, me.maxValue));
+ }
+ },
+
+ onSpinDown: function() {
+ var me = this;
+ if (!me.readOnly) {
+ var res = me.computeUpDown(me.getValue());
+ me.setValue(Ext.Number.constrain(res.down, me.minValue, me.maxValue));
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.hotplug) {
+ me.minValue = 1024;
+
+ me.on('blur', function(field) {
+ var value = me.getValue();
+ var res = me.computeUpDown(value);
+ if (value === res.start || value === res.up || value === res.down) {
+ return;
+ }
+ field.setValue(res.up);
+ });
+ }
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.form.NetworkCardSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: 'widget.pveNetworkCardSelector',
+ comboItems: [
+ ['e1000', 'Intel E1000'],
+ ['virtio', 'VirtIO (' + gettext('paravirtualized') + ')'],
+ ['rtl8139', 'Realtek RTL8139'],
+ ['vmxnet3', 'VMware vmxnet3']
+ ]
+});
+Ext.define('PVE.form.DiskFormatSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: 'widget.pveDiskFormatSelector',
+ comboItems: [
+ ['raw', gettext('Raw disk image') + ' (raw)'],
+ ['qcow2', gettext('QEMU image format') + ' (qcow2)'],
+ ['vmdk', gettext('VMware image format') + ' (vmdk)']
+ ]
+});
+Ext.define('PVE.form.DiskSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ xtype: 'pveDiskSelector',
+
+ // can be
+ // undefined: all
+ // unused: only unused
+ // journal_disk: all disks with gpt
+ diskType: undefined,
+
+ valueField: 'devpath',
+ displayField: 'devpath',
+ emptyText: gettext('No Disks unused'),
+ listConfig: {
+ width: 600,
+ columns: [
+ {
+ header: gettext('Device'),
+ flex: 3,
+ sortable: true,
+ dataIndex: 'devpath'
+ },
+ {
+ header: gettext('Size'),
+ flex: 2,
+ sortable: false,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'size'
+ },
+ {
+ header: gettext('Serial'),
+ flex: 5,
+ sortable: true,
+ dataIndex: 'serial'
+ }
+ ]
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ filterOnLoad: true,
+ model: 'pve-disk-list',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/nodes/" + nodename + "/disks/list",
+ extraParams: { type: me.diskType }
+ },
+ sorters: [
+ {
+ property : 'devpath',
+ direction: 'ASC'
+ }
+ ]
+ });
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+}, function() {
+
+ Ext.define('pve-disk-list', {
+ extend: 'Ext.data.Model',
+ fields: [ 'devpath', 'used', { name: 'size', type: 'number'},
+ {name: 'osdid', type: 'number'},
+ 'vendor', 'model', 'serial'],
+ idProperty: 'devpath'
+ });
+});
+Ext.define('PVE.form.BusTypeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: 'widget.pveBusSelector',
+
+ noVirtIO: false,
+
+ initComponent: function() {
+ var me = this;
+
+ me.comboItems = [['ide', 'IDE'], ['sata', 'SATA']];
+
+ if (!me.noVirtIO) {
+ me.comboItems.push(['virtio', 'VirtIO Block']);
+ }
+
+ me.comboItems.push(['scsi', 'SCSI']);
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.form.ControllerSelector', {
+ extend: 'Ext.form.FieldContainer',
+ alias: 'widget.pveControllerSelector',
+
+ statics: {
+ maxIds: {
+ ide: 3,
+ sata: 5,
+ virtio: 15,
+ scsi: 13
+ }
+ },
+
+ noVirtIO: false,
+
+ vmconfig: {}, // used to check for existing devices
+
+ sortByPreviousUsage: function(vmconfig, controllerList) {
+
+ var usedControllers = Ext.clone(PVE.form.ControllerSelector.maxIds);
+
+ var type;
+ for (type in usedControllers) {
+ if(usedControllers.hasOwnProperty(type)) {
+ usedControllers[type] = 0;
+ }
+ }
+
+ var property;
+ for (property in vmconfig) {
+ if (vmconfig.hasOwnProperty(property)) {
+ if (property.match(PVE.Utils.bus_match) && !vmconfig[property].match(/media=cdrom/)) {
+ var foundController = property.match(PVE.Utils.bus_match)[1];
+ usedControllers[foundController]++;
+ }
+ }
+ }
+
+ var vmDefaults = PVE.qemu.OSDefaults[vmconfig.ostype];
+
+ var sortPriority = vmDefaults && vmDefaults.busPriority
+ ? vmDefaults.busPriority : PVE.qemu.OSDefaults.generic;
+
+ var sortedList = Ext.clone(controllerList);
+ sortedList.sort(function(a,b) {
+ if (usedControllers[b] == usedControllers[a]) {
+ return sortPriority[b] - sortPriority[a];
+ }
+ return usedControllers[b] - usedControllers[a];
+ });
+
+ return sortedList;
+ },
+
+ setVMConfig: function(vmconfig, autoSelect) {
+ var me = this;
+
+ me.vmconfig = Ext.apply({}, vmconfig);
+
+ var clist = ['ide', 'virtio', 'scsi', 'sata'];
+ var bussel = me.down('field[name=controller]');
+ var deviceid = me.down('field[name=deviceid]');
+
+ if (autoSelect === 'cdrom') {
+ clist = ['ide', 'scsi', 'sata'];
+ if (!Ext.isDefined(me.vmconfig.ide2)) {
+ bussel.setValue('ide');
+ deviceid.setValue(2);
+ return;
+ }
+ } else {
+ // in most cases we want to add a disk to the same controller
+ // we previously used
+ clist = me.sortByPreviousUsage(me.vmconfig, clist);
+ }
+
+ Ext.Array.each(clist, function(controller) {
+ var confid, i;
+ bussel.setValue(controller);
+ for (i = 0; i <= PVE.form.ControllerSelector.maxIds[controller]; i++) {
+ confid = controller + i.toString();
+ if (!Ext.isDefined(me.vmconfig[confid])) {
+ deviceid.setValue(i);
+ return false; // break
+ }
+ }
+ });
+ deviceid.validate();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ Ext.apply(me, {
+ fieldLabel: gettext('Bus/Device'),
+ layout: 'hbox',
+ defaults: {
+ hideLabel: true
+ },
+ items: [
+ {
+ xtype: 'pveBusSelector',
+ name: 'controller',
+ value: PVE.qemu.OSDefaults.generic.busType,
+ noVirtIO: me.noVirtIO,
+ allowBlank: false,
+ flex: 2,
+ listeners: {
+ change: function(t, value) {
+ if (!value) {
+ return;
+ }
+ var field = me.down('field[name=deviceid]');
+ field.setMaxValue(PVE.form.ControllerSelector.maxIds[value]);
+ field.validate();
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'deviceid',
+ minValue: 0,
+ maxValue: PVE.form.ControllerSelector.maxIds.ide,
+ value: '0',
+ flex: 1,
+ allowBlank: false,
+ validator: function(value) {
+ /*jslint confusion: true */
+ if (!me.rendered) {
+ return;
+ }
+ var field = me.down('field[name=controller]');
+ var controller = field.getValue();
+ var confid = controller + value;
+ if (Ext.isDefined(me.vmconfig[confid])) {
+ return "This device is already in use.";
+ }
+ return true;
+ }
+ }
+ ]
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.form.EmailNotificationSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveEmailNotificationSelector'],
+ comboItems: [
+ ['always', gettext('Always')],
+ ['failure', gettext('On failure only')]
+ ]
+});
+/*global Proxmox*/
+Ext.define('PVE.form.RealmComboBox', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: ['widget.pveRealmComboBox'],
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ view.store.on('load', this.onLoad, view);
+ },
+
+ onLoad: function(store, records, success) {
+ if (!success) {
+ return;
+ }
+ var me = this;
+ var val = me.getValue();
+ if (!val || !me.store.findRecord('realm', val)) {
+ var def = 'pam';
+ Ext.each(records, function(rec) {
+ if (rec.data && rec.data['default']) {
+ def = rec.data.realm;
+ }
+ });
+ me.setValue(def);
+ }
+ }
+ },
+
+ fieldLabel: gettext('Realm'),
+ name: 'realm',
+ queryMode: 'local',
+ allowBlank: false,
+ editable: false,
+ forceSelection: true,
+ autoSelect: false,
+ triggerAction: 'all',
+ valueField: 'realm',
+ displayField: 'descr',
+ getState: function() {
+ return { value: this.getValue() };
+ },
+ applyState : function(state) {
+ if (state && state.value) {
+ this.setValue(state.value);
+ }
+ },
+ stateEvents: [ 'select' ],
+ stateful: true, // last chosen auth realm is saved between page reloads
+ id: 'pveloginrealm', // We need stable ids when using stateful, not autogenerated
+ stateID: 'pveloginrealm',
+
+ needOTP: function(realm) {
+ var me = this;
+ // use exact match
+ var rec = me.store.findRecord('realm', realm, 0, false, false, true);
+ return rec && rec.data && rec.data.tfa ? rec.data.tfa : undefined;
+ },
+
+ store: {
+ model: 'pve-domains',
+ autoLoad: true
+ }
+});
+/*
+ * Top left combobox, used to select a view of the underneath RessourceTree
+ */
+Ext.define('PVE.form.ViewSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: ['widget.pveViewSelector'],
+
+ editable: false,
+ allowBlank: false,
+ forceSelection: true,
+ autoSelect: false,
+ valueField: 'key',
+ displayField: 'value',
+ hideLabel: true,
+ queryMode: 'local',
+
+ initComponent: function() {
+ var me = this;
+
+ var default_views = {
+ server: {
+ text: gettext('Server View'),
+ groups: ['node']
+ },
+ folder: {
+ text: gettext('Folder View'),
+ groups: ['type']
+ },
+ storage: {
+ text: gettext('Storage View'),
+ groups: ['node'],
+ filterfn: function(node) {
+ return node.data.type === 'storage' || node.data.type === 'node';
+ }
+ },
+ pool: {
+ text: gettext('Pool View'),
+ groups: ['pool'],
+ // Pool View only lists VMs and Containers
+ filterfn: function(node) {
+ return node.data.type === 'qemu' || node.data.type === 'lxc' || node.data.type === 'openvz' ||
+ node.data.type === 'pool';
+ }
+ }
+ };
+
+ var groupdef = [];
+ Ext.Object.each(default_views, function(viewname, value) {
+ groupdef.push([viewname, value.text]);
+ });
+
+ var store = Ext.create('Ext.data.Store', {
+ model: 'KeyValue',
+ proxy: {
+ type: 'memory',
+ reader: 'array'
+ },
+ data: groupdef,
+ autoload: true
+ });
+
+ Ext.apply(me, {
+ store: store,
+ value: groupdef[0][0],
+ getViewFilter: function() {
+ var view = me.getValue();
+ return Ext.apply({ id: view }, default_views[view] || default_views.server);
+ },
+
+ getState: function() {
+ return { value: me.getValue() };
+ },
+
+ applyState : function(state, doSelect) {
+ var view = me.getValue();
+ if (state && state.value && (view != state.value)) {
+ var record = store.findRecord('key', state.value);
+ if (record) {
+ me.setValue(state.value, true);
+ if (doSelect) {
+ me.fireEvent('select', me, [record]);
+ }
+ }
+ }
+ },
+ stateEvents: [ 'select' ],
+ stateful: true,
+ stateId: 'pveview',
+ id: 'view'
+ });
+
+ me.callParent();
+
+ var statechange = function(sp, key, value) {
+ if (key === me.id) {
+ me.applyState(value, true);
+ }
+ };
+
+ var sp = Ext.state.Manager.getProvider();
+ me.mon(sp, 'statechange', statechange, me);
+ }
+});
+Ext.define('PVE.form.NodeSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveNodeSelector'],
+
+ // invalidate nodes which are offline
+ onlineValidator: false,
+
+ selectCurNode: false,
+
+ // do not allow those nodes (array)
+ disallowedNodes: undefined,
+
+ // only allow those nodes (array)
+ allowedNodes: undefined,
+ // set default value to empty array, else it inits it with
+ // null and after the store load it is an empty array,
+ // triggering dirtychange
+ value: [],
+ valueField: 'node',
+ displayField: 'node',
+ store: {
+ fields: [ 'node', 'cpu', 'maxcpu', 'mem', 'maxmem', 'uptime' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes'
+ },
+ sorters: [
+ {
+ property : 'node',
+ direction: 'ASC'
+ },
+ {
+ property : 'mem',
+ direction: 'DESC'
+ }
+ ]
+ },
+
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Node'),
+ dataIndex: 'node',
+ sortable: true,
+ hideable: false,
+ flex: 1
+ },
+ {
+ header: gettext('Memory usage') + " %",
+ renderer: PVE.Utils.render_mem_usage_percent,
+ sortable: true,
+ width: 100,
+ dataIndex: 'mem'
+ },
+ {
+ header: gettext('CPU usage'),
+ renderer: PVE.Utils.render_cpu,
+ sortable: true,
+ width: 100,
+ dataIndex: 'cpu'
+ }
+ ]
+ },
+
+ validator: function(value) {
+ /*jslint confusion: true */
+ var me = this;
+ if (!me.onlineValidator || (me.allowBlank && !value)) {
+ return true;
+ }
+
+ var offline = [];
+ var notAllowed = [];
+
+ Ext.Array.each(value.split(/\s*,\s*/), function(node) {
+ var rec = me.store.findRecord(me.valueField, node);
+ if (!(rec && rec.data) || rec.data.status !== 'online') {
+ offline.push(node);
+ } else if (me.allowedNodes && !Ext.Array.contains(me.allowedNodes, node)) {
+ notAllowed.push(node);
+ }
+ });
+
+ if (value && notAllowed.length !== 0) {
+ return "Node " + notAllowed.join(', ') + " is not allowed for this action!";
+ }
+
+ if (value && offline.length !== 0) {
+ return "Node " + offline.join(', ') + " seems to be offline!";
+ }
+ return true;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.selectCurNode && PVE.curSelectedNode && PVE.curSelectedNode.data.node) {
+ me.preferredValue = PVE.curSelectedNode.data.node;
+ }
+
+ me.callParent();
+ me.getStore().load();
+
+ // filter out disallowed nodes
+ me.getStore().addFilter(new Ext.util.Filter({
+ filterFn: function(item) {
+ if (Ext.isArray(me.disallowedNodes)) {
+ return !Ext.Array.contains(me.disallowedNodes, item.data.node);
+ } else {
+ return true;
+ }
+ }
+ }));
+
+ me.mon(me.getStore(), 'load', function(){
+ me.isValid();
+ });
+ }
+});
+Ext.define('PVE.form.FileSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: 'widget.pveFileSelector',
+
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+
+ listeners: {
+ afterrender: function() {
+ var me = this;
+ if (!me.disabled) {
+ me.setStorage(me.storage, me.nodename);
+ }
+ }
+ },
+
+ setStorage: function(storage, nodename) {
+ var me = this;
+
+ var change = false;
+ if (storage && (me.storage !== storage)) {
+ me.storage = storage;
+ change = true;
+ }
+
+ if (nodename && (me.nodename !== nodename)) {
+ me.nodename = nodename;
+ change = true;
+ }
+
+ if (!(me.storage && me.nodename && change)) {
+ return;
+ }
+
+ var url = '/api2/json/nodes/' + me.nodename + '/storage/' + me.storage + '/content';
+ if (me.storageContent) {
+ url += '?content=' + me.storageContent;
+ }
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: url
+ });
+
+ me.store.removeAll();
+ me.store.load();
+ },
+
+ setNodename: function(nodename) {
+ this.setStorage(undefined, nodename);
+ },
+
+ store: {
+ model: 'pve-storage-content'
+ },
+
+ allowBlank: false,
+ autoSelect: false,
+ valueField: 'volid',
+ displayField: 'text',
+
+ listConfig: {
+ width: 600,
+ columns: [
+ {
+ header: gettext('Name'),
+ dataIndex: 'text',
+ hideable: false,
+ flex: 1
+ },
+ {
+ header: gettext('Format'),
+ width: 60,
+ dataIndex: 'format'
+ },
+ {
+ header: gettext('Size'),
+ width: 100,
+ dataIndex: 'size',
+ renderer: Proxmox.Utils.format_size
+ }
+ ]
+ }
+});
+Ext.define('PVE.form.StorageSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: 'widget.pveStorageSelector',
+
+ allowBlank: false,
+ valueField: 'storage',
+ displayField: 'storage',
+ listConfig: {
+ width: 450,
+ columns: [
+ {
+ header: gettext('Name'),
+ dataIndex: 'storage',
+ hideable: false,
+ flex: 1
+ },
+ {
+ header: gettext('Type'),
+ width: 75,
+ dataIndex: 'type'
+ },
+ {
+ header: gettext('Avail'),
+ width: 90,
+ dataIndex: 'avail',
+ renderer: Proxmox.Utils.format_size
+ },
+ {
+ header: gettext('Capacity'),
+ width: 90,
+ dataIndex: 'total',
+ renderer: Proxmox.Utils.format_size
+ }
+ ]
+ },
+
+ reloadStorageList: function() {
+ var me = this;
+ if (!me.nodename) {
+ return;
+ }
+
+ var params = {
+ format: 1
+ };
+ var url = '/api2/json/nodes/' + me.nodename + '/storage';
+ if (me.storageContent) {
+ params.content = me.storageContent;
+ }
+ if (me.targetNode) {
+ params.target = me.targetNode;
+ params.enabled = 1; // skip disabled storages
+ }
+ me.store.setProxy({
+ type: 'proxmox',
+ url: url,
+ extraParams: params
+ });
+
+ me.store.load();
+
+ },
+
+ setTargetNode: function(targetNode) {
+ var me = this;
+
+ if (!targetNode || (me.targetNode === targetNode)) {
+ return;
+ }
+
+ me.targetNode = targetNode;
+
+ me.reloadStorageList();
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || (me.nodename === nodename)) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ me.reloadStorageList();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename;
+ me.nodename = undefined;
+
+ var store = Ext.create('Ext.data.Store', {
+ model: 'pve-storage-status',
+ sorters: {
+ property: 'storage',
+ order: 'DESC'
+ }
+ });
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+
+ if (nodename) {
+ me.setNodename(nodename);
+ }
+ }
+}, function() {
+
+ Ext.define('pve-storage-status', {
+ extend: 'Ext.data.Model',
+ fields: [ 'storage', 'active', 'type', 'avail', 'total' ],
+ idProperty: 'storage'
+ });
+
+});
+Ext.define('PVE.form.DiskStorageSelector', {
+ extend: 'Ext.container.Container',
+ alias: 'widget.pveDiskStorageSelector',
+
+ layout: 'fit',
+ defaults: {
+ margin: '0 0 5 0'
+ },
+
+ // the fieldLabel for the storageselector
+ storageLabel: gettext('Storage'),
+
+ // the content to show (e.g., images or rootdir)
+ storageContent: undefined,
+
+ // if true, selects the first available storage
+ autoSelect: false,
+
+ allowBlank: false,
+ emptyText: '',
+
+ // hides the selection field
+ // this is always hidden on creation,
+ // and only shown when the storage needs a selection and
+ // hideSelection is not true
+ hideSelection: undefined,
+
+ // hides the size field (e.g, for the efi disk dialog)
+ hideSize: false,
+
+ // sets the initial size value
+ // string because else we get a type confusion
+ defaultSize: '32',
+
+ changeStorage: function(f, value) {
+ var me = this;
+ var formatsel = me.getComponent('diskformat');
+ var hdfilesel = me.getComponent('hdimage');
+ var hdsizesel = me.getComponent('disksize');
+
+ // initial store load, and reset/deletion of the storage
+ if (!value) {
+ hdfilesel.setDisabled(true);
+ hdfilesel.setVisible(false);
+
+ formatsel.setDisabled(true);
+ return;
+ }
+
+ var rec = f.store.getById(value);
+ // if the storage is not defined, or valid,
+ // we cannot know what to enable/disable
+ if (!rec) {
+ return;
+ }
+
+ var selectformat = false;
+ if (rec.data.format) {
+ var format = rec.data.format[0]; // 0 is the formats, 1 the default in the backend
+ delete format.subvol; // we never need subvol in the gui
+ selectformat = (Ext.Object.getSize(format) > 1);
+ }
+
+ var select = !!rec.data.select_existing && !me.hideSelection;
+
+ formatsel.setDisabled(!selectformat);
+ formatsel.setValue(selectformat ? 'qcow2' : 'raw');
+
+ hdfilesel.setDisabled(!select);
+ hdfilesel.setVisible(select);
+ if (select) {
+ hdfilesel.setStorage(value);
+ }
+
+ hdsizesel.setDisabled(select || me.hideSize);
+ hdsizesel.setVisible(!select && !me.hideSize);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ var hdstorage = me.getComponent('hdstorage');
+ var hdfilesel = me.getComponent('hdimage');
+
+ hdstorage.setNodename(nodename);
+ hdfilesel.setNodename(nodename);
+ },
+
+ setDisabled: function(value) {
+ var me = this;
+ var hdstorage = me.getComponent('hdstorage');
+
+ // reset on disable
+ if (value) {
+ hdstorage.setValue();
+ }
+ hdstorage.setDisabled(value);
+
+ // disabling does not always fire this event and we do not need
+ // the value of the validity
+ hdstorage.fireEvent('validitychange');
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: 'pveStorageSelector',
+ itemId: 'hdstorage',
+ name: 'hdstorage',
+ reference: 'hdstorage',
+ fieldLabel: me.storageLabel,
+ nodename: me.nodename,
+ storageContent: me.storageContent,
+ disabled: me.disabled,
+ autoSelect: me.autoSelect,
+ allowBlank: me.allowBlank,
+ emptyText: me.emptyText,
+ listeners: {
+ change: {
+ fn: me.changeStorage,
+ scope: me
+ }
+ }
+ },
+ {
+ xtype: 'pveFileSelector',
+ name: 'hdimage',
+ reference: 'hdimage',
+ itemId: 'hdimage',
+ fieldLabel: gettext('Disk image'),
+ nodename: me.nodename,
+ disabled: true,
+ hidden: true
+ },
+ {
+ xtype: 'numberfield',
+ itemId: 'disksize',
+ reference: 'disksize',
+ name: 'disksize',
+ fieldLabel: gettext('Disk size') + ' (GiB)',
+ hidden: me.hideSize,
+ disabled: me.hideSize,
+ minValue: 0.001,
+ maxValue: 128*1024,
+ decimalPrecision: 3,
+ value: me.defaultSize,
+ allowBlank: false
+ },
+ {
+ xtype: 'pveDiskFormatSelector',
+ itemId: 'diskformat',
+ reference: 'diskformat',
+ name: 'diskformat',
+ fieldLabel: gettext('Format'),
+ nodename: me.nodename,
+ disabled: true,
+ hidden: me.storageContent === 'rootdir',
+ value: 'qcow2',
+ allowBlank: false
+ }
+ ];
+
+ // use it to disable the children but not ourself
+ me.disabled = false;
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.form.BridgeSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.PVE.form.BridgeSelector'],
+
+ bridgeType: 'any_bridge', // bridge, OVSBridge or any_bridge
+
+ store: {
+ fields: [ 'iface', 'active', 'type' ],
+ filterOnLoad: true,
+ sorters: [
+ {
+ property : 'iface',
+ direction: 'ASC'
+ }
+ ]
+ },
+ valueField: 'iface',
+ displayField: 'iface',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Bridge'),
+ dataIndex: 'iface',
+ hideable: false,
+ width: 100
+ },
+ {
+ header: gettext('Active'),
+ width: 60,
+ dataIndex: 'active',
+ renderer: Proxmox.Utils.format_boolean
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comments',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ ]
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || (me.nodename === nodename)) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/network?type=' +
+ me.bridgeType
+ });
+
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename;
+ me.nodename = undefined;
+
+ me.callParent();
+
+ me.setNodename(nodename);
+ }
+});
+
+Ext.define('PVE.form.PCISelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ xtype: 'pvePCISelector',
+
+ store: {
+ fields: [ 'id','vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev' ],
+ filterOnLoad: true,
+ sorters: [
+ {
+ property : 'id',
+ direction: 'ASC'
+ }
+ ]
+ },
+
+ autoSelect: false,
+ valueField: 'id',
+ displayField: 'id',
+
+ // can contain a load callback for the store
+ // useful to determine the state of the IOMMU
+ onLoadCallBack: undefined,
+
+ listConfig: {
+ width: 800,
+ columns: [
+ {
+ header: 'ID',
+ dataIndex: 'id',
+ width: 80
+ },
+ {
+ header: gettext('IOMMU Group'),
+ dataIndex: 'iommugroup',
+ width: 50
+ },
+ {
+ header: gettext('Vendor'),
+ dataIndex: 'vendor_name',
+ flex: 2
+ },
+ {
+ header: gettext('Device'),
+ dataIndex: 'device_name',
+ flex: 6
+ },
+ {
+ header: gettext('Mediated Devices'),
+ dataIndex: 'mdev',
+ flex: 1,
+ renderer: function(val) {
+ return Proxmox.Utils.format_boolean(!!val);
+ }
+ }
+ ]
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || (me.nodename === nodename)) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/hardware/pci'
+ });
+
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename;
+ me.nodename = undefined;
+
+ me.callParent();
+
+ if (me.onLoadCallBack !== undefined) {
+ me.mon(me.getStore(), 'load', me.onLoadCallBack);
+ }
+
+ me.setNodename(nodename);
+ }
+});
+
+Ext.define('PVE.form.MDevSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ xtype: 'pveMDevSelector',
+
+ store: {
+ fields: [ 'type','available', 'description' ],
+ filterOnLoad: true,
+ sorters: [
+ {
+ property : 'type',
+ direction: 'ASC'
+ }
+ ]
+ },
+ autoSelect: false,
+ valueField: 'type',
+ displayField: 'type',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ flex: 1
+ },
+ {
+ header: gettext('Available'),
+ dataIndex: 'available',
+ width: 80
+ },
+ {
+ header: gettext('Description'),
+ dataIndex: 'description',
+ flex: 1,
+ renderer: function(value) {
+ if (!value) {
+ return '';
+ }
+
+ return value.split('\n').join('
');
+ }
+ }
+ ]
+ },
+
+ setPciID: function(pciid, force) {
+ var me = this;
+
+ if (!force && (!pciid || (me.pciid === pciid))) {
+ return;
+ }
+
+ me.pciid = pciid;
+ me.updateProxy();
+ },
+
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || (me.nodename === nodename)) {
+ return;
+ }
+
+ me.nodename = nodename;
+ me.updateProxy();
+ },
+
+ updateProxy: function() {
+ var me = this;
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/hardware/pci/' + me.pciid + '/mdev'
+ });
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw 'no node name specified';
+ }
+
+ me.callParent();
+
+ if (me.pciid) {
+ me.setPciID(me.pciid, true);
+ }
+ }
+});
+
+Ext.define('PVE.form.SecurityGroupsSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveSecurityGroupsSelector'],
+
+ valueField: 'group',
+ displayField: 'group',
+ initComponent: function() {
+ var me = this;
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ fields: [ 'group', 'comment' ],
+ idProperty: 'group',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/firewall/groups"
+ },
+ sorters: {
+ property: 'group',
+ order: 'DESC'
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Security Group'),
+ dataIndex: 'group',
+ hideable: false,
+ width: 100
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.form.IPRefSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveIPRefSelector'],
+
+ base_url: undefined,
+
+ preferredValue: '', // hack: else Form sets dirty flag?
+
+ ref_type: undefined, // undefined = any [undefined, 'ipset' or 'alias']
+
+ valueField: 'ref',
+ displayField: 'ref',
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.base_url) {
+ throw "no base_url specified";
+ }
+
+ var url = "/api2/json" + me.base_url;
+ if (me.ref_type) {
+ url += "?type=" + me.ref_type;
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ fields: [ 'type', 'name', 'ref', 'comment' ],
+ idProperty: 'ref',
+ proxy: {
+ type: 'proxmox',
+ url: url
+ },
+ sorters: {
+ property: 'ref',
+ order: 'DESC'
+ }
+ });
+
+ var disable_query_for_ips = function(f, value) {
+ if (value === null ||
+ value.match(/^\d/)) { // IP address starts with \d
+ f.queryDelay = 9999999999; // hack: disable with long delay
+ } else {
+ f.queryDelay = 10;
+ }
+ };
+
+ var columns = [];
+
+ if (!me.ref_type) {
+ columns.push({
+ header: gettext('Type'),
+ dataIndex: 'type',
+ hideable: false,
+ width: 60
+ });
+ }
+
+ columns.push(
+ {
+ header: gettext('Name'),
+ dataIndex: 'ref',
+ hideable: false,
+ width: 140
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1
+ }
+ );
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: { columns: columns }
+ });
+
+ me.on('change', disable_query_for_ips);
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.form.IPProtocolSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveIPProtocolSelector'],
+ valueField: 'p',
+ displayField: 'p',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Protocol'),
+ dataIndex: 'p',
+ hideable: false,
+ sortable: false,
+ width: 100
+ },
+ {
+ header: gettext('Number'),
+ dataIndex: 'n',
+ hideable: false,
+ sortable: false,
+ width: 50
+ },
+ {
+ header: gettext('Description'),
+ dataIndex: 'd',
+ hideable: false,
+ sortable: false,
+ flex: 1
+ }
+ ]
+ },
+ store: {
+ fields: [ 'p', 'd', 'n'],
+ data: [
+ { p: 'tcp', n: 6, d: 'Transmission Control Protocol' },
+ { p: 'udp', n: 17, d: 'User Datagram Protocol' },
+ { p: 'icmp', n: 1, d: 'Internet Control Message Protocol' },
+ { p: 'igmp', n: 2, d: 'Internet Group Management' },
+ { p: 'ggp', n: 3, d: 'gateway-gateway protocol' },
+ { p: 'ipencap', n: 4, d: 'IP encapsulated in IP' },
+ { p: 'st', n: 5, d: 'ST datagram mode' },
+ { p: 'egp', n: 8, d: 'exterior gateway protocol' },
+ { p: 'igp', n: 9, d: 'any private interior gateway (Cisco)' },
+ { p: 'pup', n: 12, d: 'PARC universal packet protocol' },
+ { p: 'hmp', n: 20, d: 'host monitoring protocol' },
+ { p: 'xns-idp', n: 22, d: 'Xerox NS IDP' },
+ { p: 'rdp', n: 27, d: '"reliable datagram" protocol' },
+ { p: 'iso-tp4', n: 29, d: 'ISO Transport Protocol class 4 [RFC905]' },
+ { p: 'dccp', n: 33, d: 'Datagram Congestion Control Prot. [RFC4340]' },
+ { p: 'xtp', n: 36, d: 'Xpress Transfer Protocol' },
+ { p: 'ddp', n: 37, d: 'Datagram Delivery Protocol' },
+ { p: 'idpr-cmtp', n: 38, d: 'IDPR Control Message Transport' },
+ { p: 'ipv6', n: 41, d: 'Internet Protocol, version 6' },
+ { p: 'ipv6-route', n: 43, d: 'Routing Header for IPv6' },
+ { p: 'ipv6-frag', n: 44, d: 'Fragment Header for IPv6' },
+ { p: 'idrp', n: 45, d: 'Inter-Domain Routing Protocol' },
+ { p: 'rsvp', n: 46, d: 'Reservation Protocol' },
+ { p: 'gre', n: 47, d: 'General Routing Encapsulation' },
+ { p: 'esp', n: 50, d: 'Encap Security Payload [RFC2406]' },
+ { p: 'ah', n: 51, d: 'Authentication Header [RFC2402]' },
+ { p: 'skip', n: 57, d: 'SKIP' },
+ { p: 'ipv6-icmp', n: 58, d: 'ICMP for IPv6' },
+ { p: 'ipv6-nonxt', n: 59, d: 'No Next Header for IPv6' },
+ { p: 'ipv6-opts', n: 60, d: 'Destination Options for IPv6' },
+ { p: 'vmtp', n: 81, d: 'Versatile Message Transport' },
+ { p: 'eigrp', n: 88, d: 'Enhanced Interior Routing Protocol (Cisco)' },
+ { p: 'ospf', n: 89, d: 'Open Shortest Path First IGP' },
+ { p: 'ax.25', n: 93, d: 'AX.25 frames' },
+ { p: 'ipip', n: 94, d: 'IP-within-IP Encapsulation Protocol' },
+ { p: 'etherip', n: 97, d: 'Ethernet-within-IP Encapsulation [RFC3378]' },
+ { p: 'encap', n: 98, d: 'Yet Another IP encapsulation [RFC1241]' },
+ { p: 'pim', n: 103, d: 'Protocol Independent Multicast' },
+ { p: 'ipcomp', n: 108, d: 'IP Payload Compression Protocol' },
+ { p: 'vrrp', n: 112, d: 'Virtual Router Redundancy Protocol [RFC5798]' },
+ { p: 'l2tp', n: 115, d: 'Layer Two Tunneling Protocol [RFC2661]' },
+ { p: 'isis', n: 124, d: 'IS-IS over IPv4' },
+ { p: 'sctp', n: 132, d: 'Stream Control Transmission Protocol' },
+ { p: 'fc', n: 133, d: 'Fibre Channel' },
+ { p: 'mobility-header', n: 135, d: 'Mobility Support for IPv6 [RFC3775]' },
+ { p: 'udplite', n: 136, d: 'UDP-Lite [RFC3828]' },
+ { p: 'mpls-in-ip', n: 137, d: 'MPLS-in-IP [RFC4023]' },
+ { p: 'hip', n: 139, d: 'Host Identity Protocol' },
+ { p: 'shim6', n: 140, d: 'Shim6 Protocol [RFC5533]' },
+ { p: 'wesp', n: 141, d: 'Wrapped Encapsulating Security Payload' },
+ { p: 'rohc', n: 142, d: 'Robust Header Compression' }
+ ]
+ }
+});
+Ext.define('PVE.form.CPUModelSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.CPUModelSelector'],
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + ' (kvm64)'],
+ ['486', '486'],
+ ['athlon', 'athlon'],
+ ['core2duo', 'core2duo'],
+ ['coreduo', 'coreduo'],
+ ['kvm32', 'kvm32'],
+ ['kvm64', 'kvm64'],
+ ['pentium', 'pentium'],
+ ['pentium2', 'pentium2'],
+ ['pentium3', 'pentium3'],
+ ['phenom', 'phenom'],
+ ['qemu32', 'qemu32'],
+ ['qemu64', 'qemu64'],
+ ['Conroe', 'Conroe'],
+ ['Penryn', 'Penryn'],
+ ['Nehalem', 'Nehalem'],
+ ['Westmere', 'Westmere'],
+ ['SandyBridge', 'SandyBridge'],
+ ['IvyBridge', 'IvyBridge'],
+ ['Haswell', 'Haswell'],
+ ['Haswell-noTSX','Haswell-noTSX'],
+ ['Broadwell', 'Broadwell'],
+ ['Broadwell-noTSX','Broadwell-noTSX'],
+ ['Skylake-Client','Skylake-Client'],
+ ['Opteron_G1', 'Opteron_G1'],
+ ['Opteron_G2', 'Opteron_G2'],
+ ['Opteron_G3', 'Opteron_G3'],
+ ['Opteron_G4', 'Opteron_G4'],
+ ['Opteron_G5', 'Opteron_G5'],
+ ['EPYC', 'EPYC'],
+ ['host', 'host']
+
+ ]
+});
+Ext.define('PVE.form.VNCKeyboardSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.VNCKeyboardSelector'],
+ comboItems: PVE.Utils.kvm_keymap_array()
+});
+Ext.define('PVE.form.CacheTypeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.CacheTypeSelector'],
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + " (" + gettext('No cache') + ")"],
+ ['directsync', 'Direct sync'],
+ ['writethrough', 'Write through'],
+ ['writeback', 'Write back'],
+ ['unsafe', 'Write back (' + gettext('unsafe') + ')'],
+ ['none', gettext('No cache')]
+ ]
+});
+Ext.define('PVE.form.SnapshotSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.PVE.form.SnapshotSelector'],
+
+ valueField: 'name',
+ displayField: 'name',
+
+ loadStore: function(nodename, vmid) {
+ var me = this;
+
+ if (!nodename) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ if (!vmid) {
+ return;
+ }
+
+ me.vmid = vmid;
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid +'/snapshot'
+ });
+
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.guestType) {
+ throw "no guest type specified";
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: [ 'name'],
+ filterOnLoad: true
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Snapshot'),
+ dataIndex: 'name',
+ hideable: false,
+ flex: 1
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+
+ me.loadStore(me.nodename, me.vmid);
+ }
+});
+Ext.define('PVE.form.ContentTypeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveContentTypeSelector'],
+
+ cts: undefined,
+
+ initComponent: function() {
+ var me = this;
+
+ me.comboItems = [];
+
+ if (me.cts === undefined) {
+ me.cts = ['images', 'iso', 'vztmpl', 'backup', 'rootdir', 'snippets'];
+ }
+
+ Ext.Array.each(me.cts, function(ct) {
+ me.comboItems.push([ct, PVE.Utils.format_content_types(ct)]);
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.form.HotplugFeatureSelector', {
+ extend: 'Ext.form.CheckboxGroup',
+ alias: 'widget.pveHotplugFeatureSelector',
+
+ columns: 1,
+ vertical: true,
+
+ defaults: {
+ name: 'hotplug',
+ submitValue: false
+ },
+ items: [
+ {
+ boxLabel: gettext('Disk'),
+ inputValue: 'disk',
+ checked: true
+ },
+ {
+ boxLabel: gettext('Network'),
+ inputValue: 'network',
+ checked: true
+ },
+ {
+ boxLabel: 'USB',
+ inputValue: 'usb',
+ checked: true
+ },
+ {
+ boxLabel: gettext('Memory'),
+ inputValue: 'memory'
+ },
+ {
+ boxLabel: gettext('CPU'),
+ inputValue: 'cpu'
+ }
+ ],
+
+ setValue: function(value) {
+ var me = this;
+ var newVal = [];
+ if (value === '1') {
+ newVal = ['disk', 'network', 'usb'];
+ } else if (value !== '0') {
+ newVal = value.split(',');
+ }
+ me.callParent([{ hotplug: newVal }]);
+ },
+
+ // override framework function to
+ // assemble the hotplug value
+ getSubmitData: function() {
+ var me = this,
+ boxes = me.getBoxes(),
+ data = [];
+ Ext.Array.forEach(boxes, function(box){
+ if (box.getValue()) {
+ data.push(box.inputValue);
+ }
+ });
+
+ /* because above is hotplug an array */
+ /*jslint confusion: true*/
+ if (data.length === 0) {
+ return { 'hotplug':'0' };
+ } else {
+ return { 'hotplug': data.join(',') };
+ }
+ }
+
+});
+Ext.define('PVE.form.AgentFeatureSelector', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: ['widget.pveAgentFeatureSelector'],
+
+ initComponent: function() {
+ var me = this;
+ me.items= [
+ {
+ xtype: 'proxmoxcheckbox',
+ boxLabel: gettext('Qemu Agent'),
+ name: 'enabled',
+ uncheckedValue: 0,
+ listeners: {
+ change: function(f, value, old) {
+ var gtcb = me.down('proxmoxcheckbox[name=fstrim_cloned_disks]');
+ if (value) {
+ gtcb.setDisabled(false);
+ } else {
+ gtcb.setDisabled(true);
+ }
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ boxLabel: gettext('Run guest-trim after clone disk'),
+ name: 'fstrim_cloned_disks',
+ disabled: true
+ }
+ ];
+ me.callParent();
+ },
+
+ onGetValues: function(values) {
+ var agentstr = PVE.Parser.printPropertyString(values, 'enabled');
+ return { agent: agentstr };
+ },
+
+ setValues: function(values) {
+ var agent = values.agent || '';
+ var res = PVE.Parser.parsePropertyString(agent, 'enabled');
+ this.callParent([res]);
+ }
+});
+Ext.define('PVE.form.iScsiProviderSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveiScsiProviderSelector'],
+ comboItems: [
+ ['comstar', 'Comstar'],
+ [ 'istgt', 'istgt'],
+ [ 'iet', 'IET'],
+ [ 'LIO', 'LIO']
+ ]
+});
+Ext.define('PVE.form.DayOfWeekSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveDayOfWeekSelector'],
+ comboItems:[],
+ initComponent: function(){
+ var me = this;
+ me.comboItems = [
+ ['mon', Ext.util.Format.htmlDecode(Ext.Date.dayNames[1])],
+ ['tue', Ext.util.Format.htmlDecode(Ext.Date.dayNames[2])],
+ ['wed', Ext.util.Format.htmlDecode(Ext.Date.dayNames[3])],
+ ['thu', Ext.util.Format.htmlDecode(Ext.Date.dayNames[4])],
+ ['fri', Ext.util.Format.htmlDecode(Ext.Date.dayNames[5])],
+ ['sat', Ext.util.Format.htmlDecode(Ext.Date.dayNames[6])],
+ ['sun', Ext.util.Format.htmlDecode(Ext.Date.dayNames[0])]
+ ];
+ this.callParent();
+ }
+});
+Ext.define('PVE.form.BackupModeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveBackupModeSelector'],
+ comboItems: [
+ ['snapshot', gettext('Snapshot')],
+ ['suspend', gettext('Suspend')],
+ ['stop', gettext('Stop')]
+ ]
+});
+Ext.define('PVE.form.ScsiHwSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveScsiHwSelector'],
+ comboItems: [
+ ['__default__', PVE.Utils.render_scsihw('')],
+ ['lsi', PVE.Utils.render_scsihw('lsi')],
+ ['lsi53c810', PVE.Utils.render_scsihw('lsi53c810')],
+ ['megasas', PVE.Utils.render_scsihw('megasas')],
+ ['virtio-scsi-pci', PVE.Utils.render_scsihw('virtio-scsi-pci')],
+ ['virtio-scsi-single', PVE.Utils.render_scsihw('virtio-scsi-single')],
+ ['pvscsi', PVE.Utils.render_scsihw('pvscsi')]
+ ]
+});
+Ext.define('PVE.form.FirewallPolicySelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveFirewallPolicySelector'],
+ comboItems: [
+ ['ACCEPT', 'ACCEPT'],
+ ['REJECT', 'REJECT'],
+ [ 'DROP', 'DROP']
+ ]
+});
+/*
+ * This is a global search field
+ * it loads the /cluster/resources on focus
+ * and displays the result in a floating grid
+ *
+ * it filters and sorts the objects by the algorithm in
+ * the customFilter function
+ *
+ * also it does accept key up/down and enter for input
+ * and it opens to ctrl+shift+f and ctrl+space
+ */
+Ext.define('PVE.form.GlobalSearchField', {
+ extend: 'Ext.form.field.Text',
+ alias: 'widget.pveGlobalSearchField',
+
+ emptyText: gettext('Search'),
+ enableKeyEvents: true,
+ selectOnFocus: true,
+ padding: '0 5 0 5',
+
+ grid: {
+ xtype: 'gridpanel',
+ focusOnToFront: false,
+ floating: true,
+ emptyText: Proxmox.Utils.noneText,
+ width: 600,
+ height: 400,
+ scrollable: {
+ xtype: 'scroller',
+ y: true,
+ x:false
+ },
+ store: {
+ model: 'PVEResources',
+ proxy:{
+ type: 'proxmox',
+ url: '/api2/extjs/cluster/resources'
+ }
+ },
+ plugins: {
+ ptype: 'bufferedrenderer',
+ trailingBufferZone: 20,
+ leadingBufferZone: 20
+ },
+
+ hideMe: function() {
+ var me = this;
+ if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) {
+ return;
+ }
+ me.hasFocus = false;
+ if (!me.textfield.hasFocus) {
+ me.hide();
+ }
+ },
+
+ setFocus: function() {
+ var me = this;
+ me.hasFocus = true;
+ },
+
+ listeners: {
+ rowclick: function(grid, record) {
+ var me = this;
+ me.textfield.selectAndHide(record.id);
+ },
+ itemcontextmenu: function(v, record, item, index, event) {
+ var me = this;
+ me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event);
+ },
+ /* because of lint */
+ focusleave: {
+ fn: 'hideMe'
+ },
+ focusenter: 'setFocus'
+ },
+
+ columns: [
+ {
+ text: gettext('Type'),
+ dataIndex: 'type',
+ width: 100,
+ renderer: PVE.Utils.render_resource_type
+ },
+ {
+ text: gettext('Description'),
+ flex: 1,
+ dataIndex: 'text'
+ },
+ {
+ text: gettext('Node'),
+ dataIndex: 'node'
+ },
+ {
+ text: gettext('Pool'),
+ dataIndex: 'pool'
+ }
+ ]
+ },
+
+ customFilter: function(item) {
+ var me = this;
+ var match = 0;
+ var fieldArr = [];
+ var i,j, fields;
+
+ // different types of objects have different fields to search
+ // for example, a node will never have a pool and vice versa
+ switch (item.data.type) {
+ case 'pool': fieldArr = ['type', 'pool', 'text']; break;
+ case 'node': fieldArr = ['type', 'node', 'text']; break;
+ case 'storage': fieldArr = ['type', 'pool', 'node', 'storage']; break;
+ default: fieldArr = ['name', 'type', 'node', 'pool', 'vmid'];
+ }
+ if (me.filterVal === '') {
+ item.data.relevance = 0;
+ return true;
+ }
+
+ // all text is case insensitive and each word is
+ // searched alone
+ // for every partial match, the row gets
+ // 1 match point, for every exact match
+ // it gets 2 points
+ //
+ // results gets sorted by points (descending)
+ fields = me.filterVal.split(/\s+/);
+ for(i = 0; i < fieldArr.length; i++) {
+ var v = item.data[fieldArr[i]];
+ if (v !== undefined) {
+ v = v.toString().toLowerCase();
+ for(j = 0; j < fields.length; j++) {
+ if (v.indexOf(fields[j]) !== -1) {
+ match++;
+ if(v === fields[j]) {
+ match++;
+ }
+ }
+ }
+ }
+ }
+ // give the row the 'relevance' value
+ item.data.relevance = match;
+ return (match > 0);
+ },
+
+ updateFilter: function(field, newValue, oldValue) {
+ var me = this;
+ // parse input and filter store,
+ // show grid
+ me.grid.store.filterVal = newValue.toLowerCase().trim();
+ me.grid.store.clearFilter(true);
+ me.grid.store.filterBy(me.customFilter);
+ me.grid.getSelectionModel().select(0);
+ },
+
+ selectAndHide: function(id) {
+ var me = this;
+ me.tree.selectById(id);
+ me.grid.hide();
+ me.setValue('');
+ me.blur();
+ },
+
+ onKey: function(field, e) {
+ var me = this;
+ var key = e.getKey();
+
+ switch(key) {
+ case Ext.event.Event.ENTER:
+ // go to first entry if there is one
+ if (me.grid.store.getCount() > 0) {
+ me.selectAndHide(me.grid.getSelection()[0].data.id);
+ }
+ break;
+ case Ext.event.Event.UP:
+ me.grid.getSelectionModel().selectPrevious();
+ break;
+ case Ext.event.Event.DOWN:
+ me.grid.getSelectionModel().selectNext();
+ break;
+ case Ext.event.Event.ESC:
+ me.grid.hide();
+ me.blur();
+ break;
+ }
+ },
+
+ loadValues: function(field) {
+ var me = this;
+ var records = [];
+
+ me.hasFocus = true;
+ me.grid.textfield = me;
+ me.grid.store.load();
+ me.grid.showBy(me, 'tl-bl');
+ },
+
+ hideGrid: function() {
+ var me = this;
+
+ me.hasFocus = false;
+ if (!me.grid.hasFocus) {
+ me.grid.hide();
+ }
+ },
+
+ listeners: {
+ change: {
+ fn: 'updateFilter',
+ buffer: 250
+ },
+ specialkey: 'onKey',
+ focusenter: 'loadValues',
+ focusleave: {
+ fn: 'hideGrid',
+ delay: 100
+ }
+ },
+
+ toggleFocus: function() {
+ var me = this;
+ if (!me.hasFocus) {
+ me.focus();
+ } else {
+ me.blur();
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.tree) {
+ throw "no tree given";
+ }
+
+ me.grid = Ext.create(me.grid);
+
+ me.callParent();
+
+ /*jslint confusion: true*/
+ /*because shift is also a function*/
+ // bind ctrl+shift+f and ctrl+space
+ // to open/close the search
+ me.keymap = new Ext.KeyMap({
+ target: Ext.get(document),
+ binding: [{
+ key:'F',
+ ctrl: true,
+ shift: true,
+ fn: me.toggleFocus,
+ scope: me
+ },{
+ key:' ',
+ ctrl: true,
+ fn: me.toggleFocus,
+ scope: me
+ }]
+ });
+
+ // always select first item and
+ // sort by relevance after load
+ me.mon(me.grid.store, 'load', function() {
+ me.grid.getSelectionModel().select(0);
+ me.grid.store.sort({
+ property: 'relevance',
+ direction: 'DESC'
+ });
+ });
+ }
+
+});
+Ext.define('PVE.form.QemuBiosSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveQemuBiosSelector'],
+
+ initComponent: function() {
+ var me = this;
+
+ me.comboItems = [
+ ['__default__', PVE.Utils.render_qemu_bios('')],
+ ['seabios', PVE.Utils.render_qemu_bios('seabios')],
+ ['ovmf', PVE.Utils.render_qemu_bios('ovmf')]
+ ];
+
+ me.callParent();
+ }
+});
+/*jslint confusion: true*/
+/* filter is a javascript builtin, but extjs calls it also filter */
+Ext.define('PVE.form.VMSelector', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.vmselector',
+
+ mixins: {
+ field: 'Ext.form.field.Field'
+ },
+
+ allowBlank: true,
+ selectAll: false,
+ isFormField: true,
+
+ plugins: 'gridfilters',
+
+ store: {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ filters: [{
+ property: 'type',
+ value: /lxc|qemu/
+ }]
+ },
+ columns: [
+ {
+ header: 'ID',
+ dataIndex: 'vmid',
+ width: 80,
+ filter: {
+ type: 'number'
+ }
+ },
+ {
+ header: gettext('Node'),
+ dataIndex: 'node'
+ },
+ {
+ header: gettext('Status'),
+ dataIndex: 'status',
+ filter: {
+ type: 'list'
+ }
+ },
+ {
+ header: gettext('Name'),
+ dataIndex: 'name',
+ flex: 1,
+ filter: {
+ type: 'string'
+ }
+ },
+ {
+ header: gettext('Pool'),
+ dataIndex: 'pool',
+ filter: {
+ type: 'list'
+ }
+ },
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ width: 120,
+ renderer: function(value) {
+ if (value === 'qemu') {
+ return gettext('Virtual Machine');
+ } else if (value === 'lxc') {
+ return gettext('LXC Container');
+ }
+
+ return '';
+ },
+ filter: {
+ type: 'list',
+ store: {
+ data: [
+ {id: 'qemu', text: gettext('Virtual Machine')},
+ {id: 'lxc', text: gettext('LXC Container')}
+ ],
+ // due to EXTJS-18711
+ // we have to do a static list via a store
+ // but to avoid creating an object,
+ // we have to have a pseudo un function
+ un: function(){}
+ }
+ }
+ },
+ {
+ header: 'HA ' + gettext('Status'),
+ dataIndex: 'hastate',
+ flex: 1,
+ filter: {
+ type: 'list'
+ }
+ }
+ ],
+
+ selModel: {
+ selType: 'checkboxmodel',
+ mode: 'SIMPLE'
+ },
+
+ checkChangeEvents: [
+ 'selectionchange',
+ 'change'
+ ],
+
+ listeners: {
+ selectionchange: function() {
+ // to trigger validity and error checks
+ this.checkChange();
+ }
+ },
+
+ getValue: function() {
+ var me = this;
+ var sm = me.getSelectionModel();
+ var selection = sm.getSelection();
+ var values = [];
+ var store = me.getStore();
+ selection.forEach(function(item) {
+ // only add if not filtered
+ if (store.findExact('vmid', item.data.vmid) !== -1) {
+ values.push(item.data.vmid);
+ }
+ });
+ return values;
+ },
+
+ setValue: function(value) {
+ console.log(value);
+ var me = this;
+ var sm = me.getSelectionModel();
+ if (!Ext.isArray(value)) {
+ value = value.split(',');
+ }
+ var selection = [];
+ var store = me.getStore();
+
+ value.forEach(function(item) {
+ var rec = store.findRecord('vmid',item, 0, false, true, true);
+ console.log(store);
+
+ if (rec) {
+ console.log(rec);
+ selection.push(rec);
+ }
+ });
+
+ sm.select(selection);
+
+ return me.mixins.field.setValue.call(me, value);
+ },
+
+ getErrors: function(value) {
+ var me = this;
+ if (me.allowBlank === false &&
+ me.getSelectionModel().getCount() === 0) {
+ me.addBodyCls(['x-form-trigger-wrap-default','x-form-trigger-wrap-invalid']);
+ return [gettext('No VM selected')];
+ }
+
+ me.removeBodyCls(['x-form-trigger-wrap-default','x-form-trigger-wrap-invalid']);
+ return [];
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+
+ if (me.nodename) {
+ me.store.filters.add({
+ property: 'node',
+ exactMatch: true,
+ value: me.nodename
+ });
+ }
+
+ // only show the relevant guests by default
+ if (me.action) {
+ var statusfilter = '';
+ switch (me.action) {
+ case 'startall':
+ statusfilter = 'stopped';
+ break;
+ case 'stopall':
+ statusfilter = 'running';
+ break;
+ }
+ if (statusfilter !== '') {
+ me.store.filters.add({
+ property: 'template',
+ value: 0
+ },{
+ id: 'x-gridfilter-status',
+ operator: 'in',
+ property: 'status',
+ value: [statusfilter]
+ });
+ }
+ }
+
+ var store = me.getStore();
+ var sm = me.getSelectionModel();
+
+ if (me.selectAll) {
+ me.mon(store,'load', function(){
+ me.getSelectionModel().selectAll(false);
+ });
+ }
+ }
+});
+
+
+Ext.define('PVE.form.VMComboSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: 'widget.vmComboSelector',
+
+ valueField: 'vmid',
+ displayField: 'vmid',
+
+ autoSelect: false,
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+
+ store: {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ filters: [{
+ property: 'type',
+ value: /lxc|qemu/
+ }]
+ },
+
+ listConfig: {
+ width: 600,
+ plugins: 'gridfilters',
+ columns: [
+ {
+ header: 'ID',
+ dataIndex: 'vmid',
+ width: 80,
+ filter: {
+ type: 'number'
+ }
+ },
+ {
+ header: gettext('Name'),
+ dataIndex: 'name',
+ flex: 1,
+ filter: {
+ type: 'string'
+ }
+ },
+ {
+ header: gettext('Node'),
+ dataIndex: 'node'
+ },
+ {
+ header: gettext('Status'),
+ dataIndex: 'status',
+ filter: {
+ type: 'list'
+ }
+ },
+ {
+ header: gettext('Pool'),
+ dataIndex: 'pool',
+ hidden: true,
+ filter: {
+ type: 'list'
+ }
+ },
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ width: 120,
+ renderer: function(value) {
+ if (value === 'qemu') {
+ return gettext('Virtual Machine');
+ } else if (value === 'lxc') {
+ return gettext('LXC Container');
+ }
+
+ return '';
+ },
+ filter: {
+ type: 'list',
+ store: {
+ data: [
+ {id: 'qemu', text: gettext('Virtual Machine')},
+ {id: 'lxc', text: gettext('LXC Container')}
+ ],
+ un: function(){} // due to EXTJS-18711
+ }
+ }
+ },
+ {
+ header: 'HA ' + gettext('Status'),
+ dataIndex: 'hastate',
+ hidden: true,
+ flex: 1,
+ filter: {
+ type: 'list'
+ }
+ }
+ ]
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.form.VMCPUFlagSelector', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.vmcpuflagselector',
+
+ mixins: {
+ field: 'Ext.form.field.Field'
+ },
+
+ disableSelection: true,
+ columnLines: false,
+ selectable: false,
+ hideHeaders: true,
+
+ scrollable: 'y',
+ height: 200,
+
+ unkownFlags: [],
+
+ store: {
+ type: 'store',
+ fields: ['flag', { name: 'state', defaultValue: '=' }, 'desc'],
+ data: [
+ // FIXME: let qemu-server host this and autogenerate or get from API call??
+ { flag: 'md-clear', desc: 'Required to let the guest OS know if MDS is mitigated correctly' },
+ { flag: 'pcid', desc: 'Meltdown fix cost reduction on Westmere, Sandy-, and IvyBridge Intel CPUs' },
+ { flag: 'spec-ctrl', desc: 'Allows improved Spectre mitigation with Intel CPUs' },
+ { flag: 'ssbd', desc: 'Protection for "Speculative Store Bypass" for Intel models' },
+ { flag: 'ibpb', desc: 'Allows improved Spectre mitigation with AMD CPUs' },
+ { flag: 'virt-ssbd', desc: 'Basis for "Speculative Store Bypass" protection for AMD models' },
+ { flag: 'amd-ssbd', desc: 'Improves Spectre mitigation performance with AMD CPUs, best used with "virt-ssbd"' },
+ { flag: 'amd-no-ssb', desc: 'Notifies guest OS that host is not vulnerable for Spectre on AMD CPUs' },
+ { flag: 'pdpe1gb', desc: 'Allow guest OS to use 1GB size pages, if host HW supports it' }
+ ],
+ listeners: {
+ update: function() {
+ this.commitChanges();
+ }
+ }
+ },
+
+ getValue: function() {
+ var me = this;
+ var store = me.getStore();
+ var flags = '';
+
+ // ExtJS does not has a nice getAllRecords interface for stores :/
+ store.queryBy(Ext.returnTrue).each(function(rec) {
+ var s = rec.get('state');
+ if (s && s !== '=') {
+ var f = rec.get('flag');
+ if (flags === '') {
+ flags = s + f;
+ } else {
+ flags += ';' + s + f;
+ }
+ }
+ });
+
+ flags += me.unkownFlags.join(';');
+
+ return flags;
+ },
+
+ setValue: function(value) {
+ var me = this;
+ var store = me.getStore();
+
+ me.value = value || '';
+
+ me.unkownFlags = [];
+
+ me.getStore().queryBy(Ext.returnTrue).each(function(rec) {
+ rec.set('state', '=');
+ });
+
+ var flags = value ? value.split(';') : [];
+ flags.forEach(function(flag) {
+ var sign = flag.substr(0, 1);
+ flag = flag.substr(1);
+
+ var rec = store.findRecord('flag', flag);
+ if (rec !== null) {
+ rec.set('state', sign);
+ } else {
+ me.unkownFlags.push(flag);
+ }
+ });
+ store.reload();
+
+ var res = me.mixins.field.setValue.call(me, value);
+
+ return res;
+ },
+ columns: [
+ {
+ dataIndex: 'state',
+ renderer: function(v) {
+ switch(v) {
+ case '=': return 'Default';
+ case '-': return 'Off';
+ case '+': return 'On';
+ default: return 'Unknown';
+ }
+ },
+ width: 65
+ },
+ {
+ xtype: 'widgetcolumn',
+ dataIndex: 'state',
+ width: 95,
+ onWidgetAttach: function (column, widget, record) {
+ var val = record.get('state') || '=';
+ widget.down('[inputValue=' + val + ']').setValue(true);
+ // TODO: disable if selected CPU model and flag are incompatible
+ },
+ widget: {
+ xtype: 'radiogroup',
+ hideLabel: true,
+ layout: 'hbox',
+ validateOnChange: false,
+ value: '=',
+ listeners: {
+ change: function(f, value) {
+ var v = Object.values(value)[0];
+ f.getWidgetRecord().set('state', v);
+
+ var view = this.up('grid');
+ view.dirty = view.getValue() !== view.originalValue;
+ view.checkDirty();
+ //view.checkChange();
+ }
+ },
+ items: [
+ {
+ boxLabel: '-',
+ boxLabelAlign: 'before',
+ inputValue: '-'
+ },
+ {
+ checked: true,
+ inputValue: '='
+ },
+ {
+ boxLabel: '+',
+ inputValue: '+'
+ }
+ ]
+ }
+ },
+ {
+ dataIndex: 'flag',
+ width: 100
+ },
+ {
+ dataIndex: 'desc',
+ cellWrap: true,
+ flex: 1
+ }
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ // static class store, thus gets not recreated, so ensure defaults are set!
+ me.getStore().data.forEach(function(v) {
+ v.state = '=';
+ });
+
+ me.value = me.originalValue = '';
+
+ me.callParent(arguments);
+ }
+});
+Ext.define('PVE.form.USBSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveUSBSelector'],
+ allowBlank: false,
+ autoSelect: false,
+ displayField: 'usbid',
+ valueField: 'usbid',
+ editable: true,
+
+ getUSBValue: function() {
+ var me = this;
+ var rec = me.store.findRecord('usbid', me.value);
+ var val = 'host='+ me.value;
+ if (rec && rec.data.speed === "5000") {
+ val = 'host=' + me.value + ",usb3=1";
+ }
+ return val;
+ },
+
+ validator: function(value) {
+ var me = this;
+ if (me.type === 'device') {
+ return (/^[a-f0-9]{4}\:[a-f0-9]{4}$/i).test(value);
+ } else if (me.type === 'port') {
+ return (/^[0-9]+\-[0-9]+(\.[0-9]+)*$/).test(value);
+ }
+ return false;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+
+ if (!nodename) {
+ throw "no nodename specified";
+ }
+
+ if (me.type !== 'device' && me.type !== 'port') {
+ throw "no valid type specified";
+ }
+
+ var store = new Ext.data.Store({
+ model: 'pve-usb-' + me.type,
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/nodes/" + nodename + "/scan/usb"
+ },
+ filters: [
+ function (item) {
+ return !!item.data.usbpath && !!item.data.prodid && item.data['class'] != 9;
+ }
+ ]
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: (me.type === 'device')?gettext('Device'):gettext('Port'),
+ sortable: true,
+ dataIndex: 'usbid',
+ width: 80
+ },
+ {
+ header: gettext('Manufacturer'),
+ sortable: true,
+ dataIndex: 'manufacturer',
+ width: 100
+ },
+ {
+ header: gettext('Product'),
+ sortable: true,
+ dataIndex: 'product',
+ flex: 1
+ },
+ {
+ header: gettext('Speed'),
+ width: 70,
+ sortable: true,
+ dataIndex: 'speed',
+ renderer: function(value) {
+ if (value === "5000") {
+ return "USB 3.0";
+ } else if (value === "480") {
+ return "USB 2.0";
+ } else {
+ return "USB 1.x";
+ }
+ }
+ }
+ ]
+ }
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+
+}, function() {
+
+ Ext.define('pve-usb-device', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name: 'usbid',
+ convert: function(val, data) {
+ if (val) {
+ return val;
+ }
+ return data.get('vendid') + ':' + data.get('prodid');
+ }
+ },
+ 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath',
+ { name: 'port' , type: 'number' },
+ { name: 'level' , type: 'number' },
+ { name: 'class' , type: 'number' },
+ { name: 'devnum' , type: 'number' },
+ { name: 'busnum' , type: 'number' }
+ ]
+ });
+
+ Ext.define('pve-usb-port', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name: 'usbid',
+ convert: function(val,data) {
+ if (val) {
+ return val;
+ }
+ return data.get('busnum') + '-' + data.get('usbpath');
+ }
+ },
+ 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath',
+ { name: 'port' , type: 'number' },
+ { name: 'level' , type: 'number' },
+ { name: 'class' , type: 'number' },
+ { name: 'devnum' , type: 'number' },
+ { name: 'busnum' , type: 'number' }
+ ]
+ });
+});
+Ext.define('PVE.form.CalendarEvent', {
+ extend: 'Ext.form.field.ComboBox',
+ xtype: 'pveCalendarEvent',
+
+ editable: true,
+
+ valueField: 'value',
+ displayField: 'text',
+ queryMode: 'local',
+
+ store: {
+ field: [ 'value', 'text'],
+ data: [
+ { value: '*/30', text: Ext.String.format(gettext("Every {0} minutes"), 30) },
+ { value: '*/2:00', text: gettext("Every two hours")},
+ { value: '2,22:30', text: gettext("Every day") + " 02:30, 22:30"},
+ { value: 'mon..fri', text: gettext("Monday to Friday") + " 00:00"},
+ { value: 'mon..fri */1:00', text: gettext("Monday to Friday") + ': ' + gettext("hourly")},
+ { value: 'sun 01:00', text: gettext("Sunday") + " 01:00"}
+ ]
+ },
+
+ tpl: [
+ '
'
+ ],
+
+ displayTpl: [
+ '{title}:
'
+ },
+ {
+ flex: 1,
+ xtype: 'cartesian',
+ height: '100%',
+ itemId: 'chart',
+ border: false,
+ axes: [
+ {
+ type: 'numeric',
+ position: 'left',
+ hidden: true,
+ minimum: 0
+ },
+ {
+ type: 'numeric',
+ position: 'bottom',
+ hidden: true
+ }
+ ],
+
+ store: {
+ data: {}
+ },
+
+ sprites: [{
+ id: 'valueSprite',
+ type: 'text',
+ text: '0 B/s',
+ textAlign: 'end',
+ textBaseline: 'middle',
+ fontSize: 14
+ }],
+
+ series: [{
+ type: 'line',
+ xField: 'time',
+ yField: 'val',
+ fill: 'true',
+ colors: ['#cfcfcf'],
+ tooltip: {
+ trackMouse: true,
+ renderer: function( tooltip, record, ctx) {
+ var me = this.getChart();
+ var date = new Date(record.data.time);
+ var value = me.up().renderer(record.data.val);
+ tooltip.setHtml(
+ me.up().title + ': ' + value + '
' +
+ Ext.Date.format(date, 'H:i:s')
+ );
+ }
+ },
+ style: {
+ lineWidth: 1.5,
+ opacity: 0.60
+ },
+ marker: {
+ opacity: 0,
+ scaling: 0.01,
+ fx: {
+ duration: 200,
+ easing: 'easeOut'
+ }
+ },
+ highlightCfg: {
+ opacity: 1,
+ scaling: 1.5
+ }
+ }]
+ }
+ ],
+
+ // the renderer for the tooltip and last value,
+ // default just the value
+ renderer: Ext.identityFn,
+
+ // show the last x seconds
+ // default is 5 minutes
+ timeFrame: 5*60,
+
+ addDataPoint: function(value, time) {
+ var me = this.chart;
+ var panel = me.up();
+ var now = new Date();
+ var begin = new Date(now.getTime() - (1000*panel.timeFrame));
+
+ me.store.add({
+ time: time || now.getTime(),
+ val: value || 0
+ });
+
+ // delete all old records when we have 20 times more datapoints
+ // than seconds in our timeframe (so even a subsecond graph does
+ // not trigger this often)
+ //
+ // records in the store do not take much space, but like this,
+ // we prevent a memory leak when someone has the site open for a long time
+ // with minimal graphical glitches
+ if (me.store.count() > panel.timeFrame * 20) {
+ var oldData = me.store.getData().createFiltered(function(item) {
+ return item.data.time < begin.getTime();
+ });
+
+ me.store.remove(oldData.getRange());
+ }
+
+ me.timeaxis.setMinimum(begin.getTime());
+ me.timeaxis.setMaximum(now.getTime());
+ me.valuesprite.setText(panel.renderer(value || 0).toString());
+ me.valuesprite.setAttributes({
+ x: me.getWidth() - 15,
+ y: me.getHeight()/2
+ }, true);
+ me.redraw();
+ },
+
+ setTitle: function(title) {
+ this.title = title;
+ var me = this.getComponent('title');
+ me.update({title: title});
+ },
+
+ initComponent: function(){
+ var me = this;
+ me.callParent();
+
+ if (me.title) {
+ me.getComponent('title').update({title: me.title});
+ }
+ me.chart = me.getComponent('chart');
+ me.chart.timeaxis = me.chart.getAxes()[1];
+ me.chart.valuesprite = me.chart.getSurface('chart').get('valueSprite');
+ if (me.color) {
+ me.chart.series[0].setStyle({
+ fill: me.color,
+ stroke: me.color
+ });
+ }
+ }
+});
+Ext.define('PVE.widget.Info',{
+ extend: 'Ext.container.Container',
+ alias: 'widget.pveInfoWidget',
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+
+ value: 0,
+ maximum: 1,
+ printBar: true,
+ items: [
+ {
+ xtype: 'component',
+ itemId: 'label',
+ data: {
+ title: '',
+ usage: '',
+ iconCls: undefined
+ },
+ tpl: [
+ '{title}
',
+ '',
+ '
',
+ '{text}'
+ ],
+
+ updateHealth: function(data) {
+ var me = this;
+ me.update(Ext.apply(me.data, data));
+ },
+
+ initComponent: function(){
+ var me = this;
+
+ if (me.title) {
+ me.config.data.title = me.title;
+ }
+
+ me.callParent();
+ }
+
+});
+/*global u2f*/
+Ext.define('PVE.window.LoginWindow', {
+ extend: 'Ext.window.Window',
+
+ controller: {
+
+ xclass: 'Ext.app.ViewController',
+
+ onLogon: function() {
+ var me = this;
+
+ var form = this.lookupReference('loginForm');
+ var unField = this.lookupReference('usernameField');
+ var saveunField = this.lookupReference('saveunField');
+ var view = this.getView();
+
+ if (!form.isValid()) {
+ return;
+ }
+
+ view.el.mask(gettext('Please wait...'), 'x-mask-loading');
+
+ // set or clear username
+ var sp = Ext.state.Manager.getProvider();
+ if (saveunField.getValue() === true) {
+ sp.set(unField.getStateId(), unField.getValue());
+ } else {
+ sp.clear(unField.getStateId());
+ }
+ sp.set(saveunField.getStateId(), saveunField.getValue());
+
+ form.submit({
+ failure: function(f, resp){
+ me.failure(resp);
+ },
+ success: function(f, resp){
+ view.el.unmask();
+
+ var data = resp.result.data;
+ if (Ext.isDefined(data.NeedTFA)) {
+ // Store first factor login information first:
+ data.LoggedOut = true;
+ Proxmox.Utils.setAuthData(data);
+
+ if (Ext.isDefined(data.U2FChallenge)) {
+ me.perform_u2f(data);
+ } else {
+ me.perform_otp();
+ }
+ } else {
+ me.success(data);
+ }
+ }
+ });
+
+ },
+ failure: function(resp) {
+ var me = this;
+ var view = me.getView();
+ view.el.unmask();
+ var handler = function() {
+ var uf = me.lookupReference('usernameField');
+ uf.focus(true, true);
+ };
+
+ Ext.MessageBox.alert(gettext('Error'),
+ gettext("Login failed. Please try again"),
+ handler);
+ },
+ success: function(data) {
+ var me = this;
+ var view = me.getView();
+ var handler = view.handler || Ext.emptyFn;
+ handler.call(me, data);
+ view.close();
+ },
+
+ perform_otp: function() {
+ var me = this;
+ var win = Ext.create('PVE.window.TFALoginWindow', {
+ onLogin: function(value) {
+ me.finish_tfa(value);
+ },
+ onCancel: function() {
+ Proxmox.LoggedOut = false;
+ Proxmox.Utils.authClear();
+ me.getView().show();
+ }
+ });
+ win.show();
+ },
+
+ perform_u2f: function(data) {
+ var me = this;
+ // Show the message:
+ var msg = Ext.Msg.show({
+ title: 'U2F: '+gettext('Verification'),
+ message: gettext('Please press the button on your U2F Device'),
+ buttons: []
+ });
+ var chlg = data.U2FChallenge;
+ var key = {
+ version: chlg.version,
+ keyHandle: chlg.keyHandle
+ };
+ u2f.sign(chlg.appId, chlg.challenge, [key], function(res) {
+ msg.close();
+ if (res.errorCode) {
+ Proxmox.Utils.authClear();
+ Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode));
+ return;
+ }
+ delete res.errorCode;
+ me.finish_tfa(JSON.stringify(res));
+ });
+ },
+ finish_tfa: function(res) {
+ var me = this;
+ var view = me.getView();
+ view.el.mask(gettext('Please wait...'), 'x-mask-loading');
+ var params = { response: res };
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/access/tfa',
+ params: params,
+ method: 'POST',
+ timeout: 5000, // it'll delay both success & failure
+ success: function(resp, opts) {
+ view.el.unmask();
+ // Fill in what we copy over from the 1st factor:
+ var data = resp.result.data;
+ data.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
+ data.username = Proxmox.UserName;
+ // Finish logging in:
+ me.success(data);
+ },
+ failure: function(resp, opts) {
+ Proxmox.Utils.authClear();
+ me.failure(resp);
+ }
+ });
+ },
+
+ control: {
+ 'field[name=username]': {
+ specialkey: function(f, e) {
+ if (e.getKey() === e.ENTER) {
+ var pf = this.lookupReference('passwordField');
+ if (!pf.getValue()) {
+ pf.focus(false);
+ }
+ }
+ }
+ },
+ 'field[name=lang]': {
+ change: function(f, value) {
+ var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
+ Ext.util.Cookies.set('PVELangCookie', value, dt);
+ this.getView().mask(gettext('Please wait...'), 'x-mask-loading');
+ window.location.reload();
+ }
+ },
+ 'button[reference=loginButton]': {
+ click: 'onLogon'
+ },
+ '#': {
+ show: function() {
+ var sp = Ext.state.Manager.getProvider();
+ var checkboxField = this.lookupReference('saveunField');
+ var unField = this.lookupReference('usernameField');
+
+ var checked = sp.get(checkboxField.getStateId());
+ checkboxField.setValue(checked);
+
+ if(checked === true) {
+ var username = sp.get(unField.getStateId());
+ unField.setValue(username);
+ var pwField = this.lookupReference('passwordField');
+ pwField.focus();
+ }
+ }
+ }
+ }
+ },
+
+ width: 400,
+
+ modal: true,
+
+ border: false,
+
+ draggable: true,
+
+ closable: false,
+
+ resizable: false,
+
+ layout: 'auto',
+
+ title: gettext('Proxmox VE Login'),
+
+ defaultFocus: 'usernameField',
+
+ defaultButton: 'loginButton',
+
+ items: [{
+ xtype: 'form',
+ layout: 'form',
+ url: '/api2/extjs/access/ticket',
+ reference: 'loginForm',
+
+ fieldDefaults: {
+ labelAlign: 'right',
+ allowBlank: false
+ },
+
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('User name'),
+ name: 'username',
+ itemId: 'usernameField',
+ reference: 'usernameField',
+ stateId: 'login-username'
+ },
+ {
+ xtype: 'textfield',
+ inputType: 'password',
+ fieldLabel: gettext('Password'),
+ name: 'password',
+ reference: 'passwordField'
+ },
+ {
+ xtype: 'pveRealmComboBox',
+ name: 'realm'
+ },
+ {
+ xtype: 'proxmoxLanguageSelector',
+ fieldLabel: gettext('Language'),
+ value: Ext.util.Cookies.get('PVELangCookie') || Proxmox.defaultLang || 'en',
+ name: 'lang',
+ reference: 'langField',
+ submitValue: false
+ }
+ ],
+ buttons: [
+ {
+ xtype: 'checkbox',
+ fieldLabel: gettext('Save User name'),
+ name: 'saveusername',
+ reference: 'saveunField',
+ stateId: 'login-saveusername',
+ labelWidth: 'auto',
+ labelAlign: 'right',
+ submitValue: false
+ },
+ {
+ text: gettext('Login'),
+ reference: 'loginButton'
+ }
+ ]
+ }]
+ });
+Ext.define('PVE.window.TFALoginWindow', {
+ extend: 'Ext.window.Window',
+
+ modal: true,
+ resizable: false,
+ title: 'Two-Factor Authentication',
+ layout: 'form',
+ defaultButton: 'loginButton',
+ defaultFocus: 'otpField',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ login: function() {
+ var me = this;
+ var view = me.getView();
+ view.onLogin(me.lookup('otpField').getValue());
+ view.close();
+ },
+ cancel: function() {
+ var me = this;
+ var view = me.getView();
+ view.onCancel();
+ view.close();
+ }
+ },
+
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Please enter your OTP verification code:'),
+ name: 'otp',
+ itemId: 'otpField',
+ reference: 'otpField',
+ allowBlank: false
+ }
+ ],
+
+ buttons: [
+ {
+ text: gettext('Login'),
+ reference: 'loginButton',
+ handler: 'login'
+ },
+ {
+ text: gettext('Cancel'),
+ handler: 'cancel'
+ }
+ ]
+});
+Ext.define('PVE.window.Wizard', {
+ extend: 'Ext.window.Window',
+
+ activeTitle: '', // used for automated testing
+
+ width: 700,
+ height: 510,
+
+ modal: true,
+ border: false,
+
+ draggable: true,
+ closable: true,
+ resizable: false,
+
+ layout: 'border',
+
+ getValues: function(dirtyOnly) {
+ var me = this;
+
+ var values = {};
+
+ var form = me.down('form').getForm();
+
+ form.getFields().each(function(field) {
+ if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) {
+ Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
+ }
+ });
+
+ Ext.Array.each(me.query('inputpanel'), function(panel) {
+ Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly));
+ });
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var tabs = me.items || [];
+ delete me.items;
+
+ /*
+ * Items may have the following functions:
+ * validator(): per tab custom validation
+ * onSubmit(): submit handler
+ * onGetValues(): overwrite getValues results
+ */
+
+ Ext.Array.each(tabs, function(tab) {
+ tab.disabled = true;
+ });
+ tabs[0].disabled = false;
+
+ var maxidx = 0;
+ var curidx = 0;
+
+ var check_card = function(card) {
+ var valid = true;
+ var fields = card.query('field, fieldcontainer');
+ if (card.isXType('fieldcontainer')) {
+ fields.unshift(card);
+ }
+ Ext.Array.each(fields, function(field) {
+ // Note: not all fielcontainer have isValid()
+ if (Ext.isFunction(field.isValid) && !field.isValid()) {
+ valid = false;
+ }
+ });
+
+ if (Ext.isFunction(card.validator)) {
+ return card.validator();
+ }
+
+ return valid;
+ };
+
+ var disable_at = function(card) {
+ var tp = me.down('#wizcontent');
+ var idx = tp.items.indexOf(card);
+ for(;idx < tp.items.getCount();idx++) {
+ var nc = tp.items.getAt(idx);
+ if (nc) {
+ nc.disable();
+ }
+ }
+ };
+
+ var tabchange = function(tp, newcard, oldcard) {
+ if (newcard.onSubmit) {
+ me.down('#next').setVisible(false);
+ me.down('#submit').setVisible(true);
+ } else {
+ me.down('#next').setVisible(true);
+ me.down('#submit').setVisible(false);
+ }
+ var valid = check_card(newcard);
+ me.down('#next').setDisabled(!valid);
+ me.down('#submit').setDisabled(!valid);
+ me.down('#back').setDisabled(tp.items.indexOf(newcard) == 0);
+
+ var idx = tp.items.indexOf(newcard);
+ if (idx > maxidx) {
+ maxidx = idx;
+ }
+ curidx = idx;
+
+ var next = idx + 1;
+ var ntab = tp.items.getAt(next);
+ if (valid && ntab && !newcard.onSubmit) {
+ ntab.enable();
+ }
+ };
+
+ if (me.subject && !me.title) {
+ me.title = Proxmox.Utils.dialog_title(me.subject, true, false);
+ }
+
+ var sp = Ext.state.Manager.getProvider();
+ var advchecked = sp.get('proxmox-advanced-cb');
+
+ Ext.apply(me, {
+ items: [
+ {
+ xtype: 'form',
+ region: 'center',
+ layout: 'fit',
+ border: false,
+ margins: '5 5 0 5',
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: [{
+ itemId: 'wizcontent',
+ xtype: 'tabpanel',
+ activeItem: 0,
+ bodyPadding: 10,
+ listeners: {
+ afterrender: function(tp) {
+ var atab = this.getActiveTab();
+ tabchange(tp, atab);
+ },
+ tabchange: function(tp, newcard, oldcard) {
+ tabchange(tp, newcard, oldcard);
+ }
+ },
+ items: tabs
+ }]
+ }
+ ],
+ fbar: [
+ {
+ xtype: 'proxmoxHelpButton',
+ itemId: 'help'
+ },
+ '->',
+ {
+ xtype: 'proxmoxcheckbox',
+ boxLabelAlign: 'before',
+ boxLabel: gettext('Advanced'),
+ value: advchecked,
+ listeners: {
+ change: function(cb, val) {
+ var tp = me.down('#wizcontent');
+ tp.query('inputpanel').forEach(function(ip) {
+ ip.setAdvancedVisible(val);
+ });
+
+ sp.set('proxmox-advanced-cb', val);
+ }
+ }
+ },
+ {
+ text: gettext('Back'),
+ disabled: true,
+ itemId: 'back',
+ minWidth: 60,
+ handler: function() {
+ var tp = me.down('#wizcontent');
+ var atab = tp.getActiveTab();
+ var prev = tp.items.indexOf(atab) - 1;
+ if (prev < 0) {
+ return;
+ }
+ var ntab = tp.items.getAt(prev);
+ if (ntab) {
+ tp.setActiveTab(ntab);
+ }
+ }
+ },
+ {
+ text: gettext('Next'),
+ disabled: true,
+ itemId: 'next',
+ minWidth: 60,
+ handler: function() {
+
+ var form = me.down('form').getForm();
+
+ var tp = me.down('#wizcontent');
+ var atab = tp.getActiveTab();
+ if (!check_card(atab)) {
+ return;
+ }
+
+ var next = tp.items.indexOf(atab) + 1;
+ var ntab = tp.items.getAt(next);
+ if (ntab) {
+ ntab.enable();
+ tp.setActiveTab(ntab);
+ }
+
+ }
+ },
+ {
+ text: gettext('Finish'),
+ minWidth: 60,
+ hidden: true,
+ itemId: 'submit',
+ handler: function() {
+ var tp = me.down('#wizcontent');
+ var atab = tp.getActiveTab();
+ atab.onSubmit();
+ }
+ }
+ ]
+ });
+ me.callParent();
+
+ Ext.Array.each(me.query('inputpanel'), function(panel) {
+ panel.setAdvancedVisible(advchecked);
+ });
+
+ Ext.Array.each(me.query('field'), function(field) {
+ var validcheck = function() {
+ var tp = me.down('#wizcontent');
+
+ // check tabs from current to the last enabled for validity
+ // since we might have changed a validity on a later one
+ var i;
+ for (i = curidx; i <= maxidx && i < tp.items.getCount(); i++) {
+ var tab = tp.items.getAt(i);
+ var valid = check_card(tab);
+
+ // only set the buttons on the current panel
+ if (i === curidx) {
+ me.down('#next').setDisabled(!valid);
+ me.down('#submit').setDisabled(!valid);
+ }
+
+ // if a panel is invalid, then disable it and all following,
+ // else enable it and go to the next
+ var ntab = tp.items.getAt(i + 1);
+ if (!valid) {
+ disable_at(ntab);
+ return;
+ } else if (ntab && !tab.onSubmit) {
+ ntab.enable();
+ }
+ }
+ };
+ field.on('change', validcheck);
+ field.on('validitychange', validcheck);
+ });
+ }
+});
+Ext.define('PVE.window.NotesEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent : function() {
+ var me = this;
+
+ Ext.apply(me, {
+ title: gettext('Notes'),
+ width: 600,
+ height: '400px',
+ resizable: true,
+ layout: 'fit',
+ defaultButton: undefined,
+ items: {
+ xtype: 'textarea',
+ name: 'description',
+ height: '100%',
+ value: '',
+ hideLabel: true
+ }
+ });
+
+ me.callParent();
+
+ me.load();
+ }
+});
+Ext.define('PVE.window.Backup', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.vmtype) {
+ throw "no VM type specified";
+ }
+
+ var storagesel = Ext.create('PVE.form.StorageSelector', {
+ nodename: me.nodename,
+ name: 'storage',
+ value: me.storage,
+ fieldLabel: gettext('Storage'),
+ storageContent: 'backup',
+ allowBlank: false
+ });
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: [
+ storagesel,
+ {
+ xtype: 'pveBackupModeSelector',
+ fieldLabel: gettext('Mode'),
+ value: 'snapshot',
+ name: 'mode'
+ },
+ {
+ xtype: 'pveCompressionSelector',
+ name: 'compress',
+ value: 'lzo',
+ fieldLabel: gettext('Compression')
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Send email to'),
+ name: 'mailto',
+ emptyText: Proxmox.Utils.noneText
+ }
+ ]
+ });
+
+ var form = me.formPanel.getForm();
+
+ var submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Backup'),
+ handler: function(){
+ var storage = storagesel.getValue();
+ var values = form.getValues();
+ var params = {
+ storage: storage,
+ vmid: me.vmid,
+ mode: values.mode,
+ remove: 0
+ };
+
+ if ( values.mailto ) {
+ params.mailto = values.mailto;
+ }
+
+ if (values.compress) {
+ params.compress = values.compress;
+ }
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/vzdump',
+ params: params,
+ method: 'POST',
+ failure: function (response, opts) {
+ Ext.Msg.alert('Error',response.htmlStatus);
+ },
+ success: function(response, options) {
+ // close later so we reload the grid
+ // after the task has completed
+ me.hide();
+
+ var upid = response.result.data;
+
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid,
+ listeners: {
+ close: function() {
+ me.close();
+ }
+ }
+ });
+ win.show();
+ }
+ });
+ }
+ });
+
+ var helpBtn = Ext.create('Proxmox.button.Help', {
+ onlineHelp: 'chapter_vzdump',
+ listenToGlobalEvent: false,
+ hidden: false
+ });
+
+ var title = gettext('Backup') + " " +
+ ((me.vmtype === 'lxc') ? "CT" : "VM") +
+ " " + me.vmid;
+
+ Ext.apply(me, {
+ title: title,
+ width: 350,
+ modal: true,
+ layout: 'auto',
+ border: false,
+ items: [ me.formPanel ],
+ buttons: [ helpBtn, '->', submitBtn ]
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.window.Restore', {
+ extend: 'Ext.window.Window', // fixme: Proxmox.window.Edit?
+
+ resizable: false,
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.volid) {
+ throw "no volume ID specified";
+ }
+
+ if (!me.vmtype) {
+ throw "no vmtype specified";
+ }
+
+ var storagesel = Ext.create('PVE.form.StorageSelector', {
+ nodename: me.nodename,
+ name: 'storage',
+ value: '',
+ fieldLabel: gettext('Storage'),
+ storageContent: (me.vmtype === 'lxc') ? 'rootdir' : 'images',
+ allowBlank: true
+ });
+
+ var IDfield;
+ if (me.vmid) {
+ IDfield = Ext.create('Ext.form.field.Display', {
+ name: 'vmid',
+ value: me.vmid,
+ fieldLabel: (me.vmtype === 'lxc') ? 'CT' : 'VM'
+ });
+ } else {
+ IDfield = Ext.create('PVE.form.GuestIDSelector', {
+ name: 'vmid',
+ guestType: me.vmtype,
+ loadNextFreeID: true,
+ validateExists: false
+ });
+ }
+
+ var items = [
+ {
+ xtype: 'displayfield',
+ value: me.volidText || me.volid,
+ fieldLabel: gettext('Source')
+ },
+ storagesel,
+ IDfield,
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'bwlimit',
+ fieldLabel: gettext('Read Limit (MiB/s)'),
+ minValue: 0,
+ emptyText: gettext('Defaults to target storage restore limit'),
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext("Use '0' to disable all bandwidth limits.")
+ }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'unique',
+ fieldLabel: gettext('Unique'),
+ hidden: !!me.vmid,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Autogenerate unique properties, e.g., MAC addresses')
+ },
+ checked: false
+ }
+ ];
+
+ /*jslint confusion: true*/
+ if (me.vmtype === 'lxc') {
+ items.push({
+ xtype: 'proxmoxcheckbox',
+ name: 'unprivileged',
+ value: true,
+ fieldLabel: gettext('Unprivileged container')
+ });
+ }
+ /*jslint confusion: false*/
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: items
+ });
+
+ var form = me.formPanel.getForm();
+
+ var doRestore = function(url, params) {
+ Proxmox.Utils.API2Request({
+ url: url,
+ params: params,
+ method: 'POST',
+ waitMsgTarget: me,
+ failure: function (response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid
+ });
+ win.show();
+ me.close();
+ }
+ });
+ };
+
+ var submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Restore'),
+ handler: function(){
+ var storage = storagesel.getValue();
+ var values = form.getValues();
+
+ var params = {
+ storage: storage,
+ vmid: me.vmid || values.vmid,
+ force: me.vmid ? 1 : 0
+ };
+ if (values.unique) { params.unique = 1; }
+
+ if (values.bwlimit !== undefined) {
+ params.bwlimit = values.bwlimit * 1024;
+ }
+
+ var url;
+ var msg;
+ if (me.vmtype === 'lxc') {
+ url = '/nodes/' + me.nodename + '/lxc';
+ params.ostemplate = me.volid;
+ params.restore = 1;
+ if (values.unprivileged) { params.unprivileged = 1; }
+ msg = Proxmox.Utils.format_task_description('vzrestore', params.vmid);
+ } else if (me.vmtype === 'qemu') {
+ url = '/nodes/' + me.nodename + '/qemu';
+ params.archive = me.volid;
+ msg = Proxmox.Utils.format_task_description('qmrestore', params.vmid);
+ } else {
+ throw 'unknown VM type';
+ }
+
+ if (me.vmid) {
+ msg += '. ' + gettext('This will permanently erase current VM data.');
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ doRestore(url, params);
+ });
+ } else {
+ doRestore(url, params);
+ }
+ }
+ });
+
+ form.on('validitychange', function(f, valid) {
+ submitBtn.setDisabled(!valid);
+ });
+
+ var title = gettext('Restore') + ": " + (
+ (me.vmtype === 'lxc') ? 'CT' : 'VM');
+
+ if (me.vmid) {
+ title += " " + me.vmid;
+ }
+
+ Ext.apply(me, {
+ title: title,
+ width: 500,
+ modal: true,
+ layout: 'auto',
+ border: false,
+ items: [ me.formPanel ],
+ buttons: [ submitBtn ]
+ });
+
+ me.callParent();
+ }
+});
+/* Popup a message window
+ * where the user has to manually enter the resource ID
+ * to enable the destroy button
+ */
+Ext.define('PVE.window.SafeDestroy', {
+ extend: 'Ext.window.Window',
+ alias: 'widget.pveSafeDestroy',
+
+ title: gettext('Confirm'),
+ modal: true,
+ buttonAlign: 'center',
+ bodyPadding: 10,
+ width: 450,
+ layout: { type:'hbox' },
+ defaultFocus: 'confirmField',
+ showProgress: false,
+
+ config: {
+ item: {
+ id: undefined,
+ type: undefined
+ },
+ url: undefined,
+ params: {}
+ },
+
+ getParams: function() {
+ var me = this;
+ if (Ext.Object.isEmpty(me.params)) {
+ return '';
+ }
+ return '?' + Ext.Object.toQueryString(me.params);
+ },
+
+ controller: {
+
+ xclass: 'Ext.app.ViewController',
+
+ control: {
+ 'field[name=confirm]': {
+ change: function(f, value) {
+ var view = this.getView();
+ var removeButton = this.lookupReference('removeButton');
+ if (value === view.getItem().id.toString()) {
+ removeButton.enable();
+ } else {
+ removeButton.disable();
+ }
+ },
+ specialkey: function (field, event) {
+ var removeButton = this.lookupReference('removeButton');
+ if (!removeButton.isDisabled() && event.getKey() == event.ENTER) {
+ removeButton.fireEvent('click', removeButton, event);
+ }
+ }
+ },
+ 'button[reference=removeButton]': {
+ click: function() {
+ var view = this.getView();
+ Proxmox.Utils.API2Request({
+ url: view.getUrl() + view.getParams(),
+ method: 'DELETE',
+ waitMsgTarget: view,
+ failure: function(response, opts) {
+ view.close();
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ var hasProgressBar = view.showProgress &&
+ response.result.data ? true : false;
+
+ if (hasProgressBar) {
+ // stay around so we can trigger our close events
+ // when background action is completed
+ view.hide();
+
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', {
+ upid: upid,
+ listeners: {
+ destroy: function () {
+ view.close();
+ }
+ }
+ });
+ win.show();
+ } else {
+ view.close();
+ }
+ }
+ });
+ }
+ }
+ }
+ },
+
+ items: [
+ {
+ xtype: 'component',
+ cls: [ Ext.baseCSSPrefix + 'message-box-icon',
+ Ext.baseCSSPrefix + 'message-box-warning',
+ Ext.baseCSSPrefix + 'dlg-icon']
+ },
+ {
+ xtype: 'container',
+ flex: 1,
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+ items: [
+ {
+ xtype: 'component',
+ reference: 'messageCmp'
+ },
+ {
+ itemId: 'confirmField',
+ reference: 'confirmField',
+ xtype: 'textfield',
+ name: 'confirm',
+ labelWidth: 300,
+ hideTrigger: true,
+ allowBlank: false
+ }
+ ]
+ }
+ ],
+ buttons: [
+ {
+ reference: 'removeButton',
+ text: gettext('Remove'),
+ disabled: true
+ }
+ ],
+
+ initComponent : function() {
+ var me = this;
+
+ me.callParent();
+
+ var item = me.getItem();
+
+ if (!Ext.isDefined(item.id)) {
+ throw "no ID specified";
+ }
+
+ if (!Ext.isDefined(item.type)) {
+ throw "no VM type specified";
+ }
+
+ var messageCmp = me.lookupReference('messageCmp');
+ var msg;
+
+ if (item.type === 'VM') {
+ msg = Proxmox.Utils.format_task_description('qmdestroy', item.id);
+ } else if (item.type === 'CT') {
+ msg = Proxmox.Utils.format_task_description('vzdestroy', item.id);
+ } else if (item.type === 'CephPool') {
+ msg = Proxmox.Utils.format_task_description('cephdestroypool', item.id);
+ } else if (item.type === 'Image') {
+ msg = Proxmox.Utils.format_task_description('unknownimgdel', item.id);
+ } else {
+ throw "unknown item type specified";
+ }
+
+ messageCmp.setHtml(msg);
+
+ var confirmField = me.lookupReference('confirmField');
+ msg = gettext('Please enter the ID to confirm') +
+ ' (' + item.id + ')';
+ confirmField.setFieldLabel(msg);
+ }
+});
+Ext.define('PVE.window.BackupConfig', {
+ extend: 'Ext.window.Window',
+ title: gettext('Configuration'),
+ width: 600,
+ height: 400,
+ layout: 'fit',
+ modal: true,
+ items: {
+ xtype: 'component',
+ itemId: 'configtext',
+ autoScroll: true,
+ style: {
+ 'background-color': 'white',
+ 'white-space': 'pre',
+ 'font-family': 'monospace',
+ padding: '5px'
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.volume) {
+ throw "no volume specified";
+ }
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.callParent();
+
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + nodename + "/vzdump/extractconfig",
+ method: 'GET',
+ params: {
+ volume: me.volume
+ },
+ failure: function(response, opts) {
+ me.close();
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response,options) {
+ me.show();
+ me.down('#configtext').update(Ext.htmlEncode(response.result.data));
+ }
+ });
+ }
+});
+Ext.define('PVE.window.Settings', {
+ extend: 'Ext.window.Window',
+
+ width: '800px',
+ title: gettext('My Settings'),
+ iconCls: 'fa fa-gear',
+ modal: true,
+ bodyPadding: 10,
+ resizable: false,
+
+ buttons: [
+ {
+ xtype: 'proxmoxHelpButton',
+ onlineHelp: 'gui_my_settings',
+ hidden: false
+ },
+ '->',
+ {
+ text: gettext('Close'),
+ handler: function() {
+ this.up('window').close();
+ }
+ }
+ ],
+
+ layout: {
+ type: 'hbox',
+ align: 'top'
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ var me = this;
+ var sp = Ext.state.Manager.getProvider();
+
+ var username = sp.get('login-username') || Proxmox.Utils.noneText;
+ me.lookupReference('savedUserName').setValue(username);
+
+ var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
+ settings.forEach(function(setting) {
+ var val = localStorage.getItem('pve-xterm-' + setting);
+ if (val !== undefined && val !== null) {
+ var field = me.lookup(setting);
+ field.setValue(val);
+ field.resetOriginalValue();
+ }
+ });
+ },
+
+ set_button_status: function() {
+ var me = this;
+
+ var form = me.lookup('xtermform');
+ var valid = form.isValid();
+ var dirty = form.isDirty();
+
+ var hasvalues = false;
+ var values = form.getValues();
+ Ext.Object.eachValue(values, function(value) {
+ if (value) {
+ hasvalues = true;
+ return false;
+ }
+ });
+
+ me.lookup('xtermsave').setDisabled(!dirty || !valid);
+ me.lookup('xtermreset').setDisabled(!hasvalues);
+ },
+
+ control: {
+ '#xtermjs form': {
+ dirtychange: 'set_button_status',
+ validitychange: 'set_button_status'
+ },
+ '#xtermjs button': {
+ click: function(button) {
+ var me = this;
+ var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
+ settings.forEach(function(setting) {
+ var field = me.lookup(setting);
+ if (button.reference === 'xtermsave') {
+ var value = field.getValue();
+ if (value) {
+ localStorage.setItem('pve-xterm-' + setting, value);
+ } else {
+ localStorage.removeItem('pve-xterm-' + setting);
+ }
+ } else if (button.reference === 'xtermreset') {
+ field.setValue(undefined);
+ localStorage.removeItem('pve-xterm-' + setting);
+ }
+ field.resetOriginalValue();
+ });
+ me.set_button_status();
+ }
+ },
+ 'button[name=reset]': {
+ click: function () {
+ var blacklist = ['GuiCap', 'login-username', 'dash-storages'];
+ var sp = Ext.state.Manager.getProvider();
+ var state;
+ for (state in sp.state) {
+ if (sp.state.hasOwnProperty(state)) {
+ if (blacklist.indexOf(state) !== -1) {
+ continue;
+ }
+
+ sp.clear(state);
+ }
+ }
+
+ window.location.reload();
+ }
+ },
+ 'button[name=clear-username]': {
+ click: function () {
+ var me = this;
+ var usernamefield = me.lookupReference('savedUserName');
+ var sp = Ext.state.Manager.getProvider();
+
+ usernamefield.setValue(Proxmox.Utils.noneText);
+ sp.clear('login-username');
+ }
+ },
+ 'grid[reference=dashboard-storages]': {
+ selectionchange: function(grid, selected) {
+ var me = this;
+ var sp = Ext.state.Manager.getProvider();
+
+ // saves the selected storageids as
+ // "id1,id2,id3,..."
+ // or clears the variable
+ if (selected.length > 0) {
+ sp.set('dash-storages',
+ Ext.Array.pluck(selected, 'id').join(','));
+ } else {
+ sp.clear('dash-storages');
+ }
+ },
+ afterrender: function(grid) {
+ var me = grid;
+ var sp = Ext.state.Manager.getProvider();
+ var store = me.getStore();
+ var items = [];
+ me.suspendEvent('selectionchange');
+ var storages = sp.get('dash-storages') || '';
+ storages.split(',').forEach(function(storage){
+ // we have to get the records
+ // to be able to select them
+ if (storage !== '') {
+ var item = store.getById(storage);
+ if (item) {
+ items.push(item);
+ }
+ }
+ });
+ me.getSelectionModel().select(items);
+ me.resumeEvent('selectionchange');
+ }
+ }
+ }
+ },
+
+ items: [{
+ xtype: 'fieldset',
+ width: '50%',
+ title: gettext('Webinterface Settings'),
+ margin: '5',
+ layout: {
+ type: 'vbox',
+ align: 'left'
+ },
+ defaults: {
+ width: '100%',
+ margin: '0 0 10 0'
+ },
+ items: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Dashboard Storages'),
+ labelAlign: 'left',
+ labelWidth: '50%'
+ },
+ {
+ xtype: 'grid',
+ maxHeight: 150,
+ reference: 'dashboard-storages',
+ selModel: {
+ selType: 'checkboxmodel'
+ },
+ columns: [{
+ header: gettext('Name'),
+ dataIndex: 'storage',
+ flex: 1
+ },{
+ header: gettext('Node'),
+ dataIndex: 'node',
+ flex: 1
+ }],
+ store: {
+ type: 'diff',
+ field: ['type', 'storage', 'id', 'node'],
+ rstore: PVE.data.ResourceStore,
+ filters: [{
+ property: 'type',
+ value: 'storage'
+ }],
+ sorters: [ 'node','storage']
+ }
+ },
+ {
+ xtype: 'box',
+ autoEl: { tag: 'hr'}
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Saved User name'),
+ labelAlign: 'left',
+ labelWidth: '50%',
+ stateId: 'login-username',
+ reference: 'savedUserName',
+ value: ''
+ },
+ {
+ xtype: 'button',
+ cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+ text: gettext('Clear User name'),
+ width: 'auto',
+ name: 'clear-username'
+ },
+ {
+ xtype: 'box',
+ autoEl: { tag: 'hr'}
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Layout'),
+ labelAlign: 'left',
+ labelWidth: '50%'
+ },
+ {
+ xtype: 'button',
+ cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+ text: gettext('Reset Layout'),
+ width: 'auto',
+ name: 'reset'
+ }
+ ]
+ },{
+ xtype: 'fieldset',
+ itemId: 'xtermjs',
+ width: '50%',
+ margin: '5',
+ title: gettext('xterm.js Settings'),
+ items: [{
+ xtype: 'form',
+ reference: 'xtermform',
+ border: false,
+ layout: {
+ type: 'vbox',
+ algin: 'left'
+ },
+ defaults: {
+ width: '100%',
+ margin: '0 0 10 0'
+ },
+ items: [
+ {
+ xtype: 'textfield',
+ name: 'fontFamily',
+ reference: 'fontFamily',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Font-Family')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ emptyText: Proxmox.Utils.defaultText,
+ name: 'fontSize',
+ reference: 'fontSize',
+ minValue: 1,
+ fieldLabel: gettext('Font-Size')
+ },
+ {
+ xtype: 'numberfield',
+ name: 'letterSpacing',
+ reference: 'letterSpacing',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Letter Spacing')
+ },
+ {
+ xtype: 'numberfield',
+ name: 'lineHeight',
+ minValue: 0.1,
+ reference: 'lineHeight',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Line Height')
+ },
+ {
+ xtype: 'container',
+ layout: {
+ type: 'hbox',
+ pack: 'end'
+ },
+ items: [
+ {
+ xtype: 'button',
+ reference: 'xtermreset',
+ disabled: true,
+ text: gettext('Reset')
+ },
+ {
+ xtype: 'button',
+ reference: 'xtermsave',
+ disabled: true,
+ text: gettext('Save')
+ }
+ ]
+ }
+ ]
+ }]
+ }],
+
+ onShow: function() {
+ var me = this;
+ me.callParent();
+ }
+});
+Ext.define('PVE.panel.StartupInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ onlineHelp: 'qm_startup_and_shutdown',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var res = PVE.Parser.printStartup(values);
+
+ if (res === undefined || res === '') {
+ return { 'delete': 'startup' };
+ }
+
+ return { startup: res };
+ },
+
+ setStartup: function(value) {
+ var me = this;
+
+ var startup = PVE.Parser.parseStartup(value);
+ if (startup) {
+ me.setValues(startup);
+ }
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: 'textfield',
+ name: 'order',
+ defaultValue: '',
+ emptyText: 'any',
+ fieldLabel: gettext('Start/Shutdown order')
+ },
+ {
+ xtype: 'textfield',
+ name: 'up',
+ defaultValue: '',
+ emptyText: 'default',
+ fieldLabel: gettext('Startup delay')
+ },
+ {
+ xtype: 'textfield',
+ name: 'down',
+ defaultValue: '',
+ emptyText: 'default',
+ fieldLabel: gettext('Shutdown timeout')
+ }
+ ];
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.window.StartupEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveWindowStartupEdit',
+ onlineHelp: undefined,
+
+ initComponent : function() {
+
+ var me = this;
+ var ipanelConfig = me.onlineHelp ? {onlineHelp: me.onlineHelp} : {};
+ var ipanel = Ext.create('PVE.panel.StartupInputPanel', ipanelConfig);
+
+ Ext.applyIf(me, {
+ subject: gettext('Start/Shutdown order'),
+ fieldDefaults: {
+ labelWidth: 120
+ },
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var i, confid;
+ me.vmconfig = response.result.data;
+ ipanel.setStartup(me.vmconfig.startup);
+ }
+ });
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.ceph.Install', {
+ extend: 'Ext.window.Window',
+ xtype: 'pveCephInstallWindow',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ width: 220,
+ header: false,
+ resizable: false,
+ draggable: false,
+ modal: true,
+ nodename: undefined,
+ shadow: false,
+ border: false,
+ bodyBorder: false,
+ closable: false,
+ cls: 'install-mask',
+ bodyCls: 'install-mask',
+ layout: {
+ align: 'stretch',
+ pack: 'center',
+ type: 'vbox'
+ },
+ viewModel: {
+ data: {
+ cephVersion: 'nautilus',
+ isInstalled: false
+ },
+ formulas: {
+ buttonText: function (get){
+ if (get('isInstalled')) {
+ return gettext('Configure Ceph');
+ } else {
+ return gettext('Install Ceph-') + get('cephVersion');
+ }
+ },
+ windowText: function (get) {
+ if (get('isInstalled')) {
+ return '
' +
+ gettext('Would you like to install it now?') + '
Caution: This can reduce performance while it is running.",
+ buttons: Ext.Msg.YESNO,
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ doRequest();
+ }
+ });
+ } else {
+ doRequest();
+ }
+ },
+
+ create_osd: function() {
+ var me = this;
+ var vm = this.getViewModel();
+ Ext.create('PVE.CephCreateOsd', {
+ nodename: vm.get('nodename'),
+ taskDone: () => { me.reload(); }
+ }).show();
+ },
+
+ destroy_osd: function() {
+ var me = this;
+ var vm = this.getViewModel();
+ Ext.create('PVE.CephRemoveOsd', {
+ nodename: vm.get('osdhost'),
+ osdid: vm.get('osdid'),
+ taskDone: () => { me.reload(); }
+ }).show();
+ },
+
+ set_flag: function() {
+ var me = this;
+ var vm = this.getViewModel();
+ var flags = vm.get('flags');
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + vm.get('nodename') + "/ceph/flags/noout",
+ waitMsgTarget: me.getView(),
+ method: flags.includes('noout') ? 'DELETE' : 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: () => { me.reload(); }
+ });
+ },
+
+ service_cmd: function(comp) {
+ var me = this;
+ var vm = this.getViewModel();
+ var cmd = comp.cmd || comp;
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + vm.get('osdhost') + "/ceph/" + cmd,
+ params: { service: "osd." + vm.get('osdid') },
+ waitMsgTarget: me.getView(),
+ method: 'POST',
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', {
+ upid: upid,
+ taskDone: () => { me.reload(); }
+ });
+ win.show();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ },
+
+ set_selection_status: function(tp, selection) {
+ if (selection.length < 1) {
+ return;
+ }
+ var rec = selection[0];
+ var vm = this.getViewModel();
+
+ var isOsd = (rec.data.host && (rec.data.type === 'osd') && (rec.data.id >= 0));
+
+ vm.set('isOsd', isOsd);
+ vm.set('downOsd', isOsd && rec.data.status === 'down');
+ vm.set('upOsd', isOsd && rec.data.status !== 'down');
+ vm.set('inOsd', isOsd && rec.data.in);
+ vm.set('outOsd', isOsd && !rec.data.in);
+ vm.set('osdid', isOsd ? rec.data.id : undefined);
+ vm.set('osdhost', isOsd ? rec.data.host : undefined);
+ },
+
+ render_status: function(value, metaData, rec) {
+ if (!value) {
+ return value;
+ }
+ var inout = rec.data['in'] ? 'in' : 'out';
+ var updownicon = value === 'up' ? 'good fa-arrow-circle-up' :
+ 'critical fa-arrow-circle-down';
+
+ var inouticon = rec.data['in'] ? 'good fa-circle' :
+ 'warning fa-circle-o';
+
+ var text = value + ' / ' +
+ inout + ' ';
+
+ return text;
+ },
+
+ render_wal: function(value, metaData, rec) {
+ if (!value &&
+ rec.data.osdtype === 'bluestore' &&
+ rec.data.type === 'osd') {
+ return 'N/A';
+ }
+ return value;
+ },
+
+ render_version: function(value, metadata, rec) {
+ var vm = this.getViewModel();
+ var versions = vm.get('versions');
+ var icon = "";
+ var version = value || "";
+ if (value && value != vm.get('maxversion')) {
+ icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
+ }
+
+ if (!value && rec.data.type == 'host') {
+ version = versions[rec.data.name] || Proxmox.Utils.unknownText;
+ }
+
+ return icon + version;
+ },
+
+ render_osd_val: function(value, metaData, rec) {
+ return (rec.data.type === 'osd') ? value : '';
+ },
+ render_osd_weight: function(value, metaData, rec) {
+ if (rec.data.type !== 'osd') {
+ return '';
+ }
+ return Ext.util.Format.number(value, '0.00###');
+ },
+
+ render_osd_latency: function(value, metaData, rec) {
+ if (rec.data.type !== 'osd') {
+ return '';
+ }
+ let commit_ms = rec.data.commit_latency_ms,
+ apply_ms = rec.data.apply_latency_ms;
+ return apply_ms + ' / ' + commit_ms;
+ },
+
+ render_osd_size: function(value, metaData, rec) {
+ return this.render_osd_val(PVE.Utils.render_size(value), metaData, rec);
+ },
+
+ control: {
+ '#': {
+ selectionchange: 'set_selection_status'
+ }
+ },
+
+ init: function(view) {
+ var me = this;
+ var vm = this.getViewModel();
+
+ if (!view.pveSelNode.data.node) {
+ throw "no node name specified";
+ }
+
+ vm.set('nodename', view.pveSelNode.data.node);
+
+ me.callParent();
+ me.reload();
+ }
+ },
+
+ stateful: true,
+ stateId: 'grid-ceph-osd',
+ rootVisible: false,
+ useArrows: true,
+
+ columns: [
+ {
+ xtype: 'treecolumn',
+ text: 'Name',
+ dataIndex: 'name',
+ width: 150
+ },
+ {
+ text: 'Type',
+ dataIndex: 'type',
+ hidden: true,
+ align: 'right',
+ width: 75
+ },
+ {
+ text: gettext("Class"),
+ dataIndex: 'device_class',
+ align: 'right',
+ width: 75
+ },
+ {
+ text: "OSD Type",
+ dataIndex: 'osdtype',
+ align: 'right',
+ width: 100
+ },
+ {
+ text: "Bluestore Device",
+ dataIndex: 'blfsdev',
+ align: 'right',
+ width: 75,
+ hidden: true
+ },
+ {
+ text: "DB Device",
+ dataIndex: 'dbdev',
+ align: 'right',
+ width: 75,
+ hidden: true
+ },
+ {
+ text: "WAL Device",
+ dataIndex: 'waldev',
+ align: 'right',
+ renderer: 'render_wal',
+ width: 75,
+ hidden: true
+ },
+ {
+ text: 'Status',
+ dataIndex: 'status',
+ align: 'right',
+ renderer: 'render_status',
+ width: 120
+ },
+ {
+ text: gettext('Version'),
+ dataIndex: 'version',
+ align: 'right',
+ renderer: 'render_version'
+ },
+ {
+ text: 'weight',
+ dataIndex: 'crush_weight',
+ align: 'right',
+ renderer: 'render_osd_weight',
+ width: 90
+ },
+ {
+ text: 'reweight',
+ dataIndex: 'reweight',
+ align: 'right',
+ renderer: 'render_osd_weight',
+ width: 90
+ },
+ {
+ text: gettext('Used') + ' (%)',
+ dataIndex: 'percent_used',
+ align: 'right',
+ renderer: function(value, metaData, rec) {
+ if (rec.data.type !== 'osd') {
+ return '';
+ }
+ return Ext.util.Format.number(value, '0.00');
+ },
+ width: 100
+ },
+ {
+ text: gettext('Total'),
+ dataIndex: 'total_space',
+ align: 'right',
+ renderer: 'render_osd_size',
+ width: 100
+ },
+ {
+ text: 'Apply/Commit
Latency (ms)',
+ dataIndex: 'apply_latency_ms',
+ align: 'right',
+ renderer: 'render_osd_latency',
+ width: 120
+ }
+ ],
+
+
+ tbar: {
+ items: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: 'reload'
+ },
+ '-',
+ {
+ text: gettext('Create') + ': OSD',
+ handler: 'create_osd',
+ },
+ {
+ text: gettext('Set noout'),
+ itemId: 'nooutBtn',
+ handler: 'set_flag',
+ },
+ '->',
+ {
+ xtype: 'tbtext',
+ data: {
+ osd: undefined
+ },
+ bind: {
+ data: {
+ osd: "{osdid}"
+ }
+ },
+ tpl: [
+ '' + Ext.htmlEncode(record.data.detail) + '
'
+ ]
+ }]
+ });
+ win.show();
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ xtype: 'pveCephStatusDetail',
+ itemId: 'statusdetail',
+ plugins: 'responsive',
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: 1,
+ minHeight: 250
+ },
+ 'width >= 1900': {
+ columnWidth: 0.5,
+ minHeight: 300
+ }
+ },
+ title: gettext('Status')
+ },
+ {
+ title: gettext('Services'),
+ xtype: 'pveCephServices',
+ itemId: 'services',
+ plugins: 'responsive',
+ layout: {
+ type: 'hbox',
+ align: 'stretch'
+ },
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: 1,
+ minHeight: 200
+ },
+ 'width >= 1900': {
+ columnWidth: 0.5,
+ minHeight: 200
+ }
+ }
+ },
+ {
+ xtype: 'panel',
+ title: gettext('Performance'),
+ columnWidth: 1,
+ bodyPadding: 5,
+ layout: {
+ type: 'hbox',
+ align: 'center'
+ },
+ items: [
+ {
+ flex: 1,
+ xtype: 'proxmoxGauge',
+ itemId: 'space',
+ title: gettext('Usage')
+ },
+ {
+ flex: 2,
+ xtype: 'container',
+ defaults: {
+ padding: 0,
+ height: 100
+ },
+ items: [
+ {
+ itemId: 'reads',
+ xtype: 'pveRunningChart',
+ title: gettext('Reads'),
+ renderer: PVE.Utils.render_bandwidth
+ },
+ {
+ itemId: 'writes',
+ xtype: 'pveRunningChart',
+ title: gettext('Writes'),
+ renderer: PVE.Utils.render_bandwidth
+ },
+ {
+ itemId: 'iops',
+ xtype: 'pveRunningChart',
+ hidden: true,
+ title: 'IOPS', // do not localize
+ renderer: Ext.util.Format.numberRenderer('0,000')
+ },
+ {
+ itemId: 'readiops',
+ xtype: 'pveRunningChart',
+ hidden: true,
+ title: 'IOPS: ' + gettext('Reads'),
+ renderer: Ext.util.Format.numberRenderer('0,000')
+ },
+ {
+ itemId: 'writeiops',
+ xtype: 'pveRunningChart',
+ hidden: true,
+ title: 'IOPS: ' + gettext('Writes'),
+ renderer: Ext.util.Format.numberRenderer('0,000')
+ }
+ ]
+ }
+ ]
+ }
+ ],
+
+ generateCheckData: function(health) {
+ var result = [];
+ var checks = health.checks || {};
+ var keys = Ext.Object.getKeys(checks).sort();
+
+ Ext.Array.forEach(keys, function(key) {
+ var details = checks[key].detail || [];
+ result.push({
+ id: key,
+ summary: checks[key].summary.message,
+ detail: Ext.Array.reduce(
+ checks[key].detail,
+ function(first, second) {
+ return first + '\n' + second.message;
+ },
+ ''
+ ),
+ severity: checks[key].severity
+ });
+ });
+
+ return result;
+ },
+
+ updateAll: function(store, records, success) {
+ if (!success || records.length === 0) {
+ return;
+ }
+
+ var me = this;
+ var rec = records[0];
+ me.status = rec.data;
+
+ // add health panel
+ me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {}));
+ // add errors to gridstore
+ me.down('#warnings').getStore().loadRawData(me.generateCheckData(rec.data.health || {}), false);
+
+ // update services
+ me.getComponent('services').updateAll(me.metadata || {}, rec.data);
+
+ // update detailstatus panel
+ me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data);
+
+ // add performance data
+ var used = rec.data.pgmap.bytes_used;
+ var total = rec.data.pgmap.bytes_total;
+
+ var text = Ext.String.format(gettext('{0} of {1}'),
+ PVE.Utils.render_size(used),
+ PVE.Utils.render_size(total)
+ );
+
+ // update the usage widget
+ me.down('#space').updateValue(used/total, text);
+
+ // TODO: logic for jewel (iops split in read/write)
+
+ var iops = rec.data.pgmap.op_per_sec;
+ var readiops = rec.data.pgmap.read_op_per_sec;
+ var writeiops = rec.data.pgmap.write_op_per_sec;
+ var reads = rec.data.pgmap.read_bytes_sec || 0;
+ var writes = rec.data.pgmap.write_bytes_sec || 0;
+
+ if (iops !== undefined && me.version !== 'hammer') {
+ me.change_version('hammer');
+ } else if((readiops !== undefined || writeiops !== undefined) && me.version !== 'jewel') {
+ me.change_version('jewel');
+ }
+ // update the graphs
+ me.reads.addDataPoint(reads);
+ me.writes.addDataPoint(writes);
+ me.iops.addDataPoint(iops);
+ me.readiops.addDataPoint(readiops);
+ me.writeiops.addDataPoint(writeiops);
+ },
+
+ change_version: function(version) {
+ var me = this;
+ me.version = version;
+ me.sp.set('ceph-version', version);
+ me.iops.setVisible(version === 'hammer');
+ me.readiops.setVisible(version === 'jewel');
+ me.writeiops.setVisible(version === 'jewel');
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+
+ me.callParent();
+ var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph';
+ me.store = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'ceph-status-' + (nodename || 'cluster'),
+ interval: 5000,
+ proxy: {
+ type: 'proxmox',
+ url: baseurl + '/status'
+ }
+ });
+
+ me.metadatastore = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'ceph-metadata-' + (nodename || 'cluster'),
+ interval: 15*1000,
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/ceph/metadata'
+ }
+ });
+
+ // save references for the updatefunction
+ me.iops = me.down('#iops');
+ me.readiops = me.down('#readiops');
+ me.writeiops = me.down('#writeiops');
+ me.reads = me.down('#reads');
+ me.writes = me.down('#writes');
+
+ // get ceph version
+ me.sp = Ext.state.Manager.getProvider();
+ me.version = me.sp.get('ceph-version');
+ me.change_version(me.version);
+
+ var regex = new RegExp("not (installed|initialized)", "i");
+ PVE.Utils.handleStoreErrorOrMask(me, me.store, regex, function(me, error){
+ me.store.stopUpdate();
+ PVE.Utils.showCephInstallOrMask(me, error.statusText, (nodename || 'localhost'),
+ function(win){
+ me.mon(win, 'cephInstallWindowClosed', function(){
+ me.store.startUpdate();
+ });
+ }
+ );
+ });
+
+ me.mon(me.store, 'load', me.updateAll, me);
+ me.mon(me.metadatastore, 'load', function(store, records, success) {
+ if (!success || records.length < 1) {
+ return;
+ }
+ var rec = records[0];
+ me.metadata = rec.data;
+
+ // update services
+ me.getComponent('services').updateAll(rec.data, me.status || {});
+
+ // update detailstatus panel
+ me.getComponent('statusdetail').updateAll(rec.data, me.status || {});
+
+ }, me);
+
+ me.on('destroy', me.store.stopUpdate);
+ me.on('destroy', me.metadatastore.stopUpdate);
+ me.store.startUpdate();
+ me.metadatastore.startUpdate();
+ }
+
+});
+Ext.define('PVE.ceph.StatusDetail', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveCephStatusDetail',
+
+ layout: {
+ type: 'hbox',
+ align: 'stretch'
+ },
+
+ bodyPadding: '0 5',
+ defaults: {
+ xtype: 'box',
+ style: {
+ 'text-align':'center'
+ }
+ },
+
+ items: [{
+ flex: 1,
+ itemId: 'osds',
+ maxHeight: 250,
+ scrollable: true,
+ padding: '0 10 5 10',
+ data: {
+ total: 0,
+ upin: 0,
+ upout: 0,
+ downin: 0,
+ downout: 0,
+ oldosds: []
+ },
+ tpl: [
+ '' + 'OSDs' + '
',
+ '',
+ '
',
+ ' ',
+ '',
+ ' ',
+ gettext('In'),
+ ' ',
+ '',
+ gettext('Out'),
+ ' ',
+ '',
+ ' ',
+ '',
+ gettext('Up'),
+ ' ',
+ '{upin} ',
+ '{upout} ',
+ '',
+ ' ',
+ '',
+ gettext('Down'),
+ ' ',
+ '{downin} ',
+ '{downout} ',
+ '
',
+ '
",
+ '
',
+ '',
+ '
';
+ record.get('states').forEach(function(state) {
+ html += '
' +
+ state.state_name + ': ' + state.count.toString();
+ });
+ tooltip.setHtml(html);
+ }
+ },
+ subStyle: {
+ strokeStyle: false
+ }
+ }
+ ]
+ },
+ {
+ flex: 1.6,
+ itemId: 'pgs',
+ padding: '0 10',
+ maxHeight: 250,
+ scrollable: true,
+ data: {
+ states: []
+ },
+ tpl: [
+ '' + 'PGs' + '
',
+ '
',
+ '',
+ '
');
+
+ result.health = healthmap[result.health];
+
+ me[type][id] = result;
+ }
+ }
+
+ me.getComponent('mons').updateAll(Object.values(me.mon));
+ me.getComponent('mgrs').updateAll(Object.values(me.mgr));
+ me.getComponent('mdss').updateAll(Object.values(me.mds));
+ }
+});
+
+Ext.define('PVE.ceph.ServiceList', {
+ extend: 'Ext.container.Container',
+ xtype: 'pveCephServiceList',
+
+ style: {
+ 'text-align':'center'
+ },
+ defaults: {
+ xtype: 'box',
+ style: {
+ 'text-align':'center'
+ }
+ },
+
+ items: [
+ {
+ itemId: 'title',
+ data: {
+ title: ''
+ },
+ tpl: '{title}
'
+ }
+ ],
+
+ updateAll: function(list) {
+ var me = this;
+ me.suspendLayout = true;
+
+ var i;
+ list.sort(function(a,b) {
+ return a.id > b.id ? 1 : a.id < b.id ? -1 : 0;
+ });
+ var ids = {};
+ if (me.ids) {
+ me.ids.forEach(function(id) {
+ ids[id] = true;
+ });
+ }
+ for (i = 0; i < list.length; i++) {
+ var service = me.getComponent(list[i].id);
+ if (!service) {
+ // since services are already sorted, and
+ // we always have a sorted list
+ // we can add it at the service+1 position (because of the title)
+ service = me.insert(i+1, {
+ xtype: 'pveCephServiceWidget',
+ itemId: list[i].id
+ });
+ if (!me.ids) {
+ me.ids = [];
+ }
+ me.ids.push(list[i].id);
+ } else {
+ delete ids[list[i].id];
+ }
+ service.updateService(list[i].title, list[i].text, list[i].health);
+ }
+
+ Object.keys(ids).forEach(function(id) {
+ me.remove(id);
+ });
+ me.suspendLayout = false;
+ me.updateLayout();
+ },
+
+ initComponent: function() {
+ var me = this;
+ me.callParent();
+ me.getComponent('title').update({
+ title: me.title
+ });
+ }
+});
+
+/*jslint confusion: true*/
+Ext.define('PVE.ceph.ServiceWidget', {
+ extend: 'Ext.Component',
+ alias: 'widget.pveCephServiceWidget',
+
+ userCls: 'monitor inline-block',
+ data: {
+ title: '0',
+ health: 'HEALTH_ERR',
+ text: '',
+ iconCls: PVE.Utils.get_health_icon()
+ },
+
+ tpl: [
+ '{title}: ',
+ ''
+ ],
+
+ updateService: function(title, text, health) {
+ var me = this;
+
+ me.update(Ext.apply(me.data, {
+ health: health,
+ text: text,
+ title: title,
+ iconCls: PVE.Utils.get_health_icon(PVE.Utils.map_ceph_health[health])
+ }));
+
+ if (me.tooltip) {
+ me.tooltip.setHtml(text);
+ }
+ },
+
+ listeners: {
+ destroy: function() {
+ var me = this;
+ if (me.tooltip) {
+ me.tooltip.destroy();
+ delete me.tooltip;
+ }
+ },
+ mouseenter: {
+ element: 'el',
+ fn: function(events, element) {
+ var me = this.component;
+ if (!me) {
+ return;
+ }
+ if (!me.tooltip) {
+ me.tooltip = Ext.create('Ext.tip.ToolTip', {
+ target: me.el,
+ trackMouse: true,
+ dismissDelay: 0,
+ renderTo: Ext.getBody(),
+ html: me.data.text
+ });
+ }
+ me.tooltip.show();
+ }
+ },
+ mouseleave: {
+ element: 'el',
+ fn: function(events, element) {
+ var me = this.component;
+ if (me.tooltip) {
+ me.tooltip.destroy();
+ delete me.tooltip;
+ }
+ }
+ }
+ }
+});
+Ext.define('PVE.node.CephConfigDb', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveNodeCephConfigDb',
+
+ border: false,
+ store: {
+ proxy: {
+ type: 'proxmox'
+ }
+ },
+
+ columns: [
+ {
+ dataIndex: 'section',
+ text: 'WHO',
+ width: 100,
+ },
+ {
+ dataIndex: 'mask',
+ text: 'MASK',
+ hidden: true,
+ width: 80,
+ },
+ {
+ dataIndex: 'level',
+ hidden: true,
+ text: 'LEVEL',
+ },
+ {
+ dataIndex: 'name',
+ flex: 1,
+ text: 'OPTION',
+ },
+ {
+ dataIndex: 'value',
+ flex: 1,
+ text: 'VALUE',
+ },
+ {
+ dataIndex: 'can_update_at_runtime',
+ text: 'Runtime Updatable',
+ hidden: true,
+ width: 80,
+ renderer: Proxmox.Utils.format_boolean
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.store.proxy.url = '/api2/json/nodes/' + nodename + '/ceph/configdb';
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, me.getStore());
+ me.getStore().load();
+ }
+});
+Ext.define('PVE.node.CephConfig', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNodeCephConfig',
+
+ bodyStyle: 'white-space:pre',
+ bodyPadding: 5,
+ border: false,
+ scrollable: true,
+ load: function() {
+ var me = this;
+
+ Proxmox.Utils.API2Request({
+ url: me.url,
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ me.update(gettext('Error') + " " + response.htmlStatus);
+ var msg = response.htmlStatus;
+ PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node,
+ function(win){
+ me.mon(win, 'cephInstallWindowClosed', function(){
+ me.load();
+ });
+ }
+ );
+
+ },
+ success: function(response, opts) {
+ var data = response.result.data;
+ me.update(Ext.htmlEncode(data));
+ }
+ });
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.apply(me, {
+ url: '/nodes/' + nodename + '/ceph/config',
+ listeners: {
+ activate: function() {
+ me.load();
+ }
+ }
+ });
+
+ me.callParent();
+
+ me.load();
+ }
+});
+
+Ext.define('PVE.node.CephConfigCrush', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNodeCephConfigCrush',
+
+ onlineHelp: 'chapter_pveceph',
+
+ layout: 'border',
+ items: [{
+ title: gettext('Configuration'),
+ xtype: 'pveNodeCephConfig',
+ region: 'center'
+ },
+ {
+ title: 'Crush Map', // do not localize
+ xtype: 'pveNodeCephCrushMap',
+ region: 'east',
+ split: true,
+ width: '50%'
+ },
+ {
+ title: gettext('Configuration Database'),
+ xtype: 'pveNodeCephConfigDb',
+ region: 'south',
+ split: true,
+ weight: -30,
+ height: '50%'
+ }],
+
+ initComponent: function() {
+ var me = this;
+ me.defaults = {
+ pveSelNode: me.pveSelNode
+ };
+ me.callParent();
+ }
+});
+Ext.define('PVE.ceph.Log', {
+ extend: 'Proxmox.panel.LogView',
+ xtype: 'cephLogView',
+ nodename: undefined,
+ failCallback: function(response) {
+ var me = this;
+ var msg = response.htmlStatus;
+ var windowShow = PVE.Utils.showCephInstallOrMask(me, msg, me.nodename,
+ function(win){
+ me.mon(win, 'cephInstallWindowClosed', function(){
+ me.loadTask.delay(200);
+ });
+ }
+ );
+ if (!windowShow) {
+ Proxmox.Utils.setErrorMask(me, msg);
+ }
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.ceph.CephInstallWizard', {
+ extend: 'PVE.window.Wizard',
+ alias: 'widget.pveCephInstallWizard',
+ mixins: ['Proxmox.Mixin.CBind'],
+ resizable: false,
+ nodename: undefined,
+ viewModel: {
+ data: {
+ nodename: '',
+ configuration: true,
+ isInstalled: false
+ }
+ },
+ cbindData: {
+ nodename: undefined
+ },
+ title: gettext('Setup'),
+ navigateNext: function() {
+ var tp = this.down('#wizcontent');
+ var atab = tp.getActiveTab();
+
+ var next = tp.items.indexOf(atab) + 1;
+ var ntab = tp.items.getAt(next);
+ if (ntab) {
+ ntab.enable();
+ tp.setActiveTab(ntab);
+ }
+ },
+ setInitialTab: function (index) {
+ var tp = this.down('#wizcontent');
+ var initialTab = tp.items.getAt(index);
+ initialTab.enable();
+ tp.setActiveTab(initialTab);
+ },
+ onShow: function() {
+ this.callParent(arguments);
+ var isInstalled = this.getViewModel().get('isInstalled');
+ if (isInstalled) {
+ this.getViewModel().set('configuration', false);
+ this.setInitialTab(2);
+ }
+ },
+ items: [
+ {
+ title: gettext('Info'),
+ xtype: 'panel',
+ border: false,
+ bodyBorder: false,
+ onlineHelp: 'chapter_pveceph',
+ html: 'Ceph?
'+
+ '
'+
+ 'Installation successful!
'+
+ '
'+
+ '
');
+ }
+ return Proxmox.Utils.noneText;
+ }
+ }
+ };
+ /*jslint confusion: false*/
+
+ me.callParent();
+ me.mon(me.rstore, 'load', me.set_button_status, me);
+ me.rstore.startUpdate();
+ me.load_account();
+ }
+});
+Ext.define('PVE.node.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.node.Config',
+
+ onlineHelp: 'chapter_system_administration',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: "/api2/json/nodes/" + nodename + "/status",
+ interval: 1000
+ });
+
+ var node_command = function(cmd) {
+ Proxmox.Utils.API2Request({
+ params: { command: cmd },
+ url: '/nodes/' + nodename + '/status',
+ method: 'POST',
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ };
+
+ var actionBtn = Ext.create('Ext.Button', {
+ text: gettext('Bulk Actions'),
+ iconCls: 'fa fa-fw fa-ellipsis-v',
+ disabled: !caps.nodes['Sys.PowerMgmt'],
+ menu: new Ext.menu.Menu({
+ items: [
+ {
+ text: gettext('Bulk Start'),
+ iconCls: 'fa fa-fw fa-play',
+ handler: function() {
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: nodename,
+ title: gettext('Bulk Start'),
+ btnText: gettext('Start'),
+ action: 'startall'
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Bulk Stop'),
+ iconCls: 'fa fa-fw fa-stop',
+ handler: function() {
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: nodename,
+ title: gettext('Bulk Stop'),
+ btnText: gettext('Stop'),
+ action: 'stopall'
+ });
+ win.show();
+ }
+ },
+ {
+ text: gettext('Bulk Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ handler: function() {
+ var win = Ext.create('PVE.window.BulkAction', {
+ nodename: nodename,
+ title: gettext('Bulk Migrate'),
+ btnText: gettext('Migrate'),
+ action: 'migrateall'
+ });
+ win.show();
+ }
+ }
+ ]
+ })
+ });
+
+ var restartBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('Reboot'),
+ disabled: !caps.nodes['Sys.PowerMgmt'],
+ dangerous: true,
+ confirmMsg: Ext.String.format(gettext("Reboot node '{0}'?"), nodename),
+ handler: function() {
+ node_command('reboot');
+ },
+ iconCls: 'fa fa-undo'
+ });
+
+ var shutdownBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('Shutdown'),
+ disabled: !caps.nodes['Sys.PowerMgmt'],
+ dangerous: true,
+ confirmMsg: Ext.String.format(gettext("Shutdown node '{0}'?"), nodename),
+ handler: function() {
+ node_command('shutdown');
+ },
+ iconCls: 'fa fa-power-off'
+ });
+
+ var shellBtn = Ext.create('PVE.button.ConsoleButton', {
+ disabled: !caps.nodes['Sys.Console'],
+ text: gettext('Shell'),
+ consoleType: 'shell',
+ nodename: nodename
+ });
+
+ me.items = [];
+
+ Ext.apply(me, {
+ title: gettext('Node') + " '" + nodename + "'",
+ hstateid: 'nodetab',
+ defaults: { statusStore: me.statusStore },
+ tbar: [ restartBtn, shutdownBtn, shellBtn, actionBtn]
+ });
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ title: gettext('Summary'),
+ iconCls: 'fa fa-book',
+ itemId: 'summary',
+ xtype: 'pveNodeSummary'
+ },
+ {
+ title: gettext('Notes'),
+ iconCls: 'fa fa-sticky-note-o',
+ itemId: 'notes',
+ xtype: 'pveNotesView'
+ }
+ );
+ }
+
+ if (caps.nodes['Sys.Console']) {
+ me.items.push(
+ {
+ title: gettext('Shell'),
+ iconCls: 'fa fa-terminal',
+ itemId: 'jsconsole',
+ xtype: 'pveNoVncConsole',
+ consoleType: 'shell',
+ xtermjs: true,
+ nodename: nodename
+ }
+ );
+ }
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ title: gettext('System'),
+ iconCls: 'fa fa-cogs',
+ itemId: 'services',
+ expandedOnInit: true,
+ startOnlyServices: {
+ 'pveproxy': true,
+ 'pvedaemon': true,
+ 'pve-cluster': true
+ },
+ nodename: nodename,
+ onlineHelp: 'pve_service_daemons',
+ xtype: 'proxmoxNodeServiceView'
+ },
+ {
+ title: gettext('Network'),
+ iconCls: 'fa fa-exchange',
+ itemId: 'network',
+ groups: ['services'],
+ nodename: nodename,
+ onlineHelp: 'sysadmin_network_configuration',
+ xtype: 'proxmoxNodeNetworkView'
+ },
+ {
+ title: gettext('Certificates'),
+ iconCls: 'fa fa-certificate',
+ itemId: 'certificates',
+ groups: ['services'],
+ nodename: nodename,
+ xtype: 'pveCertificatesView'
+ },
+ {
+ title: gettext('DNS'),
+ iconCls: 'fa fa-globe',
+ groups: ['services'],
+ itemId: 'dns',
+ nodename: nodename,
+ onlineHelp: 'sysadmin_network_configuration',
+ xtype: 'proxmoxNodeDNSView'
+ },
+ {
+ title: gettext('Hosts'),
+ iconCls: 'fa fa-globe',
+ groups: ['services'],
+ itemId: 'hosts',
+ nodename: nodename,
+ onlineHelp: 'sysadmin_network_configuration',
+ xtype: 'proxmoxNodeHostsView'
+ },
+ {
+ title: gettext('Time'),
+ itemId: 'time',
+ groups: ['services'],
+ nodename: nodename,
+ xtype: 'proxmoxNodeTimeView',
+ iconCls: 'fa fa-clock-o'
+ });
+ }
+
+ if (caps.nodes['Sys.Syslog']) {
+ me.items.push({
+ title: 'Syslog',
+ iconCls: 'fa fa-list',
+ groups: ['services'],
+ disabled: !caps.nodes['Sys.Syslog'],
+ itemId: 'syslog',
+ xtype: 'proxmoxJournalView',
+ url: "/api2/extjs/nodes/" + nodename + "/journal"
+ });
+
+ if (caps.nodes['Sys.Modify']) {
+ me.items.push({
+ title: gettext('Updates'),
+ iconCls: 'fa fa-refresh',
+ disabled: !caps.nodes['Sys.Console'],
+ // do we want to link to system updates instead?
+ itemId: 'apt',
+ xtype: 'proxmoxNodeAPT',
+ upgradeBtn: {
+ xtype: 'pveConsoleButton',
+ disabled: Proxmox.UserName !== 'root@pam',
+ text: gettext('Upgrade'),
+ consoleType: 'upgrade',
+ nodename: nodename
+ },
+ nodename: nodename
+ });
+ }
+ }
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ xtype: 'pveFirewallRules',
+ iconCls: 'fa fa-shield',
+ title: gettext('Firewall'),
+ allow_iface: true,
+ base_url: '/nodes/' + nodename + '/firewall/rules',
+ list_refs_url: '/cluster/firewall/refs',
+ itemId: 'firewall'
+ },
+ {
+ xtype: 'pveFirewallOptions',
+ title: gettext('Options'),
+ iconCls: 'fa fa-gear',
+ onlineHelp: 'pve_firewall_host_specific_configuration',
+ groups: ['firewall'],
+ base_url: '/nodes/' + nodename + '/firewall/options',
+ fwtype: 'node',
+ itemId: 'firewall-options'
+ });
+ }
+
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ title: gettext('Disks'),
+ itemId: 'storage',
+ expandedOnInit: true,
+ iconCls: 'fa fa-hdd-o',
+ xtype: 'pveNodeDiskList'
+ },
+ {
+ title: 'LVM',
+ itemId: 'lvm',
+ onlineHelp: 'chapter_lvm',
+ iconCls: 'fa fa-square',
+ groups: ['storage'],
+ xtype: 'pveLVMList'
+ },
+ {
+ title: 'LVM-Thin',
+ itemId: 'lvmthin',
+ onlineHelp: 'chapter_lvm',
+ iconCls: 'fa fa-square-o',
+ groups: ['storage'],
+ xtype: 'pveLVMThinList'
+ },
+ {
+ title: Proxmox.Utils.directoryText,
+ itemId: 'directory',
+ onlineHelp: 'chapter_storage',
+ iconCls: 'fa fa-folder',
+ groups: ['storage'],
+ xtype: 'pveDirectoryList'
+ },
+ {
+ title: 'ZFS',
+ itemId: 'zfs',
+ onlineHelp: 'chapter_zfs',
+ iconCls: 'fa fa-th-large',
+ groups: ['storage'],
+ xtype: 'pveZFSList'
+ },
+ {
+ title: 'Ceph',
+ itemId: 'ceph',
+ iconCls: 'fa fa-ceph',
+ xtype: 'pveNodeCephStatus'
+ },
+ {
+ xtype: 'pveReplicaView',
+ iconCls: 'fa fa-retweet',
+ title: gettext('Replication'),
+ itemId: 'replication'
+ },
+ {
+ xtype: 'pveNodeCephConfigCrush',
+ title: gettext('Configuration'),
+ iconCls: 'fa fa-gear',
+ groups: ['ceph'],
+ itemId: 'ceph-config'
+ },
+ {
+ xtype: 'pveNodeCephMonMgr',
+ title: gettext('Monitor'),
+ iconCls: 'fa fa-tv',
+ groups: ['ceph'],
+ itemId: 'ceph-monlist'
+ },
+ {
+ xtype: 'pveNodeCephOsdTree',
+ title: 'OSD',
+ iconCls: 'fa fa-hdd-o',
+ groups: ['ceph'],
+ itemId: 'ceph-osdtree'
+ },
+ {
+ xtype: 'pveNodeCephFSPanel',
+ title: 'CephFS',
+ iconCls: 'fa fa-folder',
+ groups: ['ceph'],
+ nodename: nodename,
+ itemId: 'ceph-cephfspanel'
+ },
+ {
+ xtype: 'pveNodeCephPoolList',
+ title: 'Pools',
+ iconCls: 'fa fa-sitemap',
+ groups: ['ceph'],
+ itemId: 'ceph-pools'
+ }
+ );
+ }
+
+ if (caps.nodes['Sys.Syslog']) {
+ me.items.push(
+ {
+ xtype: 'proxmoxLogView',
+ title: gettext('Log'),
+ iconCls: 'fa fa-list',
+ groups: ['firewall'],
+ onlineHelp: 'chapter_pve_firewall',
+ url: '/api2/extjs/nodes/' + nodename + '/firewall/log',
+ itemId: 'firewall-fwlog'
+ },
+ {
+ title: gettext('Log'),
+ itemId: 'ceph-log',
+ iconCls: 'fa fa-list',
+ groups: ['ceph'],
+ onlineHelp: 'chapter_pveceph',
+ xtype: 'cephLogView',
+ url: "/api2/extjs/nodes/" + nodename + "/ceph/log",
+ nodename: nodename
+ });
+ }
+
+ me.items.push(
+ {
+ title: gettext('Task History'),
+ iconCls: 'fa fa-list',
+ itemId: 'tasks',
+ nodename: nodename,
+ xtype: 'proxmoxNodeTasks'
+ },
+ {
+ title: gettext('Subscription'),
+ iconCls: 'fa fa-support',
+ itemId: 'support',
+ xtype: 'pveNodeSubscription',
+ nodename: nodename
+ }
+ );
+
+ me.callParent();
+
+ me.mon(me.statusStore, 'load', function(s, records, success) {
+ var uptimerec = s.data.get('uptime');
+ var powermgmt = uptimerec ? uptimerec.data.value : false;
+ if (!caps.nodes['Sys.PowerMgmt']) {
+ powermgmt = false;
+ }
+ restartBtn.setDisabled(!powermgmt);
+ shutdownBtn.setDisabled(!powermgmt);
+ shellBtn.setDisabled(!powermgmt);
+ });
+
+ me.on('afterrender', function() {
+ me.statusStore.startUpdate();
+ });
+
+ me.on('destroy', function() {
+ me.statusStore.stopUpdate();
+ });
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.window.Migrate', {
+ extend: 'Ext.window.Window',
+
+ vmtype: undefined,
+ nodename: undefined,
+ vmid: undefined,
+
+ viewModel: {
+ data: {
+ vmid: undefined,
+ nodename: undefined,
+ vmtype: undefined,
+ running: false,
+ qemu: {
+ onlineHelp: 'qm_migration',
+ commonName: 'VM'
+ },
+ lxc: {
+ onlineHelp: 'pct_migration',
+ commonName: 'CT'
+ },
+ migration: {
+ possible: true,
+ preconditions: [],
+ 'with-local-disks': 0,
+ mode: undefined,
+ allowedNodes: undefined
+ }
+
+ },
+
+ formulas: {
+ setMigrationMode: function(get) {
+ if (get('running')){
+ if (get('vmtype') === 'qemu') {
+ return gettext('Online');
+ } else {
+ return gettext('Restart Mode');
+ }
+ } else {
+ return gettext('Offline');
+ }
+ },
+ setStorageselectorHidden: function(get) {
+ if (get('migration.with-local-disks') && get('running')) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'panel[reference=formPanel]': {
+ validityChange: function(panel, isValid) {
+ this.getViewModel().set('migration.possible', isValid);
+ this.checkMigratePreconditions();
+ }
+ }
+ },
+
+ init: function(view) {
+ var me = this,
+ vm = view.getViewModel();
+
+ if (!view.nodename) {
+ throw "missing custom view config: nodename";
+ }
+ vm.set('nodename', view.nodename);
+
+ if (!view.vmid) {
+ throw "missing custom view config: vmid";
+ }
+ vm.set('vmid', view.vmid);
+
+ if (!view.vmtype) {
+ throw "missing custom view config: vmtype";
+ }
+ vm.set('vmtype', view.vmtype);
+
+
+ view.setTitle(
+ Ext.String.format('{0} {1}{2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid)
+ );
+ me.lookup('proxmoxHelpButton').setHelpConfig({
+ onlineHelp: vm.get(view.vmtype).onlineHelp
+ });
+ me.checkMigratePreconditions();
+ me.lookup('formPanel').isValid();
+
+ },
+
+ onTargetChange: function (nodeSelector) {
+ //Always display the storages of the currently seleceted migration target
+ this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value);
+ this.checkMigratePreconditions();
+ },
+
+ startMigration: function() {
+ var me = this,
+ view = me.getView(),
+ vm = me.getViewModel();
+
+ var values = me.lookup('formPanel').getValues();
+ var params = {
+ target: values.target
+ };
+
+ if (vm.get('migration.mode')) {
+ params[vm.get('migration.mode')] = 1;
+ }
+ if (vm.get('migration.with-local-disks')) {
+ params['with-local-disks'] = 1;
+ }
+ //only submit targetstorage if vm is running, storage migration to different storage is only possible online
+ if (vm.get('migration.with-local-disks') && vm.get('running')) {
+ params.targetstorage = values.targetstorage;
+ }
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
+ waitMsgTarget: view,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var extraTitle = Ext.String.format(' ({0} ---> {1})', vm.get('nodename'), params.target);
+
+ Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid,
+ extraTitle: extraTitle
+ }).show();
+
+ view.close();
+ }
+ });
+
+ },
+
+ checkMigratePreconditions: function() {
+ var me = this,
+ vm = me.getViewModel();
+
+
+ var vmrec = PVE.data.ResourceStore.findRecord('vmid', vm.get('vmid'),
+ 0, false, false, true);
+ if (vmrec && vmrec.data && vmrec.data.running) {
+ vm.set('running', true);
+ }
+
+ if (vm.get('vmtype') === 'qemu') {
+ me.checkQemuPreconditions();
+ } else {
+ me.checkLxcPreconditions();
+ }
+ me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
+
+ // Only allow nodes where the local storage is available in case of offline migration
+ // where storage migration is not possible
+ me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes');
+
+ me.lookup('formPanel').isValid();
+
+ },
+
+ checkQemuPreconditions: function() {
+ var me = this,
+ vm = me.getViewModel(),
+ migrateStats;
+
+ if (vm.get('running')) {
+ vm.set('migration.mode', 'online');
+ }
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
+ method: 'GET',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ migrateStats = response.result.data;
+ if (migrateStats.running) {
+ vm.set('running', true);
+ }
+ // Get migration object from viewmodel to prevent
+ // to many bind callbacks
+ var migration = vm.get('migration');
+ migration.preconditions = [];
+
+ if (migrateStats.allowed_nodes) {
+ migration.allowedNodes = migrateStats.allowed_nodes;
+ var target = me.lookup('pveNodeSelector').value;
+ if (target.length && !migrateStats.allowed_nodes.includes(target)) {
+ let disallowed = migrateStats.not_allowed_nodes[target];
+ let missing_storages = disallowed.unavailable_storages.join(', ');
+
+ migration.possible = false;
+ migration.preconditions.push({
+ text: 'Storage (' + missing_storages + ') not available on selected target. ' +
+ 'Start VM to use live storage migration or select other target node',
+ severity: 'error'
+ });
+ }
+ }
+
+ if (migrateStats.local_resources.length) {
+ migration.possible = false;
+ migration.preconditions.push({
+ text: 'Can\'t migrate VM with local resources: '+ migrateStats.local_resources.join(', '),
+ severity: 'error'
+ });
+ }
+
+ if (migrateStats.local_disks.length) {
+
+ migrateStats.local_disks.forEach(function (disk) {
+ if (disk.cdrom && disk.cdrom === 1) {
+ migration.possible = false;
+ migration.preconditions.push({
+ text: "Can't migrate VM with local CD/DVD",
+ severity: 'error'
+ });
+
+ } else if (!disk.referenced_in_config) {
+ migration.possible = false;
+ migration.preconditions.push({
+ text: 'Found not referenced/unused disk via storage: '+ disk.volid,
+ severity: 'error'
+ });
+ } else {
+ migration['with-local-disks'] = 1;
+ migration.preconditions.push({
+ text:'Migration with local disk might take long: ' + disk.volid
+ +' (' + PVE.Utils.render_size(disk.size) + ')',
+ severity: 'warning'
+ });
+ }
+ });
+
+ }
+
+ vm.set('migration', migration);
+
+ }
+ });
+ },
+ checkLxcPreconditions: function() {
+ var me = this,
+ vm = me.getViewModel();
+ if (vm.get('running')) {
+ vm.set('migration.mode', 'restart');
+ }
+ }
+
+
+ },
+
+ width: 600,
+ modal: true,
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+ border: false,
+ items: [
+ {
+ xtype: 'form',
+ reference: 'formPanel',
+ bodyPadding: 10,
+ border: false,
+ layout: {
+ type: 'column'
+ },
+ items: [
+ {
+ xtype: 'container',
+ columnWidth: 0.5,
+ items: [{
+ xtype: 'displayfield',
+ name: 'source',
+ fieldLabel: gettext('Source node'),
+ bind: {
+ value: '{nodename}'
+ }
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'migrationMode',
+ fieldLabel: gettext('Mode'),
+ bind: {
+ value: '{setMigrationMode}'
+ }
+ }]
+ },
+ {
+ xtype: 'container',
+ columnWidth: 0.5,
+ items: [{
+ xtype: 'pveNodeSelector',
+ reference: 'pveNodeSelector',
+ name: 'target',
+ fieldLabel: gettext('Target node'),
+ allowBlank: false,
+ disallowedNodes: undefined,
+ onlineValidator: true,
+ listeners: {
+ change: 'onTargetChange'
+ }
+ },
+ {
+ xtype: 'pveStorageSelector',
+ reference: 'pveDiskStorageSelector',
+ name: 'targetstorage',
+ fieldLabel: gettext('Target storage'),
+ storageContent: 'images',
+ bind: {
+ hidden: '{setStorageselectorHidden}'
+ }
+ }]
+ }
+ ]
+ },
+ {
+ xtype: 'gridpanel',
+ reference: 'preconditionGrid',
+ selectable: false,
+ flex: 1,
+ columns: [{
+ text: '',
+ dataIndex: 'severity',
+ renderer: function(v) {
+ switch (v) {
+ case 'warning':
+ return ' ';
+ case 'error':
+ return '';
+ default:
+ return v;
+ }
+ },
+ width: 35
+ },
+ {
+ text: 'Info',
+ dataIndex: 'text',
+ cellWrap: true,
+ flex: 1
+ }],
+ bind: {
+ hidden: '{!migration.preconditions.length}',
+ store: {
+ fields: ['severity','text'],
+ data: '{migration.preconditions}'
+ }
+ }
+ }
+
+ ],
+ buttons: [
+ {
+ xtype: 'proxmoxHelpButton',
+ reference: 'proxmoxHelpButton',
+ onlineHelp: 'pct_migration',
+ listenToGlobalEvent: false,
+ hidden: false
+ },
+ '->',
+ {
+ xtype: 'button',
+ reference: 'submitButton',
+ text: gettext('Migrate'),
+ handler: 'startMigration',
+ bind: {
+ disabled: '{!migration.possible}'
+ }
+ }
+ ]
+});
+Ext.define('PVE.window.BulkAction', {
+ extend: 'Ext.window.Window',
+
+ resizable: true,
+ width: 800,
+ modal: true,
+ layout: {
+ type: 'fit'
+ },
+ border: false,
+
+ // the action to be set
+ // currently there are
+ // startall
+ // migrateall
+ // stopall
+ action: undefined,
+
+ submit: function(params) {
+ var me = this;
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + me.nodename + '/' + me.action,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid
+ });
+ win.show();
+ me.hide();
+ win.on('destroy', function() {
+ me.close();
+ });
+ }
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.action) {
+ throw "no action specified";
+ }
+
+ if (!me.btnText) {
+ throw "no button text specified";
+ }
+
+ if (!me.title) {
+ throw "no title specified";
+ }
+
+ var items = [];
+
+ if (me.action === 'migrateall') {
+ /*jslint confusion: true*/
+ /*value is string and number*/
+ items.push(
+ {
+ xtype: 'pveNodeSelector',
+ name: 'target',
+ disallowedNodes: [me.nodename],
+ fieldLabel: gettext('Target node'),
+ allowBlank: false,
+ onlineValidator: true
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'maxworkers',
+ minValue: 1,
+ maxValue: 100,
+ value: 1,
+ fieldLabel: gettext('Parallel jobs'),
+ allowBlank: false
+ },
+ {
+ itemId: 'lxcwarning',
+ xtype: 'displayfield',
+ userCls: 'pve-hint',
+ value: 'Warning: Running CTs will be migrated in Restart Mode.',
+ hidden: true // only visible if running container chosen
+ }
+ );
+ /*jslint confusion: false*/
+ } else if (me.action === 'startall') {
+ items.push({
+ xtype: 'hiddenfield',
+ name: 'force',
+ value: 1
+ });
+ }
+
+ items.push({
+ xtype: 'vmselector',
+ itemId: 'vms',
+ name: 'vms',
+ flex: 1,
+ height: 300,
+ selectAll: true,
+ allowBlank: false,
+ nodename: me.nodename,
+ action: me.action,
+ listeners: {
+ selectionchange: function(vmselector, records) {
+ if (me.action == 'migrateall') {
+ var showWarning = records.some(function(item) {
+ return (item.data.type == 'lxc' &&
+ item.data.status == 'running');
+ });
+ me.down('#lxcwarning').setVisible(showWarning);
+ }
+ }
+ }
+ });
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+ fieldDefaults: {
+ labelWidth: 300,
+ anchor: '100%'
+ },
+ items: items
+ });
+
+ var form = me.formPanel.getForm();
+
+ var submitBtn = Ext.create('Ext.Button', {
+ text: me.btnText,
+ handler: function() {
+ form.isValid();
+ me.submit(form.getValues());
+ }
+ });
+
+ Ext.apply(me, {
+ items: [ me.formPanel ],
+ buttons: [ submitBtn ]
+ });
+
+ me.callParent();
+
+ form.on('validitychange', function() {
+ var valid = form.isValid();
+ submitBtn.setDisabled(!valid);
+ });
+ form.isValid();
+ }
+});
+Ext.define('PVE.window.Clone', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ isTemplate: false,
+
+ onlineHelp: 'qm_copy_and_clone',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'panel[reference=cloneform]': {
+ validitychange: 'disableSubmit'
+ }
+ },
+ disableSubmit: function(form) {
+ this.lookupReference('submitBtn').setDisabled(!form.isValid());
+ }
+ },
+
+ statics: {
+ // display a snapshot selector only if needed
+ wrap: function(nodename, vmid, isTemplate, guestType) {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/' + guestType + '/' + vmid +'/snapshot',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, opts) {
+ var snapshotList = response.result.data;
+ var hasSnapshots = snapshotList.length === 1 &&
+ snapshotList[0].name === 'current' ? false : true;
+
+ Ext.create('PVE.window.Clone', {
+ nodename: nodename,
+ guestType: guestType,
+ vmid: vmid,
+ isTemplate: isTemplate,
+ hasSnapshots: hasSnapshots
+ }).show();
+ }
+ });
+ }
+ },
+
+ create_clone: function(values) {
+ var me = this;
+
+ var params = { newid: values.newvmid };
+
+ if (values.snapname && values.snapname !== 'current') {
+ params.snapname = values.snapname;
+ }
+
+ if (values.pool) {
+ params.pool = values.pool;
+ }
+
+ if (values.name) {
+ if (me.guestType === 'lxc') {
+ params.hostname = values.name;
+ } else {
+ params.name = values.name;
+ }
+ }
+
+ if (values.target) {
+ params.target = values.target;
+ }
+
+ if (values.clonemode === 'copy') {
+ params.full = 1;
+ if (values.hdstorage) {
+ params.storage = values.hdstorage;
+ if (values.diskformat && me.guestType !== 'lxc') {
+ params.format = values.diskformat;
+ }
+ }
+ }
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/clone',
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ me.close();
+ }
+ });
+
+ },
+
+ // disable the Storage selector when clone mode is linked clone
+ updateVisibility: function() {
+ var me = this;
+ var clonemode = me.lookupReference('clonemodesel').getValue();
+ var disksel = me.lookup('diskselector');
+ disksel.setDisabled(clonemode === 'clone');
+ },
+
+ // add to the list of valid nodes each node where
+ // all the VM disks are available
+ verifyFeature: function() {
+ var me = this;
+
+ var snapname = me.lookupReference('snapshotsel').getValue();
+ var clonemode = me.lookupReference('clonemodesel').getValue();
+
+ var params = { feature: clonemode };
+ if (snapname !== 'current') {
+ params.snapname = snapname;
+ }
+
+ Proxmox.Utils.API2Request({
+ waitMsgTarget: me,
+ url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/feature',
+ params: params,
+ method: 'GET',
+ failure: function(response, opts) {
+ me.lookupReference('submitBtn').setDisabled(true);
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ var res = response.result.data;
+
+ me.lookupReference('targetsel').allowedNodes = res.nodes;
+ me.lookupReference('targetsel').validate();
+ }
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.snapname) {
+ me.snapname = 'current';
+ }
+
+ if (!me.guestType) {
+ throw "no Guest Type specified";
+ }
+
+ var titletext = me.guestType === 'lxc' ? 'CT' : 'VM';
+ if (me.isTemplate) {
+ titletext += ' Template';
+ }
+ me.title = "Clone " + titletext + " " + me.vmid;
+
+ var col1 = [];
+ var col2 = [];
+
+ col1.push({
+ xtype: 'pveNodeSelector',
+ name: 'target',
+ reference: 'targetsel',
+ fieldLabel: gettext('Target node'),
+ selectCurNode: true,
+ allowBlank: false,
+ onlineValidator: true,
+ listeners: {
+ change: function(f, value) {
+ me.lookupReference('hdstorage').setTargetNode(value);
+ }
+ }
+ });
+
+ var modelist = [['copy', gettext('Full Clone')]];
+ if (me.isTemplate) {
+ modelist.push(['clone', gettext('Linked Clone')]);
+ }
+
+ col1.push({
+ xtype: 'pveGuestIDSelector',
+ name: 'newvmid',
+ guestType: me.guestType,
+ value: '',
+ loadNextFreeID: true,
+ validateExists: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'name',
+ allowBlank: true,
+ fieldLabel: me.guestType === 'lxc' ? gettext('Hostname') : gettext('Name')
+ },
+ {
+ xtype: 'pvePoolSelector',
+ fieldLabel: gettext('Resource Pool'),
+ name: 'pool',
+ value: '',
+ allowBlank: true
+ }
+ );
+
+ col2.push({
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('Mode'),
+ name: 'clonemode',
+ reference: 'clonemodesel',
+ allowBlank: false,
+ hidden: !me.isTemplate,
+ value: me.isTemplate ? 'clone' : 'copy',
+ comboItems: modelist,
+ listeners: {
+ change: function(t, value) {
+ me.updateVisibility();
+ me.verifyFeature();
+ }
+ }
+ },
+ {
+ xtype: 'PVE.form.SnapshotSelector',
+ name: 'snapname',
+ reference: 'snapshotsel',
+ fieldLabel: gettext('Snapshot'),
+ nodename: me.nodename,
+ guestType: me.guestType,
+ vmid: me.vmid,
+ hidden: me.isTemplate || !me.hasSnapshots ? true : false,
+ disabled: false,
+ allowBlank: false,
+ value : me.snapname,
+ listeners: {
+ change: function(f, value) {
+ me.verifyFeature();
+ }
+ }
+ },
+ {
+ xtype: 'pveDiskStorageSelector',
+ reference: 'diskselector',
+ nodename: me.nodename,
+ autoSelect: false,
+ hideSize: true,
+ hideSelection: true,
+ storageLabel: gettext('Target Storage'),
+ allowBlank: true,
+ storageContent: me.guestType === 'qemu' ? 'images' : 'rootdir',
+ emptyText: gettext('Same as source'),
+ disabled: me.isTemplate ? true : false // because default mode is clone for templates
+ });
+
+ var formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ reference: 'cloneform',
+ border: false,
+ layout: 'column',
+ defaultType: 'container',
+ columns: 2,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: [
+ {
+ columnWidth: 0.5,
+ padding: '0 10 0 0',
+ layout: 'anchor',
+ items: col1
+ },
+ {
+ columnWidth: 0.5,
+ padding: '0 0 0 10',
+ layout: 'anchor',
+ items: col2
+ }
+ ]
+ });
+
+ Ext.apply(me, {
+ modal: true,
+ width: 600,
+ height: 250,
+ border: false,
+ layout: 'fit',
+ buttons: [ {
+ xtype: 'proxmoxHelpButton',
+ listenToGlobalEvent: false,
+ hidden: false,
+ onlineHelp: me.onlineHelp
+ },
+ '->',
+ {
+ reference: 'submitBtn',
+ text: gettext('Clone'),
+ disabled: true,
+ handler: function() {
+ var cloneForm = me.lookupReference('cloneform');
+ if (cloneForm.isValid()) {
+ me.create_clone(cloneForm.getValues());
+ }
+ }
+ } ],
+ items: [ formPanel ]
+ });
+
+ me.callParent();
+
+ me.verifyFeature();
+ }
+});
+Ext.define('PVE.qemu.Monitor', {
+ extend: 'Ext.panel.Panel',
+
+ alias: 'widget.pveQemuMonitor',
+
+ maxLines: 500,
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var history = [];
+ var histNum = -1;
+ var lines = [];
+
+ var textbox = Ext.createWidget('panel', {
+ region: 'center',
+ xtype: 'panel',
+ autoScroll: true,
+ border: true,
+ margins: '5 5 5 5',
+ bodyStyle: 'font-family: monospace;'
+ });
+
+ var scrollToEnd = function() {
+ var el = textbox.getTargetEl();
+ var dom = Ext.getDom(el);
+
+ var clientHeight = dom.clientHeight;
+ // BrowserBug: clientHeight reports 0 in IE9 StrictMode
+ // Instead we are using offsetHeight and hardcoding borders
+ if (Ext.isIE9 && Ext.isStrict) {
+ clientHeight = dom.offsetHeight + 2;
+ }
+ dom.scrollTop = dom.scrollHeight - clientHeight;
+ };
+
+ var refresh = function() {
+ textbox.update('' + lines.join('\n') + '
');
+ scrollToEnd();
+ };
+
+ var addLine = function(line) {
+ lines.push(line);
+ if (lines.length > me.maxLines) {
+ lines.shift();
+ }
+ };
+
+ var executeCmd = function(cmd) {
+ addLine("# " + Ext.htmlEncode(cmd));
+ if (cmd) {
+ history.unshift(cmd);
+ if (history.length > 20) {
+ history.splice(20);
+ }
+ }
+ histNum = -1;
+
+ refresh();
+ Proxmox.Utils.API2Request({
+ params: { command: cmd },
+ url: '/nodes/' + nodename + '/qemu/' + vmid + "/monitor",
+ method: 'POST',
+ waitMsgTarget: me,
+ success: function(response, opts) {
+ var res = response.result.data;
+ Ext.Array.each(res.split('\n'), function(line) {
+ addLine(Ext.htmlEncode(line));
+ });
+ refresh();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ };
+
+ Ext.apply(me, {
+ layout: { type: 'border' },
+ border: false,
+ items: [
+ textbox,
+ {
+ region: 'south',
+ margins:'0 5 5 5',
+ border: false,
+ xtype: 'textfield',
+ name: 'cmd',
+ value: '',
+ fieldStyle: 'font-family: monospace;',
+ allowBlank: true,
+ listeners: {
+ afterrender: function(f) {
+ f.focus(false);
+ addLine("Type 'help' for help.");
+ refresh();
+ },
+ specialkey: function(f, e) {
+ var key = e.getKey();
+ switch (key) {
+ case e.ENTER:
+ var cmd = f.getValue();
+ f.setValue('');
+ executeCmd(cmd);
+ break;
+ case e.PAGE_UP:
+ textbox.scrollBy(0, -0.9*textbox.getHeight(), false);
+ break;
+ case e.PAGE_DOWN:
+ textbox.scrollBy(0, 0.9*textbox.getHeight(), false);
+ break;
+ case e.UP:
+ if (histNum + 1 < history.length) {
+ f.setValue(history[++histNum]);
+ }
+ e.preventDefault();
+ break;
+ case e.DOWN:
+ if (histNum > 0) {
+ f.setValue(history[--histNum]);
+ }
+ e.preventDefault();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }
+ ],
+ listeners: {
+ show: function() {
+ var field = me.query('textfield[name="cmd"]')[0];
+ field.focus(false, true);
+ }
+ }
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.qemu.Summary', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveQemuSummary',
+
+ scrollable: true,
+ bodyPadding: 5,
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.workspace) {
+ throw "no workspace specified";
+ }
+
+ if (!me.statusStore) {
+ throw "no status storage specified";
+ }
+
+ var template = !!me.pveSelNode.data.template;
+ var rstore = me.statusStore;
+
+ var width = template ? 1 : 0.5;
+ var items = [
+ {
+ xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView',
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: width
+ },
+ 'width >= 1900': {
+ columnWidth: width / 2
+ }
+ },
+ itemId: 'gueststatus',
+ pveSelNode: me.pveSelNode,
+ rstore: rstore
+ },
+ {
+ xtype: 'pveNotesView',
+ maxHeight: 330,
+ itemId: 'notesview',
+ pveSelNode: me.pveSelNode,
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: width
+ },
+ 'width >= 1900': {
+ columnWidth: width / 2
+ }
+ }
+ }
+ ];
+
+ var rrdstore;
+ if (!template) {
+
+ rrdstore = Ext.create('Proxmox.data.RRDStore', {
+ rrdurl: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/rrddata",
+ model: 'pve-rrd-guest'
+ });
+
+ items.push(
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('CPU usage'),
+ pveSelNode: me.pveSelNode,
+ fields: ['cpu'],
+ fieldTitles: [gettext('CPU usage')],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Memory usage'),
+ pveSelNode: me.pveSelNode,
+ fields: ['maxmem', 'mem'],
+ fieldTitles: [gettext('Total'), gettext('RAM usage')],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Network traffic'),
+ pveSelNode: me.pveSelNode,
+ fields: ['netin','netout'],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Disk IO'),
+ pveSelNode: me.pveSelNode,
+ fields: ['diskread','diskwrite'],
+ store: rrdstore
+ }
+ );
+
+ }
+
+ Ext.apply(me, {
+ tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ],
+ items: [
+ {
+ xtype: 'container',
+ layout: {
+ type: 'column'
+ },
+ defaults: {
+ minHeight: 330,
+ padding: 5,
+ plugins: 'responsive',
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: 1
+ },
+ 'width >= 1900': {
+ columnWidth: 0.5
+ }
+ }
+ },
+ items: items
+ }
+ ]
+ });
+
+ me.callParent();
+ if (!template) {
+ rrdstore.startUpdate();
+ me.on('destroy', rrdstore.stopUpdate);
+ }
+ }
+});
+Ext.define('PVE.qemu.OSTypeInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuOSTypePanel',
+ onlineHelp: 'qm_os_settings',
+ insideWizard: false,
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'combobox[name=osbase]': {
+ change: 'onOSBaseChange'
+ },
+ 'combobox[name=ostype]': {
+ afterrender: 'onOSTypeChange',
+ change: 'onOSTypeChange'
+ }
+ },
+ onOSBaseChange: function(field, value) {
+ this.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]);
+ },
+ onOSTypeChange: function(field) {
+ var me = this, ostype = field.getValue();
+ if (!me.getView().insideWizard) {
+ return;
+ }
+ var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype);
+
+ me.setWidget('pveBusSelector', targetValues.busType);
+ me.setWidget('pveNetworkCardSelector', targetValues.networkCard);
+ var scsihw = targetValues.scsihw || '__default__';
+ this.getViewModel().set('current.scsihw', scsihw);
+ },
+ setWidget: function(widget, newValue) {
+ // changing a widget is safe only if ComponentQuery.query returns us
+ // a single value array
+ var widgets = Ext.ComponentQuery.query('pveQemuCreateWizard ' + widget);
+ if (widgets.length === 1) {
+ widgets[0].setValue(newValue);
+ } else {
+ throw 'non unique widget :' + widget + ' in Wizard';
+ }
+ }
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ /*jslint confusion: true */
+ me.items = [
+ {
+ xtype: 'displayfield',
+ value: gettext('Guest OS') + ':',
+ hidden: !me.insideWizard
+ },
+ {
+ xtype: 'combobox',
+ submitValue: false,
+ name: 'osbase',
+ fieldLabel: gettext('Type'),
+ editable: false,
+ queryMode: 'local',
+ value: 'Linux',
+ store: Object.keys(PVE.Utils.kvm_ostypes)
+ },
+ {
+ xtype: 'combobox',
+ name: 'ostype',
+ reference: 'ostype',
+ fieldLabel: gettext('Version'),
+ value: 'l26',
+ allowBlank : false,
+ editable: false,
+ queryMode: 'local',
+ valueField: 'val',
+ displayField: 'desc',
+ store: {
+ fields: ['desc', 'val'],
+ data: PVE.Utils.kvm_ostypes.Linux,
+ listeners: {
+ datachanged: function (store) {
+ var ostype = me.lookup('ostype');
+ var old_val = ostype.getValue();
+ if (!me.insideWizard && old_val && store.find('val', old_val) != -1) {
+ ostype.setValue(old_val);
+ } else {
+ ostype.setValue(store.getAt(0));
+ }
+ }
+ }
+ }
+ }
+ ];
+ /*jslint confusion: false */
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.OSTypeEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ subject: 'OS Type',
+
+ items: [{ xtype: 'pveQemuOSTypePanel' }],
+
+ initComponent : function() {
+ var me = this;
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var value = response.result.data.ostype || 'other';
+ var osinfo = PVE.Utils.get_kvm_osinfo(value);
+ me.setValues({ ostype: value, osbase: osinfo.base });
+ }
+ });
+ }
+});
+/*
+ * This class holds performance *recommended* settings for the PVE Qemu wizards
+ * the *mandatory* settings are set in the PVE::QemuServer
+ * config_to_command sub
+ * We store this here until we get the data from the API server
+*/
+
+// this is how you would add an hypothetic FreeBSD > 10 entry
+//
+//virtio-blk is stable but virtIO net still
+// problematic as of 10.3
+// see https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=165059
+// addOS({
+// parent: 'generic', // inherits defaults
+// pveOS: 'freebsd10', // must match a radiofield in OSTypeEdit.js
+// busType: 'virtio' // must match a pveBusController value
+// // networkCard muss match a pveNetworkCardSelector
+
+
+Ext.define('PVE.qemu.OSDefaults', {
+ singleton: true, // will also force creation when loaded
+
+ constructor: function() {
+ var me = this;
+
+ var addOS = function(settings) {
+ if (me.hasOwnProperty(settings.parent)) {
+ var child = Ext.clone(me[settings.parent]);
+ me[settings.pveOS] = Ext.apply(child, settings);
+
+ } else {
+ throw("Could not find your genitor");
+ }
+ };
+
+ // default values
+ me.generic = {
+ busType: 'ide',
+ networkCard: 'e1000',
+ busPriority: {
+ ide: 4,
+ sata: 3,
+ scsi: 2,
+ virtio: 1
+ },
+ scsihw: 'virtio-scsi-pci'
+ };
+
+ // virtio-net is in kernel since 2.6.25
+ // virtio-scsi since 3.2 but backported in RHEL with 2.6 kernel
+ addOS({
+ pveOS: 'l26',
+ parent : 'generic',
+ busType: 'scsi',
+ busPriority: {
+ scsi: 4,
+ virtio: 3,
+ sata: 2,
+ ide: 1
+ },
+ networkCard: 'virtio'
+ });
+
+ // recommandation from http://wiki.qemu.org/Windows2000
+ addOS({
+ pveOS: 'w2k',
+ parent : 'generic',
+ networkCard: 'rtl8139',
+ scsihw: ''
+ });
+ // https://pve.proxmox.com/wiki/Windows_XP_Guest_Notes
+ addOS({
+ pveOS: 'wxp',
+ parent : 'w2k'
+ });
+
+ me.getDefaults = function(ostype) {
+ if (PVE.qemu.OSDefaults[ostype]) {
+ return PVE.qemu.OSDefaults[ostype];
+ } else {
+ return PVE.qemu.OSDefaults.generic;
+ }
+ };
+ }
+});
+Ext.define('PVE.qemu.ProcessorInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuProcessorPanel',
+ onlineHelp: 'qm_cpu',
+
+ insideWizard: false,
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ updateCores: function() {
+ var me = this.getView();
+ var sockets = me.down('field[name=sockets]').getValue();
+ var cores = me.down('field[name=cores]').getValue();
+ me.down('field[name=totalcores]').setValue(sockets*cores);
+ var vcpus = me.down('field[name=vcpus]');
+ vcpus.setMaxValue(sockets*cores);
+ vcpus.setEmptyText(sockets*cores);
+ vcpus.validate();
+ },
+
+ control: {
+ 'field[name=sockets]': {
+ change: 'updateCores'
+ },
+ 'field[name=cores]': {
+ change: 'updateCores'
+ }
+ }
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (Array.isArray(values['delete'])) {
+ values['delete'] = values['delete'].join(',');
+ }
+
+ PVE.Utils.delete_if_default(values, 'cpulimit', '0', 0);
+ PVE.Utils.delete_if_default(values, 'cpuunits', '1024', 0);
+
+ // build the cpu options:
+ me.cpu.cputype = values.cputype;
+
+ if (values.flags) {
+ me.cpu.flags = values.flags;
+ } else {
+ delete me.cpu.flags;
+ }
+
+ delete values.cputype;
+ delete values.flags;
+ var cpustring = PVE.Parser.printQemuCpu(me.cpu);
+
+ // remove cputype delete request:
+ var del = values['delete'];
+ delete values['delete'];
+ if (del) {
+ del = del.split(',');
+ Ext.Array.remove(del, 'cputype');
+ } else {
+ del = [];
+ }
+
+ if (cpustring) {
+ values.cpu = cpustring;
+ } else {
+ del.push('cpu');
+ }
+
+ var delarr = del.join(',');
+ if (delarr) {
+ values['delete'] = delarr;
+ }
+
+ return values;
+ },
+
+ cpu: {},
+
+ column1: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'sockets',
+ minValue: 1,
+ maxValue: 4,
+ value: '1',
+ fieldLabel: gettext('Sockets'),
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'cores',
+ minValue: 1,
+ maxValue: 128,
+ value: '1',
+ fieldLabel: gettext('Cores'),
+ allowBlank: false
+ }
+ ],
+
+ column2: [
+ {
+ xtype: 'CPUModelSelector',
+ name: 'cputype',
+ value: '__default__',
+ fieldLabel: gettext('Type')
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Total cores'),
+ name: 'totalcores',
+ value: '1'
+ }
+ ],
+
+ advancedColumn1: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'vcpus',
+ minValue: 1,
+ maxValue: 1,
+ value: '',
+ fieldLabel: gettext('VCPUs'),
+ deleteEmpty: true,
+ allowBlank: true,
+ emptyText: '1'
+ },
+ {
+ xtype: 'numberfield',
+ name: 'cpulimit',
+ minValue: 0,
+ maxValue: 128, // api maximum
+ value: '',
+ step: 1,
+ fieldLabel: gettext('CPU limit'),
+ allowBlank: true,
+ emptyText: gettext('unlimited')
+ }
+ ],
+
+ advancedColumn2: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'cpuunits',
+ fieldLabel: gettext('CPU units'),
+ minValue: 8,
+ maxValue: 500000,
+ value: '1024',
+ deleteEmpty: true,
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Enable NUMA'),
+ name: 'numa',
+ uncheckedValue: 0
+ }
+ ],
+ advancedColumnB: [
+ {
+ xtype: 'label',
+ text: 'Extra CPU Flags:'
+ },
+ {
+ xtype: 'vmcpuflagselector',
+ name: 'flags'
+ }
+ ]
+});
+
+Ext.define('PVE.qemu.ProcessorEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ width: 700,
+
+ initComponent : function() {
+ var me = this;
+
+ var ipanel = Ext.create('PVE.qemu.ProcessorInputPanel');
+
+ Ext.apply(me, {
+ subject: gettext('Processors'),
+ items: ipanel
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data;
+ var value = data.cpu;
+ if (value) {
+ var cpu = PVE.Parser.parseQemuCpu(value);
+ ipanel.cpu = cpu;
+ data.cputype = cpu.cputype;
+ if (cpu.flags) {
+ data.flags = cpu.flags;
+ }
+ }
+ me.setValues(data);
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.BootOrderPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuBootOrderPanel',
+ vmconfig: {}, // store loaded vm config
+
+ bootdisk: undefined,
+ selection: [],
+ list: [],
+ comboboxes: [],
+
+ isBootDisk: function(value) {
+ return PVE.Utils.bus_match.test(value);
+ },
+
+ setVMConfig: function(vmconfig) {
+ var me = this;
+ me.vmconfig = vmconfig;
+ var order = me.vmconfig.boot || 'cdn';
+ me.bootdisk = me.vmconfig.bootdisk || undefined;
+
+ // get the first 3 characters
+ // ignore the rest (there should never be more than 3)
+ me.selection = order.split('').slice(0,3);
+
+ // build bootdev list
+ me.list = [];
+ Ext.Object.each(me.vmconfig, function(key, value) {
+ if (me.isBootDisk(key) &&
+ !(/media=cdrom/).test(value)) {
+ me.list.push([key, "Disk '" + key + "'"]);
+ }
+ });
+
+ me.list.push(['d', 'CD-ROM']);
+ me.list.push(['n', gettext('Network')]);
+ me.list.push(['__none__', Proxmox.Utils.noneText]);
+
+ me.recomputeList();
+
+ me.comboboxes.forEach(function(box) {
+ box.resetOriginalValue();
+ });
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+ var order = me.selection.join('');
+ var res = { boot: order };
+
+ if (me.bootdisk && order.indexOf('c') !== -1) {
+ res.bootdisk = me.bootdisk;
+ } else {
+ res['delete'] = 'bootdisk';
+ }
+
+ return res;
+ },
+
+ recomputeSelection: function(combobox, newVal, oldVal) {
+ var me = this.up('#inputpanel');
+ me.selection = [];
+ me.comboboxes.forEach(function(item) {
+ var val = item.getValue();
+
+ // when selecting an already selected item,
+ // switch it around
+ if ((val === newVal || (me.isBootDisk(val) && me.isBootDisk(newVal))) &&
+ item.name !== combobox.name &&
+ newVal !== '__none__') {
+ // swap items
+ val = oldVal;
+ }
+
+ // push 'c','d' or 'n' in the array
+ if (me.isBootDisk(val)) {
+ me.selection.push('c');
+ me.bootdisk = val;
+ } else if (val === 'd' ||
+ val === 'n') {
+ me.selection.push(val);
+ }
+ });
+
+ me.recomputeList();
+ },
+
+ recomputeList: function(){
+ var me = this;
+ // set the correct values in the kvcomboboxes
+ var cnt = 0;
+ me.comboboxes.forEach(function(item) {
+ if (cnt === 0) {
+ // never show 'none' on first combobox
+ item.store.loadData(me.list.slice(0, me.list.length-1));
+ } else {
+ item.store.loadData(me.list);
+ }
+ item.suspendEvent('change');
+ if (cnt < me.selection.length) {
+ item.setValue((me.selection[cnt] !== 'c')?me.selection[cnt]:me.bootdisk);
+ } else if (cnt === 0){
+ item.setValue('');
+ } else {
+ item.setValue('__none__');
+ }
+ cnt++;
+ item.resumeEvent('change');
+ item.validate();
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ // this has to be done here, because of
+ // the way our inputPanel class handles items
+ me.comboboxes = [
+ Ext.createWidget('proxmoxKVComboBox', {
+ fieldLabel: gettext('Boot device') + " 1",
+ labelWidth: 120,
+ name: 'bd1',
+ allowBlank: false,
+ listeners: {
+ change: me.recomputeSelection
+ }
+ }),
+ Ext.createWidget('proxmoxKVComboBox', {
+ fieldLabel: gettext('Boot device') + " 2",
+ labelWidth: 120,
+ name: 'bd2',
+ allowBlank: false,
+ listeners: {
+ change: me.recomputeSelection
+ }
+ }),
+ Ext.createWidget('proxmoxKVComboBox', {
+ fieldLabel: gettext('Boot device') + " 3",
+ labelWidth: 120,
+ name: 'bd3',
+ allowBlank: false,
+ listeners: {
+ change: me.recomputeSelection
+ }
+ })
+ ];
+ Ext.apply(me, { items: me.comboboxes });
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.BootOrderEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ items: [{
+ xtype: 'pveQemuBootOrderPanel',
+ itemId: 'inputpanel'
+ }],
+
+ subject: gettext('Boot Order'),
+
+ initComponent : function() {
+ var me = this;
+ me.callParent();
+ me.load({
+ success: function(response, options) {
+ me.down('#inputpanel').setVMConfig(response.result.data);
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.MemoryInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuMemoryPanel',
+ onlineHelp: 'qm_memory',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var res = {};
+
+ res.memory = values.memory;
+ res.balloon = values.balloon;
+
+ if (!values.ballooning) {
+ res.balloon = 0;
+ res['delete'] = 'shares';
+ } else if (values.memory === values.balloon) {
+ delete res.balloon;
+ res['delete'] = 'balloon,shares';
+ } else if (Ext.isDefined(values.shares) && (values.shares !== "")) {
+ res.shares = values.shares;
+ } else {
+ res['delete'] = "shares";
+ }
+
+ return res;
+ },
+
+ initComponent: function() {
+ var me = this;
+ var labelWidth = 160;
+
+ me.items= [
+ {
+ xtype: 'pveMemoryField',
+ labelWidth: labelWidth,
+ fieldLabel: gettext('Memory') + ' (MiB)',
+ name: 'memory',
+ minValue: 1,
+ step: 32,
+ hotplug: me.hotplug,
+ listeners: {
+ change: function(f, value, old) {
+ var bf = me.down('field[name=balloon]');
+ var balloon = bf.getValue();
+ bf.setMaxValue(value);
+ if (balloon === old) {
+ bf.setValue(value);
+ }
+ bf.validate();
+ }
+ }
+ }
+ ];
+
+ me.advancedItems= [
+ {
+ xtype: 'pveMemoryField',
+ name: 'balloon',
+ minValue: 1,
+ step: 32,
+ fieldLabel: gettext('Minimum memory') + ' (MiB)',
+ hotplug: me.hotplug,
+ labelWidth: labelWidth,
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ var memory = me.down('field[name=memory]').getValue();
+ var shares = me.down('field[name=shares]');
+ shares.setDisabled(value === memory);
+ }
+ }
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'shares',
+ disabled: true,
+ minValue: 0,
+ maxValue: 50000,
+ value: '',
+ step: 10,
+ fieldLabel: gettext('Shares'),
+ labelWidth: labelWidth,
+ allowBlank: true,
+ emptyText: Proxmox.Utils.defaultText + ' (1000)',
+ submitEmptyText: false
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ labelWidth: labelWidth,
+ value: '1',
+ name: 'ballooning',
+ fieldLabel: gettext('Ballooning Device'),
+ listeners: {
+ change: function(f, value) {
+ var bf = me.down('field[name=balloon]');
+ var shares = me.down('field[name=shares]');
+ var memory = me.down('field[name=memory]');
+ bf.setDisabled(!value);
+ shares.setDisabled(!value || (bf.getValue() === memory.getValue()));
+ }
+ }
+ }
+ ];
+
+ if (me.insideWizard) {
+ me.column1 = me.items;
+ me.items = undefined;
+ me.advancedColumn1 = me.advancedItems;
+ me.advancedItems = undefined;
+ }
+ me.callParent();
+ }
+
+});
+
+Ext.define('PVE.qemu.MemoryEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent: function() {
+ var me = this;
+
+ var memoryhotplug;
+ if(me.hotplug) {
+ Ext.each(me.hotplug.split(','), function(el) {
+ if (el === 'memory') {
+ memoryhotplug = 1;
+ }
+ });
+ }
+
+ var ipanel = Ext.create('PVE.qemu.MemoryInputPanel', {
+ hotplug: memoryhotplug
+ });
+
+ Ext.apply(me, {
+ subject: gettext('Memory'),
+ items: [ ipanel ],
+ // uncomment the following to use the async configiguration API
+ // backgroundDelay: 5,
+ width: 400
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data;
+
+ var values = {
+ ballooning: data.balloon === 0 ? '0' : '1',
+ shares: data.shares,
+ memory: data.memory || '512',
+ balloon: data.balloon > 0 ? data.balloon : (data.memory || '512')
+ };
+
+ ipanel.setValues(values);
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.NetworkInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuNetworkInputPanel',
+ onlineHelp: 'qm_network_device',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ me.network.model = values.model;
+ if (values.nonetwork) {
+ return {};
+ } else {
+ me.network.bridge = values.bridge;
+ me.network.tag = values.tag;
+ me.network.firewall = values.firewall;
+ }
+ me.network.macaddr = values.macaddr;
+ me.network.disconnect = values.disconnect;
+ me.network.queues = values.queues;
+
+ if (values.rate) {
+ me.network.rate = values.rate;
+ } else {
+ delete me.network.rate;
+ }
+
+ var params = {};
+
+ params[me.confid] = PVE.Parser.printQemuNetwork(me.network);
+
+ return params;
+ },
+
+ setNetwork: function(confid, data) {
+ var me = this;
+
+ me.confid = confid;
+
+ if (data) {
+ data.networkmode = data.bridge ? 'bridge' : 'nat';
+ } else {
+ data = {};
+ data.networkmode = 'bridge';
+ }
+ me.network = data;
+
+ me.setValues(me.network);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ me.bridgesel.setNodename(nodename);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.network = {};
+ me.confid = 'net0';
+
+ me.column1 = [];
+ me.column2 = [];
+
+ me.bridgesel = Ext.create('PVE.form.BridgeSelector', {
+ name: 'bridge',
+ fieldLabel: gettext('Bridge'),
+ nodename: me.nodename,
+ autoSelect: true,
+ allowBlank: false
+ });
+
+ me.column1 = [
+ me.bridgesel,
+ {
+ xtype: 'pveVlanField',
+ name: 'tag',
+ value: ''
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Firewall'),
+ name: 'firewall',
+ checked: (me.insideWizard || me.isCreate)
+ }
+ ];
+
+ me.advancedColumn1 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Disconnect'),
+ name: 'disconnect'
+ }
+ ];
+
+ if (me.insideWizard) {
+ me.column1.unshift({
+ xtype: 'checkbox',
+ name: 'nonetwork',
+ inputValue: 'none',
+ boxLabel: gettext('No network device'),
+ listeners: {
+ change: function(cb, value) {
+ var fields = [
+ 'disconnect',
+ 'bridge',
+ 'tag',
+ 'firewall',
+ 'model',
+ 'macaddr',
+ 'rate',
+ 'queues'
+ ];
+ fields.forEach(function(fieldname) {
+ me.down('field[name='+fieldname+']').setDisabled(value);
+ });
+ me.down('field[name=bridge]').validate();
+ }
+ }
+ });
+ me.column2.unshift({
+ xtype: 'displayfield'
+ });
+ }
+
+ me.column2.push(
+ {
+ xtype: 'pveNetworkCardSelector',
+ name: 'model',
+ fieldLabel: gettext('Model'),
+ value: PVE.qemu.OSDefaults.generic.networkCard,
+ allowBlank: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'macaddr',
+ fieldLabel: gettext('MAC address'),
+ vtype: 'MacAddress',
+ allowBlank: true,
+ emptyText: 'auto'
+ });
+ me.advancedColumn2 = [
+ {
+ xtype: 'numberfield',
+ name: 'rate',
+ fieldLabel: gettext('Rate limit') + ' (MB/s)',
+ minValue: 0,
+ maxValue: 10*1024,
+ value: '',
+ emptyText: 'unlimited',
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'queues',
+ fieldLabel: 'Multiqueue',
+ minValue: 1,
+ maxValue: 8,
+ value: '',
+ allowBlank: true
+ }
+ ];
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.NetworkEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ initComponent : function() {
+ /*jslint confusion: true */
+
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = me.confid ? false : true;
+
+ var ipanel = Ext.create('PVE.qemu.NetworkInputPanel', {
+ confid: me.confid,
+ nodename: nodename,
+ isCreate: me.isCreate
+ });
+
+ Ext.applyIf(me, {
+ subject: gettext('Network Device'),
+ items: ipanel
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var i, confid;
+ me.vmconfig = response.result.data;
+ if (!me.isCreate) {
+ var value = me.vmconfig[me.confid];
+ var network = PVE.Parser.parseQemuNetwork(me.confid, value);
+ if (!network) {
+ Ext.Msg.alert(gettext('Error'), 'Unable to parse network options');
+ me.close();
+ return;
+ }
+ ipanel.setNetwork(me.confid, network);
+ } else {
+ for (i = 0; i < 100; i++) {
+ confid = 'net' + i.toString();
+ if (!Ext.isDefined(me.vmconfig[confid])) {
+ me.confid = confid;
+ break;
+ }
+ }
+ ipanel.setNetwork(me.confid);
+ }
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.Smbios1InputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.PVE.qemu.Smbios1InputPanel',
+
+ insideWizard: false,
+
+ smbios1: {},
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var params = {
+ smbios1: PVE.Parser.printQemuSmbios1(values)
+ };
+
+ return params;
+ },
+
+ setSmbios1: function(data) {
+ var me = this;
+
+ me.smbios1 = data;
+
+ me.setValues(me.smbios1);
+ },
+
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: 'UUID',
+ regex: /^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$/,
+ name: 'uuid'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Manufacturer'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'manufacturer'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Product'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'product'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Version'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'version'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Serial'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'serial'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: 'SKU',
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'sku'
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Family'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em'
+ },
+ name: 'family'
+ }
+ ]
+});
+
+Ext.define('PVE.qemu.Smbios1Edit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent : function() {
+ /*jslint confusion: true */
+
+ var me = this;
+
+ var ipanel = Ext.create('PVE.qemu.Smbios1InputPanel', {});
+
+ Ext.applyIf(me, {
+ subject: gettext('SMBIOS settings (type1)'),
+ width: 450,
+ items: ipanel
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var i, confid;
+ me.vmconfig = response.result.data;
+ var value = me.vmconfig.smbios1;
+ if (value) {
+ var data = PVE.Parser.parseQemuSmbios1(value);
+ if (!data) {
+ Ext.Msg.alert(gettext('Error'), 'Unable to parse smbios options');
+ me.close();
+ return;
+ }
+ ipanel.setSmbios1(data);
+ }
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.CDInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuCDInputPanel',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var confid = me.confid || (values.controller + values.deviceid);
+
+ me.drive.media = 'cdrom';
+ if (values.mediaType === 'iso') {
+ me.drive.file = values.cdimage;
+ } else if (values.mediaType === 'cdrom') {
+ me.drive.file = 'cdrom';
+ } else {
+ me.drive.file = 'none';
+ }
+
+ var params = {};
+
+ params[confid] = PVE.Parser.printQemuDrive(me.drive);
+
+ return params;
+ },
+
+ setVMConfig: function(vmconfig) {
+ var me = this;
+
+ if (me.bussel) {
+ me.bussel.setVMConfig(vmconfig, 'cdrom');
+ }
+ },
+
+ setDrive: function(drive) {
+ var me = this;
+
+ var values = {};
+ if (drive.file === 'cdrom') {
+ values.mediaType = 'cdrom';
+ } else if (drive.file === 'none') {
+ values.mediaType = 'none';
+ } else {
+ values.mediaType = 'iso';
+ var match = drive.file.match(/^([^:]+):/);
+ if (match) {
+ values.cdstorage = match[1];
+ values.cdimage = drive.file;
+ }
+ }
+
+ me.drive = drive;
+
+ me.setValues(values);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ me.cdstoragesel.setNodename(nodename);
+ me.cdfilesel.setStorage(undefined, nodename);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.drive = {};
+
+ var items = [];
+
+ if (!me.confid) {
+ me.bussel = Ext.create('PVE.form.ControllerSelector', {
+ noVirtIO: true
+ });
+ items.push(me.bussel);
+ }
+
+ items.push({
+ xtype: 'radiofield',
+ name: 'mediaType',
+ inputValue: 'iso',
+ boxLabel: gettext('Use CD/DVD disc image file (iso)'),
+ checked: true,
+ listeners: {
+ change: function(f, value) {
+ if (!me.rendered) {
+ return;
+ }
+ me.down('field[name=cdstorage]').setDisabled(!value);
+ me.down('field[name=cdimage]').setDisabled(!value);
+ me.down('field[name=cdimage]').validate();
+ }
+ }
+ });
+
+ me.cdfilesel = Ext.create('PVE.form.FileSelector', {
+ name: 'cdimage',
+ nodename: me.nodename,
+ storageContent: 'iso',
+ fieldLabel: gettext('ISO image'),
+ labelAlign: 'right',
+ allowBlank: false
+ });
+
+ me.cdstoragesel = Ext.create('PVE.form.StorageSelector', {
+ name: 'cdstorage',
+ nodename: me.nodename,
+ fieldLabel: gettext('Storage'),
+ labelAlign: 'right',
+ storageContent: 'iso',
+ allowBlank: false,
+ autoSelect: me.insideWizard,
+ listeners: {
+ change: function(f, value) {
+ me.cdfilesel.setStorage(value);
+ }
+ }
+ });
+
+ items.push(me.cdstoragesel);
+ items.push(me.cdfilesel);
+
+ items.push({
+ xtype: 'radiofield',
+ name: 'mediaType',
+ inputValue: 'cdrom',
+ boxLabel: gettext('Use physical CD/DVD Drive')
+ });
+
+ items.push({
+ xtype: 'radiofield',
+ name: 'mediaType',
+ inputValue: 'none',
+ boxLabel: gettext('Do not use any media')
+ });
+
+ me.items = items;
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.CDEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ width: 400,
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = me.confid ? false : true;
+
+ var ipanel = Ext.create('PVE.qemu.CDInputPanel', {
+ confid: me.confid,
+ nodename: nodename
+ });
+
+ Ext.applyIf(me, {
+ subject: 'CD/DVD Drive',
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ ipanel.setVMConfig(response.result.data);
+ if (me.confid) {
+ var value = response.result.data[me.confid];
+ var drive = PVE.Parser.parseQemuDrive(me.confid, value);
+ if (!drive) {
+ Ext.Msg.alert('Error', 'Unable to parse drive options');
+ me.close();
+ return;
+ }
+ ipanel.setDrive(drive);
+ }
+ }
+ });
+ }
+});
+/*jslint confusion: true */
+/* 'change' property is assigned a string and then a function */
+Ext.define('PVE.qemu.HDInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuHDInputPanel',
+ onlineHelp: 'qm_hard_disk',
+
+ insideWizard: false,
+
+ unused: false, // ADD usused disk imaged
+
+ vmconfig: {}, // used to select usused disks
+
+ controller: {
+
+ xclass: 'Ext.app.ViewController',
+
+ onControllerChange: function(field) {
+ var value = field.getValue();
+
+ var allowIOthread = value.match(/^(virtio|scsi)/);
+ this.lookup('iothread').setDisabled(!allowIOthread);
+ if (!allowIOthread) {
+ this.lookup('iothread').setValue(false);
+ }
+
+ var virtio = value.match(/^virtio/);
+ this.lookup('discard').setDisabled(virtio);
+ this.lookup('ssd').setDisabled(virtio);
+ if (virtio) {
+ this.lookup('discard').setValue(false);
+ this.lookup('ssd').setValue(false);
+ }
+
+ this.lookup('scsiController').setVisible(value.match(/^scsi/));
+ },
+
+ control: {
+ 'field[name=controller]': {
+ change: 'onControllerChange',
+ afterrender: 'onControllerChange'
+ },
+ 'field[name=iothread]' : {
+ change: function(f, value) {
+ if (!this.getView().insideWizard) {
+ return;
+ }
+ var vmScsiType = value ? 'virtio-scsi-single': 'virtio-scsi-pci';
+ this.lookupReference('scsiController').setValue(vmScsiType);
+ }
+ }
+ }
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var params = {};
+ var confid = me.confid || (values.controller + values.deviceid);
+
+ if (me.unused) {
+ me.drive.file = me.vmconfig[values.unusedId];
+ confid = values.controller + values.deviceid;
+ } else if (me.isCreate) {
+ if (values.hdimage) {
+ me.drive.file = values.hdimage;
+ } else {
+ me.drive.file = values.hdstorage + ":" + values.disksize;
+ }
+ me.drive.format = values.diskformat;
+ }
+
+ if (values.nobackup) {
+ me.drive.backup = 'no';
+ } else {
+ delete me.drive.backup;
+ }
+
+ if (values.noreplicate) {
+ me.drive.replicate = 'no';
+ } else {
+ delete me.drive.replicate;
+ }
+
+ if (values.discard) {
+ me.drive.discard = 'on';
+ } else {
+ delete me.drive.discard;
+ }
+
+ if (values.ssd) {
+ me.drive.ssd = 'on';
+ } else {
+ delete me.drive.ssd;
+ }
+
+ if (values.iothread) {
+ me.drive.iothread = 'on';
+ } else {
+ delete me.drive.iothread;
+ }
+
+ if (values.cache) {
+ me.drive.cache = values.cache;
+ } else {
+ delete me.drive.cache;
+ }
+
+ var names = ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'];
+ Ext.Array.each(names, function(name) {
+ if (values[name]) {
+ me.drive[name] = values[name];
+ } else {
+ delete me.drive[name];
+ }
+ var burst_name = name + '_max';
+ if (values[burst_name] && values[name]) {
+ me.drive[burst_name] = values[burst_name];
+ } else {
+ delete me.drive[burst_name];
+ }
+ });
+
+
+ params[confid] = PVE.Parser.printQemuDrive(me.drive);
+
+ return params;
+ },
+
+ setVMConfig: function(vmconfig) {
+ var me = this;
+
+ me.vmconfig = vmconfig;
+
+ if (me.bussel) {
+ me.bussel.setVMConfig(vmconfig);
+ me.scsiController.setValue(vmconfig.scsihw);
+ }
+ if (me.unusedDisks) {
+ var disklist = [];
+ Ext.Object.each(vmconfig, function(key, value) {
+ if (key.match(/^unused\d+$/)) {
+ disklist.push([key, value]);
+ }
+ });
+ me.unusedDisks.store.loadData(disklist);
+ me.unusedDisks.setValue(me.confid);
+ }
+ },
+
+ setDrive: function(drive) {
+ var me = this;
+
+ me.drive = drive;
+
+ var values = {};
+ var match = drive.file.match(/^([^:]+):/);
+ if (match) {
+ values.hdstorage = match[1];
+ }
+
+ values.hdimage = drive.file;
+ values.nobackup = !PVE.Parser.parseBoolean(drive.backup, 1);
+ values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1);
+ values.diskformat = drive.format || 'raw';
+ values.cache = drive.cache || '__default__';
+ values.discard = (drive.discard === 'on');
+ values.ssd = PVE.Parser.parseBoolean(drive.ssd);
+ values.iothread = PVE.Parser.parseBoolean(drive.iothread);
+
+ values.mbps_rd = drive.mbps_rd;
+ values.mbps_wr = drive.mbps_wr;
+ values.iops_rd = drive.iops_rd;
+ values.iops_wr = drive.iops_wr;
+ values.mbps_rd_max = drive.mbps_rd_max;
+ values.mbps_wr_max = drive.mbps_wr_max;
+ values.iops_rd_max = drive.iops_rd_max;
+ values.iops_wr_max = drive.iops_wr_max;
+
+ me.setValues(values);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ me.down('#hdstorage').setNodename(nodename);
+ me.down('#hdimage').setStorage(undefined, nodename);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ var labelWidth = 140;
+
+ me.drive = {};
+
+ me.column1 = [];
+ me.column2 = [];
+
+ me.advancedColumn1 = [];
+ me.advancedColumn2 = [];
+
+ if (!me.confid || me.unused) {
+ me.bussel = Ext.create('PVE.form.ControllerSelector', {
+ vmconfig: me.insideWizard ? {ide2: 'cdrom'} : {}
+ });
+ me.column1.push(me.bussel);
+
+ me.scsiController = Ext.create('Ext.form.field.Display', {
+ fieldLabel: gettext('SCSI Controller'),
+ reference: 'scsiController',
+ bind: me.insideWizard ? {
+ value: '{current.scsihw}'
+ } : undefined,
+ renderer: PVE.Utils.render_scsihw,
+ submitValue: false,
+ hidden: true
+ });
+ me.column1.push(me.scsiController);
+ }
+
+ if (me.unused) {
+ me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', {
+ name: 'unusedId',
+ fieldLabel: gettext('Disk image'),
+ matchFieldWidth: false,
+ listConfig: {
+ width: 350
+ },
+ data: [],
+ allowBlank: false
+ });
+ me.column1.push(me.unusedDisks);
+ } else if (me.isCreate) {
+ me.column1.push({
+ xtype: 'pveDiskStorageSelector',
+ storageContent: 'images',
+ name: 'disk',
+ nodename: me.nodename,
+ autoSelect: me.insideWizard
+ });
+ } else {
+ me.column1.push({
+ xtype: 'textfield',
+ disabled: true,
+ submitValue: false,
+ fieldLabel: gettext('Disk image'),
+ name: 'hdimage'
+ });
+ }
+
+ me.column2.push(
+ {
+ xtype: 'CacheTypeSelector',
+ name: 'cache',
+ value: '__default__',
+ fieldLabel: gettext('Cache')
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Discard'),
+ disabled: me.confid && me.confid.match(/^virtio/),
+ reference: 'discard',
+ name: 'discard'
+ }
+ );
+
+ me.advancedColumn1.push(
+ {
+ xtype: 'proxmoxcheckbox',
+ disabled: me.confid && me.confid.match(/^virtio/),
+ fieldLabel: gettext('SSD emulation'),
+ labelWidth: labelWidth,
+ name: 'ssd',
+ reference: 'ssd'
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ disabled: me.confid && !me.confid.match(/^(virtio|scsi)/),
+ fieldLabel: 'IO thread',
+ labelWidth: labelWidth,
+ reference: 'iothread',
+ name: 'iothread'
+ },
+ {
+ xtype: 'numberfield',
+ name: 'mbps_rd',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Read limit') + ' (MB/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited')
+ },
+ {
+ xtype: 'numberfield',
+ name: 'mbps_wr',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Write limit') + ' (MB/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_rd',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Read limit') + ' (ops/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_wr',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Write limit') + ' (ops/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited')
+ }
+ );
+
+ me.advancedColumn2.push(
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('No backup'),
+ labelWidth: labelWidth,
+ name: 'nobackup'
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Skip replication'),
+ labelWidth: labelWidth,
+ name: 'noreplicate'
+ },
+ {
+ xtype: 'numberfield',
+ name: 'mbps_rd_max',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Read max burst') + ' (MB)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default')
+ },
+ {
+ xtype: 'numberfield',
+ name: 'mbps_wr_max',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Write max burst') + ' (MB)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_rd_max',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Read max burst') + ' (ops)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_wr_max',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Write max burst') + ' (ops)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default')
+ }
+ );
+
+ me.callParent();
+ }
+});
+/*jslint confusion: false */
+
+Ext.define('PVE.qemu.HDEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ backgroundDelay: 5,
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var unused = me.confid && me.confid.match(/^unused\d+$/);
+
+ me.isCreate = me.confid ? unused : true;
+
+ var ipanel = Ext.create('PVE.qemu.HDInputPanel', {
+ confid: me.confid,
+ nodename: nodename,
+ unused: unused,
+ isCreate: me.isCreate
+ });
+
+ var subject;
+ if (unused) {
+ me.subject = gettext('Unused Disk');
+ } else if (me.isCreate) {
+ me.subject = gettext('Hard Disk');
+ } else {
+ me.subject = gettext('Hard Disk') + ' (' + me.confid + ')';
+ }
+
+ me.items = [ ipanel ];
+
+ me.callParent();
+ /*jslint confusion: true*/
+ /* 'data' is assigned an empty array in same file, and here we
+ * use it like an object
+ */
+ me.load({
+ success: function(response, options) {
+ ipanel.setVMConfig(response.result.data);
+ if (me.confid) {
+ var value = response.result.data[me.confid];
+ var drive = PVE.Parser.parseQemuDrive(me.confid, value);
+ if (!drive) {
+ Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options');
+ me.close();
+ return;
+ }
+ ipanel.setDrive(drive);
+ me.isValid(); // trigger validation
+ }
+ }
+ });
+ /*jslint confusion: false*/
+ }
+});
+Ext.define('PVE.window.HDResize', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ resize_disk: function(disk, size) {
+ var me = this;
+ var params = { disk: disk, size: '+' + size + 'G' };
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/resize',
+ waitMsgTarget: me,
+ method: 'PUT',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ me.close();
+ }
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ var items = [
+ {
+ xtype: 'displayfield',
+ name: 'disk',
+ value: me.disk,
+ fieldLabel: gettext('Disk'),
+ vtype: 'StorageId',
+ allowBlank: false
+ }
+ ];
+
+ me.hdsizesel = Ext.createWidget('numberfield', {
+ name: 'size',
+ minValue: 0,
+ maxValue: 128*1024,
+ decimalPrecision: 3,
+ value: '0',
+ fieldLabel: gettext('Size Increment') + ' (GiB)',
+ allowBlank: false
+ });
+
+ items.push(me.hdsizesel);
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 140,
+ anchor: '100%'
+ },
+ items: items
+ });
+
+ var form = me.formPanel.getForm();
+
+ var submitBtn;
+
+ me.title = gettext('Resize disk');
+ submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Resize disk'),
+ handler: function() {
+ if (form.isValid()) {
+ var values = form.getValues();
+ me.resize_disk(me.disk, values.size);
+ }
+ }
+ });
+
+ Ext.apply(me, {
+ modal: true,
+ width: 250,
+ height: 150,
+ border: false,
+ layout: 'fit',
+ buttons: [ submitBtn ],
+ items: [ me.formPanel ]
+ });
+
+
+ me.callParent();
+
+ if (!me.disk) {
+ return;
+ }
+
+ }
+});
+Ext.define('PVE.window.HDMove', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+
+ move_disk: function(disk, storage, format, delete_disk) {
+ var me = this;
+ var qemu = (me.type === 'qemu');
+ var params = {};
+ params.storage = storage;
+ params[qemu ? 'disk':'volume'] = disk;
+
+ if (format && qemu) {
+ params.format = format;
+ }
+
+ if (delete_disk) {
+ params['delete'] = 1;
+ }
+
+ var url = '/nodes/' + me.nodename + '/' + me.type + '/' + me.vmid + '/';
+ url += qemu ? 'move_disk' : 'move_volume';
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: url,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid
+ });
+ win.show();
+ win.on('destroy', function() { me.close(); });
+ }
+ });
+
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ var diskarray = [];
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.type) {
+ me.type = 'qemu';
+ }
+
+ var qemu = (me.type === 'qemu');
+
+ var items = [
+ {
+ xtype: 'displayfield',
+ name: qemu ? 'disk' : 'volume',
+ value: me.disk,
+ fieldLabel: qemu ? gettext('Disk') : gettext('Mount Point'),
+ vtype: 'StorageId',
+ allowBlank: false
+ }
+ ];
+
+ items.push({
+ xtype: 'pveDiskStorageSelector',
+ storageLabel: gettext('Target Storage'),
+ nodename: me.nodename,
+ storageContent: qemu ? 'images' : 'rootdir',
+ hideSize: true
+ });
+
+ items.push({
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Delete source'),
+ name: 'deleteDisk',
+ uncheckedValue: 0,
+ checked: false
+ });
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: items
+ });
+
+ var form = me.formPanel.getForm();
+
+ var submitBtn;
+
+ me.title = qemu ? gettext("Move disk") : gettext('Move Volume');
+ submitBtn = Ext.create('Ext.Button', {
+ text: me.title,
+ handler: function() {
+ if (form.isValid()) {
+ var values = form.getValues();
+ me.move_disk(me.disk, values.hdstorage, values.diskformat,
+ values.deleteDisk);
+ }
+ }
+ });
+
+ Ext.apply(me, {
+ modal: true,
+ width: 350,
+ border: false,
+ layout: 'fit',
+ buttons: [ submitBtn ],
+ items: [ me.formPanel ]
+ });
+
+
+ me.callParent();
+
+ me.mon(me.formPanel, 'validitychange', function(fp, isValid) {
+ submitBtn.setDisabled(!isValid);
+ });
+
+ me.formPanel.isValid();
+ }
+});
+Ext.define('PVE.qemu.EFIDiskInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveEFIDiskInputPanel',
+
+ insideWizard: false,
+
+ unused: false, // ADD usused disk imaged
+
+ vmconfig: {}, // used to select usused disks
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var confid = 'efidisk0';
+
+ if (values.hdimage) {
+ me.drive.file = values.hdimage;
+ } else {
+ // we use 1 here, because for efi the size gets overridden from the backend
+ me.drive.file = values.hdstorage + ":1";
+ }
+
+ me.drive.format = values.diskformat;
+ var params = {};
+ params[confid] = PVE.Parser.printQemuDrive(me.drive);
+ return params;
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ me.down('#hdstorage').setNodename(nodename);
+ me.down('#hdimage').setStorage(undefined, nodename);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.drive = {};
+
+ me.items= [];
+
+ me.items.push({
+ xtype: 'pveDiskStorageSelector',
+ name: 'efidisk0',
+ storageContent: 'images',
+ nodename: me.nodename,
+ hideSize: true
+ });
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.EFIDiskEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+ subject: gettext('EFI Disk'),
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.items = [{
+ xtype: 'pveEFIDiskInputPanel',
+ onlineHelp: 'qm_bios_and_uefi',
+ confid: me.confid,
+ nodename: nodename,
+ isCreate: true
+ }];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.qemu.DisplayInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveDisplayInputPanel',
+
+ onGetValues: function(values) {
+ var ret = PVE.Parser.printPropertyString(values, 'type');
+ if (ret === '') {
+ return {
+ 'delete': 'vga'
+ };
+ }
+ return {
+ vga: ret
+ };
+ },
+
+ items: [{
+ name: 'type',
+ xtype: 'proxmoxKVComboBox',
+ value: '__default__',
+ deleteEmpty: false,
+ fieldLabel: gettext('Graphic card'),
+ comboItems: PVE.Utils.kvm_vga_driver_array(),
+ validator: function() {
+ var v = this.getValue();
+ var cfg = this.up('proxmoxWindowEdit').vmconfig || {};
+
+ if (v.match(/^serial\d+$/) && (!cfg[v] || cfg[v] !== 'socket')) {
+ var fmt = gettext("Serial interface '{0}' is not correctly configured.");
+ return Ext.String.format(fmt, v);
+ }
+ return true;
+ },
+ listeners: {
+ change: function(cb, val) {
+ var me = this.up('panel');
+ if (!val) {
+ return;
+ }
+ var disable = false;
+ var emptyText = Proxmox.Utils.defaultText;
+ switch (val) {
+ case "cirrus":
+ emptyText = "4";
+ break;
+ case "std":
+ emptyText = "16";
+ break;
+ case "qxl":
+ case "qxl2":
+ case "qxl3":
+ case "qxl4":
+ emptyText = "16";
+ break;
+ case "vmware":
+ emptyText = "16";
+ break;
+ case "none":
+ case "serial0":
+ case "serial1":
+ case "serial2":
+ case "serial3":
+ emptyText = 'N/A';
+ disable = true;
+ break;
+ case "virtio":
+ emptyText = "256";
+ break;
+ default:
+ break;
+ }
+ var memoryfield = me.down('field[name=memory]');
+ memoryfield.setEmptyText(emptyText);
+ memoryfield.setDisabled(disable);
+ }
+ }
+ },{
+ xtype: 'proxmoxintegerfield',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Memory') + ' (MiB)',
+ minValue: 4,
+ maxValue: 512,
+ step: 4,
+ name: 'memory'
+ }]
+});
+
+Ext.define('PVE.qemu.DisplayEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ vmconfig: undefined,
+
+ subject: gettext('Display'),
+ width: 350,
+
+ items: [{
+ xtype: 'pveDisplayInputPanel'
+ }],
+
+ initComponent : function() {
+ var me = this;
+
+ me.callParent();
+
+ me.load({
+ success: function(response) {
+ me.vmconfig = response.result.data;
+ var vga = me.vmconfig.vga || '__default__';
+ me.setValues(PVE.Parser.parsePropertyString(vga, 'type'));
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.KeyboardEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent : function() {
+ var me = this;
+
+ Ext.applyIf(me, {
+ subject: gettext('Keyboard Layout'),
+ items: {
+ xtype: 'VNCKeyboardSelector',
+ name: 'keyboard',
+ value: '__default__',
+ fieldLabel: gettext('Keyboard Layout')
+ }
+ });
+
+ me.callParent();
+
+ me.load();
+ }
+});
+Ext.define('PVE.qemu.HardwareView', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ alias: ['widget.PVE.qemu.HardwareView'],
+
+ onlineHelp: 'qm_virtual_machines_settings',
+
+ renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
+ var me = this;
+ var rows = me.rows;
+ var rowdef = rows[key] || {};
+ var iconCls = rowdef.iconCls;
+ var icon = '';
+ var txt = (rowdef.header || key);
+
+ metaData.tdAttr = "valign=middle";
+
+ if (rowdef.tdCls) {
+ metaData.tdCls = rowdef.tdCls;
+ if (rowdef.tdCls == 'pve-itype-icon-storage') {
+ var value = me.getObjectValue(key, '', false);
+ if (value === '') {
+ value = me.getObjectValue(key, '', true);
+ }
+ if (value.match(/vm-.*-cloudinit/)) {
+ metaData.tdCls = 'pve-itype-icon-cloud';
+ return rowdef.cloudheader;
+ } else if (value.match(/media=cdrom/)) {
+ metaData.tdCls = 'pve-itype-icon-cdrom';
+ return rowdef.cdheader;
+ }
+ }
+ } else if (iconCls) {
+ icon = "";
+ metaData.tdCls += " pve-itype-fa";
+ }
+ return icon + txt;
+ },
+
+ initComponent : function() {
+ var me = this;
+ var i, confid;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+ var diskCap = caps.vms['VM.Config.Disk'];
+
+ /*jslint confusion: true */
+ var rows = {
+ memory: {
+ header: gettext('Memory'),
+ editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
+ never_delete: true,
+ defaultValue: '512',
+ tdCls: 'pve-itype-icon-memory',
+ group: 2,
+ multiKey: ['memory', 'balloon', 'shares'],
+ renderer: function(value, metaData, record, ri, ci, store, pending) {
+ var res = '';
+
+ var max = me.getObjectValue('memory', 512, pending);
+ var balloon = me.getObjectValue('balloon', undefined, pending);
+ var shares = me.getObjectValue('shares', undefined, pending);
+
+ res = Proxmox.Utils.format_size(max*1024*1024);
+
+ if (balloon !== undefined && balloon > 0) {
+ res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res;
+
+ if (shares) {
+ res += ' [shares=' + shares +']';
+ }
+ } else if (balloon === 0) {
+ res += ' [balloon=0]';
+ }
+ return res;
+ }
+ },
+ sockets: {
+ header: gettext('Processors'),
+ never_delete: true,
+ editor: (caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']) ?
+ 'PVE.qemu.ProcessorEdit' : undefined,
+ tdCls: 'pve-itype-icon-processor',
+ group: 3,
+ defaultValue: '1',
+ multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'],
+ renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
+
+ var sockets = me.getObjectValue('sockets', 1, pending);
+ var model = me.getObjectValue('cpu', undefined, pending);
+ var cores = me.getObjectValue('cores', 1, pending);
+ var numa = me.getObjectValue('numa', undefined, pending);
+ var vcpus = me.getObjectValue('vcpus', undefined, pending);
+ var cpulimit = me.getObjectValue('cpulimit', undefined, pending);
+ var cpuunits = me.getObjectValue('cpuunits', undefined, pending);
+
+ var res = Ext.String.format('{0} ({1} sockets, {2} cores)',
+ sockets*cores, sockets, cores);
+
+ if (model) {
+ res += ' [' + model + ']';
+ }
+
+ if (numa) {
+ res += ' [numa=' + numa +']';
+ }
+
+ if (vcpus) {
+ res += ' [vcpus=' + vcpus +']';
+ }
+
+ if (cpulimit) {
+ res += ' [cpulimit=' + cpulimit +']';
+ }
+
+ if (cpuunits) {
+ res += ' [cpuunits=' + cpuunits +']';
+ }
+
+ return res;
+ }
+ },
+ bios: {
+ header: 'BIOS',
+ group: 4,
+ never_delete: true,
+ editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
+ defaultValue: '',
+ iconCls: 'microchip',
+ renderer: PVE.Utils.render_qemu_bios
+ },
+ vga: {
+ header: gettext('Display'),
+ editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
+ never_delete: true,
+ tdCls: 'pve-itype-icon-display',
+ group:5,
+ defaultValue: '',
+ renderer: PVE.Utils.render_kvm_vga_driver
+ },
+ machine: {
+ header: gettext('Machine'),
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Machine'),
+ width: 350,
+ items: [{
+ xtype: 'proxmoxKVComboBox',
+ name: 'machine',
+ value: '__default__',
+ fieldLabel: gettext('Machine'),
+ comboItems: [
+ ['__default__', PVE.Utils.render_qemu_machine('')],
+ ['q35', 'q35']
+ ]
+ }]} : undefined,
+ iconCls: 'cogs',
+ never_delete: true,
+ group: 6,
+ defaultValue: '',
+ renderer: PVE.Utils.render_qemu_machine
+ },
+ scsihw: {
+ header: gettext('SCSI Controller'),
+ iconCls: 'database',
+ editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
+ renderer: PVE.Utils.render_scsihw,
+ group: 7,
+ never_delete: true,
+ defaultValue: ''
+ },
+ cores: {
+ visible: false
+ },
+ cpu: {
+ visible: false
+ },
+ numa: {
+ visible: false
+ },
+ balloon: {
+ visible: false
+ },
+ hotplug: {
+ visible: false
+ },
+ vcpus: {
+ visible: false
+ },
+ cpuunits: {
+ visible: false
+ },
+ cpulimit: {
+ visible: false
+ },
+ shares: {
+ visible: false
+ }
+ };
+ /*jslint confusion: false */
+
+ PVE.Utils.forEachBus(undefined, function(type, id) {
+ var confid = type + id;
+ rows[confid] = {
+ group: 10,
+ tdCls: 'pve-itype-icon-storage',
+ editor: 'PVE.qemu.HDEdit',
+ never_delete: caps.vms['VM.Config.Disk'] ? false : true,
+ header: gettext('Hard Disk') + ' (' + confid +')',
+ cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
+ cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')'
+ };
+ });
+ for (i = 0; i < 32; i++) {
+ confid = "net" + i.toString();
+ rows[confid] = {
+ group: 15,
+ order: i,
+ tdCls: 'pve-itype-icon-network',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
+ never_delete: caps.vms['VM.Config.Network'] ? false : true,
+ header: gettext('Network Device') + ' (' + confid +')'
+ };
+ }
+ rows.efidisk0 = {
+ group: 20,
+ tdCls: 'pve-itype-icon-storage',
+ editor: null,
+ never_delete: caps.vms['VM.Config.Disk'] ? false : true,
+ header: gettext('EFI Disk')
+ };
+ for (i = 0; i < 5; i++) {
+ confid = "usb" + i.toString();
+ rows[confid] = {
+ group: 25,
+ order: i,
+ tdCls: 'pve-itype-icon-usb',
+ editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
+ never_delete: caps.nodes['Sys.Console'] ? false : true,
+ header: gettext('USB Device') + ' (' + confid + ')'
+ };
+ }
+ for (i = 0; i < 4; i++) {
+ confid = "hostpci" + i.toString();
+ rows[confid] = {
+ group: 30,
+ order: i,
+ tdCls: 'pve-itype-icon-pci',
+ never_delete: caps.nodes['Sys.Console'] ? false : true,
+ editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
+ header: gettext('PCI Device') + ' (' + confid + ')'
+ };
+ }
+ for (i = 0; i < 4; i++) {
+ confid = "serial" + i.toString();
+ rows[confid] = {
+ group: 35,
+ order: i,
+ tdCls: 'pve-itype-icon-serial',
+ never_delete: caps.nodes['Sys.Console'] ? false : true,
+ header: gettext('Serial Port') + ' (' + confid + ')'
+ };
+ }
+ for (i = 0; i < 256; i++) {
+ rows["unused" + i.toString()] = {
+ group: 99,
+ order: i,
+ tdCls: 'pve-itype-icon-storage',
+ editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
+ header: gettext('Unused Disk') + ' ' + i.toString()
+ };
+ }
+
+ var sorterFn = function(rec1, rec2) {
+ var v1 = rec1.data.key;
+ var v2 = rec2.data.key;
+ var g1 = rows[v1].group || 0;
+ var g2 = rows[v2].group || 0;
+ var order1 = rows[v1].order || 0;
+ var order2 = rows[v2].order || 0;
+
+ if ((g1 - g2) !== 0) {
+ return g1 - g2;
+ }
+
+ if ((order1 - order2) !== 0) {
+ return order1 - order2;
+ }
+
+ if (v1 > v2) {
+ return 1;
+ } else if (v1 < v2) {
+ return -1;
+ } else {
+ return 0;
+ }
+ };
+
+ var reload = function() {
+ me.rstore.load();
+ };
+
+ var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var rowdef = rows[rec.data.key];
+ if (!rowdef.editor) {
+ return;
+ }
+
+ var editor = rowdef.editor;
+ if (rowdef.tdCls == 'pve-itype-icon-storage') {
+ var value = me.getObjectValue(rec.data.key, '', true);
+ if (value.match(/vm-.*-cloudinit/)) {
+ return;
+ } else if (value.match(/media=cdrom/)) {
+ editor = 'PVE.qemu.CDEdit';
+ } else if (!diskCap) {
+ return;
+ }
+ }
+
+ var win;
+
+ if (Ext.isString(editor)) {
+ win = Ext.create(editor, {
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs/' + baseurl
+ });
+ } else {
+ var config = Ext.apply({
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs/' + baseurl
+ }, rowdef.editor);
+ win = Ext.createWidget(rowdef.editor.xtype, config);
+ win.load();
+ }
+
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ var run_resize = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.HDResize', {
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid
+ });
+
+ win.show();
+
+ win.on('destroy', reload);
+ };
+
+ var run_move = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.HDMove', {
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid
+ });
+
+ win.show();
+
+ win.on('destroy', reload);
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ selModel: sm,
+ disabled: true,
+ handler: run_editor
+ });
+
+ var resize_btn = new Proxmox.button.Button({
+ text: gettext('Resize disk'),
+ selModel: sm,
+ disabled: true,
+ handler: run_resize
+ });
+
+ var move_btn = new Proxmox.button.Button({
+ text: gettext('Move disk'),
+ selModel: sm,
+ disabled: true,
+ handler: run_move
+ });
+
+ var remove_btn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ defaultText: gettext('Remove'),
+ altText: gettext('Detach'),
+ selModel: sm,
+ disabled: true,
+ dangerous: true,
+ RESTMethod: 'PUT',
+ confirmMsg: function(rec) {
+ var warn = gettext('Are you sure you want to remove entry {0}');
+ if (this.text === this.altText) {
+ warn = gettext('Are you sure you want to detach entry {0}');
+ }
+
+ var entry = rec.data.key;
+ var msg = Ext.String.format(warn, "'"
+ + me.renderKey(entry, {}, rec) + "'");
+
+ if (entry.match(/^unused\d+$/)) {
+ msg += " " + gettext('This will permanently erase all data.');
+ }
+
+ return msg;
+ },
+ handler: function(b, e, rec) {
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/' + baseurl,
+ waitMsgTarget: me,
+ method: b.RESTMethod,
+ params: {
+ 'delete': rec.data.key
+ },
+ callback: function() {
+ reload();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ if (b.RESTMethod === 'POST') {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', {
+ upid: upid,
+ listeners: {
+ destroy: function () {
+ me.reload();
+ }
+ }
+ });
+ win.show();
+ }
+ }
+ });
+ },
+ listeners: {
+ render: function(btn) {
+ // hack: calculate an optimal button width on first display
+ // to prevent the whole toolbar to move when we switch
+ // between the "Remove" and "Detach" labels
+ var def = btn.getSize().width;
+
+ btn.setText(btn.altText);
+ var alt = btn.getSize().width;
+
+ btn.setText(btn.defaultText);
+
+ var optimal = alt > def ? alt : def;
+ btn.setSize({ width: optimal });
+ }
+ }
+ });
+
+ var revert_btn = new Proxmox.button.Button({
+ text: gettext('Revert'),
+ selModel: sm,
+ disabled: true,
+ handler: function(b, e, rec) {
+ var rowdef = me.rows[rec.data.key] || {};
+ var keys = rowdef.multiKey || [ rec.data.key ];
+ var revert = keys.join(',');
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/' + baseurl,
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: {
+ 'revert': revert
+ },
+ callback: function() {
+ reload();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert('Error',response.htmlStatus);
+ }
+ });
+ }
+ });
+
+ var efidisk_menuitem = Ext.create('Ext.menu.Item',{
+ text: gettext('EFI Disk'),
+ iconCls: 'pve-itype-icon-storage',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function() {
+
+ var rstoredata = me.rstore.getData().map;
+ // check if ovmf is configured
+ if (rstoredata.bios && rstoredata.bios.data.value === 'ovmf') {
+ var win = Ext.create('PVE.qemu.EFIDiskEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ } else {
+ Ext.Msg.alert('Error',gettext('Please select OVMF(UEFI) as BIOS first.'));
+ }
+
+ }
+ });
+
+ var set_button_status = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ // disable button when we have an efidisk already
+ // disable is ok in this case, because you can instantly
+ // see that there is already one
+ efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined);
+ // en/disable usb add button
+ var usbcount = 0;
+ var pcicount = 0;
+ var hasCloudInit = false;
+ me.rstore.getData().items.forEach(function(item){
+ if (/^usb\d+/.test(item.id)) {
+ usbcount++;
+ } else if (/^hostpci\d+/.test(item.id)) {
+ pcicount++;
+ }
+ if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) {
+ hasCloudInit = true;
+ }
+ });
+
+ // heuristic only for disabling some stuff, the backend has the final word.
+ var noSysConsolePerm = !caps.nodes['Sys.Console'];
+
+ me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5));
+ me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4));
+ me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit);
+
+ if (!rec) {
+ remove_btn.disable();
+ edit_btn.disable();
+ resize_btn.disable();
+ move_btn.disable();
+ revert_btn.disable();
+ return;
+ }
+ var key = rec.data.key;
+ var value = rec.data.value;
+ var rowdef = rows[key];
+
+ var pending = rec.data['delete'] || me.hasPendingChanges(key);
+ var isCDRom = (value && !!value.toString().match(/media=cdrom/));
+ var isUnusedDisk = key.match(/^unused\d+/);
+ var isUsedDisk = !isUnusedDisk &&
+ rowdef.tdCls == 'pve-itype-icon-storage' &&
+ !isCDRom;
+
+ var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/));
+
+ var isEfi = (key === 'efidisk0');
+
+ remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true) || (isUnusedDisk && !diskCap));
+ remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText);
+ remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
+
+ edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit || (!isCDRom && !diskCap));
+
+ resize_btn.setDisabled(pending || !isUsedDisk || !diskCap);
+
+ move_btn.setDisabled(pending || !isUsedDisk || !diskCap);
+
+ revert_btn.setDisabled(!pending);
+
+ };
+
+ Ext.apply(me, {
+ url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending',
+ interval: 5000,
+ selModel: sm,
+ run_editor: run_editor,
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ items: [
+ {
+ text: gettext('Hard Disk'),
+ iconCls: 'pve-itype-icon-storage',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.HDEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: gettext('CD/DVD Drive'),
+ iconCls: 'pve-itype-icon-cdrom',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.CDEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: gettext('Network Device'),
+ iconCls: 'pve-itype-icon-network',
+ disabled: !caps.vms['VM.Config.Network'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.NetworkEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode,
+ isCreate: true
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ efidisk_menuitem,
+ {
+ text: gettext('USB Device'),
+ itemId: 'addusb',
+ iconCls: 'pve-itype-icon-usb',
+ disabled: !caps.nodes['Sys.Console'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.USBEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: gettext('PCI Device'),
+ itemId: 'addpci',
+ iconCls: 'pve-itype-icon-pci',
+ disabled: !caps.nodes['Sys.Console'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.PCIEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: gettext('Serial Port'),
+ itemId: 'addserial',
+ iconCls: 'pve-itype-icon-serial',
+ disabled: !caps.vms['VM.Config.Options'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.SerialEdit', {
+ url: '/api2/extjs/' + baseurl
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: gettext('CloudInit Drive'),
+ itemId: 'addci',
+ iconCls: 'pve-itype-icon-cloud',
+ disabled: !caps.nodes['Sys.Console'],
+ handler: function() {
+ var win = Ext.create('PVE.qemu.CIDriveEdit', {
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ }
+ ]
+ })
+ },
+ remove_btn,
+ edit_btn,
+ resize_btn,
+ move_btn,
+ revert_btn
+ ],
+ rows: rows,
+ sorterFn: sorterFn,
+ listeners: {
+ itemdblclick: run_editor,
+ selectionchange: set_button_status
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+
+ me.mon(me.rstore, 'refresh', function() {
+ set_button_status();
+ });
+ }
+});
+Ext.define('PVE.qemu.ScsiHwEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent : function() {
+ var me = this;
+
+ Ext.applyIf(me, {
+ subject: gettext('SCSI Controller Type'),
+ items: {
+ xtype: 'pveScsiHwSelector',
+ name: 'scsihw',
+ value: '__default__',
+ fieldLabel: gettext('Type')
+ }
+ });
+
+ me.callParent();
+
+ me.load();
+ }
+});
+Ext.define('PVE.qemu.BiosEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveQemuBiosEdit',
+
+ initComponent : function() {
+ var me = this;
+
+ var EFIHint = Ext.createWidget({
+ xtype: 'displayfield', //submitValue is false, so we don't get submitted
+ userCls: 'pve-hint',
+ value: 'You need to add an EFI disk for storing the ' +
+ 'EFI settings. See the online help for details.',
+ hidden: true
+ });
+
+ Ext.applyIf(me, {
+ subject: 'BIOS',
+ items: [ {
+ xtype: 'pveQemuBiosSelector',
+ onlineHelp: 'qm_bios_and_uefi',
+ name: 'bios',
+ value: '__default__',
+ fieldLabel: 'BIOS',
+ listeners: {
+ 'change' : function(field, newValue) {
+ if (newValue == 'ovmf') {
+ Proxmox.Utils.API2Request({
+ url : me.url,
+ method : 'GET',
+ failure : function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success : function(response, opts) {
+ var vmConfig = response.result.data;
+ // there can be only one
+ if (!vmConfig.efidisk0) {
+ EFIHint.setVisible(true);
+ }
+ }
+ });
+ } else {
+ if (EFIHint.isVisible()) {
+ EFIHint.setVisible(false);
+ }
+ }
+ }
+ }
+ },
+ EFIHint
+ ] });
+
+ me.callParent();
+
+ me.load();
+
+ }
+});
+/*jslint confusion: true */
+Ext.define('PVE.qemu.Options', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ alias: ['widget.PVE.qemu.Options'],
+
+ onlineHelp: 'qm_options',
+
+ initComponent : function() {
+ var me = this;
+ var i;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var rows = {
+ name: {
+ required: true,
+ defaultValue: me.pveSelNode.data.name,
+ header: gettext('Name'),
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Name'),
+ items: {
+ xtype: 'inputpanel',
+ items:{
+ xtype: 'textfield',
+ name: 'name',
+ vtype: 'DnsName',
+ value: '',
+ fieldLabel: gettext('Name'),
+ allowBlank: true
+ },
+ onGetValues: function(values) {
+ var params = values;
+ if (values.name === undefined ||
+ values.name === null ||
+ values.name === '') {
+ params = { 'delete':'name'};
+ }
+ return params;
+ }
+ }
+ } : undefined
+ },
+ onboot: {
+ header: gettext('Start at boot'),
+ defaultValue: '',
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Start at boot'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'onboot',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Start at boot')
+ }
+ } : undefined
+ },
+ startup: {
+ header: gettext('Start/Shutdown order'),
+ defaultValue: '',
+ renderer: PVE.Utils.render_kvm_startup,
+ editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ?
+ {
+ xtype: 'pveWindowStartupEdit',
+ onlineHelp: 'qm_startup_and_shutdown'
+ } : undefined
+ },
+ ostype: {
+ header: gettext('OS Type'),
+ editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.OSTypeEdit' : undefined,
+ renderer: PVE.Utils.render_kvm_ostype,
+ defaultValue: 'other'
+ },
+ bootdisk: {
+ visible: false
+ },
+ boot: {
+ header: gettext('Boot Order'),
+ defaultValue: 'cdn',
+ editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.BootOrderEdit' : undefined,
+ multiKey: ['boot', 'bootdisk'],
+ renderer: function(order, metaData, record, rowIndex, colIndex, store, pending) {
+ var i;
+ var text = '';
+ var bootdisk = me.getObjectValue('bootdisk', undefined, pending);
+ order = order || 'cdn';
+ for (i = 0; i < order.length; i++) {
+ var sel = order.substring(i, i + 1);
+ if (text) {
+ text += ', ';
+ }
+ if (sel === 'c') {
+ if (bootdisk) {
+ text += "Disk '" + bootdisk + "'";
+ } else {
+ text += "Disk";
+ }
+ } else if (sel === 'n') {
+ text += 'Network';
+ } else if (sel === 'a') {
+ text += 'Floppy';
+ } else if (sel === 'd') {
+ text += 'CD-ROM';
+ } else {
+ text += sel;
+ }
+ }
+ return text;
+ }
+ },
+ tablet: {
+ header: gettext('Use tablet for pointer'),
+ defaultValue: true,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Use tablet for pointer'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'tablet',
+ checked: true,
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled')
+ }
+ } : undefined
+ },
+ hotplug: {
+ header: gettext('Hotplug'),
+ defaultValue: 'disk,network,usb',
+ renderer: PVE.Utils.render_hotplug_features,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Hotplug'),
+ items: {
+ xtype: 'pveHotplugFeatureSelector',
+ name: 'hotplug',
+ value: '',
+ multiSelect: true,
+ fieldLabel: gettext('Hotplug'),
+ allowBlank: true
+ }
+ } : undefined
+ },
+ acpi: {
+ header: gettext('ACPI support'),
+ defaultValue: true,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('ACPI support'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'acpi',
+ checked: true,
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled')
+ }
+ } : undefined
+ },
+ kvm: {
+ header: gettext('KVM hardware virtualization'),
+ defaultValue: true,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('KVM hardware virtualization'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'kvm',
+ checked: true,
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled')
+ }
+ } : undefined
+ },
+ freeze: {
+ header: gettext('Freeze CPU at startup'),
+ defaultValue: false,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.PowerMgmt'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Freeze CPU at startup'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'freeze',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ labelWidth: 140,
+ fieldLabel: gettext('Freeze CPU at startup')
+ }
+ } : undefined
+ },
+ localtime: {
+ header: gettext('Use local time for RTC'),
+ defaultValue: false,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Use local time for RTC'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'localtime',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ labelWidth: 140,
+ fieldLabel: gettext('Use local time for RTC')
+ }
+ } : undefined
+ },
+ startdate: {
+ header: gettext('RTC start date'),
+ defaultValue: 'now',
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('RTC start date'),
+ items: {
+ xtype: 'proxmoxtextfield',
+ name: 'startdate',
+ deleteEmpty: true,
+ value: 'now',
+ fieldLabel: gettext('RTC start date'),
+ vtype: 'QemuStartDate',
+ allowBlank: true
+ }
+ } : undefined
+ },
+ smbios1: {
+ header: gettext('SMBIOS settings (type1)'),
+ defaultValue: '',
+ renderer: Ext.String.htmlEncode,
+ editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.Smbios1Edit' : undefined
+ },
+ agent: {
+ header: gettext('Qemu Agent'),
+ defaultValue: false,
+ renderer: PVE.Utils.render_qga_features,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Qemu Agent'),
+ items: {
+ xtype: 'pveAgentFeatureSelector',
+ name: 'agent'
+ }
+ } : undefined
+ },
+ protection: {
+ header: gettext('Protection'),
+ defaultValue: false,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Protection'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'protection',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled')
+ }
+ } : undefined
+ },
+ hookscript: {
+ header: gettext('Hookscript')
+ }
+ };
+
+ var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
+
+ var edit_btn = new Ext.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ handler: function() { me.run_editor(); }
+ });
+
+ var revert_btn = new Proxmox.button.Button({
+ text: gettext('Revert'),
+ disabled: true,
+ handler: function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var rowdef = me.rows[rec.data.key] || {};
+ var keys = rowdef.multiKey || [ rec.data.key ];
+ var revert = keys.join(',');
+
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/' + baseurl,
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: {
+ 'revert': revert
+ },
+ callback: function() {
+ me.reload();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert('Error',response.htmlStatus);
+ }
+ });
+ }
+ });
+
+ var set_button_status = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ return;
+ }
+
+ var key = rec.data.key;
+ var pending = rec.data['delete'] || me.hasPendingChanges(key);
+ var rowdef = rows[key];
+
+ edit_btn.setDisabled(!rowdef.editor);
+ revert_btn.setDisabled(!pending);
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/pending",
+ interval: 5000,
+ cwidth1: 250,
+ tbar: [ edit_btn, revert_btn ],
+ rows: rows,
+ editorConfig: {
+ url: "/api2/extjs/" + baseurl
+ },
+ listeners: {
+ itemdblclick: me.run_editor,
+ selectionchange: set_button_status
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+
+ me.rstore.on('datachanged', function() {
+ set_button_status();
+ });
+ }
+});
+
+Ext.define('PVE.window.Snapshot', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ // needed for finding the reference to submitbutton
+ // because we do not have a controller
+ referenceHolder: true,
+ defaultButton: 'submitbutton',
+ defaultFocus: 'field',
+
+ take_snapshot: function(snapname, descr, vmstate) {
+ var me = this;
+ var params = { snapname: snapname, vmstate: vmstate ? 1 : 0 };
+ if (descr) {
+ params.description = descr;
+ }
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot",
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
+ win.show();
+ me.close();
+ }
+ });
+ },
+
+ update_snapshot: function(snapname, descr) {
+ var me = this;
+ Proxmox.Utils.API2Request({
+ params: { description: descr },
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" +
+ snapname + '/config',
+ waitMsgTarget: me,
+ method: 'PUT',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ me.close();
+ }
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ var summarystore = Ext.create('Ext.data.Store', {
+ model: 'KeyValue',
+ sorters: [
+ {
+ property : 'key',
+ direction: 'ASC'
+ }
+ ]
+ });
+
+ var items = [
+ {
+ xtype: me.snapname ? 'displayfield' : 'textfield',
+ name: 'snapname',
+ value: me.snapname,
+ fieldLabel: gettext('Name'),
+ vtype: 'ConfigId',
+ allowBlank: false
+ }
+ ];
+
+ if (me.snapname) {
+ items.push({
+ xtype: 'displayfield',
+ name: 'snaptime',
+ renderer: PVE.Utils.render_timestamp_human_readable,
+ fieldLabel: gettext('Timestamp')
+ });
+ } else {
+ items.push({
+ xtype: 'proxmoxcheckbox',
+ name: 'vmstate',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ checked: 1,
+ fieldLabel: gettext('Include RAM')
+ });
+ }
+
+ items.push({
+ xtype: 'textareafield',
+ grow: true,
+ name: 'description',
+ fieldLabel: gettext('Description')
+ });
+
+ if (me.snapname) {
+ items.push({
+ title: gettext('Settings'),
+ xtype: 'grid',
+ height: 200,
+ store: summarystore,
+ columns: [
+ {header: gettext('Key'), width: 150, dataIndex: 'key'},
+ {header: gettext('Value'), flex: 1, dataIndex: 'value'}
+ ]
+ });
+ }
+
+ me.formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%'
+ },
+ items: items
+ });
+
+ var form = me.formPanel.getForm();
+
+ var submitBtn;
+
+ if (me.snapname) {
+ me.title = gettext('Edit') + ': ' + gettext('Snapshot');
+ submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Update'),
+ handler: function() {
+ if (form.isValid()) {
+ var values = form.getValues();
+ me.update_snapshot(me.snapname, values.description);
+ }
+ }
+ });
+ } else {
+ me.title ="VM " + me.vmid + ': ' + gettext('Take Snapshot');
+ submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Take Snapshot'),
+ reference: 'submitbutton',
+ handler: function() {
+ if (form.isValid()) {
+ var values = form.getValues();
+ me.take_snapshot(values.snapname, values.description, values.vmstate);
+ }
+ }
+ });
+ }
+
+ Ext.apply(me, {
+ modal: true,
+ width: 450,
+ border: false,
+ layout: 'fit',
+ buttons: [ submitBtn ],
+ items: [ me.formPanel ]
+ });
+
+ if (me.snapname) {
+ Ext.apply(me, {
+ width: 620,
+ height: 420
+ });
+ }
+
+ me.callParent();
+
+ if (!me.snapname) {
+ return;
+ }
+
+ // else load data
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" +
+ me.snapname + '/config',
+ waitMsgTarget: me,
+ method: 'GET',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ me.close();
+ },
+ success: function(response, options) {
+ var data = response.result.data;
+ var kvarray = [];
+ Ext.Object.each(data, function(key, value) {
+ if (key === 'description' || key === 'snaptime') {
+ return;
+ }
+ kvarray.push({ key: key, value: value });
+ });
+
+ summarystore.suspendEvents();
+ summarystore.add(kvarray);
+ summarystore.sort();
+ summarystore.resumeEvents();
+ summarystore.fireEvent('refresh', summarystore);
+
+ form.findField('snaptime').setValue(data.snaptime);
+ form.findField('description').setValue(data.description);
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.SnapshotTree', {
+ extend: 'Ext.tree.Panel',
+ alias: ['widget.pveQemuSnapshotTree'],
+
+ load_delay: 3000,
+
+ old_digest: 'invalid',
+
+ stateful: true,
+ stateId: 'grid-qemu-snapshots',
+
+ sorterFn: function(rec1, rec2) {
+ var v1 = rec1.data.snaptime;
+ var v2 = rec2.data.snaptime;
+
+ if (rec1.data.name === 'current') {
+ return 1;
+ }
+ if (rec2.data.name === 'current') {
+ return -1;
+ }
+
+ return (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0));
+ },
+
+ reload: function(repeat) {
+ var me = this;
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot',
+ method: 'GET',
+ failure: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, response.htmlStatus);
+ me.load_task.delay(me.load_delay);
+ },
+ success: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, false);
+ var digest = 'invalid';
+ var idhash = {};
+ var root = { name: '__root', expanded: true, children: [] };
+ Ext.Array.each(response.result.data, function(item) {
+ item.leaf = true;
+ item.children = [];
+ if (item.name === 'current') {
+ digest = item.digest + item.running;
+ if (item.running) {
+ item.iconCls = 'fa fa-fw fa-desktop x-fa-tree-running';
+ } else {
+ item.iconCls = 'fa fa-fw fa-desktop x-fa-tree';
+ }
+ } else {
+ item.iconCls = 'fa fa-fw fa-history x-fa-tree';
+ }
+ idhash[item.name] = item;
+ });
+
+ if (digest !== me.old_digest) {
+ me.old_digest = digest;
+
+ Ext.Array.each(response.result.data, function(item) {
+ if (item.parent && idhash[item.parent]) {
+ var parent_item = idhash[item.parent];
+ parent_item.children.push(item);
+ parent_item.leaf = false;
+ parent_item.expanded = true;
+ parent_item.expandable = false;
+ } else {
+ root.children.push(item);
+ }
+ });
+
+ me.setRootNode(root);
+ }
+
+ me.load_task.delay(me.load_delay);
+ }
+ });
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/feature',
+ params: { feature: 'snapshot' },
+ method: 'GET',
+ success: function(response, options) {
+ var res = response.result.data;
+ if (res.hasFeature) {
+ var snpBtns = Ext.ComponentQuery.query('#snapshotBtn');
+ snpBtns.forEach(function(item){
+ item.enable();
+ });
+ }
+ }
+ });
+
+
+ },
+
+ listeners: {
+ beforestatesave: function(grid, state, eopts) {
+ // extjs cannot serialize functions,
+ // so a the sorter with only the sorterFn will
+ // not be a valid sorter when restoring the state
+ delete state.storeState.sorters;
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.nodename = me.pveSelNode.data.node;
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.vmid = me.pveSelNode.data.vmid;
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ me.load_task = new Ext.util.DelayedTask(me.reload, me);
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var valid_snapshot = function(record) {
+ return record && record.data && record.data.name &&
+ record.data.name !== 'current';
+ };
+
+ var valid_snapshot_rollback = function(record) {
+ return record && record.data && record.data.name &&
+ record.data.name !== 'current' && !record.data.snapstate;
+ };
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (valid_snapshot(rec)) {
+ var win = Ext.create('PVE.window.Snapshot', {
+ snapname: rec.data.name,
+ nodename: me.nodename,
+ vmid: me.vmid
+ });
+ win.show();
+ me.mon(win, 'close', me.reload, me);
+ }
+ };
+
+ var editBtn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ enableFn: valid_snapshot,
+ handler: run_editor
+ });
+
+ var rollbackBtn = new Proxmox.button.Button({
+ text: gettext('Rollback'),
+ disabled: true,
+ selModel: sm,
+ enableFn: valid_snapshot_rollback,
+ confirmMsg: function(rec) {
+ return Proxmox.Utils.format_task_description('qmrollback', me.vmid) +
+ " '" + rec.data.name + "'";
+ },
+ handler: function(btn, event) {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ var snapname = rec.data.name;
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname + '/rollback',
+ method: 'POST',
+ waitMsgTarget: me,
+ callback: function() {
+ me.reload();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
+ win.show();
+ }
+ });
+ }
+ });
+
+ var removeBtn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ disabled: true,
+ selModel: sm,
+ confirmMsg: function(rec) {
+ var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
+ "'" + rec.data.name + "'");
+ return msg;
+ },
+ enableFn: valid_snapshot,
+ handler: function(btn, event) {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ var snapname = rec.data.name;
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ callback: function() {
+ me.reload();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
+ win.show();
+ }
+ });
+ }
+ });
+
+ var snapshotBtn = Ext.create('Ext.Button', {
+ itemId: 'snapshotBtn',
+ text: gettext('Take Snapshot'),
+ disabled: true,
+ handler: function() {
+ var win = Ext.create('PVE.window.Snapshot', {
+ nodename: me.nodename,
+ vmid: me.vmid
+ });
+ win.show();
+ }
+ });
+
+ Ext.apply(me, {
+ layout: 'fit',
+ rootVisible: false,
+ animate: false,
+ sortableColumns: false,
+ selModel: sm,
+ tbar: [ snapshotBtn, rollbackBtn, removeBtn, editBtn ],
+ fields: [
+ 'name', 'description', 'snapstate', 'vmstate', 'running',
+ { name: 'snaptime', type: 'date', dateFormat: 'timestamp' }
+ ],
+ columns: [
+ {
+ xtype: 'treecolumn',
+ text: gettext('Name'),
+ dataIndex: 'name',
+ width: 200,
+ renderer: function(value, metaData, record) {
+ if (value === 'current') {
+ return "NOW";
+ } else {
+ return value;
+ }
+ }
+ },
+ {
+ text: gettext('RAM'),
+ align: 'center',
+ resizable: false,
+ dataIndex: 'vmstate',
+ width: 50,
+ renderer: function(value, metaData, record) {
+ if (record.data.name !== 'current') {
+ return Proxmox.Utils.format_boolean(value);
+ }
+ }
+ },
+ {
+ text: gettext('Date') + "/" + gettext("Status"),
+ dataIndex: 'snaptime',
+ width: 150,
+ renderer: function(value, metaData, record) {
+ if (record.data.snapstate) {
+ return record.data.snapstate;
+ }
+ if (value) {
+ return Ext.Date.format(value,'Y-m-d H:i:s');
+ }
+ }
+ },
+ {
+ text: gettext('Description'),
+ dataIndex: 'description',
+ flex: 1,
+ renderer: function(value, metaData, record) {
+ if (record.data.name === 'current') {
+ return gettext("You are here!");
+ } else {
+ return Ext.String.htmlEncode(value);
+ }
+ }
+ }
+ ],
+ columnLines: true, // will work in 4.1?
+ listeners: {
+ activate: me.reload,
+ destroy: me.load_task.cancel,
+ itemdblclick: run_editor
+ }
+ });
+
+ me.callParent();
+
+ me.store.sorters.add(new Ext.util.Sorter({
+ sorterFn: me.sorterFn
+ }));
+ }
+});
+
+Ext.define('PVE.qemu.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.qemu.Config',
+
+ onlineHelp: 'chapter_virtual_machines',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var template = !!me.pveSelNode.data.template;
+
+ var running = !!me.pveSelNode.data.uptime;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var base_url = '/nodes/' + nodename + "/qemu/" + vmid;
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: '/api2/json' + base_url + '/status/current',
+ interval: 1000
+ });
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: base_url + '/status/' + cmd,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ };
+
+ var resumeBtn = Ext.create('Ext.Button', {
+ text: gettext('Resume'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ hidden: true,
+ handler: function() {
+ vm_command('resume');
+ },
+ iconCls: 'fa fa-play'
+ });
+
+ var startBtn = Ext.create('Ext.Button', {
+ text: gettext('Start'),
+ disabled: !caps.vms['VM.PowerMgmt'] || running,
+ hidden: template,
+ handler: function() {
+ vm_command('start');
+ },
+ iconCls: 'fa fa-play'
+ });
+
+ var migrateBtn = Ext.create('Ext.Button', {
+ text: gettext('Migrate'),
+ disabled: !caps.vms['VM.Migrate'],
+ hidden: PVE.data.ResourceStore.getNodes().length < 2,
+ handler: function() {
+ var win = Ext.create('PVE.window.Migrate', {
+ vmtype: 'qemu',
+ nodename: nodename,
+ vmid: vmid
+ });
+ win.show();
+ },
+ iconCls: 'fa fa-send-o'
+ });
+
+ var moreBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('More'),
+ menu: { items: [
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: caps.vms['VM.Clone'] ? false : true,
+ handler: function() {
+ PVE.window.Clone.wrap(nodename, vmid, template, 'qemu');
+ }
+ },
+ {
+ text: gettext('Convert to template'),
+ disabled: template,
+ xtype: 'pveMenuItem',
+ iconCls: 'fa fa-fw fa-file-o',
+ hidden: caps.vms['VM.Allocate'] ? false : true,
+ confirmMsg: Proxmox.Utils.format_task_description('qmtemplate', vmid),
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: base_url + '/template',
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ }
+ },
+ {
+ iconCls: 'fa fa-heartbeat ',
+ hidden: !caps.nodes['Sys.Console'],
+ text: gettext('Manage HA'),
+ handler: function() {
+ var ha = me.pveSelNode.data.hastate;
+ Ext.create('PVE.ha.VMResourceEdit', {
+ vmid: vmid,
+ isCreate: (!ha || ha === 'unmanaged')
+ }).show();
+ }
+ },
+ {
+ text: gettext('Remove'),
+ itemId: 'removeBtn',
+ disabled: !caps.vms['VM.Allocate'],
+ handler: function() {
+ Ext.create('PVE.window.SafeDestroy', {
+ url: base_url,
+ item: { type: 'VM', id: vmid }
+ }).show();
+ },
+ iconCls: 'fa fa-trash-o'
+ }
+ ]}
+ });
+
+ var shutdownBtn = Ext.create('PVE.button.Split', {
+ text: gettext('Shutdown'),
+ disabled: !caps.vms['VM.PowerMgmt'] || !running,
+ hidden: template,
+ confirmMsg: Proxmox.Utils.format_task_description('qmshutdown', vmid),
+ handler: function() {
+ vm_command('shutdown');
+ },
+ menu: {
+ items: [{
+ text: gettext('Pause'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ confirmMsg: Proxmox.Utils.format_task_description('qmpause', vmid),
+ handler: function() {
+ vm_command("suspend");
+ },
+ iconCls: 'fa fa-pause'
+ },{
+ text: gettext('Hibernate'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ confirmMsg: Proxmox.Utils.format_task_description('qmsuspend', vmid),
+ tooltip: gettext('Suspend to disk'),
+ handler: function() {
+ vm_command("suspend", { todisk: 1 });
+ },
+ iconCls: 'fa fa-download'
+ },{
+ text: gettext('Stop'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ dangerous: true,
+ tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
+ confirmMsg: Proxmox.Utils.format_task_description('qmstop', vmid),
+ handler: function() {
+ vm_command("stop", { timeout: 30 });
+ },
+ iconCls: 'fa fa-stop'
+ },{
+ text: gettext('Reset'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ confirmMsg: Proxmox.Utils.format_task_description('qmreset', vmid),
+ handler: function() {
+ vm_command("reset");
+ },
+ iconCls: 'fa fa-bolt'
+ }]
+ },
+ iconCls: 'fa fa-power-off'
+ });
+
+ var vm = me.pveSelNode.data;
+
+ var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
+ disabled: !caps.vms['VM.Console'],
+ hidden: template,
+ consoleType: 'kvm',
+ consoleName: vm.name,
+ nodename: nodename,
+ vmid: vmid
+ });
+
+ var statusTxt = Ext.create('Ext.toolbar.TextItem', {
+ data: {
+ lock: undefined
+ },
+ tpl: [
+ '
');
+ }
+ }
+ ]
+ }
+ ]
+});
+
+Ext.define('PVE.qemu.AgentIPView', {
+ extend: 'Ext.container.Container',
+ xtype: 'pveAgentIPView',
+
+ layout: {
+ type: 'hbox',
+ align: 'top'
+ },
+
+ nics: [],
+
+ items: [
+ {
+ xtype: 'box',
+ html: ' IPs'
+ },
+ {
+ xtype: 'container',
+ flex: 1,
+ layout: {
+ type: 'vbox',
+ align: 'right',
+ pack: 'end'
+ },
+ items: [
+ {
+ xtype: 'label',
+ flex: 1,
+ itemId: 'ipBox',
+ style: {
+ 'text-align': 'right'
+ }
+ },
+ {
+ xtype: 'button',
+ itemId: 'moreBtn',
+ hidden: true,
+ ui: 'default-toolbar',
+ handler: function(btn) {
+ var me = this.up('pveAgentIPView');
+
+ var win = Ext.create('PVE.window.IPInfo');
+ win.down('grid').getStore().setData(me.nics);
+ win.show();
+ },
+ text: gettext('More')
+ }
+ ]
+ }
+ ],
+
+ getDefaultIps: function(nics) {
+ var me = this;
+ var ips = [];
+ nics.forEach(function(nic) {
+ if (nic['hardware-address'] &&
+ nic['hardware-address'] != '00:00:00:00:00:00') {
+
+ var nic_ips = nic['ip-addresses'] || [];
+ nic_ips.forEach(function(ip) {
+ var p = ip['ip-address'];
+ // show 2 ips at maximum
+ if (ips.length < 2) {
+ ips.push(p);
+ }
+ });
+ }
+ });
+
+ return ips;
+ },
+
+ startIPStore: function(store, records, success) {
+ var me = this;
+ var agentRec = store.getById('agent');
+ /*jslint confusion: true*/
+ /* value is number and string */
+ me.agent = (agentRec && agentRec.data.value === 1);
+ me.running = (store.getById('status').data.value === 'running');
+ /*jslint confusion: false*/
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ if (!caps.vms['VM.Monitor']) {
+ var errorText = gettext("Requires '{0}' Privileges");
+ me.updateStatus(false, Ext.String.format(errorText, 'VM.Monitor'));
+ return;
+ }
+
+ if (me.agent && me.running && me.ipStore.isStopped) {
+ me.ipStore.startUpdate();
+ } else if (me.ipStore.isStopped) {
+ me.updateStatus();
+ }
+ },
+
+ updateStatus: function(unsuccessful, defaulttext) {
+ var me = this;
+ var text = defaulttext || gettext('No network information');
+ var more = false;
+ if (unsuccessful) {
+ text = gettext('Guest Agent not running');
+ } else if (me.agent && me.running) {
+ if (Ext.isArray(me.nics) && me.nics.length) {
+ more = true;
+ var ips = me.getDefaultIps(me.nics);
+ if (ips.length !== 0) {
+ text = ips.join('
');
+ }
+ } else if (me.nics && me.nics.error) {
+ var msg = gettext('Cannot get info from Guest Agent
Error: {0}');
+ text = Ext.String.format(text, me.nics.error.desc);
+ }
+ } else if (me.agent) {
+ text = gettext('Guest Agent not running');
+ } else {
+ text = gettext('No Guest Agent configured');
+ }
+
+ var ipBox = me.down('#ipBox');
+ ipBox.update(text);
+
+ var moreBtn = me.down('#moreBtn');
+ moreBtn.setVisible(more);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.rstore) {
+ throw 'rstore not given';
+ }
+
+ if (!me.pveSelNode) {
+ throw 'pveSelNode not given';
+ }
+
+ var nodename = me.pveSelNode.data.node;
+ var vmid = me.pveSelNode.data.vmid;
+
+ me.ipStore = Ext.create('Proxmox.data.UpdateStore', {
+ interval: 10000,
+ storeid: 'pve-qemu-agent-' + vmid,
+ method: 'POST',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + nodename + '/qemu/' + vmid + '/agent/network-get-interfaces'
+ }
+ });
+
+ me.callParent();
+
+ me.mon(me.ipStore, 'load', function(store, records, success) {
+ if (records && records.length) {
+ me.nics = records[0].data.result;
+ } else {
+ me.nics = undefined;
+ }
+ me.updateStatus(!success);
+ });
+
+ me.on('destroy', me.ipStore.stopUpdate);
+
+ // if we already have info about the vm, use it immediately
+ if (me.rstore.getCount()) {
+ me.startIPStore(me.rstore, me.rstore.getData(), false);
+ }
+
+ // check if the guest agent is there on every statusstore load
+ me.mon(me.rstore, 'load', me.startIPStore, me);
+ }
+});
+Ext.define('PVE.qemu.CloudInit', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ xtype: 'pveCiPanel',
+
+ onlineHelp: 'qm_cloud_init',
+
+ tbar: [
+ {
+ xtype: 'proxmoxButton',
+ disabled: true,
+ dangerous: true,
+ confirmMsg: function(rec) {
+ var me = this.up('grid');
+ var warn = gettext('Are you sure you want to remove entry {0}');
+
+ var entry = rec.data.key;
+ var msg = Ext.String.format(warn, "'"
+ + me.renderKey(entry, {}, rec) + "'");
+
+ return msg;
+ },
+ enableFn: function(record) {
+ var me = this.up('grid');
+ var caps = Ext.state.Manager.get('GuiCap');
+ if (me.rows[record.data.key].never_delete ||
+ !caps.vms['VM.Config.Network']) {
+ return false;
+ }
+
+ if (record.data.key === 'cipassword' && !record.data.value) {
+ return false;
+ }
+ return true;
+ },
+ handler: function() {
+ var me = this.up('grid');
+ var records = me.getSelection();
+ if (!records || !records.length) {
+ return;
+ }
+
+ var id = records[0].data.key;
+ var match = id.match(/^net(\d+)$/);
+ if (match) {
+ id = 'ipconfig' + match[1];
+ }
+
+ var params = {};
+ params['delete'] = id;
+ Proxmox.Utils.API2Request({
+ url: me.baseurl + '/config',
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: params,
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ callback: function() {
+ me.reload();
+ }
+ });
+ },
+ text: gettext('Remove')
+ },
+ {
+ xtype: 'proxmoxButton',
+ disabled: true,
+ handler: function() {
+ var me = this.up('grid');
+ me.run_editor();
+ },
+ text: gettext('Edit')
+ },
+ '-',
+ {
+ xtype: 'button',
+ itemId: 'savebtn',
+ text: gettext('Regenerate Image'),
+ handler: function() {
+ var me = this.up('grid');
+ var eject_params = {};
+ var insert_params = {};
+ var disk = PVE.Parser.parseQemuDrive(me.ciDriveId, me.ciDrive);
+ var storage = '';
+ var stormatch = disk.file.match(/^([^\:]+)\:/);
+ if (stormatch) {
+ storage = stormatch[1];
+ }
+ eject_params[me.ciDriveId] = 'none,media=cdrom';
+ insert_params[me.ciDriveId] = storage + ':cloudinit';
+
+ var failure = function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ };
+
+ Proxmox.Utils.API2Request({
+ url: me.baseurl + '/config',
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: eject_params,
+ failure: failure,
+ callback: function() {
+ Proxmox.Utils.API2Request({
+ url: me.baseurl + '/config',
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: insert_params,
+ failure: failure,
+ callback: function() {
+ me.reload();
+ }
+ });
+ }
+ });
+ }
+ }
+ ],
+
+ border: false,
+
+ set_button_status: function(rstore, records, success) {
+ if (!success || records.length < 1) {
+ return;
+ }
+ var me = this;
+ var found;
+ records.forEach(function(record) {
+ if (found) {
+ return;
+ }
+ var id = record.data.key;
+ var value = record.data.value;
+ var ciregex = new RegExp("vm-" + me.pveSelNode.data.vmid + "-cloudinit");
+ if (id.match(/^(ide|scsi|sata)\d+$/) && ciregex.test(value)) {
+ found = id;
+ me.ciDriveId = found;
+ me.ciDrive = value;
+ }
+ });
+
+ me.down('#savebtn').setDisabled(!found);
+ me.setDisabled(!found);
+ if (!found) {
+ me.getView().mask(gettext('No CloudInit Drive found'), ['pve-static-mask']);
+ } else {
+ me.getView().unmask();
+ }
+ },
+
+ renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
+ var me = this;
+ var rows = me.rows;
+ var rowdef = rows[key] || {};
+
+ var icon = "";
+ if (rowdef.iconCls) {
+ icon = ' ';
+ }
+ return icon + (rowdef.header || key);
+ },
+
+ listeners: {
+ activate: function () {
+ var me = this;
+ me.rstore.startUpdate();
+ },
+ itemdblclick: function() {
+ var me = this;
+ me.run_editor();
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+ var caps = Ext.state.Manager.get('GuiCap');
+ me.baseurl = '/api2/extjs/nodes/' + nodename + '/qemu/' + vmid;
+ me.url = me.baseurl + '/pending';
+ me.editorConfig.url = me.baseurl + '/config';
+ me.editorConfig.pveSelNode = me.pveSelNode;
+
+ /*jslint confusion: true*/
+ /* editor is string and object */
+ me.rows = {
+ ciuser: {
+ header: gettext('User'),
+ iconCls: 'fa fa-user',
+ never_delete: true,
+ defaultValue: '',
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('User'),
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ deleteEmpty: true,
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('User'),
+ name: 'ciuser'
+ }
+ ]
+ } : undefined,
+ renderer: function(value) {
+ return value || Proxmox.Utils.defaultText;
+ }
+ },
+ cipassword: {
+ header: gettext('Password'),
+ iconCls: 'fa fa-unlock',
+ defaultValue: '',
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Password'),
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ inputType: 'password',
+ deleteEmpty: true,
+ emptyText: Proxmox.Utils.noneText,
+ fieldLabel: gettext('Password'),
+ name: 'cipassword'
+ }
+ ]
+ } : undefined,
+ renderer: function(value) {
+ return value || Proxmox.Utils.noneText;
+ }
+ },
+ searchdomain: {
+ header: gettext('DNS domain'),
+ iconCls: 'fa fa-globe',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
+ never_delete: true,
+ defaultValue: gettext('use host settings')
+ },
+ nameserver: {
+ header: gettext('DNS servers'),
+ iconCls: 'fa fa-globe',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
+ never_delete: true,
+ defaultValue: gettext('use host settings')
+ },
+ sshkeys: {
+ header: gettext('SSH public key'),
+ iconCls: 'fa fa-key',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.SSHKeyEdit' : undefined,
+ never_delete: true,
+ renderer: function(value) {
+ value = decodeURIComponent(value);
+ var keys = value.split('\n');
+ var text = [];
+ keys.forEach(function(key) {
+ if (key.length) {
+ // First erase all quoted strings (eg. command="foo"
+ var v = key.replace(/"(?:\\.|[^"\\])*"/g, '');
+ // Now try to detect the comment:
+ var res = v.match(/^\s*(\S+\s+)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)\s+\S+\s+(.*?)\s*$/, '');
+ if (res) {
+ key = Ext.String.htmlEncode(res[2]);
+ if (res[1]) {
+ key += ' (' + gettext('with options') + ')';
+ }
+ text.push(key);
+ return;
+ }
+ // Most likely invalid at this point, so just stick to
+ // the old value.
+ text.push(Ext.String.htmlEncode(key));
+ }
+ });
+ if (text.length) {
+ return text.join('
');
+ } else {
+ return Proxmox.Utils.noneText;
+ }
+ },
+ defaultValue: ''
+ }
+ };
+ var i;
+ var ipconfig_renderer = function(value, md, record, ri, ci, store, pending) {
+ var id = record.data.key;
+ var match = id.match(/^net(\d+)$/);
+ var val = '';
+ if (match) {
+ val = me.getObjectValue('ipconfig'+match[1], '', pending);
+ }
+ return val;
+ };
+ for (i = 0; i < 32; i++) {
+ // we want to show an entry for every network device
+ // even if it is empty
+ me.rows['net' + i.toString()] = {
+ multiKey: ['ipconfig' + i.toString(), 'net' + i.toString()],
+ header: gettext('IP Config') + ' (net' + i.toString() +')',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.IPConfigEdit' : undefined,
+ iconCls: 'fa fa-exchange',
+ renderer: ipconfig_renderer
+ };
+ me.rows['ipconfig' + i.toString()] = {
+ visible: false
+ };
+ }
+ /*jslint confusion: false*/
+
+ PVE.Utils.forEachBus(['ide', 'scsi', 'sata'], function(type, id) {
+ me.rows[type+id] = {
+ visible: false
+ };
+ });
+ me.callParent();
+ me.mon(me.rstore, 'load', me.set_button_status, me);
+ }
+});
+Ext.define('PVE.qemu.CIDriveInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveCIDriveInputPanel',
+
+ insideWizard: false,
+
+ vmconfig: {}, // used to select usused disks
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var drive = {};
+ var params = {};
+ drive.file = values.hdstorage + ":cloudinit";
+ drive.format = values.diskformat;
+ params[values.controller + values.deviceid] = PVE.Parser.printQemuDrive(drive);
+ return params;
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ me.down('#hdstorage').setNodename(nodename);
+ me.down('#hdimage').setStorage(undefined, nodename);
+ },
+
+ setVMConfig: function(config) {
+ var me = this;
+ me.down('#drive').setVMConfig(config, 'cdrom');
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.drive = {};
+
+ me.items = [
+ {
+ xtype: 'pveControllerSelector',
+ noVirtIO: true,
+ itemId: 'drive',
+ fieldLabel: gettext('CloudInit Drive'),
+ name: 'drive'
+ },
+ {
+ xtype: 'pveDiskStorageSelector',
+ itemId: 'storselector',
+ storageContent: 'images',
+ nodename: me.nodename,
+ hideSize: true
+ }
+ ];
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.CIDriveEdit', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveCIDriveEdit',
+
+ isCreate: true,
+ subject: gettext('CloudInit Drive'),
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.items = [{
+ xtype: 'pveCIDriveInputPanel',
+ itemId: 'cipanel',
+ nodename: nodename
+ }];
+
+ me.callParent();
+
+ me.load({
+ success: function(response, opts) {
+ me.down('#cipanel').setVMConfig(response.result.data);
+ }
+ });
+ }
+});
+Ext.define('PVE.qemu.SSHKeyInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveQemuSSHKeyInputPanel',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+ if (values.sshkeys) {
+ values.sshkeys.trim();
+ }
+ if (!values.sshkeys.length) {
+ values = {};
+ values['delete'] = 'sshkeys';
+ return values;
+ } else {
+ values.sshkeys = encodeURIComponent(values.sshkeys);
+ }
+ return values;
+ },
+
+ items: [
+ {
+ xtype: 'textarea',
+ itemId: 'sshkeys',
+ name: 'sshkeys',
+ height: 250
+ },
+ {
+ xtype: 'filebutton',
+ itemId: 'filebutton',
+ name: 'file',
+ text: gettext('Load SSH Key File'),
+ fieldLabel: 'test',
+ listeners: {
+ change: function(btn, e, value) {
+ var me = this.up('inputpanel');
+ e = e.event;
+ Ext.Array.each(e.target.files, function(file) {
+ PVE.Utils.loadSSHKeyFromFile(file, function(res) {
+ var keysField = me.down('#sshkeys');
+ var old = keysField.getValue();
+ keysField.setValue(old + res);
+ });
+ });
+ btn.reset();
+ }
+ }
+ }
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+ if (!window.FileReader) {
+ me.down('#filebutton').setVisible(false);
+ }
+
+ }
+});
+
+Ext.define('PVE.qemu.SSHKeyEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ width: 800,
+
+ initComponent : function() {
+ var me = this;
+
+ var ipanel = Ext.create('PVE.qemu.SSHKeyInputPanel');
+
+ Ext.apply(me, {
+ subject: gettext('SSH Keys'),
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ if (!me.create) {
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data;
+ if (data.sshkeys) {
+ data.sshkeys = decodeURIComponent(data.sshkeys);
+ ipanel.setValues(data);
+ }
+ }
+ });
+ }
+ }
+});
+Ext.define('PVE.qemu.IPConfigPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveIPConfigPanel',
+
+ insideWizard: false,
+
+ vmconfig: {},
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (values.ipv4mode !== 'static') {
+ values.ip = values.ipv4mode;
+ }
+
+ if (values.ipv6mode !== 'static') {
+ values.ip6 = values.ipv6mode;
+ }
+
+ var params = {};
+
+ var cfg = PVE.Parser.printIPConfig(values);
+ if (cfg === '') {
+ params['delete'] = [me.confid];
+ } else {
+ params[me.confid] = cfg;
+ }
+ return params;
+ },
+
+ setVMConfig: function(config) {
+ var me = this;
+ me.vmconfig = config;
+ },
+
+ setIPConfig: function(confid, data) {
+ var me = this;
+
+ me.confid = confid;
+
+ if (data.ip === 'dhcp') {
+ data.ipv4mode = data.ip;
+ data.ip = '';
+ } else {
+ data.ipv4mode = 'static';
+ }
+ if (data.ip6 === 'dhcp' || data.ip6 === 'auto') {
+ data.ipv6mode = data.ip6;
+ data.ip6 = '';
+ } else {
+ data.ipv6mode = 'static';
+ }
+
+ me.ipconfig = data;
+ me.setValues(me.ipconfig);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.ipconfig = {};
+
+ me.column1 = [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Network Device'),
+ value: me.netid
+ },
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: gettext('IPv4') + ':'
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv4mode',
+ inputValue: 'static',
+ checked: false,
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip]').setDisabled(!value);
+ me.down('field[name=gw]').setDisabled(!value);
+ }
+ }
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('DHCP'),
+ name: 'ipv4mode',
+ inputValue: 'dhcp',
+ checked: false,
+ margin: '0 0 0 10'
+ }
+ ]
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip',
+ vtype: 'IPCIDRAddress',
+ value: '',
+ disabled: true,
+ fieldLabel: gettext('IPv4/CIDR')
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw',
+ value: '',
+ vtype: 'IPAddress',
+ disabled: true,
+ fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') +')'
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'displayfield'
+ },
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: gettext('IPv6') + ':'
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv6mode',
+ inputValue: 'static',
+ checked: false,
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip6]').setDisabled(!value);
+ me.down('field[name=gw6]').setDisabled(!value);
+ }
+ }
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('DHCP'),
+ name: 'ipv6mode',
+ inputValue: 'dhcp',
+ checked: false,
+ margin: '0 0 0 10'
+ }
+ ]
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip6',
+ value: '',
+ vtype: 'IP6CIDRAddress',
+ disabled: true,
+ fieldLabel: gettext('IPv6/CIDR')
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw6',
+ vtype: 'IP6Address',
+ value: '',
+ disabled: true,
+ fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')'
+ }
+ ];
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.qemu.IPConfigEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ initComponent : function() {
+ /*jslint confusion: true */
+
+ var me = this;
+
+ // convert confid from netX to ipconfigX
+ var match = me.confid.match(/^net(\d+)$/);
+ if (match) {
+ me.netid = me.confid;
+ me.confid = 'ipconfig' + match[1];
+ }
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = me.confid ? false : true;
+
+ var ipanel = Ext.create('PVE.qemu.IPConfigPanel', {
+ confid: me.confid,
+ netid: me.netid,
+ nodename: nodename
+ });
+
+ Ext.applyIf(me, {
+ subject: gettext('Network Config'),
+ items: ipanel
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ me.vmconfig = response.result.data;
+ var ipconfig = {};
+ var value = me.vmconfig[me.confid];
+ if (value) {
+ ipconfig = PVE.Parser.parseIPConfig(me.confid, value);
+ if (!ipconfig) {
+ Ext.Msg.alert(gettext('Error'), gettext('Unable to parse network configuration'));
+ me.close();
+ return;
+ }
+ }
+ ipanel.setIPConfig(me.confid, ipconfig);
+ ipanel.setVMConfig(me.vmconfig);
+ }
+ });
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.qemu.SystemInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveQemuSystemPanel',
+
+ onlineHelp: 'qm_system_settings',
+
+ viewModel: {
+ data: {
+ efi: false,
+ addefi: true
+ },
+
+ formulas: {
+ efidisk: function(get) {
+ return get('efi') && get('addefi');
+ }
+ }
+ },
+
+ onGetValues: function(values) {
+ if (values.vga && values.vga.substr(0,6) === 'serial') {
+ values['serial' + values.vga.substr(6,1)] = 'socket';
+ }
+
+ var efidrive = {};
+ if (values.hdimage) {
+ efidrive.file = values.hdimage;
+ } else if (values.hdstorage) {
+ efidrive.file = values.hdstorage + ":1";
+ }
+
+ if (values.diskformat) {
+ efidrive.format = values.diskformat;
+ }
+
+ delete values.hdimage;
+ delete values.hdstorage;
+ delete values.diskformat;
+
+ if (efidrive.file) {
+ values.efidisk0 = PVE.Parser.printQemuDrive(efidrive);
+ }
+
+ return values;
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ scsihwChange: function(field, value) {
+ var me = this;
+ if (me.getView().insideWizard) {
+ me.getViewModel().set('current.scsihw', value);
+ }
+ },
+
+ biosChange: function(field, value) {
+ var me = this;
+ if (me.getView().insideWizard) {
+ me.getViewModel().set('efi', value === 'ovmf');
+ }
+ },
+
+ control: {
+ 'pveScsiHwSelector': {
+ change: 'scsihwChange'
+ },
+ 'pveQemuBiosSelector': {
+ change: 'biosChange'
+ }
+ }
+ },
+
+ column1: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ value: '__default__',
+ deleteEmpty: false,
+ fieldLabel: gettext('Graphic card'),
+ name: 'vga',
+ comboItems: PVE.Utils.kvm_vga_driver_array()
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'agent',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Qemu Agent')
+ }
+ ],
+
+ column2: [
+ {
+ xtype: 'pveScsiHwSelector',
+ name: 'scsihw',
+ value: '__default__',
+ bind: {
+ value: '{current.scsihw}'
+ },
+ fieldLabel: gettext('SCSI Controller')
+ }
+ ],
+
+ advancedColumn1: [
+ {
+ xtype: 'pveQemuBiosSelector',
+ name: 'bios',
+ value: '__default__',
+ fieldLabel: 'BIOS'
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ bind: {
+ value: '{addefi}',
+ hidden: '{!efi}',
+ disabled: '{!efi}'
+ },
+ hidden: true,
+ submitValue: false,
+ disabled: true,
+ fieldLabel: gettext('Add EFI Disk')
+ },
+ {
+ xtype: 'pveDiskStorageSelector',
+ name: 'efidisk0',
+ storageContent: 'images',
+ bind: {
+ nodename: '{nodename}',
+ hidden: '{!efi}',
+ disabled: '{!efidisk}'
+ },
+ autoSelect: false,
+ disabled: true,
+ hidden: true,
+ hideSize: true
+ }
+ ],
+
+ advancedColumn2: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'machine',
+ value: '__default__',
+ fieldLabel: gettext('Machine'),
+ comboItems: [
+ ['__default__', PVE.Utils.render_qemu_machine('')],
+ ['q35', 'q35']
+ ]
+ }
+ ]
+
+});
+Ext.define('PVE.lxc.Summary', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveLxcSummary',
+
+ scrollable: true,
+ bodyPadding: 5,
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.workspace) {
+ throw "no workspace specified";
+ }
+
+ if (!me.statusStore) {
+ throw "no status storage specified";
+ }
+
+ var template = !!me.pveSelNode.data.template;
+ var rstore = me.statusStore;
+
+ var width = template ? 1 : 0.5;
+ var items = [
+ {
+ xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView',
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: width
+ },
+ 'width >= 1900': {
+ columnWidth: width / 2
+ }
+ },
+ itemId: 'gueststatus',
+ pveSelNode: me.pveSelNode,
+ rstore: rstore
+ },
+ {
+ xtype: 'pveNotesView',
+ maxHeight: 320,
+ itemId: 'notesview',
+ pveSelNode: me.pveSelNode,
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: width
+ },
+ 'width >= 1900': {
+ columnWidth: width / 2
+ }
+ }
+ }
+ ];
+
+ var rrdstore;
+ if (!template) {
+
+ rrdstore = Ext.create('Proxmox.data.RRDStore', {
+ rrdurl: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/rrddata",
+ model: 'pve-rrd-guest'
+ });
+
+ items.push(
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('CPU usage'),
+ pveSelNode: me.pveSelNode,
+ fields: ['cpu'],
+ fieldTitles: [gettext('CPU usage')],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Memory usage'),
+ pveSelNode: me.pveSelNode,
+ fields: ['maxmem', 'mem'],
+ fieldTitles: [gettext('Total'), gettext('RAM usage')],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Network traffic'),
+ pveSelNode: me.pveSelNode,
+ fields: ['netin','netout'],
+ store: rrdstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Disk IO'),
+ pveSelNode: me.pveSelNode,
+ fields: ['diskread','diskwrite'],
+ store: rrdstore
+ }
+ );
+
+ }
+
+ Ext.apply(me, {
+ tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ],
+ items: [
+ {
+ xtype: 'container',
+ layout: {
+ type: 'column'
+ },
+ defaults: {
+ minHeight: 320,
+ padding: 5,
+ plugins: 'responsive',
+ responsiveConfig: {
+ 'width < 1900': {
+ columnWidth: 1
+ },
+ 'width >= 1900': {
+ columnWidth: 0.5
+ }
+ }
+ },
+ items: items
+ }
+ ]
+ });
+
+ me.callParent();
+ if (!template) {
+ rrdstore.startUpdate();
+ me.on('destroy', rrdstore.stopUpdate);
+ }
+ }
+});
+Ext.define('PVE.lxc.NetworkInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveLxcNetworkInputPanel',
+
+ insideWizard: false,
+
+ onlineHelp: 'pct_container_network',
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || (me.nodename === nodename)) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ var bridgesel = me.query("[isFormField][name=bridge]")[0];
+ bridgesel.setNodename(nodename);
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var id;
+ if (me.isCreate) {
+ id = values.id;
+ delete values.id;
+ } else {
+ id = me.ifname;
+ }
+
+ if (!id) {
+ return {};
+ }
+
+ var newdata = {};
+
+ if (values.ipv6mode !== 'static') {
+ values.ip6 = values.ipv6mode;
+ }
+ if (values.ipv4mode !== 'static') {
+ values.ip = values.ipv4mode;
+ }
+ newdata[id] = PVE.Parser.printLxcNetwork(values);
+ return newdata;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ var cdata = {};
+
+ if (me.insideWizard) {
+ me.ifname = 'net0';
+ cdata.name = 'eth0';
+ me.dataCache = {};
+ }
+ cdata.firewall = (me.insideWizard || me.isCreate);
+
+ if (!me.dataCache) {
+ throw "no dataCache specified";
+ }
+
+ if (!me.isCreate) {
+ if (!me.ifname) {
+ throw "no interface name specified";
+ }
+ if (!me.dataCache[me.ifname]) {
+ throw "no such interface '" + me.ifname + "'";
+ }
+
+ cdata = PVE.Parser.parseLxcNetwork(me.dataCache[me.ifname]);
+ }
+
+ var i;
+ for (i = 0; i < 10; i++) {
+ if (me.isCreate && !me.dataCache['net'+i.toString()]) {
+ me.ifname = 'net' + i.toString();
+ break;
+ }
+ }
+
+ var idselector = {
+ xtype: 'hidden',
+ name: 'id',
+ value: me.ifname
+ };
+
+ me.column1 = [
+ idselector,
+ {
+ xtype: 'textfield',
+ name: 'name',
+ fieldLabel: gettext('Name'),
+ emptyText: '(e.g., eth0)',
+ allowBlank: false,
+ value: cdata.name,
+ validator: function(value) {
+ var result = '';
+ Ext.Object.each(me.dataCache, function(key, netstr) {
+ if (!key.match(/^net\d+/) || key === me.ifname) {
+ return; // continue
+ }
+ var net = PVE.Parser.parseLxcNetwork(netstr);
+ if (net.name === value) {
+ result = "interface name already in use";
+ return false;
+ }
+ });
+ if (result !== '') {
+ return result;
+ }
+ // validator can return bool/string
+ /*jslint confusion:true*/
+ return true;
+ }
+ },
+ {
+ xtype: 'textfield',
+ name: 'hwaddr',
+ fieldLabel: gettext('MAC address'),
+ vtype: 'MacAddress',
+ value: cdata.hwaddr,
+ allowBlank: true,
+ emptyText: 'auto'
+ },
+ {
+ xtype: 'PVE.form.BridgeSelector',
+ name: 'bridge',
+ nodename: me.nodename,
+ fieldLabel: gettext('Bridge'),
+ value: cdata.bridge,
+ allowBlank: false
+ },
+ {
+ xtype: 'pveVlanField',
+ name: 'tag',
+ value: cdata.tag
+ },
+ {
+ xtype: 'numberfield',
+ name: 'rate',
+ fieldLabel: gettext('Rate limit') + ' (MB/s)',
+ minValue: 0,
+ maxValue: 10*1024,
+ value: cdata.rate,
+ emptyText: 'unlimited',
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Firewall'),
+ name: 'firewall',
+ value: cdata.firewall
+ }
+ ];
+
+ var dhcp4 = (cdata.ip === 'dhcp');
+ if (dhcp4) {
+ cdata.ip = '';
+ cdata.gw = '';
+ }
+
+ var auto6 = (cdata.ip6 === 'auto');
+ var dhcp6 = (cdata.ip6 === 'dhcp');
+ if (auto6 || dhcp6) {
+ cdata.ip6 = '';
+ cdata.gw6 = '';
+ }
+
+ me.column2 = [
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: 'IPv4:' // do not localize
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv4mode',
+ inputValue: 'static',
+ checked: !dhcp4,
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip]').setDisabled(!value);
+ me.down('field[name=gw]').setDisabled(!value);
+ }
+ }
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: 'DHCP', // do not localize
+ name: 'ipv4mode',
+ inputValue: 'dhcp',
+ checked: dhcp4,
+ margin: '0 0 0 10'
+ }
+ ]
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip',
+ vtype: 'IPCIDRAddress',
+ value: cdata.ip,
+ disabled: dhcp4,
+ fieldLabel: 'IPv4/CIDR' // do not localize
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw',
+ value: cdata.gw,
+ vtype: 'IPAddress',
+ disabled: dhcp4,
+ fieldLabel: gettext('Gateway') + ' (IPv4)',
+ margin: '0 0 3 0' // override bottom margin to account for the menuseparator
+ },
+ {
+ xtype: 'menuseparator',
+ height: '3',
+ margin: '0'
+ },
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: 'IPv6:' // do not localize
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv6mode',
+ inputValue: 'static',
+ checked: !(auto6 || dhcp6),
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip6]').setDisabled(!value);
+ me.down('field[name=gw6]').setDisabled(!value);
+ }
+ }
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: 'DHCP', // do not localize
+ name: 'ipv6mode',
+ inputValue: 'dhcp',
+ checked: dhcp6,
+ margin: '0 0 0 10'
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: 'SLAAC', // do not localize
+ name: 'ipv6mode',
+ inputValue: 'auto',
+ checked: auto6,
+ margin: '0 0 0 10'
+ }
+ ]
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip6',
+ value: cdata.ip6,
+ vtype: 'IP6CIDRAddress',
+ disabled: (dhcp6 || auto6),
+ fieldLabel: 'IPv6/CIDR' // do not localize
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw6',
+ vtype: 'IP6Address',
+ value: cdata.gw6,
+ disabled: (dhcp6 || auto6),
+ fieldLabel: gettext('Gateway') + ' (IPv6)'
+ }
+ ];
+
+ me.callParent();
+ }
+});
+
+
+Ext.define('PVE.lxc.NetworkEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.dataCache) {
+ throw "no dataCache specified";
+ }
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var ipanel = Ext.create('PVE.lxc.NetworkInputPanel', {
+ ifname: me.ifname,
+ nodename: me.nodename,
+ dataCache: me.dataCache,
+ isCreate: me.isCreate
+ });
+
+ Ext.apply(me, {
+ subject: gettext('Network Device') + ' (veth)',
+ digest: me.dataCache.digest,
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.lxc.NetworkView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveLxcNetworkView',
+
+ onlineHelp: 'pct_container_network',
+
+ dataCache: {}, // used to store result of last load
+
+ stateful: true,
+ stateId: 'grid-lxc-network',
+
+ load: function() {
+ var me = this;
+
+ Proxmox.Utils.setErrorMask(me, true);
+
+ Proxmox.Utils.API2Request({
+ url: me.url,
+ failure: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus);
+ },
+ success: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, false);
+ var result = Ext.decode(response.responseText);
+ var data = result.data || {};
+ me.dataCache = data;
+ var records = [];
+ Ext.Object.each(data, function(key, value) {
+ if (!key.match(/^net\d+/)) {
+ return; // continue
+ }
+ var net = PVE.Parser.parseLxcNetwork(value);
+ net.id = key;
+ records.push(net);
+ });
+ me.store.loadData(records);
+ me.down('button[name=addButton]').setDisabled((records.length >= 10));
+ }
+ });
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ me.url = '/nodes/' + nodename + '/lxc/' + vmid + '/config';
+
+ var store = new Ext.data.Store({
+ model: 'pve-lxc-network',
+ sorters: [
+ {
+ property : 'id',
+ direction: 'ASC'
+ }
+ ]
+ });
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var remove_btn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ return !!caps.vms['VM.Config.Network'];
+ },
+ confirmMsg: function (rec) {
+ return Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
+ "'" + rec.data.id + "'");
+ },
+ handler: function(btn, event, rec) {
+ Proxmox.Utils.API2Request({
+ url: me.url,
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: { 'delete': rec.data.id, digest: me.dataCache.digest },
+ callback: function() {
+ me.load();
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ }
+ });
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ if (!caps.vms['VM.Config.Network']) {
+ return false;
+ }
+
+ var win = Ext.create('PVE.lxc.NetworkEdit', {
+ url: me.url,
+ nodename: nodename,
+ dataCache: me.dataCache,
+ ifname: rec.data.id
+ });
+ win.on('destroy', me.load, me);
+ win.show();
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ selModel: sm,
+ disabled: true,
+ enableFn: function(rec) {
+ if (!caps.vms['VM.Config.Network']) {
+ return false;
+ }
+ return true;
+ },
+ handler: run_editor
+ });
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: [
+ {
+ text: gettext('Add'),
+ name: 'addButton',
+ disabled: !caps.vms['VM.Config.Network'],
+ handler: function() {
+ var win = Ext.create('PVE.lxc.NetworkEdit', {
+ url: me.url,
+ nodename: nodename,
+ isCreate: true,
+ dataCache: me.dataCache
+ });
+ win.on('destroy', me.load, me);
+ win.show();
+ }
+ },
+ remove_btn,
+ edit_btn
+ ],
+ columns: [
+ {
+ header: 'ID',
+ width: 50,
+ dataIndex: 'id'
+ },
+ {
+ header: gettext('Name'),
+ width: 80,
+ dataIndex: 'name'
+ },
+ {
+ header: gettext('Bridge'),
+ width: 80,
+ dataIndex: 'bridge'
+ },
+ {
+ header: gettext('Firewall'),
+ width: 80,
+ dataIndex: 'firewall',
+ renderer: Proxmox.Utils.format_boolean
+ },
+ {
+ header: gettext('VLAN Tag'),
+ width: 80,
+ dataIndex: 'tag'
+ },
+ {
+ header: gettext('MAC address'),
+ width: 110,
+ dataIndex: 'hwaddr'
+ },
+ {
+ header: gettext('IP address'),
+ width: 150,
+ dataIndex: 'ip',
+ renderer: function(value, metaData, rec) {
+ if (rec.data.ip && rec.data.ip6) {
+ return rec.data.ip + "
" + rec.data.ip6;
+ } else if (rec.data.ip6) {
+ return rec.data.ip6;
+ } else {
+ return rec.data.ip;
+ }
+ }
+ },
+ {
+ header: gettext('Gateway'),
+ width: 150,
+ dataIndex: 'gw',
+ renderer: function(value, metaData, rec) {
+ if (rec.data.gw && rec.data.gw6) {
+ return rec.data.gw + "
" + rec.data.gw6;
+ } else if (rec.data.gw6) {
+ return rec.data.gw6;
+ } else {
+ return rec.data.gw;
+ }
+ }
+ }
+ ],
+ listeners: {
+ activate: me.load,
+ itemdblclick: run_editor
+ }
+ });
+
+ me.callParent();
+ }
+}, function() {
+
+ Ext.define('pve-lxc-network', {
+ extend: "Ext.data.Model",
+ proxy: { type: 'memory' },
+ fields: [ 'id', 'name', 'hwaddr', 'bridge',
+ 'ip', 'gw', 'ip6', 'gw6', 'tag', 'firewall' ]
+ });
+
+});
+
+/*jslint confusion: true */
+Ext.define('PVE.lxc.RessourceView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pveLxcRessourceView'],
+
+ onlineHelp: 'pct_configuration',
+
+ renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
+ var me = this;
+ var rowdef = me.rows[key] || {};
+
+ metaData.tdAttr = "valign=middle";
+ if (rowdef.tdCls) {
+ metaData.tdCls = rowdef.tdCls;
+ }
+ return rowdef.header || key;
+ },
+
+ initComponent : function() {
+ var me = this;
+ var i, confid;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+ var diskCap = caps.vms['VM.Config.Disk'];
+
+ var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined;
+
+ var rows = {
+ memory: {
+ header: gettext('Memory'),
+ editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
+ defaultValue: 512,
+ tdCls: 'pve-itype-icon-memory',
+ group: 1,
+ renderer: function(value) {
+ return Proxmox.Utils.format_size(value*1024*1024);
+ }
+ },
+ swap: {
+ header: gettext('Swap'),
+ editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
+ defaultValue: 512,
+ tdCls: 'pve-itype-icon-swap',
+ group: 2,
+ renderer: function(value) {
+ return Proxmox.Utils.format_size(value*1024*1024);
+ }
+ },
+ cores: {
+ header: gettext('Cores'),
+ editor: caps.vms['VM.Config.CPU'] ? 'PVE.lxc.CPUEdit' : undefined,
+ defaultValue: '',
+ tdCls: 'pve-itype-icon-processor',
+ group: 3,
+ renderer: function(value) {
+ var cpulimit = me.getObjectValue('cpulimit');
+ var cpuunits = me.getObjectValue('cpuunits');
+ var res;
+ if (value) {
+ res = value;
+ } else {
+ res = gettext('unlimited');
+ }
+
+ if (cpulimit) {
+ res += ' [cpulimit=' + cpulimit + ']';
+ }
+
+ if (cpuunits) {
+ res += ' [cpuunits=' + cpuunits + ']';
+ }
+ return res;
+ }
+ },
+ rootfs: {
+ header: gettext('Root Disk'),
+ defaultValue: Proxmox.Utils.noneText,
+ editor: mpeditor,
+ tdCls: 'pve-itype-icon-storage',
+ group: 4
+ },
+ cpulimit: {
+ visible: false
+ },
+ cpuunits: {
+ visible: false
+ },
+ unprivileged: {
+ visible: false
+ }
+ };
+
+ PVE.Utils.forEachMP(function(bus, i) {
+ confid = bus + i;
+ var group = 5;
+ var header;
+ if (bus === 'mp') {
+ header = gettext('Mount Point') + ' (' + confid + ')';
+ } else {
+ header = gettext('Unused Disk') + ' ' + i;
+ group += 1;
+ }
+ rows[confid] = {
+ group: group,
+ order: i,
+ tdCls: 'pve-itype-icon-storage',
+ editor: mpeditor,
+ header: header
+ };
+ }, true);
+
+ var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
+
+ me.selModel = Ext.create('Ext.selection.RowModel', {});
+
+ var run_resize = function() {
+ var rec = me.selModel.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.MPResize', {
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid
+ });
+
+ win.show();
+ };
+
+ var run_remove = function(b, e, rec) {
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/' + baseurl,
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: {
+ 'delete': rec.data.key
+ },
+ failure: function (response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ };
+
+ var run_move = function(b, e, rec) {
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.HDMove', {
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid,
+ type: 'lxc'
+ });
+
+ win.show();
+
+ win.on('destroy', me.reload, me);
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ selModel: me.selModel,
+ disabled: true,
+ enableFn: function(rec) {
+ if (!rec) {
+ return false;
+ }
+ var rowdef = rows[rec.data.key];
+ return !!rowdef.editor;
+ },
+ handler: function() { me.run_editor(); }
+ });
+
+ var resize_btn = new Proxmox.button.Button({
+ text: gettext('Resize disk'),
+ selModel: me.selModel,
+ disabled: true,
+ handler: run_resize
+ });
+
+ var remove_btn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ selModel: me.selModel,
+ disabled: true,
+ dangerous: true,
+ confirmMsg: function(rec) {
+ var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
+ "'" + me.renderKey(rec.data.key, {}, rec) + "'");
+ if (rec.data.key.match(/^unused\d+$/)) {
+ msg += " " + gettext('This will permanently erase all data.');
+ }
+
+ return msg;
+ },
+ handler: run_remove
+ });
+
+ var move_btn = new Proxmox.button.Button({
+ text: gettext('Move Volume'),
+ selModel: me.selModel,
+ disabled: true,
+ dangerous: true,
+ handler: run_move
+ });
+
+ var set_button_status = function() {
+ var rec = me.selModel.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ remove_btn.disable();
+ resize_btn.disable();
+ return;
+ }
+ var key = rec.data.key;
+ var value = rec.data.value;
+ var rowdef = rows[key];
+
+ var isDisk = (rowdef.tdCls == 'pve-itype-icon-storage');
+
+ var noedit = rec.data['delete'] || !rowdef.editor;
+ if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) {
+ var mp = PVE.Parser.parseLxcMountPoint(value);
+ if (mp.type !== 'volume') {
+ noedit = true;
+ }
+ }
+ edit_btn.setDisabled(noedit);
+
+ remove_btn.setDisabled(!isDisk || rec.data.key === 'rootfs' || !diskCap);
+ resize_btn.setDisabled(!isDisk || !diskCap);
+ move_btn.setDisabled(!isDisk || !diskCap);
+
+ };
+
+ var sorterFn = function(rec1, rec2) {
+ var v1 = rec1.data.key;
+ var v2 = rec2.data.key;
+ var g1 = rows[v1].group || 0;
+ var g2 = rows[v2].group || 0;
+ var order1 = rows[v1].order || 0;
+ var order2 = rows[v2].order || 0;
+
+ if ((g1 - g2) !== 0) {
+ return g1 - g2;
+ }
+
+ if ((order1 - order2) !== 0) {
+ return order1 - order2;
+ }
+
+ if (v1 > v2) {
+ return 1;
+ } else if (v1 < v2) {
+ return -1;
+ } else {
+ return 0;
+ }
+ };
+
+ Ext.apply(me, {
+ url: '/api2/json/' + baseurl,
+ selModel: me.selModel,
+ interval: 2000,
+ cwidth1: 170,
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ items: [
+ {
+ text: gettext('Mount Point'),
+ iconCls: 'pve-itype-icon-storage',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function() {
+ var win = Ext.create('PVE.lxc.MountPointEdit', {
+ url: '/api2/extjs/' + baseurl,
+ unprivileged: me.getObjectValue('unprivileged'),
+ pveSelNode: me.pveSelNode
+ });
+ win.show();
+ }
+ }
+ ]
+ })
+ },
+ edit_btn,
+ remove_btn,
+ resize_btn,
+ move_btn
+ ],
+ rows: rows,
+ sorterFn: sorterFn,
+ editorConfig: {
+ pveSelNode: me.pveSelNode,
+ url: '/api2/extjs/' + baseurl
+ },
+ listeners: {
+ itemdblclick: me.run_editor,
+ selectionchange: set_button_status
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+
+ Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') });
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.lxc.FeaturesInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveLxcFeaturesInputPanel',
+
+ // used to save the mounts fstypes until sending
+ mounts: [],
+
+ fstypes: ['nfs', 'cifs'],
+
+ viewModel: {
+ parent: null,
+ data: {
+ unprivileged: false
+ },
+ formulas: {
+ privilegedOnly: function(get) {
+ return (get('unprivileged') ? gettext('privileged only') : '');
+ },
+ unprivilegedOnly: function(get) {
+ return (!get('unprivileged') ? gettext('unprivileged only') : '');
+ }
+ }
+ },
+
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('keyctl'),
+ name: 'keyctl',
+ bind: {
+ disabled: '{!unprivileged}',
+ boxLabel: '{unprivilegedOnly}'
+ }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Nesting'),
+ name: 'nesting'
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'nfs',
+ fieldLabel: 'NFS',
+ bind: {
+ disabled: '{unprivileged}',
+ boxLabel: '{privilegedOnly}'
+ }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'cifs',
+ fieldLabel: 'CIFS',
+ bind: {
+ disabled: '{unprivileged}',
+ boxLabel: '{privilegedOnly}'
+ }
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'fuse',
+ fieldLabel: 'FUSE'
+ }
+ ],
+
+ onGetValues: function(values) {
+ var me = this;
+ var mounts = me.mounts;
+ me.fstypes.forEach(function(fs) {
+ if (values[fs]) {
+ mounts.push(fs);
+ }
+ delete values[fs];
+ });
+
+ if (mounts.length) {
+ values.mount = mounts.join(';');
+ }
+
+ var featuresstring = PVE.Parser.printPropertyString(values, undefined);
+ if (featuresstring == '') {
+ return { 'delete': 'features' };
+ }
+ return { features: featuresstring };
+ },
+
+ setValues: function(values) {
+ var me = this;
+
+ me.viewModel.set({ unprivileged: values.unprivileged });
+
+ if (values.features) {
+ var res = PVE.Parser.parsePropertyString(values.features);
+ me.mounts = [];
+ if (res.mount) {
+ res.mount.split(/[; ]/).forEach(function(item) {
+ if (me.fstypes.indexOf(item) === -1) {
+ me.mounts.push(item);
+ } else {
+ res[item] = 1;
+ }
+ });
+ }
+ this.callParent([res]);
+ }
+ }
+});
+
+Ext.define('PVE.lxc.FeaturesEdit', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveLxcFeaturesEdit',
+
+ subject: gettext('Features'),
+
+ items: [{
+ xtype: 'pveLxcFeaturesInputPanel'
+ }],
+
+ initComponent : function() {
+ var me = this;
+
+ me.callParent();
+
+ me.load();
+ }
+});
+/*jslint confusion: true */
+Ext.define('PVE.lxc.Options', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pveLxcOptions'],
+
+ onlineHelp: 'pct_options',
+
+ initComponent : function() {
+ var me = this;
+ var i;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var rows = {
+ onboot: {
+ header: gettext('Start at boot'),
+ defaultValue: '',
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Start at boot'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'onboot',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ fieldLabel: gettext('Start at boot')
+ }
+ } : undefined
+ },
+ startup: {
+ header: gettext('Start/Shutdown order'),
+ defaultValue: '',
+ renderer: PVE.Utils.render_kvm_startup,
+ editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ?
+ {
+ xtype: 'pveWindowStartupEdit',
+ onlineHelp: 'pct_startup_and_shutdown'
+ } : undefined
+ },
+ ostype: {
+ header: gettext('OS Type'),
+ defaultValue: Proxmox.Utils.unknownText
+ },
+ arch: {
+ header: gettext('Architecture'),
+ defaultValue: Proxmox.Utils.unknownText
+ },
+ console: {
+ header: '/dev/console',
+ defaultValue: 1,
+ renderer: Proxmox.Utils.format_enabled_toggle,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: '/dev/console',
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'console',
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ checked: true,
+ fieldLabel: '/dev/console'
+ }
+ } : undefined
+ },
+ tty: {
+ header: gettext('TTY count'),
+ defaultValue: 2,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('TTY count'),
+ items: {
+ xtype: 'proxmoxintegerfield',
+ name: 'tty',
+ minValue: 0,
+ maxValue: 6,
+ value: 2,
+ fieldLabel: gettext('TTY count'),
+ emptyText: gettext('Default'),
+ deleteEmpty: true
+ }
+ } : undefined
+ },
+ cmode: {
+ header: gettext('Console mode'),
+ defaultValue: 'tty',
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Console mode'),
+ items: {
+ xtype: 'proxmoxKVComboBox',
+ name: 'cmode',
+ deleteEmpty: true,
+ value: '__default__',
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + " (tty)"],
+ ['tty', "/dev/tty[X]"],
+ ['console', "/dev/console"],
+ ['shell', "shell"]
+ ],
+ fieldLabel: gettext('Console mode')
+ }
+ } : undefined
+ },
+ protection: {
+ header: gettext('Protection'),
+ defaultValue: false,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Protection'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'protection',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled')
+ }
+ } : undefined
+ },
+ unprivileged: {
+ header: gettext('Unprivileged container'),
+ renderer: Proxmox.Utils.format_boolean,
+ defaultValue: 0
+ },
+ features: {
+ header: gettext('Features'),
+ defaultValue: Proxmox.Utils.noneText,
+ editor: Proxmox.UserName === 'root@pam' ?
+ 'PVE.lxc.FeaturesEdit' : undefined
+ },
+ hookscript: {
+ header: gettext('Hookscript')
+ }
+ };
+
+ var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ var rowdef = rows[rec.data.key];
+ return !!rowdef.editor;
+ },
+ handler: function() { me.run_editor(); }
+ });
+
+ Ext.apply(me, {
+ url: "/api2/json/" + baseurl,
+ selModel: sm,
+ interval: 5000,
+ tbar: [ edit_btn ],
+ rows: rows,
+ editorConfig: {
+ url: '/api2/extjs/' + baseurl
+ },
+ listeners: {
+ itemdblclick: me.run_editor
+ }
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+
+ }
+});
+
+Ext.define('PVE.lxc.DNSInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveLxcDNSInputPanel',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var deletes = [];
+ if (!values.searchdomain && !me.insideWizard) {
+ deletes.push('searchdomain');
+ }
+
+ if (values.nameserver) {
+ var list = values.nameserver.split(/[\ \,\;]+/);
+ values.nameserver = list.join(' ');
+ } else if(!me.insideWizard) {
+ deletes.push('nameserver');
+ }
+
+ if (deletes.length) {
+ values['delete'] = deletes.join(',');
+ }
+
+ return values;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ var items = [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'searchdomain',
+ skipEmptyText: true,
+ fieldLabel: gettext('DNS domain'),
+ emptyText: gettext('use host settings'),
+ allowBlank: true
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('DNS servers'),
+ vtype: 'IP64AddressList',
+ allowBlank: true,
+ emptyText: gettext('use host settings'),
+ name: 'nameserver',
+ itemId: 'nameserver'
+ }
+ ];
+
+ if (me.insideWizard) {
+ me.column1 = items;
+ } else {
+ me.items = items;
+ }
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.lxc.DNSEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent : function() {
+ var me = this;
+
+ var ipanel = Ext.create('PVE.lxc.DNSInputPanel');
+
+ Ext.apply(me, {
+ subject: gettext('Resources'),
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+
+ if (values.nameserver) {
+ values.nameserver.replace(/[,;]/, ' ');
+ values.nameserver.replace(/^\s+/, '');
+ }
+
+ ipanel.setValues(values);
+ }
+ });
+ }
+ }
+});
+
+/*jslint confusion: true */
+Ext.define('PVE.lxc.DNS', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pveLxcDNS'],
+
+ onlineHelp: 'pct_container_network',
+
+ initComponent : function() {
+ var me = this;
+ var i;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var rows = {
+ hostname: {
+ required: true,
+ defaultValue: me.pveSelNode.data.name,
+ header: gettext('Hostname'),
+ editor: caps.vms['VM.Config.Network'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Hostname'),
+ items: {
+ xtype: 'inputpanel',
+ items:{
+ fieldLabel: gettext('Hostname'),
+ xtype: 'textfield',
+ name: 'hostname',
+ vtype: 'DnsName',
+ allowBlank: true,
+ emptyText: 'CT' + vmid.toString()
+ },
+ onGetValues: function(values) {
+ var params = values;
+ if (values.hostname === undefined ||
+ values.hostname === null ||
+ values.hostname === '') {
+ params = { hostname: 'CT'+vmid.toString()};
+ }
+ return params;
+ }
+ }
+ } : undefined
+ },
+ searchdomain: {
+ header: gettext('DNS domain'),
+ defaultValue: '',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
+ renderer: function(value) {
+ return value || gettext('use host settings');
+ }
+ },
+ nameserver: {
+ header: gettext('DNS server'),
+ defaultValue: '',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
+ renderer: function(value) {
+ return value || gettext('use host settings');
+ }
+ }
+ };
+
+ var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
+
+ var reload = function() {
+ me.rstore.load();
+ };
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var rowdef = rows[rec.data.key];
+ if (!rowdef.editor) {
+ return;
+ }
+
+ var win;
+ if (Ext.isString(rowdef.editor)) {
+ win = Ext.create(rowdef.editor, {
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs/' + baseurl
+ });
+ } else {
+ var config = Ext.apply({
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs/' + baseurl
+ }, rowdef.editor);
+ win = Ext.createWidget(rowdef.editor.xtype, config);
+ win.load();
+ }
+ //win.load();
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ var rowdef = rows[rec.data.key];
+ return !!rowdef.editor;
+ },
+ handler: run_editor
+ });
+
+ var set_button_status = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ return;
+ }
+ var rowdef = rows[rec.data.key];
+ edit_btn.setDisabled(!rowdef.editor);
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/config",
+ selModel: sm,
+ cwidth1: 150,
+ run_editor: run_editor,
+ tbar: [ edit_btn ],
+ rows: rows,
+ listeners: {
+ itemdblclick: run_editor,
+ selectionchange: set_button_status,
+ activate: reload
+ }
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.lxc.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.lxc.Config',
+
+ onlineHelp: 'chapter_pct',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var template = !!me.pveSelNode.data.template;
+
+ var running = !!me.pveSelNode.data.uptime;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var base_url = '/nodes/' + nodename + '/lxc/' + vmid;
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: '/api2/json' + base_url + '/status/current',
+ interval: 1000
+ });
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: base_url + "/status/" + cmd,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ };
+
+ var startBtn = Ext.create('Ext.Button', {
+ text: gettext('Start'),
+ disabled: !caps.vms['VM.PowerMgmt'] || running,
+ hidden: template,
+ handler: function() {
+ vm_command('start');
+ },
+ iconCls: 'fa fa-play'
+ });
+
+ var stopBtn = Ext.create('Ext.menu.Item',{
+ text: gettext('Stop'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ confirmMsg: Proxmox.Utils.format_task_description('vzstop', vmid),
+ tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
+ dangerous: true,
+ handler: function() {
+ vm_command("stop");
+ },
+ iconCls: 'fa fa-stop'
+ });
+
+ var shutdownBtn = Ext.create('PVE.button.Split', {
+ text: gettext('Shutdown'),
+ disabled: !caps.vms['VM.PowerMgmt'] || !running,
+ hidden: template,
+ confirmMsg: Proxmox.Utils.format_task_description('vzshutdown', vmid),
+ handler: function() {
+ vm_command('shutdown');
+ },
+ menu: {
+ items:[stopBtn]
+ },
+ iconCls: 'fa fa-power-off'
+ });
+
+ var migrateBtn = Ext.create('Ext.Button', {
+ text: gettext('Migrate'),
+ disabled: !caps.vms['VM.Migrate'],
+ hidden: PVE.data.ResourceStore.getNodes().length < 2,
+ handler: function() {
+ var win = Ext.create('PVE.window.Migrate', {
+ vmtype: 'lxc',
+ nodename: nodename,
+ vmid: vmid
+ });
+ win.show();
+ },
+ iconCls: 'fa fa-send-o'
+ });
+
+ var moreBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('More'),
+ menu: { items: [
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: caps.vms['VM.Clone'] ? false : true,
+ handler: function() {
+ PVE.window.Clone.wrap(nodename, vmid, template, 'lxc');
+ }
+ },
+ {
+ text: gettext('Convert to template'),
+ disabled: template,
+ xtype: 'pveMenuItem',
+ iconCls: 'fa fa-fw fa-file-o',
+ hidden: caps.vms['VM.Allocate'] ? false : true,
+ confirmMsg: Proxmox.Utils.format_task_description('vztemplate', vmid),
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: base_url + '/template',
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ }
+ });
+ }
+ },
+ {
+ iconCls: 'fa fa-heartbeat ',
+ hidden: !caps.nodes['Sys.Console'],
+ text: gettext('Manage HA'),
+ handler: function() {
+ var ha = me.pveSelNode.data.hastate;
+ Ext.create('PVE.ha.VMResourceEdit', {
+ vmid: vmid,
+ guestType: 'ct',
+ isCreate: (!ha || ha === 'unmanaged')
+ }).show();
+ }
+ },
+ {
+ text: gettext('Remove'),
+ disabled: !caps.vms['VM.Allocate'],
+ itemId: 'removeBtn',
+ handler: function() {
+ Ext.create('PVE.window.SafeDestroy', {
+ url: base_url,
+ item: { type: 'CT', id: vmid }
+ }).show();
+ },
+ iconCls: 'fa fa-trash-o'
+ }
+ ]}
+ });
+
+ var vm = me.pveSelNode.data;
+
+ var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
+ disabled: !caps.vms['VM.Console'],
+ consoleType: 'lxc',
+ consoleName: vm.name,
+ hidden: template,
+ nodename: nodename,
+ vmid: vmid
+ });
+
+ var statusTxt = Ext.create('Ext.toolbar.TextItem', {
+ data: {
+ lock: undefined
+ },
+ tpl: [
+ '
' + gettext("You can delete the image from the guest's hardware pane");
+
+ Ext.Msg.show({
+ title: gettext('Cannot remove disk image.'),
+ icon: Ext.Msg.ERROR,
+ msg: msg
+ });
+ return;
+ }
+ }
+ var win = Ext.create('PVE.window.SafeDestroy', {
+ title: Ext.String.format(gettext("Destroy '{0}'"), rec.data.volid),
+ showProgress: true,
+ url: url,
+ item: { type: 'Image', id: vmid }
+ }).show();
+ win.on('destroy', function() {
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status'
+ });
+ reload();
+
+ });
+ }
+ });
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status'
+ });
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: [
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Restore'),
+ selModel: sm,
+ disabled: true,
+ enableFn: function(rec) {
+ return rec && rec.data.content === 'backup';
+ },
+ handler: function(b, e, rec) {
+ var vmtype;
+ if (rec.data.volid.match(/vzdump-qemu-/)) {
+ vmtype = 'qemu';
+ } else if (rec.data.volid.match(/vzdump-openvz-/) || rec.data.volid.match(/vzdump-lxc-/)) {
+ vmtype = 'lxc';
+ } else {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.Restore', {
+ nodename: nodename,
+ volid: rec.data.volid,
+ volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
+ vmtype: vmtype
+ });
+ win.show();
+ win.on('destroy', reload);
+ }
+ },
+ removeButton,
+ imageRemoveButton,
+ templateButton,
+ uploadButton,
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Show Configuration'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ return rec && rec.data.content === 'backup';
+ },
+ handler: function(b,e,rec) {
+ var win = Ext.create('PVE.window.BackupConfig', {
+ volume: rec.data.volid,
+ pveSelNode: me.pveSelNode
+ });
+
+ win.show();
+ }
+ },
+ '->',
+ gettext('Search') + ':', ' ',
+ {
+ xtype: 'textfield',
+ width: 200,
+ enableKeyEvents: true,
+ listeners: {
+ buffer: 500,
+ keyup: function(field) {
+ store.clearFilter(true);
+ store.filter([
+ {
+ property: 'text',
+ value: field.getValue(),
+ anyMatch: true,
+ caseSensitive: false
+ }
+ ]);
+ }
+ }
+ }
+ ],
+ columns: [
+ {
+ header: gettext('Name'),
+ flex: 1,
+ sortable: true,
+ renderer: PVE.Utils.render_storage_content,
+ dataIndex: 'text'
+ },
+ {
+ header: gettext('Format'),
+ width: 100,
+ dataIndex: 'format'
+ },
+ {
+ header: gettext('Type'),
+ width: 100,
+ dataIndex: 'content',
+ renderer: PVE.Utils.format_content_types
+ },
+ {
+ header: gettext('Size'),
+ width: 100,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'size'
+ }
+ ],
+ listeners: {
+ activate: reload
+ }
+ });
+
+ me.callParent();
+
+ // disable the buttons/restrict the upload window
+ // if templates or uploads are not allowed
+ me.mon(me.statusStore, 'load', function(s, records, success) {
+ var availcontent = [];
+ Ext.Array.each(records, function(item){
+ if (item.id === 'content') {
+ availcontent = item.data.value.split(',');
+ }
+ });
+ var templ = false;
+ var upload = false;
+ var cts = [];
+
+ Ext.Array.each(availcontent, function(content) {
+ if (content === 'vztmpl') {
+ templ = true;
+ cts.push('vztmpl');
+ } else if (content === 'iso') {
+ upload = true;
+ cts.push('iso');
+ }
+ });
+
+ if (templ !== upload) {
+ uploadButton.contents = cts;
+ }
+
+ templateButton.setDisabled(!templ);
+ uploadButton.setDisabled(!upload && !templ);
+ });
+ }
+}, function() {
+
+ Ext.define('pve-storage-content', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'volid', 'content', 'format', 'size', 'used', 'vmid',
+ 'channel', 'id', 'lun',
+ {
+ name: 'text',
+ convert: function(value, record) {
+ // check for volid, because if you click on a grouping header,
+ // it calls convert (but with an empty volid)
+ if (value || record.data.volid === null) {
+ return value;
+ }
+ return PVE.Utils.render_storage_content(value, {}, record);
+ }
+ }
+ ],
+ idProperty: 'volid'
+ });
+
+});
+Ext.define('PVE.storage.StatusView', {
+ extend: 'PVE.panel.StatusView',
+ alias: 'widget.pveStorageStatusView',
+
+ height: 230,
+ title: gettext('Status'),
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+
+ defaults: {
+ xtype: 'pveInfoWidget',
+ padding: '0 30 5 30'
+ },
+ items: [
+ {
+ xtype: 'box',
+ height: 30
+ },
+ {
+ itemId: 'enabled',
+ title: gettext('Enabled'),
+ printBar: false,
+ textField: 'disabled',
+ renderer: Proxmox.Utils.format_neg_boolean
+ },
+ {
+ itemId: 'active',
+ title: gettext('Active'),
+ printBar: false,
+ textField: 'active',
+ renderer: Proxmox.Utils.format_boolean
+ },
+ {
+ itemId: 'content',
+ title: gettext('Content'),
+ printBar: false,
+ textField: 'content',
+ renderer: PVE.Utils.format_content_types
+ },
+ {
+ itemId: 'type',
+ title: gettext('Type'),
+ printBar: false,
+ textField: 'type',
+ renderer: PVE.Utils.format_storage_type
+ },
+ {
+ xtype: 'box',
+ height: 10
+ },
+ {
+ itemId: 'usage',
+ title: gettext('Usage'),
+ valueField: 'used',
+ maxField: 'total'
+ }
+ ],
+
+ updateTitle: function() {
+ return;
+ }
+});
+Ext.define('PVE.storage.Summary', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveStorageSummary',
+ scrollable: true,
+ bodyPadding: 5,
+ tbar: [
+ '->',
+ {
+ xtype: 'proxmoxRRDTypeSelector'
+ }
+ ],
+ layout: {
+ type: 'column'
+ },
+ defaults: {
+ padding: 5,
+ columnWidth: 1
+ },
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var storage = me.pveSelNode.data.storage;
+ if (!storage) {
+ throw "no storage ID specified";
+ }
+
+ var rstore = Ext.create('Proxmox.data.ObjectStore', {
+ url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status",
+ interval: 1000
+ });
+
+ var rrdstore = Ext.create('Proxmox.data.RRDStore', {
+ rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata",
+ model: 'pve-rrd-storage'
+ });
+
+ Ext.apply(me, {
+ items: [
+ {
+ xtype: 'pveStorageStatusView',
+ pveSelNode: me.pveSelNode,
+ rstore: rstore
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Usage'),
+ fields: ['total','used'],
+ fieldTitles: ['Total Size', 'Used Size'],
+ store: rrdstore
+ }
+ ],
+ listeners: {
+ activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); },
+ destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); }
+ }
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.Browser', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.storage.Browser',
+
+ onlineHelp: 'chapter_storage',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var storeid = me.pveSelNode.data.storage;
+ if (!storeid) {
+ throw "no storage ID specified";
+ }
+
+
+ me.items = [
+ {
+ title: gettext('Summary'),
+ xtype: 'pveStorageSummary',
+ iconCls: 'fa fa-book',
+ itemId: 'summary'
+ }
+ ];
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ Ext.apply(me, {
+ title: Ext.String.format(gettext("Storage {0} on node {1}"),
+ "'" + storeid + "'", "'" + nodename + "'"),
+ hstateid: 'storagetab'
+ });
+
+ if (caps.storage['Datastore.Allocate'] ||
+ caps.storage['Datastore.AllocateSpace'] ||
+ caps.storage['Datastore.Audit']) {
+ me.items.push({
+ xtype: 'pveStorageContentView',
+ title: gettext('Content'),
+ iconCls: 'fa fa-th',
+ itemId: 'content'
+ });
+ }
+
+ if (caps.storage['Permissions.Modify']) {
+ me.items.push({
+ xtype: 'pveACLView',
+ title: gettext('Permissions'),
+ iconCls: 'fa fa-unlock',
+ itemId: 'permissions',
+ path: '/storage/' + storeid
+ });
+ }
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.DirInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_directory',
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'path',
+ value: '',
+ fieldLabel: gettext('Directory'),
+ allowBlank: false
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ name: 'content',
+ value: 'images',
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'shared',
+ uncheckedValue: 0,
+ fieldLabel: gettext('Shared')
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Max Backups'),
+ disabled: true,
+ name: 'maxfiles',
+ reference: 'maxfiles',
+ minValue: 0,
+ maxValue: 365,
+ value: me.isCreate ? '1' : undefined,
+ allowBlank: false
+ }
+ ];
+
+ me.callParent();
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.storage.NFSScan', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveNFSScan',
+
+ queryParam: 'server',
+
+ valueField: 'path',
+ displayField: 'path',
+ matchFieldWidth: false,
+ listConfig: {
+ loadingText: gettext('Scanning...'),
+ width: 350
+ },
+ doRawQuery: function() {
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.nfsServer) {
+ me.store.removeAll();
+ }
+
+ me.allQuery = me.nfsServer;
+
+ me.callParent();
+ },
+
+ setServer: function(server) {
+ var me = this;
+
+ me.nfsServer = server;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: [ 'path', 'options' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/nfs'
+ }
+ });
+
+ store.sort('path', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.NFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_nfs',
+
+ options : [],
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var i;
+ var res = [];
+ for (i = 0; i < me.options.length; i++) {
+ var item = me.options[i];
+ if (!item.match(/^vers=(.*)$/)) {
+ res.push(item);
+ }
+ }
+ if (values.nfsversion && values.nfsversion !== '__default__') {
+ res.push('vers=' + values.nfsversion);
+ }
+ delete values.nfsversion;
+ values.options = res.join(',');
+ if (values.options === '') {
+ delete values.options;
+ if (!me.isCreate) {
+ values["delete"] = "options";
+ }
+ }
+
+ return me.callParent([values]);
+ },
+
+ setValues: function(values) {
+ var me = this;
+ if (values.options) {
+ var res = values.options;
+ me.options = values.options.split(',');
+ me.options.forEach(function(item) {
+ var match = item.match(/^vers=(.*)$/);
+ if (match) {
+ values.nfsversion = match[1];
+ }
+ });
+ }
+ return me.callParent([values]);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'server',
+ value: '',
+ fieldLabel: gettext('Server'),
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ var exportField = me.down('field[name=export]');
+ exportField.setServer(value);
+ exportField.setValue('');
+ }
+ }
+ }
+ },
+ {
+ xtype: me.isCreate ? 'pveNFSScan' : 'displayfield',
+ name: 'export',
+ value: '',
+ fieldLabel: 'Export',
+ allowBlank: false
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ name: 'content',
+ value: 'images',
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Max Backups'),
+ disabled: true,
+ name: 'maxfiles',
+ reference: 'maxfiles',
+ minValue: 0,
+ maxValue: 365,
+ value: me.isCreate ? '1' : undefined,
+ allowBlank: false
+ }
+ ];
+
+ me.advancedColumn1 = [
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('NFS Version'),
+ name: 'nfsversion',
+ value: '__default__',
+ deleteEmpty: false,
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText],
+ ['3', '3'],
+ ['4', '4'],
+ ['4.1', '4.1'],
+ ['4.2', '4.2']
+ ]
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.CIFSScan', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveCIFSScan',
+
+ queryParam: 'server',
+
+ valueField: 'share',
+ displayField: 'share',
+ matchFieldWidth: false,
+ listConfig: {
+ loadingText: gettext('Scanning...'),
+ width: 350
+ },
+ doRawQuery: function() {
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.cifsServer) {
+ me.store.removeAll();
+ }
+
+ var params = {};
+ if (me.cifsUsername && me.cifsPassword) {
+ params.username = me.cifsUsername;
+ params.password = me.cifsPassword;
+ }
+
+ if (me.cifsDomain) {
+ params.domain = me.cifsDomain;
+ }
+
+ me.store.getProxy().setExtraParams(params);
+ me.allQuery = me.cifsServer;
+
+ me.callParent();
+ },
+
+ setServer: function(server) {
+ this.cifsServer = server;
+ },
+
+ setUsername: function(username) {
+ this.cifsUsername = username;
+ },
+
+ setPassword: function(password) {
+ this.cifsPassword = password;
+ },
+
+ setDomain: function(domain) {
+ this.cifsDomain = domain;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: ['description', 'share'],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/cifs'
+ }
+ });
+ store.sort('share', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.CIFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_cifs',
+
+ initComponent : function() {
+ var me = this;
+
+ var passwordfield = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
+ inputType: 'password',
+ name: 'password',
+ value: me.isCreate ? '' : '********',
+ fieldLabel: gettext('Password'),
+ allowBlank: false,
+ disabled: me.isCreate,
+ minLength: 1,
+ listeners: {
+ change: function(f, value) {
+
+ if (me.isCreate) {
+ var exportField = me.down('field[name=share]');
+ exportField.setPassword(value);
+ }
+ }
+ }
+ });
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'server',
+ value: '',
+ fieldLabel: gettext('Server'),
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ var exportField = me.down('field[name=share]');
+ exportField.setServer(value);
+ }
+ }
+ }
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'username',
+ value: '',
+ fieldLabel: gettext('Username'),
+ emptyText: gettext('Guest user'),
+ allowBlank: true,
+ listeners: {
+ change: function(f, value) {
+ if (!me.isCreate) {
+ return;
+ }
+ var exportField = me.down('field[name=share]');
+ exportField.setUsername(value);
+
+ if (value == "") {
+ passwordfield.disable();
+ } else {
+ passwordfield.enable();
+ }
+ passwordfield.validate();
+ }
+ }
+ },
+ passwordfield,
+ {
+ xtype: me.isCreate ? 'pveCIFSScan' : 'displayfield',
+ name: 'share',
+ value: '',
+ fieldLabel: 'Share',
+ allowBlank: false
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Max Backups'),
+ name: 'maxfiles',
+ reference: 'maxfiles',
+ minValue: 0,
+ maxValue: 365,
+ value: me.isCreate ? '1' : undefined,
+ allowBlank: false
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ name: 'content',
+ value: 'images',
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'domain',
+ value: me.isCreate ? '' : undefined,
+ fieldLabel: gettext('Domain'),
+ allowBlank: true,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+
+ var exportField = me.down('field[name=share]');
+ exportField.setDomain(value);
+ }
+ }
+ }
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.GlusterFsScan', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveGlusterFsScan',
+
+ queryParam: 'server',
+
+ valueField: 'volname',
+ displayField: 'volname',
+ matchFieldWidth: false,
+ listConfig: {
+ loadingText: 'Scanning...',
+ width: 350
+ },
+ doRawQuery: function() {
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.glusterServer) {
+ me.store.removeAll();
+ }
+
+ me.allQuery = me.glusterServer;
+
+ me.callParent();
+ },
+
+ setServer: function(server) {
+ var me = this;
+
+ me.glusterServer = server;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: [ 'volname' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/glusterfs'
+ }
+ });
+
+ store.sort('volname', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.GlusterFsInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_glusterfs',
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'server',
+ value: '',
+ fieldLabel: gettext('Server'),
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ var volumeField = me.down('field[name=volume]');
+ volumeField.setServer(value);
+ volumeField.setValue('');
+ }
+ }
+ }
+ },
+ {
+ xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
+ name: 'server2',
+ value: '',
+ fieldLabel: gettext('Second Server'),
+ allowBlank: true
+ },
+ {
+ xtype: me.isCreate ? 'pveGlusterFsScan' : 'displayfield',
+ name: 'volume',
+ value: '',
+ fieldLabel: 'Volume name',
+ allowBlank: false
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'iso', 'backup', 'vztmpl', 'snippets'],
+ name: 'content',
+ value: 'images',
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Max Backups'),
+ disabled: true,
+ name: 'maxfiles',
+ reference: 'maxfiles',
+ minValue: 0,
+ maxValue: 365,
+ value: me.isCreate ? '1' : undefined,
+ allowBlank: false
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.IScsiScan', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveIScsiScan',
+
+ queryParam: 'portal',
+ valueField: 'target',
+ displayField: 'target',
+ matchFieldWidth: false,
+ listConfig: {
+ loadingText: gettext('Scanning...'),
+ width: 350
+ },
+ doRawQuery: function() {
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.portal) {
+ me.store.removeAll();
+ }
+
+ me.allQuery = me.portal;
+
+ me.callParent();
+ },
+
+ setPortal: function(portal) {
+ var me = this;
+
+ me.portal = portal;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: [ 'target', 'portal' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/iscsi'
+ }
+ });
+
+ store.sort('target', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.IScsiInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_open_iscsi',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ values.content = values.luns ? 'images' : 'none';
+ delete values.luns;
+
+ return me.callParent([values]);
+ },
+
+ setValues: function(values) {
+ values.luns = (values.content.indexOf('images') !== -1) ? true : false;
+ this.callParent([values]);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'portal',
+ value: '',
+ fieldLabel: 'Portal',
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ var exportField = me.down('field[name=target]');
+ exportField.setPortal(value);
+ exportField.setValue('');
+ }
+ }
+ }
+ },
+ {
+ readOnly: !me.isCreate,
+ xtype: me.isCreate ? 'pveIScsiScan' : 'displayfield',
+ name: 'target',
+ value: '',
+ fieldLabel: 'Target',
+ allowBlank: false
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'checkbox',
+ name: 'luns',
+ checked: true,
+ fieldLabel: gettext('Use LUNs directly')
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.VgSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveVgSelector',
+ valueField: 'vg',
+ displayField: 'vg',
+ queryMode: 'local',
+ editable: false,
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: {}, // true,
+ fields: [ 'vg', 'size', 'free' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/lvm'
+ }
+ });
+
+ store.sort('vg', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ loadingText: gettext('Scanning...')
+ }
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.BaseStorageSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveBaseStorageSelector',
+
+ existingGroupsText: gettext("Existing volume groups"),
+ queryMode: 'local',
+ editable: false,
+ value: '',
+ valueField: 'storage',
+ displayField: 'text',
+ initComponent : function() {
+ var me = this;
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: {
+ addRecords: true,
+ params: {
+ type: 'iscsi'
+ }
+ },
+ fields: [ 'storage', 'type', 'content',
+ {
+ name: 'text',
+ convert: function(value, record) {
+ if (record.data.storage) {
+ return record.data.storage + " (iSCSI)";
+ } else {
+ return me.existingGroupsText;
+ }
+ }
+ }],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/storage/'
+ }
+ });
+
+ store.loadData([{ storage: '' }], true);
+
+ store.sort('storage', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.LVMInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_lvm',
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [];
+
+ var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
+ name: 'vgname',
+ hidden: !!me.isCreate,
+ disabled: !!me.isCreate,
+ value: '',
+ fieldLabel: gettext('Volume group'),
+ allowBlank: false
+ });
+
+ if (me.isCreate) {
+ var vgField = Ext.create('PVE.storage.VgSelector', {
+ name: 'vgname',
+ fieldLabel: gettext('Volume group'),
+ allowBlank: false
+ });
+
+ var baseField = Ext.createWidget('pveFileSelector', {
+ name: 'base',
+ hidden: true,
+ disabled: true,
+ nodename: 'localhost',
+ storageContent: 'images',
+ fieldLabel: gettext('Base volume'),
+ allowBlank: false
+ });
+
+ me.column1.push({
+ xtype: 'pveBaseStorageSelector',
+ name: 'basesel',
+ fieldLabel: gettext('Base storage'),
+ submitValue: false,
+ listeners: {
+ change: function(f, value) {
+ if (value) {
+ vgnameField.setVisible(true);
+ vgnameField.setDisabled(false);
+ vgField.setVisible(false);
+ vgField.setDisabled(true);
+ baseField.setVisible(true);
+ baseField.setDisabled(false);
+ } else {
+ vgnameField.setVisible(false);
+ vgnameField.setDisabled(true);
+ vgField.setVisible(true);
+ vgField.setDisabled(false);
+ baseField.setVisible(false);
+ baseField.setDisabled(true);
+ }
+ baseField.setStorage(value);
+ }
+ }
+ });
+
+ me.column1.push(baseField);
+
+ me.column1.push(vgField);
+ }
+
+ me.column1.push(vgnameField);
+
+ // here value is an array,
+ // while before it was a string
+ /*jslint confusion: true*/
+ me.column1.push({
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images', 'rootdir'],
+ multiSelect: true,
+ allowBlank: false
+ });
+ /*jslint confusion: false*/
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'shared',
+ uncheckedValue: 0,
+ fieldLabel: gettext('Shared')
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.TPoolSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveTPSelector',
+
+ queryParam: 'vg',
+ valueField: 'lv',
+ displayField: 'lv',
+ editable: false,
+
+ doRawQuery: function() {
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.vg) {
+ me.store.removeAll();
+ }
+
+ me.allQuery = me.vg;
+
+ me.callParent();
+ },
+
+ setVG: function(myvg) {
+ var me = this;
+
+ me.vg = myvg;
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: [ 'lv' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/lvmthin'
+ }
+ });
+
+ store.sort('lv', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ loadingText: gettext('Scanning...')
+ }
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.BaseVGSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveBaseVGSelector',
+
+ valueField: 'vg',
+ displayField: 'vg',
+ queryMode: 'local',
+ editable: false,
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: {},
+ fields: [ 'vg', 'size', 'free'],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/lvm'
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ loadingText: gettext('Scanning...')
+ }
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.LvmThinInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_lvmthin',
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [];
+
+ var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
+ name: 'vgname',
+ hidden: !!me.isCreate,
+ disabled: !!me.isCreate,
+ value: '',
+ fieldLabel: gettext('Volume group'),
+ allowBlank: false
+ });
+
+ var thinpoolField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
+ name: 'thinpool',
+ hidden: !!me.isCreate,
+ disabled: !!me.isCreate,
+ value: '',
+ fieldLabel: gettext('Thin Pool'),
+ allowBlank: false
+ });
+
+ if (me.isCreate) {
+ var vgField = Ext.create('PVE.storage.TPoolSelector', {
+ name: 'thinpool',
+ fieldLabel: gettext('Thin Pool'),
+ allowBlank: false
+ });
+
+ me.column1.push({
+ xtype: 'pveBaseVGSelector',
+ name: 'vgname',
+ fieldLabel: gettext('Volume group'),
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ vgField.setVG(value);
+ vgField.setValue('');
+ }
+ }
+ }
+ });
+
+ me.column1.push(vgField);
+ }
+
+ me.column1.push(vgnameField);
+
+ me.column1.push(thinpoolField);
+
+ // here value is an array,
+ // while before it was a string
+ /*jslint confusion: true*/
+ me.column1.push({
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images', 'rootdir'],
+ multiSelect: true,
+ allowBlank: false
+ });
+ /*jslint confusion: false*/
+
+ me.column2 = [];
+
+ me.callParent();
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.storage.CephFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+ controller: 'cephstorage',
+
+ onlineHelp: 'storage_cephfs',
+
+ viewModel: {
+ type: 'cephstorage'
+ },
+
+ setValues: function(values) {
+ if (values.monhost) {
+ this.viewModel.set('pveceph', false);
+ this.lookupReference('pvecephRef').setValue(false);
+ this.lookupReference('pvecephRef').resetOriginalValue();
+ }
+ this.callParent([values]);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+ me.type = 'cephfs';
+
+ me.column1 = [];
+
+ me.column1.push(
+ {
+ xtype: 'textfield',
+ name: 'monhost',
+ vtype: 'HostList',
+ value: '',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ hidden: '{pveceph}'
+ },
+ fieldLabel: 'Monitor(s)',
+ allowBlank: false
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'monhost',
+ bind: {
+ disabled: '{!pveceph}',
+ hidden: '{!pveceph}'
+ },
+ value: '',
+ fieldLabel: 'Monitor(s)'
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'username',
+ value: 'admin',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}'
+ },
+ fieldLabel: gettext('User name'),
+ allowBlank: true
+ }
+ );
+
+ me.column2 = [
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['backup', 'iso', 'vztmpl', 'snippets'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: 'backup',
+ multiSelect: true,
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Max Backups'),
+ name: 'maxfiles',
+ reference: 'maxfiles',
+ minValue: 0,
+ maxValue: 365,
+ value: me.isCreate ? '1' : undefined,
+ allowBlank: false
+ }
+ ];
+
+ me.columnB = [{
+ xtype: 'proxmoxcheckbox',
+ name: 'pveceph',
+ reference: 'pvecephRef',
+ bind : {
+ disabled: '{!pvecephPossible}',
+ value: '{pveceph}'
+ },
+ checked: true,
+ uncheckedValue: 0,
+ submitValue: false,
+ hidden: !me.isCreate,
+ boxLabel: gettext('Use Proxmox VE managed hyper-converged cephFS')
+ }];
+
+ me.callParent();
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.storage.Ceph.Model', {
+ extend: 'Ext.app.ViewModel',
+ alias: 'viewmodel.cephstorage',
+
+ data: {
+ pveceph: true,
+ pvecephPossible: true
+ }
+});
+
+Ext.define('PVE.storage.Ceph.Controller', {
+ extend: 'PVE.controller.StorageEdit',
+ alias: 'controller.cephstorage',
+
+ control: {
+ '#': {
+ afterrender: 'queryMonitors'
+ },
+ 'textfield[name=username]': {
+ disable: 'resetField'
+ },
+ 'displayfield[name=monhost]': {
+ enable: 'queryMonitors'
+ },
+ 'textfield[name=monhost]': {
+ disable: 'resetField',
+ enable: 'resetField'
+ }
+ },
+ resetField: function(field) {
+ field.reset();
+ },
+ queryMonitors: function(field, newVal, oldVal) {
+ // we get called with two signatures, the above one for a field
+ // change event and the afterrender from the view, this check only
+ // can be true for the field change one and omit the API request if
+ // pveceph got unchecked - as it's not needed there.
+ if (field && !newVal && oldVal) {
+ return;
+ }
+ var view = this.getView();
+ var vm = this.getViewModel();
+ if (!(view.isCreate || vm.get('pveceph'))) {
+ return; // only query on create or if editing a pveceph store
+ }
+
+ var monhostField = this.lookupReference('monhost');
+
+ Proxmox.Utils.API2Request({
+ url: '/api2/json/nodes/localhost/ceph/mon',
+ method: 'GET',
+ scope: this,
+ callback: function(options, success, response) {
+ var data = response.result.data;
+ if (response.status === 200) {
+ if (data.length > 0) {
+ var monhost = Ext.Array.pluck(data, 'name').sort().join(',');
+ monhostField.setValue(monhost);
+ monhostField.resetOriginalValue();
+ if (view.isCreate) {
+ vm.set('pvecephPossible', true);
+ }
+ } else {
+ vm.set('pveceph', false);
+ }
+ } else {
+ vm.set('pveceph', false);
+ vm.set('pvecephPossible', false);
+ }
+ }
+ });
+ }
+});
+
+Ext.define('PVE.storage.RBDInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+ controller: 'cephstorage',
+
+ onlineHelp: 'ceph_rados_block_devices',
+
+ viewModel: {
+ type: 'cephstorage'
+ },
+
+ setValues: function(values) {
+ if (values.monhost) {
+ this.viewModel.set('pveceph', false);
+ this.lookupReference('pvecephRef').setValue(false);
+ this.lookupReference('pvecephRef').resetOriginalValue();
+ }
+ this.callParent([values]);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+ me.type = 'rbd';
+
+ me.column1 = [];
+
+ if (me.isCreate) {
+ me.column1.push({
+ xtype: 'pveCephPoolSelector',
+ nodename: me.nodename,
+ name: 'pool',
+ bind: {
+ disabled: '{!pveceph}',
+ submitValue: '{pveceph}',
+ hidden: '{!pveceph}'
+ },
+ fieldLabel: gettext('Pool'),
+ allowBlank: false
+ },{
+ xtype: 'textfield',
+ name: 'pool',
+ value: 'rbd',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ hidden: '{pveceph}'
+ },
+ fieldLabel: gettext('Pool'),
+ allowBlank: false
+ });
+ } else {
+ me.column1.push({
+ xtype: 'displayfield',
+ nodename: me.nodename,
+ name: 'pool',
+ fieldLabel: gettext('Pool'),
+ allowBlank: false
+ });
+ }
+
+ me.column1.push(
+ {
+ xtype: 'textfield',
+ name: 'monhost',
+ vtype: 'HostList',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ hidden: '{pveceph}'
+ },
+ value: '',
+ fieldLabel: 'Monitor(s)',
+ allowBlank: false
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'monhost',
+ bind: {
+ disabled: '{!pveceph}',
+ hidden: '{!pveceph}'
+ },
+ value: '',
+ fieldLabel: 'Monitor(s)'
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'username',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}'
+ },
+ value: 'admin',
+ fieldLabel: gettext('User name'),
+ allowBlank: true
+ }
+ );
+
+ me.column2 = [
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images'],
+ multiSelect: true,
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'krbd',
+ uncheckedValue: 0,
+ fieldLabel: 'KRBD'
+ }
+ ];
+
+ me.columnB = [{
+ xtype: 'proxmoxcheckbox',
+ name: 'pveceph',
+ reference: 'pvecephRef',
+ bind : {
+ disabled: '{!pvecephPossible}',
+ value: '{pveceph}'
+ },
+ checked: true,
+ uncheckedValue: 0,
+ submitValue: false,
+ hidden: !me.isCreate,
+ boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool')
+ }];
+
+ me.callParent();
+ }
+});
+/*jslint confusion: true*/
+Ext.define('PVE.storage.ZFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ viewModel: {
+ parent: null,
+ data: {
+ isLIO: false,
+ isComstar: true,
+ hasWriteCacheOption: true
+ }
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'field[name=iscsiprovider]': {
+ change: 'changeISCSIProvider'
+ }
+ },
+ changeISCSIProvider: function(f, newVal, oldVal) {
+ var vm = this.getViewModel();
+ vm.set('isLIO', newVal === 'LIO');
+ vm.set('isComstar', newVal === 'comstar');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ }
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.content = 'images';
+ }
+
+ values.nowritecache = values.writecache ? 0 : 1;
+ delete values.writecache;
+
+ return me.callParent([values]);
+ },
+
+ setValues: function diff(values) {
+ values.writecache = values.nowritecache ? 0 : 1;
+ this.callParent([values]);
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'portal',
+ value: '',
+ fieldLabel: gettext('Portal'),
+ allowBlank: false
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'pool',
+ value: '',
+ fieldLabel: gettext('Pool'),
+ allowBlank: false
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'blocksize',
+ value: '4k',
+ fieldLabel: gettext('Block Size'),
+ allowBlank: false
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'target',
+ value: '',
+ fieldLabel: gettext('Target'),
+ allowBlank: false
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'comstar_tg',
+ value: '',
+ fieldLabel: gettext('Target group'),
+ bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ allowBlank: true
+ }
+ ];
+
+ me.column2 = [
+ {
+ xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield',
+ name: 'iscsiprovider',
+ value: 'comstar',
+ fieldLabel: gettext('iSCSI Provider'),
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'sparse',
+ checked: false,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Thin provision')
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'writecache',
+ checked: true,
+ bind: me.isCreate ? { disabled: '{!hasWriteCacheOption}' } : { hidden: '{!hasWriteCacheOption}' },
+ uncheckedValue: 0,
+ fieldLabel: gettext('Write cache')
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'comstar_hg',
+ value: '',
+ bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ fieldLabel: gettext('Host group'),
+ allowBlank: true
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'lio_tpg',
+ value: '',
+ bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
+ allowBlank: false,
+ fieldLabel: gettext('Target portal group')
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.storage.ZFSPoolSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveZFSPoolSelector',
+ valueField: 'pool',
+ displayField: 'pool',
+ queryMode: 'local',
+ editable: false,
+ listConfig: {
+ loadingText: gettext('Scanning...')
+ },
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: {}, // true,
+ fields: [ 'pool', 'size', 'free' ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/zfs'
+ }
+ });
+
+ store.sort('pool', 'ASC');
+
+ Ext.apply(me, {
+ store: store
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.storage.ZFSPoolInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_zfspool',
+
+ initComponent : function() {
+ var me = this;
+
+ me.column1 = [];
+
+ if (me.isCreate) {
+ me.column1.push(Ext.create('PVE.storage.ZFSPoolSelector', {
+ name: 'pool',
+ fieldLabel: gettext('ZFS Pool'),
+ allowBlank: false
+ }));
+ } else {
+ me.column1.push(Ext.createWidget('displayfield', {
+ name: 'pool',
+ value: '',
+ fieldLabel: gettext('ZFS Pool'),
+ allowBlank: false
+ }));
+ }
+
+ // value is an array,
+ // while before it was a string
+ /*jslint confusion: true*/
+ me.column1.push(
+ {xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images', 'rootdir'],
+ multiSelect: true,
+ allowBlank: false
+ });
+ /*jslint confusion: false*/
+ me.column2 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'sparse',
+ checked: false,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Thin provision')
+ },
+ {
+ xtype: 'textfield',
+ name: 'blocksize',
+ emptyText: '8k',
+ fieldLabel: gettext('Block Size'),
+ allowBlank: true
+ }
+ ];
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.ha.StatusView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveHAStatusView'],
+
+ onlineHelp: 'chapter_ha_manager',
+
+ sortPriority: {
+ quorum: 1,
+ master: 2,
+ lrm: 3,
+ service: 4
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.rstore) {
+ throw "no rstore given";
+ }
+
+ Proxmox.Utils.monStoreErrors(me, me.rstore);
+
+ var store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: me.rstore,
+ sortAfterUpdate: true,
+ sorters: [{
+ sorterFn: function(rec1, rec2) {
+ var p1 = me.sortPriority[rec1.data.type];
+ var p2 = me.sortPriority[rec2.data.type];
+ return (p1 !== p2) ? ((p1 > p2) ? 1 : -1) : 0;
+ }
+ }],
+ filters: {
+ property: 'type',
+ value: 'service',
+ operator: '!='
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ stateful: false,
+ viewConfig: {
+ trackOver: false
+ },
+ columns: [
+ {
+ header: gettext('Type'),
+ width: 80,
+ dataIndex: 'type'
+ },
+ {
+ header: gettext('Status'),
+ width: 80,
+ flex: 1,
+ dataIndex: 'status'
+ }
+ ]
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+
+ }
+}, function() {
+
+ Ext.define('pve-ha-status', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'id', 'type', 'node', 'status', 'sid',
+ 'state', 'group', 'comment',
+ 'max_restart', 'max_relocate', 'type',
+ 'crm_state', 'request_state'
+ ],
+ idProperty: 'id'
+ });
+
+});
+Ext.define('PVE.ha.Status', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveHAStatus',
+
+ onlineHelp: 'chapter_ha_manager',
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.rstore = Ext.create('Proxmox.data.ObjectStore', {
+ interval: me.interval,
+ model: 'pve-ha-status',
+ storeid: 'pve-store-' + (++Ext.idSeed),
+ groupField: 'type',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/ha/status/current'
+ }
+ });
+
+ me.items = [{
+ xtype: 'pveHAStatusView',
+ title: gettext('Status'),
+ rstore: me.rstore,
+ border: 0,
+ collapsible: true,
+ padding: '0 0 20 0'
+ },{
+ xtype: 'pveHAResourcesView',
+ flex: 1,
+ collapsible: true,
+ title: gettext('Resources'),
+ border: 0,
+ rstore: me.rstore
+ }];
+
+ me.callParent();
+ me.on('activate', me.rstore.startUpdate);
+ }
+});
+Ext.define('PVE.ha.GroupSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveHAGroupSelector'],
+
+ value: [],
+ autoSelect: false,
+ valueField: 'group',
+ displayField: 'group',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Group'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'group'
+ },
+ {
+ header: gettext('Nodes'),
+ width: 100,
+ sortable: false,
+ dataIndex: 'nodes'
+ },
+ {
+ header: gettext('Comment'),
+ flex: 1,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode
+ }
+ ]
+ },
+ store: {
+ model: 'pve-ha-groups',
+ sorters: {
+ property: 'group',
+ order: 'DESC'
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+ me.callParent();
+ me.getStore().load();
+ }
+
+}, function() {
+
+ Ext.define('pve-ha-groups', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'group', 'type', 'digest', 'nodes', 'comment',
+ {
+ name : 'restricted',
+ type: 'boolean'
+ },
+ {
+ name : 'nofailback',
+ type: 'boolean'
+ }
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/ha/groups"
+ },
+ idProperty: 'group'
+ });
+});
+Ext.define('PVE.ha.VMResourceInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ onlineHelp: 'ha_manager_resource_config',
+ vmid: undefined,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (values.vmid) {
+ values.sid = values.vmid;
+ }
+ delete values.vmid;
+
+ PVE.Utils.delete_if_default(values, 'group', '', me.isCreate);
+ PVE.Utils.delete_if_default(values, 'max_restart', '1', me.isCreate);
+ PVE.Utils.delete_if_default(values, 'max_relocate', '1', me.isCreate);
+
+ return values;
+ },
+
+ initComponent : function() {
+ var me = this;
+ var MIN_QUORUM_VOTES = 3;
+
+ var disabledHint = Ext.createWidget({
+ xtype: 'displayfield', // won't get submitted by default
+ userCls: 'pve-hint',
+ value: 'Disabling the resource will stop the guest system. ' +
+ 'See the online help for details.',
+ hidden: true
+ });
+
+ var fewVotesHint = Ext.createWidget({
+ itemId: 'fewVotesHint',
+ xtype: 'displayfield',
+ userCls: 'pve-hint',
+ value: 'At least three quorum votes are recommended for reliable HA.',
+ hidden: true
+ });
+
+ Proxmox.Utils.API2Request({
+ url: '/cluster/config/nodes',
+ method: 'GET',
+ failure: function(response) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response) {
+ var nodes = response.result.data;
+ var votes = 0;
+ Ext.Array.forEach(nodes, function(node) {
+ var vote = parseInt(node.quorum_votes, 10); // parse as base 10
+ votes += vote || 0; // parseInt might return NaN, which is false
+ });
+
+ if (votes < MIN_QUORUM_VOTES) {
+ fewVotesHint.setVisible(true);
+ }
+ }
+ });
+
+ /*jslint confusion: true */
+ var vmidStore = (me.vmid) ? {} : {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ filters: [
+ {
+ property: 'type',
+ value: /lxc|qemu/
+ },
+ {
+ property: 'hastate',
+ value: /unmanaged/
+ }
+ ]
+ };
+
+ // value is a string above, but a number below
+ me.column1 = [
+ {
+ xtype: me.vmid ? 'displayfield' : 'vmComboSelector',
+ submitValue: me.isCreate,
+ name: 'vmid',
+ fieldLabel: (me.vmid && me.guestType === 'ct') ? 'CT' : 'VM',
+ value: me.vmid,
+ store: vmidStore,
+ validateExists: true
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'max_restart',
+ fieldLabel: gettext('Max. Restart'),
+ value: 1,
+ minValue: 0,
+ maxValue: 10,
+ allowBlank: false
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'max_relocate',
+ fieldLabel: gettext('Max. Relocate'),
+ value: 1,
+ minValue: 0,
+ maxValue: 10,
+ allowBlank: false
+ }
+ ];
+ /*jslint confusion: false */
+
+ me.column2 = [
+ {
+ xtype: 'pveHAGroupSelector',
+ name: 'group',
+ fieldLabel: gettext('Group')
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'state',
+ value: 'started',
+ fieldLabel: gettext('Request State'),
+ comboItems: [
+ ['started', 'started'],
+ ['stopped', 'stopped'],
+ ['ignored', 'ignored'],
+ ['disabled', 'disabled']
+ ],
+ listeners: {
+ 'change': function(field, newValue) {
+ if (newValue === 'disabled') {
+ disabledHint.setVisible(true);
+ }
+ else {
+ if (disabledHint.isVisible()) {
+ disabledHint.setVisible(false);
+ }
+ }
+ }
+ }
+ },
+ disabledHint
+ ];
+
+ me.columnB = [
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ fieldLabel: gettext('Comment')
+ },
+ fewVotesHint
+ ];
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.ha.VMResourceEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ vmid: undefined,
+ guestType: undefined,
+ isCreate: undefined,
+
+ initComponent : function() {
+ var me = this;
+
+ if (me.isCreate === undefined) {
+ me.isCreate = !me.vmid;
+ }
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs/cluster/ha/resources';
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs/cluster/ha/resources/' + me.vmid;
+ me.method = 'PUT';
+ }
+
+ var ipanel = Ext.create('PVE.ha.VMResourceInputPanel', {
+ isCreate: me.isCreate,
+ vmid: me.vmid,
+ guestType: me.guestType
+ });
+
+ Ext.apply(me, {
+ subject: gettext('Resource') + ': ' + gettext('Container') +
+ '/' + gettext('Virtual Machine'),
+ isAdd: true,
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+
+ var regex = /^(\S+):(\S+)$/;
+ var res = regex.exec(values.sid);
+
+ if (res[1] !== 'vm' && res[1] !== 'ct') {
+ throw "got unexpected resource type";
+ }
+
+ values.vmid = res[2];
+
+ ipanel.setValues(values);
+ }
+ });
+ }
+ }
+});
+Ext.define('PVE.ha.ResourcesView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveHAResourcesView'],
+
+ onlineHelp: 'ha_manager_resources',
+
+ stateful: true,
+ stateId: 'grid-ha-resources',
+
+ initComponent : function() {
+ var me = this;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ if (!me.rstore) {
+ throw "no store given";
+ }
+
+ Proxmox.Utils.monStoreErrors(me, me.rstore);
+
+ var store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: me.rstore,
+ filters: {
+ property: 'type',
+ value: 'service'
+ }
+ });
+
+ var reload = function() {
+ me.rstore.load();
+ };
+
+ var render_error = function(dataIndex, value, metaData, record) {
+ var errors = record.data.errors;
+ if (errors) {
+ var msg = errors[dataIndex];
+ if (msg) {
+ metaData.tdCls = 'proxmox-invalid-row';
+ var html = '' + gettext('Nodes') + '
',
+ '
',
+ '' + gettext("Virtual Machines") + '
',
+ '
',
+ '
',
+ '
',
+ '' + gettext("LXC Container") + '
',
+ '
',
+ '
',
+ '
',
+ 'No valid subscription
' + PVE.Utils.noSubKeyHtml,
+
+ communityHtml: 'Please use the public community forum for any questions.',
+
+ activeHtml: 'Please use our support portal for any questions. You can also use the public community forum to get additional information.',
+
+ bugzillaHtml: 'Bug Tracking
Our bug tracking system is available here.',
+
+ docuHtml: function() {
+ var me = this;
+ var guideUrl = window.location.origin + me.pveGuidePath;
+ var text = Ext.String.format('Documentation
'
+ + 'The official Proxmox VE Administration Guide'
+ + ' is included with this installation and can be browsed at '
+ + '{0}', guideUrl);
+ return text;
+ },
+
+ updateActive: function(data) {
+ var me = this;
+
+ var html = '' + data.productname + '
' + me.activeHtml;
+ html += '
' + me.docuHtml();
+ html += '
' + me.bugzillaHtml;
+
+ me.update(html);
+ },
+
+ updateCommunity: function(data) {
+ var me = this;
+
+ var html = '' + data.productname + '
' + me.communityHtml;
+ html += '
' + me.docuHtml();
+ html += '
' + me.bugzillaHtml;
+
+ me.update(html);
+ },
+
+ updateInactive: function(data) {
+ var me = this;
+ me.update(me.invalidHtml);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var reload = function() {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/localhost/subscription',
+ method: 'GET',
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ me.update('Unable to load subscription status' + ": " + response.htmlStatus);
+ },
+ success: function(response, opts) {
+ var data = response.result.data;
+
+ if (data.status === 'Active') {
+ if (data.level === 'c') {
+ me.updateCommunity(data);
+ } else {
+ me.updateActive(data);
+ }
+ } else {
+ me.updateInactive(data);
+ }
+ }
+ });
+ };
+
+ Ext.apply(me, {
+ autoScroll: true,
+ bodyStyle: 'padding:10px',
+ listeners: {
+ activate: reload
+ }
+ });
+
+ me.callParent();
+ }
+});
+Ext.define('pve-security-groups', {
+ extend: 'Ext.data.Model',
+
+ fields: [ 'group', 'comment', 'digest' ],
+ idProperty: 'group'
+});
+
+Ext.define('PVE.SecurityGroupEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ base_url: "/cluster/firewall/groups",
+
+ allow_iface: false,
+
+ initComponent : function() {
+ var me = this;
+
+ me.isCreate = (me.group_name === undefined);
+
+ var subject;
+
+ me.url = '/api2/extjs' + me.base_url;
+ me.method = 'POST';
+
+ var items = [
+ {
+ xtype: 'textfield',
+ name: 'group',
+ value: me.group_name || '',
+ fieldLabel: gettext('Name'),
+ allowBlank: false
+ },
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ value: me.group_comment || '',
+ fieldLabel: gettext('Comment')
+ }
+ ];
+
+ if (me.isCreate) {
+ subject = gettext('Security Group');
+ } else {
+ subject = gettext('Security Group') + " '" + me.group_name + "'";
+ items.push({
+ xtype: 'hiddenfield',
+ name: 'rename',
+ value: me.group_name
+ });
+ }
+
+ var ipanel = Ext.create('Proxmox.panel.InputPanel', {
+ // InputPanel does not have a 'create' property, does it need a 'isCreate'
+ isCreate: me.isCreate,
+ items: items
+ });
+
+
+ Ext.apply(me, {
+ subject: subject,
+ items: [ ipanel ]
+ });
+
+ me.callParent();
+ }
+});
+
+Ext.define('PVE.SecurityGroupList', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveSecurityGroupList',
+
+ stateful: true,
+ stateId: 'grid-securitygroups',
+
+ rule_panel: undefined,
+
+ addBtn: undefined,
+ removeBtn: undefined,
+ editBtn: undefined,
+
+ base_url: "/cluster/firewall/groups",
+
+ initComponent: function() {
+ /*jslint confusion: true */
+ var me = this;
+
+ if (me.rule_panel == undefined) {
+ throw "no rule panel specified";
+ }
+
+ if (me.base_url == undefined) {
+ throw "no base_url specified";
+ }
+
+ var store = new Ext.data.Store({
+ model: 'pve-security-groups',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json' + me.base_url
+ },
+ sorters: {
+ property: 'group',
+ order: 'DESC'
+ }
+ });
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var reload = function() {
+ var oldrec = sm.getSelection()[0];
+ store.load(function(records, operation, success) {
+ if (oldrec) {
+ var rec = store.findRecord('group', oldrec.data.group);
+ if (rec) {
+ sm.select(rec);
+ }
+ }
+ });
+ };
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ var win = Ext.create('PVE.SecurityGroupEdit', {
+ digest: rec.data.digest,
+ group_name: rec.data.group,
+ group_comment: rec.data.comment
+ });
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ me.editBtn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor
+ });
+
+ me.addBtn = new Proxmox.button.Button({
+ text: gettext('Create'),
+ handler: function() {
+ sm.deselectAll();
+ var win = Ext.create('PVE.SecurityGroupEdit', {});
+ win.show();
+ win.on('destroy', reload);
+ }
+ });
+
+ me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: me.base_url + '/',
+ enableFn: function(rec) {
+ return (rec && me.base_url);
+ },
+ callback: function() {
+ reload();
+ }
+ });
+
+ Ext.apply(me, {
+ store: store,
+ tbar: [ '' + gettext('Group') + ':', me.addBtn, me.removeBtn, me.editBtn ],
+ selModel: sm,
+ columns: [
+ { header: gettext('Group'), dataIndex: 'group', width: '100' },
+ { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 }
+ ],
+ listeners: {
+ itemdblclick: run_editor,
+ select: function(sm, rec) {
+ var url = '/cluster/firewall/groups/' + rec.data.group;
+ me.rule_panel.setBaseUrl(url);
+ },
+ deselect: function() {
+ me.rule_panel.setBaseUrl(undefined);
+ },
+ show: reload
+ }
+ });
+
+ me.callParent();
+
+ store.load();
+ }
+});
+
+Ext.define('PVE.SecurityGroups', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveSecurityGroups',
+
+ title: 'Security Groups',
+
+ initComponent: function() {
+ var me = this;
+
+ var rule_panel = Ext.createWidget('pveFirewallRules', {
+ region: 'center',
+ allow_groups: false,
+ list_refs_url: '/cluster/firewall/refs',
+ tbar_prefix: '' + gettext('Rules') + ':',
+ border: false
+ });
+
+ var sglist = Ext.createWidget('pveSecurityGroupList', {
+ region: 'west',
+ rule_panel: rule_panel,
+ width: '25%',
+ border: false,
+ split: true
+ });
+
+
+ Ext.apply(me, {
+ layout: 'border',
+ items: [ sglist, rule_panel ],
+ listeners: {
+ show: function() {
+ sglist.fireEvent('show', sglist);
+ }
+ }
+ });
+
+ me.callParent();
+ }
+});
+/*
+ * Datacenter config panel, located in the center of the ViewPort after the Datacenter view is selected
+ */
+
+Ext.define('PVE.dc.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.dc.Config',
+
+ onlineHelp: 'pve_admin_guide',
+
+ initComponent: function() {
+ var me = this;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ me.items = [];
+
+ Ext.apply(me, {
+ title: gettext("Datacenter"),
+ hstateid: 'dctab'
+ });
+
+ if (caps.dc['Sys.Audit']) {
+ me.items.push({
+ title: gettext('Summary'),
+ xtype: 'pveDcSummary',
+ iconCls: 'fa fa-book',
+ itemId: 'summary'
+ },
+ {
+ title: gettext('Cluster'),
+ xtype: 'pveClusterAdministration',
+ iconCls: 'fa fa-server',
+ itemId: 'cluster'
+ },
+ {
+ title: 'Ceph',
+ itemId: 'ceph',
+ iconCls: 'fa fa-ceph',
+ xtype: 'pveNodeCephStatus'
+ },
+ {
+ xtype: 'pveDcOptionView',
+ title: gettext('Options'),
+ iconCls: 'fa fa-gear',
+ itemId: 'options'
+ });
+ }
+
+ if (caps.storage['Datastore.Allocate'] || caps.dc['Sys.Audit']) {
+ me.items.push({
+ xtype: 'pveStorageView',
+ title: gettext('Storage'),
+ iconCls: 'fa fa-database',
+ itemId: 'storage'
+ });
+ }
+
+ if (caps.dc['Sys.Audit']) {
+ me.items.push({
+ xtype: 'pveDcBackupView',
+ iconCls: 'fa fa-floppy-o',
+ title: gettext('Backup'),
+ itemId: 'backup'
+ },
+ {
+ xtype: 'pveReplicaView',
+ iconCls: 'fa fa-retweet',
+ title: gettext('Replication'),
+ itemId: 'replication'
+ },
+ {
+ xtype: 'pveACLView',
+ title: gettext('Permissions'),
+ iconCls: 'fa fa-unlock',
+ itemId: 'permissions',
+ expandedOnInit: true
+ });
+ }
+
+ me.items.push({
+ xtype: 'pveUserView',
+ groups: ['permissions'],
+ iconCls: 'fa fa-user',
+ title: gettext('Users'),
+ itemId: 'users'
+ });
+
+ if (caps.dc['Sys.Audit']) {
+ me.items.push({
+ xtype: 'pveGroupView',
+ title: gettext('Groups'),
+ iconCls: 'fa fa-users',
+ groups: ['permissions'],
+ itemId: 'groups'
+ },
+ {
+ xtype: 'pvePoolView',
+ title: gettext('Pools'),
+ iconCls: 'fa fa-tags',
+ groups: ['permissions'],
+ itemId: 'pools'
+ },
+ {
+ xtype: 'pveRoleView',
+ title: gettext('Roles'),
+ iconCls: 'fa fa-male',
+ groups: ['permissions'],
+ itemId: 'roles'
+ },
+ {
+ xtype: 'pveAuthView',
+ title: gettext('Authentication'),
+ groups: ['permissions'],
+ iconCls: 'fa fa-key',
+ itemId: 'domains'
+ },
+ {
+ xtype: 'pveHAStatus',
+ title: 'HA',
+ iconCls: 'fa fa-heartbeat',
+ itemId: 'ha'
+ },
+ {
+ title: gettext('Groups'),
+ groups: ['ha'],
+ xtype: 'pveHAGroupsView',
+ iconCls: 'fa fa-object-group',
+ itemId: 'ha-groups'
+ },
+ {
+ title: gettext('Fencing'),
+ groups: ['ha'],
+ iconCls: 'fa fa-bolt',
+ xtype: 'pveFencingView',
+ itemId: 'ha-fencing'
+ },
+ {
+ xtype: 'pveFirewallRules',
+ title: gettext('Firewall'),
+ allow_iface: true,
+ base_url: '/cluster/firewall/rules',
+ list_refs_url: '/cluster/firewall/refs',
+ iconCls: 'fa fa-shield',
+ itemId: 'firewall'
+ },
+ {
+ xtype: 'pveFirewallOptions',
+ title: gettext('Options'),
+ groups: ['firewall'],
+ iconCls: 'fa fa-gear',
+ base_url: '/cluster/firewall/options',
+ onlineHelp: 'pve_firewall_cluster_wide_setup',
+ fwtype: 'dc',
+ itemId: 'firewall-options'
+ },
+ {
+ xtype: 'pveSecurityGroups',
+ title: gettext('Security Group'),
+ groups: ['firewall'],
+ iconCls: 'fa fa-group',
+ itemId: 'firewall-sg'
+ },
+ {
+ xtype: 'pveFirewallAliases',
+ title: gettext('Alias'),
+ groups: ['firewall'],
+ iconCls: 'fa fa-external-link',
+ base_url: '/cluster/firewall/aliases',
+ itemId: 'firewall-aliases'
+ },
+ {
+ xtype: 'pveIPSet',
+ title: 'IPSet',
+ groups: ['firewall'],
+ iconCls: 'fa fa-list-ol',
+ base_url: '/cluster/firewall/ipset',
+ list_refs_url: '/cluster/firewall/refs',
+ itemId: 'firewall-ipset'
+ },
+ {
+ xtype: 'pveDcSupport',
+ title: gettext('Support'),
+ itemId: 'support',
+ iconCls: 'fa fa-comments-o'
+ });
+ }
+
+ me.callParent();
+ }
+});
+Ext.define('PVE.dc.NodeView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveDcNodeView',
+
+ title: gettext('Nodes'),
+ disableSelection: true,
+ scrollable: true,
+
+ columns: [
+ {
+ header: gettext('Name'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'name'
+ },
+ {
+ header: 'ID',
+ width: 40,
+ sortable: true,
+ dataIndex: 'nodeid'
+ },
+ {
+ header: gettext('Online'),
+ width: 60,
+ sortable: true,
+ dataIndex: 'online',
+ renderer: function(value) {
+ var cls = (value)?'good':'critical';
+ return '';
+ }
+ },
+ {
+ header: gettext('Support'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'level',
+ renderer: PVE.Utils.render_support_level
+ },
+ {
+ header: gettext('Server Address'),
+ width: 115,
+ sortable: true,
+ dataIndex: 'ip'
+ },
+ {
+ header: gettext('CPU usage'),
+ sortable: true,
+ width: 110,
+ dataIndex: 'cpuusage',
+ tdCls: 'x-progressbar-default-cell',
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'pveProgressBar'
+ }
+ },
+ {
+ header: gettext('Memory usage'),
+ width: 110,
+ sortable: true,
+ tdCls: 'x-progressbar-default-cell',
+ dataIndex: 'memoryusage',
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'pveProgressBar'
+ }
+ },
+ {
+ header: gettext('Uptime'),
+ sortable: true,
+ dataIndex: 'uptime',
+ align: 'right',
+ renderer: Proxmox.Utils.render_uptime
+ }
+ ],
+
+ stateful: true,
+ stateId: 'grid-cluster-nodes',
+ tools: [
+ {
+ type: 'up',
+ handler: function(){
+ var me = this.up('grid');
+ var height = Math.max(me.getHeight()-50, 250);
+ me.setHeight(height);
+ }
+ },
+ {
+ type: 'down',
+ handler: function(){
+ var me = this.up('grid');
+ var height = me.getHeight()+50;
+ me.setHeight(height);
+ }
+ }
+ ]
+}, function() {
+
+ Ext.define('pve-dc-nodes', {
+ extend: 'Ext.data.Model',
+ fields: [ 'id', 'type', 'name', 'nodeid', 'ip', 'level', 'local', 'online'],
+ idProperty: 'id'
+ });
+
+});
+
+Ext.define('PVE.widget.ProgressBar',{
+ extend: 'Ext.Progress',
+ alias: 'widget.pveProgressBar',
+
+ animate: true,
+ textTpl: [
+ '{percent}%'
+ ],
+
+ setValue: function(value){
+ var me = this;
+ me.callParent([value]);
+
+ me.removeCls(['warning', 'critical']);
+
+ if (value > 0.89) {
+ me.addCls('critical');
+ } else if (value > 0.59) {
+ me.addCls('warning');
+ }
+ }
+});
+/*jslint confusion: true*/
+Ext.define('pve-cluster-nodes', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'node', { type: 'integer', name: 'nodeid' }, 'ring0_addr', 'ring1_addr',
+ { type: 'integer', name: 'quorum_votes' }
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/config/nodes"
+ },
+ idProperty: 'nodeid'
+});
+
+Ext.define('pve-cluster-info', {
+ extend: 'Ext.data.Model',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/config/join"
+ }
+});
+
+Ext.define('PVE.ClusterAdministration', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pveClusterAdministration',
+
+ title: gettext('Cluster Administration'),
+ onlineHelp: 'chapter_pvecm',
+
+ border: false,
+ defaults: { border: false },
+
+ viewModel: {
+ parent: null,
+ data: {
+ totem: {},
+ nodelist: [],
+ preferred_node: {
+ name: '',
+ fp: '',
+ addr: ''
+ },
+ isInCluster: false,
+ nodecount: 0
+ }
+ },
+
+ items: [
+ {
+ xtype: 'panel',
+ title: gettext('Cluster Information'),
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ view.store = Ext.create('Proxmox.data.UpdateStore', {
+ autoStart: true,
+ interval: 15 * 1000,
+ storeid: 'pve-cluster-info',
+ model: 'pve-cluster-info'
+ });
+ view.store.on('load', this.onLoad, this);
+ view.on('destroy', view.store.stopUpdate);
+ },
+
+ onLoad: function(store, records, success) {
+ var vm = this.getViewModel();
+ if (!success || !records || !records[0].data) {
+ vm.set('totem', {});
+ vm.set('isInCluster', false);
+ vm.set('nodelist', []);
+ vm.set('preferred_node', {
+ name: '',
+ addr: '',
+ fp: ''
+ });
+ return;
+ }
+ var data = records[0].data;
+ vm.set('totem', data.totem);
+ vm.set('isInCluster', !!data.totem.cluster_name);
+ vm.set('nodelist', data.nodelist);
+
+ var nodeinfo = Ext.Array.findBy(data.nodelist, function (el) {
+ return el.name === data.preferred_node;
+ });
+
+ vm.set('preferred_node', {
+ name: data.preferred_node,
+ addr: nodeinfo.pve_addr,
+ ring_addr: [ nodeinfo.ring0_addr, nodeinfo.ring1_addr ],
+ fp: nodeinfo.pve_fp
+ });
+ },
+
+ onCreate: function() {
+ var view = this.getView();
+ view.store.stopUpdate();
+ var win = Ext.create('PVE.ClusterCreateWindow', {
+ autoShow: true,
+ listeners: {
+ destroy: function() {
+ view.store.startUpdate();
+ }
+ }
+ });
+ },
+
+ onClusterInfo: function() {
+ var vm = this.getViewModel();
+ var win = Ext.create('PVE.ClusterInfoWindow', {
+ joinInfo: {
+ ipAddress: vm.get('preferred_node.addr'),
+ fingerprint: vm.get('preferred_node.fp'),
+ ring_addr: vm.get('preferred_node.ring_addr'),
+ totem: vm.get('totem')
+ }
+ });
+ win.show();
+ },
+
+ onJoin: function() {
+ var view = this.getView();
+ view.store.stopUpdate();
+ var win = Ext.create('PVE.ClusterJoinNodeWindow', {
+ autoShow: true,
+ listeners: {
+ destroy: function() {
+ view.store.startUpdate();
+ }
+ }
+ });
+ }
+ },
+ tbar: [
+ {
+ text: gettext('Create Cluster'),
+ reference: 'createButton',
+ handler: 'onCreate',
+ bind: {
+ disabled: '{isInCluster}'
+ }
+ },
+ {
+ text: gettext('Join Information'),
+ reference: 'addButton',
+ handler: 'onClusterInfo',
+ bind: {
+ disabled: '{!isInCluster}'
+ }
+ },
+ {
+ text: gettext('Join Cluster'),
+ reference: 'joinButton',
+ handler: 'onJoin',
+ bind: {
+ disabled: '{isInCluster}'
+ }
+ }
+ ],
+ layout: 'hbox',
+ bodyPadding: 5,
+ items: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Cluster Name'),
+ bind: {
+ value: '{totem.cluster_name}',
+ hidden: '{!isInCluster}'
+ },
+ flex: 1
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Config Version'),
+ bind: {
+ value: '{totem.config_version}',
+ hidden: '{!isInCluster}'
+ },
+ flex: 1
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Number of Nodes'),
+ labelWidth: 120,
+ bind: {
+ value: '{nodecount}',
+ hidden: '{!isInCluster}'
+ },
+ flex: 1
+ },
+ {
+ xtype: 'displayfield',
+ value: gettext('Standalone node - no cluster defined'),
+ bind: {
+ hidden: '{isInCluster}'
+ },
+ flex: 1
+ }
+ ]
+ },
+ {
+ xtype: 'grid',
+ title: gettext('Cluster Nodes'),
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ view.rstore = Ext.create('Proxmox.data.UpdateStore', {
+ autoLoad: true,
+ xtype: 'update',
+ interval: 5 * 1000,
+ autoStart: true,
+ storeid: 'pve-cluster-nodes',
+ model: 'pve-cluster-nodes'
+ });
+ view.setStore(Ext.create('Proxmox.data.DiffStore', {
+ rstore: view.rstore,
+ sorters: {
+ property: 'nodeid',
+ order: 'DESC'
+ }
+ }));
+ Proxmox.Utils.monStoreErrors(view, view.rstore);
+ view.rstore.on('load', this.onLoad, this);
+ view.on('destroy', view.rstore.stopUpdate);
+ },
+
+ onLoad: function(store, records, success) {
+ var vm = this.getViewModel();
+ if (!success || !records) {
+ vm.set('nodecount', 0);
+ return;
+ }
+ vm.set('nodecount', records.length);
+ }
+ },
+ columns: [
+ {
+ header: gettext('Nodename'),
+ flex: 2,
+ dataIndex: 'name'
+ },
+ {
+ header: gettext('ID'),
+ flex: 1,
+ dataIndex: 'nodeid'
+ },
+ {
+ header: gettext('Votes'),
+ flex: 1,
+ dataIndex: 'quorum_votes'
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 0),
+ flex: 2,
+ dataIndex: 'ring0_addr'
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 1),
+ flex: 2,
+ dataIndex: 'ring1_addr'
+ }
+ ]
+ }
+ ]
+});
+/*jslint confusion: true*/
+Ext.define('PVE.ClusterCreateWindow', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveClusterCreateWindow',
+
+ title: gettext('Create Cluster'),
+ width: 600,
+
+ method: 'POST',
+ url: '/cluster/config',
+
+ isCreate: true,
+ subject: gettext('Cluster'),
+ showTaskViewer: true,
+
+ onlineHelp: 'pvecm_create_cluster',
+
+ items: {
+ xtype: 'inputpanel',
+ items: [{
+ xtype: 'textfield',
+ fieldLabel: gettext('Cluster Name'),
+ allowBlank: false,
+ name: 'clustername'
+ },
+ {
+ xtype: 'proxmoxNetworkSelector',
+ fieldLabel: Ext.String.format(gettext('Link {0}'), 0),
+ emptyText: gettext("Optional, defaults to IP resolved by node's hostname"),
+ name: 'link0',
+ autoSelect: false,
+ valueField: 'address',
+ displayField: 'address',
+ skipEmptyText: true
+ }],
+ advancedItems: [{
+ xtype: 'proxmoxNetworkSelector',
+ fieldLabel: Ext.String.format(gettext('Link {0}'), 1),
+ emptyText: gettext("Optional second link for redundancy"),
+ name: 'link1',
+ autoSelect: false,
+ valueField: 'address',
+ displayField: 'address',
+ skipEmptyText: true
+ }]
+ }
+});
+
+Ext.define('PVE.ClusterInfoWindow', {
+ extend: 'Ext.window.Window',
+ xtype: 'pveClusterInfoWindow',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ width: 800,
+ modal: true,
+ resizable: false,
+ title: gettext('Cluster Join Information'),
+
+ joinInfo: {
+ ipAddress: undefined,
+ fingerprint: undefined,
+ totem: {}
+ },
+
+ items: [
+ {
+ xtype: 'component',
+ border: false,
+ padding: '10 10 10 10',
+ html: gettext("Copy the Join Information here and use it on the node you want to add.")
+ },
+ {
+ xtype: 'container',
+ layout: 'form',
+ border: false,
+ padding: '0 10 10 10',
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('IP Address'),
+ cbind: { value: '{joinInfo.ipAddress}' },
+ editable: false
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Fingerprint'),
+ cbind: { value: '{joinInfo.fingerprint}' },
+ editable: false
+ },
+ {
+ xtype: 'textarea',
+ inputId: 'pveSerializedClusterInfo',
+ fieldLabel: gettext('Join Information'),
+ grow: true,
+ cbind: { joinInfo: '{joinInfo}' },
+ editable: false,
+ listeners: {
+ afterrender: function(field) {
+ if (!field.joinInfo) {
+ return;
+ }
+ var jsons = Ext.JSON.encode(field.joinInfo);
+ var base64s = Ext.util.Base64.encode(jsons);
+ field.setValue(base64s);
+ }
+ }
+ }
+ ]
+ }
+ ],
+ dockedItems: [{
+ dock: 'bottom',
+ xtype: 'toolbar',
+ items: [{
+ xtype: 'button',
+ handler: function(b) {
+ var el = document.getElementById('pveSerializedClusterInfo');
+ el.select();
+ document.execCommand("copy");
+ },
+ text: gettext('Copy Information')
+ }]
+ }]
+});
+
+Ext.define('PVE.ClusterJoinNodeWindow', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveClusterJoinNodeWindow',
+
+ title: gettext('Cluster Join'),
+ width: 800,
+
+ method: 'POST',
+ url: '/cluster/config/join',
+
+ defaultFocus: 'textarea[name=serializedinfo]',
+ isCreate: true,
+ submitText: gettext('Join'),
+ showTaskViewer: true,
+
+ onlineHelp: 'chapter_pvecm',
+
+ viewModel: {
+ parent: null,
+ data: {
+ info: {
+ fp: '',
+ ip: '',
+ ring0Needed: false,
+ ring1Possible: false,
+ ring1Needed: false
+ }
+ },
+ formulas: {
+ ring0EmptyText: function(get) {
+ if (get('info.ring0Needed')) {
+ return gettext("Cannot use default address safely");
+ } else {
+ return gettext("Default: IP resolved by node's hostname");
+ }
+ }
+ }
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ '#': {
+ close: function() {
+ delete PVE.Utils.silenceAuthFailures;
+ }
+ },
+ 'proxmoxcheckbox[name=assistedEntry]': {
+ change: 'onInputTypeChange'
+ },
+ 'textarea[name=serializedinfo]': {
+ change: 'recomputeSerializedInfo',
+ enable: 'resetField'
+ },
+ 'proxmoxtextfield[name=ring1_addr]': {
+ enable: 'ring1Needed'
+ },
+ 'textfield': {
+ disable: 'resetField'
+ }
+ },
+ resetField: function(field) {
+ field.reset();
+ },
+ ring1Needed: function(f) {
+ var vm = this.getViewModel();
+ f.allowBlank = !vm.get('info.ring1Needed');
+ },
+ onInputTypeChange: function(field, assistedInput) {
+ var vm = this.getViewModel();
+ if (!assistedInput) {
+ vm.set('info.ring1Possible', true);
+ }
+ },
+ recomputeSerializedInfo: function(field, value) {
+ var vm = this.getViewModel();
+ var jsons = Ext.util.Base64.decode(value);
+ var joinInfo = Ext.JSON.decode(jsons, true);
+
+ var info = {
+ fp: '',
+ ring1Needed: false,
+ ring1Possible: false,
+ ip: ''
+ };
+
+ var totem = {};
+ if (!(joinInfo && joinInfo.totem)) {
+ field.valid = false;
+ } else {
+ var ring0Needed = false;
+ if (joinInfo.ring_addr !== undefined) {
+ ring0Needed = joinInfo.ring_addr[0] !== joinInfo.ipAddress;
+ }
+
+ info = {
+ ip: joinInfo.ipAddress,
+ fp: joinInfo.fingerprint,
+ ring0Needed: ring0Needed,
+ ring1Possible: !!joinInfo.totem['interface']['1'],
+ ring1Needed: !!joinInfo.totem['interface']['1']
+ };
+ totem = joinInfo.totem;
+ field.valid = true;
+ }
+
+ vm.set('info', info);
+ }
+ },
+
+ submit: function() {
+ // joining may produce temporarily auth failures, ignore as long the task runs
+ PVE.Utils.silenceAuthFailures = true;
+ this.callParent();
+ },
+
+ taskDone: function(success) {
+ delete PVE.Utils.silenceAuthFailures;
+ if (success) {
+ var txt = gettext('Cluster join task finished, node certificate may have changed, reload GUI!');
+ // ensure user cannot do harm
+ Ext.getBody().mask(txt, ['pve-static-mask']);
+ // TaskView may hide above mask, so tell him directly
+ Ext.Msg.show({
+ title: gettext('Join Task Finished'),
+ icon: Ext.Msg.INFO,
+ msg: txt
+ });
+ // reload always (if user wasn't faster), but wait a bit for pveproxy
+ Ext.defer(function() {
+ window.location.reload(true);
+ }, 5000);
+ }
+ },
+
+ items: [{
+ xtype: 'proxmoxcheckbox',
+ reference: 'assistedEntry',
+ name: 'assistedEntry',
+ submitValue: false,
+ value: true,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Select if join information should be extracted from pasted cluster information, deselect for manual entering')
+ },
+ boxLabel: gettext('Assisted join: Paste encoded cluster join information and enter password.')
+ },
+ {
+ xtype: 'textarea',
+ name: 'serializedinfo',
+ submitValue: false,
+ allowBlank: false,
+ fieldLabel: gettext('Information'),
+ emptyText: gettext('Paste encoded Cluster Information here'),
+ validator: function(val) {
+ return val === '' || this.valid ||
+ gettext('Does not seem like a valid encoded Cluster Information!');
+ },
+ bind: {
+ disabled: '{!assistedEntry.checked}',
+ hidden: '{!assistedEntry.checked}'
+ },
+ value: ''
+ },
+ {
+ xtype: 'inputpanel',
+ column1: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Peer Address'),
+ allowBlank: false,
+ bind: {
+ value: '{info.ip}',
+ readOnly: '{assistedEntry.checked}'
+ },
+ name: 'hostname'
+ },
+ {
+ xtype: 'textfield',
+ inputType: 'password',
+ emptyText: gettext("Peer's root password"),
+ fieldLabel: gettext('Password'),
+ allowBlank: false,
+ name: 'password'
+ }
+ ],
+ column2: [
+ {
+ xtype: 'proxmoxNetworkSelector',
+ fieldLabel: Ext.String.format(gettext('Link {0}'), 0),
+ bind: {
+ emptyText: '{ring0EmptyText}',
+ allowBlank: '{!info.ring0Needed}'
+ },
+ skipEmptyText: true,
+ autoSelect: false,
+ valueField: 'address',
+ displayField: 'address',
+ name: 'link0'
+ },
+ {
+ xtype: 'proxmoxNetworkSelector',
+ fieldLabel: Ext.String.format(gettext('Link {0}'), 1),
+ skipEmptyText: true,
+ autoSelect: false,
+ valueField: 'address',
+ displayField: 'address',
+ bind: {
+ disabled: '{!info.ring1Possible}',
+ allowBlank: '{!info.ring1Needed}',
+ },
+ name: 'link1'
+ }
+ ],
+ columnB: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Fingerprint'),
+ allowBlank: false,
+ bind: {
+ value: '{info.fp}',
+ readOnly: '{assistedEntry.checked}'
+ },
+ name: 'fingerprint'
+ }
+ ]
+ }]
+});
+/*
+ * Workspace base class
+ *
+ * popup login window when auth fails (call onLogin handler)
+ * update (re-login) ticket every 15 minutes
+ *
+ */
+
+Ext.define('PVE.Workspace', {
+ extend: 'Ext.container.Viewport',
+
+ title: 'Proxmox Virtual Environment',
+
+ loginData: null, // Data from last login call
+
+ onLogin: function(loginData) {},
+
+ // private
+ updateLoginData: function(loginData) {
+ var me = this;
+ me.loginData = loginData;
+ Proxmox.Utils.setAuthData(loginData);
+
+ var rt = me.down('pveResourceTree');
+ rt.setDatacenterText(loginData.clustername);
+
+ if (loginData.cap) {
+ Ext.state.Manager.set('GuiCap', loginData.cap);
+ }
+ me.response401count = 0;
+
+ me.onLogin(loginData);
+ },
+
+ // private
+ showLogin: function() {
+ var me = this;
+
+ Proxmox.Utils.authClear();
+ Proxmox.UserName = null;
+ me.loginData = null;
+
+ if (!me.login) {
+ me.login = Ext.create('PVE.window.LoginWindow', {
+ handler: function(data) {
+ me.login = null;
+ me.updateLoginData(data);
+ Proxmox.Utils.checked_command(function() {}); // display subscription status
+ }
+ });
+ }
+ me.onLogin(null);
+ me.login.show();
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ Ext.tip.QuickTipManager.init();
+
+ // fixme: what about other errors
+ Ext.Ajax.on('requestexception', function(conn, response, options) {
+ if (response.status == 401 && !PVE.Utils.silenceAuthFailures) { // auth failure
+ // don't immediately show as logged out to cope better with some big
+ // upgrades, which may temporarily produce a false positive 401 err
+ me.response401count++;
+ if (me.response401count > 5) {
+ me.showLogin();
+ }
+ }
+ });
+
+ me.callParent();
+
+ if (!Proxmox.Utils.authOK()) {
+ me.showLogin();
+ } else {
+ if (me.loginData) {
+ me.onLogin(me.loginData);
+ }
+ }
+
+ Ext.TaskManager.start({
+ run: function() {
+ var ticket = Proxmox.Utils.authOK();
+ if (!ticket || !Proxmox.UserName) {
+ return;
+ }
+
+ Ext.Ajax.request({
+ params: {
+ username: Proxmox.UserName,
+ password: ticket
+ },
+ url: '/api2/json/access/ticket',
+ method: 'POST',
+ success: function(response, opts) {
+ var obj = Ext.decode(response.responseText);
+ me.updateLoginData(obj.data);
+ }
+ });
+ },
+ interval: 15*60*1000
+ });
+
+ }
+});
+
+Ext.define('PVE.StdWorkspace', {
+ extend: 'PVE.Workspace',
+
+ alias: ['widget.pveStdWorkspace'],
+
+ // private
+ setContent: function(comp) {
+ var me = this;
+
+ var cont = me.child('#content');
+
+ var lay = cont.getLayout();
+
+ var cur = lay.getActiveItem();
+
+ if (comp) {
+ Proxmox.Utils.setErrorMask(cont, false);
+ comp.border = false;
+ cont.add(comp);
+ if (cur !== null && lay.getNext()) {
+ lay.next();
+ var task = Ext.create('Ext.util.DelayedTask', function(){
+ cont.remove(cur);
+ });
+ task.delay(10);
+ }
+ }
+ else {
+ // helper for cleaning the content when logging out
+ cont.removeAll();
+ }
+ },
+
+ selectById: function(nodeid) {
+ var me = this;
+ var tree = me.down('pveResourceTree');
+ tree.selectById(nodeid);
+ },
+
+ onLogin: function(loginData) {
+ var me = this;
+
+ me.updateUserInfo();
+
+ if (loginData) {
+ PVE.data.ResourceStore.startUpdate();
+
+ Proxmox.Utils.API2Request({
+ url: '/version',
+ method: 'GET',
+ success: function(response) {
+ PVE.VersionInfo = response.result.data;
+ me.updateVersionInfo();
+ }
+ });
+ }
+ },
+
+ updateUserInfo: function() {
+ var me = this;
+ var ui = me.query('#userinfo')[0];
+ ui.setText(Proxmox.UserName || '');
+ ui.updateLayout();
+ },
+
+ updateVersionInfo: function() {
+ var me = this;
+
+ var ui = me.query('#versioninfo')[0];
+
+ if (PVE.VersionInfo) {
+ var version = PVE.VersionInfo.version;
+ ui.update('Virtual Environment ' + version);
+ } else {
+ ui.update('Virtual Environment');
+ }
+ ui.updateLayout();
+ },
+
+ initComponent : function() {
+ var me = this;
+
+ Ext.History.init();
+
+ var sprovider = Ext.create('PVE.StateProvider');
+ Ext.state.Manager.setProvider(sprovider);
+
+ var selview = Ext.create('PVE.form.ViewSelector');
+
+ var rtree = Ext.createWidget('pveResourceTree', {
+ viewFilter: selview.getViewFilter(),
+ flex: 1,
+ selModel: {
+ selType: 'treemodel',
+ listeners: {
+ selectionchange: function(sm, selected) {
+ if (selected.length > 0) {
+ var n = selected[0];
+ var tlckup = {
+ root: 'PVE.dc.Config',
+ node: 'PVE.node.Config',
+ qemu: 'PVE.qemu.Config',
+ lxc: 'PVE.lxc.Config',
+ storage: 'PVE.storage.Browser',
+ pool: 'pvePoolConfig'
+ };
+ var comp = {
+ xtype: tlckup[n.data.type || 'root'] ||
+ 'pvePanelConfig',
+ showSearch: (n.data.id === 'root') ||
+ Ext.isDefined(n.data.groupbyid),
+ pveSelNode: n,
+ workspace: me,
+ viewFilter: selview.getViewFilter()
+ };
+ PVE.curSelectedNode = n;
+ me.setContent(comp);
+ }
+ }
+ }
+ }
+ });
+
+ selview.on('select', function(combo, records) {
+ if (records) {
+ var view = combo.getViewFilter();
+ rtree.setViewFilter(view);
+ }
+ });
+
+ var caps = sprovider.get('GuiCap');
+
+ var createVM = Ext.createWidget('button', {
+ pack: 'end',
+ margin: '3 5 0 0',
+ baseCls: 'x-btn',
+ iconCls: 'fa fa-desktop',
+ text: gettext("Create VM"),
+ disabled: !caps.vms['VM.Allocate'],
+ handler: function() {
+ var wiz = Ext.create('PVE.qemu.CreateWizard', {});
+ wiz.show();
+ }
+ });
+
+ var createCT = Ext.createWidget('button', {
+ pack: 'end',
+ margin: '3 5 0 0',
+ baseCls: 'x-btn',
+ iconCls: 'fa fa-cube',
+ text: gettext("Create CT"),
+ disabled: !caps.vms['VM.Allocate'],
+ handler: function() {
+ var wiz = Ext.create('PVE.lxc.CreateWizard', {});
+ wiz.show();
+ }
+ });
+
+ sprovider.on('statechange', function(sp, key, value) {
+ if (key === 'GuiCap' && value) {
+ caps = value;
+ createVM.setDisabled(!caps.vms['VM.Allocate']);
+ createCT.setDisabled(!caps.vms['VM.Allocate']);
+ }
+ });
+
+ Ext.apply(me, {
+ layout: { type: 'border' },
+ border: false,
+ items: [
+ {
+ region: 'north',
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ baseCls: 'x-plain',
+ defaults: {
+ baseCls: 'x-plain'
+ },
+ border: false,
+ margin: '2 0 2 5',
+ items: [
+ {
+ html: '' +
+ ''
+ },
+ {
+ minWidth: 150,
+ id: 'versioninfo',
+ html: 'Virtual Environment'
+ },
+ {
+ xtype: 'pveGlobalSearchField',
+ tree: rtree
+ },
+ {
+ flex: 1
+ },
+ {
+ xtype: 'proxmoxHelpButton',
+ hidden: false,
+ baseCls: 'x-btn',
+ iconCls: 'fa fa-book x-btn-icon-el-default-toolbar-small ',
+ listenToGlobalEvent: false,
+ onlineHelp: 'pve_documentation_index',
+ text: gettext('Documentation'),
+ margin: '0 5 0 0'
+ },
+ createVM,
+ createCT,
+ {
+ pack: 'end',
+ margin: '0 5 0 0',
+ id: 'userinfo',
+ xtype: 'button',
+ baseCls: 'x-btn',
+ style: {
+ // proxmox dark grey p light grey as border
+ backgroundColor: '#464d4d',
+ borderColor: '#ABBABA'
+ },
+ iconCls: 'fa fa-user',
+ menu: [
+ {
+ iconCls: 'fa fa-gear',
+ text: gettext('My Settings'),
+ handler: function() {
+ var win = Ext.create('PVE.window.Settings');
+ win.show();
+ }
+ },
+ {
+ text: gettext('Password'),
+ iconCls: 'fa fa-fw fa-key',
+ handler: function() {
+ var win = Ext.create('Proxmox.window.PasswordEdit', {
+ userid: Proxmox.UserName
+ });
+ win.show();
+ }
+ },
+ {
+ text: 'TFA',
+ iconCls: 'fa fa-fw fa-lock',
+ handler: function(btn, event, rec) {
+ var win = Ext.create('PVE.window.TFAEdit',{
+ userid: Proxmox.UserName
+ });
+ win.show();
+ }
+ },
+ '-',
+ {
+ iconCls: 'fa fa-fw fa-sign-out',
+ text: gettext("Logout"),
+ handler: function() {
+ PVE.data.ResourceStore.loadData([], false);
+ me.showLogin();
+ me.setContent(null);
+ var rt = me.down('pveResourceTree');
+ rt.setDatacenterText(undefined);
+ rt.clearTree();
+
+ // empty the stores of the StatusPanel child items
+ var statusPanels = Ext.ComponentQuery.query('pveStatusPanel grid');
+ Ext.Array.forEach(statusPanels, function(comp) {
+ if (comp.getStore()) {
+ comp.getStore().loadData([], false);
+ }
+ });
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ region: 'center',
+ stateful: true,
+ stateId: 'pvecenter',
+ minWidth: 100,
+ minHeight: 100,
+ id: 'content',
+ xtype: 'container',
+ layout: { type: 'card' },
+ border: false,
+ margin: '0 5 0 0',
+ items: []
+ },
+ {
+ region: 'west',
+ stateful: true,
+ stateId: 'pvewest',
+ itemId: 'west',
+ xtype: 'container',
+ border: false,
+ layout: { type: 'vbox', align: 'stretch' },
+ margin: '0 0 0 5',
+ split: true,
+ width: 200,
+ items: [ selview, rtree ],
+ listeners: {
+ resize: function(panel, width, height) {
+ var viewWidth = me.getSize().width;
+ if (width > viewWidth - 100) {
+ panel.setWidth(viewWidth - 100);
+ }
+ }
+ }
+ },
+ {
+ xtype: 'pveStatusPanel',
+ stateful: true,
+ stateId: 'pvesouth',
+ itemId: 'south',
+ region: 'south',
+ margin:'0 5 5 5',
+ title: gettext('Logs'),
+ collapsible: true,
+ header: false,
+ height: 200,
+ split:true,
+ listeners: {
+ resize: function(panel, width, height) {
+ var viewHeight = me.getSize().height;
+ if (height > (viewHeight - 150)) {
+ panel.setHeight(viewHeight - 150);
+ }
+ }
+ }
+ }
+ ]
+ });
+
+ me.callParent();
+
+ me.updateUserInfo();
+
+ // on resize, center all modal windows
+ Ext.on('resize', function(){
+ var wins = Ext.ComponentQuery.query('window[modal]');
+ if (wins.length > 0) {
+ wins.forEach(function(win){
+ win.alignTo(me, 'c-c');
+ });
+ }
+ });
+ }
+});
+
diff --git a/serverside/jsmod/changes.md b/serverside/jsmod/changes.md
index 66403b2..db9fc0c 100644
--- a/serverside/jsmod/changes.md
+++ b/serverside/jsmod/changes.md
@@ -16,3 +16,16 @@ White gauge text (`line 8935`)
* **proxmoxlib.js**
Blurple color for gauge filled meter (`line 4462`)
Dark color for gauge meter background (`line 4463`)
+
+## 6.0-4
+* **pvemanagerlib.js**
+`background-color` to `#23272a` for node summary background (`line 18019`)
+`background-color` to `#23272a` for TFA QR code background (`line 35826`)
+`background-color` to `#23272a` for S.M.A.R.T disk report (`line 16729`)
+`background-color` to `#23272a` for system report (`line 18203`)
+* **charts.js**
+Remains roughly the same as 5-4.3
+* **proxmoxlib.js**
+`defaultColor` to `#7289DA` (`line 5084`)
+`backgroundColor` to `#2C2F33` (`line 5085`)
+