Add vnc-container image build
The files in tools/vnc-container allow a container image to be built which supports Ironic's graphical console functionality. For each node with an enabled graphical console, the service ironic-novncproxy (or nova-novncproxy) will connect to a VNC server exposed by a container running this image. If the devstack ir-novnc serivce is enabled then this container image will be built locally and ironic configured to used it for the systemd console container provider. This makes a devstack environment functional in accessing graphical consoles for Dell, HPE and Supermicro. Related-Bug: 2086715 Change-Id: I0842570cca22ac0e67d358c30225e8e08561f459
This commit is contained in:
parent
e41cb93eeb
commit
4ed44172b4
@ -100,3 +100,4 @@ zstd [devstack]
|
|||||||
# For graphical console support
|
# For graphical console support
|
||||||
podman [devstack]
|
podman [devstack]
|
||||||
systemd-container [devstack]
|
systemd-container [devstack]
|
||||||
|
buildah [devstack]
|
||||||
|
@ -1279,7 +1279,7 @@ function install_ironic {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if is_service_enabled ir-novnc; then
|
if is_service_enabled ir-novnc; then
|
||||||
# a websockets/html5 or flash powered VNC console for vm instances
|
# a websockets/html5 VNC console for bare metal hosts
|
||||||
NOVNC_FROM_PACKAGE=$(trueorfalse False NOVNC_FROM_PACKAGE)
|
NOVNC_FROM_PACKAGE=$(trueorfalse False NOVNC_FROM_PACKAGE)
|
||||||
if [ "$NOVNC_FROM_PACKAGE" = "True" ]; then
|
if [ "$NOVNC_FROM_PACKAGE" = "True" ]; then
|
||||||
# Installing novnc on Debian bullseye breaks the global pip
|
# Installing novnc on Debian bullseye breaks the global pip
|
||||||
@ -1304,7 +1304,11 @@ function install_ironic {
|
|||||||
git_clone $NOVNC_REPO $NOVNC_WEB_DIR $NOVNC_BRANCH
|
git_clone $NOVNC_REPO $NOVNC_WEB_DIR $NOVNC_BRANCH
|
||||||
fi
|
fi
|
||||||
# podman, systemd-container required by the systemd container provider
|
# podman, systemd-container required by the systemd container provider
|
||||||
install_package podman systemd-container
|
# buildah required below to build the VNC container
|
||||||
|
install_package podman systemd-container buildah
|
||||||
|
pushd $IRONIC_DIR/tools/vnc-container
|
||||||
|
buildah bud -f ./Containerfile -t localhost/ironic-vnc-container
|
||||||
|
popd
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2057,8 +2061,7 @@ function configure_ironic_novnc {
|
|||||||
iniset $IRONIC_CONF_FILE vnc port $service_port
|
iniset $IRONIC_CONF_FILE vnc port $service_port
|
||||||
iniset $IRONIC_CONF_FILE vnc novnc_web $NOVNC_WEB_DIR
|
iniset $IRONIC_CONF_FILE vnc novnc_web $NOVNC_WEB_DIR
|
||||||
iniset $IRONIC_CONF_FILE vnc container_provider systemd
|
iniset $IRONIC_CONF_FILE vnc container_provider systemd
|
||||||
# TODO(stevebaker) build this locally during the devstack run
|
iniset $IRONIC_CONF_FILE vnc console_image localhost/ironic-vnc-container
|
||||||
# iniset $IRONIC_CONF_FILE vnc console_image localhost/ironic-vnc-container
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ data_files =
|
|||||||
etc/ironic =
|
etc/ironic =
|
||||||
etc/ironic/rootwrap.conf
|
etc/ironic/rootwrap.conf
|
||||||
etc/ironic/rootwrap.d = etc/ironic/rootwrap.d/*
|
etc/ironic/rootwrap.d = etc/ironic/rootwrap.d/*
|
||||||
|
share/ironic/vnc-container = tools/vnc-container/*
|
||||||
packages =
|
packages =
|
||||||
ironic
|
ironic
|
||||||
|
|
||||||
|
25
tools/vnc-container/Containerfile
Normal file
25
tools/vnc-container/Containerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM quay.io/centos/centos:stream9
|
||||||
|
|
||||||
|
RUN dnf -y install \
|
||||||
|
epel-release && \
|
||||||
|
dnf -y install \
|
||||||
|
chromium \
|
||||||
|
chromedriver \
|
||||||
|
dumb-init \
|
||||||
|
procps \
|
||||||
|
psmisc \
|
||||||
|
python3-requests \
|
||||||
|
python3-selenium \
|
||||||
|
x11vnc \
|
||||||
|
xorg-x11-server-Xvfb
|
||||||
|
|
||||||
|
ENV DISPLAY_WIDTH=1280
|
||||||
|
ENV DISPLAY_HEIGHT=960
|
||||||
|
|
||||||
|
ENV APP='fake'
|
||||||
|
|
||||||
|
ADD bin/* /usr/local/bin
|
||||||
|
ADD drivers /drivers
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
|
CMD ["/usr/local/bin/start-xvfb.sh"]
|
74
tools/vnc-container/README.rst
Normal file
74
tools/vnc-container/README.rst
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
=============
|
||||||
|
VNC Container
|
||||||
|
=============
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
This allows a container image to be built which supports Ironic's graphical
|
||||||
|
console functionality.
|
||||||
|
|
||||||
|
For each node with an enabled graphical console, the service ironic-novncproxy
|
||||||
|
(or nova-novncproxy) will connect to a VNC server exposed by a container
|
||||||
|
running this image.
|
||||||
|
|
||||||
|
Building and using
|
||||||
|
------------------
|
||||||
|
|
||||||
|
To build the container image for local use, install ``buildah`` and run the
|
||||||
|
following as the user which runs ironic-conductor::
|
||||||
|
|
||||||
|
buildah bud -f ./Containerfile -t localhost/ironic-vnc-container
|
||||||
|
|
||||||
|
The ``systemd`` container provider (or an external provider) can then be configured
|
||||||
|
to use this image in ``ironic.conf``:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[vnc]
|
||||||
|
container_provider=systemd
|
||||||
|
console_image=localhost/ironic-vnc-container
|
||||||
|
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
--------------
|
||||||
|
|
||||||
|
When the container is started the following occurs:
|
||||||
|
|
||||||
|
1. Xvfb is run, which starts a virtual X11 session
|
||||||
|
2. x11vnc is run, which exposes a VNC server port
|
||||||
|
|
||||||
|
When a VNC connection is established a Selenium python script is started
|
||||||
|
which:
|
||||||
|
|
||||||
|
1. Starts a Chromium browser
|
||||||
|
2. For the ``fake`` app displays drivers/fake/index.html
|
||||||
|
3. For the ``redfish`` app detects the vendor by looking at the ``Oem``
|
||||||
|
value in a ``/redfish/v1`` response
|
||||||
|
4. Runs vendor specific code to display an HTML5 based console
|
||||||
|
|
||||||
|
When the VNC connection is terminated, the Selenium script and Chromium is
|
||||||
|
also terminated.
|
||||||
|
|
||||||
|
Vendor specific implementations are as follows.
|
||||||
|
|
||||||
|
Dell iDRAC
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
One-time console credentials are created with a call to
|
||||||
|
``/Managers/<manager>/Oem/Dell/DelliDRACCardService/Actions/DelliDRACCardService.GetKVMSession``
|
||||||
|
and the browser loads a console URL using those credentials.
|
||||||
|
|
||||||
|
HPE iLO
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
The ``/irc.html`` URL is loaded. For iLO 6 the inline login form is populated
|
||||||
|
with credentials and submitted, showing the console. For iLO 5 the main login
|
||||||
|
page is loaded, and when the login is submitted ``irc.html`` is loaded again.
|
||||||
|
|
||||||
|
Supermicro (Experimental)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A simulated user logs in, waits for the console preview image to load, then
|
||||||
|
clicks on it.
|
||||||
|
|
5
tools/vnc-container/bin/start-browser-x11vnc.sh
Executable file
5
tools/vnc-container/bin/start-browser-x11vnc.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
x11vnc -nevershared -forever -afteraccept 'start-selenium-browser.py &' -gone 'killall -s SIGTERM python3'
|
337
tools/vnc-container/bin/start-selenium-browser.py
Executable file
337
tools/vnc-container/bin/start-selenium-browser.py
Executable file
@ -0,0 +1,337 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from requests import auth
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from urllib import parse as urlparse
|
||||||
|
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.common import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
class BaseApp:
|
||||||
|
|
||||||
|
def __init__(self, app_info):
|
||||||
|
self.app_info = app_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_exit(self, signum, frame):
|
||||||
|
print("got SIGTERM, quitting")
|
||||||
|
self.driver.quit()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def start(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
signal.signal(signal.SIGTERM, self.handle_exit)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeApp(BaseApp):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return "file:///drivers/fake/index.html"
|
||||||
|
|
||||||
|
|
||||||
|
class RedfishApp(BaseApp):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self):
|
||||||
|
return self.app_info["address"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def redfish_url(self):
|
||||||
|
return self.base_url + self.app_info.get("root_prefix", "/redfish/v1")
|
||||||
|
|
||||||
|
def disable_right_click(self, driver):
|
||||||
|
# disable right-click menu
|
||||||
|
driver.execute_script(
|
||||||
|
'window.addEventListener("contextmenu", function(e) '
|
||||||
|
"{ e.preventDefault(); })"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IdracApp(RedfishApp):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
username = self.app_info["username"]
|
||||||
|
password = self.app_info["password"]
|
||||||
|
verify = self.app_info.get("verify_ca", True)
|
||||||
|
kvm_session_url = (f"{self.redfish_url}/Managers/iDRAC.Embedded.1/Oem/"
|
||||||
|
"Dell/DelliDRACCardService/Actions/DelliDRACCardService.GetKVMSession")
|
||||||
|
netloc = urlparse.urlparse(self.base_url).netloc
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
kvm_session_url,
|
||||||
|
verify=verify,
|
||||||
|
timeout=60,
|
||||||
|
auth=auth.HTTPBasicAuth(username, password),
|
||||||
|
json={"SessionTypeName": "idrac-graphical"},
|
||||||
|
).json()
|
||||||
|
temp_username = r["TempUsername"]
|
||||||
|
temp_password = r["TempPassword"]
|
||||||
|
url = (f"{self.base_url}/restgui/vconsole/index.html?ip={netloc}&"
|
||||||
|
f"kvmport=443&title=idrac-graphical&VCSID={temp_username}&VCSID2={temp_password}")
|
||||||
|
return url
|
||||||
|
|
||||||
|
def start(self, driver):
|
||||||
|
super(IdracApp, self).start(driver)
|
||||||
|
# wait for the full screen button
|
||||||
|
wait = WebDriverWait(
|
||||||
|
driver,
|
||||||
|
timeout=10,
|
||||||
|
poll_frequency=0.2,
|
||||||
|
ignored_exceptions=[exceptions.NoSuchElementException],
|
||||||
|
)
|
||||||
|
wait.until(
|
||||||
|
lambda d: driver.find_element(By.TAG_NAME, value="full-screen")
|
||||||
|
or True
|
||||||
|
)
|
||||||
|
fs_tag = driver.find_element(By.TAG_NAME, value="full-screen")
|
||||||
|
fs_tag.find_element(By.TAG_NAME, "button").click()
|
||||||
|
|
||||||
|
|
||||||
|
class IloApp(RedfishApp):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return self.base_url + "/irc.html"
|
||||||
|
|
||||||
|
def login(self, driver):
|
||||||
|
|
||||||
|
username = self.app_info["username"]
|
||||||
|
password = self.app_info["password"]
|
||||||
|
# wait for the username field to be enabled then perform login
|
||||||
|
wait = WebDriverWait(
|
||||||
|
driver,
|
||||||
|
timeout=10,
|
||||||
|
poll_frequency=0.2,
|
||||||
|
ignored_exceptions=[exceptions.NoSuchElementException],
|
||||||
|
)
|
||||||
|
wait.until(
|
||||||
|
lambda d: driver.find_element(By.ID, value="username") or True
|
||||||
|
)
|
||||||
|
|
||||||
|
username_field = driver.find_element(By.ID, value="username")
|
||||||
|
wait = WebDriverWait(
|
||||||
|
driver,
|
||||||
|
timeout=5,
|
||||||
|
poll_frequency=0.2,
|
||||||
|
ignored_exceptions=[exceptions.ElementNotInteractableException],
|
||||||
|
)
|
||||||
|
wait.until(lambda d: username_field.send_keys(username) or True)
|
||||||
|
|
||||||
|
driver.find_element(By.ID, value="password").send_keys(password)
|
||||||
|
driver.find_element(By.ID, value="login-form__submit").click()
|
||||||
|
|
||||||
|
def start(self, driver):
|
||||||
|
super(IloApp, self).start(driver)
|
||||||
|
|
||||||
|
# Detect iLO 6 vs 5 based on whether a message box or a login form
|
||||||
|
# is presented
|
||||||
|
try:
|
||||||
|
driver.find_element(By.CLASS_NAME, value="loginBoxRestrictWidth")
|
||||||
|
is_ilo6 = True
|
||||||
|
except exceptions.NoSuchElementException:
|
||||||
|
is_ilo6 = False
|
||||||
|
|
||||||
|
if is_ilo6:
|
||||||
|
# iLO 6 has an inline login which matches the main login
|
||||||
|
self.login(driver)
|
||||||
|
self.disable_right_click(driver)
|
||||||
|
self.full_screen(driver)
|
||||||
|
return
|
||||||
|
|
||||||
|
# load the main login page
|
||||||
|
driver.get(self.base_url)
|
||||||
|
|
||||||
|
# full screen content is shown in an embedded iframe
|
||||||
|
iframe = driver.find_element(By.ID, "appFrame")
|
||||||
|
driver.switch_to.frame(iframe)
|
||||||
|
|
||||||
|
self.login(driver)
|
||||||
|
|
||||||
|
# wait for <body id="app-container"> to exist, which indicates
|
||||||
|
# the login form has submitted and session cookies are now set
|
||||||
|
wait = WebDriverWait(
|
||||||
|
driver,
|
||||||
|
timeout=10,
|
||||||
|
poll_frequency=0.2,
|
||||||
|
ignored_exceptions=[exceptions.NoSuchElementException],
|
||||||
|
)
|
||||||
|
wait.until(
|
||||||
|
lambda d: driver.find_element(By.ID, value="app-container")
|
||||||
|
or True
|
||||||
|
)
|
||||||
|
|
||||||
|
# load the actual console
|
||||||
|
driver.get(self.url)
|
||||||
|
self.disable_right_click(driver)
|
||||||
|
self.full_screen(driver)
|
||||||
|
|
||||||
|
def full_screen(self, driver):
|
||||||
|
# make console full screen to hide menu
|
||||||
|
fs_button = driver.find_element(
|
||||||
|
By.CLASS_NAME, value="btnVideoFullScreen"
|
||||||
|
)
|
||||||
|
wait = WebDriverWait(
|
||||||
|
driver,
|
||||||
|
timeout=20,
|
||||||
|
poll_frequency=0.2,
|
||||||
|
ignored_exceptions=[
|
||||||
|
exceptions.ElementNotInteractableException,
|
||||||
|
exceptions.ElementClickInterceptedException,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
wait.until(lambda d: fs_button.click() or True)
|
||||||
|
|
||||||
|
|
||||||
|
class SupermicroApp(RedfishApp):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return self.base_url
|
||||||
|
|
||||||
|
def start(self, driver):
|
||||||
|
super(SupermicroApp, self).start(driver)
|
||||||
|
username = self.app_info["username"]
|
||||||
|
password = self.app_info["password"]
|
||||||
|
|
||||||
|
# populate login and submit
|
||||||
|
driver.find_element(By.NAME, value="name").send_keys(username)
|
||||||
|
driver.find_element(By.ID, value="pwd").send_keys(password)
|
||||||
|
driver.find_element(By.ID, value="login_word").click()
|
||||||
|
|
||||||
|
# navigate down some iframes
|
||||||
|
iframe = driver.find_element(By.ID, "TOPMENU")
|
||||||
|
driver.switch_to.frame(iframe)
|
||||||
|
|
||||||
|
iframe = driver.find_element(By.ID, "frame_main")
|
||||||
|
driver.switch_to.frame(iframe)
|
||||||
|
|
||||||
|
wait = WebDriverWait(
|
||||||
|
driver,
|
||||||
|
timeout=30,
|
||||||
|
poll_frequency=0.2,
|
||||||
|
ignored_exceptions=[
|
||||||
|
exceptions.NoSuchElementException,
|
||||||
|
exceptions.ElementNotInteractableException,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
wait.until(lambda d: driver.find_element(By.ID, value="img1") or True)
|
||||||
|
|
||||||
|
# launch the console by waiting for the console preview image to be
|
||||||
|
# loaded and clickable
|
||||||
|
def snapshot_wait(d):
|
||||||
|
try:
|
||||||
|
img1 = driver.find_element(By.ID, value="img1")
|
||||||
|
except exceptions.NoSuchElementException:
|
||||||
|
print("img1 doesn't exist yet")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if "Snapshot" not in img1.get_attribute("src"):
|
||||||
|
print("img1 src not a console snapshot yet")
|
||||||
|
return False
|
||||||
|
if not img1.get_attribute("complete") == "true":
|
||||||
|
print("img1 console snapshot not loaded yet")
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
img1.click()
|
||||||
|
except exceptions.ElementNotInteractableException:
|
||||||
|
print("img1 not clickable yet")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
wait = WebDriverWait(driver, timeout=30, poll_frequency=1)
|
||||||
|
wait.until(snapshot_wait)
|
||||||
|
|
||||||
|
# self.disable_right_click(driver)
|
||||||
|
|
||||||
|
|
||||||
|
def start_driver(url, app_info):
|
||||||
|
print(f"starting app with url {url}")
|
||||||
|
opts = webdriver.ChromeOptions()
|
||||||
|
opts.binary_location = "/usr/bin/chromium-browser"
|
||||||
|
# opts.enable_bidi = True
|
||||||
|
if url:
|
||||||
|
opts.add_argument(f"--app={url}")
|
||||||
|
|
||||||
|
verify = app_info.get("verify_ca", True)
|
||||||
|
if not verify:
|
||||||
|
opts.add_argument("--ignore-certificate-errors")
|
||||||
|
opts.add_argument("--ignore-ssl-errors")
|
||||||
|
|
||||||
|
opts.add_argument("--disable-extensions")
|
||||||
|
opts.add_argument("--disable-gpu")
|
||||||
|
opts.add_argument("--disable-plugins-discovery")
|
||||||
|
|
||||||
|
opts.add_argument("--disable-context-menu")
|
||||||
|
opts.add_argument("--no-sandbox")
|
||||||
|
opts.add_argument("--disable-dev-shm-usage")
|
||||||
|
|
||||||
|
opts.add_argument("--window-position=0,0")
|
||||||
|
opts.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||||
|
if "DISPLAY_WIDTH" in os.environ and "DISPLAY_HEIGHT" in os.environ:
|
||||||
|
width = int(os.environ["DISPLAY_WIDTH"])
|
||||||
|
height = int(os.environ["DISPLAY_HEIGHT"])
|
||||||
|
opts.add_argument(f"--window-size={width},{height}")
|
||||||
|
if "CHROME_ARGS" in os.environ:
|
||||||
|
for arg in os.environ["CHROME_ARGS"].split(" "):
|
||||||
|
opts.add_argument(arg)
|
||||||
|
|
||||||
|
driver = webdriver.Chrome(options=opts)
|
||||||
|
driver.delete_all_cookies()
|
||||||
|
driver.set_window_position(0, 0)
|
||||||
|
|
||||||
|
return driver
|
||||||
|
|
||||||
|
|
||||||
|
def discover_app(app_name, app_info):
|
||||||
|
if app_name == "fake":
|
||||||
|
return FakeApp
|
||||||
|
if app_name == "redfish-graphical":
|
||||||
|
# Make an unauthenticated redfish request
|
||||||
|
# to discover which console class to use
|
||||||
|
url = app_info["address"] + app_info.get("root_prefix", "/redfish/v1")
|
||||||
|
verify = app_info.get("verify_ca", True)
|
||||||
|
r = requests.get(url, verify=verify, timeout=60).json()
|
||||||
|
oem = ",".join(r["Oem"].keys())
|
||||||
|
if "Hpe" in oem:
|
||||||
|
return IloApp
|
||||||
|
if "Dell" in oem:
|
||||||
|
return IdracApp
|
||||||
|
if "Supermicro" in oem:
|
||||||
|
return SupermicroApp
|
||||||
|
raise Exception(f"Unsupported {app_name} vendor {oem}")
|
||||||
|
|
||||||
|
raise Exception(f"Unknown app name {app_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app_name = os.environ.get("APP")
|
||||||
|
print("got app info " + os.environ.get("APP_INFO"))
|
||||||
|
app_info = json.loads(os.environ.get("APP_INFO"))
|
||||||
|
app_class = discover_app(app_name, app_info)
|
||||||
|
|
||||||
|
app = app_class(app_info)
|
||||||
|
|
||||||
|
driver = start_driver(url=app.url, app_info=app_info)
|
||||||
|
print(f"got driver {driver}")
|
||||||
|
|
||||||
|
print(f"Running app {app_name}")
|
||||||
|
app.start(driver)
|
||||||
|
while True:
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
5
tools/vnc-container/bin/start-xvfb.sh
Executable file
5
tools/vnc-container/bin/start-xvfb.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
xvfb-run -s "-screen 0 ${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}x24" start-browser-x11vnc.sh
|
80
tools/vnc-container/drivers/fake/index.html
Normal file
80
tools/vnc-container/drivers/fake/index.html
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Bouncing Pixie</title>
|
||||||
|
<style>
|
||||||
|
* {margin:0; padding: 0; color:red;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<canvas id="tv-screen"></canvas>
|
||||||
|
<script>
|
||||||
|
let speed = 50;
|
||||||
|
let scale = 0.4; // Image scale (I work on 1080p monitor)
|
||||||
|
let canvas;
|
||||||
|
let ctx;
|
||||||
|
let logoColor;
|
||||||
|
|
||||||
|
let dvd = {
|
||||||
|
x: 200,
|
||||||
|
y: 300,
|
||||||
|
xspeed: 10,
|
||||||
|
yspeed: 10,
|
||||||
|
img: new Image()
|
||||||
|
};
|
||||||
|
|
||||||
|
(function main(){
|
||||||
|
canvas = document.getElementById("tv-screen");
|
||||||
|
ctx = canvas.getContext("2d");
|
||||||
|
dvd.img.src = 'ironic_mascot_color.png';
|
||||||
|
|
||||||
|
//Draw the "tv screen"
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
|
||||||
|
pickColor();
|
||||||
|
update();
|
||||||
|
})();
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
setTimeout(() => {
|
||||||
|
//Draw the canvas background
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
//Draw DVD Logo and his background
|
||||||
|
ctx.fillStyle = logoColor;
|
||||||
|
ctx.fillRect(dvd.x, dvd.y, dvd.img.width*scale, dvd.img.height*scale);
|
||||||
|
ctx.drawImage(dvd.img, dvd.x, dvd.y, dvd.img.width*scale, dvd.img.height*scale);
|
||||||
|
//Move the logo
|
||||||
|
dvd.x+=dvd.xspeed;
|
||||||
|
dvd.y+=dvd.yspeed;
|
||||||
|
//Check for collision
|
||||||
|
checkHitBox();
|
||||||
|
update();
|
||||||
|
}, speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check for border collision
|
||||||
|
function checkHitBox(){
|
||||||
|
if(dvd.x+dvd.img.width*scale >= canvas.width || dvd.x <= 0){
|
||||||
|
dvd.xspeed *= -1;
|
||||||
|
pickColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dvd.y+dvd.img.height*scale >= canvas.height || dvd.y <= 0){
|
||||||
|
dvd.yspeed *= -1;
|
||||||
|
pickColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Pick a random color in RGB format
|
||||||
|
function pickColor(){
|
||||||
|
r = Math.random() * (254 - 0) + 0;
|
||||||
|
g = Math.random() * (254 - 0) + 0;
|
||||||
|
b = Math.random() * (254 - 0) + 0;
|
||||||
|
|
||||||
|
logoColor = 'rgb('+r+','+g+', '+b+')';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
tools/vnc-container/drivers/fake/ironic_mascot_color.png
Normal file
BIN
tools/vnc-container/drivers/fake/ironic_mascot_color.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
Loading…
x
Reference in New Issue
Block a user