Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 450a3fc08b | |||
| c3b2eb2b4f | |||
| e4c40f7e80 | |||
| 6cc9ff7fc5 | |||
| 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."
|
||||
@@ -50,9 +50,10 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: scrutiny logs
|
||||
label: scrutiny debug logs
|
||||
description: |
|
||||
Provide any captured scrutiny logs or panic dumps during your issue reproduction in this field.
|
||||
Make sure to turn on debug logging with the environment variable DEBUG=true
|
||||
render: text
|
||||
- type: input
|
||||
attributes:
|
||||
@@ -112,6 +113,14 @@ body:
|
||||
render: json
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: docker-compose.yml
|
||||
description: |
|
||||
If using docker, please provide your full docker-compose.yml file.
|
||||
render: yaml
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: scrutiny.yaml
|
||||
|
||||
+53
-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,30 @@ jobs:
|
||||
path: |
|
||||
scrutiny-web-*
|
||||
scrutiny-collector-metrics-*
|
||||
|
||||
makefile-docker-omnibus:
|
||||
name: Build Docker Omnibus From Makefile
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Build
|
||||
run: make docker-omnibus
|
||||
|
||||
makefile-docker-web:
|
||||
name: Build Docker Web From Makefile
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Build
|
||||
run: make docker-web
|
||||
|
||||
makefile-docker-collector:
|
||||
name: Build Docker Collector From Makefile
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Build
|
||||
run: make docker-collector
|
||||
|
||||
@@ -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,18 @@ 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-collector
|
||||
docker-collector:
|
||||
@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:
|
||||
@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 {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -64,9 +65,9 @@ func (mc *MetricsCollector) Run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
//filter any device with empty wwn (they are invalid)
|
||||
detectedStorageDevices := lo.Filter[models.Device](rawDetectedStorageDevices, func(dev models.Device, _ int) bool {
|
||||
return len(dev.WWN) > 0
|
||||
// Remove any device without a scrutiny UUID, but this should never happen...
|
||||
detectedStorageDevices := lo.Filter(rawDetectedStorageDevices, func(device models.Device, _ int) bool {
|
||||
return device.ScrutinyUUID.IsNil()
|
||||
})
|
||||
|
||||
mc.logger.Infoln("Sending detected devices to API, for filtering & validation")
|
||||
@@ -90,7 +91,7 @@ func (mc *MetricsCollector) Run() error {
|
||||
// execute collection in parallel go-routines
|
||||
//wg.Add(1)
|
||||
//go mc.Collect(&wg, device.WWN, device.DeviceName, device.DeviceType)
|
||||
mc.Collect(device.WWN, device.DeviceName, device.DeviceType)
|
||||
mc.Collect(device.ScrutinyUUID, device.DeviceName, device.DeviceType)
|
||||
|
||||
if mc.config.GetInt("commands.metrics_smartctl_wait") > 0 {
|
||||
time.Sleep(time.Duration(mc.config.GetInt("commands.metrics_smartctl_wait")) * time.Second)
|
||||
@@ -117,10 +118,10 @@ func (mc *MetricsCollector) Validate() error {
|
||||
}
|
||||
|
||||
// func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string, deviceType string) {
|
||||
func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceType string) {
|
||||
func (mc *MetricsCollector) Collect(scrutiny_uuid uuid.UUID, deviceName string, deviceType string) {
|
||||
//defer wg.Done()
|
||||
if len(deviceWWN) == 0 {
|
||||
mc.logger.Errorf("no device WWN detected for %s. Skipping collection for this device (no data association possible).\n", deviceName)
|
||||
if scrutiny_uuid.IsNil() {
|
||||
mc.logger.Errorf("no scrutiny UUID was created for %s. Skipping collection for this device (no data association possible).\n", deviceName)
|
||||
return
|
||||
}
|
||||
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
|
||||
@@ -140,7 +141,7 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT
|
||||
// smartctl command exited with an error, we should still push the data to the API server
|
||||
mc.logger.Errorf("smartctl returned an error code (%d) while processing %s\n", exitError.ExitCode(), deviceName)
|
||||
mc.LogSmartctlExitCode(exitError.ExitCode())
|
||||
mc.Publish(deviceWWN, resultBytes)
|
||||
mc.Publish(scrutiny_uuid, resultBytes)
|
||||
} else {
|
||||
mc.logger.Errorf("error while attempting to execute smartctl: %s\n", deviceName)
|
||||
mc.logger.Errorf("ERROR MESSAGE: %v", err)
|
||||
@@ -149,19 +150,19 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT
|
||||
return
|
||||
} else {
|
||||
//successful run, pass the results directly to webapp backend for parsing and processing.
|
||||
mc.Publish(deviceWWN, resultBytes)
|
||||
mc.Publish(scrutiny_uuid, resultBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MetricsCollector) Publish(deviceWWN string, payload []byte) error {
|
||||
mc.logger.Infof("Publishing smartctl results for %s\n", deviceWWN)
|
||||
func (mc *MetricsCollector) Publish(scrutinyUuid uuid.UUID, payload []byte) error {
|
||||
mc.logger.Infof("Publishing smartctl results for %s\n", scrutinyUuid)
|
||||
|
||||
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
|
||||
apiEndpoint, _ = apiEndpoint.Parse(fmt.Sprintf("api/device/%s/smart", strings.ToLower(deviceWWN)))
|
||||
apiEndpoint, _ = apiEndpoint.Parse(fmt.Sprintf("api/device/%s/smart", scrutinyUuid.String()))
|
||||
|
||||
resp, err := httpClient.Post(apiEndpoint.String(), "application/json", bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
mc.logger.Errorf("An error occurred while publishing SMART data for device (%s): %v", deviceWWN, err)
|
||||
mc.logger.Errorf("An error occurred while publishing SMART data for device (%s): %v", scrutinyUuid, err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -101,15 +101,11 @@ func (d *Detect) SmartCtlInfo(device *models.Device) error {
|
||||
device.WWN = strings.ToLower(wwn.ToString())
|
||||
d.Logger.Debugf("NAA: %d OUI: %d Id: %d => WWN: %s", wwn.Naa, wwn.Oui, wwn.Id, device.WWN)
|
||||
} else {
|
||||
d.Logger.Info("Using WWN Fallback")
|
||||
d.Logger.Debug("Using WWN Fallback")
|
||||
d.wwnFallback(device)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
device.ScrutinyUUID = GenerateScrutinyUUID(device.ModelName, device.SerialNumber, device.WWN)
|
||||
|
||||
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"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/jaypipes/ghw"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DevicePrefix() string {
|
||||
@@ -89,7 +90,7 @@ func (d *Detect) findMissingDevices(detectedDevices []models.Device) ([]models.D
|
||||
return missingDevices, 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 {
|
||||
@@ -102,12 +103,6 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
}
|
||||
}
|
||||
|
||||
//no WWN found, or could not open Block devices. Either way, fallback to serial number
|
||||
if len(detectedDevice.WWN) == 0 {
|
||||
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
|
||||
detectedDevice.WWN = detectedDevice.SerialNumber
|
||||
}
|
||||
|
||||
//wwn must always be lowercase.
|
||||
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/jaypipes/ghw"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DevicePrefix() string {
|
||||
@@ -27,7 +28,7 @@ func (d *Detect) Start() ([]models.Device, error) {
|
||||
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 {
|
||||
@@ -40,12 +41,6 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
}
|
||||
}
|
||||
|
||||
//no WWN found, or could not open Block devices. Either way, fallback to serial number
|
||||
if len(detectedDevice.WWN) == 0 {
|
||||
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
|
||||
detectedDevice.WWN = detectedDevice.SerialNumber
|
||||
}
|
||||
|
||||
//wwn must always be lowercase.
|
||||
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -44,12 +45,6 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
}
|
||||
}
|
||||
|
||||
//no WWN found, or could not open Block devices. Either way, fallback to serial number
|
||||
if len(detectedDevice.WWN) == 0 {
|
||||
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
|
||||
detectedDevice.WWN = detectedDevice.SerialNumber
|
||||
}
|
||||
|
||||
//wwn must always be lowercase.
|
||||
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
|
||||
}
|
||||
@@ -61,7 +56,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 +64,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 +92,5 @@ func populateUdevInfo(detectedDevice *models.Device) error {
|
||||
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package detect
|
||||
import (
|
||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DevicePrefix() string {
|
||||
@@ -26,14 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) {
|
||||
return detectedDevices, nil
|
||||
}
|
||||
|
||||
//WWN values NVMe and SCSI
|
||||
// WWN values NVMe and SCSI
|
||||
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
|
||||
//fallback to serial number
|
||||
if len(detectedDevice.WWN) == 0 {
|
||||
detectedDevice.WWN = detectedDevice.SerialNumber
|
||||
}
|
||||
|
||||
//wwn must always be lowercase.
|
||||
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
|
||||
// No fallback on windows
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
// Randomly generated UUID v4 namespace for Scrutiny
|
||||
var ScrutinyNamespaceUUID = uuid.Must(uuid.FromString("3ea22b35-682b-49fb-a655-abffed108e48"))
|
||||
|
||||
// WWN's are not actually unique so we use Model Name and Serial Number
|
||||
// to hopefully create something that is actually unique despite
|
||||
// manufacturer laziness
|
||||
func GenerateScrutinyUUID(modelName string, serialNumber string, wwn string) uuid.UUID {
|
||||
name := modelName + serialNumber + wwn
|
||||
return uuid.NewV5(ScrutinyNamespaceUUID, name)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateScrutinyUUID(t *testing.T) {
|
||||
t.Run("NVMe device from test data", func(t *testing.T) {
|
||||
testData, err := os.ReadFile("testdata/smartctl_info_nvme.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
var smartInfo collector.SmartInfo
|
||||
err = json.Unmarshal(testData, &smartInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
device := &models.Device{
|
||||
ModelName: smartInfo.ModelName,
|
||||
SerialNumber: smartInfo.SerialNumber,
|
||||
}
|
||||
// NVMe drives don't have a WWN
|
||||
// so scrutiny falls back to serial number
|
||||
device.WWN = device.SerialNumber
|
||||
|
||||
uuid := GenerateScrutinyUUID(device.ModelName, device.SerialNumber, device.WWN)
|
||||
|
||||
require.NotEmpty(t, uuid.String(), "Generated UUID should not be empty")
|
||||
require.Equal(t, uint8(5), uuid.Version(), "Expected UUID version 5")
|
||||
|
||||
uuid2 := GenerateScrutinyUUID(device.ModelName, device.SerialNumber, device.WWN)
|
||||
require.True(t, bytes.Equal(uuid.Bytes(), uuid2.Bytes()), "UUID generation should be deterministic for the same input")
|
||||
})
|
||||
|
||||
// Test with different device data to ensure uniqueness
|
||||
t.Run("different devices produce different UUIDs", func(t *testing.T) {
|
||||
device1 := models.Device{
|
||||
ModelName: "Samsung SSD 860 EVO 1TB",
|
||||
SerialNumber: "S3ZANX0K123456A",
|
||||
WWN: "5002538e40a22954",
|
||||
}
|
||||
|
||||
device2 := device1
|
||||
device2.SerialNumber = "S3ZANX0K123456B"
|
||||
|
||||
uuid1 := GenerateScrutinyUUID(device1.ModelName, device1.SerialNumber, device1.WWN)
|
||||
uuid2 := GenerateScrutinyUUID(device2.ModelName, device2.SerialNumber, device2.WWN)
|
||||
|
||||
require.False(t, bytes.Equal(uuid1.Bytes(), uuid2.Bytes()), "Different devices should produce different UUIDs")
|
||||
})
|
||||
}
|
||||
|
||||
func TestScrutinyNamespaceUUID(t *testing.T) {
|
||||
// Make sure no one changes the namespace
|
||||
expectedNamespace, err := uuid.FromString("3ea22b35-682b-49fb-a655-abffed108e48")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse expected namespace UUID: %v", err)
|
||||
}
|
||||
|
||||
require.True(t, bytes.Equal(ScrutinyNamespaceUUID.Bytes(), expectedNamespace.Bytes()), "Scrutiny Namespace UUID should never change")
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
WWN string `json:"wwn"`
|
||||
ScrutinyUUID uuid.UUID `json:"scrutiny_uuid"`
|
||||
WWN string `json:"wwn"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
DeviceSerialID string `json:"device_serial_id"`
|
||||
DeviceLabel string `json:"device_label"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
DeviceSerialID string `json:"device_serial_id"`
|
||||
DeviceLabel string `json:"device_label"`
|
||||
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
|
||||
+28
-5
@@ -6,16 +6,20 @@
|
||||
######## Build the frontend
|
||||
FROM --platform=${BUILDPLATFORM} node AS frontendbuild
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
COPY --link . /go/src/github.com/analogj/scrutiny
|
||||
COPY --link Makefile /go/src/github.com/analogj/scrutiny/
|
||||
COPY --link webapp/frontend /go/src/github.com/analogj/scrutiny/webapp/frontend
|
||||
|
||||
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
|
||||
COPY --link Makefile /go/src/github.com/analogj/scrutiny/
|
||||
COPY --link go.mod go.sum /go/src/github.com/analogj/scrutiny/
|
||||
COPY --link collector /go/src/github.com/analogj/scrutiny/collector
|
||||
COPY --link webapp/backend /go/src/github.com/analogj/scrutiny/webapp/backend
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get install -y --no-install-recommends \
|
||||
file \
|
||||
@@ -23,8 +27,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 +61,6 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
||||
ca-certificates \
|
||||
cron \
|
||||
curl \
|
||||
smartmontools \
|
||||
tzdata \
|
||||
procps \
|
||||
xz-utils \
|
||||
@@ -62,6 +82,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,21 +4,43 @@
|
||||
|
||||
|
||||
########
|
||||
FROM golang:1.20-bookworm as backendbuild
|
||||
FROM golang:1.25-trixie AS backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
COPY --link Makefile /go/src/github.com/analogj/scrutiny/
|
||||
COPY --link go.mod go.sum /go/src/github.com/analogj/scrutiny/
|
||||
COPY --link collector /go/src/github.com/analogj/scrutiny/collector
|
||||
COPY --link webapp/backend /go/src/github.com/analogj/scrutiny/webapp/backend
|
||||
|
||||
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*
|
||||
@@ -6,22 +6,26 @@
|
||||
######## Build the frontend
|
||||
FROM --platform=${BUILDPLATFORM} node AS frontendbuild
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
COPY --link . /go/src/github.com/analogj/scrutiny
|
||||
COPY --link Makefile /go/src/github.com/analogj/scrutiny/
|
||||
COPY --link webapp/frontend /go/src/github.com/analogj/scrutiny/webapp/frontend
|
||||
|
||||
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
|
||||
COPY --link Makefile /go/src/github.com/analogj/scrutiny/
|
||||
COPY --link go.mod go.sum /go/src/github.com/analogj/scrutiny/
|
||||
COPY --link collector /go/src/github.com/analogj/scrutiny/collector
|
||||
COPY --link webapp/backend /go/src/github.com/analogj/scrutiny/webapp/backend
|
||||
|
||||
RUN apt-get update && apt-get install -y file && rm -rf /var/lib/apt/lists/*
|
||||
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,93 @@
|
||||
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/gofrs/uuid/v5 v5.4.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.
|
||||
|
||||
@@ -4,8 +4,9 @@ const DeviceProtocolAta = "ATA"
|
||||
const DeviceProtocolScsi = "SCSI"
|
||||
const DeviceProtocolNvme = "NVMe"
|
||||
|
||||
//go:generate stringer -type=AttributeStatus
|
||||
// AttributeStatus bitwise flag, 1,2,4,8,16,32,etc
|
||||
//
|
||||
//go:generate stringer -type=AttributeStatus
|
||||
type AttributeStatus uint8
|
||||
|
||||
const (
|
||||
@@ -23,8 +24,9 @@ func AttributeStatusClear(b, flag AttributeStatus) AttributeStatus { return b &
|
||||
func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^ flag }
|
||||
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
|
||||
|
||||
//go:generate stringer -type=DeviceStatus
|
||||
// DeviceStatus bitwise flag, 1,2,4,8,16,32,etc
|
||||
//
|
||||
//go:generate stringer -type=DeviceStatus
|
||||
type DeviceStatus uint8
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,6 +7,7 @@ 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/models/measurements"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
// Create mock using:
|
||||
@@ -17,19 +18,19 @@ type DeviceRepo interface {
|
||||
|
||||
RegisterDevice(ctx context.Context, dev models.Device) error
|
||||
GetDevices(ctx context.Context) ([]models.Device, error)
|
||||
UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error)
|
||||
UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error)
|
||||
GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error)
|
||||
UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error
|
||||
DeleteDevice(ctx context.Context, wwn string) error
|
||||
UpdateDevice(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (models.Device, error)
|
||||
UpdateDeviceStatus(ctx context.Context, scrutiny_uuid uuid.UUID, status pkg.DeviceStatus) (models.Device, error)
|
||||
GetDeviceDetails(ctx context.Context, scrutiny_uuid uuid.UUID) (models.Device, error)
|
||||
UpdateDeviceArchived(ctx context.Context, scrutiny_uuid uuid.UUID, archived bool) error
|
||||
DeleteDevice(ctx context.Context, scrutiny_uuid uuid.UUID) error
|
||||
|
||||
SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error)
|
||||
GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error)
|
||||
SaveSmartAttributes(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (measurements.Smart, error)
|
||||
GetSmartAttributeHistory(ctx context.Context, scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error)
|
||||
|
||||
SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error
|
||||
SaveSmartTemperature(ctx context.Context, scrutiny_uuid uuid.UUID, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error
|
||||
|
||||
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
|
||||
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error)
|
||||
GetSummary(ctx context.Context) (map[uuid.UUID]*models.DeviceSummary, error)
|
||||
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[uuid.UUID][]measurements.SmartTemperature, error)
|
||||
|
||||
LoadSettings(ctx context.Context) (*models.Settings, error)
|
||||
SaveSettings(ctx context.Context, settings models.Settings) error
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package m20250221084400
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
)
|
||||
|
||||
// Deprecated: m20250221084400.Device is deprecated, only used by db migrations
|
||||
type Device struct {
|
||||
Archived bool `json:"archived"`
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package m20260216155600
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
Archived bool `json:"archived"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
WWN string `json:"wwn"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
DeviceSerialID string `json:"device_serial_id"`
|
||||
DeviceLabel string `json:"device_label"`
|
||||
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
InterfaceType string `json:"interface_type"`
|
||||
InterfaceSpeed string `json:"interface_speed"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Firmware string `json:"firmware"`
|
||||
RotationSpeed int `json:"rotational_speed"`
|
||||
Capacity int64 `json:"capacity"`
|
||||
FormFactor string `json:"form_factor"`
|
||||
SmartSupport bool `json:"smart_support"`
|
||||
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
|
||||
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
|
||||
|
||||
// User provided metadata
|
||||
Label string `json:"label"`
|
||||
HostId string `json:"host_id"`
|
||||
|
||||
// Data set by Scrutiny
|
||||
DeviceStatus pkg.DeviceStatus `json:"device_status"`
|
||||
ScrutinyUUID uuid.UUID `json:"scrutiny_uuid" gorm:"primaryKey;uniqueIndex"`
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: webapp/backend/pkg/database/interface.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -source=webapp/backend/pkg/database/interface.go -destination=webapp/backend/pkg/database/mock/mock_database.go
|
||||
//
|
||||
|
||||
// Package mock_database is a generated GoMock package.
|
||||
package mock_database
|
||||
@@ -12,13 +17,15 @@ 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"
|
||||
uuid "github.com/gofrs/uuid/v5"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockDeviceRepo is a mock of DeviceRepo interface.
|
||||
type MockDeviceRepo struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockDeviceRepoMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockDeviceRepoMockRecorder is the mock recorder for MockDeviceRepo.
|
||||
@@ -52,47 +59,33 @@ func (mr *MockDeviceRepoMockRecorder) Close() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDeviceRepo)(nil).Close))
|
||||
}
|
||||
|
||||
// UpdateDeviceArchived mocks base method.
|
||||
func (m *MockDeviceRepo) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateDeviceArchived", ctx, wwn)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateDeviceArchived indicates an expected call of UpdateDeviceArchived.
|
||||
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceArchived(ctx, wwn, archived interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceArchived", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceArchived), ctx, wwn, archived)
|
||||
}
|
||||
|
||||
// DeleteDevice mocks base method.
|
||||
func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, wwn string) error {
|
||||
func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, scrutiny_uuid uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteDevice", ctx, wwn)
|
||||
ret := m.ctrl.Call(m, "DeleteDevice", ctx, scrutiny_uuid)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteDevice indicates an expected call of DeleteDevice.
|
||||
func (mr *MockDeviceRepoMockRecorder) DeleteDevice(ctx, wwn interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) DeleteDevice(ctx, scrutiny_uuid any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDevice", reflect.TypeOf((*MockDeviceRepo)(nil).DeleteDevice), ctx, wwn)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDevice", reflect.TypeOf((*MockDeviceRepo)(nil).DeleteDevice), ctx, scrutiny_uuid)
|
||||
}
|
||||
|
||||
// GetDeviceDetails mocks base method.
|
||||
func (m *MockDeviceRepo) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) {
|
||||
func (m *MockDeviceRepo) GetDeviceDetails(ctx context.Context, scrutiny_uuid uuid.UUID) (models.Device, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, wwn)
|
||||
ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, scrutiny_uuid)
|
||||
ret0, _ := ret[0].(models.Device)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetDeviceDetails indicates an expected call of GetDeviceDetails.
|
||||
func (mr *MockDeviceRepoMockRecorder) GetDeviceDetails(ctx, wwn interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) GetDeviceDetails(ctx, scrutiny_uuid any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceDetails", reflect.TypeOf((*MockDeviceRepo)(nil).GetDeviceDetails), ctx, wwn)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceDetails", reflect.TypeOf((*MockDeviceRepo)(nil).GetDeviceDetails), ctx, scrutiny_uuid)
|
||||
}
|
||||
|
||||
// GetDevices mocks base method.
|
||||
@@ -105,52 +98,52 @@ func (m *MockDeviceRepo) GetDevices(ctx context.Context) ([]models.Device, error
|
||||
}
|
||||
|
||||
// GetDevices indicates an expected call of GetDevices.
|
||||
func (mr *MockDeviceRepoMockRecorder) GetDevices(ctx interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) GetDevices(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevices", reflect.TypeOf((*MockDeviceRepo)(nil).GetDevices), ctx)
|
||||
}
|
||||
|
||||
// GetSmartAttributeHistory mocks base method.
|
||||
func (m *MockDeviceRepo) GetSmartAttributeHistory(ctx context.Context, wwn, durationKey string, selectEntries, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
|
||||
func (m *MockDeviceRepo) GetSmartAttributeHistory(ctx context.Context, scrutiny_uuid uuid.UUID, durationKey string, selectEntries, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetSmartAttributeHistory", ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
ret := m.ctrl.Call(m, "GetSmartAttributeHistory", ctx, scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
ret0, _ := ret[0].([]measurements.Smart)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetSmartAttributeHistory indicates an expected call of GetSmartAttributeHistory.
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSmartAttributeHistory(ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSmartAttributeHistory(ctx, scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartAttributeHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartAttributeHistory), ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartAttributeHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartAttributeHistory), ctx, scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
}
|
||||
|
||||
// GetSmartTemperatureHistory mocks base method.
|
||||
func (m *MockDeviceRepo) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) {
|
||||
func (m *MockDeviceRepo) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[uuid.UUID][]measurements.SmartTemperature, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetSmartTemperatureHistory", ctx, durationKey)
|
||||
ret0, _ := ret[0].(map[string][]measurements.SmartTemperature)
|
||||
ret0, _ := ret[0].(map[uuid.UUID][]measurements.SmartTemperature)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetSmartTemperatureHistory indicates an expected call of GetSmartTemperatureHistory.
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSmartTemperatureHistory(ctx, durationKey interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSmartTemperatureHistory(ctx, durationKey any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartTemperatureHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartTemperatureHistory), ctx, durationKey)
|
||||
}
|
||||
|
||||
// GetSummary mocks base method.
|
||||
func (m *MockDeviceRepo) GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) {
|
||||
func (m *MockDeviceRepo) GetSummary(ctx context.Context) (map[uuid.UUID]*models.DeviceSummary, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetSummary", ctx)
|
||||
ret0, _ := ret[0].(map[string]*models.DeviceSummary)
|
||||
ret0, _ := ret[0].(map[uuid.UUID]*models.DeviceSummary)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetSummary indicates an expected call of GetSummary.
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSummary(ctx interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) GetSummary(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSummary", reflect.TypeOf((*MockDeviceRepo)(nil).GetSummary), ctx)
|
||||
}
|
||||
@@ -164,7 +157,7 @@ func (m *MockDeviceRepo) HealthCheck(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// HealthCheck indicates an expected call of HealthCheck.
|
||||
func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDeviceRepo)(nil).HealthCheck), ctx)
|
||||
}
|
||||
@@ -179,7 +172,7 @@ func (m *MockDeviceRepo) LoadSettings(ctx context.Context) (*models.Settings, er
|
||||
}
|
||||
|
||||
// LoadSettings indicates an expected call of LoadSettings.
|
||||
func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSettings", reflect.TypeOf((*MockDeviceRepo)(nil).LoadSettings), ctx)
|
||||
}
|
||||
@@ -193,7 +186,7 @@ func (m *MockDeviceRepo) RegisterDevice(ctx context.Context, dev models.Device)
|
||||
}
|
||||
|
||||
// RegisterDevice indicates an expected call of RegisterDevice.
|
||||
func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterDevice", reflect.TypeOf((*MockDeviceRepo)(nil).RegisterDevice), ctx, dev)
|
||||
}
|
||||
@@ -207,66 +200,80 @@ func (m *MockDeviceRepo) SaveSettings(ctx context.Context, settings models.Setti
|
||||
}
|
||||
|
||||
// SaveSettings indicates an expected call of SaveSettings.
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSettings", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSettings), ctx, settings)
|
||||
}
|
||||
|
||||
// SaveSmartAttributes mocks base method.
|
||||
func (m *MockDeviceRepo) SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
|
||||
func (m *MockDeviceRepo) SaveSmartAttributes(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, wwn, collectorSmartData)
|
||||
ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, scrutiny_uuid, collectorSmartData)
|
||||
ret0, _ := ret[0].(measurements.Smart)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SaveSmartAttributes indicates an expected call of SaveSmartAttributes.
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartAttributes(ctx, wwn, collectorSmartData interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartAttributes(ctx, scrutiny_uuid, collectorSmartData any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartAttributes", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartAttributes), ctx, wwn, collectorSmartData)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartAttributes", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartAttributes), ctx, scrutiny_uuid, collectorSmartData)
|
||||
}
|
||||
|
||||
// SaveSmartTemperature mocks base method.
|
||||
func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, wwn, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
||||
func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, scrutiny_uuid uuid.UUID, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveSmartTemperature", ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
||||
ret := m.ctrl.Call(m, "SaveSmartTemperature", ctx, scrutiny_uuid, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveSmartTemperature indicates an expected call of SaveSmartTemperature.
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, scrutiny_uuid, deviceProtocol, collectorSmartData, discardSCTTempHistory any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, scrutiny_uuid, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
||||
}
|
||||
|
||||
// UpdateDevice mocks base method.
|
||||
func (m *MockDeviceRepo) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) {
|
||||
func (m *MockDeviceRepo) UpdateDevice(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (models.Device, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateDevice", ctx, wwn, collectorSmartData)
|
||||
ret := m.ctrl.Call(m, "UpdateDevice", ctx, scrutiny_uuid, collectorSmartData)
|
||||
ret0, _ := ret[0].(models.Device)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateDevice indicates an expected call of UpdateDevice.
|
||||
func (mr *MockDeviceRepoMockRecorder) UpdateDevice(ctx, wwn, collectorSmartData interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) UpdateDevice(ctx, scrutiny_uuid, collectorSmartData any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDevice", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDevice), ctx, wwn, collectorSmartData)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDevice", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDevice), ctx, scrutiny_uuid, collectorSmartData)
|
||||
}
|
||||
|
||||
// UpdateDeviceArchived mocks base method.
|
||||
func (m *MockDeviceRepo) UpdateDeviceArchived(ctx context.Context, scrutiny_uuid uuid.UUID, archived bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateDeviceArchived", ctx, scrutiny_uuid, archived)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateDeviceArchived indicates an expected call of UpdateDeviceArchived.
|
||||
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceArchived(ctx, scrutiny_uuid, archived any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceArchived", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceArchived), ctx, scrutiny_uuid, archived)
|
||||
}
|
||||
|
||||
// UpdateDeviceStatus mocks base method.
|
||||
func (m *MockDeviceRepo) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
|
||||
func (m *MockDeviceRepo) UpdateDeviceStatus(ctx context.Context, scrutiny_uuid uuid.UUID, status pkg.DeviceStatus) (models.Device, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, wwn, status)
|
||||
ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, scrutiny_uuid, status)
|
||||
ret0, _ := ret[0].(models.Device)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateDeviceStatus indicates an expected call of UpdateDeviceStatus.
|
||||
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceStatus(ctx, wwn, status interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceStatus(ctx, scrutiny_uuid, status any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceStatus", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceStatus), ctx, wwn, status)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceStatus", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceStatus), ctx, scrutiny_uuid, status)
|
||||
}
|
||||
|
||||
@@ -5,18 +5,21 @@ 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"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api"
|
||||
"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 +32,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 +86,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 +150,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 +242,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
|
||||
}
|
||||
@@ -331,16 +335,16 @@ func (sr *scrutinyRepository) EnsureBuckets(ctx context.Context, org *domain.Org
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// get a map of all devices and associated SMART data
|
||||
func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) {
|
||||
func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[uuid.UUID]*models.DeviceSummary, error) {
|
||||
devices, err := sr.GetDevices(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
summaries := map[string]*models.DeviceSummary{}
|
||||
summaries := map[uuid.UUID]*models.DeviceSummary{}
|
||||
|
||||
for _, device := range devices {
|
||||
summaries[device.WWN] = &models.DeviceSummary{Device: device}
|
||||
summaries[device.ScrutinyUUID] = &models.DeviceSummary{Device: device}
|
||||
}
|
||||
|
||||
// Get parser flux query result
|
||||
@@ -355,7 +359,7 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|
||||
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|
||||
|> last()
|
||||
|> schema.fieldsAsCols()
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|
||||
weeklyData = from(bucket: bucketBaseName + "_weekly")
|
||||
|> range(start: -10y, stop: now())
|
||||
@@ -363,7 +367,7 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|
||||
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|
||||
|> last()
|
||||
|> schema.fieldsAsCols()
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|
||||
monthlyData = from(bucket: bucketBaseName + "_monthly")
|
||||
|> range(start: -10y, stop: now())
|
||||
@@ -371,7 +375,7 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|
||||
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|
||||
|> last()
|
||||
|> schema.fieldsAsCols()
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|
||||
yearlyData = from(bucket: bucketBaseName + "_yearly")
|
||||
|> range(start: -10y, stop: now())
|
||||
@@ -379,12 +383,12 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|
||||
|> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "power_on_hours" or r["_field"] == "date")
|
||||
|> last()
|
||||
|> schema.fieldsAsCols()
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|
||||
union(tables: [dailyData, weeklyData, monthlyData, yearlyData])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> last(column: "device_wwn")
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> last(column: "scrutiny_uuid")
|
||||
|> yield(name: "last")
|
||||
`,
|
||||
sr.appConfig.GetString("web.influxdb.bucket"),
|
||||
@@ -402,14 +406,15 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|
||||
|
||||
//get summary data from Influxdb.
|
||||
//result.Record().Values()
|
||||
if deviceWWN, ok := result.Record().Values()["device_wwn"]; ok {
|
||||
if scrutinyUUIDString, ok := result.Record().Values()["scrutiny_uuid"]; ok {
|
||||
scrutinyUUID := uuid.Must(uuid.FromString(scrutinyUUIDString.(string)))
|
||||
|
||||
//ensure summaries is intialized for this wwn
|
||||
if _, exists := summaries[deviceWWN.(string)]; !exists {
|
||||
summaries[deviceWWN.(string)] = &models.DeviceSummary{}
|
||||
//ensure summaries is intialized for this scrutiny_uuid
|
||||
if _, exists := summaries[scrutinyUUID]; !exists {
|
||||
summaries[scrutinyUUID] = &models.DeviceSummary{}
|
||||
}
|
||||
|
||||
summaries[deviceWWN.(string)].SmartResults = &models.SmartSummary{
|
||||
summaries[scrutinyUUID].SmartResults = &models.SmartSummary{
|
||||
Temp: result.Record().Values()["temp"].(int64),
|
||||
PowerOnHours: result.Record().Values()["power_on_hours"].(int64),
|
||||
CollectorDate: result.Record().Values()["_time"].(time.Time),
|
||||
@@ -432,8 +437,8 @@ func (sr *scrutinyRepository) GetSummary(ctx context.Context) (map[string]*model
|
||||
sr.logger.Printf("========================>>>>>>>>======================")
|
||||
sr.logger.Printf("Error: %v", err)
|
||||
}
|
||||
for wwn, tempHistory := range deviceTempHistory {
|
||||
summaries[wwn].TempHistory = tempHistory
|
||||
for scutiny_uuid, tempHistory := range deviceTempHistory {
|
||||
summaries[scutiny_uuid].TempHistory = tempHistory
|
||||
}
|
||||
|
||||
return summaries, nil
|
||||
@@ -445,6 +450,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 +468,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 +488,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,13 @@ 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"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -18,7 +20,7 @@ import (
|
||||
// update device fields that may change: (DeviceType, HostID)
|
||||
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
|
||||
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "wwn"}},
|
||||
Columns: []clause.Column{{Name: "scrutiny_uuid"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type", "device_uuid", "device_serial_id", "device_label"}),
|
||||
}).Create(&dev).Error; err != nil {
|
||||
return err
|
||||
@@ -31,16 +33,16 @@ 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
|
||||
}
|
||||
|
||||
// update device (only metadata) from collector
|
||||
func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) {
|
||||
func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, scrutiny_uuid uuid.UUID, 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)
|
||||
if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
|
||||
return device, fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
//TODO catch GormClient err
|
||||
@@ -52,22 +54,22 @@ func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, coll
|
||||
}
|
||||
|
||||
// Update Device Status
|
||||
func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
|
||||
func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, scrutiny_uuid uuid.UUID, 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)
|
||||
if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
|
||||
return device, fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
device.DeviceStatus = pkg.DeviceStatusSet(device.DeviceStatus, status)
|
||||
return device, sr.gormClient.Model(&device).Updates(device).Error
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) {
|
||||
func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, scrutiny_uuid uuid.UUID) (models.Device, error) {
|
||||
var device models.Device
|
||||
|
||||
fmt.Println("GetDeviceDetails from GORM")
|
||||
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
|
||||
return models.Device{}, err
|
||||
}
|
||||
|
||||
@@ -75,17 +77,17 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string)
|
||||
}
|
||||
|
||||
// Update Device Archived State
|
||||
func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error {
|
||||
func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, scrutiny_uuid uuid.UUID, 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)
|
||||
if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).First(&device).Error; err != nil {
|
||||
return fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
return sr.gormClient.Model(&device).Where("wwn = ?", wwn).Update("archived", archived).Error
|
||||
return sr.gormClient.Model(&device).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).Update("archived", archived).Error
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) error {
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).Delete(&models.Device{}).Error; err != nil {
|
||||
func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, scrutiny_uuid uuid.UUID) error {
|
||||
if err := sr.gormClient.WithContext(ctx).Where("scrutiny_uuid = ?", scrutiny_uuid.String()).Delete(&models.Device{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -98,14 +100,14 @@ func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) erro
|
||||
}
|
||||
|
||||
for _, bucket := range buckets {
|
||||
sr.logger.Infof("Deleting data for %s in bucket: %s", wwn, bucket)
|
||||
sr.logger.Infof("Deleting data for %s in bucket: %s", scrutiny_uuid.String(), bucket)
|
||||
if err := sr.influxClient.DeleteAPI().DeleteWithName(
|
||||
ctx,
|
||||
sr.appConfig.GetString("web.influxdb.org"),
|
||||
bucket,
|
||||
time.Now().AddDate(-10, 0, 0),
|
||||
time.Now(),
|
||||
fmt.Sprintf(`device_wwn="%s"`, wwn),
|
||||
fmt.Sprintf(`scrutiny_uuid="%s"`, scrutiny_uuid.String()),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,17 +8,18 @@ import (
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// SMART
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
|
||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, scrutiny_uuid uuid.UUID, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
|
||||
deviceSmartData := measurements.Smart{}
|
||||
err := deviceSmartData.FromCollectorSmartInfo(wwn, collectorSmartData)
|
||||
err := deviceSmartData.FromCollectorSmartInfo(scrutiny_uuid, collectorSmartData)
|
||||
if err != nil {
|
||||
sr.logger.Errorln("Could not process SMART metrics", err)
|
||||
return measurements.Smart{}, err
|
||||
@@ -34,14 +35,14 @@ func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn strin
|
||||
// When selectEntries is > 0, only the most recent selectEntries database entries are returned, starting from the selectEntriesOffset entry.
|
||||
// For example, with selectEntries = 5, selectEntries = 0, the most recent 5 are returned. With selectEntries = 3, selectEntries = 2, entries
|
||||
// 2 to 4 are returned (2 being the third newest, since it is zero-indexed)
|
||||
func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
|
||||
func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
|
||||
// Get SMartResults from InfluxDB
|
||||
|
||||
//TODO: change the filter startrange to a real number.
|
||||
|
||||
// Get parser flux query result
|
||||
//appConfig.GetString("web.influxdb.bucket")
|
||||
queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
queryStr := sr.aggregateSmartAttributesQuery(scrutiny_uuid, durationKey, selectEntries, selectEntriesOffset, attributes)
|
||||
log.Infoln(queryStr)
|
||||
|
||||
smartResults := []measurements.Smart{}
|
||||
@@ -100,7 +101,7 @@ func (sr *scrutinyRepository) saveDatapoint(influxWriteApi api.WriteAPIBlocking,
|
||||
return influxWriteApi.WritePoint(ctx, p)
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
|
||||
func (sr *scrutinyRepository) aggregateSmartAttributesQuery(scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
|
||||
|
||||
/*
|
||||
|
||||
@@ -108,28 +109,28 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
|
||||
weekData = from(bucket: "metrics")
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||
|> filter(fn: (r) => r["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|
||||
|> tail(n: 10, offset: 0)
|
||||
|> schema.fieldsAsCols()
|
||||
|
||||
monthData = from(bucket: "metrics_weekly")
|
||||
|> range(start: -1mo, stop: -1w)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||
|> filter(fn: (r) => r["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|
||||
|> tail(n: 10, offset: 0)
|
||||
|> schema.fieldsAsCols()
|
||||
|
||||
yearData = from(bucket: "metrics_monthly")
|
||||
|> range(start: -1y, stop: -1mo)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||
|> filter(fn: (r) => r["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|
||||
|> tail(n: 10, offset: 0)
|
||||
|> schema.fieldsAsCols()
|
||||
|
||||
foreverData = from(bucket: "metrics_yearly")
|
||||
|> range(start: -10y, stop: -1y)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||
|> filter(fn: (r) => r["scrutiny_uuid"] == "32bda933-15be-56a3-902f-9f3674b03d59" )
|
||||
|> tail(n: 10, offset: 0)
|
||||
|> schema.fieldsAsCols()
|
||||
|
||||
@@ -150,7 +151,7 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
|
||||
if len(nestedDurationKeys) == 1 {
|
||||
//there's only one bucket being queried, no need to union, just aggregate the dataset and return
|
||||
partialQueryStr = append(partialQueryStr, []string{
|
||||
sr.generateSmartAttributesSubquery(wwn, nestedDurationKeys[0], selectEntries, selectEntriesOffset, attributes),
|
||||
sr.generateSmartAttributesSubquery(scrutiny_uuid, nestedDurationKeys[0], selectEntries, selectEntriesOffset, attributes),
|
||||
fmt.Sprintf(`%sData`, nestedDurationKeys[0]),
|
||||
`|> sort(columns: ["_time"], desc: true)`,
|
||||
`|> yield()`,
|
||||
@@ -165,9 +166,9 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
|
||||
if selectEntries > 0 {
|
||||
// We only need the last `n + offset` # of entries from each table to guarantee we can
|
||||
// get the last `n` # of entries starting from `offset` of the union
|
||||
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes))
|
||||
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(scrutiny_uuid, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes))
|
||||
} else {
|
||||
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, 0, 0, attributes))
|
||||
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(scrutiny_uuid, nestedDurationKey, 0, 0, attributes))
|
||||
}
|
||||
}
|
||||
partialQueryStr = append(partialQueryStr, subQueries...)
|
||||
@@ -177,14 +178,14 @@ 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")`)
|
||||
|
||||
return strings.Join(partialQueryStr, "\n")
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
|
||||
func (sr *scrutinyRepository) generateSmartAttributesSubquery(scrutiny_uuid uuid.UUID, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
|
||||
bucketName := sr.lookupBucketName(durationKey)
|
||||
durationRange := sr.lookupDuration(durationKey)
|
||||
|
||||
@@ -192,13 +193,15 @@ func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durati
|
||||
fmt.Sprintf(`%sData = from(bucket: "%s")`, durationKey, bucketName),
|
||||
fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]),
|
||||
`|> filter(fn: (r) => r["_measurement"] == "smart" )`,
|
||||
fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn),
|
||||
fmt.Sprintf(`|> filter(fn: (r) => r["scrutiny_uuid"] == "%s" )`, scrutiny_uuid.String()),
|
||||
}
|
||||
|
||||
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()")
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220716214900"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20250221084400"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20260216155600"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
@@ -424,6 +426,53 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
return tx.Create(&defaultSettings).Error
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "m20260216155600", // add ScrutinyUUID as primary key
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
devices := []m20260216155600.Device{}
|
||||
if err := tx.Find(&devices).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
sr.logger.Debug("Generating Scrutiny UUIDs")
|
||||
for i := range devices {
|
||||
device := &devices[i]
|
||||
device.ScrutinyUUID = detect.GenerateScrutinyUUID(device.ModelName, device.SerialNumber, device.WWN)
|
||||
}
|
||||
|
||||
// sqlite doesn't support altering columns
|
||||
// so we have to create a new one, drop the old one, then rename.
|
||||
sr.logger.Debug("Creating new devices table")
|
||||
tx.Table("devices_new").AutoMigrate(&m20260216155600.Device{})
|
||||
if len(devices) > 0 {
|
||||
if err := tx.Table("devices_new").Create(&devices).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sr.logger.Debug("Dropping old devices table")
|
||||
if err := tx.Migrator().DropTable(&m20260216155600.Device{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sr.logger.Debug("Renaming new device table")
|
||||
if err := tx.Migrator().RenameTable("devices_new", "devices"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
wwnToUUID := make(map[string]string)
|
||||
for _, device := range devices {
|
||||
wwnToUUID[device.WWN] = device.ScrutinyUUID.String()
|
||||
}
|
||||
|
||||
err := m20260216155600_ChangeInfluxDBTags(sr, ctx, wwnToUUID)
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := m.Migrate(); err != nil {
|
||||
@@ -473,6 +522,91 @@ func ignorePastRetentionPolicyError(err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func m20260216155600_ChangeInfluxDBTags(sr *scrutinyRepository, ctx context.Context, wwnToUUID map[string]string) error {
|
||||
bucket := sr.appConfig.GetString("web.influxdb.bucket")
|
||||
org := sr.appConfig.GetString("web.influxdb.org")
|
||||
bucketNames := []string{
|
||||
bucket,
|
||||
fmt.Sprintf("%s_weekly", bucket),
|
||||
fmt.Sprintf("%s_monthly", bucket),
|
||||
fmt.Sprintf("%s_yearly", bucket),
|
||||
}
|
||||
|
||||
const batchSize = 1000
|
||||
bucketsAPI := sr.influxClient.BucketsAPI()
|
||||
|
||||
for _, bucketName := range bucketNames {
|
||||
newBucketName := fmt.Sprintf("%s_new", bucketName)
|
||||
|
||||
// Step 1: Create the new bucket. Copy retention rules from the original.
|
||||
sr.logger.Debugf("Creating temporary bucket %s...", newBucketName)
|
||||
oldBucket, err := bucketsAPI.FindBucketByName(ctx, bucketName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to find bucket %s: %w", bucketName, err)
|
||||
}
|
||||
|
||||
// Delete leftover _new bucket from a previous failed migration attempt.
|
||||
if existingNew, _ := bucketsAPI.FindBucketByName(ctx, newBucketName); existingNew != nil {
|
||||
sr.logger.Debugf("Found leftover bucket %s from previous migration, deleting...", newBucketName)
|
||||
if err := bucketsAPI.DeleteBucket(ctx, existingNew); err != nil {
|
||||
return fmt.Errorf("Failed to delete leftover bucket %s: %w", newBucketName, err)
|
||||
}
|
||||
}
|
||||
|
||||
orgObj, err := sr.influxClient.OrganizationsAPI().FindOrganizationByName(ctx, org)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find organization %s: %w", org, err)
|
||||
}
|
||||
|
||||
newBucket, err := bucketsAPI.CreateBucketWithName(ctx, orgObj, newBucketName, oldBucket.RetentionRules...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create bucket %s: %w", newBucketName, err)
|
||||
}
|
||||
|
||||
for wwn, scrutinyUUID := range wwnToUUID {
|
||||
sr.logger.Debugf("Copying points from %s to %s for wwn %s...", bucketName, newBucketName, wwn)
|
||||
|
||||
offset := 0
|
||||
for ; ; offset += batchSize {
|
||||
queryStr := fmt.Sprintf(`
|
||||
from(bucket: "%s")
|
||||
|> range(start: -10y, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" or r["_measurement"] == "temp")
|
||||
|> filter(fn: (r) => r["device_wwn"] == "%s")
|
||||
|> limit(n: %d, offset: %d)
|
||||
|> drop(columns: ["device_wwn"])
|
||||
|> set(key: "scrutiny_uuid", value: "%s")
|
||||
|> to(bucket: "%s")
|
||||
`, bucketName, wwn, batchSize, offset, scrutinyUUID, newBucketName)
|
||||
|
||||
result, err := sr.influxQueryApi.Query(ctx, queryStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy points from %s to %s for wwn %s (offset %d): %w", bucketName, newBucketName, wwn, offset, err)
|
||||
}
|
||||
|
||||
if !result.Next() {
|
||||
break
|
||||
}
|
||||
}
|
||||
sr.logger.Debugf("Copied approx. %d points for wwn %s", offset, wwn)
|
||||
}
|
||||
|
||||
sr.logger.Debugf("Replacing bucket %s with %s...", bucketName, newBucketName)
|
||||
if err := bucketsAPI.DeleteBucket(ctx, oldBucket); err != nil {
|
||||
return fmt.Errorf("Failed to delete old bucket %s: %w", bucketName, err)
|
||||
}
|
||||
|
||||
newBucket.Name = bucketName
|
||||
if _, err := bucketsAPI.UpdateBucket(ctx, newBucket); err != nil {
|
||||
return fmt.Errorf("Failed to rename bucket %s to %s: %w", newBucketName, bucketName, err)
|
||||
}
|
||||
|
||||
sr.logger.Debugf("Bucket %s migrated successfully", bucketName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
func m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp(preDevice m20201107210306.Device, preSmartResult m20201107210306.Smart) (error, measurements.SmartTemperature) {
|
||||
//extract temperature data for every datapoint
|
||||
@@ -647,7 +781,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
|
||||
|
||||
@@ -3,12 +3,13 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/influxdata/influxdb-client-go/v2/api"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Tasks
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error {
|
||||
weeklyTaskName := "tsk-weekly-aggr"
|
||||
weeklyTaskScript := sr.DownsampleScript("weekly", weeklyTaskName, "0 1 * * 0")
|
||||
@@ -108,7 +109,7 @@ func (sr *scrutinyRepository) DownsampleScript(aggregationType string, name stri
|
||||
smart_data = from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["device_wwn", "_field"])
|
||||
|> group(columns: ["scrutiny_uuid", "_field"])
|
||||
|
||||
non_numeric_smart_data = smart_data
|
||||
|> filter(fn: (r) => types.isType(v: r._value, type: "string") or types.isType(v: r._value, type: "bool"))
|
||||
@@ -139,20 +140,19 @@ destOrg = "%s"
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["device_wwn", "_field"])
|
||||
|> group(columns: ["scrutiny_uuid", "_field"])
|
||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||
|> set(key: "_measurement", value: "temp")
|
||||
|> set(key: "_field", value: "temp")
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
`,
|
||||
|> to(bucket: destBucket, org: destOrg)`,
|
||||
name,
|
||||
cron,
|
||||
sourceBucket,
|
||||
|
||||
@@ -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()
|
||||
@@ -43,20 +43,19 @@ destOrg = "scrutiny"
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["device_wwn", "_field"])
|
||||
|> group(columns: ["scrutiny_uuid", "_field"])
|
||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||
|> set(key: "_measurement", value: "temp")
|
||||
|> set(key: "_field", value: "temp")
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
`, influxDbScript)
|
||||
|> to(bucket: destBucket, org: destOrg)`, influxDbScript)
|
||||
}
|
||||
|
||||
func Test_DownsampleScript_Monthly(t *testing.T) {
|
||||
@@ -64,7 +63,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()
|
||||
@@ -95,20 +93,19 @@ destOrg = "scrutiny"
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["device_wwn", "_field"])
|
||||
|> group(columns: ["scrutiny_uuid", "_field"])
|
||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||
|> set(key: "_measurement", value: "temp")
|
||||
|> set(key: "_field", value: "temp")
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
`, influxDbScript)
|
||||
|> to(bucket: destBucket, org: destOrg)`, influxDbScript)
|
||||
}
|
||||
|
||||
func Test_DownsampleScript_Yearly(t *testing.T) {
|
||||
@@ -116,7 +113,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()
|
||||
@@ -147,18 +143,17 @@ destOrg = "scrutiny"
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||
|> group(columns: ["device_wwn", "_field"])
|
||||
|> group(columns: ["scrutiny_uuid", "_field"])
|
||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
|
||||
from(bucket: sourceBucket)
|
||||
|> range(start: rangeStart, stop: rangeEnd)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||
|> set(key: "_measurement", value: "temp")
|
||||
|> set(key: "_field", value: "temp")
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
`, influxDbScript)
|
||||
|> to(bucket: destBucket, org: destOrg)`, influxDbScript)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ import (
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
)
|
||||
|
||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Temperature Data
|
||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
||||
func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, scrutiny_uuid uuid.UUID, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
||||
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 && !discardSCTTempHistory {
|
||||
|
||||
for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table {
|
||||
@@ -24,15 +25,15 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
|
||||
}
|
||||
|
||||
intervalSec := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * 60
|
||||
datapointTime := collectorSmartData.LocalTime.TimeT - int64(ndx) * intervalSec
|
||||
alignedDatapointTime := datapointTime - datapointTime % intervalSec
|
||||
datapointTime := collectorSmartData.LocalTime.TimeT - int64(ndx)*intervalSec
|
||||
alignedDatapointTime := datapointTime - datapointTime%intervalSec
|
||||
smartTemp := measurements.SmartTemperature{
|
||||
Date: time.Unix(alignedDatapointTime, 0),
|
||||
Temp: temp,
|
||||
}
|
||||
|
||||
tags, fields := smartTemp.Flatten()
|
||||
tags["device_wwn"] = wwn
|
||||
tags["scrutiny_uuid"] = scrutiny_uuid.String()
|
||||
p := influxdb2.NewPoint("temp",
|
||||
tags,
|
||||
fields,
|
||||
@@ -44,7 +45,6 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Even if ata_sct_temperature_history is present, also add current temperature. See #824
|
||||
smartTemp := measurements.SmartTemperature{
|
||||
Date: time.Unix(collectorSmartData.LocalTime.TimeT, 0),
|
||||
@@ -52,7 +52,7 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
|
||||
}
|
||||
|
||||
tags, fields := smartTemp.Flatten()
|
||||
tags["device_wwn"] = wwn
|
||||
tags["scrutiny_uuid"] = scrutiny_uuid.String()
|
||||
p := influxdb2.NewPoint("temp",
|
||||
tags,
|
||||
fields,
|
||||
@@ -60,10 +60,10 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
|
||||
return sr.influxWriteApi.WritePoint(ctx, p)
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) {
|
||||
func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[uuid.UUID][]measurements.SmartTemperature, error) {
|
||||
//we can get temp history for "week", "month", DURATION_KEY_YEAR, "forever"
|
||||
|
||||
deviceTempHistory := map[string][]measurements.SmartTemperature{}
|
||||
deviceTempHistory := map[uuid.UUID][]measurements.SmartTemperature{}
|
||||
|
||||
//TODO: change the query range to a variable.
|
||||
queryStr := sr.aggregateTempQuery(durationKey)
|
||||
@@ -73,14 +73,15 @@ func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, du
|
||||
// Use Next() to iterate over query result lines
|
||||
for result.Next() {
|
||||
|
||||
if deviceWWN, ok := result.Record().Values()["device_wwn"]; ok {
|
||||
if scrutinyUUIDString, ok := result.Record().Values()["scrutiny_uuid"]; ok {
|
||||
scrutinyUUID := uuid.Must(uuid.FromString(scrutinyUUIDString.(string)))
|
||||
|
||||
//check if deviceWWN has been seen and initialized already
|
||||
if _, ok := deviceTempHistory[deviceWWN.(string)]; !ok {
|
||||
deviceTempHistory[deviceWWN.(string)] = []measurements.SmartTemperature{}
|
||||
//check if scrutinyUUID has been seen and initialized already
|
||||
if _, ok := deviceTempHistory[scrutinyUUID]; !ok {
|
||||
deviceTempHistory[scrutinyUUID] = []measurements.SmartTemperature{}
|
||||
}
|
||||
|
||||
currentTempHistory := deviceTempHistory[deviceWWN.(string)]
|
||||
currentTempHistory := deviceTempHistory[scrutinyUUID]
|
||||
smartTemp := measurements.SmartTemperature{}
|
||||
|
||||
for key, val := range result.Record().Values() {
|
||||
@@ -88,7 +89,7 @@ func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, du
|
||||
}
|
||||
smartTemp.Date = result.Record().Values()["_time"].(time.Time)
|
||||
currentTempHistory = append(currentTempHistory, smartTemp)
|
||||
deviceTempHistory[deviceWWN.(string)] = currentTempHistory
|
||||
deviceTempHistory[scrutinyUUID] = currentTempHistory
|
||||
}
|
||||
}
|
||||
if result.Err() != nil {
|
||||
@@ -113,18 +114,18 @@ func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) string {
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
monthData = from(bucket: "metrics_weekly")
|
||||
|> range(start: -1mo, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> schema.fieldsAsCols()
|
||||
|
||||
@@ -140,14 +141,15 @@ 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)`,
|
||||
`|> group(columns: ["device_wwn"])`,
|
||||
fmt.Sprintf(`|> aggregateWindow(every: %s, fn: mean, createEmpty: false)`, durationResolution),
|
||||
`|> group(columns: ["scrutiny_uuid"])`,
|
||||
`|> toInt()`,
|
||||
"",
|
||||
}...)
|
||||
@@ -163,7 +165,7 @@ func (sr *scrutinyRepository) aggregateTempQuery(durationKey string) string {
|
||||
} else {
|
||||
partialQueryStr = append(partialQueryStr, []string{
|
||||
fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")),
|
||||
`|> group(columns: ["device_wwn"])`,
|
||||
`|> group(columns: ["scrutiny_uuid"])`,
|
||||
`|> sort(columns: ["_time"], desc: false)`,
|
||||
"|> schema.fieldsAsCols()",
|
||||
}...)
|
||||
|
||||
@@ -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()
|
||||
@@ -32,7 +32,7 @@ weekData = from(bucket: "metrics")
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
weekData
|
||||
@@ -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()
|
||||
@@ -65,18 +64,18 @@ weekData = from(bucket: "metrics")
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
monthData = from(bucket: "metrics_weekly")
|
||||
|> range(start: -1mo, stop: -1w)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> schema.fieldsAsCols()`, influxDbScript)
|
||||
}
|
||||
@@ -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()
|
||||
@@ -106,25 +104,25 @@ weekData = from(bucket: "metrics")
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
monthData = from(bucket: "metrics_weekly")
|
||||
|> range(start: -1mo, stop: -1w)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
yearData = from(bucket: "metrics_monthly")
|
||||
|> range(start: -1y, stop: -1mo)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData, yearData])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> schema.fieldsAsCols()`, influxDbScript)
|
||||
}
|
||||
@@ -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()
|
||||
@@ -154,32 +151,32 @@ weekData = from(bucket: "metrics")
|
||||
|> range(start: -1w, stop: now())
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
monthData = from(bucket: "metrics_weekly")
|
||||
|> range(start: -1mo, stop: -1w)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
yearData = from(bucket: "metrics_monthly")
|
||||
|> range(start: -1y, stop: -1mo)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
foreverData = from(bucket: "metrics_yearly")
|
||||
|> range(start: -10y, stop: -1y)
|
||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> toInt()
|
||||
|
||||
union(tables: [weekData, monthData, yearData, foreverData])
|
||||
|> group(columns: ["device_wwn"])
|
||||
|> group(columns: ["scrutiny_uuid"])
|
||||
|> sort(columns: ["_time"], desc: false)
|
||||
|> schema.fieldsAsCols()`, influxDbScript)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
@@ -207,10 +207,10 @@ type SmartInfo struct {
|
||||
ID int `json:"id"`
|
||||
SubsystemID int `json:"subsystem_id"`
|
||||
} `json:"nvme_pci_vendor"`
|
||||
NvmeIeeeOuiIdentifier int `json:"nvme_ieee_oui_identifier"`
|
||||
NvmeTotalCapacity int64 `json:"nvme_total_capacity"`
|
||||
NvmeControllerID int `json:"nvme_controller_id"`
|
||||
NvmeNumberOfNamespaces int `json:"nvme_number_of_namespaces"`
|
||||
NvmeIeeeOuiIdentifier uint32 `json:"nvme_ieee_oui_identifier"`
|
||||
NvmeTotalCapacity int64 `json:"nvme_total_capacity"`
|
||||
NvmeControllerID int `json:"nvme_controller_id"`
|
||||
NvmeNumberOfNamespaces int `json:"nvme_number_of_namespaces"`
|
||||
NvmeNamespaces []struct {
|
||||
ID int `json:"id"`
|
||||
Size struct {
|
||||
@@ -226,6 +226,10 @@ type SmartInfo struct {
|
||||
Bytes int64 `json:"bytes"`
|
||||
} `json:"utilization"`
|
||||
FormattedLbaSize int `json:"formatted_lba_size"`
|
||||
Eui64 struct {
|
||||
Oui uint32 `json:"oui"`
|
||||
ExtId uint64 `json:"ext_id"`
|
||||
} `json:"eui64"`
|
||||
} `json:"nvme_namespaces"`
|
||||
NvmeSmartHealthInformationLog NvmeSmartHealthInformationLog `json:"nvme_smart_health_information_log"`
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"time"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
type DeviceWrapper struct {
|
||||
@@ -19,7 +21,7 @@ type Device struct {
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
WWN string `json:"wwn"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
@@ -45,6 +47,7 @@ type Device struct {
|
||||
|
||||
// Data set by Scrutiny
|
||||
DeviceStatus pkg.DeviceStatus `json:"device_status"`
|
||||
ScrutinyUUID uuid.UUID `json:"scrutiny_uuid" gorm:"primaryKey;uniqueIndex"`
|
||||
}
|
||||
|
||||
func (dv *Device) IsAta() bool {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
)
|
||||
|
||||
// This is used in server_test.go
|
||||
type DeviceSummaryWrapper struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []error `json:"errors"`
|
||||
|
||||
@@ -2,18 +2,21 @@ 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"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
type Smart struct {
|
||||
Date time.Time `json:"date"`
|
||||
DeviceWWN string `json:"device_wwn"` //(tag)
|
||||
DeviceWWN string `json:"device_wwn` // deprecated
|
||||
ScrutinyUUID uuid.UUID `json:"scrutiny_uuid"` //(tag)
|
||||
DeviceProtocol string `json:"device_protocol"`
|
||||
|
||||
//Metrics (fields)
|
||||
@@ -30,7 +33,7 @@ type Smart struct {
|
||||
|
||||
func (sm *Smart) Flatten() (tags map[string]string, fields map[string]interface{}) {
|
||||
tags = map[string]string{
|
||||
"device_wwn": sm.DeviceWWN,
|
||||
"scrutiny_uuid": sm.ScrutinyUUID.String(),
|
||||
"device_protocol": sm.DeviceProtocol,
|
||||
}
|
||||
|
||||
@@ -52,10 +55,15 @@ func (sm *Smart) Flatten() (tags map[string]string, fields map[string]interface{
|
||||
func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
|
||||
//go though the massive map returned from influxdb. If a key is associated with the Smart struct, assign it. If it starts with "attr.*" group it by attributeId, and pass to attribute inflate.
|
||||
|
||||
scrutiny_uuid, err := uuid.FromString(attrs["scrutiny_uuid"].(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sm := Smart{
|
||||
//required fields
|
||||
Date: attrs["_time"].(time.Time),
|
||||
DeviceWWN: attrs["device_wwn"].(string),
|
||||
ScrutinyUUID: scrutiny_uuid,
|
||||
DeviceProtocol: attrs["device_protocol"].(string),
|
||||
|
||||
Attributes: map[string]SmartAttribute{},
|
||||
@@ -102,7 +110,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,14 +119,14 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
|
||||
|
||||
}
|
||||
|
||||
log.Printf("Found Smart Device (%s) Attributes (%v)", sm.DeviceWWN, len(sm.Attributes))
|
||||
log.Printf("Found Smart Device (%s) Attributes (%v)", sm.ScrutinyUUID, len(sm.Attributes))
|
||||
|
||||
return &sm, nil
|
||||
}
|
||||
|
||||
//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
|
||||
// Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries)
|
||||
func (sm *Smart) FromCollectorSmartInfo(scrutiny_uuid uuid.UUID, info collector.SmartInfo) error {
|
||||
sm.ScrutinyUUID = scrutiny_uuid
|
||||
sm.Date = time.Unix(info.LocalTime.TimeT, 0)
|
||||
|
||||
//smart metrics
|
||||
@@ -132,18 +140,19 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er
|
||||
sm.DeviceProtocol = info.Device.Protocol
|
||||
// process ATA/NVME/SCSI protocol data
|
||||
sm.Attributes = map[string]SmartAttribute{}
|
||||
if sm.DeviceProtocol == pkg.DeviceProtocolAta {
|
||||
switch sm.DeviceProtocol {
|
||||
case pkg.DeviceProtocolAta:
|
||||
sm.ProcessAtaSmartInfo(info.AtaSmartAttributes.Table)
|
||||
} else if sm.DeviceProtocol == pkg.DeviceProtocolNvme {
|
||||
case pkg.DeviceProtocolNvme:
|
||||
sm.ProcessNvmeSmartInfo(info.NvmeSmartHealthInformationLog)
|
||||
} else if sm.DeviceProtocol == pkg.DeviceProtocolScsi {
|
||||
case pkg.DeviceProtocolScsi:
|
||||
sm.ProcessScsiSmartInfo(info.ScsiGrownDefectList, info.ScsiErrorCounterLog)
|
||||
}
|
||||
|
||||
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 +180,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 +210,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
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func (sa *SmartNvmeAttribute) Inflate(key string, val interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
//populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
// populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
// Chainable
|
||||
func (sa *SmartNvmeAttribute) PopulateAttributeStatus() *SmartNvmeAttribute {
|
||||
|
||||
|
||||
@@ -67,9 +67,8 @@ func (sa *SmartScsiAttribute) Inflate(key string, val interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
//Chainable
|
||||
// populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
// Chainable
|
||||
func (sa *SmartScsiAttribute) PopulateAttributeStatus() *SmartScsiAttribute {
|
||||
|
||||
//-1 is a special number meaning no threshold.
|
||||
|
||||
@@ -2,22 +2,25 @@ package measurements_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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"
|
||||
"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/gofrs/uuid/v5"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSmart_Flatten(t *testing.T) {
|
||||
//setup
|
||||
timeNow := time.Now()
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
smart := measurements.Smart{
|
||||
Date: timeNow,
|
||||
DeviceWWN: "test-wwn",
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceProtocol: pkg.DeviceProtocolAta,
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -30,16 +33,17 @@ func TestSmart_Flatten(t *testing.T) {
|
||||
tags, fields := smart.Flatten()
|
||||
|
||||
//assert
|
||||
require.Equal(t, map[string]string{"device_protocol": "ATA", "device_wwn": "test-wwn"}, tags)
|
||||
require.Equal(t, map[string]string{"device_protocol": "ATA", "scrutiny_uuid": smartUUID.String()}, tags)
|
||||
require.Equal(t, map[string]interface{}{"power_cycle_count": int64(10), "power_on_hours": int64(10), "temp": int64(50)}, fields)
|
||||
}
|
||||
|
||||
func TestSmart_Flatten_ATA(t *testing.T) {
|
||||
//setup
|
||||
timeNow := time.Now()
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
smart := measurements.Smart{
|
||||
Date: timeNow,
|
||||
DeviceWWN: "test-wwn",
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceProtocol: pkg.DeviceProtocolAta,
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -71,7 +75,7 @@ func TestSmart_Flatten_ATA(t *testing.T) {
|
||||
tags, fields := smart.Flatten()
|
||||
|
||||
//assert
|
||||
require.Equal(t, map[string]string{"device_protocol": "ATA", "device_wwn": "test-wwn"}, tags)
|
||||
require.Equal(t, map[string]string{"device_protocol": "ATA", "scrutiny_uuid": smartUUID.String()}, tags)
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"attr.1.attribute_id": "1",
|
||||
"attr.1.failure_rate": float64(0),
|
||||
@@ -106,9 +110,10 @@ func TestSmart_Flatten_ATA(t *testing.T) {
|
||||
func TestSmart_Flatten_SCSI(t *testing.T) {
|
||||
//setup
|
||||
timeNow := time.Now()
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
smart := measurements.Smart{
|
||||
Date: timeNow,
|
||||
DeviceWWN: "test-wwn",
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceProtocol: pkg.DeviceProtocolScsi,
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -126,7 +131,7 @@ func TestSmart_Flatten_SCSI(t *testing.T) {
|
||||
tags, fields := smart.Flatten()
|
||||
|
||||
//assert
|
||||
require.Equal(t, map[string]string{"device_protocol": "SCSI", "device_wwn": "test-wwn"}, tags)
|
||||
require.Equal(t, map[string]string{"device_protocol": "SCSI", "scrutiny_uuid": smartUUID.String()}, tags)
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast",
|
||||
"attr.read_errors_corrected_by_eccfast.failure_rate": float64(0),
|
||||
@@ -144,9 +149,10 @@ func TestSmart_Flatten_SCSI(t *testing.T) {
|
||||
func TestSmart_Flatten_NVMe(t *testing.T) {
|
||||
//setup
|
||||
timeNow := time.Now()
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
smart := measurements.Smart{
|
||||
Date: timeNow,
|
||||
DeviceWWN: "test-wwn",
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceProtocol: pkg.DeviceProtocolNvme,
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -164,7 +170,7 @@ func TestSmart_Flatten_NVMe(t *testing.T) {
|
||||
tags, fields := smart.Flatten()
|
||||
|
||||
//assert
|
||||
require.Equal(t, map[string]string{"device_protocol": "NVMe", "device_wwn": "test-wwn"}, tags)
|
||||
require.Equal(t, map[string]string{"device_protocol": "NVMe", "scrutiny_uuid": smartUUID.String()}, tags)
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"attr.available_spare.attribute_id": "available_spare",
|
||||
"attr.available_spare.failure_rate": float64(0),
|
||||
@@ -181,9 +187,10 @@ func TestSmart_Flatten_NVMe(t *testing.T) {
|
||||
func TestNewSmartFromInfluxDB_ATA(t *testing.T) {
|
||||
//setup
|
||||
timeNow := time.Now()
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
attrs := map[string]interface{}{
|
||||
"_time": timeNow,
|
||||
"device_wwn": "test-wwn",
|
||||
"scrutiny_uuid": smartUUID.String(),
|
||||
"device_protocol": pkg.DeviceProtocolAta,
|
||||
"attr.1.attribute_id": "1",
|
||||
"attr.1.failure_rate": float64(0),
|
||||
@@ -208,7 +215,7 @@ func TestNewSmartFromInfluxDB_ATA(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &measurements.Smart{
|
||||
Date: timeNow,
|
||||
DeviceWWN: "test-wwn",
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceProtocol: "ATA",
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -229,9 +236,10 @@ func TestNewSmartFromInfluxDB_ATA(t *testing.T) {
|
||||
func TestNewSmartFromInfluxDB_NVMe(t *testing.T) {
|
||||
//setup
|
||||
timeNow := time.Now()
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
attrs := map[string]interface{}{
|
||||
"_time": timeNow,
|
||||
"device_wwn": "test-wwn",
|
||||
"scrutiny_uuid": smartUUID.String(),
|
||||
"device_protocol": pkg.DeviceProtocolNvme,
|
||||
"attr.available_spare.attribute_id": "available_spare",
|
||||
"attr.available_spare.failure_rate": float64(0),
|
||||
@@ -252,7 +260,7 @@ func TestNewSmartFromInfluxDB_NVMe(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &measurements.Smart{
|
||||
Date: timeNow,
|
||||
DeviceWWN: "test-wwn",
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceProtocol: "NVMe",
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -268,9 +276,10 @@ func TestNewSmartFromInfluxDB_NVMe(t *testing.T) {
|
||||
func TestNewSmartFromInfluxDB_SCSI(t *testing.T) {
|
||||
//setup
|
||||
timeNow := time.Now()
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
attrs := map[string]interface{}{
|
||||
"_time": timeNow,
|
||||
"device_wwn": "test-wwn",
|
||||
"scrutiny_uuid": smartUUID.String(),
|
||||
"device_protocol": pkg.DeviceProtocolScsi,
|
||||
"attr.read_errors_corrected_by_eccfast.attribute_id": "read_errors_corrected_by_eccfast",
|
||||
"attr.read_errors_corrected_by_eccfast.failure_rate": float64(0),
|
||||
@@ -291,7 +300,7 @@ func TestNewSmartFromInfluxDB_SCSI(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &measurements.Smart{
|
||||
Date: timeNow,
|
||||
DeviceWWN: "test-wwn",
|
||||
ScrutinyUUID: smartUUID,
|
||||
DeviceProtocol: "SCSI",
|
||||
Temp: 50,
|
||||
PowerOnHours: 10,
|
||||
@@ -312,18 +321,19 @@ 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)
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status)
|
||||
require.Equal(t, 18, len(smartMdl.Attributes))
|
||||
|
||||
@@ -344,18 +354,19 @@ 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)
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, pkg.DeviceStatusFailedSmart, smartMdl.Status)
|
||||
require.Equal(t, 0, len(smartMdl.Attributes))
|
||||
}
|
||||
@@ -368,18 +379,19 @@ 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)
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, pkg.DeviceStatusFailedScrutiny|pkg.DeviceStatusFailedSmart, smartMdl.Status)
|
||||
require.Equal(t, 17, len(smartMdl.Attributes))
|
||||
}
|
||||
@@ -392,18 +404,19 @@ 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)
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
|
||||
require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["199"].GetStatus(),
|
||||
"scrutiny should detect that %d failed (status: %d, %s)",
|
||||
@@ -425,18 +438,19 @@ 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)
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
|
||||
require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["media_errors"].GetStatus(),
|
||||
"scrutiny should detect that %s failed (status: %d, %s)",
|
||||
@@ -456,18 +470,19 @@ 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)
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status)
|
||||
require.Equal(t, 16, len(smartMdl.Attributes))
|
||||
|
||||
@@ -483,18 +498,19 @@ 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)
|
||||
|
||||
//test
|
||||
smartMdl := measurements.Smart{}
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
smartUUID := uuid.Must(uuid.NewV4())
|
||||
err = smartMdl.FromCollectorSmartInfo(smartUUID, smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, smartUUID, smartMdl.ScrutinyUUID)
|
||||
require.Equal(t, pkg.DeviceStatusPassed, smartMdl.Status)
|
||||
require.Equal(t, 13, len(smartMdl.Attributes))
|
||||
|
||||
|
||||
+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,10 @@ 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/gofrs/uuid/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -32,7 +33,7 @@ const NotifyFailureTypeSmartFailure = "SmartFailure"
|
||||
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
|
||||
|
||||
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
|
||||
func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes, repeatNotifications bool, c *gin.Context, deviceRepo database.DeviceRepo) bool {
|
||||
func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs measurements.Smart, scrutiny_uuid uuid.UUID, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes, repeatNotifications bool, c *gin.Context, deviceRepo database.DeviceRepo) bool {
|
||||
// 1. check if the device is healthy
|
||||
if device.DeviceStatus == pkg.DeviceStatusPassed {
|
||||
return false
|
||||
@@ -64,7 +65,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
|
||||
@@ -100,7 +101,7 @@ func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs me
|
||||
var lastPoints []measurements.Smart
|
||||
var err error
|
||||
if !repeatNotifications {
|
||||
lastPoints, err = deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), database.DURATION_KEY_FOREVER, 1, 1, failingAttributes)
|
||||
lastPoints, err = deviceRepo.GetSmartAttributeHistory(c, scrutiny_uuid, database.DURATION_KEY_FOREVER, 1, 1, failingAttributes)
|
||||
if err == nil || len(lastPoints) < 1 {
|
||||
logger.Warningln("Could not get the most recent data points from the database. This is expected to happen only if this is the very first submission of data for the device.")
|
||||
}
|
||||
@@ -147,7 +148,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 +319,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 +425,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,10 @@ 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/gofrs/uuid/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
||||
@@ -26,12 +27,12 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
||||
smartAttrs := measurements.Smart{}
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
|
||||
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))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) {
|
||||
@@ -43,11 +44,11 @@ func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T
|
||||
smartAttrs := measurements.Smart{}
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) {
|
||||
@@ -59,11 +60,11 @@ func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.
|
||||
smartAttrs := measurements.Smart{}
|
||||
statusThreshold := pkg.MetricsStatusThresholdSmart
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) {
|
||||
@@ -75,11 +76,11 @@ func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testi
|
||||
smartAttrs := measurements.Smart{}
|
||||
statusThreshold := pkg.MetricsStatusThresholdScrutiny
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
|
||||
@@ -95,12 +96,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t
|
||||
}}
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
|
||||
@@ -119,12 +120,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCritical
|
||||
}}
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
|
||||
@@ -140,12 +141,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(
|
||||
}}
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
|
||||
@@ -161,12 +162,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCritica
|
||||
}}
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
|
||||
@@ -185,12 +186,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho
|
||||
}}
|
||||
statusThreshold := pkg.MetricsStatusThresholdSmart
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -205,13 +206,13 @@ func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
|
||||
}}
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, scrutinyUUID, database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, errors.New("")).Times(1)
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
|
||||
@@ -227,13 +228,13 @@ func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
|
||||
}}
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, scrutinyUUID, database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, nil).Times(1)
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
func TestShouldNotify_NoRepeat(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -249,13 +250,13 @@ func TestShouldNotify_NoRepeat(t *testing.T) {
|
||||
}}
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
scrutinyUUID := uuid.Must(uuid.NewV4())
|
||||
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)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, scrutinyUUID, database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{smartAttrs}, nil).Times(1)
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, scrutinyUUID, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
||||
}
|
||||
|
||||
func TestNewPayload(t *testing.T) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ArchiveDevice(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), true)
|
||||
scrutiny_uuid, err := uuid.FromString(c.Param("scrutiny_uuid"))
|
||||
if err != nil {
|
||||
logger.Errorln("Invalid scrutiny uuid", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
err = deviceRepo.UpdateDeviceArchived(c, scrutiny_uuid, true)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while archiving device", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func DeleteDevice(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
err := deviceRepo.DeleteDevice(c, c.Param("wwn"))
|
||||
scrutiny_uuid, err := uuid.FromString(c.Param("scrutiny_uuid"))
|
||||
if err != nil {
|
||||
logger.Errorln("Invalid scrutiny uuid", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
err = deviceRepo.DeleteDevice(c, scrutiny_uuid)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while deleting device", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
|
||||
@@ -6,14 +6,20 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func GetDeviceDetails(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
device, err := deviceRepo.GetDeviceDetails(c, c.Param("wwn"))
|
||||
scrutiny_uuid, err := uuid.FromString(c.Param("scrutiny_uuid"))
|
||||
if err != nil {
|
||||
logger.Errorln("Invalid scrutiny uuid", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
device, err := deviceRepo.GetDeviceDetails(c, scrutiny_uuid)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while retrieving device details", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
@@ -25,7 +31,7 @@ func GetDeviceDetails(c *gin.Context) {
|
||||
durationKey = "forever"
|
||||
}
|
||||
|
||||
smartResults, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), durationKey, 0, 0, nil)
|
||||
smartResults, err := deviceRepo.GetSmartAttributeHistory(c, scrutiny_uuid, durationKey, 0, 0, nil)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while retrieving device smart results", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetDevicesSummary(c *gin.Context) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// register devices that are detected by various collectors.
|
||||
@@ -23,9 +24,9 @@ func RegisterDevices(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
//filter any device with empty wwn (they are invalid)
|
||||
// Filter any device without a scrutiny UUID. This should never happen...
|
||||
detectedStorageDevices := lo.Filter[models.Device](collectorDeviceWrapper.Data, func(dev models.Device, _ int) bool {
|
||||
return len(dev.WWN) > 0
|
||||
return !dev.ScrutinyUUID.IsNil()
|
||||
})
|
||||
|
||||
errs := []error{}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func UnarchiveDevice(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), false)
|
||||
scrutiny_uuid, err := uuid.FromString(c.Param("scrutiny_uuid"))
|
||||
if err != nil {
|
||||
logger.Errorln("Invalid scrutiny uuid", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
err = deviceRepo.UpdateDeviceArchived(c, scrutiny_uuid, false)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while unarchiving device", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/notify"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -22,12 +23,15 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
|
||||
//appConfig := c.MustGet("CONFIG").(config.Interface)
|
||||
|
||||
if c.Param("wwn") == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||
scrutiny_uuid, err := uuid.FromString(c.Param("scrutiny_uuid"))
|
||||
if err != nil {
|
||||
logger.Errorln("Invalid scrutiny uuid", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
var collectorSmartData collector.SmartInfo
|
||||
err := c.BindJSON(&collectorSmartData)
|
||||
err = c.BindJSON(&collectorSmartData)
|
||||
if err != nil {
|
||||
logger.Errorln("Cannot parse SMART data", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
@@ -35,7 +39,7 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
}
|
||||
|
||||
//update the device information if necessary
|
||||
updatedDevice, err := deviceRepo.UpdateDevice(c, c.Param("wwn"), collectorSmartData)
|
||||
updatedDevice, err := deviceRepo.UpdateDevice(c, scrutiny_uuid, collectorSmartData)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while updating device data from smartctl metrics:", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
@@ -43,7 +47,7 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
}
|
||||
|
||||
// insert smart info
|
||||
smartData, err := deviceRepo.SaveSmartAttributes(c, c.Param("wwn"), collectorSmartData)
|
||||
smartData, err := deviceRepo.SaveSmartAttributes(c, scrutiny_uuid, collectorSmartData)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while saving smartctl metrics", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
@@ -52,7 +56,7 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
|
||||
if smartData.Status != pkg.DeviceStatusPassed {
|
||||
//there is a failure detected by Scrutiny, update the device status on the homepage.
|
||||
updatedDevice, err = deviceRepo.UpdateDeviceStatus(c, c.Param("wwn"), smartData.Status)
|
||||
updatedDevice, err = deviceRepo.UpdateDeviceStatus(c, scrutiny_uuid, smartData.Status)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while updating device status", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
@@ -61,7 +65,7 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
}
|
||||
|
||||
// save smart temperature data (ignore failures)
|
||||
err = deviceRepo.SaveSmartTemperature(c, c.Param("wwn"), updatedDevice.DeviceProtocol, collectorSmartData, appConfig.GetBool(fmt.Sprintf("%s.collector.discard_sct_temp_history", config.DB_USER_SETTINGS_SUBKEY)))
|
||||
err = deviceRepo.SaveSmartTemperature(c, scrutiny_uuid, updatedDevice.DeviceProtocol, collectorSmartData, appConfig.GetBool(fmt.Sprintf("%s.collector.discard_sct_temp_history", config.DB_USER_SETTINGS_SUBKEY)))
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while saving smartctl temp data", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
@@ -73,6 +77,7 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
logger,
|
||||
updatedDevice,
|
||||
smartData,
|
||||
scrutiny_uuid,
|
||||
pkg.MetricsStatusThreshold(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY))),
|
||||
pkg.MetricsStatusFilterAttributes(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY))),
|
||||
appConfig.GetBool(fmt.Sprintf("%s.metrics.repeat_notifications", config.DB_USER_SETTINGS_SUBKEY)),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||
@@ -9,9 +13,6 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/web/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AppEngine struct {
|
||||
@@ -37,15 +38,15 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
|
||||
api.GET("/health", handler.HealthCheck)
|
||||
api.POST("/health/notify", handler.SendTestNotification) //check if notifications are configured correctly
|
||||
|
||||
api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list
|
||||
api.GET("/summary", handler.GetDevicesSummary) //used by Dashboard
|
||||
api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown)
|
||||
api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data
|
||||
api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests)
|
||||
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
|
||||
api.POST("/device/:wwn/archive", handler.ArchiveDevice) //used by UI to archive device
|
||||
api.POST("/device/:wwn/unarchive", handler.UnarchiveDevice) //used by UI to unarchive device
|
||||
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
|
||||
api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list
|
||||
api.GET("/summary", handler.GetDevicesSummary) //used by Dashboard
|
||||
api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown)
|
||||
api.POST("/device/:scrutiny_uuid/smart", handler.UploadDeviceMetrics) //used by Collector to upload data
|
||||
api.POST("/device/:scrutiny_uuid/selftest", handler.UploadDeviceSelfTests)
|
||||
api.GET("/device/:scrutiny_uuid/details", handler.GetDeviceDetails) //used by Details
|
||||
api.POST("/device/:scrutiny_uuid/archive", handler.ArchiveDevice) //used by UI to archive device
|
||||
api.POST("/device/:scrutiny_uuid/unarchive", handler.UnarchiveDevice) //used by UI to unarchive device
|
||||
api.DELETE("/device/:scrutiny_uuid", handler.DeleteDevice) //used by UI to delete device
|
||||
|
||||
api.GET("/settings", handler.GetSettings) //used to get settings
|
||||
api.POST("/settings", handler.SaveSettings) //used to save settings
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -20,10 +19,11 @@ 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/gofrs/uuid/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -36,7 +36,7 @@ docker run --rm -it -p 8086:8086 \
|
||||
-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.0
|
||||
influxdb:2.2
|
||||
*/
|
||||
|
||||
//func TestMain(m *testing.M) {
|
||||
@@ -52,7 +52,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 +87,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 +130,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 +172,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)
|
||||
@@ -220,7 +217,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
require.Equal(suite.T(), 200, wr.Code)
|
||||
|
||||
mr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264eb01d7/smart", metricsfile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/9a4d34b5-b2ee-51ef-8506-90eea09be417/smart", metricsfile)
|
||||
router.ServeHTTP(mr, req)
|
||||
require.Equal(suite.T(), 200, mr.Code)
|
||||
|
||||
@@ -229,10 +226,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)
|
||||
@@ -280,28 +276,31 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
router.ServeHTTP(wr, req)
|
||||
require.Equal(suite.T(), 200, wr.Code)
|
||||
|
||||
// NOTE: The scrutiny_uuid's below must come from devicesfile because those get inserted into the database.
|
||||
// They don't match the scrutiny_uuid that would be derived from the smart info files because the drives
|
||||
// in those files don't match those in the registration. Currently, scrutiny does not reconcile the two.
|
||||
mr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264eb01d7/smart", metricsfile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c/smart", metricsfile)
|
||||
router.ServeHTTP(mr, req)
|
||||
require.Equal(suite.T(), 200, mr.Code)
|
||||
|
||||
fr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264ec3183/smart", failfile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/3ea22b35-682b-49fb-a655-abffed108e48/smart", failfile)
|
||||
router.ServeHTTP(fr, req)
|
||||
require.Equal(suite.T(), 200, fr.Code)
|
||||
|
||||
nr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5002538e40a22954/smart", nvmefile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/d8796fe7-2422-520c-8991-e970993dad3e/smart", nvmefile)
|
||||
router.ServeHTTP(nr, req)
|
||||
require.Equal(suite.T(), 200, nr.Code)
|
||||
|
||||
sr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca252c859cc/smart", scsifile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/00328b73-9f8a-53ad-8f20-8d0b1be00f47/smart", scsifile)
|
||||
router.ServeHTTP(sr, req)
|
||||
require.Equal(suite.T(), 200, sr.Code)
|
||||
|
||||
s2r := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/0x5000cca264ebc248/smart", scsi2file)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/e5ccc378-24fc-5a9d-b1ce-8732096a9ea5/smart", scsi2file)
|
||||
router.ServeHTTP(s2r, req)
|
||||
require.Equal(suite.T(), 200, s2r.Code)
|
||||
|
||||
@@ -311,10 +310,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 +333,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 +378,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 +423,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 +468,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 +512,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)
|
||||
@@ -566,7 +559,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
require.Equal(suite.T(), 200, wr.Code)
|
||||
|
||||
mr := httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/a4c8e8ed-11a0-4c97-9bba-306440f1b944/smart", metricsfile)
|
||||
req, _ = http.NewRequest("POST", suite.Basepath+"/api/device/bde1d2d2-7e5c-525a-8327-6adbfa382637/smart", metricsfile)
|
||||
router.ServeHTTP(mr, req)
|
||||
require.Equal(suite.T(), 200, mr.Code)
|
||||
|
||||
@@ -579,6 +572,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
//assert
|
||||
require.Equal(suite.T(), "a4c8e8ed-11a0-4c97-9bba-306440f1b944", deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.WWN)
|
||||
require.Equal(suite.T(), pkg.DeviceStatusPassed, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
|
||||
deviceUUIDString := "bde1d2d2-7e5c-525a-8327-6adbfa382637"
|
||||
deviceUUID := uuid.Must(uuid.FromString(deviceUUIDString))
|
||||
require.Equal(suite.T(), deviceUUID, deviceSummary.Data.Summary[deviceUUIDString].Device.ScrutinyUUID)
|
||||
require.Equal(suite.T(), pkg.DeviceStatusPassed, deviceSummary.Data.Summary[deviceUUIDString].Device.DeviceStatus)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"device_protocol": "NVMe",
|
||||
"device_type": "nvme"
|
||||
"device_type": "nvme",
|
||||
"scrutiny_uuid": "bde1d2d2-7e5c-525a-8327-6adbfa382637"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+17
-10
@@ -12,27 +12,29 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 500107862016,
|
||||
"form_factor": "",
|
||||
"smart_support": false
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c"
|
||||
},
|
||||
{
|
||||
"wwn": "0x5000cca264eb01d7",
|
||||
"device_name": "sdb",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"model_name": "WDC WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
"interface_speed": "",
|
||||
"serial_number": "9RK1XXXXX",
|
||||
"serial_number": "9RK1XXXX",
|
||||
"firmware": "",
|
||||
"rotational_speed": 0,
|
||||
"capacity": 14000519643136,
|
||||
"form_factor": "",
|
||||
"smart_support": false
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "3ea22b35-682b-49fb-a655-abffed108e48"
|
||||
},
|
||||
{
|
||||
"wwn": "0x5000cca264ec3183",
|
||||
"device_name": "sdc",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"model_name": "WDC WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
"interface_speed": "",
|
||||
"serial_number": "9RK4XXXXX",
|
||||
@@ -40,7 +42,8 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 14000519643136,
|
||||
"form_factor": "",
|
||||
"smart_support": false
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "42caca8a-9b95-5c75-b059-305771a2a193"
|
||||
},
|
||||
{
|
||||
"wwn": "0x5000cca252c859cc",
|
||||
@@ -54,7 +57,8 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 8001563222016,
|
||||
"form_factor": "",
|
||||
"smart_support": false
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "d8796fe7-2422-520c-8991-e970993dad3e"
|
||||
},
|
||||
{
|
||||
"wwn": "0x5000cca264ebc248",
|
||||
@@ -68,7 +72,8 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 14000519643136,
|
||||
"form_factor": "",
|
||||
"smart_support": false
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "00328b73-9f8a-53ad-8f20-8d0b1be00f47"
|
||||
},
|
||||
{
|
||||
"wwn": "0x50014ee20b2a72a9",
|
||||
@@ -82,7 +87,8 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 6001175126016,
|
||||
"form_factor": "",
|
||||
"smart_support": false
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "e5ccc378-24fc-5a9d-b1ce-8732096a9ea5"
|
||||
},
|
||||
{
|
||||
"wwn": "0x5000c500673e6b5f",
|
||||
@@ -96,7 +102,8 @@
|
||||
"rotational_speed": 0,
|
||||
"capacity": 6001175126016,
|
||||
"form_factor": "",
|
||||
"smart_support": false
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "acfbce7d-0e19-579b-895e-85809dab63fb"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
"wwn": "0x5000cca264eb01d7",
|
||||
"device_name": "sdb",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"model_name": "WDC WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
"interface_speed": "",
|
||||
"serial_number": "9RK1XXXXX",
|
||||
"serial_number": "9RK1XXXX",
|
||||
"firmware": "",
|
||||
"rotational_speed": 0,
|
||||
"capacity": 14000519643136,
|
||||
"form_factor": "",
|
||||
"smart_support": false
|
||||
"smart_support": false,
|
||||
"scrutiny_uuid": "9a4d34b5-b2ee-51ef-8506-90eea09be417"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+399
-166
@@ -24,7 +24,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",
|
||||
@@ -3993,23 +3993,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.2",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.14.0",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
@@ -4200,6 +4201,7 @@
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -4263,6 +4265,36 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -5572,6 +5604,20 @@
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -5741,12 +5787,42 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
|
||||
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
@@ -6207,6 +6283,7 @@
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -6265,45 +6342,50 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"body-parser": "~1.20.3",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"qs": "~6.14.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/array-flatten": {
|
||||
@@ -6312,35 +6394,12 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/express/node_modules/body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -6350,22 +6409,34 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"node_modules/express/node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "2.0.1",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6376,28 +6447,15 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/express/node_modules/raw-body": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -6599,9 +6657,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -6609,6 +6667,7 @@
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
@@ -6668,6 +6727,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -6725,9 +6785,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/functions-have-names": {
|
||||
"version": "1.2.3",
|
||||
@@ -6775,13 +6839,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
|
||||
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.3"
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -6796,6 +6871,19 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -6884,6 +6972,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -6945,6 +7045,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
@@ -6994,9 +7095,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -7024,6 +7126,18 @@
|
||||
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hdr-histogram-js": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz",
|
||||
@@ -7146,26 +7260,32 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -8020,10 +8140,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
@@ -8576,9 +8697,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
@@ -8809,6 +8931,15 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -8831,10 +8962,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
|
||||
"dev": true
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
@@ -9272,10 +9407,11 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
|
||||
"integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
|
||||
"dev": true,
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
@@ -9747,10 +9883,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -10205,10 +10345,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
|
||||
"dev": true
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
@@ -11353,12 +11494,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -11449,15 +11591,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -12144,24 +12287,25 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -12172,6 +12316,7 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@@ -12180,13 +12325,25 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
@@ -12198,22 +12355,25 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
|
||||
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
@@ -12288,20 +12448,31 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@@ -12354,14 +12525,76 @@
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -38,7 +38,7 @@ export const appRoutes: Route[] = [
|
||||
|
||||
// Example
|
||||
{path: 'dashboard', loadChildren: () => import('app/modules/dashboard/dashboard.module').then(m => m.DashboardModule)},
|
||||
{path: 'device/:wwn', loadChildren: () => import('app/modules/detail/detail.module').then(m => m.DetailModule)}
|
||||
{path: 'device/:scrutiny_uuid', loadChildren: () => import('app/modules/detail/detail.module').then(m => m.DetailModule)}
|
||||
|
||||
// 404 & Catch all
|
||||
// {path: '404-not-found', pathMatch: 'full', loadChildren: () => import('app/modules/admin/pages/errors/error-404/error-404.module').then(m => m.Error404Module)},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// maps to webapp/backend/pkg/models/device.go
|
||||
export interface DeviceModel {
|
||||
archived?: boolean;
|
||||
scrutiny_uuid: string;
|
||||
wwn: string;
|
||||
device_name?: string;
|
||||
device_uuid?: string;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {SmartAttributeModel} from './smart-attribute-model';
|
||||
export interface SmartModel {
|
||||
date: string;
|
||||
device_wwn: string;
|
||||
scrutiny_uuid: string;
|
||||
device_protocol: string;
|
||||
|
||||
temp: number;
|
||||
|
||||
@@ -40,7 +40,7 @@ export class DetailsMockApi implements TreoMockApi
|
||||
register(): void
|
||||
{
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/0x5002538e40a22954/details')
|
||||
.onGet('/api/device/ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
@@ -50,7 +50,7 @@ export class DetailsMockApi implements TreoMockApi
|
||||
});
|
||||
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/0x5000cca264eb01d7/details')
|
||||
.onGet('/api/device/3ea22b35-682b-49fb-a655-abffed108e48/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
@@ -60,7 +60,7 @@ export class DetailsMockApi implements TreoMockApi
|
||||
});
|
||||
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/0x5000cca264ec3183/details')
|
||||
.onGet('/api/device/42caca8a-9b95-5c75-b059-305771a2a193/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
@@ -70,7 +70,7 @@ export class DetailsMockApi implements TreoMockApi
|
||||
});
|
||||
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/0x5000cca252c859cc/details')
|
||||
.onGet('/api/device/d8796fe7-2422-520c-8991-e970993dad3e/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
@@ -80,7 +80,17 @@ export class DetailsMockApi implements TreoMockApi
|
||||
});
|
||||
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/0x5000cca264ebc248/details')
|
||||
.onGet('/api/device/00328b73-9f8a-53ad-8f20-8d0b1be00f47/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
200,
|
||||
_.cloneDeep(sde)
|
||||
];
|
||||
});
|
||||
|
||||
this._treoMockApiService
|
||||
.onGet('/api/device/e5ccc378-24fc-5a9d-b1ce-8732096a9ea5/details')
|
||||
.reply(() => {
|
||||
|
||||
return [
|
||||
|
||||
@@ -5,6 +5,7 @@ export const sda = {
|
||||
'UpdatedAt': '2021-10-24T16:37:56.981833-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5002538e40a22954',
|
||||
'scrutiny_uuid': 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c',
|
||||
'device_name': 'sda',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'Samsung_SSD_860_EVO_500GB',
|
||||
@@ -26,6 +27,7 @@ export const sda = {
|
||||
'smart_results': [{
|
||||
'date': '2021-10-24T23:20:44Z',
|
||||
'device_wwn': '0x5002538e40a22954',
|
||||
'scrutiny_uuid': 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c',
|
||||
'device_protocol': 'NVMe',
|
||||
'temp': 36,
|
||||
'power_on_hours': 2401,
|
||||
|
||||
@@ -5,12 +5,13 @@ export const sdb = {
|
||||
'UpdatedAt': '2021-10-24T17:06:39.436996-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000cca264eb01d7',
|
||||
'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48',
|
||||
'device_name': 'sdb',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'WDC_WD140EDFZ-11A0VA0',
|
||||
'interface_type': 'SCSI',
|
||||
'interface_speed': '',
|
||||
'serial_number': '9RK1XXXXX',
|
||||
'serial_number': '9RK1XXXX',
|
||||
'firmware': '81.00A81',
|
||||
'rotational_speed': 0,
|
||||
'capacity': 14000519643136,
|
||||
@@ -25,6 +26,7 @@ export const sdb = {
|
||||
'smart_results': [{
|
||||
'date': '2021-10-24T20:34:04Z',
|
||||
'device_wwn': '0x5000cca264eb01d7',
|
||||
'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48',
|
||||
'device_protocol': 'ATA',
|
||||
'temp': 32,
|
||||
'power_on_hours': 1730,
|
||||
@@ -245,6 +247,7 @@ export const sdb = {
|
||||
}, {
|
||||
'date': '2021-10-24T23:20:44Z',
|
||||
'device_wwn': '0x5000cca264eb01d7',
|
||||
'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48',
|
||||
'device_protocol': 'ATA',
|
||||
'temp': 32,
|
||||
'power_on_hours': 1730,
|
||||
|
||||
@@ -5,6 +5,7 @@ export const sdc = {
|
||||
'UpdatedAt': '2021-10-24T16:37:56.74865-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000cca264ec3183',
|
||||
'scrutiny_uuid': '42caca8a-9b95-5c75-b059-305771a2a193',
|
||||
'device_name': 'sdc',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'WDC_WD140EDFZ-11A0VA0',
|
||||
@@ -25,6 +26,7 @@ export const sdc = {
|
||||
'smart_results': [{
|
||||
'date': '2021-10-24T23:20:44Z',
|
||||
'device_wwn': '0x5000cca264ec3183',
|
||||
'scrutiny_uuid': '42caca8a-9b95-5c75-b059-305771a2a193',
|
||||
'device_protocol': 'ATA',
|
||||
'temp': 25,
|
||||
'power_on_hours': 65592,
|
||||
|
||||
@@ -5,6 +5,7 @@ export const sdd = {
|
||||
'UpdatedAt': '2021-10-24T16:37:57.013758-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000cca252c859cc',
|
||||
'scrutiny_uuid': 'd8796fe7-2422-520c-8991-e970993dad3e',
|
||||
'device_name': 'sdd',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'WDC_WD80EFAX-68LHPN0',
|
||||
@@ -25,6 +26,7 @@ export const sdd = {
|
||||
'smart_results': [{
|
||||
'date': '2021-10-24T23:20:44Z',
|
||||
'device_wwn': '0x5000cca252c859cc',
|
||||
'scrutiny_uuid': 'd8796fe7-2422-520c-8991-e970993dad3e',
|
||||
'device_protocol': 'SCSI',
|
||||
'temp': 34,
|
||||
'power_on_hours': 43549,
|
||||
|
||||
@@ -5,6 +5,7 @@ export const sde = {
|
||||
'UpdatedAt': '2021-10-24T16:40:16.495248-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000cca264ebc248',
|
||||
'scrutiny_uuid': '00328b73-9f8a-53ad-8f20-8d0b1be00f47',
|
||||
'device_name': 'sde',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'WDC_WD140EDFZ-11A0VA0',
|
||||
@@ -25,6 +26,7 @@ export const sde = {
|
||||
'smart_results': [{
|
||||
'date': '2021-10-24T23:20:44Z',
|
||||
'device_wwn': '0x5000cca264ebc248',
|
||||
'scrutiny_uuid': '00328b73-9f8a-53ad-8f20-8d0b1be00f47',
|
||||
'device_protocol': 'SCSI',
|
||||
'temp': 31,
|
||||
'power_on_hours': 5675,
|
||||
|
||||
@@ -5,6 +5,7 @@ export const sdf = {
|
||||
'UpdatedAt': '2021-06-24T21:17:31.305246-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x50014ee20b2a72a9',
|
||||
'scrutiny_uuid': 'e5ccc378-24fc-5a9d-b1ce-8732096a9ea5',
|
||||
'device_name': 'sdf',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'WDC_WD60EFRX-68MYMN1',
|
||||
|
||||
@@ -4,12 +4,13 @@ import * as moment from 'moment';
|
||||
export const summary = {
|
||||
'data': {
|
||||
'summary': {
|
||||
'0x5000c500673e6b5f': {
|
||||
'acfbce7d-0e19-579b-895e-85809dab63fb': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-04-30T08:17:13.155217-07:00',
|
||||
'UpdatedAt': '2021-04-30T08:17:13.155217-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000c500673e6b5f',
|
||||
'scrutiny_uuid': 'acfbce7d-0e19-579b-895e-85809dab63fb',
|
||||
'device_name': 'sdg',
|
||||
'device_label': '14TB-WD-DRIVE2',
|
||||
'device_uuid': '',
|
||||
@@ -32,12 +33,13 @@ export const summary = {
|
||||
'archived': false
|
||||
}
|
||||
},
|
||||
'0x5000cca252c859cc': {
|
||||
'd8796fe7-2422-520c-8991-e970993dad3e': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-04-30T08:17:13.152705-07:00',
|
||||
'UpdatedAt': '2021-05-02T14:22:50.357164-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000cca252c859cc',
|
||||
'scrutiny_uuid': 'd8796fe7-2422-520c-8991-e970993dad3e',
|
||||
'device_name': 'sdd',
|
||||
'device_label': '14TB-WD-DRIVE1',
|
||||
'device_uuid': '806cf4bc-d160-4d96-8ee9-3ab7cf2a2e1f',
|
||||
@@ -69,21 +71,22 @@ export const summary = {
|
||||
'temp': 34
|
||||
}]
|
||||
},
|
||||
'0x5000cca264eb01d7': {
|
||||
'3ea22b35-682b-49fb-a655-abffed108e48': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-04-28T20:52:49.047154-07:00',
|
||||
'UpdatedAt': '2021-05-02T14:22:49.86136-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000cca264eb01d7',
|
||||
'scrutiny_uuid': '3ea22b35-682b-49fb-a655-abffed108e48',
|
||||
'device_name': 'sdb',
|
||||
'device_label': '14TB-WD-DRIVE5',
|
||||
'device_uuid': '8125ec6d-a7e4-4950-ac84-72d6a4d67128',
|
||||
'device_serial_id': 'ata-WDC_WD140EDFZ-11A0VA0-9RK1XXXXX',
|
||||
'device_serial_id': 'ata-WDC_WD140EDFZ-11A0VA0-9RK1XXXX',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'WDC_WD140EDFZ-11A0VA0',
|
||||
'interface_type': 'SCSI',
|
||||
'interface_speed': '',
|
||||
'serial_number': '9RK1XXXXX',
|
||||
'serial_number': '9RK1XXXX',
|
||||
'firmware': '81.00A81',
|
||||
'rotational_speed': 0,
|
||||
'capacity': 14000519643136,
|
||||
@@ -106,12 +109,13 @@ export const summary = {
|
||||
'temp': 32
|
||||
}]
|
||||
},
|
||||
'0x5000cca264ebc248': {
|
||||
'00328b73-9f8a-53ad-8f20-8d0b1be00f47': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-04-30T08:17:13.153782-07:00',
|
||||
'UpdatedAt': '2021-05-02T14:22:50.385282-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000cca264ebc248',
|
||||
'scrutiny_uuid': '00328b73-9f8a-53ad-8f20-8d0b1be00f47',
|
||||
'device_name': 'sde',
|
||||
'device_label': '14TB-WD-DRIVE3',
|
||||
'device_uuid': '9eb60cde-d6d0-4172-b520-b241a6a5477f',
|
||||
@@ -134,12 +138,13 @@ export const summary = {
|
||||
'archived': false
|
||||
}
|
||||
},
|
||||
'0x5000cca264ec3183': {
|
||||
'42caca8a-9b95-5c75-b059-305771a2a193': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-04-30T08:17:13.151906-07:00',
|
||||
'UpdatedAt': '2021-05-02T14:49:51.645012-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000cca264ec3183',
|
||||
'scrutiny_uuid': '42caca8a-9b95-5c75-b059-305771a2a193',
|
||||
'device_name': 'sdc',
|
||||
'device_label': '14TB-WD-DRIVE6',
|
||||
'device_uuid': 'e1378723-7861-49b9-8e01-0bd063f0ecdd',
|
||||
@@ -555,12 +560,13 @@ export const summary = {
|
||||
'temp': 39
|
||||
}]
|
||||
},
|
||||
'0x50014ee20b2a72a9': {
|
||||
'e5ccc378-24fc-5a9d-b1ce-8732096a9ea5': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-04-30T08:17:13.15451-07:00',
|
||||
'UpdatedAt': '2021-04-30T08:17:13.15451-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x50014ee20b2a72a9',
|
||||
'scrutiny_uuid': 'e5ccc378-24fc-5a9d-b1ce-8732096a9ea5',
|
||||
'device_name': 'sdf',
|
||||
'device_label': '8.0TB-WD-4',
|
||||
'device_uuid': 'fc684dcc-aa2f-44f3-a958-d302dc7dd46d',
|
||||
@@ -583,12 +589,13 @@ export const summary = {
|
||||
'archived': false
|
||||
}
|
||||
},
|
||||
'0x5002538e40a22954': {
|
||||
'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-04-30T08:17:13.150792-07:00',
|
||||
'UpdatedAt': '2021-05-02T14:22:50.330706-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5002538e40a22954',
|
||||
'scrutiny_uuid': 'ecfaaf20-d1f6-558b-b33a-3e8db19a6c2c',
|
||||
'device_name': 'sda',
|
||||
'device_label': '',
|
||||
'device_uuid': '',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user