[Kimchi-devel] [PATCH] Adding ability to switch graphics devices on a cold vm, adds serial console option as well
Aline Manera
alinefm at linux.vnet.ibm.com
Thu Sep 11 01:57:01 UTC 2014
Some overview:
1. We use 4 spaces for indentation in all files.
I didn't apply the patch but it seems there are some tabs in it.
2. We use 80 characters limit
You can run "make check-local" to make sure your code is respecting
it in other pep8 rules.
3. You need to update docs/API.md to document this new behavior
4. You need to update mockmodel.py
As its name says it is a mock model used when running Kimchi with
--test. It used by user to check Kimchi feature without affecting the
system.
5. Add tests for it.
Under /tests we have unit tests that are ran by "make check"
test_rest.py uses mockmodel and test the REST API
test_model.py test the model itself and so on
6. I suggest spitting this patch in (at least) 2 patches: one for
backend and other one for UI
That way the patch is not blocked by some comments on UI and it is
easy to review small patch =)
7. More comments inline below.
On 09/09/2014 11:14 AM, bbaude at redhat.com wrote:
> 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"
You need to update the message "KCHVM0014E" in the i18n.py file to add
the new graphics type there.
> },
> "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")
It was updated to "graphics" instead of "ticket"
> + password = password if password is not None else "".join(
> + random.sample(string.ascii_letters + string.digits, 8))
> + params['ticket']['passwd'] = password
This was also changed recently. The password will be automatically
generated if the passed value is an empty string,
> 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")
> +
If the above code is not needed anymore remove the lines instead of
comment them
> + 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
Split it in 2 lines. It is easy to read.
> + 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 the code gets this point, the "graphics" is in params dict.
So you just need to check for type
if params["graphics"].get("type") is not None:
# continue the follow to change the graphics type
> + 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)
This is really a big piece of code. I suggest move it to a function.
And thanks for using etree to generate the XML.
I'd say to you create a new module to input it. As we can reuse it while
creating the VM too.
src/kimchi/xmlutils/graphics.py
And then we can insert more and more modules there.
> + 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
> +
It was removed recently
> 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"
> +
If the host is a ppc64 macihne it does not mean the VM has the serial
console, right?
So you should respect the value returned by _vm_get_graphics(name)
> 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>
> + """
By doing the XML module I suggested before you can reuse the XML
generated by etree here.
> 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;
> -}
> -
To where the above content went ?
> .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">
More information about the Kimchi-devel
mailing list