[PATCH] Adding ability to switch graphics devices on a cold vm, adds serial console option as well
by bbaude@redhat.com
From: Brent Baude <bbaude(a)redhat.com>
This patch allows you to change the graphics type for a cold guest. It will also allow
a serial choice for ppc64 machines where a user can then run virsh console to deal with
low-bandwidth issues.
---
src/kimchi/API.json | 5 +-
src/kimchi/model/config.py | 2 +
src/kimchi/model/vms.py | 177 +++++++++++++++++++++++++++------
src/kimchi/vmtemplate.py | 9 ++
ui/css/theme-default/guest-edit.css | 6 ++
ui/css/theme-default/storage.css | 58 -----------
ui/js/src/kimchi.guest_edit_main.js | 30 +++++-
ui/js/src/kimchi.guest_main.js | 23 ++++-
ui/js/src/kimchi.template_edit_main.js | 4 +
ui/pages/guest-edit.html.tmpl | 21 ++++
ui/pages/guest.html.tmpl | 5 +-
ui/pages/i18n.json.tmpl | 1 +
ui/pages/tabs/storage.html.tmpl | 4 +-
13 files changed, 246 insertions(+), 99 deletions(-)
diff --git a/src/kimchi/API.json b/src/kimchi/API.json
index 8a95804..335f1fd 100644
--- a/src/kimchi/API.json
+++ b/src/kimchi/API.json
@@ -9,7 +9,7 @@
"type": "object",
"properties": {
"type": {
- "enum": ["spice", "vnc"],
+ "enum": ["spice", "vnc", "serial"],
"error": "KCHVM0014E"
},
"listen": {
@@ -274,7 +274,8 @@
"type": "integer",
"minimum": 512,
"error": "KCHTMPL0013E"
- }
+ },
+ "graphics": { "$ref": "#/kimchitype/graphics" }
}
},
"networks_create": {
diff --git a/src/kimchi/model/config.py b/src/kimchi/model/config.py
index 1c00cfe..0b7ecc3 100644
--- a/src/kimchi/model/config.py
+++ b/src/kimchi/model/config.py
@@ -20,6 +20,7 @@
from multiprocessing.pool import ThreadPool
import cherrypy
+import platform
from kimchi.basemodel import Singleton
from kimchi.config import config as kconfig
@@ -106,6 +107,7 @@ class CapabilitiesModel(object):
return {'libvirt_stream_protocols': self.libvirt_stream_protocols,
'qemu_spice': self._qemu_support_spice(),
'qemu_stream': self.qemu_stream,
+ 'hostarch': platform.machine(),
'screenshot': VMScreenshot.get_stream_test_result(),
'system_report_tool': bool(report_tool),
'update_tool': update_tool,
diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py
index 58686cd..8f69cef 100644
--- a/src/kimchi/model/vms.py
+++ b/src/kimchi/model/vms.py
@@ -20,10 +20,12 @@
from lxml.builder import E
import lxml.etree as ET
from lxml import etree, objectify
+import platform
import os
import random
import string
import time
+from datetime import datetime,timedelta
import uuid
from xml.etree import ElementTree
@@ -247,8 +249,13 @@ class VMModel(object):
self.groups = import_class('kimchi.model.host.GroupsModel')(**kargs)
def update(self, name, params):
+ if 'ticket' in params:
+ password = params['ticket'].get("passwd")
+ password = password if password is not None else "".join(
+ random.sample(string.ascii_letters + string.digits, 8))
+ params['ticket']['passwd'] = password
dom = self.get_vm(name, self.conn)
- dom = self._static_vm_update(dom, params)
+ dom = self._static_vm_update(dom, params, name)
self._live_vm_update(dom, params)
return dom.name().decode('utf-8')
@@ -305,41 +312,107 @@ class VMModel(object):
os_elem = E.os({"distro": distro, "version": version})
set_metadata_node(dom, os_elem)
- def _update_graphics(self, dom, xml, params):
- root = objectify.fromstring(xml)
- graphics = root.devices.find("graphics")
- if graphics is None:
+ def _update_graphics(self, dom, xml, params, name):
+## root = objectify.fromstring(xml)
+## graphics = root.devices.find("graphics")
+
+ curgraphics = self._vm_get_graphics(name)
+ graphics_type, graphics_listen, graphics_port, graphics_password, graphics_validto = curgraphics
+
+ if graphics_type is None :
return xml
+
+ if graphics_password != None : params['graphics']['passwd'] = graphics_password
+ if graphics_validto != None :
+ params['graphics']['passwdValidTo'] = graphics_validto
- password = params['graphics'].get("passwd")
- if password is not None and len(password.strip()) == 0:
- password = "".join(random.sample(string.ascii_letters +
+ try:
+ ## Determine if a change in graphics type has been submitted
+ if ('graphics' in params) and (params['graphics']['type'] != None):
+ hasGraphicTypeChange = True
+ except:
+ hasGraphicTypeChange = False
+
+ if hasGraphicTypeChange:
+ newGraphicsType=params['graphics']['type']
+
+ ## Remove current graphics implementations by node
+ newxml=xml
+ root = etree.fromstring(newxml)
+ devices = root.find("devices")
+ if graphics_type == 'vnc':
+ nodeDelete=[devices.find("graphics"),devices.find("video")]
+ elif graphics_type == 'spice':
+ nodeDelete=[devices.find("graphics"),devices.find("channel"), devices.find("video")]
+ elif graphics_type == 'serial':
+ nodeDelete=[devices.find("serial"),devices.find("console")]
+ else:
+ nodeDelete=''
+ if nodeDelete:
+ for i in nodeDelete:
+ devices.remove(i)
+ ## Add new graphics information back in by node
+ if newGraphicsType == 'spice':
+ newchannel=etree.SubElement(devices,'channel',type='spicevmc')
+ newtarget=etree.SubElement(newchannel,'target',type='virtio',name='com.redhat.spice.0')
+ newgraphics=etree.SubElement(devices,'graphics',type='spice', listen='127.0.0.1', autoport='yes')
+ if 'passwd' in params['graphics']: newgraphics.attrib['passwd'] = graphics_password
+ if 'passwdValidTo' in params['graphics']: newgraphics.attrib['passwdValidTo'] = self._convert_datetime(str(graphics_validto))
+ newlisten=etree.SubElement(newgraphics,'listen',type='address',address='127.0.0.1')
+ elif newGraphicsType == 'vnc':
+ newgraphics=etree.SubElement(devices,'graphics',type='vnc', listen='127.0.0.1', autoport='yes', port='-1')
+ newlisten=etree.SubElement(newgraphics,'listen',type='address',address='127.0.0.1')
+ if 'passwd' in params['graphics']: newgraphics.attrib['passwd'] = graphics_password
+ if 'passwdValidTo' in params['graphics']: newgraphics.attrib['passwdValidTo'] = self._convert_datetime(str(graphics_validto))
+
+ elif newGraphicsType == 'serial':
+ newconsole=etree.SubElement(devices,'console',type='pty')
+ newtarget=etree.SubElement(newconsole,'target',type='serial', port='0')
+ newaddress=etree.SubElement(newconsole,'address',type='spapr-vio', reg='0x30000000')
+ xml = etree.tostring(root)
+ if ('passwd' in params['graphics']) or ('passwdValidTo' in params['graphics']):
+ root = objectify.fromstring(xml)
+ graphics = root.devices.find("graphics")
+
+ password = params['graphics'].get("passwd")
+ if password is not None and len(password.strip()) == 0:
+ password = "".join(random.sample(string.ascii_letters +
string.digits, 8))
- if password is not None:
- graphics.attrib['passwd'] = password
+ if password is not None:
+ graphics.attrib['passwd'] = password
- expire = params['graphics'].get("passwdValidTo")
- to = graphics.attrib.get('passwdValidTo')
- if to is not None:
- if (time.mktime(time.strptime(to, '%Y-%m-%dT%H:%M:%S'))
- - time.time() <= 0):
- expire = expire if expire is not None else 30
+ expire = params['graphics'].get("passwdValidTo")
+ to = graphics.attrib.get('passwdValidTo')
+ if to is not None:
+ if (time.mktime(time.strptime(to, '%Y-%m-%dT%H:%M:%S'))
+ - time.time() <= 0):
+ expire = expire if expire is not None else 30
- if expire is not None:
- expire_time = time.gmtime(time.time() + float(expire))
- valid_to = time.strftime('%Y-%m-%dT%H:%M:%S', expire_time)
- graphics.attrib['passwdValidTo'] = valid_to
+ if expire is not None:
+ expire_time = time.gmtime(time.time() + float(expire))
+ valid_to = time.strftime('%Y-%m-%dT%H:%M:%S', expire_time)
+ graphics.attrib['passwdValidTo'] = valid_to
- if not dom.isActive():
- return ET.tostring(root, encoding="utf-8")
+ if not dom.isActive():
+ return ET.tostring(root, encoding="utf-8")
- xml = dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE)
- dom.updateDeviceFlags(etree.tostring(graphics),
+ xml = dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE)
+ dom.updateDeviceFlags(etree.tostring(graphics),
libvirt.VIR_DOMAIN_AFFECT_LIVE)
return xml
- def _static_vm_update(self, dom, params):
+ def _convert_datetime(self,deltaseconds):
+ deltaseconds = int(deltaseconds)
+ # takes the delta (in seconds) between the current time
+ # and the expiration time for the vnc password
+ mycur = datetime.strptime((time.strftime('%Y-%m-%dT%H:%M:%S',time.gmtime())),'%Y-%m-%dT%H:%M:%S')
+ expiredate = time.strptime((str(mycur + timedelta(seconds=deltaseconds))),'%Y-%m-%d %H:%M:%S')
+
+ return time.strftime('%Y-%m-%dT%H:%M:%S',expiredate)
+
+
+ def _static_vm_update(self, dom, params, name):
state = DOM_STATE_MAP[dom.info()[0]]
old_xml = new_xml = dom.XMLDesc(0)
@@ -355,7 +428,7 @@ class VMModel(object):
new_xml = xmlutils.xml_item_update(new_xml, xpath, val)
if 'graphics' in params:
- new_xml = self._update_graphics(dom, new_xml, params)
+ new_xml = self._update_graphics(dom, new_xml, params, name)
conn = self.conn.get()
try:
@@ -383,15 +456,35 @@ class VMModel(object):
dom = ElementTree.fromstring(dom.XMLDesc(0))
return dom.find('devices/video') is not None
+ def _get_ticket(self, dom):
+ xml = dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE)
+ root = objectify.fromstring(xml)
+ graphic = root.devices.find("graphics")
+ if graphic is None:
+ return {"passwd": None, "expire": None}
+ passwd = graphic.attrib.get('passwd')
+ ticket = {"passwd": passwd}
+ valid_to = graphic.attrib.get('passwdValidTo')
+ ticket['expire'] = None
+ if valid_to is not None:
+ to = time.mktime(time.strptime(valid_to, '%Y-%m-%dT%H:%M:%S'))
+ ticket['expire'] = to - time.mktime(time.gmtime())
+
+ return ticket
+
def lookup(self, name):
dom = self.get_vm(name, self.conn)
info = dom.info()
state = DOM_STATE_MAP[info[0]]
screenshot = None
- # (type, listen, port, passwd, passwdValidTo)
graphics = self._vm_get_graphics(name)
+ graphics_type = graphics[0]
graphics_port = graphics[2]
graphics_port = graphics_port if state == 'running' else None
+ if graphics_type is None and platform.machine() == "ppc64":
+ ## Checking for ppc64 serial console
+ graphics_type = "serial"
+
try:
if state == 'running' and self._has_video(dom):
screenshot = self.vmscreenshot.lookup(name)
@@ -539,8 +632,21 @@ class VMModel(object):
expr = "/domain/devices/graphics/@type"
res = xmlutils.xpath_get_text(xml, expr)
- graphics_type = res[0] if res else None
-
+ if res:
+ graphics_type = res[0]
+ else:
+ ## Checking for serial console, if not
+ ## returning None
+ expr = "/domain/devices/console/@type"
+ res = xmlutils.xpath_get_text(xml, expr)
+ if res:
+ if res[0] == "pty":
+ graphics_type = "serial"
+ else:
+ graphics_type = None
+ else:
+ graphics_type = None
+
expr = "/domain/devices/graphics/@listen"
res = xmlutils.xpath_get_text(xml, expr)
graphics_listen = res[0] if res else None
@@ -558,8 +664,15 @@ class VMModel(object):
expr = "/domain/devices/graphics[@type='%s']/@passwdValidTo"
res = xmlutils.xpath_get_text(xml, expr % graphics_type)
if res:
- to = time.mktime(time.strptime(res[0], '%Y-%m-%dT%H:%M:%S'))
- graphics_passwdValidTo = to - time.mktime(time.gmtime())
+ currdate = datetime.strptime((time.strftime('%Y-%m-%dT%H:%M:%S',time.gmtime())),'%Y-%m-%dT%H:%M:%S')
+ graphics_expiredate = (datetime.strptime(res[0],'%Y-%m-%dT%H:%M:%S'))
+ mymax = max((currdate, graphics_expiredate))
+ expirediff = currdate - graphics_expiredate
+ if mymax == currdate:
+ ## time has expired
+ graphics_passwdValidTo = abs(expirediff).seconds * -1
+ else:
+ graphics_passwdValidTo = abs(expirediff).seconds
return (graphics_type, graphics_listen, graphics_port,
graphics_passwd, graphics_passwdValidTo)
@@ -567,6 +680,8 @@ class VMModel(object):
def connect(self, name):
# (type, listen, port, passwd, passwdValidTo)
graphics_port = self._vm_get_graphics(name)[2]
+ graphics = self._vm_get_graphics(name)
+ graphics_type, graphics_listen, graphics_port = graphics
if graphics_port is not None:
vnc.add_proxy_token(name, graphics_port)
else:
diff --git a/src/kimchi/vmtemplate.py b/src/kimchi/vmtemplate.py
index c5bb7b3..f735d14 100644
--- a/src/kimchi/vmtemplate.py
+++ b/src/kimchi/vmtemplate.py
@@ -217,12 +217,21 @@ drive=drive-%(bus)s0-1-0,id=%(bus)s0-1-0'/>
<target type='virtio' name='com.redhat.spice.0'/>
</channel>
"""
+
+ serial_xml = """
+ <console type='pty'>
+ <target type='serial' port='0'/>
+ <address type='spapr-vio' reg='0x30000000'/>
+ </console>
+ """
graphics = dict(self.info['graphics'])
if params:
graphics.update(params)
graphics_xml = graphics_xml % graphics
if graphics['type'] == 'spice':
graphics_xml = graphics_xml + spicevmc_xml
+ elif graphics['type'] == 'serial' :
+ graphics_xml = serial_xml
return graphics_xml
def _get_scsi_disks_xml(self):
diff --git a/ui/css/theme-default/guest-edit.css b/ui/css/theme-default/guest-edit.css
index 74c2237..8515cda 100644
--- a/ui/css/theme-default/guest-edit.css
+++ b/ui/css/theme-default/guest-edit.css
@@ -63,6 +63,12 @@
width: 470px;
}
+.guest-edit-wrapper-controls > .dropdown {
+ margin: 5px 0 0 1px;
+ width: 440px;
+}
+
+
#form-guest-edit-storage .guest-edit-wrapper-controls {
width: 486px;
}
diff --git a/ui/css/theme-default/storage.css b/ui/css/theme-default/storage.css
index f635c2f..eeb783c 100644
--- a/ui/css/theme-default/storage.css
+++ b/ui/css/theme-default/storage.css
@@ -222,64 +222,6 @@
width: 13px;
}
-.toolable {
- position: relative;
-}
-
-.toolable .tooltip {
- display: none;
- border: 2px solid #0B6BAD;
- background: #fff;
- padding: 6px;
- position: absolute;
- color: #666666;
- font-weight: bold;
- font-size: 11px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- border-radius: 5px;
- z-index: 100;
- top: -300%;
- left: -140%;
- white-space: nowrap;
-}
-
-.toolable:hover .tooltip {
- display: block;
-}
-
-.toolable .tooltip:after {
- -moz-border-bottom-colors: none;
- -moz-border-left-colors: none;
- -moz-border-right-colors: none;
- -moz-border-top-colors: none;
- border-color: #fff transparent transparent;
- border-image: none;
- border-style: solid;
- border-width: 7px;
- content: "";
- display: block;
- left: 15px;
- position: absolute;
- bottom: -14px;
-}
-
-.toolable .tooltip:before {
- -moz-border-bottom-colors: none;
- -moz-border-left-colors: none;
- -moz-border-right-colors: none;
- -moz-border-top-colors: none;
- border-color: #0B6BAD transparent transparent;
- border-image: none;
- border-style: solid;
- border-width: 8px;
- content: "";
- display: block;
- left: 14px;
- position: absolute;
- bottom: -18px;
-}
-
.inactive {
background: #E80501;
background: linear-gradient(to bottom, #E88692 0%, #E84845 50%,
diff --git a/ui/js/src/kimchi.guest_edit_main.js b/ui/js/src/kimchi.guest_edit_main.js
index 938dfd9..2e71736 100644
--- a/ui/js/src/kimchi.guest_edit_main.js
+++ b/ui/js/src/kimchi.guest_edit_main.js
@@ -386,6 +386,7 @@ kimchi.guest_edit_main = function() {
};
initStorageListeners();
+
setupInterface();
setupPermission();
@@ -398,6 +399,31 @@ kimchi.guest_edit_main = function() {
kimchi.topic('kimchi/vmCDROMReplaced').unsubscribe(onReplaced);
kimchi.topic('kimchi/vmCDROMDetached').unsubscribe(onDetached);
};
+
+ graphics_type = guest.graphics.type
+
+ var graphicsForm = $('#form-guest-edit-general');
+ $('input[name="graphics"]', graphicsForm).val(graphics_type);
+
+
+ var vncOpt = [{label: 'VNC', value: 'vnc'}];
+ kimchi.select('guest-edit-general-graphics-list', vncOpt);
+
+ var enableSpice = function() {
+ if (kimchi.capabilities == undefined) {
+ setTimeout(enableSpice, 2000);
+ return;
+ }
+ if (kimchi.capabilities.qemu_spice == true) {
+ spiceOpt = [{label: 'Spice', value: 'spice'}]
+ kimchi.select('guest-edit-general-graphics-list', spiceOpt);
+ }
+ if (kimchi.capabilities.hostarch == 'ppc64') {
+ serialOpt = [{label: 'Serial', value: 'serial'}]
+ kimchi.select('guest-edit-general-graphics-list', serialOpt);
+ }
+ };
+ enableSpice();
};
kimchi.retrieveVM(kimchi.selectedGuest, initContent);
@@ -405,13 +431,15 @@ kimchi.guest_edit_main = function() {
var generalSubmit = function(event) {
$(saveButton).prop('disabled', true);
var data=$('#form-guest-edit-general').serializeObject();
+ graphics_type=data.graphics
if(data['memory']!=undefined) {
data['memory'] = Number(data['memory']);
}
if(data['cpus']!=undefined) {
data['cpus'] = Number(data['cpus']);
}
-
+ data['graphics'] = {}
+ data['graphics']['type'] = graphics_type
kimchi.updateVM(kimchi.selectedGuest, data, function() {
kimchi.listVmsAuto();
kimchi.window.close();
diff --git a/ui/js/src/kimchi.guest_main.js b/ui/js/src/kimchi.guest_main.js
index dbe8753..5731634 100644
--- a/ui/js/src/kimchi.guest_main.js
+++ b/ui/js/src/kimchi.guest_main.js
@@ -266,11 +266,26 @@ kimchi.createGuestLi = function(vmObject, prevScreenImage, openMenu) {
}
var consoleActions=guestActions.find("[name=vm-console]");
+ if ((vmObject.graphics['type'] == 'vnc') || (vmObject.graphics['type'] == 'spice')) {
+ if (vmRunningBool) {
+ consoleActions.on("click", kimchi.openVmConsole);
+ consoleActions.show();
+ }
+ else {
+ consoleActions.hide();
+ consoleActions.off("click",kimchi.openVmConsole);
- if ((vmObject.graphics['type'] == 'vnc') || (vmObject.graphics['type'] == 'spice')) {
- consoleActions.on("click", kimchi.openVmConsole);
- consoleActions.show();
- } else { //we don't recognize the VMs supported graphics, so hide the menu choice
+ }
+
+ }
+ else if ((vmObject.graphics['type'] == 'serial') && (vmRunningBool)) {
+ consoleActions.prop('disabled', true);
+ tooltip = $("#connectserial label")
+ tooltip.show()
+ tooltip.addClass("tooltip")
+ consoleActions.show();
+ }
+ else { //we don't recognize the VMs supported graphics, so hide the menu choice
consoleActions.hide();
consoleActions.off("click",kimchi.openVmConsole);
}
diff --git a/ui/js/src/kimchi.template_edit_main.js b/ui/js/src/kimchi.template_edit_main.js
index cb43091..68d1a07 100644
--- a/ui/js/src/kimchi.template_edit_main.js
+++ b/ui/js/src/kimchi.template_edit_main.js
@@ -48,6 +48,10 @@ kimchi.template_edit_main = function() {
var vncOpt = [{label: 'VNC', value: 'vnc'}];
kimchi.select('template-edit-graphics-list', vncOpt);
+ if (kimchi.capabilities.hostarch == 'ppc64') {
+ serialOpt = [{label: 'Serial', value: 'serial'}]
+ kimchi.select('template-edit-graphics-list', serialOpt);
+ }
var enableSpice = function() {
if (kimchi.capabilities == undefined) {
setTimeout(enableSpice, 2000);
diff --git a/ui/pages/guest-edit.html.tmpl b/ui/pages/guest-edit.html.tmpl
index 0b7dad3..1e5e2b4 100644
--- a/ui/pages/guest-edit.html.tmpl
+++ b/ui/pages/guest-edit.html.tmpl
@@ -41,6 +41,7 @@
<li>
<a href="#form-guest-edit-permission">$_("Permission")</a>
</li>
+ </li>
</ul>
<form id="form-guest-edit-general">
<fieldset class="guest-edit-fieldset">
@@ -93,6 +94,26 @@
type="text"
disabled="disabled" />
</div>
+
+
+ <div>
+ <div class="guest-edit-wrapper-label">
+ <label for="guest-edit-graphics-textbox">
+ $_("Graphics")
+ </label>
+ </div>
+
+ <div class="guest-edit-wrapper-controls">
+ <div class="btn dropdown popable">
+ <input id="guest-edit-general-graphics" name="graphics" type="hidden"/>
+
+ <span class="text" id="guest-edit-general-graphics-label"></span><span class="arrow"></span>
+ <div class="popover" style="width: 100%">
+ <ul class="select-list" id="guest-edit-general-graphics-list" data-target="guest-edit-general-graphics" data-label="guest-edit-general-graphics-label">
+ </ul>
+ </div>
+ </div>
+ </div>
</div>
</fieldset>
</form>
diff --git a/ui/pages/guest.html.tmpl b/ui/pages/guest.html.tmpl
index 43fb350..de3b6bd 100644
--- a/ui/pages/guest.html.tmpl
+++ b/ui/pages/guest.html.tmpl
@@ -55,7 +55,10 @@
<div name="actionmenu" class="btn dropdown popable vm-action" style="width: 70px">
<span class="text">$_("Actions")</span><span class="arrow"></span>
<div class="popover actionsheet right-side" style="width: 250px">
- <button class="button-big shutoff-disabled" name="vm-console" ><span class="text">$_("Connect")</span></button>
+ <button class="button-big toolable shutoff-disabled" name="vm-console"><span class="text">$_("Connect")</span><label name="connectserial" class="tooltip connectserial" display="hidden">$_("Use virsh console to connect to this guest")</label></button>
+ <!-- <button class="button-big toolable shutoff-disabled" name="vm-console"><span class="text">$_("Connect")</span></button> -->
+
+ <button class="button-big shutoff-disabled" name="vm-media"><span class="text">$_("Manage Media")</span></button>
<button class="button-big running-disabled" name="vm-edit"><span class="text">$_("Edit")</span></button>
<button class="button-big shutoff-hidden" name="vm-reset"><span class="text">$_("Reset")</span></button>
<button class="button-big shutoff-hidden" name="vm-shutdown"><span class="text">$_("Shut Down")</span></button>
diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl
index d920ae2..f25c6bb 100644
--- a/ui/pages/i18n.json.tmpl
+++ b/ui/pages/i18n.json.tmpl
@@ -172,4 +172,5 @@
"KCHVMSTOR0001E": "$_("CDROM path need to be a valid local path and cannot be blank.")",
"KCHVMSTOR0002E": "$_("Disk pool or volume cannot be blank.")"
+
}
diff --git a/ui/pages/tabs/storage.html.tmpl b/ui/pages/tabs/storage.html.tmpl
index 87205bd..563504d 100644
--- a/ui/pages/tabs/storage.html.tmpl
+++ b/ui/pages/tabs/storage.html.tmpl
@@ -58,10 +58,10 @@
</div>
<div class="storage-state">
<div class="status-dot toolable active" data-state="{state}">
- <label class="tooltip">$_("active")</label>
+ <label class="tooltip storage">$_("active")</label>
</div>
<div class="status-dot toolable inactive" data-state="{state}">
- <label class="tooltip">$_("inactive")</label>
+ <label class="tooltip storage">$_("inactive")</label>
</div>
</div>
<div class="storage-location">
--
1.9.3
10 years, 3 months
[PATCH] i18n support: Update help Makefile with new languages.
by Paulo Vital
This patch adds the new languages list in ui/pages/help/Makefile.am file
to be recursively compiled/built/archived.
The new languages are: German (de_DE), Spanish (es_ES), Franch (fr_FR),
Italian (it_IT), Japanese (ja_JP), Korean (ko_KR), Russian (ru_RU) and
the Traditional Chinese (zh_TW).
Signed-off-by: Paulo Vital <pvital(a)linux.vnet.ibm.com>
---
ui/pages/help/Makefile.am | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ui/pages/help/Makefile.am b/ui/pages/help/Makefile.am
index fa9786b..ca6b3f7 100644
--- a/ui/pages/help/Makefile.am
+++ b/ui/pages/help/Makefile.am
@@ -14,7 +14,7 @@
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-SUBDIRS = en_US pt_BR zh_CN
+SUBDIRS = en_US pt_BR zh_CN de_DE es_ES fr_FR it_IT ja_JP ko_KR ru_RU zh_TW
DITA_HTML_FILES = $(patsubst %.dita,%.html,$(wildcard */*.dita))
HTML_FILES = $(if $(DITA_HTML_FILES), $(DITA_HTML_FILES), $(wildcard */*.html))
--
1.9.3
10 years, 3 months
[PATCH] bug fix: Properly set max body size to nginx proxy
by Aline Manera
Depends on:
- [Kimchi-devel] [PATCH] Fix: Use "max_request_body_size" value as int instead of string
Aline Manera (1):
bug fix: Properly set max body size to nginx proxy
src/kimchi/proxy.py | 28 ++++++++++-------------
src/nginx.conf.in | 2 ++
tests/test_rest.py | 64 ++++++++++++++++++++++++++++++++++++++++++-----------
3 files changed, 65 insertions(+), 29 deletions(-)
--
1.9.3
10 years, 3 months
[PATCH 0/2 V2] bug fix: Properly set max body size to nginx proxy
by Aline Manera
V1 -> V2:
- Close file descriptors in test cases
- Set max body size unit on nginx config file. For it add {} around Kimchi
variables
Aline Manera (2):
Identify Kimchi variables from nginx config variables in nginx.conf.in
file
bug fix: Properly set max body size to nginx proxy
src/kimchi/proxy.py | 25 +++++++------------
src/nginx.conf.in | 18 ++++++++------
tests/test_rest.py | 71 ++++++++++++++++++++++++++++++++++++++++-------------
3 files changed, 73 insertions(+), 41 deletions(-)
--
1.9.3
10 years, 3 months
[PATCH] model.host: considering older libvirt versions
by Daniel Henrique Barboza
The last changes in the file didn't consider the case where
the fc_host capability isn't implemented by libvirt < 1.0.5
(such as RHEL6 distros). This patch fixes a crash that occurs
in such ocasions.
Signed-off-by: Daniel Henrique Barboza <danielhb(a)linux.vnet.ibm.com>
---
src/kimchi/model/host.py | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py
index 553ad7c..4637b1c 100644
--- a/src/kimchi/model/host.py
+++ b/src/kimchi/model/host.py
@@ -279,8 +279,7 @@ class DevicesModel(object):
def __init__(self, **kargs):
self.conn = kargs['conn']
self.cap_map = \
- {'fc_host': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_FC_HOST,
- 'net': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_NET,
+ {'net': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_NET,
'pci': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_PCI_DEV,
'scsi': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_SCSI,
'scsi_host': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_SCSI_HOST,
@@ -288,6 +287,15 @@ class DevicesModel(object):
'usb_device': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_USB_DEV,
'usb':
libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_USB_INTERFACE}
+ # TODO: when no longer supporting Libvirt < 1.0.5 distros
+ # (like RHEL6) remove this verification and insert the
+ # key 'fc_host' with the libvirt variable in the hash
+ # declaration above.
+ try:
+ self.cap_map['fc_host'] = \
+ libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_FC_HOST
+ except AttributeError:
+ self.cap_map['fc_host'] = None
def get_list(self, _cap=None):
if _cap == 'fc_host':
@@ -316,6 +324,12 @@ class DevicesModel(object):
if 'fc_host' in xmlutils.xpath_get_text(xml, path):
ret.append(host)
return ret
+ # Double verification to catch the case where the libvirt
+ # supports fc_host but does not, for some reason, recognize
+ # the libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_FC_HOST
+ # attribute.
+ if not self.cap_map['fc_host']:
+ return conn.listDevices('fc_host', 0)
return self._get_devices_with_capability('fc_host')
--
1.8.3.1
10 years, 3 months
[PATCH] Increase read chunk size to 1MB while uploading file
by Aline Manera
With a small chunk size (the previous chunk size was 8k), the server needs to
make many read/write operations which was consuming a lot of memory from
Kimchi server for larger files. So increase it to 1MB to do not overload
the server.
Signed-off-by: Aline Manera <alinefm(a)linux.vnet.ibm.com>
---
src/kimchi/model/storagevolumes.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py
index ef8e684..4d0c231 100644
--- a/src/kimchi/model/storagevolumes.py
+++ b/src/kimchi/model/storagevolumes.py
@@ -41,7 +41,7 @@ VOLUME_TYPE_MAP = {0: 'file',
3: 'network'}
-DOWNLOAD_CHUNK_SIZE = 1048576 # 1 MiB
+READ_CHUNK_SIZE = 1048576 # 1 MiB
class StorageVolumesModel(object):
@@ -97,7 +97,7 @@ class StorageVolumesModel(object):
size = 0
with open(file_path, 'wb') as f:
while True:
- data = upload_file.file.read(8192)
+ data = upload_file.file.read(READ_CHUNK_SIZE)
if not data:
break
size += len(data)
@@ -175,7 +175,7 @@ class StorageVolumesModel(object):
try:
while True:
- chunk_data = response.read(DOWNLOAD_CHUNK_SIZE)
+ chunk_data = response.read(READ_CHUNK_SIZE)
if not chunk_data:
break
--
1.9.3
10 years, 3 months
[PATCH 0/5] List pending debug reports to all users
by Aline Manera
Today only the user who started the debug report creation is able to get
the view of a pending debug report. But while switching tabs, this information
is lost.
This patch set gets the pending debug reports by filtering running tasks by
target_uri=^/debugreports and list them among to the exinting debug reports.
That way all users will get the same view of which debug reports are being
generated.
Aline Manera (5):
Add function to get pending tasks according to filter
Add common function to track Task
Add function to list all pending debug reports
List pending debug reports while loading report grid
Only disable report buttons when the selected report is pending
ui/js/src/kimchi.api.js | 34 ++++++++++++-------
ui/js/src/kimchi.host.js | 65 ++++++++++++++++++++++++++++++++-----
ui/js/src/kimchi.report_add_main.js | 42 ++----------------------
3 files changed, 83 insertions(+), 58 deletions(-)
--
1.9.3
10 years, 3 months
[PATCH] Fix: Use "max_request_body_size" value as int instead of string
by Crístian Viana
The CherryPy variable "max_request_body_size" should be a number value,
in bytes. The current code runs something like
"eval('max_body_size' * 1024)", where 'max_body_size' is the value read
from the external configuration file, in kilobytes. Multiplying a string
by a number in Python creates a new string repeating the original string
a number of times. We expect a multiplication, not a string repetition.
This bug was introduced by commit e3b79e3.
Multiply the number 1024 by another number (i.e. the "eval" result), not
a string.
Signed-off-by: Crístian Viana <vianac(a)linux.vnet.ibm.com>
---
src/kimchi/server.py | 4 +++-
src/kimchid.in | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/kimchi/server.py b/src/kimchi/server.py
index 1c3b360..775313c 100644
--- a/src/kimchi/server.py
+++ b/src/kimchi/server.py
@@ -87,7 +87,9 @@ class Server(object):
# directly. You must go through the proxy.
cherrypy.server.socket_host = '127.0.0.1'
cherrypy.server.socket_port = options.cherrypy_port
- cherrypy.server.max_request_body_size = eval(options.max_body_size)
+
+ max_body_size_in_bytes = eval(options.max_body_size) * 1024
+ cherrypy.server.max_request_body_size = max_body_size_in_bytes
cherrypy.log.screen = True
cherrypy.log.access_file = options.access_log
diff --git a/src/kimchid.in b/src/kimchid.in
index 075b744..0746ba6 100644
--- a/src/kimchid.in
+++ b/src/kimchid.in
@@ -87,7 +87,7 @@ def main(options):
setattr(options, 'ssl_cert', config.config.get('server', 'ssl_cert'))
setattr(options, 'ssl_key', config.config.get('server', 'ssl_key'))
setattr(options, 'max_body_size',
- config.config.get('server', 'max_body_size')*1024)
+ config.config.get('server', 'max_body_size'))
kimchi.server.main(options)
--
1.9.3
10 years, 3 months
[WIP PATCH] Storage DL/UL
by Hongliang Wang
Volume.
Known kssues:
after add a remote URL and click "OK" button, manual click on the storage pool
to refresh the volumes.
Signed-off-by: Hongliang Wang <hlwang(a)linux.vnet.ibm.com>
---
src/kimchi/model/host.py | 2 +-
ui/css/theme-default/sp-add-volume.css | 36 ++++++++
ui/css/theme-default/storage.css | 34 +++++++-
ui/js/src/kimchi.api.js | 38 +++++++++
ui/js/src/kimchi.sp_add_volume_main.js | 152 +++++++++++++++++++++++++++++++++
ui/js/src/kimchi.storage_main.js | 19 ++++-
ui/pages/i18n.json.tmpl | 3 +
ui/pages/sp-add-volume.html.tmpl | 80 +++++++++++++++++
ui/pages/tabs/storage.html.tmpl | 12 ++-
9 files changed, 369 insertions(+), 7 deletions(-)
create mode 100644 ui/css/theme-default/sp-add-volume.css
create mode 100644 ui/js/src/kimchi.sp_add_volume_main.js
create mode 100644 ui/pages/sp-add-volume.html.tmpl
diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py
index 933a142..2052f58 100644
--- a/src/kimchi/model/host.py
+++ b/src/kimchi/model/host.py
@@ -280,7 +280,7 @@ class DevicesModel(object):
def __init__(self, **kargs):
self.conn = kargs['conn']
self.cap_map = \
- {'fc_host': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_FC_HOST,
+ {#'fc_host': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_FC_HOST,
'net': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_NET,
'pci': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_PCI_DEV,
'scsi': libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_SCSI,
diff --git a/ui/css/theme-default/sp-add-volume.css b/ui/css/theme-default/sp-add-volume.css
new file mode 100644
index 0000000..2c31b9a
--- /dev/null
+++ b/ui/css/theme-default/sp-add-volume.css
@@ -0,0 +1,36 @@
+/*
+ * Project Kimchi
+ *
+ * Copyright IBM, Corp. 2013-2014
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#sp-add-volume-window {
+ height: 400px;
+ width: 500px;
+}
+
+#sp-add-volume-window .textbox-wrapper input[type="text"] {
+ box-sizing: border-box;
+ width: 100%;
+}
+
+#sp-add-volume-window .textbox-wrapper label {
+ vertical-align: middle;
+}
+
+#sp-add-volume-window input[type="text"][disabled] {
+ color: #bbb;
+ background-color: #fafafa;
+ cursor: not-allowed;
+}
diff --git a/ui/css/theme-default/storage.css b/ui/css/theme-default/storage.css
index f635c2f..02c92e9 100644
--- a/ui/css/theme-default/storage.css
+++ b/ui/css/theme-default/storage.css
@@ -336,7 +336,6 @@
float: left;
padding: 4px;
margin-bottom: 5px;
- height: 40px;
width: 130px;
}
@@ -622,3 +621,36 @@
#iSCSITarget input {
width: 493px;
}
+
+/* Progress bar */
+.volume-progress {
+ clear: both;
+ width: 140px;
+}
+
+.volume-progress .progress-bar-outer {
+ background: #ccc;
+ height: 4px;
+ overflow: hidden;
+ width: 100%;
+}
+
+.volume-progress .progress-bar-inner {
+ background: #090;
+ height: 100%;
+ width: 0%;
+}
+
+.volume-progress .progress-label {
+ color: #999;
+ font-size: 10px;
+ line-height: 16px;
+}
+
+.volume-progress .progress-status {
+}
+
+.volume-progress .progress-downloaded {
+ float: right;
+}
+/* End of Progress bar */
diff --git a/ui/js/src/kimchi.api.js b/ui/js/src/kimchi.api.js
index 5fc456d..766df6a 100644
--- a/ui/js/src/kimchi.api.js
+++ b/ui/js/src/kimchi.api.js
@@ -1130,5 +1130,43 @@ var kimchi = {
kimchi.message.error(data.responseJSON.reason);
}
});
+ },
+
+ /**
+ * Add a volume to a given storage pool.
+ */
+ uploadVolumeToSP: function(settings, suc, err) {
+ var fd = settings['formData'];
+ var sp = encodeURIComponent(settings['sp']);
+ kimchi.requestJSON({
+ url : kimchi.url + 'storagepools/' + sp + '/storagevolumes',
+ type : 'POST',
+ data : fd,
+ processData : false,
+ contentType : false,
+ success : suc,
+ error : err
+ });
+ },
+
+ /**
+ * Add a volume to a given storage pool by URL.
+ */
+ downloadVolumeToSP: function(settings, suc, err) {
+ var url = settings['url'];
+ var name = settings['name'];
+ var sp = encodeURIComponent(settings['sp']);
+ kimchi.requestJSON({
+ url : kimchi.url + 'storagepools/' + sp + '/storagevolumes',
+ type : 'POST',
+ data : JSON.stringify({
+ name: name,
+ url: url
+ }),
+ contentType : 'application/json',
+ dataType : 'json',
+ success : suc,
+ error : err
+ });
}
};
diff --git a/ui/js/src/kimchi.sp_add_volume_main.js b/ui/js/src/kimchi.sp_add_volume_main.js
new file mode 100644
index 0000000..e4f25fc
--- /dev/null
+++ b/ui/js/src/kimchi.sp_add_volume_main.js
@@ -0,0 +1,152 @@
+/*
+ * Project Kimchi
+ *
+ * Copyright IBM, Corp. 2013-2014
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+kimchi.sp_add_volume_main = function() {
+ // download from remote server or upload from local file
+ var type = 'download';
+
+ var addButton = $('#sp-add-volume-button');
+
+ $('input.volume-type').change(function(e) {
+ $('.volume-input').prop('disabled', true);
+ $('.volume-input.' + this.value).prop('disabled', false);
+ type = this.value;
+ });
+
+ var volumeName;
+ var taskID = -1;
+
+ var onAccepted = function() {
+ if(inited[taskID]) {
+ return;
+ }
+ inited[taskID] = true;
+ kimchi.window.close();
+
+ kimchi.topic('kimchi/volumeDownloadStarted').publish({
+ sp: kimchi.selectedSP
+ });
+ };
+ var inited = {};
+
+ var onError = function(result) {
+ if (result['message']) {
+ var errText = result['message'];
+ }
+ else {
+ var errText = result['responseJSON']['reason'];
+ }
+ result && kimchi.message.error(errText);
+ var volumeName = result['target_uri'].split('/').pop();
+ var volumeBox = $('#volume' + kimchi.selectedSP + ' [data-volume-name="' + volumeName + '"]');
+ $('.progress-status', volumeBox).text(i18n['KCHPOOL6016M']);
+ };
+
+ var fetchRemoteFile = function() {
+ var volumeURL = $('#volume-remote-url').val();
+ volumeName = volumeURL.split(/(\\|\/)/g).pop();
+ kimchi.downloadVolumeToSP({
+ sp: kimchi.selectedSP,
+ name: volumeName,
+ url: volumeURL
+ }, function(resp) {
+ taskID = resp['id'];
+ onAccepted();
+ }, onError);
+ };
+
+ var onProgress = function(resp) {
+ var sizeArray = resp['message'].split('/');
+ var downloaded = sizeArray[0];
+ var percent = 0;
+ if(isNaN(downloaded)) {
+ downloaded = 0;
+ }
+ else {
+ var total = sizeArray[1];
+ percent = downloaded / total * 100;
+ }
+ var formatted = kimchi.formatMeasurement(downloaded);
+ var size = formatted['v'].toFixed(1) + formatted['s'];
+ var volumeBox = $('#volume' + kimchi.selectedSP + ' [data-volume-name="' + volumeName + '"]');
+ $('.volume-progress', volumeBox).removeClass('hidden');
+ $('.progress-bar-inner', volumeBox).css({
+ width: percent + '%'
+ });
+ $('.progress-status', volumeBox).text(i18n['KCHPOOL6014M']);
+ $('.progress-downloaded', volumeBox).text(size);
+ };
+
+ var onTaskResponse = function(result) {
+ var taskStatus = result['status'];
+ switch(taskStatus) {
+ case 'running':
+ onProgress(result);
+ setTimeout(function() {
+ trackTask(result['id']);
+ }, 1500);
+ break;
+ case 'finished':
+ taskID = -1;
+ var volumeName = result['target_url'].split('/').pop();
+ var volumeBox = $('#volume' + kimchi.selectedSP + ' [data-volume-name="' + volumeName + '"]');
+ $('.progress-status', volumeBox).text(i18n['KCHPOOL6015M']);
+ break;
+ case 'failed':
+ taskID = -1;
+ onError(result);
+ break;
+ default:
+ break;
+ }
+ };
+
+ var trackTask = function(task) {
+ kimchi.getTask(task, onTaskResponse, function(resp) {});
+ };
+
+ var uploadFile = function() {
+ var blobFile = $('#volume-input-file')[0].files[0];
+ var fileName = blobFile.name;
+ var fd = new FormData();
+ fd.append('name', fileName);
+ fd.append('file', blobFile);
+ kimchi.uploadVolumeToSP({
+ sp: kimchi.selectedSP,
+ formData: fd
+ }, function(resp) {
+ });
+ };
+
+ $(addButton).on('click', function(event) {
+ $(this).prop('disabled', true);
+ if(type === 'download') {
+ fetchRemoteFile();
+ }
+ else {
+ uploadFile();
+ }
+ event.preventDefault();
+ });
+
+ kimchi.topic('kimchi/volumesListed').subscribe(function() {
+ var volumeBox = $('#volume' + kimchi.selectedSP + ' [data-volume-name="' + volumeName + '"]');
+ $(volumeBox).removeClass('hidden');
+ $('.progress-status', volumeBox).text(i18n['KCHPOOL6014M']);
+ taskID > -1 && trackTask(taskID);
+ });
+};
diff --git a/ui/js/src/kimchi.storage_main.js b/ui/js/src/kimchi.storage_main.js
index ae3f963..56252a8 100644
--- a/ui/js/src/kimchi.storage_main.js
+++ b/ui/js/src/kimchi.storage_main.js
@@ -135,6 +135,12 @@ kimchi.storageBindClick = function() {
}
});
+ $('.pool-add-volume').on('click', function(event) {
+ var poolName = $(this).data('name');
+ kimchi.selectedSP = poolName;
+ kimchi.window.open('sp-add-volume.html');
+ });
+
$('.storage-action').on('click', function() {
var storage_action = $(this);
var deleteButton = storage_action.find('.pool-delete');
@@ -149,10 +155,6 @@ kimchi.storageBindClick = function() {
$("#logicalPoolExtend").dialog("option", "poolName", $(this).data('name'));
$("#logicalPoolExtend").dialog("open");
});
-
- $('#volume-doAdd').on('click', function() {
- kimchi.window.open('storagevolume-add.html');
- });
}
$('.storage-li').on('click', function(event) {
@@ -195,6 +197,9 @@ kimchi.doListVolumes = function(poolObj) {
poolObj.removeClass('in');
kimchi.changeArrow(handleArrow);
slide.slideDown('slow');
+ setTimeout(function() {
+ kimchi.topic('kimchi/volumesListed').publish();
+ }, 1500);
}
}, function(err) {
kimchi.message.error(err.responseJSON.reason);
@@ -255,6 +260,12 @@ kimchi.storage_main = function() {
}
kimchi.doListStoragePools();
kimchi.initLogicalPoolExtend();
+
+ kimchi.topic('kimchi/volumeDownloadStarted').subscribe(function(data) {
+ var sp = data['sp'];
+ var poolNode = $('.storage-li[data-name="' + sp + '"]');
+ kimchi.doListVolumes(poolNode);
+ });
}
kimchi.changeArrow = function(obj) {
diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl
index d920ae2..b3bca53 100644
--- a/ui/pages/i18n.json.tmpl
+++ b/ui/pages/i18n.json.tmpl
@@ -169,6 +169,9 @@
"KCHPOOL6011M": "$_("No available partitions found.")",
"KCHPOOL6012M": "$_("This storage pool is not persistent. Instead of deactivate, this action will permanently delete it. Would you like to continue?")",
"KCHPOOL6013M": "$_("Unable to retrieve partitions information.")",
+ "KCHPOOL6014M": "$_("Downloading...")",
+ "KCHPOOL6015M": "$_("Done!")",
+ "KCHPOOL6016M": "$_("Failed!")",
"KCHVMSTOR0001E": "$_("CDROM path need to be a valid local path and cannot be blank.")",
"KCHVMSTOR0002E": "$_("Disk pool or volume cannot be blank.")"
diff --git a/ui/pages/sp-add-volume.html.tmpl b/ui/pages/sp-add-volume.html.tmpl
new file mode 100644
index 0000000..148a17b
--- /dev/null
+++ b/ui/pages/sp-add-volume.html.tmpl
@@ -0,0 +1,80 @@
+#*
+ * Project Kimchi
+ *
+ * Copyright IBM, Corp. 2014
+ *
+ * Authors:
+ * Hongliang Wang <hlwang(a)linux.vnet.ibm.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *#
+#unicode UTF-8
+#import gettext
+#from kimchi.cachebust import href
+#silent t = gettext.translation($lang.domain, $lang.localedir, languages=$lang.lang)
+#silent _ = t.gettext
+#silent _t = t.gettext
+<div id="sp-add-volume-window" class="window">
+ <form id="form-sp-add-volume">
+ <header class="window-header">
+ <h1 class="title">$_("Add a Volume to Storage Pool")</h1>
+ <div class="close">X</div>
+ </header>
+ <section>
+ <div class="content">
+ <div class="form-section">
+ <h2>
+ <input type="radio" id="volume-type-download" class="volume-type" name="volumeType" value="download" checked="checked" />
+ <label for="volume-type-download">
+ $_("Fetch from remote URL")
+ </label>
+ </h2>
+ <div class="field">
+ <p class="text-help">
+ $_("Enter the remote URL here.")
+ </p>
+ <div class="textbox-wrapper">
+ <input type="text" id="volume-remote-url" class="text volume-input download" name="volumeRemoteURL" />
+ </div>
+ </div>
+ </div>
+ <div class="form-section">
+ <h2>
+ <input type="radio" id="volume-type-upload" class="volume-type" name="volumeType" value="upload"/>
+ <label for="volume-type-upload">
+ $_("Upload an file")
+ </label>
+ </h2>
+ <div class="field">
+ <p class="text-help">
+ $_("Choose the file you want to upload.")
+ </p>
+ <div class="textbox-wrapper">
+ <input type="file" class="volume-input upload" id="volume-input-file" name="volumeLocalFile" disabled="disabled" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer>
+ <div class="btn-group">
+ <button type="submit" id="sp-add-volume-button" class="btn-normal">
+ <span class="text">$_("OK")</span>
+ </button>
+ </div>
+ </footer>
+ </form>
+</div>
+<script type="text/javascript">
+ kimchi.sp_add_volume_main();
+</script>
diff --git a/ui/pages/tabs/storage.html.tmpl b/ui/pages/tabs/storage.html.tmpl
index 87205bd..d5aceef 100644
--- a/ui/pages/tabs/storage.html.tmpl
+++ b/ui/pages/tabs/storage.html.tmpl
@@ -82,6 +82,7 @@
<div class="popover actionsheet right-side" style="width: 250px">
<button class="button-big pool-deactivate" data-stat="{state}" data-name="{name}" data-persistent="{persistent}"><span class="text">$_("Deactivate")</span></button>
<button class="button-big pool-activate" data-stat="{state}" data-name="{name}"><span class="text">$_("Activate")</span></button>
+ <button class="button-big pool-add-volume" data-stat="{state}" data-name="{name}"><span class="text">$_("Add Volume")</span></button>
<button class="button-big pool-extend {enableExt}" data-stat="{state}" data-name="{name}"><span class="text">$_("Extend")</span></button>
<button class="button-big red pool-delete" data-stat="{state}" data-name="{name}"><span class="text">$_("Undefine")</span></button>
</div>
@@ -98,11 +99,20 @@
</li>
</script>
<script id="volumeTmpl" type="html/text">
- <div class="volume-box white-box">
+ <div class="volume-box white-box" data-volume-name="{name}">
<div class="storage-icon volume-default icon-{format} ">
</div>
<div class="volume-title">
<div class="volume-name" title="{name}">{name}</div>
+ <div class="volume-progress hidden">
+ <div class="progress-bar-outer">
+ <div class="progress-bar-inner"></div>
+ </div>
+ <div class="progress-label">
+ <span class="progress-status"></span>
+ <span class="progress-downloaded"></span>
+ </div>
+ </div>
</div>
<div class="volume-setting">
</div>
--
1.8.1.4
10 years, 3 months