Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eefdc548b2 | |||
| fb918e2d6e | |||
| 3d9001a5e4 | |||
| fbe7d63a24 | |||
| d718b0898b | |||
| 44c7211b5f | |||
| 157c93b967 | |||
| 7babc280a0 | |||
| e364e480e8 | |||
| bfefe7e98a | |||
| 831cca7853 | |||
| 46f3b1c02c | |||
| 8a1ae2ffa0 | |||
| 145c819fc1 | |||
| a9ea231de0 | |||
| c2488af1c3 | |||
| ecf7a447a7 | |||
| f8e61af2f9 | |||
| ee61d986d8 | |||
| 8fe8cec09a | |||
| b953456d6b | |||
| 4057699cad | |||
| d3e7fc6067 | |||
| 09a8574d83 | |||
| 7695cc185f | |||
| fc7208020e | |||
| 75d5930835 | |||
| 3c9e16169e | |||
| 9e1076f302 | |||
| 75ab87e109 | |||
| 0b8251fce2 | |||
| f57b71ae96 | |||
| ce324c3de1 | |||
| 281b56d287 | |||
| cbd23e334b | |||
| 7a0b9c9e0d | |||
| 44b3d982dd | |||
| 769f253e7d | |||
| fbd5bb57ac | |||
| b9eb5687cd | |||
| cbd230a7e0 | |||
| 892e9685f3 | |||
| 7ba7b6efda | |||
| 453069deec | |||
| de5f2c3324 | |||
| d486f14433 | |||
| cb47dd7185 | |||
| 6ae4d233cd | |||
| f8bb185854 | |||
| 1da07caaa6 | |||
| fe96c27732 | |||
| 7287775cca | |||
| 28ac3ac7ec | |||
| a6208c0d49 | |||
| 7840fe66da | |||
| 2ca44c967e | |||
| 4b767421f3 | |||
| 6005b8609a | |||
| df23ecdf33 | |||
| f4988cbac5 | |||
| f4f5d16b4a | |||
| 1c4dd33381 | |||
| 9e0ba4d269 | |||
| d9ecf6c0d3 | |||
| 8051ad4dde | |||
| 165f98dc09 | |||
| ca7772250c | |||
| 6e02e4da02 | |||
| 9c8498cea7 | |||
| 965fbb08da | |||
| e16933eeac | |||
| 4d0fc0eae8 | |||
| 8296a973b8 | |||
| 19a9957755 | |||
| 02e3947906 | |||
| 766a73455e | |||
| 6e64ae09aa | |||
| 411eca20e0 | |||
| 0243d9e2fa | |||
| 9aa0e97be0 | |||
| 488fcfc820 | |||
| b5dad487e5 | |||
| 8b01187892 | |||
| d9d6ce0f30 | |||
| 8d203b3547 | |||
| fe5dbcff1e | |||
| 99df104cdd | |||
| a53397210c | |||
| 2533d8d34f | |||
| af2523cfee | |||
| c6e1663f8a | |||
| ab83c389f7 | |||
| 6d22702864 | |||
| d78957353d | |||
| b208493af9 | |||
| 4aa1485246 | |||
| e1e1d321dd | |||
| 3971b37abc | |||
| cf1bd3ea6b | |||
| 9b901766e3 | |||
| e19ee78e70 | |||
| c7c55ab95c | |||
| d7ddf01ea0 | |||
| c539af1a67 | |||
| d93d24b52d | |||
| 5dbfad68ad | |||
| 92c4506cfa | |||
| fe80bed6bd | |||
| b6e69021b2 | |||
| 12e624a496 | |||
| e95b44c690 | |||
| 4ee947d55c | |||
| 21212c0a1d | |||
| d1376a2200 | |||
| 7d2daf4f6a | |||
| da4562d308 | |||
| f51de52ff7 | |||
| 987632df39 | |||
| 28a3c3e53f | |||
| 1bd86f5abd | |||
| 989fbc25f8 | |||
| 0f935ceb48 | |||
| f844a435fd | |||
| 3a970e7a27 | |||
| 307c2bcdef | |||
| d62928aaae | |||
| d08a1e3ef6 | |||
| 2292041f9f | |||
| 75e4bf1d6e | |||
| 97add04276 | |||
| 1423f55d78 | |||
| 46d0b70399 | |||
| 168ca802d1 | |||
| 8c07e91f39 | |||
| 7979950c3b | |||
| 9846ba13e0 | |||
| 83839f7faf | |||
| 85fa3b1f8f | |||
| 4190f9a633 | |||
| 743ce27d2e | |||
| 399a2450ff | |||
| 934f16f0a5 | |||
| 0aeb13c181 | |||
| 5899bf2026 | |||
| 3b137964fc | |||
| 1bfdd0043f | |||
| 999c12748c | |||
| 6f283fd736 |
@@ -1,4 +1,3 @@
|
||||
/dist
|
||||
/vendor
|
||||
/.idea
|
||||
/.github
|
||||
|
||||
@@ -22,20 +22,21 @@ See [/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](docs/TROUBLESHOOTING_DEVICE_COLL
|
||||
|
||||
```
|
||||
docker run -it --rm -p 8080:8080 \
|
||||
-v `pwd`/config:/opt/scrutiny/config \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
--cap-add SYS_RAWIO \
|
||||
--device=/dev/sda \
|
||||
--device=/dev/sdb \
|
||||
-e DEBUG=true \
|
||||
-e COLLECTOR_LOG_FILE=/tmp/collector.log \
|
||||
-e SCRUTINY_LOG_FILE=/tmp/web.log \
|
||||
-e COLLECTOR_LOG_FILE=/opt/scrutiny/config/collector.log \
|
||||
-e SCRUTINY_LOG_FILE=/opt/scrutiny/config/web.log \
|
||||
--name scrutiny \
|
||||
ghcr.io/analogj/scrutiny:master-omnibus
|
||||
|
||||
# in another terminal trigger the collector
|
||||
docker exec scrutiny scrutiny-collector-metrics run
|
||||
|
||||
# then use docker cp to copy the log files out of the container.
|
||||
docker cp scrutiny:/tmp/collector.log collector.log
|
||||
docker cp scrutiny:/tmp/web.log web.log
|
||||
```
|
||||
|
||||
The log files will be available on your host in the `config` directory. Please attach them to this issue.
|
||||
|
||||
Please also provide the output of `docker info`
|
||||
@@ -1,7 +1,5 @@
|
||||
name: Docker
|
||||
on:
|
||||
schedule:
|
||||
- cron: '36 12 * * *'
|
||||
push:
|
||||
branches: [ master, beta ]
|
||||
# Publish semver tags as releases.
|
||||
@@ -20,9 +18,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Login against a Docker registry except on PR
|
||||
@@ -40,6 +42,8 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,enable=true,event=branch,suffix=-collector
|
||||
type=ref,enable=true,event=tag,suffix=-collector
|
||||
@@ -56,8 +60,8 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -68,8 +72,21 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: "Generate frontend"
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: node:lts
|
||||
options: -v ${{ github.workspace }}:/work
|
||||
run: |
|
||||
cd /work
|
||||
make frontend && echo "print contents of /work/dist" && ls -alt /work/dist
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Login against a Docker registry except on PR
|
||||
@@ -87,11 +104,12 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,enable=true,event=branch,suffix=-web
|
||||
type=ref,enable=true,event=tag,suffix=-web
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
@@ -103,8 +121,8 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
omnibus:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -114,8 +132,22 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: "Generate frontend & version information"
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: node:lts
|
||||
options: -v ${{ github.workspace }}:/work
|
||||
run: |
|
||||
cd /work
|
||||
make frontend && echo "print contents of /work/dist" && ls -alt /work/dist
|
||||
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Login against a Docker registry except on PR
|
||||
@@ -137,7 +169,6 @@ jobs:
|
||||
type=ref,enable=true,event=branch,suffix=-omnibus
|
||||
type=ref,enable=true,event=tag,suffix=-omnibus
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
@@ -149,5 +180,5 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Docker - Nightly
|
||||
on:
|
||||
schedule:
|
||||
- cron: '36 12 * * *'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
omnibus:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: "Generate frontend & version information"
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: node:lts
|
||||
options: -v ${{ github.workspace }}:/work
|
||||
run: |
|
||||
cd /work
|
||||
make frontend && echo "print contents of /work/dist" && ls -alt /work/dist
|
||||
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
tags: |
|
||||
type=ref,enable=true,event=branch,suffix=-omnibus-nightly
|
||||
type=ref,enable=true,event=tag,suffix=-omnibus-nightly
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: false
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
@@ -15,13 +15,15 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{github.event.release.tag_name}}
|
||||
- name: "Generate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: Build Frontend
|
||||
run: |
|
||||
cd webapp/frontend
|
||||
npm install -g @angular/cli@9.1.4
|
||||
npm install
|
||||
mkdir -p dist
|
||||
ng build --output-path=dist --deploy-url="/web/" --base-href="/web/" --prod
|
||||
npm run build:prod -- --output-path=dist
|
||||
tar -czf scrutiny-web-frontend.tar.gz dist
|
||||
- name: Upload Frontend Asset
|
||||
id: upload-release-asset3
|
||||
|
||||
+125
-71
@@ -1,75 +1,109 @@
|
||||
# Contributing
|
||||
|
||||
There are multiple ways to develop on the scrutiny codebase locally. The two most popular are:
|
||||
- Docker Development Container - only requires docker
|
||||
- Run Components Locally - requires smartmontools, golang & nodejs installed locally
|
||||
The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) containing source code for:
|
||||
- Scrutiny Backend Server (API)
|
||||
- Scrutiny Frontend Angular SPA
|
||||
- S.M.A.R.T Collector
|
||||
|
||||
## Docker Development
|
||||
```
|
||||
docker build -f docker/Dockerfile . -t chcr.io/analogj/scrutiny:master-omnibus
|
||||
docker run -it --rm -p 8080:8080 \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
--cap-add SYS_RAWIO \
|
||||
--device=/dev/sda \
|
||||
--device=/dev/sdb \
|
||||
ghcr.io/analogj/scrutiny:master-omnibus
|
||||
/opt/scrutiny/bin/scrutiny-collector-metrics run
|
||||
```
|
||||
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
|
||||
|
||||
# Modifying the Scrutiny Backend Server (API)
|
||||
|
||||
## Local Development
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.17+)
|
||||
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
|
||||
```yaml
|
||||
# config file for local development. store as scrutiny.yaml
|
||||
version: 1
|
||||
|
||||
web:
|
||||
listen:
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
database:
|
||||
# can also set absolute path here
|
||||
location: ./scrutiny.db
|
||||
src:
|
||||
frontend:
|
||||
path: ./dist
|
||||
influxdb:
|
||||
retention_policy: false
|
||||
|
||||
log:
|
||||
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
||||
level: DEBUG
|
||||
|
||||
### Frontend
|
||||
The frontend is written in Angular.
|
||||
If you're working on the frontend and can use mocked data rather than a real backend, you can use
|
||||
```
|
||||
cd webapp/frontend
|
||||
npm install
|
||||
ng serve --deploy-url="/web/" --base-href="/web/"
|
||||
```
|
||||
```
|
||||
4. start a InfluxDB docker container.
|
||||
```bash
|
||||
docker run -p 8086:8086 --rm influxdb:2.2
|
||||
```
|
||||
5. start the scrutiny web server
|
||||
```bash
|
||||
go mod vendor
|
||||
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
|
||||
```
|
||||
6. open your browser to [http://localhost:8080/web](http://localhost:8080/web)
|
||||
|
||||
However, if you need to also run the backend, and use real data, you'll need to run the following command:
|
||||
```
|
||||
cd webapp/frontend && ng build --watch --output-path=../../dist --prod
|
||||
```
|
||||
# Modifying the Scrutiny Frontend Angular SPA
|
||||
|
||||
> Note: if you do not add `--prod` flag, app will display mocked data for api calls.
|
||||
The frontend is written in Angular. If you're working on the frontend and can use mocked data rather than a real backend, you can follow the instructions below:
|
||||
|
||||
### Backend
|
||||
1. install [NodeJS](https://nodejs.org/en/download/)
|
||||
2. start the Angular Frontend Application
|
||||
```bash
|
||||
cd webapp/frontend
|
||||
npm install
|
||||
npm run start -- --deploy-url="/web/" --base-href="/web/" --port 4200
|
||||
```
|
||||
3. open your browser and visit [http://localhost:4200/web](http://localhost:4200/web)
|
||||
|
||||
If you're using the `ng build` command above to generate your frontend, you'll need to create a custom config file and
|
||||
override the `web.src.frontend.path` value.
|
||||
# Modifying both Scrutiny Backend and Frontend Applications
|
||||
If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data,
|
||||
you'll need to follow the steps below:
|
||||
|
||||
```
|
||||
# config file for local development. store as scrutiny.yaml
|
||||
version: 1
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.17+)
|
||||
2. install [NodeJS](https://nodejs.org/en/download/)
|
||||
3. create a `scrutiny.yaml` config file
|
||||
```yaml
|
||||
# config file for local development. store as scrutiny.yaml
|
||||
version: 1
|
||||
|
||||
web:
|
||||
listen:
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
database:
|
||||
# can also set absolute path here
|
||||
location: ./scrutiny.db
|
||||
src:
|
||||
frontend:
|
||||
path: ./dist
|
||||
influxdb:
|
||||
retention_policy: false
|
||||
|
||||
log:
|
||||
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
||||
level: DEBUG
|
||||
|
||||
web:
|
||||
listen:
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
database:
|
||||
# can also set absolute path here
|
||||
location: ./scrutiny.db
|
||||
src:
|
||||
frontend:
|
||||
path: ./dist
|
||||
influxdb:
|
||||
retention_policy: false
|
||||
|
||||
log:
|
||||
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
||||
level: DEBUG
|
||||
|
||||
```
|
||||
|
||||
Once you've created a config file, you can pass it to the scrutiny binary during startup.
|
||||
|
||||
```
|
||||
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
|
||||
```
|
||||
|
||||
Now visit http://localhost:8080
|
||||
```
|
||||
4. start a InfluxDB docker container.
|
||||
```bash
|
||||
docker run -p 8086:8086 --rm influxdb:2.2
|
||||
```
|
||||
5. build the Angular Frontend Application
|
||||
```bash
|
||||
cd webapp/frontend
|
||||
npm install
|
||||
npm run build:prod -- --watch --output-path=../../dist
|
||||
# Note: if you do not add `--prod` flag, app will display mocked data for api calls.
|
||||
```
|
||||
6. start the scrutiny web server
|
||||
```bash
|
||||
go mod vendor
|
||||
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
|
||||
```
|
||||
7. open your browser to [http://localhost:8080/web](http://localhost:8080/web)
|
||||
|
||||
|
||||
If you'd like to populate the database with some test data, you can run the following commands:
|
||||
@@ -82,15 +116,6 @@ If you'd like to populate the database with some test data, you can run the fol
|
||||
docker run -p 8086:8086 --rm influxdb:2.2
|
||||
|
||||
|
||||
docker run --rm -p 8086:8086 \
|
||||
-e DOCKER_INFLUXDB_INIT_MODE=setup \
|
||||
-e DOCKER_INFLUXDB_INIT_USERNAME=admin \
|
||||
-e DOCKER_INFLUXDB_INIT_PASSWORD=password12345 \
|
||||
-e DOCKER_INFLUXDB_INIT_ORG=scrutiny \
|
||||
-e DOCKER_INFLUXDB_INIT_BUCKET=metrics \
|
||||
influxdb:2.2
|
||||
|
||||
|
||||
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/web/testdata/register-devices-req.json localhost:8080/api/devices/register
|
||||
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-ata.json localhost:8080/api/device/0x5000cca264eb01d7/smart
|
||||
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-ata-date.json localhost:8080/api/device/0x5000cca264eb01d7/smart
|
||||
@@ -105,14 +130,14 @@ curl localhost:8080/api/summary
|
||||
|
||||
```
|
||||
|
||||
### Collector
|
||||
# Modifying the Collector
|
||||
```
|
||||
brew install smartmontools
|
||||
go run collector/cmd/collector-metrics/collector-metrics.go run --debug
|
||||
```
|
||||
|
||||
|
||||
## Debugging
|
||||
# Debugging
|
||||
|
||||
If you need more verbose logs for debugging, you can use the following environmental variables:
|
||||
|
||||
@@ -131,3 +156,32 @@ Finally, you can copy the files from the scrutiny container to your host using t
|
||||
docker cp scrutiny:/tmp/collector.log collector.log
|
||||
docker cp scrutiny:/tmp/web.log web.log
|
||||
```
|
||||
|
||||
# Docker Development
|
||||
|
||||
```
|
||||
docker build -f docker/Dockerfile . -t chcr.io/analogj/scrutiny:master-omnibus
|
||||
docker run -it --rm -p 8080:8080 \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
--cap-add SYS_RAWIO \
|
||||
--device=/dev/sda \
|
||||
--device=/dev/sdb \
|
||||
ghcr.io/analogj/scrutiny:master-omnibus
|
||||
/opt/scrutiny/bin/scrutiny-collector-metrics run
|
||||
```
|
||||
|
||||
|
||||
# Running Tests
|
||||
|
||||
```bash
|
||||
docker run -p 8086:8086 -d --rm \
|
||||
-e DOCKER_INFLUXDB_INIT_MODE=setup \
|
||||
-e DOCKER_INFLUXDB_INIT_USERNAME=admin \
|
||||
-e DOCKER_INFLUXDB_INIT_PASSWORD=password12345 \
|
||||
-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
|
||||
go test ./...
|
||||
|
||||
```
|
||||
@@ -9,6 +9,7 @@ BINARY=\
|
||||
linux/arm-7 \
|
||||
linux/arm64 \
|
||||
|
||||
.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.
|
||||
.PHONY: all $(BINARY)
|
||||
all: $(BINARY) windows/amd64
|
||||
|
||||
@@ -38,5 +39,28 @@ windows/amd64:
|
||||
@echo "building collector binary (OS = $(OS), ARCH = $(ARCH))"
|
||||
xgo -v --targets="$(OS)/$(ARCH)" -ldflags "-extldflags=-static -X main.goos=$(OS) -X main.goarch=$(ARCH)" -out scrutiny-collector-metrics -tags "static netgo" ${GO_WORKSPACE}/collector/cmd/collector-metrics/
|
||||
|
||||
|
||||
docker-collector:
|
||||
@echo "building collector docker image"
|
||||
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile.collector -t analogj/scrutiny-dev:collector .
|
||||
|
||||
docker-web:
|
||||
@echo "building web docker image"
|
||||
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile.web -t analogj/scrutiny-dev:web .
|
||||
|
||||
docker-omnibus:
|
||||
@echo "building omnibus docker image"
|
||||
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile -t analogj/scrutiny-dev:omnibus .
|
||||
|
||||
# reduce logging, disable angular-cli analytics for ci environment
|
||||
frontend: export NPM_CONFIG_LOGLEVEL = warn
|
||||
frontend: export NG_CLI_ANALYTICS = false
|
||||
frontend:
|
||||
cd webapp/frontend
|
||||
npm install -g @angular/cli@9.1.4
|
||||
mkdir -p $(CURDIR)/dist
|
||||
npm ci
|
||||
npm run build:prod -- --output-path=$(CURDIR)/dist
|
||||
|
||||
# clean:
|
||||
# rm scrutiny-collector-metrics-* scrutiny-web-*
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
WebUI for smartd S.M.A.R.T monitoring
|
||||
|
||||
> NOTE: Scrutiny is a Work-in-Progress and still has some rough edges.
|
||||
>
|
||||
> WARNING: Once the [InfluxDB](https://github.com/AnalogJ/scrutiny/tree/influxdb) branch is merged, Scrutiny will use both sqlite and InfluxDB for data storage. Unfortunately, this may not be backwards compatible with the database structures in the master (sqlite only) branch.
|
||||
|
||||
[](https://imgur.com/a/5k8qMzS)
|
||||
|
||||
@@ -60,11 +58,12 @@ Scrutiny uses `smartctl --scan` to detect devices/drives.
|
||||
- All RAID controllers supported by `smartctl` are automatically supported by Scrutiny.
|
||||
- While some RAID controllers support passing through the underlying SMART data to `smartctl` others do not.
|
||||
- In some cases `--scan` does not correctly detect the device type, returning [incomplete SMART data](https://github.com/AnalogJ/scrutiny/issues/45).
|
||||
Scrutiny will eventually support overriding detected device type via the config file.
|
||||
Scrutiny supports overriding detected device type via the config file: see [example.collector.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml)
|
||||
- If you use docker, you **must** pass though the RAID virtual disk to the container using `--device` (see below)
|
||||
- This device may be in `/dev/*` or `/dev/bus/*`.
|
||||
- If you're unsure, run `smartctl --scan` on your host, and pass all listed devices to the container.
|
||||
|
||||
See [docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](./docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md) for help
|
||||
|
||||
## Docker
|
||||
|
||||
@@ -178,7 +177,9 @@ Scrutiny supports sending SMART device failure notifications via the following s
|
||||
- Telegram
|
||||
- Tulip
|
||||
|
||||
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for more information and documentation for service specific setup.
|
||||
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for examples.
|
||||
|
||||
For more information and troubleshooting, see the [TROUBLESHOOTING_NOTIFICATIONS.md](./docs/TROUBLESHOOTING_NOTIFICATIONS.md) file
|
||||
|
||||
### Testing Notifications
|
||||
|
||||
@@ -239,7 +240,7 @@ scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
|
||||
| arm-6 | :white_check_mark: | |
|
||||
| arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
|
||||
| arm64 | :white_check_mark: | :white_check_mark: |
|
||||
| freebsd | :white_check_mark: | |
|
||||
| freebsd | collector only. see [#238](https://github.com/AnalogJ/scrutiny/issues/238) | |
|
||||
| macos-amd64 | | :white_check_mark: |
|
||||
| macos-arm64 | | :white_check_mark: |
|
||||
| windows-amd64 | :white_check_mark: | |
|
||||
|
||||
@@ -98,10 +98,10 @@ func (mc *MetricsCollector) Run() error {
|
||||
|
||||
func (mc *MetricsCollector) Validate() error {
|
||||
mc.logger.Infoln("Verifying required tools")
|
||||
_, lookErr := exec.LookPath("smartctl")
|
||||
_, lookErr := exec.LookPath(mc.config.GetString("commands.metrics_smartctl_bin"))
|
||||
|
||||
if lookErr != nil {
|
||||
return errors.DependencyMissingError("smartctl is missing")
|
||||
return errors.DependencyMissingError(fmt.Sprintf("%s binary is missing", mc.config.GetString("commands.metrics_smartctl_bin")))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -116,14 +116,15 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT
|
||||
}
|
||||
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
|
||||
|
||||
args := []string{"-x", "-j"}
|
||||
fullDeviceName := fmt.Sprintf("%s%s", detect.DevicePrefix(), deviceName)
|
||||
args := strings.Split(mc.config.GetCommandMetricsSmartArgs(fullDeviceName), " ")
|
||||
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
|
||||
if len(deviceType) > 0 && deviceType != "scsi" && deviceType != "ata" {
|
||||
args = append(args, "-d", deviceType)
|
||||
args = append(args, "--device", deviceType)
|
||||
}
|
||||
args = append(args, fmt.Sprintf("%s%s", detect.DevicePrefix(), deviceName))
|
||||
args = append(args, fullDeviceName)
|
||||
|
||||
result, err := mc.shell.Command(mc.logger, "smartctl", args, "", os.Environ())
|
||||
result, err := mc.shell.Command(mc.logger, mc.config.GetString("commands.metrics_smartctl_bin"), args, "", os.Environ())
|
||||
resultBytes := []byte(result)
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
@@ -8,6 +9,8 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// When initializing this class the following methods must be called:
|
||||
@@ -16,6 +19,8 @@ import (
|
||||
// This is done automatically when created via the Factory.
|
||||
type configuration struct {
|
||||
*viper.Viper
|
||||
|
||||
deviceOverrides []models.ScanOverride
|
||||
}
|
||||
|
||||
//Viper uses the following precedence order. Each item takes precedence over the item below it:
|
||||
@@ -38,6 +43,11 @@ func (c *configuration) Init() error {
|
||||
|
||||
c.SetDefault("api.endpoint", "http://localhost:8080")
|
||||
|
||||
c.SetDefault("commands.metrics_smartctl_bin", "smartctl")
|
||||
c.SetDefault("commands.metrics_scan_args", "--scan --json")
|
||||
c.SetDefault("commands.metrics_info_args", "--info --json")
|
||||
c.SetDefault("commands.metrics_smart_args", "--xall --json")
|
||||
|
||||
//c.SetDefault("collect.short.command", "-a -o on -S on")
|
||||
|
||||
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
|
||||
@@ -90,16 +100,89 @@ func (c *configuration) ValidateConfig() error {
|
||||
// check that device prefix matches OS
|
||||
// check that schema of config file is valid
|
||||
|
||||
return nil
|
||||
// check that the collector commands are valid
|
||||
commandArgStrings := map[string]string{
|
||||
"commands.metrics_scan_args": c.GetString("commands.metrics_scan_args"),
|
||||
"commands.metrics_info_args": c.GetString("commands.metrics_info_args"),
|
||||
"commands.metrics_smart_args": c.GetString("commands.metrics_smart_args"),
|
||||
}
|
||||
|
||||
errorStrings := []string{}
|
||||
for configKey, commandArgString := range commandArgStrings {
|
||||
args := strings.Split(commandArgString, " ")
|
||||
//ensure that the args string contains `--json` or `-j` flag
|
||||
containsJsonFlag := false
|
||||
containsDeviceFlag := false
|
||||
for _, flag := range args {
|
||||
if strings.HasPrefix(flag, "--json") || strings.HasPrefix(flag, "-j") {
|
||||
containsJsonFlag = true
|
||||
}
|
||||
if strings.HasPrefix(flag, "--device") || strings.HasPrefix(flag, "-d") {
|
||||
containsDeviceFlag = true
|
||||
}
|
||||
}
|
||||
|
||||
if !containsJsonFlag {
|
||||
errorStrings = append(errorStrings, fmt.Sprintf("configuration key '%s' is missing '--json' flag", configKey))
|
||||
}
|
||||
|
||||
if containsDeviceFlag {
|
||||
errorStrings = append(errorStrings, fmt.Sprintf("configuration key '%s' must not contain '--device' or '-d' flag", configKey))
|
||||
}
|
||||
}
|
||||
//sort(errorStrings)
|
||||
sort.Strings(errorStrings)
|
||||
|
||||
if len(errorStrings) == 0 {
|
||||
return nil
|
||||
} else {
|
||||
return errors.ConfigValidationError(strings.Join(errorStrings, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *configuration) GetScanOverrides() []models.ScanOverride {
|
||||
func (c *configuration) GetDeviceOverrides() []models.ScanOverride {
|
||||
// we have to support 2 types of device types.
|
||||
// - simple device type (device_type: 'sat')
|
||||
// and list of device types (type: \n- 3ware,0 \n- 3ware,1 \n- 3ware,2)
|
||||
// GetString will return "" if this is a list of device types.
|
||||
|
||||
overrides := []models.ScanOverride{}
|
||||
c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true })
|
||||
return overrides
|
||||
if c.deviceOverrides == nil {
|
||||
overrides := []models.ScanOverride{}
|
||||
c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true })
|
||||
c.deviceOverrides = overrides
|
||||
}
|
||||
|
||||
return c.deviceOverrides
|
||||
}
|
||||
|
||||
func (c *configuration) GetCommandMetricsInfoArgs(deviceName string) string {
|
||||
overrides := c.GetDeviceOverrides()
|
||||
|
||||
for _, deviceOverrides := range overrides {
|
||||
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
|
||||
//found matching device
|
||||
if len(deviceOverrides.Commands.MetricsInfoArgs) > 0 {
|
||||
return deviceOverrides.Commands.MetricsInfoArgs
|
||||
} else {
|
||||
return c.GetString("commands.metrics_info_args")
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.GetString("commands.metrics_info_args")
|
||||
}
|
||||
|
||||
func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
|
||||
overrides := c.GetDeviceOverrides()
|
||||
|
||||
for _, deviceOverrides := range overrides {
|
||||
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
|
||||
//found matching device
|
||||
if len(deviceOverrides.Commands.MetricsSmartArgs) > 0 {
|
||||
return deviceOverrides.Commands.MetricsSmartArgs
|
||||
} else {
|
||||
return c.GetString("commands.metrics_smart_args")
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.GetString("commands.metrics_smart_args")
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestConfiguration_GetScanOverrides_Simple(t *testing.T) {
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "simple_device.yaml"))
|
||||
require.NoError(t, err, "should correctly load simple device config")
|
||||
scanOverrides := testConfig.GetScanOverrides()
|
||||
scanOverrides := testConfig.GetDeviceOverrides()
|
||||
|
||||
//assert
|
||||
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides)
|
||||
@@ -45,7 +45,7 @@ func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "ignore_device.yaml"))
|
||||
require.NoError(t, err, "should correctly load ignore device config")
|
||||
scanOverrides := testConfig.GetScanOverrides()
|
||||
scanOverrides := testConfig.GetDeviceOverrides()
|
||||
|
||||
//assert
|
||||
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}}, scanOverrides)
|
||||
@@ -60,7 +60,7 @@ func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "raid_device.yaml"))
|
||||
require.NoError(t, err, "should correctly load ignore device config")
|
||||
scanOverrides := testConfig.GetScanOverrides()
|
||||
scanOverrides := testConfig.GetDeviceOverrides()
|
||||
|
||||
//assert
|
||||
require.Equal(t, []models.ScanOverride{
|
||||
@@ -75,3 +75,53 @@ func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
|
||||
Ignore: false,
|
||||
}}, scanOverrides)
|
||||
}
|
||||
|
||||
func TestConfiguration_InvalidCommands_MissingJson(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
testConfig, _ := config.Create()
|
||||
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "invalid_commands_missing_json.yaml"))
|
||||
require.EqualError(t, err, `ConfigValidationError: "configuration key 'commands.metrics_scan_args' is missing '--json' flag"`, "should throw an error because json flag is missing")
|
||||
}
|
||||
|
||||
func TestConfiguration_InvalidCommands_IncludesDevice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
testConfig, _ := config.Create()
|
||||
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "invalid_commands_includes_device.yaml"))
|
||||
require.EqualError(t, err, `ConfigValidationError: "configuration key 'commands.metrics_info_args' must not contain '--device' or '-d' flag, configuration key 'commands.metrics_smart_args' must not contain '--device' or '-d' flag"`, "should throw an error because device flags detected")
|
||||
}
|
||||
|
||||
func TestConfiguration_OverrideCommands(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
testConfig, _ := config.Create()
|
||||
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "override_commands.yaml"))
|
||||
require.NoError(t, err, "should not throw an error")
|
||||
require.Equal(t, "--xall --json -T permissive", testConfig.GetString("commands.metrics_smart_args"))
|
||||
}
|
||||
|
||||
func TestConfiguration_OverrideDeviceCommands_MetricsInfoArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
testConfig, _ := config.Create()
|
||||
|
||||
//test
|
||||
err := testConfig.ReadConfig(path.Join("testdata", "override_device_commands.yaml"))
|
||||
require.NoError(t, err, "should correctly override device command")
|
||||
|
||||
//assert
|
||||
require.Equal(t, "--info --json -T permissive", testConfig.GetCommandMetricsInfoArgs("/dev/sda"))
|
||||
require.Equal(t, "--info --json", testConfig.GetCommandMetricsInfoArgs("/dev/sdb"))
|
||||
//require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Commands: {MetricsInfoArgs: "--info --json -T "}}}, scanOverrides)
|
||||
}
|
||||
|
||||
@@ -22,5 +22,7 @@ type Interface interface {
|
||||
GetStringSlice(key string) []string
|
||||
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
|
||||
|
||||
GetScanOverrides() []models.ScanOverride
|
||||
GetDeviceOverrides() []models.ScanOverride
|
||||
GetCommandMetricsInfoArgs(deviceName string) string
|
||||
GetCommandMetricsSmartArgs(deviceName string) string
|
||||
}
|
||||
|
||||
@@ -5,88 +5,37 @@
|
||||
package mock_config
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
models "github.com/analogj/scrutiny/collector/pkg/models"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
viper "github.com/spf13/viper"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Init mocks base method
|
||||
func (m *MockInterface) Init() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Init")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Init indicates an expected call of Init
|
||||
func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
|
||||
}
|
||||
|
||||
// ReadConfig mocks base method
|
||||
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadConfig", configFilePath)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ReadConfig indicates an expected call of ReadConfig
|
||||
func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockInterface)(nil).ReadConfig), configFilePath)
|
||||
}
|
||||
|
||||
// Set mocks base method
|
||||
func (m *MockInterface) Set(key string, value interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Set", key, value)
|
||||
}
|
||||
|
||||
// Set indicates an expected call of Set
|
||||
func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockInterface)(nil).Set), key, value)
|
||||
}
|
||||
|
||||
// SetDefault mocks base method
|
||||
func (m *MockInterface) SetDefault(key string, value interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetDefault", key, value)
|
||||
}
|
||||
|
||||
// SetDefault indicates an expected call of SetDefault
|
||||
func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
|
||||
}
|
||||
|
||||
// AllSettings mocks base method
|
||||
// AllSettings mocks base method.
|
||||
func (m *MockInterface) AllSettings() map[string]interface{} {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AllSettings")
|
||||
@@ -94,27 +43,13 @@ func (m *MockInterface) AllSettings() map[string]interface{} {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AllSettings indicates an expected call of AllSettings
|
||||
// AllSettings indicates an expected call of AllSettings.
|
||||
func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllSettings", reflect.TypeOf((*MockInterface)(nil).AllSettings))
|
||||
}
|
||||
|
||||
// IsSet mocks base method
|
||||
func (m *MockInterface) IsSet(key string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsSet", key)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsSet indicates an expected call of IsSet
|
||||
func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
||||
}
|
||||
|
||||
// Get mocks base method
|
||||
// Get mocks base method.
|
||||
func (m *MockInterface) Get(key string) interface{} {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get", key)
|
||||
@@ -122,13 +57,13 @@ func (m *MockInterface) Get(key string) interface{} {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), key)
|
||||
}
|
||||
|
||||
// GetBool mocks base method
|
||||
// GetBool mocks base method.
|
||||
func (m *MockInterface) GetBool(key string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBool", key)
|
||||
@@ -136,13 +71,55 @@ func (m *MockInterface) GetBool(key string) bool {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetBool indicates an expected call of GetBool
|
||||
// GetBool indicates an expected call of GetBool.
|
||||
func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockInterface)(nil).GetBool), key)
|
||||
}
|
||||
|
||||
// GetInt mocks base method
|
||||
// GetCommandMetricsInfoArgs mocks base method.
|
||||
func (m *MockInterface) GetCommandMetricsInfoArgs(deviceName string) string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCommandMetricsInfoArgs", deviceName)
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetCommandMetricsInfoArgs indicates an expected call of GetCommandMetricsInfoArgs.
|
||||
func (mr *MockInterfaceMockRecorder) GetCommandMetricsInfoArgs(deviceName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommandMetricsInfoArgs", reflect.TypeOf((*MockInterface)(nil).GetCommandMetricsInfoArgs), deviceName)
|
||||
}
|
||||
|
||||
// GetCommandMetricsSmartArgs mocks base method.
|
||||
func (m *MockInterface) GetCommandMetricsSmartArgs(deviceName string) string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCommandMetricsSmartArgs", deviceName)
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetCommandMetricsSmartArgs indicates an expected call of GetCommandMetricsSmartArgs.
|
||||
func (mr *MockInterfaceMockRecorder) GetCommandMetricsSmartArgs(deviceName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommandMetricsSmartArgs", reflect.TypeOf((*MockInterface)(nil).GetCommandMetricsSmartArgs), deviceName)
|
||||
}
|
||||
|
||||
// GetDeviceOverrides mocks base method.
|
||||
func (m *MockInterface) GetDeviceOverrides() []models.ScanOverride {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetDeviceOverrides")
|
||||
ret0, _ := ret[0].([]models.ScanOverride)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetDeviceOverrides indicates an expected call of GetDeviceOverrides.
|
||||
func (mr *MockInterfaceMockRecorder) GetDeviceOverrides() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceOverrides", reflect.TypeOf((*MockInterface)(nil).GetDeviceOverrides))
|
||||
}
|
||||
|
||||
// GetInt mocks base method.
|
||||
func (m *MockInterface) GetInt(key string) int {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetInt", key)
|
||||
@@ -150,13 +127,13 @@ func (m *MockInterface) GetInt(key string) int {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetInt indicates an expected call of GetInt
|
||||
// GetInt indicates an expected call of GetInt.
|
||||
func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
|
||||
}
|
||||
|
||||
// GetString mocks base method
|
||||
// GetString mocks base method.
|
||||
func (m *MockInterface) GetString(key string) string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetString", key)
|
||||
@@ -164,13 +141,13 @@ func (m *MockInterface) GetString(key string) string {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetString indicates an expected call of GetString
|
||||
// GetString indicates an expected call of GetString.
|
||||
func (mr *MockInterfaceMockRecorder) GetString(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetString", reflect.TypeOf((*MockInterface)(nil).GetString), key)
|
||||
}
|
||||
|
||||
// GetStringSlice mocks base method
|
||||
// GetStringSlice mocks base method.
|
||||
func (m *MockInterface) GetStringSlice(key string) []string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetStringSlice", key)
|
||||
@@ -178,13 +155,79 @@ func (m *MockInterface) GetStringSlice(key string) []string {
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetStringSlice indicates an expected call of GetStringSlice
|
||||
// GetStringSlice indicates an expected call of GetStringSlice.
|
||||
func (mr *MockInterfaceMockRecorder) GetStringSlice(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStringSlice", reflect.TypeOf((*MockInterface)(nil).GetStringSlice), key)
|
||||
}
|
||||
|
||||
// UnmarshalKey mocks base method
|
||||
// Init mocks base method.
|
||||
func (m *MockInterface) Init() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Init")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Init indicates an expected call of Init.
|
||||
func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
|
||||
}
|
||||
|
||||
// IsSet mocks base method.
|
||||
func (m *MockInterface) IsSet(key string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsSet", key)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsSet indicates an expected call of IsSet.
|
||||
func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
||||
}
|
||||
|
||||
// ReadConfig mocks base method.
|
||||
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadConfig", configFilePath)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ReadConfig indicates an expected call of ReadConfig.
|
||||
func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockInterface)(nil).ReadConfig), configFilePath)
|
||||
}
|
||||
|
||||
// Set mocks base method.
|
||||
func (m *MockInterface) Set(key string, value interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Set", key, value)
|
||||
}
|
||||
|
||||
// Set indicates an expected call of Set.
|
||||
func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockInterface)(nil).Set), key, value)
|
||||
}
|
||||
|
||||
// SetDefault mocks base method.
|
||||
func (m *MockInterface) SetDefault(key string, value interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetDefault", key, value)
|
||||
}
|
||||
|
||||
// SetDefault indicates an expected call of SetDefault.
|
||||
func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
|
||||
}
|
||||
|
||||
// UnmarshalKey mocks base method.
|
||||
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{key, rawVal}
|
||||
@@ -196,23 +239,9 @@ func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UnmarshalKey indicates an expected call of UnmarshalKey
|
||||
// UnmarshalKey indicates an expected call of UnmarshalKey.
|
||||
func (mr *MockInterfaceMockRecorder) UnmarshalKey(key, rawVal interface{}, decoderOpts ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{key, rawVal}, decoderOpts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalKey", reflect.TypeOf((*MockInterface)(nil).UnmarshalKey), varargs...)
|
||||
}
|
||||
|
||||
// GetScanOverrides mocks base method
|
||||
func (m *MockInterface) GetScanOverrides() []models.ScanOverride {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetScanOverrides")
|
||||
ret0, _ := ret[0].([]models.ScanOverride)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetScanOverrides indicates an expected call of GetScanOverrides
|
||||
func (mr *MockInterfaceMockRecorder) GetScanOverrides() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScanOverrides", reflect.TypeOf((*MockInterface)(nil).GetScanOverrides))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
commands:
|
||||
metrics_scan_args: '--scan --json' # used to detect devices
|
||||
metrics_info_args: '--info --json --device=sat' # used to determine device unique ID & register device with Scrutiny
|
||||
metrics_smart_args: '--xall --json -d sat' # used to retrieve smart data for each device.
|
||||
@@ -0,0 +1,4 @@
|
||||
commands:
|
||||
metrics_scan_args: '--scan' # used to detect devices
|
||||
metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny
|
||||
metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.
|
||||
@@ -0,0 +1,4 @@
|
||||
commands:
|
||||
metrics_scan_args: '--scan --json' # used to detect devices
|
||||
metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny
|
||||
metrics_smart_args: '--xall --json -T permissive' # used to retrieve smart data for each device.
|
||||
@@ -0,0 +1,5 @@
|
||||
version: 1
|
||||
devices:
|
||||
- device: /dev/sda
|
||||
commands:
|
||||
metrics_info_args: "--info --json -T permissive"
|
||||
@@ -28,7 +28,8 @@ type Detect struct {
|
||||
// models.Device returned from this function only contain the minimum data for smartctl to execute: device type and device name (device file).
|
||||
func (d *Detect) SmartctlScan() ([]models.Device, error) {
|
||||
//we use smartctl to detect all the drives available.
|
||||
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, "smartctl", []string{"--scan", "-j"}, "", os.Environ())
|
||||
args := strings.Split(d.Config.GetString("commands.metrics_scan_args"), " ")
|
||||
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, d.Config.GetString("commands.metrics_smartctl_bin"), args, "", os.Environ())
|
||||
if err != nil {
|
||||
d.Logger.Errorf("Error scanning for devices: %v", err)
|
||||
return nil, err
|
||||
@@ -51,15 +52,15 @@ func (d *Detect) SmartctlScan() ([]models.Device, error) {
|
||||
// - WWN is provided as component data, rather than a "string". We'll have to generate the WWN value ourselves
|
||||
// - WWN from smartctl only provided for ATA protocol drives, NVMe and SCSI drives do not include WWN.
|
||||
func (d *Detect) SmartCtlInfo(device *models.Device) error {
|
||||
|
||||
args := []string{"--info", "-j"}
|
||||
fullDeviceName := fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName)
|
||||
args := strings.Split(d.Config.GetCommandMetricsInfoArgs(fullDeviceName), " ")
|
||||
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
|
||||
if len(device.DeviceType) > 0 && device.DeviceType != "scsi" && device.DeviceType != "ata" {
|
||||
args = append(args, "-d", device.DeviceType)
|
||||
args = append(args, "--device", device.DeviceType)
|
||||
}
|
||||
args = append(args, fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName))
|
||||
args = append(args, fullDeviceName)
|
||||
|
||||
availableDeviceInfoJson, err := d.Shell.Command(d.Logger, "smartctl", args, "", os.Environ())
|
||||
availableDeviceInfoJson, err := d.Shell.Command(d.Logger, d.Config.GetString("commands.metrics_smartctl_bin"), args, "", os.Environ())
|
||||
if err != nil {
|
||||
d.Logger.Errorf("Could not retrieve device information for %s: %v", device.DeviceName, err)
|
||||
return err
|
||||
@@ -138,7 +139,7 @@ func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []mod
|
||||
|
||||
//now tha we've "grouped" all the devices, lets override any groups specified in the config file.
|
||||
|
||||
for _, overrideDevice := range d.Config.GetScanOverrides() {
|
||||
for _, overrideDevice := range d.Config.GetDeviceOverrides() {
|
||||
overrideDeviceFile := strings.ToLower(overrideDevice.Device)
|
||||
|
||||
if overrideDevice.Ignore {
|
||||
@@ -148,10 +149,35 @@ func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []mod
|
||||
//create a new device group, and replace the one generated by smartctl --scan
|
||||
overrideDeviceGroup := []models.Device{}
|
||||
|
||||
for _, overrideDeviceType := range overrideDevice.DeviceType {
|
||||
if overrideDevice.DeviceType != nil {
|
||||
for _, overrideDeviceType := range overrideDevice.DeviceType {
|
||||
overrideDeviceGroup = append(overrideDeviceGroup, models.Device{
|
||||
HostId: d.Config.GetString("host.id"),
|
||||
DeviceType: overrideDeviceType,
|
||||
DeviceName: strings.TrimPrefix(overrideDeviceFile, DevicePrefix()),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
//user may have specified device in config file without device type (default to scanned device type)
|
||||
|
||||
//check if the device file was detected by the scanner
|
||||
var deviceType string
|
||||
if scannedDevice, foundScannedDevice := groupedDevices[overrideDeviceFile]; foundScannedDevice {
|
||||
if len(scannedDevice) > 0 {
|
||||
//take the device type from the first grouped device
|
||||
deviceType = scannedDevice[0].DeviceType
|
||||
} else {
|
||||
deviceType = "ata"
|
||||
}
|
||||
|
||||
} else {
|
||||
//fallback to ata if no scanned device detected
|
||||
deviceType = "ata"
|
||||
}
|
||||
|
||||
overrideDeviceGroup = append(overrideDeviceGroup, models.Device{
|
||||
HostId: d.Config.GetString("host.id"),
|
||||
DeviceType: overrideDeviceType,
|
||||
DeviceType: deviceType,
|
||||
DeviceName: strings.TrimPrefix(overrideDeviceFile, DevicePrefix()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ func TestDetect_SmartctlScan(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
|
||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_simple.json")
|
||||
@@ -45,7 +47,9 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
|
||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_megaraid.json")
|
||||
@@ -75,7 +79,9 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
|
||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_nvme.json")
|
||||
@@ -104,7 +110,10 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
{
|
||||
@@ -134,7 +143,10 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
{
|
||||
@@ -163,7 +175,9 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{
|
||||
{
|
||||
Device: "/dev/bus/0",
|
||||
DeviceType: []string{"megaraid,14", "megaraid,15", "megaraid,18", "megaraid,19", "megaraid,20", "megaraid,21"},
|
||||
@@ -202,7 +216,9 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
{
|
||||
@@ -225,3 +241,59 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
||||
require.Equal(t, 1, len(transformedDevices))
|
||||
require.Equal(t, "sat+megaraid", transformedDevices[0].DeviceType)
|
||||
}
|
||||
|
||||
// test https://github.com/AnalogJ/scrutiny/issues/255#issuecomment-1164024126
|
||||
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")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda"}})
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
{
|
||||
Name: "/dev/sda",
|
||||
InfoName: "/dev/sda",
|
||||
Protocol: "ata",
|
||||
Type: "scsi",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
d := detect.Detect{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
|
||||
//test
|
||||
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||
|
||||
//assert
|
||||
require.Equal(t, 1, len(transformedDevices))
|
||||
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
|
||||
}
|
||||
|
||||
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")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda"}})
|
||||
detectedDevices := models.Scan{}
|
||||
|
||||
d := detect.Detect{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
|
||||
//test
|
||||
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||
|
||||
//assert
|
||||
require.Equal(t, 1, len(transformedDevices))
|
||||
require.Equal(t, "ata", transformedDevices[0].DeviceType)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func DevicePrefix() string {
|
||||
|
||||
func (d *Detect) Start() ([]models.Device, error) {
|
||||
d.Shell = shell.Create()
|
||||
// call the base/common functionality to get a list of devicess
|
||||
// call the base/common functionality to get a list of devices
|
||||
detectedDevices, err := d.SmartctlScan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/jaypipes/ghw"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -22,6 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) {
|
||||
//inflate device info for detected devices.
|
||||
for ndx, _ := range detectedDevices {
|
||||
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
populateUdevInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
}
|
||||
|
||||
return detectedDevices, nil
|
||||
@@ -49,3 +53,51 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
//wwn must always be lowercase.
|
||||
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
|
||||
}
|
||||
|
||||
// as discussed in
|
||||
// - https://github.com/AnalogJ/scrutiny/issues/225
|
||||
// - https://github.com/jaypipes/ghw/issues/59#issue-361915216
|
||||
// udev exposes its data in a standardized way under /run/udev/data/....
|
||||
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"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deviceMountPaths := []string{}
|
||||
udevInfo := make(map[string]string)
|
||||
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
|
||||
if strings.HasPrefix(udevLine, "E:") {
|
||||
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
|
||||
udevInfo[s[0]] = s[1]
|
||||
}
|
||||
} else if strings.HasPrefix(udevLine, "S:") {
|
||||
deviceMountPaths = append(deviceMountPaths, udevLine[2:])
|
||||
}
|
||||
}
|
||||
|
||||
//Set additional device information.
|
||||
if deviceLabel, exists := udevInfo["ID_FS_LABEL"]; exists {
|
||||
detectedDevice.DeviceLabel = deviceLabel
|
||||
}
|
||||
if deviceUUID, exists := udevInfo["ID_FS_UUID"]; exists {
|
||||
detectedDevice.DeviceUUID = deviceUUID
|
||||
}
|
||||
if deviceSerialID, exists := udevInfo["ID_SERIAL"]; exists {
|
||||
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package models
|
||||
|
||||
type Device struct {
|
||||
WWN string `json:"wwn"`
|
||||
HostId string `json:"host_id"`
|
||||
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"`
|
||||
@@ -17,6 +20,10 @@ type Device struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
type DeviceWrapper struct {
|
||||
|
||||
@@ -4,4 +4,8 @@ type ScanOverride struct {
|
||||
Device string `mapstructure:"device"`
|
||||
DeviceType []string `mapstructure:"type"`
|
||||
Ignore bool `mapstructure:"ignore"`
|
||||
Commands struct {
|
||||
MetricsInfoArgs string `mapstructure:"metrics_info_args"`
|
||||
MetricsSmartArgs string `mapstructure:"metrics_smart_args"`
|
||||
} `mapstructure:"commands"`
|
||||
}
|
||||
|
||||
+11
-26
@@ -1,49 +1,34 @@
|
||||
########
|
||||
FROM golang:1.17.10-buster as backendbuild
|
||||
FROM golang:1.17-bullseye as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
|
||||
RUN go mod vendor && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-selftest collector/cmd/collector-selftest/collector-selftest.go && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
|
||||
go build -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go && \
|
||||
go build -o scrutiny-collector-selftest collector/cmd/collector-selftest/collector-selftest.go && \
|
||||
go build -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
|
||||
|
||||
########
|
||||
FROM node:lts-slim as frontendbuild
|
||||
|
||||
#reduce logging, disable angular-cli analytics for ci environment
|
||||
ENV NPM_CONFIG_LOGLEVEL=warn NG_CLI_ANALYTICS=false
|
||||
|
||||
WORKDIR /opt/scrutiny/src
|
||||
COPY webapp/frontend /opt/scrutiny/src
|
||||
|
||||
RUN npm install -g @angular/cli@9.1.4 && \
|
||||
mkdir -p /scrutiny/dist && \
|
||||
npm install && \
|
||||
ng build --output-path=/opt/scrutiny/dist --prod
|
||||
|
||||
|
||||
########
|
||||
FROM ubuntu:bionic as runtime
|
||||
FROM debian:bullseye-slim as runtime
|
||||
ARG TARGETARCH
|
||||
EXPOSE 8080
|
||||
WORKDIR /opt/scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb
|
||||
|
||||
RUN apt-get update && apt-get install -y cron smartmontools=7.0-0ubuntu1~ubuntu18.04.1 ca-certificates curl tzdata \
|
||||
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl tzdata \
|
||||
&& update-ca-certificates \
|
||||
&& case ${TARGETARCH} in \
|
||||
"amd64") S6_ARCH=amd64 ;; \
|
||||
"arm64") S6_ARCH=aarch64 ;; \
|
||||
esac \
|
||||
&& curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \
|
||||
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C /
|
||||
|
||||
ADD https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb /tmp/
|
||||
RUN dpkg -i /tmp/influxdb2-2.2.0-${TARGETARCH}.deb && rm -rf /tmp/influxdb2-2.2.0-${TARGETARCH}.deb
|
||||
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \
|
||||
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \
|
||||
&& curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \
|
||||
&& dpkg -i --force-all /tmp/influxdb2-2.2.0-${TARGETARCH}.deb
|
||||
|
||||
COPY /rootfs /
|
||||
|
||||
@@ -51,7 +36,7 @@ COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /opt/scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
|
||||
COPY --from=frontendbuild /opt/scrutiny/dist /opt/scrutiny/web
|
||||
COPY dist /opt/scrutiny/web
|
||||
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-selftest && \
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics && \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
########
|
||||
FROM golang:1.17.10-buster as backendbuild
|
||||
FROM golang:1.17-bullseye as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
@@ -10,11 +10,11 @@ RUN go mod vendor && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
|
||||
|
||||
########
|
||||
FROM ubuntu:bionic as runtime
|
||||
FROM debian:bullseye-slim as runtime
|
||||
WORKDIR /scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
|
||||
RUN apt-get update && apt-get install -y cron smartmontools=7.0-0ubuntu1~ubuntu18.04.1 ca-certificates tzdata && update-ca-certificates
|
||||
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates
|
||||
|
||||
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
|
||||
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
||||
|
||||
+4
-19
@@ -1,30 +1,15 @@
|
||||
########
|
||||
FROM golang:1.17.10-buster as backendbuild
|
||||
FROM golang:1.17-bullseye as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
|
||||
RUN go mod vendor && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go
|
||||
go build -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go
|
||||
|
||||
########
|
||||
FROM node:lts-slim as frontendbuild
|
||||
|
||||
#reduce logging, disable angular-cli analytics for ci environment
|
||||
ENV NPM_CONFIG_LOGLEVEL=warn NG_CLI_ANALYTICS=false
|
||||
|
||||
WORKDIR /opt/scrutiny/src
|
||||
COPY webapp/frontend /opt/scrutiny/src
|
||||
|
||||
RUN npm install -g @angular/cli@9.1.4 && \
|
||||
mkdir -p /opt/scrutiny/dist && \
|
||||
npm install && \
|
||||
ng build --output-path=/opt/scrutiny/dist --prod
|
||||
|
||||
|
||||
########
|
||||
FROM ubuntu:bionic as runtime
|
||||
FROM debian:bullseye-slim as runtime
|
||||
EXPOSE 8080
|
||||
WORKDIR /opt/scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
@@ -32,7 +17,7 @@ ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && update-ca-certificates
|
||||
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
|
||||
COPY --from=frontendbuild /opt/scrutiny/dist /opt/scrutiny/web
|
||||
COPY dist /opt/scrutiny/web
|
||||
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
|
||||
mkdir -p /opt/scrutiny/web && \
|
||||
mkdir -p /opt/scrutiny/config && \
|
||||
|
||||
@@ -1 +1 @@
|
||||
> See [docker/example.hubspoke.docker-compose.yml](./docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
|
||||
> 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.
|
||||
|
||||
@@ -57,7 +57,7 @@ web:
|
||||
# and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide
|
||||
# the `token`
|
||||
influxdb:
|
||||
host: 0.0.0.0
|
||||
host: localhost
|
||||
port: 8086
|
||||
# token: 'my-token'
|
||||
# org: 'my-org'
|
||||
@@ -83,9 +83,11 @@ Now that we have downloaded the required files, let's prepare the filesystem.
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-web-linux-amd64
|
||||
|
||||
# Next, lets extract the frontend files.
|
||||
# NOTE: after extraction, there **should not** be a `dist` subdirectory in `/opt/scrutiny/web` directory.
|
||||
cd /opt/scrutiny/web
|
||||
tar xvzf scrutiny-web-frontend.tar.gz --strip-components 1 -C .
|
||||
|
||||
|
||||
# Cleanup
|
||||
rm -rf scrutiny-web-frontend.tar.gz
|
||||
```
|
||||
@@ -113,7 +115,8 @@ Unlike the webapp, the collector does have some dependencies:
|
||||
Unfortunately the version of `smartmontools` (which contains `smartctl`) available in some of the base OS repositories is ancient.
|
||||
So you'll need to install the v7+ version using one of the following commands:
|
||||
|
||||
- **Ubuntu:** `apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1`
|
||||
- **Ubuntu (22.04/Jammy/LTS):** `apt-get install -y smartmontools`
|
||||
- **Ubuntu (18.04/Bionic):** `apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1`
|
||||
- **Centos8:**
|
||||
- `dnf install https://extras.getpagespeed.com/release-el8-latest.rpm`
|
||||
- `dnf install smartmontools`
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# pfsense Install
|
||||
|
||||
This bascially follows the [Manual collector instructions](https://github.com/AnalogJ/scrutiny/blob/master/docs/INSTALL_MANUAL.md#collector) and assumes you are running a hub and spoke deployment and already have the web app setup.
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
SSH into pfsense, hit `8` for the shell and install the required dependencies.
|
||||
|
||||
```
|
||||
pkg install smartmontools
|
||||
```
|
||||
|
||||
Ensure smartmontools is v7+. This won't be a problem in pfsense 2.6.0+
|
||||
|
||||
|
||||
### Directory Structure
|
||||
|
||||
Now let's create a directory structure to contain the Scrutiny collector binary.
|
||||
|
||||
```
|
||||
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).
|
||||
|
||||
> NOTE: Ensure you have the latest version in the below command
|
||||
|
||||
```
|
||||
fetch -o /opt/scrutiny/bin https://github.com/AnalogJ/scrutiny/releases/download/vX.X.X/scrutiny-collector-metrics-freebsd-amd64
|
||||
```
|
||||
|
||||
|
||||
### Prepare Scrutiny
|
||||
|
||||
Now that we have downloaded the required files, let's prepare the filesystem.
|
||||
|
||||
```
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics-freebsd-amd64
|
||||
```
|
||||
|
||||
|
||||
### 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-freebsd-amd64 run --api-endpoint "http://localhost:8080"
|
||||
```
|
||||
> NOTE: change the IP address to that of your web app
|
||||
|
||||
### Schedule Collector with Cron
|
||||
|
||||
Finally you need to schedule the collector to run periodically.
|
||||
|
||||
Login to the pfsense webGUI and head to `Services/Cron` add an entry with the following details:
|
||||
|
||||
```
|
||||
Minute: */15
|
||||
Hour: *
|
||||
Day of the Month: *
|
||||
Month of the Year: *
|
||||
Day of the Week: *
|
||||
User: root
|
||||
Command: /opt/scrutiny/bin/scrutiny-collector-metrics-freebsd-amd64 run --api-endpoint "http://localhost:8080" >/dev/null 2>&1
|
||||
```
|
||||
> NOTE: `>/dev/null 2>&1` is used to stop cron confirmation emails being sent.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Install collector on Synology
|
||||
|
||||
## Install Entware
|
||||
|
||||
This will allow you to install a newer version of smartmontools on your Synology. Follow the instructions here (This is tested on DSM7) - https://github.com/Entware/Entware/wiki/Install-on-Synology-NAS
|
||||
|
||||
**PLEASE NOTE THAT IF YOU UPDATE DSM FIRMWARE YOU MAY BORK THE EXISTING ENTWARE INSTALLATION, FOR ANYTHING THAT MAY RELATE TO ENTWARE PLEASE VISIT THEIR REPO**
|
||||
|
||||
## Collector Setup
|
||||
|
||||
**1. Run an update**
|
||||
|
||||
`sudo opkg update`
|
||||
|
||||
**2. Run an upgrade**
|
||||
|
||||
`sudo opkg upgrade`
|
||||
|
||||
**3. Install smartmontools**
|
||||
|
||||
`sudo opkg install smartmontools`
|
||||
|
||||
*It should install v7.2-2*
|
||||
|
||||
`Installing smartmontools (7.2-2) to root...`
|
||||
|
||||
**4. We will now create the directories.**
|
||||
|
||||
```
|
||||
mkdir -p /volume1/\@Entware/scrutiny/bin
|
||||
mkdir -p /volume1/\@Entware/scrutiny/conf
|
||||
```
|
||||
|
||||
**5. change into the bin directory**
|
||||
|
||||
`cd /volume1/\@Entware/scrutiny/bin`
|
||||
|
||||
**6. Download the collector binary for your architecture and make it executable**
|
||||
|
||||
`wget https://github.com/AnalogJ/scrutiny/releases/download/v0.4.12/scrutiny-collector-metrics-linux-arm64`
|
||||
|
||||
`chmod +x /volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64`
|
||||
|
||||
**7. Create a config file for the collector**
|
||||
|
||||
```
|
||||
cd /volume1/\@Entware/scrutiny/conf
|
||||
wget https://raw.githubusercontent.com/AnalogJ/scrutiny/master/example.collector.yaml
|
||||
mv example.collector.yaml collector.yaml
|
||||
```
|
||||
|
||||
**8. Lets make some changes in the [collector config file](../example.collector.yaml), these are what i uncommented/added, please tweak the device paths to your needs**
|
||||
|
||||
```
|
||||
host:
|
||||
id: 'Server_Name'
|
||||
|
||||
|
||||
devices:
|
||||
# # example for forcing device type detection for a single disk
|
||||
- device: /dev/sda
|
||||
type: 'sat'
|
||||
- device: /dev/sdb
|
||||
type: 'sat'
|
||||
- device: /dev/sdc
|
||||
type: 'sat'
|
||||
- device: /dev/sdd
|
||||
type: 'sat'
|
||||
|
||||
api:
|
||||
endpoint: 'http://<url>:8080'
|
||||
```
|
||||
|
||||
**9. Let's update the smartd db**
|
||||
|
||||
```
|
||||
cd /volume1/\@Entware/scrutiny/bin/
|
||||
wget https://raw.githubusercontent.com/smartmontools/smartmontools/master/smartmontools/drivedb.h
|
||||
```
|
||||
|
||||
**10. I ran it like this but you can tweak to your liking, the most important part is the --drivedb, as this loads it into the aplication for future use**
|
||||
|
||||
`smartctl -d sat --all /dev/sda --drivedb=/volume1/\@Entware/scrutiny/bin/drivedb.h`
|
||||
|
||||
**11. Now lets create a small bash script, this will be used for the scheduled task inside Synology**
|
||||
|
||||
`vim /volume1/\@Entware/scrutiny/bin/run_collect.sh`
|
||||
|
||||
**The contents are below, copy and paste them in**
|
||||
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
/volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64 run --config /volume1/\@Entware/scrutiny/config/collector.yaml
|
||||
```
|
||||
|
||||
## Set up Synology to run a scheduled task.
|
||||
|
||||
Log in to DSM and do the following:
|
||||
|
||||
Goto: DSM > Control Panel > Task Scheduler
|
||||
|
||||
Create > Scheduled Task > User Defined Script
|
||||
|
||||
###### General
|
||||
|
||||
```
|
||||
Task: Scrutiny_Collector
|
||||
User: root
|
||||
Enabled: yes
|
||||
```
|
||||
|
||||
###### Schedule
|
||||
```
|
||||
Run on the following days: Daily
|
||||
```
|
||||
###### Time:
|
||||
|
||||
```
|
||||
Frequency: <Your desired frequency>
|
||||
```
|
||||
|
||||
###### Task Settings
|
||||
|
||||
**Run Command**
|
||||
|
||||
```
|
||||
. /opt/etc/profile; /volume1/\@Entware/scrutiny/bin/run_collect.sh
|
||||
```
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you have any issues with your devices being detected, or incorrect data, please take a look at [TROUBLESHOOTING_DEVICE_COLLECTOR.md](./TROUBLESHOOTING_DEVICE_COLLECTOR.md)
|
||||
@@ -4,11 +4,14 @@ These are the officially supported NAS OS's (with documentation and setup guides
|
||||
Once a guide is created (in `docs/guides/`) it will be linked here.
|
||||
|
||||
- [ ] freenas/truenas
|
||||
- [x] [unraid](https://github.com/AnalogJ/scrutiny/blob/master/docs/INSTALL_UNRAID.md)
|
||||
- [x] [unraid](./INSTALL_UNRAID.md)
|
||||
- [ ] ESXI
|
||||
- [ ] Proxmox
|
||||
- [ ] Synology
|
||||
- [x] Synology(./INSTALL_SYNOLOGY_COLLECTOR.md)
|
||||
- [ ] OMV
|
||||
- [ ] Amahi
|
||||
- [ ] Running in a LXC container
|
||||
- [x] [PFSense](./INSTALL_UNRAID.md)
|
||||
- [ ] QNAP
|
||||
- [ ] RockStor
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ If the output is the same, your devices will be processed by Scrutiny.
|
||||
In some cases `--scan` does not correctly detect the device type, returning [incomplete SMART data](https://github.com/AnalogJ/scrutiny/issues/45).
|
||||
Scrutiny will supports overriding the detected device type via the config file.
|
||||
|
||||
[example.collector.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml)
|
||||
|
||||
### RAID Controllers (Megaraid/3ware/HBA/Adaptec/HPE/etc)
|
||||
Smartctl has support for a large number of [RAID controllers](https://www.smartmontools.org/wiki/Supported_RAID-Controllers), however this
|
||||
support is not automatic, and may require some additional device type hinting. You can provide this information to the Scrutiny collector
|
||||
@@ -111,12 +113,60 @@ instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/Anal
|
||||
|
||||
### ATA
|
||||
|
||||
### Standby/Sleeping Disks
|
||||
### Exit Codes
|
||||
|
||||
If you see an error message similar to `smartctl returned an error code (2) while processing /dev/sda`, this means that
|
||||
`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you debug,
|
||||
but you can look at the table (and associated links) below to debug `smartctl`.
|
||||
|
||||
> smartctl Return Values
|
||||
> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of
|
||||
> smartctl is 0 (all bits turned off). If a problem occurs, or an error, potential error, or fault is detected, then
|
||||
> a non-zero status is returned. In this case, the eight different bits in the return value have the following meanings
|
||||
> for ATA disks; some of these values may also be returned for SCSI disks.
|
||||
>
|
||||
> source: http://www.linuxguide.it/command_line/linux-manpage/do.php?file=smartctl#sect7
|
||||
|
||||
|
||||
| Exit Code (Isolated) | Binary | Problem Message |
|
||||
| --- | --- | --- |
|
||||
| 1 | Bit 0 | Command line did not parse. |
|
||||
| 2 | Bit 1 | Device open failed, or device did not return an IDENTIFY DEVICE structure. |
|
||||
| 4 | Bit 2 | Some SMART command to the disk failed, or there was a checksum error in a SMART data structure (see В´-bВ´ option above). |
|
||||
| 8 | Bit 3 | SMART status check returned “DISK FAILING". |
|
||||
| 16 | Bit 4 | We found prefail Attributes <= threshold. |
|
||||
| 32 | Bit 5 | SMART status check returned “DISK OK” but we found that some (usage or prefail) Attributes have been <= threshold at some time in the past. |
|
||||
| 64 | Bit 6 | The device error log contains records of errors. |
|
||||
| 128 | Bit 7 | The device self-test log contains records of errors. |
|
||||
|
||||
#### Standby/Sleeping Disks
|
||||
|
||||
Disks in Standby/Sleep can also cause `smartctl` to exit abnormally, usually with `exit code: 2`.
|
||||
|
||||
- https://github.com/AnalogJ/scrutiny/issues/221
|
||||
- https://github.com/AnalogJ/scrutiny/issues/157
|
||||
|
||||
### Volume Mount All Devices (`/dev`) - Privileged
|
||||
|
||||
> WARNING: This is an insecure/dangerous workaround. Running Scrutiny (or any Docker image) with `--privileged` is equivalent to running it with root access.
|
||||
|
||||
If you have exhausted all other mechanisms to get your disks working with `smartctl` running within a container, you can try running the docker image with the following additional flags:
|
||||
|
||||
- `--privileged` (instead of `--cap-add`) - this gives the docker container full access to your system. Scrutiny does not require this permission, however it can be helpful for `smartctl`
|
||||
- `-v /dev:/dev:ro` (instead of `--device`) - this mounts the `/dev` folder (containing all your device files) into the container, allowing `smartctl` to see your disks, exactly as if it were running on your host directly.
|
||||
|
||||
With this workaround your `docker run` command would look similar to the following:
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8080:8080 -p 8086:8086 \
|
||||
-v `pwd`/scrutiny:/opt/scrutiny/config \
|
||||
-v `pwd`/influxdb2:/opt/scrutiny/influxdb \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
--privileged \
|
||||
-v /dev:/dev \
|
||||
--name scrutiny \
|
||||
ghcr.io/analogj/scrutiny:master-omnibus
|
||||
```
|
||||
|
||||
## Scrutiny detects Failure but SMART Passed?
|
||||
|
||||
@@ -130,6 +180,72 @@ If Scrutiny detects that an attribute corresponds with a high rate of failure us
|
||||
This can cause some confusion when comparing Scrutiny's dashboard against other SMART analysis tools.
|
||||
If you hover over the "failed" label beside an attribute, Scrutiny will tell you if the failure was due to SMART or Scrutiny/BackBlaze data.
|
||||
|
||||
### Device failed but Smart & Scrutiny passed
|
||||
|
||||
Device SMART results are the source of truth for Scrutiny, however we don't just take into account the current SMART results, but also historical analysis of a disk.
|
||||
This means that if a device is marked as failed at any point in its history, it will continue to be stored in the database as failed until the device is removed (or status is reset -- see below).
|
||||
|
||||
In some cases, this historical failure may have been due to attribute analysis/thresholds that have since been relaxed:
|
||||
|
||||
- NVME - Numb Error Log Entries (v0.4.7)
|
||||
- ATA - Power Cycle Count (v0.4.7)
|
||||
- ATA - Read Error Rate (v0.4.13)
|
||||
- ATA - Seek Error Rate (v0.4.13)
|
||||
|
||||
If you'd like to reset the status of a disk (to healthy) and allow the next run of the collector to determine the actual status, you can run the following command:
|
||||
|
||||
```bash
|
||||
# connect to scrutiny docker container
|
||||
docker exec -it scrutiny bash
|
||||
|
||||
# install sqlite CLI tools (inside container)
|
||||
apt update && apt install -y sqlite3
|
||||
|
||||
# connect to the scrutiny database
|
||||
sqlite3 /opt/scrutiny/config/scrutiny.db
|
||||
|
||||
# reset/update the devices table, unset the failure status.
|
||||
UPDATE devices SET device_status = null;
|
||||
|
||||
# exit sqlite CLI
|
||||
.exit
|
||||
```
|
||||
|
||||
### Seagate Drives Failing
|
||||
|
||||
As thoroughly discussed in [#255](https://github.com/AnalogJ/scrutiny/issues/255), Seagate (Ironwolf & others) drives are almost always marked as failed by Scrutiny.
|
||||
|
||||
> The `Seek Error Rate` & `Read Error Rate` attribute raw values are typically very high, and the
|
||||
> normalised values (Current / Worst / Threshold) are usually quite low. Despite this, the numbers in most cases are perfectly OK
|
||||
>
|
||||
> The anxiety arises because we intuitively expect that the normalised values should reflect a "health" score, with
|
||||
> 100 being the ideal value. Similarly, we would expect that the raw values should reflect an error count, in
|
||||
> which case a value of 0 would be most desirable. However, Seagate calculates and applies these attribute values
|
||||
> in a counterintuitive way.
|
||||
>
|
||||
> http://www.users.on.net/~fzabkar/HDD/Seagate_SER_RRER_HEC.html
|
||||
|
||||
Some analysis has been done which shows that Seagate drives break the common SMART conventions, which also causes Scrutiny's
|
||||
comparison against BackBlaze data to detect these drives as failed.
|
||||
|
||||
**So what's the Solution?**
|
||||
|
||||
After taking a look at the BackBlaze data for the relevant Attributes (`Seek Error Rate` & `Read Error Rate`), I've decided
|
||||
to disable Scrutiny analysis for them. Both are non-critical, and have low-correlation with failure.
|
||||
|
||||
> Please note: SMART failures for these attributes will still cause the drive to be marked as failed. Only BackBlaze analysis has been disabled
|
||||
|
||||
If this is effecting your drives, you'll need to do the following:
|
||||
|
||||
1. Upgrade to v0.4.13+
|
||||
2. Reset your drive status using the SQLite script in [#device-failed-but-smart--scrutiny-passed](https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#device-failed-but-smart--scrutiny-passed)
|
||||
3. Wait for (or manually start) the collector.
|
||||
|
||||
If you'd like to learn more about how the Seagate Ironwolf SMART attributes work under the hood, and how they differ from
|
||||
other drives, please read the following:
|
||||
|
||||
- http://www.users.on.net/~fzabkar/HDD/Seagate_SER_RRER_HEC.html
|
||||
- https://www.truenas.com/community/threads/seagate-ironwolf-smart-test-raw_read_error_rate-seek_error_rate.68634/
|
||||
|
||||
## Hub & Spoke model, with multiple Hosts.
|
||||
|
||||
@@ -138,3 +254,17 @@ Thankfully the collector has a special `--host-id` flag (or `COLLECTOR_HOST_ID`
|
||||
|
||||
See the [docs/INSTALL_HUB_SPOKE.md](/docs/INSTALL_HUB_SPOKE.md) guide for more information.
|
||||
|
||||
## Collector DEBUG mode
|
||||
|
||||
You can use environmental variables to enable debug logging and/or log files for the collector:
|
||||
|
||||
```bash
|
||||
DEBUG=true
|
||||
COLLECTOR_LOG_FILE=/tmp/collector.log
|
||||
```
|
||||
|
||||
Or if you're not using docker, you can pass CLI arguments to the collector during startup:
|
||||
|
||||
```bash
|
||||
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
|
||||
```
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Docker Images `master-omnibus` vs `latest`
|
||||
|
||||
> TL;DR; The `master-omnibus` and `latest` tags are almost semantically identical, as I follow a `golden master`
|
||||
development process. However if you want to ensure you're only using the latest release, you can change to `latest`
|
||||
|
||||
The CI script used to orchestrate the docker image builds can be found here: https://github.com/AnalogJ/scrutiny/blob/master/.github/workflows/docker-build.yaml#L166-L184
|
||||
|
||||
In general Scrutiny follows a `golden master` development process, which means that the `master` branch is not directly updated (unless its for documentation changes),
|
||||
instead development is done in a feature branch, or committed to the `beta` branch.
|
||||
|
||||
As development progresses, and we're satisfied that a feature is complete, and the quality is acceptable,
|
||||
I merge the changes to `master` and trigger the creation of a new release -- ie, when master is updated, a new release
|
||||
is almost immediately created (and tagged with `latest`)
|
||||
|
||||
So changing from `master-omnibus -> latest` will be the same thing for all intents and purposes.
|
||||
|
||||
> NOTE: Previously, there was a `automated cron build` that ran on the `master` and `beta` branches.
|
||||
They used to trigger a `nightly` build, even if nothing has changed on the branch. This has a couple of benefits, but one is to
|
||||
ensure that there's no broken external dependencies in our (unchanged) code. This `nightly` build no longer updates the `master-omnibus` tag.
|
||||
@@ -54,6 +54,15 @@ time="2022-05-13T14:38:05Z" level=info msg="Successfully connected to scrutiny s
|
||||
panic: a username and password is required for a setup
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
Start the scrutiny server
|
||||
time="2022-06-11T10:35:04-04:00" level=info msg="Trying to connect to scrutiny sqlite db: \n"
|
||||
time="2022-06-11T10:35:04-04:00" level=info msg="Successfully connected to scrutiny sqlite db: \n"
|
||||
panic: failed to check influxdb setup status - parse "://:": missing protocol scheme
|
||||
```
|
||||
|
||||
As discussed in [#248](https://github.com/AnalogJ/scrutiny/issues/248) and [#234](https://github.com/AnalogJ/scrutiny/issues/234),
|
||||
this usually related to either:
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Notifications
|
||||
|
||||
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/
|
||||
|
||||
|
||||
# Script Notifications
|
||||
|
||||
While the Shoutrrr library supports many popular providers for sending notifications Scrutiny also supports a "script" based
|
||||
notification system, allowing you to execute a custom script whenever a notification needs to be sent.
|
||||
Data is provided to this script using the following environmental variables:
|
||||
|
||||
```
|
||||
SCRUTINY_SUBJECT - eg. "Scrutiny SMART error (%s) detected on device: %s"
|
||||
SCRUTINY_DATE
|
||||
SCRUTINY_FAILURE_TYPE - EmailTest, SmartFail, ScrutinyFail
|
||||
SCRUTINY_DEVICE_NAME - eg. /dev/sda
|
||||
SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe
|
||||
SCRUTINY_DEVICE_SERIAL - eg. WDDJ324KSO
|
||||
SCRUTINY_MESSAGE - eg. "Scrutiny SMART error notification for device: %s\nFailure Type: %s\nDevice Name: %s\nDevice Serial: %s\nDevice Type: %s\nDate: %s"
|
||||
```
|
||||
|
||||
@@ -55,4 +55,52 @@ api:
|
||||
You may also configure these values using the following environmental variables (both are required).
|
||||
|
||||
- `COLLECTOR_API_ENDPOINT=http://localhost:8080/custombasepath`
|
||||
- `SCRUTINY_WEB_LISTEN_BASEPATH=/custombasepath`
|
||||
- `SCRUTINY_WEB_LISTEN_BASEPATH=/custombasepath`
|
||||
|
||||
# Real Examples
|
||||
|
||||
## Caddy
|
||||
|
||||
1. Create a Caddyfile
|
||||
```yaml
|
||||
# Caddyfile
|
||||
:9090
|
||||
|
||||
# The `scrutiny` text in this file must match the service name in the docker-compose file below.
|
||||
# The `/custom/` text is the custom base path scrutiny will be availble on.
|
||||
reverse_proxy /custom/* scrutiny:8080
|
||||
|
||||
```
|
||||
2. Create a `docker-compose.yml` file
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
scrutiny:
|
||||
container_name: scrutiny
|
||||
image: ghcr.io/analogj/scrutiny:master-omnibus
|
||||
cap_add:
|
||||
- SYS_RAWIO
|
||||
ports:
|
||||
- "8086:8086" # influxDB admin
|
||||
volumes:
|
||||
- /run/udev:/run/udev:ro
|
||||
- ./config:/opt/scrutiny/config
|
||||
- ./influxdb:/opt/scrutiny/influxdb
|
||||
devices:
|
||||
- "/dev/sda"
|
||||
- "/dev/sdb"
|
||||
environment:
|
||||
- SCRUTINY_WEB_LISTEN_BASEPATH=/custom
|
||||
- COLLECTOR_API_ENDPOINT=http://localhost:8080/custom
|
||||
caddy:
|
||||
image: caddy
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
ports:
|
||||
- "9090:9090"
|
||||
```
|
||||
3. run `docker-compose up`
|
||||
4. visit [http://localhost:9090/custom/web](http://localhost:9090/custom/web) - access the scrutiny container via caddy reverse proxy
|
||||
@@ -53,6 +53,13 @@ devices:
|
||||
# - 3ware,3
|
||||
# - 3ware,4
|
||||
# - 3ware,5
|
||||
#
|
||||
# # example to show how to override the smartctl command args (per device), see below for how to override these globally.
|
||||
# - device: /dev/sda
|
||||
# commands:
|
||||
# metrics_info_args: '--info --json -T permissive' # used to determine device unique ID & register device with Scrutiny
|
||||
# metrics_smart_args: '--xall --json -T permissive' # used to retrieve smart data for each device.
|
||||
|
||||
|
||||
#log:
|
||||
# file: '' #absolute or relative paths allowed, eg. web.log
|
||||
@@ -64,6 +71,13 @@ devices:
|
||||
# if you need to use a custom base path (for a reverse proxy), you can add a suffix to the endpoint.
|
||||
# See docs/TROUBLESHOOTING_REVERSE_PROXY.md for more info,
|
||||
|
||||
# example to show how to override the smartctl command args globally
|
||||
#commands:
|
||||
# metrics_smartctl_bin: 'smartctl' # change to provide custom `smartctl` binary path, eg. `/usr/sbin/smartctl`
|
||||
# metrics_scan_args: '--scan --json' # used to detect devices
|
||||
# metrics_info_args: '--info --json' # used to determine device unique ID & register device with Scrutiny
|
||||
# metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.
|
||||
|
||||
|
||||
########################################################################################################################
|
||||
# FEATURES COMING SOON
|
||||
|
||||
@@ -40,6 +40,7 @@ web:
|
||||
# and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide
|
||||
# the `token`
|
||||
influxdb:
|
||||
# scheme: 'http'
|
||||
host: 0.0.0.0
|
||||
port: 8086
|
||||
# token: 'my-token'
|
||||
@@ -72,6 +73,8 @@ log:
|
||||
# - "join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]"
|
||||
# - "script:///file/path/on/disk"
|
||||
# - "https://www.example.com/path"
|
||||
# filter_attributes: 'all' # options: 'all' or 'critical'
|
||||
# level: 'fail' # options: 'fail', 'fail_scrutiny', 'fail_smart'
|
||||
|
||||
########################################################################################################################
|
||||
# FEATURES COMING SOON
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
@@ -38,7 +39,10 @@ func (c *configuration) Init() error {
|
||||
c.SetDefault("log.file", "")
|
||||
|
||||
c.SetDefault("notify.urls", []string{})
|
||||
c.SetDefault("notify.filter_attributes", pkg.NotifyFilterAttributesAll)
|
||||
c.SetDefault("notify.level", pkg.NotifyLevelFail)
|
||||
|
||||
c.SetDefault("web.influxdb.scheme", "http")
|
||||
c.SetDefault("web.influxdb.host", "localhost")
|
||||
c.SetDefault("web.influxdb.port", "8086")
|
||||
c.SetDefault("web.influxdb.org", "scrutiny")
|
||||
|
||||
@@ -4,25 +4,41 @@ const DeviceProtocolAta = "ATA"
|
||||
const DeviceProtocolScsi = "SCSI"
|
||||
const DeviceProtocolNvme = "NVMe"
|
||||
|
||||
const SmartAttributeStatusPassed = 0
|
||||
const SmartAttributeStatusFailed = 1
|
||||
const SmartAttributeStatusWarning = 2
|
||||
const NotifyFilterAttributesAll = "all"
|
||||
const NotifyFilterAttributesCritical = "critical"
|
||||
|
||||
const SmartWhenFailedFailingNow = "FAILING_NOW"
|
||||
const SmartWhenFailedInThePast = "IN_THE_PAST"
|
||||
const NotifyLevelFail = "fail"
|
||||
const NotifyLevelFailScrutiny = "fail_scrutiny"
|
||||
const NotifyLevelFailSmart = "fail_smart"
|
||||
|
||||
//const SmartStatusPassed = "passed"
|
||||
//const SmartStatusFailed = "failed"
|
||||
|
||||
type DeviceStatus int
|
||||
type AttributeStatus uint8
|
||||
|
||||
const (
|
||||
DeviceStatusPassed DeviceStatus = 0
|
||||
DeviceStatusFailedSmart DeviceStatus = iota
|
||||
DeviceStatusFailedScrutiny DeviceStatus = iota
|
||||
// AttributeStatusPassed binary, 1,2,4,8,16,32,etc
|
||||
AttributeStatusPassed AttributeStatus = 0
|
||||
AttributeStatusFailedSmart AttributeStatus = 1
|
||||
AttributeStatusWarningScrutiny AttributeStatus = 2
|
||||
AttributeStatusFailedScrutiny AttributeStatus = 4
|
||||
)
|
||||
|
||||
func Set(b, flag DeviceStatus) DeviceStatus { return b | flag }
|
||||
func Clear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
|
||||
func Toggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
|
||||
func Has(b, flag DeviceStatus) bool { return b&flag != 0 }
|
||||
const AttributeWhenFailedFailingNow = "FAILING_NOW"
|
||||
const AttributeWhenFailedInThePast = "IN_THE_PAST"
|
||||
|
||||
func AttributeStatusSet(b, flag AttributeStatus) AttributeStatus { return b | flag }
|
||||
func AttributeStatusClear(b, flag AttributeStatus) AttributeStatus { return b &^ flag }
|
||||
func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^ flag }
|
||||
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
|
||||
|
||||
type DeviceStatus uint8
|
||||
|
||||
const (
|
||||
// DeviceStatusPassed binary, 1,2,4,8,16,32,etc
|
||||
DeviceStatusPassed DeviceStatus = 0
|
||||
DeviceStatusFailedSmart DeviceStatus = 1
|
||||
DeviceStatusFailedScrutiny DeviceStatus = 2
|
||||
)
|
||||
|
||||
func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | flag }
|
||||
func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
|
||||
func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
|
||||
func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 }
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func sortSmartMeasurementsDesc(smartResults []measurements.Smart) {
|
||||
sort.SliceStable(smartResults, func(i, j int) bool {
|
||||
return smartResults[i].Date.After(smartResults[j].Date)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_sortSmartMeasurementsDesc_LatestFirst(t *testing.T) {
|
||||
//setup
|
||||
timeNow := time.Now()
|
||||
smartResults := []measurements.Smart{
|
||||
{
|
||||
Date: timeNow.AddDate(0, 0, -2),
|
||||
},
|
||||
{
|
||||
Date: timeNow,
|
||||
},
|
||||
{
|
||||
Date: timeNow.AddDate(0, 0, -1),
|
||||
},
|
||||
}
|
||||
|
||||
//test
|
||||
sortSmartMeasurementsDesc(smartResults)
|
||||
|
||||
//assert
|
||||
require.Equal(t, smartResults[0].Date, timeNow)
|
||||
}
|
||||
@@ -19,6 +19,7 @@ type DeviceRepo interface {
|
||||
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)
|
||||
DeleteDevice(ctx context.Context, wwn string) error
|
||||
|
||||
SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error)
|
||||
GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Deprecated: m20220503120000.Device is deprecated, only used by db migrations
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package m20220509170100
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Create a new client using an InfluxDB server base URL and an authentication token
|
||||
influxdbUrl := fmt.Sprintf("http://%s:%s", appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
|
||||
influxdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.influxdb.scheme"), appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
|
||||
globalLogger.Debugf("InfluxDB url: %s", influxdbUrl)
|
||||
|
||||
client := influxdb2.NewClient(influxdbUrl, appConfig.GetString("web.influxdb.token"))
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
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"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type"}),
|
||||
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type", "device_uuid", "device_serial_id", "device_label"}),
|
||||
}).Create(&dev).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -57,7 +58,7 @@ func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string
|
||||
return device, fmt.Errorf("Could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
device.DeviceStatus = pkg.Set(device.DeviceStatus, status)
|
||||
device.DeviceStatus = pkg.DeviceStatusSet(device.DeviceStatus, status)
|
||||
return device, sr.gormClient.Model(&device).Updates(device).Error
|
||||
}
|
||||
|
||||
@@ -72,3 +73,33 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string)
|
||||
|
||||
return device, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
//delete data from influxdb.
|
||||
buckets := []string{
|
||||
sr.appConfig.GetString("web.influxdb.bucket"),
|
||||
fmt.Sprintf("%s_weekly", sr.appConfig.GetString("web.influxdb.bucket")),
|
||||
fmt.Sprintf("%s_monthly", sr.appConfig.GetString("web.influxdb.bucket")),
|
||||
fmt.Sprintf("%s_yearly", sr.appConfig.GetString("web.influxdb.bucket")),
|
||||
}
|
||||
|
||||
for _, bucket := range buckets {
|
||||
sr.logger.Infof("Deleting data for %s in bucket: %s", wwn, 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),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn strin
|
||||
return deviceSmartData, sr.saveDatapoint(sr.influxWriteApi, "smart", tags, fields, deviceSmartData.Date, ctx)
|
||||
}
|
||||
|
||||
// GetSmartAttributeHistory MUST return in sorted order, where newest entries are at the beginning of the list, and oldest are at the end.
|
||||
func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error) {
|
||||
// Get SMartResults from InfluxDB
|
||||
|
||||
@@ -64,6 +65,9 @@ func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//we have to sort the smartResults again, because the `union` command will return multiple 'tables' and only sort the records in each table.
|
||||
sortSmartMeasurementsDesc(smartResults)
|
||||
|
||||
return smartResults, nil
|
||||
|
||||
//if err := device.SquashHistory(); err != nil {
|
||||
|
||||
@@ -2,14 +2,18 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api/http"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -22,9 +26,12 @@ import (
|
||||
|
||||
func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
|
||||
sr.logger.Infoln("Database migration starting")
|
||||
sr.logger.Infoln("Database migration starting. Please wait, this process may take a long time....")
|
||||
|
||||
m := gormigrate.New(sr.gormClient, gormigrate.DefaultOptions, []*gormigrate.Migration{
|
||||
gormMigrateOptions := gormigrate.DefaultOptions
|
||||
gormMigrateOptions.UseTransaction = true
|
||||
|
||||
m := gormigrate.New(sr.gormClient, gormMigrateOptions, []*gormigrate.Migration{
|
||||
{
|
||||
ID: "20201107210306", // v0.3.13 (pre-influxdb schema). 9fac3c6308dc6cb6cd5bbc43a68cd93e8fb20b87
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
@@ -38,16 +45,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
&m20201107210306.SmartNvmeAttribute{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropTable(
|
||||
&m20201107210306.Device{},
|
||||
&m20201107210306.Smart{},
|
||||
&m20201107210306.SmartAtaAttribute{},
|
||||
&m20201107210306.SmartNvmeAttribute{},
|
||||
&m20201107210306.SmartNvmeAttribute{},
|
||||
"self_tests",
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "20220503113100", // backwards compatible - influxdb schema
|
||||
@@ -137,7 +134,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartTags,
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -147,7 +144,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -166,7 +163,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -176,7 +173,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -194,7 +191,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartTags,
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -204,7 +201,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -221,7 +218,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartTags,
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -231,7 +228,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -257,20 +254,44 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
//migrate the device database to the current version
|
||||
//migrate the device database
|
||||
return tx.AutoMigrate(m20220503120000.Device{})
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "m20220509170100", // addl udev device data
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
//migrate the device database.
|
||||
// adding addl columns (device_label, device_uuid, device_serial_id)
|
||||
return tx.AutoMigrate(m20220509170100.Device{})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := m.Migrate(); err != nil {
|
||||
sr.logger.Errorf("Database migration failed with error: %w", err)
|
||||
sr.logger.Errorf("Database migration failed with error. \n Please open a github issue at https://github.com/AnalogJ/scrutiny and attach a copy of your scrutiny.db file. \n %v", err)
|
||||
return err
|
||||
}
|
||||
sr.logger.Infoln("Database migration completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
//When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy.
|
||||
//This function will ignore retention policy errors, and allow the migration to continue.
|
||||
func ignorePastRetentionPolicyError(err error) error {
|
||||
var influxDbWriteError *http.Error
|
||||
if errors.As(err, &influxDbWriteError) {
|
||||
if influxDbWriteError.StatusCode == 422 {
|
||||
log.Infoln("ignoring error: attempted to writePoint past retention period duration")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
func m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp(preDevice m20201107210306.Device, preSmartResult m20201107210306.Smart) (error, measurements.SmartTemperature) {
|
||||
//extract temperature data for every datapoint
|
||||
|
||||
@@ -109,7 +109,7 @@ func (sr *scrutinyRepository) DownsampleScript(aggregationType string) string {
|
||||
|> toInt()
|
||||
|
||||
temp_data
|
||||
|> aggregateWindow(fn: mean, every: aggWindow)
|
||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||
|> to(bucket: destBucket, org: destOrg)
|
||||
`,
|
||||
sourceBucket,
|
||||
|
||||
@@ -21,6 +21,10 @@ type Device struct {
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
|
||||
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"`
|
||||
@@ -162,7 +166,7 @@ func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInfo) error {
|
||||
dv.DeviceProtocol = info.Device.Protocol
|
||||
|
||||
if !info.SmartStatus.Passed {
|
||||
dv.DeviceStatus = pkg.Set(dv.DeviceStatus, pkg.DeviceStatusFailedSmart)
|
||||
dv.DeviceStatus = pkg.DeviceStatusSet(dv.DeviceStatus, pkg.DeviceStatusFailedSmart)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -110,7 +110,7 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er
|
||||
sm.PowerCycleCount = info.PowerCycleCount
|
||||
sm.PowerOnHours = info.PowerOnTime.Hours
|
||||
if !info.SmartStatus.Passed {
|
||||
sm.Status = pkg.DeviceStatusFailedSmart
|
||||
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedSmart)
|
||||
}
|
||||
|
||||
sm.DeviceProtocol = info.Device.Protocol
|
||||
@@ -148,8 +148,9 @@ func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTa
|
||||
}
|
||||
attrModel.PopulateAttributeStatus()
|
||||
sm.Attributes[strconv.Itoa(collectorAttr.ID)] = &attrModel
|
||||
if attrModel.Status == pkg.SmartAttributeStatusFailed {
|
||||
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
|
||||
if pkg.AttributeStatusHas(attrModel.Status, pkg.AttributeStatusFailedScrutiny) {
|
||||
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,15 +172,15 @@ func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.Nv
|
||||
"power_on_hours": (&SmartNvmeAttribute{AttributeId: "power_on_hours", Value: nvmeSmartHealthInformationLog.PowerOnHours, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"unsafe_shutdowns": (&SmartNvmeAttribute{AttributeId: "unsafe_shutdowns", Value: nvmeSmartHealthInformationLog.UnsafeShutdowns, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"media_errors": (&SmartNvmeAttribute{AttributeId: "media_errors", Value: nvmeSmartHealthInformationLog.MediaErrors, Threshold: 0}).PopulateAttributeStatus(),
|
||||
"num_err_log_entries": (&SmartNvmeAttribute{AttributeId: "num_err_log_entries", Value: nvmeSmartHealthInformationLog.NumErrLogEntries, Threshold: 0}).PopulateAttributeStatus(),
|
||||
"num_err_log_entries": (&SmartNvmeAttribute{AttributeId: "num_err_log_entries", Value: nvmeSmartHealthInformationLog.NumErrLogEntries, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"warning_temp_time": (&SmartNvmeAttribute{AttributeId: "warning_temp_time", Value: nvmeSmartHealthInformationLog.WarningTempTime, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"critical_comp_time": (&SmartNvmeAttribute{AttributeId: "critical_comp_time", Value: nvmeSmartHealthInformationLog.CriticalCompTime, Threshold: -1}).PopulateAttributeStatus(),
|
||||
}
|
||||
|
||||
//find analyzed attribute status
|
||||
for _, val := range sm.Attributes {
|
||||
if val.GetStatus() == pkg.SmartAttributeStatusFailed {
|
||||
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
if pkg.AttributeStatusHas(val.GetStatus(), pkg.AttributeStatusFailedScrutiny) {
|
||||
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,8 +205,8 @@ func (sm *Smart) ProcessScsiSmartInfo(defectGrownList int64, scsiErrorCounterLog
|
||||
|
||||
//find analyzed attribute status
|
||||
for _, val := range sm.Attributes {
|
||||
if val.GetStatus() == pkg.SmartAttributeStatusFailed {
|
||||
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
if pkg.AttributeStatusHas(val.GetStatus(), pkg.AttributeStatusFailedScrutiny) {
|
||||
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@ type SmartAtaAttribute struct {
|
||||
WhenFailed string `json:"when_failed"`
|
||||
|
||||
//Generated data
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status int64 `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status pkg.AttributeStatus `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
}
|
||||
|
||||
func (sa *SmartAtaAttribute) GetStatus() int64 {
|
||||
func (sa *SmartAtaAttribute) GetStatus() pkg.AttributeStatus {
|
||||
return sa.Status
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (sa *SmartAtaAttribute) Flatten() map[string]interface{} {
|
||||
|
||||
//Generated Data
|
||||
fmt.Sprintf("attr.%s.transformed_value", idString): sa.TransformedValue,
|
||||
fmt.Sprintf("attr.%s.status", idString): sa.Status,
|
||||
fmt.Sprintf("attr.%s.status", idString): int64(sa.Status),
|
||||
fmt.Sprintf("attr.%s.status_reason", idString): sa.StatusReason,
|
||||
fmt.Sprintf("attr.%s.failure_rate", idString): sa.FailureRate,
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
|
||||
case "transformed_value":
|
||||
sa.TransformedValue = val.(int64)
|
||||
case "status":
|
||||
sa.Status = val.(int64)
|
||||
sa.Status = pkg.AttributeStatus(val.(int64))
|
||||
case "status_reason":
|
||||
sa.StatusReason = val.(string)
|
||||
case "failure_rate":
|
||||
@@ -89,16 +89,16 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
|
||||
//populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
// Chainable
|
||||
func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttribute {
|
||||
if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedFailingNow {
|
||||
if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedFailingNow {
|
||||
//this attribute has previously failed
|
||||
sa.Status = pkg.SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Attribute is failing manufacturer SMART threshold"
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedSmart)
|
||||
sa.StatusReason += "Attribute is failing manufacturer SMART threshold"
|
||||
//if the Smart Status is failed, we should exit early, no need to look at thresholds.
|
||||
return sa
|
||||
|
||||
} else if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedInThePast {
|
||||
sa.Status = pkg.SmartAttributeStatusWarning
|
||||
sa.StatusReason = "Attribute has previously failed manufacturer SMART threshold"
|
||||
} else if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedInThePast {
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
|
||||
sa.StatusReason += "Attribute has previously failed manufacturer SMART threshold"
|
||||
}
|
||||
|
||||
if smartMetadata, ok := thresholds.AtaMetadata[sa.AttributeId]; ok {
|
||||
@@ -138,16 +138,16 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
|
||||
|
||||
if smartMetadata.Critical {
|
||||
if obsThresh.AnnualFailureRate >= 0.10 {
|
||||
sa.Status = pkg.SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Observed Failure Rate for Critical Attribute is greater than 10%"
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
|
||||
sa.StatusReason += "Observed Failure Rate for Critical Attribute is greater than 10%"
|
||||
}
|
||||
} else {
|
||||
if obsThresh.AnnualFailureRate >= 0.20 {
|
||||
sa.Status = pkg.SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 20%"
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
|
||||
sa.StatusReason += "Observed Failure Rate for Non-Critical Attribute is greater than 20%"
|
||||
} else if obsThresh.AnnualFailureRate >= 0.10 {
|
||||
sa.Status = pkg.SmartAttributeStatusWarning
|
||||
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 10%"
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
|
||||
sa.StatusReason += "Observed Failure Rate for Non-Critical Attribute is greater than 10%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
|
||||
}
|
||||
// no bucket found
|
||||
if smartMetadata.Critical {
|
||||
sa.Status = pkg.SmartAttributeStatusWarning
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
|
||||
sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package measurements
|
||||
|
||||
import "github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
|
||||
type SmartAttribute interface {
|
||||
Flatten() (fields map[string]interface{})
|
||||
Inflate(key string, val interface{})
|
||||
GetStatus() int64
|
||||
GetStatus() pkg.AttributeStatus
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ type SmartNvmeAttribute struct {
|
||||
Value int64 `json:"value"`
|
||||
Threshold int64 `json:"thresh"`
|
||||
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status int64 `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status pkg.AttributeStatus `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
}
|
||||
|
||||
func (sa *SmartNvmeAttribute) GetStatus() int64 {
|
||||
func (sa *SmartNvmeAttribute) GetStatus() pkg.AttributeStatus {
|
||||
return sa.Status
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (sa *SmartNvmeAttribute) Flatten() map[string]interface{} {
|
||||
|
||||
//Generated Data
|
||||
fmt.Sprintf("attr.%s.transformed_value", sa.AttributeId): sa.TransformedValue,
|
||||
fmt.Sprintf("attr.%s.status", sa.AttributeId): sa.Status,
|
||||
fmt.Sprintf("attr.%s.status", sa.AttributeId): int64(sa.Status),
|
||||
fmt.Sprintf("attr.%s.status_reason", sa.AttributeId): sa.StatusReason,
|
||||
fmt.Sprintf("attr.%s.failure_rate", sa.AttributeId): sa.FailureRate,
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (sa *SmartNvmeAttribute) Inflate(key string, val interface{}) {
|
||||
case "transformed_value":
|
||||
sa.TransformedValue = val.(int64)
|
||||
case "status":
|
||||
sa.Status = val.(int64)
|
||||
sa.Status = pkg.AttributeStatus(val.(int64))
|
||||
case "status_reason":
|
||||
sa.StatusReason = val.(string)
|
||||
case "failure_rate":
|
||||
@@ -72,8 +72,8 @@ func (sa *SmartNvmeAttribute) PopulateAttributeStatus() *SmartNvmeAttribute {
|
||||
//check what the ideal is. Ideal tells us if we our recorded value needs to be above, or below the threshold
|
||||
if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) ||
|
||||
(smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) {
|
||||
sa.Status = pkg.SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Attribute is failing recommended SMART threshold"
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
|
||||
sa.StatusReason += "Attribute is failing recommended SMART threshold"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ type SmartScsiAttribute struct {
|
||||
Value int64 `json:"value"`
|
||||
Threshold int64 `json:"thresh"`
|
||||
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status int64 `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status pkg.AttributeStatus `json:"status"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||
}
|
||||
|
||||
func (sa *SmartScsiAttribute) GetStatus() int64 {
|
||||
func (sa *SmartScsiAttribute) GetStatus() pkg.AttributeStatus {
|
||||
return sa.Status
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (sa *SmartScsiAttribute) Flatten() map[string]interface{} {
|
||||
|
||||
//Generated Data
|
||||
fmt.Sprintf("attr.%s.transformed_value", sa.AttributeId): sa.TransformedValue,
|
||||
fmt.Sprintf("attr.%s.status", sa.AttributeId): sa.Status,
|
||||
fmt.Sprintf("attr.%s.status", sa.AttributeId): int64(sa.Status),
|
||||
fmt.Sprintf("attr.%s.status_reason", sa.AttributeId): sa.StatusReason,
|
||||
fmt.Sprintf("attr.%s.failure_rate", sa.AttributeId): sa.FailureRate,
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (sa *SmartScsiAttribute) Inflate(key string, val interface{}) {
|
||||
case "transformed_value":
|
||||
sa.TransformedValue = val.(int64)
|
||||
case "status":
|
||||
sa.Status = val.(int64)
|
||||
sa.Status = pkg.AttributeStatus(val.(int64))
|
||||
case "status_reason":
|
||||
sa.StatusReason = val.(string)
|
||||
case "failure_rate":
|
||||
@@ -73,7 +73,7 @@ func (sa *SmartScsiAttribute) PopulateAttributeStatus() *SmartScsiAttribute {
|
||||
//check what the ideal is. Ideal tells us if we our recorded value needs to be above, or below the threshold
|
||||
if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) ||
|
||||
(smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) {
|
||||
sa.Status = pkg.SmartAttributeStatusFailed
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
|
||||
sa.StatusReason = "Attribute is failing recommended SMART threshold"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,9 +328,12 @@ func TestFromCollectorSmartInfo(t *testing.T) {
|
||||
require.Equal(t, 18, len(smartMdl.Attributes))
|
||||
|
||||
//check that temperature was correctly parsed
|
||||
|
||||
require.Equal(t, int64(163210330144), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).RawValue)
|
||||
require.Equal(t, int64(32), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).TransformedValue)
|
||||
|
||||
//ensure that Scrutiny warning for a non critical attribute does not set device status to failed.
|
||||
require.Equal(t, pkg.AttributeStatusWarningScrutiny, smartMdl.Attributes["3"].GetStatus())
|
||||
|
||||
}
|
||||
|
||||
func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) {
|
||||
@@ -402,7 +405,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
|
||||
require.Equal(t, int64(pkg.SmartAttributeStatusFailed), smartMdl.Attributes["199"].GetStatus(),
|
||||
require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["199"].GetStatus(),
|
||||
"scrutiny should detect that %d failed (status: %d, %s)",
|
||||
smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).AttributeId,
|
||||
smartMdl.Attributes["199"].GetStatus(), smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).StatusReason,
|
||||
@@ -435,7 +438,7 @@ func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
|
||||
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
|
||||
require.Equal(t, int64(pkg.SmartAttributeStatusFailed), smartMdl.Attributes["media_errors"].GetStatus(),
|
||||
require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["media_errors"].GetStatus(),
|
||||
"scrutiny should detect that %s failed (status: %d, %s)",
|
||||
smartMdl.Attributes["media_errors"].(*measurements.SmartNvmeAttribute).AttributeId,
|
||||
smartMdl.Attributes["media_errors"].GetStatus(),
|
||||
|
||||
@@ -6,7 +6,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"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/sirupsen/logrus"
|
||||
@@ -14,28 +18,130 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const NotifyFailureTypeEmailTest = "EmailTest"
|
||||
const NotifyFailureTypeSmartPrefail = "SmartPreFailure"
|
||||
const NotifyFailureTypeBothFailure = "SmartFailure" //SmartFailure always takes precedence when Scrutiny & Smart failed.
|
||||
const NotifyFailureTypeSmartFailure = "SmartFailure"
|
||||
const NotifyFailureTypeSmartErrorLog = "SmartErrorLog"
|
||||
const NotifyFailureTypeSmartSelfTest = "SmartSelfTestLog"
|
||||
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
|
||||
|
||||
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
|
||||
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLevel string, notifyFilterAttributes string) bool {
|
||||
// 1. check if the device is healthy
|
||||
if device.DeviceStatus == pkg.DeviceStatusPassed {
|
||||
return false
|
||||
}
|
||||
|
||||
// setup constants for comparison
|
||||
var requiredDeviceStatus pkg.DeviceStatus
|
||||
var requiredAttrStatus pkg.AttributeStatus
|
||||
if notifyLevel == pkg.NotifyLevelFail {
|
||||
// either scrutiny or smart failures should trigger an email
|
||||
requiredDeviceStatus = pkg.DeviceStatusSet(pkg.DeviceStatusFailedSmart, pkg.DeviceStatusFailedScrutiny)
|
||||
requiredAttrStatus = pkg.AttributeStatusSet(pkg.AttributeStatusFailedSmart, pkg.AttributeStatusFailedScrutiny)
|
||||
} else if notifyLevel == pkg.NotifyLevelFailSmart {
|
||||
//only smart failures
|
||||
requiredDeviceStatus = pkg.DeviceStatusFailedSmart
|
||||
requiredAttrStatus = pkg.AttributeStatusFailedSmart
|
||||
} else {
|
||||
requiredDeviceStatus = pkg.DeviceStatusFailedScrutiny
|
||||
requiredAttrStatus = pkg.AttributeStatusFailedScrutiny
|
||||
}
|
||||
|
||||
// 2. check if the attributes that are failing should be filtered (non-critical)
|
||||
// 3. for any unfiltered attribute, store the failure reason (Smart or Scrutiny)
|
||||
if notifyFilterAttributes == pkg.NotifyFilterAttributesCritical {
|
||||
hasFailingCriticalAttr := false
|
||||
var statusFailingCrtiticalAttr pkg.AttributeStatus
|
||||
|
||||
for attrId, attrData := range smartAttrs.Attributes {
|
||||
//find failing attribute
|
||||
if attrData.GetStatus() == pkg.AttributeStatusPassed {
|
||||
continue //skip all passing attributes
|
||||
}
|
||||
|
||||
// merge the status's of all critical attributes
|
||||
statusFailingCrtiticalAttr = pkg.AttributeStatusSet(statusFailingCrtiticalAttr, attrData.GetStatus())
|
||||
|
||||
//found a failing attribute, see if its critical
|
||||
if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical {
|
||||
hasFailingCriticalAttr = true
|
||||
} else if device.IsNvme() && thresholds.NmveMetadata[attrId].Critical {
|
||||
hasFailingCriticalAttr = true
|
||||
} else {
|
||||
//this is ATA
|
||||
attrIdInt, err := strconv.Atoi(attrId)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if thresholds.AtaMetadata[attrIdInt].Critical {
|
||||
hasFailingCriticalAttr = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !hasFailingCriticalAttr {
|
||||
//no critical attributes are failing, and notifyFilterAttributes == "critical"
|
||||
return false
|
||||
} else {
|
||||
// check if any of the critical attributes have a status that we're looking for
|
||||
return pkg.AttributeStatusHas(statusFailingCrtiticalAttr, requiredAttrStatus)
|
||||
}
|
||||
|
||||
} else {
|
||||
// 2. SKIP - we are processing every attribute.
|
||||
// 3. check if the device failure level matches the wanted failure level.
|
||||
return pkg.DeviceStatusHas(device.DeviceStatus, requiredDeviceStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: include host and/or user label for device.
|
||||
type Payload struct {
|
||||
Date string `json:"date"` //populated by Send function.
|
||||
FailureType string `json:"failure_type"` //EmailTest, SmartFail, ScrutinyFail
|
||||
DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
|
||||
DeviceName string `json:"device_name"` //dev/sda
|
||||
DeviceSerial string `json:"device_serial"` //WDDJ324KSO
|
||||
Test bool `json:"test"` // false
|
||||
|
||||
//should not be populated
|
||||
Subject string `json:"subject"`
|
||||
Message string `json:"message"`
|
||||
//private, populated during init (marked as Public for JSON serialization)
|
||||
Date string `json:"date"` //populated by Send function.
|
||||
FailureType string `json:"failure_type"` //EmailTest, BothFail, SmartFail, ScrutinyFail
|
||||
Subject string `json:"subject"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func NewPayload(device models.Device, test bool) Payload {
|
||||
payload := Payload{
|
||||
DeviceType: device.DeviceType,
|
||||
DeviceName: device.DeviceName,
|
||||
DeviceSerial: device.SerialNumber,
|
||||
Test: test,
|
||||
}
|
||||
|
||||
//validate that the Payload is populated
|
||||
sendDate := time.Now()
|
||||
payload.Date = sendDate.Format(time.RFC3339)
|
||||
payload.FailureType = payload.GenerateFailureType(device.DeviceStatus)
|
||||
payload.Subject = payload.GenerateSubject()
|
||||
payload.Message = payload.GenerateMessage()
|
||||
return payload
|
||||
}
|
||||
|
||||
func (p *Payload) GenerateFailureType(deviceStatus pkg.DeviceStatus) string {
|
||||
//generate a failure type, given Test and DeviceStatus
|
||||
if p.Test {
|
||||
return NotifyFailureTypeEmailTest // must be an email test if "Test" is true
|
||||
}
|
||||
if pkg.DeviceStatusHas(deviceStatus, pkg.DeviceStatusFailedSmart) && pkg.DeviceStatusHas(deviceStatus, pkg.DeviceStatusFailedScrutiny) {
|
||||
return NotifyFailureTypeBothFailure //both failed
|
||||
} else if pkg.DeviceStatusHas(deviceStatus, pkg.DeviceStatusFailedSmart) {
|
||||
return NotifyFailureTypeSmartFailure //only SMART failed
|
||||
} else {
|
||||
return NotifyFailureTypeScrutinyFailure //only Scrutiny failed
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) GenerateSubject() string {
|
||||
@@ -61,6 +167,14 @@ Date: %s`, p.DeviceName, p.FailureType, p.DeviceName, p.DeviceSerial, p.DeviceTy
|
||||
return message
|
||||
}
|
||||
|
||||
func New(logger logrus.FieldLogger, appconfig config.Interface, device models.Device, test bool) Notify {
|
||||
return Notify{
|
||||
Logger: logger,
|
||||
Config: appconfig,
|
||||
Payload: NewPayload(device, test),
|
||||
}
|
||||
}
|
||||
|
||||
type Notify struct {
|
||||
Logger logrus.FieldLogger
|
||||
Config config.Interface
|
||||
@@ -68,11 +182,6 @@ type Notify struct {
|
||||
}
|
||||
|
||||
func (n *Notify) Send() error {
|
||||
//validate that the Payload is populated
|
||||
sendDate := time.Now()
|
||||
n.Payload.Date = sendDate.Format(time.RFC3339)
|
||||
n.Payload.Subject = n.Payload.GenerateSubject()
|
||||
n.Payload.Message = n.Payload.GenerateMessage()
|
||||
|
||||
//retrieve list of notification endpoints from config file
|
||||
configUrls := n.Config.GetStringSlice("notify.urls")
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusPassed,
|
||||
}
|
||||
smartAttrs := measurements.Smart{}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyLevelFail_FailingSmartDevice(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyLevelFailSmart_FailingSmartDevice(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{}
|
||||
notifyLevel := pkg.NotifyLevelFailSmart
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyLevelFailScrutiny_FailingSmartDevice(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{}
|
||||
notifyLevel := pkg.NotifyLevelFailScrutiny
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
|
||||
"5": &measurements.SmartAtaAttribute{
|
||||
Status: pkg.AttributeStatusFailedSmart,
|
||||
},
|
||||
}}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
|
||||
"5": &measurements.SmartAtaAttribute{
|
||||
Status: pkg.AttributeStatusPassed,
|
||||
},
|
||||
"10": &measurements.SmartAtaAttribute{
|
||||
Status: pkg.AttributeStatusFailedScrutiny,
|
||||
},
|
||||
}}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||
|
||||
//assert
|
||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
|
||||
"1": &measurements.SmartAtaAttribute{
|
||||
Status: pkg.AttributeStatusFailedSmart,
|
||||
},
|
||||
}}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
|
||||
"5": &measurements.SmartAtaAttribute{
|
||||
Status: pkg.AttributeStatusPassed,
|
||||
},
|
||||
}}
|
||||
notifyLevel := pkg.NotifyLevelFail
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
}
|
||||
|
||||
func TestShouldNotify_NotifyFilterAttributesCritical_NotifyLevelFailSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
device := models.Device{
|
||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||
}
|
||||
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
|
||||
"5": &measurements.SmartAtaAttribute{
|
||||
Status: pkg.AttributeStatusPassed,
|
||||
},
|
||||
"10": &measurements.SmartAtaAttribute{
|
||||
Status: pkg.AttributeStatusFailedScrutiny,
|
||||
},
|
||||
}}
|
||||
notifyLevel := pkg.NotifyLevelFailSmart
|
||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||
|
||||
//assert
|
||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||
}
|
||||
@@ -36,56 +36,6 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
|
||||
Ideal: ObservedThresholdIdealLow,
|
||||
Critical: false,
|
||||
Description: "(Vendor specific raw value.) Stores data related to the rate of hardware read errors that occurred when reading data from a disk surface. The raw value has different structure for different vendors and is often not meaningful as a decimal number.",
|
||||
ObservedThresholds: []ObservedThreshold{
|
||||
{
|
||||
Low: 80,
|
||||
High: 95,
|
||||
AnnualFailureRate: 0.8879749768303985,
|
||||
ErrorInterval: []float64{0.682344353388663, 1.136105732920724},
|
||||
},
|
||||
{
|
||||
Low: 95,
|
||||
High: 110,
|
||||
AnnualFailureRate: 0.034155719633986996,
|
||||
ErrorInterval: []float64{0.030188482024981093, 0.038499386872354435},
|
||||
},
|
||||
{
|
||||
Low: 110,
|
||||
High: 125,
|
||||
AnnualFailureRate: 0.06390002135229157,
|
||||
ErrorInterval: []float64{0.05852004676110847, 0.06964160930553712},
|
||||
},
|
||||
{
|
||||
Low: 125,
|
||||
High: 140,
|
||||
AnnualFailureRate: 0,
|
||||
ErrorInterval: []float64{0, 0},
|
||||
},
|
||||
{
|
||||
Low: 140,
|
||||
High: 155,
|
||||
AnnualFailureRate: 0,
|
||||
ErrorInterval: []float64{0, 0},
|
||||
},
|
||||
{
|
||||
Low: 155,
|
||||
High: 170,
|
||||
AnnualFailureRate: 0,
|
||||
ErrorInterval: []float64{0, 0},
|
||||
},
|
||||
{
|
||||
Low: 170,
|
||||
High: 185,
|
||||
AnnualFailureRate: 0,
|
||||
ErrorInterval: []float64{0, 0},
|
||||
},
|
||||
{
|
||||
Low: 185,
|
||||
High: 200,
|
||||
AnnualFailureRate: 0.044823775021490854,
|
||||
ErrorInterval: []float64{0.032022762038723306, 0.06103725943096589},
|
||||
},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
ID: 2,
|
||||
@@ -290,56 +240,6 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
|
||||
Ideal: "",
|
||||
Critical: false,
|
||||
Description: "(Vendor specific raw value.) Rate of seek errors of the magnetic heads. If there is a partial failure in the mechanical positioning system, then seek errors will arise. Such a failure may be due to numerous factors, such as damage to a servo, or thermal widening of the hard disk. The raw value has different structure for different vendors and is often not meaningful as a decimal number.",
|
||||
ObservedThresholds: []ObservedThreshold{
|
||||
{
|
||||
Low: 58,
|
||||
High: 76,
|
||||
AnnualFailureRate: 0.2040131025936549,
|
||||
ErrorInterval: []float64{0.17032852883286412, 0.2424096283327138},
|
||||
},
|
||||
{
|
||||
Low: 76,
|
||||
High: 94,
|
||||
AnnualFailureRate: 0.08725919610118257,
|
||||
ErrorInterval: []float64{0.08077138510999876, 0.09412943212007528},
|
||||
},
|
||||
{
|
||||
Low: 94,
|
||||
High: 112,
|
||||
AnnualFailureRate: 0.01087335627722523,
|
||||
ErrorInterval: []float64{0.008732197944943352, 0.013380600544561905},
|
||||
},
|
||||
{
|
||||
Low: 112,
|
||||
High: 130,
|
||||
AnnualFailureRate: 0,
|
||||
ErrorInterval: []float64{0, 0},
|
||||
},
|
||||
{
|
||||
Low: 130,
|
||||
High: 148,
|
||||
AnnualFailureRate: 0,
|
||||
ErrorInterval: []float64{0, 0},
|
||||
},
|
||||
{
|
||||
Low: 148,
|
||||
High: 166,
|
||||
AnnualFailureRate: 0,
|
||||
ErrorInterval: []float64{0, 0},
|
||||
},
|
||||
{
|
||||
Low: 166,
|
||||
High: 184,
|
||||
AnnualFailureRate: 0,
|
||||
ErrorInterval: []float64{0, 0},
|
||||
},
|
||||
{
|
||||
Low: 184,
|
||||
High: 202,
|
||||
AnnualFailureRate: 0.05316285755900475,
|
||||
ErrorInterval: []float64{0.03370069132942804, 0.07977038905848267},
|
||||
},
|
||||
},
|
||||
},
|
||||
8: {
|
||||
ID: 8,
|
||||
@@ -445,50 +345,6 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
|
||||
Ideal: ObservedThresholdIdealLow,
|
||||
Critical: false,
|
||||
Description: "This attribute indicates the count of full hard disk power on/off cycles.",
|
||||
ObservedThresholds: []ObservedThreshold{
|
||||
{
|
||||
Low: 0,
|
||||
High: 13,
|
||||
AnnualFailureRate: 0.019835987118930823,
|
||||
ErrorInterval: []float64{0.016560870164523494, 0.023569242386797896},
|
||||
},
|
||||
{
|
||||
Low: 13,
|
||||
High: 26,
|
||||
AnnualFailureRate: 0.038210930067894826,
|
||||
ErrorInterval: []float64{0.03353859179329295, 0.0433520775718649},
|
||||
},
|
||||
{
|
||||
Low: 26,
|
||||
High: 39,
|
||||
AnnualFailureRate: 0.11053528307302571,
|
||||
ErrorInterval: []float64{0.09671061589521368, 0.1257816678419765},
|
||||
},
|
||||
{
|
||||
Low: 39,
|
||||
High: 52,
|
||||
AnnualFailureRate: 0.16831189443375036,
|
||||
ErrorInterval: []float64{0.1440976510675928, 0.19543066007594895},
|
||||
},
|
||||
{
|
||||
Low: 52,
|
||||
High: 65,
|
||||
AnnualFailureRate: 0.20630344262550107,
|
||||
ErrorInterval: []float64{0.1693965932069108, 0.2488633537247856},
|
||||
},
|
||||
{
|
||||
Low: 65,
|
||||
High: 78,
|
||||
AnnualFailureRate: 0.1030972634140512,
|
||||
ErrorInterval: []float64{0.06734655535304743, 0.15106137807407605},
|
||||
},
|
||||
{
|
||||
Low: 78,
|
||||
High: 91,
|
||||
AnnualFailureRate: 0.12354840389522469,
|
||||
ErrorInterval: []float64{0.06578432170016109, 0.21127153335749593},
|
||||
},
|
||||
},
|
||||
},
|
||||
13: {
|
||||
ID: 13,
|
||||
|
||||
@@ -19,7 +19,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "low",
|
||||
Critical: true,
|
||||
Description: "",
|
||||
Description: "The grown defect count shows the amount of swapped (defective) blocks since the drive was shipped by it's vendor. Each additional defective block increases the count by one.",
|
||||
},
|
||||
"read_errors_corrected_by_eccfast": {
|
||||
ID: "read_errors_corrected_by_eccfast",
|
||||
@@ -27,7 +27,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "",
|
||||
Critical: false,
|
||||
Description: "",
|
||||
Description: "An error correction was applied to get perfect data (a.k.a. ECC on-the-fly). \"Without substantial delay\" means the correction did not postpone reading of later sectors (e.g. a revolution was not lost). The counter is incremented once for each logical block that requires correction. Two different blocks corrected during the same command are counted as two events.",
|
||||
},
|
||||
"read_errors_corrected_by_eccdelayed": {
|
||||
ID: "read_errors_corrected_by_eccdelayed",
|
||||
@@ -35,7 +35,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "",
|
||||
Critical: false,
|
||||
Description: "",
|
||||
Description: "An error code or algorithm (e.g. ECC, checksum) is applied in order to get perfect data with substantial delay. \"With possible delay\" means the correction took longer than a sector time so that reading/writing of subsequent sectors was delayed (e.g. a lost revolution). The counter is incremented once for each logical block that requires correction. A block with a double error that is correctable counts as one event and two different blocks corrected during the same command count as two events. ",
|
||||
},
|
||||
"read_errors_corrected_by_rereads_rewrites": {
|
||||
ID: "read_errors_corrected_by_rereads_rewrites",
|
||||
@@ -43,7 +43,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "low",
|
||||
Critical: true,
|
||||
Description: "",
|
||||
Description: "This parameter code specifies the counter counting the number of errors that are corrected by applying retries. This counts errors recovered, not the number of retries. If five retries were required to recover one block of data, the counter increments by one, not five. The counter is incremented once for each logical block that is recovered using retries. If an error is not recoverable while applying retries and is recovered by ECC, it isn't counted by this counter; it will be counted by the counter specified by parameter code 01h - Errors Corrected With Possible Delays. ",
|
||||
},
|
||||
"read_total_errors_corrected": {
|
||||
ID: "read_total_errors_corrected",
|
||||
@@ -51,7 +51,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "",
|
||||
Critical: false,
|
||||
Description: "",
|
||||
Description: "This counter counts the total of parameter code errors 00h, 01h and 02h (i.e. error corrected by ECC: fast and delayed plus errors corrected by rereads and rewrites). There is no \"double counting\" of data errors among these three counters. The sum of all correctable errors can be reached by adding parameter code 01h and 02h errors, not by using this total.",
|
||||
},
|
||||
"read_correction_algorithm_invocations": {
|
||||
ID: "read_correction_algorithm_invocations",
|
||||
@@ -59,7 +59,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "",
|
||||
Critical: false,
|
||||
Description: "",
|
||||
Description: "This parameter code specifies the counter that counts the total number of retries, or \"times the retry algorithm is invoked\". If after five attempts a counter 02h type error is recovered, then five is added to this counter. If three retries are required to get stable ECC syndrome before a counter 01h type error is corrected, then those three retries are also counted here. The number of retries applied to unsuccessfully recover an error (counter 06h type error) are also counted by this counter. ",
|
||||
},
|
||||
"read_total_uncorrected_errors": {
|
||||
ID: "read_total_uncorrected_errors",
|
||||
@@ -67,7 +67,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "low",
|
||||
Critical: true,
|
||||
Description: "",
|
||||
Description: "This parameter code specifies the counter that contains the total number of blocks for which an uncorrected data error has occurred. ",
|
||||
},
|
||||
"write_errors_corrected_by_eccfast": {
|
||||
ID: "write_errors_corrected_by_eccfast",
|
||||
@@ -75,7 +75,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "",
|
||||
Critical: false,
|
||||
Description: "",
|
||||
Description: "An error correction was applied to get perfect data (a.k.a. ECC on-the-fly). \"Without substantial delay\" means the correction did not postpone reading of later sectors (e.g. a revolution was not lost). The counter is incremented once for each logical block that requires correction. Two different blocks corrected during the same command are counted as two events. ",
|
||||
},
|
||||
"write_errors_corrected_by_eccdelayed": {
|
||||
ID: "write_errors_corrected_by_eccdelayed",
|
||||
@@ -83,7 +83,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "",
|
||||
Critical: false,
|
||||
Description: "",
|
||||
Description: "An error code or algorithm (e.g. ECC, checksum) is applied in order to get perfect data with substantial delay. \"With possible delay\" means the correction took longer than a sector time so that reading/writing of subsequent sectors was delayed (e.g. a lost revolution). The counter is incremented once for each logical block that requires correction. A block with a double error that is correctable counts as one event and two different blocks corrected during the same command count as two events. ",
|
||||
},
|
||||
"write_errors_corrected_by_rereads_rewrites": {
|
||||
ID: "write_errors_corrected_by_rereads_rewrites",
|
||||
@@ -91,7 +91,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "low",
|
||||
Critical: true,
|
||||
Description: "",
|
||||
Description: "This parameter code specifies the counter counting the number of errors that are corrected by applying retries. This counts errors recovered, not the number of retries. If five retries were required to recover one block of data, the counter increments by one, not five. The counter is incremented once for each logical block that is recovered using retries. If an error is not recoverable while applying retries and is recovered by ECC, it isn't counted by this counter; it will be counted by the counter specified by parameter code 01h - Errors Corrected With Possible Delays.",
|
||||
},
|
||||
"write_total_errors_corrected": {
|
||||
ID: "write_total_errors_corrected",
|
||||
@@ -99,7 +99,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "",
|
||||
Critical: false,
|
||||
Description: "",
|
||||
Description: "This counter counts the total of parameter code errors 00h, 01h and 02h (i.e. error corrected by ECC: fast and delayed plus errors corrected by rereads and rewrites). There is no \"double counting\" of data errors among these three counters. The sum of all correctable errors can be reached by adding parameter code 01h and 02h errors, not by using this total.",
|
||||
},
|
||||
"write_correction_algorithm_invocations": {
|
||||
ID: "write_correction_algorithm_invocations",
|
||||
@@ -107,7 +107,7 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "",
|
||||
Critical: false,
|
||||
Description: "",
|
||||
Description: "This parameter code specifies the counter that counts the total number of retries, or \"times the retry algorithm is invoked\". If after five attempts a counter 02h type error is recovered, then five is added to this counter. If three retries are required to get stable ECC syndrome before a counter 01h type error is corrected, then those three retries are also counted here. The number of retries applied to unsuccessfully recover an error (counter 06h type error) are also counted by this counter. ",
|
||||
},
|
||||
"write_total_uncorrected_errors": {
|
||||
ID: "write_total_uncorrected_errors",
|
||||
@@ -115,6 +115,6 @@ var ScsiMetadata = map[string]ScsiAttributeMetadata{
|
||||
DisplayType: "",
|
||||
Ideal: "low",
|
||||
Critical: true,
|
||||
Description: "",
|
||||
Description: " This parameter code specifies the counter that contains the total number of blocks for which an uncorrected data error has occurred.",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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.4.6"
|
||||
const VERSION = "0.4.14"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func DeleteDevice(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
err := deviceRepo.DeleteDevice(c, c.Param("wwn"))
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while deleting device", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
@@ -15,17 +15,16 @@ func SendTestNotification(c *gin.Context) {
|
||||
appConfig := c.MustGet("CONFIG").(config.Interface)
|
||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||
|
||||
testNotify := notify.Notify{
|
||||
Logger: logger,
|
||||
Config: appConfig,
|
||||
Payload: notify.Payload{
|
||||
FailureType: "EmailTest",
|
||||
DeviceSerial: "FAKEWDDJ324KSO",
|
||||
testNotify := notify.New(
|
||||
logger,
|
||||
appConfig,
|
||||
models.Device{
|
||||
SerialNumber: "FAKEWDDJ324KSO",
|
||||
DeviceType: pkg.DeviceProtocolAta,
|
||||
DeviceName: "/dev/sda",
|
||||
Test: true,
|
||||
},
|
||||
}
|
||||
true,
|
||||
)
|
||||
err := testNotify.Send()
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while sending test notification", err)
|
||||
|
||||
@@ -63,20 +63,16 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
}
|
||||
|
||||
//check for error
|
||||
if updatedDevice.DeviceStatus != pkg.DeviceStatusPassed {
|
||||
if notify.ShouldNotify(updatedDevice, smartData, appConfig.GetString("notify.level"), appConfig.GetString("notify.filter_attributes")) {
|
||||
//send notifications
|
||||
testNotify := notify.Notify{
|
||||
Config: appConfig,
|
||||
Payload: notify.Payload{
|
||||
FailureType: notify.NotifyFailureTypeSmartFailure,
|
||||
DeviceName: updatedDevice.DeviceName,
|
||||
DeviceType: updatedDevice.DeviceProtocol,
|
||||
DeviceSerial: updatedDevice.SerialNumber,
|
||||
Test: false,
|
||||
},
|
||||
Logger: logger,
|
||||
}
|
||||
_ = testNotify.Send() //we ignore error message when sending notifications.
|
||||
|
||||
liveNotify := notify.New(
|
||||
logger,
|
||||
appConfig,
|
||||
updatedDevice,
|
||||
false,
|
||||
)
|
||||
_ = liveNotify.Send() //we ignore error message when sending notifications.
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AppEngine struct {
|
||||
@@ -47,6 +48,8 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
|
||||
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.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +69,11 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
|
||||
}
|
||||
|
||||
func (ae *AppEngine) Start() error {
|
||||
//set the gin mode
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
if strings.ToLower(ae.Config.GetString("log.level")) == "debug" {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
}
|
||||
|
||||
logger := logrus.New()
|
||||
//set default log level
|
||||
|
||||
@@ -93,6 +93,7 @@ func (suite *ServerTestSuite) TestHealthRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -132,6 +133,7 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -171,6 +173,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -183,6 +186,8 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
} else {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("localhost").AnyTimes()
|
||||
}
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
|
||||
ae := web.AppEngine{
|
||||
Config: fakeConfig,
|
||||
@@ -216,9 +221,12 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
//fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return("testdata/scrutiny_test.db")
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").Return([]string{}).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -314,6 +322,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -321,6 +330,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://unroutable.domain.example.asdfghj"})
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
|
||||
@@ -352,6 +364,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -359,6 +372,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///missing/path/on/disk"})
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
|
||||
@@ -390,6 +406,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -397,6 +414,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///usr/bin/env"})
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
|
||||
@@ -428,6 +448,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -435,6 +456,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"discord://invalidtoken@channel"})
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
|
||||
@@ -465,6 +489,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -472,6 +497,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{})
|
||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
|
||||
@@ -509,5 +536,5 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
|
||||
//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.DeviceStatusFailedScrutiny, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
|
||||
require.Equal(suite.T(), pkg.DeviceStatusPassed, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
|
||||
}
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ -z "${CI}" ]]; then
|
||||
echo "running locally (not in Github Actions). generating version file from git client"
|
||||
GIT_TAG=`git describe --tags`
|
||||
GIT_BRANCH=`git rev-parse --abbrev-ref HEAD`
|
||||
|
||||
if [[ "$GIT_BRANCH" == "master" ]]; then
|
||||
VERSION_INFO="${GIT_TAG}"
|
||||
else
|
||||
VERSION_INFO="${GIT_BRANCH}#${GIT_TAG}"
|
||||
fi
|
||||
else
|
||||
echo "running in Github Actions, generating version file from environmental variables"
|
||||
# https://docs.github.com/en/actions/learn-github-actions/environment-variables
|
||||
VERSION_INFO="${GITHUB_REF_NAME}"
|
||||
|
||||
if [[ "$GITHUB_REF_TYPE" == "branch" ]]; then
|
||||
VERSION_INFO="${VERSION_INFO}#${GITHUB_SHA::7}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "writing version file (version: ${VERSION_INFO})"
|
||||
cat <<EOT > src/environments/versions.ts
|
||||
// this file is automatically generated by git.version.ts script
|
||||
export const versionInfo = {
|
||||
version: '${VERSION_INFO}',
|
||||
};
|
||||
EOT
|
||||
Generated
+20
-20
@@ -29,13 +29,13 @@
|
||||
"@fullcalendar/rrule": "4.4.0",
|
||||
"@fullcalendar/timegrid": "4.4.0",
|
||||
"@types/humanize-duration": "^3.18.1",
|
||||
"apexcharts": "3.19.0",
|
||||
"apexcharts": "3.19.2",
|
||||
"crypto-js": "3.3.0",
|
||||
"highlight.js": "10.0.1",
|
||||
"humanize-duration": "^3.24.0",
|
||||
"lodash": "4.17.15",
|
||||
"moment": "2.24.0",
|
||||
"ng-apexcharts": "1.2.3",
|
||||
"ng-apexcharts": "1.5.12",
|
||||
"ngx-markdown": "9.0.0",
|
||||
"ngx-quill": "9.1.0",
|
||||
"perfect-scrollbar": "1.5.0",
|
||||
@@ -2964,9 +2964,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/apexcharts": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.19.0.tgz",
|
||||
"integrity": "sha512-fzupCGVDvOoU6kEzguLAfgRgrlHynHM5fnkkyCL85tYf9U8bw1hCijs4A+kWXurC/SNytJrArBc21kA/2wuHYg==",
|
||||
"version": "3.19.2",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.19.2.tgz",
|
||||
"integrity": "sha512-hMFLRE2Lyx4WrN9pYfQLvBDcn+HOodZrqRwc+kucxM+hcUmI2NHY4z+GI14+VcSFmD4aKiMbS3z3Q2jiBxUrcg==",
|
||||
"dependencies": {
|
||||
"svg.draggable.js": "^2.2.2",
|
||||
"svg.easing.js": "^2.0.0",
|
||||
@@ -9958,17 +9958,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ng-apexcharts": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.2.3.tgz",
|
||||
"integrity": "sha512-4G+JRCWp8uSSBJKvYP9vKHEZIC0w6YuRLasumZS35fCCc7bzLY+L907n8khG9Xeoo4LBt7pVbmjb9P+lSWs/5g==",
|
||||
"version": "1.5.12",
|
||||
"resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.5.12.tgz",
|
||||
"integrity": "sha512-k82AdWNbZs5yqGCjiX7PGS11Cy1+1Oo/RGt2lT89xReD9N9Vvo1t34p1dmzS+U6W5wOFlLEKKVLGNQqENW8cTQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.0"
|
||||
"tslib": "^1.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^8.0.0",
|
||||
"@angular/core": "^8.0.0",
|
||||
"apexcharts": "^3.11.2",
|
||||
"rxjs": "^6.4.0"
|
||||
"@angular/common": ">=9.0.0 <13.0.0",
|
||||
"@angular/core": ">=9.0.0 <13.0.0",
|
||||
"apexcharts": "^3.19.2",
|
||||
"rxjs": "^6.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-markdown": {
|
||||
@@ -20708,9 +20708,9 @@
|
||||
}
|
||||
},
|
||||
"apexcharts": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.19.0.tgz",
|
||||
"integrity": "sha512-fzupCGVDvOoU6kEzguLAfgRgrlHynHM5fnkkyCL85tYf9U8bw1hCijs4A+kWXurC/SNytJrArBc21kA/2wuHYg==",
|
||||
"version": "3.19.2",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.19.2.tgz",
|
||||
"integrity": "sha512-hMFLRE2Lyx4WrN9pYfQLvBDcn+HOodZrqRwc+kucxM+hcUmI2NHY4z+GI14+VcSFmD4aKiMbS3z3Q2jiBxUrcg==",
|
||||
"requires": {
|
||||
"svg.draggable.js": "^2.2.2",
|
||||
"svg.easing.js": "^2.0.0",
|
||||
@@ -26486,11 +26486,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"ng-apexcharts": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.2.3.tgz",
|
||||
"integrity": "sha512-4G+JRCWp8uSSBJKvYP9vKHEZIC0w6YuRLasumZS35fCCc7bzLY+L907n8khG9Xeoo4LBt7pVbmjb9P+lSWs/5g==",
|
||||
"version": "1.5.12",
|
||||
"resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.5.12.tgz",
|
||||
"integrity": "sha512-k82AdWNbZs5yqGCjiX7PGS11Cy1+1Oo/RGt2lT89xReD9N9Vvo1t34p1dmzS+U6W5wOFlLEKKVLGNQqENW8cTQ==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"ngx-markdown": {
|
||||
|
||||
@@ -40,13 +40,13 @@
|
||||
"@fullcalendar/rrule": "4.4.0",
|
||||
"@fullcalendar/timegrid": "4.4.0",
|
||||
"@types/humanize-duration": "^3.18.1",
|
||||
"apexcharts": "3.19.0",
|
||||
"apexcharts": "3.19.2",
|
||||
"crypto-js": "3.3.0",
|
||||
"highlight.js": "10.0.1",
|
||||
"humanize-duration": "^3.24.0",
|
||||
"lodash": "4.17.15",
|
||||
"moment": "2.24.0",
|
||||
"ng-apexcharts": "1.2.3",
|
||||
"ng-apexcharts": "1.5.12",
|
||||
"ngx-markdown": "9.0.0",
|
||||
"ngx-quill": "9.1.0",
|
||||
"perfect-scrollbar": "1.5.0",
|
||||
|
||||
@@ -2,6 +2,9 @@ import { Inject, Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import * as _ from 'lodash';
|
||||
import { TREO_APP_CONFIG } from '@treo/services/config/config.constants';
|
||||
import { AppConfig } from 'app/core/config/app.config';
|
||||
|
||||
const SCRUTINY_CONFIG_LOCAL_STORAGE_KEY = 'scrutiny';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -14,10 +17,18 @@ export class TreoConfigService
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(@Inject(TREO_APP_CONFIG) config: any)
|
||||
constructor(@Inject(TREO_APP_CONFIG) defaultConfig: any)
|
||||
{
|
||||
let currentScrutinyConfig = defaultConfig
|
||||
|
||||
const localConfigStr = localStorage.getItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY)
|
||||
if (localConfigStr){
|
||||
// check localstorage for a value
|
||||
const localConfig = JSON.parse(localConfigStr)
|
||||
currentScrutinyConfig = Object.assign({}, currentScrutinyConfig, localConfig) // make sure defaults are available if missing from localStorage.
|
||||
}
|
||||
// Set the private defaults
|
||||
this._config = new BehaviorSubject(config);
|
||||
this._config = new BehaviorSubject(currentScrutinyConfig);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@@ -27,20 +38,29 @@ export class TreoConfigService
|
||||
/**
|
||||
* Setter and getter for config
|
||||
*/
|
||||
// Setter
|
||||
set config(value: any)
|
||||
{
|
||||
// Merge the new config over to the current config
|
||||
const config = _.merge({}, this._config.getValue(), value);
|
||||
|
||||
// Store the config in localstorage
|
||||
localStorage.setItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
|
||||
|
||||
// Execute the observable
|
||||
this._config.next(config);
|
||||
}
|
||||
|
||||
// Getter
|
||||
get config$(): Observable<any>
|
||||
{
|
||||
return this._config.asObservable();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Layout } from "app/layout/layout.types";
|
||||
import { Layout } from 'app/layout/layout.types';
|
||||
|
||||
// Theme type
|
||||
export type Theme = "light" | "dark";
|
||||
export type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
/**
|
||||
* AppConfig interface. Update this interface to strictly type your config
|
||||
@@ -11,6 +11,12 @@ export interface AppConfig
|
||||
{
|
||||
theme: Theme;
|
||||
layout: Layout;
|
||||
|
||||
// Dashboard options
|
||||
dashboardDisplay: string;
|
||||
dashboardSort: string;
|
||||
|
||||
temperatureUnit: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,7 +28,12 @@ export interface AppConfig
|
||||
* "ConfigService".
|
||||
*/
|
||||
export const appConfig: AppConfig = {
|
||||
theme : "light",
|
||||
layout: "material"
|
||||
theme : 'light',
|
||||
layout: 'material',
|
||||
|
||||
dashboardDisplay: 'name',
|
||||
dashboardSort: 'status',
|
||||
|
||||
temperatureUnit: 'celsius',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,264 +1,264 @@
|
||||
export const sda = {
|
||||
"data": {
|
||||
"device": {
|
||||
"CreatedAt": "2021-06-24T21:17:31.301226-07:00",
|
||||
"UpdatedAt": "2021-10-24T16:37:56.981833-07:00",
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5002538e40a22954",
|
||||
"device_name": "sda",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "Samsung_SSD_860_EVO_500GB",
|
||||
"interface_type": "SCSI",
|
||||
"interface_speed": "",
|
||||
"serial_number": "S3YZNB0KBXXXXXX",
|
||||
"firmware": "002C",
|
||||
"rotational_speed": 0,
|
||||
"capacity": 500107862016,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"device_protocol": "NVMe",
|
||||
"device_type": "",
|
||||
"label": "",
|
||||
"host_id": "",
|
||||
"device_status": 0
|
||||
'data': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-06-24T21:17:31.301226-07:00',
|
||||
'UpdatedAt': '2021-10-24T16:37:56.981833-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5002538e40a22954',
|
||||
'device_name': 'sda',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'Samsung_SSD_860_EVO_500GB',
|
||||
'interface_type': 'SCSI',
|
||||
'interface_speed': '',
|
||||
'serial_number': 'S3YZNB0KBXXXXXX',
|
||||
'firmware': '002C',
|
||||
'rotational_speed': 0,
|
||||
'capacity': 500107862016,
|
||||
'form_factor': '',
|
||||
'smart_support': false,
|
||||
'device_protocol': 'NVMe',
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
},
|
||||
"smart_results": [{
|
||||
"date": "2021-10-24T23:20:44Z",
|
||||
"device_wwn": "0x5002538e40a22954",
|
||||
"device_protocol": "NVMe",
|
||||
"temp": 36,
|
||||
"power_on_hours": 2401,
|
||||
"power_cycle_count": 266,
|
||||
"attrs": {
|
||||
"available_spare": {
|
||||
"attribute_id": "available_spare",
|
||||
"value": 100,
|
||||
"thresh": 10,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'smart_results': [{
|
||||
'date': '2021-10-24T23:20:44Z',
|
||||
'device_wwn': '0x5002538e40a22954',
|
||||
'device_protocol': 'NVMe',
|
||||
'temp': 36,
|
||||
'power_on_hours': 2401,
|
||||
'power_cycle_count': 266,
|
||||
'attrs': {
|
||||
'available_spare': {
|
||||
'attribute_id': 'available_spare',
|
||||
'value': 100,
|
||||
'thresh': 10,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"controller_busy_time": {
|
||||
"attribute_id": "controller_busy_time",
|
||||
"value": 3060,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'controller_busy_time': {
|
||||
'attribute_id': 'controller_busy_time',
|
||||
'value': 3060,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"critical_comp_time": {
|
||||
"attribute_id": "critical_comp_time",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'critical_comp_time': {
|
||||
'attribute_id': 'critical_comp_time',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"critical_warning": {
|
||||
"attribute_id": "critical_warning",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'critical_warning': {
|
||||
'attribute_id': 'critical_warning',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"data_units_read": {
|
||||
"attribute_id": "data_units_read",
|
||||
"value": 9511859,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'data_units_read': {
|
||||
'attribute_id': 'data_units_read',
|
||||
'value': 9511859,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"data_units_written": {
|
||||
"attribute_id": "data_units_written",
|
||||
"value": 7773431,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'data_units_written': {
|
||||
'attribute_id': 'data_units_written',
|
||||
'value': 7773431,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"host_reads": {
|
||||
"attribute_id": "host_reads",
|
||||
"value": 111303174,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'host_reads': {
|
||||
'attribute_id': 'host_reads',
|
||||
'value': 111303174,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"host_writes": {
|
||||
"attribute_id": "host_writes",
|
||||
"value": 83170961,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'host_writes': {
|
||||
'attribute_id': 'host_writes',
|
||||
'value': 83170961,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"media_errors": {
|
||||
"attribute_id": "media_errors",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'media_errors': {
|
||||
'attribute_id': 'media_errors',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"num_err_log_entries": {
|
||||
"attribute_id": "num_err_log_entries",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'num_err_log_entries': {
|
||||
'attribute_id': 'num_err_log_entries',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"percentage_used": {
|
||||
"attribute_id": "percentage_used",
|
||||
"value": 0,
|
||||
"thresh": 100,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'percentage_used': {
|
||||
'attribute_id': 'percentage_used',
|
||||
'value': 0,
|
||||
'thresh': 100,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"power_cycles": {
|
||||
"attribute_id": "power_cycles",
|
||||
"value": 266,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'power_cycles': {
|
||||
'attribute_id': 'power_cycles',
|
||||
'value': 266,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"power_on_hours": {
|
||||
"attribute_id": "power_on_hours",
|
||||
"value": 2401,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'power_on_hours': {
|
||||
'attribute_id': 'power_on_hours',
|
||||
'value': 2401,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"temperature": {
|
||||
"attribute_id": "temperature",
|
||||
"value": 36,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'temperature': {
|
||||
'attribute_id': 'temperature',
|
||||
'value': 36,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"unsafe_shutdowns": {
|
||||
"attribute_id": "unsafe_shutdowns",
|
||||
"value": 43,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'unsafe_shutdowns': {
|
||||
'attribute_id': 'unsafe_shutdowns',
|
||||
'value': 43,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"warning_temp_time": {
|
||||
"attribute_id": "warning_temp_time",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'warning_temp_time': {
|
||||
'attribute_id': 'warning_temp_time',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
}
|
||||
},
|
||||
"Status": 0
|
||||
'Status': 0
|
||||
}]
|
||||
},
|
||||
"metadata": {
|
||||
"available_spare": {
|
||||
"display_name": "Available Spare",
|
||||
"ideal": "high",
|
||||
"critical": true,
|
||||
"description": "Contains a normalized percentage (0 to 100%) of the remaining spare capacity available.",
|
||||
"display_type": ""
|
||||
'metadata': {
|
||||
'available_spare': {
|
||||
'display_name': 'Available Spare',
|
||||
'ideal': 'high',
|
||||
'critical': true,
|
||||
'description': 'Contains a normalized percentage (0 to 100%) of the remaining spare capacity available.',
|
||||
'display_type': ''
|
||||
},
|
||||
"controller_busy_time": {
|
||||
"display_name": "Controller Busy Time",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "Contains the amount of time the controller is busy with I/O commands. The controller is busy when there is a command outstanding to an I/O Queue (specifically, a command was issued via an I/O Submission Queue Tail doorbell write and the corresponding completion queue entry has not been posted yet to the associated I/O Completion Queue). This value is reported in minutes.",
|
||||
"display_type": ""
|
||||
'controller_busy_time': {
|
||||
'display_name': 'Controller Busy Time',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': 'Contains the amount of time the controller is busy with I/O commands. The controller is busy when there is a command outstanding to an I/O Queue (specifically, a command was issued via an I/O Submission Queue Tail doorbell write and the corresponding completion queue entry has not been posted yet to the associated I/O Completion Queue). This value is reported in minutes.',
|
||||
'display_type': ''
|
||||
},
|
||||
"critical_comp_time": {
|
||||
"display_name": "Critical CompTime",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "Contains the amount of time in minutes that the controller is operational and the Composite Temperature is greater the Critical Composite Temperature Threshold (CCTEMP) field in the Identify Controller data structure.",
|
||||
"display_type": ""
|
||||
'critical_comp_time': {
|
||||
'display_name': 'Critical CompTime',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': 'Contains the amount of time in minutes that the controller is operational and the Composite Temperature is greater the Critical Composite Temperature Threshold (CCTEMP) field in the Identify Controller data structure.',
|
||||
'display_type': ''
|
||||
},
|
||||
"critical_warning": {
|
||||
"display_name": "Critical Warning",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "This field indicates critical warnings for the state of the controller. Each bit corresponds to a critical warning type; multiple bits may be set. If a bit is cleared to ‘0’, then that critical warning does not apply. Critical warnings may result in an asynchronous event notification to the host. Bits in this field represent the current associated state and are not persistent.",
|
||||
"display_type": ""
|
||||
'critical_warning': {
|
||||
'display_name': 'Critical Warning',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': 'This field indicates critical warnings for the state of the controller. Each bit corresponds to a critical warning type; multiple bits may be set. If a bit is cleared to ‘0’, then that critical warning does not apply. Critical warnings may result in an asynchronous event notification to the host. Bits in this field represent the current associated state and are not persistent.',
|
||||
'display_type': ''
|
||||
},
|
||||
"data_units_read": {
|
||||
"display_name": "Data Units Read",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "Contains the number of 512 byte data units the host has read from the controller; this value does not include metadata. This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes read) and is rounded up. When the LBA size is a value other than 512 bytes, the controller shall convert the amount of data read to 512 byte units.",
|
||||
"display_type": ""
|
||||
'data_units_read': {
|
||||
'display_name': 'Data Units Read',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': 'Contains the number of 512 byte data units the host has read from the controller; this value does not include metadata. This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes read) and is rounded up. When the LBA size is a value other than 512 bytes, the controller shall convert the amount of data read to 512 byte units.',
|
||||
'display_type': ''
|
||||
},
|
||||
"data_units_written": {
|
||||
"display_name": "Data Units Written",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "Contains the number of 512 byte data units the host has written to the controller; this value does not include metadata. This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes written) and is rounded up. When the LBA size is a value other than 512 bytes, the controller shall convert the amount of data written to 512 byte units.",
|
||||
"display_type": ""
|
||||
'data_units_written': {
|
||||
'display_name': 'Data Units Written',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': 'Contains the number of 512 byte data units the host has written to the controller; this value does not include metadata. This value is reported in thousands (i.e., a value of 1 corresponds to 1000 units of 512 bytes written) and is rounded up. When the LBA size is a value other than 512 bytes, the controller shall convert the amount of data written to 512 byte units.',
|
||||
'display_type': ''
|
||||
},
|
||||
"host_reads": {
|
||||
"display_name": "Host Reads",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "Contains the number of read commands completed by the controller",
|
||||
"display_type": ""
|
||||
'host_reads': {
|
||||
'display_name': 'Host Reads',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': 'Contains the number of read commands completed by the controller',
|
||||
'display_type': ''
|
||||
},
|
||||
"host_writes": {
|
||||
"display_name": "Host Writes",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "Contains the number of write commands completed by the controller",
|
||||
"display_type": ""
|
||||
'host_writes': {
|
||||
'display_name': 'Host Writes',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': 'Contains the number of write commands completed by the controller',
|
||||
'display_type': ''
|
||||
},
|
||||
"media_errors": {
|
||||
"display_name": "Media Errors",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "Contains the number of occurrences where the controller detected an unrecovered data integrity error. Errors such as uncorrectable ECC, CRC checksum failure, or LBA tag mismatch are included in this field.",
|
||||
"display_type": ""
|
||||
'media_errors': {
|
||||
'display_name': 'Media Errors',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': 'Contains the number of occurrences where the controller detected an unrecovered data integrity error. Errors such as uncorrectable ECC, CRC checksum failure, or LBA tag mismatch are included in this field.',
|
||||
'display_type': ''
|
||||
},
|
||||
"num_err_log_entries": {
|
||||
"display_name": "Numb Err Log Entries",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "Contains the number of Error Information log entries over the life of the controller.",
|
||||
"display_type": ""
|
||||
'num_err_log_entries': {
|
||||
'display_name': 'Numb Err Log Entries',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': 'Contains the number of Error Information log entries over the life of the controller.',
|
||||
'display_type': ''
|
||||
},
|
||||
"percentage_used": {
|
||||
"display_name": "Percentage Used",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "Contains a vendor specific estimate of the percentage of NVM subsystem life used based on the actual usage and the manufacturer’s prediction of NVM life. A value of 100 indicates that the estimated endurance of the NVM in the NVM subsystem has been consumed, but may not indicate an NVM subsystem failure. The value is allowed to exceed 100. Percentages greater than 254 shall be represented as 255. This value shall be updated once per power-on hour (when the controller is not in a sleep state).",
|
||||
"display_type": ""
|
||||
'percentage_used': {
|
||||
'display_name': 'Percentage Used',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': 'Contains a vendor specific estimate of the percentage of NVM subsystem life used based on the actual usage and the manufacturer’s prediction of NVM life. A value of 100 indicates that the estimated endurance of the NVM in the NVM subsystem has been consumed, but may not indicate an NVM subsystem failure. The value is allowed to exceed 100. Percentages greater than 254 shall be represented as 255. This value shall be updated once per power-on hour (when the controller is not in a sleep state).',
|
||||
'display_type': ''
|
||||
},
|
||||
"power_cycles": {
|
||||
"display_name": "Power Cycles",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "Contains the number of power cycles.",
|
||||
"display_type": ""
|
||||
'power_cycles': {
|
||||
'display_name': 'Power Cycles',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': 'Contains the number of power cycles.',
|
||||
'display_type': ''
|
||||
},
|
||||
"power_on_hours": {
|
||||
"display_name": "Power on Hours",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "Contains the number of power-on hours. Power on hours is always logging, even when in low power mode.",
|
||||
"display_type": ""
|
||||
'power_on_hours': {
|
||||
'display_name': 'Power on Hours',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': 'Contains the number of power-on hours. Power on hours is always logging, even when in low power mode.',
|
||||
'display_type': ''
|
||||
},
|
||||
"temperature": {
|
||||
"display_name": "Temperature",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'temperature': {
|
||||
'display_name': 'Temperature',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"unsafe_shutdowns": {
|
||||
"display_name": "Unsafe Shutdowns",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "Contains the number of unsafe shutdowns. This count is incremented when a shutdown notification (CC.SHN) is not received prior to loss of power.",
|
||||
"display_type": ""
|
||||
'unsafe_shutdowns': {
|
||||
'display_name': 'Unsafe Shutdowns',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': 'Contains the number of unsafe shutdowns. This count is incremented when a shutdown notification (CC.SHN) is not received prior to loss of power.',
|
||||
'display_type': ''
|
||||
},
|
||||
"warning_temp_time": {
|
||||
"display_name": "Warning Temp Time",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "Contains the amount of time in minutes that the controller is operational and the Composite Temperature is greater than or equal to the Warning Composite Temperature Threshold (WCTEMP) field and less than the Critical Composite Temperature Threshold (CCTEMP) field in the Identify Controller data structure.",
|
||||
"display_type": ""
|
||||
'warning_temp_time': {
|
||||
'display_name': 'Warning Temp Time',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': 'Contains the amount of time in minutes that the controller is operational and the Composite Temperature is greater than or equal to the Warning Composite Temperature Threshold (WCTEMP) field and less than the Critical Composite Temperature Threshold (CCTEMP) field in the Identify Controller data structure.',
|
||||
'display_type': ''
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
'success': true
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,222 +1,222 @@
|
||||
export const sdd = {
|
||||
"data": {
|
||||
"device": {
|
||||
"CreatedAt": "2021-06-24T21:17:31.30374-07:00",
|
||||
"UpdatedAt": "2021-10-24T16:37:57.013758-07:00",
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000cca252c859cc",
|
||||
"device_name": "sdd",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD80EFAX-68LHPN0",
|
||||
"interface_type": "SCSI",
|
||||
"interface_speed": "",
|
||||
"serial_number": "7SGLXXXXX",
|
||||
"firmware": "",
|
||||
"rotational_speed": 0,
|
||||
"capacity": 8001563222016,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"device_protocol": "SCSI",
|
||||
"device_type": "",
|
||||
"label": "",
|
||||
"host_id": "",
|
||||
"device_status": 0
|
||||
'data': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-06-24T21:17:31.30374-07:00',
|
||||
'UpdatedAt': '2021-10-24T16:37:57.013758-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000cca252c859cc',
|
||||
'device_name': 'sdd',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'WDC_WD80EFAX-68LHPN0',
|
||||
'interface_type': 'SCSI',
|
||||
'interface_speed': '',
|
||||
'serial_number': '7SGLXXXXX',
|
||||
'firmware': '',
|
||||
'rotational_speed': 0,
|
||||
'capacity': 8001563222016,
|
||||
'form_factor': '',
|
||||
'smart_support': false,
|
||||
'device_protocol': 'SCSI',
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
},
|
||||
"smart_results": [{
|
||||
"date": "2021-10-24T23:20:44Z",
|
||||
"device_wwn": "0x5000cca252c859cc",
|
||||
"device_protocol": "SCSI",
|
||||
"temp": 34,
|
||||
"power_on_hours": 43549,
|
||||
"power_cycle_count": 0,
|
||||
"attrs": {
|
||||
"read_correction_algorithm_invocations": {
|
||||
"attribute_id": "read_correction_algorithm_invocations",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'smart_results': [{
|
||||
'date': '2021-10-24T23:20:44Z',
|
||||
'device_wwn': '0x5000cca252c859cc',
|
||||
'device_protocol': 'SCSI',
|
||||
'temp': 34,
|
||||
'power_on_hours': 43549,
|
||||
'power_cycle_count': 0,
|
||||
'attrs': {
|
||||
'read_correction_algorithm_invocations': {
|
||||
'attribute_id': 'read_correction_algorithm_invocations',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"read_errors_corrected_by_eccdelayed": {
|
||||
"attribute_id": "read_errors_corrected_by_eccdelayed",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'read_errors_corrected_by_eccdelayed': {
|
||||
'attribute_id': 'read_errors_corrected_by_eccdelayed',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"read_errors_corrected_by_eccfast": {
|
||||
"attribute_id": "read_errors_corrected_by_eccfast",
|
||||
"value": 300357663,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'read_errors_corrected_by_eccfast': {
|
||||
'attribute_id': 'read_errors_corrected_by_eccfast',
|
||||
'value': 300357663,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"read_errors_corrected_by_rereads_rewrites": {
|
||||
"attribute_id": "read_errors_corrected_by_rereads_rewrites",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'read_errors_corrected_by_rereads_rewrites': {
|
||||
'attribute_id': 'read_errors_corrected_by_rereads_rewrites',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"read_total_errors_corrected": {
|
||||
"attribute_id": "read_total_errors_corrected",
|
||||
"value": 300357663,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'read_total_errors_corrected': {
|
||||
'attribute_id': 'read_total_errors_corrected',
|
||||
'value': 300357663,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"read_total_uncorrected_errors": {
|
||||
"attribute_id": "read_total_uncorrected_errors",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'read_total_uncorrected_errors': {
|
||||
'attribute_id': 'read_total_uncorrected_errors',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"scsi_grown_defect_list": {
|
||||
"attribute_id": "scsi_grown_defect_list",
|
||||
"value": 56,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'scsi_grown_defect_list': {
|
||||
'attribute_id': 'scsi_grown_defect_list',
|
||||
'value': 56,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_correction_algorithm_invocations": {
|
||||
"attribute_id": "write_correction_algorithm_invocations",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_correction_algorithm_invocations': {
|
||||
'attribute_id': 'write_correction_algorithm_invocations',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_errors_corrected_by_eccdelayed": {
|
||||
"attribute_id": "write_errors_corrected_by_eccdelayed",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_errors_corrected_by_eccdelayed': {
|
||||
'attribute_id': 'write_errors_corrected_by_eccdelayed',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_errors_corrected_by_eccfast": {
|
||||
"attribute_id": "write_errors_corrected_by_eccfast",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_errors_corrected_by_eccfast': {
|
||||
'attribute_id': 'write_errors_corrected_by_eccfast',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_errors_corrected_by_rereads_rewrites": {
|
||||
"attribute_id": "write_errors_corrected_by_rereads_rewrites",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_errors_corrected_by_rereads_rewrites': {
|
||||
'attribute_id': 'write_errors_corrected_by_rereads_rewrites',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_total_errors_corrected": {
|
||||
"attribute_id": "write_total_errors_corrected",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_total_errors_corrected': {
|
||||
'attribute_id': 'write_total_errors_corrected',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_total_uncorrected_errors": {
|
||||
"attribute_id": "write_total_uncorrected_errors",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_total_uncorrected_errors': {
|
||||
'attribute_id': 'write_total_uncorrected_errors',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
}
|
||||
},
|
||||
"Status": 0
|
||||
'Status': 0
|
||||
}]
|
||||
},
|
||||
"metadata": {
|
||||
"read_correction_algorithm_invocations": {
|
||||
"display_name": "Read Correction Algorithm Invocations",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'metadata': {
|
||||
'read_correction_algorithm_invocations': {
|
||||
'display_name': 'Read Correction Algorithm Invocations',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"read_errors_corrected_by_eccdelayed": {
|
||||
"display_name": "Read Errors Corrected by ECC Delayed",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'read_errors_corrected_by_eccdelayed': {
|
||||
'display_name': 'Read Errors Corrected by ECC Delayed',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"read_errors_corrected_by_eccfast": {
|
||||
"display_name": "Read Errors Corrected by ECC Fast",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'read_errors_corrected_by_eccfast': {
|
||||
'display_name': 'Read Errors Corrected by ECC Fast',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"read_errors_corrected_by_rereads_rewrites": {
|
||||
"display_name": "Read Errors Corrected by ReReads/ReWrites",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'read_errors_corrected_by_rereads_rewrites': {
|
||||
'display_name': 'Read Errors Corrected by ReReads/ReWrites',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"read_total_errors_corrected": {
|
||||
"display_name": "Read Total Errors Corrected",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'read_total_errors_corrected': {
|
||||
'display_name': 'Read Total Errors Corrected',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"read_total_uncorrected_errors": {
|
||||
"display_name": "Read Total Uncorrected Errors",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'read_total_uncorrected_errors': {
|
||||
'display_name': 'Read Total Uncorrected Errors',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"scsi_grown_defect_list": {
|
||||
"display_name": "Grown Defect List",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'scsi_grown_defect_list': {
|
||||
'display_name': 'Grown Defect List',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_correction_algorithm_invocations": {
|
||||
"display_name": "Write Correction Algorithm Invocations",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_correction_algorithm_invocations': {
|
||||
'display_name': 'Write Correction Algorithm Invocations',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_errors_corrected_by_eccdelayed": {
|
||||
"display_name": "Write Errors Corrected by ECC Delayed",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_errors_corrected_by_eccdelayed': {
|
||||
'display_name': 'Write Errors Corrected by ECC Delayed',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_errors_corrected_by_eccfast": {
|
||||
"display_name": "Write Errors Corrected by ECC Fast",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_errors_corrected_by_eccfast': {
|
||||
'display_name': 'Write Errors Corrected by ECC Fast',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_errors_corrected_by_rereads_rewrites": {
|
||||
"display_name": "Write Errors Corrected by ReReads/ReWrites",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_errors_corrected_by_rereads_rewrites': {
|
||||
'display_name': 'Write Errors Corrected by ReReads/ReWrites',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_total_errors_corrected": {
|
||||
"display_name": "Write Total Errors Corrected",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_total_errors_corrected': {
|
||||
'display_name': 'Write Total Errors Corrected',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_total_uncorrected_errors": {
|
||||
"display_name": "Write Total Uncorrected Errors",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_total_uncorrected_errors': {
|
||||
'display_name': 'Write Total Uncorrected Errors',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
'success': true
|
||||
}
|
||||
|
||||
@@ -1,222 +1,222 @@
|
||||
export const sde = {
|
||||
"data": {
|
||||
"device": {
|
||||
"CreatedAt": "2021-06-24T21:17:31.304461-07:00",
|
||||
"UpdatedAt": "2021-10-24T16:40:16.495248-07:00",
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000cca264ebc248",
|
||||
"device_name": "sde",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
"interface_speed": "",
|
||||
"serial_number": "9RK3XXXXX",
|
||||
"firmware": "",
|
||||
"rotational_speed": 0,
|
||||
"capacity": 14000519643136,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"device_protocol": "SCSI",
|
||||
"device_type": "",
|
||||
"label": "",
|
||||
"host_id": "",
|
||||
"device_status": 0
|
||||
'data': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-06-24T21:17:31.304461-07:00',
|
||||
'UpdatedAt': '2021-10-24T16:40:16.495248-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x5000cca264ebc248',
|
||||
'device_name': 'sde',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'WDC_WD140EDFZ-11A0VA0',
|
||||
'interface_type': 'SCSI',
|
||||
'interface_speed': '',
|
||||
'serial_number': '9RK3XXXXX',
|
||||
'firmware': '',
|
||||
'rotational_speed': 0,
|
||||
'capacity': 14000519643136,
|
||||
'form_factor': '',
|
||||
'smart_support': false,
|
||||
'device_protocol': 'SCSI',
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
},
|
||||
"smart_results": [{
|
||||
"date": "2021-10-24T23:20:44Z",
|
||||
"device_wwn": "0x5000cca264ebc248",
|
||||
"device_protocol": "SCSI",
|
||||
"temp": 31,
|
||||
"power_on_hours": 5675,
|
||||
"power_cycle_count": 0,
|
||||
"attrs": {
|
||||
"read_correction_algorithm_invocations": {
|
||||
"attribute_id": "read_correction_algorithm_invocations",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'smart_results': [{
|
||||
'date': '2021-10-24T23:20:44Z',
|
||||
'device_wwn': '0x5000cca264ebc248',
|
||||
'device_protocol': 'SCSI',
|
||||
'temp': 31,
|
||||
'power_on_hours': 5675,
|
||||
'power_cycle_count': 0,
|
||||
'attrs': {
|
||||
'read_correction_algorithm_invocations': {
|
||||
'attribute_id': 'read_correction_algorithm_invocations',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"read_errors_corrected_by_eccdelayed": {
|
||||
"attribute_id": "read_errors_corrected_by_eccdelayed",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'read_errors_corrected_by_eccdelayed': {
|
||||
'attribute_id': 'read_errors_corrected_by_eccdelayed',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"read_errors_corrected_by_eccfast": {
|
||||
"attribute_id": "read_errors_corrected_by_eccfast",
|
||||
"value": 1410362924,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'read_errors_corrected_by_eccfast': {
|
||||
'attribute_id': 'read_errors_corrected_by_eccfast',
|
||||
'value': 1410362924,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"read_errors_corrected_by_rereads_rewrites": {
|
||||
"attribute_id": "read_errors_corrected_by_rereads_rewrites",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'read_errors_corrected_by_rereads_rewrites': {
|
||||
'attribute_id': 'read_errors_corrected_by_rereads_rewrites',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"read_total_errors_corrected": {
|
||||
"attribute_id": "read_total_errors_corrected",
|
||||
"value": 1410362924,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'read_total_errors_corrected': {
|
||||
'attribute_id': 'read_total_errors_corrected',
|
||||
'value': 1410362924,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"read_total_uncorrected_errors": {
|
||||
"attribute_id": "read_total_uncorrected_errors",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'read_total_uncorrected_errors': {
|
||||
'attribute_id': 'read_total_uncorrected_errors',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"scsi_grown_defect_list": {
|
||||
"attribute_id": "scsi_grown_defect_list",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'scsi_grown_defect_list': {
|
||||
'attribute_id': 'scsi_grown_defect_list',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_correction_algorithm_invocations": {
|
||||
"attribute_id": "write_correction_algorithm_invocations",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_correction_algorithm_invocations': {
|
||||
'attribute_id': 'write_correction_algorithm_invocations',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_errors_corrected_by_eccdelayed": {
|
||||
"attribute_id": "write_errors_corrected_by_eccdelayed",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_errors_corrected_by_eccdelayed': {
|
||||
'attribute_id': 'write_errors_corrected_by_eccdelayed',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_errors_corrected_by_eccfast": {
|
||||
"attribute_id": "write_errors_corrected_by_eccfast",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_errors_corrected_by_eccfast': {
|
||||
'attribute_id': 'write_errors_corrected_by_eccfast',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_errors_corrected_by_rereads_rewrites": {
|
||||
"attribute_id": "write_errors_corrected_by_rereads_rewrites",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_errors_corrected_by_rereads_rewrites': {
|
||||
'attribute_id': 'write_errors_corrected_by_rereads_rewrites',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_total_errors_corrected": {
|
||||
"attribute_id": "write_total_errors_corrected",
|
||||
"value": 0,
|
||||
"thresh": -1,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_total_errors_corrected': {
|
||||
'attribute_id': 'write_total_errors_corrected',
|
||||
'value': 0,
|
||||
'thresh': -1,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
},
|
||||
"write_total_uncorrected_errors": {
|
||||
"attribute_id": "write_total_uncorrected_errors",
|
||||
"value": 0,
|
||||
"thresh": 0,
|
||||
"transformed_value": 0,
|
||||
"status": 0
|
||||
'write_total_uncorrected_errors': {
|
||||
'attribute_id': 'write_total_uncorrected_errors',
|
||||
'value': 0,
|
||||
'thresh': 0,
|
||||
'transformed_value': 0,
|
||||
'status': 0
|
||||
}
|
||||
},
|
||||
"Status": 0
|
||||
'Status': 0
|
||||
}]
|
||||
},
|
||||
"metadata": {
|
||||
"read_correction_algorithm_invocations": {
|
||||
"display_name": "Read Correction Algorithm Invocations",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'metadata': {
|
||||
'read_correction_algorithm_invocations': {
|
||||
'display_name': 'Read Correction Algorithm Invocations',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"read_errors_corrected_by_eccdelayed": {
|
||||
"display_name": "Read Errors Corrected by ECC Delayed",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'read_errors_corrected_by_eccdelayed': {
|
||||
'display_name': 'Read Errors Corrected by ECC Delayed',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"read_errors_corrected_by_eccfast": {
|
||||
"display_name": "Read Errors Corrected by ECC Fast",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'read_errors_corrected_by_eccfast': {
|
||||
'display_name': 'Read Errors Corrected by ECC Fast',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"read_errors_corrected_by_rereads_rewrites": {
|
||||
"display_name": "Read Errors Corrected by ReReads/ReWrites",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'read_errors_corrected_by_rereads_rewrites': {
|
||||
'display_name': 'Read Errors Corrected by ReReads/ReWrites',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"read_total_errors_corrected": {
|
||||
"display_name": "Read Total Errors Corrected",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'read_total_errors_corrected': {
|
||||
'display_name': 'Read Total Errors Corrected',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"read_total_uncorrected_errors": {
|
||||
"display_name": "Read Total Uncorrected Errors",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'read_total_uncorrected_errors': {
|
||||
'display_name': 'Read Total Uncorrected Errors',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"scsi_grown_defect_list": {
|
||||
"display_name": "Grown Defect List",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'scsi_grown_defect_list': {
|
||||
'display_name': 'Grown Defect List',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_correction_algorithm_invocations": {
|
||||
"display_name": "Write Correction Algorithm Invocations",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_correction_algorithm_invocations': {
|
||||
'display_name': 'Write Correction Algorithm Invocations',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_errors_corrected_by_eccdelayed": {
|
||||
"display_name": "Write Errors Corrected by ECC Delayed",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_errors_corrected_by_eccdelayed': {
|
||||
'display_name': 'Write Errors Corrected by ECC Delayed',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_errors_corrected_by_eccfast": {
|
||||
"display_name": "Write Errors Corrected by ECC Fast",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_errors_corrected_by_eccfast': {
|
||||
'display_name': 'Write Errors Corrected by ECC Fast',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_errors_corrected_by_rereads_rewrites": {
|
||||
"display_name": "Write Errors Corrected by ReReads/ReWrites",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_errors_corrected_by_rereads_rewrites': {
|
||||
'display_name': 'Write Errors Corrected by ReReads/ReWrites',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_total_errors_corrected": {
|
||||
"display_name": "Write Total Errors Corrected",
|
||||
"ideal": "",
|
||||
"critical": false,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_total_errors_corrected': {
|
||||
'display_name': 'Write Total Errors Corrected',
|
||||
'ideal': '',
|
||||
'critical': false,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
},
|
||||
"write_total_uncorrected_errors": {
|
||||
"display_name": "Write Total Uncorrected Errors",
|
||||
"ideal": "low",
|
||||
"critical": true,
|
||||
"description": "",
|
||||
"display_type": ""
|
||||
'write_total_uncorrected_errors': {
|
||||
'display_name': 'Write Total Uncorrected Errors',
|
||||
'ideal': 'low',
|
||||
'critical': true,
|
||||
'description': '',
|
||||
'display_type': ''
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
'success': true
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
export const sdf = {
|
||||
"data": {
|
||||
"device": {
|
||||
"CreatedAt": "2021-06-24T21:17:31.305246-07:00",
|
||||
"UpdatedAt": "2021-06-24T21:17:31.305246-07:00",
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x50014ee20b2a72a9",
|
||||
"device_name": "sdf",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD60EFRX-68MYMN1",
|
||||
"interface_type": "SCSI",
|
||||
"interface_speed": "",
|
||||
"serial_number": "WD-WXL1HXXXXX",
|
||||
"firmware": "",
|
||||
"rotational_speed": 0,
|
||||
"capacity": 6001175126016,
|
||||
"form_factor": "",
|
||||
"smart_support": false,
|
||||
"device_protocol": "",
|
||||
"device_type": "",
|
||||
"label": "",
|
||||
"host_id": "",
|
||||
"device_status": 0
|
||||
'data': {
|
||||
'device': {
|
||||
'CreatedAt': '2021-06-24T21:17:31.305246-07:00',
|
||||
'UpdatedAt': '2021-06-24T21:17:31.305246-07:00',
|
||||
'DeletedAt': null,
|
||||
'wwn': '0x50014ee20b2a72a9',
|
||||
'device_name': 'sdf',
|
||||
'manufacturer': 'ATA',
|
||||
'model_name': 'WDC_WD60EFRX-68MYMN1',
|
||||
'interface_type': 'SCSI',
|
||||
'interface_speed': '',
|
||||
'serial_number': 'WD-WXL1HXXXXX',
|
||||
'firmware': '',
|
||||
'rotational_speed': 0,
|
||||
'capacity': 6001175126016,
|
||||
'form_factor': '',
|
||||
'smart_support': false,
|
||||
'device_protocol': '',
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
},
|
||||
"smart_results": []
|
||||
'smart_results': []
|
||||
},
|
||||
"metadata": null,
|
||||
"success": true
|
||||
'metadata': null,
|
||||
'success': true
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+10
@@ -0,0 +1,10 @@
|
||||
<h2 mat-dialog-title>Delete {{data.title}}?</h2>
|
||||
<mat-dialog-content>This will delete all data associated with this device (including all historical data).</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||
<button class="red-600" mat-button (click)="onDeleteClick()">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'delete_forever'"></mat-icon>
|
||||
Delete</button>
|
||||
</mat-dialog-actions>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardDeviceDeleteDialogComponent } from './dashboard-device-delete-dialog.component';
|
||||
|
||||
describe('DashboardDeviceDeleteDialogComponent', () => {
|
||||
let component: DashboardDeviceDeleteDialogComponent;
|
||||
let fixture: ComponentFixture<DashboardDeviceDeleteDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DashboardDeviceDeleteDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardDeviceDeleteDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
||||
import {DashboardDeviceDeleteDialogService} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service';
|
||||
import {Subject} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-device-delete-dialog',
|
||||
templateUrl: './dashboard-device-delete-dialog.component.html',
|
||||
styleUrls: ['./dashboard-device-delete-dialog.component.scss']
|
||||
})
|
||||
export class DashboardDeviceDeleteDialogComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DashboardDeviceDeleteDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string},
|
||||
private _deleteService: DashboardDeviceDeleteDialogService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
onDeleteClick(): void {
|
||||
this._deleteService.deleteDevice(this.data.wwn)
|
||||
.subscribe((data) => {
|
||||
this.dialogRef.close(data);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'
|
||||
import { MatButtonToggleModule} from '@angular/material/button-toggle';
|
||||
import {MatTabsModule} from '@angular/material/tabs';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing';
|
||||
import {MatDividerModule} from '@angular/material/divider';
|
||||
import {MatMenuModule} from '@angular/material/menu';
|
||||
import {MatProgressBarModule} from '@angular/material/progress-bar';
|
||||
import {MatSortModule} from '@angular/material/sort';
|
||||
import {MatTableModule} from '@angular/material/table';
|
||||
import {NgApexchartsModule} from 'ng-apexcharts';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardDeviceDeleteDialogComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild([]),
|
||||
RouterModule.forChild(dashboardRoutes),
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatProgressBarModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgApexchartsModule,
|
||||
SharedModule,
|
||||
MatDialogModule
|
||||
],
|
||||
exports : [
|
||||
DashboardDeviceDeleteDialogComponent,
|
||||
],
|
||||
providers : []
|
||||
})
|
||||
export class DashboardDeviceDeleteDialogModule
|
||||
{
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { getBasePath } from 'app/app.routing';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DashboardDeviceDeleteDialogService
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {HttpClient} _httpClient
|
||||
*/
|
||||
constructor(
|
||||
private _httpClient: HttpClient
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
deleteDevice(wwn: string): Observable<any>
|
||||
{
|
||||
return this._httpClient.delete( `${getBasePath()}/api/device/${wwn}`, {});
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<div [ngClass]="{ 'border-green': deviceSummary.device.device_status == 0 && deviceSummary.smart,
|
||||
'border-red': deviceSummary.device.device_status != 0 }"
|
||||
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
|
||||
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
|
||||
<mat-icon class="icon-size-96 opacity-12 text-green"
|
||||
*ngIf="deviceSummary.device.device_status == 0 && deviceSummary.smart"
|
||||
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-red"
|
||||
*ngIf="deviceSummary.device.device_status != 0"
|
||||
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-yellow"
|
||||
*ngIf="!deviceSummary.smart"
|
||||
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col">
|
||||
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
|
||||
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboardDisplay}}</a>
|
||||
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
|
||||
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="deviceSummary.device">
|
||||
<button mat-icon-button
|
||||
[matMenuTriggerFor]="previousStatementMenu">
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #previousStatementMenu="matMenu">
|
||||
<a mat-menu-item [routerLink]="'/device/'+ deviceSummary.device.wwn">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'assessment'"></mat-icon>
|
||||
<span>View Details</span>
|
||||
</span>
|
||||
</a>
|
||||
<a mat-menu-item (click)="openDeleteDialog()">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'delete_forever'"></mat-icon>
|
||||
<span>Delete Device</span>
|
||||
</span>
|
||||
</a>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap mt-4 -mx-6">
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusString(deviceSummary.device.device_status) | titlecase}}</div>
|
||||
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownTemp">{{ deviceSummary.smart?.temp | temperature:config.temperatureUnit:true }}</div>
|
||||
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize}}</div>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(deviceSummary.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
|
||||
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardDeviceComponent } from './dashboard-device.component';
|
||||
|
||||
describe('DashboardDeviceComponent', () => {
|
||||
let component: DashboardDeviceComponent;
|
||||
let fixture: ComponentFixture<DashboardDeviceComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DashboardDeviceComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardDeviceComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
|
||||
import * as moment from 'moment';
|
||||
import {takeUntil} from 'rxjs/operators';
|
||||
import {AppConfig} from 'app/core/config/app.config';
|
||||
import {TreoConfigService} from '@treo/services/config';
|
||||
import {Subject} from 'rxjs';
|
||||
import humanizeDuration from 'humanize-duration'
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component';
|
||||
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-device',
|
||||
templateUrl: './dashboard-device.component.html',
|
||||
styleUrls: ['./dashboard-device.component.scss']
|
||||
})
|
||||
export class DashboardDeviceComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private _configService: TreoConfigService,
|
||||
public dialog: MatDialog,
|
||||
) {
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
@Input() deviceSummary: any;
|
||||
@Input() deviceWWN: string;
|
||||
@Output() deviceDeleted = new EventEmitter<string>();
|
||||
|
||||
config: AppConfig;
|
||||
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
readonly humanizeDuration = humanizeDuration;
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to config changes
|
||||
this._configService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
this.config = config;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
classDeviceLastUpdatedOn(deviceSummary): string {
|
||||
if (deviceSummary.device.device_status !== 0) {
|
||||
return 'text-red' // if the device has failed, always highlight in red
|
||||
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
|
||||
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last 2 weeks.
|
||||
return 'text-green'
|
||||
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last month
|
||||
return 'text-yellow'
|
||||
} else{
|
||||
// last updated more than a month ago.
|
||||
return 'text-red'
|
||||
}
|
||||
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
deviceStatusString(deviceStatus): string {
|
||||
if(deviceStatus === 0){
|
||||
return 'passed'
|
||||
} else {
|
||||
return 'failed'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
openDeleteDialog(): void {
|
||||
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
|
||||
// width: '250px',
|
||||
data: {wwn: this.deviceWWN, title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboardDisplay)}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
console.log('The dialog was closed', result);
|
||||
if(result.success){
|
||||
this.deviceDeleted.emit(this.deviceWWN)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component'
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatButtonToggleModule} from '@angular/material/button-toggle';
|
||||
import {MatTabsModule} from '@angular/material/tabs';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
import {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing';
|
||||
import {MatDividerModule} from '@angular/material/divider';
|
||||
import {MatMenuModule} from '@angular/material/menu';
|
||||
import {MatProgressBarModule} from '@angular/material/progress-bar';
|
||||
import {MatSortModule} from '@angular/material/sort';
|
||||
import {MatTableModule} from '@angular/material/table';
|
||||
import {NgApexchartsModule} from 'ng-apexcharts';
|
||||
import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardDeviceComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild([]),
|
||||
RouterModule.forChild(dashboardRoutes),
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatProgressBarModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgApexchartsModule,
|
||||
SharedModule,
|
||||
DashboardDeviceDeleteDialogModule
|
||||
],
|
||||
exports : [
|
||||
DashboardDeviceComponent,
|
||||
],
|
||||
providers : []
|
||||
})
|
||||
export class DashboardDeviceModule
|
||||
{
|
||||
}
|
||||
+63
-30
@@ -1,77 +1,110 @@
|
||||
<h2 mat-dialog-title>Scrutiny Settings</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
|
||||
<form class="flex flex-col p-8 pb-0 overflow-hidden">
|
||||
<div class="flex flex-col gt-xs:flex-row">
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3">
|
||||
<mat-label>Sort By</mat-label>
|
||||
<mat-select [value]="'status'">
|
||||
<mat-option value="status">Status</mat-option>
|
||||
<mat-option value="name" disabled>Name</mat-option>
|
||||
<mat-option value="label" disabled>Label</mat-option>
|
||||
<div class="flex flex-col p-8 pb-0 overflow-hidden">
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||
<mat-label>Dark Mode</mat-label>
|
||||
<mat-select [(ngModel)]="theme">
|
||||
<mat-option value="system">System</mat-option>
|
||||
<mat-option value="dark">Dark</mat-option>
|
||||
<mat-option value="light">Light</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||
<mat-label>Display Title</mat-label>
|
||||
<mat-select [(ngModel)]="dashboardDisplay">
|
||||
<mat-option value="name">Name</mat-option>
|
||||
<mat-option value="serial_id">Serial ID</mat-option>
|
||||
<mat-option value="uuid">UUID</mat-option>
|
||||
<mat-option value="label">Label</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pl-3">
|
||||
<mat-label>Sort By</mat-label>
|
||||
<mat-select [(ngModel)]="dashboardSort">
|
||||
<mat-option value="status">Status</mat-option>
|
||||
<mat-option value="title">Title</mat-option>
|
||||
<mat-option value="age">Age</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||
<mat-label>Temperature Display Unit</mat-label>
|
||||
<mat-select [(ngModel)]="temperatureUnit">
|
||||
<mat-option value="celsius">Celsius</mat-option>
|
||||
<mat-option value="fahrenheit">Fahrenheit</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<mat-tab-group mat-align-tabs="start">
|
||||
<mat-tab label="Ata">
|
||||
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Critical Error Threshold</mat-label>
|
||||
<input matInput [value]="'10%'">
|
||||
<mat-label class="text-hint">Critical Error Threshold</mat-label>
|
||||
<input disabled matInput [value]="'10%'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Critical Warning Threshold</mat-label>
|
||||
<input matInput>
|
||||
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
|
||||
<input disabled matInput>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="flex flex-col gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Error Threshold</mat-label>
|
||||
<input matInput [value]="'20%'">
|
||||
<mat-label class="text-hint">Error Threshold</mat-label>
|
||||
<input disabled matInput [value]="'20%'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Warning Threshold</mat-label>
|
||||
<input matInput [value]="'10%'">
|
||||
<mat-label class="text-hint">Warning Threshold</mat-label>
|
||||
<input disabled matInput [value]="'10%'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
</mat-tab>
|
||||
<mat-tab label="NVMe">
|
||||
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Critical Error Threshold</mat-label>
|
||||
<input matInput [value]="'enabled'">
|
||||
<mat-label class="text-hint">Critical Error Threshold</mat-label>
|
||||
<input disabled matInput [value]="'enabled'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Critical Warning Threshold</mat-label>
|
||||
<input matInput>
|
||||
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
|
||||
<input disabled matInput>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
</mat-tab>
|
||||
<mat-tab label="SCSI">
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Critical Error Threshold</mat-label>
|
||||
<input matInput [value]="'enabled'">
|
||||
<mat-label class="text-hint">Critical Error Threshold</mat-label>
|
||||
<input disabled matInput [value]="'enabled'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Critical Warning Threshold</mat-label>
|
||||
<input matInput>
|
||||
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
|
||||
<input disabled matInput>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button matTooltip="not yet implemented" [mat-dialog-close]="true" cdkFocusInitial>Save</button>
|
||||
<button mat-button mat-dialog-close (click)="saveSettings()" cdkFocusInitial>Save</button>
|
||||
</mat-dialog-actions>
|
||||
|
||||
+46
-2
@@ -1,4 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import {AppConfig} from 'app/core/config/app.config';
|
||||
import { TreoConfigService } from '@treo/services/config';
|
||||
import {Subject} from 'rxjs';
|
||||
import {takeUntil} from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-settings',
|
||||
@@ -7,11 +11,51 @@ import { Component, OnInit } from '@angular/core';
|
||||
})
|
||||
export class DashboardSettingsComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
dashboardDisplay: string;
|
||||
dashboardSort: string;
|
||||
temperatureUnit: string;
|
||||
theme: string;
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
constructor(
|
||||
private _configService: TreoConfigService,
|
||||
) {
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to config changes
|
||||
this._configService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
|
||||
// Store the config
|
||||
this.dashboardDisplay = config.dashboardDisplay;
|
||||
this.dashboardSort = config.dashboardSort;
|
||||
this.temperatureUnit = config.temperatureUnit;
|
||||
this.theme = config.theme;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
formatLabel(value: number) {
|
||||
|
||||
saveSettings(): void {
|
||||
|
||||
|
||||
const newSettings = {
|
||||
dashboardDisplay: this.dashboardDisplay,
|
||||
dashboardSort: this.dashboardSort,
|
||||
temperatureUnit: this.temperatureUnit,
|
||||
theme: this.theme
|
||||
}
|
||||
this._configService.config = newSettings
|
||||
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
|
||||
}
|
||||
|
||||
formatLabel(value: number): number {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -9,12 +9,12 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import {DashboardSettingsComponent} from 'app/layout/common/dashboard-settings/dashboard-settings.component'
|
||||
import { MatDialogModule } from "@angular/material/dialog";
|
||||
import { MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatSliderModule} from "@angular/material/slider";
|
||||
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatButtonToggleModule} from '@angular/material/button-toggle';
|
||||
import {MatTabsModule} from '@angular/material/tabs';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
||||
@@ -9,12 +9,12 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component'
|
||||
import { MatDialogModule } from "@angular/material/dialog";
|
||||
import { MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatSliderModule} from "@angular/material/slider";
|
||||
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatButtonToggleModule} from '@angular/material/button-toggle';
|
||||
import {MatTabsModule} from '@angular/material/tabs';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
||||
@@ -23,6 +23,7 @@ export class LayoutComponent implements OnInit, OnDestroy
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
private systemPrefersDark: boolean;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -43,6 +44,9 @@ export class LayoutComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
this.systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@@ -66,7 +70,7 @@ export class LayoutComponent implements OnInit, OnDestroy
|
||||
this.theme = config.theme;
|
||||
|
||||
// Update the selected theme class name on body
|
||||
const themeName = 'treo-theme-' + config.theme;
|
||||
const themeName = 'treo-theme-' + this.determineTheme(config);
|
||||
this._document.body.classList.forEach((className) => {
|
||||
if ( className.startsWith('treo-theme-') && className !== themeName )
|
||||
{
|
||||
@@ -105,6 +109,17 @@ export class LayoutComponent implements OnInit, OnDestroy
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks if theme should be set to dark based on config & system settings
|
||||
*/
|
||||
private determineTheme(config:AppConfig): string {
|
||||
if (config.theme === 'system') {
|
||||
return this.systemPrefersDark ? 'dark' : 'light'
|
||||
} else {
|
||||
return config.theme
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the selected layout
|
||||
*/
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<!-- Spacer -->
|
||||
<div class="spacer"></div>
|
||||
|
||||
<code>{{appVersion}}</code>
|
||||
|
||||
<!-- Shortcuts -->
|
||||
<!-- <shortcuts [shortcuts]="data.shortcuts"></shortcuts>-->
|
||||
@@ -48,6 +48,7 @@
|
||||
<!-- <notifications [notifications]="data.notifications"></notifications>-->
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { TreoMediaWatcherService } from '@treo/services/media-watcher';
|
||||
import { TreoNavigationService } from '@treo/components/navigation';
|
||||
import {versionInfo} from 'environments/versions';
|
||||
|
||||
@Component({
|
||||
selector : 'material-layout',
|
||||
@@ -13,6 +14,7 @@ import { TreoNavigationService } from '@treo/components/navigation';
|
||||
})
|
||||
export class MaterialLayoutComponent implements OnInit, OnDestroy
|
||||
{
|
||||
appVersion: string;
|
||||
data: any;
|
||||
isScreenSmall: boolean;
|
||||
|
||||
@@ -46,6 +48,8 @@ export class MaterialLayoutComponent implements OnInit, OnDestroy
|
||||
// Set the defaults
|
||||
this.fixedHeader = false;
|
||||
this.fixedFooter = false;
|
||||
|
||||
this.appVersion = versionInfo.version
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -47,71 +47,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap w-full">
|
||||
<div *ngFor="let summary of data.data.summary | keyvalue" class="flex gt-sm:w-1/2 min-w-80 p-4">
|
||||
<div [ngClass]="{ 'border-green': summary.value.device.device_status == 0 && summary.value.smart,
|
||||
'border-red': summary.value.device.device_status != 0 }"
|
||||
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
|
||||
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
|
||||
<mat-icon class="icon-size-96 opacity-12 text-green"
|
||||
*ngIf="summary.value.device.device_status == 0 && summary.value.smart"
|
||||
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-red"
|
||||
*ngIf="summary.value.device.device_status != 0"
|
||||
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-yellow"
|
||||
*ngIf="!summary.value.smart"
|
||||
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col">
|
||||
<a [routerLink]="'/device/'+ summary.value.device.wwn"
|
||||
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceTitle(summary.value.device)}}</a>
|
||||
<div [ngClass]="classDeviceLastUpdatedOn(summary.value)" class="font-medium text-sm" *ngIf="summary.value.smart">
|
||||
Last Updated on {{summary.value.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="summary.value.device">
|
||||
<button mat-icon-button
|
||||
[matMenuTriggerFor]="previousStatementMenu">
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #previousStatementMenu="matMenu">
|
||||
<a mat-menu-item [routerLink]="'/device/'+ summary.value.device.wwn">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'payment'"></mat-icon>
|
||||
<span>View Details</span>
|
||||
</span>
|
||||
</a>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap mt-4 -mx-6">
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownStatus">{{ deviceStatusString(summary.value.device.device_status) | titlecase}}</div>
|
||||
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownTemp">{{ summary.value.smart?.temp }}°C</div>
|
||||
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none">{{ summary.value.device.capacity | fileSize}}</div>
|
||||
</div>
|
||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(summary.value.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
|
||||
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
|
||||
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
|
||||
<div class="flex flex-wrap w-full">
|
||||
<app-dashboard-device (deviceDeleted)="onDeviceDeleted($event)" class="flex gt-sm:w-1/2 min-w-80 p-4" *ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboardSort:config.dashboardDisplay )" [deviceWWN]="deviceSummary.device.wwn" [deviceSummary]="deviceSummary"></app-dashboard-device>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Drive Temperatures -->
|
||||
<div class="flex flex-auto w-full min-w-80 h-90 p-4">
|
||||
<div class="flex flex-col flex-auto bg-card shadow-md rounded overflow-hidden">
|
||||
@@ -123,22 +67,22 @@
|
||||
</div>
|
||||
<div>
|
||||
<button class="h-8 min-h-8 px-2"
|
||||
matTooltip="not yet implemented"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="tempRangeMenu">
|
||||
<span class="font-medium text-sm text-hint">1 week</span>
|
||||
<span class="font-medium text-sm text-hint">{{tempDurationKey}}</span>
|
||||
</button>
|
||||
<mat-menu #tempRangeMenu="matMenu">
|
||||
<button mat-menu-item>1 month</button>
|
||||
<button mat-menu-item>12 months</button>
|
||||
<button mat-menu-item>all time</button>
|
||||
<button (click)="changeSummaryTempDuration('forever')" mat-menu-item>forever</button>
|
||||
<button (click)="changeSummaryTempDuration('year')" mat-menu-item>year</button>
|
||||
<button (click)="changeSummaryTempDuration('month')" mat-menu-item>month</button>
|
||||
<button (click)="changeSummaryTempDuration('week')" mat-menu-item>week</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col flex-auto">
|
||||
<apx-chart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
|
||||
<apx-chart #tempChart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
|
||||
[chart]="temperatureOptions.chart"
|
||||
[colors]="temperatureOptions.colors"
|
||||
[fill]="temperatureOptions.fill"
|
||||
@@ -167,6 +111,4 @@
|
||||
<code>scrutiny-collector-metrics run</code>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</ng-template>
|
||||
|
||||
@@ -3,12 +3,15 @@ import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { ApexOptions } from 'ng-apexcharts';
|
||||
import {ApexOptions, ChartComponent} from 'ng-apexcharts';
|
||||
import { DashboardService } from 'app/modules/dashboard/dashboard.service';
|
||||
import * as moment from "moment";
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component';
|
||||
import humanizeDuration from 'humanize-duration'
|
||||
import {AppConfig} from 'app/core/config/app.config';
|
||||
import {TreoConfigService} from '@treo/services/config';
|
||||
import {Router} from '@angular/router';
|
||||
import {TemperaturePipe} from 'app/shared/temperature.pipe';
|
||||
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
|
||||
|
||||
@Component({
|
||||
selector : 'example',
|
||||
@@ -20,10 +23,14 @@ import humanizeDuration from 'humanize-duration'
|
||||
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
{
|
||||
data: any;
|
||||
hostGroups: { [hostId: string]: string[] } = {}
|
||||
temperatureOptions: ApexOptions;
|
||||
tempDurationKey = 'forever'
|
||||
config: AppConfig;
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
@ViewChild('tempChart', { static: false }) tempChart: ChartComponent;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -32,7 +39,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
*/
|
||||
constructor(
|
||||
private _smartService: DashboardService,
|
||||
public dialog: MatDialog
|
||||
private _configService: TreoConfigService,
|
||||
public dialog: MatDialog,
|
||||
private router: Router,
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
@@ -49,6 +58,28 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
|
||||
// Subscribe to config changes
|
||||
this._configService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
|
||||
// check if the old config and the new config do not match.
|
||||
const oldConfig = JSON.stringify(this.config)
|
||||
const newConfig = JSON.stringify(config)
|
||||
|
||||
if(oldConfig !== newConfig){
|
||||
console.log(`Configuration updated: ${newConfig} vs ${oldConfig}`)
|
||||
// Store the config
|
||||
this.config = config;
|
||||
|
||||
if(oldConfig){
|
||||
console.log('reloading component...')
|
||||
this.refreshComponent()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get the data
|
||||
this._smartService.data$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
@@ -57,6 +88,15 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
// Store the data
|
||||
this.data = data;
|
||||
|
||||
// generate group data.
|
||||
for(const wwn in this.data.data.summary){
|
||||
const hostid = this.data.data.summary[wwn].device.host_id
|
||||
const hostDeviceList = this.hostGroups[hostid] || []
|
||||
hostDeviceList.push(wwn)
|
||||
this.hostGroups[hostid] = hostDeviceList
|
||||
}
|
||||
console.log(this.hostGroups)
|
||||
|
||||
// Prepare the chart data
|
||||
this._prepareChartData();
|
||||
});
|
||||
@@ -81,26 +121,37 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
private _deviceDataTemperatureSeries() {
|
||||
var deviceTemperatureSeries = []
|
||||
private refreshComponent(): void {
|
||||
|
||||
console.log("DEVICE DATA SUMMARY", this.data)
|
||||
const currentUrl = this.router.url;
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.router.onSameUrlNavigation = 'reload';
|
||||
this.router.navigate([currentUrl]);
|
||||
}
|
||||
|
||||
private _deviceDataTemperatureSeries(): any[] {
|
||||
const deviceTemperatureSeries = []
|
||||
|
||||
console.log('DEVICE DATA SUMMARY', this.data)
|
||||
|
||||
for(const wwn in this.data.data.summary){
|
||||
var deviceSummary = this.data.data.summary[wwn]
|
||||
const deviceSummary = this.data.data.summary[wwn]
|
||||
if (!deviceSummary.temp_history){
|
||||
continue
|
||||
}
|
||||
var deviceSeriesMetadata = {
|
||||
name: `/dev/${deviceSummary.device.device_name}`,
|
||||
|
||||
const deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboardDisplay)
|
||||
|
||||
const deviceSeriesMetadata = {
|
||||
name: deviceName,
|
||||
data: []
|
||||
}
|
||||
|
||||
for(let tempHistory of deviceSummary.temp_history){
|
||||
let newDate = new Date(tempHistory.date);
|
||||
for(const tempHistory of deviceSummary.temp_history){
|
||||
const newDate = new Date(tempHistory.date);
|
||||
deviceSeriesMetadata.data.push({
|
||||
x: newDate,
|
||||
y: tempHistory.temp
|
||||
y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperatureUnit, false)
|
||||
})
|
||||
}
|
||||
deviceTemperatureSeries.push(deviceSeriesMetadata)
|
||||
@@ -149,8 +200,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
format: 'MMM dd, yyyy HH:mm:ss'
|
||||
},
|
||||
y : {
|
||||
|
||||
formatter: (value) => {
|
||||
return value + '°C';
|
||||
return TemperaturePipe.formatTemperature(value, this.config.temperatureUnit, true) as string;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -164,7 +216,17 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
openDialog() {
|
||||
deviceSummariesForHostGroup(hostGroupWWNs: string[]): any[] {
|
||||
const deviceSummaries = []
|
||||
for(const wwn of hostGroupWWNs){
|
||||
if(this.data.data.summary[wwn]){
|
||||
deviceSummaries.push(this.data.data.summary[wwn])
|
||||
}
|
||||
}
|
||||
return deviceSummaries
|
||||
}
|
||||
|
||||
openDialog(): void {
|
||||
const dialogRef = this.dialog.open(DashboardSettingsComponent);
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
@@ -172,48 +234,33 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
});
|
||||
}
|
||||
|
||||
deviceTitle(disk){
|
||||
let title = []
|
||||
|
||||
if (disk.host_id) title.push(disk.host_id)
|
||||
|
||||
title.push(`/dev/${disk.device_name}`)
|
||||
|
||||
if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){
|
||||
title.push(disk.device_type)
|
||||
}
|
||||
|
||||
title.push(disk.model_name)
|
||||
|
||||
return title.join(' - ')
|
||||
onDeviceDeleted(wwn: string): void {
|
||||
delete this.data.data.summary[wwn] // remove the device from the summary list.
|
||||
}
|
||||
|
||||
deviceStatusString(deviceStatus){
|
||||
if(deviceStatus == 0){
|
||||
return "passed"
|
||||
} else {
|
||||
return "failed"
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
classDeviceLastUpdatedOn(deviceSummary){
|
||||
if (deviceSummary.device.device_status !== 0) {
|
||||
return 'text-red' // if the device has failed, always highlight in red
|
||||
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
|
||||
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last 2 weeks.
|
||||
return 'text-green'
|
||||
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last month
|
||||
return 'text-yellow'
|
||||
} else{
|
||||
// last updated more than a month ago.
|
||||
return 'text-red'
|
||||
}
|
||||
DURATION_KEY_WEEK = "week"
|
||||
DURATION_KEY_MONTH = "month"
|
||||
DURATION_KEY_YEAR = "year"
|
||||
DURATION_KEY_FOREVER = "forever"
|
||||
*/
|
||||
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
changeSummaryTempDuration(durationKey: string){
|
||||
this.tempDurationKey = durationKey
|
||||
|
||||
this._smartService.getSummaryTempData(durationKey)
|
||||
.subscribe((data) => {
|
||||
|
||||
// given a list of device temp history, override the data in the "summary" object.
|
||||
for(const wwn in this.data.data.summary) {
|
||||
// console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`)
|
||||
this.data.data.summary[wwn].temp_history = data.data.temp_history[wwn] || []
|
||||
}
|
||||
|
||||
// Prepare the chart series data
|
||||
this.tempChart.updateSeries(this._deviceDataTemperatureSeries())
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,6 +274,4 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
return item.id || index;
|
||||
}
|
||||
|
||||
readonly humanizeDuration = humanizeDuration;
|
||||
|
||||
}
|
||||
|
||||
@@ -12,13 +12,14 @@ import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { NgApexchartsModule } from 'ng-apexcharts';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip'
|
||||
import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/dashboard-settings.module";
|
||||
import { DashboardSettingsModule } from 'app/layout/common/dashboard-settings/dashboard-settings.module';
|
||||
import { DashboardDeviceModule } from 'app/layout/common/dashboard-device/dashboard-device.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardComponent
|
||||
],
|
||||
imports : [
|
||||
imports: [
|
||||
RouterModule.forChild(dashboardRoutes),
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
@@ -30,7 +31,8 @@ import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/da
|
||||
MatTableModule,
|
||||
NgApexchartsModule,
|
||||
SharedModule,
|
||||
DashboardSettingsModule
|
||||
DashboardSettingsModule,
|
||||
DashboardDeviceModule
|
||||
]
|
||||
})
|
||||
export class DashboardModule
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user