diff --git a/include/Driver.h b/include/Driver.h
index b54b4a6b1f..a2ab32f308 100644
--- a/include/Driver.h
+++ b/include/Driver.h
@@ -54,7 +54,10 @@ public:
~Driver()
{
- stream_thr.join();
+ if (stream_thr.joinable())
+ {
+ stream_thr.join();
+ }
}
/**
diff --git a/install.sh b/install.sh
index eeb9b03243..1bc096fecb 100755
--- a/install.sh
+++ b/install.sh
@@ -2906,6 +2906,7 @@ FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml \
src/fireedge/etc/sunstone/admin/vm-template-tab.yaml \
src/fireedge/etc/sunstone/admin/marketplace-app-tab.yaml \
src/fireedge/etc/sunstone/admin/vnet-tab.yaml \
+ src/fireedge/etc/sunstone/admin/image-tab.yaml\
src/fireedge/etc/sunstone/admin/host-tab.yaml"
FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \
diff --git a/share/doc/xsd/opennebula_configuration.xsd b/share/doc/xsd/opennebula_configuration.xsd
index ab05c987b1..44ce92ea81 100644
--- a/share/doc/xsd/opennebula_configuration.xsd
+++ b/share/doc/xsd/opennebula_configuration.xsd
@@ -28,6 +28,8 @@
+
+
diff --git a/src/cli/oneflow-template b/src/cli/oneflow-template
index 60505657ca..e77f53a5ba 100755
--- a/src/cli/oneflow-template
+++ b/src/cli/oneflow-template
@@ -239,21 +239,21 @@ CommandParser::CmdParser.new(ARGV) do
number = options[:multiple] || 1
params = {}
rc = 0
+ client = helper.client(options)
number.times do
params['merge_template'] = nil
params['merge_template'] = JSON.parse(File.read(args[1])) if args[1]
unless params['merge_template']
- secret = "#{options[:username]}:#{options[:password]}"
- one_client = OpenNebula::Client.new(secret)
+ response = client.get("#{RESOURCE_PATH}/#{args[0]}")
- service_template = OpenNebula::ServiceTemplate.new_with_id(
- args[0], one_client
- )
- service_template.info
+ if CloudClient.is_error?(response)
+ rc = [response.code.to_i, response.to_s]
+ break
+ end
- body = JSON.parse(service_template['/DOCUMENT/TEMPLATE/BODY'])
+ body = JSON.parse(response.body)['DOCUMENT']['TEMPLATE']['BODY']
params['merge_template'] = helper.custom_attrs(
body['custom_attrs']
@@ -266,7 +266,6 @@ CommandParser::CmdParser.new(ARGV) do
end
json = Service.build_json_action('instantiate', params)
- client = helper.client(options)
response = client.post("#{RESOURCE_PATH}/#{args[0]}/action", json)
if CloudClient.is_error?(response)
diff --git a/src/fireedge/etc/sunstone/admin/image-tab.yaml b/src/fireedge/etc/sunstone/admin/image-tab.yaml
index eea47474bd..c7c91e1d60 100644
--- a/src/fireedge/etc/sunstone/admin/image-tab.yaml
+++ b/src/fireedge/etc/sunstone/admin/image-tab.yaml
@@ -25,23 +25,25 @@ resource_name: "IMAGE"
actions:
create_dialog: true
- clone_dialog: true
import_dialog: true
- create_app_dialog: true # reference to create_dialog in marketplace-app-tab.yaml
- chown: true
- chgrp: true
- enable: true
- disable: true
- persistent: true
- nonpersistent: true
- delete: true
+ dockerfile_dialog: false
+ clone: true
lock: true
unlock: true
+ disable: true
+ enable: true
+ persistent: true
+ nonpersistent: true
+ chown: true
+ chgrp: true
+ delete: true
# Filters - List of criteria to filter the resources
filters:
label: true
+ state: true
+
# Info Tabs - Which info tabs are used to show extended information
@@ -52,8 +54,6 @@ info-tabs:
enabled: true
actions:
rename: true
- chtype: true
- persistent: true
permissions_panel:
enabled: true
actions:
@@ -70,13 +70,11 @@ info-tabs:
add: true
edit: true
delete: true
-
- vm:
+ vms:
enabled: true
-
snapshot:
enabled: true
actions:
snapshot_flatten: true
snapshot_revert: true
- snapshot_delete: true
+ snapshot_delete: true
\ No newline at end of file
diff --git a/src/fireedge/src/client/apps/sunstone/_app.js b/src/fireedge/src/client/apps/sunstone/_app.js
index 4a0aa7ef5e..2439c2baed 100644
--- a/src/fireedge/src/client/apps/sunstone/_app.js
+++ b/src/fireedge/src/client/apps/sunstone/_app.js
@@ -29,6 +29,7 @@ import { useGeneralApi } from 'client/features/General'
import systemApi from 'client/features/OneApi/system'
import Sidebar from 'client/components/Sidebar'
import Notifier from 'client/components/Notifier'
+import NotifierUpload from 'client/components/Notifier/upload'
import { AuthLayout } from 'client/components/HOC'
import { isDevelopment } from 'client/utils'
import { _APPS } from 'client/constants'
@@ -73,6 +74,7 @@ const SunstoneApp = () => {
<>
+
>
)}
diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js
index 65b05186f8..2193575ffb 100644
--- a/src/fireedge/src/client/apps/sunstone/routesOne.js
+++ b/src/fireedge/src/client/apps/sunstone/routesOne.js
@@ -97,6 +97,15 @@ const Datastores = loadable(() => import('client/containers/Datastores'), {
const Images = loadable(() => import('client/containers/Images'), {
ssr: false,
})
+const CreateImages = loadable(() => import('client/containers/Images/Create'), {
+ ssr: false,
+})
+const CreateDockerfile = loadable(
+ () => import('client/containers/Images/Dockerfile'),
+ {
+ ssr: false,
+ }
+)
const Marketplaces = loadable(() => import('client/containers/Marketplaces'), {
ssr: false,
})
@@ -189,6 +198,8 @@ export const PATH = {
IMAGES: {
LIST: `/${RESOURCE_NAMES.IMAGE}`,
DETAIL: `/${RESOURCE_NAMES.IMAGE}/:id`,
+ CREATE: `/${RESOURCE_NAMES.IMAGE}/create`,
+ DOCKERFILE: `/${RESOURCE_NAMES.IMAGE}/dockerfile`,
},
MARKETPLACES: {
LIST: `/${RESOURCE_NAMES.MARKETPLACE}`,
@@ -364,6 +375,16 @@ const ENDPOINTS = [
icon: ImageIcon,
Component: Images,
},
+ {
+ title: T.CreateImage,
+ path: PATH.STORAGE.IMAGES.CREATE,
+ Component: CreateImages,
+ },
+ {
+ title: T.CreateDockerfile,
+ path: PATH.STORAGE.IMAGES.DOCKERFILE,
+ Component: CreateDockerfile,
+ },
{
title: T.Marketplaces,
path: PATH.STORAGE.MARKETPLACES.LIST,
diff --git a/src/fireedge/src/client/components/Cards/ImageCreateCard.js b/src/fireedge/src/client/components/Cards/ImageCreateCard.js
new file mode 100644
index 0000000000..7312f69ca4
--- /dev/null
+++ b/src/fireedge/src/client/components/Cards/ImageCreateCard.js
@@ -0,0 +1,98 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { ReactElement, memo } from 'react'
+import PropTypes from 'prop-types'
+import { Typography, Paper, Grid, Box } from '@mui/material'
+import makeStyles from '@mui/styles/makeStyles'
+
+const useStyles = makeStyles(
+ ({ palette, typography, breakpoints, shadows }) => ({
+ root: {
+ padding: '0.8em',
+ color: palette.text.primary,
+ backgroundColor: palette.background.paper,
+ fontWeight: typography.fontWeightRegular,
+ fontSize: '1em',
+ borderRadius: 6,
+ display: 'flex',
+ gap: 8,
+ cursor: 'pointer',
+ [breakpoints.down('md')]: {
+ flexWrap: 'wrap',
+ },
+ },
+ figure: {
+ flexBasis: '10%',
+ aspectRatio: '16/9',
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ main: {
+ flex: 'auto',
+ overflow: 'hidden',
+ alignSelf: 'center',
+ },
+ title: {
+ color: palette.text.primary,
+ display: 'flex',
+ gap: 6,
+ alignItems: 'center',
+ },
+ })
+)
+
+const ImageCreateCard = memo(
+ /**
+ * @param {object} props - Props
+ * @param {string} props.name - Card name
+ * @param {Function} props.onClick - Card name
+ * @param {ReactElement} props.Icon - Card Icon
+ * @returns {ReactElement} - Card
+ */
+ ({ name = '', onClick, Icon }) => {
+ const classes = useStyles()
+
+ return (
+
+
+ {Icon && (
+
+
+
+ )}
+
+
+
+
+ )
+ }
+)
+
+ImageCreateCard.propTypes = {
+ name: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired,
+ Icon: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
+}
+
+ImageCreateCard.displayName = 'ImageCreateCard'
+
+export default ImageCreateCard
diff --git a/src/fireedge/src/client/components/Cards/ImageSnapshotCard.js b/src/fireedge/src/client/components/Cards/ImageSnapshotCard.js
new file mode 100644
index 0000000000..23081faaf7
--- /dev/null
+++ b/src/fireedge/src/client/components/Cards/ImageSnapshotCard.js
@@ -0,0 +1,99 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { ReactElement, memo, useMemo } from 'react'
+import PropTypes from 'prop-types'
+import { ModernTv } from 'iconoir-react'
+import { Typography, Paper } from '@mui/material'
+
+import { StatusChip } from 'client/components/Status'
+import { rowStyles } from 'client/components/Tables/styles'
+import { Tr, Translate } from 'client/components/HOC'
+
+import { stringToBoolean, timeFromMilliseconds } from 'client/models/Helper'
+import { prettyBytes } from 'client/utils'
+import { T, DiskSnapshot } from 'client/constants'
+
+const ImageSnapshotCard = memo(
+ /**
+ * @param {object} props - Props
+ * @param {DiskSnapshot} props.snapshot - Disk snapshot
+ * @param {function({ snapshot: DiskSnapshot }):ReactElement} [props.actions] - Actions
+ * @returns {ReactElement} - Card
+ */
+ ({ snapshot = {}, actions }) => {
+ const classes = rowStyles()
+
+ const {
+ ID,
+ NAME,
+ ACTIVE,
+ DATE,
+ SIZE: SNAPSHOT_SIZE,
+ MONITOR_SIZE: SNAPSHOT_MONITOR_SIZE,
+ } = snapshot
+
+ const isActive = useMemo(() => stringToBoolean(ACTIVE), [ACTIVE])
+ const time = useMemo(() => timeFromMilliseconds(+DATE), [DATE])
+ const timeFormat = useMemo(() => time.toFormat('ff'), [DATE])
+ const timeAgo = useMemo(() => `created ${time.toRelative()}`, [DATE])
+
+ const sizeInfo = useMemo(() => {
+ const size = +SNAPSHOT_SIZE ? prettyBytes(+SNAPSHOT_SIZE, 'MB') : '-'
+ const monitorSize = +SNAPSHOT_MONITOR_SIZE
+ ? prettyBytes(+SNAPSHOT_MONITOR_SIZE, 'MB')
+ : '-'
+
+ return `${monitorSize}/${size}`
+ }, [SNAPSHOT_SIZE, SNAPSHOT_MONITOR_SIZE])
+
+ return (
+
+
+
+
+ {NAME}
+
+
+ {isActive && } />}
+ } />
+
+
+
+ {`#${ID} ${timeAgo}`}
+
+
+ {` ${sizeInfo}`}
+
+
+
+ {typeof actions === 'function' && (
+ {actions({ snapshot })}
+ )}
+
+ )
+ }
+)
+
+ImageSnapshotCard.propTypes = {
+ snapshot: PropTypes.object.isRequired,
+ actions: PropTypes.func,
+}
+
+ImageSnapshotCard.displayName = 'ImageSnapshotCard'
+
+export default ImageSnapshotCard
diff --git a/src/fireedge/src/client/components/Cards/index.js b/src/fireedge/src/client/components/Cards/index.js
index 9f341cff7d..96143519fc 100644
--- a/src/fireedge/src/client/components/Cards/index.js
+++ b/src/fireedge/src/client/components/Cards/index.js
@@ -23,6 +23,8 @@ import DiskCard from 'client/components/Cards/DiskCard'
import DiskSnapshotCard from 'client/components/Cards/DiskSnapshotCard'
import EmptyCard from 'client/components/Cards/EmptyCard'
import HostCard from 'client/components/Cards/HostCard'
+import ImageCreateCard from 'client/components/Cards/ImageCreateCard'
+import ImageSnapshotCard from 'client/components/Cards/ImageSnapshotCard'
import MarketplaceAppCard from 'client/components/Cards/MarketplaceAppCard'
import MarketplaceCard from 'client/components/Cards/MarketplaceCard'
import NetworkCard from 'client/components/Cards/NetworkCard'
@@ -52,6 +54,8 @@ export {
DiskSnapshotCard,
EmptyCard,
HostCard,
+ ImageCreateCard,
+ ImageSnapshotCard,
MarketplaceAppCard,
MarketplaceCard,
NetworkCard,
diff --git a/src/fireedge/src/client/components/FormControl/DockerfileController.js b/src/fireedge/src/client/components/FormControl/DockerfileController.js
new file mode 100644
index 0000000000..34ae5acab6
--- /dev/null
+++ b/src/fireedge/src/client/components/FormControl/DockerfileController.js
@@ -0,0 +1,69 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { memo, useEffect, useState } from 'react'
+import PropTypes from 'prop-types'
+import { useFormContext } from 'react-hook-form'
+import { ErrorHelper } from 'client/components/FormControl'
+
+import { generateKey } from 'client/utils'
+import InputCode from 'client/components/FormControl/InputCode'
+
+const DockerfileController = memo(
+ ({ control, cy = `input-${generateKey()}`, name = '' }) => {
+ const {
+ getValues,
+ setValue,
+ formState: { errors },
+ } = useFormContext()
+
+ const [internalError, setInternalError] = useState()
+ const messageError = name
+ .split('.')
+ .reduce((errs, current) => errs?.[current], errors)?.message?.[0]
+
+ useEffect(() => {
+ setInternalError(messageError)
+ }, [messageError])
+
+ return (
+
+ {
+ setValue(name, value)
+ }}
+ onFocus={(e) => {
+ setInternalError()
+ }}
+ />
+ {internalError && }
+
+ )
+ },
+ (prevProps, nextProps) => prevProps.cy === nextProps.cy
+)
+
+DockerfileController.propTypes = {
+ control: PropTypes.object,
+ cy: PropTypes.string,
+ name: PropTypes.string.isRequired,
+}
+
+DockerfileController.displayName = 'DockerfileController'
+
+export default DockerfileController
diff --git a/src/fireedge/src/client/components/FormControl/InputCode.js b/src/fireedge/src/client/components/FormControl/InputCode.js
index 4901b742cf..16ec493568 100644
--- a/src/fireedge/src/client/components/FormControl/InputCode.js
+++ b/src/fireedge/src/client/components/FormControl/InputCode.js
@@ -35,9 +35,9 @@ const WrapperToLoadMode = ({ children, mode }) => {
return () => {
// remove all styles when component will be unmounted
- document
- .querySelectorAll('[id^=ace]')
- .forEach((child) => child.parentNode.removeChild(child))
+ document.querySelectorAll('[id^=ace-]').forEach((child) => {
+ child.parentNode.removeChild(child)
+ })
}
}, [])
diff --git a/src/fireedge/src/client/components/FormControl/index.js b/src/fireedge/src/client/components/FormControl/index.js
index 7c1586255d..f9545d28d0 100644
--- a/src/fireedge/src/client/components/FormControl/index.js
+++ b/src/fireedge/src/client/components/FormControl/index.js
@@ -29,6 +29,7 @@ import SubmitButton, {
SubmitButtonPropTypes,
} from 'client/components/FormControl/SubmitButton'
import InputCode from 'client/components/FormControl/InputCode'
+import DockerfileController from 'client/components/FormControl/DockerfileController'
import ErrorHelper from 'client/components/FormControl/ErrorHelper'
import Tooltip from 'client/components/FormControl/Tooltip'
@@ -47,6 +48,7 @@ export {
SubmitButton,
SubmitButtonPropTypes,
InputCode,
+ DockerfileController,
ErrorHelper,
Tooltip,
}
diff --git a/src/fireedge/src/client/components/Forms/FormWithSchema.js b/src/fireedge/src/client/components/Forms/FormWithSchema.js
index ef1d5d8839..e657374ff3 100644
--- a/src/fireedge/src/client/components/Forms/FormWithSchema.js
+++ b/src/fireedge/src/client/components/Forms/FormWithSchema.js
@@ -51,6 +51,7 @@ const INPUT_CONTROLLER = {
[INPUT_TYPES.TIME]: FC.TimeController,
[INPUT_TYPES.TABLE]: FC.TableController,
[INPUT_TYPES.TOGGLE]: FC.ToggleController,
+ [INPUT_TYPES.DOCKERFILE]: FC.DockerfileController,
}
/**
diff --git a/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/index.js b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/index.js
new file mode 100644
index 0000000000..a1e8cd716a
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/index.js
@@ -0,0 +1,59 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import PropTypes from 'prop-types'
+
+import FormWithSchema from 'client/components/Forms/FormWithSchema'
+
+import {
+ SCHEMA,
+ FIELDS,
+} from 'client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/schema'
+import { Step } from 'client/utils'
+import { T } from 'client/constants'
+
+export const STEP_ID = 'configuration'
+
+const Content = (props) => (
+ FIELDS(props)}
+ />
+)
+
+/**
+ * Step to configure the marketplace app.
+ *
+ * @param {object} isMultiple - is multiple rows
+ * @returns {Step} Configuration step
+ */
+const ConfigurationStep = (isMultiple) => ({
+ id: STEP_ID,
+ label: T.Configuration,
+ resolver: () => SCHEMA(isMultiple),
+ optionsValidate: { abortEarly: false },
+ content: () => Content(isMultiple),
+})
+
+Content.propTypes = {
+ data: PropTypes.any,
+ setFormData: PropTypes.func,
+ nics: PropTypes.array,
+ isMultiple: PropTypes.bool,
+}
+
+export default ConfigurationStep
diff --git a/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/schema.js b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/schema.js
new file mode 100644
index 0000000000..25397f69ce
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/schema.js
@@ -0,0 +1,55 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { string, object, ObjectSchema } from 'yup'
+import { Field, getValidationFromFields } from 'client/utils'
+import { T, INPUT_TYPES } from 'client/constants'
+
+const PREFIX = {
+ name: 'prefix',
+ label: T.Prefix,
+ tooltip: T.PrefixMultipleConcept,
+ type: INPUT_TYPES.TEXT,
+ validation: string()
+ .trim()
+ .required()
+ .default(() => T.CopyOf),
+}
+
+const NAME = {
+ name: 'name',
+ label: T.Name,
+ tooltip: T.NewTemplateNameConcept,
+ type: INPUT_TYPES.TEXT,
+ validation: string()
+ .trim()
+ .required()
+ .default(() => undefined),
+}
+
+/**
+ * @param {object} [stepProps] - Step props
+ * @param {boolean} [stepProps.isMultiple]
+ * - If true, the prefix will be added to the name of the new template
+ * @returns {Field[]} Fields
+ */
+export const FIELDS = ({ isMultiple } = {}) => [isMultiple ? PREFIX : NAME]
+
+/**
+ * @param {object} [stepProps] - Step props
+ * @returns {ObjectSchema} Schema
+ */
+export const SCHEMA = (stepProps) =>
+ object(getValidationFromFields(FIELDS(stepProps)))
diff --git a/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/index.js b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/index.js
new file mode 100644
index 0000000000..ecb0a1149c
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/index.js
@@ -0,0 +1,79 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { useMemo } from 'react'
+import PropTypes from 'prop-types'
+import { useFormContext } from 'react-hook-form'
+
+import { DatastoresTable } from 'client/components/Tables'
+import { SCHEMA } from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable/schema'
+
+import { Step, decodeBase64 } from 'client/utils'
+import { T } from 'client/constants'
+
+export const STEP_ID = 'datastore'
+
+const Content = ({ data, app }) => {
+ const { NAME } = data?.[0] ?? {}
+ const { setValue } = useFormContext()
+
+ const isKernelType = useMemo(() => {
+ const appTemplate = String(decodeBase64(app?.TEMPLATE?.APPTEMPLATE64, ''))
+
+ return appTemplate.includes('TYPE="KERNEL"')
+ }, [])
+
+ const handleSelectedRows = (rows) => {
+ const { original = {} } = rows?.[0] ?? {}
+
+ setValue(STEP_ID, original.ID !== undefined ? [original] : [])
+ }
+
+ return (
+ String(row.NAME)}
+ initialState={{
+ selectedRowIds: { [NAME]: true },
+ filters: [{ id: 'TYPE', value: isKernelType ? 'FILE' : 'IMAGE' }],
+ }}
+ onSelectedRowsChange={handleSelectedRows}
+ />
+ )
+}
+
+/**
+ * Step to select the Datastore.
+ *
+ * @param {object} app - Marketplace App resource
+ * @returns {Step} Datastore step
+ */
+const DatastoreStep = (app) => ({
+ id: STEP_ID,
+ label: T.SelectDatastoreImage,
+ resolver: SCHEMA,
+ content: (props) => Content({ ...props, app }),
+})
+
+Content.propTypes = {
+ data: PropTypes.any,
+ setFormData: PropTypes.func,
+ app: PropTypes.object,
+}
+
+export default DatastoreStep
diff --git a/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/schema.js b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/schema.js
new file mode 100644
index 0000000000..2432cb8c5e
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/schema.js
@@ -0,0 +1,24 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { array, object, ArraySchema } from 'yup'
+
+/** @type {ArraySchema} Datastore table schema */
+export const SCHEMA = array(object())
+ .min(1)
+ .max(1)
+ .required()
+ .ensure()
+ .default(() => [])
diff --git a/src/fireedge/src/client/components/Forms/Image/CloneForm/index.js b/src/fireedge/src/client/components/Forms/Image/CloneForm/index.js
new file mode 100644
index 0000000000..0c6567bf1b
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CloneForm/index.js
@@ -0,0 +1,41 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import BasicConfiguration, {
+ STEP_ID as BASIC_ID,
+} from 'client/components/Forms/Image/CloneForm/Steps/BasicConfiguration'
+import DatastoresTable, {
+ STEP_ID as DATASTORE_ID,
+} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
+import { createSteps } from 'client/utils'
+
+const Steps = createSteps(
+ (app) => [BasicConfiguration, DatastoresTable].filter(Boolean),
+ {
+ transformInitialValue: (app, schema) =>
+ schema.cast({}, { context: { app } }),
+ transformBeforeSubmit: (formData) => {
+ const { [BASIC_ID]: configuration, [DATASTORE_ID]: [datastore] = [] } =
+ formData
+
+ return {
+ datastore: datastore?.ID,
+ ...configuration,
+ }
+ },
+ }
+)
+
+export default Steps
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/index.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/index.js
new file mode 100644
index 0000000000..40bc07c35f
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/index.js
@@ -0,0 +1,41 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import FormWithSchema from 'client/components/Forms/FormWithSchema'
+
+import {
+ SCHEMA,
+ FIELDS,
+} from 'client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/schema'
+import { T } from 'client/constants'
+
+export const STEP_ID = 'dockerfile'
+
+const Content = () =>
+
+/**
+ * Docker file.
+ *
+ * @returns {object} Dockerfile step
+ */
+const DockerFile = () => ({
+ id: STEP_ID,
+ label: T.Dockerfile,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: Content,
+})
+
+export default DockerFile
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/schema.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/schema.js
new file mode 100644
index 0000000000..953d742caf
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/schema.js
@@ -0,0 +1,41 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { string, object, ObjectSchema } from 'yup'
+import { Field, getValidationFromFields, encodeBase64 } from 'client/utils'
+import { T, INPUT_TYPES } from 'client/constants'
+
+/** @type {Field} Dockerfile field */
+export const DOCKERFILE = {
+ name: 'PATH',
+ label: T.Dockerfile,
+ type: INPUT_TYPES.DOCKERFILE,
+ cy: 'dockerfile',
+ validation: string()
+ .trim()
+ .required()
+ .afterSubmit((value) => encodeBase64(value)),
+ grid: { md: 12 },
+}
+
+/**
+ * @returns {Field[]} Fields
+ */
+export const FIELDS = [DOCKERFILE]
+
+/**
+ * @returns {ObjectSchema} Schema
+ */
+export const SCHEMA = object(getValidationFromFields(FIELDS))
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/index.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/index.js
new file mode 100644
index 0000000000..5daba02baa
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/index.js
@@ -0,0 +1,42 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+
+import FormWithSchema from 'client/components/Forms/FormWithSchema'
+
+import {
+ SCHEMA,
+ FIELDS,
+} from 'client/components/Forms/Image/CreateDockerfile/Steps/General/schema'
+import { T } from 'client/constants'
+
+export const STEP_ID = 'general'
+
+const Content = () =>
+
+/**
+ * General configuration about VM Template.
+ *
+ * @returns {object} General configuration step
+ */
+const General = () => ({
+ id: STEP_ID,
+ label: T.Configuration,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: Content,
+})
+
+export default General
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/schema.js
new file mode 100644
index 0000000000..882a729afa
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/schema.js
@@ -0,0 +1,64 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { string, boolean, number, object, ObjectSchema } from 'yup'
+import { Field, getValidationFromFields } from 'client/utils'
+import { T, INPUT_TYPES } from 'client/constants'
+
+export const IMAGE_LOCATION_TYPES = {
+ PATH: 'path',
+ UPLOAD: 'upload',
+ EMPTY: 'empty',
+}
+
+/** @type {Field} Name field */
+export const NAME = {
+ name: 'NAME',
+ label: T.Name,
+ type: INPUT_TYPES.TEXT,
+ validation: string().trim().required(),
+ grid: { xs: 12, md: 6 },
+}
+
+/** @type {Field} Context field */
+export const CONTEXT = {
+ name: 'CONTEXT',
+ label: T.Context,
+ type: INPUT_TYPES.SWITCH,
+ validation: boolean().yesOrNo(),
+ grid: { xs: 12, md: 6 },
+}
+
+/** @type {Field} Size field */
+export const SIZE = {
+ name: 'SIZE',
+ htmlType: 'number',
+ label: T.Size,
+ type: INPUT_TYPES.TEXT,
+ tooltip: T.ImageSize,
+ validation: number().positive().required(),
+ grid: { md: 12 },
+}
+
+/**
+ * @returns {Field[]} Fields
+ */
+export const FIELDS = [NAME, CONTEXT, SIZE]
+
+/**
+ * @param {object} [stepProps] - Step props
+ * @returns {ObjectSchema} Schema
+ */
+export const SCHEMA = object(getValidationFromFields(FIELDS))
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/index.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/index.js
new file mode 100644
index 0000000000..220565fa04
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/index.js
@@ -0,0 +1,63 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import Datastore, {
+ STEP_ID as DATASTORE_ID,
+} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
+
+import General, {
+ STEP_ID as GENERAL_ID,
+} from 'client/components/Forms/Image/CreateForm/Steps/General'
+
+import AdvancedOptions, {
+ STEP_ID as ADVANCE_ID,
+} from 'client/components/Forms/Image/CreateForm/Steps/AdvancedOptions'
+
+import CustomAttributes, {
+ STEP_ID as CUSTOM_ID,
+} from 'client/components/Forms/Image/CreateForm/Steps/CustomAttributes'
+
+import { jsonToXml } from 'client/models/Helper'
+import { createSteps, cloneObject, set } from 'client/utils'
+
+const Steps = createSteps(
+ [General, Datastore, AdvancedOptions, CustomAttributes],
+ {
+ transformBeforeSubmit: (formData) => {
+ const {
+ [GENERAL_ID]: general = {},
+ [DATASTORE_ID]: [datastore] = [],
+ [ADVANCE_ID]: advanced = {},
+ [CUSTOM_ID]: custom = {},
+ } = formData ?? {}
+
+ const generalData = cloneObject(general)
+ set(generalData, 'UPLOAD', undefined)
+ set(generalData, 'IMAGE_LOCATION', undefined)
+
+ return {
+ template: jsonToXml({
+ ...custom,
+ ...advanced,
+ ...generalData,
+ }),
+ datastore: datastore?.ID,
+ file: general?.UPLOAD,
+ }
+ },
+ }
+)
+
+export default Steps
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/index.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/index.js
new file mode 100644
index 0000000000..e89c034c82
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/index.js
@@ -0,0 +1,55 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import Datastore, {
+ STEP_ID as DATASTORE_ID,
+} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
+
+import General, {
+ STEP_ID as GENERAL_ID,
+} from 'client/components/Forms/Image/CreateDockerfile/Steps/General'
+
+import Dockerfile, {
+ STEP_ID as DOCKERFILE_ID,
+} from 'client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile'
+
+import { jsonToXml } from 'client/models/Helper'
+import { createSteps, cloneObject, set } from 'client/utils'
+
+const Steps = createSteps([General, Datastore, Dockerfile], {
+ transformBeforeSubmit: (formData) => {
+ const {
+ [GENERAL_ID]: general = {},
+ [DATASTORE_ID]: [datastore] = [],
+ [DOCKERFILE_ID]: dockerfile = {},
+ } = formData ?? {}
+
+ const generalData = cloneObject(general)
+ set(generalData, 'CONTEXT', undefined)
+ set(generalData, 'SIZE', undefined)
+
+ return {
+ template: jsonToXml({
+ ...{
+ PATH: `dockerfile://?fileb64=${dockerfile.PATH}&context=${general.CONTEXT}&size=${general.SIZE}`,
+ },
+ ...generalData,
+ }),
+ datastore: datastore?.ID,
+ }
+ },
+})
+
+export default Steps
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/index.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/index.js
new file mode 100644
index 0000000000..cf211cbe66
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/index.js
@@ -0,0 +1,43 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import FormWithSchema from 'client/components/Forms/FormWithSchema'
+
+import {
+ SCHEMA,
+ FIELDS,
+} from 'client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/schema'
+import { T } from 'client/constants'
+
+export const STEP_ID = 'advanced'
+
+const Content = () => (
+
+)
+
+/**
+ * Advanced options create image.
+ *
+ * @returns {object} Advanced options configuration step
+ */
+const AdvancedOptions = () => ({
+ id: STEP_ID,
+ label: T.AdvancedOptions,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: Content,
+})
+
+export default AdvancedOptions
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/schema.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/schema.js
new file mode 100644
index 0000000000..0008d37980
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/schema.js
@@ -0,0 +1,180 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { string, object, ObjectSchema } from 'yup'
+import { Field, arrayToOptions, getValidationFromFields } from 'client/utils'
+import { T, INPUT_TYPES } from 'client/constants'
+import {
+ IMAGE_LOCATION_TYPES,
+ IMAGE_LOCATION_FIELD,
+} from 'client/components/Forms/Image/CreateForm/Steps/General/schema'
+import { useGetSunstoneConfigQuery } from 'client/features/OneApi/system'
+
+export const BUS_TYPES = {
+ VD: 'path',
+ SD: 'upload',
+ HD: 'empty',
+ CUSTOM: 'custom',
+}
+
+const FORMAT_TYPES = {
+ RAW: 'raw',
+ QCOW2: 'qcow2',
+ CUSTOM: 'custom',
+}
+
+const BUS = {
+ [BUS_TYPES.VD]: T.Vd,
+ [BUS_TYPES.SD]: T.Sd,
+ [BUS_TYPES.HD]: T.Hd,
+ [BUS_TYPES.CUSTOM]: T.Custom,
+}
+
+const htmlType = (opt) => (value) => value !== opt && INPUT_TYPES.HIDDEN
+
+/** @type {Field} Bus field */
+export const DEV_PREFIX = {
+ name: 'DEV_PREFIX',
+ label: T.Bus,
+ type: INPUT_TYPES.SELECT,
+ values: arrayToOptions(Object.entries(BUS), {
+ addEmpty: true,
+ getText: ([_, name]) => name,
+ getValue: ([key]) => key,
+ }),
+ validation: string()
+ .trim()
+ .default(() => undefined)
+ .afterSubmit((value, { context }) => {
+ const notEmptyString = value === '' ? undefined : value
+
+ return BUS_TYPES.CUSTOM === value
+ ? context?.advanced?.CUSTOM_DEV_PREFIX
+ : notEmptyString
+ }),
+ grid: { md: 6 },
+}
+
+/** @type {Field} Dev Prefix field */
+export const CUSTOM_DEV_PREFIX = {
+ name: 'CUSTOM_DEV_PREFIX',
+ dependOf: DEV_PREFIX.name,
+ htmlType: htmlType(BUS_TYPES.CUSTOM),
+ label: T.CustomBus,
+ type: INPUT_TYPES.TEXT,
+ validation: string()
+ .trim()
+ .when(DEV_PREFIX.name, {
+ is: (location) => location === BUS_TYPES.CUSTOM,
+ then: (schema) => schema.required(),
+ otherwise: (schema) => schema.strip(),
+ })
+ .default(() => undefined),
+ grid: { md: 12 },
+}
+
+/** @type {Field} Device field */
+export const DEVICE = {
+ name: 'DEVICE',
+ label: T.TargetDevice,
+ type: INPUT_TYPES.TEXT,
+ validation: string()
+ .trim()
+ .default(() => undefined),
+ grid: { md: 6 },
+}
+
+/** @type {Field} Format field */
+export const FORMAT_FIELD = {
+ name: 'FORMAT',
+ dependOf: `$general.${IMAGE_LOCATION_FIELD.name}`,
+ htmlType: htmlType(IMAGE_LOCATION_TYPES.EMPTY),
+ label: T.Format,
+ type: INPUT_TYPES.SELECT,
+ values: arrayToOptions(Object.values(FORMAT_TYPES), {
+ addEmpty: true,
+ getText: (type) => type.toUpperCase(),
+ getValue: (type) => type,
+ }),
+ validation: string()
+ .trim()
+ .afterSubmit((value, { context }) => {
+ const notEmptyString = value === '' ? undefined : value
+
+ return FORMAT_TYPES.CUSTOM === value
+ ? context?.advanced?.CUSTOM_FORMAT
+ : notEmptyString
+ }),
+ grid: { md: 6 },
+}
+
+/** @type {Field} Custom format field */
+export const CUSTOM_FORMAT = {
+ name: 'CUSTOM_FORMAT',
+ dependOf: [`$general.${IMAGE_LOCATION_FIELD.name}`, FORMAT_FIELD.name],
+ htmlType: ([imageLocation, formatField] = []) =>
+ (imageLocation !== IMAGE_LOCATION_TYPES.EMPTY ||
+ formatField !== FORMAT_TYPES.CUSTOM) &&
+ INPUT_TYPES.HIDDEN,
+ label: T.CustomFormat,
+ type: INPUT_TYPES.TEXT,
+ validation: string()
+ .trim()
+ .when(FORMAT_FIELD.name, {
+ is: (format) => format === FORMAT_TYPES.CUSTOM,
+ then: (schema) => schema.required(),
+ otherwise: (schema) => schema.strip(),
+ }),
+ grid: { md: 12 },
+}
+
+/** @type {Field} FS field */
+export const FS = {
+ name: 'FS',
+ dependOf: `$general.${IMAGE_LOCATION_FIELD.name}`,
+ htmlType: htmlType(IMAGE_LOCATION_TYPES.EMPTY),
+ label: T.Fs,
+ type: INPUT_TYPES.SELECT,
+ values: () => {
+ const { data: sunstoneConfig = {} } = useGetSunstoneConfigQuery()
+
+ return arrayToOptions(sunstoneConfig?.supported_fs || [], {
+ addEmpty: true,
+ getText: (fs) => fs.toUpperCase(),
+ getValue: (fs) => fs,
+ })
+ },
+ validation: string()
+ .trim()
+ .afterSubmit((value) => (value === '' ? undefined : value)),
+ grid: { md: 6 },
+}
+
+/**
+ * @returns {Field[]} Fields
+ */
+export const FIELDS = [
+ DEV_PREFIX,
+ DEVICE,
+ CUSTOM_DEV_PREFIX,
+ FORMAT_FIELD,
+ FS,
+ CUSTOM_FORMAT,
+]
+
+/**
+ * @returns {ObjectSchema} Schema
+ */
+export const SCHEMA = object(getValidationFromFields(FIELDS))
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/CustomAttributes.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/CustomAttributes.js
new file mode 100644
index 0000000000..bfb414ad93
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/CustomAttributes.js
@@ -0,0 +1,67 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { useCallback } from 'react'
+import { object } from 'yup'
+import { useFormContext, useWatch } from 'react-hook-form'
+import { Box } from '@mui/material'
+
+import { AttributePanel } from 'client/components/Tabs/Common'
+import { cleanEmpty, cloneObject, set } from 'client/utils'
+import { T } from 'client/constants'
+
+export const STEP_ID = 'custom-attributes'
+
+const Content = () => {
+ const { setValue } = useFormContext()
+ const customVars = useWatch({ name: STEP_ID, defaultValue: {} })
+
+ const handleChangeAttribute = useCallback(
+ (path, newValue) => {
+ const newCustomVars = cloneObject(customVars)
+ set(newCustomVars, path, newValue)
+ setValue(STEP_ID, cleanEmpty(newCustomVars))
+ },
+ [customVars]
+ )
+
+ return (
+
+
+
+ )
+}
+
+/**
+ * Custom variables about VM Template.
+ *
+ * @returns {object} Custom configuration step
+ */
+const CustomAttributes = () => ({
+ id: STEP_ID,
+ label: T.CustomAttributes,
+ resolver: object(),
+ optionsValidate: { abortEarly: false },
+ content: Content,
+})
+
+export default CustomAttributes
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/index.js
new file mode 100644
index 0000000000..11a9557113
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/index.js
@@ -0,0 +1,44 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+
+import FormWithSchema from 'client/components/Forms/FormWithSchema'
+
+import {
+ SCHEMA,
+ FIELDS,
+} from 'client/components/Forms/Image/CreateForm/Steps/General/schema'
+import { T } from 'client/constants'
+
+export const STEP_ID = 'general'
+
+const Content = () => (
+
+)
+
+/**
+ * General configuration about VM Template.
+ *
+ * @returns {object} General configuration step
+ */
+const General = () => ({
+ id: STEP_ID,
+ label: T.Configuration,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: Content,
+})
+
+export default General
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/schema.js
new file mode 100644
index 0000000000..f3bfa22bfa
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/schema.js
@@ -0,0 +1,185 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { string, boolean, number, object, ObjectSchema, mixed } from 'yup'
+import {
+ Field,
+ arrayToOptions,
+ getValidationFromFields,
+ upperCaseFirst,
+} from 'client/utils'
+import { T, INPUT_TYPES, IMAGE_TYPES_STR } from 'client/constants'
+
+export const IMAGE_LOCATION_TYPES = {
+ PATH: 'path',
+ UPLOAD: 'upload',
+ EMPTY: 'empty',
+}
+
+const IMAGE_LOCATION = {
+ [IMAGE_LOCATION_TYPES.PATH]: T.Path,
+ [IMAGE_LOCATION_TYPES.UPLOAD]: T.Upload,
+ [IMAGE_LOCATION_TYPES.EMPTY]: T.EmptyDisk,
+}
+
+const htmlType = (opt, inputNumber) => (location) => {
+ if (location === opt && inputNumber) {
+ return 'number'
+ }
+
+ return location !== opt && INPUT_TYPES.HIDDEN
+}
+
+/** @type {Field} name field */
+export const NAME = {
+ name: 'NAME',
+ label: T.Name,
+ type: INPUT_TYPES.TEXT,
+ validation: string().trim().required(),
+ grid: { xs: 12, md: 6 },
+}
+
+/** @type {Field} Description field */
+export const DESCRIPTION = {
+ name: 'DESCRIPTION',
+ label: T.Description,
+ type: INPUT_TYPES.TEXT,
+ multiline: true,
+ validation: string().trim(),
+ grid: { xs: 12, md: 6 },
+}
+
+/** @type {Field} Type field */
+export const TYPE = {
+ name: 'TYPE',
+ label: T.Type,
+ type: INPUT_TYPES.SELECT,
+ values: arrayToOptions(Object.values(IMAGE_TYPES_STR), {
+ addEmpty: false,
+ getText: (type) => {
+ switch (type) {
+ case IMAGE_TYPES_STR.OS:
+ return T.Os
+ case IMAGE_TYPES_STR.CDROM:
+ return T.Cdrom
+ case IMAGE_TYPES_STR.DATABLOCK:
+ return T.Datablock
+ default:
+ return upperCaseFirst(type.toLowerCase())
+ }
+ },
+ getValue: (type) => type,
+ }),
+ validation: string()
+ .trim()
+ .default(() => IMAGE_TYPES_STR.OS),
+ grid: { xs: 12, md: 6 },
+}
+
+/** @type {Field} Persistent field */
+export const PERSISTENT = {
+ name: 'PERSISTENT',
+ label: T.MakePersistent,
+ type: INPUT_TYPES.SWITCH,
+ validation: boolean().yesOrNo(),
+ grid: { xs: 12, md: 6 },
+}
+
+/** @type {Field} Toggle select type image */
+export const IMAGE_LOCATION_FIELD = {
+ name: 'IMAGE_LOCATION',
+ type: INPUT_TYPES.TOGGLE,
+ values: arrayToOptions(Object.entries(IMAGE_LOCATION), {
+ addEmpty: false,
+ getText: ([_, name]) => name,
+ getValue: ([image]) => image,
+ }),
+ validation: string()
+ .trim()
+ .required()
+ .default(() => IMAGE_LOCATION_TYPES.PATH),
+ grid: { md: 12 },
+ notNull: true,
+}
+/** @type {Field} path field */
+export const PATH_FIELD = {
+ name: 'PATH',
+ dependOf: IMAGE_LOCATION_FIELD.name,
+ htmlType: htmlType(IMAGE_LOCATION_TYPES.PATH),
+ label: T.ImagePath,
+ type: INPUT_TYPES.TEXT,
+ validation: string()
+ .trim()
+ .when(IMAGE_LOCATION_FIELD.name, {
+ is: (location) => location === IMAGE_LOCATION_TYPES.PATH,
+ then: (schema) => schema.required(),
+ otherwise: (schema) => schema.strip(),
+ }),
+ grid: { md: 12 },
+}
+
+/** @type {Field} upload field */
+export const UPLOAD_FIELD = {
+ name: 'UPLOAD',
+ dependOf: IMAGE_LOCATION_FIELD.name,
+ htmlType: htmlType(IMAGE_LOCATION_TYPES.UPLOAD),
+ label: T.Upload,
+ type: INPUT_TYPES.FILE,
+ validation: mixed().when(IMAGE_LOCATION_FIELD.name, {
+ is: (location) => location === IMAGE_LOCATION_TYPES.UPLOAD,
+ then: (schema) => schema.required(),
+ otherwise: (schema) => schema.strip(),
+ }),
+ grid: { md: 12 },
+}
+
+/** @type {Field} size field */
+export const SIZE = {
+ name: 'SIZE',
+ dependOf: IMAGE_LOCATION_FIELD.name,
+ htmlType: htmlType(IMAGE_LOCATION_TYPES.EMPTY, true),
+ label: T.Size,
+ type: INPUT_TYPES.TEXT,
+ tooltip: T.ImageSize,
+ validation: number()
+ .positive()
+ .default(() => undefined)
+ .when(IMAGE_LOCATION_FIELD.name, {
+ is: (location) => location === IMAGE_LOCATION_TYPES.EMPTY,
+ then: (schema) => schema.required(),
+ otherwise: (schema) => schema.strip(),
+ }),
+ grid: { md: 12 },
+}
+
+/**
+ * @returns {Field[]} Fields
+ */
+export const FIELDS = [
+ NAME,
+ DESCRIPTION,
+ TYPE,
+ PERSISTENT,
+ IMAGE_LOCATION_FIELD,
+ PATH_FIELD,
+ UPLOAD_FIELD,
+ SIZE,
+]
+
+/**
+ * @param {object} [stepProps] - Step props
+ * @returns {ObjectSchema} Schema
+ */
+export const SCHEMA = object(getValidationFromFields(FIELDS))
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/index.js
new file mode 100644
index 0000000000..40e5be8d62
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/index.js
@@ -0,0 +1,62 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import Datastore, {
+ STEP_ID as DATASTORE_ID,
+} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
+
+import General, {
+ STEP_ID as GENERAL_ID,
+} from 'client/components/Forms/Image/CreateForm/Steps/General'
+
+import AdvancedOptions, {
+ STEP_ID as ADVANCE_ID,
+} from 'client/components/Forms/Image/CreateForm/Steps/AdvancedOptions'
+
+import CustomAttributes, {
+ STEP_ID as CUSTOM_ID,
+} from 'client/components/Forms/Image/CreateForm/Steps/CustomAttributes'
+
+import { createSteps, cloneObject, set } from 'client/utils'
+
+const Steps = createSteps(
+ [General, Datastore, AdvancedOptions, CustomAttributes],
+ {
+ transformBeforeSubmit: (formData) => {
+ const {
+ [GENERAL_ID]: general = {},
+ [DATASTORE_ID]: [datastore] = [],
+ [ADVANCE_ID]: advanced = {},
+ [CUSTOM_ID]: custom = {},
+ } = formData ?? {}
+
+ const generalData = cloneObject(general)
+ set(generalData, 'UPLOAD', undefined)
+ set(generalData, 'IMAGE_LOCATION', undefined)
+
+ return {
+ template: {
+ ...custom,
+ ...advanced,
+ ...generalData,
+ },
+ datastore: datastore?.ID,
+ file: general?.UPLOAD,
+ }
+ },
+ }
+)
+
+export default Steps
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/index.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/index.js
new file mode 100644
index 0000000000..abc8b4f240
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/index.js
@@ -0,0 +1,16 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+export { default } from 'client/components/Forms/Image/CreateForm/Steps'
diff --git a/src/fireedge/src/client/components/Forms/Image/index.js b/src/fireedge/src/client/components/Forms/Image/index.js
new file mode 100644
index 0000000000..94153475b7
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/index.js
@@ -0,0 +1,41 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { ReactElement } from 'react'
+import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
+import { CreateFormCallback, CreateStepsCallback } from 'client/utils/schema'
+
+/**
+ * @param {ConfigurationProps} configProps - Configuration
+ * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
+ */
+const CloneForm = (configProps) =>
+ AsyncLoadForm({ formPath: 'Image/CloneForm' }, configProps)
+
+/**
+ * @param {ConfigurationProps} configProps - Configuration
+ * @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
+ */
+const CreateForm = (configProps) =>
+ AsyncLoadForm({ formPath: 'Image/CreateForm' }, configProps)
+
+/**
+ * @param {ConfigurationProps} configProps - Configuration
+ * @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
+ */
+const CreateDockerfileForm = (configProps) =>
+ AsyncLoadForm({ formPath: 'Image/CreateDockerfile' }, configProps)
+
+export { CloneForm, CreateForm, CreateDockerfileForm }
diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js
index 8a9aa0ac8a..e484a1a223 100644
--- a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js
+++ b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js
@@ -22,7 +22,7 @@ import AdvancedOptions, {
import { createSteps } from 'client/utils'
const Steps = createSteps([NetworksTable, AdvancedOptions], {
- transformInitialValue: (nic) => {
+ transformInitialValue: (nic, schema) => {
const {
NETWORK,
NETWORK_ID: ID,
@@ -32,6 +32,11 @@ const Steps = createSteps([NetworksTable, AdvancedOptions], {
...rest
} = nic ?? {}
+ const castedValue = schema.cast(
+ { [ADVANCED_ID]: rest },
+ { stripUnknown: true }
+ )
+
return {
[NETWORK_ID]: [
{
@@ -43,7 +48,7 @@ const Steps = createSteps([NetworksTable, AdvancedOptions], {
SECURITY_GROUPS,
},
],
- [ADVANCED_ID]: rest,
+ [ADVANCED_ID]: castedValue[ADVANCED_ID],
}
},
transformBeforeSubmit: (formData) => {
diff --git a/src/fireedge/src/client/components/Notifier/upload.js b/src/fireedge/src/client/components/Notifier/upload.js
new file mode 100644
index 0000000000..a8bbc59f43
--- /dev/null
+++ b/src/fireedge/src/client/components/Notifier/upload.js
@@ -0,0 +1,84 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { ReactElement } from 'react'
+import {
+ Snackbar,
+ Alert,
+ CircularProgress,
+ Box,
+ Typography,
+} from '@mui/material'
+import { useGeneral, useGeneralApi } from 'client/features/General'
+
+/**
+ * @returns {ReactElement} App rendered.
+ */
+const NotifierUpload = () => {
+ const { upload } = useGeneral()
+ const { uploadSnackbar } = useGeneralApi()
+
+ const handleClose = () => uploadSnackbar(0)
+
+ return (
+ 0}
+ autoHideDuration={10000}
+ onClose={handleClose}
+ anchorOrigin={{
+ vertical: 'bottom',
+ horizontal: 'right',
+ }}
+ >
+
+
+
+
+
+ {`${upload}%`}
+
+
+
+
+
+ )
+}
+
+export default NotifierUpload
diff --git a/src/fireedge/src/client/components/Tables/Images/actions.js b/src/fireedge/src/client/components/Tables/Images/actions.js
new file mode 100644
index 0000000000..72794d067c
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/Images/actions.js
@@ -0,0 +1,388 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { useMemo } from 'react'
+import { useHistory } from 'react-router-dom'
+import { Typography, Grid } from '@mui/material'
+import {
+ MoreVert,
+ AddCircledOutline,
+ Lock,
+ Cart,
+ Group,
+ Trash,
+ Code,
+ PageEdit,
+} from 'iconoir-react'
+
+import { useViews } from 'client/features/Auth'
+import {
+ useLockImageMutation,
+ useCloneImageMutation,
+ useUnlockImageMutation,
+ useEnableImageMutation,
+ useDisableImageMutation,
+ usePersistentImageMutation,
+ useChangeImageOwnershipMutation,
+ useRemoveImageMutation,
+} from 'client/features/OneApi/image'
+
+import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm'
+import { CloneForm } from 'client/components/Forms/Image'
+import {
+ createActions,
+ GlobalAction,
+} from 'client/components/Tables/Enhanced/Utils'
+import ImageCreateCard from 'client/components/Cards/ImageCreateCard'
+
+import { Tr, Translate } from 'client/components/HOC'
+import { PATH } from 'client/apps/sunstone/routesOne'
+import { isAvailableAction } from 'client/models/VirtualMachine'
+import { T, IMAGE_ACTIONS, VM_ACTIONS, RESOURCE_NAMES } from 'client/constants'
+
+const isDisabled = (action) => (rows) =>
+ !isAvailableAction(
+ action,
+ rows.map(({ original }) => original)
+ )
+
+const ListImagesNames = ({ rows = [] }) =>
+ rows?.map?.(({ id, original }) => {
+ const { ID, NAME } = original
+
+ return (
+
+ {`#${ID} ${NAME}`}
+
+ )
+ })
+
+const SubHeader = (rows) =>
+
+const MessageToConfirmAction = (rows) => (
+ <>
+
+
+ >
+)
+
+/**
+ * Generates the actions to operate resources on Image table.
+ *
+ * @returns {GlobalAction} - Actions
+ */
+const Actions = () => {
+ const history = useHistory()
+ const { view, getResourceView } = useViews()
+ const [clone] = useCloneImageMutation()
+ const [lock] = useLockImageMutation()
+ const [unlock] = useUnlockImageMutation()
+ const [enable] = useEnableImageMutation()
+ const [disable] = useDisableImageMutation()
+ const [persistent] = usePersistentImageMutation()
+ const [changeOwnership] = useChangeImageOwnershipMutation()
+ const [deleteImage] = useRemoveImageMutation()
+
+ const resourcesView = getResourceView(RESOURCE_NAMES.IMAGE)?.actions
+
+ const imageActions = useMemo(
+ () =>
+ createActions({
+ filters: resourcesView,
+ actions: [
+ {
+ accessor: IMAGE_ACTIONS.CREATE_DIALOG,
+ dataCy: `image_${IMAGE_ACTIONS.CREATE_DIALOG}`,
+ tooltip: T.Create,
+ icon: AddCircledOutline,
+ options: [
+ {
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.CreateImage,
+ children: () => (
+
+ history.push(PATH.STORAGE.IMAGES.CREATE)}
+ />
+ {resourcesView?.dockerfile_dialog && (
+
+ history.push(PATH.STORAGE.IMAGES.DOCKERFILE)
+ }
+ />
+ )}
+
+ ),
+ fixedWidth: true,
+ fixedHeight: true,
+ handleAccept: undefined,
+ dataCy: `modal-${IMAGE_ACTIONS.CREATE_DIALOG}`,
+ },
+ },
+ ],
+ },
+ {
+ accessor: VM_ACTIONS.CREATE_APP_DIALOG,
+ dataCy: `image_${VM_ACTIONS.CREATE_APP_DIALOG}`,
+ disabled: isDisabled(VM_ACTIONS.CREATE_APP_DIALOG),
+ tooltip: T.CreateMarketApp,
+ selected: { max: 1 },
+ icon: Cart,
+ action: (rows) => {
+ const vm = rows?.[0]?.original ?? {}
+ const path = PATH.STORAGE.MARKETPLACE_APPS.CREATE
+
+ history.push(path, [RESOURCE_NAMES.VM, vm])
+ },
+ },
+ {
+ accessor: IMAGE_ACTIONS.CLONE,
+ label: T.Clone,
+ tooltip: T.Clone,
+ selected: true,
+ color: 'secondary',
+ options: [
+ {
+ dialogProps: {
+ title: (rows) => {
+ const isMultiple = rows?.length > 1
+ const { ID, NAME } = rows?.[0]?.original ?? {}
+
+ return [
+ Tr(
+ isMultiple ? T.CloneSeveralTemplates : T.CloneTemplate
+ ),
+ !isMultiple && `#${ID} ${NAME}`,
+ ]
+ .filter(Boolean)
+ .join(' - ')
+ },
+ dataCy: 'modal-clone',
+ },
+ form: (rows) => {
+ const names = rows?.map(({ original }) => original?.NAME)
+ const stepProps = { isMultiple: names.length > 1 }
+ const initialValues = { name: `Copy of ${names?.[0]}` }
+
+ return CloneForm({ stepProps, initialValues })
+ },
+ onSubmit:
+ (rows) =>
+ async ({ prefix, name, datastore } = {}) => {
+ const images = rows?.map?.(
+ ({ original: { ID, NAME } = {} }) =>
+ // overwrite all names with prefix+NAME
+ ({
+ id: ID,
+ name: prefix ? `${prefix} ${NAME}` : name,
+ datastore,
+ })
+ )
+
+ await Promise.all(images.map(clone))
+ },
+ },
+ ],
+ },
+ {
+ tooltip: T.Lock,
+ icon: Lock,
+ selected: true,
+ color: 'secondary',
+ dataCy: 'image-lock',
+ options: [
+ {
+ accessor: IMAGE_ACTIONS.LOCK,
+ name: T.Lock,
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Lock,
+ dataCy: `modal-${IMAGE_ACTIONS.LOCK}`,
+ children: MessageToConfirmAction,
+ },
+ onSubmit: (rows) => async () => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(ids.map((id) => lock({ id })))
+ },
+ },
+ {
+ accessor: IMAGE_ACTIONS.UNLOCK,
+ name: T.Unlock,
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Unlock,
+ dataCy: `modal-${IMAGE_ACTIONS.UNLOCK}`,
+ children: MessageToConfirmAction,
+ },
+ onSubmit: (rows) => async () => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(ids.map((id) => unlock({ id })))
+ },
+ },
+ ],
+ },
+ {
+ tooltip: T.Enable,
+ icon: MoreVert,
+ selected: true,
+ color: 'secondary',
+ dataCy: 'image-enable',
+ options: [
+ {
+ accessor: IMAGE_ACTIONS.ENABLE,
+ name: T.Enable,
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Enable,
+ children: MessageToConfirmAction,
+ dataCy: `modal-${IMAGE_ACTIONS.ENABLE}`,
+ },
+ onSubmit: (rows) => async () => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(ids.map((id) => enable(id)))
+ },
+ },
+ {
+ accessor: IMAGE_ACTIONS.DISABLE,
+ name: T.Disable,
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Disable,
+ children: MessageToConfirmAction,
+ dataCy: `modal-${IMAGE_ACTIONS.DISABLE}`,
+ },
+ onSubmit: (rows) => async () => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(ids.map((id) => disable(id)))
+ },
+ },
+ {
+ accessor: IMAGE_ACTIONS.PERSISTENT,
+ name: T.Persistent,
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Persistent,
+ children: MessageToConfirmAction,
+ dataCy: `modal-${IMAGE_ACTIONS.PERSISTENT}`,
+ },
+ onSubmit: (rows) => async () => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(
+ ids.map((id) => persistent({ id, persistent: true }))
+ )
+ },
+ },
+ {
+ accessor: IMAGE_ACTIONS.NON_PERSISTENT,
+ name: T.NonPersistyent,
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.NonPersistyent,
+ children: MessageToConfirmAction,
+ dataCy: `modal-${IMAGE_ACTIONS.NON_PERSISTENT}`,
+ },
+ onSubmit: (rows) => async () => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(
+ ids.map((id) => persistent({ id, persistent: false }))
+ )
+ },
+ },
+ ],
+ },
+ {
+ tooltip: T.Ownership,
+ icon: Group,
+ selected: true,
+ color: 'secondary',
+ dataCy: 'image-ownership',
+ options: [
+ {
+ accessor: IMAGE_ACTIONS.CHANGE_OWNER,
+ disabled: isDisabled(IMAGE_ACTIONS.CHANGE_OWNER),
+ name: T.ChangeOwner,
+ dialogProps: {
+ title: T.ChangeOwner,
+ subheader: SubHeader,
+ dataCy: `modal-${IMAGE_ACTIONS.CHANGE_OWNER}`,
+ },
+ form: ChangeUserForm,
+ onSubmit: (rows) => async (newOwnership) => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(
+ ids.map((id) => changeOwnership({ id, ...newOwnership }))
+ )
+ },
+ },
+ {
+ accessor: IMAGE_ACTIONS.CHANGE_GROUP,
+ disabled: isDisabled(IMAGE_ACTIONS.CHANGE_GROUP),
+ name: T.ChangeGroup,
+ dialogProps: {
+ title: T.ChangeGroup,
+ subheader: SubHeader,
+ dataCy: `modal-${IMAGE_ACTIONS.CHANGE_GROUP}`,
+ },
+ form: ChangeGroupForm,
+ onSubmit: (rows) => async (newOwnership) => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(
+ ids.map((id) => changeOwnership({ id, ...newOwnership }))
+ )
+ },
+ },
+ ],
+ },
+ {
+ accessor: IMAGE_ACTIONS.DELETE,
+ tooltip: T.Delete,
+ icon: Trash,
+ color: 'error',
+ selected: { min: 1 },
+ dataCy: `image_${IMAGE_ACTIONS.DELETE}`,
+ options: [
+ {
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Delete,
+ dataCy: `modal-${IMAGE_ACTIONS.DELETE}`,
+ children: MessageToConfirmAction,
+ },
+ onSubmit: (rows) => async () => {
+ const ids = rows?.map?.(({ original }) => original?.ID)
+ await Promise.all(ids.map((id) => deleteImage({ id })))
+ },
+ },
+ ],
+ },
+ ],
+ }),
+ [view]
+ )
+
+ return imageActions
+}
+
+export default Actions
diff --git a/src/fireedge/src/client/components/Tables/Images/columns.js b/src/fireedge/src/client/components/Tables/Images/columns.js
index 1b9cbd6fb9..6520cc6ea0 100644
--- a/src/fireedge/src/client/components/Tables/Images/columns.js
+++ b/src/fireedge/src/client/components/Tables/Images/columns.js
@@ -25,6 +25,7 @@ export default [
{ Header: 'Name', accessor: 'NAME' },
{ Header: 'Owner', accessor: 'UNAME' },
{ Header: 'Group', accessor: 'GNAME' },
+ { Header: 'Locked', id: 'locked', accessor: 'LOCK' },
{
Header: 'State',
id: 'STATE',
diff --git a/src/fireedge/src/client/components/Tables/Images/row.js b/src/fireedge/src/client/components/Tables/Images/row.js
index 42e70263cb..c760f48e98 100644
--- a/src/fireedge/src/client/components/Tables/Images/row.js
+++ b/src/fireedge/src/client/components/Tables/Images/row.js
@@ -36,7 +36,7 @@ const Row = ({ original, value, ...props }) => {
TYPE,
DISK_TYPE,
PERSISTENT,
- LOCK,
+ locked,
DATASTORE,
TOTAL_VMS,
RUNNING_VMS,
@@ -59,7 +59,7 @@ const Row = ({ original, value, ...props }) => {
{NAME}
- {LOCK && }
+ {locked && }
{labels.map((label) => (
diff --git a/src/fireedge/src/client/components/Tables/Vms/index.js b/src/fireedge/src/client/components/Tables/Vms/index.js
index 8c0abf60d9..a6ee0f797a 100644
--- a/src/fireedge/src/client/components/Tables/Vms/index.js
+++ b/src/fireedge/src/client/components/Tables/Vms/index.js
@@ -21,7 +21,7 @@ import { useGetVmsQuery } from 'client/features/OneApi/vm'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import VmColumns from 'client/components/Tables/Vms/columns'
import VmRow from 'client/components/Tables/Vms/row'
-import { RESOURCE_NAMES } from 'client/constants'
+import { RESOURCE_NAMES, VM_STATES, STATES } from 'client/constants'
const DEFAULT_DATA_CY = 'vms'
@@ -55,7 +55,7 @@ const VmsTable = (props) => {
?.filter((vm) =>
host?.ID ? [host?.VMS?.ID ?? []].flat().includes(vm.ID) : true
)
- ?.filter(({ STATE }) => STATE !== '6') ?? [],
+ ?.filter(({ STATE }) => VM_STATES[STATE]?.name !== STATES.DONE) ?? [],
}),
})
diff --git a/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js b/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
index 71fe2fb7b5..1fd41804ff 100644
--- a/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
+++ b/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
@@ -1,18 +1,3 @@
-/* ------------------------------------------------------------------------- *
- * Copyright 2002-2022, 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. *
- * ------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
diff --git a/src/fireedge/src/client/components/Tabs/EmptyTab/index.js b/src/fireedge/src/client/components/Tabs/EmptyTab/index.js
index bb5e8a7a70..fc575eb577 100644
--- a/src/fireedge/src/client/components/Tabs/EmptyTab/index.js
+++ b/src/fireedge/src/client/components/Tabs/EmptyTab/index.js
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
+import PropTypes from 'prop-types'
import { ReactElement } from 'react'
import { InfoEmpty } from 'iconoir-react'
@@ -24,19 +25,23 @@ import { T } from 'client/constants'
/**
* Renders default empty tab.
*
+ * @param {object} props - Props
+ * @param {string} props.label - label string
* @returns {ReactElement} Empty tab
*/
-const EmptyTab = () => {
+const EmptyTab = ({ label = T.NoDataAvailable }) => {
const classes = useStyles()
return (
-
+
)
}
-
+EmptyTab.propTypes = {
+ label: PropTypes.string,
+}
EmptyTab.displayName = 'EmptyTab'
export default EmptyTab
diff --git a/src/fireedge/src/client/components/Tabs/Image/Info/information.js b/src/fireedge/src/client/components/Tabs/Image/Info/information.js
index e775031ccb..10369ad68b 100644
--- a/src/fireedge/src/client/components/Tabs/Image/Info/information.js
+++ b/src/fireedge/src/client/components/Tabs/Image/Info/information.js
@@ -26,7 +26,11 @@ import { StatusChip } from 'client/components/Status'
import { List } from 'client/components/Tabs/Common'
import { getType, getState } from 'client/models/Image'
-import { timeToString, booleanToString } from 'client/models/Helper'
+import {
+ timeToString,
+ booleanToString,
+ levelLockToString,
+} from 'client/models/Helper'
import { arrayToOptions, prettyBytes } from 'client/utils'
import { T, Image, IMAGE_ACTIONS, IMAGE_TYPES } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
@@ -49,6 +53,7 @@ const InformationPanel = ({ image = {}, actions }) => {
NAME,
SIZE,
PERSISTENT,
+ LOCK,
REGTIME,
DATASTORE_ID,
DATASTORE = '--',
@@ -110,6 +115,11 @@ const InformationPanel = ({ image = {}, actions }) => {
handleEdit: handleChangeType,
dataCy: 'type',
},
+ {
+ name: T.Locked,
+ value: levelLockToString(LOCK?.LOCKED),
+ dataCy: 'locked',
+ },
{
name: T.Persistent,
value: booleanToString(+PERSISTENT),
@@ -127,6 +137,7 @@ const InformationPanel = ({ image = {}, actions }) => {
{
name: T.State,
value: ,
+ dataCy: 'state',
},
{
name: T.RunningVMs,
diff --git a/src/fireedge/src/client/components/Tabs/Image/Snapshots/Actions.js b/src/fireedge/src/client/components/Tabs/Image/Snapshots/Actions.js
new file mode 100644
index 0000000000..cd6982ce99
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/Image/Snapshots/Actions.js
@@ -0,0 +1,142 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { memo } from 'react'
+import PropTypes from 'prop-types'
+
+import { Trash, UndoAction } from 'iconoir-react'
+
+import {
+ useFlattenImageSnapshotMutation,
+ useRevertImageSnapshotMutation,
+ useDeleteImageSnapshotMutation,
+} from 'client/features/OneApi/image'
+import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
+
+import { Tr, Translate } from 'client/components/HOC'
+import { T, IMAGE_ACTIONS } from 'client/constants'
+
+const SnapshotFlattenAction = memo(({ id, snapshot }) => {
+ const [flattenImageSnapshot] = useFlattenImageSnapshotMutation()
+ const { ID, NAME = T.Snapshot } = snapshot
+
+ const handleDelete = async () => {
+ await flattenImageSnapshot({ id, snapshot: ID })
+ }
+
+ return (
+
+ ),
+ children: (
+ <>
+ {Tr(T.DeleteOtherSnapshots)}
+ {Tr(T.DoYouWantProceed)}
+ >
+ ),
+ },
+ onSubmit: handleDelete,
+ },
+ ]}
+ />
+ )
+})
+
+const SnapshotRevertAction = memo(({ id, snapshot }) => {
+ const [revertImageSnapshot] = useRevertImageSnapshotMutation()
+ const { ID, NAME = T.Snapshot } = snapshot
+ const handleRevert = async () => {
+ await revertImageSnapshot({ id, snapshot: ID })
+ }
+
+ return (
+ ,
+ tooltip: Tr(T.Revert),
+ }}
+ options={[
+ {
+ isConfirmDialog: true,
+ dialogProps: {
+ title: (
+
+ ),
+ children: {Tr(T.DoYouWantProceed)}
,
+ },
+ onSubmit: handleRevert,
+ },
+ ]}
+ />
+ )
+})
+
+const SnapshotDeleteAction = memo(({ id, snapshot }) => {
+ const [deleteImageSnapshot] = useDeleteImageSnapshotMutation()
+ const { ID, NAME = T.Snapshot } = snapshot
+
+ const handleDelete = async () => {
+ await deleteImageSnapshot({ id, snapshot: ID })
+ }
+
+ return (
+ ,
+ tooltip: Tr(T.Delete),
+ }}
+ options={[
+ {
+ isConfirmDialog: true,
+ dialogProps: {
+ title: (
+
+ ),
+ children: {Tr(T.DoYouWantProceed)}
,
+ },
+ onSubmit: handleDelete,
+ },
+ ]}
+ />
+ )
+})
+
+const ActionPropTypes = {
+ id: PropTypes.string,
+ snapshot: PropTypes.object,
+}
+
+SnapshotFlattenAction.propTypes = ActionPropTypes
+SnapshotFlattenAction.displayName = 'SnapshotFlattenAction'
+
+SnapshotRevertAction.propTypes = ActionPropTypes
+SnapshotRevertAction.displayName = 'SnapshotRevertAction'
+
+SnapshotDeleteAction.propTypes = ActionPropTypes
+SnapshotDeleteAction.displayName = 'SnapshotDeleteAction'
+
+export { SnapshotFlattenAction, SnapshotDeleteAction, SnapshotRevertAction }
diff --git a/src/fireedge/src/client/components/Tabs/Image/Snapshots/index.js b/src/fireedge/src/client/components/Tabs/Image/Snapshots/index.js
new file mode 100644
index 0000000000..0da46613d1
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/Image/Snapshots/index.js
@@ -0,0 +1,84 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { ReactElement, useMemo } from 'react'
+import PropTypes from 'prop-types'
+
+import { Stack } from '@mui/material'
+import { T } from 'client/constants'
+import { useGetImageQuery } from 'client/features/OneApi/image'
+import EmptyTab from 'client/components/Tabs/EmptyTab'
+import ImageSnapshotCard from 'client/components/Cards/ImageSnapshotCard'
+import {
+ SnapshotFlattenAction,
+ SnapshotDeleteAction,
+ SnapshotRevertAction,
+} from 'client/components/Tabs/Image/Snapshots/Actions'
+
+import { getSnapshots } from 'client/models/Image'
+
+/**
+ * Renders the list of disks from a VM.
+ *
+ * @param {object} props - Props
+ * @param {object} props.tabProps - Tab information
+ * @param {string[]} props.tabProps.actions - Actions tab
+ * @param {string} props.id - Image id
+ * @returns {ReactElement} Storage tab
+ */
+const ImageStorageTab = ({ tabProps: { actions } = {}, id }) => {
+ const { data: image = {} } = useGetImageQuery({ id })
+
+ const [snapshots] = useMemo(() => [getSnapshots(image)], [image])
+
+ return (
+
+
+ {snapshots.length ? (
+ snapshots?.map?.((snapshot) => (
+ (
+ <>
+ {actions.snapshot_flatten && (
+
+ )}
+ {actions.snapshot_revert && (
+
+ )}
+ {actions.snapshot_delete && (
+
+ )}
+ >
+ )}
+ />
+ ))
+ ) : (
+
+ )}
+
+
+ )
+}
+
+ImageStorageTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+ImageStorageTab.displayName = 'ImageStorageTab'
+
+export default ImageStorageTab
diff --git a/src/fireedge/src/client/components/Tabs/Image/Vms/index.js b/src/fireedge/src/client/components/Tabs/Image/Vms/index.js
new file mode 100644
index 0000000000..9d3c5ae441
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/Image/Vms/index.js
@@ -0,0 +1,59 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { ReactElement } from 'react'
+import PropTypes from 'prop-types'
+import { T } from 'client/constants'
+import EmptyTab from 'client/components/Tabs/EmptyTab'
+import { useHistory, generatePath } from 'react-router-dom'
+import { PATH } from 'client/apps/sunstone/routesOne'
+import { useGetImageQuery } from 'client/features/OneApi/image'
+import { VmsTable } from 'client/components/Tables'
+
+/**
+ * Renders mainly Vms tab.
+ *
+ * @param {object} props - Props
+ * @param {string} props.id - Image id
+ * @returns {ReactElement} vms tab
+ */
+const VmsTab = ({ id }) => {
+ const { data: image = {} } = useGetImageQuery({ id })
+ const path = PATH.INSTANCE.VMS.DETAIL
+ const history = useHistory()
+
+ const handleRowClick = (rowId) => {
+ history.push(generatePath(path, { id: String(rowId) }))
+ }
+
+ return (
+ handleRowClick(row.ID)}
+ noDataMessage={}
+ />
+ )
+}
+
+VmsTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+VmsTab.displayName = 'VmsTab'
+
+export default VmsTab
diff --git a/src/fireedge/src/client/components/Tabs/Image/index.js b/src/fireedge/src/client/components/Tabs/Image/index.js
index 7631e21fb2..0bed8794b3 100644
--- a/src/fireedge/src/client/components/Tabs/Image/index.js
+++ b/src/fireedge/src/client/components/Tabs/Image/index.js
@@ -24,10 +24,14 @@ import { RESOURCE_NAMES } from 'client/constants'
import Tabs from 'client/components/Tabs'
import Info from 'client/components/Tabs/Image/Info'
+import Vms from 'client/components/Tabs/Image/Vms'
+import Snapshots from 'client/components/Tabs/Image/Snapshots'
const getTabComponent = (tabName) =>
({
info: Info,
+ vms: Vms,
+ snapshot: Snapshots,
}[tabName])
const ImageTabs = memo(({ id }) => {
diff --git a/src/fireedge/src/client/constants/image.js b/src/fireedge/src/client/constants/image.js
index e7f4edccdd..a7c3c9ead0 100644
--- a/src/fireedge/src/client/constants/image.js
+++ b/src/fireedge/src/client/constants/image.js
@@ -165,6 +165,13 @@ export const IMAGE_STATES = [
export const IMAGE_ACTIONS = {
CREATE_DIALOG: 'create_dialog',
DELETE: 'delete',
+ LOCK: 'lock',
+ UNLOCK: 'unlock',
+ CLONE: 'clone',
+ ENABLE: 'enable',
+ DISABLE: 'disable',
+ PERSISTENT: 'persistent',
+ NON_PERSISTENT: 'nonpersistent',
// INFORMATION
RENAME: ACTIONS.RENAME,
@@ -172,4 +179,7 @@ export const IMAGE_ACTIONS = {
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
CHANGE_TYPE: 'chtype',
CHANGE_PERS: 'persistent',
+ SNAPSHOT_FLATTEN: 'flatten',
+ SNAPSHOT_REVERT: 'revert',
+ SNAPSHOT_DELETE: 'delete',
}
diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js
index 368c435ff5..1a8ca7a918 100644
--- a/src/fireedge/src/client/constants/index.js
+++ b/src/fireedge/src/client/constants/index.js
@@ -128,6 +128,7 @@ export const INPUT_TYPES = {
TEXT: 'text',
TABLE: 'table',
TOGGLE: 'toggle',
+ DOCKERFILE: 'dockerfile',
}
export const DEBUG_LEVEL = {
diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js
index ddf505bd14..60593a2bda 100644
--- a/src/fireedge/src/client/constants/translates.js
+++ b/src/fireedge/src/client/constants/translates.js
@@ -71,6 +71,8 @@ module.exports = {
CreateServiceTemplate: 'Create Service Template',
CreateVirtualNetwork: 'Create Virtual Network',
CreateVmTemplate: 'Create VM Template',
+ CreateImage: 'Create Image',
+ CreateDockerfile: 'Create Dockerfile',
CurrentGroup: 'Current group: %s',
CurrentOwner: 'Current owner: %s',
Delete: 'Delete',
@@ -83,6 +85,7 @@ module.exports = {
DeleteAddressRange: 'Delete Address Range',
DeleteTemplate: 'Delete Template',
DeleteVirtualNetwork: 'Delete Virtual Network',
+ DeleteOtherSnapshots: 'This will delete all the other image snapshots',
Deploy: 'Deploy',
DeployServiceTemplate: 'Deploy Service Template',
Detach: 'Detach',
@@ -95,6 +98,8 @@ module.exports = {
Enable: 'Enable',
Failure: 'Failure',
Finish: 'Finish',
+ Flatten: 'Flatten',
+ FlattenSnapshot: 'Flatten %s',
Hold: 'Hold',
Import: 'Import',
Info: 'Info',
@@ -138,6 +143,7 @@ module.exports = {
Select: 'Select',
SelectCluster: 'Select Cluster',
SelectDatastore: 'Select a Datastore to store the resource',
+ SelectDatastoreImage: 'Select a Datastore',
SelectDockerHubTag: 'Select DockerHub image tag (default latest)',
SelectGroup: 'Select a group',
SelectHost: 'Select a host',
@@ -358,6 +364,21 @@ module.exports = {
Marketplaces: 'Marketplaces',
App: 'App',
Apps: 'Apps',
+ Os: 'Operating system image',
+ Cdrom: 'Readonly CD-ROM',
+ Datablock: 'Generic storage datablock',
+ Path: 'Path/URL',
+ ImagePath: 'Path in OpenNebula server or URL',
+ Upload: 'Upload',
+ EmptyDisk: 'Empty disk image',
+ ImageSize: 'Image size, in Megabytes',
+ Vd: 'Virtio',
+ Sd: 'SCSI/SATA',
+ Hd: 'Parallel ATA (IDE)',
+ CustomBus: 'Custom bus',
+ Fs: 'Fs',
+ CustomFormat: 'Custom Format',
+ Dockerfile: 'Dockerfile',
/* sections - templates & instances */
Instances: 'Instances',
@@ -560,6 +581,7 @@ module.exports = {
CustomAttributes: 'Custom Attributes',
Hypervisor: 'Hypervisor',
Logo: 'Logo',
+ MakePersistent: 'Make Persistent',
MakeNewImagePersistent: 'Make the new images persistent',
TemplateName: 'Template name',
Virtualization: 'Virtualization',
@@ -1039,6 +1061,7 @@ module.exports = {
BasePath: 'Base path',
FileSystemType: 'Filesystem type',
Persistent: 'Persistent',
+ NonPersistyent: 'Non Persistent',
RunningVMs: 'Running VMs',
/* Disk - general */
DiskType: 'Disk type',
@@ -1074,6 +1097,7 @@ module.exports = {
Iothread id used by this disk. Default is round robin.
Can be used only if IOTHREADS > 0. If this input is disabled
please first configure IOTHREADS value on OS & CPU -> Features`,
+ ImageLocation: 'Image Location',
/* Provision schema */
/* Provision - general */
@@ -1097,6 +1121,11 @@ module.exports = {
Mandatory: 'Mandatory',
PressKeysToAddAValue: 'Press any of the following keys to add a value: %s',
+ /** Image */
+ NotVmsCurrenty: 'There are currently no VMs associated with this image',
+ NotSnapshotCurrenty:
+ 'There are currently no snapshots associated with this image',
+
/* Validation */
/* Validation - mixed */
'validation.mixed.default': 'Is invalid',
diff --git a/src/fireedge/src/client/containers/Images/Create.js b/src/fireedge/src/client/containers/Images/Create.js
new file mode 100644
index 0000000000..cd6d89e31c
--- /dev/null
+++ b/src/fireedge/src/client/containers/Images/Create.js
@@ -0,0 +1,81 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { ReactElement } from 'react'
+import { useHistory } from 'react-router'
+
+import { jsonToXml } from 'client/models/Helper'
+import { useGeneralApi } from 'client/features/General'
+import {
+ useAllocateImageMutation,
+ useUploadImageMutation,
+} from 'client/features/OneApi/image'
+import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
+
+import {
+ DefaultFormStepper,
+ SkeletonStepsForm,
+} from 'client/components/FormStepper'
+import { CreateForm } from 'client/components/Forms/Image'
+import { PATH } from 'client/apps/sunstone/routesOne'
+
+/**
+ * Displays the creation or modification form to a VM Template.
+ *
+ * @returns {ReactElement} VM Template form
+ */
+function CreateImage() {
+ const history = useHistory()
+ const [allocate] = useAllocateImageMutation()
+ const [upload] = useUploadImageMutation()
+ const { enqueueSuccess, uploadSnackbar } = useGeneralApi()
+ useGetDatastoresQuery(undefined, { refetchOnMountOrArgChange: false })
+
+ const onSubmit = async ({ template, datastore, file }) => {
+ if (file) {
+ const uploadProcess = (progressEvent) => {
+ const percentCompleted = Math.round(
+ (progressEvent.loaded / progressEvent.total) * 100
+ )
+ uploadSnackbar(percentCompleted)
+ percentCompleted === 100 && uploadSnackbar(0)
+ }
+ try {
+ const fileUploaded = await upload({
+ file,
+ uploadProcess,
+ }).unwrap()
+ template.PATH = fileUploaded[0]
+ } catch {}
+ }
+
+ try {
+ const newTemplateId = await allocate({
+ template: jsonToXml(template),
+ datastore,
+ }).unwrap()
+ history.push(PATH.STORAGE.IMAGES.LIST)
+ enqueueSuccess(`Image created - #${newTemplateId}`)
+ } catch {}
+ }
+
+ return (
+ }>
+ {(config) => }
+
+ )
+}
+
+export default CreateImage
diff --git a/src/fireedge/src/client/containers/Images/Dockerfile.js b/src/fireedge/src/client/containers/Images/Dockerfile.js
new file mode 100644
index 0000000000..cd598aa097
--- /dev/null
+++ b/src/fireedge/src/client/containers/Images/Dockerfile.js
@@ -0,0 +1,56 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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. *
+ * ------------------------------------------------------------------------- */
+import { ReactElement } from 'react'
+import { useHistory } from 'react-router'
+
+import { useGeneralApi } from 'client/features/General'
+import { useAllocateImageMutation } from 'client/features/OneApi/image'
+import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
+
+import {
+ DefaultFormStepper,
+ SkeletonStepsForm,
+} from 'client/components/FormStepper'
+import { CreateDockerfileForm } from 'client/components/Forms/Image'
+import { PATH } from 'client/apps/sunstone/routesOne'
+
+/**
+ * Displays the creation or modification form to a VM Template.
+ *
+ * @returns {ReactElement} VM Template form
+ */
+function CreateDockerfile() {
+ const history = useHistory()
+ const [allocate] = useAllocateImageMutation()
+ const { enqueueSuccess } = useGeneralApi()
+ useGetDatastoresQuery(undefined, { refetchOnMountOrArgChange: false })
+
+ const onSubmit = async (stepTemplate) => {
+ try {
+ const newTemplateId = await allocate(stepTemplate).unwrap()
+ history.push(PATH.STORAGE.IMAGES.LIST)
+ enqueueSuccess(`Image created - #${newTemplateId}`)
+ } catch {}
+ }
+
+ return (
+ }>
+ {(config) => }
+
+ )
+}
+
+export default CreateDockerfile
diff --git a/src/fireedge/src/client/containers/Images/index.js b/src/fireedge/src/client/containers/Images/index.js
index f1086c51d2..f66bdb6819 100644
--- a/src/fireedge/src/client/containers/Images/index.js
+++ b/src/fireedge/src/client/containers/Images/index.js
@@ -21,11 +21,9 @@ import Cancel from 'iconoir-react/dist/Cancel'
import { Typography, Box, Stack, Chip } from '@mui/material'
import { Row } from 'react-table'
-import {
- useLazyGetImageQuery,
- useUpdateImageMutation,
-} from 'client/features/OneApi/image'
+import { useLazyGetImageQuery } from 'client/features/OneApi/image'
import { ImagesTable } from 'client/components/Tables'
+import ImageActions from 'client/components/Tables/Images/actions'
import ImageTabs from 'client/components/Tabs/Image'
import SplitPane from 'client/components/SplitPane'
import MultipleTags from 'client/components/MultipleTags'
@@ -40,6 +38,7 @@ import { T, Image } from 'client/constants'
*/
function Images() {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
+ const actions = ImageActions()
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
@@ -50,7 +49,7 @@ function Images() {
{hasSelectedRows && (
@@ -82,24 +81,19 @@ function Images() {
* @returns {ReactElement} Image details
*/
const InfoTabs = memo(({ image, gotoPage, unselect }) => {
- const [get, { data: lazyData, isFetching }] = useLazyGetImageQuery()
+ const [getImage, { data: lazyData, isFetching }] = useLazyGetImageQuery()
const id = lazyData?.ID ?? image.ID
const name = lazyData?.NAME ?? image.NAME
return (
-
-
- {`#${id} | ${name}`}
-
-
- {/* -- ACTIONS -- */}
+
}
tooltip={Tr(T.Refresh)}
isSubmitting={isFetching}
- onClick={() => get({ id })}
+ onClick={() => getImage({ id })}
/>
{typeof gotoPage === 'function' && (
{
onClick={() => unselect()}
/>
)}
- {/* -- END ACTIONS -- */}
+
+ {`#${id} | ${name}`}
+
-
+
)
})
diff --git a/src/fireedge/src/client/features/General/actions.js b/src/fireedge/src/client/features/General/actions.js
index 2125ca9877..49420a90e5 100644
--- a/src/fireedge/src/client/features/General/actions.js
+++ b/src/fireedge/src/client/features/General/actions.js
@@ -22,6 +22,7 @@ export const changeAppTitle = createAction('Change App title')
export const dismissSnackbar = createAction('Dismiss snackbar')
export const deleteSnackbar = createAction('Delete snackbar')
+export const setUploadSnackbar = createAction('Change upload snackbar')
export const enqueueSnackbar = createAction(
'Enqueue snackbar',
diff --git a/src/fireedge/src/client/features/General/hooks.js b/src/fireedge/src/client/features/General/hooks.js
index 6948cab549..c3d3031928 100644
--- a/src/fireedge/src/client/features/General/hooks.js
+++ b/src/fireedge/src/client/features/General/hooks.js
@@ -32,6 +32,8 @@ export const useGeneralApi = () => {
changeAppTitle: (appTitle) => dispatch(actions.changeAppTitle(appTitle)),
changeZone: (zone) => dispatch(actions.changeZone(zone)),
+ uploadSnackbar: (percent) => dispatch(actions.setUploadSnackbar(percent)),
+
// dismiss all if no key has been defined
dismissSnackbar: (key) =>
dispatch(actions.dismissSnackbar({ key, dismissAll: !key })),
diff --git a/src/fireedge/src/client/features/General/slice.js b/src/fireedge/src/client/features/General/slice.js
index 6be92feac6..7422be930e 100644
--- a/src/fireedge/src/client/features/General/slice.js
+++ b/src/fireedge/src/client/features/General/slice.js
@@ -27,6 +27,7 @@ const initial = {
withGroupSwitcher: false,
isLoading: false,
isFixMenu: false,
+ upload: 0,
notifications: [],
}
@@ -64,7 +65,11 @@ const slice = createSlice({
...state,
zone: payload,
}))
-
+ /* UPLOAD NOTIFICATION */
+ .addCase(actions.setUploadSnackbar, (state, { payload }) => ({
+ ...state,
+ upload: payload,
+ }))
/* NOTIFICATION ACTIONS */
.addCase(actions.enqueueSnackbar, (state, { payload }) => {
const { key, options, message } = payload
diff --git a/src/fireedge/src/client/features/OneApi/image.js b/src/fireedge/src/client/features/OneApi/image.js
index c626552eed..2fbcfc9efb 100644
--- a/src/fireedge/src/client/features/OneApi/image.js
+++ b/src/fireedge/src/client/features/OneApi/image.js
@@ -20,12 +20,14 @@ import {
ONE_RESOURCES_POOL,
} from 'client/features/OneApi'
import { UpdateFromSocket } from 'client/features/OneApi/socket'
+import http from 'client/utils/rest'
import {
FilterFlag,
Image,
Permission,
IMAGE_TYPES_STR,
} from 'client/constants'
+import { getType } from 'client/models/Image'
const { IMAGE } = ONE_RESOURCES
const { IMAGE_POOL } = ONE_RESOURCES_POOL
@@ -49,7 +51,19 @@ const imageApi = oneApi.injectEndpoints({
return { params, command }
},
- transformResponse: (data) => [data?.IMAGE_POOL?.IMAGE ?? []].flat(),
+ transformResponse: (data) => {
+ const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) => {
+ const type = getType(image)
+
+ return (
+ type === IMAGE_TYPES_STR.OS ||
+ type === IMAGE_TYPES_STR.CDROM ||
+ type === IMAGE_TYPES_STR.DATABLOCK
+ )
+ })
+
+ return [images ?? []].flat()
+ },
providesTags: (images) =>
images
? [
@@ -101,6 +115,35 @@ const imageApi = oneApi.injectEndpoints({
},
invalidatesTags: [IMAGE_POOL],
}),
+ uploadImage: builder.mutation({
+ /**
+ * Upload image.
+ *
+ * @param {object} params - request params
+ * @param {object} params.file - image file
+ * @param {Function} params.uploadProcess - upload process function
+ * @returns {number} Virtual machine id
+ * @throws Fails when response isn't code 200
+ */
+ queryFn: async ({ file, uploadProcess }) => {
+ try {
+ const data = new FormData()
+ data.append('files', file)
+ const response = await http.request({
+ url: '/api/image/upload',
+ method: 'POST',
+ data,
+ onUploadProgress: uploadProcess,
+ })
+
+ return { data: response.data }
+ } catch (axiosError) {
+ const { response } = axiosError
+
+ return { error: { status: response?.status, data: response?.data } }
+ }
+ },
+ }),
cloneImage: builder.mutation({
/**
* Clones an existing image.
@@ -408,4 +451,5 @@ export const {
useFlattenImageSnapshotMutation,
useLockImageMutation,
useUnlockImageMutation,
+ useUploadImageMutation,
} = imageApi
diff --git a/src/fireedge/src/client/features/OneApi/system.js b/src/fireedge/src/client/features/OneApi/system.js
index 8cc6e5402b..6e797bc9a3 100644
--- a/src/fireedge/src/client/features/OneApi/system.js
+++ b/src/fireedge/src/client/features/OneApi/system.js
@@ -81,6 +81,22 @@ const systemApi = oneApi.injectEndpoints({
providesTags: [{ type: SYSTEM, id: 'sunstone-views' }],
keepUnusedDataFor: 600,
}),
+ getSunstoneConfig: builder.query({
+ /**
+ * Returns the Sunstone configuration for resource tabs.
+ *
+ * @returns {object} The loaded sunstone view files
+ * @throws Fails when response isn't code 200
+ */
+ query: () => {
+ const name = SunstoneActions.SUNSTONE_CONFIG
+ const command = { name, ...SunstoneCommands[name] }
+
+ return { command }
+ },
+ providesTags: [{ type: SYSTEM, id: 'sunstone-config' }],
+ keepUnusedDataFor: 600,
+ }),
}),
})
@@ -90,6 +106,8 @@ export const {
useLazyGetOneVersionQuery,
useGetOneConfigQuery,
useLazyGetOneConfigQuery,
+ useGetSunstoneConfigQuery,
+ useLazyGetSunstoneConfigQuery,
useGetSunstoneViewsQuery,
useLazyGetSunstoneViewsQuery,
} = systemApi
diff --git a/src/fireedge/src/client/models/Image.js b/src/fireedge/src/client/models/Image.js
index 09b58a91b1..95f9e5cddf 100644
--- a/src/fireedge/src/client/models/Image.js
+++ b/src/fireedge/src/client/models/Image.js
@@ -19,6 +19,7 @@ import {
IMAGE_STATES,
STATES,
Image,
+ DiskSnapshot,
} from 'client/constants'
import { prettyBytes } from 'client/utils'
@@ -60,3 +61,15 @@ export const getDiskName = ({ IMAGE, SIZE, TYPE, FORMAT } = {}) => {
return IMAGE ?? { fs: `${FORMAT} - ${size}`, swap: size }[type]
}
+
+/**
+ * @param {Image} image - Image
+ * @returns {DiskSnapshot[]} List of snapshots from resource
+ */
+export const getSnapshots = (image) => {
+ const {
+ SNAPSHOTS: { SNAPSHOT },
+ } = image ?? {}
+
+ return [SNAPSHOT].flat().filter(Boolean)
+}
diff --git a/src/fireedge/src/server/routes/api/files/functions.js b/src/fireedge/src/server/routes/api/files/functions.js
index 92f3de800e..a18fc7464f 100644
--- a/src/fireedge/src/server/routes/api/files/functions.js
+++ b/src/fireedge/src/server/routes/api/files/functions.js
@@ -157,59 +157,60 @@ const upload = (
const { app, files, public: publicFile } = params
const { id, user, password } = userData
if (
- global.paths.CPI &&
- app &&
- checkValidApp(app) &&
- files &&
- id &&
- user &&
- password
- ) {
- const oneConnect = oneConnection(user, password)
- checkUserAdmin(
- oneConnect,
- id,
- (admin = false) => {
- const pathUserData =
- publicFile && admin ? `${app}` : `${app}${sep}${id}`
- const pathUser = `${global.paths.CPI}${sep}${pathUserData}`
- if (!existsSync(pathUser)) {
- mkdirsSync(pathUser)
- }
- let method = ok
- let message = ''
- const data = []
- for (const file of files) {
- if (file && file.originalname && file.path && file.filename) {
- const extFile = extname(file.originalname)
- try {
- const filenameApi = `${pathUserData}${sep}${file.filename}${extFile}`
- const filename = `${pathUser}${sep}${file.filename}${extFile}`
- moveSync(file.path, filename)
- data.push(filenameApi)
- } catch (error) {
- method = internalServerError
- message = error && error.message
- break
- }
- }
- }
- res.locals.httpCode = httpResponse(
- method,
- data.length ? data : '',
- message
- )
- next()
- },
- () => {
- res.locals.httpCode = internalServerError
- next()
- }
+ !(
+ global.paths.CPI &&
+ app &&
+ checkValidApp(app) &&
+ files &&
+ id &&
+ user &&
+ password
)
- } else {
+ ) {
res.locals.httpCode = httpBadRequest
next()
}
+
+ const oneConnect = oneConnection(user, password)
+ checkUserAdmin(
+ oneConnect,
+ id,
+ (admin = false) => {
+ const pathUserData = publicFile && admin ? `${app}` : `${app}${sep}${id}`
+ const pathUser = `${global.paths.CPI}${sep}${pathUserData}`
+ if (!existsSync(pathUser)) {
+ mkdirsSync(pathUser)
+ }
+ let method = ok
+ let message = ''
+ const data = []
+ for (const file of files) {
+ if (file && file.originalname && file.path && file.filename) {
+ const extFile = extname(file.originalname)
+ try {
+ const filenameApi = `${pathUserData}${sep}${file.filename}${extFile}`
+ const filename = `${pathUser}${sep}${file.filename}${extFile}`
+ moveSync(file.path, filename)
+ data.push(filenameApi)
+ } catch (error) {
+ method = internalServerError
+ message = error && error.message
+ break
+ }
+ }
+ }
+ res.locals.httpCode = httpResponse(
+ method,
+ data.length ? data : '',
+ message
+ )
+ next()
+ },
+ () => {
+ res.locals.httpCode = internalServerError
+ next()
+ }
+ )
}
/**
diff --git a/src/fireedge/src/server/routes/api/image/functions.js b/src/fireedge/src/server/routes/api/image/functions.js
new file mode 100644
index 0000000000..4cca70821c
--- /dev/null
+++ b/src/fireedge/src/server/routes/api/image/functions.js
@@ -0,0 +1,55 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { defaults, httpCodes } = require('server/utils/constants')
+
+const { httpResponse } = require('server/utils/server')
+
+const { defaultEmptyFunction } = defaults
+
+const { ok, badRequest } = httpCodes
+
+const httpBadRequest = httpResponse(badRequest, '', '')
+
+/**
+ * Upload File.
+ *
+ * @param {object} res - response http
+ * @param {Function} next - express stepper
+ * @param {string} params - data response http
+ * @param {object} userData - user of http request
+ */
+const upload = (
+ res = {},
+ next = defaultEmptyFunction,
+ params = {},
+ userData = {}
+) => {
+ const { files } = params
+ const { user, password } = userData
+ if (!(files && user && password)) {
+ res.locals.httpCode = httpBadRequest
+ next()
+ }
+
+ const data = files.map((file) => file.path)
+ res.locals.httpCode = httpResponse(ok, data.length ? data : '')
+ next()
+}
+
+const functionRoutes = {
+ upload,
+}
+module.exports = functionRoutes
diff --git a/src/fireedge/src/server/routes/api/image/index.js b/src/fireedge/src/server/routes/api/image/index.js
new file mode 100644
index 0000000000..50829ea03e
--- /dev/null
+++ b/src/fireedge/src/server/routes/api/image/index.js
@@ -0,0 +1,27 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { Actions, Commands } = require('server/routes/api/image/routes')
+const { upload } = require('server/routes/api/image/functions')
+
+const { IMAGE_UPLOAD } = Actions
+
+module.exports = [
+ {
+ ...Commands[IMAGE_UPLOAD],
+ action: upload,
+ },
+]
diff --git a/src/fireedge/src/server/routes/api/image/routes.js b/src/fireedge/src/server/routes/api/image/routes.js
new file mode 100644
index 0000000000..7d0d03f742
--- /dev/null
+++ b/src/fireedge/src/server/routes/api/image/routes.js
@@ -0,0 +1,42 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2022, 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 { httpMethod } = require('../../../utils/constants/defaults')
+
+const { POST } = httpMethod
+
+const basepath = '/image'
+const IMAGE_UPLOAD = 'image.upload'
+
+const Actions = {
+ IMAGE_UPLOAD,
+}
+
+module.exports = {
+ Actions,
+ Commands: {
+ [IMAGE_UPLOAD]: {
+ path: `${basepath}/upload`,
+ httpMethod: POST,
+ auth: true,
+ params: {
+ files: {
+ from: 'files',
+ },
+ },
+ },
+ },
+}
diff --git a/src/fireedge/src/server/routes/api/index.js b/src/fireedge/src/server/routes/api/index.js
index 471f431515..1edd3a6717 100644
--- a/src/fireedge/src/server/routes/api/index.js
+++ b/src/fireedge/src/server/routes/api/index.js
@@ -36,6 +36,7 @@ const routes = [
'2fa',
'auth',
'files',
+ 'image',
'marketapp',
'oneflow',
'vcenter',
diff --git a/src/fireedge/src/server/routes/api/vcenter/functions.js b/src/fireedge/src/server/routes/api/vcenter/functions.js
index 545a41c8e1..ef4999c697 100644
--- a/src/fireedge/src/server/routes/api/vcenter/functions.js
+++ b/src/fireedge/src/server/routes/api/vcenter/functions.js
@@ -526,7 +526,11 @@ const getToken = (
const serverAdmin = getSunstoneAuth() ?? {}
const { token: authToken } = createTokenServerAdmin(serverAdmin) ?? {}
- !authToken && responser()
+ if (!authToken) {
+ responser()
+
+ return
+ }
const { username } = serverAdmin
const oneClient = xmlrpc(`${username}:${username}`, authToken)
@@ -537,6 +541,8 @@ const getToken = (
callback: (vmInfoErr, { VM } = {}) => {
if (vmInfoErr || !VM) {
responser(vmInfoErr, unauthorized)
+
+ return
}
if (!VM?.MONITORING?.VCENTER_ESX_HOST) {
@@ -544,6 +550,8 @@ const getToken = (
Could not determine the vCenter ESX host where
the VM is running. Wait till the VCENTER_ESX_HOST attribute is
retrieved once the host has been monitored`)
+
+ return
}
const history = VM?.HISTORY_RECORDS?.HISTORY
@@ -558,80 +566,104 @@ const getToken = (
if (String(hostHypervisor).toLowerCase() !== 'vcenter') {
responser('VMRC Connection is only for vCenter hypervisor')
+
+ return
}
if (!VM?.DEPLOY_ID || isNaN(hostId)) {
responser('VM is not deployed')
+
+ return
+ }
+
+ const responseError = (error) =>
+ responser(error && error.message, internalServerError)
+
+ const responseToken = (ticketData) => {
+ const { ticket } = ticketData
+ const { protocol, hostname, port, path } = parse(ticket)
+
+ const httpProtocol = protocol === 'wss:' ? 'https' : 'http'
+ const esxUrl = `${httpProtocol}://${hostname}:${port}`
+ const token = path.replace('/ticket/', '')
+ global.vcenterToken = { [token]: esxUrl }
+
+ responser(token, ok)
+ }
+
+ /**
+ * Get the vcenter token of vm.
+ *
+ * @param {string} sessionId - session id
+ * @param {string} vcenterHost - host ip
+ */
+ const getVcenterToken = (sessionId, vcenterHost) => {
+ const vmIdFromDeployId = VM.DEPLOY_ID.match(regexGetVcenterId).groups.id
+
+ executeRequest(
+ {
+ params: {
+ url: `https://${vcenterHost}/api/vcenter/vm/vm-${vmIdFromDeployId}/console/tickets`,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'vmware-api-session-id': sessionId,
+ },
+ data: JSON.stringify({ type: 'WEBMKS' }),
+ },
+ agent: 'https',
+ },
+ {
+ success: responseToken,
+ error: responseError,
+ }
+ )
+ }
+
+ /**
+ * Get vmware-api-session-id.
+ *
+ * @param {string} hostInfoError - error when get info host.
+ * @param {object} hostData - host data
+ * @param {object} hostData.HOST - data host
+ */
+ const getSession = (hostInfoError, { HOST } = {}) => {
+ const { VCENTER_HOST, VCENTER_USER, VCENTER_PASSWORD } =
+ HOST?.TEMPLATE ?? {}
+
+ if (
+ hostInfoError ||
+ !VCENTER_HOST ||
+ !VCENTER_USER ||
+ !VCENTER_PASSWORD
+ ) {
+ responser(hostInfoError, unauthorized)
+
+ return
+ }
+
+ executeRequest(
+ {
+ params: {
+ url: `https://${VCENTER_HOST}/api/session`,
+ headers: {
+ Authorization: `Basic ${btoa(
+ `${VCENTER_USER}:${VCENTER_PASSWORD}`
+ )}`,
+ },
+ },
+ agent: 'https',
+ },
+ {
+ success: (sessionId) => getVcenterToken(sessionId, VCENTER_HOST),
+ error: responseError,
+ }
+ )
}
oneClient({
action: ActionHost.HOST_INFO,
parameters: [parseInt(hostId, 10), true],
- callback: (hostInfoError, { HOST } = {}) => {
- const { VCENTER_HOST, VCENTER_USER, VCENTER_PASSWORD } =
- HOST?.TEMPLATE ?? {}
-
- if (
- hostInfoError ||
- !VCENTER_HOST ||
- !VCENTER_USER ||
- !VCENTER_PASSWORD
- ) {
- responser(hostInfoError, unauthorized)
- }
-
- executeRequest(
- {
- params: {
- url: `https://${VCENTER_HOST}/api/session`,
- headers: {
- Authorization: `Basic ${btoa(
- `${VCENTER_USER}:${VCENTER_PASSWORD}`
- )}`,
- },
- },
- agent: 'https',
- },
- {
- success: (sessionId) => {
- const vmIdFromDeployId =
- VM.DEPLOY_ID.match(regexGetVcenterId).groups.id
-
- executeRequest(
- {
- params: {
- url: `https://${VCENTER_HOST}/api/vcenter/vm/vm-${vmIdFromDeployId}/console/tickets`,
- headers: {
- 'Content-Type': 'application/json',
- 'vmware-api-session-id': sessionId,
- },
- data: JSON.stringify({ type: 'WEBMKS' }),
- },
- agent: 'https',
- },
- {
- success: (ticketData) => {
- const { ticket } = ticketData
- const { protocol, hostname, port, path } = parse(ticket)
-
- const httpProtocol =
- protocol === 'wss:' ? 'https' : 'http'
- const esxUrl = `${httpProtocol}://${hostname}:${port}`
- const token = path.replace('/ticket/', '')
- global.vcenterToken = { [token]: esxUrl }
-
- responser(token, ok)
- },
- error: (error) =>
- responser(error && error.message, internalServerError),
- }
- )
- },
- error: (error) =>
- responser(error && error.message, internalServerError),
- }
- )
- },
+ callback: getSession,
})
},
})
diff --git a/src/template/OpenNebulaTemplate.cc b/src/template/OpenNebulaTemplate.cc
index be77ebc152..144187a58c 100644
--- a/src/template/OpenNebulaTemplate.cc
+++ b/src/template/OpenNebulaTemplate.cc
@@ -399,6 +399,8 @@ void OpenNebulaTemplate::set_conf_default()
set_conf_single("HOST_ENCRYPTED_ATTR", "NSX_PASSWORD");
set_conf_single("HOST_ENCRYPTED_ATTR", "ONE_PASSWORD");
set_conf_single("SHOWBACK_ONLY_RUNNING", "NO");
+ set_conf_single("CONTEXT_RESTRICTED_DIRS", "/etc");
+ set_conf_single("CONTEXT_SAFE_DIRS", "");
//DB CONFIGURATION
vvalue.insert(make_pair("BACKEND","sqlite"));
diff --git a/src/tm_mad/fs_lvm/mv b/src/tm_mad/fs_lvm/mv
index 82fb827a15..e2e4045882 100755
--- a/src/tm_mad/fs_lvm/mv
+++ b/src/tm_mad/fs_lvm/mv
@@ -122,11 +122,23 @@ EOF
)
LOCK="tm-fs_lvm-${DST_DS_SYS_ID}.lock"
- exclusive "${LOCK}" 120 ssh_exec_and_log "${SRC_HOST}" "${CREATE_CMD}" \
+ exclusive "${LOCK}" 120 ssh_exec_and_log "${DST_HOST}" "${CREATE_CMD}" \
"Error creating LV named ${LV_NAME}"
+ # activate src volume (on DST)
+ CMD=$(cat <"${DST_DIR}/.host" || :
+ hostname -f >"${DST_DIR}/.host" || :
EOF
)
- ssh_exec_and_log "$DST_HOST" "$CMD" \
- "Error activating disk $DST_PATH"
+ ssh_exec_and_log "$DST_HOST" "$CMD" \
+ "Error activating disk $DST_PATH"
+
+ fi
exit 0
fi
diff --git a/src/tm_mad/fs_lvm_ssh/mv b/src/tm_mad/fs_lvm_ssh/mv
index 70c8ced92c..9956b46202 100755
--- a/src/tm_mad/fs_lvm_ssh/mv
+++ b/src/tm_mad/fs_lvm_ssh/mv
@@ -122,11 +122,23 @@ EOF
)
LOCK="tm-fs_lvm-${DST_DS_SYS_ID}.lock"
- exclusive "${LOCK}" 120 ssh_exec_and_log "${SRC_HOST}" "${CREATE_CMD}" \
+ exclusive "${LOCK}" 120 ssh_exec_and_log "${DST_HOST}" "${CREATE_CMD}" \
"Error creating LV named ${LV_NAME}"
+ # activate src volume (on DST)
+ CMD=$(cat <"${DST_DIR}/.host" || :
+ hostname -f >"${DST_DIR}/.host" || :
EOF
)
- ssh_exec_and_log "$DST_HOST" "$CMD" \
- "Error activating disk $DST_PATH"
-
+ ssh_exec_and_log "$DST_HOST" "$CMD" \
+ "Error activating disk $DST_PATH"
+ fi
fi
# After managing LV de/activation on different hosts, transfer normal files
diff --git a/src/vm/VirtualMachineContext.cc b/src/vm/VirtualMachineContext.cc
index def2b9ac7b..74ffbcacdb 100644
--- a/src/vm/VirtualMachineContext.cc
+++ b/src/vm/VirtualMachineContext.cc
@@ -70,6 +70,39 @@ const std::vector NETWORK6_CONTEXT = {
{"EXTERNAL", "EXTERNAL", "", false},
};
+bool is_restricted(const string& path,
+ const set& restricted,
+ const set& safe)
+{
+ auto canonical_c = realpath(path.c_str(), nullptr);
+
+ if (canonical_c == nullptr)
+ {
+ return false;
+ }
+
+ string canonical_str(canonical_c);
+ free(canonical_c);
+
+ for (auto& s : safe)
+ {
+ if (canonical_str.find(s) == 0)
+ {
+ return false;
+ }
+ }
+
+ for (auto& r : restricted)
+ {
+ if (canonical_str.find(r) == 0)
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* CONTEXT - Public Interface */
@@ -129,6 +162,33 @@ int VirtualMachine::generate_context(string &files, int &disk_id,
}
files = context->vector_value("FILES");
+
+ auto& nd = Nebula::instance();
+ string restricted_dirs, safe_dirs;
+ nd.get_configuration_attribute("CONTEXT_RESTRICTED_DIRS", restricted_dirs);
+ nd.get_configuration_attribute("CONTEXT_SAFE_DIRS", safe_dirs);
+
+ set restricted, safe;
+
+ one_util::split_unique(restricted_dirs, ' ', restricted);
+ one_util::split_unique(safe_dirs, ' ', safe);
+
+ set files_set;
+ one_util::split_unique(files, ' ', files_set);
+ for (auto& f : files_set)
+ {
+ if (is_restricted(f, restricted, safe))
+ {
+ string error = "CONTEXT/FILES cannot use " + f
+ + ", it's in restricted directories";
+
+ log("VM", Log::ERROR, error);
+ set_template_error_message(error);
+
+ return -1;
+ }
+ }
+
files_ds = context->vector_value("FILES_DS");
if (!files_ds.empty())
diff --git a/src/vmm/VirtualMachineManager.cc b/src/vmm/VirtualMachineManager.cc
index 961c1209eb..e1b9774e01 100644
--- a/src/vmm/VirtualMachineManager.cc
+++ b/src/vmm/VirtualMachineManager.cc
@@ -333,6 +333,9 @@ static int do_context_command(VirtualMachine * vm, const string& password,
if ( rc == -1 )
{
+ auto vmpool = Nebula::instance().get_vmpool();
+ vmpool->update(vm);
+
return -1;
}
else if ( rc == 1 )
diff --git a/src/vmm_mad/remotes/kvm/migrate b/src/vmm_mad/remotes/kvm/migrate
index 09ac53ee3c..933157d47a 100755
--- a/src/vmm_mad/remotes/kvm/migrate
+++ b/src/vmm_mad/remotes/kvm/migrate
@@ -180,6 +180,11 @@ else
fi
done
+ # copy vm.xml and ds.xml from the $VM_DIR
+ if ls $VM_DIR/*.xml > /dev/null; then
+ multiline_exec_and_log "tar -cSf - $VM_DIR/*.xml | $SSH $DEST_HOST 'tar xSf - -C / '" \
+ "Failed to copy xml files to $DEST_HOST"
+ fi
# freeze/suspend domain and rsync raw disks again
if [ -n "$RAW_DISKS" ]; then
@@ -287,4 +292,3 @@ fi
if [ "x$CLEANUP_MEMORY_ON_STOP" = "xyes" ]; then
(sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null &
fi
-