Compare commits

...

119 Commits

Author SHA1 Message Date
Jason Kulatunga f8adf53e30 Change artifact download name to './' 2026-02-06 22:32:59 -05:00
Aram Akhavan 51f0ba6ee2 Update release workflows (#874)
* Bump action versions
* Merge frontend release into main release workflow
* Fix bugs with asset naming
2026-02-06 16:20:57 -08:00
Aram Akhavan 34b0347acd Fix release workflow (#873)
Newer version of upload-artifact requires unique artifact names, but the matrix was using the same name for all of them.
2026-02-06 12:29:15 -08:00
Jason Kulatunga 0565962a14 Check result of attribute casting to avoid panics (#528) 2026-02-05 22:14:03 -08:00
Liu Xiaoyi 184bc4bec5 Improve temperature logging (#825)
* Always log current temperature
* Forcefully align each ata_sct_temperature_history data point to an integer multiple of the logging interval to prevent repeated data points

Fixes #824
2026-02-05 21:35:35 -08:00
mcarbonne bdbe13e320 Add option to discard SCT Data Table Temperature History (#557)
Fixes #494
2026-02-05 20:59:24 -08:00
Aram Akhavan 761014a93f Fix codecov upload (#850)
Update Codecov action to version 5 and add token
2026-02-04 16:11:13 -08:00
Aram Akhavan 27be0b8327 Add AI Policy (#851) 2026-02-01 21:48:33 -08:00
Aram Akhavan 69abe43a1d Update authors (#849)
Add Aram Akhavan as a maintainer
2026-01-31 13:29:45 -08:00
Jason Kulatunga 7c35d59552 Merge pull request #784 from Peppercorn27/master
Fix web ui latency
2025-10-19 08:50:10 -04:00
Jason Kulatunga 742153e5dc Merge pull request #773 from Impact123/restart-policy
Unify docker restart policy among docs and example files
2025-10-19 08:43:40 -04:00
Jason Kulatunga 5f7e4a3808 Merge pull request #793 from pabsi/patch-1
feat: Update dashboard.component.ts
2025-08-14 11:38:02 -04:00
Pablo bb98b8c45b feat: Update dashboard.component.ts
Addresses https://github.com/AnalogJ/scrutiny/issues/755
2025-08-08 21:02:05 +02:00
Peppercorn27 b71897fa5f fix web ui latency
fix web ui latency in situations where cron shedule has been reduced resulting in more data being present in influxDB than expected
2025-07-06 17:11:42 +01:00
Impact a182c691fb Unify docker restart policy among docs and example files. 2025-05-06 05:07:56 +00:00
Jason Kulatunga 4066c84c8e Merge pull request #771 from RoboMagus/docker_semver_tags
Add docker semver tags
2025-04-30 10:32:48 -04:00
Jason Kulatunga 4a72c9ef55 Merge pull request #754 from Berry-95/491-FEAT-Allow-disks-to-be-archived
Fixes 491 [FEAT] Allow disks to be hidden/archived
2025-04-30 10:31:21 -04:00
Sam 3e11583283 491 [FEAT] Allow disks to be hidden/archived
- Fix mock device type definition mismatch in the frontend.
- Make DeviceModel archived field optional.
2025-04-28 15:01:31 +02:00
RoboMagus ea9799d963 Add docker semver tags 2025-04-24 22:38:31 +02:00
Jason Kulatunga e46ab7373e Merge pull request #739 from RickZaki/GHissue-643
fix: issue 643 - Fahrenheit values in graph were converted twice
2025-04-23 08:06:20 -04:00
Jason Kulatunga 87f923e1f2 Merge branch 'master' into GHissue-643 2025-04-11 07:16:29 -04:00
Sam Beresford 2244504023 Merge branch 'master' into 491-FEAT-Allow-disks-to-be-archived 2025-04-10 21:01:31 +01:00
Jason Kulatunga 192ae40f74 Merge pull request #744 from mcarbonne/fix_ci
Fix CI (conflicting artifact names)
2025-04-10 04:27:19 -04:00
Sam 600cd153e0 491 [FEAT] Allow disks to be hidden/archived
- Add archived to device type & db
- Add archive/unarchive handlers to webapp backend
- Add archive toggle & styling to webapp frontend
2025-02-21 09:25:27 +01:00
Maximilien Carbonne d11bf0a2fc fix CI (conflicting artifact names) 2025-01-26 21:51:51 +01:00
Rick Zaki 50561f34ea fix: https://github.com/AnalogJ/scrutiny/issues/643
needed to separate formatting temps from converting
dashboard was using format method to convert and send Fahrenheit values to chart, then passing the same method into chart formatter causing the Fahrenheit value to be passed in as Celsius and converted again.
2025-01-09 14:27:26 -05:00
Jason Kulatunga a58f9445c1 Merge pull request #619 from datenzar/override-config-with-env-variables
feat: Ability to override commands args
2025-01-08 10:46:10 -07:00
Jason Kulatunga 1ec478302f Merge pull request #737 from AnalogJ/AnalogJ-patch-1
Update TROUBLESHOOTING_DEVICE_COLLECTOR.md
2025-01-05 11:54:06 -07:00
Jason Kulatunga 412f956782 Update TROUBLESHOOTING_DEVICE_COLLECTOR.md 2025-01-05 11:53:43 -07:00
Jason Kulatunga 9b28ac5069 Update TESTERS.md 2025-01-04 17:46:17 -07:00
Jason Kulatunga db2869ffc6 Merge pull request #736 from AnalogJ/AnalogJ-patch-1
Update example.hubspoke.docker-compose.yml
2025-01-04 17:42:03 -07:00
Jason Kulatunga 6e349244d1 Update example.hubspoke.docker-compose.yml 2025-01-04 17:41:51 -07:00
Jason Kulatunga e6cd3ee3c6 Update TESTERS.md 2025-01-04 17:35:41 -07:00
Jason Kulatunga df6a4cef59 Update TROUBLESHOOTING_INFLUXDB.md 2025-01-04 17:01:29 -07:00
Jason Kulatunga 8cf7d64da7 Merge pull request #684 from enoch85/patch-1
Add info about rootless Docker
2025-01-04 15:29:20 -07:00
Jason Kulatunga 3de12cd739 Merge branch 'master' into override-config-with-env-variables 2025-01-04 15:26:10 -07:00
Jason Kulatunga affe05e145 Merge pull request #725 from pabsi/706-add-wait-time-between-checks-fix-unit
Issue 706: Fix time unit
2024-11-26 09:48:53 -05:00
Pablo Garcia 9ad96e6d37 Change to time.Seconds 2024-11-26 15:13:44 +01:00
Pablo Garcia 85d98316f3 Issue 706: Fix time unit 2024-11-26 10:46:27 +01:00
Jason Kulatunga 0641b5e79d Merge pull request #710 from pabsi/706-add-wait-time-between-checks
Add a wait between disks checks
2024-11-22 07:57:13 -05:00
Pablo Garcia Alvarez c168e1e9fc Add check for the wait 2024-11-11 22:07:57 +01:00
Pablo Garcia 56a9454730 Add a wait between disks checks 2024-11-07 11:54:46 +01:00
Martin Kleine a783604c4e Feature: Use automatic binding of env variables 2024-10-15 00:18:54 +02:00
Martin Kleine 604dcf355c feat: Ability to override commands args
In order to override the arguments which are used e.g. to call smartctl, they need to be bind to the respective environment variable.
2024-10-15 00:18:54 +02:00
Jason Kulatunga 57dc547265 fixing github actions. 2024-09-20 11:24:56 -04:00
Jason Kulatunga e0fe17afbf Merge pull request #686 from nicjohnson145/feat--device-allowlist
feat: create allow-list for filtering down devices to only a subset
2024-09-20 11:22:43 -04:00
Nic Johnson c9429c61b2 feat: create allow-list for filtering down devices to only a subset 2024-09-11 23:12:00 -05:00
Daniel Hansson 394ac0af2c Add info about rootless Docker
This avoids session being killed when running rootless.
2024-09-09 21:33:59 +02:00
Jason Kulatunga 48feee51d0 Merge pull request #672 from Hudater/master
Updated containrrr/shoutrrr from v0.7.1 to v0.8.0
2024-09-08 10:27:22 +09:00
Harshit Mani Tripathi d4fb7786d2 reverted accidental bump of spf13/viper from v0.14.0 to v0.15.0 2024-08-04 18:59:37 +05:30
Harshit Mani Tripathi c316f996c6 updated containrrr/shoutrrr from v0.7.1 to v0.8.0 2024-08-04 18:48:23 +05:30
Jason Kulatunga 49108bd1ef Merge pull request #634 from bauzer714/addDeviceHoursSetting
Create a setting for user to indicate humanized or hours on dashboard/device detail
2024-07-25 13:33:57 -07:00
Jason Kulatunga 0dafb65c5f Merge branch 'master' into addDeviceHoursSetting 2024-07-25 13:29:29 -07:00
Brice Bauer c5943a1ca4 Adjust null input response, and tests 2024-07-25 15:40:28 -04:00
Brice Bauer a5893f0bf9 Add tests for DeviceHoursPipe 2024-07-22 14:02:27 -04:00
Brice Bauer 142fe06df1 Move powered_on_hours_unit to a new migration id 2024-07-22 08:37:35 -04:00
Jason Kulatunga 8b7ddd3042 Merge pull request #644 from luomie/patch-1
fix example Shoutrrr discord notification url structure
2024-07-18 20:14:31 -07:00
Jason Kulatunga db57281557 Merge pull request #666 from phcreery/patch-1
Update INSTALL_HUB_SPOKE.md
2024-07-17 18:48:37 -07:00
Peyton Creery 5a5877b729 Update INSTALL_HUB_SPOKE.md 2024-07-17 20:14:26 -05:00
luomie 0a89c2bab3 fix Shoutrrr discord notification url structure 2024-05-19 23:13:05 +02:00
Brice Bauer a18e2842ac Update db migration description to match setting possible values 2024-05-08 08:43:25 -04:00
Brice Bauer 806f7c64a0 Add pipe and implement to dashboard/device component 2024-05-08 08:30:49 -04:00
Brice Bauer 8fa32c6dd7 Add DB Migration and config/settings 2024-05-07 16:45:09 -04:00
packagrio-bot 5e6ab2290b (v0.8.1) Automated packaging of release by Packagr 2024-04-08 04:48:11 +00:00
Jason Kulatunga 67c0af9f59 fix amd64 s6_arch. 2024-04-05 14:11:11 -07:00
Jason Kulatunga 55565e509d Merge pull request #625 from AnalogJ/cron_fixes
fixing cron in #602
2024-04-05 14:04:39 -07:00
Jason Kulatunga f74d9c108a fixing cron in #602
Updated s6overlay to v3
Note:  xz-utils was added as a requirement for s6-overlay (using safe 5.4.1 instead of compromised 5.6.x versions)
2024-04-05 10:01:04 -07:00
packagrio-bot 5977f7c7d4 (v0.8.0) Automated packaging of release by Packagr 2024-03-13 23:47:26 +00:00
Jason Kulatunga 3490a2ffc2 Merge pull request #597 from joserebelo/sigterm
Use exec on scrutiny-collector cron to ensure signal handling
2024-03-12 21:08:16 -07:00
Jason Kulatunga a0f9e6e3f3 Merge pull request #596 from dropsignal/master
rebase docker image to debian 12 (bookworm)
2024-03-12 20:53:35 -07:00
Drop Signal 6a9b89b38a fixed missing && 2024-03-12 21:39:36 -05:00
Drop Signal 543f376015 performing requested changes 2024-03-09 21:37:11 -06:00
José Rebelo ca7bd2c151 Use exec on scrutiny-collector cron to ensure signal handling
This way SIGTERM gets propagated and the container terminates
gracefully.
2024-03-09 22:46:07 +00:00
Drop Signal 1e74e96658 rebase to debian 12 (bookworm) 2024-03-08 22:30:43 -06:00
packagrio-bot 5e33c33e75 (v0.7.3) Automated packaging of release by Packagr 2024-02-24 23:12:54 +00:00
Jason Kulatunga 3ea223fa8e Merge pull request #547 from kaysond/master
Add support for disabling repeat notifications if the values haven't changed
2024-02-24 15:10:29 -08:00
Jason Kulatunga 44275c66ca Merge pull request #569 from uhthomas/466
fix(collector): show correct nvme capacity
2024-02-24 09:32:59 -08:00
Jason Kulatunga 19bd59dc27 Merge pull request #577 from DrFrankensteinUK/patch-1
Update SUPPORTED_NAS_OS.md
2024-02-24 09:20:23 -08:00
DrFrankensteinUK b7fab3c94e Update SUPPORTED_NAS_OS.md
Added another link for the new Container Manager version of my guide for those on the newer DSM versions. The older guide while archived still functions correctly.
2024-02-17 16:47:05 +00:00
Aram Akhavan 09f4b34bf0 fix server test 2024-02-04 11:52:30 -08:00
Aram Akhavan f24abf254b Add tests for not repeating notifications 2024-02-04 11:38:52 -08:00
Aram Akhavan cc889f2a2d fix notify tests 2024-02-04 11:38:52 -08:00
Jason Kulatunga 2aa242e364 update mockgen instructions 2024-02-04 11:38:52 -08:00
Jason Kulatunga 1c193aa043 add database interface mock 2024-02-04 11:38:52 -08:00
Aram Akhavan 01c5a7fdfe Address review comments 2024-02-04 11:38:51 -08:00
Aram Akhavan 98d958888c refactor common code 2024-02-04 11:38:51 -08:00
Aram Akhavan 4e5c76b259 Add support for disabling repeat notifications
* Add a new database function for getting the tail

* Update ShouldNotify() to handle ignoring repeat notifications if set
2024-02-04 11:38:51 -08:00
Aram Akhavan 6417d71311 Add a setting for repeating notifications or not 2024-02-04 11:38:50 -08:00
Aram Akhavan 3285eb659f Fix some development issues 2024-02-04 11:38:50 -08:00
Thomas Way db86bac9ef fix(collector): show correct nvme capacity
Some nvme devices do not report their capacity through the usual
'user_capacity' field, instead the total capacity is reported with the
'nvme_total_capacity' field.

Fixes: #466
2024-01-23 22:02:02 +00:00
Jason Kulatunga a3dfce3561 Update INSTALL_HUB_SPOKE.md
fixes https://github.com/AnalogJ/scrutiny/issues/495
2024-01-23 12:57:22 -08:00
Jason Kulatunga 240178d742 Merge pull request #529 from KaeTuuN/patch-1
Update README.md Links to compose files
2024-01-23 12:19:39 -08:00
Jason Kulatunga 2dcb6cd6b6 Merge pull request #566 from PrplHaz4/patch-3
Add COLLECTOR_HOST_ID env var to hubspoke example
2024-01-23 12:16:27 -08:00
PrplHaz4 56df7b5797 Add COLLECTOR_HOST_ID env var to hubspoke example
Added COLLECTOR_HOST_ID environment variable to hubspoke example
2024-01-15 16:51:10 -05:00
Jason Kulatunga d54a0abc8c Merge pull request #559 from ibizaman/patch-1
Fix typo in readme
2023-12-26 14:50:02 -07:00
Pierre Penninckx 061f55f5b1 Fix typo in readme 2023-12-19 15:51:38 -08:00
Jason Kulatunga 5bbd4c3b64 update delete device message to clarify that no data will actually be effected, only scrutiny data.
fixes #544
2023-11-22 14:25:26 -08:00
Jason Kulatunga fb6c3d6a24 document Shoutrrr special characters in username & password -
fixes #532
2023-11-18 08:50:47 -08:00
KaeTuuN 87dc51a9c0 Update README.md Links to compose files
Links were not working, so I replaced them with working ones.
2023-10-18 10:33:37 +02:00
packagrio-bot c3a0fb7fb5 (v0.7.2) Automated packaging of release by Packagr 2023-10-17 13:19:35 +00:00
Jason Kulatunga 5e87608587 Merge pull request #520 from kaysond/patch-1 2023-10-17 06:09:52 -07:00
Jason Kulatunga ab7fd107e7 Merge pull request #527 from kaysond/master 2023-10-17 06:08:58 -07:00
Aram Akhavan 550cd59093 Fix parsing of attribute 188 on seagate drives 2023-10-14 21:39:12 -07:00
Aram Akhavan a8621d2bb0 Update permissions setting in Dockerfile.web
This fixes issues with assets loading when you run as non root users
2023-09-29 11:51:47 -07:00
Jason Kulatunga 4b1d9dc2d3 Merge pull request #519 from SlavikCA/patch-1 2023-09-27 10:17:26 -07:00
Slavik 22769b962e Docs: few more details about Traefik proxy 2023-09-26 21:11:59 -07:00
Slavik feb7909961 Docs: add Traefik REVERSE_PROXY config example 2023-09-26 21:04:26 -07:00
Jason Kulatunga 2f01e8c8e0 Merge pull request #512 from kaysond/master 2023-09-06 12:51:50 -07:00
Aram Akhavan 31c2daedf7 fix smart 188 thresholds 2023-09-03 10:37:43 -07:00
packagrio-bot ee893cc360 (v0.7.1) Automated packaging of release by Packagr 2023-04-08 23:01:22 +00:00
Jason Kulatunga d73907d357 Merge pull request #474 from AnalogJ/beta 2023-04-08 15:58:51 -07:00
Jason Kulatunga 0b50305f38 fix invalid COPY instruction. 2023-04-07 00:00:11 -07:00
Jason Kulatunga ee3d719c3a simplify docker image build
changes contributed by @modem7

fixes #461
2023-04-06 23:57:15 -07:00
Jason Kulatunga d76cdca4a5 Merge pull request #472 from adamantike/misc/add-support-for-yml-config-files 2023-04-06 17:46:03 -07:00
Jason Kulatunga b34ed607b7 Merge pull request #471 from adamantike/feat/add-support-for-ntfy-notifications 2023-04-06 17:42:50 -07:00
Michael Manganiello 932e191510 Allow configuration files with yml extension
If a `collector.yml` or `scrutiny.yml` configuration file is present,
use it as long as a `.yaml` version is not available too.

Fixes #79
2023-04-06 20:55:22 -03:00
Michael Manganiello 3a6c407fe7 Add support for ntfy notifications
Updates [`shoutrrr`](containrrr.dev/shoutrrr/) to `v0.7.1` to enable
support for [ntfy](https://ntfy.sh/) notifications.

Fixes #433.
2023-04-06 17:04:48 -03:00
Jason Kulatunga 8c3afc31f4 Merge pull request #449 from adamantike/fix/delete-influxdb-deb-from-docker-image 2023-04-05 23:25:52 -07:00
Michael Manganiello fb7848f341 Delete temporary deb file from omnibus Docker image
This considerably reduces the Docker image size for the `omnibus` variant:

```
scrutiny-omnibus-master         latest            cd7a7dde100b   9 minutes ago   538MB
scrutiny-omnibus-fix-applied    latest            5f7ac124ef50   6 minutes ago   431MB
```
2023-02-22 16:25:09 -03:00
102 changed files with 2410 additions and 727 deletions
+7 -6
View File
@@ -13,7 +13,7 @@ jobs:
run: |
make binary-frontend-test-coverage
- name: Upload coverage
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: coverage
path: ${{ github.workspace }}/webapp/frontend/coverage/lcov.info
@@ -49,7 +49,7 @@ jobs:
run: |
make binary-clean binary-test-coverage
- name: Upload coverage
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: coverage
path: ${{ github.workspace }}/coverage.txt
@@ -64,12 +64,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Download coverage reports
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ${{ github.workspace }}/coverage.txt,${{ github.workspace }}/lcov.info
flags: unittests
fail_ci_if_error: true
@@ -106,9 +107,9 @@ jobs:
run: |
make binary-clean binary-all
- name: Archive
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: binaries.zip
name: binaries-${{ matrix.cfg.on }}-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}-${{ matrix.cfg.goarm || 'na' }}.zip
path: |
scrutiny-web-*
scrutiny-collector-metrics-*
+9 -17
View File
@@ -46,7 +46,9 @@ jobs:
latest=false
tags: |
type=ref,enable=true,event=branch,suffix=-collector
type=ref,enable=true,event=tag,suffix=-collector
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},suffix=-collector
type=semver,pattern=v{{major}}.{{minor}},suffix=-collector
type=semver,pattern=v{{major}},suffix=-collector
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
@@ -74,13 +76,6 @@ jobs:
uses: actions/checkout@v2
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: "Install Node"
uses: actions/setup-node@v3
with:
node-version: 16
- name: "Generate frontend"
run: |
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
@@ -106,7 +101,9 @@ jobs:
latest=false
tags: |
type=ref,enable=true,event=branch,suffix=-web
type=ref,enable=true,event=tag,suffix=-web
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},suffix=-web
type=semver,pattern=v{{major}}.{{minor}},suffix=-web
type=semver,pattern=v{{major}},suffix=-web
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
@@ -132,13 +129,6 @@ jobs:
uses: actions/checkout@v2
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: "Install Node"
uses: actions/setup-node@v3
with:
node-version: 16
- name: "Generate frontend"
run: |
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
@@ -162,7 +152,9 @@ jobs:
with:
tags: |
type=ref,enable=true,event=branch,suffix=-omnibus
type=ref,enable=true,event=tag,suffix=-omnibus
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},suffix=-omnibus
type=semver,pattern=v{{major}}.{{minor}},suffix=-omnibus
type=semver,pattern=v{{major}},suffix=-omnibus
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
-7
View File
@@ -19,13 +19,6 @@ jobs:
uses: actions/checkout@v2
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: "Install Node"
uses: actions/setup-node@v3
with:
node-version: 16
- name: "Generate frontend"
run: |
make binary-frontend && echo "print contents of ./dist" && ls -alt ./dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
-34
View File
@@ -1,34 +0,0 @@
# compiles angular frontend and attaches it to the latest release.
name: Release Frontend
on:
release:
# Only use the types keyword to narrow down the activity types that will trigger your workflow.
types: [published]
jobs:
release-frontend:
name: Release Frontend
runs-on: ubuntu-latest
container: node:lts-slim
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{github.event.release.tag_name}}
- name: "Generate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: Build Frontend
run: |
apt-get update && apt-get install -y make
make binary-frontend
tar -czf scrutiny-web-frontend.tar.gz dist
- name: Upload Frontend Asset
id: upload-release-asset3
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.SCRUTINY_GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: './scrutiny-web-frontend.tar.gz'
asset_name: scrutiny-web-frontend.tar.gz
asset_content_type: application/gzip
+45 -14
View File
@@ -39,7 +39,7 @@ jobs:
add-apt-repository ppa:git-core/ppa && apt-get update && apt-get install -y git
git --version
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Bump version
@@ -61,7 +61,7 @@ jobs:
with:
version_metadata_path: ${{ github.event.inputs.version_metadata_path }}
- name: Upload workspace
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: workspace
path: ${{ github.workspace }}/**/*
@@ -91,36 +91,66 @@ jobs:
- { on: windows-latest, goos: windows, goarch: arm64 }
steps:
- name: Download workspace
uses: actions/download-artifact@v3
uses: actions/download-artifact@v7
with:
name: workspace
- uses: actions/setup-go@v3
- uses: actions/setup-go@v6
with:
go-version: '1.20.1' # The Go version to download (if necessary) and use.
- name: Build Binaries
run: |
make binary-clean binary-all
- name: Archive
uses: actions/upload-artifact@v2
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: binaries.zip
name: scrutiny-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}.zip
path: |
scrutiny-web-*
scrutiny-collector-metrics-*
scrutiny-web-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}${{ case(matrix.cfg.goos == 'windows', '.exe', '') }}
scrutiny-collector-metrics-${{ matrix.cfg.goos }}-${{ matrix.cfg.goarch }}${{ case(matrix.cfg.goarm != '', format('-{0}', matrix.cfg.goarm), '') }}${{ case(matrix.cfg.goos == 'windows', '.exe', '') }}
build_frontend:
name: Build Frontend
needs: release
runs-on: ubuntu-latest
container: node:lts-slim
steps:
- name: Download workspace
uses: actions/download-artifact@v7
with:
name: workspace
- name: "Generate frontend version information"
run: "cd webapp/frontend && chmod +x git.version.sh && ./git.version.sh"
- name: Build Frontend
run: |
apt-get update && apt-get install -y make
make binary-frontend
tar -czf scrutiny-web-frontend.tar.gz dist
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: scrutiny-web-frontend.zip
path: scrutiny-web-frontend.tar.gz
release-publish:
name: Publish Release
needs: build
needs:
- build
- build_frontend
runs-on: ubuntu-latest
steps:
- name: Download workspace
uses: actions/download-artifact@v3
uses: actions/download-artifact@v7
with:
name: workspace
name: ./
- name: Download binaries
uses: actions/download-artifact@v3
uses: actions/download-artifact@v7
with:
name: binaries.zip
merge-multiple: true
pattern: scrutiny-*.zip
- name: Download frontend
uses: actions/download-artifact@v7
with:
name: scrutiny-web-frontend.zip
- name: List
shell: bash
run: |
@@ -144,6 +174,7 @@ jobs:
scrutiny-collector-metrics-linux-arm64
scrutiny-collector-metrics-windows-amd64.exe
scrutiny-collector-metrics-windows-arm64.exe
scrutiny-web-frontend.tar.gz
scrutiny-web-darwin-amd64
scrutiny-web-darwin-arm64
scrutiny-web-freebsd-amd64
+3 -1
View File
@@ -65,4 +65,6 @@ scrutiny_test.db
scrutiny.yaml
coverage.txt
/config
/influxdb
/influxdb
.angular
web.log
+73
View File
@@ -0,0 +1,73 @@
# AI Usage Policy
scrutiny has strict rules for AI usage:
- **All AI usage in any form must be disclosed.** You must state
the tool you used (e.g. Claude Code, Cursor, Amp) along with
the extent that the work was AI-assisted.
- **Pull requests created in any way by AI can only be for accepted issues.**
Drive-by pull requests that do not reference an accepted issue will be
closed. If AI isn't disclosed but a maintainer suspects its use, the
PR will be closed. If you want to share code for a non-accepted issue,
open a discussion or attach it to an existing discussion.
- **Pull requests created by AI must have been fully verified with
human use.** AI must not create hypothetically correct code that
hasn't been tested. Importantly, you must not allow AI to write
code for platforms or environments you don't have access to manually
test on.
- **Issues and discussions can use AI assistance but must have a full
human-in-the-loop.** This means that any content generated with AI
must have been reviewed _and edited_ by a human before submission.
AI is very good at being overly verbose and including noise that
distracts from the main point. Humans must do their research and
trim this down.
- **No AI-generated media is allowed (art, images, videos, audio, etc.).**
Text and code are the only acceptable AI-generated content, per the
other rules in this policy.
- **Bad AI drivers will be banned and ridiculed in public.** You've
been warned. We love to help junior developers learn and grow, but
if you're interested in that then don't use AI, and we'll help you.
I'm sorry that bad AI drivers have ruined this for you.
These rules apply only to outside contributions to scrutiny. Maintainers
and repeat contributors (with explicit permission) are exempt from these
rules and may use AI tools at their discretion; they've proven themselves
trustworthy to apply good judgment.
## There are Humans Here
Please remember that scrutiny is maintained by humans.
Every discussion, issue, and pull request is read and reviewed by
humans (and sometimes machines, too). It is a boundary point at which
people interact with each other and the work done. It is rude and
disrespectful to approach this boundary with low-effort, unqualified
work, since it puts the burden of validation on the maintainer.
In a perfect world, AI would produce high-quality, accurate work
every time. But today, that reality depends on the driver of the AI.
And today, most drivers of AI are just not good enough. So, until either
the people get better, the AI gets better, or both, we have to have
strict rules to protect maintainers.
## AI is Welcome Here
Many maintainers embrace AI tools as a productive tool in their workflow.
As a project, scrutiny welcomes AI as a tool!
**Our reason for the strict AI policy is not due to an anti-AI stance**, but
instead due to the number of highly unqualified people using AI. It's the
people, not the tools, that are the problem.
This section is included to be transparent about the project's usage about
AI for people who may disagree with it, and to address the misconception
that this policy is anti-AI in nature.
# Credit
Adopted from [ghostty's AI policy](https://github.com/ghostty-org/ghostty/blob/1b7a15899ad40fba4ce020f537055d30eaf99ee8/AI_POLICY.md)
+3 -1
View File
@@ -1,5 +1,7 @@
# Contributing
**Please see our [AI policy](./AI_POLICY.md).**
The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) containing source code for:
- Scrutiny Backend Server (API)
- Scrutiny Frontend Angular SPA
@@ -161,7 +163,7 @@ docker cp scrutiny:/tmp/web.log web.log
# Docker Development
```
docker build -f docker/Dockerfile . -t chcr.io/analogj/scrutiny:master-omnibus
docker build -f docker/Dockerfile . -t ghcr.io/analogj/scrutiny:master-omnibus
docker run -it --rm -p 8080:8080 \
-v /run/udev:/run/udev:ro \
--cap-add SYS_RAWIO \
+1 -1
View File
@@ -100,7 +100,7 @@ binary-frontend: export NPM_CONFIG_LOGLEVEL = warn
binary-frontend: export NG_CLI_ANALYTICS = false
binary-frontend:
cd webapp/frontend
npm install -g @angular/cli@9.1.4
npm install -g @angular/cli@v13-lts
mkdir -p $(CURDIR)/dist
npm ci
npm run build:prod -- --output-path=$(CURDIR)/dist
+12 -10
View File
@@ -26,7 +26,7 @@ If you run a server with more than a couple of hard drives, you're probably alre
> smartd is a daemon that monitors the Self-Monitoring, Analysis and Reporting Technology (SMART) system built into many ATA, IDE and SCSI-3 hard drives. The purpose of SMART is to monitor the reliability of the hard drive and predict drive failures, and to carry out different types of drive self-tests.
Theses S.M.A.R.T hard drive self-tests can help you detect and replace failing hard drives before they cause permanent data loss. However, there's a couple issues with `smartd`:
These S.M.A.R.T hard drive self-tests can help you detect and replace failing hard drives before they cause permanent data loss. However, there's a couple issues with `smartd`:
- There are more than a hundred S.M.A.R.T attributes, however `smartd` does not differentiate between critical and informational metrics
- `smartd` does not record S.M.A.R.T attribute history, so it can be hard to determine if an attribute is degrading slowly over time.
@@ -69,10 +69,10 @@ See [docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](./docs/TROUBLESHOOTING_DEVICE_COL
If you're using Docker, getting started is as simple as running the following command:
> See [docker/example.omnibus.docker-compose.yml](./docker/example.omnibus.docker-compose.yml) for a docker-compose file.
> See [docker/example.omnibus.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.omnibus.docker-compose.yml) for a docker-compose file.
```bash
docker run -it --rm -p 8080:8080 -p 8086:8086 \
docker run -p 8080:8080 -p 8086:8086 --restart unless-stopped \
-v `pwd`/scrutiny:/opt/scrutiny/config \
-v `pwd`/influxdb2:/opt/scrutiny/influxdb \
-v /run/udev:/run/udev:ro \
@@ -100,20 +100,20 @@ other Docker images:
- `influxdb:2.2` - InfluxDB image, used by the Web container to persist SMART data. Only one container necessary
See [docs/TROUBLESHOOTING_INFLUXDB.md](./docs/TROUBLESHOOTING_INFLUXDB.md)
> See [docker/example.hubspoke.docker-compose.yml](./docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
> See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
```bash
docker run --rm -p 8086:8086 \
docker run -p 8086:8086 --restart unless-stopped \
-v `pwd`/influxdb2:/var/lib/influxdb2 \
--name scrutiny-influxdb \
influxdb:2.2
docker run --rm -p 8080:8080 \
docker run -p 8080:8080 --restart unless-stopped \
-v `pwd`/scrutiny:/opt/scrutiny/config \
--name scrutiny-web \
ghcr.io/analogj/scrutiny:master-web
docker run --rm \
docker run --restart unless-stopped \
-v /run/udev:/run/udev:ro \
--cap-add SYS_RAWIO \
--device=/dev/sda \
@@ -157,7 +157,7 @@ Neither file is required, however if provided, it allows you to configure how Sc
## Cron Schedule
Unfortunately the Cron schedule cannot be configured via the `collector.yaml` (as the collector binary needs to be trigged by a scheduler/cron).
However, if you are using the official `ghcr.io/analogj/scrutiny:master-collector` or `ghcr.io/analogj/scrutiny:master-omnibus` docker images,
However, if you are using the official `ghcr.io/analogj/scrutiny:master-collector` or `ghcr.io/analogj/scrutiny:master-omnibus` docker images,
you can use the `COLLECTOR_CRON_SCHEDULE` environmental variable to override the default cron schedule (daily @ midnight - `0 0 * * *`).
`docker run -e COLLECTOR_CRON_SCHEDULE="0 0 * * *" ...`
@@ -174,6 +174,7 @@ Scrutiny supports sending SMART device failure notifications via the following s
- IFTTT
- Join
- Mattermost
- ntfy
- Pushbullet
- Pushover
- Slack
@@ -243,7 +244,7 @@ scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
| linux-arm-6 | :white_check_mark: | |
| linux-arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
| linux-arm64 | :white_check_mark: | :white_check_mark: |
| freebsd-amd64 | :white_check_mark: | |
| freebsd-amd64 | :white_check_mark: | |
| macos-amd64 | :white_check_mark: | :white_check_mark: |
| macos-arm64 | :white_check_mark: | :white_check_mark: |
| windows-amd64 | :white_check_mark: | WIP, see [#15](https://github.com/AnalogJ/scrutiny/issues/15) |
@@ -264,7 +265,8 @@ We use SemVer for versioning. For the versions available, see the tags on this r
# Authors
Jason Kulatunga - Initial Development - @AnalogJ
* Jason Kulatunga - Initial Development - [@AnalogJ](https://github.com/AnalogJ/)
* Aram Akhavan - Maintenence - [@kaysond](https://github.com/kaysond/)
# Licenses
@@ -30,8 +30,14 @@ func main() {
os.Exit(1)
}
configFilePath := "/opt/scrutiny/config/collector.yaml"
configFilePathAlternative := "/opt/scrutiny/config/collector.yml"
if !utils.FileExists(configFilePath) && utils.FileExists(configFilePathAlternative) {
configFilePath = configFilePathAlternative
}
//we're going to load the config file manually, since we need to validate it.
err = config.ReadConfig("/opt/scrutiny/config/collector.yaml") // Find and read the config file
err = config.ReadConfig(configFilePath) // Find and read the config file
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
//ignore "could not find config file"
} else if err != nil {
+10 -7
View File
@@ -4,6 +4,12 @@ import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"os"
"os/exec"
"strings"
"time"
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/config"
"github.com/analogj/scrutiny/collector/pkg/detect"
@@ -11,10 +17,6 @@ import (
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
"net/url"
"os"
"os/exec"
"strings"
)
type MetricsCollector struct {
@@ -90,8 +92,9 @@ func (mc *MetricsCollector) Run() error {
//go mc.Collect(&wg, device.WWN, device.DeviceName, device.DeviceType)
mc.Collect(device.WWN, device.DeviceName, device.DeviceType)
// TODO: we may need to sleep for between each call to smartctl -a
//time.Sleep(30 * time.Millisecond)
if mc.config.GetInt("commands.metrics_smartctl_wait") > 0 {
time.Sleep(time.Duration(mc.config.GetInt("commands.metrics_smartctl_wait")) * time.Second)
}
}
//mc.logger.Infoln("Main: Waiting for workers to finish")
@@ -113,7 +116,7 @@ func (mc *MetricsCollector) Validate() error {
return nil
}
//func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string, deviceType string) {
// func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string, deviceType string) {
func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceType string) {
//defer wg.Done()
if len(deviceWWN) == 0 {
+24 -1
View File
@@ -20,7 +20,7 @@ import (
type configuration struct {
*viper.Viper
deviceOverrides []models.ScanOverride
deviceOverrides []models.ScanOverride
}
//Viper uses the following precedence order. Each item takes precedence over the item below it:
@@ -47,9 +47,17 @@ func (c *configuration) Init() error {
c.SetDefault("commands.metrics_scan_args", "--scan --json")
c.SetDefault("commands.metrics_info_args", "--info --json")
c.SetDefault("commands.metrics_smart_args", "--xall --json")
c.SetDefault("commands.metrics_smartctl_wait", 0)
//configure env variable parsing.
c.SetEnvPrefix("COLLECTOR")
c.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
c.AutomaticEnv()
//c.SetDefault("collect.short.command", "-a -o on -S on")
c.SetDefault("allow_listed_devices", []string{})
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
c.SetConfigType("yaml")
//c.SetConfigName("drawbridge")
@@ -186,3 +194,18 @@ func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
}
return c.GetString("commands.metrics_smart_args")
}
func (c *configuration) IsAllowlistedDevice(deviceName string) bool {
allowList := c.GetStringSlice("allow_listed_devices")
if len(allowList) == 0 {
return true
}
for _, item := range allowList {
if item == deviceName {
return true
}
}
return false
}
+26
View File
@@ -144,3 +144,29 @@ func TestConfiguration_OverrideDeviceCommands_MetricsInfoArgs(t *testing.T) {
require.Equal(t, "--info --json", testConfig.GetCommandMetricsInfoArgs("/dev/sdb"))
//require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Commands: {MetricsInfoArgs: "--info --json -T "}}}, scanOverrides)
}
func TestConfiguration_DeviceAllowList(t *testing.T) {
t.Parallel()
t.Run("present", func(t *testing.T) {
testConfig, err := config.Create()
require.NoError(t, err)
require.NoError(t, testConfig.ReadConfig(path.Join("testdata", "allow_listed_devices_present.yaml")))
require.True(t, testConfig.IsAllowlistedDevice("/dev/sda"), "/dev/sda should be allow listed")
require.False(t, testConfig.IsAllowlistedDevice("/dev/sdc"), "/dev/sda should not be allow listed")
})
t.Run("missing", func(t *testing.T) {
testConfig, err := config.Create()
require.NoError(t, err)
// Really just any other config where the key is full missing
require.NoError(t, testConfig.ReadConfig(path.Join("testdata", "override_device_commands.yaml")))
// Anything should be allow listed if the key isnt there
require.True(t, testConfig.IsAllowlistedDevice("/dev/sda"), "/dev/sda should be allow listed")
require.True(t, testConfig.IsAllowlistedDevice("/dev/sdc"), "/dev/sda should be allow listed")
})
}
+2
View File
@@ -25,4 +25,6 @@ type Interface interface {
GetDeviceOverrides() []models.ScanOverride
GetCommandMetricsInfoArgs(deviceName string) string
GetCommandMetricsSmartArgs(deviceName string) string
IsAllowlistedDevice(deviceName string) bool
}
+14
View File
@@ -175,6 +175,20 @@ func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
}
// IsAllowlistedDevice mocks base method.
func (m *MockInterface) IsAllowlistedDevice(deviceName string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsAllowlistedDevice", deviceName)
ret0, _ := ret[0].(bool)
return ret0
}
// IsAllowlistedDevice indicates an expected call of IsAllowlistedDevice.
func (mr *MockInterfaceMockRecorder) IsAllowlistedDevice(deviceName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAllowlistedDevice", reflect.TypeOf((*MockInterface)(nil).IsAllowlistedDevice), deviceName)
}
// IsSet mocks base method.
func (m *MockInterface) IsSet(key string) bool {
m.ctrl.T.Helper()
@@ -0,0 +1,3 @@
allow_listed_devices:
- /dev/sda
- /dev/sdb
+11 -4
View File
@@ -3,13 +3,14 @@ package detect
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/config"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/sirupsen/logrus"
"os"
"strings"
)
type Detect struct {
@@ -47,7 +48,7 @@ func (d *Detect) SmartctlScan() ([]models.Device, error) {
return detectedDevices, nil
}
//updates a device model with information from smartctl --scan
// updates a device model with information from smartctl --scan
// It has a couple of issues however:
// - 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.
@@ -81,8 +82,9 @@ func (d *Detect) SmartCtlInfo(device *models.Device) error {
device.SerialNumber = availableDeviceInfo.SerialNumber
device.Firmware = availableDeviceInfo.FirmwareVersion
device.RotationSpeed = availableDeviceInfo.RotationRate
device.Capacity = availableDeviceInfo.UserCapacity.Bytes
device.Capacity = availableDeviceInfo.Capacity()
device.FormFactor = availableDeviceInfo.FormFactor.Name
device.DeviceType = availableDeviceInfo.Device.Type
device.DeviceProtocol = availableDeviceInfo.Device.Protocol
if len(availableDeviceInfo.Vendor) > 0 {
device.Manufacturer = availableDeviceInfo.Vendor
@@ -122,6 +124,11 @@ func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []mod
deviceFile := strings.ToLower(scannedDevice.Name)
// If the user has defined a device allow list, and this device isnt there, then ignore it
if !d.Config.IsAllowlistedDevice(deviceFile) {
continue
}
detectedDevice := models.Device{
HostId: d.Config.GetString("host.id"),
DeviceType: scannedDevice.Type,
+147 -36
View File
@@ -1,19 +1,22 @@
package detect_test
import (
"os"
"strings"
"testing"
mock_shell "github.com/analogj/scrutiny/collector/pkg/common/shell/mock"
mock_config "github.com/analogj/scrutiny/collector/pkg/config/mock"
"github.com/analogj/scrutiny/collector/pkg/detect"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io/ioutil"
"testing"
)
func TestDetect_SmartctlScan(t *testing.T) {
//setup
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
@@ -21,9 +24,10 @@ func TestDetect_SmartctlScan(t *testing.T) {
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_simple.json")
testScanResults, err := os.ReadFile("testdata/smartctl_scan_simple.json")
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
d := detect.Detect{
@@ -32,17 +36,17 @@ func TestDetect_SmartctlScan(t *testing.T) {
Config: fakeConfig,
}
//test
// test
scannedDevices, err := d.SmartctlScan()
//assert
// assert
require.NoError(t, err)
require.Equal(t, 7, len(scannedDevices))
require.Equal(t, "scsi", scannedDevices[0].DeviceType)
}
func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
//setup
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
@@ -50,9 +54,10 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_megaraid.json")
testScanResults, err := os.ReadFile("testdata/smartctl_scan_megaraid.json")
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
d := detect.Detect{
@@ -61,20 +66,20 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
Config: fakeConfig,
}
//test
// test
scannedDevices, err := d.SmartctlScan()
//assert
// assert
require.NoError(t, err)
require.Equal(t, 2, len(scannedDevices))
require.Equal(t, []models.Device{
models.Device{DeviceName: "bus/0", DeviceType: "megaraid,0"},
models.Device{DeviceName: "bus/0", DeviceType: "megaraid,1"},
{DeviceName: "bus/0", DeviceType: "megaraid,0"},
{DeviceName: "bus/0", DeviceType: "megaraid,1"},
}, scannedDevices)
}
func TestDetect_SmartctlScan_Nvme(t *testing.T) {
//setup
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
@@ -82,9 +87,10 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_nvme.json")
testScanResults, err := os.ReadFile("testdata/smartctl_scan_nvme.json")
fakeShell.EXPECT().Command(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(string(testScanResults), err)
d := detect.Detect{
@@ -93,19 +99,19 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
Config: fakeConfig,
}
//test
// test
scannedDevices, err := d.SmartctlScan()
//assert
// assert
require.NoError(t, err)
require.Equal(t, 1, len(scannedDevices))
require.Equal(t, []models.Device{
models.Device{DeviceName: "nvme0", DeviceType: "nvme"},
{DeviceName: "nvme0", DeviceType: "nvme"},
}, scannedDevices)
}
func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
//setup
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
@@ -113,6 +119,7 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
@@ -129,16 +136,16 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
Config: fakeConfig,
}
//test
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
//assert
// assert
require.Equal(t, "sda", transformedDevices[0].DeviceName)
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
}
func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
//setup
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
@@ -146,6 +153,7 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
@@ -162,21 +170,22 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
Config: fakeConfig,
}
//test
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
//assert
// assert
require.Equal(t, []models.Device{}, transformedDevices)
}
func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
//setup
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{
{
Device: "/dev/bus/0",
@@ -187,7 +196,8 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
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{
{
@@ -203,15 +213,15 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
Config: fakeConfig,
}
//test
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
//assert
// assert
require.Equal(t, 12, len(transformedDevices))
}
func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
//setup
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
@@ -219,6 +229,7 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
@@ -234,17 +245,17 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
Config: fakeConfig,
}
//test
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
//assert
// assert
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "sat+megaraid", transformedDevices[0].DeviceType)
}
// test https://github.com/AnalogJ/scrutiny/issues/255#issuecomment-1164024126
func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T) {
//setup
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
@@ -252,6 +263,7 @@ func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T)
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda"}})
fakeConfig.EXPECT().IsAllowlistedDevice(gomock.Any()).AnyTimes().Return(true)
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
@@ -267,16 +279,16 @@ func TestDetect_TransformDetectedDevices_WithoutDeviceTypeOverride(t *testing.T)
Config: fakeConfig,
}
//test
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
//assert
// assert
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "scsi", transformedDevices[0].DeviceType)
}
func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
//setup
// setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
@@ -290,10 +302,109 @@ func TestDetect_TransformDetectedDevices_WhenDeviceNotDetected(t *testing.T) {
Config: fakeConfig,
}
//test
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
//assert
// assert
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "ata", transformedDevices[0].DeviceType)
}
func TestDetect_TransformDetectedDevices_AllowListFilters(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetString("commands.metrics_smartctl_bin").AnyTimes().Return("smartctl")
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
fakeConfig.EXPECT().IsAllowlistedDevice("/dev/sda").Return(true)
fakeConfig.EXPECT().IsAllowlistedDevice("/dev/sdb").Return(false)
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
Name: "/dev/sda",
InfoName: "/dev/sda",
Protocol: "ata",
Type: "ata",
},
{
Name: "/dev/sdb",
InfoName: "/dev/sdb",
Protocol: "ata",
Type: "ata",
},
},
}
d := detect.Detect{
Config: fakeConfig,
}
// test
transformedDevices := d.TransformDetectedDevices(detectedDevices)
// assert
require.Equal(t, 1, len(transformedDevices))
require.Equal(t, "sda", transformedDevices[0].DeviceName)
}
func TestDetect_SmartCtlInfo(t *testing.T) {
t.Run("should report nvme info", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
const (
someArgs = "--info --json"
// device info
someDeviceName = "some-device-name"
someModelName = "KCD61LUL3T84"
someSerialNumber = "61Q0A05UT7B8"
someFirmware = "8002"
someDeviceProtocol = "NVMe"
someDeviceType = "nvme"
someCapacity int64 = 3840755982336
)
fakeConfig := mock_config.NewMockInterface(ctrl)
fakeConfig.EXPECT().
GetCommandMetricsInfoArgs("/dev/" + someDeviceName).
Return(someArgs)
fakeConfig.EXPECT().
GetString("commands.metrics_smartctl_bin").
Return("smartctl")
someLogger := logrus.WithFields(logrus.Fields{})
smartctlInfoResults, err := os.ReadFile("testdata/smartctl_info_nvme.json")
require.NoError(t, err)
fakeShell := mock_shell.NewMockInterface(ctrl)
fakeShell.EXPECT().
Command(someLogger, "smartctl", append(strings.Split(someArgs, " "), "/dev/"+someDeviceName), "", gomock.Any()).
Return(string(smartctlInfoResults), err)
d := detect.Detect{
Logger: someLogger,
Shell: fakeShell,
Config: fakeConfig,
}
someDevice := &models.Device{
WWN: "some wwn",
DeviceName: someDeviceName,
}
require.NoError(t, d.SmartCtlInfo(someDevice))
assert.Equal(t, someDeviceName, someDevice.DeviceName)
assert.Equal(t, someModelName, someDevice.ModelName)
assert.Equal(t, someSerialNumber, someDevice.SerialNumber)
assert.Equal(t, someFirmware, someDevice.Firmware)
assert.Equal(t, someDeviceProtocol, someDevice.DeviceProtocol)
assert.Equal(t, someDeviceType, someDevice.DeviceType)
assert.Equal(t, someCapacity, someDevice.Capacity)
})
}
+48
View File
@@ -0,0 +1,48 @@
{
"json_format_version": [
1,
0
],
"smartctl": {
"version": [
7,
2
],
"svn_revision": "5155",
"platform_info": "x86_64-linux-6.1.69-talos",
"build_info": "(local build)",
"argv": [
"smartctl",
"--info",
"--json",
"/dev/nvme4"
],
"exit_status": 0
},
"device": {
"name": "/dev/nvme4",
"info_name": "/dev/nvme4",
"type": "nvme",
"protocol": "NVMe"
},
"model_name": "KCD61LUL3T84",
"serial_number": "61Q0A05UT7B8",
"firmware_version": "8002",
"nvme_pci_vendor": {
"id": 7695,
"subsystem_id": 7695
},
"nvme_ieee_oui_identifier": 9233294,
"nvme_total_capacity": 3840755982336,
"nvme_unallocated_capacity": 0,
"nvme_controller_id": 1,
"nvme_version": {
"string": "1.4",
"value": 66560
},
"nvme_number_of_namespaces": 16,
"local_time": {
"time_t": 1706045146,
"asctime": "Tue Jan 23 21:25:46 2024 UTC"
}
}
+52 -25
View File
@@ -1,50 +1,77 @@
# syntax=docker/dockerfile:1.4
########################################################################################################################
# Omnibus Image
# NOTE: this image requires the `make binary-frontend` target to have been run before `docker build` The `dist` directory must exist.
########################################################################################################################
######## Build the frontend
FROM --platform=${BUILDPLATFORM} node AS frontendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY --link . /go/src/github.com/analogj/scrutiny
########
FROM golang:1.20-bullseye as backendbuild
RUN make binary-frontend
######## Build the backend
FROM golang:1.20-bookworm as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY . /go/src/github.com/analogj/scrutiny
COPY --link . /go/src/github.com/analogj/scrutiny
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends \
file \
&& rm -rf /var/lib/apt/lists/*
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
########
FROM debian:bullseye-slim as runtime
######## Combine build artifacts in runtime image
FROM debian:bookworm-slim as runtime
ARG TARGETARCH
EXPOSE 8080
WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}"
ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb
ENV S6VER="3.1.6.2"
ENV INFLUXVER="2.2.0"
ENV S6_SERVICES_READYTIME=1000
SHELL ["/usr/bin/sh", "-c"]
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl tzdata \
RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends \
ca-certificates \
cron \
curl \
smartmontools \
tzdata \
procps \
xz-utils \
&& rm -rf /var/lib/apt/lists/* \
&& update-ca-certificates \
&& case ${TARGETARCH} in \
"amd64") S6_ARCH=amd64 ;; \
"arm64") S6_ARCH=aarch64 ;; \
esac \
&& curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \
&& curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \
&& dpkg -i --force-all /tmp/influxdb2-2.2.0-${TARGETARCH}.deb
&& case ${TARGETARCH} in \
"amd64") S6_ARCH=x86_64 ;; \
"arm64") S6_ARCH=aarch64 ;; \
esac \
&& curl https://github.com/just-containers/s6-overlay/releases/download/v${S6VER}/s6-overlay-noarch.tar.xz -L -s --output /tmp/s6-overlay-noarch.tar.xz \
&& tar -Jxpf /tmp/s6-overlay-noarch.tar.xz -C / \
&& rm -rf /tmp/s6-overlay-noarch.tar.xz \
&& curl https://github.com/just-containers/s6-overlay/releases/download/v${S6VER}/s6-overlay-${S6_ARCH}.tar.xz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.xz \
&& tar -Jxpf /tmp/s6-overlay-${S6_ARCH}.tar.xz -C / \
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.xz
RUN curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-${INFLUXVER}-${TARGETARCH}.deb --output /tmp/influxdb2-${INFLUXVER}-${TARGETARCH}.deb \
&& dpkg -i --force-all /tmp/influxdb2-${INFLUXVER}-${TARGETARCH}.deb \
&& rm -rf /tmp/influxdb2-${INFLUXVER}-${TARGETARCH}.deb
COPY /rootfs /
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
COPY dist /opt/scrutiny/web
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics && \
chmod 0644 /etc/cron.d/scrutiny && \
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
COPY --link --from=frontendbuild --chmod=644 /go/src/github.com/analogj/scrutiny/dist /opt/scrutiny/web
RUN chmod 0644 /etc/cron.d/scrutiny && \
rm -f /etc/cron.daily/* && \
mkdir -p /opt/scrutiny/web && \
mkdir -p /opt/scrutiny/config && \
chmod -R ugo+rwx /opt/scrutiny/config
chmod -R ugo+rwx /opt/scrutiny/config && \
chmod +x /etc/cont-init.d/* && \
chmod +x /etc/services.d/*/run && \
chmod +x /etc/services.d/*/finish
CMD ["/init"]
+4 -3
View File
@@ -4,20 +4,21 @@
########
FROM golang:1.20-bullseye as backendbuild
FROM golang:1.20-bookworm as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY . /go/src/github.com/analogj/scrutiny
RUN apt-get update && apt-get install -y file && rm -rf /var/lib/apt/lists/*
RUN make binary-clean binary-collector
########
FROM debian:bullseye-slim as runtime
FROM debian:bookworm-slim as runtime
WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}"
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
+19 -13
View File
@@ -1,31 +1,37 @@
# syntax=docker/dockerfile:1.4
########################################################################################################################
# Web Image
# NOTE: this image requires the `make binary-frontend` target to have been run before `docker build` The `dist` directory must exist.
########################################################################################################################
######## Build the frontend
FROM --platform=${BUILDPLATFORM} node AS frontendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY --link . /go/src/github.com/analogj/scrutiny
########
FROM golang:1.20-bullseye as backendbuild
RUN make binary-frontend
######## Build the backend
FROM golang:1.20-bookworm as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
COPY --link . /go/src/github.com/analogj/scrutiny
COPY . /go/src/github.com/analogj/scrutiny
RUN apt-get update && apt-get install -y file && rm -rf /var/lib/apt/lists/*
RUN make binary-clean binary-all WEB_BINARY_NAME=scrutiny
########
FROM debian:bullseye-slim as runtime
######## Combine build artifacts in runtime image
FROM debian:bookworm-slim as runtime
EXPOSE 8080
WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}"
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && update-ca-certificates
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && rm -rf /var/lib/apt/lists/* && update-ca-certificates
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
COPY dist /opt/scrutiny/web
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
mkdir -p /opt/scrutiny/web && \
COPY --link --from=backendbuild --chmod=755 /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
COPY --link --from=frontendbuild --chmod=644 /go/src/github.com/analogj/scrutiny/dist /opt/scrutiny/web
RUN mkdir -p /opt/scrutiny/web && \
mkdir -p /opt/scrutiny/config && \
chmod -R ugo+rwx /opt/scrutiny/config
chmod -R a+rX /opt/scrutiny && \
chmod -R a+w /opt/scrutiny/config
CMD ["/opt/scrutiny/bin/scrutiny", "start"]
+1 -1
View File
@@ -25,4 +25,4 @@ fi
# now that we have the env start cron in the foreground
echo "starting cron"
su -c "cron -f -L 15" root
exec su -c "cron -f -L 15" root
+8 -1
View File
@@ -2,6 +2,7 @@ version: '2.4'
services:
influxdb:
restart: unless-stopped
image: influxdb:2.2
ports:
- '8086:8086'
@@ -15,6 +16,7 @@ services:
web:
restart: unless-stopped
image: 'ghcr.io/analogj/scrutiny:master-web'
ports:
- '8080:8080'
@@ -33,6 +35,7 @@ services:
start_period: 10s
collector:
restart: unless-stopped
image: 'ghcr.io/analogj/scrutiny:master-collector'
cap_add:
- SYS_RAWIO
@@ -40,9 +43,13 @@ services:
- '/run/udev:/run/udev:ro'
environment:
COLLECTOR_API_ENDPOINT: 'http://web:8080'
COLLECTOR_HOST_ID: 'scrutiny-collector-hostname'
# If true forces the collector to run on startup (cron will be started after the collector completes)
# see: https://github.com/AnalogJ/scrutiny/blob/master/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md#collector-trigger-on-startup
COLLECTOR_RUN_STARTUP: false
depends_on:
web:
condition: service_healthy
devices:
- "/dev/sda"
- "/dev/sdb"
- "/dev/sdb"
@@ -2,6 +2,7 @@ version: '3.5'
services:
scrutiny:
restart: unless-stopped
container_name: scrutiny
image: ghcr.io/analogj/scrutiny:master-omnibus
cap_add:
+5 -4
View File
@@ -59,6 +59,7 @@ networks:
services:
influxdb:
restart: unless-stopped
container_name: influxdb
image: influxdb:2.1-alpine
ports:
@@ -73,11 +74,11 @@ services:
- DOCKER_INFLUXDB_INIT_ORG=homelab
- DOCKER_INFLUXDB_INIT_BUCKET=scrutiny
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=your-very-secret-token
restart: unless-stopped
networks:
- monitoring
scrutiny:
restart: unless-stopped
container_name: scrutiny
image: ghcr.io/analogj/scrutiny:master-web
ports:
@@ -91,10 +92,9 @@ services:
- SCRUTINY_WEB_INFLUXDB_ORG=homelab
- SCRUTINY_WEB_INFLUXDB_BUCKET=scrutiny
# Optional but highly recommended to notify you in case of a problem
- SCRUTINY_WEB_NOTIFY_URLS=["http://gotify:80/message?token=a-gotify-token"]
- SCRUTINY_NOTIFY_URLS=["http://gotify:80/message?token=a-gotify-token"]
depends_on:
- influxdb
restart: unless-stopped
networks:
- notifications
- monitoring
@@ -121,7 +121,7 @@ apt install smartmontools -y
# 3. Make it exacutable
# 4. List the contents of the library for confirmation
mkdir -p /opt/scrutiny/bin && \
curl -L https://github.com/AnalogJ/scrutiny/releases/download/v0.5.0/scrutiny-collector-metrics-linux-amd64 > /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 && \
curl -L https://github.com/AnalogJ/scrutiny/releases/download/v0.8.1/scrutiny-collector-metrics-linux-amd64 > /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 && \
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics-linux-amd64 && \
ls -lha /opt/scrutiny/bin
```
@@ -168,6 +168,7 @@ version: "3.4"
services:
collector:
restart: unless-stopped
image: 'ghcr.io/analogj/scrutiny:master-collector'
cap_add:
- SYS_RAWIO
+2 -1
View File
@@ -9,7 +9,8 @@ in `docs/guides/` or elsewhere) it will be linked here.
- [ ] Proxmox
- [x] Synology
- [Hub/Spoke Deployment - Collector](./INSTALL_SYNOLOGY_COLLECTOR.md)
- [Omnibus Deployment](https://drfrankenstein.co.uk/2022/07/28/scrutiny-in-docker-on-a-synology-nas)
- [Omnibus Deployment](https://drfrankenstein.co.uk/2022/07/28/scrutiny-in-docker-on-a-synology-nas) - Docker Package
- [Omnibus Deployment](https://drfrankenstein.co.uk/scrutiny-in-container-manager-on-a-synology-nas/) - Container Manager Package
- [ ] OMV
- [ ] Amahi
- [ ] Running in a LXC container
+2 -2
View File
@@ -8,7 +8,7 @@ Thankfully the following users have been gracious enough to test/validate Scruti
| Architecture Name | Binaries | Docker |
| --- | --- | --- |
| linux-amd64 | -- | @feroxy @rshxyz |
| linux-amd64 | @TizzAmmazz | @feroxy @rshxyz |
| linux-arm-5 | -- | |
| linux-arm-6 | -- | |
| linux-arm-7 | @Zorlin | @martini1992 |
@@ -17,4 +17,4 @@ Thankfully the following users have been gracious enough to test/validate Scruti
| macos-amd64 | -- | -- |
| macos-arm64 | -- | -- |
| windows-amd64 | @gabrielv33 | -- |
| windows-arm64 | -- | -- |
| windows-arm64 | -- | -- |
+11 -1
View File
@@ -250,8 +250,9 @@ UPDATE devices SET device_status = null;
### Seagate Drives Failing
As thoroughly discussed in [#255](https://github.com/AnalogJ/scrutiny/issues/255), Seagate (Ironwolf & others) drives are almost always marked as failed by Scrutiny.
As thoroughly discussed in [#255](https://github.com/AnalogJ/scrutiny/issues/255) and [#522](https://github.com/AnalogJ/scrutiny/issues/522), Seagate (Ironwolf & others) drives are almost always marked as failed by Scrutiny.
#### Seek Error Rate & Read Error Rate (#255)
> The `Seek Error Rate` & `Read Error Rate` attribute raw values are typically very high, and the
> normalised values (Current / Worst / Threshold) are usually quite low. Despite this, the numbers in most cases are perfectly OK
>
@@ -286,6 +287,15 @@ other drives, please read the following:
- http://www.users.on.net/~fzabkar/HDD/Seagate_SER_RRER_HEC.html
- https://www.truenas.com/community/threads/seagate-ironwolf-smart-test-raw_read_error_rate-seek_error_rate.68634/
#### Seagate Raw Values are incorrect (#522)
Basically Seagate drives are known to use a custom data format for a number of their SMART attributes. This causes issues with Scrutiny's threshold analysis.
- The workaround is to customize the smartctl command that Scrutiny uses for your drive by [creating a collector.yaml file](https://github.com/AnalogJ/scrutiny/issues/522#issuecomment-1766727988) and "fixing" each attribute
- The **real "fix"** is to make sure your Seagate drive is correctly supported by smartmontools. See this [PR](https://github.com/smartmontools/smartmontools/pull/247)
Sorry for the bad news, but this is a known issue and there's just not much we can do on the Scrutiny side.
## Hub & Spoke model, with multiple Hosts.
![multiple-host-ids image](multiple-host-ids.png)
+8
View File
@@ -17,3 +17,11 @@ So changing from `master-omnibus -> latest` will be the same thing for all inten
> NOTE: Previously, there was a `automated cron build` that ran on the `master` and `beta` branches.
They used to trigger a `nightly` build, even if nothing has changed on the branch. This has a couple of benefits, but one is to
ensure that there's no broken external dependencies in our (unchanged) code. This `nightly` build no longer updates the `master-omnibus` tag.
# Running Docker `rootless`
To avoid that the container(s) restart when you installed Docker as `rootless` you need to isssue the following commands to allow the session to stay alive even after you close your (SSH) sesssion:
`sudo loginctl enable-linger $(whoami)`
`systemctl --user enable docker`
+26
View File
@@ -11,6 +11,32 @@ dependency. It's a dedicated timeseries database, as opposed to the general purp
a bunch of testing and analysis before I made the change. With InfluxDB the memory footprint for Scrutiny (at idle) is ~
100mb, which is still fairly reasonable.
### Data Size
It's surprisingly easy to reach extremely large database sizes, if you don't use downsampling, or you downsample incorrectly.
The growth rate is pretty unintuitive -- see https://github.com/AnalogJ/scrutiny/issues/650#issuecomment-2365174940
> Fasten stores the SMART metrics in a timeseries database (InfluxDB), and automatically downsamples the data on a schedule.
>
> The expectation was that cron would run daily, and there would be:
>
> - 7 daily data points
> - 3 weekly data points
> - 11 monthly data points
> - and infinite yearly data points.
>
> These data points would be for each SMART metric, for each device.
> eg. in one year, (7+3+11)*80ish SMART attributes = 1680 datapoints for one device
>
> If you're running cron every 15 minutes, your browser will instead be attempting to display:
>
> - 96*7 daily data points
> - 3 weekly
> - 11 monthly
>
> so (96*7 + 3 + 11)*80 = 54,880 datapoints for each device 😭
## Installation
InfluxDB is a required dependency for Scrutiny v0.4.0+.
+16 -1
View File
@@ -24,8 +24,23 @@ SCRUTINY_MESSAGE - eg. "Scrutiny SMART error notification for device: %s\nFailur
SCRUTINY_HOST_ID - (optional) eg. "my-custom-host-id"
```
# Special Characters
`Shoutrrr` supports special characters in the username and password fields, however you'll need to url-encode the
username and the password separately.
- if your username is: `myname@example.com`
- if your password is `124@34$1`
Then your `shoutrrr` url will look something like:
- `smtp://myname%40example%2Ecom:124%4034%241@ms.my.domain.com:587`
# Testing Notifications
You can test that your notifications are configured correctly by posting an empty payload to the notifications health check API.
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
```
+34 -1
View File
@@ -103,4 +103,37 @@ You may also configure these values using the following environmental variables
- "9090:9090"
```
3. run `docker-compose up`
4. visit [http://localhost:9090/custom/web](http://localhost:9090/custom/web) - access the scrutiny container via caddy reverse proxy
4. visit [http://localhost:9090/custom/web](http://localhost:9090/custom/web) - access the scrutiny container via caddy reverse proxy
## Traefik
Assuming, that you have Traefik up and running with [AutoDiscovery Using Traefik For Docker ](https://doc.traefik.io/traefik/providers/docker/),
here is an example of a `docker-compose.yml` file, with labels to enable Traefik reverse proxy and basic auth
```yaml
version: '3.5'
services:
scrutiny:
container_name: scrutiny
image: ghcr.io/analogj/scrutiny:master-omnibus
cap_add:
- SYS_RAWIO
- SYS_ADMIN
volumes:
- /run/udev:/run/udev:ro
- ./config:/opt/scrutiny/config
- ./influxdb:/opt/scrutiny/influxdb
labels:
- traefik.enable=true
- traefik.http.routers.scrutiny.rule=Host(`example.com`)
- traefik.http.services.scrutiny.loadbalancer.server.port=8080
# 2 labels below are optional, in case you want basic auth in Traefik:
- traefik.http.routers.scrutiny.middlewares=auth
- "traefik.http.middlewares.auth.basicauth.users=user:$$2y$$05$$G11Wm/dlWpXHENK..m8se.zxvaE8USJBp1Ws56sSCrOcwWDjsYHni"
# Note: when used in docker-compose.yml all dollar signs in the hash need to be doubled for escaping.
# To create user:password pair, it's possible to use this command:
# echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g
devices:
- "/dev/sda"
- "/dev/sdb"
- "/dev/nvme0"
```
+1
View File
@@ -2,6 +2,7 @@
// SQLite Table(s)
Table Device {
Archived bool
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time
UpdatedAt time
+1
View File
@@ -81,6 +81,7 @@ devices:
# metrics_scan_args: '--scan --json' # used to detect devices
# metrics_info_args: '--info --json' # used to determine device unique ID & register device with Scrutiny
# metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.
# metrics_smartctl_wait: 0 # time to wait in seconds between each disk's check
########################################################################################################################
+6 -1
View File
@@ -60,10 +60,14 @@ log:
# Notification "urls" look like the following. For more information about service specific configuration see
# Shoutrrr's documentation: https://containrrr.dev/shoutrrr/services/overview/
#
# note, usernames and passwords containing special characters will need to be urlencoded.
# if your username is: "myname@example.com" and your password is "124@34$1"
# your shoutrrr url will look like: "smtp://myname%40example%2Ecom:124%4034%241@ms.my.domain.com:587"
#notify:
# urls:
# - "discord://token@channel"
# - "discord://token@webhookid"
# - "telegram://token@telegram?channels=channel-1[,channel-2,...]"
# - "pushover://shoutrrr:apiToken@userKey/?priority=1&devices=device1[,device2, ...]"
# - "slack://[botname@]token-a/token-b/token-c"
@@ -73,6 +77,7 @@ log:
# - "pushbullet://api-token[/device/#channel/email]"
# - "ifttt://key/?events=event1[,event2,...]&value1=value1&value2=value2&value3=value3"
# - "mattermost://[username@]mattermost-host/token[/channel]"
# - "ntfy://username:password@host:port/topic"
# - "hangouts://chat.googleapis.com/v1/spaces/FOO/messages?key=bar&token=baz"
# - "zulip://bot-mail:bot-key@zulip-domain/?stream=name-or-id&topic=name"
# - "join://shoutrrr:api-key@join/?devices=device1[,device2, ...][&icon=icon][&title=title]"
+29 -29
View File
@@ -4,30 +4,30 @@ go 1.20
require (
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
github.com/containrrr/shoutrrr v0.6.1
github.com/fatih/color v1.10.0
github.com/containrrr/shoutrrr v0.8.0
github.com/fatih/color v1.15.0
github.com/gin-gonic/gin v1.6.3
github.com/glebarez/sqlite v1.4.5
github.com/go-gormigrate/gormigrate/v2 v2.0.0
github.com/golang/mock v1.4.3
github.com/golang/mock v1.6.0
github.com/influxdata/influxdb-client-go/v2 v2.9.0
github.com/jaypipes/ghw v0.6.1
github.com/mitchellh/mapstructure v1.2.2
github.com/mitchellh/mapstructure v1.5.0
github.com/samber/lo v1.25.0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.7.1
github.com/sirupsen/logrus v1.6.0
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.1
github.com/urfave/cli/v2 v2.2.0
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/sync v0.1.0
gorm.io/gorm v1.23.5
)
require (
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.8.2 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.17.2 // indirect
@@ -35,44 +35,44 @@ require (
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.2.0 // indirect
github.com/golang/protobuf v1.4.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/jaypipes/pcidb v0.5.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pelletier/go-toml v1.7.0 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
golang.org/x/text v0.3.5 // indirect
google.golang.org/protobuf v1.23.0 // indirect
gopkg.in/ini.v1 v1.55.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
modernc.org/libc v1.16.8 // indirect
modernc.org/mathutil v1.4.1 // indirect
+331 -192
View File
@@ -3,49 +3,63 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
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/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/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 h1:wsrSjiqQtseStRIoLLxS4C5IEtXkazZVEPDHq8jW7r8=
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14/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/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-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/containrrr/shoutrrr v0.6.1 h1:6ih7jA6mo3t6C97MZbd3SxL/kRizOE3bI9CpBQZ6wzg=
github.com/containrrr/shoutrrr v0.6.1/go.mod h1:ye9jGX5YzMnJ76waaNVWlJ4luhMEyt1EWU5unYTQSb0=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -56,14 +70,18 @@ github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRk
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzqk8QCaRC4os14xoKDdbHqqlJtJA0oc1ZAjg=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
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/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -77,11 +95,11 @@ github.com/glebarez/sqlite v1.4.5 h1:oaJupO4X9iTn4sXRvP5Vs15BNvKh9dx5AQfciKlDvV4
github.com/glebarez/sqlite v1.4.5/go.mod h1:6D+bB+DdXlEC4mO+pUFJWixVcnrHTIAJ9U6Ynnn4Lxk=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gormigrate/gormigrate/v2 v2.0.0 h1:e2A3Uznk4viUC4UuemuVgsNnvYZyOA8B3awlYk3UioU=
github.com/go-gormigrate/gormigrate/v2 v2.0.0/go.mod h1:YuVJ+D/dNt4HWrThTBnjgZuRbt7AuwINeg4q52ZE3Jw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
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-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@@ -97,29 +115,40 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
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-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -127,47 +156,43 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/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/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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb-client-go/v2 v2.9.0 h1:1Ejxpt+cpWkadefxd5xvVx7pFgFaafdNp1ItfHzKRW4=
github.com/influxdata/influxdb-client-go/v2 v2.9.0/go.mod h1:x7Jo5UHHl+w8wu8UnGiNobDDHygojXwJX4mx7rXGKMk=
@@ -223,8 +248,7 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
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.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
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/pcidb v0.5.0 h1:4W5gZ+G7QxydevI8/MmmKdnIPJpURqJ2JNXTzfLxF5c=
@@ -237,22 +261,20 @@ github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -267,140 +289,103 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.6 h1:11TGpSHY7Esh/i/qnq02Jo5oVrI1Gue8Slbq0ujPZFQ=
github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samber/lo v1.25.0 h1:H8F6cB0RotRdgcRCivTByAQePaYhGMdOTJIj2QFS2I0=
github.com/samber/lo v1.25.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
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/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
@@ -409,13 +394,18 @@ github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@@ -424,8 +414,6 @@ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKY
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
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-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-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
@@ -437,13 +425,21 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
@@ -455,54 +451,82 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-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.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -513,50 +537,76 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 h1:D1v9ucDTYBtbz5vNuBbAhIMAGhQhJ6Ym5ah3maMVNX4=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@@ -566,26 +616,74 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -595,41 +693,79 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.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.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw=
gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw=
gorm.io/driver/postgres v1.0.0 h1:Yh4jyFQ0a7F+JPU0Gtiam/eKmpT/XFc1FKxotGqc6FM=
@@ -645,7 +781,10 @@ gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/with-contenv bash
#!/command/with-contenv bash
if [ -n "${TZ}" ]
then
+2 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/with-contenv bash
#!/command/with-contenv bash
# Cron runs in its own isolated environment (usually using only /etc/environment )
# So when the container starts up, we will do a dump of the runtime environment into a .env file that we
@@ -12,4 +12,4 @@ COLLECTOR_CRON_SCHEDULE=${COLLECTOR_CRON_SCHEDULE:-"0 0 * * *"}
[[ "${COLLECTOR_CRON_SCHEDULE}" == \"*\" || "${COLLECTOR_CRON_SCHEDULE}" == \'*\' ]] && COLLECTOR_CRON_SCHEDULE="${COLLECTOR_CRON_SCHEDULE:1:-1}"
# replace placeholder with correct value
sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny
sed -i 's|{COLLECTOR_CRON_SCHEDULE}|'"${COLLECTOR_CRON_SCHEDULE}"'|g' /etc/cron.d/scrutiny
+17 -5
View File
@@ -1,13 +1,25 @@
#!/usr/bin/with-contenv bash
#!/command/with-contenv bash
# ensure not run (successfully) before
if [ -f /tmp/custom-init-performed ]; then
echo 'INFO: custom init already performed'
s6-svc -D /run/service/collector-once # prevent s6 from restarting service
exit 0
fi
echo "waiting for scrutiny service to start"
s6-svwait -u /var/run/s6/services/scrutiny
#tell s6 to only run this script once
s6-svc -O /var/run/s6/services/collector-once
s6-svwait -u /run/service/scrutiny
# wait until scrutiny is "Ready"
until $(curl --output /dev/null --silent --head --fail http://localhost:8080/api/health); do echo "scrutiny api not ready" && sleep 5; done
echo "starting scrutiny collector (run-once mode. subsequent calls will be triggered via cron service)"
/opt/scrutiny/bin/scrutiny-collector-metrics run
# prevent script's core logic from running again
touch /tmp/custom-init-performed
# prevent s6 from restarting service
s6-svc -D /run/service/collector-once
exit 0
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/execlineb -S0
#!/command/execlineb -S0
echo "cron exiting"
s6-svscanctl -t /var/run/s6/services
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/with-contenv bash
#!/command/with-contenv bash
echo "starting cron"
cron -f -L 15
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/with-contenv bash
#!/command/with-contenv bash
mkdir -p /opt/scrutiny/influxdb/
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/with-contenv bash
#!/command/with-contenv bash
echo "waiting for influxdb"
until $(curl --output /dev/null --silent --head --fail http://localhost:8086/health); do echo "influxdb not ready" && sleep 5; done
+7 -1
View File
@@ -29,8 +29,14 @@ func main() {
os.Exit(1)
}
configFilePath := "/opt/scrutiny/config/scrutiny.yaml"
configFilePathAlternative := "/opt/scrutiny/config/scrutiny.yml"
if !utils.FileExists(configFilePath) && utils.FileExists(configFilePathAlternative) {
configFilePath = configFilePathAlternative
}
//we're going to load the config file manually, since we need to validate it.
err = config.ReadConfig("/opt/scrutiny/config/scrutiny.yaml") // Find and read the config file
err = config.ReadConfig(configFilePath) // Find and read the config file
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
//ignore "could not find config file"
} else if err != nil {
-12
View File
@@ -1,12 +0,0 @@
package database
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"sort"
)
func sortSmartMeasurementsDesc(smartResults []measurements.Smart) {
sort.SliceStable(smartResults, func(i, j int) bool {
return smartResults[i].Date.After(smartResults[j].Date)
})
}
@@ -1,30 +0,0 @@
package database
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func Test_sortSmartMeasurementsDesc_LatestFirst(t *testing.T) {
//setup
timeNow := time.Now()
smartResults := []measurements.Smart{
{
Date: timeNow.AddDate(0, 0, -2),
},
{
Date: timeNow,
},
{
Date: timeNow.AddDate(0, 0, -1),
},
}
//test
sortSmartMeasurementsDesc(smartResults)
//assert
require.Equal(t, smartResults[0].Date, timeNow)
}
+6 -2
View File
@@ -2,12 +2,15 @@ package database
import (
"context"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
)
// Create mock using:
// mockgen -source=webapp/backend/pkg/database/interface.go -destination=webapp/backend/pkg/database/mock/mock_database.go
type DeviceRepo interface {
Close() error
HealthCheck(ctx context.Context) error
@@ -17,12 +20,13 @@ type DeviceRepo interface {
UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error)
UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error)
GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error)
UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error
DeleteDevice(ctx context.Context, wwn string) error
SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error)
GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error)
GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error)
SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo) error
SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error)
@@ -5,6 +5,7 @@ import (
"time"
)
// Deprecated: m20220509170100.Device is deprecated, only used by db migrations
type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time.Time
@@ -14,9 +15,9 @@ type Device struct {
WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
@@ -38,4 +39,3 @@ type Device struct {
// Data set by Scrutiny
DeviceStatus pkg.DeviceStatus `json:"device_status"`
}
@@ -0,0 +1,41 @@
package m20250221084400
import (
"github.com/analogj/scrutiny/webapp/backend/pkg"
"time"
)
type Device struct {
Archived bool `json:"archived"`
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
InterfaceSpeed string `json:"interface_speed"`
SerialNumber string `json:"serial_number"`
Firmware string `json:"firmware"`
RotationSpeed int `json:"rotational_speed"`
Capacity int64 `json:"capacity"`
FormFactor string `json:"form_factor"`
SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`
// Data set by Scrutiny
DeviceStatus pkg.DeviceStatus `json:"device_status"`
}
@@ -0,0 +1,272 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: webapp/backend/pkg/database/interface.go
// Package mock_database is a generated GoMock package.
package mock_database
import (
context "context"
reflect "reflect"
pkg "github.com/analogj/scrutiny/webapp/backend/pkg"
models "github.com/analogj/scrutiny/webapp/backend/pkg/models"
collector "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
measurements "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
gomock "github.com/golang/mock/gomock"
)
// MockDeviceRepo is a mock of DeviceRepo interface.
type MockDeviceRepo struct {
ctrl *gomock.Controller
recorder *MockDeviceRepoMockRecorder
}
// MockDeviceRepoMockRecorder is the mock recorder for MockDeviceRepo.
type MockDeviceRepoMockRecorder struct {
mock *MockDeviceRepo
}
// NewMockDeviceRepo creates a new mock instance.
func NewMockDeviceRepo(ctrl *gomock.Controller) *MockDeviceRepo {
mock := &MockDeviceRepo{ctrl: ctrl}
mock.recorder = &MockDeviceRepoMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDeviceRepo) EXPECT() *MockDeviceRepoMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockDeviceRepo) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockDeviceRepoMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDeviceRepo)(nil).Close))
}
// UpdateDeviceArchived mocks base method.
func (m *MockDeviceRepo) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDeviceArchived", ctx, wwn)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateDeviceArchived indicates an expected call of UpdateDeviceArchived.
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceArchived(ctx, wwn, archived interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceArchived", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceArchived), ctx, wwn, archived)
}
// DeleteDevice mocks base method.
func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, wwn string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDevice", ctx, wwn)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteDevice indicates an expected call of DeleteDevice.
func (mr *MockDeviceRepoMockRecorder) DeleteDevice(ctx, wwn interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDevice", reflect.TypeOf((*MockDeviceRepo)(nil).DeleteDevice), ctx, wwn)
}
// GetDeviceDetails mocks base method.
func (m *MockDeviceRepo) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDeviceDetails", ctx, wwn)
ret0, _ := ret[0].(models.Device)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDeviceDetails indicates an expected call of GetDeviceDetails.
func (mr *MockDeviceRepoMockRecorder) GetDeviceDetails(ctx, wwn interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceDetails", reflect.TypeOf((*MockDeviceRepo)(nil).GetDeviceDetails), ctx, wwn)
}
// GetDevices mocks base method.
func (m *MockDeviceRepo) GetDevices(ctx context.Context) ([]models.Device, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDevices", ctx)
ret0, _ := ret[0].([]models.Device)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDevices indicates an expected call of GetDevices.
func (mr *MockDeviceRepoMockRecorder) GetDevices(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevices", reflect.TypeOf((*MockDeviceRepo)(nil).GetDevices), ctx)
}
// GetSmartAttributeHistory mocks base method.
func (m *MockDeviceRepo) GetSmartAttributeHistory(ctx context.Context, wwn, durationKey string, selectEntries, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSmartAttributeHistory", ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
ret0, _ := ret[0].([]measurements.Smart)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSmartAttributeHistory indicates an expected call of GetSmartAttributeHistory.
func (mr *MockDeviceRepoMockRecorder) GetSmartAttributeHistory(ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartAttributeHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartAttributeHistory), ctx, wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
}
// GetSmartTemperatureHistory mocks base method.
func (m *MockDeviceRepo) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSmartTemperatureHistory", ctx, durationKey)
ret0, _ := ret[0].(map[string][]measurements.SmartTemperature)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSmartTemperatureHistory indicates an expected call of GetSmartTemperatureHistory.
func (mr *MockDeviceRepoMockRecorder) GetSmartTemperatureHistory(ctx, durationKey interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartTemperatureHistory", reflect.TypeOf((*MockDeviceRepo)(nil).GetSmartTemperatureHistory), ctx, durationKey)
}
// GetSummary mocks base method.
func (m *MockDeviceRepo) GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSummary", ctx)
ret0, _ := ret[0].(map[string]*models.DeviceSummary)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSummary indicates an expected call of GetSummary.
func (mr *MockDeviceRepoMockRecorder) GetSummary(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSummary", reflect.TypeOf((*MockDeviceRepo)(nil).GetSummary), ctx)
}
// HealthCheck mocks base method.
func (m *MockDeviceRepo) HealthCheck(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockDeviceRepoMockRecorder) HealthCheck(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDeviceRepo)(nil).HealthCheck), ctx)
}
// LoadSettings mocks base method.
func (m *MockDeviceRepo) LoadSettings(ctx context.Context) (*models.Settings, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadSettings", ctx)
ret0, _ := ret[0].(*models.Settings)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadSettings indicates an expected call of LoadSettings.
func (mr *MockDeviceRepoMockRecorder) LoadSettings(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSettings", reflect.TypeOf((*MockDeviceRepo)(nil).LoadSettings), ctx)
}
// RegisterDevice mocks base method.
func (m *MockDeviceRepo) RegisterDevice(ctx context.Context, dev models.Device) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RegisterDevice", ctx, dev)
ret0, _ := ret[0].(error)
return ret0
}
// RegisterDevice indicates an expected call of RegisterDevice.
func (mr *MockDeviceRepoMockRecorder) RegisterDevice(ctx, dev interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterDevice", reflect.TypeOf((*MockDeviceRepo)(nil).RegisterDevice), ctx, dev)
}
// SaveSettings mocks base method.
func (m *MockDeviceRepo) SaveSettings(ctx context.Context, settings models.Settings) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveSettings", ctx, settings)
ret0, _ := ret[0].(error)
return ret0
}
// SaveSettings indicates an expected call of SaveSettings.
func (mr *MockDeviceRepoMockRecorder) SaveSettings(ctx, settings interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSettings", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSettings), ctx, settings)
}
// SaveSmartAttributes mocks base method.
func (m *MockDeviceRepo) SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveSmartAttributes", ctx, wwn, collectorSmartData)
ret0, _ := ret[0].(measurements.Smart)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SaveSmartAttributes indicates an expected call of SaveSmartAttributes.
func (mr *MockDeviceRepoMockRecorder) SaveSmartAttributes(ctx, wwn, collectorSmartData interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartAttributes", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartAttributes), ctx, wwn, collectorSmartData)
}
// SaveSmartTemperature mocks base method.
func (m *MockDeviceRepo) SaveSmartTemperature(ctx context.Context, wwn, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveSmartTemperature", ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory)
ret0, _ := ret[0].(error)
return ret0
}
// SaveSmartTemperature indicates an expected call of SaveSmartTemperature.
func (mr *MockDeviceRepoMockRecorder) SaveSmartTemperature(ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSmartTemperature", reflect.TypeOf((*MockDeviceRepo)(nil).SaveSmartTemperature), ctx, wwn, deviceProtocol, collectorSmartData, discardSCTTempHistory)
}
// UpdateDevice mocks base method.
func (m *MockDeviceRepo) UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDevice", ctx, wwn, collectorSmartData)
ret0, _ := ret[0].(models.Device)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateDevice indicates an expected call of UpdateDevice.
func (mr *MockDeviceRepoMockRecorder) UpdateDevice(ctx, wwn, collectorSmartData interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDevice", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDevice), ctx, wwn, collectorSmartData)
}
// UpdateDeviceStatus mocks base method.
func (m *MockDeviceRepo) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDeviceStatus", ctx, wwn, status)
ret0, _ := ret[0].(models.Device)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateDeviceStatus indicates an expected call of UpdateDeviceStatus.
func (mr *MockDeviceRepoMockRecorder) UpdateDeviceStatus(ctx, wwn, status interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceStatus", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceStatus), ctx, wwn, status)
}
@@ -14,7 +14,7 @@ import (
// Device
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//insert device into DB (and update specified columns if device is already registered)
// insert device into DB (and update specified columns if device is already registered)
// update device fields that may change: (DeviceType, HostID)
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
@@ -51,7 +51,7 @@ func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, coll
return device, sr.gormClient.Model(&device).Updates(device).Error
}
//Update Device Status
// Update Device Status
func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) {
var device models.Device
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
@@ -74,6 +74,16 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string)
return device, nil
}
// Update Device Archived State
func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error {
var device models.Device
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil {
return fmt.Errorf("Could not get device from DB: %v", err)
}
return sr.gormClient.Model(&device).Where("wwn = ?", wwn).Update("archived", archived).Error
}
func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) error {
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).Delete(&models.Device{}).Error; err != nil {
return err
@@ -3,13 +3,14 @@ package database
import (
"context"
"fmt"
"strings"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/influxdata/influxdb-client-go/v2/api"
log "github.com/sirupsen/logrus"
"strings"
"time"
)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -30,14 +31,17 @@ func (sr *scrutinyRepository) SaveSmartAttributes(ctx context.Context, wwn strin
}
// GetSmartAttributeHistory MUST return in sorted order, where newest entries are at the beginning of the list, and oldest are at the end.
func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error) {
// When selectEntries is > 0, only the most recent selectEntries database entries are returned, starting from the selectEntriesOffset entry.
// For example, with selectEntries = 5, selectEntries = 0, the most recent 5 are returned. With selectEntries = 3, selectEntries = 2, entries
// 2 to 4 are returned (2 being the third newest, since it is zero-indexed)
func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) ([]measurements.Smart, error) {
// Get SMartResults from InfluxDB
//TODO: change the filter startrange to a real number.
// Get parser flux query result
//appConfig.GetString("web.influxdb.bucket")
queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey)
queryStr := sr.aggregateSmartAttributesQuery(wwn, durationKey, selectEntries, selectEntriesOffset, attributes)
log.Infoln(queryStr)
smartResults := []measurements.Smart{}
@@ -65,9 +69,6 @@ func (sr *scrutinyRepository) GetSmartAttributeHistory(ctx context.Context, wwn
return nil, err
}
//we have to sort the smartResults again, because the `union` command will return multiple 'tables' and only sort the records in each table.
sortSmartMeasurementsDesc(smartResults)
return smartResults, nil
//if err := device.SquashHistory(); err != nil {
@@ -99,7 +100,7 @@ func (sr *scrutinyRepository) saveDatapoint(influxWriteApi api.WriteAPIBlocking,
return influxWriteApi.WritePoint(ctx, p)
}
func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string) string {
func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
/*
@@ -108,28 +109,34 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
|> range(start: -1w, stop: now())
|> filter(fn: (r) => r["_measurement"] == "smart" )
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|> tail(n: 10, offset: 0)
|> schema.fieldsAsCols()
monthData = from(bucket: "metrics_weekly")
|> range(start: -1mo, stop: -1w)
|> filter(fn: (r) => r["_measurement"] == "smart" )
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|> tail(n: 10, offset: 0)
|> schema.fieldsAsCols()
yearData = from(bucket: "metrics_monthly")
|> range(start: -1y, stop: -1mo)
|> filter(fn: (r) => r["_measurement"] == "smart" )
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|> tail(n: 10, offset: 0)
|> schema.fieldsAsCols()
foreverData = from(bucket: "metrics_yearly")
|> range(start: -10y, stop: -1y)
|> filter(fn: (r) => r["_measurement"] == "smart" )
|> filter(fn: (r) => r["device_wwn"] == "0x5000c5002df89099" )
|> tail(n: 10, offset: 0)
|> schema.fieldsAsCols()
union(tables: [weekData, monthData, yearData, foreverData])
|> sort(columns: ["_time"], desc: false)
|> group()
|> sort(columns: ["_time"], desc: true)
|> tail(n: 6, offset: 4)
|> yield(name: "last")
*/
@@ -140,34 +147,60 @@ func (sr *scrutinyRepository) aggregateSmartAttributesQuery(wwn string, duration
nestedDurationKeys := sr.lookupNestedDurationKeys(durationKey)
subQueryNames := []string{}
for _, nestedDurationKey := range nestedDurationKeys {
bucketName := sr.lookupBucketName(nestedDurationKey)
durationRange := sr.lookupDuration(nestedDurationKey)
subQueryNames = append(subQueryNames, fmt.Sprintf(`%sData`, nestedDurationKey))
partialQueryStr = append(partialQueryStr, []string{
fmt.Sprintf(`%sData = from(bucket: "%s")`, nestedDurationKey, bucketName),
fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]),
`|> filter(fn: (r) => r["_measurement"] == "smart" )`,
fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn),
"|> schema.fieldsAsCols()",
}...)
}
if len(subQueryNames) == 1 {
if len(nestedDurationKeys) == 1 {
//there's only one bucket being queried, no need to union, just aggregate the dataset and return
partialQueryStr = append(partialQueryStr, []string{
subQueryNames[0],
sr.generateSmartAttributesSubquery(wwn, nestedDurationKeys[0], selectEntries, selectEntriesOffset, attributes),
fmt.Sprintf(`%sData`, nestedDurationKeys[0]),
`|> sort(columns: ["_time"], desc: true)`,
`|> yield()`,
}...)
} else {
partialQueryStr = append(partialQueryStr, []string{
fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")),
`|> sort(columns: ["_time"], desc: false)`,
`|> yield(name: "last")`,
}...)
return strings.Join(partialQueryStr, "\n")
}
subQueries := []string{}
subQueryNames := []string{}
for _, nestedDurationKey := range nestedDurationKeys {
subQueryNames = append(subQueryNames, fmt.Sprintf(`%sData`, nestedDurationKey))
if selectEntries > 0 {
// We only need the last `n + offset` # of entries from each table to guarantee we can
// get the last `n` # of entries starting from `offset` of the union
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, selectEntries+selectEntriesOffset, 0, attributes))
} else {
subQueries = append(subQueries, sr.generateSmartAttributesSubquery(wwn, nestedDurationKey, 0, 0, attributes))
}
}
partialQueryStr = append(partialQueryStr, subQueries...)
partialQueryStr = append(partialQueryStr, []string{
fmt.Sprintf("union(tables: [%s])", strings.Join(subQueryNames, ", ")),
`|> group()`,
`|> sort(columns: ["_time"], desc: true)`,
}...)
if selectEntries > 0 {
partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, selectEntries, selectEntriesOffset))
}
partialQueryStr = append(partialQueryStr, `|> yield(name: "last")`)
return strings.Join(partialQueryStr, "\n")
}
func (sr *scrutinyRepository) generateSmartAttributesSubquery(wwn string, durationKey string, selectEntries int, selectEntriesOffset int, attributes []string) string {
bucketName := sr.lookupBucketName(durationKey)
durationRange := sr.lookupDuration(durationKey)
partialQueryStr := []string{
fmt.Sprintf(`%sData = from(bucket: "%s")`, durationKey, bucketName),
fmt.Sprintf(`|> range(start: %s, stop: %s)`, durationRange[0], durationRange[1]),
`|> filter(fn: (r) => r["_measurement"] == "smart" )`,
fmt.Sprintf(`|> filter(fn: (r) => r["device_wwn"] == "%s" )`, wwn),
}
partialQueryStr = append(partialQueryStr, `|> aggregateWindow(every: 1d, fn: last, createEmpty: false)`)
if selectEntries > 0 {
partialQueryStr = append(partialQueryStr, fmt.Sprintf(`|> tail(n: %d, offset: %d)`, selectEntries, selectEntriesOffset))
}
partialQueryStr = append(partialQueryStr, "|> schema.fieldsAsCols()")
return strings.Join(partialQueryStr, "\n")
}
@@ -4,11 +4,15 @@ import (
"context"
"errors"
"fmt"
"strconv"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220716214900"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20250221084400"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
@@ -17,8 +21,6 @@ import (
"github.com/influxdata/influxdb-client-go/v2/api/http"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"strconv"
"time"
)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -331,7 +333,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
SettingDataType: "string",
SettingValueString: "smooth",
},
{
SettingKeyName: "metrics.notify_level",
SettingKeyDescription: "Determines which device status will cause a notification (fail or warn)",
@@ -369,6 +370,60 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return tx.Create(&defaultSettings).Error
},
},
{
ID: "m20231123123300", // add repeat_notifications setting.
Migrate: func(tx *gorm.DB) error {
//add repeat_notifications setting default.
var defaultSettings = []m20220716214900.Setting{
{
SettingKeyName: "metrics.repeat_notifications",
SettingKeyDescription: "Whether to repeat all notifications or just when values change (true | false)",
SettingDataType: "bool",
SettingValueBool: true,
},
}
return tx.Create(&defaultSettings).Error
},
},
{
ID: "m20240722082740", // add powered_on_hours_unit setting.
Migrate: func(tx *gorm.DB) error {
//add powered_on_hours_unit setting default.
var defaultSettings = []m20220716214900.Setting{
{
SettingKeyName: "powered_on_hours_unit",
SettingKeyDescription: "Presentation format for device powered on time ('humanize' | 'device_hours')",
SettingDataType: "string",
SettingValueString: "humanize",
},
}
return tx.Create(&defaultSettings).Error
},
},
{
ID: "m20250221084400", // add archived to device data
Migrate: func(tx *gorm.DB) error {
//migrate the device database.
// adding column (archived)
return tx.AutoMigrate(m20250221084400.Device{})
},
},
{
ID: "m20260105083200", // add discard_sct_temp_history setting.
Migrate: func(tx *gorm.DB) error {
//add discard_sct_temp_history setting default.
var defaultSettings = []m20220716214900.Setting{
{
SettingKeyName: "collector.discard_sct_temp_history",
SettingKeyDescription: "Whether to discard SCT Temperature history (true | false)",
SettingDataType: "bool",
SettingValueBool: false,
},
}
return tx.Create(&defaultSettings).Error
},
},
})
if err := m.Migrate(); err != nil {
@@ -405,8 +460,8 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
// helpers
//When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy.
//This function will ignore retention policy errors, and allow the migration to continue.
// When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy.
// This function will ignore retention policy errors, and allow the migration to continue.
func ignorePastRetentionPolicyError(err error) error {
var influxDbWriteError *http.Error
if errors.As(err, &influxDbWriteError) {
@@ -3,18 +3,19 @@ package database
import (
"context"
"fmt"
"strings"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"strings"
"time"
)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Temperature Data
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo) error {
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 {
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn string, deviceProtocol string, collectorSmartData collector.SmartInfo, discardSCTTempHistory bool) error {
if len(collectorSmartData.AtaSctTemperatureHistory.Table) > 0 && !discardSCTTempHistory {
for ndx, temp := range collectorSmartData.AtaSctTemperatureHistory.Table {
//temp value may be null, we must skip/ignore them. See #393
@@ -22,9 +23,11 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
continue
}
minutesOffset := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * int64(ndx) * 60
intervalSec := collectorSmartData.AtaSctTemperatureHistory.LoggingIntervalMinutes * 60
datapointTime := collectorSmartData.LocalTime.TimeT - int64(ndx) * intervalSec
alignedDatapointTime := datapointTime - datapointTime % intervalSec
smartTemp := measurements.SmartTemperature{
Date: time.Unix(collectorSmartData.LocalTime.TimeT-minutesOffset, 0),
Date: time.Unix(alignedDatapointTime, 0),
Temp: temp,
}
@@ -39,23 +42,22 @@ func (sr *scrutinyRepository) SaveSmartTemperature(ctx context.Context, wwn stri
return err
}
}
// also add the current temperature.
} else {
smartTemp := measurements.SmartTemperature{
Date: time.Unix(collectorSmartData.LocalTime.TimeT, 0),
Temp: collectorSmartData.Temperature.Current,
}
tags, fields := smartTemp.Flatten()
tags["device_wwn"] = wwn
p := influxdb2.NewPoint("temp",
tags,
fields,
smartTemp.Date)
return sr.influxWriteApi.WritePoint(ctx, p)
}
return nil
// Even if ata_sct_temperature_history is present, also add current temperature. See #824
smartTemp := measurements.SmartTemperature{
Date: time.Unix(collectorSmartData.LocalTime.TimeT, 0),
Temp: collectorSmartData.Temperature.Current,
}
tags, fields := smartTemp.Flatten()
tags["device_wwn"] = wwn
p := influxdb2.NewPoint("temp",
tags,
fields,
smartTemp.Date)
return sr.influxWriteApi.WritePoint(ctx, p)
}
func (sr *scrutinyRepository) GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error) {
+26 -12
View File
@@ -27,14 +27,11 @@ type SmartInfo struct {
Oui uint64 `json:"oui"`
ID uint64 `json:"id"`
} `json:"wwn"`
FirmwareVersion string `json:"firmware_version"`
UserCapacity struct {
Blocks int64 `json:"blocks"`
Bytes int64 `json:"bytes"`
} `json:"user_capacity"`
LogicalBlockSize int `json:"logical_block_size"`
PhysicalBlockSize int `json:"physical_block_size"`
RotationRate int `json:"rotation_rate"`
FirmwareVersion string `json:"firmware_version"`
UserCapacity UserCapacity `json:"user_capacity"`
LogicalBlockSize int `json:"logical_block_size"`
PhysicalBlockSize int `json:"physical_block_size"`
RotationRate int `json:"rotation_rate"`
FormFactor struct {
AtaValue int `json:"ata_value"`
Name string `json:"name"`
@@ -210,9 +207,10 @@ type SmartInfo struct {
ID int `json:"id"`
SubsystemID int `json:"subsystem_id"`
} `json:"nvme_pci_vendor"`
NvmeIeeeOuiIdentifier int `json:"nvme_ieee_oui_identifier"`
NvmeControllerID int `json:"nvme_controller_id"`
NvmeNumberOfNamespaces int `json:"nvme_number_of_namespaces"`
NvmeIeeeOuiIdentifier int `json:"nvme_ieee_oui_identifier"`
NvmeTotalCapacity int64 `json:"nvme_total_capacity"`
NvmeControllerID int `json:"nvme_controller_id"`
NvmeNumberOfNamespaces int `json:"nvme_number_of_namespaces"`
NvmeNamespaces []struct {
ID int `json:"id"`
Size struct {
@@ -239,7 +237,23 @@ type SmartInfo struct {
ScsiErrorCounterLog ScsiErrorCounterLog `json:"scsi_error_counter_log"`
}
//Primary Attribute Structs
// Capacity finds the total capacity of the device in bytes, or 0 if unknown.
func (s *SmartInfo) Capacity() int64 {
switch {
case s.NvmeTotalCapacity > 0:
return s.NvmeTotalCapacity
case s.UserCapacity.Bytes > 0:
return s.UserCapacity.Bytes
}
return 0
}
type UserCapacity struct {
Blocks int64 `json:"blocks"`
Bytes int64 `json:"bytes"`
}
// Primary Attribute Structs
type AtaSmartAttributesTableItem struct {
ID int `json:"id"`
Name string `json:"name"`
@@ -0,0 +1,33 @@
package collector
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSmartInfo_Capacity(t *testing.T) {
t.Run("should report nvme capacity", func(t *testing.T) {
smartInfo := SmartInfo{
UserCapacity: UserCapacity{
Bytes: 1234,
},
NvmeTotalCapacity: 5678,
}
assert.Equal(t, int64(5678), smartInfo.Capacity())
})
t.Run("should report user capacity", func(t *testing.T) {
smartInfo := SmartInfo{
UserCapacity: UserCapacity{
Bytes: 1234,
},
}
assert.Equal(t, int64(1234), smartInfo.Capacity())
})
t.Run("should report 0 for unknown capacities", func(t *testing.T) {
var smartInfo SmartInfo
assert.Zero(t, smartInfo.Capacity())
})
}
+1
View File
@@ -14,6 +14,7 @@ type DeviceWrapper struct {
type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
Archived bool `json:"archived"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
@@ -64,11 +64,27 @@ func NewSmartFromInfluxDB(attrs map[string]interface{}) (*Smart, error) {
for key, val := range attrs {
switch key {
case "temp":
sm.Temp = val.(int64)
temp, tempOk := val.(int64)
if tempOk {
sm.Temp = temp
} else {
log.Printf("unable to parse temp information: %v", val)
}
case "power_on_hours":
sm.PowerOnHours = val.(int64)
powerOn, powerOnOk := val.(int64)
if powerOnOk {
sm.PowerOnHours = powerOn
} else {
log.Printf("unable to parse power_on_hours information: %v", val)
}
case "power_cycle_count":
sm.PowerCycleCount = val.(int64)
powerCycle, powerCycleOk := val.(int64)
if powerCycleOk {
sm.PowerCycleCount = powerCycle
} else {
log.Printf("unable to parse power_cycle_count information: %v", val)
}
default:
// this key is unknown.
if !strings.HasPrefix(key, "attr.") {
@@ -2,10 +2,11 @@ package measurements
import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
"strconv"
"strings"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
)
type SmartAtaAttribute struct {
@@ -24,6 +25,10 @@ type SmartAtaAttribute struct {
FailureRate float64 `json:"failure_rate,omitempty"`
}
func (sa *SmartAtaAttribute) GetTransformedValue() int64 {
return sa.TransformedValue
}
func (sa *SmartAtaAttribute) GetStatus() pkg.AttributeStatus {
return sa.Status
}
@@ -6,4 +6,5 @@ type SmartAttribute interface {
Flatten() (fields map[string]interface{})
Inflate(key string, val interface{})
GetStatus() pkg.AttributeStatus
GetTransformedValue() int64
}
@@ -2,9 +2,10 @@ package measurements
import (
"fmt"
"strings"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
"strings"
)
type SmartNvmeAttribute struct {
@@ -18,6 +19,10 @@ type SmartNvmeAttribute struct {
FailureRate float64 `json:"failure_rate,omitempty"`
}
func (sa *SmartNvmeAttribute) GetTransformedValue() int64 {
return sa.TransformedValue
}
func (sa *SmartNvmeAttribute) GetStatus() pkg.AttributeStatus {
return sa.Status
}
@@ -2,9 +2,10 @@ package measurements
import (
"fmt"
"strings"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
"strings"
)
type SmartScsiAttribute struct {
@@ -18,6 +19,10 @@ type SmartScsiAttribute struct {
FailureRate float64 `json:"failure_rate,omitempty"`
}
func (sa *SmartScsiAttribute) GetTransformedValue() int64 {
return sa.TransformedValue
}
func (sa *SmartScsiAttribute) GetStatus() pkg.AttributeStatus {
return sa.Status
}
+16 -10
View File
@@ -8,17 +8,23 @@ package models
//}
type Settings struct {
Theme string `json:"theme" mapstructure:"theme"`
Layout string `json:"layout" mapstructure:"layout"`
DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
LineStroke string `json:"line_stroke" mapstructure:"line_stroke"`
Theme string `json:"theme" mapstructure:"theme"`
Layout string `json:"layout" mapstructure:"layout"`
DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
LineStroke string `json:"line_stroke" mapstructure:"line_stroke"`
PoweredOnHoursUnit string `json:"powered_on_hours_unit" mapstructure:"powered_on_hours_unit"`
Collector struct {
DiscardSCTTempHistory bool `json:"discard_sct_temp_history" mapstructure:"discard_sct_temp_history"`
} `json:"collector" mapstructure:"collector"`
Metrics struct {
NotifyLevel int `json:"notify_level" mapstructure:"notify_level"`
StatusFilterAttributes int `json:"status_filter_attributes" mapstructure:"status_filter_attributes"`
StatusThreshold int `json:"status_threshold" mapstructure:"status_threshold"`
NotifyLevel int `json:"notify_level" mapstructure:"notify_level"`
StatusFilterAttributes int `json:"status_filter_attributes" mapstructure:"status_filter_attributes"`
StatusThreshold int `json:"status_threshold" mapstructure:"status_threshold"`
RepeatNotifications bool `json:"repeat_notifications" mapstructure:"repeat_notifications"`
} `json:"metrics" mapstructure:"metrics"`
}
+4 -3
View File
@@ -4,13 +4,14 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
)
func main() {
@@ -32,7 +33,7 @@ func main() {
log.Fatalf("ERROR %v", err)
}
defer file.Close()
_, err = SendPostRequest("http://localhost:9090/api/devices/register", file)
_, err = SendPostRequest("http://localhost:8080/api/devices/register", file)
if err != nil {
log.Fatalf("ERROR %v", err)
}
@@ -46,7 +47,7 @@ func main() {
log.Fatalf("ERROR %v", err)
}
_, err = SendPostRequest(fmt.Sprintf("http://localhost:9090/api/device/%s/smart", diskId), smartDataReader)
_, err = SendPostRequest(fmt.Sprintf("http://localhost:8080/api/device/%s/smart", diskId), smartDataReader)
if err != nil {
log.Fatalf("ERROR %v", err)
}
+69 -46
View File
@@ -5,22 +5,25 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
"github.com/containrrr/shoutrrr"
shoutrrrTypes "github.com/containrrr/shoutrrr/pkg/types"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
"github.com/containrrr/shoutrrr"
shoutrrrTypes "github.com/containrrr/shoutrrr/pkg/types"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
)
const NotifyFailureTypeEmailTest = "EmailTest"
@@ -29,7 +32,7 @@ const NotifyFailureTypeSmartFailure = "SmartFailure"
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes) bool {
func ShouldNotify(logger logrus.FieldLogger, device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes, repeatNotifications bool, c *gin.Context, deviceRepo database.DeviceRepo) bool {
// 1. check if the device is healthy
if device.DeviceStatus == pkg.DeviceStatusPassed {
return false
@@ -53,52 +56,69 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThr
requiredAttrStatus = pkg.AttributeStatusFailedScrutiny
}
// 2. check if the attributes that are failing should be filtered (non-critical)
// 3. for any unfiltered attribute, store the failure reason (Smart or Scrutiny)
if statusFilterAttributes == pkg.MetricsStatusFilterAttributesCritical {
hasFailingCriticalAttr := false
var statusFailingCriticalAttr pkg.AttributeStatus
// This is the only case where individual attributes need not be considered
if statusFilterAttributes == pkg.MetricsStatusFilterAttributesAll && repeatNotifications {
return pkg.DeviceStatusHas(device.DeviceStatus, requiredDeviceStatus)
}
for attrId, attrData := range smartAttrs.Attributes {
//find failing attribute
if attrData.GetStatus() == pkg.AttributeStatusPassed {
continue //skip all passing attributes
}
var failingAttributes []string
// Loop through the attributes to find the failing ones
for attrId, attrData := range smartAttrs.Attributes {
var status pkg.AttributeStatus = attrData.GetStatus()
// Skip over passing attributes
if status == pkg.AttributeStatusPassed {
continue
}
// merge the status's of all critical attributes
statusFailingCriticalAttr = pkg.AttributeStatusSet(statusFailingCriticalAttr, attrData.GetStatus())
//found a failing attribute, see if its critical
if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical {
hasFailingCriticalAttr = true
} else if device.IsNvme() && thresholds.NmveMetadata[attrId].Critical {
hasFailingCriticalAttr = true
// If the user only wants to consider critical attributes, we have to check
// if the not-passing attribute is critical or not
if statusFilterAttributes == pkg.MetricsStatusFilterAttributesCritical {
critical := false
if device.IsScsi() {
critical = thresholds.ScsiMetadata[attrId].Critical
} else if device.IsNvme() {
critical = thresholds.NmveMetadata[attrId].Critical
} else {
//this is ATA
attrIdInt, err := strconv.Atoi(attrId)
if err != nil {
continue
}
if thresholds.AtaMetadata[attrIdInt].Critical {
hasFailingCriticalAttr = true
}
critical = thresholds.AtaMetadata[attrIdInt].Critical
}
// Skip non-critical, non-passing attributes when this setting is on
if !critical {
continue
}
}
if !hasFailingCriticalAttr {
//no critical attributes are failing, and notifyFilterAttributes == "critical"
return false
} else {
// check if any of the critical attributes have a status that we're looking for
return pkg.AttributeStatusHas(statusFailingCriticalAttr, requiredAttrStatus)
}
} else {
// 2. SKIP - we are processing every attribute.
// 3. check if the device failure level matches the wanted failure level.
return pkg.DeviceStatusHas(device.DeviceStatus, requiredDeviceStatus)
// Record any attribute that doesn't get skipped by the above two checks
failingAttributes = append(failingAttributes, attrId)
}
// If the user doesn't want repeated notifications when the failing value doesn't change, we need to get the last value from the db
var lastPoints []measurements.Smart
var err error
if !repeatNotifications {
lastPoints, err = deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), database.DURATION_KEY_FOREVER, 1, 1, failingAttributes)
if err == nil || len(lastPoints) < 1 {
logger.Warningln("Could not get the most recent data points from the database. This is expected to happen only if this is the very first submission of data for the device.")
}
}
for _, attrId := range failingAttributes {
attrStatus := smartAttrs.Attributes[attrId].GetStatus()
if pkg.AttributeStatusHas(attrStatus, requiredAttrStatus) {
if repeatNotifications {
return true
}
// This is checked again here to avoid repeating the entire for loop in the check above.
// Probably unnoticeably worse performance, but cleaner code.
if err != nil || len(lastPoints) < 1 || lastPoints[0].Attributes[attrId].GetTransformedValue() != smartAttrs.Attributes[attrId].GetTransformedValue() {
return true
}
}
}
return false
}
// TODO: include user label for device.
@@ -221,7 +241,7 @@ func (n *Notify) Send() error {
notifyScripts := []string{}
notifyShoutrrr := []string{}
for ndx, _ := range configUrls {
for ndx := range configUrls {
if strings.HasPrefix(configUrls[ndx], "https://") || strings.HasPrefix(configUrls[ndx], "http://") {
notifyWebhooks = append(notifyWebhooks, configUrls[ndx])
} else if strings.HasPrefix(configUrls[ndx], "script://") {
@@ -386,6 +406,9 @@ func (n *Notify) GenShoutrrrNotificationParams(shoutrrrUrl string) (string, *sho
case "join":
(*params)["title"] = subject
(*params)["icon"] = logoUrl
case "ntfy":
(*params)["title"] = subject
(*params)["icon"] = logoUrl
case "opsgenie":
(*params)["title"] = subject
case "pushbullet":
+113 -17
View File
@@ -1,13 +1,20 @@
package notify
import (
"errors"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/stretchr/testify/require"
"testing"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
mock_database "github.com/analogj/scrutiny/webapp/backend/pkg/database/mock"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/gin-gonic/gin"
"github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
@@ -20,22 +27,27 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) {
t.Parallel()
//setup
//setupD
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedSmart,
}
smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) {
@@ -47,9 +59,11 @@ func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.
smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdSmart
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) {
@@ -61,9 +75,11 @@ func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testi
smartAttrs := measurements.Smart{}
statusThreshold := pkg.MetricsStatusThresholdScrutiny
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
@@ -79,9 +95,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t
}}
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
@@ -100,9 +119,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCritical
}}
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
@@ -118,9 +140,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(
}}
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
@@ -136,9 +161,12 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCritica
}}
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
@@ -157,9 +185,77 @@ func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresho
}}
statusThreshold := pkg.MetricsStatusThresholdSmart
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
//assert
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, true, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_NoRepeat_DatabaseFailure(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
}
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
"5": &measurements.SmartAtaAttribute{
Status: pkg.AttributeStatusFailedScrutiny,
},
}}
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, errors.New("")).Times(1)
//assert
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_NoRepeat_NoDatabaseData(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
}
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
"5": &measurements.SmartAtaAttribute{
Status: pkg.AttributeStatusFailedScrutiny,
},
}}
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{}, nil).Times(1)
//assert
require.True(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
}
func TestShouldNotify_NoRepeat(t *testing.T) {
t.Parallel()
//setup
device := models.Device{
DeviceStatus: pkg.DeviceStatusFailedScrutiny,
}
smartAttrs := measurements.Smart{Attributes: map[string]measurements.SmartAttribute{
"5": &measurements.SmartAtaAttribute{
Status: pkg.AttributeStatusFailedScrutiny,
TransformedValue: 0,
},
}}
statusThreshold := pkg.MetricsStatusThresholdBoth
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeDatabase := mock_database.NewMockDeviceRepo(mockCtrl)
fakeDatabase.EXPECT().GetSmartAttributeHistory(&gin.Context{}, "", database.DURATION_KEY_FOREVER, 1, 1, []string{"5"}).Return([]measurements.Smart{smartAttrs}, nil).Times(1)
//assert
require.False(t, ShouldNotify(logrus.StandardLogger(), device, smartAttrs, statusThreshold, notifyFilterAttributes, false, &gin.Context{}, fakeDatabase))
}
func TestNewPayload(t *testing.T) {
@@ -1,5 +1,10 @@
package thresholds
import (
"strconv"
"strings"
)
const AtaSmartAttributeDisplayTypeRaw = "raw"
const AtaSmartAttributeDisplayTypeNormalized = "normalized"
const AtaSmartAttributeDisplayTypeTransformed = "transformed"
@@ -662,62 +667,84 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
188: {
ID: 188,
DisplayName: "Command Timeout",
DisplayType: AtaSmartAttributeDisplayTypeRaw,
DisplayType: AtaSmartAttributeDisplayTypeTransformed,
Ideal: ObservedThresholdIdealLow,
Critical: true,
Description: "The count of aborted operations due to HDD timeout. Normally this attribute value should be equal to zero.",
Transform: func(normValue int64, rawValue int64, rawString string) int64 {
// Parse Seagate command timeout values if the string contains 3 pieces
// and each piece is less than or equal to the next (as a sanity check)
// See https://github.com/AnalogJ/scrutiny/issues/522
pieces := strings.Split(rawString, " ")
if len(pieces) == 3 {
int_pieces := make([]int, len(pieces))
var err error
for i, s := range pieces {
int_pieces[i], err = strconv.Atoi(s)
if err != nil {
return rawValue
}
}
if int_pieces[2] >= int_pieces[1] && int_pieces[1] >= int_pieces[0] {
return int64(int_pieces[2])
}
}
return rawValue
},
ObservedThresholds: []ObservedThreshold{
{
Low: 0,
High: 0,
Low: 0,
// This is set arbitrarily to avoid notifications caused by low
// historical numbers of command timeouts (e.g. caused by a bad cable)
High: 100,
AnnualFailureRate: 0.024893587674442153,
ErrorInterval: []float64{0.020857343769186413, 0.0294830350167543},
},
{
Low: 0,
High: 13,
Low: 100,
High: 13000000000,
AnnualFailureRate: 0.10044174089362015,
ErrorInterval: []float64{0.0812633664077498, 0.1227848196758574},
},
{
Low: 13,
High: 26,
Low: 13000000000,
High: 26000000000,
AnnualFailureRate: 0.334030592234279,
ErrorInterval: []float64{0.2523231196342665, 0.4337665082489293},
},
{
Low: 26,
High: 39,
Low: 26000000000,
High: 39000000000,
AnnualFailureRate: 0.36724705400842445,
ErrorInterval: []float64{0.30398009356575617, 0.4397986538328568},
},
{
Low: 39,
High: 52,
Low: 39000000000,
High: 52000000000,
AnnualFailureRate: 0.29848155926978354,
ErrorInterval: []float64{0.2509254838615984, 0.35242890006477073},
},
{
Low: 52,
High: 65,
Low: 52000000000,
High: 65000000000,
AnnualFailureRate: 0.2203079701535098,
ErrorInterval: []float64{0.18366082845676174, 0.26212468677179274},
},
{
Low: 65,
High: 78,
Low: 65000000000,
High: 78000000000,
AnnualFailureRate: 0.3018169948863018,
ErrorInterval: []float64{0.23779746376787655, 0.37776897542831006},
},
{
Low: 78,
High: 91,
Low: 78000000000,
High: 91000000000,
AnnualFailureRate: 0.32854928239235887,
ErrorInterval: []float64{0.2301118782147336, 0.4548506948185028},
},
{
Low: 91,
High: 104,
Low: 91000000000,
High: 104000000000,
AnnualFailureRate: 0.28488916640649387,
ErrorInterval: []float64{0.1366154288236293, 0.5239213202729072},
},
+1 -1
View File
@@ -2,4 +2,4 @@ package version
// VERSION is the app-global version string, which will be replaced with a
// new value during packaging
const VERSION = "0.7.0"
const VERSION = "0.8.1"
@@ -0,0 +1,22 @@
package handler
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
func ArchiveDevice(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), true)
if err != nil {
logger.Errorln("An error occurred while archiving device", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
@@ -1,11 +1,12 @@
package handler
import (
"net/http"
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/analogj/scrutiny/webapp/backend/pkg/thresholds"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
func GetDeviceDetails(c *gin.Context) {
@@ -24,7 +25,7 @@ func GetDeviceDetails(c *gin.Context) {
durationKey = "forever"
}
smartResults, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), durationKey, nil)
smartResults, err := deviceRepo.GetSmartAttributeHistory(c, c.Param("wwn"), durationKey, 0, 0, nil)
if err != nil {
logger.Errorln("An error occurred while retrieving device smart results", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -0,0 +1,22 @@
package handler
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
func UnarchiveDevice(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), false)
if err != nil {
logger.Errorln("An error occurred while unarchiving device", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
@@ -2,6 +2,8 @@ package handler
import (
"fmt"
"net/http"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
@@ -9,7 +11,6 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/notify"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
func UploadDeviceMetrics(c *gin.Context) {
@@ -60,7 +61,7 @@ func UploadDeviceMetrics(c *gin.Context) {
}
// save smart temperature data (ignore failures)
err = deviceRepo.SaveSmartTemperature(c, c.Param("wwn"), updatedDevice.DeviceProtocol, collectorSmartData)
err = deviceRepo.SaveSmartTemperature(c, c.Param("wwn"), updatedDevice.DeviceProtocol, collectorSmartData, appConfig.GetBool(fmt.Sprintf("%s.collector.discard_sct_temp_history", config.DB_USER_SETTINGS_SUBKEY)))
if err != nil {
logger.Errorln("An error occurred while saving smartctl temp data", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
@@ -69,10 +70,14 @@ func UploadDeviceMetrics(c *gin.Context) {
//check for error
if notify.ShouldNotify(
logger,
updatedDevice,
smartData,
pkg.MetricsStatusThreshold(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY))),
pkg.MetricsStatusFilterAttributes(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY))),
appConfig.GetBool(fmt.Sprintf("%s.metrics.repeat_notifications", config.DB_USER_SETTINGS_SUBKEY)),
c,
deviceRepo,
) {
//send notifications
+4 -2
View File
@@ -42,8 +42,10 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown)
api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data
api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests)
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
api.POST("/device/:wwn/archive", handler.ArchiveDevice) //used by UI to archive device
api.POST("/device/:wwn/unarchive", handler.UnarchiveDevice) //used by UI to unarchive device
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
api.GET("/settings", handler.GetSettings) //used to get settings
api.POST("/settings", handler.SaveSettings) //used to save settings
+16 -9
View File
@@ -4,6 +4,16 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path"
"strings"
"testing"
"time"
"github.com/analogj/scrutiny/webapp/backend/pkg"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
@@ -14,15 +24,6 @@ import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path"
"strings"
"testing"
"time"
)
/*
@@ -189,6 +190,8 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes()
fakeConfig.EXPECT().GetBool("user.collector.discard_sct_temp_history").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
@@ -247,6 +250,8 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes()
fakeConfig.EXPECT().GetBool("user.collector.discard_sct_temp_history").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
@@ -529,6 +534,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
fakeConfig.EXPECT().GetBool("user.metrics.repeat_notifications").Return(true).AnyTimes()
fakeConfig.EXPECT().GetBool("user.collector.discard_sct_temp_history").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.tls.insecure_skip_verify").Return(false).AnyTimes()
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{})
@@ -12,6 +12,8 @@ export type TemperatureUnit = 'celsius' | 'fahrenheit'
export type LineStroke = 'smooth' | 'straight' | 'stepline'
export type DevicePoweredOnUnit = 'humanize' | 'device_hours'
export enum MetricsNotifyLevel {
Warn = 1,
@@ -47,14 +49,21 @@ export interface AppConfig {
file_size_si_units?: boolean;
powered_on_hours_unit?: DevicePoweredOnUnit;
line_stroke?: LineStroke;
// Settings from Scrutiny API
collector?: {
discard_sct_temp_history?: boolean
}
metrics?: {
notify_level?: MetricsNotifyLevel
status_filter_attributes?: MetricsStatusFilterAttributes
status_threshold?: MetricsStatusThreshold
repeat_notifications?: boolean
}
}
@@ -76,13 +85,19 @@ export const appConfig: AppConfig = {
temperature_unit: 'celsius',
file_size_si_units: false,
powered_on_hours_unit: 'humanize',
line_stroke: 'smooth',
collector: {
discard_sct_temp_history : false,
},
metrics: {
notify_level: MetricsNotifyLevel.Fail,
status_filter_attributes: MetricsStatusFilterAttributes.All,
status_threshold: MetricsStatusThreshold.Both
status_threshold: MetricsStatusThreshold.Both,
repeat_notifications: true
}
};
@@ -1,5 +1,6 @@
// maps to webapp/backend/pkg/models/device.go
export interface DeviceModel {
archived?: boolean;
wwn: string;
device_name?: string;
device_uuid?: string;
@@ -20,7 +20,8 @@ export const sda = {
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
'device_status': 0,
'archived': false
},
'smart_results': [{
'date': '2021-10-24T23:20:44Z',
@@ -28,7 +28,8 @@ export const summary = {
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
'device_status': 0,
'archived': false
}
},
'0x5000cca252c859cc': {
@@ -55,7 +56,8 @@ export const summary = {
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
'device_status': 0,
'archived': false
},
'smart': {
'collector_date': '2020-08-21T22:27:02Z',
@@ -91,7 +93,8 @@ export const summary = {
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
'device_status': 0,
'archived': false
},
'smart': {
'collector_date': '2020-06-21T00:03:30Z',
@@ -127,7 +130,8 @@ export const summary = {
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
'device_status': 0,
'archived': false
}
},
'0x5000cca264ec3183': {
@@ -154,7 +158,8 @@ export const summary = {
'device_type': '',
'label': '',
'host_id': 'custom host id',
'device_status': 1
'device_status': 1,
'archived': false
},
'smart': {
'collector_date': '2020-09-13T16:29:23Z',
@@ -574,7 +579,8 @@ export const summary = {
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
'device_status': 0,
'archived': false
}
},
'0x5002538e40a22954': {
@@ -601,7 +607,8 @@ export const summary = {
'device_type': '',
'label': '',
'host_id': '',
'device_status': 0
'device_status': 0,
'archived': false
},
'smart': {
'collector_date': '2020-06-10T12:01:02Z',
@@ -0,0 +1,11 @@
<h2 mat-dialog-title>Archive {{data.title}}?</h2>
<mat-dialog-content>This will remove the device from Scrutiny dashboard, unless you toggle show archived. <strong>Any data about the device
itself will remain untouched.</strong></mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
<button class="yellow-600" mat-button (click)="onArchiveClick()">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'archive'"></mat-icon>
Archive
</button>
</mat-dialog-actions>
@@ -0,0 +1,64 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {DashboardDeviceArchiveDialogComponent} from './dashboard-device-archive-dialog.component';
import {HttpClientModule} from '@angular/common/http';
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import {SharedModule} from '../../../shared/shared.module';
import {DashboardDeviceArchiveDialogService} from './dashboard-device-archive-dialog.service';
import {of} from 'rxjs';
describe('DashboardDeviceArchiveDialogComponent', () => {
let component: DashboardDeviceArchiveDialogComponent;
let fixture: ComponentFixture<DashboardDeviceArchiveDialogComponent>;
const matDialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['closeDialog', 'close']);
const dashboardDeviceArchiveDialogServiceSpy = jasmine.createSpyObj('DashboardDeviceArchiveDialogService', ['archiveDevice']);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
MatDialogModule,
MatButtonModule,
MatIconModule,
SharedModule,
],
providers: [
{provide: MatDialogRef, useValue: matDialogRefSpy},
{provide: MAT_DIALOG_DATA, useValue: {wwn: 'test-wwn', title: 'my-test-device-title'}},
{provide: DashboardDeviceArchiveDialogService, useValue: dashboardDeviceArchiveDialogServiceSpy}
],
declarations: [DashboardDeviceArchiveDialogComponent]
})
.compileComponents()
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardDeviceArchiveDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should close the component if cancel is clicked', () => {
matDialogRefSpy.closeDialog.calls.reset();
matDialogRefSpy.closeDialog()
expect(matDialogRefSpy.closeDialog).toHaveBeenCalled();
});
it('should attempt to archive device if archive is clicked', () => {
dashboardDeviceArchiveDialogServiceSpy.archiveDevice.and.returnValue(of({'success': true}));
component.onArchiveClick()
expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice).toHaveBeenCalledWith('test-wwn');
expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice.calls.count())
.withContext('one call')
.toBe(1);
});
});
@@ -0,0 +1,29 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {DashboardDeviceArchiveDialogService} from 'app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service';
@Component({
selector: 'app-dashboard-device-archive-dialog',
templateUrl: './dashboard-device-archive-dialog.component.html',
styleUrls: ['./dashboard-device-archive-dialog.component.scss'],
})
export class DashboardDeviceArchiveDialogComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<DashboardDeviceArchiveDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string},
private _archiveService: DashboardDeviceArchiveDialogService,
) {
}
ngOnInit(): void {
}
onArchiveClick(): void {
this._archiveService.archiveDevice(this.data.wwn)
.subscribe((data) => {
this.dialogRef.close(data);
});
}
}
@@ -0,0 +1,29 @@
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import {SharedModule} from 'app/shared/shared.module';
import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing';
import {MatDialogModule} from '@angular/material/dialog';
import {DashboardDeviceArchiveDialogComponent} from './dashboard-device-archive-dialog.component';
@NgModule({
declarations: [
DashboardDeviceArchiveDialogComponent
],
imports: [
RouterModule.forChild([]),
RouterModule.forChild(dashboardRoutes),
MatButtonModule,
MatIconModule,
SharedModule,
MatDialogModule
],
exports : [
DashboardDeviceArchiveDialogComponent,
],
providers : []
})
export class DashboardDeviceArchiveDialogModule
{
}
@@ -0,0 +1,38 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {getBasePath} from 'app/app.routing';
@Injectable({
providedIn: 'root'
})
export class DashboardDeviceArchiveDialogService
{
/**
* Constructor
*
* @param {HttpClient} _httpClient
*/
constructor(
private _httpClient: HttpClient
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
archiveDevice(wwn: string): Observable<any>
{
return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/archive`, {});
}
unarchiveDevice(wwn: string): Observable<any>
{
return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/unarchive`, {});
}
}
@@ -1,10 +1,12 @@
<h2 mat-dialog-title>Delete {{data.title}}?</h2>
<mat-dialog-content>This will delete all data associated with this device (including all historical data).</mat-dialog-content>
<mat-dialog-content>This will remove the device and all historical data from Scrutiny. <strong>Any data on the device
itself will remain untouched.</strong></mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button class="red-600" mat-button (click)="onDeleteClick()">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'delete_forever'"></mat-icon>
Delete</button>
Delete
</button>
</mat-dialog-actions>
@@ -1,5 +1,7 @@
<div [ngClass]="{ 'border-green': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed',
'border-red': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed' }"
<div
[ngClass]="{ 'border-green': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed',
'border-red': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed',
'text-disabled': deviceSummary.device.archived }"
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
<mat-icon class="icon-size-96 opacity-12 text-green"
@@ -21,6 +23,8 @@
</div>
</div>
<div class="ml-auto" *ngIf="deviceSummary.device">
<mat-icon *ngIf="deviceSummary.device.archived"
[svgIcon]="'archive'"></mat-icon>
<button mat-icon-button
[matMenuTriggerFor]="previousStatementMenu">
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
@@ -33,6 +37,14 @@
<span>View Details</span>
</span>
</a>
<a mat-menu-item
(click)="openArchiveDialog()">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="deviceSummary.device.archived ? 'unarchive':'archive'"></mat-icon>
<span>{{deviceSummary.device.archived ? "Unarchive" : "Archive"}} Device</span>
</span>
</a>
<a mat-menu-item (click)="openDeleteDialog()">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
@@ -63,7 +75,7 @@
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(deviceSummary.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.power_on_hours; else unknownPoweredOn">{{ deviceSummary.smart?.power_on_hours | deviceHours:config.powered_on_hours_unit:{ round: true, largest: 1, units: ['y', 'd', 'h'] } }}</div>
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
</div>
@@ -0,0 +1,3 @@
.text-disabled{
opacity: 0.8;
}
@@ -4,12 +4,13 @@ import {takeUntil} from 'rxjs/operators';
import {AppConfig} from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {Subject} from 'rxjs';
import humanizeDuration from 'humanize-duration'
import {MatDialog} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
import {DeviceStatusPipe} from 'app/shared/device-status.pipe';
import {DashboardDeviceArchiveDialogComponent} from '../dashboard-device-archive-dialog/dashboard-device-archive-dialog.component';
import {DashboardDeviceArchiveDialogService} from '../dashboard-device-archive-dialog/dashboard-device-archive-dialog.service';
@Component({
selector: 'app-dashboard-device',
@@ -20,6 +21,7 @@ export class DashboardDeviceComponent implements OnInit {
constructor(
private _configService: ScrutinyConfigService,
private _archiveService: DashboardDeviceArchiveDialogService,
public dialog: MatDialog,
) {
// Set the private defaults
@@ -27,15 +29,14 @@ export class DashboardDeviceComponent implements OnInit {
}
@Input() deviceSummary: DeviceSummaryModel;
@Input() deviceWWN: string;
@Output() deviceArchived = new EventEmitter<string>();
@Output() deviceUnarchived = new EventEmitter<string>();
@Output() deviceDeleted = new EventEmitter<string>();
config: AppConfig;
private _unsubscribeAll: Subject<void>;
readonly humanizeDuration = humanizeDuration;
deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold
ngOnInit(): void {
@@ -72,11 +73,33 @@ export class DashboardDeviceComponent implements OnInit {
}
}
openArchiveDialog(): void {
if(this.deviceSummary.device.archived){
this._archiveService.unarchiveDevice(this.deviceSummary.device.wwn).subscribe((result) => {
if(result) {
this.deviceUnarchived.emit(this.deviceSummary.device.wwn)
}
})
return;
}
const dialogRef = this.dialog.open(DashboardDeviceArchiveDialogComponent, {
data: {
wwn: this.deviceSummary.device.wwn,
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
}
});
dialogRef.afterClosed().subscribe(result => {
if(result) {
this.deviceArchived.emit(this.deviceSummary.device.wwn);
}
})
}
openDeleteDialog(): void {
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
// width: '250px',
data: {
wwn: this.deviceWWN,
wwn: this.deviceSummary.device.wwn,
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
}
});
@@ -84,7 +107,7 @@ export class DashboardDeviceComponent implements OnInit {
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed', result);
if (result.success) {
this.deviceDeleted.emit(this.deviceWWN)
this.deviceDeleted.emit(this.deviceSummary.device.wwn)
}
});
}
@@ -7,6 +7,7 @@ import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashb
import {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing';
import {MatMenuModule} from '@angular/material/menu';
import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module';
import {DashboardDeviceArchiveDialogModule} from '../dashboard-device-archive-dialog/dashboard-device-archive-dialog.module';
@NgModule({
declarations: [
@@ -19,7 +20,8 @@ import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-dev
MatIconModule,
MatMenuModule,
SharedModule,
DashboardDeviceDeleteDialogModule
DashboardDeviceDeleteDialogModule,
DashboardDeviceArchiveDialogModule
],
exports: [
DashboardDeviceComponent,
@@ -54,6 +54,14 @@
</div>
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Powered On Format</mat-label>
<mat-select [(ngModel)]="poweredOnHoursUnit">
<mat-option value="humanize">Humanize</mat-option>
<mat-option value="device_hours">Device Hours</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Line stroke</mat-label>
<mat-select [(ngModel)]="lineStroke">
@@ -84,6 +92,33 @@
</mat-select>
</mat-form-field>
</div>
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Repeat Notifications</mat-label>
<mat-select [(ngModel)]=repeatNotifications>
<mat-option [value]=true>Always</mat-option>
<mat-option [value]=false>Only when the value has changed</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="mt-6 mb-2">
<h3 class="text-lg font-medium">Quirks</h3>
<div class="w-full border-b mt-1"></div>
</div>
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3"
matTooltip="Discard historical temperature data retrieved from the SMART Command Transport (SCT) Data Table, which may be inaccurate for some drives. The current temperature is always stored."
matTooltipPosition="right">
<mat-label>Discard SCT Temperature History</mat-label>
<mat-select [(ngModel)]=discardSCTTempHistory>
<mat-option [value]=true>Enabled</mat-option>
<mat-option [value]=false>Disabled</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</mat-dialog-content>
@@ -7,7 +7,8 @@ import {
MetricsStatusThreshold,
TemperatureUnit,
LineStroke,
Theme
Theme,
DevicePoweredOnUnit
} from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {Subject} from 'rxjs';
@@ -24,10 +25,13 @@ export class DashboardSettingsComponent implements OnInit {
dashboardSort: string;
temperatureUnit: string;
fileSizeSIUnits: boolean;
poweredOnHoursUnit: string;
lineStroke: string;
theme: string;
discardSCTTempHistory: boolean;
statusThreshold: number;
statusFilterAttributes: number;
repeatNotifications: boolean;
// Private
private _unsubscribeAll: Subject<void>;
@@ -50,11 +54,15 @@ export class DashboardSettingsComponent implements OnInit {
this.dashboardSort = config.dashboard_sort;
this.temperatureUnit = config.temperature_unit;
this.fileSizeSIUnits = config.file_size_si_units;
this.poweredOnHoursUnit = config.powered_on_hours_unit;
this.lineStroke = config.line_stroke;
this.theme = config.theme;
this.discardSCTTempHistory = config.collector.discard_sct_temp_history;
this.statusFilterAttributes = config.metrics.status_filter_attributes;
this.statusThreshold = config.metrics.status_threshold;
this.repeatNotifications = config.metrics.repeat_notifications;
});
@@ -66,11 +74,16 @@ export class DashboardSettingsComponent implements OnInit {
dashboard_sort: this.dashboardSort as DashboardSort,
temperature_unit: this.temperatureUnit as TemperatureUnit,
file_size_si_units: this.fileSizeSIUnits,
powered_on_hours_unit: this.poweredOnHoursUnit as DevicePoweredOnUnit,
line_stroke: this.lineStroke as LineStroke,
theme: this.theme as Theme,
collector: {
discard_sct_temp_history: this.discardSCTTempHistory
},
metrics: {
status_filter_attributes: this.statusFilterAttributes as MetricsStatusFilterAttributes,
status_threshold: this.statusThreshold as MetricsStatusThreshold
status_threshold: this.statusThreshold as MetricsStatusThreshold,
repeat_notifications: this.repeatNotifications
}
}
this._configService.config = newSettings
@@ -1,4 +1,3 @@
<div *ngIf="summaryData; else emptyDashboard">
<div class="flex flex-col flex-auto w-full p-8 xs:p-2">
@@ -11,7 +10,15 @@
</div>
<!-- Action buttons -->
<div class="flex items-center">
<button matTooltip="not yet implemented" class="xs:hidden" mat-stroked-button>
<button class="xs:hidden" mat-stroked-button
[color]="showArchived ? 'primary' : null"
(click)="showArchived=!showArchived">
<mat-icon class="icon-size-20"
[color]="showArchived ? 'primary' : null"
[svgIcon]="'archive'"></mat-icon>
<span class="ml-2">Archived</span>
</button>
<button matTooltip="not yet implemented" class="ml-2 xs:hidden" mat-stroked-button>
<mat-icon class="icon-size-20"
[svgIcon]="'save'"></mat-icon>
<span class="ml-2">Export</span>
@@ -31,6 +38,12 @@
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
</button>
<mat-menu #actionsMenu="matMenu">
<button mat-menu-item (click)="showArchived=!showArchived">
<mat-icon class="icon-size-20"
[color]="showArchived ? 'primary' : null"
[svgIcon]="'archive'"></mat-icon>
<span class="ml-2">Archived</span>
</button>
<button mat-menu-item
matTooltip="not yet implemented">
<mat-icon class="icon-size-20"
@@ -49,13 +62,16 @@
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
<h3 class="ml-4" *ngIf="hostId.key">{{ hostId.key }}</h3>
<div class="flex flex-wrap w-full">
<app-dashboard-device (deviceDeleted)="onDeviceDeleted($event)"
class="flex gt-sm:w-1/2 min-w-80 p-4"
*ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboard_sort:config.dashboard_display )"
[deviceWWN]="deviceSummary.device.wwn"
[deviceSummary]="deviceSummary"></app-dashboard-device>
<ng-container *ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboard_sort:config.dashboard_display )">
<app-dashboard-device *ngIf="showArchived || !deviceSummary.device.archived"
(deviceArchived)="onDeviceArchived($event)"
(deviceUnarchived)="onDeviceUnarchived($event)"
(deviceDeleted)="onDeviceDeleted($event)"
class="flex gt-sm:w-1/2 min-w-80 p-4"
[deviceSummary]="deviceSummary"></app-dashboard-device>
</ng-container>
</div>
</div>
@@ -67,13 +83,13 @@
<div class="flex items-center justify-between">
<div class="flex flex-col">
<div class="font-bold text-md text-secondary uppercase tracking-wider mr-4">Temperature</div>
<div class="text-sm text-hint font-medium">Temperature history for each device </div>
<div class="text-sm text-hint font-medium">Temperature history for each device</div>
</div>
<div>
<button class="h-8 min-h-8 px-2"
mat-button
[matMenuTriggerFor]="tempRangeMenu">
<span class="font-medium text-sm text-hint">{{tempDurationKey}}</span>
<span class="font-medium text-sm text-hint">{{ tempDurationKey }}</span>
</button>
<mat-menu #tempRangeMenu="matMenu">
<button (click)="changeSummaryTempDuration('forever')" mat-menu-item>forever</button>
@@ -109,7 +125,8 @@
src="assets/images/dashboard-placeholder.png">
<h1>No Devices Detected!</h1>
<p style="max-width:700px;">Scrutiny includes a Collector agent that you must run on all of your systems. The Collector is responsible for detecting connected storage devices and collecting S.M.A.R.T data on a configurable schedule.</p>
<p style="max-width:700px;">Scrutiny includes a Collector agent that you must run on all of your systems. The Collector is responsible for detecting connected storage
devices and collecting S.M.A.R.T data on a configurable schedule.</p>
<p><br/>You can trigger the Collector manually by running the following command, then refreshing this page:</p>
<code>scrutiny-collector-metrics run</code>
@@ -34,6 +34,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
temperatureOptions: ApexOptions;
tempDurationKey = 'forever'
config: AppConfig;
showArchived: boolean;
// Private
private _unsubscribeAll: Subject<void>;
@@ -159,9 +160,18 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
for(const tempHistory of deviceSummary.temp_history){
const newDate = new Date(tempHistory.date);
let temperature;
switch (this.config.temperature_unit) {
case 'celsius':
temperature = tempHistory.temp;
break
case 'fahrenheit':
temperature = TemperaturePipe.celsiusToFahrenheit(tempHistory.temp)
break
}
deviceSeriesMetadata.data.push({
x: newDate,
y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperature_unit, false)
y: temperature
})
}
deviceTemperatureSeries.push(deviceSeriesMetadata)
@@ -206,6 +216,8 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
},
tooltip: {
theme: 'dark',
shared: true,
intersect: false,
x : {
format: 'MMM dd, yyyy HH:mm:ss'
},
@@ -248,6 +260,14 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
delete this.summaryData[wwn] // remove the device from the summary list.
}
onDeviceArchived(wwn: string): void {
this.summaryData[wwn].device.archived = true;
}
onDeviceUnarchived(wwn: string): void {
this.summaryData[wwn].device.archived = false;
}
/*
DURATION_KEY_WEEK = "week"
@@ -123,7 +123,7 @@
<div class="text-secondary text-md">Power Cycle Count</div>
</div>
<div *ngIf="smart_results[0]?.power_on_hours" class="my-2 col-span-2 lt-md:col-span-1">
<div matTooltip="{{humanizeDuration(smart_results[0]?.power_on_hours * 60 * 60 * 1000, { conjunction: ' and ', serialComma: false })}}">{{humanizeDuration(smart_results[0]?.power_on_hours *60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] })}}</div>
<div matTooltip="{{humanizeDuration(smart_results[0]?.power_on_hours * 60 * 60 * 1000, { conjunction: ' and ', serialComma: false })}}">{{ smart_results[0]?.power_on_hours | deviceHours:config.powered_on_hours_unit:{ round: true, largest: 1, units: ['y', 'd', 'h'] } }}</div>
<div class="text-secondary text-md">Powered On</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
@@ -0,0 +1,52 @@
import { DeviceHoursPipe } from "./device-hours.pipe";
describe("DeviceHoursPipe", () => {
it("create an instance", () => {
const pipe = new DeviceHoursPipe();
expect(pipe).toBeTruthy();
});
describe("#transform", () => {
const testCases = [
{
input: 12345,
configuration: "device_hours",
result: "12345 hours",
},
{
input: 15273,
configuration: "humanize",
result: "1 year, 8 months, 3 weeks, 6 days, 15 hours",
},
{
input: 48,
configuration: null,
result: "2 days",
},
{
input: 168,
configuration: "scrutiny",
result: "1 week",
},
{
input: null,
configuration: "device_hours",
result: "Unknown",
},
{
input: null,
configuration: "humanize",
result: "Unknown",
},
];
testCases.forEach((test, index) => {
it(`format input '${test.input}' with configuration '${test.configuration}', should be '${test.result}' (testcase: ${index + 1})`, () => {
// test
const pipe = new DeviceHoursPipe();
const formatted = pipe.transform(test.input, test.configuration);
expect(formatted).toEqual(test.result);
});
});
});
});
@@ -0,0 +1,19 @@
import { Pipe, PipeTransform } from '@angular/core';
import humanizeDuration from 'humanize-duration';
@Pipe({ name: 'deviceHours' })
export class DeviceHoursPipe implements PipeTransform {
static format(hoursOfRunTime: number, unit: string, humanizeConfig: object): string {
if (hoursOfRunTime === null) {
return 'Unknown';
}
if (unit === 'device_hours') {
return `${hoursOfRunTime} hours`;
}
return humanizeDuration(hoursOfRunTime * 60 * 60 * 1000, humanizeConfig);
}
transform(hoursOfRunTime: number, unit = 'humanize', humanizeConfig: any = {}): string {
return DeviceHoursPipe.format(hoursOfRunTime, unit, humanizeConfig)
}
}
@@ -6,6 +6,7 @@ import { DeviceSortPipe } from './device-sort.pipe';
import { TemperaturePipe } from './temperature.pipe';
import { DeviceTitlePipe } from './device-title.pipe';
import { DeviceStatusPipe } from './device-status.pipe';
import { DeviceHoursPipe } from './device-hours.pipe';
@NgModule({
declarations: [
@@ -13,7 +14,8 @@ import { DeviceStatusPipe } from './device-status.pipe';
DeviceSortPipe,
TemperaturePipe,
DeviceTitlePipe,
DeviceStatusPipe
DeviceStatusPipe,
DeviceHoursPipe
],
imports: [
CommonModule,
@@ -28,7 +30,8 @@ import { DeviceStatusPipe } from './device-status.pipe';
DeviceSortPipe,
DeviceTitlePipe,
DeviceStatusPipe,
TemperaturePipe
TemperaturePipe,
DeviceHoursPipe
]
})
export class SharedModule

Some files were not shown because too many files have changed in this diff Show More