1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-16 22:50:10 +03:00

F OpenNebula/one#6684: Add firmware options fetching (#3198)

Signed-off-by: Victor Hansson <vhansson@opennebula.io>
This commit is contained in:
vichansson 2024-08-09 15:16:43 +03:00 committed by GitHub
parent 3d527c3968
commit 6d60b43a04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 405 additions and 175 deletions

View File

@ -16,6 +16,7 @@
import { string, boolean } from 'yup'
import { useGetHostsQuery } from 'client/features/OneApi/host'
import { useGetVmmConfigQuery } from 'client/features/OneApi/system'
import { getKvmMachines } from 'client/models/Host'
import { Field, arrayToOptions } from 'client/utils'
import {
@ -24,7 +25,6 @@ import {
CPU_ARCHITECTURES,
SD_DISK_BUSES,
FIRMWARE_TYPES,
KVM_FIRMWARE_TYPES,
HYPERVISORS,
} from 'client/constants'
@ -143,10 +143,14 @@ export const FIRMWARE = {
.default(() => undefined),
dependOf: ['HYPERVISOR', '$general.HYPERVISOR'],
values: ([templateHyperv, hypervisor = templateHyperv] = []) => {
const types =
{
[kvm]: KVM_FIRMWARE_TYPES,
}[hypervisor] ?? FIRMWARE_TYPES
const configurableHypervisors = [kvm]
const { data: { OVMF_UEFIS = '' } = {} } =
configurableHypervisors?.includes(hypervisor) &&
useGetVmmConfigQuery({ hypervisor })
const types = FIRMWARE_TYPES.concat(
OVMF_UEFIS?.replace(/"/g, '')?.split(' ') ?? []
)
return arrayToOptions(types)
},

View File

@ -96,11 +96,6 @@ export const COMMON_RESOLUTIONS = {
export const FIRMWARE_TYPES = ['BIOS', 'EFI']
export const KVM_FIRMWARE_TYPES = FIRMWARE_TYPES.concat([
'/usr/share/OVMF/OVMF_CODE.fd',
'/usr/share/OVMF/OVMF_CODE.secboot.fd',
])
export const PCI_TYPES = { MANUAL: 'pci_manual', AUTOMATIC: 'pci_automatic' }
export const VCENTER_FIRMWARE_TYPES = FIRMWARE_TYPES.concat(['uefi'])

View File

@ -14,6 +14,10 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Actions, Commands } from 'server/utils/constants/commands/system'
import {
Actions as VmmActions,
Commands as VmmCommands,
} from 'server/routes/api/system/routes'
import {
Actions as SunstoneActions,
Commands as SunstoneCommands,
@ -117,6 +121,24 @@ const systemApi = oneApi.injectEndpoints({
providesTags: [{ type: SYSTEM, id: 'sunstone-avalaibles-views' }],
keepUnusedDataFor: 600,
}),
getVmmConfig: builder.query({
/**
* Returns the hypervisor VMM_EXEC config.
*
* @param {object} params - Request params
* @returns {object} The set config options
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = VmmActions.VMM_CONFIG
const command = { name, ...VmmCommands[name] }
return { params, command }
},
providesTags: [{ type: SYSTEM, id: 'vmm_config' }],
keepUnusedDataFor: 600,
}),
}),
})
@ -126,6 +148,8 @@ export const {
useLazyGetOneVersionQuery,
useGetOneConfigQuery,
useLazyGetOneConfigQuery,
useGetVmmConfigQuery,
useLazyGetVmmConfigQuery,
useGetSunstoneConfigQuery,
useLazyGetSunstoneConfigQuery,
useGetSunstoneViewsQuery,

View File

@ -18,7 +18,7 @@ const {
getAllLogos,
validateLogo,
encodeLogo,
} = require('server/routes/api/logo/utils')
} = require('server/utils/logo')
const { defaults, httpCodes } = require('server/utils/constants')
const { httpResponse } = require('server/utils/server')

View File

@ -1,149 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2024, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
const { getSunstoneViewConfig } = require('server/utils/yml')
const { existsSync, readdirSync } = require('fs')
const path = require('path')
const { global } = require('window-or-global')
const Jimp = require('jimp')
/**
* Retrieves the logo filename.
*
* @returns {string|null} The validated logo filename or null if the filename is invalid or not specified.
*/
const getLogo = () => {
const config = getSunstoneViewConfig()
const logo = config?.logo
const validFilenameRegex = /^[a-zA-Z0-9-_]+\.(jpg|jpeg|png|)$/
if (
logo &&
typeof logo === 'string' &&
logo.trim() !== '' &&
validFilenameRegex.test(logo)
) {
return { valid: true, filename: logo }
}
return { valid: false, filename: null, ...(!logo ? { NOTSET: true } : {}) }
}
/**
* Validates the specified logo file path.
*
* @param {string} logo - The logo file name to validate.
* @param {boolean} relativePaths - Return relative paths instead of absolute
* @returns {string|boolean} Full logo path or false if invalid.
*/
const validateLogo = (logo, relativePaths = false) => {
const imagesDirectory = global?.paths?.SUNSTONE_IMAGES
if (!logo || !imagesDirectory) {
return { valid: false, path: null }
}
const filePath = path.isAbsolute(logo)
? logo
: path.join(imagesDirectory, path.normalize(logo))
if (!filePath?.startsWith(imagesDirectory)) {
return { valid: false, path: null }
}
if (!existsSync(filePath)) {
return { valid: false, path: 'Not found' }
}
if (relativePaths) {
const relativePath = path.relative(imagesDirectory, filePath)
return { valid: true, path: `images/logos/${relativePath}` }
}
return { valid: true, path: filePath }
}
/**
* Encodes an image file at a specified path into a base64 string.
*
* @param {string} filePath - The full path to the image file.
* @returns {Promise<string>} A promise that resolves to the base64 encoded image string.
*/
const encodeLogo = async (filePath) => {
try {
const image = await Jimp.read(filePath)
const data = await image.getBufferAsync(Jimp.MIME_PNG)
return `data:image/png;base64,${data.toString('base64')}`
} catch (error) {
return null
}
}
/**
* Resizes and encodes an image file to be used as a favicon.
*
* @param {string} filePath - The full path to the image file.
* @returns {Promise<string>} A promise that resolves to the base64 encoded image string suitable for favicon use.
*/
const encodeFavicon = async (filePath) => {
try {
const image = await Jimp.read(filePath)
const resizedImage = await image.resize(32, 32)
const data = await resizedImage.getBufferAsync(Jimp.MIME_PNG)
return `data:image/png;base64,${data.toString('base64')}`
} catch (error) {
return null
}
}
/**
* Retrieves all logo files from the assets directory.
*
* @returns {object} A JSON object with filename as key and full path as value.
*/
const getAllLogos = () => {
const imagesDirectory = global?.paths?.SUNSTONE_IMAGES
if (!imagesDirectory || !existsSync(imagesDirectory)) {
return null
}
const files = readdirSync(imagesDirectory)
const validFilenameRegex = /^[a-zA-Z0-9-_]+\.(jpg|jpeg|png|)$/
const logos = files.reduce((acc, file) => {
if (validFilenameRegex.test(file)) {
acc[file.replace(/\.(jpg|jpeg|png)$/, '')] = path.join(
imagesDirectory,
file
)
}
return acc
}, {})
return logos
}
module.exports = {
getLogo,
getAllLogos,
validateLogo,
encodeLogo,
encodeFavicon,
}

View File

@ -22,10 +22,12 @@ const {
Actions: ActionSystem,
} = require('server/utils/constants/commands/system')
const { createTokenServerAdmin } = require('server/routes/api/auth/utils')
const { getVmmConfig } = require('server/utils/vmm')
const { defaultEmptyFunction, httpMethod } = defaults
const { ok, internalServerError, badRequest } = httpCodes
const { ok, internalServerError, badRequest, notFound } = httpCodes
const { GET } = httpMethod
const { writeInLogger } = require('server/utils/logger')
const ALLOWED_KEYS_ONED_CONF = [
'DEFAULT_COST',
@ -112,6 +114,56 @@ const getConfig = (
})
}
/**
*
* @param {object} res - http response
* @param {Function} next - express stepper
* @param {object} params - params of http request
* @param {object} [params.hypervisor="kvm"] - fetch vmm_exec_[hypervisor].conf
* @returns {void}
*/
const getVmmConfigHandler = async (
res = {},
next = defaultEmptyFunction,
params = {}
) => {
try {
const { hypervisor } = params
const vmmConfig = (await getVmmConfig(hypervisor)) ?? {}
if (!vmmConfig) {
res.locals.httpCode = httpResponse(
notFound,
'No vmm_exec config found',
''
)
return next()
}
if (Object.keys(vmmConfig)?.length === 0) {
res.locals.httpCode = httpResponse(
notFound,
'No valid vmm_exec config found',
''
)
} else {
res.locals.httpCode = httpResponse(ok, vmmConfig)
}
} catch (error) {
const httpError = httpResponse(
internalServerError,
'Failed to load vmm_exec config',
''
)
writeInLogger(httpError)
res.locals.httpCode = httpError
}
next()
}
module.exports = {
getConfig,
getVmmConfigHandler,
}

View File

@ -15,13 +15,20 @@
* ------------------------------------------------------------------------- */
const { Actions, Commands } = require('server/routes/api/system/routes')
const { getConfig } = require('server/routes/api/system/functions')
const {
getConfig,
getVmmConfigHandler,
} = require('server/routes/api/system/functions')
const { SYSTEM_CONFIG } = Actions
const { SYSTEM_CONFIG, VMM_CONFIG } = Actions
module.exports = [
{
...Commands[SYSTEM_CONFIG],
action: getConfig,
},
{
...Commands[VMM_CONFIG],
action: getVmmConfigHandler,
},
]

View File

@ -14,14 +14,19 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
const { httpMethod } = require('server/utils/constants/defaults')
const {
from: { query },
httpMethod,
} = require('../../../utils/constants/defaults')
const basepath = '/system'
const { GET } = httpMethod
const SYSTEM_CONFIG = 'system.config'
const VMM_CONFIG = 'vmm.config'
const Actions = {
SYSTEM_CONFIG,
VMM_CONFIG,
}
module.exports = {
@ -32,5 +37,16 @@ module.exports = {
httpMethod: GET,
auth: true,
},
[VMM_CONFIG]: {
path: `${basepath}/vmmconfig`,
httpMethod: GET,
params: {
hypervisor: {
from: query,
default: 'kvm',
},
},
auth: true,
},
},
}

View File

@ -13,11 +13,132 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
const {
getLogo,
validateLogo,
encodeFavicon,
} = require('server/routes/api/logo/utils')
const { getSunstoneViewConfig } = require('server/utils/yml')
const { existsSync, readdirSync } = require('fs')
const path = require('path')
const { global } = require('window-or-global')
const Jimp = require('jimp')
/**
* Retrieves the logo filename.
*
* @returns {string|null} The validated logo filename or null if the filename is invalid or not specified.
*/
const getLogo = () => {
const config = getSunstoneViewConfig()
const logo = config?.logo
const validFilenameRegex = /^[a-zA-Z0-9-_]+\.(jpg|jpeg|png|)$/
if (
logo &&
typeof logo === 'string' &&
logo.trim() !== '' &&
validFilenameRegex.test(logo)
) {
return { valid: true, filename: logo }
}
return { valid: false, filename: null, ...(!logo ? { NOTSET: true } : {}) }
}
/**
* Validates the specified logo file path.
*
* @param {string} logo - The logo file name to validate.
* @param {boolean} relativePaths - Return relative paths instead of absolute
* @returns {string|boolean} Full logo path or false if invalid.
*/
const validateLogo = (logo, relativePaths = false) => {
const imagesDirectory = global?.paths?.SUNSTONE_IMAGES
if (!logo || !imagesDirectory) {
return { valid: false, path: null }
}
const filePath = path.isAbsolute(logo)
? logo
: path.join(imagesDirectory, path.normalize(logo))
if (!filePath?.startsWith(imagesDirectory)) {
return { valid: false, path: null }
}
if (!existsSync(filePath)) {
return { valid: false, path: 'Not found' }
}
if (relativePaths) {
const relativePath = path.relative(imagesDirectory, filePath)
return { valid: true, path: `images/logos/${relativePath}` }
}
return { valid: true, path: filePath }
}
/**
* Encodes an image file at a specified path into a base64 string.
*
* @param {string} filePath - The full path to the image file.
* @returns {Promise<string>} A promise that resolves to the base64 encoded image string.
*/
const encodeLogo = async (filePath) => {
try {
const image = await Jimp.read(filePath)
const data = await image.getBufferAsync(Jimp.MIME_PNG)
return `data:image/png;base64,${data.toString('base64')}`
} catch (error) {
return null
}
}
/**
* Resizes and encodes an image file to be used as a favicon.
*
* @param {string} filePath - The full path to the image file.
* @returns {Promise<string>} A promise that resolves to the base64 encoded image string suitable for favicon use.
*/
const encodeFavicon = async (filePath) => {
try {
const image = await Jimp.read(filePath)
const resizedImage = await image.resize(32, 32)
const data = await resizedImage.getBufferAsync(Jimp.MIME_PNG)
return `data:image/png;base64,${data.toString('base64')}`
} catch (error) {
return null
}
}
/**
* Retrieves all logo files from the assets directory.
*
* @returns {object} A JSON object with filename as key and full path as value.
*/
const getAllLogos = () => {
const imagesDirectory = global?.paths?.SUNSTONE_IMAGES
if (!imagesDirectory || !existsSync(imagesDirectory)) {
return null
}
const files = readdirSync(imagesDirectory)
const validFilenameRegex = /^[a-zA-Z0-9-_]+\.(jpg|jpeg|png|)$/
const logos = files.reduce((acc, file) => {
if (validFilenameRegex.test(file)) {
acc[file.replace(/\.(jpg|jpeg|png)$/, '')] = path.join(
imagesDirectory,
file
)
}
return acc
}, {})
return logos
}
/**
* Retrieves, validates, and encodes a custom favicon image.
@ -54,5 +175,10 @@ const getEncodedFavicon = async () => {
}
module.exports = {
getLogo,
getAllLogos,
validateLogo,
encodeLogo,
encodeFavicon,
getEncodedFavicon,
}

View File

@ -616,6 +616,9 @@ const genPathResources = () => {
if (!global.paths.SUNSTONE_VIEWS) {
global.paths.SUNSTONE_VIEWS = `${ETC_LOCATION}/${defaultSunstonePath}/${defaultSunstoneViews}`
}
if (!global.paths.VMM_EXEC_CONFIG) {
global.paths.VMM_EXEC_CONFIG = `${ETC_LOCATION}/vmm_exec`
}
if (!global.paths.FIREEDGE_KEY_PATH) {
global.paths.FIREEDGE_KEY_PATH = `${VAR_LOCATION}/.one/${defaultKeyFilename}`
}

View File

@ -0,0 +1,109 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2024, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
const fs = require('fs-extra')
const path = require('path')
const { global } = require('window-or-global')
/**
* Parse custom configuration file format.
*
* @param {string} fileContent - Content of the configuration file.
* @returns {object} Parsed configuration object.
*/
const parseConfigFileContent = (fileContent) => {
const lines = fileContent
.split('\n')
.filter((line) => line && !line.startsWith('#'))
const config = {}
let inBlock = false
let currentKey = ''
let blockContent = []
let blockEndMarker = ''
lines.forEach((line) => {
if (!line) return
const trimLine = line?.trim()
if (!inBlock) {
const [key, ...rest] = trimLine.split('=')
const value = rest.join('=').trim()
if (key && /^[A-Za-z0-9_]+$/.test(key.trim())) {
currentKey = key.trim()
if (value.startsWith('[') || value.startsWith('"')) {
blockEndMarker = value.startsWith('[') ? ']' : '"'
if (value.endsWith(blockEndMarker) && value.length > 1) {
config[currentKey] = value
} else {
inBlock = true
blockContent = [value]
}
} else {
config[currentKey] = value
}
}
} else {
blockContent.push(line)
if (line.endsWith(blockEndMarker)) {
config[currentKey] = blockContent.join('\n')
inBlock = false
currentKey = ''
blockContent = []
}
}
})
if (inBlock && currentKey) {
config[currentKey] = blockContent?.join('\n')
}
return config
}
/**
* Get the configuration for a specific hypervisor.
*
* @param {string} hypervisor - The hypervisor type.
* @returns {Promise<object>} Parsed configuration object.
*/
const getVmmConfig = async (hypervisor) => {
const vmmExecConfigDirectory = global?.paths?.VMM_EXEC_CONFIG
const configFilePath = path.join(
vmmExecConfigDirectory,
`vmm_exec_${hypervisor}.conf`
)
if (!(await fs.pathExists(configFilePath))) {
throw new Error(`Configuration file not found: ${configFilePath}`)
}
try {
const fileContent = await fs.readFile(configFilePath, 'utf-8')
const config = parseConfigFileContent(fileContent)
return config
} catch (error) {
throw new Error(`Error parsing config file: ${configFilePath}`)
}
}
module.exports = {
getVmmConfig,
}

View File

@ -157,6 +157,7 @@ const getProvisionConfig = (options) =>
getConfiguration(defaultApps.provision.name, options)
module.exports = {
readYAMLFile,
getFireedgeConfig,
getSunstoneConfig,
getSunstoneViewConfig,

View File

@ -24,6 +24,7 @@ define(function(require) {
var Locale = require("utils/locale");
var Tips = require("utils/tips");
var WizardFields = require("utils/wizard-fields");
var TemplateUtils = require('utils/template-utils');
var FilesTable = require("tabs/files-tab/datatable");
var UniqueId = require("utils/unique-id");
var OpenNebulaHost = require("opennebula/host");
@ -136,8 +137,6 @@ define(function(require) {
var FIRMWARE_VALUES = {
"BIOS": false,
"EFI": false,
"/usr/share/OVMF/OVMF_CODE.fd": false,
"/usr/share/OVMF/OVMF_CODE.secboot.fd": true,
"custom": true
};
@ -322,7 +321,18 @@ define(function(require) {
});
that.initrdFilesTable.refreshResourceTableSelect();
$("#firmwareType", context).change(function() {
var firmwareTypeSelect = $("#firmwareType", context);
TemplateUtils.fetchOvmfValues().done(function(response) {
var ovmfUefis = response.ovmf_uefis;
ovmfUefis.forEach(function(uefi) {
firmwareTypeSelect.append('<option value="' + uefi + '" class="only_kvm">UEFI: ' + uefi + '</option>');
});
}).fail(function() {
console.error('Failed to load UEFI options');
});
firmwareTypeSelect.change(function() {
if (FIRMWARE_VALUES[$(this).val()]){
$("#firmwareSecure", context).show();
}

View File

@ -114,8 +114,6 @@
<select id="firmwareType" wizard_field="FIRMWARE">
<option value="">{{tr "None"}}</option>
<option value="BIOS">{{tr "BIOS"}}</option>
<option value="/usr/share/OVMF/OVMF_CODE.fd" class="only_kvm">UEFI: /usr/share/OVMF/OVMF_CODE.fd</option>
<option value="/usr/share/OVMF/OVMF_CODE.secboot.fd" class="only_kvm">UEFI: /usr/share/OVMF/OVMF_CODE.secboot.fd</option>
<option value="custom" class="only_kvm">{{tr "Custom"}}</option>
<option value="EFI" class="only_vcenter">{{tr "EFI"}}</option>
</select>

View File

@ -181,13 +181,21 @@ define(function(require) {
return rtn;
}
function _fetchOvmfValues() {
return $.ajax({
url: '/ovmf_uefis',
method: 'GET'
});
}
return {
"stringToTemplate": _convert_string_to_template,
"templateToString": _convert_template_to_string,
"htmlDecode": _htmlDecode,
"htmlEncode": _htmlEncode,
"escapeDoubleQuotes": _escapeDoubleQuotes,
"removeHTMLTags": _removeHTMLTags
"removeHTMLTags": _removeHTMLTags,
"fetchOvmfValues": _fetchOvmfValues,
};
});

View File

@ -38,6 +38,7 @@ else
end
VMS_LOCATION = VAR_LOCATION + "/vms"
VMM_EXEC_CONF = ETC_LOCATION + "/vmm_exec/vmm_exec_kvm.conf"
SUNSTONE_AUTH = VAR_LOCATION + '/.one/sunstone_auth'
SUNSTONE_LOG = LOG_LOCATION + '/sunstone.log'
@ -356,6 +357,23 @@ helpers do
session[:csrftoken] && session[:csrftoken] == csrftoken
end
def get_ovmf_uefis
ovmf_uefis = []
if File.exist?(VMM_EXEC_CONF)
File.foreach(VMM_EXEC_CONF) do |line|
if line =~ /^OVMF_UEFIS\s*=\s*"(.+)"$/
ovmf_uefis = $1.split(" ")
break
end
end
else
logger.error("Configuration file not found: #{VMM_EXEC_CONF}")
end
ovmf_uefis
end
def authorized?
session[:ip] && session[:ip] == request.ip
end
@ -862,6 +880,14 @@ get '/version' do
[200, version.to_json]
end
get '/ovmf_uefis' do
content_type 'application/json', :charset => 'utf-8'
ovmf_uefis = {}
ovmf_uefis["ovmf_uefis"] = get_ovmf_uefis
[200, ovmf_uefis.to_json]
end
##############################################################################
# Login
##############################################################################