Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a014337167 | |||
| 3a5ee0a762 | |||
| 625a0244e2 | |||
| a269ba57df | |||
| 6a76b5aa26 | |||
| 939d40eb20 | |||
| ad738508e5 | |||
| 971249ba3f | |||
| 73417ca653 | |||
| a6d092983d | |||
| 1988b101e1 | |||
| 746ae76cfc | |||
| 3380023ad0 | |||
| 6362512406 | |||
| c6323fb7ce | |||
| 349c7d4def | |||
| 19ac712b78 | |||
| c95b272485 | |||
| 43231d7ec3 | |||
| 3f6537e94c | |||
| b021797919 | |||
| 7ad997cc0e | |||
| a3000fd6b0 | |||
| af59f2639c | |||
| b0ff0b3a48 | |||
| 56056b2d6a | |||
| 51f0ba6ee2 | |||
| 34b0347acd | |||
| 0565962a14 | |||
| 184bc4bec5 | |||
| bdbe13e320 | |||
| 761014a93f | |||
| 27be0b8327 | |||
| 69abe43a1d | |||
| 7c35d59552 | |||
| 742153e5dc | |||
| 5f7e4a3808 | |||
| bb98b8c45b | |||
| b71897fa5f | |||
| a182c691fb | |||
| 4066c84c8e | |||
| 4a72c9ef55 | |||
| 3e11583283 | |||
| ea9799d963 | |||
| e46ab7373e | |||
| 87f923e1f2 | |||
| 2244504023 | |||
| 192ae40f74 | |||
| 600cd153e0 | |||
| d11bf0a2fc | |||
| 50561f34ea | |||
| a58f9445c1 | |||
| 1ec478302f | |||
| 412f956782 | |||
| 9b28ac5069 | |||
| db2869ffc6 | |||
| 6e349244d1 | |||
| e6cd3ee3c6 | |||
| df6a4cef59 | |||
| 8cf7d64da7 | |||
| 3de12cd739 | |||
| affe05e145 | |||
| 9ad96e6d37 | |||
| 85d98316f3 | |||
| 0641b5e79d | |||
| c168e1e9fc | |||
| 56a9454730 | |||
| a783604c4e | |||
| 604dcf355c | |||
| 57dc547265 | |||
| e0fe17afbf | |||
| c9429c61b2 | |||
| 394ac0af2c | |||
| 48feee51d0 | |||
| d4fb7786d2 | |||
| c316f996c6 | |||
| 49108bd1ef | |||
| 0dafb65c5f | |||
| c5943a1ca4 | |||
| a5893f0bf9 | |||
| 142fe06df1 | |||
| 8b7ddd3042 | |||
| db57281557 | |||
| 5a5877b729 | |||
| 0a89c2bab3 | |||
| a18e2842ac | |||
| 806f7c64a0 | |||
| 8fa32c6dd7 | |||
| 5e6ab2290b | |||
| 67c0af9f59 | |||
| 55565e509d | |||
| f74d9c108a | |||
| 5977f7c7d4 | |||
| 3490a2ffc2 | |||
| a0f9e6e3f3 | |||
| 6a9b89b38a | |||
| 543f376015 | |||
| ca7bd2c151 | |||
| 1e74e96658 |
@@ -0,0 +1,154 @@
|
||||
labels: ["needs-confirmation"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
> [!IMPORTANT]
|
||||
> Please read through [the Discussion rules](https://github.com/AnalogJ/scrutiny/discussions/876), review [the docs](https://github.com/AnalogJ/scrutiny/tree/master/docs), and check for both existing [Discussions](https://github.com/AnalogJ/scrutiny/discussions?discussions_q=) and [Issues](https://github.com/AnalogJ/scrutiny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "# Issue Details"
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Issue Description
|
||||
description: |
|
||||
Provide a detailed description of the issue. Include relevant information, such as:
|
||||
- The feature or configuration option you encounter the issue with.
|
||||
- Screenshots, screen recordings, or other supporting media (as needed).
|
||||
- If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description.
|
||||
placeholder: |
|
||||
Temperature data is missing from the plots.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: |
|
||||
Describe how you expect scrutiny to behave in this situation. Include any relevant documentation links.
|
||||
placeholder: |
|
||||
All temperature data uploaded by collectors should make it into the plots.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: |
|
||||
Describe how scrutiny actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically.
|
||||
placeholder: |
|
||||
Only half the points appear.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: |
|
||||
Provide a detailed set of step-by-step instructions for reproducing this issue. If you can't, describe what you were doing when the issue occurred.
|
||||
placeholder: |
|
||||
1. Set up the omnibus docker image
|
||||
2. Launch the web dashboard
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: scrutiny logs
|
||||
description: |
|
||||
Provide any captured scrutiny logs or panic dumps during your issue reproduction in this field.
|
||||
render: text
|
||||
- type: input
|
||||
attributes:
|
||||
label: Scrutiny Version
|
||||
description: The version of scrutiny you are using
|
||||
placeholder: v0.8.2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Smartmontools Version
|
||||
description: The version of smartmontools you are using (or "docker", if you're using the docker image)
|
||||
placeholder: "7.2"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: OS Version Information
|
||||
description: |
|
||||
Please tell us what operating system (name and version) you are using.
|
||||
placeholder: Ubuntu 24.04.1 (Noble Numbat)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Component
|
||||
description: Which component of scrutiny has a problem?
|
||||
options:
|
||||
- web
|
||||
- collector
|
||||
- omnibus (docker only)
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Deployment Method
|
||||
description: How are you running scrutiny?
|
||||
options:
|
||||
- label: docker
|
||||
- label: binaries
|
||||
- label: systemd
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Hard Drive Information
|
||||
description: |
|
||||
If the problem is related to a specific hard drive, what are the make and model?
|
||||
placeholder: Seagate ST8000DM004-2CX188
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: smartctl output
|
||||
description: |
|
||||
What is the output of smartctl --xall --json <drive>?
|
||||
render: json
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: scrutiny.yaml
|
||||
description: |
|
||||
Please provide your full scrutiny.yaml file.
|
||||
render: yaml
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: collector.yaml
|
||||
description: |
|
||||
Please provide your full collector.yaml file.
|
||||
render: yaml
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional relevant configuration
|
||||
description: |
|
||||
Please any additional relevant configuration (e.g. systemd service definitions, OS configuration)
|
||||
render: text
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# User Acknowledgements
|
||||
> [!TIP]
|
||||
> Use these links to review the existing scrutiny [Discussions](https://github.com/AnalogJ/scrutiny/discussions?discussions_q=) and [Issues](https://github.com/AnalogJ/scrutiny/issues?q=sort%3Areactions-desc).
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: "I acknowledge that:"
|
||||
options:
|
||||
- label: I have reviewed the FAQ and confirm that my issue is NOT among them.
|
||||
required: true
|
||||
- label: I have searched the scrutiny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
|
||||
required: true
|
||||
- label: I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines.
|
||||
required: true
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Log Files**
|
||||
If related to missing devices or SMART data, please run the `collector` in DEBUG mode, and attach the log file.
|
||||
See [/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md) for other troubleshooting tips.
|
||||
|
||||
```
|
||||
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=/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
|
||||
```
|
||||
|
||||
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`
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Features, Bug Reports, Questions
|
||||
url: https://github.com/AnalogJ/scrutiny/discussions/new/choose
|
||||
about: Our preferred starting point if you have any questions or suggestions about configuration, features or behavior.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEAT]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: Pre-Discussed and Approved Topics
|
||||
about: |-
|
||||
Only for topics already discussed and approved in the GitHub Discussions section.
|
||||
---
|
||||
|
||||
**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
|
||||
|
||||
**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**
|
||||
+32
-15
@@ -1,6 +1,13 @@
|
||||
name: CI
|
||||
# This workflow is triggered on pushes & pull requests
|
||||
on: [pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-frontend:
|
||||
@@ -13,7 +20,7 @@ jobs:
|
||||
run: |
|
||||
make binary-frontend-test-coverage
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
path: ${{ github.workspace }}/webapp/frontend/coverage/lcov.info
|
||||
@@ -21,7 +28,6 @@ jobs:
|
||||
test-backend:
|
||||
name: Test Backend
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/packagrio/packagr:latest-golang
|
||||
# Service containers to run with `build` (Required for end-to-end testing)
|
||||
services:
|
||||
influxdb:
|
||||
@@ -38,18 +44,15 @@ jobs:
|
||||
env:
|
||||
STATIC: true
|
||||
steps:
|
||||
- name: Git
|
||||
run: |
|
||||
apt-get update && apt-get install -y software-properties-common
|
||||
add-apt-repository ppa:git-core/ppa && apt-get update && apt-get install -y git
|
||||
git --version
|
||||
- name: Add influxdb to hosts
|
||||
run: echo "127.0.0.1 influxdb" | sudo tee -a /etc/hosts
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
- name: Test Backend
|
||||
run: |
|
||||
make binary-clean binary-test-coverage
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
path: ${{ github.workspace }}/coverage.txt
|
||||
@@ -64,17 +67,31 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Download coverage reports
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
- name: Upload coverage reports
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ${{ github.workspace }}/coverage.txt,${{ github.workspace }}/lcov.info
|
||||
flags: unittests
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
args: --issues-exit-code=0
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}
|
||||
runs-on: ${{ matrix.cfg.on }}
|
||||
@@ -101,14 +118,14 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.20.1'
|
||||
go-version: '^1.25'
|
||||
- name: Build Binaries
|
||||
run: |
|
||||
make binary-clean binary-all
|
||||
- name: Archive
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binaries.zip
|
||||
name: binaries-${{ matrix.cfg.on }}-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}-${{ matrix.cfg.goarm || 'na' }}.zip
|
||||
path: |
|
||||
scrutiny-web-*
|
||||
scrutiny-collector-metrics-*
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name: Docker
|
||||
on:
|
||||
push:
|
||||
branches: [ master, beta ]
|
||||
# Publish semver tags as releases.
|
||||
tags: [ 'v*.*.*' ]
|
||||
|
||||
@@ -18,20 +17,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# 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
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -40,28 +36,30 @@ jobs:
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
latest=false
|
||||
latest=true
|
||||
suffix=-collector,onlatest=true
|
||||
tags: |
|
||||
type=ref,enable=true,event=branch,suffix=-collector
|
||||
type=ref,enable=true,event=tag,suffix=-collector
|
||||
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# Build and push Docker image with Buildx
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
file: docker/Dockerfile.collector
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: true
|
||||
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
|
||||
@@ -71,20 +69,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# 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
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -93,27 +90,31 @@ jobs:
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
latest=false
|
||||
latest=true
|
||||
suffix=-web,onlatest=true
|
||||
tags: |
|
||||
type=ref,enable=true,event=branch,suffix=-web
|
||||
type=ref,enable=true,event=tag,suffix=-web
|
||||
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
|
||||
# Build and push Docker image with Buildx
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
file: docker/Dockerfile.web
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: true
|
||||
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:
|
||||
@@ -122,20 +123,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# 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
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -144,22 +144,29 @@ jobs:
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
# tag latest and latest-omnibus
|
||||
with:
|
||||
flavor: |
|
||||
latest=true
|
||||
suffix=-omnibus,onlatest=false
|
||||
tags: |
|
||||
type=ref,enable=true,event=branch,suffix=-omnibus
|
||||
type=ref,enable=true,event=tag,suffix=-omnibus
|
||||
type=raw,value=latest
|
||||
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
|
||||
# Build and push Docker image with Buildx
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: true
|
||||
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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
name: Docker - Nightly
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# Note: this only runs on the default branch
|
||||
schedule:
|
||||
- cron: '36 12 * * *'
|
||||
|
||||
@@ -8,7 +10,7 @@ env:
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
omnibus:
|
||||
build_nightlies:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -16,44 +18,86 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
- name: "Populate frontend version information"
|
||||
run: "cd webapp/frontend && ./git.version.sh"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
platforms: 'arm64'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# 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
|
||||
uses: docker/login-action@v3
|
||||
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
|
||||
- name: Extract Docker metadata for omnibus
|
||||
id: meta_omnibus
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
tags: |
|
||||
type=ref,enable=true,event=branch,suffix=-omnibus-nightly
|
||||
type=ref,enable=true,event=tag,suffix=-omnibus-nightly
|
||||
type=raw,enable=true,value=nightly,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
|
||||
uses: docker/build-push-action@v3
|
||||
- name: Build and push omnibus Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
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
|
||||
push: true
|
||||
tags: ${{ steps.meta_omnibus.outputs.tags }}
|
||||
labels: ${{ steps.meta_omnibus.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata for collector
|
||||
id: meta_collector
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
tags: |
|
||||
type=raw,enable=true,value=nightly,suffix=-collector
|
||||
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 collector Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
file: docker/Dockerfile.collector
|
||||
push: true
|
||||
tags: ${{ steps.meta_collector.outputs.tags }}
|
||||
labels: ${{ steps.meta_collector.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata for web
|
||||
id: meta_web
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
tags: |
|
||||
type=raw,enable=true,value=nightly,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 web Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
file: docker/Dockerfile.web
|
||||
push: true
|
||||
tags: ${{ steps.meta_web.outputs.tags }}
|
||||
labels: ${{ steps.meta_web.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# compiles angular frontend and attaches it to the latest release.
|
||||
name: Release Frontend
|
||||
|
||||
on:
|
||||
release:
|
||||
# Only use the types keyword to narrow down the activity types that will trigger your workflow.
|
||||
types: [published]
|
||||
jobs:
|
||||
release-frontend:
|
||||
name: Release Frontend
|
||||
runs-on: ubuntu-latest
|
||||
container: node:lts-slim
|
||||
steps:
|
||||
- name: Checkout
|
||||
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: |
|
||||
apt-get update && apt-get install -y make
|
||||
make binary-frontend
|
||||
tar -czf scrutiny-web-frontend.tar.gz dist
|
||||
- name: Upload Frontend Asset
|
||||
id: upload-release-asset3
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
asset_path: './scrutiny-web-frontend.tar.gz'
|
||||
asset_name: scrutiny-web-frontend.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
add-apt-repository ppa:git-core/ppa && apt-get update && apt-get install -y git
|
||||
git --version
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Bump version
|
||||
@@ -61,9 +61,10 @@ jobs:
|
||||
with:
|
||||
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
|
||||
- name: Upload workspace
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: workspace
|
||||
include-hidden-files: true
|
||||
path: ${{ github.workspace }}/**/*
|
||||
retention-days: 1
|
||||
|
||||
@@ -91,36 +92,66 @@ jobs:
|
||||
- { on: windows-latest, goos: windows, goarch: arm64 }
|
||||
steps:
|
||||
- name: Download workspace
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: workspace
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.20.1' # The Go version to download (if necessary) and use.
|
||||
go-version: '1.25' # The Go version to download (if necessary) and use.
|
||||
- name: Build Binaries
|
||||
run: |
|
||||
make binary-clean binary-all
|
||||
- name: Archive
|
||||
uses: actions/upload-artifact@v2
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: binaries.zip
|
||||
name: scrutiny-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}.zip
|
||||
path: |
|
||||
scrutiny-web-*
|
||||
scrutiny-collector-metrics-*
|
||||
scrutiny-web-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}${{ case(matrix.cfg.goos == 'windows', '.exe', '') }}
|
||||
scrutiny-collector-metrics-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}${{ case(matrix.cfg.goos == 'windows', '.exe', '') }}
|
||||
|
||||
build_frontend:
|
||||
name: Build Frontend
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
container: node:lts-slim
|
||||
steps:
|
||||
- name: Download workspace
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: workspace
|
||||
- name: "Generate frontend version information"
|
||||
run: "cd webapp/frontend && chmod +x git.version.sh && ./git.version.sh"
|
||||
- name: Build Frontend
|
||||
run: |
|
||||
apt-get update && apt-get install -y make
|
||||
make binary-frontend
|
||||
tar -czf scrutiny-web-frontend.tar.gz dist
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: scrutiny-web-frontend.zip
|
||||
path: scrutiny-web-frontend.tar.gz
|
||||
|
||||
release-publish:
|
||||
name: Publish Release
|
||||
needs: build
|
||||
needs:
|
||||
- build
|
||||
- build_frontend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download workspace
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: workspace
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: binaries.zip
|
||||
merge-multiple: true
|
||||
pattern: scrutiny-*.zip
|
||||
- name: Download frontend
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: scrutiny-web-frontend.zip
|
||||
- name: List
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -144,6 +175,7 @@ jobs:
|
||||
scrutiny-collector-metrics-linux-arm64
|
||||
scrutiny-collector-metrics-windows-amd64.exe
|
||||
scrutiny-collector-metrics-windows-arm64.exe
|
||||
scrutiny-web-frontend.tar.gz
|
||||
scrutiny-web-darwin-amd64
|
||||
scrutiny-web-darwin-arm64
|
||||
scrutiny-web-freebsd-amd64
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
version: "2"
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
settings:
|
||||
errcheck:
|
||||
check-blank: true
|
||||
@@ -0,0 +1,73 @@
|
||||
# AI Usage Policy
|
||||
|
||||
scrutiny has strict rules for AI usage:
|
||||
|
||||
- **All AI usage in any form must be disclosed.** You must state
|
||||
the tool you used (e.g. Claude Code, Cursor, Amp) along with
|
||||
the extent that the work was AI-assisted.
|
||||
|
||||
- **Pull requests created in any way by AI can only be for accepted issues.**
|
||||
Drive-by pull requests that do not reference an accepted issue will be
|
||||
closed. If AI isn't disclosed but a maintainer suspects its use, the
|
||||
PR will be closed. If you want to share code for a non-accepted issue,
|
||||
open a discussion or attach it to an existing discussion.
|
||||
|
||||
- **Pull requests created by AI must have been fully verified with
|
||||
human use.** AI must not create hypothetically correct code that
|
||||
hasn't been tested. Importantly, you must not allow AI to write
|
||||
code for platforms or environments you don't have access to manually
|
||||
test on.
|
||||
|
||||
- **Issues and discussions can use AI assistance but must have a full
|
||||
human-in-the-loop.** This means that any content generated with AI
|
||||
must have been reviewed _and edited_ by a human before submission.
|
||||
AI is very good at being overly verbose and including noise that
|
||||
distracts from the main point. Humans must do their research and
|
||||
trim this down.
|
||||
|
||||
- **No AI-generated media is allowed (art, images, videos, audio, etc.).**
|
||||
Text and code are the only acceptable AI-generated content, per the
|
||||
other rules in this policy.
|
||||
|
||||
- **Bad AI drivers will be banned and ridiculed in public.** You've
|
||||
been warned. We love to help junior developers learn and grow, but
|
||||
if you're interested in that then don't use AI, and we'll help you.
|
||||
I'm sorry that bad AI drivers have ruined this for you.
|
||||
|
||||
These rules apply only to outside contributions to scrutiny. Maintainers
|
||||
and repeat contributors (with explicit permission) are exempt from these
|
||||
rules and may use AI tools at their discretion; they've proven themselves
|
||||
trustworthy to apply good judgment.
|
||||
|
||||
## There are Humans Here
|
||||
|
||||
Please remember that scrutiny is maintained by humans.
|
||||
|
||||
Every discussion, issue, and pull request is read and reviewed by
|
||||
humans (and sometimes machines, too). It is a boundary point at which
|
||||
people interact with each other and the work done. It is rude and
|
||||
disrespectful to approach this boundary with low-effort, unqualified
|
||||
work, since it puts the burden of validation on the maintainer.
|
||||
|
||||
In a perfect world, AI would produce high-quality, accurate work
|
||||
every time. But today, that reality depends on the driver of the AI.
|
||||
And today, most drivers of AI are just not good enough. So, until either
|
||||
the people get better, the AI gets better, or both, we have to have
|
||||
strict rules to protect maintainers.
|
||||
|
||||
## AI is Welcome Here
|
||||
|
||||
Many maintainers embrace AI tools as a productive tool in their workflow.
|
||||
As a project, scrutiny welcomes AI as a tool!
|
||||
|
||||
**Our reason for the strict AI policy is not due to an anti-AI stance**, but
|
||||
instead due to the number of highly unqualified people using AI. It's the
|
||||
people, not the tools, that are the problem.
|
||||
|
||||
This section is included to be transparent about the project's usage about
|
||||
AI for people who may disagree with it, and to address the misconception
|
||||
that this policy is anti-AI in nature.
|
||||
|
||||
# Credit
|
||||
|
||||
Adopted from [ghostty's AI policy](https://github.com/ghostty-org/ghostty/blob/1b7a15899ad40fba4ce020f537055d30eaf99ee8/AI_POLICY.md)
|
||||
+143
-3
@@ -1,4 +1,144 @@
|
||||
# Contributing
|
||||
# Contributing to scrutiny
|
||||
|
||||
This document describes the process of contributing to scrutiny. It is intended
|
||||
for anyone considering opening an **issue**, **discussion** or **pull request**.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The intention of these policies is not to be difficult, and
|
||||
> contributions are greatly appreciated. The goal is to streamline
|
||||
> and simplify the efforts of both contributers and maintainers.
|
||||
|
||||
## AI Usage
|
||||
|
||||
scrutiny has strict rules for AI usage. Please see
|
||||
the [AI Usage Policy](AI_POLICY.md). **This is very important.**
|
||||
|
||||
## Quick Guide
|
||||
|
||||
### I'd like to contribute
|
||||
|
||||
[All issues are actionable](#issues-are-actionable). Pick one and start
|
||||
working on it. Thank you. If you need help or guidance, comment on the issue.
|
||||
Issues that are extra friendly to new contributors are tagged with
|
||||
["contributor friendly"].
|
||||
|
||||
["contributor friendly"]: https://github.com/AnalogJ/scrutiny/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22contributor%20friendly%22
|
||||
|
||||
### I have a bug! / Something isn't working
|
||||
|
||||
First, search the issue tracker and discussions for similar issues. Tip: also
|
||||
search for [closed issues] and [discussions] — your issue might have already
|
||||
been fixed!
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> If there is an _open_ issue or discussion that matches your problem,
|
||||
> **please do not comment on it unless you have valuable insight to add**.
|
||||
>
|
||||
> GitHub has a very _noisy_ set of default notification settings which
|
||||
> sends an email to _every participant_ in an issue/discussion every time
|
||||
> someone adds a comment. Instead, use the handy upvote button for discussions,
|
||||
> and/or emoji reactions on both discussions and issues, which are a visible
|
||||
> yet non-disruptive way to show your support.
|
||||
|
||||
If your issue hasn't been reported already, open an ["Issue Triage"] discussion
|
||||
and make sure to fill in the template **completely**. They are vital for
|
||||
maintainers to figure out important details about your setup.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> A _very_ common mistake is to file a bug report either as a Q&A or a Feature
|
||||
> Request. **Please don't do this.** Otherwise, maintainers would have to ask
|
||||
> for your system information again manually, and sometimes they will even ask
|
||||
> you to create a new discussion because of how few detailed information is
|
||||
> required for other discussion types compared to Issue Triage.
|
||||
>
|
||||
> Because of this, please make sure that you _only_ use the "Issue Triage"
|
||||
> category for reporting bugs — thank you!
|
||||
|
||||
[closed issues]: https://github.com/AnalogJ/scrutiny/issues?q=is%3Aissue%20state%3Aclosed
|
||||
[discussions]: https://github.com/AnalogJ/scrutiny/discussions?discussions_q=is%3Aclosed
|
||||
["Issue Triage"]: https://github.com/AnalogJ/scrutiny/discussions/new?category=issue-triage
|
||||
|
||||
### I have an idea for a feature
|
||||
|
||||
Like bug reports, first search through both issues and discussions and try to
|
||||
find if your feature has already been requested. Otherwise, open a discussion
|
||||
in the ["Feature Requests, Ideas"] category.
|
||||
|
||||
["Feature Requests, Ideas"]: https://github.com/AnalogJ/scrutiny/discussions/new?category=feature-requests-ideas
|
||||
|
||||
### I've implemented a feature
|
||||
|
||||
1. If there is an issue for the feature, open a pull request straight away.
|
||||
2. If there is no issue, open a discussion and link to your branch.
|
||||
3. If you want to live dangerously, open a pull request and
|
||||
[hope for the best](#pull-requests-implement-an-issue).
|
||||
|
||||
### I have a question which is neither a bug report nor a feature request
|
||||
|
||||
Open a [Q&A discussion].
|
||||
|
||||
> [!NOTE]
|
||||
> If your question is about a missing feature, please open a discussion under
|
||||
> the ["Feature Requests, Ideas"] category. If scrutiny is behaving
|
||||
> unexpectedly, use the ["Issue Triage"] category.
|
||||
>
|
||||
> The "Q&A" category is strictly for other kinds of discussions and do not
|
||||
> require detailed information unlike the two other categories, meaning that
|
||||
> maintainers would have to spend the extra effort to ask for basic information
|
||||
> if you submit a bug report under this category.
|
||||
>
|
||||
> Therefore, please **pay attention to the category** before opening
|
||||
> discussions to save us all some time and energy. Thank you!
|
||||
|
||||
[Q&A discussion]: https://github.com/AnalogJ/scrutiny/discussions/new?category=q-a
|
||||
|
||||
## General Patterns
|
||||
|
||||
### Issues are Actionable
|
||||
|
||||
The scrutiny [issue tracker](https://github.com/AnalogJ/scrutiny/issues)
|
||||
is for _actionable items_.
|
||||
|
||||
Unlike some other projects, scrutiny **does not use the issue tracker for
|
||||
discussion or feature requests**. Instead, we use GitHub
|
||||
[discussions](https://github.com/AnalogJ/scrutiny/discussions) for that.
|
||||
Once a discussion reaches a point where a well-understood, actionable
|
||||
item is identified, it is moved to the issue tracker. **This pattern
|
||||
makes it easier for maintainers or contributors to find issues to work on
|
||||
since _every issue_ is ready to be worked on.**
|
||||
|
||||
If you are experiencing a bug and have clear steps to reproduce it, please
|
||||
open an issue. If you are experiencing a bug but you are not sure how to
|
||||
reproduce it or aren't sure if it's a bug, please open a discussion.
|
||||
If you have an idea for a feature, please open a discussion.
|
||||
|
||||
### Pull Requests Implement an Issue
|
||||
|
||||
Pull requests should be associated with a previously accepted issue.
|
||||
**If you open a pull request for something that wasn't previously discussed,**
|
||||
it may be closed or remain stale for an indefinite period of time. I'm not
|
||||
saying it will never be accepted, but the odds are stacked against you.
|
||||
|
||||
Issues tagged with "feature" represent accepted, well-scoped feature requests.
|
||||
If you implement an issue tagged with feature as described in the issue, your
|
||||
pull request will be accepted with a high degree of certainty.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> **Pull requests are NOT a place to discuss feature design.** Please do
|
||||
> not open a WIP pull request to discuss a feature. Instead, use a discussion
|
||||
> and link to your branch.
|
||||
|
||||
# Developer Guide
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> **The remainder of this file is dedicated to developers actively
|
||||
> working on scrutiny.** If you're a user reporting an issue, you can
|
||||
> ignore the rest of this document.
|
||||
|
||||
The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) containing source code for:
|
||||
- Scrutiny Backend Server (API)
|
||||
@@ -9,7 +149,7 @@ Depending on the functionality you are adding, you may need to setup a developme
|
||||
|
||||
# Modifying the Scrutiny Backend Server (API)
|
||||
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.20+)
|
||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.25)
|
||||
2. download the `scrutiny-web-frontend.tar.gz` for
|
||||
the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist`
|
||||
3. create a `scrutiny.yaml` config file
|
||||
@@ -161,7 +301,7 @@ docker cp scrutiny:/tmp/web.log web.log
|
||||
# Docker Development
|
||||
|
||||
```
|
||||
docker build -f docker/Dockerfile . -t chcr.io/analogj/scrutiny:master-omnibus
|
||||
docker build -f docker/Dockerfile . -t ghcr.io/analogj/scrutiny:master-omnibus
|
||||
docker run -it --rm -p 8080:8080 \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
--cap-add SYS_RAWIO \
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and provide it with the entire recipe, regardless of how many lines it contains.
|
||||
.SHELLFLAGS = -ec
|
||||
export GOTOOLCHAIN=go1.25.5
|
||||
|
||||
########################################################################################################################
|
||||
# Global Env Settings
|
||||
@@ -66,6 +67,11 @@ binary-dep:
|
||||
binary-test: binary-dep
|
||||
go test -v $(STATIC_TAGS) ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
GOTOOLCHAIN=go1.25.5 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0
|
||||
golangci-lint run ./...
|
||||
|
||||
.PHONY: binary-test-coverage
|
||||
binary-test-coverage: binary-dep
|
||||
go test -coverprofile=coverage.txt -covermode=atomic -v $(STATIC_TAGS) ./...
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
# scrutiny
|
||||
|
||||
[](https://github.com/AnalogJ/scrutiny/actions?query=workflow%3ACI)
|
||||
[](https://github.com/AnalogJ/scrutiny/actions/workflows/ci.yaml)
|
||||
[](https://codecov.io/gh/AnalogJ/scrutiny)
|
||||
[](https://github.com/AnalogJ/scrutiny/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/analogj/scrutiny)
|
||||
@@ -67,12 +67,17 @@ See [docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](./docs/TROUBLESHOOTING_DEVICE_COL
|
||||
|
||||
## Docker
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Using `latest-` tags is dangerous as it can update your image without warning. It is a best practice to pin a specific version. scrutiny pushes releases with semver tags,
|
||||
> so you can use tags like `v0.8.2-omnibus`, `v0.8-web`, `v0-collector`, etc. For a list of all image tags see
|
||||
> [scrutiny package versions](https://github.com/AnalogJ/scrutiny/pkgs/container/scrutiny/versions?filters%5Bversion_type%5D=tagged)
|
||||
|
||||
If you're using Docker, getting started is as simple as running the following command:
|
||||
|
||||
> See [docker/example.omnibus.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.omnibus.docker-compose.yml) for a docker-compose file.
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8080:8080 -p 8086:8086 \
|
||||
docker run -p 8080:8080 -p 8086:8086 --restart unless-stopped \
|
||||
-v `pwd`/scrutiny:/opt/scrutiny/config \
|
||||
-v `pwd`/influxdb2:/opt/scrutiny/influxdb \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
@@ -80,47 +85,47 @@ docker run -it --rm -p 8080:8080 -p 8086:8086 \
|
||||
--device=/dev/sda \
|
||||
--device=/dev/sdb \
|
||||
--name scrutiny \
|
||||
ghcr.io/analogj/scrutiny:master-omnibus
|
||||
ghcr.io/analogj/scrutiny:latest-omnibus
|
||||
```
|
||||
|
||||
- `/run/udev` is necessary to provide the Scrutiny collector with access to your device metadata
|
||||
- `--cap-add SYS_RAWIO` is necessary to allow `smartctl` permission to query your device SMART data
|
||||
- NOTE: If you have **NVMe** drives, you must add `--cap-add SYS_ADMIN` as well. See issue [#26](https://github.com/AnalogJ/scrutiny/issues/26#issuecomment-696817130)
|
||||
- `--device` entries are required to ensure that your hard disk devices are accessible within the container.
|
||||
- `ghcr.io/analogj/scrutiny:master-omnibus` is a omnibus image, containing both the webapp server (frontend & api) as well as the S.M.A.R.T metric collector. (see below)
|
||||
- `ghcr.io/analogj/scrutiny:latest-omnibus` is a omnibus image, containing both the webapp server (frontend & api) as well as the S.M.A.R.T metric collector. (see below)
|
||||
|
||||
### Hub/Spoke Deployment
|
||||
|
||||
In addition to the Omnibus image (available under the `latest` tag) you can deploy in Hub/Spoke mode, which requires 3
|
||||
other Docker images:
|
||||
|
||||
- `ghcr.io/analogj/scrutiny:master-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like
|
||||
- `ghcr.io/analogj/scrutiny:latest-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like
|
||||
scheduler. You can run one collector on each server.
|
||||
- `ghcr.io/analogj/scrutiny:master-web` - Contains the Web UI and API. Only one container necessary
|
||||
- `ghcr.io/analogj/scrutiny:latest-web` - Contains the Web UI and API. Only one container necessary
|
||||
- `influxdb:2.2` - InfluxDB image, used by the Web container to persist SMART data. Only one container necessary
|
||||
See [docs/TROUBLESHOOTING_INFLUXDB.md](./docs/TROUBLESHOOTING_INFLUXDB.md)
|
||||
|
||||
> See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
|
||||
|
||||
```bash
|
||||
docker run --rm -p 8086:8086 \
|
||||
docker run -p 8086:8086 --restart unless-stopped \
|
||||
-v `pwd`/influxdb2:/var/lib/influxdb2 \
|
||||
--name scrutiny-influxdb \
|
||||
influxdb:2.2
|
||||
|
||||
docker run --rm -p 8080:8080 \
|
||||
docker run -p 8080:8080 --restart unless-stopped \
|
||||
-v `pwd`/scrutiny:/opt/scrutiny/config \
|
||||
--name scrutiny-web \
|
||||
ghcr.io/analogj/scrutiny:master-web
|
||||
ghcr.io/analogj/scrutiny:latest-web
|
||||
|
||||
docker run --rm \
|
||||
docker run --restart unless-stopped \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
--cap-add SYS_RAWIO \
|
||||
--device=/dev/sda \
|
||||
--device=/dev/sdb \
|
||||
-e COLLECTOR_API_ENDPOINT=http://SCRUTINY_WEB_IPADDRESS:8080 \
|
||||
--name scrutiny-collector \
|
||||
ghcr.io/analogj/scrutiny:master-collector
|
||||
ghcr.io/analogj/scrutiny:latest-collector
|
||||
```
|
||||
|
||||
## Manual Installation (without-Docker)
|
||||
@@ -157,7 +162,7 @@ Neither file is required, however if provided, it allows you to configure how Sc
|
||||
|
||||
## Cron Schedule
|
||||
Unfortunately the Cron schedule cannot be configured via the `collector.yaml` (as the collector binary needs to be trigged by a scheduler/cron).
|
||||
However, if you are using the official `ghcr.io/analogj/scrutiny:master-collector` or `ghcr.io/analogj/scrutiny:master-omnibus` docker images,
|
||||
However, if you are using the official `ghcr.io/analogj/scrutiny:latest-collector` or `ghcr.io/analogj/scrutiny:latest-omnibus` docker images,
|
||||
you can use the `COLLECTOR_CRON_SCHEDULE` environmental variable to override the default cron schedule (daily @ midnight - `0 0 * * *`).
|
||||
|
||||
`docker run -e COLLECTOR_CRON_SCHEDULE="0 0 * * *" ...`
|
||||
@@ -265,7 +270,8 @@ We use SemVer for versioning. For the versions available, see the tags on this r
|
||||
|
||||
# Authors
|
||||
|
||||
Jason Kulatunga - Initial Development - @AnalogJ
|
||||
* Jason Kulatunga - Initial Development - [@AnalogJ](https://github.com/AnalogJ/)
|
||||
* Aram Akhavan - Maintenence - [@kaysond](https://github.com/kaysond/)
|
||||
|
||||
# Licenses
|
||||
|
||||
|
||||
@@ -3,17 +3,18 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/collector/pkg/config"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/collector/pkg/config"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
utils "github.com/analogj/go-util/utils"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -37,8 +38,8 @@ func main() {
|
||||
}
|
||||
|
||||
//we're going to load the config file manually, since we need to validate it.
|
||||
err = config.ReadConfig(configFilePath) // Find and read the config file
|
||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||
err = config.ReadConfig(configFilePath) // Find and read the config file
|
||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||
//ignore "could not find config file"
|
||||
} else if err != nil {
|
||||
os.Exit(1)
|
||||
@@ -81,7 +82,7 @@ OPTIONS:
|
||||
|
||||
subtitle := collectorMetrics + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorMetrics))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
@@ -89,7 +90,7 @@ OPTIONS:
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
`), subtitle)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -2,14 +2,15 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
utils "github.com/analogj/go-util/utils"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -57,7 +58,7 @@ OPTIONS:
|
||||
|
||||
subtitle := collectorSelfTest + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorSelfTest))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
@@ -65,7 +66,7 @@ OPTIONS:
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
`), subtitle)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -3,9 +3,10 @@ package collector
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{Timeout: 60 * time.Second}
|
||||
@@ -14,17 +15,6 @@ type BaseCollector struct {
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func (c *BaseCollector) getJson(url string, target interface{}) error {
|
||||
|
||||
r, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
return json.NewDecoder(r.Body).Decode(target)
|
||||
}
|
||||
|
||||
func (c *BaseCollector) postJson(url string, body interface{}, target interface{}) error {
|
||||
requestBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,12 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||
"github.com/analogj/scrutiny/collector/pkg/config"
|
||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||
@@ -11,10 +17,6 @@ import (
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MetricsCollector struct {
|
||||
@@ -90,8 +92,9 @@ func (mc *MetricsCollector) Run() error {
|
||||
//go mc.Collect(&wg, device.WWN, device.DeviceName, device.DeviceType)
|
||||
mc.Collect(device.WWN, device.DeviceName, device.DeviceType)
|
||||
|
||||
// TODO: we may need to sleep for between each call to smartctl -a
|
||||
//time.Sleep(30 * time.Millisecond)
|
||||
if mc.config.GetInt("commands.metrics_smartctl_wait") > 0 {
|
||||
time.Sleep(time.Duration(mc.config.GetInt("commands.metrics_smartctl_wait")) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
//mc.logger.Infoln("Main: Waiting for workers to finish")
|
||||
@@ -113,7 +116,7 @@ func (mc *MetricsCollector) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string, deviceType string) {
|
||||
// func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string, deviceType string) {
|
||||
func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceType string) {
|
||||
//defer wg.Done()
|
||||
if len(deviceWWN) == 0 {
|
||||
|
||||
@@ -3,11 +3,12 @@ package shell
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type localShell struct{}
|
||||
@@ -36,7 +37,7 @@ func (s *localShell) Command(logger *logrus.Entry, cmdName string, cmdArgs []str
|
||||
if workingDir != "" && path.IsAbs(workingDir) {
|
||||
cmd.Dir = workingDir
|
||||
} else if workingDir != "" {
|
||||
return "", errors.New("Working Directory must be an absolute path")
|
||||
return "", errors.New("working directory must be an absolute path")
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
@@ -7,8 +7,8 @@ package mock_shell
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
logrus "github.com/sirupsen/logrus"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface.
|
||||
|
||||
@@ -2,15 +2,16 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// When initializing this class the following methods must be called:
|
||||
@@ -47,9 +48,17 @@ func (c *configuration) Init() error {
|
||||
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("commands.metrics_smartctl_wait", 0)
|
||||
|
||||
//configure env variable parsing.
|
||||
c.SetEnvPrefix("COLLECTOR")
|
||||
c.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
|
||||
c.AutomaticEnv()
|
||||
|
||||
//c.SetDefault("collect.short.command", "-a -o on -S on")
|
||||
|
||||
c.SetDefault("allow_listed_devices", []string{})
|
||||
|
||||
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
|
||||
c.SetConfigType("yaml")
|
||||
//c.SetConfigName("drawbridge")
|
||||
@@ -159,7 +168,7 @@ func (c *configuration) GetCommandMetricsInfoArgs(deviceName string) string {
|
||||
overrides := c.GetDeviceOverrides()
|
||||
|
||||
for _, deviceOverrides := range overrides {
|
||||
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
|
||||
if strings.EqualFold(deviceName, deviceOverrides.Device) {
|
||||
//found matching device
|
||||
if len(deviceOverrides.Commands.MetricsInfoArgs) > 0 {
|
||||
return deviceOverrides.Commands.MetricsInfoArgs
|
||||
@@ -175,7 +184,7 @@ func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
|
||||
overrides := c.GetDeviceOverrides()
|
||||
|
||||
for _, deviceOverrides := range overrides {
|
||||
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
|
||||
if strings.EqualFold(deviceName, deviceOverrides.Device) {
|
||||
//found matching device
|
||||
if len(deviceOverrides.Commands.MetricsSmartArgs) > 0 {
|
||||
return deviceOverrides.Commands.MetricsSmartArgs
|
||||
@@ -186,3 +195,18 @@ func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
|
||||
}
|
||||
return c.GetString("commands.metrics_smart_args")
|
||||
}
|
||||
|
||||
func (c *configuration) IsAllowlistedDevice(deviceName string) bool {
|
||||
allowList := c.GetStringSlice("allow_listed_devices")
|
||||
if len(allowList) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, item := range allowList {
|
||||
if item == deviceName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -144,3 +144,29 @@ func TestConfiguration_OverrideDeviceCommands_MetricsInfoArgs(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
func TestConfiguration_DeviceAllowList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("present", func(t *testing.T) {
|
||||
testConfig, err := config.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, testConfig.ReadConfig(path.Join("testdata", "allow_listed_devices_present.yaml")))
|
||||
|
||||
require.True(t, testConfig.IsAllowlistedDevice("/dev/sda"), "/dev/sda should be allow listed")
|
||||
require.False(t, testConfig.IsAllowlistedDevice("/dev/sdc"), "/dev/sda should not be allow listed")
|
||||
})
|
||||
|
||||
t.Run("missing", func(t *testing.T) {
|
||||
testConfig, err := config.Create()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Really just any other config where the key is full missing
|
||||
require.NoError(t, testConfig.ReadConfig(path.Join("testdata", "override_device_commands.yaml")))
|
||||
|
||||
// Anything should be allow listed if the key isnt there
|
||||
require.True(t, testConfig.IsAllowlistedDevice("/dev/sda"), "/dev/sda should be allow listed")
|
||||
require.True(t, testConfig.IsAllowlistedDevice("/dev/sdc"), "/dev/sda should be allow listed")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,4 +25,6 @@ type Interface interface {
|
||||
GetDeviceOverrides() []models.ScanOverride
|
||||
GetCommandMetricsInfoArgs(deviceName string) string
|
||||
GetCommandMetricsSmartArgs(deviceName string) string
|
||||
|
||||
IsAllowlistedDevice(deviceName string) bool
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
reflect "reflect"
|
||||
|
||||
models "github.com/analogj/scrutiny/collector/pkg/models"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
viper "github.com/spf13/viper"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface.
|
||||
@@ -175,6 +175,20 @@ func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
|
||||
}
|
||||
|
||||
// IsAllowlistedDevice mocks base method.
|
||||
func (m *MockInterface) IsAllowlistedDevice(deviceName string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsAllowlistedDevice", deviceName)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsAllowlistedDevice indicates an expected call of IsAllowlistedDevice.
|
||||
func (mr *MockInterfaceMockRecorder) IsAllowlistedDevice(deviceName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAllowlistedDevice", reflect.TypeOf((*MockInterface)(nil).IsAllowlistedDevice), deviceName)
|
||||
}
|
||||
|
||||
// IsSet mocks base method.
|
||||
func (m *MockInterface) IsSet(key string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
allow_listed_devices:
|
||||
- /dev/sda
|
||||
- /dev/sdb
|
||||
@@ -107,8 +107,8 @@ func (d *Detect) SmartCtlInfo(device *models.Device) error {
|
||||
if len(device.WWN) == 0 {
|
||||
// no WWN populated after WWN lookup and fallback. we need to throw an error
|
||||
errMsg := fmt.Sprintf("no WWN (or fallback) populated for device: %s. Device will be registered, but no data will be published for this device. ", device.DeviceName)
|
||||
d.Logger.Errorf(errMsg)
|
||||
return fmt.Errorf(errMsg)
|
||||
d.Logger.Errorf("%v", errMsg)
|
||||
return fmt.Errorf("%v", errMsg)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -124,6 +124,11 @@ func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []mod
|
||||
|
||||
deviceFile := strings.ToLower(scannedDevice.Name)
|
||||
|
||||
// If the user has defined a device allow list, and this device isnt there, then ignore it
|
||||
if !d.Config.IsAllowlistedDevice(deviceFile) {
|
||||
continue
|
||||
}
|
||||
|
||||
detectedDevice := models.Device{
|
||||
HostId: d.Config.GetString("host.id"),
|
||||
DeviceType: scannedDevice.Type,
|
||||
|
||||
@@ -9,21 +9,21 @@ import (
|
||||
mock_config "github.com/analogj/scrutiny/collector/pkg/config/mock"
|
||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestDetect_SmartctlScan(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
||||
|
||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||
testScanResults, err := os.ReadFile("testdata/smartctl_scan_simple.json")
|
||||
@@ -47,12 +47,12 @@ func TestDetect_SmartctlScan(t *testing.T) {
|
||||
func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
||||
|
||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||
testScanResults, err := os.ReadFile("testdata/smartctl_scan_megaraid.json")
|
||||
@@ -79,12 +79,12 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
|
||||
func TestDetect_SmartctlScan_Nvme(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
||||
|
||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||
testScanResults, err := os.ReadFile("testdata/smartctl_scan_nvme.json")
|
||||
@@ -110,12 +110,12 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
|
||||
func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
||||
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
@@ -143,12 +143,12 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
||||
func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
||||
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
@@ -175,11 +175,11 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
||||
func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{
|
||||
{
|
||||
Device: "/dev/bus/0",
|
||||
@@ -217,12 +217,12 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
||||
func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
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"}}})
|
||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
{
|
||||
@@ -250,12 +250,12 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
||||
func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda"}})
|
||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
{
|
||||
@@ -282,7 +282,6 @@ func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T)
|
||||
func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
|
||||
// setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||
@@ -302,10 +301,48 @@ func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
|
||||
require.Equal(t, "ata", transformedDevices[0].DeviceType)
|
||||
}
|
||||
|
||||
func TestDetect_TransformDetectedDevices_AllowListFilters(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
|
||||
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", DeviceType: []string{"sat+megaraid"}}})
|
||||
fakeConfig.EXPECT().IsAllowlistedDevice("/dev/sda").Return(true)
|
||||
fakeConfig.EXPECT().IsAllowlistedDevice("/dev/sdb").Return(false)
|
||||
detectedDevices := models.Scan{
|
||||
Devices: []models.ScanDevice{
|
||||
{
|
||||
Name: "/dev/sda",
|
||||
InfoName: "/dev/sda",
|
||||
Protocol: "ata",
|
||||
Type: "ata",
|
||||
},
|
||||
{
|
||||
Name: "/dev/sdb",
|
||||
InfoName: "/dev/sdb",
|
||||
Protocol: "ata",
|
||||
Type: "ata",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
d := detect.Detect{
|
||||
Config: fakeConfig,
|
||||
}
|
||||
|
||||
// test
|
||||
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||
|
||||
// assert
|
||||
require.Equal(t, 1, len(transformedDevices))
|
||||
require.Equal(t, "sda", transformedDevices[0].DeviceName)
|
||||
}
|
||||
|
||||
func TestDetect_SmartCtlInfo(t *testing.T) {
|
||||
t.Run("should report nvme info", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
const (
|
||||
someArgs = "--info --json"
|
||||
|
||||
@@ -2,12 +2,13 @@ package detect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/jaypipes/ghw"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DevicePrefix() string {
|
||||
@@ -23,15 +24,15 @@ func (d *Detect) Start() ([]models.Device, error) {
|
||||
}
|
||||
|
||||
//inflate device info for detected devices.
|
||||
for ndx, _ := range detectedDevices {
|
||||
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
for ndx := range detectedDevices {
|
||||
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
populateUdevInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
}
|
||||
|
||||
return detectedDevices, nil
|
||||
}
|
||||
|
||||
//WWN values NVMe and SCSI
|
||||
// WWN values NVMe and SCSI
|
||||
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
block, err := ghw.Block()
|
||||
if err == nil {
|
||||
@@ -61,7 +62,7 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
func populateUdevInfo(detectedDevice *models.Device) error {
|
||||
// Get device major:minor numbers
|
||||
// `cat /sys/class/block/sda/dev`
|
||||
devNo, err := ioutil.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
|
||||
devNo, err := os.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -69,7 +70,7 @@ func populateUdevInfo(detectedDevice *models.Device) error {
|
||||
// Look up block device in udev runtime database
|
||||
// `cat /run/udev/data/b8:0`
|
||||
udevID := "b" + strings.TrimSpace(string(devNo))
|
||||
udevBytes, err := ioutil.ReadFile(filepath.Join("/run/udev/data/", udevID))
|
||||
udevBytes, err := os.ReadFile(filepath.Join("/run/udev/data/", udevID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -97,7 +98,5 @@ func populateUdevInfo(detectedDevice *models.Device) error {
|
||||
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package detect_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWwn_FromStringTable(t *testing.T) {
|
||||
@@ -25,8 +25,7 @@ func TestWwn_FromStringTable(t *testing.T) {
|
||||
}
|
||||
//test
|
||||
for _, tt := range tests {
|
||||
testname := fmt.Sprintf("%s", tt.wwnStr)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
t.Run(tt.wwnStr, func(t *testing.T) {
|
||||
str := tt.wwn.ToString()
|
||||
require.Equal(t, tt.wwnStr, str)
|
||||
})
|
||||
|
||||
+28
-13
@@ -12,22 +12,28 @@ RUN make binary-frontend
|
||||
|
||||
|
||||
######## Build the backend
|
||||
FROM golang:1.20-bullseye as backendbuild
|
||||
FROM golang:1.25-bookworm as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
COPY --link . /go/src/github.com/analogj/scrutiny
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get install -y --no-install-recommends \
|
||||
file \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
|
||||
|
||||
|
||||
######## Combine build artifacts in runtime image
|
||||
FROM debian:bullseye-slim as runtime
|
||||
FROM debian:bookworm-slim as runtime
|
||||
ARG TARGETARCH
|
||||
EXPOSE 8080
|
||||
WORKDIR /opt/scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb
|
||||
ENV S6VER="1.21.8.0"
|
||||
ENV S6VER="3.1.6.2"
|
||||
ENV INFLUXVER="2.2.0"
|
||||
ENV S6_SERVICES_READYTIME=1000
|
||||
SHELL ["/usr/bin/sh", "-c"]
|
||||
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get install -y --no-install-recommends \
|
||||
@@ -36,17 +42,23 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
|
||||
curl \
|
||||
smartmontools \
|
||||
tzdata \
|
||||
procps \
|
||||
xz-utils \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& 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/v${S6VER}/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 / \
|
||||
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \
|
||||
&& curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-${INFLUXVER}-${TARGETARCH}.deb --output /tmp/influxdb2-${INFLUXVER}-${TARGETARCH}.deb \
|
||||
&& case ${TARGETARCH} in \
|
||||
"amd64") S6_ARCH=x86_64 ;; \
|
||||
"arm64") S6_ARCH=aarch64 ;; \
|
||||
esac \
|
||||
&& curl https://github.com/just-containers/s6-overlay/releases/download/v${S6VER}/s6-overlay-noarch.tar.xz -L -s --output /tmp/s6-overlay-noarch.tar.xz \
|
||||
&& tar -Jxpf /tmp/s6-overlay-noarch.tar.xz -C / \
|
||||
&& rm -rf /tmp/s6-overlay-noarch.tar.xz \
|
||||
&& curl https://github.com/just-containers/s6-overlay/releases/download/v${S6VER}/s6-overlay-${S6_ARCH}.tar.xz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.xz \
|
||||
&& tar -Jxpf /tmp/s6-overlay-${S6_ARCH}.tar.xz -C / \
|
||||
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.xz
|
||||
RUN curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-${INFLUXVER}-${TARGETARCH}.deb --output /tmp/influxdb2-${INFLUXVER}-${TARGETARCH}.deb \
|
||||
&& dpkg -i --force-all /tmp/influxdb2-${INFLUXVER}-${TARGETARCH}.deb \
|
||||
&& rm -rf /tmp/influxdb2-2.2.0-${TARGETARCH}.deb
|
||||
&& rm -rf /tmp/influxdb2-${INFLUXVER}-${TARGETARCH}.deb
|
||||
|
||||
COPY /rootfs /
|
||||
|
||||
@@ -57,6 +69,9 @@ RUN chmod 0644 /etc/cron.d/scrutiny && \
|
||||
rm -f /etc/cron.daily/* && \
|
||||
mkdir -p /opt/scrutiny/web && \
|
||||
mkdir -p /opt/scrutiny/config && \
|
||||
chmod -R ugo+rwx /opt/scrutiny/config
|
||||
chmod -R ugo+rwx /opt/scrutiny/config && \
|
||||
chmod +x /etc/cont-init.d/* && \
|
||||
chmod +x /etc/services.d/*/run && \
|
||||
chmod +x /etc/services.d/*/finish
|
||||
|
||||
CMD ["/init"]
|
||||
|
||||
@@ -4,20 +4,21 @@
|
||||
|
||||
|
||||
########
|
||||
FROM golang:1.20-bullseye as backendbuild
|
||||
FROM golang:1.25-bookworm as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
|
||||
RUN apt-get update && apt-get install -y file && rm -rf /var/lib/apt/lists/*
|
||||
RUN make binary-clean binary-collector
|
||||
|
||||
########
|
||||
FROM debian:bullseye-slim as runtime
|
||||
FROM debian:bookworm-slim as runtime
|
||||
WORKDIR /opt/scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
|
||||
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates
|
||||
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates
|
||||
|
||||
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
|
||||
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
||||
|
||||
@@ -11,21 +11,22 @@ COPY --link . /go/src/github.com/analogj/scrutiny
|
||||
RUN make binary-frontend
|
||||
|
||||
######## Build the backend
|
||||
FROM golang:1.20-bullseye as backendbuild
|
||||
FROM golang:1.25-bookworm as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
COPY --link . /go/src/github.com/analogj/scrutiny
|
||||
|
||||
RUN apt-get update && apt-get install -y file && rm -rf /var/lib/apt/lists/*
|
||||
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
|
||||
|
||||
|
||||
######## Combine build artifacts in runtime image
|
||||
FROM debian:bullseye-slim as runtime
|
||||
FROM debian:bookworm-slim as runtime
|
||||
EXPOSE 8080
|
||||
WORKDIR /opt/scrutiny
|
||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && update-ca-certificates
|
||||
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates
|
||||
|
||||
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
|
||||
COPY --link --from=frontendbuild --chmod=644 /go/src/github.com/analogj/scrutiny/dist /opt/scrutiny/web
|
||||
|
||||
@@ -25,4 +25,4 @@ fi
|
||||
|
||||
# now that we have the env start cron in the foreground
|
||||
echo "starting cron"
|
||||
su -c "cron -f -L 15" root
|
||||
exec su -c "cron -f -L 15" root
|
||||
|
||||
@@ -2,6 +2,7 @@ version: '2.4'
|
||||
|
||||
services:
|
||||
influxdb:
|
||||
restart: unless-stopped
|
||||
image: influxdb:2.2
|
||||
ports:
|
||||
- '8086:8086'
|
||||
@@ -15,6 +16,7 @@ services:
|
||||
|
||||
|
||||
web:
|
||||
restart: unless-stopped
|
||||
image: 'ghcr.io/analogj/scrutiny:master-web'
|
||||
ports:
|
||||
- '8080:8080'
|
||||
@@ -33,6 +35,7 @@ services:
|
||||
start_period: 10s
|
||||
|
||||
collector:
|
||||
restart: unless-stopped
|
||||
image: 'ghcr.io/analogj/scrutiny:master-collector'
|
||||
cap_add:
|
||||
- SYS_RAWIO
|
||||
@@ -41,6 +44,9 @@ services:
|
||||
environment:
|
||||
COLLECTOR_API_ENDPOINT: 'http://web:8080'
|
||||
COLLECTOR_HOST_ID: 'scrutiny-collector-hostname'
|
||||
# If true forces the collector to run on startup (cron will be started after the collector completes)
|
||||
# see: https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#collector-trigger-on-startup
|
||||
COLLECTOR_RUN_STARTUP: false
|
||||
depends_on:
|
||||
web:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -2,6 +2,7 @@ version: '3.5'
|
||||
|
||||
services:
|
||||
scrutiny:
|
||||
restart: unless-stopped
|
||||
container_name: scrutiny
|
||||
image: ghcr.io/analogj/scrutiny:master-omnibus
|
||||
cap_add:
|
||||
|
||||
@@ -59,6 +59,7 @@ networks:
|
||||
|
||||
services:
|
||||
influxdb:
|
||||
restart: unless-stopped
|
||||
container_name: influxdb
|
||||
image: influxdb:2.1-alpine
|
||||
ports:
|
||||
@@ -73,13 +74,14 @@ services:
|
||||
- DOCKER_INFLUXDB_INIT_ORG=homelab
|
||||
- DOCKER_INFLUXDB_INIT_BUCKET=scrutiny
|
||||
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=your-very-secret-token
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring
|
||||
|
||||
scrutiny:
|
||||
restart: unless-stopped
|
||||
container_name: scrutiny
|
||||
image: ghcr.io/analogj/scrutiny:master-web
|
||||
# best practice: pin to a specific release instead of latest
|
||||
image: ghcr.io/analogj/scrutiny:latest-web
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
@@ -94,7 +96,6 @@ services:
|
||||
- SCRUTINY_NOTIFY_URLS=["http://gotify:80/message?token=a-gotify-token"]
|
||||
depends_on:
|
||||
- influxdb
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- notifications
|
||||
- monitoring
|
||||
@@ -121,7 +122,7 @@ apt install smartmontools -y
|
||||
# 3. Make it exacutable
|
||||
# 4. List the contents of the library for confirmation
|
||||
mkdir -p /opt/scrutiny/bin && \
|
||||
curl -L https://github.com/AnalogJ/scrutiny/releases/download/v0.5.0/scrutiny-collector-metrics-linux-amd64 > /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 && \
|
||||
curl -L https://github.com/AnalogJ/scrutiny/releases/download/v0.8.1/scrutiny-collector-metrics-linux-amd64 > /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 && \
|
||||
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 && \
|
||||
ls -lha /opt/scrutiny/bin
|
||||
```
|
||||
@@ -168,7 +169,9 @@ version: "3.4"
|
||||
services:
|
||||
|
||||
collector:
|
||||
image: 'ghcr.io/analogj/scrutiny:master-collector'
|
||||
restart: unless-stopped
|
||||
# best practice: pin to a specific release instead of latest
|
||||
image: 'ghcr.io/analogj/scrutiny:latest-collector'
|
||||
cap_add:
|
||||
- SYS_RAWIO
|
||||
volumes:
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ Thankfully the following users have been gracious enough to test/validate Scruti
|
||||
|
||||
| Architecture Name | Binaries | Docker |
|
||||
| --- | --- | --- |
|
||||
| linux-amd64 | -- | @feroxy @rshxyz |
|
||||
| linux-amd64 | @TizzAmmazz | @feroxy @rshxyz |
|
||||
| linux-arm-5 | -- | |
|
||||
| linux-arm-6 | -- | |
|
||||
| linux-arm-7 | @Zorlin | @martini1992 |
|
||||
@@ -17,4 +17,4 @@ Thankfully the following users have been gracious enough to test/validate Scruti
|
||||
| macos-amd64 | -- | -- |
|
||||
| macos-arm64 | -- | -- |
|
||||
| windows-amd64 | @gabrielv33 | -- |
|
||||
| windows-arm64 | -- | -- |
|
||||
| windows-arm64 | -- | -- |
|
||||
|
||||
@@ -250,8 +250,9 @@ UPDATE devices SET device_status = null;
|
||||
|
||||
### 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.
|
||||
As thoroughly discussed in [#255](https://github.com/AnalogJ/scrutiny/issues/255) and [#522](https://github.com/AnalogJ/scrutiny/issues/522), Seagate (Ironwolf & others) drives are almost always marked as failed by Scrutiny.
|
||||
|
||||
#### Seek Error Rate & Read Error Rate (#255)
|
||||
> 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
|
||||
>
|
||||
@@ -286,6 +287,15 @@ 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/
|
||||
|
||||
#### Seagate Raw Values are incorrect (#522)
|
||||
Basically Seagate drives are known to use a custom data format for a number of their SMART attributes. This causes issues with Scrutiny's threshold analysis.
|
||||
|
||||
- The workaround is to customize the smartctl command that Scrutiny uses for your drive by [creating a collector.yaml file](https://github.com/AnalogJ/scrutiny/issues/522#issuecomment-1766727988) and "fixing" each attribute
|
||||
- The **real "fix"** is to make sure your Seagate drive is correctly supported by smartmontools. See this [PR](https://github.com/smartmontools/smartmontools/pull/247)
|
||||
|
||||
Sorry for the bad news, but this is a known issue and there's just not much we can do on the Scrutiny side.
|
||||
|
||||
|
||||
## Hub & Spoke model, with multiple Hosts.
|
||||
|
||||

|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
# Docker Images `master-omnibus` vs `latest`
|
||||
# Docker Images `latest` vs `nightly`
|
||||
|
||||
> 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`
|
||||
> TL;DR; The `latest-omnibus`, `latest-collector`, and `latest-web` tags point to the most recent release. (`latest` points to `latest-omnibus`)
|
||||
> The `nightly-omnibus`, `nightly-collector`, and `nightly-web` tags point to builds that are generated every night from the latest commit on the `master` branch.
|
||||
|
||||
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
|
||||
The CD scripts used to orchestrate the docker image builds can be found here:
|
||||
* https://github.com/AnalogJ/scrutiny/blob/master/.github/workflows/docker-build.yaml
|
||||
* https://github.com/AnalogJ/scrutiny/blob/master/.github/workflows/docker-nightly.yaml
|
||||
|
||||
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.
|
||||
In general scrutiny follows a feature branch development process, which means that the `master` branch should ideally always be free of bugs
|
||||
This is driven by the requirement that every PR be reviewed and pass all tests. Unfortunately, bugs do make it through, especially because of the
|
||||
enormous number of hard drives that scrutiny must support..
|
||||
|
||||
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`)
|
||||
This means that while the nightly builds should have the latest features and bug fixes, there may be things that sneak through. Unless you need a particular
|
||||
feature or bug fix, we recommend sticking to releases. Also note that using `latest` tags is generally considered a bad practice; pin a specific version instead.
|
||||
|
||||
So changing from `master-omnibus -> latest` will be the same thing for all intents and purposes.
|
||||
# Running Docker `rootless`
|
||||
|
||||
> 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.
|
||||
To avoid that the container(s) restart when you installed Docker as `rootless` you need to isssue the following commands to allow the session to stay alive even after you close your (SSH) sesssion:
|
||||
|
||||
`sudo loginctl enable-linger $(whoami)`
|
||||
|
||||
`systemctl --user enable docker`
|
||||
|
||||
@@ -11,6 +11,32 @@ dependency. It's a dedicated timeseries database, as opposed to the general purp
|
||||
a bunch of testing and analysis before I made the change. With InfluxDB the memory footprint for Scrutiny (at idle) is ~
|
||||
100mb, which is still fairly reasonable.
|
||||
|
||||
### Data Size
|
||||
|
||||
It's surprisingly easy to reach extremely large database sizes, if you don't use downsampling, or you downsample incorrectly.
|
||||
The growth rate is pretty unintuitive -- see https://github.com/AnalogJ/scrutiny/issues/650#issuecomment-2365174940
|
||||
|
||||
> Fasten stores the SMART metrics in a timeseries database (InfluxDB), and automatically downsamples the data on a schedule.
|
||||
>
|
||||
> The expectation was that cron would run daily, and there would be:
|
||||
>
|
||||
> - 7 daily data points
|
||||
> - 3 weekly data points
|
||||
> - 11 monthly data points
|
||||
> - and infinite yearly data points.
|
||||
>
|
||||
> These data points would be for each SMART metric, for each device.
|
||||
> eg. in one year, (7+3+11)*80ish SMART attributes = 1680 datapoints for one device
|
||||
>
|
||||
> If you're running cron every 15 minutes, your browser will instead be attempting to display:
|
||||
>
|
||||
> - 96*7 daily data points
|
||||
> - 3 weekly
|
||||
> - 11 monthly
|
||||
>
|
||||
> so (96*7 + 3 + 11)*80 = 54,880 datapoints for each device 😭
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
InfluxDB is a required dependency for Scrutiny v0.4.0+.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SQLite Table(s)
|
||||
|
||||
Table Device {
|
||||
Archived bool
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time
|
||||
UpdatedAt time
|
||||
|
||||
@@ -81,6 +81,7 @@ devices:
|
||||
# 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.
|
||||
# metrics_smartctl_wait: 0 # time to wait in seconds between each disk's check
|
||||
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
@@ -67,7 +67,7 @@ log:
|
||||
|
||||
#notify:
|
||||
# urls:
|
||||
# - "discord://token@channel"
|
||||
# - "discord://token@webhookid"
|
||||
# - "telegram://token@telegram?channels=channel-1[,channel-2,...]"
|
||||
# - "pushover://shoutrrr:apiToken@userKey/?priority=1&devices=device1[,device2, ...]"
|
||||
# - "slack://[botname@]token-a/token-b/token-c"
|
||||
|
||||
@@ -1,82 +1,93 @@
|
||||
module github.com/analogj/scrutiny
|
||||
|
||||
go 1.20
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
|
||||
github.com/containrrr/shoutrrr v0.7.1
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/glebarez/sqlite v1.4.5
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.0.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.9.0
|
||||
github.com/jaypipes/ghw v0.6.1
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/samber/lo v1.25.0
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/spf13/viper v1.14.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
golang.org/x/sync v0.1.0
|
||||
gorm.io/gorm v1.23.5
|
||||
github.com/analogj/go-util v0.0.0-20210417161720-39b497cca03b
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.14.0
|
||||
github.com/jaypipes/ghw v0.21.2
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v2 v2.27.7
|
||||
go.uber.org/mock v0.6.0
|
||||
golang.org/x/sync v0.19.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.8.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.17.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||
github.com/go-playground/locales v0.13.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.2.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
||||
github.com/jaypipes/pcidb v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
|
||||
github.com/jaypipes/pcidb v1.1.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.4 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
|
||||
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 // indirect
|
||||
github.com/leodido/go-urn v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kvz/logstreamer v0.0.0-20221024075423-bf5cfbd32e39 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/oapi-codegen/runtime v1.1.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/afero v1.9.2 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
|
||||
modernc.org/libc v1.16.8 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.1.1 // indirect
|
||||
modernc.org/sqlite v1.17.2 // indirect
|
||||
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect
|
||||
modernc.org/libc v1.67.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.44.3 // indirect
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
#!/command/with-contenv bash
|
||||
|
||||
if [ -n "${TZ}" ]
|
||||
then
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
#!/command/with-contenv bash
|
||||
|
||||
# Cron runs in its own isolated environment (usually using only /etc/environment )
|
||||
# So when the container starts up, we will do a dump of the runtime environment into a .env file that we
|
||||
@@ -12,4 +12,4 @@ COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
|
||||
[[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}"
|
||||
|
||||
# replace placeholder with correct value
|
||||
sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny
|
||||
sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
#!/command/with-contenv bash
|
||||
|
||||
# ensure not run (successfully) before
|
||||
if [ -f /tmp/custom-init-performed ]; then
|
||||
echo 'INFO: custom init already performed'
|
||||
s6-svc -D /run/service/collector-once # prevent s6 from restarting service
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "waiting for scrutiny service to start"
|
||||
s6-svwait -u /var/run/s6/services/scrutiny
|
||||
|
||||
#tell s6 to only run this script once
|
||||
s6-svc -O /var/run/s6/services/collector-once
|
||||
s6-svwait -u /run/service/scrutiny
|
||||
|
||||
# wait until scrutiny is "Ready"
|
||||
until $(curl --output /dev/null --silent --head --fail http://localhost:8080/api/health); do echo "scrutiny api not ready" && sleep 5; done
|
||||
|
||||
echo "starting scrutiny collector (run-once mode. subsequent calls will be triggered via cron service)"
|
||||
/opt/scrutiny/bin/scrutiny-collector-metrics run
|
||||
|
||||
# prevent script's core logic from running again
|
||||
touch /tmp/custom-init-performed
|
||||
|
||||
# prevent s6 from restarting service
|
||||
s6-svc -D /run/service/collector-once
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/execlineb -S0
|
||||
#!/command/execlineb -S0
|
||||
|
||||
echo "cron exiting"
|
||||
s6-svscanctl -t /var/run/s6/services
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
#!/command/with-contenv bash
|
||||
|
||||
echo "starting cron"
|
||||
cron -f -L 15
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
#!/command/with-contenv bash
|
||||
|
||||
mkdir -p /opt/scrutiny/influxdb/
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
#!/command/with-contenv bash
|
||||
|
||||
echo "waiting for influxdb"
|
||||
until $(curl --output /dev/null --silent --head --fail http://localhost:8086/health); do echo "influxdb not ready" && sleep 5; done
|
||||
|
||||
@@ -3,15 +3,16 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
utils "github.com/analogj/go-util/utils"
|
||||
"github.com/fatih/color"
|
||||
@@ -36,8 +37,8 @@ func main() {
|
||||
}
|
||||
|
||||
//we're going to load the config file manually, since we need to validate it.
|
||||
err = config.ReadConfig(configFilePath) // Find and read the config file
|
||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||
err = config.ReadConfig(configFilePath) // Find and read the config file
|
||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||
//ignore "could not find config file"
|
||||
} else if err != nil {
|
||||
log.Print(color.HiRedString("CONFIG ERROR: %v", err))
|
||||
@@ -81,7 +82,7 @@ OPTIONS:
|
||||
|
||||
subtitle := scrutiny + utils.LeftPad2Len(versionInfo, " ", 65-len(scrutiny))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
@@ -89,7 +90,7 @@ OPTIONS:
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
`), subtitle)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
reflect "reflect"
|
||||
|
||||
config "github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
viper "github.com/spf13/viper"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface.
|
||||
|
||||
@@ -20,12 +20,13 @@ 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)
|
||||
UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) 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, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error)
|
||||
|
||||
SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo) error
|
||||
SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error
|
||||
|
||||
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
|
||||
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Deprecated: m20220509170100.Device is deprecated, only used by db migrations
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
@@ -14,9 +15,9 @@ 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"`
|
||||
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"`
|
||||
@@ -38,4 +39,3 @@ type Device struct {
|
||||
// Data set by Scrutiny
|
||||
DeviceStatus pkg.DeviceStatus `json:"device_status"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package m20250221084400
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
Archived bool `json:"archived"`
|
||||
//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"`
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
models "github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
collector "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
measurements "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockDeviceRepo is a mock of DeviceRepo interface.
|
||||
@@ -52,6 +52,20 @@ func (mr *MockDeviceRepoMockRecorder) Close() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDeviceRepo)(nil).Close))
|
||||
}
|
||||
|
||||
// UpdateDeviceArchived mocks base method.
|
||||
func (m *MockDeviceRepo) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateDeviceArchived", ctx, wwn)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateDeviceArchived indicates an expected call of UpdateDeviceArchived.
|
||||
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceArchived(ctx, wwn, archived interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceArchived", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceArchived), ctx, wwn, archived)
|
||||
}
|
||||
|
||||
// DeleteDevice mocks base method.
|
||||
func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, wwn string) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -214,17 +228,17 @@ func (mr *MockDeviceRepoMockRecorder) SaveSmartAttributes(ctx, wwn, collectorSma
|
||||
}
|
||||
|
||||
// SaveSmartTemperature mocks base method.
|
||||
func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, wwn, deviceProtocol string, collectorSmartData collector.SmartInfo) error {
|
||||
func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, wwn, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveSmartTemperature", ctx, wwn, deviceProtocol, collectorSmartData)
|
||||
ret := m.ctrl.Call(m, "SaveSmartTemperature", ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveSmartTemperature indicates an expected call of SaveSmartTemperature.
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, wwn, deviceProtocol, collectorSmartData interface{}) *gomock.Call {
|
||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, wwn, deviceProtocol, collectorSmartData)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
||||
}
|
||||
|
||||
// UpdateDevice mocks base method.
|
||||
|
||||
@@ -5,6 +5,11 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/glebarez/sqlite"
|
||||
@@ -13,10 +18,6 @@ import (
|
||||
"github.com/influxdata/influxdb-client-go/v2/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -82,7 +83,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to connect to database! - %v", err)
|
||||
return nil, fmt.Errorf("failed to connect to database! - %v", err)
|
||||
}
|
||||
globalLogger.Infof("Successfully connected to scrutiny sqlite db: %s\n", appConfig.GetString("web.database.location"))
|
||||
|
||||
@@ -146,7 +147,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
||||
taskAPI := client.TasksAPI()
|
||||
|
||||
if writeAPI == nil || queryAPI == nil || taskAPI == nil {
|
||||
return nil, fmt.Errorf("Failed to connect to influxdb!")
|
||||
return nil, fmt.Errorf("failed to connect to influxdb")
|
||||
}
|
||||
|
||||
deviceRepo := scrutinyRepository{
|
||||
@@ -238,13 +239,13 @@ func InfluxSetupComplete(influxEndpoint string, tlsConfig *tls.Config) (bool, er
|
||||
return false, err
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
res, err := client.Get(influxUri.String())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -3,18 +3,19 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Device
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//insert device into DB (and update specified columns if device is already registered)
|
||||
// insert device into DB (and update specified columns if device is already registered)
|
||||
// update device fields that may change: (DeviceType, HostID)
|
||||
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
|
||||
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
@@ -31,7 +32,7 @@ func (sr *scrutinyRepository) GetDevices(ctx context.Context) ([]models.Device,
|
||||
//Get a list of all the active devices.
|
||||
devices := []models.Device{}
|
||||
if err := sr.gormClient.WithContext(ctx).Find(&devices).Error; err != nil {
|
||||
return nil, fmt.Errorf("Could not get device summary from DB: %v", err)
|
||||
return nil, fmt.Errorf("could not get device summary from DB: %v", err)
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
@@ -40,7 +41,7 @@ func (sr *scrutinyRepository) GetDevices(ctx context.Context) ([]models.Device,
|
||||
func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) {
|
||||
var device models.Device
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
return device, fmt.Errorf("Could not get device from DB: %v", err)
|
||||
return device, fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
//TODO catch GormClient err
|
||||
@@ -51,11 +52,11 @@ func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, coll
|
||||
return device, sr.gormClient.Model(&device).Updates(device).Error
|
||||
}
|
||||
|
||||
//Update Device Status
|
||||
// Update Device Status
|
||||
func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
|
||||
var device models.Device
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
return device, fmt.Errorf("Could not get device from DB: %v", err)
|
||||
return device, fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
device.DeviceStatus = pkg.DeviceStatusSet(device.DeviceStatus, status)
|
||||
@@ -74,6 +75,16 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string)
|
||||
return device, nil
|
||||
}
|
||||
|
||||
// Update Device Archived State
|
||||
func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error {
|
||||
var device models.Device
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||
return fmt.Errorf("could not get device from DB: %v", err)
|
||||
}
|
||||
|
||||
return sr.gormClient.Model(&device).Where("wwn = ?", wwn).Update("archived", archived).Error
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) error {
|
||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).Delete(&models.Device{}).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -194,6 +194,9 @@ func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durati
|
||||
`|> filter(fn: (r) => r["_measurement"] == "smart" )`,
|
||||
fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn),
|
||||
}
|
||||
|
||||
partialQueryStr = append(partialQueryStr, `|> aggregateWindow(every: 1d, fn: last, createEmpty: false)`)
|
||||
|
||||
if selectEntries > 0 {
|
||||
partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, selectEntries, selectEntriesOffset))
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220716214900"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20250221084400"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
@@ -332,7 +333,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "smooth",
|
||||
},
|
||||
|
||||
{
|
||||
SettingKeyName: "metrics.notify_level",
|
||||
SettingKeyDescription: "Determines which device status will cause a notification (fail or warn)",
|
||||
@@ -385,6 +385,45 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
return tx.Create(&defaultSettings).Error
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "m20240722082740", // add powered_on_hours_unit setting.
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
//add powered_on_hours_unit setting default.
|
||||
var defaultSettings = []m20220716214900.Setting{
|
||||
{
|
||||
SettingKeyName: "powered_on_hours_unit",
|
||||
SettingKeyDescription: "Presentation format for device powered on time ('humanize' | 'device_hours')",
|
||||
SettingDataType: "string",
|
||||
SettingValueString: "humanize",
|
||||
},
|
||||
}
|
||||
return tx.Create(&defaultSettings).Error
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "m20250221084400", // add archived to device data
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
//migrate the device database.
|
||||
// adding column (archived)
|
||||
return tx.AutoMigrate(m20250221084400.Device{})
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "m20260105083200", // add discard_sct_temp_history setting.
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
//add discard_sct_temp_history setting default.
|
||||
var defaultSettings = []m20220716214900.Setting{
|
||||
{
|
||||
SettingKeyName: "collector.discard_sct_temp_history",
|
||||
SettingKeyDescription: "Whether to discard SCT Temperature history (true | false)",
|
||||
SettingDataType: "bool",
|
||||
SettingValueBool: false,
|
||||
},
|
||||
}
|
||||
return tx.Create(&defaultSettings).Error
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := m.Migrate(); err != nil {
|
||||
@@ -421,8 +460,8 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
|
||||
// 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.
|
||||
// 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) {
|
||||
@@ -608,7 +647,7 @@ func m20201107210306_FromPreInfluxDBSmartResultsCreatePostInfluxDBSmartResults(d
|
||||
}
|
||||
postDeviceSmartData.ProcessScsiSmartInfo(postScsiGrownDefectList, postScsiErrorCounterLog)
|
||||
} else {
|
||||
return fmt.Errorf("Unknown device protocol: %s", preDevice.DeviceProtocol), postDeviceSmartData
|
||||
return fmt.Errorf("unknown device protocol: %s", preDevice.DeviceProtocol), postDeviceSmartData
|
||||
}
|
||||
|
||||
return nil, postDeviceSmartData
|
||||
|
||||
@@ -3,17 +3,18 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"strings"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
)
|
||||
|
||||
// LoadSettings will retrieve settings from the database, store them in the AppConfig object, and return a Settings struct
|
||||
func (sr *scrutinyRepository) LoadSettings(ctx context.Context) (*models.Settings, error) {
|
||||
settingsEntries := []models.SettingEntry{}
|
||||
if err := sr.gormClient.WithContext(ctx).Find(&settingsEntries).Error; err != nil {
|
||||
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
return nil, fmt.Errorf("could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
// store retrieved settings in the AppConfig obj
|
||||
@@ -58,7 +59,7 @@ func (sr *scrutinyRepository) SaveSettings(ctx context.Context, settings models.
|
||||
//retrieve current settings from the database
|
||||
settingsEntries := []models.SettingEntry{}
|
||||
if err := sr.gormClient.WithContext(ctx).Find(&settingsEntries).Error; err != nil {
|
||||
return fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
return fmt.Errorf("could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
//update settingsEntries
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func Test_DownsampleScript_Weekly(t *testing.T) {
|
||||
@@ -12,7 +13,6 @@ func Test_DownsampleScript_Weekly(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
@@ -64,7 +64,6 @@ func Test_DownsampleScript_Monthly(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
@@ -116,7 +115,6 @@ func Test_DownsampleScript_Yearly(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
@@ -3,18 +3,19 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Temperature Data
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo) error {
|
||||
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 {
|
||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
||||
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 && !discardSCTTempHistory {
|
||||
|
||||
for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table {
|
||||
//temp value may be null, we must skip/ignore them. See #393
|
||||
@@ -22,9 +23,11 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
|
||||
continue
|
||||
}
|
||||
|
||||
minutesOffset := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * int64(ndx) * 60
|
||||
intervalSec := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * 60
|
||||
datapointTime := collectorSmartData.LocalTime.TimeT - int64(ndx) * intervalSec
|
||||
alignedDatapointTime := datapointTime - datapointTime % intervalSec
|
||||
smartTemp := measurements.SmartTemperature{
|
||||
Date: time.Unix(collectorSmartData.LocalTime.TimeT-minutesOffset, 0),
|
||||
Date: time.Unix(alignedDatapointTime, 0),
|
||||
Temp: temp,
|
||||
}
|
||||
|
||||
@@ -39,23 +42,22 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
|
||||
return err
|
||||
}
|
||||
}
|
||||
// also add the current temperature.
|
||||
} else {
|
||||
|
||||
smartTemp := measurements.SmartTemperature{
|
||||
Date: time.Unix(collectorSmartData.LocalTime.TimeT, 0),
|
||||
Temp: collectorSmartData.Temperature.Current,
|
||||
}
|
||||
|
||||
tags, fields := smartTemp.Flatten()
|
||||
tags["device_wwn"] = wwn
|
||||
p := influxdb2.NewPoint("temp",
|
||||
tags,
|
||||
fields,
|
||||
smartTemp.Date)
|
||||
return sr.influxWriteApi.WritePoint(ctx, p)
|
||||
}
|
||||
return nil
|
||||
|
||||
|
||||
// Even if ata_sct_temperature_history is present, also add current temperature. See #824
|
||||
smartTemp := measurements.SmartTemperature{
|
||||
Date: time.Unix(collectorSmartData.LocalTime.TimeT, 0),
|
||||
Temp: collectorSmartData.Temperature.Current,
|
||||
}
|
||||
|
||||
tags, fields := smartTemp.Flatten()
|
||||
tags["device_wwn"] = wwn
|
||||
p := influxdb2.NewPoint("temp",
|
||||
tags,
|
||||
fields,
|
||||
smartTemp.Date)
|
||||
return sr.influxWriteApi.WritePoint(ctx, p)
|
||||
}
|
||||
|
||||
func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
|
||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func Test_aggregateTempQuery_Week(t *testing.T) {
|
||||
@@ -12,7 +13,6 @@ func Test_aggregateTempQuery_Week(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
@@ -45,7 +45,6 @@ func Test_aggregateTempQuery_Month(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
@@ -86,7 +85,6 @@ func Test_aggregateTempQuery_Year(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
@@ -134,7 +132,6 @@ func Test_aggregateTempQuery_Forever(t *testing.T) {
|
||||
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
|
||||
@@ -14,6 +14,7 @@ type DeviceWrapper struct {
|
||||
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
Archived bool `json:"archived"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
@@ -2,13 +2,14 @@ package measurements
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||
)
|
||||
|
||||
type Smart struct {
|
||||
@@ -64,11 +65,27 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
|
||||
for key, val := range attrs {
|
||||
switch key {
|
||||
case "temp":
|
||||
sm.Temp = val.(int64)
|
||||
temp, tempOk := val.(int64)
|
||||
if tempOk {
|
||||
sm.Temp = temp
|
||||
} else {
|
||||
log.Printf("unable to parse temp information: %v", val)
|
||||
}
|
||||
|
||||
case "power_on_hours":
|
||||
sm.PowerOnHours = val.(int64)
|
||||
powerOn, powerOnOk := val.(int64)
|
||||
if powerOnOk {
|
||||
sm.PowerOnHours = powerOn
|
||||
} else {
|
||||
log.Printf("unable to parse power_on_hours information: %v", val)
|
||||
}
|
||||
case "power_cycle_count":
|
||||
sm.PowerCycleCount = val.(int64)
|
||||
powerCycle, powerCycleOk := val.(int64)
|
||||
if powerCycleOk {
|
||||
sm.PowerCycleCount = powerCycle
|
||||
} else {
|
||||
log.Printf("unable to parse power_cycle_count information: %v", val)
|
||||
}
|
||||
default:
|
||||
// this key is unknown.
|
||||
if !strings.HasPrefix(key, "attr.") {
|
||||
@@ -86,7 +103,7 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
|
||||
} else if sm.DeviceProtocol == pkg.DeviceProtocolScsi {
|
||||
sm.Attributes[attributeId] = &SmartScsiAttribute{}
|
||||
} else {
|
||||
return nil, fmt.Errorf("Unknown Device Protocol: %s", sm.DeviceProtocol)
|
||||
return nil, fmt.Errorf("unknown Device Protocol: %s", sm.DeviceProtocol)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +117,7 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
|
||||
return &sm, nil
|
||||
}
|
||||
|
||||
//Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries)
|
||||
// Parse Collector SMART data results and create Smart object (and associated SmartAtaAttribute entries)
|
||||
func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) error {
|
||||
sm.DeviceWWN = wwn
|
||||
sm.Date = time.Unix(info.LocalTime.TimeT, 0)
|
||||
@@ -127,7 +144,7 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er
|
||||
return nil
|
||||
}
|
||||
|
||||
//generate SmartAtaAttribute entries from Scrutiny Collector Smart data.
|
||||
// generate SmartAtaAttribute entries from Scrutiny Collector Smart data.
|
||||
func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTableItem) {
|
||||
for _, collectorAttr := range tableItems {
|
||||
attrModel := SmartAtaAttribute{
|
||||
@@ -155,7 +172,7 @@ func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTa
|
||||
}
|
||||
}
|
||||
|
||||
//generate SmartNvmeAttribute entries from Scrutiny Collector Smart data.
|
||||
// generate SmartNvmeAttribute entries from Scrutiny Collector Smart data.
|
||||
func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.NvmeSmartHealthInformationLog) {
|
||||
|
||||
sm.Attributes = map[string]SmartAttribute{
|
||||
@@ -185,7 +202,7 @@ func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.Nv
|
||||
}
|
||||
}
|
||||
|
||||
//generate SmartScsiAttribute entries from Scrutiny Collector Smart data.
|
||||
// generate SmartScsiAttribute entries from Scrutiny Collector Smart data.
|
||||
func (sm *Smart) ProcessScsiSmartInfo(defectGrownList int64, scsiErrorCounterLog collector.ScsiErrorCounterLog) {
|
||||
sm.Attributes = map[string]SmartAttribute{
|
||||
"scsi_grown_defect_list": (&SmartScsiAttribute{AttributeId: "scsi_grown_defect_list", Value: defectGrownList, Threshold: 0}).PopulateAttributeStatus(),
|
||||
|
||||
@@ -91,7 +91,7 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
//populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
// populate attribute status, using SMART Thresholds & Observed Metadata
|
||||
// Chainable
|
||||
func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttribute {
|
||||
if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedFailingNow {
|
||||
@@ -165,6 +165,4 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
|
||||
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
|
||||
sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ package measurements_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSmart_Flatten(t *testing.T) {
|
||||
@@ -312,7 +313,7 @@ func TestFromCollectorSmartInfo(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -344,7 +345,7 @@ func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -368,7 +369,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinySmart(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -392,7 +393,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -425,7 +426,7 @@ func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -456,7 +457,7 @@ func TestFromCollectorSmartInfo_Nvme(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
@@ -483,7 +484,7 @@ func TestFromCollectorSmartInfo_Scsi(t *testing.T) {
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
smartDataBytes, err := io.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -8,13 +8,18 @@ package models
|
||||
//}
|
||||
|
||||
type Settings struct {
|
||||
Theme string `json:"theme" mapstructure:"theme"`
|
||||
Layout string `json:"layout" mapstructure:"layout"`
|
||||
DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
|
||||
DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
|
||||
TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
|
||||
FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
|
||||
LineStroke string `json:"line_stroke" mapstructure:"line_stroke"`
|
||||
Theme string `json:"theme" mapstructure:"theme"`
|
||||
Layout string `json:"layout" mapstructure:"layout"`
|
||||
DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
|
||||
DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
|
||||
TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
|
||||
FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
|
||||
LineStroke string `json:"line_stroke" mapstructure:"line_stroke"`
|
||||
PoweredOnHoursUnit string `json:"powered_on_hours_unit" mapstructure:"powered_on_hours_unit"`
|
||||
|
||||
Collector struct {
|
||||
DiscardSCTTempHistory bool `json:"discard_sct_temp_history" mapstructure:"discard_sct_temp_history"`
|
||||
} `json:"collector" mapstructure:"collector"`
|
||||
|
||||
Metrics struct {
|
||||
NotifyLevel int `json:"notify_level" mapstructure:"notify_level"`
|
||||
|
||||
+2
-3
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -68,7 +67,7 @@ func SendPostRequest(url string, file io.Reader) ([]byte, error) {
|
||||
|
||||
log.Printf("%v\n", response.Status)
|
||||
|
||||
return ioutil.ReadAll(response.Body)
|
||||
return io.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
// InfluxDB will throw an error/ignore any submitted data with a timestamp older than the
|
||||
@@ -79,7 +78,7 @@ func readSmartDataFileFixTimestamp(daysToSubtract int, smartDataFilepath string)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricsFileData, err := ioutil.ReadAll(metricsfile)
|
||||
metricsFileData, err := io.ReadAll(metricsfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs me
|
||||
var failingAttributes []string
|
||||
// Loop through the attributes to find the failing ones
|
||||
for attrId, attrData := range smartAttrs.Attributes {
|
||||
var status pkg.AttributeStatus = attrData.GetStatus()
|
||||
var status = attrData.GetStatus()
|
||||
// Skip over passing attributes
|
||||
if status == pkg.AttributeStatusPassed {
|
||||
continue
|
||||
@@ -147,7 +147,7 @@ func NewPayload(device models.Device, test bool, currentTime ...time.Time) Paylo
|
||||
|
||||
//validate that the Payload is populated
|
||||
var sendDate time.Time
|
||||
if currentTime != nil && len(currentTime) > 0 {
|
||||
if len(currentTime) > 0 {
|
||||
sendDate = currentTime[0]
|
||||
} else {
|
||||
sendDate = time.Now()
|
||||
@@ -318,7 +318,7 @@ func (n *Notify) SendScriptNotification(scriptUrl string) error {
|
||||
|
||||
if !utils.FileExists(scriptPath) {
|
||||
n.Logger.Errorf("Script does not exist: %s", scriptPath)
|
||||
return errors.New(fmt.Sprintf("custom script path does not exist: %s", scriptPath))
|
||||
return fmt.Errorf("custom script path does not exist: %s", scriptPath)
|
||||
}
|
||||
|
||||
copyEnv := os.Environ()
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
||||
@@ -28,7 +28,6 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
//assert
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
@@ -44,7 +43,6 @@ func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
//assert
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
@@ -60,7 +58,6 @@ func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.
|
||||
statusThreshold := pkg.MetricsStatusThresholdSmart
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
//assert
|
||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
@@ -76,7 +73,6 @@ func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testi
|
||||
statusThreshold := pkg.MetricsStatusThresholdScrutiny
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
//assert
|
||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
||||
@@ -96,7 +92,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
|
||||
//assert
|
||||
@@ -120,7 +115,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCritical
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
|
||||
//assert
|
||||
@@ -141,7 +135,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
|
||||
//assert
|
||||
@@ -162,7 +155,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCritica
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
|
||||
//assert
|
||||
@@ -186,7 +178,6 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho
|
||||
statusThreshold := pkg.MetricsStatusThresholdSmart
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
|
||||
//assert
|
||||
@@ -206,7 +197,6 @@ func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, errors.New("")).Times(1)
|
||||
|
||||
@@ -228,7 +218,6 @@ func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, nil).Times(1)
|
||||
|
||||
@@ -250,7 +239,6 @@ func TestShouldNotify_NoRepeat(t *testing.T) {
|
||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
||||
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{smartAttrs}, nil).Times(1)
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@ package version
|
||||
|
||||
// VERSION is the app-global version string, which will be replaced with a
|
||||
// new value during packaging
|
||||
const VERSION = "0.7.3"
|
||||
const VERSION = "0.8.6"
|
||||
|
||||
@@ -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 ArchiveDevice(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), true)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while archiving device", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
@@ -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 UnarchiveDevice(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), false)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while unarchiving device", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func UploadDeviceMetrics(c *gin.Context) {
|
||||
}
|
||||
|
||||
// save smart temperature data (ignore failures)
|
||||
err = deviceRepo.SaveSmartTemperature(c, c.Param("wwn"), updatedDevice.DeviceProtocol, collectorSmartData)
|
||||
err = deviceRepo.SaveSmartTemperature(c, c.Param("wwn"), updatedDevice.DeviceProtocol, collectorSmartData, appConfig.GetBool(fmt.Sprintf("%s.collector.discard_sct_temp_history", config.DB_USER_SETTINGS_SUBKEY)))
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while saving smartctl temp data", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
|
||||
@@ -3,15 +3,15 @@ package middleware
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Middleware based on https://github.com/toorop/gin-logrus/blob/master/logger.go
|
||||
@@ -40,9 +40,9 @@ func LoggerMiddleware(logger *logrus.Entry) gin.HandlerFunc {
|
||||
//clone the request body reader.
|
||||
var reqBody string
|
||||
if c.Request.Body != nil {
|
||||
buf, _ := ioutil.ReadAll(c.Request.Body)
|
||||
reqBodyReader1 := ioutil.NopCloser(bytes.NewBuffer(buf))
|
||||
reqBodyReader2 := ioutil.NopCloser(bytes.NewBuffer(buf)) //We have to create a new Buffer, because reqBodyReader1 will be read.
|
||||
buf, _ := io.ReadAll(c.Request.Body)
|
||||
reqBodyReader1 := io.NopCloser(bytes.NewBuffer(buf))
|
||||
reqBodyReader2 := io.NopCloser(bytes.NewBuffer(buf)) //We have to create a new Buffer, because reqBodyReader1 will be read.
|
||||
c.Request.Body = reqBodyReader2
|
||||
reqBody = readBody(reqBodyReader1)
|
||||
}
|
||||
|
||||
@@ -42,8 +42,10 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
|
||||
api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown)
|
||||
api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data
|
||||
api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests)
|
||||
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
|
||||
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
|
||||
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
|
||||
api.POST("/device/:wwn/archive", handler.ArchiveDevice) //used by UI to archive device
|
||||
api.POST("/device/:wwn/unarchive", handler.UnarchiveDevice) //used by UI to unarchive device
|
||||
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
|
||||
|
||||
api.GET("/settings", handler.GetSettings) //used to get settings
|
||||
api.POST("/settings", handler.SaveSettings) //used to save settings
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -20,10 +19,10 @@ import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -52,7 +51,7 @@ func helperReadSmartDataFileFixTimestamp(t *testing.T, smartDataFilepath string)
|
||||
metricsfile, err := os.Open(smartDataFilepath)
|
||||
require.NoError(t, err)
|
||||
|
||||
metricsFileData, err := ioutil.ReadAll(metricsfile)
|
||||
metricsFileData, err := io.ReadAll(metricsfile)
|
||||
require.NoError(t, err)
|
||||
|
||||
//unmarshal because we need to change the timestamp
|
||||
@@ -87,10 +86,9 @@ func TestServerTestSuite_WithCustomBasePath(t *testing.T) {
|
||||
|
||||
func (suite *ServerTestSuite) TestHealthRoute() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -131,10 +129,9 @@ func (suite *ServerTestSuite) TestHealthRoute() {
|
||||
|
||||
func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -174,10 +171,9 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
||||
|
||||
func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -191,6 +187,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("user.collector.discard_sct_temp_history").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
@@ -228,10 +225,9 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
|
||||
func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -250,6 +246,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("user.collector.discard_sct_temp_history").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||
@@ -309,10 +306,9 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
//TODO: this test should use a recorded request/response playback.
|
||||
//func TestSendTestNotificationRoute(t *testing.T) {
|
||||
// //setup
|
||||
// parentPath, _ := ioutil.TempDir("", "")
|
||||
// parentPath, _ := os.MkdirTemp("", "")
|
||||
// defer os.RemoveAll(parentPath)
|
||||
// mockCtrl := gomock.NewController(t)
|
||||
// defer mockCtrl.Finish()
|
||||
// fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
// fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||
// fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||
@@ -333,10 +329,9 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
|
||||
func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -379,10 +374,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
|
||||
func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -425,10 +419,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
|
||||
func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -471,10 +464,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
|
||||
func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -516,10 +508,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
|
||||
func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
//setup
|
||||
parentPath, _ := ioutil.TempDir("", "")
|
||||
parentPath, _ := os.MkdirTemp("", "")
|
||||
defer os.RemoveAll(parentPath)
|
||||
mockCtrl := gomock.NewController(suite.T())
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||
@@ -533,6 +524,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("user.collector.discard_sct_temp_history").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{})
|
||||
|
||||
Generated
+395
-163
@@ -24,7 +24,7 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"highlight.js": "^11.6.0",
|
||||
"humanize-duration": "^3.27.3",
|
||||
"lodash": "4.17.21",
|
||||
"lodash": "4.17.23",
|
||||
"moment": "^2.29.4",
|
||||
"ng-apexcharts": "^1.7.4",
|
||||
"ngx-markdown": "^13.1.0",
|
||||
@@ -3993,23 +3993,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.2",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.14.0",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
@@ -4200,6 +4201,7 @@
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -4263,6 +4265,36 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -5572,6 +5604,20 @@
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -5741,12 +5787,42 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
|
||||
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
@@ -6207,6 +6283,7 @@
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -6265,45 +6342,50 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"body-parser": "~1.20.3",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"qs": "~6.14.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/array-flatten": {
|
||||
@@ -6312,35 +6394,12 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/express/node_modules/body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -6350,22 +6409,34 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"node_modules/express/node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "2.0.1",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6376,28 +6447,15 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/express/node_modules/raw-body": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -6599,9 +6657,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -6609,6 +6667,7 @@
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
@@ -6668,6 +6727,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -6725,9 +6785,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/functions-have-names": {
|
||||
"version": "1.2.3",
|
||||
@@ -6775,13 +6839,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
|
||||
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.3"
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -6796,6 +6871,19 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -6884,6 +6972,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -6945,6 +7045,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
@@ -6994,9 +7095,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -7024,6 +7126,18 @@
|
||||
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hdr-histogram-js": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz",
|
||||
@@ -7146,26 +7260,32 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -8020,10 +8140,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
@@ -8576,9 +8697,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
@@ -8809,6 +8931,15 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -8831,10 +8962,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
|
||||
"dev": true
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
@@ -9272,10 +9407,11 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
|
||||
"integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
|
||||
"dev": true,
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
@@ -9747,10 +9883,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -10205,10 +10345,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
|
||||
"dev": true
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
@@ -11353,12 +11494,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -11449,15 +11591,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -12144,24 +12287,25 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -12172,6 +12316,7 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@@ -12180,13 +12325,25 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
@@ -12198,13 +12355,15 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -12288,20 +12447,31 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@@ -12354,14 +12524,76 @@
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"highlight.js": "^11.6.0",
|
||||
"humanize-duration": "^3.27.3",
|
||||
"lodash": "4.17.21",
|
||||
"lodash": "4.17.23",
|
||||
"moment": "^2.29.4",
|
||||
"ng-apexcharts": "^1.7.4",
|
||||
"ngx-markdown": "^13.1.0",
|
||||
|
||||
@@ -12,6 +12,8 @@ export type TemperatureUnit = 'celsius' | 'fahrenheit'
|
||||
|
||||
export type LineStroke = 'smooth' | 'straight' | 'stepline'
|
||||
|
||||
export type DevicePoweredOnUnit = 'humanize' | 'device_hours'
|
||||
|
||||
|
||||
export enum MetricsNotifyLevel {
|
||||
Warn = 1,
|
||||
@@ -47,9 +49,15 @@ export interface AppConfig {
|
||||
|
||||
file_size_si_units?: boolean;
|
||||
|
||||
powered_on_hours_unit?: DevicePoweredOnUnit;
|
||||
|
||||
line_stroke?: LineStroke;
|
||||
|
||||
// Settings from Scrutiny API
|
||||
|
||||
collector?: {
|
||||
discard_sct_temp_history?: boolean
|
||||
}
|
||||
|
||||
metrics?: {
|
||||
notify_level?: MetricsNotifyLevel
|
||||
@@ -77,8 +85,13 @@ export const appConfig: AppConfig = {
|
||||
|
||||
temperature_unit: 'celsius',
|
||||
file_size_si_units: false,
|
||||
powered_on_hours_unit: 'humanize',
|
||||
|
||||
line_stroke: 'smooth',
|
||||
|
||||
collector: {
|
||||
discard_sct_temp_history : false,
|
||||
},
|
||||
|
||||
metrics: {
|
||||
notify_level: MetricsNotifyLevel.Fail,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// maps to webapp/backend/pkg/models/device.go
|
||||
export interface DeviceModel {
|
||||
archived?: boolean;
|
||||
wwn: string;
|
||||
device_name?: string;
|
||||
device_uuid?: string;
|
||||
|
||||
@@ -20,7 +20,8 @@ export const sda = {
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
'device_status': 0,
|
||||
'archived': false
|
||||
},
|
||||
'smart_results': [{
|
||||
'date': '2021-10-24T23:20:44Z',
|
||||
|
||||
@@ -28,7 +28,8 @@ export const summary = {
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
'device_status': 0,
|
||||
'archived': false
|
||||
}
|
||||
},
|
||||
'0x5000cca252c859cc': {
|
||||
@@ -55,7 +56,8 @@ export const summary = {
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
'device_status': 0,
|
||||
'archived': false
|
||||
},
|
||||
'smart': {
|
||||
'collector_date': '2020-08-21T22:27:02Z',
|
||||
@@ -91,7 +93,8 @@ export const summary = {
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
'device_status': 0,
|
||||
'archived': false
|
||||
},
|
||||
'smart': {
|
||||
'collector_date': '2020-06-21T00:03:30Z',
|
||||
@@ -127,7 +130,8 @@ export const summary = {
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
'device_status': 0,
|
||||
'archived': false
|
||||
}
|
||||
},
|
||||
'0x5000cca264ec3183': {
|
||||
@@ -154,7 +158,8 @@ export const summary = {
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': 'custom host id',
|
||||
'device_status': 1
|
||||
'device_status': 1,
|
||||
'archived': false
|
||||
},
|
||||
'smart': {
|
||||
'collector_date': '2020-09-13T16:29:23Z',
|
||||
@@ -574,7 +579,8 @@ export const summary = {
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
'device_status': 0,
|
||||
'archived': false
|
||||
}
|
||||
},
|
||||
'0x5002538e40a22954': {
|
||||
@@ -601,7 +607,8 @@ export const summary = {
|
||||
'device_type': '',
|
||||
'label': '',
|
||||
'host_id': '',
|
||||
'device_status': 0
|
||||
'device_status': 0,
|
||||
'archived': false
|
||||
},
|
||||
'smart': {
|
||||
'collector_date': '2020-06-10T12:01:02Z',
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<h2 mat-dialog-title>Archive {{data.title}}?</h2>
|
||||
<mat-dialog-content>This will remove the device from Scrutiny dashboard, unless you toggle show archived. <strong>Any data about the device
|
||||
itself will remain untouched.</strong></mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button class="yellow-600" mat-button (click)="onArchiveClick()">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'archive'"></mat-icon>
|
||||
Archive
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {DashboardDeviceArchiveDialogComponent} from './dashboard-device-archive-dialog.component';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {SharedModule} from '../../../shared/shared.module';
|
||||
import {DashboardDeviceArchiveDialogService} from './dashboard-device-archive-dialog.service';
|
||||
import {of} from 'rxjs';
|
||||
|
||||
|
||||
describe('DashboardDeviceArchiveDialogComponent', () => {
|
||||
let component: DashboardDeviceArchiveDialogComponent;
|
||||
let fixture: ComponentFixture<DashboardDeviceArchiveDialogComponent>;
|
||||
|
||||
const matDialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['closeDialog', 'close']);
|
||||
const dashboardDeviceArchiveDialogServiceSpy = jasmine.createSpyObj('DashboardDeviceArchiveDialogService', ['archiveDevice']);
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
MatDialogModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
SharedModule,
|
||||
],
|
||||
providers: [
|
||||
{provide: MatDialogRef, useValue: matDialogRefSpy},
|
||||
{provide: MAT_DIALOG_DATA, useValue: {wwn: 'test-wwn', title: 'my-test-device-title'}},
|
||||
{provide: DashboardDeviceArchiveDialogService, useValue: dashboardDeviceArchiveDialogServiceSpy}
|
||||
],
|
||||
declarations: [DashboardDeviceArchiveDialogComponent]
|
||||
})
|
||||
.compileComponents()
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardDeviceArchiveDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should close the component if cancel is clicked', () => {
|
||||
matDialogRefSpy.closeDialog.calls.reset();
|
||||
matDialogRefSpy.closeDialog()
|
||||
expect(matDialogRefSpy.closeDialog).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should attempt to archive device if archive is clicked', () => {
|
||||
dashboardDeviceArchiveDialogServiceSpy.archiveDevice.and.returnValue(of({'success': true}));
|
||||
|
||||
component.onArchiveClick()
|
||||
expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice).toHaveBeenCalledWith('test-wwn');
|
||||
expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice.calls.count())
|
||||
.withContext('one call')
|
||||
.toBe(1);
|
||||
});
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
||||
import {DashboardDeviceArchiveDialogService} from 'app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-device-archive-dialog',
|
||||
templateUrl: './dashboard-device-archive-dialog.component.html',
|
||||
styleUrls: ['./dashboard-device-archive-dialog.component.scss'],
|
||||
})
|
||||
export class DashboardDeviceArchiveDialogComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DashboardDeviceArchiveDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string},
|
||||
private _archiveService: DashboardDeviceArchiveDialogService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
onArchiveClick(): void {
|
||||
this._archiveService.archiveDevice(this.data.wwn)
|
||||
.subscribe((data) => {
|
||||
this.dialogRef.close(data);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {SharedModule} from 'app/shared/shared.module';
|
||||
import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing';
|
||||
import {MatDialogModule} from '@angular/material/dialog';
|
||||
import {DashboardDeviceArchiveDialogComponent} from './dashboard-device-archive-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardDeviceArchiveDialogComponent
|
||||
],
|
||||
imports: [
|
||||
RouterModule.forChild([]),
|
||||
RouterModule.forChild(dashboardRoutes),
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
SharedModule,
|
||||
MatDialogModule
|
||||
],
|
||||
exports : [
|
||||
DashboardDeviceArchiveDialogComponent,
|
||||
],
|
||||
providers : []
|
||||
})
|
||||
export class DashboardDeviceArchiveDialogModule
|
||||
{
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Observable} from 'rxjs';
|
||||
import {getBasePath} from 'app/app.routing';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DashboardDeviceArchiveDialogService
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {HttpClient} _httpClient
|
||||
*/
|
||||
constructor(
|
||||
private _httpClient: HttpClient
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
archiveDevice(wwn: string): Observable<any>
|
||||
{
|
||||
return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/archive`, {});
|
||||
}
|
||||
|
||||
unarchiveDevice(wwn: string): Observable<any>
|
||||
{
|
||||
return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/unarchive`, {});
|
||||
}
|
||||
}
|
||||
+15
-3
@@ -1,5 +1,7 @@
|
||||
<div [ngClass]="{ 'border-green': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed',
|
||||
'border-red': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed' }"
|
||||
<div
|
||||
[ngClass]="{ 'border-green': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed',
|
||||
'border-red': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed',
|
||||
'text-disabled': deviceSummary.device.archived }"
|
||||
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"
|
||||
@@ -21,6 +23,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="deviceSummary.device">
|
||||
<mat-icon *ngIf="deviceSummary.device.archived"
|
||||
[svgIcon]="'archive'"></mat-icon>
|
||||
<button mat-icon-button
|
||||
[matMenuTriggerFor]="previousStatementMenu">
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
@@ -33,6 +37,14 @@
|
||||
<span>View Details</span>
|
||||
</span>
|
||||
</a>
|
||||
<a mat-menu-item
|
||||
(click)="openArchiveDialog()">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="deviceSummary.device.archived ? 'unarchive':'archive'"></mat-icon>
|
||||
<span>{{deviceSummary.device.archived ? "Unarchive" : "Archive"}} Device</span>
|
||||
</span>
|
||||
</a>
|
||||
<a mat-menu-item (click)="openDeleteDialog()">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
@@ -63,7 +75,7 @@
|
||||
</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>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.power_on_hours; else unknownPoweredOn">{{ deviceSummary.smart?.power_on_hours | deviceHours:config.powered_on_hours_unit:{ 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>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.text-disabled{
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
+29
-6
@@ -4,12 +4,13 @@ import {takeUntil} from 'rxjs/operators';
|
||||
import {AppConfig} from 'app/core/config/app.config';
|
||||
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
|
||||
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';
|
||||
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
|
||||
import {DeviceStatusPipe} from 'app/shared/device-status.pipe';
|
||||
import {DashboardDeviceArchiveDialogComponent} from '../dashboard-device-archive-dialog/dashboard-device-archive-dialog.component';
|
||||
import {DashboardDeviceArchiveDialogService} from '../dashboard-device-archive-dialog/dashboard-device-archive-dialog.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-device',
|
||||
@@ -20,6 +21,7 @@ export class DashboardDeviceComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private _configService: ScrutinyConfigService,
|
||||
private _archiveService: DashboardDeviceArchiveDialogService,
|
||||
public dialog: MatDialog,
|
||||
) {
|
||||
// Set the private defaults
|
||||
@@ -27,15 +29,14 @@ export class DashboardDeviceComponent implements OnInit {
|
||||
}
|
||||
|
||||
@Input() deviceSummary: DeviceSummaryModel;
|
||||
@Input() deviceWWN: string;
|
||||
@Output() deviceArchived = new EventEmitter<string>();
|
||||
@Output() deviceUnarchived = new EventEmitter<string>();
|
||||
@Output() deviceDeleted = new EventEmitter<string>();
|
||||
|
||||
config: AppConfig;
|
||||
|
||||
private _unsubscribeAll: Subject<void>;
|
||||
|
||||
readonly humanizeDuration = humanizeDuration;
|
||||
|
||||
deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -72,11 +73,33 @@ export class DashboardDeviceComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
openArchiveDialog(): void {
|
||||
if(this.deviceSummary.device.archived){
|
||||
this._archiveService.unarchiveDevice(this.deviceSummary.device.wwn).subscribe((result) => {
|
||||
if(result) {
|
||||
this.deviceUnarchived.emit(this.deviceSummary.device.wwn)
|
||||
}
|
||||
})
|
||||
return;
|
||||
}
|
||||
const dialogRef = this.dialog.open(DashboardDeviceArchiveDialogComponent, {
|
||||
data: {
|
||||
wwn: this.deviceSummary.device.wwn,
|
||||
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if(result) {
|
||||
this.deviceArchived.emit(this.deviceSummary.device.wwn);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
openDeleteDialog(): void {
|
||||
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
|
||||
// width: '250px',
|
||||
data: {
|
||||
wwn: this.deviceWWN,
|
||||
wwn: this.deviceSummary.device.wwn,
|
||||
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
|
||||
}
|
||||
});
|
||||
@@ -84,7 +107,7 @@ export class DashboardDeviceComponent implements OnInit {
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
console.log('The dialog was closed', result);
|
||||
if (result.success) {
|
||||
this.deviceDeleted.emit(this.deviceWWN)
|
||||
this.deviceDeleted.emit(this.deviceSummary.device.wwn)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashb
|
||||
import {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing';
|
||||
import {MatMenuModule} from '@angular/material/menu';
|
||||
import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module';
|
||||
import {DashboardDeviceArchiveDialogModule} from '../dashboard-device-archive-dialog/dashboard-device-archive-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -19,7 +20,8 @@ import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-dev
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
SharedModule,
|
||||
DashboardDeviceDeleteDialogModule
|
||||
DashboardDeviceDeleteDialogModule,
|
||||
DashboardDeviceArchiveDialogModule
|
||||
],
|
||||
exports: [
|
||||
DashboardDeviceComponent,
|
||||
|
||||
+25
@@ -54,6 +54,14 @@
|
||||
</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>Powered On Format</mat-label>
|
||||
<mat-select [(ngModel)]="poweredOnHoursUnit">
|
||||
<mat-option value="humanize">Humanize</mat-option>
|
||||
<mat-option value="device_hours">Device Hours</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||
<mat-label>Line stroke</mat-label>
|
||||
<mat-select [(ngModel)]="lineStroke">
|
||||
@@ -94,6 +102,23 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 mb-2">
|
||||
<h3 class="text-lg font-medium">Quirks</h3>
|
||||
<div class="w-full border-b mt-1"></div>
|
||||
</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"
|
||||
matTooltip="Discard historical temperature data retrieved from the SMART Command Transport (SCT) Data Table, which may be inaccurate for some drives. The current temperature is always stored."
|
||||
matTooltipPosition="right">
|
||||
<mat-label>Discard SCT Temperature History</mat-label>
|
||||
<mat-select [(ngModel)]=discardSCTTempHistory>
|
||||
<mat-option [value]=true>Enabled</mat-option>
|
||||
<mat-option [value]=false>Disabled</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-dialog-content>
|
||||
|
||||
+11
-1
@@ -7,7 +7,8 @@ import {
|
||||
MetricsStatusThreshold,
|
||||
TemperatureUnit,
|
||||
LineStroke,
|
||||
Theme
|
||||
Theme,
|
||||
DevicePoweredOnUnit
|
||||
} from 'app/core/config/app.config';
|
||||
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
|
||||
import {Subject} from 'rxjs';
|
||||
@@ -24,8 +25,10 @@ export class DashboardSettingsComponent implements OnInit {
|
||||
dashboardSort: string;
|
||||
temperatureUnit: string;
|
||||
fileSizeSIUnits: boolean;
|
||||
poweredOnHoursUnit: string;
|
||||
lineStroke: string;
|
||||
theme: string;
|
||||
discardSCTTempHistory: boolean;
|
||||
statusThreshold: number;
|
||||
statusFilterAttributes: number;
|
||||
repeatNotifications: boolean;
|
||||
@@ -51,9 +54,12 @@ export class DashboardSettingsComponent implements OnInit {
|
||||
this.dashboardSort = config.dashboard_sort;
|
||||
this.temperatureUnit = config.temperature_unit;
|
||||
this.fileSizeSIUnits = config.file_size_si_units;
|
||||
this.poweredOnHoursUnit = config.powered_on_hours_unit;
|
||||
this.lineStroke = config.line_stroke;
|
||||
this.theme = config.theme;
|
||||
|
||||
this.discardSCTTempHistory = config.collector.discard_sct_temp_history;
|
||||
|
||||
this.statusFilterAttributes = config.metrics.status_filter_attributes;
|
||||
this.statusThreshold = config.metrics.status_threshold;
|
||||
this.repeatNotifications = config.metrics.repeat_notifications;
|
||||
@@ -68,8 +74,12 @@ export class DashboardSettingsComponent implements OnInit {
|
||||
dashboard_sort: this.dashboardSort as DashboardSort,
|
||||
temperature_unit: this.temperatureUnit as TemperatureUnit,
|
||||
file_size_si_units: this.fileSizeSIUnits,
|
||||
powered_on_hours_unit: this.poweredOnHoursUnit as DevicePoweredOnUnit,
|
||||
line_stroke: this.lineStroke as LineStroke,
|
||||
theme: this.theme as Theme,
|
||||
collector: {
|
||||
discard_sct_temp_history: this.discardSCTTempHistory
|
||||
},
|
||||
metrics: {
|
||||
status_filter_attributes: this.statusFilterAttributes as MetricsStatusFilterAttributes,
|
||||
status_threshold: this.statusThreshold as MetricsStatusThreshold,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user