Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 838b7c09c4 | |||
| 0aea6b96ca | |||
| afbf1450c2 | |||
| 6a278bc2cf | |||
| 9d1ce790d0 | |||
| fb5d4818b0 | |||
| 3a06920354 | |||
| dd8a6757d1 | |||
| d433a6a54e | |||
| c365988a52 | |||
| 6a1a985306 | |||
| 02996d6288 | |||
| 3d2671650e | |||
| 28658790c8 | |||
| 18f10a9295 | |||
| 67b7a08e4a | |||
| a014337167 | |||
| 3a5ee0a762 | |||
| 625a0244e2 | |||
| a269ba57df | |||
| 6a76b5aa26 | |||
| 939d40eb20 | |||
| ad738508e5 | |||
| 971249ba3f | |||
| 73417ca653 | |||
| a6d092983d | |||
| 1988b101e1 | |||
| 746ae76cfc | |||
| 3380023ad0 | |||
| 6362512406 |
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
app:
|
||||
image: mcr.microsoft.com/devcontainers/base:ubuntu-22.04
|
||||
volumes:
|
||||
- ..:/workspaces/scrutiny:cached
|
||||
command: sleep infinity
|
||||
network_mode: service:influxdb
|
||||
|
||||
influxdb:
|
||||
image: influxdb:2.8
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8086:8086"
|
||||
environment:
|
||||
- DOCKER_INFLUXDB_INIT_MODE=setup
|
||||
- DOCKER_INFLUXDB_INIT_USERNAME=admin
|
||||
- DOCKER_INFLUXDB_INIT_PASSWORD=password12345
|
||||
- DOCKER_INFLUXDB_INIT_ORG=scrutiny
|
||||
- DOCKER_INFLUXDB_INIT_BUCKET=metrics
|
||||
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token
|
||||
volumes:
|
||||
- scrutiny-influxdb-data:/var/lib/influxdb2
|
||||
|
||||
volumes:
|
||||
scrutiny-influxdb-data:
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "Scrutiny Dev (rootless docker)",
|
||||
"dockerComposeFile": "../docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/scrutiny",
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/go:1": "1.25",
|
||||
"ghcr.io/devcontainers/features/node:1": "lts"
|
||||
},
|
||||
|
||||
"onCreateCommand": "sudo apt-get update && sudo apt-get install -y smartmontools iputils-ping chromium-browser",
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"golang.go",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"forwardPorts": [8080, 8086],
|
||||
|
||||
"postCreateCommand": "bash .devcontainer/setup.sh",
|
||||
"remoteUser": "root",
|
||||
"containerUser": "root",
|
||||
"updateRemoteUserUID": false
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Scrutiny Dev (docker)",
|
||||
"dockerComposeFile": "../docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/scrutiny",
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/go:1": "1.25",
|
||||
"ghcr.io/devcontainers/features/node:1": "lts"
|
||||
},
|
||||
|
||||
"onCreateCommand": "sudo apt-get update && sudo apt-get install -y smartmontools iputils-ping chromium-browser",
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"golang.go",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"forwardPorts": [8080, 8086],
|
||||
|
||||
"postCreateCommand": "bash .devcontainer/setup.sh",
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Scrutiny Dev (podman)",
|
||||
"dockerComposeFile": "../docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/scrutiny",
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/go:1": "1.25",
|
||||
"ghcr.io/devcontainers/features/node:1": "lts"
|
||||
},
|
||||
|
||||
"onCreateCommand": "sudo apt-get update && sudo apt-get install -y smartmontools iputils-ping chromium-browser",
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"golang.go",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"forwardPorts": [8080, 8086],
|
||||
|
||||
"postCreateCommand": "bash .devcontainer/setup.sh",
|
||||
"remoteEnv": {
|
||||
"PODMAN_USERNS": "keep-id"
|
||||
},
|
||||
"containerUser": "vscode",
|
||||
"updateRemoteUserUID": true
|
||||
}
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Starting Scrutiny Setup..."
|
||||
|
||||
if [ ! -f "scrutiny.yaml" ]; then
|
||||
echo "Creating scrutiny.yaml from template..."
|
||||
cat <<EOF > scrutiny.yaml
|
||||
version: 1
|
||||
web:
|
||||
listen:
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
database:
|
||||
location: ./scrutiny.db
|
||||
src:
|
||||
frontend:
|
||||
path: ./dist
|
||||
influxdb:
|
||||
retention_policy: false
|
||||
token: "my-super-secret-auth-token"
|
||||
org: "scrutiny"
|
||||
bucket: "metrics"
|
||||
host: "localhost"
|
||||
port: 8086
|
||||
log:
|
||||
file: 'web.log'
|
||||
level: DEBUG
|
||||
EOF
|
||||
else
|
||||
echo "scrutiny.yaml already exists."
|
||||
fi
|
||||
|
||||
echo "Vendoring Go modules..."
|
||||
go mod vendor
|
||||
|
||||
echo "Installing Node modules..."
|
||||
cd webapp/frontend
|
||||
npm install
|
||||
|
||||
echo "Setup Complete! Ready to code."
|
||||
+68
-10
@@ -1,6 +1,13 @@
|
||||
name: CI
|
||||
# This workflow is triggered on pushes & pull requests
|
||||
on: [pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-frontend:
|
||||
@@ -21,11 +28,10 @@ jobs:
|
||||
test-backend:
|
||||
name: Test Backend
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/packagrio/packagr:latest-golang
|
||||
# Service containers to run with `build` (Required for end-to-end testing)
|
||||
services:
|
||||
influxdb:
|
||||
image: influxdb:2.2
|
||||
image: influxdb:2.8
|
||||
env:
|
||||
DOCKER_INFLUXDB_INIT_MODE: setup
|
||||
DOCKER_INFLUXDB_INIT_USERNAME: admin
|
||||
@@ -38,13 +44,10 @@ jobs:
|
||||
env:
|
||||
STATIC: true
|
||||
steps:
|
||||
- name: Git
|
||||
run: |
|
||||
apt-get update && apt-get install -y software-properties-common
|
||||
add-apt-repository ppa:git-core/ppa && apt-get update && apt-get install -y git
|
||||
git --version
|
||||
- name: Add influxdb to hosts
|
||||
run: echo "127.0.0.1 influxdb" | sudo tee -a /etc/hosts
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
- name: Test Backend
|
||||
run: |
|
||||
make binary-clean binary-test-coverage
|
||||
@@ -76,6 +79,19 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
args: --issues-exit-code=0
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}
|
||||
runs-on: ${{ matrix.cfg.on }}
|
||||
@@ -102,7 +118,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.20.1'
|
||||
go-version: '^1.25'
|
||||
- name: Build Binaries
|
||||
run: |
|
||||
make binary-clean binary-all
|
||||
@@ -113,3 +129,45 @@ jobs:
|
||||
path: |
|
||||
scrutiny-web-*
|
||||
scrutiny-collector-metrics-*
|
||||
|
||||
build-docker:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build omnibus
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
- name: Build collector
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
file: docker/Dockerfile.collector
|
||||
push: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
- name: Build web
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
file: docker/Dockerfile.web
|
||||
push: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
file: docker/Dockerfile.collector
|
||||
file: docker/Dockerfile.web
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -145,10 +145,10 @@ jobs:
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
# tag latest and latest-omnibus
|
||||
with:
|
||||
flavor: |
|
||||
latest=true
|
||||
# tag latest and latest-omnibus
|
||||
suffix=-omnibus,onlatest=false
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
@@ -162,9 +162,9 @@ jobs:
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
file: docker/Dockerfile.collector
|
||||
file: docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
# Service containers to run with `build` (Required for end-to-end testing)
|
||||
services:
|
||||
influxdb:
|
||||
image: influxdb:2.2
|
||||
image: influxdb:2.8
|
||||
env:
|
||||
DOCKER_INFLUXDB_INIT_MODE: setup
|
||||
DOCKER_INFLUXDB_INIT_USERNAME: admin
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
name: workspace
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.20.1' # The Go version to download (if necessary) and use.
|
||||
go-version: '1.25' # The Go version to download (if necessary) and use.
|
||||
- name: Build Binaries
|
||||
run: |
|
||||
make binary-clean binary-all
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
version: "2"
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
settings:
|
||||
errcheck:
|
||||
check-blank: true
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Scrutiny",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/webapp/backend/cmd/scrutiny/scrutiny.go",
|
||||
"args": ["start", "--config", "./scrutiny.yaml"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"DEBUG": "true"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"preLaunchTask": "Build Frontend",
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "Listening and serving HTTP on",
|
||||
"uriFormat": "http://localhost:8080/web/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Collector",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/collector/cmd/collector-metrics/collector-metrics.go",
|
||||
"args": ["run", "--debug"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"COLLECTOR_DEBUG": "true"
|
||||
},
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build Frontend",
|
||||
"type": "shell",
|
||||
"command": "cd webapp/frontend && npm run build:prod -- --output-path=../../dist"
|
||||
}
|
||||
]
|
||||
}
|
||||
+10
-5
@@ -147,9 +147,14 @@ The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo)
|
||||
|
||||
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
|
||||
|
||||
# Devcontainer
|
||||
Devcontainer configurations are available to build and run Scrutiny (WebUI and Collector) in a fully isolated environment.
|
||||
When opening the project with vscode, choose "Reopen in Container". Three configurations are available depending on your
|
||||
container runtime and setup: docker, docker-rootless, and podman.
|
||||
|
||||
# Modifying the Scrutiny Backend Server (API)
|
||||
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.20+)
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.25)
|
||||
2. download the `scrutiny-web-frontend.tar.gz` for
|
||||
the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist`
|
||||
3. create a `scrutiny.yaml` config file
|
||||
@@ -177,7 +182,7 @@ Depending on the functionality you are adding, you may need to setup a developme
|
||||
```
|
||||
4. start a InfluxDB docker container.
|
||||
```bash
|
||||
docker run -p 8086:8086 --rm influxdb:2.2
|
||||
docker run -p 8086:8086 --rm influxdb:2.8
|
||||
```
|
||||
5. start the scrutiny web server
|
||||
```bash
|
||||
@@ -230,7 +235,7 @@ you'll need to follow the steps below:
|
||||
```
|
||||
4. start a InfluxDB docker container.
|
||||
```bash
|
||||
docker run -p 8086:8086 --rm influxdb:2.2
|
||||
docker run -p 8086:8086 --rm influxdb:2.8
|
||||
```
|
||||
5. build the Angular Frontend Application
|
||||
```bash
|
||||
@@ -254,7 +259,7 @@ If you'd like to populate the database with some test data, you can run the fol
|
||||
> This is done automatically by the `webapp/backend/pkg/models/testdata/helper.go` script
|
||||
|
||||
```
|
||||
docker run -p 8086:8086 --rm influxdb:2.2
|
||||
docker run -p 8086:8086 --rm influxdb:2.8
|
||||
|
||||
|
||||
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/web/testdata/register-devices-req.json localhost:8080/api/devices/register
|
||||
@@ -322,7 +327,7 @@ docker run -p 8086:8086 -d --rm \
|
||||
-e DOCKER_INFLUXDB_INIT_ORG=scrutiny \
|
||||
-e DOCKER_INFLUXDB_INIT_BUCKET=metrics \
|
||||
-e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token \
|
||||
influxdb:2.2
|
||||
influxdb:2.8
|
||||
go test ./...
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and provide it with the entire recipe, regardless of how many lines it contains.
|
||||
.SHELLFLAGS = -ec
|
||||
export GOTOOLCHAIN=go1.25.5
|
||||
|
||||
########################################################################################################################
|
||||
# Global Env Settings
|
||||
@@ -66,6 +67,11 @@ binary-dep:
|
||||
binary-test: binary-dep
|
||||
go test -v $(STATIC_TAGS) ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
GOTOOLCHAIN=go1.25.5 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0
|
||||
golangci-lint run ./...
|
||||
|
||||
.PHONY: binary-test-coverage
|
||||
binary-test-coverage: binary-dep
|
||||
go test -coverprofile=coverage.txt -covermode=atomic -v $(STATIC_TAGS) ./...
|
||||
@@ -115,19 +121,23 @@ binary-frontend-test-coverage:
|
||||
########################################################################################################################
|
||||
# Docker
|
||||
# NOTE: these docker make targets are only used for local development (not used by Github Actions/CI)
|
||||
# NOTE: docker-web and docker-omnibus require `make binary-frontend` or frontend.tar.gz content in /dist before executing.
|
||||
########################################################################################################################
|
||||
.PHONY: docker-smartmontools
|
||||
docker-smartmontools:
|
||||
@echo "building smartmontools docker image"
|
||||
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile.smartmontools -t smartmontools-build .
|
||||
|
||||
.PHONY: docker-collector
|
||||
docker-collector:
|
||||
docker-collector: docker-smartmontools
|
||||
@echo "building collector docker image"
|
||||
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile.collector -t analogj/scrutiny-dev:collector .
|
||||
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile.collector -t ghcr.io/analogj/scrutiny-dev:collector .
|
||||
|
||||
.PHONY: docker-web
|
||||
docker-web:
|
||||
@echo "building web docker image"
|
||||
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile.web -t analogj/scrutiny-dev:web .
|
||||
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile.web -t ghcr.io/analogj/scrutiny-dev:web .
|
||||
|
||||
.PHONY: docker-omnibus
|
||||
docker-omnibus:
|
||||
docker-omnibus: docker-smartmontools
|
||||
@echo "building omnibus docker image"
|
||||
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile -t analogj/scrutiny-dev:omnibus .
|
||||
docker build $(DOCKER_TARGETARCH_BUILD_ARG) -f docker/Dockerfile -t ghcr.io/analogj/scrutiny-dev:omnibus .
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
# scrutiny
|
||||
|
||||
[](https://github.com/AnalogJ/scrutiny/actions?query=workflow%3ACI)
|
||||
[](https://github.com/AnalogJ/scrutiny/actions/workflows/ci.yaml)
|
||||
[](https://codecov.io/gh/AnalogJ/scrutiny)
|
||||
[](https://github.com/AnalogJ/scrutiny/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/analogj/scrutiny)
|
||||
@@ -102,7 +102,7 @@ other Docker images:
|
||||
- `ghcr.io/analogj/scrutiny:latest-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like
|
||||
scheduler. You can run one collector on each server.
|
||||
- `ghcr.io/analogj/scrutiny:latest-web` - Contains the Web UI and API. Only one container necessary
|
||||
- `influxdb:2.2` - InfluxDB image, used by the Web container to persist SMART data. Only one container necessary
|
||||
- `influxdb:2.8` - InfluxDB image, used by the Web container to persist SMART data. Only one container necessary
|
||||
See [docs/TROUBLESHOOTING_INFLUXDB.md](./docs/TROUBLESHOOTING_INFLUXDB.md)
|
||||
|
||||
> See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
|
||||
@@ -111,7 +111,7 @@ other Docker images:
|
||||
docker run -p 8086:8086 --restart unless-stopped \
|
||||
-v `pwd`/influxdb2:/var/lib/influxdb2 \
|
||||
--name scrutiny-influxdb \
|
||||
influxdb:2.2
|
||||
influxdb:2.8
|
||||
|
||||
docker run -p 8080:8080 --restart unless-stopped \
|
||||
-v `pwd`/scrutiny:/opt/scrutiny/config \
|
||||
@@ -128,6 +128,10 @@ docker run --restart unless-stopped \
|
||||
ghcr.io/analogj/scrutiny:latest-collector
|
||||
```
|
||||
|
||||
### Hub rootless installation using Podman Quadlets
|
||||
|
||||
See [docs/INSTALL_ROOTLESS_PODMAN.md](docs/INSTALL_ROOTLESS_PODMAN.md) for instructions.
|
||||
|
||||
## Manual Installation (without-Docker)
|
||||
|
||||
While the easiest way to get started with [Scrutiny is using Docker](https://github.com/AnalogJ/scrutiny#docker),
|
||||
|
||||
@@ -3,17 +3,18 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/collector/pkg/config"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/collector/pkg/config"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
utils "github.com/analogj/go-util/utils"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -37,8 +38,8 @@ func main() {
|
||||
}
|
||||
|
||||
//we're going to load the config file manually, since we need to validate it.
|
||||
err = config.ReadConfig(configFilePath) // Find and read the config file
|
||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||
err = config.ReadConfig(configFilePath) // Find and read the config file
|
||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||
//ignore "could not find config file"
|
||||
} else if err != nil {
|
||||
os.Exit(1)
|
||||
@@ -81,7 +82,7 @@ OPTIONS:
|
||||
|
||||
subtitle := collectorMetrics + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorMetrics))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
@@ -89,7 +90,7 @@ OPTIONS:
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
`), subtitle)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -2,14 +2,15 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
utils "github.com/analogj/go-util/utils"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -57,7 +58,7 @@ OPTIONS:
|
||||
|
||||
subtitle := collectorSelfTest + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorSelfTest))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
@@ -65,7 +66,7 @@ OPTIONS:
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
`), subtitle)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -3,9 +3,10 @@ package collector
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{Timeout: 60 * time.Second}
|
||||
@@ -14,17 +15,6 @@ type BaseCollector struct {
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func (c *BaseCollector) getJson(url string, target interface{}) error {
|
||||
|
||||
r, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
return json.NewDecoder(r.Body).Decode(target)
|
||||
}
|
||||
|
||||
func (c *BaseCollector) postJson(url string, body interface{}, target interface{}) error {
|
||||
requestBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,11 +3,12 @@ package shell
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type localShell struct{}
|
||||
@@ -36,7 +37,7 @@ func (s *localShell) Command(logger *logrus.Entry, cmdName string, cmdArgs []str
|
||||
if workingDir != "" && path.IsAbs(workingDir) {
|
||||
cmd.Dir = workingDir
|
||||
} else if workingDir != "" {
|
||||
return "", errors.New("Working Directory must be an absolute path")
|
||||
return "", errors.New("working directory must be an absolute path")
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
@@ -7,8 +7,8 @@ package mock_shell
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
logrus "github.com/sirupsen/logrus"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface.
|
||||
|
||||
@@ -2,15 +2,16 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// When initializing this class the following methods must be called:
|
||||
@@ -20,7 +21,7 @@ import (
|
||||
type configuration struct {
|
||||
*viper.Viper
|
||||
|
||||
deviceOverrides []models.ScanOverride
|
||||
deviceOverrides []models.ScanOverride
|
||||
}
|
||||
|
||||
//Viper uses the following precedence order. Each item takes precedence over the item below it:
|
||||
@@ -53,7 +54,7 @@ func (c *configuration) Init() error {
|
||||
c.SetEnvPrefix("COLLECTOR")
|
||||
c.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
|
||||
c.AutomaticEnv()
|
||||
|
||||
|
||||
//c.SetDefault("collect.short.command", "-a -o on -S on")
|
||||
|
||||
c.SetDefault("allow_listed_devices", []string{})
|
||||
@@ -167,7 +168,7 @@ func (c *configuration) GetCommandMetricsInfoArgs(deviceName string) string {
|
||||
overrides := c.GetDeviceOverrides()
|
||||
|
||||
for _, deviceOverrides := range overrides {
|
||||
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
|
||||
if strings.EqualFold(deviceName, deviceOverrides.Device) {
|
||||
//found matching device
|
||||
if len(deviceOverrides.Commands.MetricsInfoArgs) > 0 {
|
||||
return deviceOverrides.Commands.MetricsInfoArgs
|
||||
@@ -183,7 +184,7 @@ func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
|
||||
overrides := c.GetDeviceOverrides()
|
||||
|
||||
for _, deviceOverrides := range overrides {
|
||||
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
|
||||
if strings.EqualFold(deviceName, deviceOverrides.Device) {
|
||||
//found matching device
|
||||
if len(deviceOverrides.Commands.MetricsSmartArgs) > 0 {
|
||||
return deviceOverrides.Commands.MetricsSmartArgs
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
reflect "reflect"
|
||||
|
||||
models "github.com/analogj/scrutiny/collector/pkg/models"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
viper "github.com/spf13/viper"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface.
|
||||
|
||||
@@ -107,8 +107,8 @@ func (d *Detect) SmartCtlInfo(device *models.Device) error {
|
||||
if len(device.WWN) == 0 {
|
||||
// no WWN populated after WWN lookup and fallback. we need to throw an error
|
||||
errMsg := fmt.Sprintf("no WWN (or fallback) populated for device: %s. Device will be registered, but no data will be published for this device. ", device.DeviceName)
|
||||
d.Logger.Errorf(errMsg)
|
||||
return fmt.Errorf(errMsg)
|
||||
d.Logger.Errorf("%v", errMsg)
|
||||
return fmt.Errorf("%v", errMsg)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -9,16 +9,15 @@ import (
|
||||
mock_config "github.com/analogj/scrutiny/collector/pkg/config/mock"
|
||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestDetect_SmartctlScan(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
@@ -48,7 +47,6 @@ func TestDetect_SmartctlScan(t *testing.T) {
|
||||
func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
@@ -81,7 +79,6 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
|
||||
func TestDetect_SmartctlScan_Nvme(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
@@ -113,7 +110,6 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
|
||||
func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
@@ -147,7 +143,6 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
||||
func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
|
||||
@@ -180,7 +175,6 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
||||
func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
@@ -223,7 +217,6 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
||||
func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
@@ -257,7 +250,6 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
||||
func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
@@ -290,7 +282,6 @@ func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T)
|
||||
func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
@@ -312,7 +303,6 @@ func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
|
||||
|
||||
func TestDetect_TransformDetectedDevices_AllowListFilters(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
@@ -353,7 +343,6 @@ func TestDetect_TransformDetectedDevices_AllowListFilters(t *testing.T) {
|
||||
func TestDetect_SmartCtlInfo(t *testing.T) {
|
||||
t.Run("should report nvme info", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
const (
|
||||
someArgs = "--info --json"
|
||||
|
||||
@@ -2,12 +2,13 @@ package detect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/jaypipes/ghw"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DevicePrefix() string {
|
||||
@@ -23,15 +24,15 @@ func (d *Detect) Start() ([]models.Device, error) {
|
||||
}
|
||||
|
||||
//inflate device info for detected devices.
|
||||
for ndx, _ := range detectedDevices {
|
||||
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
for ndx := range detectedDevices {
|
||||
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
populateUdevInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
}
|
||||
|
||||
return detectedDevices, nil
|
||||
}
|
||||
|
||||
//WWN values NVMe and SCSI
|
||||
// WWN values NVMe and SCSI
|
||||
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
block, err := ghw.Block()
|
||||
if err == nil {
|
||||
@@ -61,7 +62,7 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
func populateUdevInfo(detectedDevice *models.Device) error {
|
||||
// Get device major:minor numbers
|
||||
// `cat /sys/class/block/sda/dev`
|
||||
devNo, err := ioutil.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
|
||||
devNo, err := os.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -69,7 +70,7 @@ func populateUdevInfo(detectedDevice *models.Device) error {
|
||||
// Look up block device in udev runtime database
|
||||
// `cat /run/udev/data/b8:0`
|
||||
udevID := "b" + strings.TrimSpace(string(devNo))
|
||||
udevBytes, err := ioutil.ReadFile(filepath.Join("/run/udev/data/", udevID))
|
||||
udevBytes, err := os.ReadFile(filepath.Join("/run/udev/data/", udevID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -97,7 +98,5 @@ func populateUdevInfo(detectedDevice *models.Device) error {
|
||||
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package detect_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWwn_FromStringTable(t *testing.T) {
|
||||
@@ -25,8 +25,7 @@ func TestWwn_FromStringTable(t *testing.T) {
|
||||
}
|
||||
//test
|
||||
for _, tt := range tests {
|
||||
testname := fmt.Sprintf("%s", tt.wwnStr)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
t.Run(tt.wwnStr, func(t *testing.T) {
|
||||
str := tt.wwn.ToString()
|
||||
require.Equal(t, tt.wwnStr, str)
|
||||
})
|
||||
|
||||
+22
-3
@@ -12,7 +12,7 @@ RUN make binary-frontend
|
||||
|
||||
|
||||
######## Build the backend
|
||||
FROM golang:1.20-bookworm as backendbuild
|
||||
FROM golang:1.25-trixie as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
COPY --link . /go/src/github.com/analogj/scrutiny
|
||||
@@ -23,8 +23,25 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
||||
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
|
||||
|
||||
|
||||
######## Build smartmontools from source
|
||||
FROM debian:trixie-slim AS smartmontoolsbuild
|
||||
ARG SMARTMONTOOLS_VER=7.5
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl gcc g++ gnupg make \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN curl -L "https://github.com/smartmontools/smartmontools/releases/download/RELEASE_$(echo ${SMARTMONTOOLS_VER} | tr '.' '_')/smartmontools-${SMARTMONTOOLS_VER}.tar.gz" -o /tmp/smartmontools.tar.gz \
|
||||
&& tar -xzf /tmp/smartmontools.tar.gz -C /tmp \
|
||||
&& cd /tmp/smartmontools-${SMARTMONTOOLS_VER} \
|
||||
&& ./configure --prefix=/usr LDFLAGS='-static' --without-libcap-ng --without-libsystemd \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install \
|
||||
&& /usr/sbin/update-smart-drivedb \
|
||||
&& rm -rf /tmp/smartmontools*
|
||||
|
||||
|
||||
######## Combine build artifacts in runtime image
|
||||
FROM debian:bookworm-slim as runtime
|
||||
FROM debian:trixie-slim AS runtime
|
||||
ARG TARGETARCH
|
||||
EXPOSE 8080
|
||||
WORKDIR /opt/scrutiny
|
||||
@@ -40,7 +57,6 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
||||
ca-certificates \
|
||||
cron \
|
||||
curl \
|
||||
smartmontools \
|
||||
tzdata \
|
||||
procps \
|
||||
xz-utils \
|
||||
@@ -62,6 +78,9 @@ RUN curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-${INFLUXVER}-$
|
||||
|
||||
COPY /rootfs /
|
||||
|
||||
COPY --from=smartmontoolsbuild /usr/sbin/smartctl /usr/sbin/smartctl
|
||||
COPY --from=smartmontoolsbuild /usr/share/smartmontools/ /usr/share/smartmontools/
|
||||
|
||||
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
|
||||
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
|
||||
COPY --link --from=frontendbuild --chmod=644 /go/src/github.com/analogj/scrutiny/dist /opt/scrutiny/web
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
|
||||
########
|
||||
FROM golang:1.20-bookworm as backendbuild
|
||||
FROM golang:1.25-trixie AS backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
@@ -13,12 +13,31 @@ COPY . /go/src/github.com/analogj/scrutiny
|
||||
RUN apt-get update && apt-get install -y file && rm -rf /var/lib/apt/lists/*
|
||||
RUN make binary-clean binary-collector
|
||||
|
||||
######## Build smartmontools from source
|
||||
FROM debian:trixie-slim AS smartmontoolsbuild
|
||||
ARG SMARTMONTOOLS_VER=7.5
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl gcc g++ gnupg make \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN curl -L "https://github.com/smartmontools/smartmontools/releases/download/RELEASE_$(echo ${SMARTMONTOOLS_VER} | tr '.' '_')/smartmontools-${SMARTMONTOOLS_VER}.tar.gz" -o /tmp/smartmontools.tar.gz \
|
||||
&& tar -xzf /tmp/smartmontools.tar.gz -C /tmp \
|
||||
&& cd /tmp/smartmontools-${SMARTMONTOOLS_VER} \
|
||||
&& ./configure --prefix=/usr LDFLAGS='-static' --without-libcap-ng --without-libsystemd \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install \
|
||||
&& /usr/sbin/update-smart-drivedb \
|
||||
&& rm -rf /tmp/smartmontools*
|
||||
|
||||
########
|
||||
FROM debian:bookworm-slim as runtime
|
||||
FROM debian:trixie-slim AS runtime
|
||||
WORKDIR /opt/scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
|
||||
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates
|
||||
RUN apt-get update && apt-get install -y cron ca-certificates tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates
|
||||
|
||||
COPY --from=smartmontoolsbuild /usr/sbin/smartctl /usr/sbin/smartctl
|
||||
COPY --from=smartmontoolsbuild /usr/share/smartmontools/ /usr/share/smartmontools/
|
||||
|
||||
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
|
||||
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
########################################################################################################################
|
||||
# Smartmontools Builder
|
||||
# - Builds smartctl from source as a static binary.
|
||||
# - Updates the drive database to include the latest drive models since it can change between releases.
|
||||
# - Used as a shared build stage by Dockerfile and Dockerfile.collector.
|
||||
########################################################################################################################
|
||||
FROM debian:trixie-slim
|
||||
ARG SMARTMONTOOLS_VER=7.5
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl gcc g++ gnupg make \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN curl -L "https://github.com/smartmontools/smartmontools/releases/download/RELEASE_$(echo ${SMARTMONTOOLS_VER} | tr '.' '_')/smartmontools-${SMARTMONTOOLS_VER}.tar.gz" -o /tmp/smartmontools.tar.gz \
|
||||
&& tar -xzf /tmp/smartmontools.tar.gz -C /tmp \
|
||||
&& cd /tmp/smartmontools-${SMARTMONTOOLS_VER} \
|
||||
&& ./configure --prefix=/usr LDFLAGS='-static' --without-libcap-ng --without-libsystemd \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install \
|
||||
&& /usr/sbin/update-smart-drivedb \
|
||||
&& rm -rf /tmp/smartmontools*
|
||||
@@ -11,7 +11,7 @@ COPY --link . /go/src/github.com/analogj/scrutiny
|
||||
RUN make binary-frontend
|
||||
|
||||
######## Build the backend
|
||||
FROM golang:1.20-bookworm as backendbuild
|
||||
FROM golang:1.25-trixie as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
COPY --link . /go/src/github.com/analogj/scrutiny
|
||||
@@ -21,7 +21,7 @@ RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
|
||||
|
||||
|
||||
######## Combine build artifacts in runtime image
|
||||
FROM debian:bookworm-slim as runtime
|
||||
FROM debian:trixie-slim as runtime
|
||||
EXPOSE 8080
|
||||
WORKDIR /opt/scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
|
||||
@@ -3,7 +3,7 @@ version: '2.4'
|
||||
services:
|
||||
influxdb:
|
||||
restart: unless-stopped
|
||||
image: influxdb:2.2
|
||||
image: influxdb:2.8
|
||||
ports:
|
||||
- '8086:8086'
|
||||
volumes:
|
||||
|
||||
+11
-13
@@ -49,19 +49,15 @@ contains the connection and notification details but I always find it easier to
|
||||
docker-compose.
|
||||
|
||||
```yaml
|
||||
version: "3.4"
|
||||
|
||||
networks:
|
||||
monitoring: # A common network for all monitoring services to communicate into
|
||||
external: true
|
||||
notifications: # To Gotify or another Notification service
|
||||
external: true
|
||||
|
||||
services:
|
||||
influxdb:
|
||||
restart: unless-stopped
|
||||
container_name: influxdb
|
||||
image: influxdb:2.1-alpine
|
||||
image: influxdb:2.8
|
||||
ports:
|
||||
- 8086:8086
|
||||
volumes:
|
||||
@@ -73,7 +69,8 @@ services:
|
||||
- DOCKER_INFLUXDB_INIT_PASSWORD=${PASSWORD}
|
||||
- DOCKER_INFLUXDB_INIT_ORG=homelab
|
||||
- DOCKER_INFLUXDB_INIT_BUCKET=scrutiny
|
||||
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=your-very-secret-token
|
||||
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=SUPER-SECRET-TOKEN
|
||||
- TZ=Europe/Stockholm
|
||||
networks:
|
||||
- monitoring
|
||||
|
||||
@@ -85,17 +82,20 @@ services:
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ${DIR_CONFIG}/scrutiny/config:/opt/scrutiny/config
|
||||
- ${DIR_CONFIG}/config:/opt/scrutiny/config
|
||||
environment:
|
||||
- SCRUTINY_WEB_INFLUXDB_HOST=influxdb
|
||||
- SCRUTINY_WEB_INFLUXDB_PORT=8086
|
||||
- SCRUTINY_WEB_INFLUXDB_TOKEN=your-very-secret-token
|
||||
- SCRUTINY_WEB_INFLUXDB_TOKEN=SUPER-SECRET-TOKEN
|
||||
- SCRUTINY_WEB_INFLUXDB_ORG=homelab
|
||||
- SCRUTINY_WEB_INFLUXDB_BUCKET=scrutiny
|
||||
# Optional but highly recommended to notify you in case of a problem
|
||||
- SCRUTINY_NOTIFY_URLS=["http://gotify:80/message?token=a-gotify-token"]
|
||||
# Optional but highly recommended to notify you in case of a problem; space-separated list of shoutrrr uri's
|
||||
# https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_NOTIFICATIONS.md
|
||||
- SCRUTINY_NOTIFY_URLS=http://gotify:80/message?token=a-gotify-token ntfy://username:password@host:port/topic
|
||||
- TZ=Europe/Stockholm
|
||||
depends_on:
|
||||
- influxdb
|
||||
influxdb:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- notifications
|
||||
- monitoring
|
||||
@@ -164,8 +164,6 @@ Also all drives that you wish to monitor need to be presented to the container u
|
||||
The image handles the periodic scanning of the drives.
|
||||
|
||||
```yaml
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
|
||||
collector:
|
||||
|
||||
+315
-13
@@ -10,9 +10,9 @@ Scrutiny is made up of three components: an influxdb Database, a collector and a
|
||||
|
||||
## InfluxDB
|
||||
|
||||
Please follow the official InfluxDB installation guide. Note, you'll need to install v2.2.0+.
|
||||
Please follow the official InfluxDB installation guide. Note, you'll need to install v2.8.0+.
|
||||
|
||||
https://docs.influxdata.com/influxdb/v2.2/install/
|
||||
https://docs.influxdata.com/influxdb/v2/install/
|
||||
|
||||
## Webapp/API
|
||||
|
||||
@@ -122,6 +122,11 @@ So you'll need to install the v7+ version using one of the following commands:
|
||||
- `dnf install smartmontools`
|
||||
- **FreeBSD:** `pkg install smartmontools`
|
||||
|
||||
The following additional dependencies are needed if you want to run the collector as an unprivileged user:
|
||||
|
||||
- systemd version > 235
|
||||
- a restricted user account
|
||||
|
||||
### Directory Structure
|
||||
|
||||
Now let's create a directory structure to contain the Scrutiny collector binary.
|
||||
@@ -133,40 +138,337 @@ mkdir -p /opt/scrutiny/bin
|
||||
|
||||
### Download Files
|
||||
|
||||
Next, we'll download the Scrutiny collector binary from the [latest Github release](https://github.com/analogj/scrutiny/releases).
|
||||
The file you need to download is named:
|
||||
Next, we'll download the Scrutiny collector binary from the [latest Github release](https://github.com/analogj/scrutiny/releases). You are looking for the one titled **scrutiny-collector-metrics-linux-amd64** unless you know you are on arm.
|
||||
|
||||
- **scrutiny-collector-metrics-linux-amd64** - save this file to `/opt/scrutiny/bin`
|
||||
```sh
|
||||
wget -O /tmp/scrutiny-collector-metrics https://github.com/AnalogJ/scrutiny/releases/latest/download/scrutiny-collector-metrics-linux-amd64
|
||||
```
|
||||
|
||||
Optional, but recommended: Before continuing it's recommended you compare the sha from the release page with the downloaded file to ensure it's the same file and not corrupted/tampered with. The command to do this is:
|
||||
|
||||
`echo "SHA_GOES_HERE /tmp/scrutiny-collector-metrics" | sha256sum -c`
|
||||
|
||||
example for the v0.8.6 release:
|
||||
|
||||
`echo "4c163645ce24e5487f4684a25ec73485d77a82a57f084808ff5aad0c11499ad2 /tmp/scrutiny-collector-metrics" | sha256sum -c`
|
||||
|
||||
followed by:
|
||||
|
||||
`sudo mv /tmp/scrutiny-collector-metrics /opt/scrutiny/bin/`
|
||||
|
||||
to move the binary to its final resting place
|
||||
|
||||
|
||||
### Prepare Scrutiny
|
||||
|
||||
Now that we have downloaded the required files, let's prepare the filesystem.
|
||||
|
||||
```
|
||||
```sh
|
||||
# Let's make sure the Scrutiny collector is executable.
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics
|
||||
```
|
||||
|
||||
if you are using SELinux, you may need to also do the following:
|
||||
|
||||
```sh
|
||||
# tell SELinux to allow these binaries
|
||||
sudo semanage fcontext -a -t bin_t "/opt/scrutiny/bin(/.*)?"
|
||||
# update labels
|
||||
sudo restorecon -Rv /opt/scrutiny/bin
|
||||
```
|
||||
|
||||
|
||||
### Start Scrutiny Collector, Populate Webapp
|
||||
|
||||
Next, we will manually trigger the collector, to populate the Scrutiny dashboard:
|
||||
|
||||
> NOTE: if you need to pass a config file to the scrutiny collector, you can provide it using the `--config` flag.
|
||||
|
||||
```
|
||||
/opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 run --api-endpoint "http://localhost:8080"
|
||||
```sh
|
||||
/opt/scrutiny/bin/scrutiny-collector-metrics run --api-endpoint "http://localhost:8080"
|
||||
```
|
||||
|
||||
### Schedule Collector with Cron
|
||||
### Schedule Collector with (root) Cron
|
||||
|
||||
Finally you need to schedule the collector to run periodically.
|
||||
This may be different depending on your OS/environment, but it may look something like this:
|
||||
|
||||
```
|
||||
```sh
|
||||
# open crontab
|
||||
crontab -e
|
||||
sudo crontab -e
|
||||
|
||||
# add a line for Scrutiny
|
||||
*/15 * * * * . /etc/profile; /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 run --api-endpoint "http://localhost:8080"
|
||||
*/15 * * * * . /etc/profile; /opt/scrutiny/bin/scrutiny-collector-metrics run --api-endpoint "http://localhost:8080"
|
||||
```
|
||||
|
||||
### Schedule Collector with Systemd (rootless)
|
||||
|
||||
Alternatively you can run `scrutiny-collector-metrics` as non-root so long as the relevant capabilities and permissions are granted.
|
||||
|
||||
|
||||
#### Creating a Restricted Service Account
|
||||
|
||||
This is the account that will run `scrutiny-collector-metrics`. Note this isn't strictly needed for all setups, but is useful from a logging/auditing perspective.
|
||||
|
||||
- Debian-based distros:
|
||||
- `sudo adduser --system scrutiny-svc --group --home /opt/scrutiny-svc`
|
||||
- RHEL-based distros:
|
||||
- `sudo useradd --system --home-dir /opt/scrutiny-svc --shell /sbin/nologin scrutiny-svc`
|
||||
|
||||
Next, add the user to the `disk` group:
|
||||
|
||||
```sh
|
||||
sudo usermod -aG disk scrutiny-svc
|
||||
```
|
||||
|
||||
|
||||
#### Creating a Restricted Systemd Service using AmbientCapabilities (easier)
|
||||
|
||||
This is the simpler setup, which allows you to run scrutiny rootless, but depending on what you want, may require granting more permissions to scrutiny than you would like to.
|
||||
|
||||
1. go to `/etc/systemd/system`
|
||||
2. create scrutiny-collector.service with the following contents:
|
||||
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Daily Restricted Scrutiny Collector
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
[Unit]
|
||||
Description=Daily Restricted Scrutiny Collector
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=scrutiny-svc
|
||||
Group=disk
|
||||
ExecStart=/opt/scrutiny/bin/scrutiny-collector-metrics run --api-endpoint "http://localhost:8080"
|
||||
|
||||
# --- PRIVILEGE LOCKDOWN ---
|
||||
## CAP_SYS_RAWIO is needed for SATA drives
|
||||
AmbientCapabilities=CAP_SYS_RAWIO
|
||||
CapabilityBoundingSet=CAP_SYS_RAWIO
|
||||
## unfortunately nvme drives require CAP_SYS_ADMIN
|
||||
## if you want nvme drives you must do the following:
|
||||
#AmbientCapabilities=CAP_SYS_RAWIO CAP_SYS_ADMIN
|
||||
#CapabilityBoundingSet=
|
||||
|
||||
NoNewPrivileges=yes
|
||||
|
||||
# Security/sandboxing settings
|
||||
KeyringMode=private
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateDevices=no
|
||||
## you can restrict devices using:
|
||||
#DevicePolicy=closed
|
||||
#DeviceAllow=/dev/sda r
|
||||
#DeviceAllow=/dev/nvme0 r
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectClock=yes
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
RemoveIPC=yes
|
||||
RestrictSUIDSGID=true
|
||||
|
||||
|
||||
# --- NETWORK LOCKDOWN
|
||||
## use these to restrict what scrutiny can talk to over the network
|
||||
## if using a hub on a different host you will need to change the values accordingly
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
IPAddressDeny=any
|
||||
IPAddressAllow=localhost
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
```
|
||||
|
||||
Additionally, for nvme drives you may need to create a udev rule on many systems, as /dev/nvme* is often owned only by root:
|
||||
|
||||
##### add udev rule `/etc/udev/rules.d/99-nvme.rules` with contents:
|
||||
|
||||
```
|
||||
KERNEL=="nvme[0-9]*", GROUP="disk", MODE="0640"
|
||||
```
|
||||
|
||||
then run the following commands to load the udev rule:
|
||||
|
||||
```sh
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --subsystem-match=nvme --action=add
|
||||
```
|
||||
|
||||
|
||||
##### Pros:
|
||||
|
||||
- easy to maintain
|
||||
- much better than running as root (especially if you don't need nvme drives)
|
||||
- there are no privilege escalations needed
|
||||
|
||||
|
||||
##### Cons:
|
||||
|
||||
NOTE: These cons basically only apply if a major supply-chain attack happens against scrutiny, and reflect a worst-case scenario that is unlikely to ever occur:
|
||||
|
||||
- CAP_SYS_RAWIO allows for data exfiltration/modification from SATA drives (ssh keys, /etc/shadow, etc)
|
||||
- CAP_SYS_ADMIN would theoretically allow for significant system compromise
|
||||
- nvme drives requires a udev rule for reliable access
|
||||
|
||||
|
||||
If you are happy with that, you can jump to [Create a Systemd Timer to run scrutiny-collector.service](#create-a-systemd-timer-to-run-scrutiny-collectorservice)
|
||||
|
||||
|
||||
#### Creating a Restricted Systemd Service using sudo and Shim Script
|
||||
|
||||
If granting scrutiny `CAP_SYS_RAWIO` and/or `CAP_SYS_ADMIN` exceeds your risk appetite, you have another option, though one more complicated and with its own set of pros/cons
|
||||
|
||||
1. run `sudo mkdir -p /opt/smartctl-shim/bin`
|
||||
2. edit `/opt/smartctl-shim/bin/smartctl` with the following content:
|
||||
|
||||
```sh
|
||||
#!/bin/bash
|
||||
# Shim for accounts to use smartctl without being root
|
||||
# for automation requires the account be in sudoers
|
||||
exec /usr/bin/sudo /usr/sbin/smartctl "$@"
|
||||
```
|
||||
|
||||
3. create a new `scrutiny-collector` file in `/etc/sudoers.d/`
|
||||
4. inside `/etc/sudoers.d/scrutiny-collector` add the following:
|
||||
|
||||
```sh
|
||||
scrutiny-svc ALL=(root) NOPASSWD: /usr/sbin/smartctl *
|
||||
```
|
||||
|
||||
5. go to `/etc/systemd/system`
|
||||
6. create scrutiny-collector.service with the following contents:
|
||||
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Daily Restricted Scrutiny Collector
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=scrutiny-svc
|
||||
Environment="PATH=/opt/smartctl-shim/bin:/usr/bin:/bin"
|
||||
ExecStart=/opt/scrutiny/bin/scrutiny-collector-metrics run --api-endpoint "http://localhost:8080"
|
||||
|
||||
# --- PRIVILEGE LOCKDOWN ---
|
||||
## we use sudo to elevate privileges for smartctl only, so no Ambient Capabilities are needed
|
||||
AmbientCapabilities=
|
||||
## CAP_SYS_RAWIO is needed for SATA drives
|
||||
CapabilityBoundingSet=CAP_SETUID CAP_SETGID CAP_AUDIT_WRITE CAP_SYS_RAWIO CAP_SYS_RESOURCE
|
||||
## unfortunately nvme drives require CAP_SYS_ADMIN
|
||||
## if you want nvme drives you must do the following:
|
||||
# CapabilityBoundingSet=CAP_SETUID CAP_SETGID CAP_AUDIT_WRITE CAP_SYS_RAWIO CAP_SYS_ADMIN CAP_SYS_RESOURCE
|
||||
|
||||
## since sudo needs to be used to elevate permissions in this setup, we need to allow new privileges
|
||||
NoNewPrivileges=no
|
||||
|
||||
# Security/sandboxing settings
|
||||
KeyringMode=private
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateDevices=no
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectClock=yes
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
RemoveIPC=yes
|
||||
RestrictSUIDSGID=true
|
||||
|
||||
|
||||
# --- NETWORK LOCKDOWN
|
||||
## use these to restrict what scrutiny can talk to over the network
|
||||
## if using a hub on a different host you will need to change the values accordingly
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
IPAddressDeny=any
|
||||
IPAddressAllow=localhost
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
|
||||
##### Pros:
|
||||
|
||||
- the scrutiny binary itself will not have permissions like CAP_SYS_ADMIN
|
||||
- much better than running as root (especially if you don't need nvme drives)
|
||||
- `sudo` restricts privilege escalation to just `smartctl`
|
||||
- no udev rule needed
|
||||
|
||||
|
||||
##### Cons:
|
||||
|
||||
NOTE: These cons basically only apply if a major supply-chain attack happens against scrutiny, and reflect a worst-case scenario that is unlikely to ever occur:
|
||||
|
||||
- Any sort of privilege escalation attack in sudo could theoretically allow a compromised scrutiny to gain additional privileges, since the process has permission to escelate privileges in general
|
||||
- Even though sudo only allows `smartctl`, it still has `CAP_SYS_RAWIO` and `CAP_SYS_ADMIN` so in theory the same attacks from the first method are possible, though now only with an exploit using smartctl instead of scrutiny directly
|
||||
- even though you don't need a udev rule, this adds a lot of additional administrative overhead
|
||||
- while the scrutiny binary itself isn't elevated, it has a sub-process that is running as root (systemctl)
|
||||
|
||||
#### Create a Systemd Timer to run scrutiny-collector.service
|
||||
|
||||
First, lets test our service. It doesn't matter which method you used above, as either way you need to load and run it.
|
||||
|
||||
```sh
|
||||
# reload changes for systemd services
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# enable the service
|
||||
sudo systemctl enable scrutiny-collector.service
|
||||
|
||||
# now run the service
|
||||
sudo systemctl start scrutiny-collector.service
|
||||
```
|
||||
|
||||
You should see the data in your hub instance of scrutiny now. If your run into issues I recommend turning on debug logging for scrutiny and checking your system logs using journalctl. It may be a permission is missing or wrong.
|
||||
|
||||
Now that things have been validated, lets create the systemd timer to run the service for us on a schedule:
|
||||
|
||||
1. if you are not still there, go to `/etc/systemd/system`
|
||||
2. create scrutiny-collector.timer with the following contents:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Run Scruitiny Collector daily at 2am
|
||||
|
||||
[Timer]
|
||||
# Standard calendar trigger
|
||||
OnCalendar=*-*-* 02:00:00
|
||||
# Ensures the job runs if the computer was off at 2am
|
||||
Persistent=true
|
||||
# Minimizes I/O spikes by staggering start time
|
||||
RandomizedDelaySec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
||||
```
|
||||
|
||||
Update the schedule as you see fit for your needs
|
||||
|
||||
Once you are satisfied with our timer, you'll need to load and enable it:
|
||||
|
||||
```sh
|
||||
# reload changes for systemd services
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# now enable the timer
|
||||
sudo systemctl enable --now scrutiny-collector.timer
|
||||
```
|
||||
|
||||
That's it! you're done. You can check the status of the timer using `sudo systemctl status scrutiny-collector.timer
|
||||
`
|
||||
@@ -0,0 +1,170 @@
|
||||
# Rootless Podman Quadlet Install
|
||||
|
||||
Note: These instructions are written with Podman 4.9 in mind, as that's what's available on Ubuntu 24.04. Podman 5+ can simplify the process using a .pod file to run both the hub and influxdb instance in the same pod, sharing localhost. This is a fairly trivial change should anyone want to add the documentation for it. While this document isn't Ubuntu-specific, this is being purposefully done to allow it to apply to the vast majority of Podman users, regardless of what Linux distro they use.
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Podman > 4.9
|
||||
- Systemd > 250 (for quadlet support)
|
||||
- a restricted service account
|
||||
|
||||
|
||||
### Creating a Service Account
|
||||
|
||||
See [Creating a Restricted Service Account](INSTALL_MANUAL.md#creating-a-restricted-service-account) for instructions.
|
||||
|
||||
While you do not need to use the same account as the collector, this guide will assume you will be for all its examples.
|
||||
|
||||
In addition to those steps, you will need to create sub ids and enable lingering for the user:
|
||||
|
||||
```sh
|
||||
# add sub-uids and sub-gids, you may need to adjust numbers if you have other rootless quadlets running for other users already
|
||||
# it is not recommended to go below 100000
|
||||
# we choose to start at 500000 in the event you have some other podman accounts
|
||||
sudo usermod --add-subuids 500000-565535 scrutiny-svc
|
||||
sudo usermod --add-subgids 500000-565535 scrutiny-svc
|
||||
|
||||
# We want the quadlets to stay running even if the user isn't logged in
|
||||
sudo loginctl enable-linger scrutiny-svc
|
||||
```
|
||||
|
||||
|
||||
### Directory Structure
|
||||
|
||||
Once the account is created, you will need to grab its id to create a few drectories for the data files and rootless quadlet files:
|
||||
|
||||
```sh
|
||||
# create folders for config and influxdb
|
||||
sudo mkdir -p /opt/scrutiny-svc/scrutiny/{config,influxdb}
|
||||
|
||||
# get the config file for scrutiny hub
|
||||
sudo wget -O /opt/scrutiny-svc/scrutiny/config/scrutiny.yaml https://raw.githubusercontent.com/AnalogJ/scrutiny/refs/heads/master/example.scrutiny.yaml
|
||||
|
||||
# set permissions on everything
|
||||
sudo chown -R scrutiny-svc:scrutiny-svc /opt/scrutiny-svc
|
||||
|
||||
# Get the ID of scrutiny-svc so you know it for your own record-keeping
|
||||
id -u scrutiny-svc
|
||||
|
||||
# create a directory
|
||||
sudo mkdir -p /etc/containers/systemd/users/$(id -u scrutiny-svc)
|
||||
|
||||
## go into the directory you just created for the rest of the guide
|
||||
cd /etc/containers/systemd/users/$(id -u scrutiny-svc)
|
||||
```
|
||||
|
||||
|
||||
### Quadlet Files
|
||||
|
||||
Now that everything is set up and configured for the account to run quadlets, we just need to create a few quadlet files.
|
||||
|
||||
All remaining system actions will take place in `/etc/containers/systemd/users/$(id -u scrutiny-svc)` which is why we had you cd into it.
|
||||
|
||||
|
||||
#### Networking
|
||||
|
||||
We need the hub and influxdb instances to be able to talk to each other, and in the case of Podman 4.9, they will run separately not sharing a localhost, and as such we need to configure a network for them to share. The file is pretty simple:
|
||||
|
||||
|
||||
##### scrutiny-net.network
|
||||
|
||||
```ini
|
||||
[Network]
|
||||
NetworkName=scrutiny-net
|
||||
```
|
||||
|
||||
|
||||
#### Containers
|
||||
|
||||
Now we're ready for creating the containers
|
||||
|
||||
|
||||
##### influxdb.container
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=influxdb
|
||||
|
||||
[Container]
|
||||
ContainerName=influxdb
|
||||
Image=docker.io/library/influxdb:2.8
|
||||
AutoUpdate=registry
|
||||
Timezone=local
|
||||
## not strictly necessary, but keeps file permission sane for influxdb
|
||||
PodmanArgs=--group-add keep-groups
|
||||
## versions of podman after 5.1 should do the below instead
|
||||
#GroupAdd=keep-groups
|
||||
Volume=/opt/scrutiny-svc/scrutiny/influxdb:/var/lib/influxdb2:Z
|
||||
Network=scrutiny-net
|
||||
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
# Start by default on boot
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
|
||||
##### scrutiny-web.container
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=scrutiny-web
|
||||
After=influxdb.service
|
||||
Requires=influxdb.service
|
||||
|
||||
[Container]
|
||||
ContainerName=scrutiny-web
|
||||
Image=ghcr.io/analogj/scrutiny:latest-web
|
||||
AutoUpdate=registry
|
||||
Timezone=local
|
||||
Volume=/opt/scrutiny-svc/scrutiny/config:/opt/scrutiny/config:Z
|
||||
Network=scrutiny-net
|
||||
PublishPort=8080:8080/tcp
|
||||
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
# Start by default on boot
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
#### Update scrutiny config
|
||||
|
||||
Since our containers are running separately, we need to update `/opt/scrutiny-svc/scrutiny/config/scrutiny.yaml` to the new influxdb host:
|
||||
|
||||
1. edit `/opt/scrutiny-svc/scrutiny/config/scrutiny.yaml`
|
||||
2. under `influxdb` section, change `host: 0.0.0.0` to `host: influxdb` -- remember that yaml is whitespace-sensitive! so be mindful of the indents
|
||||
|
||||
```yaml
|
||||
influxdb:
|
||||
# scheme: 'http'
|
||||
host: influxdb
|
||||
port: 8086
|
||||
```
|
||||
|
||||
# Running the hub and doing the
|
||||
|
||||
With that done, we're now ready to start up the services:
|
||||
|
||||
```sh
|
||||
# reload all the systemd user files for scrutiny-svc
|
||||
sudo systemctl --user -M scrutiny-svc@ daemon-reload
|
||||
|
||||
# start the scrutiny-net network:
|
||||
sudo systemctl --user -M scrutiny-svc@ start scrutiny-net-network.service
|
||||
|
||||
# start influxdb first and wait for it to come up
|
||||
sudo systemctl --user -M scrutiny-svc@ start influxdb.service
|
||||
|
||||
# check if it's fully up
|
||||
sudo systemctl --user -M scrutiny-svc@ status influxdb.service
|
||||
|
||||
# now start scrutiny
|
||||
sudo systemctl --user -M scrutiny-svc@ start scrutiny-web.service
|
||||
```
|
||||
|
||||
You are now ready to run the collector, if you would like to run that rootless as well, see the guide at [Schedule Collector with Systemd (rootless)](INSTALL_MANUAL.md#schedule-collector-with-systemd-rootless)
|
||||
@@ -41,14 +41,14 @@ The growth rate is pretty unintuitive -- see https://github.com/AnalogJ/scrutiny
|
||||
|
||||
InfluxDB is a required dependency for Scrutiny v0.4.0+.
|
||||
|
||||
https://docs.influxdata.com/influxdb/v2.2/install/
|
||||
https://docs.influxdata.com/influxdb/v2/install/
|
||||
|
||||
## Persistence
|
||||
|
||||
To ensure that all data is correctly stored, you must also persist the InfluxDB database directory
|
||||
|
||||
- If you're using the Official Scrutiny Omnibus image (`ghcr.io/analogj/scrutiny:master-omnibus`), the path is `/opt/scrutiny/influxdb`
|
||||
- If you're deploying in Hub/Spoke mode with the InfluxDB maintained image (`influxdb:2.2`), the path is `/var/lib/influxdb2`
|
||||
- If you're deploying in Hub/Spoke mode with the InfluxDB maintained image (`influxdb:2.8`), the path is `/var/lib/influxdb2`
|
||||
|
||||
If you attempt to restart Scrutiny but you forgot to persist the InfluxDB directory, you will get an error message like follows:
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
As documented in [example.scrutiny.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml#L59-L75)
|
||||
there are multiple ways to configure notifications for Scrutiny.
|
||||
|
||||
Under the hood we use a library called [Shoutrrr](https://github.com/containrrr/shoutrrr) to send our notifications, and you should use their documentation if you run into
|
||||
any issues: https://containrrr.dev/shoutrrr/services/overview/
|
||||
Under the hood we use a library called [Shoutrrr](https://github.com/nicholas-fedor/shoutrrr) to send our notifications, and you should use their documentation if you run into
|
||||
any issues: https://shoutrrr.nickfedor.com/services/overview/
|
||||
|
||||
|
||||
# Script Notifications
|
||||
|
||||
+24
-39
@@ -59,7 +59,7 @@ log:
|
||||
|
||||
|
||||
# Notification "urls" look like the following. For more information about service specific configuration see
|
||||
# Shoutrrr's documentation: https://containrrr.dev/shoutrrr/services/overview/
|
||||
# Shoutrrr's documentation: https://shoutrrr.nickfedor.com/services/overview/
|
||||
#
|
||||
# note, usernames and passwords containing special characters will need to be urlencoded.
|
||||
# if your username is: "myname@example.com" and your password is "124@34$1"
|
||||
@@ -67,41 +67,26 @@ log:
|
||||
|
||||
#notify:
|
||||
# urls:
|
||||
# - "discord://token@webhookid"
|
||||
# - "telegram://token@telegram?channels=channel-1[,channel-2,...]"
|
||||
# - "pushover://shoutrrr:apiToken@userKey/?priority=1&devices=device1[,device2, ...]"
|
||||
# - "slack://[botname@]token-a/token-b/token-c"
|
||||
# - "smtp://username:password@host:port/?fromAddress=fromAddress&toAddresses=recipient1[,recipient2,...]"
|
||||
# - "teams://token-a/token-b/token-c"
|
||||
# - "gotify://gotify-host/token"
|
||||
# - "pushbullet://api-token[/device/#channel/email]"
|
||||
# - "ifttt://key/?events=event1[,event2,...]&value1=value1&value2=value2&value3=value3"
|
||||
# - "mattermost://[username@]mattermost-host/token[/channel]"
|
||||
# - "ntfy://username:password@host:port/topic"
|
||||
# - "hangouts://chat.googleapis.com/v1/spaces/FOO/messages?key=bar&token=baz"
|
||||
# - "zulip://bot-mail:bot-key@zulip-domain/?stream=name-or-id&topic=name"
|
||||
# - "join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]"
|
||||
# - "script:///file/path/on/disk"
|
||||
# - "https://www.example.com/path"
|
||||
|
||||
########################################################################################################################
|
||||
# FEATURES COMING SOON
|
||||
#
|
||||
# The following commented out sections are a preview of additional configuration options that will be available soon.
|
||||
#
|
||||
########################################################################################################################
|
||||
|
||||
#limits:
|
||||
# ata:
|
||||
# critical:
|
||||
# error: 10
|
||||
# standard:
|
||||
# error: 20
|
||||
# warn: 10
|
||||
# scsi:
|
||||
# critical: true
|
||||
# standard: true
|
||||
# nvme:
|
||||
# critical: true
|
||||
# standard: true
|
||||
|
||||
# - discord://token@id[?thread_id=threadid]
|
||||
# - googlechat://chat.googleapis.com/v1/spaces/FOO/messages?key=bar&token=baz
|
||||
# - hangouts://chat.googleapis.com/v1/spaces/FOO/messages?key=bar&token=baz
|
||||
# - lark://host/token?secret=secret&title=title&link=url
|
||||
# - matrix://username:password@host:port/[?rooms=!roomID1[,roomAlias2]]
|
||||
# - mattermost://[username@]mattermost-host/token[/channel]
|
||||
# - rocketchat://[username@]rocketchat-host/token[/channel|@recipient]
|
||||
# - signal://[user[:password]@]host[:port]/source_phone/recipient1[,recipient2,...]
|
||||
# - slack://[botname@]token-a/token-b/token-c
|
||||
# - teams://group@tenant/altId/groupOwner?host=organization.webhook.office.com
|
||||
# - telegram://token@telegram?chats=@channel-1[,chat-id-1,chat-id-2:message-thread-id,...]
|
||||
# - wecom://key
|
||||
# - zulip://bot-mail:bot-key@zulip-domain/?stream=name-or-id&topic=name
|
||||
# - bark://devicekey@host
|
||||
# - gotify://gotify-host/token
|
||||
# - ifttt://key/?events=event1[,event2,...]&value1=value1&value2=value2&value3=value3
|
||||
# - join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]
|
||||
# - ntfy://username:password@ntfy.sh/topic
|
||||
# - pushbullet://api-token[/device/#channel/email]
|
||||
# - pushover://shoutrrr:apiToken@userKey/?devices=device1[,device2, ...]
|
||||
# - opsgenie://host/token?responders=responder1[,responder2]
|
||||
# - pagerduty://[host[:port]]/integration-key[?query-parameters]
|
||||
# - smtp://username:password@host:port/?fromaddress=fromAddress&toaddresses=recipient1[,recipient2,...][&additional_params]
|
||||
|
||||
@@ -1,81 +1,92 @@
|
||||
module github.com/analogj/scrutiny
|
||||
|
||||
go 1.20
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/glebarez/sqlite v1.4.5
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.0.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.9.0
|
||||
github.com/jaypipes/ghw v0.6.1
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/samber/lo v1.25.0
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
golang.org/x/sync v0.1.0
|
||||
gorm.io/gorm v1.23.5
|
||||
github.com/analogj/go-util v0.0.0-20210417161720-39b497cca03b
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.14.0
|
||||
github.com/jaypipes/ghw v0.21.2
|
||||
github.com/nicholas-fedor/shoutrrr v0.13.2
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v2 v2.27.7
|
||||
go.uber.org/mock v0.6.0
|
||||
golang.org/x/sync v0.19.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.8.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.17.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||
github.com/go-playground/locales v0.13.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.2.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
||||
github.com/jaypipes/pcidb v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // 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.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
|
||||
github.com/jaypipes/pcidb v1.1.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.4 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
|
||||
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 // indirect
|
||||
github.com/leodido/go-urn v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kvz/logstreamer v0.0.0-20221024075423-bf5cfbd32e39 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/oapi-codegen/runtime v1.1.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
|
||||
modernc.org/libc v1.16.8 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.1.1 // indirect
|
||||
modernc.org/sqlite v1.17.2 // indirect
|
||||
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect
|
||||
modernc.org/libc v1.67.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.44.3 // indirect
|
||||
)
|
||||
|
||||
@@ -3,15 +3,16 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
utils "github.com/analogj/go-util/utils"
|
||||
"github.com/fatih/color"
|
||||
@@ -36,8 +37,8 @@ func main() {
|
||||
}
|
||||
|
||||
//we're going to load the config file manually, since we need to validate it.
|
||||
err = config.ReadConfig(configFilePath) // Find and read the config file
|
||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||
err = config.ReadConfig(configFilePath) // Find and read the config file
|
||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||
//ignore "could not find config file"
|
||||
} else if err != nil {
|
||||
log.Print(color.HiRedString("CONFIG ERROR: %v", err))
|
||||
@@ -81,7 +82,7 @@ OPTIONS:
|
||||
|
||||
subtitle := scrutiny + utils.LeftPad2Len(versionInfo, " ", 65-len(scrutiny))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
@@ -89,7 +90,7 @@ OPTIONS:
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
`), subtitle)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
reflect "reflect"
|
||||
|
||||
config "github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
viper "github.com/spf13/viper"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface.
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
models "github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
collector "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
measurements "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockDeviceRepo is a mock of DeviceRepo interface.
|
||||
|
||||
@@ -5,6 +5,11 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/glebarez/sqlite"
|
||||
@@ -13,10 +18,6 @@ import (
|
||||
"github.com/influxdata/influxdb-client-go/v2/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,6 +30,7 @@ const (
|
||||
// 60seconds * 60minutes * 24hours * 7 days * (52 + 52 + 4)weeks
|
||||
RETENTION_PERIOD_25_MONTHS_IN_SECONDS = 65_318_400
|
||||
|
||||
DURATION_KEY_DAY = "day"
|
||||
DURATION_KEY_WEEK = "week"
|
||||
DURATION_KEY_MONTH = "month"
|
||||
DURATION_KEY_YEAR = "year"
|
||||
@@ -82,7 +84,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to connect to database! - %v", err)
|
||||
return nil, fmt.Errorf("failed to connect to database! - %v", err)
|
||||
}
|
||||
globalLogger.Infof("Successfully connected to scrutiny sqlite db: %s\n", appConfig.GetString("web.database.location"))
|
||||
|
||||
@@ -146,7 +148,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
||||
taskAPI := client.TasksAPI()
|
||||
|
||||
if writeAPI == nil || queryAPI == nil || taskAPI == nil {
|
||||
return nil, fmt.Errorf("Failed to connect to influxdb!")
|
||||
return nil, fmt.Errorf("failed to connect to influxdb")
|
||||
}
|
||||
|
||||
deviceRepo := scrutinyRepository{
|
||||
@@ -238,13 +240,13 @@ func InfluxSetupComplete(influxEndpoint string, tlsConfig *tls.Config) (bool, er
|
||||
return false, err
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
res, err := client.Get(influxUri.String())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -445,6 +447,7 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|
||||
|
||||
func (sr *scrutinyRepository) lookupBucketName(durationKey string) string {
|
||||
switch durationKey {
|
||||
case DURATION_KEY_DAY:
|
||||
case DURATION_KEY_WEEK:
|
||||
//data stored in the last week
|
||||
return sr.appConfig.GetString("web.influxdb.bucket")
|
||||
@@ -462,8 +465,10 @@ func (sr *scrutinyRepository) lookupBucketName(durationKey string) string {
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) lookupDuration(durationKey string) []string {
|
||||
|
||||
switch durationKey {
|
||||
case DURATION_KEY_DAY:
|
||||
//data stored in the last day
|
||||
return []string{"-1d", "now()"}
|
||||
case DURATION_KEY_WEEK:
|
||||
//data stored in the last week
|
||||
return []string{"-1w", "now()"}
|
||||
@@ -480,8 +485,22 @@ func (sr *scrutinyRepository) lookupDuration(durationKey string) []string {
|
||||
return []string{"-1w", "now()"}
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) lookupResolution(durationKey string) string {
|
||||
switch durationKey {
|
||||
case DURATION_KEY_DAY:
|
||||
// Return data with higher resolution for daily summaries
|
||||
return "10m"
|
||||
default:
|
||||
// Return data with 1h resolution for other summaries
|
||||
return "1h"
|
||||
}
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) lookupNestedDurationKeys(durationKey string) []string {
|
||||
switch durationKey {
|
||||
case DURATION_KEY_DAY:
|
||||
//all data is stored in a single bucket, but we want a finer resolution
|
||||
return []string{DURATION_KEY_DAY}
|
||||
case DURATION_KEY_WEEK:
|
||||
//all data is stored in a single bucket
|
||||
return []string{DURATION_KEY_WEEK}
|
||||
|
||||
@@ -3,11 +3,12 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -31,7 +32,7 @@ func (sr *scrutinyRepository) GetDevices(ctx context.Context) ([]models.Device,
|
||||
//Get a list of all the active devices.
|
||||
devices := []models.Device{}
|
||||
if err := sr.gormClient.WithContext(ctx).Find(&devices).Error; err != nil {
|
||||
return nil, fmt.Errorf("Could not get device summary from DB: %v", err)
|
||||
return nil, fmt.Errorf("could not get device summary from DB: %v", err)
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
@@ -40,7 +41,7 @@ func (sr *scrutinyRepository) GetDevices(ctx context.Context) ([]models.Device,
|
||||
func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) {
|
||||
var device models.Device
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
return device, fmt.Errorf("Could not get device from DB: %v", err)
|
||||
return device, fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
//TODO catch GormClient err
|
||||
@@ -55,7 +56,7 @@ func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, coll
|
||||
func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
|
||||
var device models.Device
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
return device, fmt.Errorf("Could not get device from DB: %v", err)
|
||||
return device, fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
device.DeviceStatus = pkg.DeviceStatusSet(device.DeviceStatus, status)
|
||||
@@ -78,7 +79,7 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string)
|
||||
func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error {
|
||||
var device models.Device
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
return fmt.Errorf("Could not get device from DB: %v", err)
|
||||
return fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
return sr.gormClient.Model(&device).Where("wwn = ?", wwn).Update("archived", archived).Error
|
||||
|
||||
@@ -177,7 +177,7 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
|
||||
`|> sort(columns: ["_time"], desc: true)`,
|
||||
}...)
|
||||
if selectEntries > 0 {
|
||||
partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, selectEntries, selectEntriesOffset))
|
||||
partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> limit(n: %d, offset: %d)`, selectEntries, selectEntriesOffset))
|
||||
}
|
||||
partialQueryStr = append(partialQueryStr, `|> yield(name: "last")`)
|
||||
|
||||
@@ -196,9 +196,11 @@ func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durati
|
||||
}
|
||||
|
||||
partialQueryStr = append(partialQueryStr, `|> aggregateWindow(every: 1d, fn: last, createEmpty: false)`)
|
||||
|
||||
|
||||
// ensure we are selecting the latest entries when paging
|
||||
partialQueryStr = append(partialQueryStr, `|> sort(columns: ["_time"], desc: true)`)
|
||||
if selectEntries > 0 {
|
||||
partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, selectEntries, selectEntriesOffset))
|
||||
partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> limit(n: %d, offset: %d)`, selectEntries, selectEntriesOffset))
|
||||
}
|
||||
partialQueryStr = append(partialQueryStr, "|> schema.fieldsAsCols()")
|
||||
|
||||
|
||||
@@ -647,7 +647,7 @@ func m20201107210306_FromPreInfluxDBSmartResultsCreatePostInfluxDBSmartResults(d
|
||||
}
|
||||
postDeviceSmartData.ProcessScsiSmartInfo(postScsiGrownDefectList, postScsiErrorCounterLog)
|
||||
} else {
|
||||
return fmt.Errorf("Unknown device protocol: %s", preDevice.DeviceProtocol), postDeviceSmartData
|
||||
return fmt.Errorf("unknown device protocol: %s", preDevice.DeviceProtocol), postDeviceSmartData
|
||||
}
|
||||
|
||||
return nil, postDeviceSmartData
|
||||
|
||||
@@ -3,17 +3,18 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"strings"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
)
|
||||
|
||||
// LoadSettings will retrieve settings from the database, store them in the AppConfig object, and return a Settings struct
|
||||
func (sr *scrutinyRepository) LoadSettings(ctx context.Context) (*models.Settings, error) {
|
||||
settingsEntries := []models.SettingEntry{}
|
||||
if err := sr.gormClient.WithContext(ctx).Find(&settingsEntries).Error; err != nil {
|
||||
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
return nil, fmt.Errorf("could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
// store retrieved settings in the AppConfig obj
|
||||
@@ -58,7 +59,7 @@ func (sr *scrutinyRepository) SaveSettings(ctx context.Context, settings models.
|
||||
//retrieve current settings from the database
|
||||
settingsEntries := []models.SettingEntry{}
|
||||
if err := sr.gormClient.WithContext(ctx).Find(&settingsEntries).Error; err != nil {
|
||||
return fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
return fmt.Errorf("could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
//update settingsEntries
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func Test_DownsampleScript_Weekly(t *testing.T) {
|
||||
@@ -12,7 +13,6 @@ func Test_DownsampleScript_Weekly(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
@@ -64,7 +64,6 @@ func Test_DownsampleScript_Monthly(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
@@ -116,7 +115,6 @@ func Test_DownsampleScript_Yearly(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
@@ -140,13 +140,14 @@ func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) string {
|
||||
for _, nestedDurationKey := range nestedDurationKeys {
|
||||
bucketName := sr.lookupBucketName(nestedDurationKey)
|
||||
durationRange := sr.lookupDuration(nestedDurationKey)
|
||||
durationResolution := sr.lookupResolution(nestedDurationKey)
|
||||
|
||||
subQueryNames = append(subQueryNames, fmt.Sprintf(`%sData`, nestedDurationKey))
|
||||
partialQueryStr = append(partialQueryStr, []string{
|
||||
fmt.Sprintf(`%sData = from(bucket: "%s")`, nestedDurationKey, bucketName),
|
||||
fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]),
|
||||
`|> filter(fn: (r) => r["_measurement"] == "temp" )`,
|
||||
`|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)`,
|
||||
fmt.Sprintf(`|> aggregateWindow(every: %s, fn: mean, createEmpty: false)`, durationResolution),
|
||||
`|> group(columns: ["device_wwn"])`,
|
||||
`|> toInt()`,
|
||||
"",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func Test_aggregateTempQuery_Week(t *testing.T) {
|
||||
@@ -12,7 +13,6 @@ func Test_aggregateTempQuery_Week(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
@@ -45,7 +45,6 @@ func Test_aggregateTempQuery_Month(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
@@ -86,7 +85,6 @@ func Test_aggregateTempQuery_Year(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
@@ -134,7 +132,6 @@ func Test_aggregateTempQuery_Forever(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
@@ -143,21 +143,21 @@ type SmartInfo struct {
|
||||
ErrorNumber int `json:"error_number"`
|
||||
LifetimeHours int `json:"lifetime_hours"`
|
||||
CompletionRegisters struct {
|
||||
Error int `json:"error"`
|
||||
Status int `json:"status"`
|
||||
Count int `json:"count"`
|
||||
Lba int `json:"lba"`
|
||||
Device int `json:"device"`
|
||||
Error int `json:"error"`
|
||||
Status int `json:"status"`
|
||||
Count int `json:"count"`
|
||||
Lba uint64 `json:"lba"`
|
||||
Device int `json:"device"`
|
||||
} `json:"completion_registers"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
PreviousCommands []struct {
|
||||
Registers struct {
|
||||
Command int `json:"command"`
|
||||
Features int `json:"features"`
|
||||
Count int `json:"count"`
|
||||
Lba int `json:"lba"`
|
||||
Device int `json:"device"`
|
||||
DeviceControl int `json:"device_control"`
|
||||
Command int `json:"command"`
|
||||
Features int `json:"features"`
|
||||
Count int `json:"count"`
|
||||
Lba uint64 `json:"lba"`
|
||||
Device int `json:"device"`
|
||||
DeviceControl int `json:"device_control"`
|
||||
} `json:"registers"`
|
||||
PowerupMilliseconds int `json:"powerup_milliseconds"`
|
||||
CommandName string `json:"command_name"`
|
||||
@@ -188,8 +188,8 @@ type SmartInfo struct {
|
||||
AtaSmartSelectiveSelfTestLog struct {
|
||||
Revision int `json:"revision"`
|
||||
Table []struct {
|
||||
LbaMin int `json:"lba_min"`
|
||||
LbaMax int `json:"lba_max"`
|
||||
LbaMin uint64 `json:"lba_min"`
|
||||
LbaMax uint64 `json:"lba_max"`
|
||||
Status struct {
|
||||
Value int `json:"value"`
|
||||
String string `json:"string"`
|
||||
|
||||
@@ -2,13 +2,14 @@ package measurements
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||
)
|
||||
|
||||
type Smart struct {
|
||||
@@ -102,7 +103,7 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
|
||||
} else if sm.DeviceProtocol == pkg.DeviceProtocolScsi {
|
||||
sm.Attributes[attributeId] = &SmartScsiAttribute{}
|
||||
} else {
|
||||
return nil, fmt.Errorf("Unknown Device Protocol: %s", sm.DeviceProtocol)
|
||||
return nil, fmt.Errorf("unknown Device Protocol: %s", sm.DeviceProtocol)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +117,7 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
|
||||
return &sm, nil
|
||||
}
|
||||
|
||||
//Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries)
|
||||
// Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries)
|
||||
func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) error {
|
||||
sm.DeviceWWN = wwn
|
||||
sm.Date = time.Unix(info.LocalTime.TimeT, 0)
|
||||
@@ -143,7 +144,7 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er
|
||||
return nil
|
||||
}
|
||||
|
||||
//generate SmartAtaAttribute entries from Scrutiny Collector Smart data.
|
||||
// generate SmartAtaAttribute entries from Scrutiny Collector Smart data.
|
||||
func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTableItem) {
|
||||
for _, collectorAttr := range tableItems {
|
||||
attrModel := SmartAtaAttribute{
|
||||
@@ -171,7 +172,7 @@ func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTa
|
||||
}
|
||||
}
|
||||
|
||||
//generate SmartNvmeAttribute entries from Scrutiny Collector Smart data.
|
||||
// generate SmartNvmeAttribute entries from Scrutiny Collector Smart data.
|
||||
func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.NvmeSmartHealthInformationLog) {
|
||||
|
||||
sm.Attributes = map[string]SmartAttribute{
|
||||
@@ -201,7 +202,7 @@ func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.Nv
|
||||
}
|
||||
}
|
||||
|
||||
//generate SmartScsiAttribute entries from Scrutiny Collector Smart data.
|
||||
// generate SmartScsiAttribute entries from Scrutiny Collector Smart data.
|
||||
func (sm *Smart) ProcessScsiSmartInfo(defectGrownList int64, scsiErrorCounterLog collector.ScsiErrorCounterLog) {
|
||||
sm.Attributes = map[string]SmartAttribute{
|
||||
"scsi_grown_defect_list": (&SmartScsiAttribute{AttributeId: "scsi_grown_defect_list", Value: defectGrownList, Threshold: 0}).PopulateAttributeStatus(),
|
||||
|
||||
@@ -91,7 +91,7 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
//populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
// populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
// Chainable
|
||||
func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttribute {
|
||||
if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedFailingNow {
|
||||
@@ -165,6 +165,4 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
|
||||
sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ package measurements_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSmart_Flatten(t *testing.T) {
|
||||
@@ -312,7 +313,7 @@ func TestFromCollectorSmartInfo(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -344,7 +345,7 @@ func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -368,7 +369,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinySmart(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -392,7 +393,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -425,7 +426,7 @@ func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -456,7 +457,7 @@ func TestFromCollectorSmartInfo_Nvme(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -483,7 +484,7 @@ func TestFromCollectorSmartInfo_Scsi(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
|
||||
+2
-3
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -68,7 +67,7 @@ func SendPostRequest(url string, file io.Reader) ([]byte, error) {
|
||||
|
||||
log.Printf("%v\n", response.Status)
|
||||
|
||||
return ioutil.ReadAll(response.Body)
|
||||
return io.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
// InfluxDB will throw an error/ignore any submitted data with a timestamp older than the
|
||||
@@ -79,7 +78,7 @@ func readSmartDataFileFixTimestamp(daysToSubtract int, smartDataFilepath string)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricsFileData, err := ioutil.ReadAll(metricsfile)
|
||||
metricsFileData, err := io.ReadAll(metricsfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||
"github.com/containrrr/shoutrrr"
|
||||
shoutrrrTypes "github.com/containrrr/shoutrrr/pkg/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicholas-fedor/shoutrrr"
|
||||
shoutrrrTypes "github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -64,7 +64,7 @@ func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs me
|
||||
var failingAttributes []string
|
||||
// Loop through the attributes to find the failing ones
|
||||
for attrId, attrData := range smartAttrs.Attributes {
|
||||
var status pkg.AttributeStatus = attrData.GetStatus()
|
||||
var status = attrData.GetStatus()
|
||||
// Skip over passing attributes
|
||||
if status == pkg.AttributeStatusPassed {
|
||||
continue
|
||||
@@ -147,7 +147,7 @@ func NewPayload(device models.Device, test bool, currentTime ...time.Time) Paylo
|
||||
|
||||
//validate that the Payload is populated
|
||||
var sendDate time.Time
|
||||
if currentTime != nil && len(currentTime) > 0 {
|
||||
if len(currentTime) > 0 {
|
||||
sendDate = currentTime[0]
|
||||
} else {
|
||||
sendDate = time.Now()
|
||||
@@ -318,7 +318,7 @@ func (n *Notify) SendScriptNotification(scriptUrl string) error {
|
||||
|
||||
if !utils.FileExists(scriptPath) {
|
||||
n.Logger.Errorf("Script does not exist: %s", scriptPath)
|
||||
return errors.New(fmt.Sprintf("custom script path does not exist: %s", scriptPath))
|
||||
return fmt.Errorf("custom script path does not exist: %s", scriptPath)
|
||||
}
|
||||
|
||||
copyEnv := os.Environ()
|
||||
@@ -424,6 +424,17 @@ func (n *Notify) GenShoutrrrNotificationParams(shoutrrrUrl string) (string, *sho
|
||||
case "telegram":
|
||||
(*params)["title"] = subject
|
||||
case "zulip":
|
||||
query := serviceURL.Query()
|
||||
urlTopic := query["topic"]
|
||||
delete(query, "topic")
|
||||
if len(urlTopic) > 0 && urlTopic[len(urlTopic)-1] != "" {
|
||||
subject = urlTopic[len(urlTopic)-1]
|
||||
}
|
||||
subjectRunes := []rune(subject)
|
||||
if len(subjectRunes) > 60 {
|
||||
n.Logger.Warningf("Zulip notification subject too long (%d characters), truncating to 60 characters", len(subjectRunes))
|
||||
subject = string(subjectRunes[:60])
|
||||
}
|
||||
(*params)["topic"] = subject
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
||||
@@ -28,7 +28,6 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
//assert
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
@@ -44,7 +43,6 @@ func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
//assert
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
@@ -60,7 +58,6 @@ func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.
|
||||
statusThreshold := pkg.MetricsStatusThresholdSmart
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
//assert
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
@@ -76,7 +73,6 @@ func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testi
|
||||
statusThreshold := pkg.MetricsStatusThresholdScrutiny
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
//assert
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
@@ -96,7 +92,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
|
||||
//assert
|
||||
@@ -120,7 +115,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCritical
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
|
||||
//assert
|
||||
@@ -141,7 +135,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
|
||||
//assert
|
||||
@@ -162,7 +155,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCritica
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
|
||||
//assert
|
||||
@@ -186,7 +178,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho
|
||||
statusThreshold := pkg.MetricsStatusThresholdSmart
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
|
||||
//assert
|
||||
@@ -206,7 +197,6 @@ func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, errors.New("")).Times(1)
|
||||
|
||||
@@ -228,7 +218,6 @@ func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, nil).Times(1)
|
||||
|
||||
@@ -250,7 +239,6 @@ func TestShouldNotify_NoRepeat(t *testing.T) {
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{smartAttrs}, nil).Times(1)
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@ package version
|
||||
|
||||
// VERSION is the app-global version string, which will be replaced with a
|
||||
// new value during packaging
|
||||
const VERSION = "0.8.3"
|
||||
const VERSION = "0.8.6"
|
||||
|
||||
@@ -3,15 +3,15 @@ package middleware
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Middleware based on https://github.com/toorop/gin-logrus/blob/master/logger.go
|
||||
@@ -40,9 +40,9 @@ func LoggerMiddleware(logger *logrus.Entry) gin.HandlerFunc {
|
||||
//clone the request body reader.
|
||||
var reqBody string
|
||||
if c.Request.Body != nil {
|
||||
buf, _ := ioutil.ReadAll(c.Request.Body)
|
||||
reqBodyReader1 := ioutil.NopCloser(bytes.NewBuffer(buf))
|
||||
reqBodyReader2 := ioutil.NopCloser(bytes.NewBuffer(buf)) //We have to create a new Buffer, because reqBodyReader1 will be read.
|
||||
buf, _ := io.ReadAll(c.Request.Body)
|
||||
reqBodyReader1 := io.NopCloser(bytes.NewBuffer(buf))
|
||||
reqBodyReader2 := io.NopCloser(bytes.NewBuffer(buf)) //We have to create a new Buffer, because reqBodyReader1 will be read.
|
||||
c.Request.Body = reqBodyReader2
|
||||
reqBody = readBody(reqBodyReader1)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -20,10 +19,10 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -52,7 +51,7 @@ func helperReadSmartDataFileFixTimestamp(t *testing.T, smartDataFilepath string)
|
||||
metricsfile, err := os.Open(smartDataFilepath)
|
||||
require.NoError(t, err)
|
||||
|
||||
metricsFileData, err := ioutil.ReadAll(metricsfile)
|
||||
metricsFileData, err := io.ReadAll(metricsfile)
|
||||
require.NoError(t, err)
|
||||
|
||||
//unmarshal because we need to change the timestamp
|
||||
@@ -87,10 +86,9 @@ func TestServerTestSuite_WithCustomBasePath(t *testing.T) {
|
||||
|
||||
func (suite *ServerTestSuite) TestHealthRoute() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -131,10 +129,9 @@ func (suite *ServerTestSuite) TestHealthRoute() {
|
||||
|
||||
func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -174,10 +171,9 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
||||
|
||||
func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -229,10 +225,9 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
|
||||
func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -311,10 +306,9 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
//TODO: this test should use a recorded request/response playback.
|
||||
//func TestSendTestNotificationRoute(t *testing.T) {
|
||||
// //setup
|
||||
// parentPath, _ := ioutil.TempDir("", "")
|
||||
// parentPath, _ := os.MkdirTemp("", "")
|
||||
// defer os.RemoveAll(parentPath)
|
||||
// mockCtrl := gomock.NewController(t)
|
||||
// defer mockCtrl.Finish()
|
||||
// fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
// fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
// fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
@@ -335,10 +329,9 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
|
||||
func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -381,10 +374,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
|
||||
func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -427,10 +419,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
|
||||
func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -473,10 +464,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
|
||||
func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -518,10 +508,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
|
||||
func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
|
||||
Generated
+7852
-4314
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"highlight.js": "^11.6.0",
|
||||
"humanize-duration": "^3.27.3",
|
||||
"lodash": "4.17.21",
|
||||
"lodash": "4.17.23",
|
||||
"moment": "^2.29.4",
|
||||
"ng-apexcharts": "^1.7.4",
|
||||
"ngx-markdown": "^13.1.0",
|
||||
@@ -47,7 +47,7 @@
|
||||
"web-animations-js": "^2.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "v13-lts",
|
||||
"@angular-devkit/build-angular": "v21-lts",
|
||||
"@angular/cli": "v13-lts",
|
||||
"@angular/compiler-cli": "v13-lts",
|
||||
"@angular/language-service": "v13-lts",
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
<button (click)="changeSummaryTempDuration('year')" mat-menu-item>year</button>
|
||||
<button (click)="changeSummaryTempDuration('month')" mat-menu-item>month</button>
|
||||
<button (click)="changeSummaryTempDuration('week')" mat-menu-item>week</button>
|
||||
<button (click)="changeSummaryTempDuration('day')" mat-menu-item>day</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
summaryData: { [key: string]: DeviceSummaryModel };
|
||||
hostGroups: { [hostId: string]: string[] } = {}
|
||||
temperatureOptions: ApexOptions;
|
||||
tempDurationKey = 'forever'
|
||||
tempDurationKey = 'week'
|
||||
config: AppConfig;
|
||||
showArchived: boolean;
|
||||
|
||||
@@ -272,11 +272,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
DURATION_KEY_DAY = "day"
|
||||
DURATION_KEY_WEEK = "week"
|
||||
DURATION_KEY_MONTH = "month"
|
||||
DURATION_KEY_YEAR = "year"
|
||||
DURATION_KEY_FOREVER = "forever"
|
||||
DURATION_KEY_MONTH = "month"
|
||||
DURATION_KEY_YEAR = "year"
|
||||
DURATION_KEY_FOREVER = "forever"
|
||||
*/
|
||||
|
||||
changeSummaryTempDuration(durationKey: string): void {
|
||||
|
||||
Reference in New Issue
Block a user