Merge "Replace Chrome/Selenium console with Firefox extension"
This commit is contained in:
@@ -250,6 +250,12 @@ packages = [
|
||||
"share/ironic/vnc-container/drivers/fake/" = [
|
||||
"tools/vnc-container/drivers/fake/*",
|
||||
]
|
||||
"share/ironic/vnc-container/drivers/launch/" = [
|
||||
"tools/vnc-container/drivers/launch/*",
|
||||
]
|
||||
"share/ironic/vnc-container/extension/" = [
|
||||
"tools/vnc-container/extension/*",
|
||||
]
|
||||
|
||||
[tool.doc8]
|
||||
ignore = ["D001"]
|
||||
|
||||
34
releasenotes/notes/chromium2firefox-df4b2ea296fc458b.yaml
Normal file
34
releasenotes/notes/chromium2firefox-df4b2ea296fc458b.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The container build recipe for the graphical console container image has
|
||||
replaced the Chrome/Selenium approach with a Firefox extension.
|
||||
|
||||
The previous containerised graphical console approach had a Selenium
|
||||
script managing a Chrome browser session. This change replaces that with
|
||||
firefox and a custom extension to perform the required actions to login
|
||||
and load the BMC console. This supports the same vendors as the previous
|
||||
approach (iDRAC, iLO, Supermicro).
|
||||
|
||||
Functional differences with the chrome/selenium version:
|
||||
|
||||
* Firefox kiosk mode has a more locked-down environment, including
|
||||
disabling context menus. This means the brittle workaround to disable
|
||||
them is no longer required.
|
||||
* Firefox global policy allows the environment to be locked down
|
||||
further, including limiting accessing to all URLs except the BMC.
|
||||
* There is now a dedicated loading page which can show status updates
|
||||
until the first BMC page loads. This page shows error messages if any
|
||||
of the early redfish calls fail.
|
||||
* VNC client sessions are now shared with multiple clients, and firefox
|
||||
will be started on the first connection, and stopped when the last
|
||||
connection ends.
|
||||
* Starting Xvfb is now deferred until the first VNC client connection.
|
||||
This results in a never-connected container using 5MB vs 30MB
|
||||
once Xvfb is started. Starting Xvfb has ~1sec time penalty on first
|
||||
connection.
|
||||
* The browser now runs in a dedicated non-root user
|
||||
* All redfish consoles now hide toolbar elements with a CSS overlay rather than
|
||||
simulating other methods such as clicking the "Full Screen" button.
|
||||
* ilo6/ilo5 detection is now done by a redfish call and the ilo5 path
|
||||
has less moving parts.
|
||||
@@ -3,23 +3,27 @@ FROM quay.io/centos/centos:stream9
|
||||
RUN dnf -y install \
|
||||
epel-release && \
|
||||
dnf -y install \
|
||||
chromium \
|
||||
chromedriver \
|
||||
firefox \
|
||||
dumb-init \
|
||||
iproute \
|
||||
procps \
|
||||
psmisc \
|
||||
python3-requests \
|
||||
python3-selenium \
|
||||
x11vnc \
|
||||
xorg-x11-server-Xvfb
|
||||
xorg-x11-server-Xvfb && \
|
||||
useradd --create-home --shell /bin/bash firefox
|
||||
|
||||
ENV DISPLAY_WIDTH=1280
|
||||
ENV DISPLAY_HEIGHT=960
|
||||
|
||||
ENV APP='fake'
|
||||
ENV APP_INFO={}
|
||||
ENV READ_ONLY=False
|
||||
ENV DEBUG=0
|
||||
|
||||
ADD bin/* /usr/local/bin
|
||||
ADD drivers /drivers
|
||||
ADD extension /usr/share/mozilla/extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}/@ironic-console.openstack.org
|
||||
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
CMD ["/usr/local/bin/start-xvfb.sh"]
|
||||
CMD ["/usr/local/bin/start-x11vnc.sh"]
|
||||
@@ -26,6 +26,7 @@ to use this image in ``ironic.conf``:
|
||||
.. code-block:: ini
|
||||
|
||||
[vnc]
|
||||
enabled = True
|
||||
container_provider=systemd
|
||||
console_image=localhost/ironic-vnc-container
|
||||
|
||||
@@ -35,20 +36,20 @@ Implementation
|
||||
|
||||
When the container is started the following occurs:
|
||||
|
||||
1. x11vnc is run, which exposes a VNC server port
|
||||
|
||||
When a VNC connection is established, the following occurs:
|
||||
|
||||
1. Xvfb is run, which starts a virtual X11 session
|
||||
2. x11vnc is run, which exposes a VNC server port
|
||||
2. A firefox browser is started in kiosk mode
|
||||
3. A firefox extension automates loading the requested console app
|
||||
4. For the ``fake`` app, display drivers/fake/index.html
|
||||
5. For the ``redfish-graphical`` app, detect the vendor by looking at the
|
||||
``Oem`` value in a ``/redfish/v1`` response
|
||||
6. Runs vendor specific scripts to display an HTML5 based console
|
||||
|
||||
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.
|
||||
Multiple VNC connections can share a single instance. When the last VNC
|
||||
connection is closed, the running Firefox is closed.
|
||||
|
||||
Vendor specific implementations are as follows.
|
||||
|
||||
|
||||
43
tools/vnc-container/bin/cert-override.py
Executable file
43
tools/vnc-container/bin/cert-override.py
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
app_name = os.environ.get("APP", "fake")
|
||||
app_info = json.loads(os.environ.get("APP_INFO"))
|
||||
verify = app_info.get("verify_ca", True)
|
||||
print("""# PSM Certificate Override Settings file
|
||||
# This is a generated file! Do not edit.
|
||||
""")
|
||||
|
||||
address = app_info.get("address")
|
||||
if verify or not address:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
parsed_url = urlparse(address)
|
||||
addr = parsed_url.hostname
|
||||
port = parsed_url.port or 443
|
||||
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
with socket.create_connection((addr, int(port)), timeout=5) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=addr) as wrappedSocket:
|
||||
der_cert_bin = wrappedSocket.getpeercert(True)
|
||||
|
||||
digest = hashlib.sha256(der_cert_bin).hexdigest()
|
||||
formatted_digest = ':'.join(
|
||||
a + b for a, b in zip(digest[::2], digest[1::2])).upper()
|
||||
|
||||
print(f"{addr}:{port}:\tOID.2.16.840.1.101.3.4.2.1\t{formatted_digest}\t")
|
||||
except Exception as e:
|
||||
print("# Problem fetching certificate fingerprint.")
|
||||
print(f"# {e}")
|
||||
43
tools/vnc-container/bin/discover-app.py
Executable file
43
tools/vnc-container/bin/discover-app.py
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import urllib
|
||||
import sys
|
||||
|
||||
REDFISH_SUPPORTED = {
|
||||
"Dell",
|
||||
"Hpe",
|
||||
"Supermicro",
|
||||
}
|
||||
|
||||
def discover_app(app_name, app_info):
|
||||
if app_name == "fake":
|
||||
return "fake"
|
||||
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 oem in REDFISH_SUPPORTED:
|
||||
return oem
|
||||
raise Exception(f"Unsupported {app_name} vendor {oem}")
|
||||
|
||||
raise Exception(f"Unknown app name {app_name}")
|
||||
|
||||
|
||||
def main():
|
||||
app_name = os.environ.get("APP")
|
||||
app_info = json.loads(os.environ.get("APP_INFO"))
|
||||
print(discover_app(app_name, app_info))
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
print(urllib.parse.quote(str(e)))
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
79
tools/vnc-container/bin/policies.py
Executable file
79
tools/vnc-container/bin/policies.py
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
|
||||
import json
|
||||
|
||||
app_name = os.environ.get("APP", "fake")
|
||||
app_info = json.loads(os.environ.get("APP_INFO"))
|
||||
debug = int(os.environ.get("DEBUG", 0))
|
||||
verify = app_info.get("verify_ca", True)
|
||||
error = os.environ.get("ERROR", "")
|
||||
|
||||
if app_name == "fake":
|
||||
# Extensions cannot set file:// URLs so special case the fake driver
|
||||
homepage = "file:///drivers/fake/index.html"
|
||||
else:
|
||||
homepage = "file:///drivers/launch/index.html"
|
||||
if error:
|
||||
homepage += f"?error={error}"
|
||||
|
||||
policies = {
|
||||
"AppAutoUpdate": False,
|
||||
"AutofillAddressEnabled": False,
|
||||
"AutofillCreditCardEnabled": False,
|
||||
"DisableAppUpdate": True,
|
||||
"DisableFirefoxScreenshots": True,
|
||||
"DisableFirefoxStudies": True,
|
||||
"DisableFirefoxStudies_comment": "Disable Firefox studies",
|
||||
"DisablePocket": True,
|
||||
"DisableSystemAddonUpdate": True,
|
||||
"DisableTelemetry": True,
|
||||
"DontCheckDefaultBrowser": True,
|
||||
"Homepage": {
|
||||
"URL": homepage,
|
||||
"StartPage": "homepage",
|
||||
},
|
||||
"NoDefaultBookmarks": True,
|
||||
"OfferToSaveLogins": False,
|
||||
"OverrideFirstRunPage": "",
|
||||
"OverridePostUpdatePage": "",
|
||||
"PasswordManagerEnabled": False,
|
||||
"Preferences": {
|
||||
"security.ssl.enable_ocsp_stapling": {
|
||||
"Value": verify,
|
||||
"Status": "locked",
|
||||
},
|
||||
"dom.disable_open_during_load": {
|
||||
"Value": False,
|
||||
"Status": "locked",
|
||||
},
|
||||
},
|
||||
"PrintingEnabled": False,
|
||||
"PromptForDownloadLocation": False,
|
||||
"SanitizeOnShutdown": True,
|
||||
"SkipTermsOfUse": True,
|
||||
"StartDownloadsInTempDirectory": True,
|
||||
"WebsiteFilter": {
|
||||
"Block": ["<all_urls>"],
|
||||
"Exceptions": ["file:///drivers/*"],
|
||||
},
|
||||
}
|
||||
|
||||
if not debug:
|
||||
policies.update(
|
||||
{
|
||||
"BlockAboutConfig": True,
|
||||
"BlockAboutAddons": True,
|
||||
"BlockAboutProfiles": True,
|
||||
"BlockAboutSupport": True,
|
||||
}
|
||||
)
|
||||
|
||||
address = app_info.get("address")
|
||||
if address:
|
||||
policies["WebsiteFilter"]["Exceptions"].append(
|
||||
f"{address}/*"
|
||||
)
|
||||
|
||||
print(json.dumps({"policies": policies}, indent=2))
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eux
|
||||
|
||||
if [ "$READ_ONLY" = "True" ]; then
|
||||
viewonly="-viewonly"
|
||||
else
|
||||
viewonly=""
|
||||
fi
|
||||
|
||||
x11vnc $viewonly -nevershared -forever -afteraccept 'start-selenium-browser.py &' -gone 'killall -s SIGTERM python3'
|
||||
29
tools/vnc-container/bin/start-firefox.sh
Executable file
29
tools/vnc-container/bin/start-firefox.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
if pgrep -x firefox >/dev/null; then
|
||||
echo "Firefox is already running. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
rm -rf ~/.mozilla/firefox
|
||||
|
||||
firefox -CreateProfile ironic-vnc
|
||||
|
||||
pushd ~/.mozilla/firefox/*.ironic-vnc
|
||||
cert-override.py > cert_override.txt
|
||||
popd
|
||||
|
||||
# support a DEBUG variable to aid development
|
||||
DEBUG=${DEBUG:-0}
|
||||
if [ "$DEBUG" = "2" ]; then
|
||||
# show tabs and a javascript console
|
||||
firefox -width ${DISPLAY_WIDTH} -height ${DISPLAY_HEIGHT} -P ironic-vnc -jsconsole &
|
||||
elif [ "$DEBUG" = "1" ]; then
|
||||
# show tabs
|
||||
firefox -width ${DISPLAY_WIDTH} -height ${DISPLAY_HEIGHT} -P ironic-vnc &
|
||||
else
|
||||
# fully locked down kiosk mode
|
||||
firefox -width ${DISPLAY_WIDTH} -height ${DISPLAY_HEIGHT} -P ironic-vnc --kiosk &
|
||||
fi
|
||||
@@ -1,337 +0,0 @@
|
||||
#!/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())
|
||||
37
tools/vnc-container/bin/start-x11vnc.sh
Executable file
37
tools/vnc-container/bin/start-x11vnc.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
|
||||
extension_path="/usr/share/mozilla/extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}/@ironic-console.openstack.org"
|
||||
|
||||
set +e
|
||||
APP_NAME=$(discover-app.py)
|
||||
if [ $? -ne 0 ]; then
|
||||
export ERROR="${APP_NAME}"
|
||||
APP_NAME="error"
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
cat << EOF > "${extension_path}/config.js"
|
||||
let config = {
|
||||
app: "${APP_NAME}",
|
||||
app_info: ${APP_INFO}
|
||||
};
|
||||
EOF
|
||||
|
||||
sed -i "s#APP_NAME#${APP_NAME}#g" "${extension_path}/manifest.json"
|
||||
|
||||
mkdir -p /etc/firefox/policies
|
||||
policies.py > /etc/firefox/policies/policies.json
|
||||
|
||||
READ_ONLY=${READ_ONLY:-False}
|
||||
if [ "$READ_ONLY" = "True" ]; then
|
||||
viewonly="-viewonly -nocursor"
|
||||
else
|
||||
viewonly=""
|
||||
fi
|
||||
|
||||
export X11VNC_CREATE_GEOM=${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}x24
|
||||
runuser -u firefox -- x11vnc -ncache 10 $viewonly -create -shared -forever -afteraccept start-firefox.sh -gone stop-firefox.sh
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eux
|
||||
|
||||
xvfb-run -s "-screen 0 ${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}x24" start-browser-x11vnc.sh
|
||||
11
tools/vnc-container/bin/stop-firefox.sh
Executable file
11
tools/vnc-container/bin/stop-firefox.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
connections=$(ss --no-header state established '( dport = :5900 or sport = :5900 )' | wc -l)
|
||||
|
||||
if [ "$connections" -eq 0 ]; then
|
||||
killall -s SIGTERM firefox
|
||||
else
|
||||
echo "Active VNC connection detected, deferring firefox shutdown."
|
||||
fi
|
||||
@@ -1,9 +1,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Bouncing Pixie</title>
|
||||
<style>
|
||||
* {margin:0; padding: 0; color:red;}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
156
tools/vnc-container/drivers/launch/index.html
Normal file
156
tools/vnc-container/drivers/launch/index.html
Normal file
@@ -0,0 +1,156 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Starting Console</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #000000;
|
||||
font-family: 'Arial', sans-serif;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Spinning Circle Loader */
|
||||
.spinner {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 8px solid rgba(255, 255, 255, 0.1);
|
||||
border-left: 8px solid #00ff88;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 30px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Text */
|
||||
.loading-text {
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0;
|
||||
animation: fadeInOut 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.spinner {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-width: 6px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#error-messages {
|
||||
margin-top: 20px;
|
||||
font-size: 16px;
|
||||
color: #ff0000;
|
||||
text-align: center;
|
||||
max-width: 50%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#status-messages {
|
||||
margin-top: 20px;
|
||||
font-size: 16px;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const errorMessage = urlParams.get('error');
|
||||
const statusMessage = urlParams.get('status');
|
||||
statusText = document.getElementById("status-messages")
|
||||
if (statusMessage){
|
||||
statusText.textContent = statusMessage;
|
||||
}
|
||||
if (errorMessage) {
|
||||
document.querySelector('.spinner').style.animation = 'none';
|
||||
loadingText = document.querySelector('.loading-text')
|
||||
loadingText.style.animation = 'none';
|
||||
loadingText.style.opacity = 1;
|
||||
loadingText.textContent = "ERROR LOADING CONSOLE"
|
||||
statusText.textContent = "";
|
||||
document.getElementById('error-messages').textContent = decodeURIComponent(errorMessage);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="loading-container">
|
||||
|
||||
<!-- Main Spinner -->
|
||||
<div class="spinner"></div>
|
||||
|
||||
<!-- Loading Text -->
|
||||
<div class="loading-text">STARTING CONSOLE</div>
|
||||
|
||||
<div id="status-messages"></div>
|
||||
|
||||
<div id="error-messages"></div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
4
tools/vnc-container/extension/Dell.css
Normal file
4
tools/vnc-container/extension/Dell.css
Normal file
@@ -0,0 +1,4 @@
|
||||
/* CSS file for iDRAC Graphical Console */
|
||||
app-header {
|
||||
display: none;
|
||||
}
|
||||
50
tools/vnc-container/extension/Dell.js
Normal file
50
tools/vnc-container/extension/Dell.js
Normal file
@@ -0,0 +1,50 @@
|
||||
window.addEventListener("load", function () {
|
||||
if (window.location.protocol === "file:" && window.location.pathname.endsWith("/drivers/launch/index.html")) {
|
||||
console.log("idrac-graphical driver launch page loaded");
|
||||
set_status("Getting console credentials");
|
||||
loadConsole();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Loads the iDRAC graphical console by requesting a KVM session URL and redirecting the window.
|
||||
*/
|
||||
function loadConsole() {
|
||||
const kvm_session_url = redfish_url("/Managers/iDRAC.Embedded.1/Oem/Dell/DelliDRACCardService/Actions/DelliDRACCardService.GetKVMSession")
|
||||
const url = new URL(kvm_session_url);
|
||||
const netloc = url.host;
|
||||
const username = config.app_info.username;
|
||||
const password = config.app_info.password;
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", kvm_session_url, true);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
|
||||
xhr.withCredentials = true;
|
||||
|
||||
xhr.onload = function () {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
console.log("KVM Session Request successful:", xhr.responseText);
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
temp_username = response.TempUsername;
|
||||
temp_password = response.TempPassword;
|
||||
|
||||
console_url = bmc_url(`/restgui/vconsole/index.html?ip=${netloc}&kvmport=443&title=${config.app}&VCSID=${temp_username}&VCSID2=${temp_password}`);
|
||||
|
||||
console.log("idrac-graphical loading console", console_url);
|
||||
window.location.href = console_url; // Redirect to the KVM session
|
||||
} else {
|
||||
console.error("KVM Session Request failed:", xhr.status, xhr.statusText);
|
||||
set_error(`Failed to get console credentials: (${xhr.status}) ${xhr.statusText}`);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
console.error("KVM Session Request network error.");
|
||||
set_error(`Failed to get console credentials: (${xhr.status}) ${xhr.statusText}`);
|
||||
};
|
||||
|
||||
console.log("idrac-graphical sending request to:", xhr);
|
||||
xhr.send(JSON.stringify({ "SessionTypeName": config.app }));
|
||||
}
|
||||
11
tools/vnc-container/extension/Hpe.css
Normal file
11
tools/vnc-container/extension/Hpe.css
Normal file
@@ -0,0 +1,11 @@
|
||||
/* CSS file for iLO Graphical Console */
|
||||
|
||||
/* Hide header controls */
|
||||
|
||||
#app-container #videoOuter #videoContainer div.control.windowedMode {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#irc_statusbar {
|
||||
display: none !important;
|
||||
}
|
||||
121
tools/vnc-container/extension/Hpe.js
Normal file
121
tools/vnc-container/extension/Hpe.js
Normal file
@@ -0,0 +1,121 @@
|
||||
window.addEventListener("load", function () {
|
||||
if (window.location.protocol === "file:" && window.location.pathname.endsWith("/drivers/launch/index.html")) {
|
||||
set_status("Detecting iLO version");
|
||||
console.log("ilo-graphical driver launch page loaded");
|
||||
detectIloVersion();
|
||||
}
|
||||
else if (window.location.pathname.endsWith("/irc.html")) {
|
||||
console.log("ilo-graphical logging in");
|
||||
login(false);
|
||||
}
|
||||
else if (window.location.pathname.endsWith("/html/login.html")) {
|
||||
// ilo5 login
|
||||
console.log("ilo-graphical ilo5 logging in");
|
||||
login(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Detects the iLO version by making a Redfish API call and redirects to the appropriate login page or console.
|
||||
*/
|
||||
function detectIloVersion() {
|
||||
const url = redfish_url("");
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.onload = function () {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
console.log("Redfish Request successful:", xhr.responseText);
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
manager_type = response.Oem.Hpe.Manager[0].ManagerType;
|
||||
if (manager_type == "iLO 5") {
|
||||
console.log("ilo-graphical loading login page");
|
||||
set_status("Logging in to BMC");
|
||||
const console_url = bmc_url("/html/login.html");
|
||||
window.location.href = console_url; // Redirect to the KVM session
|
||||
}
|
||||
else {
|
||||
// ilo6 console screen has an inline login
|
||||
console.log("ilo-graphical loading console");
|
||||
set_status("Loading console");
|
||||
const console_url = bmc_url("/irc.html");
|
||||
window.location.href = console_url; // Redirect to the KVM session
|
||||
}
|
||||
} else {
|
||||
console.error("iLO version detection failed:", xhr.status, xhr.statusText);
|
||||
set_error(`Failed to detect iLO version: (${xhr.status}) ${xhr.statusText}`);
|
||||
}
|
||||
}
|
||||
xhr.onerror = function () {
|
||||
console.error("iLO version detection failed:", xhr.status, xhr.statusText);
|
||||
set_error(`Failed to detect iLO version: (${xhr.status}) ${xhr.statusText}`);
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fills in the username and password fields and clicks the login button.
|
||||
* @param {HTMLInputElement} usernameField - The username input element.
|
||||
* @param {HTMLInputElement} passwordField - The password input element.
|
||||
* @param {HTMLButtonElement} loginButton - The login button element.
|
||||
* @param {boolean} redirect - Whether to redirect after successful login (for iLO5).
|
||||
*/
|
||||
function clickLoginButton(usernameField, passwordField, loginButton, redirect) {
|
||||
const username = config.app_info.username;
|
||||
const password = config.app_info.password;
|
||||
|
||||
usernameField.value = username;
|
||||
passwordField.value = password;
|
||||
console.log("logging in", username);
|
||||
loginButton.click();
|
||||
|
||||
if (redirect) {
|
||||
const console_url = bmc_url("/irc.html");
|
||||
let intervalId = setInterval(() => {
|
||||
if (document.cookie.includes("sessionKey")) {
|
||||
console.log("sessionKey cookie found, redirecting...");
|
||||
window.location.href = console_url;
|
||||
clearInterval(intervalId);
|
||||
} else {
|
||||
console.log("Waiting for sessionKey cookie...");
|
||||
}
|
||||
}, 200); // Check every 200 milliseconds
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles the login process by filling in credentials and clicking the login button.
|
||||
* It also observes for disabled login fields and waits for them to become enabled.
|
||||
*/
|
||||
function login(redirect) {
|
||||
const usernameField = document.getElementById("username");
|
||||
const passwordField = document.getElementById("password");
|
||||
const loginButton = document.getElementById("login-form__submit");
|
||||
|
||||
if (!usernameField || !passwordField || !loginButton) {
|
||||
console.log("Username or password field not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (usernameField.disabled) {
|
||||
console.log("Waiting for login fields to be enabled");
|
||||
const observer = new MutationObserver((mutationsList, observer) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
|
||||
if (!usernameField.disabled) {
|
||||
console.log("Login fields are now enabled");
|
||||
clickLoginButton(usernameField, passwordField, loginButton, redirect);
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(usernameField, { attributes: true });
|
||||
} else {
|
||||
clickLoginButton(usernameField, passwordField, loginButton, redirect);
|
||||
}
|
||||
}
|
||||
13
tools/vnc-container/extension/Supermicro.css
Normal file
13
tools/vnc-container/extension/Supermicro.css
Normal file
@@ -0,0 +1,13 @@
|
||||
/* CSS file for Supermicro Graphical Console */
|
||||
|
||||
div#VideoRecordingModal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
nav.navbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
div.modal-backdrop {
|
||||
display: none !important;
|
||||
}
|
||||
73
tools/vnc-container/extension/Supermicro.js
Normal file
73
tools/vnc-container/extension/Supermicro.js
Normal file
@@ -0,0 +1,73 @@
|
||||
window.addEventListener("load", function () {
|
||||
if (window.location.protocol === "file:" && window.location.pathname.endsWith("/drivers/launch/index.html")) {
|
||||
console.log("supermicro-graphical driver launch page loaded");
|
||||
window.location.replace(bmc_url("/"));
|
||||
}
|
||||
else if (window.location.pathname.endsWith("/")) {
|
||||
console.log("supermicro-graphical logging in");
|
||||
login();
|
||||
}
|
||||
else if (window.location.search.includes("url_name=mainmenu")) {
|
||||
console.log("supermicro-graphical waiting for console to be ready");
|
||||
waitForConsoleSnapshot();
|
||||
}
|
||||
else if (window.location.search.includes("url_name=man_ikvm_html5_bootstrap")) {
|
||||
console.log("supermicro-graphical console page loaded");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Fills in the username and password fields and clicks the login button.
|
||||
*/
|
||||
function login() {
|
||||
const username_field = document.querySelector('input[name="name"]');
|
||||
const password_field = document.getElementById('pwd');
|
||||
const login_button = document.getElementById('login_word');
|
||||
|
||||
if (username_field && password_field && login_button) {
|
||||
username_field.value = config.app_info.username;
|
||||
password_field.value = config.app_info.password;
|
||||
login_button.click();
|
||||
} else {
|
||||
console.error("Login elements not found.", username_field, password_field, login_button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Waits for the console snapshot image to load and then clicks it to launch the HTML5 KVM console.
|
||||
*/
|
||||
function waitForConsoleSnapshot() {
|
||||
|
||||
const checkExist = setInterval(() => {
|
||||
const topMenuFrame = document.getElementById("TOPMENU");
|
||||
const topMenuDoc = topMenuFrame.contentDocument || topMenuFrame.contentWindow.document;
|
||||
if (! topMenuDoc){
|
||||
console.log('waiting for topMenuDoc...');
|
||||
return;
|
||||
}
|
||||
const mainFrame = topMenuDoc.getElementById("frame_main");
|
||||
const mainDoc = mainFrame.contentDocument || mainFrame.contentWindow.document;
|
||||
if (! mainDoc){
|
||||
console.log('waiting for mainDoc...');
|
||||
return;
|
||||
}
|
||||
const img1 = mainDoc.getElementById("img1");
|
||||
if (! img1){
|
||||
console.log('waiting for img1');
|
||||
return;
|
||||
}
|
||||
console.log('waiting for img1 to load')
|
||||
if (img1 && img1.src.includes("Snapshot") && img1.complete) {
|
||||
console.log("supermicro-graphical snapshot ready, clicking");
|
||||
clearInterval(checkExist);
|
||||
// override onclick to open as a tab instead of a popup
|
||||
img1.onclick = () => {
|
||||
window.open(bmc_url("/cgi/url_redirect.cgi?url_name=man_ikvm_html5_bootstrap"));
|
||||
}
|
||||
// open by clicking so that window.opener is set on the console page
|
||||
img1.click();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
8
tools/vnc-container/extension/config.js
Normal file
8
tools/vnc-container/extension/config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
/**
|
||||
* Configuration object with placeholder values that are replaced before the browser is run.
|
||||
*/
|
||||
let config = {
|
||||
app: "APP_NAME",
|
||||
app_info: APP_INFO
|
||||
};
|
||||
0
tools/vnc-container/extension/error.css
Normal file
0
tools/vnc-container/extension/error.css
Normal file
6
tools/vnc-container/extension/fake.css
Normal file
6
tools/vnc-container/extension/fake.css
Normal file
@@ -0,0 +1,6 @@
|
||||
/* CSS file for fake driver */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: red;
|
||||
}
|
||||
1
tools/vnc-container/extension/fake.js
Normal file
1
tools/vnc-container/extension/fake.js
Normal file
@@ -0,0 +1 @@
|
||||
log.console("fake startup")
|
||||
37
tools/vnc-container/extension/library.js
Normal file
37
tools/vnc-container/extension/library.js
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a URL using the configured BMC URL.
|
||||
* @returns {string} The complete URL.
|
||||
*/
|
||||
function bmc_url(path) {
|
||||
let url = config.app_info.address;
|
||||
return url + path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the Redfish API base URL using the configured BMC URL.
|
||||
* @returns {string} The complete Redfish API base URL.
|
||||
*/
|
||||
function redfish_url(path) {
|
||||
root_prefix = config.app_info.root_prefix;
|
||||
if (!root_prefix) {
|
||||
root_prefix = "/redfish/v1";
|
||||
}
|
||||
return bmc_url(root_prefix + path);
|
||||
}
|
||||
|
||||
function set_status(status) {
|
||||
qs = new URLSearchParams(window.location.search);
|
||||
if (qs.get("status") == status){
|
||||
return
|
||||
}
|
||||
qs.set("status", status);
|
||||
window.location.search = qs.toString();
|
||||
}
|
||||
|
||||
function set_error(error) {
|
||||
qs = new URLSearchParams(window.location.search);
|
||||
qs.set("error", error);
|
||||
window.location.search = qs.toString();
|
||||
}
|
||||
41
tools/vnc-container/extension/manifest.json
Normal file
41
tools/vnc-container/extension/manifest.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "ironic-console",
|
||||
"version": "1.0",
|
||||
"description": "A Firefox extension that displays bare metal remote graphical consoles",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"cookies",
|
||||
"http://*/*",
|
||||
"https://*/*",
|
||||
"file://*/*"
|
||||
],
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "@ironic-console.openstack.org",
|
||||
"strict_min_version": "42.0"
|
||||
}
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"config.js",
|
||||
"library.js",
|
||||
"APP_NAME.js"
|
||||
],
|
||||
"css": [
|
||||
"APP_NAME.css"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"scripts": [
|
||||
],
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user