skyline-console/src/resources/instance.jsx

616 lines
17 KiB
JavaScript

// Copyright 2021 99cloud
//
// 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 React, { useEffect, useState } from 'react';
import ImageType from 'components/ImageType';
import { getLocalTimeStr } from 'utils/time';
import { Table, Popover } from 'antd';
import globalActionLogStore from 'stores/nova/action-log';
import { ironicOriginEndpoint } from 'client/client/constants';
import lockSvg from 'asset/image/lock.svg';
import unlockSvg from 'asset/image/unlock.svg';
const lockIcon = <img src={lockSvg} alt="lock" style={{ width: '12px' }} />;
const unlockIcon = (
<img src={unlockSvg} alt="unlock" style={{ width: '12px' }} />
);
export const transitionStatus = {
build: t('Build'),
building: t('Building'),
stopped: t('Stopped'),
recovering: t('Recovering'),
rescued: t('Rescued'),
resized: t('Resized'),
scheduling: t('Scheduling'),
reboot: t('Reboot'),
hard_reboot: t('Hard Reboot'),
migrating: t('Migrating'),
};
export const stableStatus = {
deleted: t('Deleted'),
active: t('Active'),
shutoff: t('Shutoff'),
paused: t('Paused'),
error: t('Error'),
resize: t('Resizing or Migrating'),
verify_resize: t('Resizing or Migrating'),
revert_resize: t('Revert Resize/Migrate'),
// reboot: t('Reboot'),
// hard_reboot: t('Hard Reboot'),
password: t('Password'),
rebuild: t('Rebuild'),
rescue: t('Rescue'),
'soft-delete': t('Soft Deleted'),
soft_deleted: t('Soft Deleted'),
shelved: t('Shelved'),
shelved_offloaded: t('Shelved Offloaded'),
suspended: t('Suspended'),
stopped: t('Shutoff'),
};
export const taskStatus = {
null: t('No Task'),
scheduling: t('Scheduling'),
block_device_mapping: t('Block Device Mapping'),
networking: t('Networking'),
spawning: t('Spawning'),
image_snapshot: t('Snapshotting'),
image_snapshot_pending: t('Image Snapshot Pending'),
image_pending_upload: t('Image Pending Upload'),
image_uploading: t('Image Uploading'),
image_backup: t('Image Backup'),
updating_password: t('Updating Password'),
resize_prep: t('Resizing or Migrating'),
resize_migrating: t('Resizing or Migrating'),
resize_migrated: t('Resizing or Migrating'),
resize_finish: t('Resizing or Migrating'),
resize_reverting: t('Reverting Resize or Migrate'),
resize_confirming: t('Confirming Resize or Migrate'),
rebooting: t('Rebooting'),
reboot_pending: t('Rebooting'),
reboot_started: t('Rebooting'),
rebooting_hard: t('Hard Rebooting'),
reboot_pending_hard: t('Hard Rebooting'),
reboot_started_hard: t('Hard Rebooting'),
pausing: t('Pausing'),
unpausing: t('Resuming'),
suspending: t('Suspending'),
resuming: t('Resuming'),
'powering-off': t('Powering Off'),
'powering-on': t('Powering On'),
rescuing: t('Rescuing'),
unrescuing: t('Unrescuing'),
rebuilding: t('Rebuilding'),
rebuild_block_device_mapping: t('Rebuild Block Device Mapping'),
rebuild_spawning: t('Rebuild Spawning'),
migrating: t('Migrating'),
deleting: t('Deleting'),
'soft-deleting': t('Soft Deleting'),
restoring: t('Restoring'),
shelving: t('Shelving'),
shelving_image_pending_upload: t('Shelving Image Pending Upload'),
shelving_image_uploading: t('Shelving Image Uploading'),
shelving_offloading: t('Shelving Offloading'),
unshelving: t('Unshelving'),
};
export const powerStatus = {
'NO STATE': t('No State'),
RUNNING: t('Running'),
BLOCKED: t('Blocked'),
PAUSED: t('Paused'),
SHUTDOWN: t('Shut Down'),
SHUTOFF: t('Shut Off'),
CRASHED: t('Crashed'),
SUSPENDED: t('Suspended'),
FAILED: t('Failed'),
BUILDING: t('Building'),
};
export const instanceStatus = {
...transitionStatus,
...stableStatus,
...taskStatus,
...powerStatus,
};
export const isBuilding = (instance) => instance.status === 'build';
export const isNotLocked = (instance) => !instance.locked;
export const isNotDeleting = (instance) => {
if (instance.task_state) {
return instance.task_state.toLowerCase() !== 'deleting';
}
return true;
};
export const isLocked = (instance) => !!instance.locked;
export const lockRender = (value) => (value ? lockIcon : unlockIcon);
export const checkStatus = (statusList = [], instance) => {
const { status, vm_state } = instance;
return (
statusList.includes(status.toLowerCase()) ||
(vm_state && statusList.includes(vm_state.toLowerCase()))
);
};
export const isNotLockedOrAdmin = (instance, isAdmin = false) => {
if (isLocked(instance)) {
return isAdmin;
}
return true;
};
export const isActiveOrShutOff = (item) =>
checkStatus(['active', 'shutoff'], item);
export const isShutOff = (item) => checkStatus(['shutoff'], item);
export const isActive = (item) => checkStatus(['active'], item);
export const isStopped = (item) => checkStatus(['stopped'], item);
export const isPaused = (item) => checkStatus(['paused'], item);
export const isNotError = (item) => !checkStatus(['error'], item);
export const isIsoInstance = (item) => {
const { iso_server = false } = item;
return iso_server;
};
export const hasRootVolume = (item) => {
const {
root_device_name: rootDeviceName = '/dev/vda',
volumes_attached: volumes = [],
} = item;
const rootVolume = volumes.find(
(it) => it.is_root_volume || it.device === rootDeviceName
);
return !!rootVolume;
};
const passwordAndUserData =
'Content-Type: multipart/mixed; boundary="===============2309984059743762475=="\n' +
'MIME-Version: 1.0\n' +
'\n' +
'--===============2309984059743762475==\n' +
'Content-Type: text/cloud-config; charset="us-ascii" \n' +
'MIME-Version: 1.0\n' +
'Content-Transfer-Encoding: 7bit\n' +
'Content-Disposition: attachment; filename="ssh-pwauth-script.txt" \n' +
'\n' +
'#cloud-config\n' +
'disable_root: false\n' +
'ssh_pwauth: true\n' +
'password: USER_PASSWORD\n' +
'\n' +
'--===============2309984059743762475==\n' +
'Content-Type: text/x-shellscript; charset="us-ascii" \n' +
'MIME-Version: 1.0\n' +
'Content-Transfer-Encoding: 7bit\n' +
'Content-Disposition: attachment; filename="passwd-script.txt" \n' +
'\n' +
'#!/bin/sh\n' +
"echo 'root:USER_PASSWORD' | chpasswd\n" +
'\n' +
'--===============2309984059743762475==\n' +
'Content-Type: text/x-shellscript; charset="us-ascii" \n' +
'MIME-Version: 1.0\n' +
'Content-Transfer-Encoding: 7bit\n' +
'Content-Disposition: attachment; filename="init-shell.txt" \n' +
'\n' +
'USER_DATA\n' +
'\n' +
'--===============2309984059743762475==--';
const onlyPassword =
'Content-Type: multipart/mixed; boundary="===============2309984059743762475==" \n' +
'MIME-Version: 1.0\n' +
'\n' +
'--===============2309984059743762475==\n' +
'Content-Type: text/cloud-config; charset="us-ascii" \n' +
'MIME-Version: 1.0\n' +
'Content-Transfer-Encoding: 7bit\n' +
'Content-Disposition: attachment; filename="ssh-pwauth-script.txt" \n' +
'\n' +
'#cloud-config\n' +
'disable_root: false\n' +
'ssh_pwauth: true\n' +
'password: USER_PASSWORD\n' +
'\n' +
'--===============2309984059743762475==\n' +
'Content-Type: text/x-shellscript; charset="us-ascii" \n' +
'MIME-Version: 1.0\n' +
'Content-Transfer-Encoding: 7bit\n' +
'Content-Disposition: attachment; filename="passwd-script.txt" \n' +
'\n' +
'#!/bin/sh\n' +
"echo 'root:USER_PASSWORD' | chpasswd\n" +
'\n' +
'--===============2309984059743762475==--';
const onlyUserData =
'Content-Type: multipart/mixed; boundary="===============2309984059743762475==" \n' +
'MIME-Version: 1.0\n' +
'\n' +
'--===============2309984059743762475==\n' +
'Content-Type: text/x-shellscript; charset="us-ascii" \n' +
'MIME-Version: 1.0\n' +
'Content-Transfer-Encoding: 7bit\n' +
'Content-Disposition: attachment; filename="init-shell.txt" \n' +
'\n' +
'USER_DATA\n' +
'\n' +
'--===============2309984059743762475==--';
export const getUserData = (password, userData) => {
if (password && userData) {
const str = passwordAndUserData.replace(/USER_PASSWORD/g, password);
return str.replace(/USER_DATA/g, userData);
}
if (password) {
return onlyPassword.replace(/USER_PASSWORD/g, password);
}
return onlyUserData.replace(/USER_DATA/g, userData);
};
export const getIpInitValue = (subnet) => {
if (!subnet) {
return null;
}
const { start } = subnet.allocation_pools[0];
return start;
};
export const physicalNodeTypes = [
{
label: t('Smart Scheduling'),
value: 'smart',
},
{
label: t('Manually Specify'),
value: 'manually',
},
];
export const isIronicInstance = (item) => {
const { flavor_info: { extra_specs: extra = {} } = {} } = item;
return (
extra[':architecture'] === 'bare_metal' ||
extra['trait:CUSTOM_GOLD'] === 'required'
);
};
export const instanceColumnsBackend = [
{
title: t('Name'),
dataIndex: 'name',
sortKey: 'display_name',
},
{
title: t('Image'),
sorter: false,
dataIndex: 'image_os_distro',
render: (value, record) => (
<ImageType type={value} title={record.image_name} />
),
},
{
title: t('Fixed IP'),
dataIndex: 'fixed_addresses',
width: 120,
sorter: false,
render: (fixed_addresses) => {
if (!fixed_addresses || !fixed_addresses.length) {
return '-';
}
return fixed_addresses.map((it) => (
<span key={it}>
{it}
<br />
</span>
));
},
},
{
title: t('Floating IP'),
dataIndex: 'floating_addresses',
width: 120,
sorter: false,
render: (addresses) => {
if (!addresses || !addresses.length) {
return '-';
}
return addresses.map((it) => (
<span key={it}>
{it}
<br />
</span>
));
},
},
{
title: t('Flavor'),
dataIndex: 'flavor',
sorter: false,
},
{
title: t('Status'),
dataIndex: 'status',
sorter: false,
render: (value) => instanceStatus[value && value.toLowerCase()] || '-',
},
{
title: t('Locked'),
dataIndex: 'locked',
isHideable: true,
render: lockRender,
},
{
title: t('Created At'),
dataIndex: 'created_at',
valueRender: 'sinceTime',
},
];
export const instanceFilters = [
{
label: t('Name'),
name: 'name',
},
];
export const instanceSortProps = {
isSortByBack: true,
defaultSortKey: 'created_at',
defaultSortOrder: 'descend',
};
export const instanceSelectTablePropsBackend = {
...instanceSortProps,
filterParams: instanceFilters,
columns: instanceColumnsBackend,
};
export const canCreateIronicByEndpoint = () => ironicOriginEndpoint();
export const allowAttachVolumeInstance = (item) => {
const statusResult = checkStatus(
[
'active',
'paused',
'stopped',
'resized',
'soft-delete',
'shelved',
'shelved_offloaded',
],
item
);
return (
statusResult &&
isNotDeleting(item) &&
isNotLocked(item) &&
!isIronicInstance(item)
);
};
export const instanceStatusFilter = {
label: t('Status'),
name: 'status',
options: [
{ label: t('Active'), key: 'ACTIVE' },
{ label: t('Building'), key: 'BUILD' },
{ label: t('Paused'), key: 'PAUSED' },
{ label: t('Suspended'), key: 'SUSPENDED' },
{ label: t('Error'), key: 'ERROR' },
{ label: t('Shutoff'), key: 'SHUTOFF' },
{ label: t('Shelved Offloaded'), key: 'SHELVED_OFFLOADED' },
],
};
export const hasOnlineResizeFlavor = (item) => {
const {
extra_specs: { 'hw:live_resize': liveResize = false },
} = (item || {}).flavor_info || {};
return !!liveResize;
};
export const actionMap = {
attach_interface: t('Attach Interface'),
detach_interface: t('Detach Interface'),
attach_volume: t('Attach Volume'),
detach_volume: t('Detach Volume'),
create: t('Create'),
stop: t('Stop'),
reboot: t('Reboot'),
suspend: t('Suspend'),
resume: t('Resume'),
shelve: t('Shelve'),
unshelve: t('Unshelve'),
start: t('Start'),
lock: t('Lock'),
unlock: t('Unlock'),
pause: t('Pause'),
unpause: t('Unpause'),
createImage: t('Create Snapshot'),
resize: t('Extend Root Volume'),
confirmResize: t('Resize'),
'live-resize': t('Online Resize'),
extend_volume: t('Extend Volume'),
changePassword: t('Change Password'),
rebuild: t('Rebuild'),
migrate: t('Migrate'),
'live-migration': t('Live Migrate'),
delete: t('Delete'),
restore: t('Recover'),
};
export const actionEvent = {
compute_restore_instance: t('Resume Instance'),
compute_soft_delete_instance: t('Soft Delete Instance'),
compute_post_live_migration_at_destination: t(
'Live Migration At Destination'
),
compute_pre_live_migration: t('Pre Live Migration'),
compute_live_migration: t('Compute Live Migration'),
compute_check_can_live_migrate_source: t('Check Can Live Migrate Source'),
compute_check_can_live_migrate_destination: t(
'Check Can Live Migrate Destination'
),
conductor_live_migrate_instance: t('Conductor Live Migrate Instance'),
compute_confirm_resize: t('Resized'),
compute_finish_resize: t('Finish Resize'),
compute_resize_instance: t('Resize Instance'),
compute_prep_resize: t('Prep Resize'),
cold_migrate: t('Cold Migrate'),
conductor_migrate_server: t('Conductor Migrate Server'),
compute_rebuild_instance: t('Rebuild Instance'),
rebuild_server: t('Rebuild Instance'),
compute_set_admin_password: t('Set Admin Password'),
compute_extend_volume: t('Extend Volume'),
compute_live_resize_instance: t('Compute Live Resize Instance'),
conductor_live_resize_instance: t('Conductor Live Resize Instance'),
api_snapshot_instance: t('Snapshot Instance'),
api_lock: t('Lock'),
api_unlock: t('Unlock'),
compute_detach_volume: t('Detach Volume'),
compute_attach_volume: t('Attach Volume'),
compute_detach_interface: t('Detach Interface'),
compute_attach_interface: t('Attach Interface'),
compute__do_build_and_run_instance: t('Do Build And Run Instance'),
compute_suspend_instance: t('Compute Suspend Instance'),
compute_start_instance: t('Compute Start Instance'),
compute_stop_instance: t('Compute Stop Instance'),
compute_resume_instance: t('Compute Resume Instance'),
compute_pause_instance: t('Compute Pause Instance'),
compute_unpause_instance: t('Compute Unpause Instance'),
compute_reboot_instance: t('Compute Reboot Instance'),
};
function PopUpContent({ id, requestId }) {
const [event, setEvent] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
let timeout = null;
(async function () {
setLoading(true);
const cb = await globalActionLogStore.fetchDetail({ id, requestId });
const { events = [] } = cb;
timeout = setTimeout(() => {
setLoading(false);
setEvent(events.slice().reverse());
}, 200);
})();
return () => {
clearTimeout(timeout);
};
}, []);
const columns = [
{
title: t('Operation Name'),
dataIndex: 'event',
key: 'event',
render: (value) => actionEvent[value],
},
{
title: t('Start Time'),
dataIndex: 'start_time',
key: 'start_time',
render: (value) => getLocalTimeStr(value),
},
{
title: t('End Time'),
dataIndex: 'finish_time',
key: 'finish_time',
render: (value) => (value ? getLocalTimeStr(value) : '-'),
},
{
title: t('Execution Result'),
dataIndex: 'result',
key: 'result',
render: (value) => (value === 'Success' ? t('Success') : '-'),
},
];
const table = (
<Table
columns={columns}
dataSource={event}
pagination={false}
loading={loading}
size="small"
rowKey="event"
/>
);
return table;
}
export const actionColumn = (self) => {
return [
{
title: t('Operation Name'),
dataIndex: 'action',
render: (value) => actionMap[value],
},
{
title: t('Project ID/Name'),
dataIndex: 'project_name',
isHideable: true,
hidden: !self.isAdminPage,
},
{
title: t('Operation Time'),
dataIndex: 'start_time',
valueRender: 'toLocalTimeMoment',
},
{
title: t('Request ID'),
dataIndex: 'request_id',
isHideable: true,
render: (value, record) => {
const content = (
<PopUpContent id={record.instance_uuid} requestId={value} />
);
return (
<>
{value && (
<Popover content={content} destroyTooltipOnHide trigger="click">
<span className="link-class">{value}</span>
</Popover>
)}
</>
);
},
},
{
title: t('User ID'),
dataIndex: 'user_id',
isHideable: true,
hidden: !self.isAdminPage,
render: (value) =>
self.getLinkRender('userDetail', value, { id: value }, null),
},
];
};
export const allowAttachInterfaceStatus = ['active', 'paused', 'stopped'];