1、VNC
在计算领域,虚拟网络计算(VNC)是一种图形桌面共享系统,它使用RFB协议远程控制另一台计算机。它将键盘和鼠标事件从一台计算机传输到另一台计算机,并通过网络将图形屏幕更新传递回另一个方向。在像OpenStack这样的IaaS系统中,VNC是最终用户通过GUI访问VMs的一个非常方便的工具。Nova提供两种VNC代理:noVNC和Nova xvpvncproxy。
2、整体流程
1)·使用nova get-vnc-console去获取VNC URL,nova-api接受到消息后向虚拟机所在的计算节点发送get_vnc_console rpc请求;
2)计算节点收到请求后生成一个token,并调用libvirt驱动的get_vnc_console函数;
3)libvirt连接libvirt服务端获取虚拟机的vnc port等信息;
4)nova-api向nova-consoleauth发送authorize_console请求,token作为键值将计算节点返回的信息进行缓存;
5)返回URL;
6)浏览器访问URL;
7)noVNC监听6080 HTTP 端口,接受此URL请求,然后向Nova-consoleauth发送check_token消息;
8)在浏览器中出现虚拟机桌面。
3、代码解析
/nova/api/openstack/compute/remote_consoles.py
def get_vnc_console(self, req, id, body):
"""Get text console output."""
context = req.environ['nova.context']
context.can(rc_policies.BASE_POLICY_NAME)
# If type is not supplied or unknown, get_vnc_console below will cope
#获取vnc类型,在命令行中增加的novnc参数
console_type = body['os-getVNCConsole'].get('type')
#获取虚拟机,调用计算节点上的函数,获取url
instance = common.get_instance(self.compute_api, context, id)
try:
output = self.compute_api.get_vnc_console(context,
instance,
console_type)
except exception.ConsoleTypeUnavailable as e:
raise webob.exc.HTTPBadRequest(explanation=e.format_message())
except (exception.InstanceUnknownCell,
exception.InstanceNotFound) as e:
raise webob.exc.HTTPNotFound(explanation=e.format_message())
except exception.InstanceNotReady as e:
raise webob.exc.HTTPConflict(explanation=e.format_message())
except NotImplementedError:
common.raise_feature_not_supported()
return {'console': {'type': console_type, 'url': output['url']}}
/nova/compute/api.py
def get_vnc_console(self, context, instance, console_type):
"""Get a url to an instance Console."""
#向计算节点发送rpc请求,请求消息发送至mq,获取connect_info
connect_info = self.compute_rpcapi.get_vnc_console(context,
instance=instance, console_type=console_type)
#向nova-consoleauth发送请求,缓存connect_info
self.consoleauth_rpcapi.authorize_console(context,
connect_info['token'], console_type,
connect_info['host'], connect_info['port'],
connect_info['internal_access_path'], instance.uuid,
access_url=connect_info['access_url'])
return {'url': connect_info['access_url']}
/nova/compute/manager.py
def get_vnc_console(self, context, console_type, instance):
"""Return connection information for a vnc console."""
context = context.elevated()
LOG.debug("Getting vnc console", instance=instance)
#随机生成一个token
token = uuidutils.generate_uuid()
if not CONF.vnc.enabled:
raise exception.ConsoleTypeUnavailable(console_type=console_type)
#对返回信息进行格式化
if console_type == 'novnc':
# For essex, novncproxy_base_url must include the full path
# including the html file (like http://myhost/vnc_auto.html)
access_url = '%s?token=%s' % (CONF.vnc.novncproxy_base_url, token)
elif console_type == 'xvpvnc':
access_url = '%s?token=%s' % (CONF.vnc.xvpvncproxy_base_url, token)
else:
raise exception.ConsoleTypeInvalid(console_type=console_type)
try:
# Retrieve connect info from driver, and then decorate with our
# access info token
#调用驱动函数获取真正的连接信息
console = self.driver.get_vnc_console(context, instance)
connect_info = console.get_connection_info(token, access_url)
except exception.InstanceNotFound:
if instance.vm_state != vm_states.BUILDING:
raise
raise exception.InstanceNotReady(instance_id=instance.uuid)
return connect_info
/nova/consoleauth/manager.py
def authorize_console(self, context, token, console_type, host, port,
internal_access_path, instance_uuid,
access_url=None):
#以token作为键值
token_dict = {'token': token,
'instance_uuid': instance_uuid,
'console_type': console_type,
'host': host,
'port': port,
'internal_access_path': internal_access_path,
'access_url': access_url,
'last_activity_at': time.time()}
data = jsonutils.dumps(token_dict)
#将token放在缓存中
self.mc.set(token.encode('UTF-8'), data)
tokens = self._get_tokens_for_instance(instance_uuid)
# Remove the expired tokens from cache.
token_values = self.mc.get_multi(
[tok.encode('UTF-8') for tok in tokens])
tokens = [name for name, value in zip(tokens, token_values)
if value is not None]
tokens.append(token)
self.mc_instance.set(instance_uuid.encode('UTF-8'),
jsonutils.dumps(tokens))
LOG.info(_LI("Received Token: %(token)s, %(token_dict)s"),
{'token': token, 'token_dict': token_dict})
/nova/console/websocketproxy.py
def new_websocket_client(self):
"""Called after a new WebSocket connection has been established."""
# Reopen the eventlet hub to make sure we don't share an epoll
# fd with parent and/or siblings, which would be bad
from eventlet import hubs
hubs.use_hub()
# The nova expected behavior is to have token
# passed to the method GET of the request
parse = urlparse.urlparse(self.path)
if parse.scheme not in ('http', 'https'):
# From a bug in urlparse in Python < 2.7.4 we cannot support
# special schemes (cf: http://bugs.python.org/issue9374)
if sys.version_info < (2, 7, 4):
raise exception.NovaException(
_("We do not support scheme '%s' under Python < 2.7.4, "
"please use http or https") % parse.scheme)
query = parse.query
#请求信息中获取token
token = urlparse.parse_qs(query).get("token", [""]).pop()
if not token:
# NoVNC uses it's own convention that forward token
# from the request to a cookie header, we should check
# also for this behavior
hcookie = self.headers.getheader('cookie')
if hcookie:
cookie = Cookie.SimpleCookie()
for hcookie_part in hcookie.split(';'):
hcookie_part = hcookie_part.lstrip()
try:
cookie.load(hcookie_part)
except Cookie.CookieError:
# NOTE(stgleb): Do not print out cookie content
# for security reasons.
LOG.warning(_LW('Found malformed cookie'))
else:
if 'token' in cookie:
token = cookie['token'].value
ctxt = context.get_admin_context()
rpcapi = consoleauth_rpcapi.ConsoleAuthAPI()
#对token信息进行校验
connect_info = rpcapi.check_token(ctxt, token=token)
if not connect_info:
raise exception.InvalidToken(token=token)
# Verify Origin
expected_origin_hostname = self.headers.getheader('Host')
if ':' in expected_origin_hostname:
e = expected_origin_hostname
if '[' in e and ']' in e:
expected_origin_hostname = e.split(']')[0][1:]
else:
expected_origin_hostname = e.split(':')[0]
expected_origin_hostnames = CONF.console.allowed_origins
expected_origin_hostnames.append(expected_origin_hostname)
origin_url = self.headers.getheader('Origin')
# missing origin header indicates non-browser client which is OK
if origin_url is not None:
origin = urlparse.urlparse(origin_url)
origin_hostname = origin.hostname
origin_scheme = origin.scheme
if origin_hostname == '' or origin_scheme == '':
detail = _("Origin header not valid.")
raise exception.ValidationError(detail=detail)
if origin_hostname not in expected_origin_hostnames:
detail = _("Origin header does not match this host.")
raise exception.ValidationError(detail=detail)
if not self.verify_origin_proto(connect_info, origin_scheme):
detail = _("Origin header protocol does not match this host.")
raise exception.ValidationError(detail=detail)
self.msg(_('connect info: %s'), str(connect_info))
host = connect_info['host']
port = int(connect_info['port'])
# Connect to the target
self.msg(_("connecting to: %(host)s:%(port)s") % {'host': host,
'port': port})
tsock = self.socket(host, port, connect=True)
# Handshake as necessary
if connect_info.get('internal_access_path'):
tsock.send("CONNECT %s HTTP/1.1\r\n\r\n" %
connect_info['internal_access_path'])
while True:
data = tsock.recv(4096, socket.MSG_PEEK)
if data.find("\r\n\r\n") != -1:
if data.split("\r\n")[0].find("200") == -1:
raise exception.InvalidConnectionInfo()
tsock.recv(len(data))
break
# Start proxying
try:
self.do_proxy(tsock)
except Exception:
if tsock:
tsock.shutdown(socket.SHUT_RDWR)
tsock.close()
self.vmsg(_("%(host)s:%(port)s: "
"Websocket client or target closed") %
{'host': host, 'port': port})
raise