Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb307409a0 | |||
| dd202c63ec | |||
| 9dd80d7d66 | |||
| bf07077154 | |||
| bd2f305d80 | |||
| e32fc5947e | |||
| ac6b02808c | |||
| 0a55a7076f | |||
| 9e10f3bf50 | |||
| f410e60235 | |||
| 1d35b14c9d | |||
| aff9a0b258 | |||
| 9ffc55ba13 | |||
| 99ec2eb2dc | |||
| 7e78fb4e7d | |||
| 4bd3dd4311 | |||
| f27883ea4c | |||
| 1e18086c4d | |||
| b04f1b07e8 | |||
| 8b5e95f1e1 | |||
| 22429800a7 | |||
| 27e9d2f465 | |||
| 9fac3c6308 | |||
| 1cd5ebaa43 | |||
| b44ef5cb9c | |||
| 9dfc57432b | |||
| 32e7044c67 | |||
| ac7c1f28cf | |||
| c8a4be6b07 | |||
| 80a382d098 | |||
| 81b05515d3 | |||
| 1c7ca35ea7 | |||
| a3438297e6 | |||
| 732eb039da | |||
| 2d903453d5 | |||
| 6ca4ce39de | |||
| 4a12caf043 | |||
| 46d04734b6 | |||
| d339e967a6 | |||
| 6377a258f6 | |||
| be378bd147 | |||
| 1246f5bba9 | |||
| adbbcb285a | |||
| f0c3fddf26 | |||
| 9816c750b4 | |||
| cd6c59599f | |||
| 94559d265c | |||
| 95e5a5e768 | |||
| 7742f4c6c3 | |||
| 6473ec1a28 | |||
| c7c1e37170 | |||
| 7de7935790 | |||
| 7988381433 | |||
| 013eccdd58 | |||
| a80794590a | |||
| f56200452a | |||
| 32682283da | |||
| 8775b02970 | |||
| 38ea7f7839 | |||
| cb0aa9823e |
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Build
|
- name: Test
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: linux
|
||||||
GOARCH: amd64
|
GOARCH: amd64
|
||||||
@@ -22,39 +22,90 @@ jobs:
|
|||||||
cd $PROJECT_PATH
|
cd $PROJECT_PATH
|
||||||
|
|
||||||
go mod vendor
|
go mod vendor
|
||||||
|
|
||||||
go test -race -coverprofile=coverage.txt -covermode=atomic -v -tags "static" $(go list ./... | grep -v /vendor/)
|
go test -race -coverprofile=coverage.txt -covermode=atomic -v -tags "static" $(go list ./... | grep -v /vendor/)
|
||||||
|
- name: Build amd64
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
run: |
|
||||||
|
apt-get update && apt-get install -y file
|
||||||
|
|
||||||
go build -ldflags "-X main.goos=linux -X main.goarch=amd64" -o scrutiny-web-linux-amd64 -tags "static" webapp/backend/cmd/scrutiny/scrutiny.go
|
cd $PROJECT_PATH
|
||||||
go build -ldflags "-X main.goos=linux -X main.goarch=amd64" -o scrutiny-collector-metrics-linux-amd64 -tags "static" collector/cmd/collector-metrics/collector-metrics.go
|
echo "###### Build Web ######"
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=amd64" -o scrutiny-web-linux-amd64 -tags "static netgo sqlite_omit_load_extension" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||||
|
echo "###### Build Collector ######"
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=amd64" -o scrutiny-collector-metrics-linux-amd64 -tags "static netgo" collector/cmd/collector-metrics/collector-metrics.go
|
||||||
|
|
||||||
chmod +x scrutiny-web-linux-amd64
|
chmod +x scrutiny-web-linux-amd64
|
||||||
chmod +x scrutiny-collector-metrics-linux-amd64
|
chmod +x scrutiny-collector-metrics-linux-amd64
|
||||||
|
|
||||||
|
file scrutiny-web-linux-amd64
|
||||||
|
ldd scrutiny-web-linux-amd64 || true
|
||||||
|
file scrutiny-collector-metrics-linux-amd64
|
||||||
|
ldd scrutiny-collector-metrics-linux-amd64 || true
|
||||||
|
- name: Build arm
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: arm
|
||||||
|
run: |
|
||||||
|
cd $PROJECT_PATH
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=arm" -o scrutiny-web-linux-arm -tags "static netgo sqlite_omit_load_extension" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=arm" -o scrutiny-collector-metrics-linux-arm -tags "static netgo" collector/cmd/collector-metrics/collector-metrics.go
|
||||||
|
|
||||||
|
chmod +x scrutiny-web-linux-arm
|
||||||
|
chmod +x scrutiny-collector-metrics-linux-arm
|
||||||
|
|
||||||
|
- name: Build arm64
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: arm64
|
||||||
|
run: |
|
||||||
|
cd $PROJECT_PATH
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=arm64" -o scrutiny-web-linux-arm64 -tags "static netgo sqlite_omit_load_extension" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=arm64" -o scrutiny-collector-metrics-linux-arm64 -tags "static netgo" collector/cmd/collector-metrics/collector-metrics.go
|
||||||
|
|
||||||
|
chmod +x scrutiny-web-linux-arm64
|
||||||
|
chmod +x scrutiny-collector-metrics-linux-arm64
|
||||||
|
|
||||||
|
- name: Build windows
|
||||||
|
env:
|
||||||
|
GOOS: windows
|
||||||
|
GOARCH: amd64
|
||||||
|
run: |
|
||||||
|
cd $PROJECT_PATH
|
||||||
|
go build -ldflags "-X main.goos=windows -X main.goarch=amd64" -o scrutiny-web-windows-amd64.exe -tags "static" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||||
|
go build -ldflags "-X main.goos=windows -X main.goarch=amd64" -o scrutiny-collector-metrics-windows-amd64.exe -tags "static" collector/cmd/collector-metrics/collector-metrics.go
|
||||||
|
|
||||||
|
- name: Build freebsd
|
||||||
|
env:
|
||||||
|
GOOS: freebsd
|
||||||
|
GOARCH: amd64
|
||||||
|
run: |
|
||||||
|
cd $PROJECT_PATH
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=freebsd -X main.goarch=amd64" -o scrutiny-web-freebsd-amd64 -tags "static netgo sqlite_omit_load_extension" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=freebsd -X main.goarch=amd64" -o scrutiny-collector-metrics-freebsd-amd64 -tags "static netgo" collector/cmd/collector-metrics/collector-metrics.go
|
||||||
|
|
||||||
|
chmod +x scrutiny-web-freebsd-amd64
|
||||||
|
chmod +x scrutiny-collector-metrics-freebsd-amd64
|
||||||
|
|
||||||
- name: Archive
|
- name: Archive
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: binaries
|
name: binaries.zip
|
||||||
path: |
|
path: |
|
||||||
${{ env.PROJECT_PATH }}/scrutiny-web-linux-amd64
|
${{ env.PROJECT_PATH }}/scrutiny-web-linux-amd64
|
||||||
${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-linux-amd64
|
${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-linux-amd64
|
||||||
|
${{ env.PROJECT_PATH }}/scrutiny-web-linux-arm64
|
||||||
|
${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-linux-arm64
|
||||||
|
${{ env.PROJECT_PATH }}/scrutiny-web-linux-arm
|
||||||
|
${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-linux-arm
|
||||||
|
${{ env.PROJECT_PATH }}/scrutiny-web-windows-amd64.exe
|
||||||
|
${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-windows-amd64.exe
|
||||||
|
${{ env.PROJECT_PATH }}/scrutiny-web-freebsd-amd64
|
||||||
|
${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-freebsd-amd64
|
||||||
- uses: codecov/codecov-action@v1
|
- uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
file: ${{ env.PROJECT_PATH }}/coverage.txt
|
file: ${{ env.PROJECT_PATH }}/coverage.txt
|
||||||
flags: unittests
|
flags: unittests
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
build-docker:
|
|
||||||
name: Build Docker
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: docker:19.03.2
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Build Docker
|
|
||||||
run: |
|
|
||||||
docker build -t analogj/scrutiny -f docker/Dockerfile .
|
|
||||||
docker save -o docker-analogj-scrutiny-latest.tar analogj/scrutiny:latest
|
|
||||||
- name: Archive Docker
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: docker
|
|
||||||
path: docker-analogj-scrutiny-latest.tar
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 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: Build Frontend
|
||||||
|
run: |
|
||||||
|
cd webapp/frontend
|
||||||
|
npm install -g @angular/cli@9.1.4
|
||||||
|
npm install
|
||||||
|
mkdir -p dist
|
||||||
|
ng build --output-path=dist --deploy-url="/web/" --base-href="/web/" --prod
|
||||||
|
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: './webapp/frontend/scrutiny-web-frontend.tar.gz'
|
||||||
|
asset_name: scrutiny-web-frontend.tar.gz
|
||||||
|
asset_content_type: application/gzip
|
||||||
@@ -29,24 +29,75 @@ jobs:
|
|||||||
version_bump_type: ${{ github.event.inputs.version_bump_type }}
|
version_bump_type: ${{ github.event.inputs.version_bump_type }}
|
||||||
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
|
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
|
||||||
github_token: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
|
github_token: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
|
||||||
- name: Build
|
- name: Test
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: linux
|
||||||
GOARCH: amd64
|
GOARCH: amd64
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $PROJECT_PATH
|
mkdir -p $PROJECT_PATH
|
||||||
cp -a $GITHUB_WORKSPACE/. $PROJECT_PATH/
|
cp -a $GITHUB_WORKSPACE/* $PROJECT_PATH/
|
||||||
cd $PROJECT_PATH
|
cd $PROJECT_PATH
|
||||||
|
|
||||||
go mod vendor
|
go mod vendor
|
||||||
|
|
||||||
go test -v -tags "static" $(go list ./... | grep -v /vendor/)
|
go test -v -tags "static" $(go list ./... | grep -v /vendor/)
|
||||||
|
|
||||||
go build -ldflags "-X main.goos=linux -X main.goarch=amd64" -o scrutiny-web-linux-amd64 -tags "static" webapp/backend/cmd/scrutiny/scrutiny.go
|
- name: Build amd64
|
||||||
go build -ldflags "-X main.goos=linux -X main.goarch=amd64" -o scrutiny-collector-metrics-linux-amd64 -tags "static" collector/cmd/collector-metrics/collector-metrics.go
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
|
run: |
|
||||||
|
cd $PROJECT_PATH
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=amd64" -o scrutiny-web-linux-amd64 -tags "static netgo sqlite_omit_load_extension" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=amd64" -o scrutiny-collector-metrics-linux-amd64 -tags "static netgo" collector/cmd/collector-metrics/collector-metrics.go
|
||||||
|
|
||||||
chmod +x scrutiny-web-linux-amd64
|
chmod +x scrutiny-web-linux-amd64
|
||||||
chmod +x scrutiny-collector-metrics-linux-amd64
|
chmod +x scrutiny-collector-metrics-linux-amd64
|
||||||
|
|
||||||
|
- name: Build arm
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: arm
|
||||||
|
run: |
|
||||||
|
cd $PROJECT_PATH
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=arm" -o scrutiny-web-linux-arm -tags "static netgo sqlite_omit_load_extension" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=arm" -o scrutiny-collector-metrics-linux-arm -tags "static netgo" collector/cmd/collector-metrics/collector-metrics.go
|
||||||
|
|
||||||
|
chmod +x scrutiny-web-linux-arm
|
||||||
|
chmod +x scrutiny-collector-metrics-linux-arm
|
||||||
|
|
||||||
|
- name: Build arm64
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: arm64
|
||||||
|
run: |
|
||||||
|
cd $PROJECT_PATH
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=arm64" -o scrutiny-web-linux-arm64 -tags "static netgo sqlite_omit_load_extension" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=linux -X main.goarch=arm64" -o scrutiny-collector-metrics-linux-arm64 -tags "static netgo" collector/cmd/collector-metrics/collector-metrics.go
|
||||||
|
|
||||||
|
chmod +x scrutiny-web-linux-arm64
|
||||||
|
chmod +x scrutiny-collector-metrics-linux-arm64
|
||||||
|
|
||||||
|
- name: Build windows
|
||||||
|
env:
|
||||||
|
GOOS: windows
|
||||||
|
GOARCH: amd64
|
||||||
|
run: |
|
||||||
|
cd $PROJECT_PATH
|
||||||
|
go build -ldflags "-X main.goos=windows -X main.goarch=amd64" -o scrutiny-web-windows-amd64.exe -tags "static" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||||
|
go build -ldflags "-X main.goos=windows -X main.goarch=amd64" -o scrutiny-collector-metrics-windows-amd64.exe -tags "static" collector/cmd/collector-metrics/collector-metrics.go
|
||||||
|
|
||||||
|
- name: Build freebsd
|
||||||
|
env:
|
||||||
|
GOOS: freebsd
|
||||||
|
GOARCH: amd64
|
||||||
|
run: |
|
||||||
|
cd $PROJECT_PATH
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=freebsd -X main.goarch=amd64" -o scrutiny-web-freebsd-amd64 -tags "static netgo sqlite_omit_load_extension" webapp/backend/cmd/scrutiny/scrutiny.go
|
||||||
|
go build -ldflags "-extldflags=-static -X main.goos=freebsd -X main.goarch=amd64" -o scrutiny-collector-metrics-freebsd-amd64 -tags "static netgo" collector/cmd/collector-metrics/collector-metrics.go
|
||||||
|
|
||||||
|
chmod +x scrutiny-web-freebsd-amd64
|
||||||
|
chmod +x scrutiny-collector-metrics-freebsd-amd64
|
||||||
|
|
||||||
- name: Commit
|
- name: Commit
|
||||||
uses: EndBug/add-and-commit@v4 # You can change this to use a specific version
|
uses: EndBug/add-and-commit@v4 # You can change this to use a specific version
|
||||||
with:
|
with:
|
||||||
@@ -71,7 +122,8 @@ jobs:
|
|||||||
release_name: Release ${{ steps.bump_version.outputs.release_version }}
|
release_name: Release ${{ steps.bump_version.outputs.release_version }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
- name: Upload Web Backend Release Asset
|
|
||||||
|
- name: Release Asset - Web - linux-amd64
|
||||||
id: upload-release-asset1
|
id: upload-release-asset1
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
@@ -81,7 +133,7 @@ jobs:
|
|||||||
asset_path: ${{ env.PROJECT_PATH }}/scrutiny-web-linux-amd64
|
asset_path: ${{ env.PROJECT_PATH }}/scrutiny-web-linux-amd64
|
||||||
asset_name: scrutiny-web-linux-amd64
|
asset_name: scrutiny-web-linux-amd64
|
||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
- name: Upload Collector Release Asset
|
- name: Release Asset - Collector - linux-amd64
|
||||||
id: upload-release-asset2
|
id: upload-release-asset2
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
@@ -91,3 +143,67 @@ jobs:
|
|||||||
asset_path: ${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-linux-amd64
|
asset_path: ${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-linux-amd64
|
||||||
asset_name: scrutiny-collector-metrics-linux-amd64
|
asset_name: scrutiny-collector-metrics-linux-amd64
|
||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
|
|
||||||
|
- name: Release Asset - Web - linux-arm64
|
||||||
|
id: upload-release-asset3
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.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: ${{ env.PROJECT_PATH }}/scrutiny-web-linux-arm64
|
||||||
|
asset_name: scrutiny-web-linux-arm64
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
- name: Release Asset - Collector - linux-arm64
|
||||||
|
id: upload-release-asset4
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.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: ${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-linux-arm64
|
||||||
|
asset_name: scrutiny-collector-metrics-linux-arm64
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
|
- name: Release Asset - Web - linux-arm
|
||||||
|
id: upload-release-asset5
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.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: ${{ env.PROJECT_PATH }}/scrutiny-web-linux-arm
|
||||||
|
asset_name: scrutiny-web-linux-arm
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
- name: Release Asset - Collector - linux-arm
|
||||||
|
id: upload-release-asset6
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.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: ${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-linux-arm
|
||||||
|
asset_name: scrutiny-collector-metrics-linux-arm
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
|
- name: Release Asset - Web - freebsd-amd64
|
||||||
|
id: upload-release-asset7
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.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: ${{ env.PROJECT_PATH }}/scrutiny-web-freebsd-amd64
|
||||||
|
asset_name: scrutiny-web-freebsd-amd64
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
- name: Release Asset - Collector - freebsd-amd64
|
||||||
|
id: upload-release-asset8
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.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: ${{ env.PROJECT_PATH }}/scrutiny-collector-metrics-freebsd-amd64
|
||||||
|
asset_name: scrutiny-collector-metrics-freebsd-amd64
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|||||||
+2
-2
@@ -58,8 +58,8 @@ scrutiny.db
|
|||||||
/dist/
|
/dist/
|
||||||
vendor
|
vendor
|
||||||
/scrutiny
|
/scrutiny
|
||||||
/scrutiny-collector-metrics-linux-amd64
|
/scrutiny-collector-metrics-*
|
||||||
/scrutiny-web-linux-amd64
|
/scrutiny-web-*
|
||||||
scrutiny-*.db
|
scrutiny-*.db
|
||||||
scrutiny_test.db
|
scrutiny_test.db
|
||||||
scrutiny.yaml
|
scrutiny.yaml
|
||||||
|
|||||||
@@ -52,6 +52,19 @@ Scrutiny is a simple but focused application, with a couple of core features:
|
|||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
|
|
||||||
|
## RAID/Virtual Drives
|
||||||
|
|
||||||
|
Scrutiny uses `smartctl --scan` to detect devices/drives.
|
||||||
|
|
||||||
|
- All RAID controllers supported by `smartctl` are automatically supported by Scrutiny.
|
||||||
|
- While some RAID controllers support passing through the underlying SMART data to `smartctl` others do not.
|
||||||
|
- In some cases `--scan` does not correctly detect the device type, returning [incomplete SMART data](https://github.com/AnalogJ/scrutiny/issues/45).
|
||||||
|
Scrutiny will eventually support overriding detected device type via the config file.
|
||||||
|
- If you use docker, you **must** pass though the RAID virtual disk to the container using `--device` (see below)
|
||||||
|
- This device may be in `/dev/*` or `/dev/bus/*`.
|
||||||
|
- If you're unsure, run `smartctl --scan` on your host, and pass all listed devices to the container.
|
||||||
|
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
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:
|
||||||
@@ -68,8 +81,8 @@ analogj/scrutiny
|
|||||||
|
|
||||||
- `/run/udev` is necessary to provide the Scrutiny collector with access to your device metadata
|
- `/run/udev` is necessary to provide the Scrutiny collector with access to your device metadata
|
||||||
- `--cap-add SYS_RAWIO` is necessary to allow `smartctl` permission to query your device SMART data
|
- `--cap-add SYS_RAWIO` is necessary to allow `smartctl` permission to query your device SMART data
|
||||||
- NOTE: If you have NVMe drives, you must use `--cap-add SYS_ADMIN` instead. See [#26](https://github.com/AnalogJ/scrutiny/issues/26)
|
- NOTE: If you have **NVMe** drives, you must add `--cap-add SYS_ADMIN` as well. See issue [#26](https://github.com/AnalogJ/scrutiny/issues/26#issuecomment-696817130)
|
||||||
- `--device` entries are required to ensure that your hard disk devices are accessible within the container
|
- `--device` entries are required to ensure that your hard disk devices are accessible within the container.
|
||||||
- `analogj/scrutiny` is a omnibus image, containing both the webapp server (frontend & api) as well as the S.M.A.R.T metric collector. (see below)
|
- `analogj/scrutiny` is a omnibus image, containing both the webapp server (frontend & api) as well as the S.M.A.R.T metric collector. (see below)
|
||||||
|
|
||||||
### Hub/Spoke Deployment
|
### Hub/Spoke Deployment
|
||||||
@@ -94,6 +107,13 @@ docker run -it --rm \
|
|||||||
analogj/scrutiny:collector
|
analogj/scrutiny:collector
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Manual Installation (without-Docker)
|
||||||
|
|
||||||
|
While the easiest way to get started with [Scrutiny is using Docker](https://github.com/AnalogJ/scrutiny#docker),
|
||||||
|
it is possible to run it manually without much work. You can even mix and match, using Docker for one component and
|
||||||
|
a manual installation for the other.
|
||||||
|
|
||||||
|
See [docs/INSTALL_MANUAL.md](docs/INSTALL_MANUAL.md) for instructions.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -108,10 +128,80 @@ docker exec scrutiny /scrutiny/bin/scrutiny-collector-metrics run
|
|||||||
```
|
```
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
We support a global YAML configuration file that must be located at /scrutiny/config/scrutiny.yaml
|
We support a global YAML configuration file that must be located at `/scrutiny/config/scrutiny.yaml`
|
||||||
|
|
||||||
Check the [example.scrutiny.yml](example.scrutiny.yaml) file for a fully commented version.
|
Check the [example.scrutiny.yml](example.scrutiny.yaml) file for a fully commented version.
|
||||||
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
Scrutiny supports sending SMART device failure notifications via the following services:
|
||||||
|
- Custom Script (data provided via environmental variables)
|
||||||
|
- Email
|
||||||
|
- Webhooks
|
||||||
|
- Discord
|
||||||
|
- Gotify
|
||||||
|
- Hangouts
|
||||||
|
- IFTTT
|
||||||
|
- Join
|
||||||
|
- Mattermost
|
||||||
|
- Pushbullet
|
||||||
|
- Pushover
|
||||||
|
- Slack
|
||||||
|
- Teams
|
||||||
|
- Telegram
|
||||||
|
- Tulip
|
||||||
|
|
||||||
|
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for more information and documentation for service specific setup.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
# Debug mode & Log Files
|
||||||
|
Scrutiny provides various methods to change the log level to debug and generate log files.
|
||||||
|
|
||||||
|
## Web Server/API
|
||||||
|
|
||||||
|
You can use environmental variables to enable debug logging and/or log files for the web server:
|
||||||
|
|
||||||
|
```
|
||||||
|
DEBUG=true
|
||||||
|
SCRUTINY_LOG_FILE=/tmp/web.log
|
||||||
|
```
|
||||||
|
|
||||||
|
You can configure the log level and log file in the config file:
|
||||||
|
|
||||||
|
```
|
||||||
|
log:
|
||||||
|
file: '/tmp/web.log'
|
||||||
|
level: DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you're not using docker, you can pass CLI arguments to the web server during startup:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrutiny start --debug --log-file /tmp/web.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Collector
|
||||||
|
|
||||||
|
You can use environmental variables to enable debug logging and/or log files for the collector:
|
||||||
|
|
||||||
|
```
|
||||||
|
DEBUG=true
|
||||||
|
COLLECTOR_LOG_FILE=/tmp/collector.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you're not using docker, you can pass CLI arguments to the collector during startup:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
|
||||||
|
```
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Please see the [CONTRIBUTING.md](CONTRIBUTING.md) for instructions for how to develop and contribute to the scrutiny codebase.
|
Please see the [CONTRIBUTING.md](CONTRIBUTING.md) for instructions for how to develop and contribute to the scrutiny codebase.
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"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/errors"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"io"
|
"io"
|
||||||
@@ -20,6 +22,20 @@ var goarch string
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
config, err := config.Create()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("FATAL: %+v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
//we're going to load the config file manually, since we need to validate it.
|
||||||
|
err = config.ReadConfig("/scrutiny/config/collector.yaml") // Find and read the config file
|
||||||
|
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||||
|
//ignore "could not find config file"
|
||||||
|
} else if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
cli.CommandHelpTemplate = `NAME:
|
cli.CommandHelpTemplate = `NAME:
|
||||||
{{.HelpName}} - {{.Usage}}
|
{{.HelpName}} - {{.Usage}}
|
||||||
USAGE:
|
USAGE:
|
||||||
@@ -75,6 +91,17 @@ OPTIONS:
|
|||||||
Name: "run",
|
Name: "run",
|
||||||
Usage: "Run the scrutiny smartctl metrics collector",
|
Usage: "Run the scrutiny smartctl metrics collector",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
|
if c.IsSet("config") {
|
||||||
|
err = config.ReadConfig(c.String("config")) // Find and read the config file
|
||||||
|
if err != nil { // Handle errors reading the config file
|
||||||
|
//ignore "could not find config file"
|
||||||
|
fmt.Printf("Could not find config file at specified path: %s", c.String("config"))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.IsSet("host-id") {
|
||||||
|
config.Set("host.id", c.String("host-id")) // set/override the host-id using CLI.
|
||||||
|
}
|
||||||
|
|
||||||
collectorLogger := logrus.WithFields(logrus.Fields{
|
collectorLogger := logrus.WithFields(logrus.Fields{
|
||||||
"type": "metrics",
|
"type": "metrics",
|
||||||
@@ -97,6 +124,7 @@ OPTIONS:
|
|||||||
}
|
}
|
||||||
|
|
||||||
metricCollector, err := collector.CreateMetricsCollector(
|
metricCollector, err := collector.CreateMetricsCollector(
|
||||||
|
config,
|
||||||
collectorLogger,
|
collectorLogger,
|
||||||
c.String("api-endpoint"),
|
c.String("api-endpoint"),
|
||||||
)
|
)
|
||||||
@@ -109,6 +137,10 @@ OPTIONS:
|
|||||||
},
|
},
|
||||||
|
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "config",
|
||||||
|
Usage: "Specify the path to the devices file",
|
||||||
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "api-endpoint",
|
Name: "api-endpoint",
|
||||||
Usage: "The api server endpoint",
|
Usage: "The api server endpoint",
|
||||||
@@ -128,12 +160,18 @@ OPTIONS:
|
|||||||
Usage: "Enable debug logging",
|
Usage: "Enable debug logging",
|
||||||
EnvVars: []string{"COLLECTOR_DEBUG", "DEBUG"},
|
EnvVars: []string{"COLLECTOR_DEBUG", "DEBUG"},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "host-id",
|
||||||
|
Usage: "Host identifier/label, used for grouping devices",
|
||||||
|
EnvVars: []string{"COLLECTOR_HOST_ID"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.Run(os.Args)
|
err = app.Run(os.Args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(color.HiRedString("ERROR: %v", err))
|
log.Fatal(color.HiRedString("ERROR: %v", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/common"
|
"github.com/analogj/scrutiny/collector/pkg/common"
|
||||||
|
"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"
|
||||||
@@ -16,17 +17,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MetricsCollector struct {
|
type MetricsCollector struct {
|
||||||
|
config config.Interface
|
||||||
BaseCollector
|
BaseCollector
|
||||||
apiEndpoint *url.URL
|
apiEndpoint *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateMetricsCollector(logger *logrus.Entry, apiEndpoint string) (MetricsCollector, error) {
|
func CreateMetricsCollector(appConfig config.Interface, logger *logrus.Entry, apiEndpoint string) (MetricsCollector, error) {
|
||||||
apiEndpointUrl, err := url.Parse(apiEndpoint)
|
apiEndpointUrl, err := url.Parse(apiEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return MetricsCollector{}, err
|
return MetricsCollector{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sc := MetricsCollector{
|
sc := MetricsCollector{
|
||||||
|
config: appConfig,
|
||||||
apiEndpoint: apiEndpointUrl,
|
apiEndpoint: apiEndpointUrl,
|
||||||
BaseCollector: BaseCollector{
|
BaseCollector: BaseCollector{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@@ -49,6 +52,7 @@ func (mc *MetricsCollector) Run() error {
|
|||||||
|
|
||||||
deviceDetector := detect.Detect{
|
deviceDetector := detect.Detect{
|
||||||
Logger: mc.logger,
|
Logger: mc.logger,
|
||||||
|
Config: mc.config,
|
||||||
}
|
}
|
||||||
detectedStorageDevices, err := deviceDetector.Start()
|
detectedStorageDevices, err := deviceDetector.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -105,7 +109,7 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT
|
|||||||
//defer wg.Done()
|
//defer wg.Done()
|
||||||
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
|
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
|
||||||
|
|
||||||
args := []string{"-a", "-j"}
|
args := []string{"-x", "-j"}
|
||||||
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
|
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
|
||||||
if len(deviceType) > 0 && deviceType != "scsi" && deviceType != "ata" {
|
if len(deviceType) > 0 && deviceType != "scsi" && deviceType != "ata" {
|
||||||
args = append(args, "-d", deviceType)
|
args = append(args, "-d", deviceType)
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/analogj/go-util/utils"
|
||||||
|
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||||
|
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// When initializing this class the following methods must be called:
|
||||||
|
// Config.New
|
||||||
|
// Config.Init
|
||||||
|
// This is done automatically when created via the Factory.
|
||||||
|
type configuration struct {
|
||||||
|
*viper.Viper
|
||||||
|
}
|
||||||
|
|
||||||
|
//Viper uses the following precedence order. Each item takes precedence over the item below it:
|
||||||
|
// explicit call to Set
|
||||||
|
// flag
|
||||||
|
// env
|
||||||
|
// config
|
||||||
|
// key/value store
|
||||||
|
// default
|
||||||
|
|
||||||
|
func (c *configuration) Init() error {
|
||||||
|
c.Viper = viper.New()
|
||||||
|
//set defaults
|
||||||
|
c.SetDefault("host.id", "")
|
||||||
|
|
||||||
|
c.SetDefault("devices", []string{})
|
||||||
|
|
||||||
|
//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
|
||||||
|
c.SetConfigType("yaml")
|
||||||
|
//c.SetConfigName("drawbridge")
|
||||||
|
//c.AddConfigPath("$HOME/")
|
||||||
|
|
||||||
|
//CLI options will be added via the `Set()` function
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configuration) ReadConfig(configFilePath string) error {
|
||||||
|
configFilePath, err := utils.ExpandPath(configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.FileExists(configFilePath) {
|
||||||
|
log.Printf("No configuration file found at %v. Using Defaults.", configFilePath)
|
||||||
|
return errors.ConfigFileMissingError("The configuration file could not be found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
//validate config file contents
|
||||||
|
//err = c.ValidateConfigFile(configFilePath)
|
||||||
|
//if err != nil {
|
||||||
|
// log.Printf("Config file at `%v` is invalid: %s", configFilePath, err)
|
||||||
|
// return err
|
||||||
|
//}
|
||||||
|
|
||||||
|
log.Printf("Loading configuration file: %s", configFilePath)
|
||||||
|
|
||||||
|
config_data, err := os.Open(configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading configuration file: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.MergeConfig(config_data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.ValidateConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function ensures that the merged config works correctly.
|
||||||
|
func (c *configuration) ValidateConfig() error {
|
||||||
|
|
||||||
|
//TODO:
|
||||||
|
// check that device prefix matches OS
|
||||||
|
// check that schema of config file is valid
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configuration) GetScanOverrides() []models.ScanOverride {
|
||||||
|
// we have to support 2 types of device types.
|
||||||
|
// - simple device type (device_type: 'sat')
|
||||||
|
// and list of device types (type: \n- 3ware,0 \n- 3ware,1 \n- 3ware,2)
|
||||||
|
// GetString will return "" if this is a list of device types.
|
||||||
|
|
||||||
|
overrides := []models.ScanOverride{}
|
||||||
|
c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true })
|
||||||
|
return overrides
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/analogj/scrutiny/collector/pkg/config"
|
||||||
|
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfiguration_GetScanOverrides_Simple(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
//setup
|
||||||
|
testConfig, _ := config.Create()
|
||||||
|
|
||||||
|
//test
|
||||||
|
err := testConfig.ReadConfig(path.Join("testdata", "simple_device.yaml"))
|
||||||
|
require.NoError(t, err, "should correctly load simple device config")
|
||||||
|
scanOverrides := testConfig.GetScanOverrides()
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
//setup
|
||||||
|
testConfig, _ := config.Create()
|
||||||
|
|
||||||
|
//test
|
||||||
|
err := testConfig.ReadConfig(path.Join("testdata", "ignore_device.yaml"))
|
||||||
|
require.NoError(t, err, "should correctly load ignore device config")
|
||||||
|
scanOverrides := testConfig.GetScanOverrides()
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}}, scanOverrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
//setup
|
||||||
|
testConfig, _ := config.Create()
|
||||||
|
|
||||||
|
//test
|
||||||
|
err := testConfig.ReadConfig(path.Join("testdata", "raid_device.yaml"))
|
||||||
|
require.NoError(t, err, "should correctly load ignore device config")
|
||||||
|
scanOverrides := testConfig.GetScanOverrides()
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.Equal(t, []models.ScanOverride{
|
||||||
|
{
|
||||||
|
Device: "/dev/bus/0",
|
||||||
|
DeviceType: []string{"megaraid,14", "megaraid,15", "megaraid,18", "megaraid,19", "megaraid,20", "megaraid,21"},
|
||||||
|
Ignore: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Device: "/dev/twa0",
|
||||||
|
DeviceType: []string{"3ware,0", "3ware,1", "3ware,2", "3ware,3", "3ware,4", "3ware,5"},
|
||||||
|
Ignore: false,
|
||||||
|
}}, scanOverrides)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
func Create() (Interface, error) {
|
||||||
|
config := new(configuration)
|
||||||
|
if err := config.Init(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create mock using:
|
||||||
|
// mockgen -source=collector/pkg/config/interface.go -destination=collector/pkg/config/mock/mock_config.go
|
||||||
|
type Interface interface {
|
||||||
|
Init() error
|
||||||
|
ReadConfig(configFilePath string) error
|
||||||
|
Set(key string, value interface{})
|
||||||
|
SetDefault(key string, value interface{})
|
||||||
|
|
||||||
|
AllSettings() map[string]interface{}
|
||||||
|
IsSet(key string) bool
|
||||||
|
Get(key string) interface{}
|
||||||
|
GetBool(key string) bool
|
||||||
|
GetInt(key string) int
|
||||||
|
GetString(key string) string
|
||||||
|
GetStringSlice(key string) []string
|
||||||
|
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
|
||||||
|
|
||||||
|
GetScanOverrides() []models.ScanOverride
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: collector/pkg/config/interface.go
|
||||||
|
|
||||||
|
// Package mock_config is a generated GoMock package.
|
||||||
|
package mock_config
|
||||||
|
|
||||||
|
import (
|
||||||
|
models "github.com/analogj/scrutiny/collector/pkg/models"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
viper "github.com/spf13/viper"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockInterface is a mock of Interface interface
|
||||||
|
type MockInterface struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockInterfaceMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||||
|
type MockInterfaceMockRecorder struct {
|
||||||
|
mock *MockInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockInterface creates a new mock instance
|
||||||
|
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||||
|
mock := &MockInterface{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init mocks base method
|
||||||
|
func (m *MockInterface) Init() error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Init")
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init indicates an expected call of Init
|
||||||
|
func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadConfig mocks base method
|
||||||
|
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ReadConfig", configFilePath)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadConfig indicates an expected call of ReadConfig
|
||||||
|
func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockInterface)(nil).ReadConfig), configFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set mocks base method
|
||||||
|
func (m *MockInterface) Set(key string, value interface{}) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "Set", key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set indicates an expected call of Set
|
||||||
|
func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockInterface)(nil).Set), key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefault mocks base method
|
||||||
|
func (m *MockInterface) SetDefault(key string, value interface{}) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "SetDefault", key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefault indicates an expected call of SetDefault
|
||||||
|
func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllSettings mocks base method
|
||||||
|
func (m *MockInterface) AllSettings() map[string]interface{} {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "AllSettings")
|
||||||
|
ret0, _ := ret[0].(map[string]interface{})
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllSettings indicates an expected call of AllSettings
|
||||||
|
func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllSettings", reflect.TypeOf((*MockInterface)(nil).AllSettings))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet mocks base method
|
||||||
|
func (m *MockInterface) IsSet(key string) bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "IsSet", key)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet indicates an expected call of IsSet
|
||||||
|
func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mocks base method
|
||||||
|
func (m *MockInterface) Get(key string) interface{} {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Get", key)
|
||||||
|
ret0, _ := ret[0].(interface{})
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get indicates an expected call of Get
|
||||||
|
func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBool mocks base method
|
||||||
|
func (m *MockInterface) GetBool(key string) bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetBool", key)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBool indicates an expected call of GetBool
|
||||||
|
func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockInterface)(nil).GetBool), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt mocks base method
|
||||||
|
func (m *MockInterface) GetInt(key string) int {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetInt", key)
|
||||||
|
ret0, _ := ret[0].(int)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt indicates an expected call of GetInt
|
||||||
|
func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString mocks base method
|
||||||
|
func (m *MockInterface) GetString(key string) string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetString", key)
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString indicates an expected call of GetString
|
||||||
|
func (mr *MockInterfaceMockRecorder) GetString(key interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetString", reflect.TypeOf((*MockInterface)(nil).GetString), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStringSlice mocks base method
|
||||||
|
func (m *MockInterface) GetStringSlice(key string) []string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetStringSlice", key)
|
||||||
|
ret0, _ := ret[0].([]string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStringSlice indicates an expected call of GetStringSlice
|
||||||
|
func (mr *MockInterfaceMockRecorder) GetStringSlice(key interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStringSlice", reflect.TypeOf((*MockInterface)(nil).GetStringSlice), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalKey mocks base method
|
||||||
|
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
varargs := []interface{}{key, rawVal}
|
||||||
|
for _, a := range decoderOpts {
|
||||||
|
varargs = append(varargs, a)
|
||||||
|
}
|
||||||
|
ret := m.ctrl.Call(m, "UnmarshalKey", varargs...)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalKey indicates an expected call of UnmarshalKey
|
||||||
|
func (mr *MockInterfaceMockRecorder) UnmarshalKey(key, rawVal interface{}, decoderOpts ...interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
varargs := append([]interface{}{key, rawVal}, decoderOpts...)
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalKey", reflect.TypeOf((*MockInterface)(nil).UnmarshalKey), varargs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScanOverrides mocks base method
|
||||||
|
func (m *MockInterface) GetScanOverrides() []models.ScanOverride {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetScanOverrides")
|
||||||
|
ret0, _ := ret[0].([]models.ScanOverride)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScanOverrides indicates an expected call of GetScanOverrides
|
||||||
|
func (mr *MockInterfaceMockRecorder) GetScanOverrides() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScanOverrides", reflect.TypeOf((*MockInterface)(nil).GetScanOverrides))
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
version: 1
|
||||||
|
devices:
|
||||||
|
- device: /dev/sda
|
||||||
|
ignore: true
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
version: 1
|
||||||
|
devices:
|
||||||
|
- device: /dev/bus/0
|
||||||
|
type:
|
||||||
|
- megaraid,14
|
||||||
|
- megaraid,15
|
||||||
|
- megaraid,18
|
||||||
|
- megaraid,19
|
||||||
|
- megaraid,20
|
||||||
|
- megaraid,21
|
||||||
|
|
||||||
|
- device: /dev/twa0
|
||||||
|
type:
|
||||||
|
- 3ware,0
|
||||||
|
- 3ware,1
|
||||||
|
- 3ware,2
|
||||||
|
- 3ware,3
|
||||||
|
- 3ware,4
|
||||||
|
- 3ware,5
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
version: 1
|
||||||
|
devices:
|
||||||
|
- device: /dev/sda
|
||||||
|
type: 'sat'
|
||||||
|
#
|
||||||
|
# # example to show how to ignore a specific disk/device.
|
||||||
|
# - device: /dev/sda
|
||||||
|
# ignore: true
|
||||||
|
#
|
||||||
|
# # examples showing how to force smartctl to detect disks inside a raid array/virtual disk
|
||||||
|
# - device: /dev/bus/0
|
||||||
|
# type:
|
||||||
|
# - megaraid,14
|
||||||
|
# - megaraid,15
|
||||||
|
# - megaraid,18
|
||||||
|
# - megaraid,19
|
||||||
|
# - megaraid,20
|
||||||
|
# - megaraid,21
|
||||||
|
#
|
||||||
|
# - device: /dev/twa0
|
||||||
|
# type:
|
||||||
|
# - 3ware,0
|
||||||
|
# - 3ware,1
|
||||||
|
# - 3ware,2
|
||||||
|
# - 3ware,3
|
||||||
|
# - 3ware,4
|
||||||
|
# - 3ware,5
|
||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/analogj/scrutiny/collector/pkg/common"
|
"github.com/analogj/scrutiny/collector/pkg/common"
|
||||||
|
"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/denisbrodbeck/machineid"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
type Detect struct {
|
type Detect struct {
|
||||||
Logger *logrus.Entry
|
Logger *logrus.Entry
|
||||||
|
Config config.Interface
|
||||||
}
|
}
|
||||||
|
|
||||||
//private/common functions
|
//private/common functions
|
||||||
@@ -24,7 +25,7 @@ type Detect struct {
|
|||||||
//
|
//
|
||||||
// To handle these issues, we have OS specific wrapper functions that update/modify these detected devices.
|
// To handle these issues, we have OS specific wrapper functions that update/modify these detected devices.
|
||||||
// models.Device returned from this function only contain the minimum data for smartctl to execute: device type and device name (device file).
|
// models.Device returned from this function only contain the minimum data for smartctl to execute: device type and device name (device file).
|
||||||
func (d *Detect) smartctlScan() ([]models.Device, error) {
|
func (d *Detect) SmartctlScan() ([]models.Device, error) {
|
||||||
//we use smartctl to detect all the drives available.
|
//we use smartctl to detect all the drives available.
|
||||||
detectedDeviceConnJson, err := common.ExecCmd(d.Logger, "smartctl", []string{"--scan", "-j"}, "", os.Environ())
|
detectedDeviceConnJson, err := common.ExecCmd(d.Logger, "smartctl", []string{"--scan", "-j"}, "", os.Environ())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -39,14 +40,7 @@ func (d *Detect) smartctlScan() ([]models.Device, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
detectedDevices := []models.Device{}
|
detectedDevices := d.TransformDetectedDevices(detectedDeviceConns)
|
||||||
|
|
||||||
for _, detectedDevice := range detectedDeviceConns.Devices {
|
|
||||||
detectedDevices = append(detectedDevices, models.Device{
|
|
||||||
DeviceType: detectedDevice.Type,
|
|
||||||
DeviceName: strings.TrimPrefix(detectedDevice.Name, DevicePrefix()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return detectedDevices, nil
|
return detectedDevices, nil
|
||||||
}
|
}
|
||||||
@@ -55,7 +49,7 @@ func (d *Detect) smartctlScan() ([]models.Device, error) {
|
|||||||
// 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.
|
||||||
func (d *Detect) smartCtlInfo(device *models.Device) error {
|
func (d *Detect) SmartCtlInfo(device *models.Device) error {
|
||||||
|
|
||||||
args := []string{"--info", "-j"}
|
args := []string{"--info", "-j"}
|
||||||
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
|
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
|
||||||
@@ -77,8 +71,8 @@ func (d *Detect) smartCtlInfo(device *models.Device) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//DeviceType and DeviceName are already populated.
|
//WWN: this is a serial number/world-wide number that will not change.
|
||||||
//WWN
|
//DeviceType and DeviceName are already populated, however may change between collector runs (eg. config/host restart)
|
||||||
//InterfaceType:
|
//InterfaceType:
|
||||||
device.ModelName = availableDeviceInfo.ModelName
|
device.ModelName = availableDeviceInfo.ModelName
|
||||||
device.InterfaceSpeed = availableDeviceInfo.InterfaceSpeed.Current.String
|
device.InterfaceSpeed = availableDeviceInfo.InterfaceSpeed.Current.String
|
||||||
@@ -110,7 +104,60 @@ func (d *Detect) smartCtlInfo(device *models.Device) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//uses https://github.com/denisbrodbeck/machineid to get a OS specific unique machine ID.
|
// function will remove devices that are marked for "ignore" in config file
|
||||||
func (d *Detect) getMachineId() (string, error) {
|
// will also add devices that are specified in config file, but "missing" from smartctl --scan
|
||||||
return machineid.ProtectedID("scrutiny")
|
// this function will also update the deviceType to the option specified in config.
|
||||||
|
func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []models.Device {
|
||||||
|
groupedDevices := map[string][]models.Device{}
|
||||||
|
|
||||||
|
for _, scannedDevice := range detectedDeviceConns.Devices {
|
||||||
|
|
||||||
|
deviceFile := strings.ToLower(scannedDevice.Name)
|
||||||
|
|
||||||
|
detectedDevice := models.Device{
|
||||||
|
HostId: d.Config.GetString("host.id"),
|
||||||
|
DeviceType: scannedDevice.Type,
|
||||||
|
DeviceName: strings.TrimPrefix(deviceFile, DevicePrefix()),
|
||||||
|
}
|
||||||
|
|
||||||
|
//find (or create) a slice to contain the devices in this group
|
||||||
|
if groupedDevices[deviceFile] == nil {
|
||||||
|
groupedDevices[deviceFile] = []models.Device{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add this scanned device to the group
|
||||||
|
groupedDevices[deviceFile] = append(groupedDevices[deviceFile], detectedDevice)
|
||||||
|
}
|
||||||
|
|
||||||
|
//now tha we've "grouped" all the devices, lets override any groups specified in the config file.
|
||||||
|
|
||||||
|
for _, overrideDevice := range d.Config.GetScanOverrides() {
|
||||||
|
overrideDeviceFile := strings.ToLower(overrideDevice.Device)
|
||||||
|
|
||||||
|
if overrideDevice.Ignore {
|
||||||
|
// this device file should be deleted if it exists
|
||||||
|
delete(groupedDevices, overrideDeviceFile)
|
||||||
|
} else {
|
||||||
|
//create a new device group, and replace the one generated by smartctl --scan
|
||||||
|
overrideDeviceGroup := []models.Device{}
|
||||||
|
|
||||||
|
for _, overrideDeviceType := range overrideDevice.DeviceType {
|
||||||
|
overrideDeviceGroup = append(overrideDeviceGroup, models.Device{
|
||||||
|
HostId: d.Config.GetString("host.id"),
|
||||||
|
DeviceType: overrideDeviceType,
|
||||||
|
DeviceName: strings.TrimPrefix(overrideDeviceFile, DevicePrefix()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedDevices[overrideDeviceFile] = overrideDeviceGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//flatten map
|
||||||
|
detectedDevices := []models.Device{}
|
||||||
|
for _, group := range groupedDevices {
|
||||||
|
detectedDevices = append(detectedDevices, group...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return detectedDevices
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package detect_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
mock_config "github.com/analogj/scrutiny/collector/pkg/config/mock"
|
||||||
|
"github.com/analogj/scrutiny/collector/pkg/detect"
|
||||||
|
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
|
||||||
|
//setup
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||||
|
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
|
||||||
|
detectedDevices := models.Scan{
|
||||||
|
Devices: []models.ScanDevice{
|
||||||
|
{
|
||||||
|
Name: "/dev/sda",
|
||||||
|
InfoName: "/dev/sda",
|
||||||
|
Protocol: "scsi",
|
||||||
|
Type: "scsi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
d := detect.Detect{
|
||||||
|
Config: fakeConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
//test
|
||||||
|
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.Equal(t, "sda", transformedDevices[0].DeviceName)
|
||||||
|
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
|
||||||
|
//setup
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||||
|
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
|
||||||
|
detectedDevices := models.Scan{
|
||||||
|
Devices: []models.ScanDevice{
|
||||||
|
{
|
||||||
|
Name: "/dev/sda",
|
||||||
|
InfoName: "/dev/sda",
|
||||||
|
Protocol: "scsi",
|
||||||
|
Type: "scsi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
d := detect.Detect{
|
||||||
|
Config: fakeConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
//test
|
||||||
|
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.Equal(t, []models.Device{}, transformedDevices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
|
||||||
|
//setup
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||||
|
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{
|
||||||
|
{
|
||||||
|
Device: "/dev/bus/0",
|
||||||
|
DeviceType: []string{"megaraid,14", "megaraid,15", "megaraid,18", "megaraid,19", "megaraid,20", "megaraid,21"},
|
||||||
|
Ignore: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Device: "/dev/twa0",
|
||||||
|
DeviceType: []string{"3ware,0", "3ware,1", "3ware,2", "3ware,3", "3ware,4", "3ware,5"},
|
||||||
|
Ignore: false,
|
||||||
|
}})
|
||||||
|
detectedDevices := models.Scan{
|
||||||
|
Devices: []models.ScanDevice{
|
||||||
|
{
|
||||||
|
Name: "/dev/bus/0",
|
||||||
|
InfoName: "/dev/bus/0",
|
||||||
|
Protocol: "scsi",
|
||||||
|
Type: "scsi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
d := detect.Detect{
|
||||||
|
Config: fakeConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
//test
|
||||||
|
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.Equal(t, 12, len(transformedDevices))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
|
||||||
|
//setup
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
|
||||||
|
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
|
||||||
|
detectedDevices := models.Scan{
|
||||||
|
Devices: []models.ScanDevice{
|
||||||
|
{
|
||||||
|
Name: "/dev/sda",
|
||||||
|
InfoName: "/dev/sda",
|
||||||
|
Protocol: "ata",
|
||||||
|
Type: "ata",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
d := detect.Detect{
|
||||||
|
Config: fakeConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
//test
|
||||||
|
transformedDevices := d.TransformDetectedDevices(detectedDevices)
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.Equal(t, 1, len(transformedDevices))
|
||||||
|
require.Equal(t, "sat+megaraid", transformedDevices[0].DeviceType)
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ func DevicePrefix() string {
|
|||||||
|
|
||||||
func (d *Detect) Start() ([]models.Device, error) {
|
func (d *Detect) Start() ([]models.Device, error) {
|
||||||
// call the base/common functionality to get a list of devicess
|
// call the base/common functionality to get a list of devicess
|
||||||
detectedDevices, err := d.smartctlScan()
|
detectedDevices, err := d.SmartctlScan()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) {
|
|||||||
|
|
||||||
//inflate device info for detected devices.
|
//inflate device info for detected devices.
|
||||||
for ndx, _ := range detectedDevices {
|
for ndx, _ := range detectedDevices {
|
||||||
d.smartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||||
}
|
}
|
||||||
|
|
||||||
return detectedDevices, nil
|
return detectedDevices, nil
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package detect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||||
|
"github.com/jaypipes/ghw"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DevicePrefix() string {
|
||||||
|
return "/dev/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Detect) Start() ([]models.Device, error) {
|
||||||
|
// call the base/common functionality to get a list of devices
|
||||||
|
detectedDevices, err := d.SmartctlScan()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//inflate device info for detected devices.
|
||||||
|
for ndx, _ := range detectedDevices {
|
||||||
|
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||||
|
}
|
||||||
|
|
||||||
|
return detectedDevices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//WWN values NVMe and SCSI
|
||||||
|
func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||||
|
block, err := ghw.Block()
|
||||||
|
if err == nil {
|
||||||
|
for _, disk := range block.Disks {
|
||||||
|
if disk.Name == detectedDevice.DeviceName {
|
||||||
|
d.Logger.Debugf("Found matching block device. WWN: %s", disk.WWN)
|
||||||
|
detectedDevice.WWN = disk.WWN
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//no WWN found, or could not open Block devices. Either way, fallback to serial number
|
||||||
|
if len(detectedDevice.WWN) == 0 {
|
||||||
|
d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber)
|
||||||
|
detectedDevice.WWN = detectedDevice.SerialNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
//wwn must always be lowercase.
|
||||||
|
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
|
||||||
|
}
|
||||||
@@ -12,14 +12,14 @@ func DevicePrefix() string {
|
|||||||
|
|
||||||
func (d *Detect) Start() ([]models.Device, error) {
|
func (d *Detect) Start() ([]models.Device, error) {
|
||||||
// call the base/common functionality to get a list of devices
|
// call the base/common functionality to get a list of devices
|
||||||
detectedDevices, err := d.smartctlScan()
|
detectedDevices, err := d.SmartctlScan()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//inflate device info for detected devices.
|
//inflate device info for detected devices.
|
||||||
for ndx, _ := range detectedDevices {
|
for ndx, _ := range detectedDevices {
|
||||||
d.smartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||||
}
|
}
|
||||||
|
|
||||||
return detectedDevices, nil
|
return detectedDevices, nil
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package detect
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||||
"github.com/jaypipes/ghw"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,14 +11,14 @@ func DevicePrefix() string {
|
|||||||
|
|
||||||
func (d *Detect) Start() ([]models.Device, error) {
|
func (d *Detect) Start() ([]models.Device, error) {
|
||||||
// call the base/common functionality to get a list of devices
|
// call the base/common functionality to get a list of devices
|
||||||
detectedDevices, err := d.smartctlScan()
|
detectedDevices, err := d.SmartctlScan()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//inflate device info for detected devices.
|
//inflate device info for detected devices.
|
||||||
for ndx, _ := range detectedDevices {
|
for ndx, _ := range detectedDevices {
|
||||||
d.smartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||||
}
|
}
|
||||||
|
|
||||||
return detectedDevices, nil
|
return detectedDevices, nil
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
WWN string `json:"wwn"`
|
WWN string `json:"wwn"`
|
||||||
|
HostId string `json:"host_id"`
|
||||||
|
|
||||||
DeviceName string `json:"device_name"`
|
DeviceName string `json:"device_name"`
|
||||||
Manufacturer string `json:"manufacturer"`
|
Manufacturer string `json:"manufacturer"`
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ type Scan struct {
|
|||||||
Argv []string `json:"argv"`
|
Argv []string `json:"argv"`
|
||||||
ExitStatus int `json:"exit_status"`
|
ExitStatus int `json:"exit_status"`
|
||||||
} `json:"smartctl"`
|
} `json:"smartctl"`
|
||||||
Devices []struct {
|
Devices []ScanDevice `json:"devices"`
|
||||||
Name string `json:"name"`
|
}
|
||||||
InfoName string `json:"info_name"`
|
type ScanDevice struct {
|
||||||
Type string `json:"type"`
|
Name string `json:"name"`
|
||||||
Protocol string `json:"protocol"`
|
InfoName string `json:"info_name"`
|
||||||
} `json:"devices"`
|
Type string `json:"type"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type ScanOverride struct {
|
||||||
|
Device string `mapstructure:"device"`
|
||||||
|
DeviceType []string `mapstructure:"type"`
|
||||||
|
Ignore bool `mapstructure:"ignore"`
|
||||||
|
}
|
||||||
+1
-2
@@ -34,13 +34,12 @@ ENV PATH="/scrutiny/bin:${PATH}"
|
|||||||
ADD https://github.com/dshearer/jobber/releases/download/v1.4.4/jobber_1.4.4-1_amd64.deb /tmp/
|
ADD https://github.com/dshearer/jobber/releases/download/v1.4.4/jobber_1.4.4-1_amd64.deb /tmp/
|
||||||
RUN apt install /tmp/jobber_1.4.4-1_amd64.deb
|
RUN apt install /tmp/jobber_1.4.4-1_amd64.deb
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1
|
RUN apt-get update && apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1 ca-certificates && update-ca-certificates
|
||||||
|
|
||||||
ADD https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-amd64.tar.gz /tmp/
|
ADD https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-amd64.tar.gz /tmp/
|
||||||
RUN tar xzf /tmp/s6-overlay-amd64.tar.gz -C /
|
RUN tar xzf /tmp/s6-overlay-amd64.tar.gz -C /
|
||||||
COPY /rootfs /
|
COPY /rootfs /
|
||||||
|
|
||||||
|
|
||||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /scrutiny/bin/
|
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /scrutiny/bin/
|
||||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /scrutiny/bin/
|
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /scrutiny/bin/
|
||||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /scrutiny/bin/
|
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /scrutiny/bin/
|
||||||
|
|||||||
@@ -18,11 +18,10 @@ ENV PATH="/scrutiny/bin:${PATH}"
|
|||||||
ADD https://github.com/dshearer/jobber/releases/download/v1.4.4/jobber_1.4.4-1_amd64.deb /tmp/
|
ADD https://github.com/dshearer/jobber/releases/download/v1.4.4/jobber_1.4.4-1_amd64.deb /tmp/
|
||||||
RUN apt install /tmp/jobber_1.4.4-1_amd64.deb
|
RUN apt install /tmp/jobber_1.4.4-1_amd64.deb
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1
|
RUN apt-get update && apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1 ca-certificates && update-ca-certificates
|
||||||
|
|
||||||
COPY /rootfs/scrutiny /scrutiny
|
COPY /rootfs/scrutiny /scrutiny
|
||||||
|
|
||||||
|
|
||||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /scrutiny/bin/
|
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /scrutiny/bin/
|
||||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /scrutiny/bin/
|
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /scrutiny/bin/
|
||||||
RUN chmod +x /scrutiny/bin/scrutiny-collector-selftest && \
|
RUN chmod +x /scrutiny/bin/scrutiny-collector-selftest && \
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
version: '3.5'
|
||||||
|
|
||||||
|
services:
|
||||||
|
scrutiny:
|
||||||
|
container_name: scrutiny
|
||||||
|
image: analogj/scrutiny
|
||||||
|
cap_add:
|
||||||
|
- SYS_RAWIO
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- /run/udev:/run/udev:ro
|
||||||
|
- ./config:/scrutiny/config
|
||||||
|
devices:
|
||||||
|
- "/dev/sda"
|
||||||
|
- "/dev/sdb"
|
||||||
@@ -1 +1,150 @@
|
|||||||
|
# Manual Install
|
||||||
|
|
||||||
|
While the easiest way to get started with [Scrutiny is using Docker](https://github.com/AnalogJ/scrutiny#docker),
|
||||||
|
it is possible to run it manually without much work. You can even mix and match, using Docker for one component and
|
||||||
|
a manual installation for the other.
|
||||||
|
|
||||||
|
Scrutiny is made up of two components: a collector and a webapp/api. Here's how each component can be deployed manually.
|
||||||
|
|
||||||
|
> Note: the `/opt/scrutiny` directory is not hardcoded, you can use any directory name/path.
|
||||||
|
|
||||||
|
## Webapp/API
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
Since the webapp is packaged as a stand alone binary, there isn't really any software you need to install other than `glibc`
|
||||||
|
which is included by most linux OS's already.
|
||||||
|
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
Now let's create a directory structure to contain the Scrutiny files & binary.
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p /opt/scrutiny/config
|
||||||
|
mkdir -p /opt/scrutiny/web
|
||||||
|
mkdir -p /opt/scrutiny/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config file
|
||||||
|
|
||||||
|
While it is possible to run the webapp/api without a config file, the defaults are designed for use in a container environment,
|
||||||
|
and so will need to be overridden. So the first thing you'll need to do is create a config file that looks like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
# stored in /opt/scrutiny/config/scrutiny.yaml
|
||||||
|
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
web:
|
||||||
|
database:
|
||||||
|
# The Scrutiny webapp will create a database for you, however the parent directory must exist.
|
||||||
|
location: /opt/scrutiny/config/scrutiny.db
|
||||||
|
src:
|
||||||
|
frontend:
|
||||||
|
# The path to the Scrutiny frontend files (js, css, images) must be specified.
|
||||||
|
# We'll populate it with files in the next section
|
||||||
|
path: /opt/scrutiny/web
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: for a full list of available configuration options, please check the [example.scrutiny.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml) file.
|
||||||
|
|
||||||
|
### Download Files
|
||||||
|
|
||||||
|
Next, we'll download the Scrutiny API binary and frontend files from the [latest Github release](https://github.com/analogj/scrutiny/releases).
|
||||||
|
The files you need to download are named:
|
||||||
|
|
||||||
|
- **scrutiny-web-linux-amd64** - save this file to `/opt/scrutiny/bin`
|
||||||
|
- **scrutiny-web-frontend.tar.gz** - save this file to `/opt/scrutiny/web`
|
||||||
|
|
||||||
|
### Prepare Scrutiny
|
||||||
|
|
||||||
|
Now that we have downloaded the required files, let's prepare the filesystem.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Let's make sure the Scrutiny webapp is executable.
|
||||||
|
chmod +x /opt/scrutiny/bin/scrutiny-web-linux-amd64
|
||||||
|
|
||||||
|
# Next, lets extract the frontend files.
|
||||||
|
cd /opt/scrutiny/web
|
||||||
|
tar xvzf scrutiny-web-frontend.tar.gz --strip-components 1 -C .
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -rf scrutiny-web-frontend.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start Scrutiny Webapp
|
||||||
|
|
||||||
|
Finally, we start the Scrutiny webapp:
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/scrutiny/bin/scrutiny-web-linux-amd64 start --config /opt/scrutiny/config/scrutiny.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
The webapp listens for traffic on `http://0.0.0.0:8080` by default.
|
||||||
|
|
||||||
|
|
||||||
|
## Collector
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
Unlike the webapp, the collector does have some dependencies:
|
||||||
|
|
||||||
|
- `smartctl`, v7+
|
||||||
|
- `cron` (or an alternative process scheduler)
|
||||||
|
|
||||||
|
Unfortunately the version of `smartmontools` (which contains `smartctl`) available in some of the base OS repositories is ancient.
|
||||||
|
So you'll need to install the v7+ version using one of the following commands:
|
||||||
|
|
||||||
|
- **Ubuntu:** `apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1`
|
||||||
|
- **Centos8:**
|
||||||
|
- `dnf install https://extras.getpagespeed.com/release-el8-latest.rpm`
|
||||||
|
- `dnf install smartmontools`
|
||||||
|
- **FreeBSD:** `pkg install smartmontools`
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
Now let's create a directory structure to contain the Scrutiny collector binary.
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p /opt/scrutiny/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Download Files
|
||||||
|
|
||||||
|
Next, we'll download the Scrutiny collector binary from the [latest Github release](https://github.com/analogj/scrutiny/releases).
|
||||||
|
The file you need to download is named:
|
||||||
|
|
||||||
|
- **scrutiny-collector-metrics-linux-amd64** - save this file to `/opt/scrutiny/bin`
|
||||||
|
|
||||||
|
|
||||||
|
### Prepare Scrutiny
|
||||||
|
|
||||||
|
Now that we have downloaded the required files, let's prepare the filesystem.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Let's make sure the Scrutiny collector is executable.
|
||||||
|
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start Scrutiny Collector, Populate Webapp
|
||||||
|
|
||||||
|
Next, we will manually trigger the collector, to populate the Scrutiny dashboard:
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 run --api-endpoint "http://localhost:8080"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schedule Collector with Cron
|
||||||
|
|
||||||
|
Finally you need to schedule the collector to run periodically.
|
||||||
|
This may be different depending on your OS/environment, but it may look something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
# open crontab
|
||||||
|
crontab -e
|
||||||
|
|
||||||
|
# add a line for Scrutiny
|
||||||
|
*/15 * * * * . /etc/profile; /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 run --api-endpoint "http://localhost:8080"
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Officially Supported NAS OS's
|
||||||
|
|
||||||
|
These are the officially supported NAS OS's (with documentation and setup guides).
|
||||||
|
Once a guide is created (in `docs/guides/`) it will be linked here.
|
||||||
|
|
||||||
|
- [ ] freenas/truenas
|
||||||
|
- [ ] unraid
|
||||||
|
- [ ] ESXI
|
||||||
|
- [ ] Proxmox
|
||||||
|
- [ ] Synology
|
||||||
|
- [ ] OMV
|
||||||
|
- [ ] Amahi
|
||||||
|
- [ ] Running in a LXC container
|
||||||
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Commented Scrutiny Configuration File
|
||||||
|
#
|
||||||
|
# The default location for this file is /scrutiny/config/collector.yaml.
|
||||||
|
# In some cases to improve clarity default values are specified,
|
||||||
|
# uncommented. Other example values are commented out.
|
||||||
|
#
|
||||||
|
# When this file is parsed by Scrutiny, all configuration file keys are
|
||||||
|
# lowercased automatically. As such, Configuration keys are case-insensitive,
|
||||||
|
# and should be lowercase in this file to be consistent with usage.
|
||||||
|
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
# Version
|
||||||
|
#
|
||||||
|
# version specifies the version of this configuration file schema, not
|
||||||
|
# the scrutiny binary. There is only 1 version available at the moment
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
# The host id is a label used for identifying groups of disks running on the same host
|
||||||
|
# Primiarly used for hub/spoke deployments (can be left empty if using all-in-one image).
|
||||||
|
host:
|
||||||
|
id: ""
|
||||||
|
|
||||||
|
|
||||||
|
# This block allows you to override/customize the settings for devices detected by
|
||||||
|
# Scrutiny via `smartctl --scan`
|
||||||
|
# See the "--device=TYPE" section of https://linux.die.net/man/8/smartctl
|
||||||
|
# type can be a 'string' or a 'list'
|
||||||
|
devices:
|
||||||
|
# # example for forcing device type detection for a single disk
|
||||||
|
# - device: /dev/sda
|
||||||
|
# type: 'sat'
|
||||||
|
#
|
||||||
|
# # example to show how to ignore a specific disk/device.
|
||||||
|
# - device: /dev/sda
|
||||||
|
# ignore: true
|
||||||
|
#
|
||||||
|
# # examples showing how to force smartctl to detect disks inside a raid array/virtual disk
|
||||||
|
# - device: /dev/bus/0
|
||||||
|
# type:
|
||||||
|
# - megaraid,14
|
||||||
|
# - megaraid,15
|
||||||
|
# - megaraid,18
|
||||||
|
# - megaraid,19
|
||||||
|
# - megaraid,20
|
||||||
|
# - megaraid,21
|
||||||
|
#
|
||||||
|
# - device: /dev/twa0
|
||||||
|
# type:
|
||||||
|
# - 3ware,0
|
||||||
|
# - 3ware,1
|
||||||
|
# - 3ware,2
|
||||||
|
# - 3ware,3
|
||||||
|
# - 3ware,4
|
||||||
|
# - 3ware,5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
# FEATURES COMING SOON
|
||||||
|
#
|
||||||
|
# The following commented out sections are a preview of additional configuration options that will be available soon.
|
||||||
|
#
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
+17
-8
@@ -1,6 +1,6 @@
|
|||||||
# Commented Scrutiny Configuration File
|
# Commented Scrutiny Configuration File
|
||||||
#
|
#
|
||||||
# The default location for this file is ~/scrutiny.yaml.
|
# The default location for this file is /scrutiny/config/scrutiny.yaml.
|
||||||
# In some cases to improve clarity default values are specified,
|
# In some cases to improve clarity default values are specified,
|
||||||
# uncommented. Other example values are commented out.
|
# uncommented. Other example values are commented out.
|
||||||
#
|
#
|
||||||
@@ -32,19 +32,15 @@ log:
|
|||||||
file: '' #absolute or relative paths allowed, eg. web.log
|
file: '' #absolute or relative paths allowed, eg. web.log
|
||||||
level: INFO
|
level: INFO
|
||||||
|
|
||||||
# The following commented out sections are a preview of additional configuration options that will be available soon.
|
|
||||||
|
|
||||||
#disks:
|
# Notification "urls" look like the following. For more information about service specific configuration see
|
||||||
# include:
|
# Shoutrrr's documentation: https://containrrr.dev/shoutrrr/services/overview/
|
||||||
# # - /dev/sda
|
|
||||||
# exclude:
|
|
||||||
# # - /dev/sdb
|
|
||||||
|
|
||||||
#notify:
|
#notify:
|
||||||
# urls:
|
# urls:
|
||||||
# - "discord://token@channel"
|
# - "discord://token@channel"
|
||||||
# - "telegram://token@telegram?channels=channel-1[,channel-2,...]"
|
# - "telegram://token@telegram?channels=channel-1[,channel-2,...]"
|
||||||
# - "pushover://shoutrrr:apiToken@userKey/?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"
|
||||||
# - "smtp://username:password@host:port/?fromAddress=fromAddress&toAddresses=recipient1[,recipient2,...]"
|
# - "smtp://username:password@host:port/?fromAddress=fromAddress&toAddresses=recipient1[,recipient2,...]"
|
||||||
# - "teams://token-a/token-b/token-c"
|
# - "teams://token-a/token-b/token-c"
|
||||||
@@ -58,6 +54,19 @@ log:
|
|||||||
# - "script:///file/path/on/disk"
|
# - "script:///file/path/on/disk"
|
||||||
# - "https://www.example.com/path"
|
# - "https://www.example.com/path"
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
# FEATURES COMING SOON
|
||||||
|
#
|
||||||
|
# The following commented out sections are a preview of additional configuration options that will be available soon.
|
||||||
|
#
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
|
#disks:
|
||||||
|
# include:
|
||||||
|
# # - /dev/sda
|
||||||
|
# exclude:
|
||||||
|
# # - /dev/sdb
|
||||||
|
|
||||||
#limits:
|
#limits:
|
||||||
# ata:
|
# ata:
|
||||||
# critical:
|
# critical:
|
||||||
|
|||||||
@@ -3,20 +3,23 @@ module github.com/analogj/scrutiny
|
|||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AnalogJ/go-util v0.0.0-20200905200945-3b93d31215ae // indirect
|
|
||||||
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.0.0-20200828202222-1da53231b05a
|
github.com/containrrr/shoutrrr v0.0.0-20200828202222-1da53231b05a
|
||||||
github.com/denisbrodbeck/machineid v1.0.1
|
|
||||||
github.com/fatih/color v1.9.0
|
github.com/fatih/color v1.9.0
|
||||||
github.com/gin-gonic/gin v1.6.3
|
github.com/gin-gonic/gin v1.6.3
|
||||||
github.com/golang/mock v1.4.3
|
github.com/golang/mock v1.4.3
|
||||||
github.com/jaypipes/ghw v0.6.1
|
github.com/jaypipes/ghw v0.6.1
|
||||||
github.com/jinzhu/gorm v1.9.14
|
|
||||||
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // indirect
|
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.4 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.2.2
|
||||||
github.com/sirupsen/logrus v1.2.0
|
github.com/sirupsen/logrus v1.2.0
|
||||||
github.com/spf13/viper v1.7.0
|
github.com/spf13/viper v1.7.0
|
||||||
github.com/stretchr/testify v1.5.1
|
github.com/stretchr/testify v1.5.1
|
||||||
github.com/urfave/cli/v2 v2.2.0
|
github.com/urfave/cli/v2 v2.2.0
|
||||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect
|
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||||
|
gorm.io/driver/sqlite v1.1.3
|
||||||
|
gorm.io/gorm v1.20.2
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,19 +11,16 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
|
|||||||
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/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
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/AnalogJ/go-util v0.0.0-20200905200945-3b93d31215ae h1:iYSadgTTTmFTvZwdDImnytps8Hq9zlpWeNfYpe1RTPs=
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/AnalogJ/go-util v0.0.0-20200905200945-3b93d31215ae/go.mod h1:0jFBtvNk8rNzZjL8j1b852fcka5/VJfJvRiU2w6OIkI=
|
|
||||||
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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
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/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
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/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/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
@@ -50,17 +47,11 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr
|
|||||||
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=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
|
||||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
@@ -77,6 +68,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
|||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
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-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
@@ -84,11 +76,9 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
|
|||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
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/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
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-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/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=
|
||||||
@@ -104,6 +94,7 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
|
|||||||
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=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
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/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=
|
||||||
@@ -112,6 +103,7 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI
|
|||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
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/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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
@@ -142,22 +134,23 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
|
|||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
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/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/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=
|
||||||
github.com/jaypipes/pcidb v0.5.0/go.mod h1:L2RGk04sfRhp5wvHO0gfRAMoLY/F3PKv/nwJeVoho0o=
|
github.com/jaypipes/pcidb v0.5.0/go.mod h1:L2RGk04sfRhp5wvHO0gfRAMoLY/F3PKv/nwJeVoho0o=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jinzhu/gorm v1.9.14 h1:Kg3ShyTPcM6nzVo148fRrcMO6MNKuqtOUwnzqMgVniM=
|
|
||||||
github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
|
||||||
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
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.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 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/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/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/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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
@@ -165,14 +158,15 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
|||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
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/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
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/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 h1:3tLzEnUizyN9YLWFTT9loC30lSBvh2y70LTDcZOTs1s=
|
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 h1:3tLzEnUizyN9YLWFTT9loC30lSBvh2y70LTDcZOTs1s=
|
||||||
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA=
|
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA=
|
||||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
@@ -184,8 +178,10 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
|||||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
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-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA=
|
||||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
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/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
@@ -211,6 +207,7 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
|
|||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
||||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
||||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||||
@@ -242,7 +239,9 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
|
|||||||
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.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
@@ -297,10 +296,8 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
|||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
|
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
|
||||||
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/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=
|
||||||
@@ -321,7 +318,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
|
|||||||
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/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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -338,7 +334,7 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||||
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/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=
|
||||||
@@ -347,6 +343,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
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 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
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/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -422,6 +419,7 @@ google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
|
|||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
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/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 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
@@ -440,6 +438,11 @@ 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 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=
|
||||||
|
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
|
||||||
|
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
|
||||||
|
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
|
gorm.io/gorm v1.20.2 h1:bZzSEnq7NDGsrd+n3evOOedDrY5oLM5QPlCjZJUK2ro=
|
||||||
|
gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
gosrc.io/xmpp v0.1.1 h1:iMtE9W3fx254+4E6rI34AOPJDqWvpfQR6EYaVMzhJ4s=
|
gosrc.io/xmpp v0.1.1 h1:iMtE9W3fx254+4E6rI34AOPJDqWvpfQR6EYaVMzhJ4s=
|
||||||
gosrc.io/xmpp v0.1.1/go.mod h1:4JgaXzw4MnEv2sGltONtK3GMhj+h9gpQ7cO8nwbFJLU=
|
gosrc.io/xmpp v0.1.1/go.mod h1:4JgaXzw4MnEv2sGltONtK3GMhj+h9gpQ7cO8nwbFJLU=
|
||||||
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=
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ OPTIONS:
|
|||||||
},
|
},
|
||||||
Before: func(c *cli.Context) error {
|
Before: func(c *cli.Context) error {
|
||||||
|
|
||||||
drawbridge := "github.com/AnalogJ/scrutiny"
|
scrutiny := "github.com/AnalogJ/scrutiny"
|
||||||
|
|
||||||
var versionInfo string
|
var versionInfo string
|
||||||
if len(goos) > 0 && len(goarch) > 0 {
|
if len(goos) > 0 && len(goarch) > 0 {
|
||||||
@@ -69,7 +69,7 @@ OPTIONS:
|
|||||||
versionInfo = fmt.Sprintf("dev-%s", version.VERSION)
|
versionInfo = fmt.Sprintf("dev-%s", version.VERSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
subtitle := drawbridge + utils.LeftPad2Len(versionInfo, " ", 65-len(drawbridge))
|
subtitle := scrutiny + utils.LeftPad2Len(versionInfo, " ", 65-len(scrutiny))
|
||||||
|
|
||||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ func (c *configuration) Init() error {
|
|||||||
c.SetDefault("log.level", "INFO")
|
c.SetDefault("log.level", "INFO")
|
||||||
c.SetDefault("log.file", "")
|
c.SetDefault("log.file", "")
|
||||||
|
|
||||||
|
c.SetDefault("notify.urls", []string{})
|
||||||
|
|
||||||
//c.SetDefault("disks.include", []string{})
|
//c.SetDefault("disks.include", []string{})
|
||||||
//c.SetDefault("disks.exclude", []string{})
|
//c.SetDefault("disks.exclude", []string{})
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ func (c *configuration) ReadConfig(configFilePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !utils.FileExists(configFilePath) {
|
if !utils.FileExists(configFilePath) {
|
||||||
log.Printf("No configuration file found at %v. Skipping", configFilePath)
|
log.Printf("No configuration file found at %v. Using Defaults.", configFilePath)
|
||||||
return errors.ConfigFileMissingError("The configuration file could not be found.")
|
return errors.ConfigFileMissingError("The configuration file could not be found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Create mock using:
|
// Create mock using:
|
||||||
// mockgen -source=pkg/config/interface.go -destination=pkg/config/mock/mock_config.go
|
// mockgen -source=webapp/backend/pkg/config/interface.go -destination=webapp/backend/pkg/config/mock/mock_config.go
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
Init() error
|
Init() error
|
||||||
ReadConfig(configFilePath string) error
|
ReadConfig(configFilePath string) error
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ type Device struct {
|
|||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt *time.Time
|
DeletedAt *time.Time
|
||||||
|
|
||||||
WWN string `json:"wwn" gorm:"primary_key"`
|
WWN string `json:"wwn" gorm:"primary_key"`
|
||||||
|
HostId string `json:"host_id"`
|
||||||
|
|
||||||
DeviceName string `json:"device_name"`
|
DeviceName string `json:"device_name"`
|
||||||
Manufacturer string `json:"manufacturer"`
|
Manufacturer string `json:"manufacturer"`
|
||||||
@@ -151,6 +152,8 @@ func (dv *Device) ApplyMetadataRules() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function is called every time the collector sends SMART data to the API.
|
||||||
|
// It can be used to update device data that can change over time.
|
||||||
func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInfo) error {
|
func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInfo) error {
|
||||||
dv.Firmware = info.FirmwareVersion
|
dv.Firmware = info.FirmwareVersion
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ package db
|
|||||||
import (
|
import (
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||||
"github.com/jinzhu/gorm"
|
"gorm.io/gorm"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const SmartWhenFailedFailingNow = "FAILING_NOW"
|
const SmartWhenFailedFailingNow = "FAILING_NOW"
|
||||||
const SmartWhenFailedInThePast = "IN_THE_PAST"
|
const SmartWhenFailedInThePast = "IN_THE_PAST"
|
||||||
|
|
||||||
|
const SmartStatusPassed = "passed"
|
||||||
|
const SmartStatusFailed = "failed"
|
||||||
|
|
||||||
type Smart struct {
|
type Smart struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
|
|
||||||
@@ -17,7 +20,7 @@ type Smart struct {
|
|||||||
Device Device `json:"-" gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
|
Device Device `json:"-" gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
|
||||||
|
|
||||||
TestDate time.Time `json:"date"`
|
TestDate time.Time `json:"date"`
|
||||||
SmartStatus string `json:"smart_status"`
|
SmartStatus string `json:"smart_status"` // SmartStatusPassed or SmartStatusFailed
|
||||||
|
|
||||||
//Metrics
|
//Metrics
|
||||||
Temp int64 `json:"temp"`
|
Temp int64 `json:"temp"`
|
||||||
@@ -49,9 +52,9 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
if info.SmartStatus.Passed {
|
if info.SmartStatus.Passed {
|
||||||
sm.SmartStatus = "passed"
|
sm.SmartStatus = SmartStatusPassed
|
||||||
} else {
|
} else {
|
||||||
sm.SmartStatus = "failed"
|
sm.SmartStatus = SmartStatusFailed
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
||||||
"github.com/jinzhu/gorm"
|
"gorm.io/gorm"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
||||||
"github.com/jinzhu/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SmartNvmeAttribute struct {
|
type SmartNvmeAttribute struct {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
||||||
"github.com/jinzhu/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SmartScsiAttribute struct {
|
type SmartScsiAttribute struct {
|
||||||
|
|||||||
@@ -3,30 +3,66 @@ package notify
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/analogj/go-util/utils"
|
"github.com/analogj/go-util/utils"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
"github.com/containrrr/shoutrrr"
|
"github.com/containrrr/shoutrrr"
|
||||||
log "github.com/sirupsen/logrus"
|
shoutrrrTypes "github.com/containrrr/shoutrrr/pkg/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const NotifyFailureTypeEmailTest = "EmailTest"
|
||||||
|
const NotifyFailureTypeSmartPrefail = "SmartPreFailure"
|
||||||
|
const NotifyFailureTypeSmartFailure = "SmartFailure"
|
||||||
|
const NotifyFailureTypeSmartErrorLog = "SmartErrorLog"
|
||||||
|
const NotifyFailureTypeSmartSelfTest = "SmartSelfTestLog"
|
||||||
|
|
||||||
|
// TODO: include host and/or user label for device.
|
||||||
type Payload struct {
|
type Payload struct {
|
||||||
Mailer string `json:"mailer"`
|
Date string `json:"date"` //populated by Send function.
|
||||||
Subject string `json:"subject"`
|
FailureType string `json:"failure_type"` //EmailTest, SmartFail, ScrutinyFail
|
||||||
Date string `json:"date"`
|
DeviceType string `json:"device_type"` //ATA/SCSI/NVMe
|
||||||
FailureType string `json:"failure_type"`
|
DeviceName string `json:"device_name"` //dev/sda
|
||||||
Device string `json:"device"`
|
DeviceSerial string `json:"device_serial"` //WDDJ324KSO
|
||||||
DeviceType string `json:"device_type"`
|
Test bool `json:"test"` // false
|
||||||
DeviceString string `json:"device_string"`
|
|
||||||
Message string `json:"message"`
|
//should not be populated
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) GenerateSubject() string {
|
||||||
|
//generate a detailed failure message
|
||||||
|
return fmt.Sprintf("Scrutiny SMART error (%s) detected on device: %s", p.FailureType, p.DeviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) GenerateMessage() string {
|
||||||
|
//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
|
||||||
|
|
||||||
|
Date: %s`, p.DeviceName, p.FailureType, p.DeviceName, p.DeviceSerial, p.DeviceType, p.Date)
|
||||||
|
|
||||||
|
if p.Test {
|
||||||
|
message = "TEST NOTIFICATION:\n" + message
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notify struct {
|
type Notify struct {
|
||||||
|
Logger logrus.FieldLogger
|
||||||
Config config.Interface
|
Config config.Interface
|
||||||
Payload Payload
|
Payload Payload
|
||||||
}
|
}
|
||||||
@@ -35,9 +71,17 @@ func (n *Notify) Send() error {
|
|||||||
//validate that the Payload is populated
|
//validate that the Payload is populated
|
||||||
sendDate := time.Now()
|
sendDate := time.Now()
|
||||||
n.Payload.Date = sendDate.Format(time.RFC3339)
|
n.Payload.Date = sendDate.Format(time.RFC3339)
|
||||||
|
n.Payload.Subject = n.Payload.GenerateSubject()
|
||||||
|
n.Payload.Message = n.Payload.GenerateMessage()
|
||||||
|
|
||||||
//retrieve list of notification endpoints from config file
|
//retrieve list of notification endpoints from config file
|
||||||
configUrls := n.Config.GetStringSlice("notify.urls")
|
configUrls := n.Config.GetStringSlice("notify.urls")
|
||||||
|
n.Logger.Debugf("Configured notification services: %v", configUrls)
|
||||||
|
|
||||||
|
if len(configUrls) == 0 {
|
||||||
|
n.Logger.Infof("No notification endpoints configured. Skipping failure notification.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
//remove http:// https:// and script:// prefixed urls
|
//remove http:// https:// and script:// prefixed urls
|
||||||
notifyWebhooks := []string{}
|
notifyWebhooks := []string{}
|
||||||
@@ -54,108 +98,166 @@ func (n *Notify) Send() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n.Logger.Debugf("Configured scripts: %v", notifyScripts)
|
||||||
|
n.Logger.Debugf("Configured webhooks: %v", notifyWebhooks)
|
||||||
|
n.Logger.Debugf("Configured shoutrrr: %v", notifyShoutrrr)
|
||||||
|
|
||||||
//run all scripts, webhooks and shoutrr commands in parallel
|
//run all scripts, webhooks and shoutrr commands in parallel
|
||||||
var wg sync.WaitGroup
|
//var wg sync.WaitGroup
|
||||||
|
var eg errgroup.Group
|
||||||
|
|
||||||
for _, notifyWebhook := range notifyWebhooks {
|
for _, notifyWebhook := range notifyWebhooks {
|
||||||
// execute collection in parallel go-routines
|
// execute collection in parallel go-routines
|
||||||
wg.Add(1)
|
eg.Go(func() error { return n.SendWebhookNotification(notifyWebhook) })
|
||||||
go n.SendWebhookNotification(&wg, notifyWebhook)
|
|
||||||
}
|
}
|
||||||
for _, notifyScript := range notifyScripts {
|
for _, notifyScript := range notifyScripts {
|
||||||
// execute collection in parallel go-routines
|
// execute collection in parallel go-routines
|
||||||
wg.Add(1)
|
eg.Go(func() error { return n.SendScriptNotification(notifyScript) })
|
||||||
go n.SendScriptNotification(&wg, notifyScript)
|
|
||||||
}
|
}
|
||||||
if len(notifyScripts) > 0 {
|
for _, shoutrrrUrl := range notifyShoutrrr {
|
||||||
wg.Add(1)
|
eg.Go(func() error { return n.SendShoutrrrNotification(shoutrrrUrl) })
|
||||||
go n.SendShoutrrrNotification(&wg, notifyShoutrrr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//and wait for completion, error or timeout.
|
//and wait for completion, error or timeout.
|
||||||
if waitTimeout(&wg, time.Minute) { //wait for 1 minute
|
n.Logger.Debugf("Main: waiting for notifications to complete.")
|
||||||
fmt.Println("Timed out while sending notifications")
|
|
||||||
|
if err := eg.Wait(); err == nil {
|
||||||
|
n.Logger.Info("Successfully sent notifications. Check logs for more information.")
|
||||||
|
return nil
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Sent notifications. Check logs for more information.")
|
n.Logger.Error("One or more notifications failed to send successfully. See logs for more information.")
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
////wg.Wait()
|
||||||
|
//if waitTimeout(&wg, time.Minute) { //wait for 1 minute
|
||||||
|
// fmt.Println("Timed out while sending notifications")
|
||||||
|
//} else {
|
||||||
|
//}
|
||||||
|
//return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notify) SendWebhookNotification(wg *sync.WaitGroup, webhookUrl string) {
|
func (n *Notify) SendWebhookNotification(webhookUrl string) error {
|
||||||
defer wg.Done()
|
n.Logger.Infof("Sending Webhook to %s", webhookUrl)
|
||||||
log.Infof("Sending Webhook to %s", webhookUrl)
|
|
||||||
requestBody, err := json.Marshal(n.Payload)
|
requestBody, err := json.Marshal(n.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("An error occurred while sending Webhook to %s: %v", webhookUrl, err)
|
n.Logger.Errorf("An error occurred while sending Webhook to %s: %v", webhookUrl, err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Post(webhookUrl, "application/json", bytes.NewBuffer(requestBody))
|
resp, err := http.Post(webhookUrl, "application/json", bytes.NewBuffer(requestBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("An error occurred while sending Webhook to %s: %v", webhookUrl, err)
|
n.Logger.Errorf("An error occurred while sending Webhook to %s: %v", webhookUrl, err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
//we don't care about resp body content, but maybe we should log it?
|
//we don't care about resp body content, but maybe we should log it?
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notify) SendScriptNotification(wg *sync.WaitGroup, scriptUrl string) {
|
func (n *Notify) SendScriptNotification(scriptUrl string) error {
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
//check if the script exists.
|
//check if the script exists.
|
||||||
scriptPath := strings.TrimPrefix(scriptUrl, "script://")
|
scriptPath := strings.TrimPrefix(scriptUrl, "script://")
|
||||||
log.Infof("Executing Script %s", scriptPath)
|
n.Logger.Infof("Executing Script %s", scriptPath)
|
||||||
|
|
||||||
if !utils.FileExists(scriptPath) {
|
if !utils.FileExists(scriptPath) {
|
||||||
log.Errorf("Script does not exist: %s", scriptPath)
|
n.Logger.Errorf("Script does not exist: %s", scriptPath)
|
||||||
return
|
return errors.New(fmt.Sprintf("custom script path does not exist: %s", scriptPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
copyEnv := os.Environ()
|
copyEnv := os.Environ()
|
||||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_MAILER=%s", n.Payload.Mailer))
|
|
||||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_SUBJECT=%s", n.Payload.Subject))
|
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_SUBJECT=%s", n.Payload.Subject))
|
||||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DATE=%s", n.Payload.Date))
|
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DATE=%s", n.Payload.Date))
|
||||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_FAILURE_TYPE=%s", n.Payload.FailureType))
|
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_FAILURE_TYPE=%s", n.Payload.FailureType))
|
||||||
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE=%s", n.Payload.Device))
|
copyEnv = append(copyEnv, fmt.Sprintf("SCRUTINY_DEVICE_NAME=%s", n.Payload.DeviceName))
|
||||||
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_STRING=%s", n.Payload.DeviceString))
|
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))
|
||||||
err := utils.CmdExec(scriptPath, []string{}, "", copyEnv, "")
|
err := utils.CmdExec(scriptPath, []string{}, "", copyEnv, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("An error occurred while executing script %s: %v", scriptPath, err)
|
n.Logger.Errorf("An error occurred while executing script %s: %v", scriptPath, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notify) SendShoutrrrNotification(wg *sync.WaitGroup, shoutrrrUrls []string) {
|
func (n *Notify) SendShoutrrrNotification(shoutrrrUrl string) error {
|
||||||
log.Infof("Sending notifications to %v", shoutrrrUrls)
|
|
||||||
|
|
||||||
defer wg.Done()
|
fmt.Printf("Sending Notifications to %v", shoutrrrUrl)
|
||||||
sender, err := shoutrrr.CreateSender(shoutrrrUrls...)
|
n.Logger.Infof("Sending notifications to %v", shoutrrrUrl)
|
||||||
|
|
||||||
|
sender, err := shoutrrr.CreateSender(shoutrrrUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("An error occurred while sending notifications %v: %v", shoutrrrUrls, err)
|
n.Logger.Errorf("An error occurred while sending notifications %v: %v", shoutrrrUrl, err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := sender.Send(n.Payload.Subject, nil) //structs.Map(n.Payload).())
|
//sender.SetLogger(n.Logger.)
|
||||||
|
serviceName, params, err := n.GenShoutrrrNotificationParams(shoutrrrUrl)
|
||||||
|
n.Logger.Debug("notification data for %s: (%s)\n%v", serviceName, shoutrrrUrl, params)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
n.Logger.Errorf("An error occurred occurred while generating notification payload for %s:\n %v", serviceName, shoutrrrUrl, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := sender.Send(n.Payload.Message, params)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
log.Errorf("One or more errors occurred occurred while sending notifications %v:\n %v", shoutrrrUrls, errs)
|
var errstrings []string
|
||||||
|
|
||||||
|
for _, err := range errs {
|
||||||
|
if err == nil || err.Error() == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
errstrings = append(errstrings, err.Error())
|
||||||
|
}
|
||||||
|
//sometimes there are empty errs, we're going to skip them.
|
||||||
|
if len(errstrings) == 0 {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
n.Logger.Errorf("One or more errors occurred while sending notifications for %s:", shoutrrrUrl)
|
||||||
|
n.Logger.Error(errs)
|
||||||
|
return errors.New(strings.Join(errstrings, "\n"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//utility functions
|
func (n *Notify) GenShoutrrrNotificationParams(shoutrrrUrl string) (string, *shoutrrrTypes.Params, error) {
|
||||||
// waitTimeout waits for the waitgroup for the specified max timeout.
|
serviceURL, err := url.Parse(shoutrrrUrl)
|
||||||
// Returns true if waiting timed out.
|
if err != nil {
|
||||||
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
|
return "", nil, err
|
||||||
c := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(c)
|
|
||||||
wg.Wait()
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-c:
|
|
||||||
return false // completed normally
|
|
||||||
case <-time.After(timeout):
|
|
||||||
return true // timed out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serviceName := serviceURL.Scheme
|
||||||
|
params := &shoutrrrTypes.Params{}
|
||||||
|
|
||||||
|
logoUrl := "https://raw.githubusercontent.com/AnalogJ/scrutiny/master/webapp/frontend/src/ms-icon-144x144.png"
|
||||||
|
subject := n.Payload.Subject
|
||||||
|
switch serviceName {
|
||||||
|
// no params supported for these services
|
||||||
|
case "discord", "hangouts", "ifttt", "mattermost", "teams":
|
||||||
|
break
|
||||||
|
case "gotify":
|
||||||
|
(*params)["title"] = subject
|
||||||
|
case "join":
|
||||||
|
(*params)["title"] = subject
|
||||||
|
(*params)["icon"] = logoUrl
|
||||||
|
case "pushbullet":
|
||||||
|
(*params)["title"] = subject
|
||||||
|
case "pushover":
|
||||||
|
(*params)["subject"] = subject
|
||||||
|
case "slack":
|
||||||
|
(*params)["title"] = subject
|
||||||
|
(*params)["thumb_url"] = logoUrl
|
||||||
|
case "smtp":
|
||||||
|
(*params)["subject"] = subject
|
||||||
|
case "standard":
|
||||||
|
(*params)["subject"] = subject
|
||||||
|
case "telegram":
|
||||||
|
(*params)["subject"] = subject
|
||||||
|
case "zulip":
|
||||||
|
(*params)["topic"] = subject
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceName, params, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.2.1"
|
const VERSION = "0.3.0"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
||||||
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package handler
|
|||||||
import (
|
import (
|
||||||
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ package handler
|
|||||||
import (
|
import (
|
||||||
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// filter devices that are detected by various collectors.
|
// register devices that are detected by various collectors.
|
||||||
|
// This function is run everytime a collector is about to start a run. It can be used to update device data.
|
||||||
func RegisterDevices(c *gin.Context) {
|
func RegisterDevices(c *gin.Context) {
|
||||||
db := c.MustGet("DB").(*gorm.DB)
|
db := c.MustGet("DB").(*gorm.DB)
|
||||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||||
@@ -21,11 +24,15 @@ func RegisterDevices(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: filter devices here (remove excludes, force includes)
|
|
||||||
errs := []error{}
|
errs := []error{}
|
||||||
for _, dev := range collectorDeviceWrapper.Data {
|
for _, dev := range collectorDeviceWrapper.Data {
|
||||||
//insert devices into DB if not already there.
|
//insert devices into DB (and update specified columns if device is already registered)
|
||||||
if err := db.Where(dbModels.Device{WWN: dev.WWN}).FirstOrCreate(&dev).Error; err != nil {
|
// update device fields that may change: (DeviceType, HostID)
|
||||||
|
if err := db.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "wwn"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type"}),
|
||||||
|
}).Create(&dev).Error; err != nil {
|
||||||
|
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/notify"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/notify"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send test notification
|
// Send test notification
|
||||||
@@ -17,15 +15,14 @@ func SendTestNotification(c *gin.Context) {
|
|||||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||||
|
|
||||||
testNotify := notify.Notify{
|
testNotify := notify.Notify{
|
||||||
|
Logger: logger,
|
||||||
Config: appConfig,
|
Config: appConfig,
|
||||||
Payload: notify.Payload{
|
Payload: notify.Payload{
|
||||||
Mailer: os.Args[0],
|
|
||||||
Subject: fmt.Sprintf("Scrutiny SMART error (EmailTest) detected on disk: XXXXX"),
|
|
||||||
FailureType: "EmailTest",
|
FailureType: "EmailTest",
|
||||||
Device: "/dev/sda",
|
DeviceSerial: "FAKEWDDJ324KSO",
|
||||||
DeviceType: "ata",
|
DeviceType: dbModels.DeviceProtocolAta,
|
||||||
DeviceString: "/dev/sda",
|
DeviceName: "/dev/sda",
|
||||||
Message: "TEST EMAIL from smartd for device: /dev/sda",
|
Test: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := testNotify.Send()
|
err := testNotify.Send()
|
||||||
@@ -33,6 +30,7 @@ func SendTestNotification(c *gin.Context) {
|
|||||||
logger.Errorln("An error occurred while sending test notification", err)
|
logger.Errorln("An error occurred while sending test notification", err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
"errors": []string{err.Error()},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, dbModels.DeviceWrapper{
|
c.JSON(http.StatusOK, dbModels.DeviceWrapper{
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||||
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/notify"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func UploadDeviceMetrics(c *gin.Context) {
|
func UploadDeviceMetrics(c *gin.Context) {
|
||||||
db := c.MustGet("DB").(*gorm.DB)
|
db := c.MustGet("DB").(*gorm.DB)
|
||||||
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
|
||||||
|
appConfig := c.MustGet("CONFIG").(config.Interface)
|
||||||
|
|
||||||
var collectorSmartData collector.SmartInfo
|
var collectorSmartData collector.SmartInfo
|
||||||
err := c.BindJSON(&collectorSmartData)
|
err := c.BindJSON(&collectorSmartData)
|
||||||
@@ -45,5 +48,22 @@ func UploadDeviceMetrics(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//check for error
|
||||||
|
if deviceSmartData.SmartStatus == dbModels.SmartStatusFailed {
|
||||||
|
//send notifications
|
||||||
|
testNotify := notify.Notify{
|
||||||
|
Config: appConfig,
|
||||||
|
Payload: notify.Payload{
|
||||||
|
FailureType: notify.NotifyFailureTypeSmartFailure,
|
||||||
|
DeviceName: device.DeviceName,
|
||||||
|
DeviceType: device.DeviceProtocol,
|
||||||
|
DeviceSerial: device.SerialNumber,
|
||||||
|
Test: false,
|
||||||
|
},
|
||||||
|
Logger: logger,
|
||||||
|
}
|
||||||
|
_ = testNotify.Send() //we ignore error message when sending notifications.
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,24 @@ import (
|
|||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DatabaseMiddleware(appConfig config.Interface, logger logrus.FieldLogger) gin.HandlerFunc {
|
func DatabaseMiddleware(appConfig config.Interface, globalLogger logrus.FieldLogger) gin.HandlerFunc {
|
||||||
|
|
||||||
//var database *gorm.DB
|
//var database *gorm.DB
|
||||||
fmt.Printf("Trying to connect to database stored: %s\n", appConfig.GetString("web.database.location"))
|
fmt.Printf("Trying to connect to database stored: %s\n", appConfig.GetString("web.database.location"))
|
||||||
database, err := gorm.Open("sqlite3", appConfig.GetString("web.database.location"))
|
database, err := gorm.Open(sqlite.Open(appConfig.GetString("web.database.location")), &gorm.Config{
|
||||||
|
//TODO: figure out how to log database queries again.
|
||||||
|
//Logger: logger
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("Failed to connect to database!")
|
panic("Failed to connect to database!")
|
||||||
}
|
}
|
||||||
|
|
||||||
database.SetLogger(&GormLogger{Logger: logger})
|
//database.SetLogger()
|
||||||
database.AutoMigrate(&db.Device{})
|
database.AutoMigrate(&db.Device{})
|
||||||
database.AutoMigrate(&db.SelfTest{})
|
database.AutoMigrate(&db.SelfTest{})
|
||||||
database.AutoMigrate(&db.Smart{})
|
database.AutoMigrate(&db.Smart{})
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/analogj/go-util/utils"
|
||||||
"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/web/handler"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/web/handler"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/web/middleware"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/web/middleware"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -10,6 +12,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppEngine struct {
|
type AppEngine struct {
|
||||||
@@ -77,6 +80,13 @@ func (ae *AppEngine) Start() error {
|
|||||||
logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
|
logger.SetOutput(io.MultiWriter(os.Stderr, logFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//check if the database parent directory exists, fail here rather than in a handler.
|
||||||
|
if !utils.FileExists(filepath.Dir(ae.Config.GetString("web.database.location"))) {
|
||||||
|
return errors.ConfigValidationError(fmt.Sprintf(
|
||||||
|
"Database parent directory does not exist. Please check path (%s)",
|
||||||
|
filepath.Dir(ae.Config.GetString("web.database.location"))))
|
||||||
|
}
|
||||||
|
|
||||||
r := ae.Setup(logger)
|
r := ae.Setup(logger)
|
||||||
|
|
||||||
return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")))
|
return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port")))
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ func TestPopulateMultiple(t *testing.T) {
|
|||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
//fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return("testdata/scrutiny_test.db")
|
//fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return("testdata/scrutiny_test.db")
|
||||||
|
fakeConfig.EXPECT().GetStringSlice("notify.urls").Return([]string{}).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
ae := web.AppEngine{
|
ae := web.AppEngine{
|
||||||
@@ -163,7 +164,32 @@ func TestPopulateMultiple(t *testing.T) {
|
|||||||
//assert
|
//assert
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendTestNotificationRoute(t *testing.T) {
|
//TODO: this test should use a recorded request/response playback.
|
||||||
|
//func TestSendTestNotificationRoute(t *testing.T) {
|
||||||
|
// //setup
|
||||||
|
// parentPath, _ := ioutil.TempDir("", "")
|
||||||
|
// defer os.RemoveAll(parentPath)
|
||||||
|
// mockCtrl := gomock.NewController(t)
|
||||||
|
// defer mockCtrl.Finish()
|
||||||
|
// fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
// fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
|
// fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
|
// fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://scrutiny.requestcatcher.com/test"})
|
||||||
|
// ae := web.AppEngine{
|
||||||
|
// Config: fakeConfig,
|
||||||
|
// }
|
||||||
|
// router := ae.Setup(logrus.New())
|
||||||
|
//
|
||||||
|
// //test
|
||||||
|
// wr := httptest.NewRecorder()
|
||||||
|
// req, _ := http.NewRequest("POST", "/api/health/notify", strings.NewReader("{}"))
|
||||||
|
// router.ServeHTTP(wr, req)
|
||||||
|
//
|
||||||
|
// //assert
|
||||||
|
// require.Equal(t, 200, wr.Code)
|
||||||
|
//}
|
||||||
|
|
||||||
|
func TestSendTestNotificationRoute_WebhookFailure(t *testing.T) {
|
||||||
//setup
|
//setup
|
||||||
parentPath, _ := ioutil.TempDir("", "")
|
parentPath, _ := ioutil.TempDir("", "")
|
||||||
defer os.RemoveAll(parentPath)
|
defer os.RemoveAll(parentPath)
|
||||||
@@ -172,7 +198,7 @@ func TestSendTestNotificationRoute(t *testing.T) {
|
|||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://scrutiny.requestcatcher.com/test"})
|
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://unroutable.domain.example.asdfghj"})
|
||||||
ae := web.AppEngine{
|
ae := web.AppEngine{
|
||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
}
|
}
|
||||||
@@ -184,7 +210,55 @@ func TestSendTestNotificationRoute(t *testing.T) {
|
|||||||
router.ServeHTTP(wr, req)
|
router.ServeHTTP(wr, req)
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.Equal(t, 200, wr.Code)
|
require.Equal(t, 500, wr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendTestNotificationRoute_ScriptFailure(t *testing.T) {
|
||||||
|
//setup
|
||||||
|
parentPath, _ := ioutil.TempDir("", "")
|
||||||
|
defer os.RemoveAll(parentPath)
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
|
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///missing/path/on/disk"})
|
||||||
|
ae := web.AppEngine{
|
||||||
|
Config: fakeConfig,
|
||||||
|
}
|
||||||
|
router := ae.Setup(logrus.New())
|
||||||
|
|
||||||
|
//test
|
||||||
|
wr := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/health/notify", strings.NewReader("{}"))
|
||||||
|
router.ServeHTTP(wr, req)
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.Equal(t, 500, wr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendTestNotificationRoute_ShoutrrrFailure(t *testing.T) {
|
||||||
|
//setup
|
||||||
|
parentPath, _ := ioutil.TempDir("", "")
|
||||||
|
defer os.RemoveAll(parentPath)
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
|
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"discord://invalidtoken@channel"})
|
||||||
|
ae := web.AppEngine{
|
||||||
|
Config: fakeConfig,
|
||||||
|
}
|
||||||
|
router := ae.Setup(logrus.New())
|
||||||
|
|
||||||
|
//test
|
||||||
|
wr := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/health/notify", strings.NewReader("{}"))
|
||||||
|
router.ServeHTTP(wr, req)
|
||||||
|
|
||||||
|
//assert
|
||||||
|
require.Equal(t, 500, wr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetDevicesSummaryRoute_Nvme(t *testing.T) {
|
func TestGetDevicesSummaryRoute_Nvme(t *testing.T) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { sdb } from 'app/data/mock/device/details/sdb';
|
|||||||
import { sdc } from 'app/data/mock/device/details/sdc';
|
import { sdc } from 'app/data/mock/device/details/sdc';
|
||||||
import { sdd } from 'app/data/mock/device/details/sdd';
|
import { sdd } from 'app/data/mock/device/details/sdd';
|
||||||
import { sde } from 'app/data/mock/device/details/sde';
|
import { sde } from 'app/data/mock/device/details/sde';
|
||||||
|
import { sdf } from 'app/data/mock/device/details/sdf';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -84,7 +85,7 @@ export class DetailsMockApi implements TreoMockApi
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
200,
|
200,
|
||||||
_.cloneDeep(sde)
|
_.cloneDeep(sdf)
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -98,13 +98,21 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
// @ Private methods
|
// @ Private methods
|
||||||
// -----------------------------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------------------------
|
||||||
getAttributeDescription(attribute_data){
|
getAttributeDescription(attribute_data){
|
||||||
return this.data.metadata[attribute_data.attribute_id]?.description
|
let attribute_metadata = this.data.metadata[attribute_data.attribute_id]
|
||||||
|
if(!attribute_metadata){
|
||||||
|
return 'Unknown'
|
||||||
|
} else {
|
||||||
|
return attribute_metadata.description
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributeValue(attribute_data){
|
getAttributeValue(attribute_data){
|
||||||
if(this.isAta()) {
|
if(this.isAta()) {
|
||||||
let attribute_metadata = this.data.metadata[attribute_data.attribute_id]
|
let attribute_metadata = this.data.metadata[attribute_data.attribute_id]
|
||||||
if (attribute_metadata.display_type == "raw") {
|
if(!attribute_metadata){
|
||||||
|
return attribute_data.value
|
||||||
|
} else if (attribute_metadata.display_type == "raw") {
|
||||||
return attribute_data.raw_value
|
return attribute_data.raw_value
|
||||||
} else if (attribute_metadata.display_type == "transformed" && attribute_data.transformed_value) {
|
} else if (attribute_metadata.display_type == "transformed" && attribute_data.transformed_value) {
|
||||||
return attribute_data.transformed_value
|
return attribute_data.transformed_value
|
||||||
@@ -120,7 +128,11 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
getAttributeValueType(attribute_data){
|
getAttributeValueType(attribute_data){
|
||||||
if(this.isAta()) {
|
if(this.isAta()) {
|
||||||
let attribute_metadata = this.data.metadata[attribute_data.attribute_id]
|
let attribute_metadata = this.data.metadata[attribute_data.attribute_id]
|
||||||
return attribute_metadata.display_type
|
if(!attribute_metadata){
|
||||||
|
return ''
|
||||||
|
} else {
|
||||||
|
return attribute_metadata.display_type
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -135,12 +147,18 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAttributeWorst(attribute_data){
|
getAttributeWorst(attribute_data){
|
||||||
return this.data.metadata[attribute_data.attribute_id]?.display_type == "normalized" ? attribute_data.worst : ''
|
let attribute_metadata = this.data.metadata[attribute_data.attribute_id]
|
||||||
|
if(!attribute_metadata){
|
||||||
|
return attribute_data.worst
|
||||||
|
} else {
|
||||||
|
return attribute_metadata?.display_type == "normalized" ? attribute_data.worst : ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributeThreshold(attribute_data){
|
getAttributeThreshold(attribute_data){
|
||||||
if(this.isAta()){
|
if(this.isAta()){
|
||||||
if (this.data.metadata[attribute_data.attribute_id]?.display_type == "normalized"){
|
let attribute_metadata = this.data.metadata[attribute_data.attribute_id]
|
||||||
|
if(!attribute_metadata || attribute_metadata.display_type == "normalized"){
|
||||||
return attribute_data.thresh
|
return attribute_data.thresh
|
||||||
} else {
|
} else {
|
||||||
// if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){
|
// if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){
|
||||||
|
|||||||
Reference in New Issue
Block a user