[Kimchi-devel] [PATCH] Adding ability to switch graphics devices on a cold vm, adds serial console option as well

bbaude at redhat.com bbaude at redhat.com
Tue Sep 9 14:14:03 UTC 2014


From: Brent Baude <bbaude at 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




More information about the Kimchi-devel mailing list