Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc09df19f5 |
+11
-48
@@ -3,25 +3,11 @@ name: CI
|
|||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-frontend:
|
test:
|
||||||
name: Test Frontend
|
name: Test
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Test Frontend
|
|
||||||
run: |
|
|
||||||
make binary-frontend-test-coverage
|
|
||||||
- name: Upload coverage
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: coverage
|
|
||||||
path: ${{ github.workspace }}/webapp/frontend/coverage/lcov.info
|
|
||||||
retention-days: 1
|
|
||||||
test-backend:
|
|
||||||
name: Test Backend
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: ghcr.io/packagrio/packagr:latest-golang
|
container: ghcr.io/packagrio/packagr:latest-golang
|
||||||
|
|
||||||
# Service containers to run with `build` (Required for end-to-end testing)
|
# Service containers to run with `build` (Required for end-to-end testing)
|
||||||
services:
|
services:
|
||||||
influxdb:
|
influxdb:
|
||||||
@@ -36,6 +22,7 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 8086:8086
|
- 8086:8086
|
||||||
env:
|
env:
|
||||||
|
PROJECT_PATH: /go/src/github.com/analogj/scrutiny
|
||||||
STATIC: true
|
STATIC: true
|
||||||
steps:
|
steps:
|
||||||
- name: Git
|
- name: Git
|
||||||
@@ -45,37 +32,16 @@ jobs:
|
|||||||
git --version
|
git --version
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Test Backend
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
make binary-clean binary-test-coverage
|
make binary-clean binary-test-coverage
|
||||||
- name: Upload coverage
|
- name: Generate coverage report
|
||||||
uses: actions/upload-artifact@v4
|
uses: codecov/codecov-action@v2
|
||||||
with:
|
with:
|
||||||
name: coverage
|
files: ${{ github.workspace }}/coverage.txt
|
||||||
path: ${{ github.workspace }}/coverage.txt
|
|
||||||
retention-days: 1
|
|
||||||
test-coverage:
|
|
||||||
name: Test Coverage Upload
|
|
||||||
needs:
|
|
||||||
- test-backend
|
|
||||||
- test-frontend
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Download coverage reports
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: coverage
|
|
||||||
- name: Upload coverage reports
|
|
||||||
uses: codecov/codecov-action@v5
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
files: ${{ github.workspace }}/coverage.txt,${{ github.workspace }}/lcov.info
|
|
||||||
flags: unittests
|
flags: unittests
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}
|
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}
|
||||||
runs-on: ${{ matrix.cfg.on }}
|
runs-on: ${{ matrix.cfg.on }}
|
||||||
@@ -100,16 +66,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '^1.20.1'
|
|
||||||
- name: Build Binaries
|
- name: Build Binaries
|
||||||
run: |
|
run: |
|
||||||
make binary-clean binary-all
|
make binary-clean binary-all
|
||||||
- name: Archive
|
- name: Archive
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: binaries-${{ matrix.cfg.on }}-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}-${{ matrix.cfg.goarm || 'na' }}.zip
|
name: binaries.zip
|
||||||
path: |
|
path: |
|
||||||
scrutiny-web-*
|
scrutiny-web-*
|
||||||
scrutiny-collector-metrics-*
|
scrutiny-collector-metrics-*
|
||||||
@@ -46,9 +46,7 @@ jobs:
|
|||||||
latest=false
|
latest=false
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,enable=true,event=branch,suffix=-collector
|
type=ref,enable=true,event=branch,suffix=-collector
|
||||||
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},suffix=-collector
|
type=ref,enable=true,event=tag,suffix=-collector
|
||||||
type=semver,pattern=v{{major}}.{{minor}},suffix=-collector
|
|
||||||
type=semver,pattern=v{{major}},suffix=-collector
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
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 (don't push on PR)
|
||||||
@@ -76,6 +74,15 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: "Populate frontend version information"
|
- name: "Populate frontend version information"
|
||||||
run: "cd webapp/frontend && ./git.version.sh"
|
run: "cd webapp/frontend && ./git.version.sh"
|
||||||
|
- name: "Generate frontend"
|
||||||
|
uses: addnab/docker-run-action@v3
|
||||||
|
with:
|
||||||
|
image: node:lts
|
||||||
|
options: -v ${{ github.workspace }}:/work
|
||||||
|
run: |
|
||||||
|
cd /work
|
||||||
|
make binary-frontend && echo "print contents of /work/dist" && ls -alt /work/dist
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
with:
|
with:
|
||||||
@@ -101,9 +108,7 @@ jobs:
|
|||||||
latest=false
|
latest=false
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,enable=true,event=branch,suffix=-web
|
type=ref,enable=true,event=branch,suffix=-web
|
||||||
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},suffix=-web
|
type=ref,enable=true,event=tag,suffix=-web
|
||||||
type=semver,pattern=v{{major}}.{{minor}},suffix=-web
|
|
||||||
type=semver,pattern=v{{major}},suffix=-web
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
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 (don't push on PR)
|
||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
@@ -129,6 +134,16 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: "Populate frontend version information"
|
- name: "Populate frontend version information"
|
||||||
run: "cd webapp/frontend && ./git.version.sh"
|
run: "cd webapp/frontend && ./git.version.sh"
|
||||||
|
- name: "Generate frontend & version information"
|
||||||
|
uses: addnab/docker-run-action@v3
|
||||||
|
with:
|
||||||
|
image: node:lts
|
||||||
|
options: -v ${{ github.workspace }}:/work
|
||||||
|
run: |
|
||||||
|
cd /work
|
||||||
|
make binary-frontend && echo "print contents of /work/dist" && ls -alt /work/dist
|
||||||
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
with:
|
with:
|
||||||
@@ -152,9 +167,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,enable=true,event=branch,suffix=-omnibus
|
type=ref,enable=true,event=branch,suffix=-omnibus
|
||||||
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},suffix=-omnibus
|
type=ref,enable=true,event=tag,suffix=-omnibus
|
||||||
type=semver,pattern=v{{major}}.{{minor}},suffix=-omnibus
|
|
||||||
type=semver,pattern=v{{major}},suffix=-omnibus
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
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 (don't push on PR)
|
||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
@@ -168,4 +181,4 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
# cache-from: type=gha
|
# cache-from: type=gha
|
||||||
# cache-to: type=gha,mode=max
|
# cache-to: type=gha,mode=max
|
||||||
@@ -19,6 +19,16 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: "Populate frontend version information"
|
- name: "Populate frontend version information"
|
||||||
run: "cd webapp/frontend && ./git.version.sh"
|
run: "cd webapp/frontend && ./git.version.sh"
|
||||||
|
- name: "Generate frontend & version information"
|
||||||
|
uses: addnab/docker-run-action@v3
|
||||||
|
with:
|
||||||
|
image: node:lts
|
||||||
|
options: -v ${{ github.workspace }}:/work
|
||||||
|
run: |
|
||||||
|
cd /work
|
||||||
|
make binary-frontend && echo "print contents of /work/dist" && ls -alt /work/dist
|
||||||
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 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: |
|
||||||
|
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
|
add-apt-repository ppa:git-core/ppa && apt-get update && apt-get install -y git
|
||||||
git --version
|
git --version
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
|
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
|
||||||
- name: Upload workspace
|
- name: Upload workspace
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: workspace
|
name: workspace
|
||||||
path: ${{ github.workspace }}/**/*
|
path: ${{ github.workspace }}/**/*
|
||||||
@@ -91,66 +91,36 @@ jobs:
|
|||||||
- { on: windows-latest, goos: windows, goarch: arm64 }
|
- { on: windows-latest, goos: windows, goarch: arm64 }
|
||||||
steps:
|
steps:
|
||||||
- name: Download workspace
|
- name: Download workspace
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: workspace
|
name: workspace
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: '1.20.1' # The Go version to download (if necessary) and use.
|
go-version: '1.18.3' # The Go version to download (if necessary) and use.
|
||||||
- name: Build Binaries
|
- name: Build Binaries
|
||||||
run: |
|
run: |
|
||||||
make binary-clean binary-all
|
make binary-clean binary-all
|
||||||
- name: Upload artifacts
|
- name: Archive
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: scrutiny-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}.zip
|
name: binaries.zip
|
||||||
path: |
|
path: |
|
||||||
scrutiny-web-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}${{ case(matrix.cfg.goos == 'windows', '.exe', '') }}
|
scrutiny-web-*
|
||||||
scrutiny-collector-metrics-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}${{ case(matrix.cfg.goos == 'windows', '.exe', '') }}
|
scrutiny-collector-metrics-*
|
||||||
|
|
||||||
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:
|
release-publish:
|
||||||
name: Publish Release
|
name: Publish Release
|
||||||
needs:
|
needs: build
|
||||||
- build
|
|
||||||
- build_frontend
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download workspace
|
- name: Download workspace
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ./
|
name: workspace
|
||||||
- name: Download binaries
|
- name: Download binaries
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
merge-multiple: true
|
name: binaries.zip
|
||||||
pattern: scrutiny-*.zip
|
|
||||||
- name: Download frontend
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: scrutiny-web-frontend.zip
|
|
||||||
- name: List
|
- name: List
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -174,7 +144,6 @@ jobs:
|
|||||||
scrutiny-collector-metrics-linux-arm64
|
scrutiny-collector-metrics-linux-arm64
|
||||||
scrutiny-collector-metrics-windows-amd64.exe
|
scrutiny-collector-metrics-windows-amd64.exe
|
||||||
scrutiny-collector-metrics-windows-arm64.exe
|
scrutiny-collector-metrics-windows-arm64.exe
|
||||||
scrutiny-web-frontend.tar.gz
|
|
||||||
scrutiny-web-darwin-amd64
|
scrutiny-web-darwin-amd64
|
||||||
scrutiny-web-darwin-arm64
|
scrutiny-web-darwin-arm64
|
||||||
scrutiny-web-freebsd-amd64
|
scrutiny-web-freebsd-amd64
|
||||||
|
|||||||
+1
-3
@@ -65,6 +65,4 @@ scrutiny_test.db
|
|||||||
scrutiny.yaml
|
scrutiny.yaml
|
||||||
coverage.txt
|
coverage.txt
|
||||||
/config
|
/config
|
||||||
/influxdb
|
/influxdb
|
||||||
.angular
|
|
||||||
web.log
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# 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)
|
|
||||||
+13
-16
@@ -1,24 +1,21 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
**Please see our [AI policy](./AI_POLICY.md).**
|
|
||||||
|
|
||||||
The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) containing source code for:
|
The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) containing source code for:
|
||||||
- Scrutiny Backend Server (API)
|
- Scrutiny Backend Server (API)
|
||||||
- Scrutiny Frontend Angular SPA
|
- Scrutiny Frontend Angular SPA
|
||||||
- S.M.A.R.T Collector
|
- S.M.A.R.T Collector
|
||||||
|
|
||||||
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
|
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
|
||||||
|
|
||||||
# Modifying the Scrutiny Backend Server (API)
|
# 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.17+)
|
||||||
2. download the `scrutiny-web-frontend.tar.gz` for
|
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`
|
||||||
the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist`
|
|
||||||
3. create a `scrutiny.yaml` config file
|
3. create a `scrutiny.yaml` config file
|
||||||
```yaml
|
```yaml
|
||||||
# config file for local development. store as scrutiny.yaml
|
# config file for local development. store as scrutiny.yaml
|
||||||
version: 1
|
version: 1
|
||||||
|
|
||||||
web:
|
web:
|
||||||
listen:
|
listen:
|
||||||
port: 8080
|
port: 8080
|
||||||
@@ -31,13 +28,13 @@ Depending on the functionality you are adding, you may need to setup a developme
|
|||||||
path: ./dist
|
path: ./dist
|
||||||
influxdb:
|
influxdb:
|
||||||
retention_policy: false
|
retention_policy: false
|
||||||
|
|
||||||
log:
|
log:
|
||||||
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
||||||
level: DEBUG
|
level: DEBUG
|
||||||
|
|
||||||
```
|
```
|
||||||
4. start a InfluxDB docker container.
|
4. start a InfluxDB docker container.
|
||||||
```bash
|
```bash
|
||||||
docker run -p 8086:8086 --rm influxdb:2.2
|
docker run -p 8086:8086 --rm influxdb:2.2
|
||||||
```
|
```
|
||||||
@@ -57,21 +54,21 @@ The frontend is written in Angular. If you're working on the frontend and can us
|
|||||||
```bash
|
```bash
|
||||||
cd webapp/frontend
|
cd webapp/frontend
|
||||||
npm install
|
npm install
|
||||||
npm run start -- --serve-path="/web/" --port 4200
|
npm run start -- --deploy-url="/web/" --base-href="/web/" --port 4200
|
||||||
```
|
```
|
||||||
3. open your browser and visit [http://localhost:4200/web](http://localhost:4200/web)
|
3. open your browser and visit [http://localhost:4200/web](http://localhost:4200/web)
|
||||||
|
|
||||||
# Modifying both Scrutiny Backend and Frontend Applications
|
# Modifying both Scrutiny Backend and Frontend Applications
|
||||||
If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data,
|
If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data,
|
||||||
you'll need to follow the steps below:
|
you'll need to follow the steps below:
|
||||||
|
|
||||||
1. install the [Go runtime](https://go.dev/doc/install) (v1.20+)
|
1. install the [Go runtime](https://go.dev/doc/install) (v1.17+)
|
||||||
2. install [NodeJS](https://nodejs.org/en/download/)
|
2. install [NodeJS](https://nodejs.org/en/download/)
|
||||||
3. create a `scrutiny.yaml` config file
|
3. create a `scrutiny.yaml` config file
|
||||||
```yaml
|
```yaml
|
||||||
# config file for local development. store as scrutiny.yaml
|
# config file for local development. store as scrutiny.yaml
|
||||||
version: 1
|
version: 1
|
||||||
|
|
||||||
web:
|
web:
|
||||||
listen:
|
listen:
|
||||||
port: 8080
|
port: 8080
|
||||||
@@ -84,7 +81,7 @@ you'll need to follow the steps below:
|
|||||||
path: ./dist
|
path: ./dist
|
||||||
influxdb:
|
influxdb:
|
||||||
retention_policy: false
|
retention_policy: false
|
||||||
|
|
||||||
log:
|
log:
|
||||||
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
file: 'web.log' #absolute or relative paths allowed, eg. web.log
|
||||||
level: DEBUG
|
level: DEBUG
|
||||||
@@ -163,7 +160,7 @@ docker cp scrutiny:/tmp/web.log web.log
|
|||||||
# Docker Development
|
# Docker Development
|
||||||
|
|
||||||
```
|
```
|
||||||
docker build -f docker/Dockerfile . -t ghcr.io/analogj/scrutiny:master-omnibus
|
docker build -f docker/Dockerfile . -t chcr.io/analogj/scrutiny:master-omnibus
|
||||||
docker run -it --rm -p 8080:8080 \
|
docker run -it --rm -p 8080:8080 \
|
||||||
-v /run/udev:/run/udev:ro \
|
-v /run/udev:/run/udev:ro \
|
||||||
--cap-add SYS_RAWIO \
|
--cap-add SYS_RAWIO \
|
||||||
@@ -187,4 +184,4 @@ docker run -p 8086:8086 -d --rm \
|
|||||||
influxdb:2.2
|
influxdb:2.2
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
.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.
|
.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
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
# Global Env Settings
|
# Global Env Settings
|
||||||
@@ -90,27 +89,17 @@ ifneq ($(OS),Windows_NT)
|
|||||||
./$(WEB_BINARY_NAME) || true
|
./$(WEB_BINARY_NAME) || true
|
||||||
endif
|
endif
|
||||||
|
|
||||||
########################################################################################################################
|
|
||||||
# Binary
|
|
||||||
########################################################################################################################
|
|
||||||
|
|
||||||
.PHONY: binary-frontend
|
.PHONY: binary-frontend
|
||||||
# reduce logging, disable angular-cli analytics for ci environment
|
# reduce logging, disable angular-cli analytics for ci environment
|
||||||
binary-frontend: export NPM_CONFIG_LOGLEVEL = warn
|
binary-frontend: export NPM_CONFIG_LOGLEVEL = warn
|
||||||
binary-frontend: export NG_CLI_ANALYTICS = false
|
binary-frontend: export NG_CLI_ANALYTICS = false
|
||||||
binary-frontend:
|
binary-frontend:
|
||||||
cd webapp/frontend
|
cd webapp/frontend
|
||||||
npm install -g @angular/cli@v13-lts
|
npm install -g @angular/cli@9.1.4
|
||||||
mkdir -p $(CURDIR)/dist
|
mkdir -p $(CURDIR)/dist
|
||||||
npm ci
|
npm ci
|
||||||
npm run build:prod -- --output-path=$(CURDIR)/dist
|
npm run build:prod -- --output-path=$(CURDIR)/dist
|
||||||
|
|
||||||
.PHONY: binary-frontend-test-coverage
|
|
||||||
# reduce logging, disable angular-cli analytics for ci environment
|
|
||||||
binary-frontend-test-coverage:
|
|
||||||
cd webapp/frontend
|
|
||||||
npm ci
|
|
||||||
npx ng test --watch=false --browsers=ChromeHeadless --code-coverage
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
# Docker
|
# Docker
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ If you run a server with more than a couple of hard drives, you're probably alre
|
|||||||
|
|
||||||
> smartd is a daemon that monitors the Self-Monitoring, Analysis and Reporting Technology (SMART) system built into many ATA, IDE and SCSI-3 hard drives. The purpose of SMART is to monitor the reliability of the hard drive and predict drive failures, and to carry out different types of drive self-tests.
|
> smartd is a daemon that monitors the Self-Monitoring, Analysis and Reporting Technology (SMART) system built into many ATA, IDE and SCSI-3 hard drives. The purpose of SMART is to monitor the reliability of the hard drive and predict drive failures, and to carry out different types of drive self-tests.
|
||||||
|
|
||||||
These S.M.A.R.T hard drive self-tests can help you detect and replace failing hard drives before they cause permanent data loss. However, there's a couple issues with `smartd`:
|
Theses S.M.A.R.T hard drive self-tests can help you detect and replace failing hard drives before they cause permanent data loss. However, there's a couple issues with `smartd`:
|
||||||
|
|
||||||
- There are more than a hundred S.M.A.R.T attributes, however `smartd` does not differentiate between critical and informational metrics
|
- There are more than a hundred S.M.A.R.T attributes, however `smartd` does not differentiate between critical and informational metrics
|
||||||
- `smartd` does not record S.M.A.R.T attribute history, so it can be hard to determine if an attribute is degrading slowly over time.
|
- `smartd` does not record S.M.A.R.T attribute history, so it can be hard to determine if an attribute is degrading slowly over time.
|
||||||
@@ -46,7 +46,7 @@ Scrutiny is a simple but focused application, with a couple of core features:
|
|||||||
- Customized thresholds using real world failure rates
|
- Customized thresholds using real world failure rates
|
||||||
- Temperature tracking
|
- Temperature tracking
|
||||||
- Provided as an all-in-one Docker image (but can be installed manually)
|
- Provided as an all-in-one Docker image (but can be installed manually)
|
||||||
- Configurable Alerting/Notifications via Webhooks
|
- Future Configurable Alerting/Notifications via Webhooks
|
||||||
- (Future) Hard Drive performance testing & tracking
|
- (Future) Hard Drive performance testing & tracking
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
@@ -69,10 +69,10 @@ See [docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](./docs/TROUBLESHOOTING_DEVICE_COL
|
|||||||
|
|
||||||
If you're using Docker, getting started is as simple as running the following command:
|
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.
|
> See [docker/example.omnibus.docker-compose.yml](./docker/example.omnibus.docker-compose.yml) for a docker-compose file.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 8080:8080 -p 8086:8086 --restart unless-stopped \
|
docker run -it --rm -p 8080:8080 -p 8086:8086 \
|
||||||
-v `pwd`/scrutiny:/opt/scrutiny/config \
|
-v `pwd`/scrutiny:/opt/scrutiny/config \
|
||||||
-v `pwd`/influxdb2:/opt/scrutiny/influxdb \
|
-v `pwd`/influxdb2:/opt/scrutiny/influxdb \
|
||||||
-v /run/udev:/run/udev:ro \
|
-v /run/udev:/run/udev:ro \
|
||||||
@@ -91,29 +91,25 @@ docker run -p 8080:8080 -p 8086:8086 --restart unless-stopped \
|
|||||||
|
|
||||||
### Hub/Spoke Deployment
|
### Hub/Spoke Deployment
|
||||||
|
|
||||||
In addition to the Omnibus image (available under the `latest` tag) you can deploy in Hub/Spoke mode, which requires 3
|
In addition to the Omnibus image (available under the `latest` tag) there are 2 other Docker images available:
|
||||||
other Docker images:
|
|
||||||
|
|
||||||
- `ghcr.io/analogj/scrutiny:master-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like
|
- `ghcr.io/analogj/scrutiny:master-collector` - Contains the Scrutiny data collector, `smartctl` binary and cron-like scheduler. You can run one collector on each server.
|
||||||
scheduler. You can run one collector on each server.
|
- `ghcr.io/analogj/scrutiny:master-web` - Contains the Web UI, API and Database. Only one container necessary
|
||||||
- `ghcr.io/analogj/scrutiny:master-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.
|
> See [docker/example.hubspoke.docker-compose.yml](./docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 8086:8086 --restart unless-stopped \
|
docker run --rm -p 8086:8086 \
|
||||||
-v `pwd`/influxdb2:/var/lib/influxdb2 \
|
-v `pwd`/influxdb2:/var/lib/influxdb2 \
|
||||||
--name scrutiny-influxdb \
|
--name scrutiny-influxdb \
|
||||||
influxdb:2.2
|
influxdb:2.2
|
||||||
|
|
||||||
docker run -p 8080:8080 --restart unless-stopped \
|
docker run --rm -p 8080:8080 \
|
||||||
-v `pwd`/scrutiny:/opt/scrutiny/config \
|
-v `pwd`/scrutiny:/opt/scrutiny/config \
|
||||||
--name scrutiny-web \
|
--name scrutiny-web \
|
||||||
ghcr.io/analogj/scrutiny:master-web
|
ghcr.io/analogj/scrutiny:master-web
|
||||||
|
|
||||||
docker run --restart unless-stopped \
|
docker run --rm \
|
||||||
-v /run/udev:/run/udev:ro \
|
-v /run/udev:/run/udev:ro \
|
||||||
--cap-add SYS_RAWIO \
|
--cap-add SYS_RAWIO \
|
||||||
--device=/dev/sda \
|
--device=/dev/sda \
|
||||||
@@ -157,7 +153,7 @@ Neither file is required, however if provided, it allows you to configure how Sc
|
|||||||
|
|
||||||
## Cron Schedule
|
## 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).
|
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:master-collector` or `ghcr.io/analogj/scrutiny:master-omnibus` docker images,
|
||||||
you can use the `COLLECTOR_CRON_SCHEDULE` environmental variable to override the default cron schedule (daily @ midnight - `0 0 * * *`).
|
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 * * *" ...`
|
`docker run -e COLLECTOR_CRON_SCHEDULE="0 0 * * *" ...`
|
||||||
@@ -174,7 +170,6 @@ Scrutiny supports sending SMART device failure notifications via the following s
|
|||||||
- IFTTT
|
- IFTTT
|
||||||
- Join
|
- Join
|
||||||
- Mattermost
|
- Mattermost
|
||||||
- ntfy
|
|
||||||
- Pushbullet
|
- Pushbullet
|
||||||
- Pushover
|
- Pushover
|
||||||
- Slack
|
- Slack
|
||||||
@@ -244,9 +239,9 @@ scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
|
|||||||
| linux-arm-6 | :white_check_mark: | |
|
| linux-arm-6 | :white_check_mark: | |
|
||||||
| linux-arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
|
| linux-arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
|
||||||
| linux-arm64 | :white_check_mark: | :white_check_mark: |
|
| linux-arm64 | :white_check_mark: | :white_check_mark: |
|
||||||
| freebsd-amd64 | :white_check_mark: | |
|
| freebsd-amd64 | collector only. see [#238](https://github.com/AnalogJ/scrutiny/issues/238) | |
|
||||||
| macos-amd64 | :white_check_mark: | :white_check_mark: |
|
| macos-amd64 | | :white_check_mark: |
|
||||||
| macos-arm64 | :white_check_mark: | :white_check_mark: |
|
| macos-arm64 | | :white_check_mark: |
|
||||||
| windows-amd64 | :white_check_mark: | WIP, see [#15](https://github.com/AnalogJ/scrutiny/issues/15) |
|
| windows-amd64 | :white_check_mark: | WIP, see [#15](https://github.com/AnalogJ/scrutiny/issues/15) |
|
||||||
| windows-arm64 | :white_check_mark: | |
|
| windows-arm64 | :white_check_mark: | |
|
||||||
|
|
||||||
@@ -265,8 +260,7 @@ We use SemVer for versioning. For the versions available, see the tags on this r
|
|||||||
|
|
||||||
# Authors
|
# Authors
|
||||||
|
|
||||||
* Jason Kulatunga - Initial Development - [@AnalogJ](https://github.com/AnalogJ/)
|
Jason Kulatunga - Initial Development - @AnalogJ
|
||||||
* Aram Akhavan - Maintenence - [@kaysond](https://github.com/kaysond/)
|
|
||||||
|
|
||||||
# Licenses
|
# Licenses
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/config"
|
"github.com/analogj/scrutiny/collector/pkg/config"
|
||||||
@@ -30,14 +29,8 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
configFilePath := "/opt/scrutiny/config/collector.yaml"
|
|
||||||
configFilePathAlternative := "/opt/scrutiny/config/collector.yml"
|
|
||||||
if !utils.FileExists(configFilePath) && utils.FileExists(configFilePathAlternative) {
|
|
||||||
configFilePath = configFilePathAlternative
|
|
||||||
}
|
|
||||||
|
|
||||||
//we're going to load the config file manually, since we need to validate it.
|
//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
|
err = config.ReadConfig("/opt/scrutiny/config/collector.yaml") // Find and read the config file
|
||||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||||
//ignore "could not find config file"
|
//ignore "could not find config file"
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@@ -127,16 +120,26 @@ OPTIONS:
|
|||||||
config.Set("api.endpoint", apiEndpoint)
|
config.Set("api.endpoint", apiEndpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
collectorLogger, logFile, err := CreateLogger(config)
|
collectorLogger := logrus.WithFields(logrus.Fields{
|
||||||
if logFile != nil {
|
"type": "metrics",
|
||||||
defer logFile.Close()
|
})
|
||||||
}
|
|
||||||
if err != nil {
|
if level, err := logrus.ParseLevel(config.GetString("log.level")); err == nil {
|
||||||
return err
|
logrus.SetLevel(level)
|
||||||
|
} else {
|
||||||
|
logrus.SetLevel(logrus.InfoLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.IsSet("log.file") && len(config.GetString("log.file")) > 0 {
|
||||||
|
logFile, err := os.OpenFile(config.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to open log file %s for output: %s", config.GetString("log.file"), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer logFile.Close()
|
||||||
|
logrus.SetOutput(io.MultiWriter(os.Stderr, logFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsData, err := json.MarshalIndent(config.AllSettings(), "", "\t")
|
|
||||||
collectorLogger.Debug(string(settingsData), err)
|
|
||||||
metricCollector, err := collector.CreateMetricsCollector(
|
metricCollector, err := collector.CreateMetricsCollector(
|
||||||
config,
|
config,
|
||||||
collectorLogger,
|
collectorLogger,
|
||||||
@@ -189,28 +192,5 @@ OPTIONS:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(color.HiRedString("ERROR: %v", err))
|
log.Fatal(color.HiRedString("ERROR: %v", err))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func CreateLogger(appConfig config.Interface) (*logrus.Entry, *os.File, error) {
|
|
||||||
logger := logrus.WithFields(logrus.Fields{
|
|
||||||
"type": "metrics",
|
|
||||||
})
|
|
||||||
|
|
||||||
if level, err := logrus.ParseLevel(appConfig.GetString("log.level")); err == nil {
|
|
||||||
logger.Logger.SetLevel(level)
|
|
||||||
} else {
|
|
||||||
logger.Logger.SetLevel(logrus.InfoLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
var logFile *os.File
|
|
||||||
var err error
|
|
||||||
if appConfig.IsSet("log.file") && len(appConfig.GetString("log.file")) > 0 {
|
|
||||||
logFile, err = os.OpenFile(appConfig.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
logger.Logger.Errorf("Failed to open log file %s for output: %s", appConfig.GetString("log.file"), err)
|
|
||||||
return nil, logFile, err
|
|
||||||
}
|
|
||||||
logger.Logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
|
|
||||||
}
|
|
||||||
return logger, logFile, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var httpClient = &http.Client{Timeout: 60 * time.Second}
|
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
type BaseCollector struct {
|
type BaseCollector struct {
|
||||||
logger *logrus.Entry
|
logger *logrus.Entry
|
||||||
|
|||||||
@@ -4,19 +4,16 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/config"
|
"github.com/analogj/scrutiny/collector/pkg/config"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||||
"github.com/samber/lo"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetricsCollector struct {
|
type MetricsCollector struct {
|
||||||
@@ -59,16 +56,11 @@ func (mc *MetricsCollector) Run() error {
|
|||||||
Logger: mc.logger,
|
Logger: mc.logger,
|
||||||
Config: mc.config,
|
Config: mc.config,
|
||||||
}
|
}
|
||||||
rawDetectedStorageDevices, err := deviceDetector.Start()
|
detectedStorageDevices, err := deviceDetector.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//filter any device with empty wwn (they are invalid)
|
|
||||||
detectedStorageDevices := lo.Filter[models.Device](rawDetectedStorageDevices, func(dev models.Device, _ int) bool {
|
|
||||||
return len(dev.WWN) > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
mc.logger.Infoln("Sending detected devices to API, for filtering & validation")
|
mc.logger.Infoln("Sending detected devices to API, for filtering & validation")
|
||||||
jsonObj, _ := json.Marshal(detectedStorageDevices)
|
jsonObj, _ := json.Marshal(detectedStorageDevices)
|
||||||
mc.logger.Debugf("Detected devices: %v", string(jsonObj))
|
mc.logger.Debugf("Detected devices: %v", string(jsonObj))
|
||||||
@@ -92,9 +84,8 @@ func (mc *MetricsCollector) Run() error {
|
|||||||
//go mc.Collect(&wg, device.WWN, device.DeviceName, device.DeviceType)
|
//go mc.Collect(&wg, device.WWN, device.DeviceName, device.DeviceType)
|
||||||
mc.Collect(device.WWN, device.DeviceName, device.DeviceType)
|
mc.Collect(device.WWN, device.DeviceName, device.DeviceType)
|
||||||
|
|
||||||
if mc.config.GetInt("commands.metrics_smartctl_wait") > 0 {
|
// TODO: we may need to sleep for between each call to smartctl -a
|
||||||
time.Sleep(time.Duration(mc.config.GetInt("commands.metrics_smartctl_wait")) * time.Second)
|
//time.Sleep(30 * time.Millisecond)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//mc.logger.Infoln("Main: Waiting for workers to finish")
|
//mc.logger.Infoln("Main: Waiting for workers to finish")
|
||||||
@@ -116,7 +107,7 @@ func (mc *MetricsCollector) Validate() error {
|
|||||||
return nil
|
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) {
|
func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceType string) {
|
||||||
//defer wg.Done()
|
//defer wg.Done()
|
||||||
if len(deviceWWN) == 0 {
|
if len(deviceWWN) == 0 {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
type configuration struct {
|
type configuration struct {
|
||||||
*viper.Viper
|
*viper.Viper
|
||||||
|
|
||||||
deviceOverrides []models.ScanOverride
|
deviceOverrides []models.ScanOverride
|
||||||
}
|
}
|
||||||
|
|
||||||
//Viper uses the following precedence order. Each item takes precedence over the item below it:
|
//Viper uses the following precedence order. Each item takes precedence over the item below it:
|
||||||
@@ -47,17 +47,9 @@ func (c *configuration) Init() error {
|
|||||||
c.SetDefault("commands.metrics_scan_args", "--scan --json")
|
c.SetDefault("commands.metrics_scan_args", "--scan --json")
|
||||||
c.SetDefault("commands.metrics_info_args", "--info --json")
|
c.SetDefault("commands.metrics_info_args", "--info --json")
|
||||||
c.SetDefault("commands.metrics_smart_args", "--xall --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("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
|
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
|
||||||
c.SetConfigType("yaml")
|
c.SetConfigType("yaml")
|
||||||
//c.SetConfigName("drawbridge")
|
//c.SetConfigName("drawbridge")
|
||||||
@@ -194,18 +186,3 @@ func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
|
|||||||
}
|
}
|
||||||
return c.GetString("commands.metrics_smart_args")
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,25 +36,6 @@ func TestConfiguration_GetScanOverrides_Simple(t *testing.T) {
|
|||||||
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides)
|
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixes #418
|
|
||||||
func TestConfiguration_GetScanOverrides_DeviceTypeComma(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//setup
|
|
||||||
testConfig, _ := config.Create()
|
|
||||||
|
|
||||||
//test
|
|
||||||
err := testConfig.ReadConfig(path.Join("testdata", "device_type_comma.yaml"))
|
|
||||||
require.NoError(t, err, "should correctly load simple device config")
|
|
||||||
scanOverrides := testConfig.GetDeviceOverrides()
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, []models.ScanOverride{
|
|
||||||
{Device: "/dev/sda", DeviceType: []string{"sat", "auto"}, Ignore: false},
|
|
||||||
{Device: "/dev/sdb", DeviceType: []string{"sat,auto"}, Ignore: false},
|
|
||||||
}, scanOverrides)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
|
func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -144,29 +125,3 @@ func TestConfiguration_OverrideDeviceCommands_MetricsInfoArgs(t *testing.T) {
|
|||||||
require.Equal(t, "--info --json", testConfig.GetCommandMetricsInfoArgs("/dev/sdb"))
|
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)
|
//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,6 +25,4 @@ type Interface interface {
|
|||||||
GetDeviceOverrides() []models.ScanOverride
|
GetDeviceOverrides() []models.ScanOverride
|
||||||
GetCommandMetricsInfoArgs(deviceName string) string
|
GetCommandMetricsInfoArgs(deviceName string) string
|
||||||
GetCommandMetricsSmartArgs(deviceName string) string
|
GetCommandMetricsSmartArgs(deviceName string) string
|
||||||
|
|
||||||
IsAllowlistedDevice(deviceName string) bool
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,20 +175,6 @@ func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
|
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.
|
// IsSet mocks base method.
|
||||||
func (m *MockInterface) IsSet(key string) bool {
|
func (m *MockInterface) IsSet(key string) bool {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
allow_listed_devices:
|
|
||||||
- /dev/sda
|
|
||||||
- /dev/sdb
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
version: 1
|
|
||||||
devices:
|
|
||||||
# the scrutiny config parser will detect `sat,auto` as two separate items in a list. If you want to use `-d sat,auto` you must
|
|
||||||
# set 'sat,auto' in a list (see eg. /dev/sbd)
|
|
||||||
- device: /dev/sda
|
|
||||||
type: 'sat,auto'
|
|
||||||
- device: /dev/sdb
|
|
||||||
type:
|
|
||||||
- sat,auto
|
|
||||||
@@ -3,14 +3,13 @@ package detect
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/config"
|
"github.com/analogj/scrutiny/collector/pkg/config"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Detect struct {
|
type Detect struct {
|
||||||
@@ -48,7 +47,7 @@ func (d *Detect) SmartctlScan() ([]models.Device, error) {
|
|||||||
return detectedDevices, nil
|
return detectedDevices, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updates a device model with information from smartctl --scan
|
//updates a device model with information from smartctl --scan
|
||||||
// It has a couple of issues however:
|
// It has a couple of issues however:
|
||||||
// - WWN is provided as component data, rather than a "string". We'll have to generate the WWN value ourselves
|
// - WWN is provided as component data, rather than a "string". We'll have to generate the WWN value ourselves
|
||||||
// - WWN from smartctl only provided for ATA protocol drives, NVMe and SCSI drives do not include WWN.
|
// - WWN from smartctl only provided for ATA protocol drives, NVMe and SCSI drives do not include WWN.
|
||||||
@@ -82,9 +81,8 @@ func (d *Detect) SmartCtlInfo(device *models.Device) error {
|
|||||||
device.SerialNumber = availableDeviceInfo.SerialNumber
|
device.SerialNumber = availableDeviceInfo.SerialNumber
|
||||||
device.Firmware = availableDeviceInfo.FirmwareVersion
|
device.Firmware = availableDeviceInfo.FirmwareVersion
|
||||||
device.RotationSpeed = availableDeviceInfo.RotationRate
|
device.RotationSpeed = availableDeviceInfo.RotationRate
|
||||||
device.Capacity = availableDeviceInfo.Capacity()
|
device.Capacity = availableDeviceInfo.UserCapacity.Bytes
|
||||||
device.FormFactor = availableDeviceInfo.FormFactor.Name
|
device.FormFactor = availableDeviceInfo.FormFactor.Name
|
||||||
device.DeviceType = availableDeviceInfo.Device.Type
|
|
||||||
device.DeviceProtocol = availableDeviceInfo.Device.Protocol
|
device.DeviceProtocol = availableDeviceInfo.Device.Protocol
|
||||||
if len(availableDeviceInfo.Vendor) > 0 {
|
if len(availableDeviceInfo.Vendor) > 0 {
|
||||||
device.Manufacturer = availableDeviceInfo.Vendor
|
device.Manufacturer = availableDeviceInfo.Vendor
|
||||||
@@ -124,11 +122,6 @@ func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []mod
|
|||||||
|
|
||||||
deviceFile := strings.ToLower(scannedDevice.Name)
|
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{
|
detectedDevice := models.Device{
|
||||||
HostId: d.Config.GetString("host.id"),
|
HostId: d.Config.GetString("host.id"),
|
||||||
DeviceType: scannedDevice.Type,
|
DeviceType: scannedDevice.Type,
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
package detect_test
|
package detect_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
mock_shell "github.com/analogj/scrutiny/collector/pkg/common/shell/mock"
|
mock_shell "github.com/analogj/scrutiny/collector/pkg/common/shell/mock"
|
||||||
mock_config "github.com/analogj/scrutiny/collector/pkg/config/mock"
|
mock_config "github.com/analogj/scrutiny/collector/pkg/config/mock"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/detect"
|
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDetect_SmartctlScan(t *testing.T) {
|
func TestDetect_SmartctlScan(t *testing.T) {
|
||||||
// setup
|
//setup
|
||||||
mockCtrl := gomock.NewController(t)
|
mockCtrl := gomock.NewController(t)
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
@@ -24,10 +21,9 @@ func TestDetect_SmartctlScan(t *testing.T) {
|
|||||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
|
||||||
|
|
||||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||||
testScanResults, err := os.ReadFile("testdata/smartctl_scan_simple.json")
|
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_simple.json")
|
||||||
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
|
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
|
||||||
|
|
||||||
d := detect.Detect{
|
d := detect.Detect{
|
||||||
@@ -36,17 +32,17 @@ func TestDetect_SmartctlScan(t *testing.T) {
|
|||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// test
|
//test
|
||||||
scannedDevices, err := d.SmartctlScan()
|
scannedDevices, err := d.SmartctlScan()
|
||||||
|
|
||||||
// assert
|
//assert
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 7, len(scannedDevices))
|
require.Equal(t, 7, len(scannedDevices))
|
||||||
require.Equal(t, "scsi", scannedDevices[0].DeviceType)
|
require.Equal(t, "scsi", scannedDevices[0].DeviceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
|
func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
|
||||||
// setup
|
//setup
|
||||||
mockCtrl := gomock.NewController(t)
|
mockCtrl := gomock.NewController(t)
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
@@ -54,10 +50,9 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
|
|||||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
|
||||||
|
|
||||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||||
testScanResults, err := os.ReadFile("testdata/smartctl_scan_megaraid.json")
|
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_megaraid.json")
|
||||||
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
|
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
|
||||||
|
|
||||||
d := detect.Detect{
|
d := detect.Detect{
|
||||||
@@ -66,20 +61,20 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
|
|||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// test
|
//test
|
||||||
scannedDevices, err := d.SmartctlScan()
|
scannedDevices, err := d.SmartctlScan()
|
||||||
|
|
||||||
// assert
|
//assert
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 2, len(scannedDevices))
|
require.Equal(t, 2, len(scannedDevices))
|
||||||
require.Equal(t, []models.Device{
|
require.Equal(t, []models.Device{
|
||||||
{DeviceName: "bus/0", DeviceType: "megaraid,0"},
|
models.Device{DeviceName: "bus/0", DeviceType: "megaraid,0"},
|
||||||
{DeviceName: "bus/0", DeviceType: "megaraid,1"},
|
models.Device{DeviceName: "bus/0", DeviceType: "megaraid,1"},
|
||||||
}, scannedDevices)
|
}, scannedDevices)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_SmartctlScan_Nvme(t *testing.T) {
|
func TestDetect_SmartctlScan_Nvme(t *testing.T) {
|
||||||
// setup
|
//setup
|
||||||
mockCtrl := gomock.NewController(t)
|
mockCtrl := gomock.NewController(t)
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
@@ -87,10 +82,9 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
|
|||||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
|
||||||
|
|
||||||
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
fakeShell := mock_shell.NewMockInterface(mockCtrl)
|
||||||
testScanResults, err := os.ReadFile("testdata/smartctl_scan_nvme.json")
|
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_nvme.json")
|
||||||
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
|
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
|
||||||
|
|
||||||
d := detect.Detect{
|
d := detect.Detect{
|
||||||
@@ -99,19 +93,19 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
|
|||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// test
|
//test
|
||||||
scannedDevices, err := d.SmartctlScan()
|
scannedDevices, err := d.SmartctlScan()
|
||||||
|
|
||||||
// assert
|
//assert
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 1, len(scannedDevices))
|
require.Equal(t, 1, len(scannedDevices))
|
||||||
require.Equal(t, []models.Device{
|
require.Equal(t, []models.Device{
|
||||||
{DeviceName: "nvme0", DeviceType: "nvme"},
|
models.Device{DeviceName: "nvme0", DeviceType: "nvme"},
|
||||||
}, scannedDevices)
|
}, scannedDevices)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
||||||
// setup
|
//setup
|
||||||
mockCtrl := gomock.NewController(t)
|
mockCtrl := gomock.NewController(t)
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
@@ -119,7 +113,6 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
|||||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
|
||||||
|
|
||||||
detectedDevices := models.Scan{
|
detectedDevices := models.Scan{
|
||||||
Devices: []models.ScanDevice{
|
Devices: []models.ScanDevice{
|
||||||
@@ -136,16 +129,16 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
|||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// test
|
//test
|
||||||
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||||
|
|
||||||
// assert
|
//assert
|
||||||
require.Equal(t, "sda", transformedDevices[0].DeviceName)
|
require.Equal(t, "sda", transformedDevices[0].DeviceName)
|
||||||
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
|
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
||||||
// setup
|
//setup
|
||||||
mockCtrl := gomock.NewController(t)
|
mockCtrl := gomock.NewController(t)
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
@@ -153,7 +146,6 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
|||||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
|
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
|
||||||
|
|
||||||
detectedDevices := models.Scan{
|
detectedDevices := models.Scan{
|
||||||
Devices: []models.ScanDevice{
|
Devices: []models.ScanDevice{
|
||||||
@@ -170,22 +162,21 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
|||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// test
|
//test
|
||||||
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||||
|
|
||||||
// assert
|
//assert
|
||||||
require.Equal(t, []models.Device{}, transformedDevices)
|
require.Equal(t, []models.Device{}, transformedDevices)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
||||||
// setup
|
//setup
|
||||||
mockCtrl := gomock.NewController(t)
|
mockCtrl := gomock.NewController(t)
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
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{
|
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{
|
||||||
{
|
{
|
||||||
Device: "/dev/bus/0",
|
Device: "/dev/bus/0",
|
||||||
@@ -196,8 +187,7 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
|||||||
Device: "/dev/twa0",
|
Device: "/dev/twa0",
|
||||||
DeviceType: []string{"3ware,0", "3ware,1", "3ware,2", "3ware,3", "3ware,4", "3ware,5"},
|
DeviceType: []string{"3ware,0", "3ware,1", "3ware,2", "3ware,3", "3ware,4", "3ware,5"},
|
||||||
Ignore: false,
|
Ignore: false,
|
||||||
},
|
}})
|
||||||
})
|
|
||||||
detectedDevices := models.Scan{
|
detectedDevices := models.Scan{
|
||||||
Devices: []models.ScanDevice{
|
Devices: []models.ScanDevice{
|
||||||
{
|
{
|
||||||
@@ -213,15 +203,15 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
|||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// test
|
//test
|
||||||
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||||
|
|
||||||
// assert
|
//assert
|
||||||
require.Equal(t, 12, len(transformedDevices))
|
require.Equal(t, 12, len(transformedDevices))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
||||||
// setup
|
//setup
|
||||||
mockCtrl := gomock.NewController(t)
|
mockCtrl := gomock.NewController(t)
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
@@ -229,7 +219,6 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
|||||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
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().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
|
||||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
|
||||||
detectedDevices := models.Scan{
|
detectedDevices := models.Scan{
|
||||||
Devices: []models.ScanDevice{
|
Devices: []models.ScanDevice{
|
||||||
{
|
{
|
||||||
@@ -245,17 +234,17 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
|||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// test
|
//test
|
||||||
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||||
|
|
||||||
// assert
|
//assert
|
||||||
require.Equal(t, 1, len(transformedDevices))
|
require.Equal(t, 1, len(transformedDevices))
|
||||||
require.Equal(t, "sat+megaraid", transformedDevices[0].DeviceType)
|
require.Equal(t, "sat+megaraid", transformedDevices[0].DeviceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// test https://github.com/AnalogJ/scrutiny/issues/255#issuecomment-1164024126
|
// test https://github.com/AnalogJ/scrutiny/issues/255#issuecomment-1164024126
|
||||||
func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T) {
|
func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T) {
|
||||||
// setup
|
//setup
|
||||||
mockCtrl := gomock.NewController(t)
|
mockCtrl := gomock.NewController(t)
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
@@ -263,7 +252,6 @@ func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T)
|
|||||||
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
|
||||||
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
|
||||||
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda"}})
|
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda"}})
|
||||||
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
|
|
||||||
detectedDevices := models.Scan{
|
detectedDevices := models.Scan{
|
||||||
Devices: []models.ScanDevice{
|
Devices: []models.ScanDevice{
|
||||||
{
|
{
|
||||||
@@ -279,16 +267,16 @@ func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T)
|
|||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// test
|
//test
|
||||||
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||||
|
|
||||||
// assert
|
//assert
|
||||||
require.Equal(t, 1, len(transformedDevices))
|
require.Equal(t, 1, len(transformedDevices))
|
||||||
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
|
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
|
func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
|
||||||
// setup
|
//setup
|
||||||
mockCtrl := gomock.NewController(t)
|
mockCtrl := gomock.NewController(t)
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
@@ -302,109 +290,10 @@ func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
|
|||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
// test
|
//test
|
||||||
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||||
|
|
||||||
// assert
|
//assert
|
||||||
require.Equal(t, 1, len(transformedDevices))
|
require.Equal(t, 1, len(transformedDevices))
|
||||||
require.Equal(t, "ata", transformedDevices[0].DeviceType)
|
require.Equal(t, "ata", transformedDevices[0].DeviceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_TransformDetectedDevices_AllowListFilters(t *testing.T) {
|
|
||||||
mockCtrl := gomock.NewController(t)
|
|
||||||
defer mockCtrl.Finish()
|
|
||||||
|
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
|
||||||
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
|
||||||
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"
|
|
||||||
|
|
||||||
// device info
|
|
||||||
someDeviceName = "some-device-name"
|
|
||||||
someModelName = "KCD61LUL3T84"
|
|
||||||
someSerialNumber = "61Q0A05UT7B8"
|
|
||||||
someFirmware = "8002"
|
|
||||||
someDeviceProtocol = "NVMe"
|
|
||||||
someDeviceType = "nvme"
|
|
||||||
someCapacity int64 = 3840755982336
|
|
||||||
)
|
|
||||||
|
|
||||||
fakeConfig := mock_config.NewMockInterface(ctrl)
|
|
||||||
fakeConfig.EXPECT().
|
|
||||||
GetCommandMetricsInfoArgs("/dev/" + someDeviceName).
|
|
||||||
Return(someArgs)
|
|
||||||
fakeConfig.EXPECT().
|
|
||||||
GetString("commands.metrics_smartctl_bin").
|
|
||||||
Return("smartctl")
|
|
||||||
|
|
||||||
someLogger := logrus.WithFields(logrus.Fields{})
|
|
||||||
|
|
||||||
smartctlInfoResults, err := os.ReadFile("testdata/smartctl_info_nvme.json")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
fakeShell := mock_shell.NewMockInterface(ctrl)
|
|
||||||
fakeShell.EXPECT().
|
|
||||||
Command(someLogger, "smartctl", append(strings.Split(someArgs, " "), "/dev/"+someDeviceName), "", gomock.Any()).
|
|
||||||
Return(string(smartctlInfoResults), err)
|
|
||||||
|
|
||||||
d := detect.Detect{
|
|
||||||
Logger: someLogger,
|
|
||||||
Shell: fakeShell,
|
|
||||||
Config: fakeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
someDevice := &models.Device{
|
|
||||||
WWN: "some wwn",
|
|
||||||
DeviceName: someDeviceName,
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, d.SmartCtlInfo(someDevice))
|
|
||||||
|
|
||||||
assert.Equal(t, someDeviceName, someDevice.DeviceName)
|
|
||||||
assert.Equal(t, someModelName, someDevice.ModelName)
|
|
||||||
assert.Equal(t, someSerialNumber, someDevice.SerialNumber)
|
|
||||||
assert.Equal(t, someFirmware, someDevice.Firmware)
|
|
||||||
assert.Equal(t, someDeviceProtocol, someDevice.DeviceProtocol)
|
|
||||||
assert.Equal(t, someDeviceType, someDevice.DeviceType)
|
|
||||||
assert.Equal(t, someCapacity, someDevice.Capacity)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"json_format_version": [
|
|
||||||
1,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"smartctl": {
|
|
||||||
"version": [
|
|
||||||
7,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"svn_revision": "5155",
|
|
||||||
"platform_info": "x86_64-linux-6.1.69-talos",
|
|
||||||
"build_info": "(local build)",
|
|
||||||
"argv": [
|
|
||||||
"smartctl",
|
|
||||||
"--info",
|
|
||||||
"--json",
|
|
||||||
"/dev/nvme4"
|
|
||||||
],
|
|
||||||
"exit_status": 0
|
|
||||||
},
|
|
||||||
"device": {
|
|
||||||
"name": "/dev/nvme4",
|
|
||||||
"info_name": "/dev/nvme4",
|
|
||||||
"type": "nvme",
|
|
||||||
"protocol": "NVMe"
|
|
||||||
},
|
|
||||||
"model_name": "KCD61LUL3T84",
|
|
||||||
"serial_number": "61Q0A05UT7B8",
|
|
||||||
"firmware_version": "8002",
|
|
||||||
"nvme_pci_vendor": {
|
|
||||||
"id": 7695,
|
|
||||||
"subsystem_id": 7695
|
|
||||||
},
|
|
||||||
"nvme_ieee_oui_identifier": 9233294,
|
|
||||||
"nvme_total_capacity": 3840755982336,
|
|
||||||
"nvme_unallocated_capacity": 0,
|
|
||||||
"nvme_controller_id": 1,
|
|
||||||
"nvme_version": {
|
|
||||||
"string": "1.4",
|
|
||||||
"value": 66560
|
|
||||||
},
|
|
||||||
"nvme_number_of_namespaces": 16,
|
|
||||||
"local_time": {
|
|
||||||
"time_t": 1706045146,
|
|
||||||
"asctime": "Tue Jan 23 21:25:46 2024 UTC"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+25
-52
@@ -1,77 +1,50 @@
|
|||||||
# syntax=docker/dockerfile:1.4
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
# Omnibus Image
|
# Omnibus Image
|
||||||
|
# NOTE: this image requires the `make binary-frontend` target to have been run before `docker build` The `dist` directory must exist.
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
######## Build the frontend
|
|
||||||
FROM --platform=${BUILDPLATFORM} node AS frontendbuild
|
|
||||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
|
||||||
COPY --link . /go/src/github.com/analogj/scrutiny
|
|
||||||
|
|
||||||
RUN make binary-frontend
|
########
|
||||||
|
FROM golang:1.17-bullseye as backendbuild
|
||||||
|
|
||||||
######## Build the backend
|
|
||||||
FROM golang:1.20-bookworm as backendbuild
|
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||||
COPY --link . /go/src/github.com/analogj/scrutiny
|
COPY . /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
|
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
|
||||||
|
|
||||||
|
|
||||||
######## Combine build artifacts in runtime image
|
########
|
||||||
FROM debian:bookworm-slim as runtime
|
FROM debian:bullseye-slim as runtime
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
WORKDIR /opt/scrutiny
|
WORKDIR /opt/scrutiny
|
||||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||||
ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb
|
ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb
|
||||||
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 \
|
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl tzdata \
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates \
|
|
||||||
cron \
|
|
||||||
curl \
|
|
||||||
smartmontools \
|
|
||||||
tzdata \
|
|
||||||
procps \
|
|
||||||
xz-utils \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& update-ca-certificates \
|
&& update-ca-certificates \
|
||||||
&& case ${TARGETARCH} in \
|
&& case ${TARGETARCH} in \
|
||||||
"amd64") S6_ARCH=x86_64 ;; \
|
"amd64") S6_ARCH=amd64 ;; \
|
||||||
"arm64") S6_ARCH=aarch64 ;; \
|
"arm64") S6_ARCH=aarch64 ;; \
|
||||||
esac \
|
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 \
|
&& curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \
|
||||||
&& tar -Jxpf /tmp/s6-overlay-noarch.tar.xz -C / \
|
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \
|
||||||
&& rm -rf /tmp/s6-overlay-noarch.tar.xz \
|
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \
|
||||||
&& 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 \
|
&& curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \
|
||||||
&& tar -Jxpf /tmp/s6-overlay-${S6_ARCH}.tar.xz -C / \
|
&& dpkg -i --force-all /tmp/influxdb2-2.2.0-${TARGETARCH}.deb
|
||||||
&& 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-${INFLUXVER}-${TARGETARCH}.deb
|
|
||||||
|
|
||||||
COPY /rootfs /
|
COPY /rootfs /
|
||||||
|
|
||||||
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
|
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
||||||
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
|
COPY --from=backendbuild /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
|
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
|
||||||
RUN chmod 0644 /etc/cron.d/scrutiny && \
|
COPY dist /opt/scrutiny/web
|
||||||
|
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
|
||||||
|
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics && \
|
||||||
|
chmod 0644 /etc/cron.d/scrutiny && \
|
||||||
rm -f /etc/cron.daily/* && \
|
rm -f /etc/cron.daily/* && \
|
||||||
mkdir -p /opt/scrutiny/web && \
|
mkdir -p /opt/scrutiny/web && \
|
||||||
mkdir -p /opt/scrutiny/config && \
|
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"]
|
CMD ["/init"]
|
||||||
|
|||||||
@@ -4,21 +4,20 @@
|
|||||||
|
|
||||||
|
|
||||||
########
|
########
|
||||||
FROM golang:1.20-bookworm as backendbuild
|
FROM golang:1.17-bullseye as backendbuild
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||||
|
|
||||||
COPY . /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
|
RUN make binary-clean binary-collector
|
||||||
|
|
||||||
########
|
########
|
||||||
FROM debian:bookworm-slim as runtime
|
FROM debian:bullseye-slim as runtime
|
||||||
WORKDIR /opt/scrutiny
|
WORKDIR /scrutiny
|
||||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates
|
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates
|
||||||
|
|
||||||
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
|
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
|
||||||
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
|
||||||
|
|||||||
+13
-19
@@ -1,37 +1,31 @@
|
|||||||
# syntax=docker/dockerfile:1.4
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
# Web Image
|
# Web Image
|
||||||
|
# NOTE: this image requires the `make binary-frontend` target to have been run before `docker build` The `dist` directory must exist.
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
######## Build the frontend
|
|
||||||
FROM --platform=${BUILDPLATFORM} node AS frontendbuild
|
|
||||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
|
||||||
COPY --link . /go/src/github.com/analogj/scrutiny
|
|
||||||
|
|
||||||
RUN make binary-frontend
|
########
|
||||||
|
FROM golang:1.17-bullseye as backendbuild
|
||||||
######## Build the backend
|
|
||||||
FROM golang:1.20-bookworm as backendbuild
|
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
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/*
|
COPY . /go/src/github.com/analogj/scrutiny
|
||||||
|
|
||||||
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
|
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
|
||||||
|
|
||||||
|
|
||||||
######## Combine build artifacts in runtime image
|
########
|
||||||
FROM debian:bookworm-slim as runtime
|
FROM debian:bullseye-slim as runtime
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
WORKDIR /opt/scrutiny
|
WORKDIR /opt/scrutiny
|
||||||
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
ENV PATH="/opt/scrutiny/bin:${PATH}"
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates
|
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && update-ca-certificates
|
||||||
|
|
||||||
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
|
COPY --from=backendbuild /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
|
COPY dist /opt/scrutiny/web
|
||||||
RUN mkdir -p /opt/scrutiny/web && \
|
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
|
||||||
|
mkdir -p /opt/scrutiny/web && \
|
||||||
mkdir -p /opt/scrutiny/config && \
|
mkdir -p /opt/scrutiny/config && \
|
||||||
chmod -R a+rX /opt/scrutiny && \
|
chmod -R ugo+rwx /opt/scrutiny/config
|
||||||
chmod -R a+w /opt/scrutiny/config
|
|
||||||
CMD ["/opt/scrutiny/bin/scrutiny", "start"]
|
CMD ["/opt/scrutiny/bin/scrutiny", "start"]
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
|
|
||||||
# adding ability to customize the cron schedule.
|
# adding ability to customize the cron schedule.
|
||||||
COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
|
COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
|
||||||
COLLECTOR_RUN_STARTUP=${COLLECTOR_RUN_STARTUP:-"false"}
|
|
||||||
COLLECTOR_RUN_STARTUP_SLEEP=${COLLECTOR_RUN_STARTUP_SLEEP:-"1"}
|
|
||||||
|
|
||||||
# if the cron schedule has been overridden via env variable (eg docker-compose) we should make sure to strip quotes
|
# if the cron schedule has been overridden via env variable (eg docker-compose) we should make sure to strip quotes
|
||||||
[[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}"
|
[[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}"
|
||||||
@@ -16,13 +14,6 @@ COLLECTOR_RUN_STARTUP_SLEEP=${COLLECTOR_RUN_STARTUP_SLEEP:-"1"}
|
|||||||
# replace placeholder with correct value
|
# 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
|
||||||
|
|
||||||
if [[ "${COLLECTOR_RUN_STARTUP}" == "true" ]]; then
|
|
||||||
sleep ${COLLECTOR_RUN_STARTUP_SLEEP}
|
|
||||||
echo "starting scrutiny collector (run-once mode. subsequent calls will be triggered via cron service)"
|
|
||||||
/opt/scrutiny/bin/scrutiny-collector-metrics run
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# now that we have the env start cron in the foreground
|
# now that we have the env start cron in the foreground
|
||||||
echo "starting cron"
|
echo "starting cron"
|
||||||
exec su -c "cron -f -L 15" root
|
su -c "cron -f -L 15" root
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ version: '2.4'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
influxdb:
|
influxdb:
|
||||||
restart: unless-stopped
|
|
||||||
image: influxdb:2.2
|
image: influxdb:2.2
|
||||||
ports:
|
ports:
|
||||||
- '8086:8086'
|
- '8086:8086'
|
||||||
@@ -16,7 +15,6 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
web:
|
web:
|
||||||
restart: unless-stopped
|
|
||||||
image: 'ghcr.io/analogj/scrutiny:master-web'
|
image: 'ghcr.io/analogj/scrutiny:master-web'
|
||||||
ports:
|
ports:
|
||||||
- '8080:8080'
|
- '8080:8080'
|
||||||
@@ -35,7 +33,6 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
collector:
|
collector:
|
||||||
restart: unless-stopped
|
|
||||||
image: 'ghcr.io/analogj/scrutiny:master-collector'
|
image: 'ghcr.io/analogj/scrutiny:master-collector'
|
||||||
cap_add:
|
cap_add:
|
||||||
- SYS_RAWIO
|
- SYS_RAWIO
|
||||||
@@ -43,13 +40,9 @@ services:
|
|||||||
- '/run/udev:/run/udev:ro'
|
- '/run/udev:/run/udev:ro'
|
||||||
environment:
|
environment:
|
||||||
COLLECTOR_API_ENDPOINT: 'http://web:8080'
|
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:
|
depends_on:
|
||||||
web:
|
web:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
devices:
|
devices:
|
||||||
- "/dev/sda"
|
- "/dev/sda"
|
||||||
- "/dev/sdb"
|
- "/dev/sdb"
|
||||||
@@ -2,7 +2,6 @@ version: '3.5'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
scrutiny:
|
scrutiny:
|
||||||
restart: unless-stopped
|
|
||||||
container_name: scrutiny
|
container_name: scrutiny
|
||||||
image: ghcr.io/analogj/scrutiny:master-omnibus
|
image: ghcr.io/analogj/scrutiny:master-omnibus
|
||||||
cap_add:
|
cap_add:
|
||||||
|
|||||||
+1
-182
@@ -1,182 +1 @@
|
|||||||
>
|
> 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.
|
||||||
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.
|
|
||||||
|
|
||||||
> The following guide was contributed by @TinJoy59 in #417
|
|
||||||
> It describes how to deploy the Scrutiny in Hub/Spoke mode, where the Hub is running in Docker, and the Spokes (
|
|
||||||
> collectors) are running as binaries.
|
|
||||||
> He's using Proxmox & Synology in his guide, however this should be applicable for almost anyone
|
|
||||||
|
|
||||||
# S.M.A.R.T. Monitoring with Scrutiny across machines
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 🤔 The problem:
|
|
||||||
|
|
||||||
Scrutiny offers a nice Docker package called "Omnibus" that can monitor HDDs attached to a Docker host with relative
|
|
||||||
ease. Scrutiny can also be installed in a Hub-Spoke layout where Web interface, Database and Collector come in 3
|
|
||||||
separate packages. The official documentation assumes that the spokes in the "Hub-Spokes layout" run Docker, which is
|
|
||||||
not always the case. The third approach is to install Scrutiny manually, entirely outside of Docker.
|
|
||||||
|
|
||||||
### 💡 The solution:
|
|
||||||
|
|
||||||
This tutorial provides a hybrid configuration where the Hub lives in a Docker instance while the spokes have only
|
|
||||||
Scrutiny Collector installed manually. The Collector periodically send data to the Hub. It's not mind-boggling hard to
|
|
||||||
understand but someone might struggle with the setup. This is for them.
|
|
||||||
|
|
||||||
### 🖥️ My setup:
|
|
||||||
|
|
||||||
I have a Proxmox cluster where one VM runs Docker and all monitoring services - Grafana, Prometheus, various exporters,
|
|
||||||
InfluxDB and so forth. Another VM runs the NAS - OpenMediaVault v6, where all hard drives reside. The Scrutiny Collector
|
|
||||||
is triggered every 30min to collect data on the drives. The data is sent to the Docker VM, running InfluxDB.
|
|
||||||
|
|
||||||
## Setting up the Hub
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
The Hub consists of Scrutiny Web - a web interface for viewing the SMART data. And InfluxDB, where the smartmon data is
|
|
||||||
stored.
|
|
||||||
|
|
||||||
[🔗This is the official Hub-Spoke layout in docker-compose.](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
|
|
||||||
We are going to reuse parts of it. The ENV variables provide the necessary configuration for the initial setup, both for
|
|
||||||
InfluxDB and Scrutiny.
|
|
||||||
|
|
||||||
If you are working with and existing InfluxDB instance, you can forgo all the `INIT` variables as they already exist.
|
|
||||||
|
|
||||||
The official Scrutiny documentation has a
|
|
||||||
sample [scrutiny.yaml ](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml)file that normally
|
|
||||||
contains the connection and notification details but I always find it easier to configure as much as possible in the
|
|
||||||
docker-compose.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: "3.4"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
monitoring: # A common network for all monitoring services to communicate into
|
|
||||||
external: true
|
|
||||||
notifications: # To Gotify or another Notification service
|
|
||||||
external: true
|
|
||||||
|
|
||||||
services:
|
|
||||||
influxdb:
|
|
||||||
restart: unless-stopped
|
|
||||||
container_name: influxdb
|
|
||||||
image: influxdb:2.1-alpine
|
|
||||||
ports:
|
|
||||||
- 8086:8086
|
|
||||||
volumes:
|
|
||||||
- ${DIR_CONFIG}/influxdb2/db:/var/lib/influxdb2
|
|
||||||
- ${DIR_CONFIG}/influxdb2/config:/etc/influxdb2
|
|
||||||
environment:
|
|
||||||
- DOCKER_INFLUXDB_INIT_MODE=setup
|
|
||||||
- DOCKER_INFLUXDB_INIT_USERNAME=Admin
|
|
||||||
- DOCKER_INFLUXDB_INIT_PASSWORD=${PASSWORD}
|
|
||||||
- DOCKER_INFLUXDB_INIT_ORG=homelab
|
|
||||||
- DOCKER_INFLUXDB_INIT_BUCKET=scrutiny
|
|
||||||
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=your-very-secret-token
|
|
||||||
networks:
|
|
||||||
- monitoring
|
|
||||||
|
|
||||||
scrutiny:
|
|
||||||
restart: unless-stopped
|
|
||||||
container_name: scrutiny
|
|
||||||
image: ghcr.io/analogj/scrutiny:master-web
|
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
volumes:
|
|
||||||
- ${DIR_CONFIG}/scrutiny/config:/opt/scrutiny/config
|
|
||||||
environment:
|
|
||||||
- SCRUTINY_WEB_INFLUXDB_HOST=influxdb
|
|
||||||
- SCRUTINY_WEB_INFLUXDB_PORT=8086
|
|
||||||
- SCRUTINY_WEB_INFLUXDB_TOKEN=your-very-secret-token
|
|
||||||
- SCRUTINY_WEB_INFLUXDB_ORG=homelab
|
|
||||||
- SCRUTINY_WEB_INFLUXDB_BUCKET=scrutiny
|
|
||||||
# Optional but highly recommended to notify you in case of a problem
|
|
||||||
- SCRUTINY_NOTIFY_URLS=["http://gotify:80/message?token=a-gotify-token"]
|
|
||||||
depends_on:
|
|
||||||
- influxdb
|
|
||||||
networks:
|
|
||||||
- notifications
|
|
||||||
- monitoring
|
|
||||||
```
|
|
||||||
|
|
||||||
A freshly initialized Scrutiny instance can be accessed on port 8080, eg. `192.168.0.100:8080`. The interface will be
|
|
||||||
empty because no metrics have been collected yet.
|
|
||||||
|
|
||||||
## Setting up a Spoke ***without*** Docker
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
A spoke consists of the Scrutiny Collector binary that is run on a set interval via crontab and sends the data to the
|
|
||||||
Hub. The official
|
|
||||||
documentation [describes the manual setup of the Collector](https://github.com/AnalogJ/scrutiny/blob/master/docs/INSTALL_MANUAL.md#collector)
|
|
||||||
- dependencies and step by step commands. I have a shortened version that does the same thing but in one line of code.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Installing dependencies
|
|
||||||
apt install smartmontools -y
|
|
||||||
|
|
||||||
# 1. Create directory for the binary
|
|
||||||
# 2. Download the binary into that directory
|
|
||||||
# 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.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
|
|
||||||
```
|
|
||||||
|
|
||||||
<p class="callout warning">When downloading Github Release Assests, make sure that you have the correct version. The provided example is with Release v0.5.0. [The release list can be found here.](https://github.com/analogj/scrutiny/releases) </p>
|
|
||||||
|
|
||||||
Once the Collector is installed, you can run it with the following command. Make sure to add the correct address and
|
|
||||||
port of your Hub as `--api-endpoint`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 run --api-endpoint "http://192.168.0.100:8080"
|
|
||||||
```
|
|
||||||
|
|
||||||
This will run the Collector once and populate the Web interface of your Scrutiny instance. In order to collect metrics
|
|
||||||
for a time series, you need to run the command repeatedly. Here is an example for crontab, running the Collector every
|
|
||||||
15min.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# open crontab
|
|
||||||
crontab -e
|
|
||||||
|
|
||||||
# add a line for Scrutiny
|
|
||||||
*/15 * * * * /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 run --api-endpoint "http://192.168.0.100:8080"
|
|
||||||
```
|
|
||||||
|
|
||||||
The Collector has its own independent config file that lives in `/opt/scrutiny/config/collector.yaml` but I did not find
|
|
||||||
a need to modify
|
|
||||||
it. [A default collector.yaml can be found in the official documentation.](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml)
|
|
||||||
|
|
||||||
## Setting up a Spoke ***with*** Docker
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Setting up a remote Spoke in Docker requires you to split
|
|
||||||
the [official Hub-Spoke layout docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
|
|
||||||
. In the following docker-compose you need to provide the `${API_ENDPOINT}`, in my case `http://192.168.0.100:8080`.
|
|
||||||
Also all drives that you wish to monitor need to be presented to the container under `devices`.
|
|
||||||
|
|
||||||
The image handles the periodic scanning of the drives.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: "3.4"
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
collector:
|
|
||||||
restart: unless-stopped
|
|
||||||
image: 'ghcr.io/analogj/scrutiny:master-collector'
|
|
||||||
cap_add:
|
|
||||||
- SYS_RAWIO
|
|
||||||
volumes:
|
|
||||||
- '/run/udev:/run/udev:ro'
|
|
||||||
environment:
|
|
||||||
COLLECTOR_API_ENDPOINT: ${API_ENDPOINT}
|
|
||||||
devices:
|
|
||||||
- "/dev/sda"
|
|
||||||
- "/dev/sdb"
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
# Manual Windows Install
|
|
||||||
|
|
||||||
This guide is specifically for people who are on a Windows machine using [WSL](https://learn.microsoft.com/en-us/windows/wsl/about) with Docker.
|
|
||||||
|
|
||||||
Scrutiny is made up of three components: an influxdb Database, a collector and a webapp/api. Docker will be used for
|
|
||||||
the influxdb and webapp/API, the collector component will be facilitated by [Windows Task Scheduler](https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page).
|
|
||||||
|
|
||||||
> **NOTE:** If you are **NOT** using WSL with docker, then the easiest way to get started with [Scrutiny is the omnibus Docker image](https://github.com/AnalogJ/scrutiny#docker).
|
|
||||||
|
|
||||||
## InfluxDB and Webapp/API (Docker)
|
|
||||||
|
|
||||||
1. Copy the [example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml)
|
|
||||||
file and delete the collector section near the bottom of the file.
|
|
||||||
2. Run `docker-compose up -d` to verify that the DB and webapp are working correctly and once its completed, your webapp
|
|
||||||
should be up and running but the dashboard will be empty (default location is `localhost:8080`)
|
|
||||||
|
|
||||||
## Collector (Windows Task Scheduler)
|
|
||||||
|
|
||||||
1. Download the latest `scrutiny-collector-metrics-windows-amd64.exe` from the [releases page](https://github.com/AnalogJ/scrutiny/releases) (under assets)
|
|
||||||
2. On your windows host, open [Windows Task Scheduler](https://www.wikihow.com/Open-Task-Scheduler-in-Windows-10) as **Administrator**
|
|
||||||
1. In the **Start Menu** (Windows key), type `Task Scheduler` and then right click `Run as Administrator` to open
|
|
||||||
3. On the status bar (under the `action` tab), click `Create Task...`
|
|
||||||
4. A new window should open with the `General` Tab open, enter relevant information into the `Name` and `Description` fields
|
|
||||||
1. Under **Security Options** check:
|
|
||||||
1. **Run whether user is logged on or not**
|
|
||||||
2. **Run with highest privileges**
|
|
||||||
5. Next, click the `Triggers` tab and then click `New...` (bottom left-hand side of the window)
|
|
||||||
6. Here you can set how often you want this task to run, example settings are the following:
|
|
||||||
1. **Settings:**
|
|
||||||
1. `Daily`, start at `TODAYS_DATE` `12:00:00 AM`, Recur every `1` days,
|
|
||||||
2. **Advanced Settings:**
|
|
||||||
1. Repeat Task every: `1 hour` for a duration of `Indefinitely`
|
|
||||||
2. Stop task if it runs longer than: `30 minutes`
|
|
||||||
3. Click Ok when satisfied with your schedule
|
|
||||||
> **NOTE:** The above settings will trigger the task **every day at midnight** and then **run every hour after that** (modify as needed)
|
|
||||||
7. Next, click the `Actions` tab and then click `New...` (bottom left-hand side of the window)
|
|
||||||
1. **Action Settings:**
|
|
||||||
1. In the **Program/Script** field, put: `scrutiny-collector-metrics-windows-amd64.exe`
|
|
||||||
2. In the **Add arguments (optional)** field, put: `run --api-endpoint "http://localhost:8080" --config collector.yaml`
|
|
||||||
> **NOTE:**
|
|
||||||
> * Make sure that you put the correct port number (as specified in the docker-compose file) for the webapp (default is `8080`)
|
|
||||||
> * The `--config` param is optional and is not needed if you just want to use the default collector config, see
|
|
||||||
[example.collector.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml) for more info on the collector config.
|
|
||||||
3. In the **Start in (optional)** field, put: FOLDER_PATH_TO_YOUR `scrutiny-collector-metrics-windows-amd64.exe` file
|
|
||||||
> **NOTE:** Must be exact and do not include `scrutiny-collector-metrics-windows-amd64.exe` in the path
|
|
||||||
4. Click Ok when finished
|
|
||||||
8. Next, click the `Conditions` tab and make sure that everything is unchecked (unless you want to specify otherwise)
|
|
||||||
9. Next, click the `Settings` tab and check everything except for the last checkbox
|
|
||||||
1. **Examples for the following settings:**
|
|
||||||
1. If the task fails, restart every: `5 minutes`
|
|
||||||
2. Attempt restart up to: `3` times
|
|
||||||
3. Stop the task if it runs longer than `1 hour`
|
|
||||||
10. Next, once satisfied with everything, click Ok
|
|
||||||
11. Then, find your newly created task (by its name) in the scheduler task list and then manually run it (right click it and then click `Run`)
|
|
||||||
12. Finally, refresh your dashboard after a minute or two and your drive information should have populated the webapp dashboard.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -91,13 +91,9 @@ wget https://raw.githubusercontent.com/smartmontools/smartmontools/master/smartm
|
|||||||
```
|
```
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
/volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64 run --config /volume1/\@Entware/scrutiny/conf/collector.yaml
|
/volume1/\@Entware/scrutiny/bin/scrutiny-collector-metrics-linux-arm64 run --config /volume1/\@Entware/scrutiny/config/collector.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
**Make `run_collect.sh` executable**
|
|
||||||
|
|
||||||
`chmod +x /volume1/\@Entware/scrutiny/bin/run_collect.sh`
|
|
||||||
|
|
||||||
## Set up Synology to run a scheduled task.
|
## Set up Synology to run a scheduled task.
|
||||||
|
|
||||||
Log in to DSM and do the following:
|
Log in to DSM and do the following:
|
||||||
@@ -135,4 +131,4 @@ Frequency: <Your desired frequency>
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
If you have any issues with your devices being detected, or incorrect data, please take a look at [TROUBLESHOOTING_DEVICE_COLLECTOR.md](./TROUBLESHOOTING_DEVICE_COLLECTOR.md)
|
If you have any issues with your devices being detected, or incorrect data, please take a look at [TROUBLESHOOTING_DEVICE_COLLECTOR.md](./TROUBLESHOOTING_DEVICE_COLLECTOR.md)
|
||||||
@@ -1,22 +1,17 @@
|
|||||||
# Officially Supported NAS/OS's
|
# Officially Supported NAS OS's
|
||||||
|
|
||||||
These are the officially supported NAS OS's (with documentation and setup guides). Once a guide is created (
|
These are the officially supported NAS OS's (with documentation and setup guides).
|
||||||
in `docs/guides/` or elsewhere) it will be linked here.
|
Once a guide is created (in `docs/guides/`) it will be linked here.
|
||||||
|
|
||||||
- [x] [freenas/truenas](https://blog.stefandroid.com/2022/01/14/smart-scrutiny.html)
|
- [ ] freenas/truenas
|
||||||
- [x] [unraid](./INSTALL_UNRAID.md)
|
- [x] [unraid](./INSTALL_UNRAID.md)
|
||||||
- [ ] ESXI
|
- [ ] ESXI
|
||||||
- [ ] Proxmox
|
- [ ] Proxmox
|
||||||
- [x] Synology
|
- [x] Synology(./INSTALL_SYNOLOGY_COLLECTOR.md)
|
||||||
- [Hub/Spoke Deployment - Collector](./INSTALL_SYNOLOGY_COLLECTOR.md)
|
|
||||||
- [Omnibus Deployment](https://drfrankenstein.co.uk/2022/07/28/scrutiny-in-docker-on-a-synology-nas) - Docker Package
|
|
||||||
- [Omnibus Deployment](https://drfrankenstein.co.uk/scrutiny-in-container-manager-on-a-synology-nas/) - Container Manager Package
|
|
||||||
- [ ] OMV
|
- [ ] OMV
|
||||||
- [ ] Amahi
|
- [ ] Amahi
|
||||||
- [ ] Running in a LXC container
|
- [ ] Running in a LXC container
|
||||||
- [x] [PFSense](./INSTALL_PFSENSE.md)
|
- [x] [PFSense](./INSTALL_UNRAID.md)
|
||||||
- [x] QNAP
|
- [ ] QNAP
|
||||||
- [x] [RockStor](https://rockstor.com/docs/interface/docker-based-rock-ons/scrutiny.html)
|
- [ ] RockStor
|
||||||
- [ ] Solaris/OmniOS CE Support
|
|
||||||
- [ ] Kubernetes
|
|
||||||
- [x] [Windows](./INSTALL_MANUAL_WINDOWS.md)
|
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@ Thankfully the following users have been gracious enough to test/validate Scruti
|
|||||||
|
|
||||||
| Architecture Name | Binaries | Docker |
|
| Architecture Name | Binaries | Docker |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| linux-amd64 | @TizzAmmazz | @feroxy @rshxyz |
|
| linux-amd64 | -- | @feroxy @rshxyz |
|
||||||
| linux-arm-5 | -- | |
|
| linux-arm-5 | -- | |
|
||||||
| linux-arm-6 | -- | |
|
| linux-arm-6 | -- | |
|
||||||
| linux-arm-7 | @Zorlin | @martini1992 |
|
| linux-arm-7 | @Zorlin | @martini1992 |
|
||||||
@@ -17,4 +17,4 @@ Thankfully the following users have been gracious enough to test/validate Scruti
|
|||||||
| macos-amd64 | -- | -- |
|
| macos-amd64 | -- | -- |
|
||||||
| macos-arm64 | -- | -- |
|
| macos-arm64 | -- | -- |
|
||||||
| windows-amd64 | @gabrielv33 | -- |
|
| windows-amd64 | @gabrielv33 | -- |
|
||||||
| windows-arm64 | -- | -- |
|
| windows-arm64 | -- | -- |
|
||||||
@@ -19,25 +19,6 @@ Scrutiny stores and references the devices by their `WWN` which is globally uniq
|
|||||||
As such, passing devices to the Scrutiny collector container using `/dev/disk/by-id/`, `/dev/disk/by-label/`, `/dev/disk/by-path/` and `/dev/disk/by-uuid/`
|
As such, passing devices to the Scrutiny collector container using `/dev/disk/by-id/`, `/dev/disk/by-label/`, `/dev/disk/by-path/` and `/dev/disk/by-uuid/`
|
||||||
paths are unnecessary, unless you'd like to ensure the docker run command never needs to change.
|
paths are unnecessary, unless you'd like to ensure the docker run command never needs to change.
|
||||||
|
|
||||||
#### Force /dev/disk/by-id paths
|
|
||||||
|
|
||||||
Since Scrutiny uses WWN under the hood, it really doesn't care about `/dev/sd*` vs `/dev/disk/by-id/`. The problem is the interaction between docker and smartmontools when using `--device /dev/disk/by-id` paths.
|
|
||||||
|
|
||||||
Basically Scrutiny offloads all device detection to smartmontools, which doesn't seem to detect devices that have been passed into the docker container using `/dev/disk/by-id` paths.
|
|
||||||
|
|
||||||
If you must use "static" device references, you can map the host device id/uuid/wwn references to device names within the container:
|
|
||||||
|
|
||||||
```
|
|
||||||
# --device=<Host Device>:<Container Device Mapping>
|
|
||||||
|
|
||||||
docker run ....
|
|
||||||
--device=/dev/disk/by-id/wwn-0x5000xxxxx:/dev/sda
|
|
||||||
--device=/dev/disk/by-id/wwn-0x5001xxxxx:/dev/sdb
|
|
||||||
--device=/dev/disk/by-id/wwn-0x5003xxxxx:/dev/sdc
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Device Detection By Smartctl
|
## Device Detection By Smartctl
|
||||||
|
|
||||||
@@ -80,8 +61,7 @@ using a collector config file. See [example.collector.yaml](/example.collector.y
|
|||||||
|
|
||||||
> NOTE: If you use docker, you **must** pass though the RAID virtual disk to the container using `--device` (see below)
|
> NOTE: If you use docker, you **must** pass though the RAID virtual disk to the container using `--device` (see below)
|
||||||
>
|
>
|
||||||
> This device may be in `/dev/*` or `/dev/bus/*`.
|
> This device may be in `/dev/*` or `/dev/bus/*`.
|
||||||
> If you do not see a virtual device file `/dev/bus/*` you may need to use the `--privileged` flag. See [#366 for more info](https://github.com/AnalogJ/scrutiny/issues/366#issuecomment-1253196407)
|
|
||||||
>
|
>
|
||||||
> If you're unsure, run `smartctl --scan` on your host, and pass all listed devices to the container.
|
> If you're unsure, run `smartctl --scan` on your host, and pass all listed devices to the container.
|
||||||
|
|
||||||
@@ -112,7 +92,7 @@ devices:
|
|||||||
type:
|
type:
|
||||||
- aacraid,0,0,0
|
- aacraid,0,0,0
|
||||||
- aacraid,0,0,1
|
- aacraid,0,0,1
|
||||||
|
|
||||||
# HPE Smart Array example: https://github.com/AnalogJ/scrutiny/issues/213
|
# HPE Smart Array example: https://github.com/AnalogJ/scrutiny/issues/213
|
||||||
- device: /dev/sda
|
- device: /dev/sda
|
||||||
type:
|
type:
|
||||||
@@ -120,14 +100,11 @@ devices:
|
|||||||
- 'cciss,1'
|
- 'cciss,1'
|
||||||
```
|
```
|
||||||
|
|
||||||
>
|
|
||||||
|
|
||||||
### NVMe Drives
|
### NVMe Drives
|
||||||
|
As mentioned in the [README.md](/README.md), NVMe devices require both `--cap-add SYS_RAWIO` and `--cap-add SYS_ADMIN`
|
||||||
As mentioned in the [README.md](/README.md), NVMe devices require both `--cap-add SYS_RAWIO` and `--cap-add SYS_ADMIN`
|
|
||||||
to allow smartctl permission to query your NVMe device SMART data [#26](https://github.com/AnalogJ/scrutiny/issues/26)
|
to allow smartctl permission to query your NVMe device SMART data [#26](https://github.com/AnalogJ/scrutiny/issues/26)
|
||||||
|
|
||||||
When attaching NVMe devices using `--device=/dev/nvme..`, make sure to provide the device controller (`/dev/nvme0`)
|
When attaching NVMe devices using `--device=/dev/nvme..`, make sure to provide the device controller (`/dev/nvme0`)
|
||||||
instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/AnalogJ/scrutiny/issues/209).
|
instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/AnalogJ/scrutiny/issues/209).
|
||||||
|
|
||||||
> The character device /dev/nvme0 is the NVME device controller, and block devices like /dev/nvme0n1 are the NVME storage namespaces: the devices you use for actual storage, which will behave essentially as disks.
|
> The character device /dev/nvme0 is the NVME device controller, and block devices like /dev/nvme0n1 are the NVME storage namespaces: the devices you use for actual storage, which will behave essentially as disks.
|
||||||
@@ -136,29 +113,15 @@ instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/Anal
|
|||||||
|
|
||||||
### ATA
|
### ATA
|
||||||
|
|
||||||
### USB Devices
|
|
||||||
|
|
||||||
The following information is extracted from [#266](https://github.com/AnalogJ/scrutiny/issues/266)
|
|
||||||
|
|
||||||
External HDDs support two modes of operation usb-storage (old, slower, stable) and uas (new, faster, sometimes unstable)
|
|
||||||
. On some external HDDs, uas mode does not properly pass through SMART information, or even causes hardware issues, so
|
|
||||||
it has been disabled by the kernel. No amount of smartctl parameters will fix this, as it is being rejected by the
|
|
||||||
kernel. This is especially true with Seagate HDDs. One solution is to force these devices into usb-storage mode, which
|
|
||||||
will incur some performance penalty, but may work well enough for you. More info:
|
|
||||||
|
|
||||||
- https://smartmontools.org/wiki/Supported_USB-Devices
|
|
||||||
- https://smartmontools.org/wiki/SAT-with-UAS-Linux
|
|
||||||
- https://forums.raspberrypi.com/viewtopic.php?t=245931
|
|
||||||
|
|
||||||
### Exit Codes
|
### Exit Codes
|
||||||
|
|
||||||
If you see an error message similar to `smartctl returned an error code (2) while processing /dev/sda`, this means that
|
If you see an error message similar to `smartctl returned an error code (2) while processing /dev/sda`, this means that
|
||||||
`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you
|
`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you debug,
|
||||||
debug, but you can look at the table (and associated links) below to debug `smartctl`.
|
but you can look at the table (and associated links) below to debug `smartctl`.
|
||||||
|
|
||||||
> smartctl Return Values
|
> smartctl Return Values
|
||||||
> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of
|
> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of
|
||||||
> smartctl is 0 (all bits turned off). If a problem occurs, or an error, potential error, or fault is detected, then
|
> smartctl is 0 (all bits turned off). If a problem occurs, or an error, potential error, or fault is detected, then
|
||||||
> a non-zero status is returned. In this case, the eight different bits in the return value have the following meanings
|
> a non-zero status is returned. In this case, the eight different bits in the return value have the following meanings
|
||||||
> for ATA disks; some of these values may also be returned for SCSI disks.
|
> for ATA disks; some of these values may also be returned for SCSI disks.
|
||||||
>
|
>
|
||||||
@@ -250,9 +213,8 @@ UPDATE devices SET device_status = null;
|
|||||||
|
|
||||||
### Seagate Drives Failing
|
### Seagate Drives Failing
|
||||||
|
|
||||||
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.
|
As thoroughly discussed in [#255](https://github.com/AnalogJ/scrutiny/issues/255), 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
|
> 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
|
> normalised values (Current / Worst / Threshold) are usually quite low. Despite this, the numbers in most cases are perfectly OK
|
||||||
>
|
>
|
||||||
@@ -276,45 +238,21 @@ to disable Scrutiny analysis for them. Both are non-critical, and have low-corre
|
|||||||
If this is effecting your drives, you'll need to do the following:
|
If this is effecting your drives, you'll need to do the following:
|
||||||
|
|
||||||
1. Upgrade to v0.4.13+
|
1. Upgrade to v0.4.13+
|
||||||
2. Reset your drive status using the SQLite script
|
2. Reset your drive status using the SQLite script in [#device-failed-but-smart--scrutiny-passed](https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#device-failed-but-smart--scrutiny-passed)
|
||||||
in [#device-failed-but-smart--scrutiny-passed](https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#device-failed-but-smart--scrutiny-passed)
|
|
||||||
3. Wait for (or manually start) the collector.
|
3. Wait for (or manually start) the collector.
|
||||||
|
|
||||||
If you'd like to learn more about how the Seagate Ironwolf SMART attributes work under the hood, and how they differ
|
If you'd like to learn more about how the Seagate Ironwolf SMART attributes work under the hood, and how they differ from
|
||||||
from
|
|
||||||
other drives, please read the following:
|
other drives, please read the following:
|
||||||
|
|
||||||
- http://www.users.on.net/~fzabkar/HDD/Seagate_SER_RRER_HEC.html
|
- 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/
|
- 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.
|
## Hub & Spoke model, with multiple Hosts.
|
||||||
|
|
||||||

|
When deploying Scrutiny in a hub & spoke model, it can be difficult to determine exactly which node a set of devices are associated with.
|
||||||
|
Thankfully the collector has a special `--host-id` flag (or `COLLECTOR_HOST_ID` env variable) that can be used to associate devices with a friendly host name.
|
||||||
|
|
||||||
When deploying Scrutiny in a hub & spoke model, it can be difficult to determine exactly which node a set of devices are
|
See the [docs/INSTALL_HUB_SPOKE.md](/docs/INSTALL_HUB_SPOKE.md) guide for more information.
|
||||||
associated with.
|
|
||||||
Thankfully the collector has a special `--host-id` flag (or `COLLECTOR_HOST_ID` env variable) that can be used to
|
|
||||||
associate devices with a friendly host name.
|
|
||||||
|
|
||||||
The host-id is passed from the collector to the web-api when SMART device data is uploaded. There's 3 ways you can set
|
|
||||||
the host-id:
|
|
||||||
|
|
||||||
- using the collector config
|
|
||||||
file: [master/example.collector.yaml#L19-L22](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml?rgh-link-date=2022-05-25T15%3A08%3A56Z#L19-L22)
|
|
||||||
- using the `--host-id` collector CLI
|
|
||||||
argument: [master/collector/cmd/collector-metrics/collector-metrics.go#L180-L185](https://github.com/AnalogJ/scrutiny/blob/master/collector/cmd/collector-metrics/collector-metrics.go?rgh-link-date=2022-05-25T15%3A08%3A56Z#L180-L185)
|
|
||||||
- using the `COLLECTOR_HOST_ID` environmental variable.
|
|
||||||
|
|
||||||
See the [docs/INSTALL_HUB_SPOKE.md](/docs/INSTALL_HUB_SPOKE.md) guide for more information.
|
|
||||||
|
|
||||||
## Collector DEBUG mode
|
## Collector DEBUG mode
|
||||||
|
|
||||||
@@ -330,20 +268,3 @@ Or if you're not using docker, you can pass CLI arguments to the collector durin
|
|||||||
```bash
|
```bash
|
||||||
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
|
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
|
||||||
```
|
```
|
||||||
|
|
||||||
## Collector trigger on startup
|
|
||||||
|
|
||||||
When the `omnibus` docker image starts up, it will automatically trigger the collector, which will populate the Scrutiny
|
|
||||||
Webui with your disks.
|
|
||||||
This is not the case when running the collector docker image in **hub/spoke** mode, as the collector and webui are
|
|
||||||
running in different containers (and potentially different host machines), so
|
|
||||||
the web container may not be ready for incoming connections. By default the container will only run the collector at the
|
|
||||||
time specified in the cron schedule.
|
|
||||||
|
|
||||||
You can force the collector to run on startup using the following env variables:
|
|
||||||
|
|
||||||
- `-e COLLECTOR_RUN_STARTUP=true` - forces the collector to run on startup (cron will be started after the collector
|
|
||||||
completes)
|
|
||||||
- `-e COLLECTOR_RUN_STARTUP_SLEEP=10` - if `COLLECTOR_RUN_STARTUP` is enabled, you can use this env variable to
|
|
||||||
configure the delay before the collector is run (default: `1` second). Used to ensure the web container has started
|
|
||||||
successfully.
|
|
||||||
|
|||||||
@@ -17,11 +17,3 @@ So changing from `master-omnibus -> latest` will be the same thing for all inten
|
|||||||
> NOTE: Previously, there was a `automated cron build` that ran on the `master` and `beta` branches.
|
> 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
|
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.
|
ensure that there's no broken external dependencies in our (unchanged) code. This `nightly` build no longer updates the `master-omnibus` tag.
|
||||||
|
|
||||||
# Running Docker `rootless`
|
|
||||||
|
|
||||||
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`
|
|
||||||
|
|||||||
@@ -1,45 +1,7 @@
|
|||||||
# InfluxDB Troubleshooting
|
# InfluxDB Troubleshooting
|
||||||
|
|
||||||
## Why??
|
## Installation
|
||||||
|
InfluxDB is a required dependency for Scrutiny v0.4.0+.
|
||||||
Scrutiny has many features, but the relevant one to this conversation is the "S.M.A.R.T metric tracking for historical
|
|
||||||
trends". Basically Scrutiny not only shows you the current SMART values, but how they've changed over weeks, months (or
|
|
||||||
even years).
|
|
||||||
|
|
||||||
To efficiently handle that data at scale (and to make my life easier as a developer) I decided to add InfluxDB as a
|
|
||||||
dependency. It's a dedicated timeseries database, as opposed to the general purpose sqlite DB I used before. I also did
|
|
||||||
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+.
|
|
||||||
|
|
||||||
https://docs.influxdata.com/influxdb/v2.2/install/
|
https://docs.influxdata.com/influxdb/v2.2/install/
|
||||||
|
|
||||||
@@ -104,349 +66,12 @@ panic: failed to check influxdb setup status - parse "://:": missing protocol sc
|
|||||||
As discussed in [#248](https://github.com/AnalogJ/scrutiny/issues/248) and [#234](https://github.com/AnalogJ/scrutiny/issues/234),
|
As discussed in [#248](https://github.com/AnalogJ/scrutiny/issues/248) and [#234](https://github.com/AnalogJ/scrutiny/issues/234),
|
||||||
this usually related to either:
|
this usually related to either:
|
||||||
|
|
||||||
- Upgrading from the LSIO Scrutiny image to the Official Scrutiny image, without removing LSIO specific environmental
|
- Upgrading from the LSIO Scrutiny image to the Official Scrutiny image, without removing LSIO specific environmental variables
|
||||||
variables
|
- remove the `SCRUTINY_WEB=true` and `SCRUTINY_COLLECTOR=true` environmental variables. They were used by the LSIO image, but are unnecessary and cause issues with the official Scrutiny image.
|
||||||
- remove the `SCRUTINY_WEB=true` and `SCRUTINY_COLLECTOR=true` environmental variables. They were used by the LSIO
|
- Updated versions of the [LSIO Scrutiny images are broken](https://github.com/linuxserver/docker-scrutiny/issues/22), as they have not installed InfluxDB which is a required dependency of Scrutiny v0.4.x
|
||||||
image, but are unnecessary and cause issues with the official Scrutiny image.
|
- You can revert to an earlier version of the LSIO image (`lscr.io/linuxserver/scrutiny:060ac7b8-ls34`), or just change to the official Scrutiny image (`ghcr.io/analogj/scrutiny:master-omnibus`)
|
||||||
- Change your volume mappings to `/opt/scrutiny` from `/scrutiny`
|
|
||||||
- Updated versions of the [LSIO Scrutiny images are broken](https://github.com/linuxserver/docker-scrutiny/issues/22),
|
|
||||||
as they have not installed InfluxDB which is a required dependency of Scrutiny v0.4.x
|
|
||||||
- You can revert to an earlier version of the LSIO image (`lscr.io/linuxserver/scrutiny:060ac7b8-ls34`), or just
|
|
||||||
change to the official Scrutiny image (`ghcr.io/analogj/scrutiny:master-omnibus`)
|
|
||||||
|
|
||||||
Here's a couple of confirmed working docker-compose files that you may want to look at:
|
Here's a couple of confirmed working docker-compose files that you may want to look at:
|
||||||
|
|
||||||
- https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml
|
- https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml
|
||||||
- https://github.com/AnalogJ/scrutiny/blob/master/docker/example.omnibus.docker-compose.yml
|
- https://github.com/AnalogJ/scrutiny/blob/master/docker/example.omnibus.docker-compose.yml
|
||||||
|
|
||||||
## Bring your own InfluxDB
|
|
||||||
|
|
||||||
> WARNING: Most users should not follow these steps. This is ONLY for users who have an EXISTING InfluxDB installation which contains data from multiple services.
|
|
||||||
> The Scrutiny Docker omnibus image includes an empty InfluxDB instance which it can configure.
|
|
||||||
> If you're deploying manually or via Hub/Spoke, you can just follow the installation instructions, Scrutiny knows how
|
|
||||||
> to run the first-time setup automatically.
|
|
||||||
|
|
||||||
The goal here is to create an InfluxDB API key with minimal permissions for use by Scrutiny.
|
|
||||||
|
|
||||||
- Create Scrutiny buckets (`metrics`, `metrics_weekly`, `metrics_monthly`, `metrics_yearly`) with placeholder config
|
|
||||||
- Create Downsampling tasks (`tsk-weekly-aggr`, `tsk-monthly-aggr`, `tsk-yearly-aggr`) with placeholder script.
|
|
||||||
- Create API token with restricted scope
|
|
||||||
- NOTE: Placeholder bucket & task configuration will be replaced automatically by Scrutiny during startup
|
|
||||||
|
|
||||||
The placeholder buckets and tasks need to be created before the API token can be created, as the resource ID's need to
|
|
||||||
exist for the scope restriction to work.
|
|
||||||
|
|
||||||
Scopes:
|
|
||||||
|
|
||||||
- `orgs`: read - required for scrutiny to find it's configured org_id
|
|
||||||
- `tasks`: scrutiny specific read/write access - Scrutiny only needs access to the downsampling tasks you created above
|
|
||||||
- `buckets`: scrutiny specific read/write access - Scrutiny only needs access to the buckets you created above
|
|
||||||
|
|
||||||
### Setup Environmental Variables
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# replace the following values with correct values for your InfluxDB installation
|
|
||||||
export INFLUXDB_ADMIN_TOKEN=pCqRq7xxxxxx-FZgNLfstIs0w==
|
|
||||||
export INFLUXDB_ORG_ID=b2495xxxxx
|
|
||||||
export INFLUXDB_HOSTNAME=http://localhost:8086
|
|
||||||
|
|
||||||
# if you want to change the bucket name prefix below, you'll also need to update the setting in the scrutiny.yaml config file.
|
|
||||||
export INFLUXDB_SCRUTINY_BUCKET_BASENAME=metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create placeholder buckets
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Click to expand!</summary>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
|
||||||
--data-binary @- << EOF
|
|
||||||
{
|
|
||||||
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}",
|
|
||||||
"retentionRules": []
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
|
||||||
--data-binary @- << EOF
|
|
||||||
{
|
|
||||||
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_weekly",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}",
|
|
||||||
"retentionRules": []
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
|
||||||
--data-binary @- << EOF
|
|
||||||
{
|
|
||||||
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_monthly",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}",
|
|
||||||
"retentionRules": []
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
|
||||||
--data-binary @- << EOF
|
|
||||||
{
|
|
||||||
"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_yearly",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}",
|
|
||||||
"retentionRules": []
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Create placeholder tasks
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Click to expand!</summary>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
|
||||||
--data-binary @- << EOF
|
|
||||||
{
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}",
|
|
||||||
"flux": "option task = {name: \"tsk-weekly-aggr\", every: 1y} \nyield now()"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
|
||||||
--data-binary @- << EOF
|
|
||||||
{
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}",
|
|
||||||
"flux": "option task = {name: \"tsk-monthly-aggr\", every: 1y} \nyield now()"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
|
||||||
--data-binary @- << EOF
|
|
||||||
{
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}",
|
|
||||||
"flux": "option task = {name: \"tsk-yearly-aggr\", every: 1y} \nyield now()"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Create InfluxDB API Token
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Click to expand!</summary>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# replace these values with placeholder bucket and task ids from your InfluxDB installation.
|
|
||||||
export INFLUXDB_SCRUTINY_BASE_BUCKET_ID=1e0709xxxx
|
|
||||||
export INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID=1af03dexxxxx
|
|
||||||
export INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID=b3c59c7xxxxx
|
|
||||||
export INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID=f381d8cxxxxx
|
|
||||||
|
|
||||||
export INFLUXDB_SCRUTINY_WEEKLY_TASK_ID=09a64ecxxxxx
|
|
||||||
export INFLUXDB_SCRUTINY_MONTHLY_TASK_ID=09a64xxxxx
|
|
||||||
export INFLUXDB_SCRUTINY_YEARLY_TASK_ID=09a64ecxxxxx
|
|
||||||
|
|
||||||
|
|
||||||
curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/authorizations \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \
|
|
||||||
--data-binary @- << EOF
|
|
||||||
{
|
|
||||||
"description": "scrutiny - restricted scope token",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"action": "read",
|
|
||||||
"resource": {
|
|
||||||
"type": "orgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "read",
|
|
||||||
"resource": {
|
|
||||||
"type": "tasks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "write",
|
|
||||||
"resource": {
|
|
||||||
"type": "tasks",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_WEEKLY_TASK_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "write",
|
|
||||||
"resource": {
|
|
||||||
"type": "tasks",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_MONTHLY_TASK_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "write",
|
|
||||||
"resource": {
|
|
||||||
"type": "tasks",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_YEARLY_TASK_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "read",
|
|
||||||
"resource": {
|
|
||||||
"type": "buckets",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_BASE_BUCKET_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "write",
|
|
||||||
"resource": {
|
|
||||||
"type": "buckets",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_BASE_BUCKET_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "read",
|
|
||||||
"resource": {
|
|
||||||
"type": "buckets",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "write",
|
|
||||||
"resource": {
|
|
||||||
"type": "buckets",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "read",
|
|
||||||
"resource": {
|
|
||||||
"type": "buckets",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "write",
|
|
||||||
"resource": {
|
|
||||||
"type": "buckets",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "read",
|
|
||||||
"resource": {
|
|
||||||
"type": "buckets",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "write",
|
|
||||||
"resource": {
|
|
||||||
"type": "buckets",
|
|
||||||
"id": "${INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID}",
|
|
||||||
"orgID": "${INFLUXDB_ORG_ID}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Save InfluxDB API Token
|
|
||||||
|
|
||||||
After running the Curl command above, you'll see a JSON response that looks like the following:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"token": "ksVU2t5SkQwYkvIxxxxxxxYt2xUt0uRKSbSF1Po0UQ==",
|
|
||||||
"status": "active",
|
|
||||||
"description": "scrutiny - restricted scope token",
|
|
||||||
"orgID": "b2495586xxxx",
|
|
||||||
"org": "my-org",
|
|
||||||
"user": "admin",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"action": "read",
|
|
||||||
"resource": {
|
|
||||||
"type": "orgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "read",
|
|
||||||
"resource": {
|
|
||||||
"type": "tasks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "write",
|
|
||||||
"resource": {
|
|
||||||
"type": "tasks",
|
|
||||||
"id": "09a64exxxxx",
|
|
||||||
"orgID": "b24955860xxxxx",
|
|
||||||
"org": "my-org"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You must copy the token field from the JSON response, and save it in your `scrutiny.yaml` config file. After that's
|
|
||||||
done, you can start the Scrutiny server
|
|
||||||
|
|
||||||
## Customize InfluxDB Admin Username & Password
|
|
||||||
|
|
||||||
The full set of InfluxDB configuration options are available
|
|
||||||
in [code](https://github.com/AnalogJ/scrutiny/blob/master/webapp/backend/pkg/config/config.go?rgh-link-date=2023-01-19T16%3A23%3A40Z#L49-L51)
|
|
||||||
.
|
|
||||||
|
|
||||||
During first startup Scrutiny will connect to the unprotected InfluxDB server, start the setup process (via API) using a
|
|
||||||
username and password of `admin`:`password12345` and then create an API token of `scrutiny-default-admin-token`.
|
|
||||||
|
|
||||||
After that's complete, it will use the api token for all subsequent communication with InfluxDB.
|
|
||||||
|
|
||||||
You can configure the values for the Admin username, password and token using the config file, or env variables:
|
|
||||||
|
|
||||||
#### Config File Example
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
web:
|
|
||||||
influxdb:
|
|
||||||
token: 'my-custom-token'
|
|
||||||
init_username: 'my-custom-username'
|
|
||||||
init_password: 'my-custom-password'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Environmental Variables Example
|
|
||||||
|
|
||||||
`SCRUTINY_WEB_INFLUXDB_TOKEN` , `SCRUTINY_WEB_INFLUXDB_INIT_USERNAME` and `SCRUTINY_WEB_INFLUXDB_INIT_PASSWORD`
|
|
||||||
|
|
||||||
It's safe to change the InfluxDB Admin username/password after setup has completed, only the API token is used for
|
|
||||||
subsequent communication with InfluxDB.
|
|
||||||
|
|||||||
@@ -21,26 +21,5 @@ SCRUTINY_DEVICE_NAME - eg. /dev/sda
|
|||||||
SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe
|
SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe
|
||||||
SCRUTINY_DEVICE_SERIAL - eg. WDDJ324KSO
|
SCRUTINY_DEVICE_SERIAL - eg. WDDJ324KSO
|
||||||
SCRUTINY_MESSAGE - eg. "Scrutiny SMART error notification for device: %s\nFailure Type: %s\nDevice Name: %s\nDevice Serial: %s\nDevice Type: %s\nDate: %s"
|
SCRUTINY_MESSAGE - eg. "Scrutiny SMART error notification for device: %s\nFailure Type: %s\nDevice Name: %s\nDevice Serial: %s\nDevice Type: %s\nDate: %s"
|
||||||
SCRUTINY_HOST_ID - (optional) eg. "my-custom-host-id"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Special Characters
|
|
||||||
|
|
||||||
`Shoutrrr` supports special characters in the username and password fields, however you'll need to url-encode the
|
|
||||||
username and the password separately.
|
|
||||||
|
|
||||||
- if your username is: `myname@example.com`
|
|
||||||
- if your password is `124@34$1`
|
|
||||||
|
|
||||||
Then your `shoutrrr` url will look something like:
|
|
||||||
|
|
||||||
- `smtp://myname%40example%2Ecom:124%4034%241@ms.my.domain.com:587`
|
|
||||||
|
|
||||||
# Testing Notifications
|
|
||||||
|
|
||||||
You can test that your notifications are configured correctly by posting an empty payload to the notifications health
|
|
||||||
check API.
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -X POST http://localhost:8080/api/health/notify
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -103,37 +103,4 @@ You may also configure these values using the following environmental variables
|
|||||||
- "9090:9090"
|
- "9090:9090"
|
||||||
```
|
```
|
||||||
3. run `docker-compose up`
|
3. run `docker-compose up`
|
||||||
4. visit [http://localhost:9090/custom/web](http://localhost:9090/custom/web) - access the scrutiny container via caddy reverse proxy
|
4. visit [http://localhost:9090/custom/web](http://localhost:9090/custom/web) - access the scrutiny container via caddy reverse proxy
|
||||||
|
|
||||||
## Traefik
|
|
||||||
|
|
||||||
Assuming, that you have Traefik up and running with [AutoDiscovery Using Traefik For Docker ](https://doc.traefik.io/traefik/providers/docker/),
|
|
||||||
here is an example of a `docker-compose.yml` file, with labels to enable Traefik reverse proxy and basic auth
|
|
||||||
```yaml
|
|
||||||
version: '3.5'
|
|
||||||
services:
|
|
||||||
scrutiny:
|
|
||||||
container_name: scrutiny
|
|
||||||
image: ghcr.io/analogj/scrutiny:master-omnibus
|
|
||||||
cap_add:
|
|
||||||
- SYS_RAWIO
|
|
||||||
- SYS_ADMIN
|
|
||||||
volumes:
|
|
||||||
- /run/udev:/run/udev:ro
|
|
||||||
- ./config:/opt/scrutiny/config
|
|
||||||
- ./influxdb:/opt/scrutiny/influxdb
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.http.routers.scrutiny.rule=Host(`example.com`)
|
|
||||||
- traefik.http.services.scrutiny.loadbalancer.server.port=8080
|
|
||||||
# 2 labels below are optional, in case you want basic auth in Traefik:
|
|
||||||
- traefik.http.routers.scrutiny.middlewares=auth
|
|
||||||
- "traefik.http.middlewares.auth.basicauth.users=user:$$2y$$05$$G11Wm/dlWpXHENK..m8se.zxvaE8USJBp1Ws56sSCrOcwWDjsYHni"
|
|
||||||
# Note: when used in docker-compose.yml all dollar signs in the hash need to be doubled for escaping.
|
|
||||||
# To create user:password pair, it's possible to use this command:
|
|
||||||
# echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g
|
|
||||||
devices:
|
|
||||||
- "/dev/sda"
|
|
||||||
- "/dev/sdb"
|
|
||||||
- "/dev/nvme0"
|
|
||||||
```
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Operating systems without udev
|
|
||||||
|
|
||||||
Some operating systems do not come with `udev` out of the box, for example Alpine Linux. In these instances you will not be able to bind `/run/udev` to the container for sharing device metadata. Some operating systems offer `udev` as a package that can be installed separately, or an alternative (such as `eudev` in the case of Alpine Linux) that provides the same functionality.
|
|
||||||
|
|
||||||
To install `eudev` in Alpine Linux (run as root):
|
|
||||||
|
|
||||||
```
|
|
||||||
apk add eudev
|
|
||||||
setup-udev
|
|
||||||
```
|
|
||||||
|
|
||||||
Once your `udev` implementation is installed, create `/run/udev` with the following command:
|
|
||||||
|
|
||||||
```
|
|
||||||
udevadm trigger
|
|
||||||
```
|
|
||||||
|
|
||||||
On Alpine Linux, this also has the benefit of creating symlinks to device serial numbers in `/dev/disk/by-id`.
|
|
||||||
+44
-71
@@ -1,89 +1,62 @@
|
|||||||
|
|
||||||
// SQLite Table(s)
|
// SQLite Table(s)
|
||||||
|
Table device {
|
||||||
|
created_at timestamp
|
||||||
|
|
||||||
Table Device {
|
wwn varchar [pk]
|
||||||
Archived bool
|
|
||||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
|
||||||
CreatedAt time
|
|
||||||
UpdatedAt time
|
|
||||||
DeletedAt time
|
|
||||||
|
|
||||||
WWN string
|
//user provided
|
||||||
|
label varchar
|
||||||
|
host_id varchar
|
||||||
|
|
||||||
DeviceName string
|
// smartctl provided
|
||||||
DeviceUUID string
|
device_name varchar
|
||||||
DeviceSerialID string
|
manufacturer varchar
|
||||||
DeviceLabel string
|
model_name varchar
|
||||||
|
interface_type varchar
|
||||||
|
interface_speed varchar
|
||||||
|
serial_number varchar
|
||||||
|
firmware varchar
|
||||||
|
rotational_speed varchar
|
||||||
|
capacity varchar
|
||||||
|
form_factor varchar
|
||||||
|
smart_support varchar
|
||||||
|
device_protocol varchar
|
||||||
|
device_type varchar
|
||||||
|
|
||||||
Manufacturer string
|
|
||||||
ModelName string
|
|
||||||
InterfaceType string
|
|
||||||
InterfaceSpeed string
|
|
||||||
SerialNumber string
|
|
||||||
Firmware string
|
|
||||||
RotationSpeed int
|
|
||||||
Capacity int64
|
|
||||||
FormFactor string
|
|
||||||
SmartSupport bool
|
|
||||||
DeviceProtocol string//protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
|
|
||||||
DeviceType string//device type is used for querying with -d/t flag, should only be used by collector.
|
|
||||||
|
|
||||||
// User provided metadata
|
|
||||||
Label string
|
|
||||||
HostId string
|
|
||||||
|
|
||||||
// Data set by Scrutiny
|
|
||||||
DeviceStatus enum
|
|
||||||
}
|
|
||||||
|
|
||||||
Table Setting {
|
|
||||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
|
||||||
|
|
||||||
SettingKeyName string
|
|
||||||
SettingKeyDescription string
|
|
||||||
SettingDataType string
|
|
||||||
|
|
||||||
SettingValueNumeric int64
|
|
||||||
SettingValueString string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// InfluxDB Tables
|
// InfluxDB Tables
|
||||||
Table SmartTemperature {
|
Table device_temperature {
|
||||||
Date time
|
//timestamp
|
||||||
DeviceWWN string //(tag)
|
created_at timestamp
|
||||||
Temp int64
|
|
||||||
}
|
//tags (indexed & queryable)
|
||||||
|
device_wwn varchar [pk]
|
||||||
|
|
||||||
|
//fields
|
||||||
|
temp bigint
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Table Smart {
|
Table smart_ata_results {
|
||||||
Date time
|
//timestamp
|
||||||
DeviceWWN string //(tag)
|
created_at timestamp
|
||||||
DeviceProtocol string
|
|
||||||
|
|
||||||
//Metrics (fields)
|
//tags (indexed & queryable)
|
||||||
Temp int64
|
device_wwn varchar [pk]
|
||||||
PowerOnHours int64
|
smart_status varchar
|
||||||
PowerCycleCount int64
|
scrutiny_status varchar
|
||||||
|
|
||||||
//Smart Status
|
|
||||||
Status enum
|
|
||||||
|
|
||||||
//SMART Attributes (fields)
|
|
||||||
Attr_ID_AttributeId int
|
//fields
|
||||||
Attr_ID_Value int64
|
temp bigint
|
||||||
Attr_ID_Threshold int64
|
power_on_hours bigint
|
||||||
Attr_ID_Worst int64
|
power_cycle_count bigint
|
||||||
Attr_ID_RawValue int64
|
|
||||||
Attr_ID_RawString string
|
|
||||||
Attr_ID_WhenFailed string
|
|
||||||
//Generated data
|
|
||||||
Attr_ID_TransformedValue int64
|
|
||||||
Attr_ID_Status enum
|
|
||||||
Attr_ID_StatusReason string
|
|
||||||
Attr_ID_FailureRate float64
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ref: Device.WWN < Smart.DeviceWWN
|
Ref: device.wwn < smart_ata_results.device_wwn
|
||||||
Ref: Device.WWN < SmartTemperature.DeviceWWN
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 78 KiB |
@@ -31,10 +31,6 @@ devices:
|
|||||||
# - device: /dev/sda
|
# - device: /dev/sda
|
||||||
# type: 'sat'
|
# type: 'sat'
|
||||||
#
|
#
|
||||||
# # example for using `-d sat,auto`, notice the square brackets (workaround for #418)
|
|
||||||
# - device: /dev/sda
|
|
||||||
# type: ['sat,auto']
|
|
||||||
#
|
|
||||||
# # example to show how to ignore a specific disk/device.
|
# # example to show how to ignore a specific disk/device.
|
||||||
# - device: /dev/sda
|
# - device: /dev/sda
|
||||||
# ignore: true
|
# ignore: true
|
||||||
@@ -81,7 +77,6 @@ devices:
|
|||||||
# metrics_scan_args: '--scan --json' # used to detect 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_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_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
|
|
||||||
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|||||||
+3
-11
@@ -47,11 +47,6 @@ web:
|
|||||||
# org: 'my-org'
|
# org: 'my-org'
|
||||||
# bucket: 'bucket'
|
# bucket: 'bucket'
|
||||||
retention_policy: true
|
retention_policy: true
|
||||||
# if you wish to disable TLS certificate verification,
|
|
||||||
# when using self-signed certificates for example,
|
|
||||||
# then uncomment the lines below and set `insecure_skip_verify: true`
|
|
||||||
# tls:
|
|
||||||
# insecure_skip_verify: false
|
|
||||||
|
|
||||||
log:
|
log:
|
||||||
file: '' #absolute or relative paths allowed, eg. web.log
|
file: '' #absolute or relative paths allowed, eg. web.log
|
||||||
@@ -60,14 +55,10 @@ log:
|
|||||||
|
|
||||||
# Notification "urls" look like the following. For more information about service specific configuration see
|
# Notification "urls" look like the following. For more information about service specific configuration see
|
||||||
# Shoutrrr's documentation: https://containrrr.dev/shoutrrr/services/overview/
|
# Shoutrrr's documentation: https://containrrr.dev/shoutrrr/services/overview/
|
||||||
#
|
|
||||||
# note, usernames and passwords containing special characters will need to be urlencoded.
|
|
||||||
# if your username is: "myname@example.com" and your password is "124@34$1"
|
|
||||||
# your shoutrrr url will look like: "smtp://myname%40example%2Ecom:124%4034%241@ms.my.domain.com:587"
|
|
||||||
|
|
||||||
#notify:
|
#notify:
|
||||||
# urls:
|
# urls:
|
||||||
# - "discord://token@webhookid"
|
# - "discord://token@channel"
|
||||||
# - "telegram://token@telegram?channels=channel-1[,channel-2,...]"
|
# - "telegram://token@telegram?channels=channel-1[,channel-2,...]"
|
||||||
# - "pushover://shoutrrr:apiToken@userKey/?priority=1&devices=device1[,device2, ...]"
|
# - "pushover://shoutrrr:apiToken@userKey/?priority=1&devices=device1[,device2, ...]"
|
||||||
# - "slack://[botname@]token-a/token-b/token-c"
|
# - "slack://[botname@]token-a/token-b/token-c"
|
||||||
@@ -77,12 +68,13 @@ log:
|
|||||||
# - "pushbullet://api-token[/device/#channel/email]"
|
# - "pushbullet://api-token[/device/#channel/email]"
|
||||||
# - "ifttt://key/?events=event1[,event2,...]&value1=value1&value2=value2&value3=value3"
|
# - "ifttt://key/?events=event1[,event2,...]&value1=value1&value2=value2&value3=value3"
|
||||||
# - "mattermost://[username@]mattermost-host/token[/channel]"
|
# - "mattermost://[username@]mattermost-host/token[/channel]"
|
||||||
# - "ntfy://username:password@host:port/topic"
|
|
||||||
# - "hangouts://chat.googleapis.com/v1/spaces/FOO/messages?key=bar&token=baz"
|
# - "hangouts://chat.googleapis.com/v1/spaces/FOO/messages?key=bar&token=baz"
|
||||||
# - "zulip://bot-mail:bot-key@zulip-domain/?stream=name-or-id&topic=name"
|
# - "zulip://bot-mail:bot-key@zulip-domain/?stream=name-or-id&topic=name"
|
||||||
# - "join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]"
|
# - "join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]"
|
||||||
# - "script:///file/path/on/disk"
|
# - "script:///file/path/on/disk"
|
||||||
# - "https://www.example.com/path"
|
# - "https://www.example.com/path"
|
||||||
|
# filter_attributes: 'all' # options: 'all' or 'critical'
|
||||||
|
# level: 'fail' # options: 'fail', 'fail_scrutiny', 'fail_smart'
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
# FEATURES COMING SOON
|
# FEATURES COMING SOON
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
module github.com/analogj/scrutiny
|
module github.com/analogj/scrutiny
|
||||||
|
|
||||||
go 1.20
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
|
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
|
||||||
github.com/containrrr/shoutrrr v0.8.0
|
github.com/containrrr/shoutrrr v0.4.4
|
||||||
github.com/fatih/color v1.15.0
|
github.com/fatih/color v1.10.0
|
||||||
github.com/gin-gonic/gin v1.6.3
|
github.com/gin-gonic/gin v1.6.3
|
||||||
github.com/glebarez/sqlite v1.4.5
|
github.com/glebarez/sqlite v1.4.5
|
||||||
github.com/go-gormigrate/gormigrate/v2 v2.0.0
|
github.com/go-gormigrate/gormigrate/v2 v2.0.0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.4.3
|
||||||
github.com/influxdata/influxdb-client-go/v2 v2.9.0
|
github.com/influxdata/influxdb-client-go/v2 v2.9.0
|
||||||
github.com/jaypipes/ghw v0.6.1
|
github.com/jaypipes/ghw v0.6.1
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.2.2
|
||||||
github.com/samber/lo v1.25.0
|
github.com/sirupsen/logrus v1.4.2
|
||||||
github.com/sirupsen/logrus v1.6.0
|
github.com/spf13/viper v1.7.0
|
||||||
github.com/spf13/viper v1.15.0
|
github.com/stretchr/testify v1.7.1
|
||||||
github.com/stretchr/testify v1.8.1
|
|
||||||
github.com/urfave/cli/v2 v2.2.0
|
github.com/urfave/cli/v2 v2.2.0
|
||||||
golang.org/x/sync v0.1.0
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||||
gorm.io/gorm v1.23.5
|
gorm.io/gorm v1.23.5
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
|
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/citilinkru/libudev v1.0.0 // indirect
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/deepmap/oapi-codegen v1.8.2 // indirect
|
github.com/deepmap/oapi-codegen v1.8.2 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.17.2 // indirect
|
github.com/glebarez/go-sqlite v1.17.2 // indirect
|
||||||
@@ -35,47 +35,54 @@ require (
|
|||||||
github.com/go-playground/locales v0.13.0 // indirect
|
github.com/go-playground/locales v0.13.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.2.0 // indirect
|
github.com/go-playground/validator/v10 v10.2.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.4.2 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
|
||||||
github.com/jaypipes/pcidb v0.5.0 // indirect
|
github.com/jaypipes/pcidb v0.5.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.4 // indirect
|
github.com/jinzhu/now v1.1.4 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.9 // indirect
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
|
github.com/klauspost/compress v1.11.7 // indirect
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||||
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 // indirect
|
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 // indirect
|
||||||
github.com/leodido/go-urn v1.2.0 // indirect
|
github.com/leodido/go-urn v1.2.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
github.com/nxadm/tail v1.4.6 // indirect
|
||||||
|
github.com/onsi/ginkgo v1.14.2 // indirect
|
||||||
|
github.com/pelletier/go-toml v1.7.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||||
github.com/spf13/afero v1.9.3 // indirect
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
github.com/spf13/cast v1.5.0 // indirect
|
github.com/spf13/afero v1.2.2 // indirect
|
||||||
|
github.com/spf13/cast v1.3.1 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.4.2 // indirect
|
github.com/subosito/gotenv v1.2.0 // indirect
|
||||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||||
golang.org/x/crypto v0.1.0 // indirect
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
|
||||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
|
||||||
golang.org/x/net v0.8.0 // indirect
|
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect
|
||||||
golang.org/x/sys v0.7.0 // indirect
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
|
||||||
golang.org/x/term v0.6.0 // indirect
|
golang.org/x/text v0.3.5 // indirect
|
||||||
golang.org/x/text v0.8.0 // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.23.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.55.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||||
|
gosrc.io/xmpp v0.5.1 // indirect
|
||||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
|
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
|
||||||
modernc.org/libc v1.16.8 // indirect
|
modernc.org/libc v1.16.8 // indirect
|
||||||
modernc.org/mathutil v1.4.1 // indirect
|
modernc.org/mathutil v1.4.1 // indirect
|
||||||
modernc.org/memory v1.1.1 // indirect
|
modernc.org/memory v1.1.1 // indirect
|
||||||
modernc.org/sqlite v1.17.2 // indirect
|
modernc.org/sqlite v1.17.2 // indirect
|
||||||
|
nhooyr.io/websocket v1.8.6 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,63 +3,59 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
|||||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
|
||||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
|
||||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
|
||||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
|
||||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
|
||||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
|
||||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
|
||||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
|
||||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
|
||||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
|
||||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
|
||||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
|
||||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
|
||||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
|
||||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
|
||||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
|
||||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
|
||||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
|
||||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
|
||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
|
||||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
|
||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
|
||||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
||||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||||
|
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 h1:wsrSjiqQtseStRIoLLxS4C5IEtXkazZVEPDHq8jW7r8=
|
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 h1:wsrSjiqQtseStRIoLLxS4C5IEtXkazZVEPDHq8jW7r8=
|
||||||
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14/go.mod h1:lJQVqFKMV5/oDGYR2bra2OljcF3CvolAoyDRyOA4k4E=
|
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14/go.mod h1:lJQVqFKMV5/oDGYR2bra2OljcF3CvolAoyDRyOA4k4E=
|
||||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||||
|
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||||
|
github.com/citilinkru/libudev v1.0.0 h1:upErSdhsJGdiKxwxPmvcz43fwJJD9R+y1j8BqU4wHog=
|
||||||
|
github.com/citilinkru/libudev v1.0.0/go.mod h1:yaNdhdtfJMs5flqeXzUOMO0mT9QnyNh/U/jdY4WhA/I=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
|
||||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
|
github.com/containrrr/shoutrrr v0.4.4 h1:vHZ4E/76pKVY+Jyn/qhBz3X540Bn8NI5ppPHK4PyILY=
|
||||||
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
github.com/containrrr/shoutrrr v0.4.4/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -70,18 +66,16 @@ github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRk
|
|||||||
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzqk8QCaRC4os14xoKDdbHqqlJtJA0oc1ZAjg=
|
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzqk8QCaRC4os14xoKDdbHqqlJtJA0oc1ZAjg=
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
|
||||||
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
|
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
|
||||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
@@ -95,11 +89,13 @@ github.com/glebarez/sqlite v1.4.5 h1:oaJupO4X9iTn4sXRvP5Vs15BNvKh9dx5AQfciKlDvV4
|
|||||||
github.com/glebarez/sqlite v1.4.5/go.mod h1:6D+bB+DdXlEC4mO+pUFJWixVcnrHTIAJ9U6Ynnn4Lxk=
|
github.com/glebarez/sqlite v1.4.5/go.mod h1:6D+bB+DdXlEC4mO+pUFJWixVcnrHTIAJ9U6Ynnn4Lxk=
|
||||||
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
|
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/go-gormigrate/gormigrate/v2 v2.0.0 h1:e2A3Uznk4viUC4UuemuVgsNnvYZyOA8B3awlYk3UioU=
|
github.com/go-gormigrate/gormigrate/v2 v2.0.0 h1:e2A3Uznk4viUC4UuemuVgsNnvYZyOA8B3awlYk3UioU=
|
||||||
github.com/go-gormigrate/gormigrate/v2 v2.0.0/go.mod h1:YuVJ+D/dNt4HWrThTBnjgZuRbt7AuwINeg4q52ZE3Jw=
|
github.com/go-gormigrate/gormigrate/v2 v2.0.0/go.mod h1:YuVJ+D/dNt4HWrThTBnjgZuRbt7AuwINeg4q52ZE3Jw=
|
||||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
@@ -115,40 +111,36 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
|
|||||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
||||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
@@ -156,43 +148,52 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
|||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo=
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
|
||||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||||
|
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||||
|
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||||
|
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||||
|
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/influxdata/influxdb-client-go/v2 v2.9.0 h1:1Ejxpt+cpWkadefxd5xvVx7pFgFaafdNp1ItfHzKRW4=
|
github.com/influxdata/influxdb-client-go/v2 v2.9.0 h1:1Ejxpt+cpWkadefxd5xvVx7pFgFaafdNp1ItfHzKRW4=
|
||||||
github.com/influxdata/influxdb-client-go/v2 v2.9.0/go.mod h1:x7Jo5UHHl+w8wu8UnGiNobDDHygojXwJX4mx7rXGKMk=
|
github.com/influxdata/influxdb-client-go/v2 v2.9.0/go.mod h1:x7Jo5UHHl+w8wu8UnGiNobDDHygojXwJX4mx7rXGKMk=
|
||||||
@@ -248,7 +249,8 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
|
|||||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA=
|
||||||
|
github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||||
github.com/jaypipes/ghw v0.6.1 h1:Ewt3mdpiyhWotGyzg1ursV/6SnToGcG4215X6rR2af8=
|
github.com/jaypipes/ghw v0.6.1 h1:Ewt3mdpiyhWotGyzg1ursV/6SnToGcG4215X6rR2af8=
|
||||||
github.com/jaypipes/ghw v0.6.1/go.mod h1:QOXppNRCLGYR1H+hu09FxZPqjNt09bqUZUnOL3Rcero=
|
github.com/jaypipes/ghw v0.6.1/go.mod h1:QOXppNRCLGYR1H+hu09FxZPqjNt09bqUZUnOL3Rcero=
|
||||||
github.com/jaypipes/pcidb v0.5.0 h1:4W5gZ+G7QxydevI8/MmmKdnIPJpURqJ2JNXTzfLxF5c=
|
github.com/jaypipes/pcidb v0.5.0 h1:4W5gZ+G7QxydevI8/MmmKdnIPJpURqJ2JNXTzfLxF5c=
|
||||||
@@ -261,24 +263,31 @@ github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
|
|||||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
|
github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg=
|
||||||
|
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 h1:dkCjlgGN81ahDFtM9R1x16gFGTa7ZvgZfdtAfM9lWOs=
|
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 h1:dkCjlgGN81ahDFtM9R1x16gFGTa7ZvgZfdtAfM9lWOs=
|
||||||
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA=
|
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA=
|
||||||
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
|
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
|
||||||
@@ -289,103 +298,148 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
|||||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||||
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||||
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
|
||||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||||
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
|
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
|
||||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||||
|
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||||
|
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
|
||||||
|
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
github.com/nxadm/tail v1.4.6 h1:11TGpSHY7Esh/i/qnq02Jo5oVrI1Gue8Slbq0ujPZFQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
|
||||||
|
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
||||||
|
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/samber/lo v1.25.0 h1:H8F6cB0RotRdgcRCivTByAQePaYhGMdOTJIj2QFS2I0=
|
|
||||||
github.com/samber/lo v1.25.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
|
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||||
|
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||||
|
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
|
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
|
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||||
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
|
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
|
||||||
|
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||||
|
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
@@ -394,18 +448,14 @@ github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2
|
|||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
|
||||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
@@ -414,6 +464,9 @@ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKY
|
|||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
@@ -425,23 +478,13 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
|
||||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
|
||||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
|
||||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
|
|
||||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -451,84 +494,59 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
|
|||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
|
||||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -536,154 +554,85 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 h1:D1v9ucDTYBtbz5vNuBbAhIMAGhQhJ6Ym5ah3maMVNX4=
|
||||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
|
||||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
|
||||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
|
||||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
|
||||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
|
||||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
|
||||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
|
||||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
|
||||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
|
||||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
|
||||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
@@ -693,79 +642,41 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
|||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
|
||||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
|
||||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
|
||||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
|
||||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
|
||||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
|
||||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
|
||||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
|
||||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
|
||||||
|
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw=
|
gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw=
|
||||||
gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw=
|
gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw=
|
||||||
gorm.io/driver/postgres v1.0.0 h1:Yh4jyFQ0a7F+JPU0Gtiam/eKmpT/XFc1FKxotGqc6FM=
|
gorm.io/driver/postgres v1.0.0 h1:Yh4jyFQ0a7F+JPU0Gtiam/eKmpT/XFc1FKxotGqc6FM=
|
||||||
@@ -778,13 +689,14 @@ gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
|||||||
gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
|
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
|
||||||
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
|
gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4=
|
||||||
|
gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
|
||||||
|
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
|
||||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
|
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
|
||||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
@@ -813,6 +725,10 @@ modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw
|
|||||||
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
||||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
||||||
|
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||||
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||||
|
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
|
||||||
|
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/command/with-contenv bash
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
if [ -n "${TZ}" ]
|
if [ -n "${TZ}" ]
|
||||||
then
|
then
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/command/with-contenv bash
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
# Cron runs in its own isolated environment (usually using only /etc/environment )
|
# 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
|
# 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}"
|
[[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}"
|
||||||
|
|
||||||
# replace placeholder with correct value
|
# 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,25 +1,13 @@
|
|||||||
#!/command/with-contenv bash
|
#!/usr/bin/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"
|
echo "waiting for scrutiny service to start"
|
||||||
s6-svwait -u /run/service/scrutiny
|
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
|
||||||
|
|
||||||
# wait until scrutiny is "Ready"
|
# 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
|
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)"
|
echo "starting scrutiny collector (run-once mode. subsequent calls will be triggered via cron service)"
|
||||||
/opt/scrutiny/bin/scrutiny-collector-metrics run
|
/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 @@
|
|||||||
#!/command/execlineb -S0
|
#!/usr/bin/execlineb -S0
|
||||||
|
|
||||||
echo "cron exiting"
|
echo "cron exiting"
|
||||||
s6-svscanctl -t /var/run/s6/services
|
s6-svscanctl -t /var/run/s6/services
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/command/with-contenv bash
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
echo "starting cron"
|
echo "starting cron"
|
||||||
cron -f -L 15
|
cron -f -L 15
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/command/with-contenv bash
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
mkdir -p /opt/scrutiny/influxdb/
|
mkdir -p /opt/scrutiny/influxdb/
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/command/with-contenv bash
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
echo "waiting for influxdb"
|
echo "waiting for influxdb"
|
||||||
until $(curl --output /dev/null --silent --head --fail http://localhost:8086/health); do echo "influxdb not ready" && sleep 5; done
|
until $(curl --output /dev/null --silent --head --fail http://localhost:8086/health); do echo "influxdb not ready" && sleep 5; done
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
|
||||||
"github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -29,18 +26,11 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
configFilePath := "/opt/scrutiny/config/scrutiny.yaml"
|
|
||||||
configFilePathAlternative := "/opt/scrutiny/config/scrutiny.yml"
|
|
||||||
if !utils.FileExists(configFilePath) && utils.FileExists(configFilePathAlternative) {
|
|
||||||
configFilePath = configFilePathAlternative
|
|
||||||
}
|
|
||||||
|
|
||||||
//we're going to load the config file manually, since we need to validate it.
|
//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
|
err = config.ReadConfig("/opt/scrutiny/config/scrutiny.yaml") // Find and read the config file
|
||||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||||
//ignore "could not find config file"
|
//ignore "could not find config file"
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.Print(color.HiRedString("CONFIG ERROR: %v", err))
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,18 +107,7 @@ OPTIONS:
|
|||||||
config.Set("log.file", c.String("log-file"))
|
config.Set("log.file", c.String("log-file"))
|
||||||
}
|
}
|
||||||
|
|
||||||
webLogger, logFile, err := CreateLogger(config)
|
webServer := web.AppEngine{Config: config}
|
||||||
if logFile != nil {
|
|
||||||
defer logFile.Close()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsData, err := json.Marshal(config.AllSettings())
|
|
||||||
webLogger.Debug(string(settingsData), err)
|
|
||||||
|
|
||||||
webServer := web.AppEngine{Config: config, Logger: webLogger}
|
|
||||||
|
|
||||||
return webServer.Start()
|
return webServer.Start()
|
||||||
},
|
},
|
||||||
@@ -161,27 +140,3 @@ OPTIONS:
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateLogger(appConfig config.Interface) (*logrus.Entry, *os.File, error) {
|
|
||||||
logger := logrus.WithFields(logrus.Fields{
|
|
||||||
"type": "web",
|
|
||||||
})
|
|
||||||
//set default log level
|
|
||||||
if level, err := logrus.ParseLevel(appConfig.GetString("log.level")); err == nil {
|
|
||||||
logger.Logger.SetLevel(level)
|
|
||||||
} else {
|
|
||||||
logger.Logger.SetLevel(logrus.InfoLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
var logFile *os.File
|
|
||||||
var err error
|
|
||||||
if appConfig.IsSet("log.file") && len(appConfig.GetString("log.file")) > 0 {
|
|
||||||
logFile, err = os.OpenFile(appConfig.GetString("log.file"), os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
logger.Logger.Errorf("Failed to open log file %s for output: %s", appConfig.GetString("log.file"), err)
|
|
||||||
return nil, logFile, err
|
|
||||||
}
|
|
||||||
logger.Logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
|
|
||||||
}
|
|
||||||
return logger, logFile, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/analogj/go-util/utils"
|
"github.com/analogj/go-util/utils"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"log"
|
"log"
|
||||||
@@ -9,8 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DB_USER_SETTINGS_SUBKEY = "user"
|
|
||||||
|
|
||||||
// When initializing this class the following methods must be called:
|
// When initializing this class the following methods must be called:
|
||||||
// Config.New
|
// Config.New
|
||||||
// Config.Init
|
// Config.Init
|
||||||
@@ -40,6 +39,8 @@ func (c *configuration) Init() error {
|
|||||||
c.SetDefault("log.file", "")
|
c.SetDefault("log.file", "")
|
||||||
|
|
||||||
c.SetDefault("notify.urls", []string{})
|
c.SetDefault("notify.urls", []string{})
|
||||||
|
c.SetDefault("notify.filter_attributes", pkg.NotifyFilterAttributesAll)
|
||||||
|
c.SetDefault("notify.level", pkg.NotifyLevelFail)
|
||||||
|
|
||||||
c.SetDefault("web.influxdb.scheme", "http")
|
c.SetDefault("web.influxdb.scheme", "http")
|
||||||
c.SetDefault("web.influxdb.host", "localhost")
|
c.SetDefault("web.influxdb.host", "localhost")
|
||||||
@@ -49,12 +50,22 @@ func (c *configuration) Init() error {
|
|||||||
c.SetDefault("web.influxdb.init_username", "admin")
|
c.SetDefault("web.influxdb.init_username", "admin")
|
||||||
c.SetDefault("web.influxdb.init_password", "password12345")
|
c.SetDefault("web.influxdb.init_password", "password12345")
|
||||||
c.SetDefault("web.influxdb.token", "scrutiny-default-admin-token")
|
c.SetDefault("web.influxdb.token", "scrutiny-default-admin-token")
|
||||||
c.SetDefault("web.influxdb.tls.insecure_skip_verify", false)
|
|
||||||
c.SetDefault("web.influxdb.retention_policy", true)
|
c.SetDefault("web.influxdb.retention_policy", true)
|
||||||
|
|
||||||
//c.SetDefault("disks.include", []string{})
|
//c.SetDefault("disks.include", []string{})
|
||||||
//c.SetDefault("disks.exclude", []string{})
|
//c.SetDefault("disks.exclude", []string{})
|
||||||
|
|
||||||
|
//c.SetDefault("notify.metric.script", "/opt/scrutiny/config/notify-metrics.sh")
|
||||||
|
//c.SetDefault("notify.long.script", "/opt/scrutiny/config/notify-long-test.sh")
|
||||||
|
//c.SetDefault("notify.short.script", "/opt/scrutiny/config/notify-short-test.sh")
|
||||||
|
|
||||||
|
//c.SetDefault("collect.metric.enable", true)
|
||||||
|
//c.SetDefault("collect.metric.command", "-a -o on -S on")
|
||||||
|
//c.SetDefault("collect.long.enable", true)
|
||||||
|
//c.SetDefault("collect.long.command", "-a -o on -S on")
|
||||||
|
//c.SetDefault("collect.short.enable", true)
|
||||||
|
//c.SetDefault("collect.short.command", "-a -o on -S on")
|
||||||
|
|
||||||
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
|
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
|
||||||
c.SetConfigType("yaml")
|
c.SetConfigType("yaml")
|
||||||
//c.SetConfigName("drawbridge")
|
//c.SetConfigName("drawbridge")
|
||||||
@@ -66,18 +77,7 @@ func (c *configuration) Init() error {
|
|||||||
c.AutomaticEnv()
|
c.AutomaticEnv()
|
||||||
|
|
||||||
//CLI options will be added via the `Set()` function
|
//CLI options will be added via the `Set()` function
|
||||||
return c.ValidateConfig()
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
func (c *configuration) SubKeys(key string) []string {
|
|
||||||
return c.Sub(key).AllKeys()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *configuration) Sub(key string) Interface {
|
|
||||||
config := configuration{
|
|
||||||
Viper: c.Viper.Sub(key),
|
|
||||||
}
|
|
||||||
return &config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *configuration) ReadConfig(configFilePath string) error {
|
func (c *configuration) ReadConfig(configFilePath string) error {
|
||||||
@@ -120,18 +120,24 @@ func (c *configuration) ReadConfig(configFilePath string) error {
|
|||||||
// This function ensures that the merged config works correctly.
|
// This function ensures that the merged config works correctly.
|
||||||
func (c *configuration) ValidateConfig() error {
|
func (c *configuration) ValidateConfig() error {
|
||||||
|
|
||||||
//the following keys are deprecated, and no longer supported
|
////deserialize Questions
|
||||||
/*
|
//questionsMap := map[string]Question{}
|
||||||
- notify.filter_attributes (replaced by metrics.status.filter_attributes SETTING)
|
//err := c.UnmarshalKey("questions", &questionsMap)
|
||||||
- notify.level (replaced by metrics.notify.level and metrics.status.threshold SETTING)
|
//
|
||||||
*/
|
//if err != nil {
|
||||||
//TODO add docs and upgrade doc.
|
// log.Printf("questions could not be deserialized correctly. %v", err)
|
||||||
if c.IsSet("notify.filter_attributes") {
|
// return err
|
||||||
return errors.ConfigValidationError("`notify.filter_attributes` configuration option is deprecated. Replaced by option in Dashboard Settings page")
|
//}
|
||||||
}
|
//
|
||||||
if c.IsSet("notify.level") {
|
//for _, v := range questionsMap {
|
||||||
return errors.ConfigValidationError("`notify.level` configuration option is deprecated. Replaced by option in Dashboard Settings page")
|
//
|
||||||
}
|
// typeContent, ok := v.Schema["type"].(string)
|
||||||
|
// if !ok || len(typeContent) == 0 {
|
||||||
|
// return errors.QuestionSyntaxError("`type` is required for questions")
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_MergeConfigMap(t *testing.T) {
|
|
||||||
//setup
|
|
||||||
testConfig := configuration{
|
|
||||||
Viper: viper.New(),
|
|
||||||
}
|
|
||||||
testConfig.Set("user.dashboard_display", "hello")
|
|
||||||
testConfig.SetDefault("user.layout", "hello")
|
|
||||||
|
|
||||||
mergeSettings := map[string]interface{}{
|
|
||||||
"user": map[string]interface{}{
|
|
||||||
"dashboard_display": "dashboard_display",
|
|
||||||
"layout": "layout",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
//test
|
|
||||||
err := testConfig.MergeConfigMap(mergeSettings)
|
|
||||||
|
|
||||||
//verify
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// if using Set, the MergeConfigMap functionality will not override
|
|
||||||
// if using SetDefault, the MergeConfigMap will override correctly
|
|
||||||
require.Equal(t, "hello", testConfig.GetString("user.dashboard_display"))
|
|
||||||
require.Equal(t, "layout", testConfig.GetString("user.layout"))
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -12,17 +12,12 @@ type Interface interface {
|
|||||||
WriteConfig() error
|
WriteConfig() error
|
||||||
Set(key string, value interface{})
|
Set(key string, value interface{})
|
||||||
SetDefault(key string, value interface{})
|
SetDefault(key string, value interface{})
|
||||||
MergeConfigMap(cfg map[string]interface{}) error
|
|
||||||
|
|
||||||
Sub(key string) Interface
|
|
||||||
AllSettings() map[string]interface{}
|
AllSettings() map[string]interface{}
|
||||||
AllKeys() []string
|
|
||||||
SubKeys(key string) []string
|
|
||||||
IsSet(key string) bool
|
IsSet(key string) bool
|
||||||
Get(key string) interface{}
|
Get(key string) interface{}
|
||||||
GetBool(key string) bool
|
GetBool(key string) bool
|
||||||
GetInt(key string) int
|
GetInt(key string) int
|
||||||
GetInt64(key string) int64
|
|
||||||
GetString(key string) string
|
GetString(key string) string
|
||||||
GetStringSlice(key string) []string
|
GetStringSlice(key string) []string
|
||||||
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
|
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package mock_config
|
|||||||
import (
|
import (
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
config "github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
viper "github.com/spf13/viper"
|
viper "github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@@ -35,20 +34,6 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
|||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllKeys mocks base method.
|
|
||||||
func (m *MockInterface) AllKeys() []string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "AllKeys")
|
|
||||||
ret0, _ := ret[0].([]string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllKeys indicates an expected call of AllKeys.
|
|
||||||
func (mr *MockInterfaceMockRecorder) AllKeys() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllKeys", reflect.TypeOf((*MockInterface)(nil).AllKeys))
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllSettings mocks base method.
|
// AllSettings mocks base method.
|
||||||
func (m *MockInterface) AllSettings() map[string]interface{} {
|
func (m *MockInterface) AllSettings() map[string]interface{} {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -105,20 +90,6 @@ func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt64 mocks base method.
|
|
||||||
func (m *MockInterface) GetInt64(key string) int64 {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetInt64", key)
|
|
||||||
ret0, _ := ret[0].(int64)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInt64 indicates an expected call of GetInt64.
|
|
||||||
func (mr *MockInterfaceMockRecorder) GetInt64(key interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt64", reflect.TypeOf((*MockInterface)(nil).GetInt64), key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetString mocks base method.
|
// GetString mocks base method.
|
||||||
func (m *MockInterface) GetString(key string) string {
|
func (m *MockInterface) GetString(key string) string {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -175,20 +146,6 @@ func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeConfigMap mocks base method.
|
|
||||||
func (m *MockInterface) MergeConfigMap(cfg map[string]interface{}) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "MergeConfigMap", cfg)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeConfigMap indicates an expected call of MergeConfigMap.
|
|
||||||
func (mr *MockInterfaceMockRecorder) MergeConfigMap(cfg interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MergeConfigMap", reflect.TypeOf((*MockInterface)(nil).MergeConfigMap), cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadConfig mocks base method.
|
// ReadConfig mocks base method.
|
||||||
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -227,34 +184,6 @@ func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub mocks base method.
|
|
||||||
func (m *MockInterface) Sub(key string) config.Interface {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Sub", key)
|
|
||||||
ret0, _ := ret[0].(config.Interface)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sub indicates an expected call of Sub.
|
|
||||||
func (mr *MockInterfaceMockRecorder) Sub(key interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sub", reflect.TypeOf((*MockInterface)(nil).Sub), key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubKeys mocks base method.
|
|
||||||
func (m *MockInterface) SubKeys(key string) []string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SubKeys", key)
|
|
||||||
ret0, _ := ret[0].([]string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubKeys indicates an expected call of SubKeys.
|
|
||||||
func (mr *MockInterfaceMockRecorder) SubKeys(key interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubKeys", reflect.TypeOf((*MockInterface)(nil).SubKeys), key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalKey mocks base method.
|
// UnmarshalKey mocks base method.
|
||||||
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
|
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ const DeviceProtocolAta = "ATA"
|
|||||||
const DeviceProtocolScsi = "SCSI"
|
const DeviceProtocolScsi = "SCSI"
|
||||||
const DeviceProtocolNvme = "NVMe"
|
const DeviceProtocolNvme = "NVMe"
|
||||||
|
|
||||||
//go:generate stringer -type=AttributeStatus
|
const NotifyFilterAttributesAll = "all"
|
||||||
// AttributeStatus bitwise flag, 1,2,4,8,16,32,etc
|
const NotifyFilterAttributesCritical = "critical"
|
||||||
type AttributeStatus uint8
|
|
||||||
|
|
||||||
|
const NotifyLevelFail = "fail"
|
||||||
|
const NotifyLevelFailScrutiny = "fail_scrutiny"
|
||||||
|
const NotifyLevelFailSmart = "fail_smart"
|
||||||
|
|
||||||
|
//go:generate stringer -type=AttributeStatus
|
||||||
|
type AttributeStatus uint8
|
||||||
const (
|
const (
|
||||||
|
// AttributeStatusPassed binary, 1,2,4,8,16,32,etc
|
||||||
AttributeStatusPassed AttributeStatus = 0
|
AttributeStatusPassed AttributeStatus = 0
|
||||||
AttributeStatusFailedSmart AttributeStatus = 1
|
AttributeStatusFailedSmart AttributeStatus = 1
|
||||||
AttributeStatusWarningScrutiny AttributeStatus = 2
|
AttributeStatusWarningScrutiny AttributeStatus = 2
|
||||||
@@ -24,10 +30,9 @@ func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^
|
|||||||
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
|
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
|
||||||
|
|
||||||
//go:generate stringer -type=DeviceStatus
|
//go:generate stringer -type=DeviceStatus
|
||||||
// DeviceStatus bitwise flag, 1,2,4,8,16,32,etc
|
|
||||||
type DeviceStatus uint8
|
type DeviceStatus uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// DeviceStatusPassed binary, 1,2,4,8,16,32,etc
|
||||||
DeviceStatusPassed DeviceStatus = 0
|
DeviceStatusPassed DeviceStatus = 0
|
||||||
DeviceStatusFailedSmart DeviceStatus = 1
|
DeviceStatusFailedSmart DeviceStatus = 1
|
||||||
DeviceStatusFailedScrutiny DeviceStatus = 2
|
DeviceStatusFailedScrutiny DeviceStatus = 2
|
||||||
@@ -37,29 +42,3 @@ func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | flag }
|
|||||||
func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
|
func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
|
||||||
func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
|
func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
|
||||||
func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 }
|
func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 }
|
||||||
|
|
||||||
// Metrics Specific Filtering & Threshold Constants
|
|
||||||
type MetricsNotifyLevel int64
|
|
||||||
|
|
||||||
const (
|
|
||||||
MetricsNotifyLevelWarn MetricsNotifyLevel = 1
|
|
||||||
MetricsNotifyLevelFail MetricsNotifyLevel = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
type MetricsStatusFilterAttributes int64
|
|
||||||
|
|
||||||
const (
|
|
||||||
MetricsStatusFilterAttributesAll MetricsStatusFilterAttributes = 0
|
|
||||||
MetricsStatusFilterAttributesCritical MetricsStatusFilterAttributes = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// MetricsStatusThreshold bitwise flag, 1,2,4,8,16,32,etc
|
|
||||||
type MetricsStatusThreshold int64
|
|
||||||
|
|
||||||
const (
|
|
||||||
MetricsStatusThresholdSmart MetricsStatusThreshold = 1
|
|
||||||
MetricsStatusThresholdScrutiny MetricsStatusThreshold = 2
|
|
||||||
|
|
||||||
//shortcut
|
|
||||||
MetricsStatusThresholdBoth MetricsStatusThreshold = 3
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sortSmartMeasurementsDesc(smartResults []measurements.Smart) {
|
||||||
|
sort.SliceStable(smartResults, func(i, j int) bool {
|
||||||
|
return smartResults[i].Date.After(smartResults[j].Date)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_sortSmartMeasurementsDesc_LatestFirst(t *testing.T) {
|
||||||
|
//setup
|
||||||
|
timeNow := time.Now()
|
||||||
|
smartResults := []measurements.Smart{
|
||||||
|
{
|
||||||
|
Date: timeNow.AddDate(0, 0, -2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Date: timeNow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Date: timeNow.AddDate(0, 0, -1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//test
|
||||||
|
sortSmartMeasurementsDesc(smartResults)
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.Equal(t, smartResults[0].Date, timeNow)
|
||||||
|
}
|
||||||
@@ -2,35 +2,30 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
"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/collector"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create mock using:
|
|
||||||
// mockgen -source=webapp/backend/pkg/database/interface.go -destination=webapp/backend/pkg/database/mock/mock_database.go
|
|
||||||
type DeviceRepo interface {
|
type DeviceRepo interface {
|
||||||
Close() error
|
Close() error
|
||||||
HealthCheck(ctx context.Context) error
|
|
||||||
|
//GetSettings()
|
||||||
|
//SaveSetting()
|
||||||
|
|
||||||
RegisterDevice(ctx context.Context, dev models.Device) error
|
RegisterDevice(ctx context.Context, dev models.Device) error
|
||||||
GetDevices(ctx context.Context) ([]models.Device, error)
|
GetDevices(ctx context.Context) ([]models.Device, error)
|
||||||
UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error)
|
UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error)
|
||||||
UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error)
|
UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error)
|
||||||
GetDeviceDetails(ctx context.Context, wwn string) (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
|
DeleteDevice(ctx context.Context, wwn string) error
|
||||||
|
|
||||||
SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, 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)
|
GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error)
|
||||||
|
|
||||||
SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error
|
SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo) error
|
||||||
|
|
||||||
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
|
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
|
||||||
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error)
|
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error)
|
||||||
|
|
||||||
LoadSettings(ctx context.Context) (*models.Settings, error)
|
|
||||||
SaveSettings(ctx context.Context, settings models.Settings) error
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Deprecated: m20220509170100.Device is deprecated, only used by db migrations
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
@@ -15,9 +14,9 @@ type Device struct {
|
|||||||
WWN string `json:"wwn" gorm:"primary_key"`
|
WWN string `json:"wwn" gorm:"primary_key"`
|
||||||
|
|
||||||
DeviceName string `json:"device_name"`
|
DeviceName string `json:"device_name"`
|
||||||
DeviceUUID string `json:"device_uuid"`
|
DeviceUUID string `json:"device_uuid"`
|
||||||
DeviceSerialID string `json:"device_serial_id"`
|
DeviceSerialID string `json:"device_serial_id"`
|
||||||
DeviceLabel string `json:"device_label"`
|
DeviceLabel string `json:"device_label"`
|
||||||
|
|
||||||
Manufacturer string `json:"manufacturer"`
|
Manufacturer string `json:"manufacturer"`
|
||||||
ModelName string `json:"model_name"`
|
ModelName string `json:"model_name"`
|
||||||
@@ -39,3 +38,4 @@ type Device struct {
|
|||||||
// Data set by Scrutiny
|
// Data set by Scrutiny
|
||||||
DeviceStatus pkg.DeviceStatus `json:"device_status"`
|
DeviceStatus pkg.DeviceStatus `json:"device_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package m20220716214900
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Setting struct {
|
|
||||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
|
||||||
gorm.Model
|
|
||||||
|
|
||||||
SettingKeyName string `json:"setting_key_name"`
|
|
||||||
SettingKeyDescription string `json:"setting_key_description"`
|
|
||||||
SettingDataType string `json:"setting_data_type"`
|
|
||||||
|
|
||||||
SettingValueNumeric int `json:"setting_value_numeric"`
|
|
||||||
SettingValueString string `json:"setting_value_string"`
|
|
||||||
SettingValueBool bool `json:"setting_value_bool"`
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: webapp/backend/pkg/database/interface.go
|
|
||||||
|
|
||||||
// Package mock_database is a generated GoMock package.
|
|
||||||
package mock_database
|
|
||||||
|
|
||||||
import (
|
|
||||||
context "context"
|
|
||||||
reflect "reflect"
|
|
||||||
|
|
||||||
pkg "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/collector"
|
|
||||||
measurements "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockDeviceRepo is a mock of DeviceRepo interface.
|
|
||||||
type MockDeviceRepo struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockDeviceRepoMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockDeviceRepoMockRecorder is the mock recorder for MockDeviceRepo.
|
|
||||||
type MockDeviceRepoMockRecorder struct {
|
|
||||||
mock *MockDeviceRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockDeviceRepo creates a new mock instance.
|
|
||||||
func NewMockDeviceRepo(ctrl *gomock.Controller) *MockDeviceRepo {
|
|
||||||
mock := &MockDeviceRepo{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockDeviceRepoMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockDeviceRepo) EXPECT() *MockDeviceRepoMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close mocks base method.
|
|
||||||
func (m *MockDeviceRepo) Close() error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Close")
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close indicates an expected call of Close.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) Close() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
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()
|
|
||||||
ret := m.ctrl.Call(m, "DeleteDevice", ctx, wwn)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteDevice indicates an expected call of DeleteDevice.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) DeleteDevice(ctx, wwn interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDevice", reflect.TypeOf((*MockDeviceRepo)(nil).DeleteDevice), ctx, wwn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDeviceDetails mocks base method.
|
|
||||||
func (m *MockDeviceRepo) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, wwn)
|
|
||||||
ret0, _ := ret[0].(models.Device)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDeviceDetails indicates an expected call of GetDeviceDetails.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) GetDeviceDetails(ctx, wwn interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceDetails", reflect.TypeOf((*MockDeviceRepo)(nil).GetDeviceDetails), ctx, wwn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDevices mocks base method.
|
|
||||||
func (m *MockDeviceRepo) GetDevices(ctx context.Context) ([]models.Device, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetDevices", ctx)
|
|
||||||
ret0, _ := ret[0].([]models.Device)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDevices indicates an expected call of GetDevices.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) GetDevices(ctx interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevices", reflect.TypeOf((*MockDeviceRepo)(nil).GetDevices), ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSmartAttributeHistory mocks base method.
|
|
||||||
func (m *MockDeviceRepo) GetSmartAttributeHistory(ctx context.Context, wwn, durationKey string, selectEntries, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetSmartAttributeHistory", ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
|
|
||||||
ret0, _ := ret[0].([]measurements.Smart)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSmartAttributeHistory indicates an expected call of GetSmartAttributeHistory.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) GetSmartAttributeHistory(ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartAttributeHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartAttributeHistory), ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSmartTemperatureHistory mocks base method.
|
|
||||||
func (m *MockDeviceRepo) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetSmartTemperatureHistory", ctx, durationKey)
|
|
||||||
ret0, _ := ret[0].(map[string][]measurements.SmartTemperature)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSmartTemperatureHistory indicates an expected call of GetSmartTemperatureHistory.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) GetSmartTemperatureHistory(ctx, durationKey interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartTemperatureHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartTemperatureHistory), ctx, durationKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSummary mocks base method.
|
|
||||||
func (m *MockDeviceRepo) GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetSummary", ctx)
|
|
||||||
ret0, _ := ret[0].(map[string]*models.DeviceSummary)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSummary indicates an expected call of GetSummary.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) GetSummary(ctx interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSummary", reflect.TypeOf((*MockDeviceRepo)(nil).GetSummary), ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HealthCheck mocks base method.
|
|
||||||
func (m *MockDeviceRepo) HealthCheck(ctx context.Context) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "HealthCheck", ctx)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// HealthCheck indicates an expected call of HealthCheck.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDeviceRepo)(nil).HealthCheck), ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadSettings mocks base method.
|
|
||||||
func (m *MockDeviceRepo) LoadSettings(ctx context.Context) (*models.Settings, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "LoadSettings", ctx)
|
|
||||||
ret0, _ := ret[0].(*models.Settings)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadSettings indicates an expected call of LoadSettings.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSettings", reflect.TypeOf((*MockDeviceRepo)(nil).LoadSettings), ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterDevice mocks base method.
|
|
||||||
func (m *MockDeviceRepo) RegisterDevice(ctx context.Context, dev models.Device) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "RegisterDevice", ctx, dev)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterDevice indicates an expected call of RegisterDevice.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterDevice", reflect.TypeOf((*MockDeviceRepo)(nil).RegisterDevice), ctx, dev)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveSettings mocks base method.
|
|
||||||
func (m *MockDeviceRepo) SaveSettings(ctx context.Context, settings models.Settings) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SaveSettings", ctx, settings)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveSettings indicates an expected call of SaveSettings.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSettings", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSettings), ctx, settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveSmartAttributes mocks base method.
|
|
||||||
func (m *MockDeviceRepo) SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, wwn, collectorSmartData)
|
|
||||||
ret0, _ := ret[0].(measurements.Smart)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveSmartAttributes indicates an expected call of SaveSmartAttributes.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartAttributes(ctx, wwn, collectorSmartData interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartAttributes", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartAttributes), ctx, wwn, collectorSmartData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveSmartTemperature mocks base method.
|
|
||||||
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, discardSCTTempHistory)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveSmartTemperature indicates an expected call of SaveSmartTemperature.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateDevice mocks base method.
|
|
||||||
func (m *MockDeviceRepo) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UpdateDevice", ctx, wwn, collectorSmartData)
|
|
||||||
ret0, _ := ret[0].(models.Device)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateDevice indicates an expected call of UpdateDevice.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) UpdateDevice(ctx, wwn, collectorSmartData interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDevice", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDevice), ctx, wwn, collectorSmartData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateDeviceStatus mocks base method.
|
|
||||||
func (m *MockDeviceRepo) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, wwn, status)
|
|
||||||
ret0, _ := ret[0].(models.Device)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateDeviceStatus indicates an expected call of UpdateDeviceStatus.
|
|
||||||
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceStatus(ctx, wwn, status interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceStatus", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceStatus), ctx, wwn, status)
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
@@ -63,20 +62,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
|||||||
// Gorm/SQLite setup
|
// Gorm/SQLite setup
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
globalLogger.Infof("Trying to connect to scrutiny sqlite db: %s\n", appConfig.GetString("web.database.location"))
|
globalLogger.Infof("Trying to connect to scrutiny sqlite db: %s\n", appConfig.GetString("web.database.location"))
|
||||||
|
database, err := gorm.Open(sqlite.Open(appConfig.GetString("web.database.location")), &gorm.Config{
|
||||||
// When a transaction cannot lock the database, because it is already locked by another one,
|
|
||||||
// SQLite by default throws an error: database is locked. This behavior is usually not appropriate when
|
|
||||||
// concurrent access is needed, typically when multiple processes write to the same database.
|
|
||||||
// PRAGMA busy_timeout lets you set a timeout or a handler for these events. When setting a timeout,
|
|
||||||
// SQLite will try the transaction multiple times within this timeout.
|
|
||||||
// fixes #341
|
|
||||||
// https://rsqlite.r-dbi.org/reference/sqlitesetbusyhandler
|
|
||||||
// retrying for 30000 milliseconds, 30seconds - this would be unreasonable for a distributed multi-tenant application,
|
|
||||||
// but should be fine for local usage.
|
|
||||||
pragmaStr := sqlitePragmaString(map[string]string{
|
|
||||||
"busy_timeout": "30000",
|
|
||||||
})
|
|
||||||
database, err := gorm.Open(sqlite.Open(appConfig.GetString("web.database.location")+pragmaStr), &gorm.Config{
|
|
||||||
//TODO: figure out how to log database queries again.
|
//TODO: figure out how to log database queries again.
|
||||||
//Logger: logger
|
//Logger: logger
|
||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
@@ -96,20 +82,11 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
|||||||
influxdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.influxdb.scheme"), appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
|
influxdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.influxdb.scheme"), appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
|
||||||
globalLogger.Debugf("InfluxDB url: %s", influxdbUrl)
|
globalLogger.Debugf("InfluxDB url: %s", influxdbUrl)
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
client := influxdb2.NewClient(influxdbUrl, appConfig.GetString("web.influxdb.token"))
|
||||||
InsecureSkipVerify: appConfig.GetBool("web.influxdb.tls.insecure_skip_verify"),
|
|
||||||
}
|
|
||||||
globalLogger.Infof("InfluxDB certificate verification: %t\n", !tlsConfig.InsecureSkipVerify)
|
|
||||||
|
|
||||||
client := influxdb2.NewClientWithOptions(
|
|
||||||
influxdbUrl,
|
|
||||||
appConfig.GetString("web.influxdb.token"),
|
|
||||||
influxdb2.DefaultOptions().SetTLSConfig(tlsConfig),
|
|
||||||
)
|
|
||||||
|
|
||||||
//if !appConfig.IsSet("web.influxdb.token") {
|
//if !appConfig.IsSet("web.influxdb.token") {
|
||||||
globalLogger.Debugf("Determine Influxdb setup status...")
|
globalLogger.Debugf("Determine Influxdb setup status...")
|
||||||
influxSetupComplete, err := InfluxSetupComplete(influxdbUrl, tlsConfig)
|
influxSetupComplete, err := InfluxSetupComplete(influxdbUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to check influxdb setup status - %w", err)
|
return nil, fmt.Errorf("failed to check influxdb setup status - %w", err)
|
||||||
}
|
}
|
||||||
@@ -205,30 +182,7 @@ func (sr *scrutinyRepository) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr *scrutinyRepository) HealthCheck(ctx context.Context) error {
|
func InfluxSetupComplete(influxEndpoint string) (bool, error) {
|
||||||
//check influxdb
|
|
||||||
status, err := sr.influxClient.Health(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("influxdb healthcheck failed: %w", err)
|
|
||||||
}
|
|
||||||
if status.Status != "pass" {
|
|
||||||
return fmt.Errorf("influxdb healthcheckf failed: status=%s", status.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
//check sqlite db.
|
|
||||||
database, err := sr.gormClient.DB()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("sqlite healthcheck failed: %w", err)
|
|
||||||
}
|
|
||||||
err = database.Ping()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("sqlite healthcheck failed during ping: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func InfluxSetupComplete(influxEndpoint string, tlsConfig *tls.Config) (bool, error) {
|
|
||||||
influxUri, err := url.Parse(influxEndpoint)
|
influxUri, err := url.Parse(influxEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -238,8 +192,7 @@ func InfluxSetupComplete(influxEndpoint string, tlsConfig *tls.Config) (bool, er
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
res, err := http.Get(influxUri.String())
|
||||||
res, err := client.Get(influxUri.String())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -289,29 +242,21 @@ func (sr *scrutinyRepository) EnsureBuckets(ctx context.Context, org *domain.Org
|
|||||||
|
|
||||||
//create buckets (used for downsampling)
|
//create buckets (used for downsampling)
|
||||||
weeklyBucket := fmt.Sprintf("%s_weekly", sr.appConfig.GetString("web.influxdb.bucket"))
|
weeklyBucket := fmt.Sprintf("%s_weekly", sr.appConfig.GetString("web.influxdb.bucket"))
|
||||||
if foundWeeklyBucket, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, weeklyBucket); foundErr != nil {
|
if _, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, weeklyBucket); foundErr != nil {
|
||||||
// metrics_weekly bucket will have a retention period of 8+1 weeks (since it will be down-sampled once a month)
|
// metrics_weekly bucket will have a retention period of 8+1 weeks (since it will be down-sampled once a month)
|
||||||
_, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, weeklyBucket, weeklyBucketRetentionRule)
|
_, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, weeklyBucket, weeklyBucketRetentionRule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if sr.appConfig.GetBool("web.influxdb.retention_policy") {
|
|
||||||
//correctly set the retention period for the bucket (may not be able to do it during setup/creation)
|
|
||||||
foundWeeklyBucket.RetentionRules = domain.RetentionRules{weeklyBucketRetentionRule}
|
|
||||||
sr.influxClient.BucketsAPI().UpdateBucket(ctx, foundWeeklyBucket)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
monthlyBucket := fmt.Sprintf("%s_monthly", sr.appConfig.GetString("web.influxdb.bucket"))
|
monthlyBucket := fmt.Sprintf("%s_monthly", sr.appConfig.GetString("web.influxdb.bucket"))
|
||||||
if foundMonthlyBucket, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, monthlyBucket); foundErr != nil {
|
if _, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, monthlyBucket); foundErr != nil {
|
||||||
// metrics_monthly bucket will have a retention period of 24+1 months (since it will be down-sampled once a year)
|
// metrics_monthly bucket will have a retention period of 24+1 months (since it will be down-sampled once a year)
|
||||||
_, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, monthlyBucket, monthlyBucketRetentionRule)
|
_, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, monthlyBucket, monthlyBucketRetentionRule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if sr.appConfig.GetBool("web.influxdb.retention_policy") {
|
|
||||||
//correctly set the retention period for the bucket (may not be able to do it during setup/creation)
|
|
||||||
foundMonthlyBucket.RetentionRules = domain.RetentionRules{monthlyBucketRetentionRule}
|
|
||||||
sr.influxClient.BucketsAPI().UpdateBucket(ctx, foundMonthlyBucket)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yearlyBucket := fmt.Sprintf("%s_yearly", sr.appConfig.GetString("web.influxdb.bucket"))
|
yearlyBucket := fmt.Sprintf("%s_yearly", sr.appConfig.GetString("web.influxdb.bucket"))
|
||||||
@@ -497,16 +442,3 @@ func (sr *scrutinyRepository) lookupNestedDurationKeys(durationKey string) []str
|
|||||||
}
|
}
|
||||||
return []string{DURATION_KEY_WEEK}
|
return []string{DURATION_KEY_WEEK}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sqlitePragmaString(pragmas map[string]string) string {
|
|
||||||
q := url.Values{}
|
|
||||||
for key, val := range pragmas {
|
|
||||||
q.Add("_pragma", key+"="+val)
|
|
||||||
}
|
|
||||||
|
|
||||||
queryStr := q.Encode()
|
|
||||||
if len(queryStr) > 0 {
|
|
||||||
return "?" + queryStr
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// Device
|
// 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)
|
// update device fields that may change: (DeviceType, HostID)
|
||||||
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
|
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
|
||||||
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
|
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
|
||||||
@@ -51,7 +51,7 @@ func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, coll
|
|||||||
return device, sr.gormClient.Model(&device).Updates(device).Error
|
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) {
|
func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
|
||||||
var device models.Device
|
var device models.Device
|
||||||
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
|
||||||
@@ -74,16 +74,6 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string)
|
|||||||
return device, nil
|
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 {
|
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 {
|
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).Delete(&models.Device{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ package database
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||||
"github.com/influxdata/influxdb-client-go/v2/api"
|
"github.com/influxdata/influxdb-client-go/v2/api"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -31,17 +30,14 @@ func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetSmartAttributeHistory MUST return in sorted order, where newest entries are at the beginning of the list, and oldest are at the end.
|
// GetSmartAttributeHistory MUST return in sorted order, where newest entries are at the beginning of the list, and oldest are at the end.
|
||||||
// When selectEntries is > 0, only the most recent selectEntries database entries are returned, starting from the selectEntriesOffset entry.
|
func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error) {
|
||||||
// For example, with selectEntries = 5, selectEntries = 0, the most recent 5 are returned. With selectEntries = 3, selectEntries = 2, entries
|
|
||||||
// 2 to 4 are returned (2 being the third newest, since it is zero-indexed)
|
|
||||||
func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
|
|
||||||
// Get SMartResults from InfluxDB
|
// Get SMartResults from InfluxDB
|
||||||
|
|
||||||
//TODO: change the filter startrange to a real number.
|
//TODO: change the filter startrange to a real number.
|
||||||
|
|
||||||
// Get parser flux query result
|
// Get parser flux query result
|
||||||
//appConfig.GetString("web.influxdb.bucket")
|
//appConfig.GetString("web.influxdb.bucket")
|
||||||
queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
|
queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey)
|
||||||
log.Infoln(queryStr)
|
log.Infoln(queryStr)
|
||||||
|
|
||||||
smartResults := []measurements.Smart{}
|
smartResults := []measurements.Smart{}
|
||||||
@@ -69,6 +65,9 @@ func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//we have to sort the smartResults again, because the `union` command will return multiple 'tables' and only sort the records in each table.
|
||||||
|
sortSmartMeasurementsDesc(smartResults)
|
||||||
|
|
||||||
return smartResults, nil
|
return smartResults, nil
|
||||||
|
|
||||||
//if err := device.SquashHistory(); err != nil {
|
//if err := device.SquashHistory(); err != nil {
|
||||||
@@ -100,7 +99,7 @@ func (sr *scrutinyRepository) saveDatapoint(influxWriteApi api.WriteAPIBlocking,
|
|||||||
return influxWriteApi.WritePoint(ctx, p)
|
return influxWriteApi.WritePoint(ctx, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
|
func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string) string {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
@@ -109,34 +108,28 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
|
|||||||
|> range(start: -1w, stop: now())
|
|> range(start: -1w, stop: now())
|
||||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||||
|> tail(n: 10, offset: 0)
|
|
||||||
|> schema.fieldsAsCols()
|
|> schema.fieldsAsCols()
|
||||||
|
|
||||||
monthData = from(bucket: "metrics_weekly")
|
monthData = from(bucket: "metrics_weekly")
|
||||||
|> range(start: -1mo, stop: -1w)
|
|> range(start: -1mo, stop: -1w)
|
||||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||||
|> tail(n: 10, offset: 0)
|
|
||||||
|> schema.fieldsAsCols()
|
|> schema.fieldsAsCols()
|
||||||
|
|
||||||
yearData = from(bucket: "metrics_monthly")
|
yearData = from(bucket: "metrics_monthly")
|
||||||
|> range(start: -1y, stop: -1mo)
|
|> range(start: -1y, stop: -1mo)
|
||||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||||
|> tail(n: 10, offset: 0)
|
|
||||||
|> schema.fieldsAsCols()
|
|> schema.fieldsAsCols()
|
||||||
|
|
||||||
foreverData = from(bucket: "metrics_yearly")
|
foreverData = from(bucket: "metrics_yearly")
|
||||||
|> range(start: -10y, stop: -1y)
|
|> range(start: -10y, stop: -1y)
|
||||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||||
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|
||||||
|> tail(n: 10, offset: 0)
|
|
||||||
|> schema.fieldsAsCols()
|
|> schema.fieldsAsCols()
|
||||||
|
|
||||||
union(tables: [weekData, monthData, yearData, foreverData])
|
union(tables: [weekData, monthData, yearData, foreverData])
|
||||||
|> group()
|
|> sort(columns: ["_time"], desc: false)
|
||||||
|> sort(columns: ["_time"], desc: true)
|
|
||||||
|> tail(n: 6, offset: 4)
|
|
||||||
|> yield(name: "last")
|
|> yield(name: "last")
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@@ -147,60 +140,34 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
|
|||||||
|
|
||||||
nestedDurationKeys := sr.lookupNestedDurationKeys(durationKey)
|
nestedDurationKeys := sr.lookupNestedDurationKeys(durationKey)
|
||||||
|
|
||||||
if len(nestedDurationKeys) == 1 {
|
|
||||||
//there's only one bucket being queried, no need to union, just aggregate the dataset and return
|
|
||||||
partialQueryStr = append(partialQueryStr, []string{
|
|
||||||
sr.generateSmartAttributesSubquery(wwn, nestedDurationKeys[0], selectEntries, selectEntriesOffset, attributes),
|
|
||||||
fmt.Sprintf(`%sData`, nestedDurationKeys[0]),
|
|
||||||
`|> sort(columns: ["_time"], desc: true)`,
|
|
||||||
`|> yield()`,
|
|
||||||
}...)
|
|
||||||
return strings.Join(partialQueryStr, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
subQueries := []string{}
|
|
||||||
subQueryNames := []string{}
|
subQueryNames := []string{}
|
||||||
for _, nestedDurationKey := range nestedDurationKeys {
|
for _, nestedDurationKey := range nestedDurationKeys {
|
||||||
|
bucketName := sr.lookupBucketName(nestedDurationKey)
|
||||||
|
durationRange := sr.lookupDuration(nestedDurationKey)
|
||||||
|
|
||||||
subQueryNames = append(subQueryNames, fmt.Sprintf(`%sData`, nestedDurationKey))
|
subQueryNames = append(subQueryNames, fmt.Sprintf(`%sData`, nestedDurationKey))
|
||||||
if selectEntries > 0 {
|
partialQueryStr = append(partialQueryStr, []string{
|
||||||
// We only need the last `n + offset` # of entries from each table to guarantee we can
|
fmt.Sprintf(`%sData = from(bucket: "%s")`, nestedDurationKey, bucketName),
|
||||||
// get the last `n` # of entries starting from `offset` of the union
|
fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]),
|
||||||
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes))
|
`|> filter(fn: (r) => r["_measurement"] == "smart" )`,
|
||||||
} else {
|
fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn),
|
||||||
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, 0, 0, attributes))
|
"|> schema.fieldsAsCols()",
|
||||||
}
|
}...)
|
||||||
}
|
}
|
||||||
partialQueryStr = append(partialQueryStr, subQueries...)
|
|
||||||
partialQueryStr = append(partialQueryStr, []string{
|
if len(subQueryNames) == 1 {
|
||||||
fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")),
|
//there's only one bucket being queried, no need to union, just aggregate the dataset and return
|
||||||
`|> group()`,
|
partialQueryStr = append(partialQueryStr, []string{
|
||||||
`|> sort(columns: ["_time"], desc: true)`,
|
subQueryNames[0],
|
||||||
}...)
|
`|> yield()`,
|
||||||
if selectEntries > 0 {
|
}...)
|
||||||
partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, selectEntries, selectEntriesOffset))
|
} else {
|
||||||
|
partialQueryStr = append(partialQueryStr, []string{
|
||||||
|
fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")),
|
||||||
|
`|> sort(columns: ["_time"], desc: false)`,
|
||||||
|
`|> yield(name: "last")`,
|
||||||
|
}...)
|
||||||
}
|
}
|
||||||
partialQueryStr = append(partialQueryStr, `|> yield(name: "last")`)
|
|
||||||
|
|
||||||
return strings.Join(partialQueryStr, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
|
|
||||||
bucketName := sr.lookupBucketName(durationKey)
|
|
||||||
durationRange := sr.lookupDuration(durationKey)
|
|
||||||
|
|
||||||
partialQueryStr := []string{
|
|
||||||
fmt.Sprintf(`%sData = from(bucket: "%s")`, durationKey, bucketName),
|
|
||||||
fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]),
|
|
||||||
`|> filter(fn: (r) => r["_measurement"] == "smart" )`,
|
|
||||||
fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn),
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
partialQueryStr = append(partialQueryStr, "|> schema.fieldsAsCols()")
|
|
||||||
|
|
||||||
return strings.Join(partialQueryStr, "\n")
|
return strings.Join(partialQueryStr, "\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
|
"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/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"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||||
@@ -21,6 +15,8 @@ import (
|
|||||||
"github.com/influxdata/influxdb-client-go/v2/api/http"
|
"github.com/influxdata/influxdb-client-go/v2/api/http"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -271,159 +267,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
|||||||
return tx.AutoMigrate(m20220509170100.Device{})
|
return tx.AutoMigrate(m20220509170100.Device{})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
ID: "m20220709181300",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
|
|
||||||
// delete devices with empty `wwn` field (they are impossible to delete manually), and are invalid.
|
|
||||||
return tx.Where("wwn = ?", "").Delete(&models.Device{}).Error
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "m20220716214900", // add settings table.
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
|
|
||||||
// adding the settings table.
|
|
||||||
err := tx.AutoMigrate(m20220716214900.Setting{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
//add defaults.
|
|
||||||
|
|
||||||
var defaultSettings = []m20220716214900.Setting{
|
|
||||||
{
|
|
||||||
SettingKeyName: "theme",
|
|
||||||
SettingKeyDescription: "Frontend theme ('light' | 'dark' | 'system')",
|
|
||||||
SettingDataType: "string",
|
|
||||||
SettingValueString: "system", // options: 'light' | 'dark' | 'system'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SettingKeyName: "layout",
|
|
||||||
SettingKeyDescription: "Frontend layout ('material')",
|
|
||||||
SettingDataType: "string",
|
|
||||||
SettingValueString: "material",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SettingKeyName: "dashboard_display",
|
|
||||||
SettingKeyDescription: "Frontend device display title ('name' | 'serial_id' | 'uuid' | 'label')",
|
|
||||||
SettingDataType: "string",
|
|
||||||
SettingValueString: "name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SettingKeyName: "dashboard_sort",
|
|
||||||
SettingKeyDescription: "Frontend device sort by ('status' | 'title' | 'age')",
|
|
||||||
SettingDataType: "string",
|
|
||||||
SettingValueString: "status",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SettingKeyName: "temperature_unit",
|
|
||||||
SettingKeyDescription: "Frontend temperature unit ('celsius' | 'fahrenheit')",
|
|
||||||
SettingDataType: "string",
|
|
||||||
SettingValueString: "celsius",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SettingKeyName: "file_size_si_units",
|
|
||||||
SettingKeyDescription: "File size in SI units (true | false)",
|
|
||||||
SettingDataType: "bool",
|
|
||||||
SettingValueBool: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SettingKeyName: "line_stroke",
|
|
||||||
SettingKeyDescription: "Temperature chart line stroke ('smooth' | 'straight' | 'stepline')",
|
|
||||||
SettingDataType: "string",
|
|
||||||
SettingValueString: "smooth",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SettingKeyName: "metrics.notify_level",
|
|
||||||
SettingKeyDescription: "Determines which device status will cause a notification (fail or warn)",
|
|
||||||
SettingDataType: "numeric",
|
|
||||||
SettingValueNumeric: int(pkg.MetricsNotifyLevelFail), // options: 'fail' or 'warn'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SettingKeyName: "metrics.status_filter_attributes",
|
|
||||||
SettingKeyDescription: "Determines which attributes should impact device status",
|
|
||||||
SettingDataType: "numeric",
|
|
||||||
SettingValueNumeric: int(pkg.MetricsStatusFilterAttributesAll), // options: 'all' or 'critical'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SettingKeyName: "metrics.status_threshold",
|
|
||||||
SettingKeyDescription: "Determines which threshold should impact device status",
|
|
||||||
SettingDataType: "numeric",
|
|
||||||
SettingValueNumeric: int(pkg.MetricsStatusThresholdBoth), // options: 'scrutiny', 'smart', 'both'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return tx.Create(&defaultSettings).Error
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "m20221115214900", // add line_stroke setting.
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
//add line_stroke setting default.
|
|
||||||
var defaultSettings = []m20220716214900.Setting{
|
|
||||||
{
|
|
||||||
SettingKeyName: "line_stroke",
|
|
||||||
SettingKeyDescription: "Temperature chart line stroke ('smooth' | 'straight' | 'stepline')",
|
|
||||||
SettingDataType: "string",
|
|
||||||
SettingValueString: "smooth",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return tx.Create(&defaultSettings).Error
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "m20231123123300", // add repeat_notifications setting.
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
//add repeat_notifications setting default.
|
|
||||||
var defaultSettings = []m20220716214900.Setting{
|
|
||||||
{
|
|
||||||
SettingKeyName: "metrics.repeat_notifications",
|
|
||||||
SettingKeyDescription: "Whether to repeat all notifications or just when values change (true | false)",
|
|
||||||
SettingDataType: "bool",
|
|
||||||
SettingValueBool: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
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 {
|
if err := m.Migrate(); err != nil {
|
||||||
@@ -431,37 +274,13 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sr.logger.Infoln("Database migration completed successfully")
|
sr.logger.Infoln("Database migration completed successfully")
|
||||||
|
|
||||||
//these migrations cannot be done within a transaction, so they are done as a separate group, with `UseTransaction = false`
|
|
||||||
sr.logger.Infoln("SQLite global configuration migrations starting. Please wait....")
|
|
||||||
globalMigrateOptions := gormigrate.DefaultOptions
|
|
||||||
globalMigrateOptions.UseTransaction = false
|
|
||||||
gm := gormigrate.New(sr.gormClient, globalMigrateOptions, []*gormigrate.Migration{
|
|
||||||
{
|
|
||||||
ID: "g20220802211500",
|
|
||||||
Migrate: func(tx *gorm.DB) error {
|
|
||||||
//shrink the Database (maybe necessary after 20220503113100)
|
|
||||||
if err := tx.Exec("VACUUM;").Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := gm.Migrate(); err != nil {
|
|
||||||
sr.logger.Errorf("SQLite global configuration migrations failed with error. \n Please open a github issue at https://github.com/AnalogJ/scrutiny and attach a copy of your scrutiny.db file. \n %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sr.logger.Infoln("SQLite global configuration migrations completed successfully")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
// When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy.
|
//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.
|
//This function will ignore retention policy errors, and allow the migration to continue.
|
||||||
func ignorePastRetentionPolicyError(err error) error {
|
func ignorePastRetentionPolicyError(err error) error {
|
||||||
var influxDbWriteError *http.Error
|
var influxDbWriteError *http.Error
|
||||||
if errors.As(err, &influxDbWriteError) {
|
if errors.As(err, &influxDbWriteError) {
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// store retrieved settings in the AppConfig obj
|
|
||||||
for _, settingsEntry := range settingsEntries {
|
|
||||||
configKey := fmt.Sprintf("%s.%s", config.DB_USER_SETTINGS_SUBKEY, settingsEntry.SettingKeyName)
|
|
||||||
|
|
||||||
if settingsEntry.SettingDataType == "numeric" {
|
|
||||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueNumeric)
|
|
||||||
} else if settingsEntry.SettingDataType == "string" {
|
|
||||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueString)
|
|
||||||
} else if settingsEntry.SettingDataType == "bool" {
|
|
||||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueBool)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// unmarshal the dbsetting object data to a settings object.
|
|
||||||
var settings models.Settings
|
|
||||||
err := sr.appConfig.UnmarshalKey(config.DB_USER_SETTINGS_SUBKEY, &settings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &settings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// testing
|
|
||||||
// curl -d '{"metrics": { "notify_level": 5, "status_filter_attributes": 5, "status_threshold": 5 }}' -H "Content-Type: application/json" -X POST http://localhost:9090/api/settings
|
|
||||||
// SaveSettings will update settings in AppConfig object, then save the settings to the database.
|
|
||||||
func (sr *scrutinyRepository) SaveSettings(ctx context.Context, settings models.Settings) error {
|
|
||||||
//save the entries to the appconfig
|
|
||||||
settingsMap := &map[string]interface{}{}
|
|
||||||
err := mapstructure.Decode(settings, &settingsMap)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
settingsWrapperMap := map[string]interface{}{}
|
|
||||||
settingsWrapperMap[config.DB_USER_SETTINGS_SUBKEY] = *settingsMap
|
|
||||||
err = sr.appConfig.MergeConfigMap(settingsWrapperMap)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sr.logger.Debugf("after merge settings: %v", sr.appConfig.AllSettings())
|
|
||||||
//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)
|
|
||||||
}
|
|
||||||
|
|
||||||
//update settingsEntries
|
|
||||||
for ndx, settingsEntry := range settingsEntries {
|
|
||||||
configKey := fmt.Sprintf("%s.%s", config.DB_USER_SETTINGS_SUBKEY, strings.ToLower(settingsEntry.SettingKeyName))
|
|
||||||
|
|
||||||
if settingsEntry.SettingDataType == "numeric" {
|
|
||||||
settingsEntries[ndx].SettingValueNumeric = sr.appConfig.GetInt(configKey)
|
|
||||||
} else if settingsEntry.SettingDataType == "string" {
|
|
||||||
settingsEntries[ndx].SettingValueString = sr.appConfig.GetString(configKey)
|
|
||||||
} else if settingsEntry.SettingDataType == "bool" {
|
|
||||||
settingsEntries[ndx].SettingValueBool = sr.appConfig.GetBool(configKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// store in database.
|
|
||||||
//TODO: this should be `sr.gormClient.Updates(&settingsEntries).Error`
|
|
||||||
err := sr.gormClient.Model(&models.SettingEntry{}).Where([]uint{settingsEntry.ID}).Select("setting_value_numeric", "setting_value_string", "setting_value_bool").Updates(settingsEntries[ndx]).Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -11,71 +11,35 @@ import (
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error {
|
func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error {
|
||||||
weeklyTaskName := "tsk-weekly-aggr"
|
weeklyTaskName := "tsk-weekly-aggr"
|
||||||
weeklyTaskScript := sr.DownsampleScript("weekly", weeklyTaskName, "0 1 * * 0")
|
|
||||||
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: weeklyTaskName}); findErr == nil && len(found) == 0 {
|
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: weeklyTaskName}); findErr == nil && len(found) == 0 {
|
||||||
//weekly on Sunday at 1:00am
|
//weekly on Sunday at 1:00am
|
||||||
_, err := sr.influxTaskApi.CreateTaskByFlux(ctx, weeklyTaskScript, orgID)
|
_, err := sr.influxTaskApi.CreateTaskWithCron(ctx, weeklyTaskName, sr.DownsampleScript("weekly"), "0 1 * * 0", orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if len(found) == 1 {
|
|
||||||
//check if we should update
|
|
||||||
task := &found[0]
|
|
||||||
if weeklyTaskScript != task.Flux {
|
|
||||||
sr.logger.Infoln("updating weekly task script")
|
|
||||||
task.Flux = weeklyTaskScript
|
|
||||||
_, err := sr.influxTaskApi.UpdateTask(ctx, task)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
monthlyTaskName := "tsk-monthly-aggr"
|
monthlyTaskName := "tsk-monthly-aggr"
|
||||||
monthlyTaskScript := sr.DownsampleScript("monthly", monthlyTaskName, "30 1 1 * *")
|
|
||||||
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: monthlyTaskName}); findErr == nil && len(found) == 0 {
|
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: monthlyTaskName}); findErr == nil && len(found) == 0 {
|
||||||
//monthly on first day of the month at 1:30am
|
//monthly on first day of the month at 1:30am
|
||||||
_, err := sr.influxTaskApi.CreateTaskByFlux(ctx, monthlyTaskScript, orgID)
|
_, err := sr.influxTaskApi.CreateTaskWithCron(ctx, monthlyTaskName, sr.DownsampleScript("monthly"), "30 1 1 * *", orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if len(found) == 1 {
|
|
||||||
//check if we should update
|
|
||||||
task := &found[0]
|
|
||||||
if monthlyTaskScript != task.Flux {
|
|
||||||
sr.logger.Infoln("updating monthly task script")
|
|
||||||
task.Flux = monthlyTaskScript
|
|
||||||
_, err := sr.influxTaskApi.UpdateTask(ctx, task)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yearlyTaskName := "tsk-yearly-aggr"
|
yearlyTaskName := "tsk-yearly-aggr"
|
||||||
yearlyTaskScript := sr.DownsampleScript("yearly", yearlyTaskName, "0 2 1 1 *")
|
|
||||||
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: yearlyTaskName}); findErr == nil && len(found) == 0 {
|
if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: yearlyTaskName}); findErr == nil && len(found) == 0 {
|
||||||
//yearly on the first day of the year at 2:00am
|
//yearly on the first day of the year at 2:00am
|
||||||
_, err := sr.influxTaskApi.CreateTaskByFlux(ctx, yearlyTaskScript, orgID)
|
_, err := sr.influxTaskApi.CreateTaskWithCron(ctx, yearlyTaskName, sr.DownsampleScript("yearly"), "0 2 1 1 *", orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if len(found) == 1 {
|
|
||||||
//check if we should update
|
|
||||||
task := &found[0]
|
|
||||||
if yearlyTaskScript != task.Flux {
|
|
||||||
sr.logger.Infoln("updating yearly task script")
|
|
||||||
task.Flux = yearlyTaskScript
|
|
||||||
_, err := sr.influxTaskApi.UpdateTask(ctx, task)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr *scrutinyRepository) DownsampleScript(aggregationType string, name string, cron string) string {
|
func (sr *scrutinyRepository) DownsampleScript(aggregationType string) string {
|
||||||
var sourceBucket string // the source of the data
|
var sourceBucket string // the source of the data
|
||||||
var destBucket string // the destination for the aggregated data
|
var destBucket string // the destination for the aggregated data
|
||||||
var rangeStart string
|
var rangeStart string
|
||||||
@@ -124,37 +88,30 @@ func (sr *scrutinyRepository) DownsampleScript(aggregationType string, name stri
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
return fmt.Sprintf(`
|
return fmt.Sprintf(`
|
||||||
option task = {
|
sourceBucket = "%s"
|
||||||
name: "%s",
|
rangeStart = %s
|
||||||
cron: "%s",
|
rangeEnd = %s
|
||||||
}
|
aggWindow = %s
|
||||||
|
destBucket = "%s"
|
||||||
|
destOrg = "%s"
|
||||||
|
|
||||||
sourceBucket = "%s"
|
from(bucket: sourceBucket)
|
||||||
rangeStart = %s
|
|> range(start: rangeStart, stop: rangeEnd)
|
||||||
rangeEnd = %s
|
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
||||||
aggWindow = %s
|
|> group(columns: ["device_wwn", "_field"])
|
||||||
destBucket = "%s"
|
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
||||||
destOrg = "%s"
|
|> to(bucket: destBucket, org: destOrg)
|
||||||
|
|
||||||
from(bucket: sourceBucket)
|
temp_data = from(bucket: sourceBucket)
|
||||||
|> range(start: rangeStart, stop: rangeEnd)
|
|> range(start: rangeStart, stop: rangeEnd)
|
||||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
|> filter(fn: (r) => r["_measurement"] == "temp")
|
||||||
|> group(columns: ["device_wwn", "_field"])
|
|> group(columns: ["device_wwn"])
|
||||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
|> toInt()
|
||||||
|> to(bucket: destBucket, org: destOrg)
|
|
||||||
|
|
||||||
from(bucket: sourceBucket)
|
temp_data
|
||||||
|> range(start: rangeStart, stop: rangeEnd)
|
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
|> to(bucket: destBucket, org: destOrg)
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
|
||||||
|> set(key: "_measurement", value: "temp")
|
|
||||||
|> set(key: "_field", value: "temp")
|
|
||||||
|> to(bucket: destBucket, org: destOrg)
|
|
||||||
`,
|
`,
|
||||||
name,
|
|
||||||
cron,
|
|
||||||
sourceBucket,
|
sourceBucket,
|
||||||
rangeStart,
|
rangeStart,
|
||||||
rangeEnd,
|
rangeEnd,
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_DownsampleScript_Weekly(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//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()
|
|
||||||
|
|
||||||
deviceRepo := scrutinyRepository{
|
|
||||||
appConfig: fakeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregationType := "weekly"
|
|
||||||
|
|
||||||
//test
|
|
||||||
influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-weekly-aggr", "0 1 * * 0")
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, `
|
|
||||||
option task = {
|
|
||||||
name: "tsk-weekly-aggr",
|
|
||||||
cron: "0 1 * * 0",
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceBucket = "metrics"
|
|
||||||
rangeStart = -2w
|
|
||||||
rangeEnd = -1w
|
|
||||||
aggWindow = 1w
|
|
||||||
destBucket = "metrics_weekly"
|
|
||||||
destOrg = "scrutiny"
|
|
||||||
|
|
||||||
from(bucket: sourceBucket)
|
|
||||||
|> range(start: rangeStart, stop: rangeEnd)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
|
||||||
|> group(columns: ["device_wwn", "_field"])
|
|
||||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
|
||||||
|> to(bucket: destBucket, org: destOrg)
|
|
||||||
|
|
||||||
from(bucket: sourceBucket)
|
|
||||||
|> range(start: rangeStart, stop: rangeEnd)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
|
||||||
|> set(key: "_measurement", value: "temp")
|
|
||||||
|> set(key: "_field", value: "temp")
|
|
||||||
|> to(bucket: destBucket, org: destOrg)
|
|
||||||
`, influxDbScript)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_DownsampleScript_Monthly(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//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()
|
|
||||||
|
|
||||||
deviceRepo := scrutinyRepository{
|
|
||||||
appConfig: fakeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregationType := "monthly"
|
|
||||||
|
|
||||||
//test
|
|
||||||
influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-monthly-aggr", "30 1 1 * *")
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, `
|
|
||||||
option task = {
|
|
||||||
name: "tsk-monthly-aggr",
|
|
||||||
cron: "30 1 1 * *",
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceBucket = "metrics_weekly"
|
|
||||||
rangeStart = -2mo
|
|
||||||
rangeEnd = -1mo
|
|
||||||
aggWindow = 1mo
|
|
||||||
destBucket = "metrics_monthly"
|
|
||||||
destOrg = "scrutiny"
|
|
||||||
|
|
||||||
from(bucket: sourceBucket)
|
|
||||||
|> range(start: rangeStart, stop: rangeEnd)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
|
||||||
|> group(columns: ["device_wwn", "_field"])
|
|
||||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
|
||||||
|> to(bucket: destBucket, org: destOrg)
|
|
||||||
|
|
||||||
from(bucket: sourceBucket)
|
|
||||||
|> range(start: rangeStart, stop: rangeEnd)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
|
||||||
|> set(key: "_measurement", value: "temp")
|
|
||||||
|> set(key: "_field", value: "temp")
|
|
||||||
|> to(bucket: destBucket, org: destOrg)
|
|
||||||
`, influxDbScript)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_DownsampleScript_Yearly(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//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()
|
|
||||||
|
|
||||||
deviceRepo := scrutinyRepository{
|
|
||||||
appConfig: fakeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregationType := "yearly"
|
|
||||||
|
|
||||||
//test
|
|
||||||
influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-yearly-aggr", "0 2 1 1 *")
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, `
|
|
||||||
option task = {
|
|
||||||
name: "tsk-yearly-aggr",
|
|
||||||
cron: "0 2 1 1 *",
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceBucket = "metrics_monthly"
|
|
||||||
rangeStart = -2y
|
|
||||||
rangeEnd = -1y
|
|
||||||
aggWindow = 1y
|
|
||||||
destBucket = "metrics_yearly"
|
|
||||||
destOrg = "scrutiny"
|
|
||||||
|
|
||||||
from(bucket: sourceBucket)
|
|
||||||
|> range(start: rangeStart, stop: rangeEnd)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "smart" )
|
|
||||||
|> group(columns: ["device_wwn", "_field"])
|
|
||||||
|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false)
|
|
||||||
|> to(bucket: destBucket, org: destOrg)
|
|
||||||
|
|
||||||
from(bucket: sourceBucket)
|
|
||||||
|> range(start: rangeStart, stop: rangeEnd)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp")
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false)
|
|
||||||
|> set(key: "_measurement", value: "temp")
|
|
||||||
|> set(key: "_field", value: "temp")
|
|
||||||
|> to(bucket: destBucket, org: destOrg)
|
|
||||||
`, influxDbScript)
|
|
||||||
}
|
|
||||||
@@ -3,31 +3,24 @@ package database
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Temperature Data
|
// Temperature Data
|
||||||
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
|
func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo) error {
|
||||||
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 && !discardSCTTempHistory {
|
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 {
|
||||||
|
|
||||||
for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table {
|
for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table {
|
||||||
//temp value may be null, we must skip/ignore them. See #393
|
|
||||||
if temp == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
intervalSec := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * 60
|
minutesOffset := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * int64(ndx) * 60
|
||||||
datapointTime := collectorSmartData.LocalTime.TimeT - int64(ndx) * intervalSec
|
|
||||||
alignedDatapointTime := datapointTime - datapointTime % intervalSec
|
|
||||||
smartTemp := measurements.SmartTemperature{
|
smartTemp := measurements.SmartTemperature{
|
||||||
Date: time.Unix(alignedDatapointTime, 0),
|
Date: time.Unix(collectorSmartData.LocalTime.TimeT-minutesOffset, 0),
|
||||||
Temp: temp,
|
Temp: temp,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,22 +35,23 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
|
|||||||
return err
|
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) {
|
func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) {
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_aggregateTempQuery_Week(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//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()
|
|
||||||
|
|
||||||
deviceRepo := scrutinyRepository{
|
|
||||||
appConfig: fakeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregationType := DURATION_KEY_WEEK
|
|
||||||
|
|
||||||
//test
|
|
||||||
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, `import "influxdata/influxdb/schema"
|
|
||||||
weekData = from(bucket: "metrics")
|
|
||||||
|> range(start: -1w, stop: now())
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
|
||||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|
|
||||||
weekData
|
|
||||||
|> schema.fieldsAsCols()
|
|
||||||
|> yield()`, influxDbScript)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_aggregateTempQuery_Month(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//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()
|
|
||||||
|
|
||||||
deviceRepo := scrutinyRepository{
|
|
||||||
appConfig: fakeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregationType := DURATION_KEY_MONTH
|
|
||||||
|
|
||||||
//test
|
|
||||||
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, `import "influxdata/influxdb/schema"
|
|
||||||
weekData = from(bucket: "metrics")
|
|
||||||
|> range(start: -1w, stop: now())
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
|
||||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|
|
||||||
monthData = from(bucket: "metrics_weekly")
|
|
||||||
|> range(start: -1mo, stop: -1w)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
|
||||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|
|
||||||
union(tables: [weekData, monthData])
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> sort(columns: ["_time"], desc: false)
|
|
||||||
|> schema.fieldsAsCols()`, influxDbScript)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_aggregateTempQuery_Year(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//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()
|
|
||||||
|
|
||||||
deviceRepo := scrutinyRepository{
|
|
||||||
appConfig: fakeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregationType := DURATION_KEY_YEAR
|
|
||||||
|
|
||||||
//test
|
|
||||||
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, `import "influxdata/influxdb/schema"
|
|
||||||
weekData = from(bucket: "metrics")
|
|
||||||
|> range(start: -1w, stop: now())
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
|
||||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|
|
||||||
monthData = from(bucket: "metrics_weekly")
|
|
||||||
|> range(start: -1mo, stop: -1w)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
|
||||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|
|
||||||
yearData = from(bucket: "metrics_monthly")
|
|
||||||
|> range(start: -1y, stop: -1mo)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
|
||||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|
|
||||||
union(tables: [weekData, monthData, yearData])
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> sort(columns: ["_time"], desc: false)
|
|
||||||
|> schema.fieldsAsCols()`, influxDbScript)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_aggregateTempQuery_Forever(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//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()
|
|
||||||
|
|
||||||
deviceRepo := scrutinyRepository{
|
|
||||||
appConfig: fakeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregationType := DURATION_KEY_FOREVER
|
|
||||||
|
|
||||||
//test
|
|
||||||
influxDbScript := deviceRepo.aggregateTempQuery(aggregationType)
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, `import "influxdata/influxdb/schema"
|
|
||||||
weekData = from(bucket: "metrics")
|
|
||||||
|> range(start: -1w, stop: now())
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
|
||||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|
|
||||||
monthData = from(bucket: "metrics_weekly")
|
|
||||||
|> range(start: -1mo, stop: -1w)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
|
||||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|
|
||||||
yearData = from(bucket: "metrics_monthly")
|
|
||||||
|> range(start: -1y, stop: -1mo)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
|
||||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|
|
||||||
foreverData = from(bucket: "metrics_yearly")
|
|
||||||
|> range(start: -10y, stop: -1y)
|
|
||||||
|> filter(fn: (r) => r["_measurement"] == "temp" )
|
|
||||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> toInt()
|
|
||||||
|
|
||||||
union(tables: [weekData, monthData, yearData, foreverData])
|
|
||||||
|> group(columns: ["device_wwn"])
|
|
||||||
|> sort(columns: ["_time"], desc: false)
|
|
||||||
|> schema.fieldsAsCols()`, influxDbScript)
|
|
||||||
}
|
|
||||||
@@ -27,11 +27,14 @@ type SmartInfo struct {
|
|||||||
Oui uint64 `json:"oui"`
|
Oui uint64 `json:"oui"`
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
} `json:"wwn"`
|
} `json:"wwn"`
|
||||||
FirmwareVersion string `json:"firmware_version"`
|
FirmwareVersion string `json:"firmware_version"`
|
||||||
UserCapacity UserCapacity `json:"user_capacity"`
|
UserCapacity struct {
|
||||||
LogicalBlockSize int `json:"logical_block_size"`
|
Blocks int64 `json:"blocks"`
|
||||||
PhysicalBlockSize int `json:"physical_block_size"`
|
Bytes int64 `json:"bytes"`
|
||||||
RotationRate int `json:"rotation_rate"`
|
} `json:"user_capacity"`
|
||||||
|
LogicalBlockSize int `json:"logical_block_size"`
|
||||||
|
PhysicalBlockSize int `json:"physical_block_size"`
|
||||||
|
RotationRate int `json:"rotation_rate"`
|
||||||
FormFactor struct {
|
FormFactor struct {
|
||||||
AtaValue int `json:"ata_value"`
|
AtaValue int `json:"ata_value"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -207,10 +210,9 @@ type SmartInfo struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
SubsystemID int `json:"subsystem_id"`
|
SubsystemID int `json:"subsystem_id"`
|
||||||
} `json:"nvme_pci_vendor"`
|
} `json:"nvme_pci_vendor"`
|
||||||
NvmeIeeeOuiIdentifier int `json:"nvme_ieee_oui_identifier"`
|
NvmeIeeeOuiIdentifier int `json:"nvme_ieee_oui_identifier"`
|
||||||
NvmeTotalCapacity int64 `json:"nvme_total_capacity"`
|
NvmeControllerID int `json:"nvme_controller_id"`
|
||||||
NvmeControllerID int `json:"nvme_controller_id"`
|
NvmeNumberOfNamespaces int `json:"nvme_number_of_namespaces"`
|
||||||
NvmeNumberOfNamespaces int `json:"nvme_number_of_namespaces"`
|
|
||||||
NvmeNamespaces []struct {
|
NvmeNamespaces []struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Size struct {
|
Size struct {
|
||||||
@@ -237,23 +239,7 @@ type SmartInfo struct {
|
|||||||
ScsiErrorCounterLog ScsiErrorCounterLog `json:"scsi_error_counter_log"`
|
ScsiErrorCounterLog ScsiErrorCounterLog `json:"scsi_error_counter_log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capacity finds the total capacity of the device in bytes, or 0 if unknown.
|
//Primary Attribute Structs
|
||||||
func (s *SmartInfo) Capacity() int64 {
|
|
||||||
switch {
|
|
||||||
case s.NvmeTotalCapacity > 0:
|
|
||||||
return s.NvmeTotalCapacity
|
|
||||||
case s.UserCapacity.Bytes > 0:
|
|
||||||
return s.UserCapacity.Bytes
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserCapacity struct {
|
|
||||||
Blocks int64 `json:"blocks"`
|
|
||||||
Bytes int64 `json:"bytes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Primary Attribute Structs
|
|
||||||
type AtaSmartAttributesTableItem struct {
|
type AtaSmartAttributesTableItem struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package collector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSmartInfo_Capacity(t *testing.T) {
|
|
||||||
t.Run("should report nvme capacity", func(t *testing.T) {
|
|
||||||
smartInfo := SmartInfo{
|
|
||||||
UserCapacity: UserCapacity{
|
|
||||||
Bytes: 1234,
|
|
||||||
},
|
|
||||||
NvmeTotalCapacity: 5678,
|
|
||||||
}
|
|
||||||
assert.Equal(t, int64(5678), smartInfo.Capacity())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("should report user capacity", func(t *testing.T) {
|
|
||||||
smartInfo := SmartInfo{
|
|
||||||
UserCapacity: UserCapacity{
|
|
||||||
Bytes: 1234,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.Equal(t, int64(1234), smartInfo.Capacity())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("should report 0 for unknown capacities", func(t *testing.T) {
|
|
||||||
var smartInfo SmartInfo
|
|
||||||
assert.Zero(t, smartInfo.Capacity())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ type DeviceWrapper struct {
|
|||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||||
Archived bool `json:"archived"`
|
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt *time.Time
|
DeletedAt *time.Time
|
||||||
|
|||||||
@@ -64,27 +64,11 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
|
|||||||
for key, val := range attrs {
|
for key, val := range attrs {
|
||||||
switch key {
|
switch key {
|
||||||
case "temp":
|
case "temp":
|
||||||
temp, tempOk := val.(int64)
|
sm.Temp = val.(int64)
|
||||||
if tempOk {
|
|
||||||
sm.Temp = temp
|
|
||||||
} else {
|
|
||||||
log.Printf("unable to parse temp information: %v", val)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "power_on_hours":
|
case "power_on_hours":
|
||||||
powerOn, powerOnOk := val.(int64)
|
sm.PowerOnHours = val.(int64)
|
||||||
if powerOnOk {
|
|
||||||
sm.PowerOnHours = powerOn
|
|
||||||
} else {
|
|
||||||
log.Printf("unable to parse power_on_hours information: %v", val)
|
|
||||||
}
|
|
||||||
case "power_cycle_count":
|
case "power_cycle_count":
|
||||||
powerCycle, powerCycleOk := val.(int64)
|
sm.PowerCycleCount = val.(int64)
|
||||||
if powerCycleOk {
|
|
||||||
sm.PowerCycleCount = powerCycle
|
|
||||||
} else {
|
|
||||||
log.Printf("unable to parse power_cycle_count information: %v", val)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
// this key is unknown.
|
// this key is unknown.
|
||||||
if !strings.HasPrefix(key, "attr.") {
|
if !strings.HasPrefix(key, "attr.") {
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ package measurements
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SmartAtaAttribute struct {
|
type SmartAtaAttribute struct {
|
||||||
@@ -25,10 +24,6 @@ type SmartAtaAttribute struct {
|
|||||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa *SmartAtaAttribute) GetTransformedValue() int64 {
|
|
||||||
return sa.TransformedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sa *SmartAtaAttribute) GetStatus() pkg.AttributeStatus {
|
func (sa *SmartAtaAttribute) GetStatus() pkg.AttributeStatus {
|
||||||
return sa.Status
|
return sa.Status
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,4 @@ type SmartAttribute interface {
|
|||||||
Flatten() (fields map[string]interface{})
|
Flatten() (fields map[string]interface{})
|
||||||
Inflate(key string, val interface{})
|
Inflate(key string, val interface{})
|
||||||
GetStatus() pkg.AttributeStatus
|
GetStatus() pkg.AttributeStatus
|
||||||
GetTransformedValue() int64
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package measurements
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SmartNvmeAttribute struct {
|
type SmartNvmeAttribute struct {
|
||||||
@@ -19,10 +18,6 @@ type SmartNvmeAttribute struct {
|
|||||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa *SmartNvmeAttribute) GetTransformedValue() int64 {
|
|
||||||
return sa.TransformedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sa *SmartNvmeAttribute) GetStatus() pkg.AttributeStatus {
|
func (sa *SmartNvmeAttribute) GetStatus() pkg.AttributeStatus {
|
||||||
return sa.Status
|
return sa.Status
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package measurements
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SmartScsiAttribute struct {
|
type SmartScsiAttribute struct {
|
||||||
@@ -19,10 +18,6 @@ type SmartScsiAttribute struct {
|
|||||||
FailureRate float64 `json:"failure_rate,omitempty"`
|
FailureRate float64 `json:"failure_rate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa *SmartScsiAttribute) GetTransformedValue() int64 {
|
|
||||||
return sa.TransformedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sa *SmartScsiAttribute) GetStatus() pkg.AttributeStatus {
|
func (sa *SmartScsiAttribute) GetStatus() pkg.AttributeStatus {
|
||||||
return sa.Status
|
return sa.Status
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Temperature Format
|
||||||
|
// Date Format
|
||||||
|
// Device History window
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SettingEntry matches a setting row in the database
|
|
||||||
type SettingEntry struct {
|
|
||||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
|
||||||
gorm.Model
|
|
||||||
|
|
||||||
SettingKeyName string `json:"setting_key_name" gorm:"unique;not null"`
|
|
||||||
SettingKeyDescription string `json:"setting_key_description"`
|
|
||||||
SettingDataType string `json:"setting_data_type"`
|
|
||||||
|
|
||||||
SettingValueNumeric int `json:"setting_value_numeric"`
|
|
||||||
SettingValueString string `json:"setting_value_string"`
|
|
||||||
SettingValueBool bool `json:"setting_value_bool"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SettingEntry) TableName() string {
|
|
||||||
return "settings"
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
// Settings is made up of parsed SettingEntry objects retrieved from the database
|
|
||||||
//type Settings struct {
|
|
||||||
// MetricsNotifyLevel pkg.MetricsNotifyLevel `json:"metrics.notify.level" mapstructure:"metrics.notify.level"`
|
|
||||||
// MetricsStatusFilterAttributes pkg.MetricsStatusFilterAttributes `json:"metrics.status.filter_attributes" mapstructure:"metrics.status.filter_attributes"`
|
|
||||||
// MetricsStatusThreshold pkg.MetricsStatusThreshold `json:"metrics.status.threshold" mapstructure:"metrics.status.threshold"`
|
|
||||||
//}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
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"`
|
|
||||||
StatusFilterAttributes int `json:"status_filter_attributes" mapstructure:"status_filter_attributes"`
|
|
||||||
StatusThreshold int `json:"status_threshold" mapstructure:"status_threshold"`
|
|
||||||
RepeatNotifications bool `json:"repeat_notifications" mapstructure:"repeat_notifications"`
|
|
||||||
} `json:"metrics" mapstructure:"metrics"`
|
|
||||||
}
|
|
||||||
+3
-4
@@ -4,14 +4,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -33,7 +32,7 @@ func main() {
|
|||||||
log.Fatalf("ERROR %v", err)
|
log.Fatalf("ERROR %v", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
_, err = SendPostRequest("http://localhost:8080/api/devices/register", file)
|
_, err = SendPostRequest("http://localhost:9090/api/devices/register", file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("ERROR %v", err)
|
log.Fatalf("ERROR %v", err)
|
||||||
}
|
}
|
||||||
@@ -47,7 +46,7 @@ func main() {
|
|||||||
log.Fatalf("ERROR %v", err)
|
log.Fatalf("ERROR %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = SendPostRequest(fmt.Sprintf("http://localhost:8080/api/device/%s/smart", diskId), smartDataReader)
|
_, err = SendPostRequest(fmt.Sprintf("http://localhost:9090/api/device/%s/smart", diskId), smartDataReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("ERROR %v", err)
|
log.Fatalf("ERROR %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,22 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/analogj/go-util/utils"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||||
|
"github.com/containrrr/shoutrrr"
|
||||||
|
shoutrrrTypes "github.com/containrrr/shoutrrr/pkg/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/analogj/go-util/utils"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
|
||||||
"github.com/containrrr/shoutrrr"
|
|
||||||
shoutrrrTypes "github.com/containrrr/shoutrrr/pkg/types"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const NotifyFailureTypeEmailTest = "EmailTest"
|
const NotifyFailureTypeEmailTest = "EmailTest"
|
||||||
@@ -32,22 +29,20 @@ const NotifyFailureTypeSmartFailure = "SmartFailure"
|
|||||||
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
|
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
|
||||||
|
|
||||||
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
|
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
|
||||||
func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes, repeatNotifications bool, c *gin.Context, deviceRepo database.DeviceRepo) bool {
|
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLevel string, notifyFilterAttributes string) bool {
|
||||||
// 1. check if the device is healthy
|
// 1. check if the device is healthy
|
||||||
if device.DeviceStatus == pkg.DeviceStatusPassed {
|
if device.DeviceStatus == pkg.DeviceStatusPassed {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: cannot check for warning notifyLevel yet.
|
|
||||||
|
|
||||||
// setup constants for comparison
|
// setup constants for comparison
|
||||||
var requiredDeviceStatus pkg.DeviceStatus
|
var requiredDeviceStatus pkg.DeviceStatus
|
||||||
var requiredAttrStatus pkg.AttributeStatus
|
var requiredAttrStatus pkg.AttributeStatus
|
||||||
if statusThreshold == pkg.MetricsStatusThresholdBoth {
|
if notifyLevel == pkg.NotifyLevelFail {
|
||||||
// either scrutiny or smart failures should trigger an email
|
// either scrutiny or smart failures should trigger an email
|
||||||
requiredDeviceStatus = pkg.DeviceStatusSet(pkg.DeviceStatusFailedSmart, pkg.DeviceStatusFailedScrutiny)
|
requiredDeviceStatus = pkg.DeviceStatusSet(pkg.DeviceStatusFailedSmart, pkg.DeviceStatusFailedScrutiny)
|
||||||
requiredAttrStatus = pkg.AttributeStatusSet(pkg.AttributeStatusFailedSmart, pkg.AttributeStatusFailedScrutiny)
|
requiredAttrStatus = pkg.AttributeStatusSet(pkg.AttributeStatusFailedSmart, pkg.AttributeStatusFailedScrutiny)
|
||||||
} else if statusThreshold == pkg.MetricsStatusThresholdSmart {
|
} else if notifyLevel == pkg.NotifyLevelFailSmart {
|
||||||
//only smart failures
|
//only smart failures
|
||||||
requiredDeviceStatus = pkg.DeviceStatusFailedSmart
|
requiredDeviceStatus = pkg.DeviceStatusFailedSmart
|
||||||
requiredAttrStatus = pkg.AttributeStatusFailedSmart
|
requiredAttrStatus = pkg.AttributeStatusFailedSmart
|
||||||
@@ -56,78 +51,60 @@ func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs me
|
|||||||
requiredAttrStatus = pkg.AttributeStatusFailedScrutiny
|
requiredAttrStatus = pkg.AttributeStatusFailedScrutiny
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is the only case where individual attributes need not be considered
|
// 2. check if the attributes that are failing should be filtered (non-critical)
|
||||||
if statusFilterAttributes == pkg.MetricsStatusFilterAttributesAll && repeatNotifications {
|
// 3. for any unfiltered attribute, store the failure reason (Smart or Scrutiny)
|
||||||
return pkg.DeviceStatusHas(device.DeviceStatus, requiredDeviceStatus)
|
if notifyFilterAttributes == pkg.NotifyFilterAttributesCritical {
|
||||||
}
|
hasFailingCriticalAttr := false
|
||||||
|
var statusFailingCrtiticalAttr pkg.AttributeStatus
|
||||||
|
|
||||||
var failingAttributes []string
|
for attrId, attrData := range smartAttrs.Attributes {
|
||||||
// Loop through the attributes to find the failing ones
|
//find failing attribute
|
||||||
for attrId, attrData := range smartAttrs.Attributes {
|
if attrData.GetStatus() == pkg.AttributeStatusPassed {
|
||||||
var status pkg.AttributeStatus = attrData.GetStatus()
|
continue //skip all passing attributes
|
||||||
// Skip over passing attributes
|
}
|
||||||
if status == pkg.AttributeStatusPassed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user only wants to consider critical attributes, we have to check
|
// merge the status's of all critical attributes
|
||||||
// if the not-passing attribute is critical or not
|
statusFailingCrtiticalAttr = pkg.AttributeStatusSet(statusFailingCrtiticalAttr, attrData.GetStatus())
|
||||||
if statusFilterAttributes == pkg.MetricsStatusFilterAttributesCritical {
|
|
||||||
critical := false
|
//found a failing attribute, see if its critical
|
||||||
if device.IsScsi() {
|
if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical {
|
||||||
critical = thresholds.ScsiMetadata[attrId].Critical
|
hasFailingCriticalAttr = true
|
||||||
} else if device.IsNvme() {
|
} else if device.IsNvme() && thresholds.NmveMetadata[attrId].Critical {
|
||||||
critical = thresholds.NmveMetadata[attrId].Critical
|
hasFailingCriticalAttr = true
|
||||||
} else {
|
} else {
|
||||||
//this is ATA
|
//this is ATA
|
||||||
attrIdInt, err := strconv.Atoi(attrId)
|
attrIdInt, err := strconv.Atoi(attrId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
critical = thresholds.AtaMetadata[attrIdInt].Critical
|
if thresholds.AtaMetadata[attrIdInt].Critical {
|
||||||
}
|
hasFailingCriticalAttr = true
|
||||||
// Skip non-critical, non-passing attributes when this setting is on
|
}
|
||||||
if !critical {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record any attribute that doesn't get skipped by the above two checks
|
if !hasFailingCriticalAttr {
|
||||||
failingAttributes = append(failingAttributes, attrId)
|
//no critical attributes are failing, and notifyFilterAttributes == "critical"
|
||||||
}
|
return false
|
||||||
|
} else {
|
||||||
|
// check if any of the critical attributes have a status that we're looking for
|
||||||
|
return pkg.AttributeStatusHas(statusFailingCrtiticalAttr, requiredAttrStatus)
|
||||||
|
}
|
||||||
|
|
||||||
// If the user doesn't want repeated notifications when the failing value doesn't change, we need to get the last value from the db
|
} else {
|
||||||
var lastPoints []measurements.Smart
|
// 2. SKIP - we are processing every attribute.
|
||||||
var err error
|
// 3. check if the device failure level matches the wanted failure level.
|
||||||
if !repeatNotifications {
|
return pkg.DeviceStatusHas(device.DeviceStatus, requiredDeviceStatus)
|
||||||
lastPoints, err = deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), database.DURATION_KEY_FOREVER, 1, 1, failingAttributes)
|
|
||||||
if err == nil || len(lastPoints) < 1 {
|
|
||||||
logger.Warningln("Could not get the most recent data points from the database. This is expected to happen only if this is the very first submission of data for the device.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, attrId := range failingAttributes {
|
|
||||||
attrStatus := smartAttrs.Attributes[attrId].GetStatus()
|
|
||||||
if pkg.AttributeStatusHas(attrStatus, requiredAttrStatus) {
|
|
||||||
if repeatNotifications {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// This is checked again here to avoid repeating the entire for loop in the check above.
|
|
||||||
// Probably unnoticeably worse performance, but cleaner code.
|
|
||||||
if err != nil || len(lastPoints) < 1 || lastPoints[0].Attributes[attrId].GetTransformedValue() != smartAttrs.Attributes[attrId].GetTransformedValue() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: include user label for device.
|
// TODO: include host and/or user label for device.
|
||||||
type Payload struct {
|
type Payload struct {
|
||||||
HostId string `json:"host_id,omitempty"` //host id (optional)
|
DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
|
||||||
DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
|
DeviceName string `json:"device_name"` //dev/sda
|
||||||
DeviceName string `json:"device_name"` //dev/sda
|
DeviceSerial string `json:"device_serial"` //WDDJ324KSO
|
||||||
DeviceSerial string `json:"device_serial"` //WDDJ324KSO
|
Test bool `json:"test"` // false
|
||||||
Test bool `json:"test"` // false
|
|
||||||
|
|
||||||
//private, populated during init (marked as Public for JSON serialization)
|
//private, populated during init (marked as Public for JSON serialization)
|
||||||
Date string `json:"date"` //populated by Send function.
|
Date string `json:"date"` //populated by Send function.
|
||||||
@@ -136,9 +113,8 @@ type Payload struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPayload(device models.Device, test bool, currentTime ...time.Time) Payload {
|
func NewPayload(device models.Device, test bool) Payload {
|
||||||
payload := Payload{
|
payload := Payload{
|
||||||
HostId: strings.TrimSpace(device.HostId),
|
|
||||||
DeviceType: device.DeviceType,
|
DeviceType: device.DeviceType,
|
||||||
DeviceName: device.DeviceName,
|
DeviceName: device.DeviceName,
|
||||||
DeviceSerial: device.SerialNumber,
|
DeviceSerial: device.SerialNumber,
|
||||||
@@ -146,13 +122,7 @@ func NewPayload(device models.Device, test bool, currentTime ...time.Time) Paylo
|
|||||||
}
|
}
|
||||||
|
|
||||||
//validate that the Payload is populated
|
//validate that the Payload is populated
|
||||||
var sendDate time.Time
|
sendDate := time.Now()
|
||||||
if currentTime != nil && len(currentTime) > 0 {
|
|
||||||
sendDate = currentTime[0]
|
|
||||||
} else {
|
|
||||||
sendDate = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
payload.Date = sendDate.Format(time.RFC3339)
|
payload.Date = sendDate.Format(time.RFC3339)
|
||||||
payload.FailureType = payload.GenerateFailureType(device.DeviceStatus)
|
payload.FailureType = payload.GenerateFailureType(device.DeviceStatus)
|
||||||
payload.Subject = payload.GenerateSubject()
|
payload.Subject = payload.GenerateSubject()
|
||||||
@@ -176,39 +146,25 @@ func (p *Payload) GenerateFailureType(deviceStatus pkg.DeviceStatus) string {
|
|||||||
|
|
||||||
func (p *Payload) GenerateSubject() string {
|
func (p *Payload) GenerateSubject() string {
|
||||||
//generate a detailed failure message
|
//generate a detailed failure message
|
||||||
var subject string
|
return fmt.Sprintf("Scrutiny SMART error (%s) detected on device: %s", p.FailureType, p.DeviceName)
|
||||||
if len(p.HostId) > 0 {
|
|
||||||
subject = fmt.Sprintf("Scrutiny SMART error (%s) detected on [host]device: [%s]%s", p.FailureType, p.HostId, p.DeviceName)
|
|
||||||
} else {
|
|
||||||
subject = fmt.Sprintf("Scrutiny SMART error (%s) detected on device: %s", p.FailureType, p.DeviceName)
|
|
||||||
}
|
|
||||||
return subject
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Payload) GenerateMessage() string {
|
func (p *Payload) GenerateMessage() string {
|
||||||
//generate a detailed failure message
|
//generate a detailed failure message
|
||||||
|
message := fmt.Sprintf(
|
||||||
|
`Scrutiny SMART error notification for device: %s
|
||||||
|
Failure Type: %s
|
||||||
|
Device Name: %s
|
||||||
|
Device Serial: %s
|
||||||
|
Device Type: %s
|
||||||
|
|
||||||
messageParts := []string{}
|
Date: %s`, p.DeviceName, p.FailureType, p.DeviceName, p.DeviceSerial, p.DeviceType, p.Date)
|
||||||
|
|
||||||
messageParts = append(messageParts, fmt.Sprintf("Scrutiny SMART error notification for device: %s", p.DeviceName))
|
|
||||||
if len(p.HostId) > 0 {
|
|
||||||
messageParts = append(messageParts, fmt.Sprintf("Host Id: %s", p.HostId))
|
|
||||||
}
|
|
||||||
|
|
||||||
messageParts = append(messageParts,
|
|
||||||
fmt.Sprintf("Failure Type: %s", p.FailureType),
|
|
||||||
fmt.Sprintf("Device Name: %s", p.DeviceName),
|
|
||||||
fmt.Sprintf("Device Serial: %s", p.DeviceSerial),
|
|
||||||
fmt.Sprintf("Device Type: %s", p.DeviceType),
|
|
||||||
"",
|
|
||||||
fmt.Sprintf("Date: %s", p.Date),
|
|
||||||
)
|
|
||||||
|
|
||||||
if p.Test {
|
if p.Test {
|
||||||
messageParts = append([]string{"TEST NOTIFICATION:"}, messageParts...)
|
message = "TEST NOTIFICATION:\n" + message
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(messageParts, "\n")
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger logrus.FieldLogger, appconfig config.Interface, device models.Device, test bool) Notify {
|
func New(logger logrus.FieldLogger, appconfig config.Interface, device models.Device, test bool) Notify {
|
||||||
@@ -241,7 +197,7 @@ func (n *Notify) Send() error {
|
|||||||
notifyScripts := []string{}
|
notifyScripts := []string{}
|
||||||
notifyShoutrrr := []string{}
|
notifyShoutrrr := []string{}
|
||||||
|
|
||||||
for ndx := range configUrls {
|
for ndx, _ := range configUrls {
|
||||||
if strings.HasPrefix(configUrls[ndx], "https://") || strings.HasPrefix(configUrls[ndx], "http://") {
|
if strings.HasPrefix(configUrls[ndx], "https://") || strings.HasPrefix(configUrls[ndx], "http://") {
|
||||||
notifyWebhooks = append(notifyWebhooks, configUrls[ndx])
|
notifyWebhooks = append(notifyWebhooks, configUrls[ndx])
|
||||||
} else if strings.HasPrefix(configUrls[ndx], "script://") {
|
} else if strings.HasPrefix(configUrls[ndx], "script://") {
|
||||||
@@ -329,9 +285,6 @@ func (n *Notify) SendScriptNotification(scriptUrl string) error {
|
|||||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_TYPE=%s", n.Payload.DeviceType))
|
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_TYPE=%s", n.Payload.DeviceType))
|
||||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_SERIAL=%s", n.Payload.DeviceSerial))
|
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_SERIAL=%s", n.Payload.DeviceSerial))
|
||||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_MESSAGE=%s", n.Payload.Message))
|
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_MESSAGE=%s", n.Payload.Message))
|
||||||
if len(n.Payload.HostId) > 0 {
|
|
||||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_HOST_ID=%s", n.Payload.HostId))
|
|
||||||
}
|
|
||||||
err := utils.CmdExec(scriptPath, []string{}, "", copyEnv, "")
|
err := utils.CmdExec(scriptPath, []string{}, "", copyEnv, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.Logger.Errorf("An error occurred while executing script %s: %v", scriptPath, err)
|
n.Logger.Errorf("An error occurred while executing script %s: %v", scriptPath, err)
|
||||||
@@ -406,9 +359,6 @@ func (n *Notify) GenShoutrrrNotificationParams(shoutrrrUrl string) (string, *sho
|
|||||||
case "join":
|
case "join":
|
||||||
(*params)["title"] = subject
|
(*params)["title"] = subject
|
||||||
(*params)["icon"] = logoUrl
|
(*params)["icon"] = logoUrl
|
||||||
case "ntfy":
|
|
||||||
(*params)["title"] = subject
|
|
||||||
(*params)["icon"] = logoUrl
|
|
||||||
case "opsgenie":
|
case "opsgenie":
|
||||||
(*params)["title"] = subject
|
(*params)["title"] = subject
|
||||||
case "pushbullet":
|
case "pushbullet":
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
package notify
|
package notify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
|
||||||
mock_database "github.com/analogj/scrutiny/webapp/backend/pkg/database/mock"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
"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"
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
||||||
@@ -24,65 +15,56 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
|||||||
DeviceStatus: pkg.DeviceStatusPassed,
|
DeviceStatus: pkg.DeviceStatusPassed,
|
||||||
}
|
}
|
||||||
smartAttrs := measurements.Smart{}
|
smartAttrs := measurements.Smart{}
|
||||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
notifyLevel := pkg.NotifyLevelFail
|
||||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||||
|
|
||||||
mockCtrl := gomock.NewController(t)
|
|
||||||
defer mockCtrl.Finish()
|
|
||||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
|
||||||
//assert
|
//assert
|
||||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) {
|
func TestShouldNotify_NotifyLevelFail_FailingSmartDevice(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
//setupD
|
|
||||||
device := models.Device{
|
|
||||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
|
||||||
}
|
|
||||||
smartAttrs := measurements.Smart{}
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||||
}
|
}
|
||||||
smartAttrs := measurements.Smart{}
|
smartAttrs := measurements.Smart{}
|
||||||
statusThreshold := pkg.MetricsStatusThresholdSmart
|
notifyLevel := pkg.NotifyLevelFail
|
||||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||||
mockCtrl := gomock.NewController(t)
|
|
||||||
defer mockCtrl.Finish()
|
|
||||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
|
||||||
//assert
|
//assert
|
||||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) {
|
func TestShouldNotify_NotifyLevelFailSmart_FailingSmartDevice(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||||
}
|
}
|
||||||
smartAttrs := measurements.Smart{}
|
smartAttrs := measurements.Smart{}
|
||||||
statusThreshold := pkg.MetricsStatusThresholdScrutiny
|
notifyLevel := pkg.NotifyLevelFailSmart
|
||||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||||
mockCtrl := gomock.NewController(t)
|
|
||||||
defer mockCtrl.Finish()
|
|
||||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
|
||||||
//assert
|
//assert
|
||||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
|
func TestShouldNotify_NotifyLevelFailScrutiny_FailingSmartDevice(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
//setup
|
||||||
|
device := models.Device{
|
||||||
|
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||||
|
}
|
||||||
|
smartAttrs := measurements.Smart{}
|
||||||
|
notifyLevel := pkg.NotifyLevelFailScrutiny
|
||||||
|
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldNotify_NotifyFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
@@ -93,17 +75,14 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t
|
|||||||
Status: pkg.AttributeStatusFailedSmart,
|
Status: pkg.AttributeStatusFailedSmart,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
notifyLevel := pkg.NotifyLevelFail
|
||||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||||
mockCtrl := gomock.NewController(t)
|
|
||||||
defer mockCtrl.Finish()
|
|
||||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
|
func TestShouldNotify_NotifyFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
@@ -117,17 +96,14 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCritical
|
|||||||
Status: pkg.AttributeStatusFailedScrutiny,
|
Status: pkg.AttributeStatusFailedScrutiny,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
notifyLevel := pkg.NotifyLevelFail
|
||||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||||
mockCtrl := gomock.NewController(t)
|
|
||||||
defer mockCtrl.Finish()
|
|
||||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
|
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
@@ -138,17 +114,14 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(
|
|||||||
Status: pkg.AttributeStatusFailedSmart,
|
Status: pkg.AttributeStatusFailedSmart,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
notifyLevel := pkg.NotifyLevelFail
|
||||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||||
mockCtrl := gomock.NewController(t)
|
|
||||||
defer mockCtrl.Finish()
|
|
||||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
|
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
@@ -159,17 +132,14 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCritica
|
|||||||
Status: pkg.AttributeStatusPassed,
|
Status: pkg.AttributeStatusPassed,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
statusThreshold := pkg.MetricsStatusThresholdBoth
|
notifyLevel := pkg.NotifyLevelFail
|
||||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||||
mockCtrl := gomock.NewController(t)
|
|
||||||
defer mockCtrl.Finish()
|
|
||||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
|
func TestShouldNotify_NotifyFilterAttributesCritical_NotifyLevelFailSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
@@ -183,158 +153,9 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho
|
|||||||
Status: pkg.AttributeStatusFailedScrutiny,
|
Status: pkg.AttributeStatusFailedScrutiny,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
statusThreshold := pkg.MetricsStatusThresholdSmart
|
notifyLevel := pkg.NotifyLevelFailSmart
|
||||||
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
||||||
mockCtrl := gomock.NewController(t)
|
|
||||||
defer mockCtrl.Finish()
|
|
||||||
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
|
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
|
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
||||||
}
|
|
||||||
func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
//setup
|
|
||||||
device := models.Device{
|
|
||||||
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
|
|
||||||
}
|
|
||||||
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
|
|
||||||
"5": &measurements.SmartAtaAttribute{
|
|
||||||
Status: pkg.AttributeStatusFailedScrutiny,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
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)
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
//setup
|
|
||||||
device := models.Device{
|
|
||||||
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
|
|
||||||
}
|
|
||||||
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
|
|
||||||
"5": &measurements.SmartAtaAttribute{
|
|
||||||
Status: pkg.AttributeStatusFailedScrutiny,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
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)
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
|
||||||
}
|
|
||||||
func TestShouldNotify_NoRepeat(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
//setup
|
|
||||||
device := models.Device{
|
|
||||||
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
|
|
||||||
}
|
|
||||||
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
|
|
||||||
"5": &measurements.SmartAtaAttribute{
|
|
||||||
Status: pkg.AttributeStatusFailedScrutiny,
|
|
||||||
TransformedValue: 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
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)
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewPayload(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//setup
|
|
||||||
device := models.Device{
|
|
||||||
SerialNumber: "FAKEWDDJ324KSO",
|
|
||||||
DeviceType: pkg.DeviceProtocolAta,
|
|
||||||
DeviceName: "/dev/sda",
|
|
||||||
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
|
|
||||||
}
|
|
||||||
currentTime := time.Now()
|
|
||||||
//test
|
|
||||||
|
|
||||||
payload := NewPayload(device, false, currentTime)
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, "Scrutiny SMART error (ScrutinyFailure) detected on device: /dev/sda", payload.Subject)
|
|
||||||
require.Equal(t, fmt.Sprintf(`Scrutiny SMART error notification for device: /dev/sda
|
|
||||||
Failure Type: ScrutinyFailure
|
|
||||||
Device Name: /dev/sda
|
|
||||||
Device Serial: FAKEWDDJ324KSO
|
|
||||||
Device Type: ATA
|
|
||||||
|
|
||||||
Date: %s`, currentTime.Format(time.RFC3339)), payload.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewPayload_TestMode(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//setup
|
|
||||||
device := models.Device{
|
|
||||||
SerialNumber: "FAKEWDDJ324KSO",
|
|
||||||
DeviceType: pkg.DeviceProtocolAta,
|
|
||||||
DeviceName: "/dev/sda",
|
|
||||||
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
|
|
||||||
}
|
|
||||||
currentTime := time.Now()
|
|
||||||
//test
|
|
||||||
|
|
||||||
payload := NewPayload(device, true, currentTime)
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, "Scrutiny SMART error (EmailTest) detected on device: /dev/sda", payload.Subject)
|
|
||||||
require.Equal(t, fmt.Sprintf(`TEST NOTIFICATION:
|
|
||||||
Scrutiny SMART error notification for device: /dev/sda
|
|
||||||
Failure Type: EmailTest
|
|
||||||
Device Name: /dev/sda
|
|
||||||
Device Serial: FAKEWDDJ324KSO
|
|
||||||
Device Type: ATA
|
|
||||||
|
|
||||||
Date: %s`, currentTime.Format(time.RFC3339)), payload.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewPayload_WithHostId(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
//setup
|
|
||||||
device := models.Device{
|
|
||||||
SerialNumber: "FAKEWDDJ324KSO",
|
|
||||||
DeviceType: pkg.DeviceProtocolAta,
|
|
||||||
DeviceName: "/dev/sda",
|
|
||||||
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
|
|
||||||
HostId: "custom-host",
|
|
||||||
}
|
|
||||||
currentTime := time.Now()
|
|
||||||
//test
|
|
||||||
|
|
||||||
payload := NewPayload(device, false, currentTime)
|
|
||||||
|
|
||||||
//assert
|
|
||||||
require.Equal(t, "Scrutiny SMART error (ScrutinyFailure) detected on [host]device: [custom-host]/dev/sda", payload.Subject)
|
|
||||||
require.Equal(t, fmt.Sprintf(`Scrutiny SMART error notification for device: /dev/sda
|
|
||||||
Host Id: custom-host
|
|
||||||
Failure Type: ScrutinyFailure
|
|
||||||
Device Name: /dev/sda
|
|
||||||
Device Serial: FAKEWDDJ324KSO
|
|
||||||
Device Type: ATA
|
|
||||||
|
|
||||||
Date: %s`, currentTime.Format(time.RFC3339)), payload.Message)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
package thresholds
|
package thresholds
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const AtaSmartAttributeDisplayTypeRaw = "raw"
|
const AtaSmartAttributeDisplayTypeRaw = "raw"
|
||||||
const AtaSmartAttributeDisplayTypeNormalized = "normalized"
|
const AtaSmartAttributeDisplayTypeNormalized = "normalized"
|
||||||
const AtaSmartAttributeDisplayTypeTransformed = "transformed"
|
const AtaSmartAttributeDisplayTypeTransformed = "transformed"
|
||||||
@@ -667,84 +662,62 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
|
|||||||
188: {
|
188: {
|
||||||
ID: 188,
|
ID: 188,
|
||||||
DisplayName: "Command Timeout",
|
DisplayName: "Command Timeout",
|
||||||
DisplayType: AtaSmartAttributeDisplayTypeTransformed,
|
DisplayType: AtaSmartAttributeDisplayTypeRaw,
|
||||||
Ideal: ObservedThresholdIdealLow,
|
Ideal: ObservedThresholdIdealLow,
|
||||||
Critical: true,
|
Critical: true,
|
||||||
Description: "The count of aborted operations due to HDD timeout. Normally this attribute value should be equal to zero.",
|
Description: "The count of aborted operations due to HDD timeout. Normally this attribute value should be equal to zero.",
|
||||||
Transform: func(normValue int64, rawValue int64, rawString string) int64 {
|
|
||||||
// Parse Seagate command timeout values if the string contains 3 pieces
|
|
||||||
// and each piece is less than or equal to the next (as a sanity check)
|
|
||||||
// See https://github.com/AnalogJ/scrutiny/issues/522
|
|
||||||
pieces := strings.Split(rawString, " ")
|
|
||||||
if len(pieces) == 3 {
|
|
||||||
int_pieces := make([]int, len(pieces))
|
|
||||||
var err error
|
|
||||||
for i, s := range pieces {
|
|
||||||
int_pieces[i], err = strconv.Atoi(s)
|
|
||||||
if err != nil {
|
|
||||||
return rawValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if int_pieces[2] >= int_pieces[1] && int_pieces[1] >= int_pieces[0] {
|
|
||||||
return int64(int_pieces[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rawValue
|
|
||||||
},
|
|
||||||
ObservedThresholds: []ObservedThreshold{
|
ObservedThresholds: []ObservedThreshold{
|
||||||
{
|
{
|
||||||
Low: 0,
|
Low: 0,
|
||||||
// This is set arbitrarily to avoid notifications caused by low
|
High: 0,
|
||||||
// historical numbers of command timeouts (e.g. caused by a bad cable)
|
|
||||||
High: 100,
|
|
||||||
AnnualFailureRate: 0.024893587674442153,
|
AnnualFailureRate: 0.024893587674442153,
|
||||||
ErrorInterval: []float64{0.020857343769186413, 0.0294830350167543},
|
ErrorInterval: []float64{0.020857343769186413, 0.0294830350167543},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Low: 100,
|
Low: 0,
|
||||||
High: 13000000000,
|
High: 13,
|
||||||
AnnualFailureRate: 0.10044174089362015,
|
AnnualFailureRate: 0.10044174089362015,
|
||||||
ErrorInterval: []float64{0.0812633664077498, 0.1227848196758574},
|
ErrorInterval: []float64{0.0812633664077498, 0.1227848196758574},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Low: 13000000000,
|
Low: 13,
|
||||||
High: 26000000000,
|
High: 26,
|
||||||
AnnualFailureRate: 0.334030592234279,
|
AnnualFailureRate: 0.334030592234279,
|
||||||
ErrorInterval: []float64{0.2523231196342665, 0.4337665082489293},
|
ErrorInterval: []float64{0.2523231196342665, 0.4337665082489293},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Low: 26000000000,
|
Low: 26,
|
||||||
High: 39000000000,
|
High: 39,
|
||||||
AnnualFailureRate: 0.36724705400842445,
|
AnnualFailureRate: 0.36724705400842445,
|
||||||
ErrorInterval: []float64{0.30398009356575617, 0.4397986538328568},
|
ErrorInterval: []float64{0.30398009356575617, 0.4397986538328568},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Low: 39000000000,
|
Low: 39,
|
||||||
High: 52000000000,
|
High: 52,
|
||||||
AnnualFailureRate: 0.29848155926978354,
|
AnnualFailureRate: 0.29848155926978354,
|
||||||
ErrorInterval: []float64{0.2509254838615984, 0.35242890006477073},
|
ErrorInterval: []float64{0.2509254838615984, 0.35242890006477073},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Low: 52000000000,
|
Low: 52,
|
||||||
High: 65000000000,
|
High: 65,
|
||||||
AnnualFailureRate: 0.2203079701535098,
|
AnnualFailureRate: 0.2203079701535098,
|
||||||
ErrorInterval: []float64{0.18366082845676174, 0.26212468677179274},
|
ErrorInterval: []float64{0.18366082845676174, 0.26212468677179274},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Low: 65000000000,
|
Low: 65,
|
||||||
High: 78000000000,
|
High: 78,
|
||||||
AnnualFailureRate: 0.3018169948863018,
|
AnnualFailureRate: 0.3018169948863018,
|
||||||
ErrorInterval: []float64{0.23779746376787655, 0.37776897542831006},
|
ErrorInterval: []float64{0.23779746376787655, 0.37776897542831006},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Low: 78000000000,
|
Low: 78,
|
||||||
High: 91000000000,
|
High: 91,
|
||||||
AnnualFailureRate: 0.32854928239235887,
|
AnnualFailureRate: 0.32854928239235887,
|
||||||
ErrorInterval: []float64{0.2301118782147336, 0.4548506948185028},
|
ErrorInterval: []float64{0.2301118782147336, 0.4548506948185028},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Low: 91000000000,
|
Low: 91,
|
||||||
High: 104000000000,
|
High: 104,
|
||||||
AnnualFailureRate: 0.28488916640649387,
|
AnnualFailureRate: 0.28488916640649387,
|
||||||
ErrorInterval: []float64{0.1366154288236293, 0.5239213202729072},
|
ErrorInterval: []float64{0.1366154288236293, 0.5239213202729072},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ package version
|
|||||||
|
|
||||||
// VERSION is the app-global version string, which will be replaced with a
|
// VERSION is the app-global version string, which will be replaced with a
|
||||||
// new value during packaging
|
// new value during packaging
|
||||||
const VERSION = "0.8.1"
|
const VERSION = "0.4.15"
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
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})
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func DeleteDevice(c *gin.Context) {
|
func DeleteDevice(c *gin.Context) {
|
||||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||||
|
|
||||||
err := deviceRepo.DeleteDevice(c, c.Param("wwn"))
|
err := deviceRepo.DeleteDevice(c, c.Param("wwn"))
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetDeviceDetails(c *gin.Context) {
|
func GetDeviceDetails(c *gin.Context) {
|
||||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||||
|
|
||||||
device, err := deviceRepo.GetDeviceDetails(c, c.Param("wwn"))
|
device, err := deviceRepo.GetDeviceDetails(c, c.Param("wwn"))
|
||||||
@@ -25,7 +24,7 @@ func GetDeviceDetails(c *gin.Context) {
|
|||||||
durationKey = "forever"
|
durationKey = "forever"
|
||||||
}
|
}
|
||||||
|
|
||||||
smartResults, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), durationKey, 0, 0, nil)
|
smartResults, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), durationKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorln("An error occurred while retrieving device smart results", err)
|
logger.Errorln("An error occurred while retrieving device smart results", err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetDevicesSummary(c *gin.Context) {
|
func GetDevicesSummary(c *gin.Context) {
|
||||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||||
|
|
||||||
summary, err := deviceRepo.GetSummary(c)
|
summary, err := deviceRepo.GetSummary(c)
|
||||||
@@ -18,7 +18,6 @@ func GetDevicesSummary(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//this must match DeviceSummaryWrapper (webapp/backend/pkg/models/device_summary.go)
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": map[string]interface{}{
|
"data": map[string]interface{}{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetDevicesSummaryTempHistory(c *gin.Context) {
|
func GetDevicesSummaryTempHistory(c *gin.Context) {
|
||||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||||
|
|
||||||
durationKey, exists := c.GetQuery("duration_key")
|
durationKey, exists := c.GetQuery("duration_key")
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetSettings(c *gin.Context) {
|
|
||||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
|
||||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
|
||||||
|
|
||||||
settings, err := deviceRepo.LoadSettings(c)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorln("An error occurred while retrieving settings", err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": true,
|
|
||||||
"settings": settings,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HealthCheck(c *gin.Context) {
|
|
||||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
|
||||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
|
||||||
logger.Infof("Checking Influxdb & Sqlite health")
|
|
||||||
|
|
||||||
//check sqlite and influxdb health
|
|
||||||
err := deviceRepo.HealthCheck(c)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorln("An error occurred during healthcheck", err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO:
|
|
||||||
// check if the /web folder is populated.
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/samber/lo"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
@@ -13,7 +12,7 @@ import (
|
|||||||
// This function is run everytime a collector is about to start a run. It can be used to update device metadata.
|
// This function is run everytime a collector is about to start a run. It can be used to update device metadata.
|
||||||
func RegisterDevices(c *gin.Context) {
|
func RegisterDevices(c *gin.Context) {
|
||||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||||
|
|
||||||
var collectorDeviceWrapper models.DeviceWrapper
|
var collectorDeviceWrapper models.DeviceWrapper
|
||||||
err := c.BindJSON(&collectorDeviceWrapper)
|
err := c.BindJSON(&collectorDeviceWrapper)
|
||||||
@@ -23,13 +22,8 @@ func RegisterDevices(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//filter any device with empty wwn (they are invalid)
|
|
||||||
detectedStorageDevices := lo.Filter[models.Device](collectorDeviceWrapper.Data, func(dev models.Device, _ int) bool {
|
|
||||||
return len(dev.WWN) > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
errs := []error{}
|
errs := []error{}
|
||||||
for _, dev := range detectedStorageDevices {
|
for _, dev := range collectorDeviceWrapper.Data {
|
||||||
//insert devices into DB (and update specified columns if device is already registered)
|
//insert devices into DB (and update specified columns if device is already registered)
|
||||||
// update device fields that may change: (DeviceType, HostID)
|
// update device fields that may change: (DeviceType, HostID)
|
||||||
if err := deviceRepo.RegisterDevice(c, dev); err != nil {
|
if err := deviceRepo.RegisterDevice(c, dev); err != nil {
|
||||||
@@ -46,7 +40,7 @@ func RegisterDevices(c *gin.Context) {
|
|||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, models.DeviceWrapper{
|
c.JSON(http.StatusOK, models.DeviceWrapper{
|
||||||
Success: true,
|
Success: true,
|
||||||
Data: detectedStorageDevices,
|
Data: collectorDeviceWrapper.Data,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SaveSettings(c *gin.Context) {
|
|
||||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
|
||||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
|
||||||
|
|
||||||
var settings models.Settings
|
|
||||||
err := c.BindJSON(&settings)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorln("Cannot parse updated settings", err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = deviceRepo.SaveSettings(c, settings)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorln("An error occurred while saving settings", err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": true,
|
|
||||||
"settings": settings,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user