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