mirror of
https://github.com/azukaar/Cosmos-Server.git
synced 2026-01-05 20:05:02 -06:00
[release] v0.15.8
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
## Version 0.15.0
|
||||
- Added Disk management (Format, mount, SMART, etc...)
|
||||
- Added Storage management (MergeFS, SnapRAID, RClone, etc...)
|
||||
- Overwrite all docker networks size to prevent Cosmos from running out of IP addresses
|
||||
- Added optional subnet input to the network creation
|
||||
|
||||
## Version 0.14.6
|
||||
- Fix custom back-up folder logic
|
||||
|
||||
|
||||
@@ -43,13 +43,13 @@ const disks = {
|
||||
}))
|
||||
},
|
||||
|
||||
format({disk, format}, onProgress) {
|
||||
format({disk, format, password}, onProgress) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({disk, format})
|
||||
body: JSON.stringify({disk, format, password})
|
||||
};
|
||||
|
||||
return fetch('/cosmos/api/disks/format', requestOptions)
|
||||
|
||||
@@ -53,24 +53,32 @@ const LogsInModal = ({title, request, OnSuccess, OnError, closeAnytime, OnClose,
|
||||
|
||||
if(request === null) return;
|
||||
|
||||
request((newlog) => {
|
||||
setLogs((old) => smartDockerLogConcat(old, newlog));
|
||||
try {
|
||||
request((newlog) => {
|
||||
setLogs((old) => smartDockerLogConcat(old, newlog));
|
||||
|
||||
if(preRef.current)
|
||||
preRef.current.scrollTop = preRef.current.scrollHeight;
|
||||
|
||||
if (newlog.includes('[OPERATION SUCCEEDED]')) {
|
||||
setDone(true);
|
||||
if(!alwaysShow)
|
||||
close();
|
||||
OnSuccess && OnSuccess();
|
||||
} else if (newlog.includes('[OPERATION FAILED]')) {
|
||||
setDone(true);
|
||||
OnError && OnError(newlog);
|
||||
} else {
|
||||
setOpenModal(true);
|
||||
}
|
||||
});
|
||||
if(preRef.current)
|
||||
preRef.current.scrollTop = preRef.current.scrollHeight;
|
||||
|
||||
if (newlog.includes('[OPERATION SUCCEEDED]')) {
|
||||
setDone(true);
|
||||
if(!alwaysShow)
|
||||
close();
|
||||
OnSuccess && OnSuccess();
|
||||
} else if (newlog.includes('[OPERATION FAILED]')) {
|
||||
setDone(true);
|
||||
OnError && OnError(newlog);
|
||||
} else {
|
||||
setOpenModal(true);
|
||||
}
|
||||
}).catch((err) => {
|
||||
setLogs((old) => smartDockerLogConcat(old, err.message));
|
||||
OnError && OnError(err);
|
||||
});
|
||||
} catch(err) {
|
||||
setLogs((old) => smartDockerLogConcat(old, err.message));
|
||||
OnError && OnError(err);
|
||||
};
|
||||
}, [request]);
|
||||
|
||||
return <>
|
||||
|
||||
82
client/src/components/passwordModal.jsx
Normal file
82
client/src/components/passwordModal.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
// material-ui
|
||||
import * as React from 'react';
|
||||
import { Alert, Button, Checkbox, FormControl, FormHelperText, Grid, InputLabel, MenuItem, Select, Stack, TextField } from '@mui/material';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { useFormik, FormikProvider } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const PasswordModal = ({ textInfos, cb, OnClose }) => {
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
password: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
password: Yup.string().required('Required'),
|
||||
}),
|
||||
onSubmit: async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
cb(values.password).then((res) => {
|
||||
setStatus({ success: true });
|
||||
setSubmitting(false);
|
||||
OnClose();
|
||||
}).catch((err) => {
|
||||
setStatus({ success: false });
|
||||
setErrors({ submit: err.message });
|
||||
setSubmitting(false);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={true} onClose={() => OnClose()}>
|
||||
<FormikProvider value={formik}>
|
||||
<DialogTitle>Confirm Password</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={2} width={'300px'} style={{ marginTop: '10px' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="password"
|
||||
name="password"
|
||||
label="Your Password"
|
||||
value={formik.values.password}
|
||||
type="password"
|
||||
onChange={formik.handleChange}
|
||||
error={formik.touched.password && Boolean(formik.errors.password)}
|
||||
helperText={formik.touched.password && formik.errors.password}
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
{formik.errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => OnClose()}>Cancel</Button>
|
||||
<LoadingButton
|
||||
disabled={formik.errors.submit}
|
||||
onClick={formik.handleSubmit}
|
||||
loading={formik.isSubmitting}
|
||||
>
|
||||
Confirm
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
</FormikProvider>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordModal;
|
||||
@@ -204,7 +204,7 @@ const NewInstall = () => {
|
||||
setDatabaseEnable(false);
|
||||
}
|
||||
pullRequestOnSuccess();
|
||||
API.getStatus().then((res) => {
|
||||
return API.getStatus().then((res) => {
|
||||
formik.setSubmitting(false);
|
||||
formik.setStatus({ success: true });
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ const GetActions = ({
|
||||
|
||||
if(action === 'update') {
|
||||
setPullRequest(() => ((cb) => {
|
||||
API.docker.updateContainerImage(Id, cb, true)
|
||||
return API.docker.updateContainerImage(Id, cb, true)
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ const NewNetworkButton = ({ fullWidth, refresh }) => {
|
||||
driver: 'bridge',
|
||||
attachCosmos: false,
|
||||
parentInterface: '',
|
||||
subnet: '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required('Required'),
|
||||
@@ -93,6 +94,18 @@ const NewNetworkButton = ({ fullWidth, refresh }) => {
|
||||
<MenuItem value="mcvlan">MCVLAN</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="subnet"
|
||||
name="subnet"
|
||||
label="Subnet (optional)"
|
||||
value={formik.values.subnet}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.touched.subnet && Boolean(formik.errors.subnet)}
|
||||
helperText={formik.touched.subnet && formik.errors.subnet}
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
{formik.values.driver === 'mcvlan' && (
|
||||
<TextField
|
||||
|
||||
103
client/src/pages/storage/FormatModal.jsx
Normal file
103
client/src/pages/storage/FormatModal.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
// material-ui
|
||||
import * as React from 'react';
|
||||
import { Alert, Button, Checkbox, FormControl, FormHelperText, Grid, InputLabel, MenuItem, Select, Stack, TextField } from '@mui/material';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { useFormik, FormikProvider } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const FormatModal = ({ cb, OnClose }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
password: '',
|
||||
format: 'ext4'
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
password: Yup.string().required('Required'),
|
||||
}),
|
||||
onSubmit: async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
cb(values).then((res) => {
|
||||
setStatus({ success: true });
|
||||
setSubmitting(false);
|
||||
OnClose();
|
||||
}).catch((err) => {
|
||||
setStatus({ success: false });
|
||||
setErrors({ submit: err.message });
|
||||
setSubmitting(false);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={true} onClose={() => OnClose()}>
|
||||
<FormikProvider value={formik}>
|
||||
<DialogTitle>Format Disk</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={2} width={'300px'} style={{ marginTop: '10px' }}>
|
||||
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
error={formik.touched.format && Boolean(formik.errors.format)}
|
||||
style={{ marginBottom: '16px' }}
|
||||
>
|
||||
<InputLabel htmlFor="format">Disk Format</InputLabel>
|
||||
<Select
|
||||
id="format"
|
||||
name="format"
|
||||
value={formik.values.format}
|
||||
onChange={formik.handleChange}
|
||||
label="Disk Format"
|
||||
>
|
||||
<MenuItem value="ext4">Ext4 (Recommended)</MenuItem>
|
||||
<MenuItem value="ext3">Ext3</MenuItem>
|
||||
<MenuItem value="exfat">ExFAT</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="password"
|
||||
name="password"
|
||||
label="Confirm Your Password"
|
||||
value={formik.values.password}
|
||||
type="password"
|
||||
onChange={formik.handleChange}
|
||||
error={formik.touched.password && Boolean(formik.errors.password)}
|
||||
helperText={formik.touched.password && formik.errors.password}
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
{formik.errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => OnClose()}>Cancel</Button>
|
||||
<LoadingButton
|
||||
disabled={formik.errors.submit}
|
||||
onClick={formik.handleSubmit}
|
||||
loading={formik.isSubmitting}
|
||||
>
|
||||
Confirm
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
</FormikProvider>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormatModal;
|
||||
@@ -22,6 +22,8 @@ import raidIcon from '../../assets/images/icons/database.svg';
|
||||
import { simplifyNumber } from "../dashboard/components/utils";
|
||||
import LogsInModal from "../../components/logsInModal";
|
||||
import MountDialog from "./mountDialog";
|
||||
import PasswordModal from "../../components/passwordModal";
|
||||
import FormatModal from "./FormatModal";
|
||||
|
||||
const diskStyle = {
|
||||
width: "100%",
|
||||
@@ -44,21 +46,37 @@ const icons = {
|
||||
const FormatButton = ({disk, refresh}) => {
|
||||
const [formatting, setFormatting] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [passwordConfirm, setPasswordConfirm] = useState(false);
|
||||
const [values, setValues] = useState("");
|
||||
|
||||
return <>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
onClick={() => {setFormatting(true); setLoading(true)}}
|
||||
variant="contained"
|
||||
onClick={() => {setPasswordConfirm(true); setLoading(true)}}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
>Format</LoadingButton>
|
||||
|
||||
{passwordConfirm && <FormatModal
|
||||
OnClose={() => {
|
||||
setPasswordConfirm(false);
|
||||
setLoading(false);
|
||||
}}
|
||||
textInfos={`Enter your password to confirm you want to format ${disk.name}`}
|
||||
cb={async (values) => {
|
||||
setPasswordConfirm(false);
|
||||
setFormatting(values);
|
||||
}}
|
||||
/>}
|
||||
|
||||
{formatting && <LogsInModal
|
||||
request={(cb) => {
|
||||
API.storage.disks.format({
|
||||
disk: disk.name,
|
||||
format: "ext4",
|
||||
}, cb)
|
||||
return API.storage.disks.format({
|
||||
disk: disk.name,
|
||||
format: formatting.format,
|
||||
password: formatting.password,
|
||||
}, cb)
|
||||
}}
|
||||
initialLogs={[
|
||||
"Starting format disk " + disk.name + "..."
|
||||
@@ -85,92 +103,94 @@ const Disk = ({disk, refresh}) => {
|
||||
percent = Math.round((disk.usage / disk.size) * 100);
|
||||
}
|
||||
|
||||
return <TreeItem sx={{
|
||||
'&.Mui-selected': {
|
||||
color: 'red !important',
|
||||
backgroundColor: 'red'
|
||||
}
|
||||
}} style={diskStyle} nodeId={disk.name} label={
|
||||
<div style={{
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
border: "1px solid #eee",
|
||||
borderRadius: "5px",
|
||||
borderColor: darkMode ? "#555" : "#eee",
|
||||
backgroundColor: darkMode ? "#333" : "#fff",
|
||||
color: darkMode ? "#fff" : "#000",
|
||||
}}>
|
||||
<Stack direction="row" justifyContent="space-between" style={{
|
||||
borderLeft: ("4px solid " + (disk.smart.Temperature ? (disk.smart.Temperature > 55 ? (disk.smart.Temperature > 70 ? "red" : "orange") : "green") : "gray")),
|
||||
}}>
|
||||
<Stack direction="row" spacing={2} alignItems="center" sx={{paddingLeft: '5px'}}>
|
||||
<div>
|
||||
{disk.smart.Temperature ? <div style={{
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
left: '25px',
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
color: 'white',
|
||||
borderRadius: '5px',
|
||||
padding: '0px 5px',
|
||||
}}>
|
||||
<span style={{fontSize: '80%', opacity: 0.8}}>
|
||||
{disk.smart.Temperature > 55 && <WarningFilled />} {disk.smart.Temperature}°C
|
||||
</span></div> : ""}
|
||||
|
||||
<Tooltip title={disk.type}>
|
||||
{icons[disk.type] ? <img width="64px" height={"64px"} src={icons[disk.type]} /> : <img width="64px" height={"64px"} src={icons["drive"]} />}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<Stack spacing={1}>
|
||||
<div style={{fontWeight: 'bold'}}>
|
||||
{disk.name.replace("/dev/", "")}
|
||||
<span style={{opacity: 0.8, fontSize: '80%'}}>
|
||||
{disk.rota ? " (HDD)" : " (SSD)"}
|
||||
{disk.fstype ? ` - ${disk.fstype}` : ""}
|
||||
{disk.model ? ` - ${disk.model}` : ""} {disk.mountpoint ? ` (${disk.mountpoint})` : ""}
|
||||
</span>
|
||||
</div>
|
||||
{disk.usage ? <>
|
||||
<div><LinearProgress
|
||||
variant="determinate"
|
||||
color={percent > 95 ? 'error' : (percent > 75 ? 'warning' : 'info')}
|
||||
value={percent} /></div>
|
||||
<div>{simplifyNumber(disk.usage, 'b')} / {simplifyNumber(disk.size, 'b')} ({percent}%)</div>
|
||||
</>: simplifyNumber(disk.size, 'b')}
|
||||
return <>
|
||||
<TreeItem sx={{
|
||||
'&.Mui-selected': {
|
||||
color: 'red !important',
|
||||
backgroundColor: 'red'
|
||||
}
|
||||
}} style={diskStyle} nodeId={disk.name} label={
|
||||
<div style={{
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
border: "1px solid #eee",
|
||||
borderRadius: "5px",
|
||||
borderColor: darkMode ? "#555" : "#eee",
|
||||
backgroundColor: darkMode ? "#333" : "#fff",
|
||||
color: darkMode ? "#fff" : "#000",
|
||||
}}>
|
||||
<Stack direction="row" justifyContent="space-between" style={{
|
||||
borderLeft: ("4px solid " + (disk.smart.Temperature ? (disk.smart.Temperature > 55 ? (disk.smart.Temperature > 70 ? "red" : "orange") : "green") : "gray")),
|
||||
}}>
|
||||
<Stack direction="row" spacing={2} alignItems="center" sx={{paddingLeft: '5px'}}>
|
||||
<div>
|
||||
{disk.smart.Temperature ? <div style={{
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
left: '25px',
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
color: 'white',
|
||||
borderRadius: '5px',
|
||||
padding: '0px 5px',
|
||||
}}>
|
||||
<span style={{fontSize: '80%', opacity: 0.8}}>
|
||||
{disk.smart.Temperature > 55 && <WarningFilled />} {disk.smart.Temperature}°C
|
||||
</span></div> : ""}
|
||||
|
||||
<Tooltip title={disk.type}>
|
||||
{icons[disk.type] ? <img width="64px" height={"64px"} src={icons[disk.type]} /> : <img width="64px" height={"64px"} src={icons["drive"]} />}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<Stack spacing={1}>
|
||||
<div style={{fontWeight: 'bold'}}>
|
||||
{disk.name.replace("/dev/", "")}
|
||||
<span style={{opacity: 0.8, fontSize: '80%'}}>
|
||||
{disk.rota ? " (HDD)" : " (SSD)"}
|
||||
{disk.fstype ? ` - ${disk.fstype}` : ""}
|
||||
{disk.model ? ` - ${disk.model}` : ""} {disk.mountpoint ? ` (${disk.mountpoint})` : ""}
|
||||
</span>
|
||||
</div>
|
||||
{disk.usage ? <>
|
||||
<div><LinearProgress
|
||||
variant="determinate"
|
||||
color={percent > 95 ? 'error' : (percent > 75 ? 'warning' : 'info')}
|
||||
value={percent} /></div>
|
||||
<div>{simplifyNumber(disk.usage, 'b')} / {simplifyNumber(disk.size, 'b')} ({percent}%)</div>
|
||||
</>: simplifyNumber(disk.size, 'b')}
|
||||
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack direction={"row"} spacing={2} style={{textAlign: 'right'}}>
|
||||
<Stack spacing={0} style={{textAlign: 'right', opacity: 0.8, fontSize: '80%'}}>
|
||||
{Object.keys(disk).filter(key => key !== "name" && key !== "children" && key !== "smart"&& key !== "fstype"&& key !== "mountpoint"&& key !== "model" && key !== "usage" &&key !== "rota" && key !== "type" && key !== "size").map((key, index) => {
|
||||
return <div key={index}>{key}: {
|
||||
(typeof disk[key] == "object" ? JSON.stringify(disk[key]) : disk[key])
|
||||
}</div>
|
||||
})}
|
||||
</Stack>
|
||||
<Stack spacing={2} direction="column" justifyContent={"center"}>
|
||||
{(disk.type == "disk" || disk.type == "part") ? <FormatButton disk={disk} refresh={refresh}/> : ""}
|
||||
|
||||
{disk.mountpoint ? <MountDialog disk={disk} unmount={true} refresh={refresh} /> : ""}
|
||||
|
||||
{(
|
||||
(disk.type == "part" || (disk.type == "disk" && (!disk.children || !disk.children.length))) &&
|
||||
disk.fstype &&
|
||||
disk.fstype !== "swap" &&
|
||||
!disk.mountpoint
|
||||
) ? <MountDialog disk={disk} refresh={refresh} /> : ""}
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack direction={"row"} spacing={2} style={{textAlign: 'right'}}>
|
||||
<Stack spacing={0} style={{textAlign: 'right', opacity: 0.8, fontSize: '80%'}}>
|
||||
{Object.keys(disk).filter(key => key !== "name" && key !== "children" && key !== "smart"&& key !== "fstype"&& key !== "mountpoint"&& key !== "model" && key !== "usage" &&key !== "rota" && key !== "type" && key !== "size").map((key, index) => {
|
||||
return <div key={index}>{key}: {
|
||||
(typeof disk[key] == "object" ? JSON.stringify(disk[key]) : disk[key])
|
||||
}</div>
|
||||
})}
|
||||
</Stack>
|
||||
<Stack spacing={2} direction="column" justifyContent={"center"}>
|
||||
{(disk.type == "disk" || disk.type == "part") ? <FormatButton disk={disk} refresh={refresh}/> : ""}
|
||||
|
||||
{disk.mountpoint ? <MountDialog disk={disk} unmount={true} refresh={refresh} /> : ""}
|
||||
|
||||
{(
|
||||
(disk.type == "part" || (disk.type == "disk" && (!disk.children || !disk.children.length))) &&
|
||||
disk.fstype &&
|
||||
disk.fstype !== "swap" &&
|
||||
!disk.mountpoint
|
||||
) ? <MountDialog disk={disk} refresh={refresh} /> : ""}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
}>
|
||||
{disk.children && disk.children.map((child, index) => {
|
||||
return <Disk disk={child} refresh={refresh} />
|
||||
})}
|
||||
</TreeItem>
|
||||
</div>
|
||||
}>
|
||||
{disk.children && disk.children.map((child, index) => {
|
||||
return <Disk disk={child} refresh={refresh} />
|
||||
})}
|
||||
</TreeItem>
|
||||
</>;
|
||||
}
|
||||
|
||||
export const StorageDisks = () => {
|
||||
@@ -206,7 +226,6 @@ export const StorageDisks = () => {
|
||||
>
|
||||
{disks && disks.map((disk, index) => {
|
||||
return <Disk disk={disk} refresh={async () => {
|
||||
// wait 1s
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
await refresh()
|
||||
}} />
|
||||
|
||||
@@ -37,7 +37,7 @@ export const StorageMounts = () => {
|
||||
<div>
|
||||
{mounts && mounts.map((mount, index) => {
|
||||
return <div>
|
||||
<FolderOutlined/> {mount.Device} - {mount.Path} ({mount.Type})
|
||||
<FolderOutlined/> {mount.device} - {mount.path} ({mount.type})
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
|
||||
1
go.mod
1
go.mod
@@ -249,4 +249,5 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.4.0 // indirect
|
||||
k8s.io/klog/v2 v2.80.1 // indirect
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.15.0-unstable7",
|
||||
"version": "0.15.0-unstable8",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
||||
@@ -320,16 +320,18 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = DockerClient.NetworkCreate(DockerContext, networkToCreateName, doctype.NetworkCreate{
|
||||
Driver: networkToCreate.Driver,
|
||||
Attachable: networkToCreate.Attachable,
|
||||
Internal: networkToCreate.Internal,
|
||||
EnableIPv6: networkToCreate.EnableIPv6,
|
||||
IPAM: &network.IPAM{
|
||||
Driver: networkToCreate.IPAM.Driver,
|
||||
Config: ipamConfig,
|
||||
},
|
||||
})
|
||||
networkPayload := doctype.NetworkCreate{
|
||||
Driver: networkToCreate.Driver,
|
||||
Attachable: networkToCreate.Attachable,
|
||||
Internal: networkToCreate.Internal,
|
||||
EnableIPv6: networkToCreate.EnableIPv6,
|
||||
IPAM: &network.IPAM{
|
||||
Driver: networkToCreate.IPAM.Driver,
|
||||
Config: ipamConfig,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = CreateReasonableNetwork(networkToCreateName, networkPayload)
|
||||
|
||||
if err != nil {
|
||||
utils.Error("CreateService: Rolling back changes because of -- Network", err)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/docker/docker/api/types"
|
||||
network "github.com/docker/docker/api/types/network"
|
||||
)
|
||||
|
||||
func ListNetworksRoute(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -251,7 +252,8 @@ type createNetworkPayload struct {
|
||||
Name string `json:"name"`
|
||||
Driver string `json:"driver"`
|
||||
AttachCosmos bool `json:"attachCosmos"`
|
||||
parentInterface string `json:"parentInterface"`
|
||||
ParentInterface string `json:"parentInterface"`
|
||||
Subnet string `json:"subnet"`
|
||||
}
|
||||
|
||||
func CreateNetworkRoute(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -279,11 +281,22 @@ func CreateNetworkRoute(w http.ResponseWriter, req *http.Request) {
|
||||
CheckDuplicate: true,
|
||||
Driver: payload.Driver,
|
||||
Options: map[string]string{
|
||||
"parent": payload.parentInterface,
|
||||
"parent": payload.ParentInterface,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := DockerClient.NetworkCreate(context.Background(), payload.Name, networkCreate)
|
||||
if payload.Subnet != "" {
|
||||
networkCreate.IPAM = &network.IPAM{
|
||||
Driver: "default",
|
||||
Config: []network.IPAMConfig{
|
||||
{
|
||||
Subnet: payload.Subnet,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := CreateReasonableNetwork(payload.Name, networkCreate)
|
||||
if err != nil {
|
||||
utils.Error("CreateNetworkRoute: Error while creating network", err)
|
||||
utils.HTTPError(w, "Network Create Error: " + err.Error(), http.StatusInternalServerError, "CN004")
|
||||
|
||||
@@ -55,19 +55,63 @@ func doesSubnetOverlap(networks []types.NetworkResource, subnet string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func findAvailableSubnet() string {
|
||||
func findAvailableSubnets(nb int) ([]string, error) {
|
||||
result := []string{}
|
||||
baseSubnet := "100.0.0.0/29"
|
||||
|
||||
networks, err := DockerClient.NetworkList(DockerContext, types.NetworkListOptions{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
utils.Error("Docker Network List", err)
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
for doesSubnetOverlap(networks, baseSubnet) {
|
||||
baseSubnet = getNextSubnet(baseSubnet)
|
||||
for i := 0; i < nb; i++ {
|
||||
for doesSubnetOverlap(networks, baseSubnet) {
|
||||
baseSubnet = getNextSubnet(baseSubnet)
|
||||
}
|
||||
result = append(result, baseSubnet)
|
||||
}
|
||||
|
||||
return baseSubnet
|
||||
if len(result) != nb {
|
||||
return []string{}, errors.New("Not enough subnets available. Try cleaning up networks.")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func CreateReasonableNetwork(name string, networkDef types.NetworkCreate) (types.NetworkCreateResponse, error) {
|
||||
// if no subnet
|
||||
if networkDef.IPAM == nil {
|
||||
networkDef.IPAM = &network.IPAM{
|
||||
Driver: "default",
|
||||
}
|
||||
}
|
||||
|
||||
if len(networkDef.IPAM.Config) == 0 {
|
||||
subnets, err := findAvailableSubnets(1)
|
||||
if err != nil {
|
||||
return types.NetworkCreateResponse{}, err
|
||||
}
|
||||
|
||||
networkDef.IPAM.Config = []network.IPAMConfig{
|
||||
network.IPAMConfig{
|
||||
Subnet: subnets[0],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
subnets, err := findAvailableSubnets(len(networkDef.IPAM.Config))
|
||||
if err != nil {
|
||||
return types.NetworkCreateResponse{}, err
|
||||
}
|
||||
|
||||
for i, config := range networkDef.IPAM.Config {
|
||||
if config.Subnet == "" {
|
||||
config.Subnet = subnets[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DockerClient.NetworkCreate(DockerContext, name, networkDef)
|
||||
}
|
||||
|
||||
func CreateCosmosNetwork(name string) (string, error) {
|
||||
@@ -93,7 +137,11 @@ func CreateCosmosNetwork(name string) (string, error) {
|
||||
|
||||
utils.Log("Creating new secure network: " + newNeworkName)
|
||||
|
||||
subnet := findAvailableSubnet()
|
||||
subnet, err := findAvailableSubnets(1)
|
||||
if err != nil {
|
||||
utils.Error("Docker Network Create", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// create network
|
||||
newNetHan, err := DockerClient.NetworkCreate(DockerContext, newNeworkName, types.NetworkCreate{
|
||||
@@ -102,7 +150,7 @@ func CreateCosmosNetwork(name string) (string, error) {
|
||||
Driver: "default",
|
||||
Config: []network.IPAMConfig{
|
||||
network.IPAMConfig{
|
||||
Subnet: subnet,
|
||||
Subnet: subnet[0],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -345,11 +393,14 @@ func _debounceNetworkCleanUp() func(string) {
|
||||
}
|
||||
|
||||
func CreateLinkNetwork(containerName string, container2Name string) error {
|
||||
subnet := findAvailableSubnet()
|
||||
subnet, err := findAvailableSubnets(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create network
|
||||
networkName := "cosmos-link-" + containerName + "-" + container2Name + "-" + utils.GenerateRandomString(2)
|
||||
_, err := DockerClient.NetworkCreate(DockerContext, networkName, types.NetworkCreate{
|
||||
_, err = DockerClient.NetworkCreate(DockerContext, networkName, types.NetworkCreate{
|
||||
CheckDuplicate: true,
|
||||
Labels: map[string]string{
|
||||
"cosmos-link": "true",
|
||||
@@ -360,7 +411,7 @@ func CreateLinkNetwork(containerName string, container2Name string) error {
|
||||
IPAM: &network.IPAM{
|
||||
Config: []network.IPAMConfig{
|
||||
network.IPAMConfig{
|
||||
Subnet: subnet,
|
||||
Subnet: subnet[0],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -161,7 +161,7 @@ func AggloMetrics(metricsList []string) []DataDefDB {
|
||||
|
||||
metric.ValuesAggl["hour_" + valueHourlyPool] = currentPool
|
||||
} else {
|
||||
utils.Warn("Metrics: Agglomeration - Pool not found : " + "hour_" + valueHourlyPool)
|
||||
utils.Debug("Metrics: Agglomeration - Pool not found : " + "hour_" + valueHourlyPool)
|
||||
}
|
||||
|
||||
if _, ok := metric.ValuesAggl["day_" + valueDailyPool]; ok {
|
||||
@@ -174,7 +174,7 @@ func AggloMetrics(metricsList []string) []DataDefDB {
|
||||
|
||||
metric.ValuesAggl["day_" + valueDailyPool] = currentPool
|
||||
} else {
|
||||
utils.Warn("Metrics: Agglomeration - Pool not found: " + "day_" + valueDailyPool)
|
||||
utils.Debug("Metrics: Agglomeration - Pool not found: " + "day_" + valueDailyPool)
|
||||
}
|
||||
|
||||
values[valInd].Processed = true
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
type FormatDiskJSON struct {
|
||||
Disk string `json:"disk"`
|
||||
Format string `json:"format"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func isDiskOrPartition(path string) (string, error) {
|
||||
@@ -43,15 +44,8 @@ func FormatDiskRoute(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
var request FormatDiskJSON
|
||||
err := json.NewDecoder(req.Body).Decode(&request)
|
||||
if err != nil {
|
||||
utils.Error("FormatDiskRoute: Invalid User Request", err)
|
||||
utils.HTTPError(w, "FormatDiskRoute Error", http.StatusInternalServerError, "FD001")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
// Enable streaming of response by setting appropriate headers
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
@@ -63,10 +57,31 @@ func FormatDiskRoute(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var request FormatDiskJSON
|
||||
err := json.NewDecoder(req.Body).Decode(&request)
|
||||
if err != nil {
|
||||
utils.Error("FormatDiskRoute: Invalid User Request", err)
|
||||
fmt.Fprintf(w, "[OPERATION FAILED] FormatDiskRoute Syntax Error")
|
||||
http.Error(w, "FormatDiskRoute Syntax Error", http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
nickname := req.Header.Get("x-cosmos-user")
|
||||
|
||||
errp := utils.CheckPassword(nickname, request.Password)
|
||||
if errp != nil {
|
||||
utils.Error("FormatDiskRoute: Invalid User Request", errp)
|
||||
fmt.Fprintf(w, "[OPERATION FAILED] Wrong password supplied. Try again")
|
||||
http.Error(w, "Wrong password supplied. Try again", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := FormatDisk(request.Disk, request.Format)
|
||||
if err != nil {
|
||||
utils.Error("FormatDiskRoute: Error formatting disk", err)
|
||||
utils.HTTPError(w, "FormatDiskRoute Error", http.StatusInternalServerError, "FD002")
|
||||
fmt.Fprintf(w, "[OPERATION FAILED] Error formatting disk: " + err.Error())
|
||||
http.Error(w, "Error formatting disk", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,7 +100,8 @@ func FormatDiskRoute(w http.ResponseWriter, req *http.Request) {
|
||||
out, err = CreateSinglePartition(request.Disk)
|
||||
if err != nil {
|
||||
utils.Error("FormatDiskRoute: Error creating partition", err)
|
||||
utils.HTTPError(w, "FormatDiskRoute Error", http.StatusInternalServerError, "FD003")
|
||||
fmt.Fprintf(w, "[OPERATION FAILED] Error creating partition: " + err.Error())
|
||||
http.Error(w, "Error creating partition", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -99,7 +115,8 @@ func FormatDiskRoute(w http.ResponseWriter, req *http.Request) {
|
||||
out, err = FormatDisk(request.Disk + "1", request.Format)
|
||||
if err != nil {
|
||||
utils.Error("FormatDiskRoute: Error formatting partition", err)
|
||||
utils.HTTPError(w, "FormatDiskRoute Error", http.StatusInternalServerError, "FD002")
|
||||
fmt.Fprintf(w, "[OPERATION FAILED] Error formatting partition: " + err.Error())
|
||||
http.Error(w, "Error formatting partition", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -60,10 +60,10 @@ func GetRecursiveDiskUsageAndSMARTInfo(devices []lsblk.BlockDevice) ([]BlockDevi
|
||||
if err == nil {
|
||||
devicesF[i].SMART = *smartInfo
|
||||
} else {
|
||||
utils.Error("GetRecursiveDiskUsageAndSMARTInfo - Error fetching SMART info for " + device.Name + " : ", err)
|
||||
utils.Warn("GetRecursiveDiskUsageAndSMARTInfo - Error fetching SMART info for " + device.Name + " : " + err.Error())
|
||||
}
|
||||
} else {
|
||||
utils.Error("GetRecursiveDiskUsageAndSMARTInfo - Error fetching SMART info for " + device.Name + " : ", err)
|
||||
utils.Warn("GetRecursiveDiskUsageAndSMARTInfo - Error fetching SMART info for " + device.Name + " : " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +78,6 @@ func GetRecursiveDiskUsageAndSMARTInfo(devices []lsblk.BlockDevice) ([]BlockDevi
|
||||
}
|
||||
|
||||
func GetDiskUsage(path string) (perc uint64, err error) {
|
||||
fmt.Println("[STORAGE] Getting usage of disk " + path)
|
||||
|
||||
// Get the disk usage using the df command
|
||||
cmd := exec.Command("df", "-k", path)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ func UserLogin(w http.ResponseWriter, req *http.Request) {
|
||||
utils.Error("UserLogin: User already logged ing", nil)
|
||||
utils.HTTPError(w, "User is already logged in", http.StatusUnauthorized, "UL002")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var request LoginRequestJSON
|
||||
err1 := json.NewDecoder(req.Body).Decode(&request)
|
||||
@@ -36,7 +36,7 @@ func UserLogin(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "users")
|
||||
defer closeDb()
|
||||
defer closeDb()
|
||||
if errCo != nil {
|
||||
utils.Error("Database Connect", errCo)
|
||||
utils.HTTPError(w, "Database Error", http.StatusInternalServerError, "DB001")
|
||||
|
||||
@@ -13,13 +13,18 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"github.com/Masterminds/semver"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
var ConfigLock sync.Mutex
|
||||
@@ -711,3 +716,39 @@ func CompareSemver(v1, v2 string) (int, error) {
|
||||
|
||||
return ver1.Compare(ver2), nil
|
||||
}
|
||||
|
||||
|
||||
func CheckPassword(nickname, password string) error {
|
||||
time.Sleep(time.Duration(rand.Float64()*1)*time.Second)
|
||||
|
||||
c, closeDb, errCo := GetEmbeddedCollection(GetRootAppId(), "users")
|
||||
defer closeDb()
|
||||
if errCo != nil {
|
||||
return errCo
|
||||
}
|
||||
|
||||
user := User{}
|
||||
|
||||
err3 := c.FindOne(nil, map[string]interface{}{
|
||||
"Nickname": nickname,
|
||||
}).Decode(&user)
|
||||
|
||||
|
||||
if err3 == mongo.ErrNoDocuments {
|
||||
bcrypt.CompareHashAndPassword([]byte("$2a$14$4nzsVwEnR3.jEbMTME7kqeCo4gMgR/Tuk7ivNExvXjr73nKvLgHka"), []byte("dummyPassword"))
|
||||
return err3
|
||||
} else if err3 != nil {
|
||||
bcrypt.CompareHashAndPassword([]byte("$2a$14$4nzsVwEnR3.jEbMTME7kqeCo4gMgR/Tuk7ivNExvXjr73nKvLgHka"), []byte("dummyPassword"))
|
||||
return err3
|
||||
} else if user.Password == "" {
|
||||
return errors.New("User not registered")
|
||||
} else {
|
||||
err2 := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user