9 Commits

Author SHA1 Message Date
63c92ac272 Docker Implementation 2024-01-22 01:49:13 +02:00
10bb300087 * Updated README 2024-01-22 00:47:09 +02:00
6d1bb15ea6 Updated Readme 2024-01-22 00:45:12 +02:00
f039369ec1 * Updated Readme 2024-01-22 00:36:57 +02:00
b4574eb73d + License
* Migrated to github
2024-01-22 00:36:05 +02:00
cdbab95930 * Readme Update 2024-01-22 00:28:24 +02:00
1d970aa6ba Rootless Run
+ Debug Tools
* ReadME
* Optimized config, now it's cached
* ReWrote and optimized logic.go
* rewrote and optimized
+ Styling improvements
2024-01-22 00:28:07 +02:00
d7e856aca2 Fixed Bugs
* Fixed gin release mode
- Removed debug logs
2024-01-21 22:56:53 +02:00
39e16ce408 * Ginmode force 2024-01-21 22:50:50 +02:00
24 changed files with 532 additions and 116 deletions

View File

@@ -23,3 +23,6 @@ LISTEN=":8080"
# Basic Security, these are required to view the data
IDENTITY_USERNAME=admin
IDENTITY_PASSWORD=admin
# Enable/Disable debug features
DEBUG_MODE=false

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ snapshots.dat
dist
data.db
data.sqlite
dev_data

15
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}"
}
]
}

52
Dockerfile Normal file
View File

@@ -0,0 +1,52 @@
# Build Stage
FROM debian:bullseye
ENV IS_DOCKER TRUE
# Install build dependencies and runtime dependencies
RUN apt-get update && apt-get install -y \
gcc \
musl-dev \
libsqlite3-dev \
libsqlite3-0 \
wget \
&& rm -rf /var/lib/apt/lists/*
# Manually install Go 1.21
ENV GOLANG_VERSION 1.21.0
RUN wget https://golang.org/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz -O go.tgz \
&& tar -C /usr/local -xzf go.tgz \
&& rm go.tgz
ENV PATH /usr/local/go/bin:$PATH
# Set the environment variable for Go
ENV GOPATH=/go
ENV PATH=$GOPATH/bin:$PATH
ENV GO111MODULE=on
ENV DIST_DIR=/app
# Create the directory and set it as the working directory
WORKDIR $DIST_DIR
# Copy the Go files and download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy the source from the current directory to the Working Directory inside the container
COPY . .
# Build the Go app
RUN go build -o drive-health
# Cleanup build dependencies to reduce image size
RUN apt-get purge -y gcc musl-dev libsqlite3-dev wget \
&& apt-get autoremove -y \
&& apt-get clean
# Expose the necessary port
EXPOSE 8080
# Volume for external data
VOLUME [ "/data" ]
# Command to run the executable
CMD ["./drive-health"]

13
LICENSE Normal file
View File

@@ -0,0 +1,13 @@
Copyright [2024] [Daniel Legt]
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.

View File

@@ -1,11 +1,57 @@
## About
## 📖About
## Disclaimer
Drive Health is a program written in golang to help with tracking and monitoring of your hardware's temperature.
This software is only meant to run on linux, it's something I threw together all at once in one sitting without really thinking about structure or even caring about anything but making it work.
This tool has been with the purpose of installing it in different servers I own with different configurations to help keep track of the temperature of different hard-disks, ssds, nvme drives, etc... The testing has been very limited to only 4 different computers and not on laptops so expect some mishaps.
It's actually meant to run on my own server just so I can track the temperature of my harddrives in a live environment, I just want this service to run, record the status of the harddrives, and be able via my VPN to access it in a private manner, it's not meant to be dockerized, it's not meant to be ran in a VM, it's meant to be ran either as a `service` or in a [screen](https://git.savannah.gnu.org/cgit/screen.git) in the background.
![UI Example](./media/design_v1.webp)
It's not really meant to run all the time or anything like that, I just wanted to write this disclaimer, feel free to use the code any way you want to :)
## ❗Disclaimer
## How to use
I'm not exactly a linux hardware wizard, so I honestly have no clue about a lot of things and I myself can tell there's a lot to improve upon and that there's a lot of other things missing that are a little bit more obscure, I personally don't currently own any m.2 sata drives to test the code on, or many of the other drive types, I have only tested on HDD, SSD and NVMe drives, any issues opened would help me so much!
## ❗Requirements
1. A linux machine, this will NOT work on macOS or on Windows, it's meant to be ran on servers as a service with which administrators can privately connect to for temperature logging.
2. Please make sure you have the [**drivetemp kernel drive**](https://docs.kernel.org/hwmon/drivetemp.html) you can check this by running `sudo modprobe drivetemp`.
The program depends on this to be able to log the temperature of your devices.
## 📖How to use
The program is straight forward to use really, edit the [.env](./.env) file and make the changes you would like applied.
### Docker ( Recommended/Hassle free )
### SystemD
```ini
[Unit]
Description=Drive Health Service
After=network.target
[Service]
Type=simple
User=daniel # Your user here
WorkingDirectory=/home/daniel/services/drive-health # The path to the service's directory
ExecStart=/home/daniel/services/drive-health/drive-health # The path to the binary
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
## ❔FAQ
### How does it work?
Currently the program does not depend on any hardware library as I couldn't find anything that would not require root access while giving me the possibility to interrogate the temperature of the drives, I chose not to depend on `lsblk` either, so how does the program work? Well it looks in `/sys/block` and simply
### Why not just run as root?
I really, really, **really** want to avoid asking people to run **ANY** program I write as root and even try and prevent that from happening since that's how things can go bad, especially because I am runnig actions over hardware items. I think you can see how easy it is for a mistake or a **malicious attack** to easily deal damage
## Support & Contribution
For support, bug reports, or feature requests, please open an issue on the [GitHub repository](https://github.com/JustKato/drive-health/issues). Contributions are welcome! Fork the repository, make your changes, and submit a pull request.
## License
This project is licensed under the [Apache License 2.0](./LICENSE).

View File

@@ -1,17 +1,19 @@
#!/bin/bash
#!/bin/sh
set -o pipefail
set -u
APP_NAME="drive-health"
DIST_DIR="dist"
DIST_DIR="${DIST_DIR:-dist}"
# Create the dist directory if it doesn't exist
mkdir -p $DIST_DIR
# Build the application
echo "Building the application..."
GOOS=linux GOARCH=amd64 go build -o $DIST_DIR/$APP_NAME
GOOS=linux CGO_ENABLED=1 GOARCH=amd64 go build -o $DIST_DIR/$APP_NAME
# Copy additional resources (like .env, static files, templates) to the dist directory
echo "Copying additional resources..."
cp -r .env static templates $DIST_DIR/
# echo "Copying additional resources..."
cp -r static templates $DIST_DIR/
echo "Compilation and packaging completed."

56
deploy.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
# Function to display messages in color
echo_color() {
color=$1
text=$2
case $color in
"green") echo -e "\033[0;32m$text\033[0m" ;;
"yellow") echo -e "\033[0;33m$text\033[0m" ;;
"red") echo -e "\033[0;31m$text\033[0m" ;;
*) echo "$text" ;;
esac
}
# Getting GIT_VERSION from the most recent tag or commit hash
GIT_VERSION=$(git describe --tags --always)
if [ -z "$GIT_VERSION" ]; then
echo_color red "Error: Unable to determine GIT_VERSION."
exit 1
fi
# Run tests before proceeding
echo_color yellow "Running tests..."
if ! go test; then
echo_color red "Tests failed. Cancelling build process."
exit 1
fi
echo_color green "All tests passed successfully."
echo_color green "Starting the Docker build process with version $GIT_VERSION..."
IMAGE_NAME="ghcr.io/JustKato/drive-health:$GIT_VERSION"
echo_color yellow "Image to be built: $IMAGE_NAME"
# Confirmation to build
read -p "Are you sure you want to build an image? (y/N) " response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
# Building the Docker image
echo "Building Docker image: $IMAGE_NAME"
docker build --no-cache -t $IMAGE_NAME .
else
echo_color red "Build cancelled."
exit 1
fi
# Prompt to push the image
read -p "Push image to repository? (y/N) " push_response
if [[ "$push_response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
# Pushing the image
echo "Pushing image: $IMAGE_NAME"
docker push $IMAGE_NAME
else
echo_color red "Push cancelled."
fi
echo_color green "Ending the Docker build process..."

14
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,14 @@
version: "3.8"
services:
drive-health:
# Build the current image
build: .
# Read straight from the .env file, or use the environment path
env_file:
- .env
volumes:
- ./dev_data:/data
# Setup application ports
ports:
- 8080:8080

17
go.mod
View File

@@ -1,15 +1,22 @@
module tea.chunkbyte.com/kato/drive-health
module github.com/JustKato/drive-health
go 1.21.6
require (
github.com/anatol/smart.go v0.0.0-20230705044831-c3b27137baa3 // indirect
github.com/gin-gonic/gin v1.9.1
github.com/joho/godotenv v1.5.1
github.com/wcharczuk/go-chart/v2 v2.1.1
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5
)
require (
github.com/blend/go-sdk v1.20220411.3 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.17.0 // indirect
@@ -17,7 +24,6 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
@@ -28,7 +34,6 @@ require (
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/wcharczuk/go-chart/v2 v2.1.1 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/image v0.11.0 // indirect
@@ -37,6 +42,4 @@ require (
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/sqlite v1.5.4 // indirect
gorm.io/gorm v1.25.5 // indirect
)

17
go.sum
View File

@@ -1,5 +1,5 @@
github.com/anatol/smart.go v0.0.0-20230705044831-c3b27137baa3 h1:kAF2MWFD8tyDqD74OQizymjj2cnZAURwSzBrEslCDnI=
github.com/anatol/smart.go v0.0.0-20230705044831-c3b27137baa3/go.mod h1:llkexGSe52bW0OjNva0kvIqGZxfSnVfpKHrnKBI2+pU=
github.com/blend/go-sdk v1.20220411.3 h1:GFV4/FQX5UzXLPwWV03gP811pj7B8J2sbuq+GJQofXc=
github.com/blend/go-sdk v1.20220411.3/go.mod h1:7lnH8fTi6U4i1fArEXRyOIY2E1X4MALg09qsQqY1+ak=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
@@ -12,6 +12,7 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
@@ -19,6 +20,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@@ -29,7 +32,7 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -57,6 +60,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -67,6 +71,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
@@ -102,8 +107,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -121,9 +124,11 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -16,10 +16,21 @@ type DHConfig struct {
IdentityUsername string `json:"identityUsername"`
IdentityPassword string `json:"identityPassword"`
IsDocker bool `json:isDocker`
DebugMode bool `json:"debugMode"`
}
func GetConfiguration() DHConfig {
config := DHConfig{
var config *DHConfig = nil
func GetConfiguration() *DHConfig {
if config != nil {
return config
}
config = &DHConfig{
DiskFetchFrequency: 5,
CleanupServiceFrequency: 3600,
MaxHistoryAge: 2592000,
@@ -27,6 +38,8 @@ func GetConfiguration() DHConfig {
IdentityUsername: "admin",
IdentityPassword: "admin",
IsDocker: false,
Listen: ":8080",
}
@@ -64,5 +77,19 @@ func GetConfiguration() DHConfig {
config.IdentityPassword = val
}
if val, exists := os.LookupEnv("DEBUG_MODE"); exists {
if isDebug, err := strconv.ParseBool(val); err == nil {
config.DebugMode = isDebug
}
}
if val, exists := os.LookupEnv("IS_DOCKER"); exists {
if isDocker, err := strconv.ParseBool(val); err == nil {
config.IsDocker = isDocker
config.DatabaseFilePath = "/data/data.sqlite"
}
}
return config
}

View File

@@ -1,70 +1,78 @@
package hardware
import (
"bufio"
"bytes"
"fmt"
"os/exec"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/JustKato/drive-health/lib/config"
"gorm.io/gorm"
)
func GetSystemHardDrives(db *gorm.DB, olderThan *time.Time, newerThan *time.Time) ([]*HardDrive, error) {
// Execute the lsblk command to get detailed block device information
cmd := exec.Command("lsblk", "-d", "-o", "NAME,TRAN,SIZE,MODEL,SERIAL,TYPE")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
fmt.Println("Failed to execute command:", err)
return nil, err
}
var systemHardDrives []*HardDrive
// Scan the output line by line
scanner := bufio.NewScanner(&out)
for scanner.Scan() {
line := scanner.Text()
// List all block devices
devices, err := os.ReadDir("/sys/block/")
if err != nil {
return nil, fmt.Errorf("failed to list block devices: %w", err)
}
// Skip the header line
if strings.Contains(line, "NAME") {
for _, device := range devices {
deviceName := device.Name()
// Skip non-physical devices (like loop and ram devices)
// TODO: Read more about this, there might be some other devices we should or should not skip
if strings.HasPrefix(deviceName, "loop") || strings.HasPrefix(deviceName, "ram") {
continue
}
// Split the line into columns
cols := strings.Fields(line)
if len(cols) < 6 {
// Read device details
model, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/device/model", deviceName))
serial, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/device/serial", deviceName))
sizeBytes, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/size", deviceName))
size := convertSizeToString(sizeBytes)
transport := getTransportType(deviceName)
// TODO: Maybe find a better way?
if size == "0 Bytes" {
// This looks like an invalid device, skip it.
if config.GetConfiguration().DebugMode {
fmt.Printf("[🟨] Igoring device:[/dev/%s], reported size of 0\n", deviceName)
}
continue
}
hwid, err := getHardwareID(deviceName)
if err != nil {
if config.GetConfiguration().DebugMode {
fmt.Printf("[🟨] No unique identifier found for device:[/dev/%s] unique identifier\n", deviceName)
}
continue
}
// Filter out nvme drives (M.2)
if cols[1] != "usb" {
hd := &HardDrive{
Name: cols[0],
Transport: cols[1],
Size: cols[2],
Model: cols[3],
Serial: cols[4],
Type: cols[5],
}
systemHardDrives = append(systemHardDrives, hd)
}
Name: deviceName,
Transport: transport,
Model: strings.TrimSpace(string(model)),
Serial: strings.TrimSpace(string(serial)),
Size: size,
Type: getDriveType(deviceName),
HWID: hwid,
}
// Handle error
if err := scanner.Err(); err != nil {
return nil, err
systemHardDrives = append(systemHardDrives, hd)
}
var updatedHardDrives []*HardDrive
for _, sysHDD := range systemHardDrives {
var existingHD HardDrive
q := db.Where("serial = ? AND model = ? AND type = ?", sysHDD.Serial, sysHDD.Model, sysHDD.Type)
q := db.Where("hw_id = ?", sysHDD.HWID)
if newerThan != nil && olderThan != nil {
q = q.Preload("Temperatures", "time_stamp < ? AND time_stamp > ?", newerThan, olderThan)
@@ -90,3 +98,101 @@ func GetSystemHardDrives(db *gorm.DB, olderThan *time.Time, newerThan *time.Time
return updatedHardDrives, nil
}
func getTransportType(deviceName string) string {
transportLink, err := filepath.EvalSymlinks(fmt.Sprintf("/sys/block/%s/device", deviceName))
if err != nil {
return "Unknown"
}
if strings.Contains(transportLink, "/usb/") {
return "USB"
} else if strings.Contains(transportLink, "/ata") {
return "SATA"
} else if strings.Contains(transportLink, "/nvme/") {
return "NVMe"
}
return "Other"
}
func convertSizeToString(sizeBytes []byte) string {
// Convert the size from a byte slice to a string, then to an integer
sizeStr := strings.TrimSpace(string(sizeBytes))
sizeSectors, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
return "Unknown"
}
// Convert from 512-byte sectors to bytes
sizeInBytes := sizeSectors * 512
// Define size units
const (
_ = iota // ignore first value by assigning to blank identifier
KB float64 = 1 << (10 * iota)
MB
GB
TB
)
var size float64 = float64(sizeInBytes)
var unit string
// Determine the unit to use
switch {
case size >= TB:
size /= TB
unit = "TB"
case size >= GB:
size /= GB
unit = "GB"
case size >= MB:
size /= MB
unit = "MB"
case size >= KB:
size /= KB
unit = "KB"
default:
unit = "Bytes"
}
// Return the formatted size
return fmt.Sprintf("%.2f %s", size, unit)
}
// Look throug /sys/block/device/ and try and find the unique identifier of the device.
func getHardwareID(deviceName string) (string, error) {
// Define potential ID file paths
idFilePaths := []string{
"/sys/block/" + deviceName + "/device/wwid",
"/sys/block/" + deviceName + "/device/wwn",
"/sys/block/" + deviceName + "/device/serial",
}
// Try to read each file and return the first successful read
for _, path := range idFilePaths {
if idBytes, err := os.ReadFile(path); err == nil {
return strings.TrimSpace(string(idBytes)), nil
}
}
// Return an empty string if no ID is found
return "", fmt.Errorf("could not find unique identifier for %s", deviceName)
}
// Figure out what kind of device this is by reading if it's rotational or not
func getDriveType(deviceName string) string {
// Check if the drive is rotational (HDD)
if isRotational, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/queue/rotational", deviceName)); string(isRotational) == "1\n" {
return "HDD"
}
// Check if the drive is NVMe
if strings.HasPrefix(deviceName, "nvme") {
return "NVMe"
}
// Default to SSD for non-rotational and non-NVMe drives
return "SSD"
}

View File

@@ -2,9 +2,12 @@ package hardware
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/anatol/smart.go"
"gorm.io/gorm"
)
@@ -19,6 +22,7 @@ type HardDrive struct {
Model string
Serial string
Type string
HWID string
Temperatures []HardDriveTemperature `gorm:"foreignKey:HardDriveID"`
}
@@ -39,26 +43,49 @@ type Snapshots struct {
List []*HardwareSnapshot
}
// Fetch the temperature of the device, optinally update the reference object
func (h *HardDrive) GetTemperature() int {
// Fetch the device by name
disk, err := smart.Open("/dev/" + h.Name)
if err != nil {
fmt.Printf("Failed to open device %s: %s\n", h.Name, err)
return -1
// Try HDD/SSD path
temp, found := h.getTemperatureFromPath("/sys/block/" + h.Name + "/device/hwmon/")
if found {
return temp
}
defer disk.Close()
// Fetch SMART data
smartInfo, err := disk.ReadGenericAttributes()
if err != nil {
fmt.Printf("Failed to get SMART data for %s: %s\n", h.Name, err)
// Try NVMe path
temp, found = h.getTemperatureFromPath("/sys/block/" + h.Name + "/device/")
if found {
return temp
}
fmt.Printf("[🛑] Failed to get temperature for %s\n", h.Name)
return -1
}
// Parse the temperature
temperature := int(smartInfo.Temperature)
// Return the found value
return temperature
func (h *HardDrive) getTemperatureFromPath(basePath string) (int, bool) {
hwmonDirs, err := os.ReadDir(basePath)
if err != nil {
return 0, false
}
for _, dir := range hwmonDirs {
if strings.HasPrefix(dir.Name(), "hwmon") {
tempPath := filepath.Join(basePath, dir.Name(), "temp1_input")
if _, err := os.Stat(tempPath); err == nil {
tempBytes, err := os.ReadFile(tempPath)
if err != nil {
continue
}
tempStr := strings.TrimSpace(string(tempBytes))
temperature, err := strconv.Atoi(tempStr)
if err != nil {
continue
}
// Convert millidegree Celsius to degree Celsius
return temperature / 1000, true
}
}
}
return 0, false
}

View File

@@ -5,11 +5,11 @@ import (
"fmt"
"time"
"github.com/JustKato/drive-health/lib/config"
"github.com/JustKato/drive-health/lib/hardware"
"github.com/wcharczuk/go-chart/v2"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"tea.chunkbyte.com/kato/drive-health/lib/config"
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
)
var db *gorm.DB

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"time"
"tea.chunkbyte.com/kato/drive-health/lib/config"
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
"github.com/JustKato/drive-health/lib/config"
"github.com/JustKato/drive-health/lib/hardware"
)
// Delete all thermal entries that are older than X amount of seconds

View File

@@ -1,14 +1,13 @@
package web
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/JustKato/drive-health/lib/hardware"
"github.com/JustKato/drive-health/lib/svc"
"github.com/gin-gonic/gin"
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
"tea.chunkbyte.com/kato/drive-health/lib/svc"
)
func setupApi(r *gin.Engine) {
@@ -30,7 +29,6 @@ func setupApi(r *gin.Engine) {
var olderThan, newerThan *time.Time
if ot := ctx.Query("older"); ot != "" {
fmt.Printf("ot = %s\n", ot)
if otInt, err := strconv.ParseInt(ot, 10, 64); err == nil {
otTime := time.UnixMilli(otInt)
olderThan = &otTime
@@ -38,16 +36,12 @@ func setupApi(r *gin.Engine) {
}
if nt := ctx.Query("newer"); nt != "" {
fmt.Printf("nt = %s\n", nt)
if ntInt, err := strconv.ParseInt(nt, 10, 64); err == nil {
ntTime := time.UnixMilli(ntInt)
newerThan = &ntTime
}
}
fmt.Printf("olderThan = %s\n", olderThan)
fmt.Printf("newerThan = %s\n", newerThan)
graphData, err := svc.GetDiskGraphImage(diskId, newerThan, olderThan)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{

View File

@@ -6,9 +6,9 @@ import (
"strconv"
"time"
"github.com/JustKato/drive-health/lib/hardware"
"github.com/JustKato/drive-health/lib/svc"
"github.com/gin-gonic/gin"
"tea.chunkbyte.com/kato/drive-health/lib/hardware"
"tea.chunkbyte.com/kato/drive-health/lib/svc"
)
func setupFrontend(r *gin.Engine) {

View File

@@ -1,13 +1,19 @@
package web
import (
"github.com/JustKato/drive-health/lib/config"
"github.com/gin-gonic/gin"
"tea.chunkbyte.com/kato/drive-health/lib/config"
)
func SetupRouter() *gin.Engine {
// Initialize the Gin engine
cfg := config.GetConfiguration()
if !cfg.DebugMode {
// Set gin to release
gin.SetMode(gin.ReleaseMode)
}
// Initialize the Gin engine
r := gin.Default()
r.Use(BasicAuthMiddleware(cfg.IdentityUsername, cfg.IdentityPassword))

View File

@@ -9,10 +9,10 @@ import (
"syscall"
"time"
"github.com/JustKato/drive-health/lib/config"
"github.com/JustKato/drive-health/lib/svc"
"github.com/JustKato/drive-health/lib/web"
"github.com/joho/godotenv"
"tea.chunkbyte.com/kato/drive-health/lib/config"
"tea.chunkbyte.com/kato/drive-health/lib/svc"
"tea.chunkbyte.com/kato/drive-health/lib/web"
)
func main() {

BIN
media/design_v1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
media/old-look.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -10,7 +10,17 @@
--fg0: #bbc0ca;
--fg1: #434c56;
--acc: #bbc0ca;
--acc1: #2aa3f4;
--acc1BG0: #2aa3f450;
--acc1BG1: #2aa3f430;
--acc2: #2af488;
--acc2BG0: #2af48850;
--acc2BG1: #2af48830;
--acc3: #f4e02a;
--acc3BG0: #f4e02a50;
--acc3BG1: #f4e02a30;
}
:root {
@@ -55,6 +65,41 @@ html, body {
margin: 0;
}
.badge {
font-size: 12px;
font-weight: bold;
padding: .1rem .4rem;
display: inline-block;
border-radius: 3px;
color: var(--acc1);
background-color: var(--acc1BG0);
border: 1px solid var(--acc1BG1);
}
.badge[type="HDD"] {
color: var(--acc2);
background-color: var(--acc2BG0);
border: 1px solid var(--acc2BG1);
}
.badge[type="NVMe"] {
color: var(--acc3);
background-color: var(--acc3BG0);
border: 1px solid var(--acc3BG1);
}
.grooved {
background-color: var(--bg0);
border: 1px solid var(--bg1);
font-size: 12px;
padding: .1rem .3rem;
border-radius: 4px;
}
/* Table */

View File

@@ -99,8 +99,9 @@
<!-- Drives -->
{{ if len .drives }}
{{ range .drives }}
<div class="disk-graph-entry bordered" id="disk-temp-{{ .ID }}">
<h4>{{.Name}}:{{.Serial}} [{{.Size}}]</h4>
<div class="disk-graph-entry bordered" id="disk-temp-{{ .ID }}" style="position: relative;">
<div class="badge" type="{{.Type}}" style="position: absolute; top: 1rem; right: 1rem;">/dev/{{.Name}}</div>
<h4>{{.Model}}:{{.HWID}} <span class="grooved">{{.Size}}</span></h4>
<a href="/api/v1/disks/{{.ID}}/chart" target="_blank">
<img class="graph-image" src="/api/v1/disks/{{.ID}}/chart?older={{ $older }}&newer={{ $newer }}" alt="{{ .Model }} Image">
</a>