Compare commits

..

111 Commits

Author SHA1 Message Date
packagrio-bot de5f2c3324 (v0.4.10) Automated packaging of release by Packagr 2022-06-09 06:10:18 +00:00
Jason Kulatunga d486f14433 Merge pull request #291 from AnalogJ/beta 2022-06-08 22:52:41 -07:00
Jason Kulatunga cb47dd7185 revert s6-overlay changes. 2022-06-07 22:03:02 -07:00
Jason Kulatunga 6ae4d233cd update bug report form to require docker info output. 2022-06-07 21:42:39 -07:00
Jason Kulatunga f8bb185854 trying to fix seg fault issues. Attempting to consolidate on debian-bullseye for runtime docker images. 2022-06-07 21:29:15 -07:00
Jason Kulatunga 1da07caaa6 fix background color for details page history tooltip.
fixes #283
2022-06-07 20:17:25 -07:00
Jason Kulatunga fe96c27732 trying to fix webUI. 2022-06-07 19:51:05 -07:00
Jason Kulatunga 7287775cca trying to fix webUI. 2022-06-07 18:59:25 -07:00
Jason Kulatunga 28ac3ac7ec fix settings persistence. 2022-06-04 22:53:27 -07:00
packagrio-bot a6208c0d49 (v0.4.9) Automated packaging of release by Packagr 2022-06-05 05:24:54 +00:00
Jason Kulatunga 7840fe66da Merge pull request #280 from AnalogJ/beta 2022-06-04 22:15:53 -07:00
Jason Kulatunga 2ca44c967e simplify darkmode ui toggle. 2022-06-04 20:32:12 -07:00
Jason Kulatunga 4b767421f3 change highlight color for dark mode. 2022-06-04 19:01:18 -07:00
Jason Kulatunga 6005b8609a trying to fix docker image builds (take 1h+ right now).
trying to fix docker image builds (take 1h+ right now).

trying to fix docker image builds (take 1h+ right now).

trying to fix docker image builds (take 1h+ right now).

trying to fix docker image builds (take 1h+ right now).

trying to fix docker image builds (take 1h+ right now).

trying to fix docker image builds (take 1h+ right now).

trying to fix docker image builds (take 1h+ right now).

trying to fix docker image builds (take 1h+ right now).
2022-06-04 12:22:07 -07:00
Jason Kulatunga df23ecdf33 fix typing for attribute status enum stored in database. 2022-06-04 09:42:45 -07:00
Jason Kulatunga f4988cbac5 try to speed up multi-arch docker builds by limiting qemu vm's to amd and arm only. 2022-06-04 09:29:17 -07:00
Jason Kulatunga f4f5d16b4a rename variable to themeUseSystem from darkModeUseSystem. 2022-06-04 08:18:40 -07:00
Jason Kulatunga 1c4dd33381 Merge pull request #276 from shamoon/dark-mode 2022-06-04 08:13:43 -07:00
Jason Kulatunga 9e0ba4d269 Merge branch 'beta' into dark-mode 2022-06-04 08:12:37 -07:00
Jason Kulatunga d9ecf6c0d3 make sure defaults are available if missing from localStorage
fixes #277
2022-06-04 08:08:45 -07:00
Michael Shamoon 8051ad4dde Tweak / fix some dark mode colors
Update styles.scss
2022-06-03 00:50:17 -07:00
Michael Shamoon 165f98dc09 Add settings UI for dark mode 2022-06-03 00:50:17 -07:00
Jason Kulatunga ca7772250c fix s6-overlay overwriting bin symlinks:
https://github.com/just-containers/s6-overlay/tree/v2.1.0.1#bin-and-sbin-are-symlinks

adding a makefile to build docker images locally.
2022-06-02 21:06:43 -07:00
Jason Kulatunga 6e02e4da02 fixing func def. 2022-06-02 12:21:54 -07:00
Jason Kulatunga 9c8498cea7 disable and re-enable bitwise operations 2022-06-02 12:20:50 -07:00
Jason Kulatunga 965fbb08da trying to fix installation. 2022-06-02 11:35:30 -07:00
Jason Kulatunga e16933eeac trying to fix installation. 2022-06-02 11:06:15 -07:00
Jason Kulatunga 4d0fc0eae8 trying to fix installation. 2022-06-02 10:49:22 -07:00
Jason Kulatunga 8296a973b8 trying to fix installation. 2022-06-02 10:48:44 -07:00
Jason Kulatunga 19a9957755 using ARG DEBIAN_FRONTEND=noninteractive 2022-06-02 10:40:28 -07:00
Jason Kulatunga 02e3947906 disable github action docker build caching - may be causing "cannot reuse body, request must be retried" errors 2022-06-02 10:22:55 -07:00
Jason Kulatunga 766a73455e update the base image for docker iamges to ubuntu:latest - which follows the LTS.
fixes #274
2022-06-02 10:04:36 -07:00
Jason Kulatunga 6e64ae09aa Update SUPPORTED_NAS_OS.md 2022-06-01 16:39:46 -07:00
Jason Kulatunga 411eca20e0 Update SUPPORTED_NAS_OS.md 2022-05-31 18:11:25 -07:00
Jason Kulatunga 0243d9e2fa Merge pull request #272 from BadCo-NZ/patch-1
Create INSTALL_PFSENSE.md
2022-05-31 18:10:33 -07:00
Jason Kulatunga 9aa0e97be0 display the device UUID and device Label in the details page.
fixes #265
2022-05-31 13:36:58 -07:00
Jason Kulatunga 488fcfc820 added AttributeStatus bit flag
ensure DeviceStatus is a valid bit flag.
[docs] added running tests section to contribution guide.
make sure UI correctly treats scrutiny failures as failed.
2022-05-31 13:31:34 -07:00
Jason Kulatunga b5dad487e5 updating bug report. 2022-05-31 11:32:58 -07:00
Jason Kulatunga 8b01187892 woarkound for volume mount w/privileged 2022-05-31 09:13:47 -07:00
Jason Kulatunga d9d6ce0f30 added docuemtnation about exit codes. 2022-05-31 08:50:38 -07:00
BadCo-NZ 8d203b3547 Create INSTALL_PFSENSE.md
As requested by @AnalogJ
2022-05-30 10:17:51 +00:00
Jason Kulatunga fe5dbcff1e documentation changes. 2022-05-28 16:15:26 -07:00
Jason Kulatunga 99df104cdd documentation changes. 2022-05-28 15:50:05 -07:00
Jason Kulatunga a53397210c adding mechanism to override the smartctl commands used by scrutiny for device scanning, device identification and smart data retrieval.
adding tests for command overrides.

rename GetScanOverrides() to GetDeviceOverrides()

fixes #255
2022-05-28 15:32:44 -07:00
Jason Kulatunga 2533d8d34f using Constants for git release/debug modes. 2022-05-28 09:53:45 -07:00
Jason Kulatunga af2523cfee setting GinMode to release by default. Users get confused otherwise. 2022-05-28 09:50:06 -07:00
Jason Kulatunga c6e1663f8a Update README.md 2022-05-28 09:10:52 -07:00
Jason Kulatunga ab83c389f7 Update INSTALL_HUB_SPOKE.md 2022-05-28 07:39:42 -07:00
Jason Kulatunga 6d22702864 Update INSTALL_MANUAL.md 2022-05-27 22:34:32 -07:00
packagrio-bot d78957353d (v0.4.8) Automated packaging of release by Packagr 2022-05-28 02:05:59 +00:00
Jason Kulatunga b208493af9 Merge pull request #263 from AnalogJ/beta 2022-05-27 18:56:56 -07:00
Jason Kulatunga 4aa1485246 using device title pipe to consistently set the device name based on configuration setting.
adding device status pipe to set the device status in a more readable way.
2022-05-27 16:12:27 -07:00
Jason Kulatunga e1e1d321dd fix git.version.sh script. 2022-05-27 13:05:58 -07:00
Jason Kulatunga 3971b37abc attempting to fix docker image build by generating frontend version information before docker build. 2022-05-27 12:59:32 -07:00
Jason Kulatunga cf1bd3ea6b trying to fix docker build, so it includes git sha info. 2022-05-27 00:13:34 -07:00
Jason Kulatunga 9b901766e3 trying to fix docker build, so it includes git sha info. 2022-05-27 00:02:11 -07:00
Jason Kulatunga e19ee78e70 trying to fix docker build, so it includes git sha info. 2022-05-26 23:54:17 -07:00
Jason Kulatunga c7c55ab95c trying to fix docker build, so it includes git sha info. 2022-05-26 23:44:13 -07:00
Jason Kulatunga d7ddf01ea0 trying to fix docker build, so it includes git sha info. 2022-05-26 23:04:57 -07:00
Jason Kulatunga c539af1a67 trying to fix docker build, so it includes git sha info. 2022-05-26 22:53:00 -07:00
Jason Kulatunga d93d24b52d using npm run commands for building angular application (supports pre steps).
Automatically embed the application version in the UI.
2022-05-26 22:37:45 -07:00
Jason Kulatunga 5dbfad68ad fix titles. 2022-05-26 21:51:02 -07:00
Jason Kulatunga 92c4506cfa adding a Caddy example to the TROUBLESHOOTING_REVERSE_PROXY.md guide
fixes #257
2022-05-26 21:49:57 -07:00
Jason Kulatunga fe80bed6bd adding a Caddy example to the TROUBLESHOOTING_REVERSE_PROXY.md guide
fixes #257
2022-05-26 21:48:27 -07:00
Jason Kulatunga b6e69021b2 ensure that the base href is set, as it's required when reloading subpages.
fixes #264
2022-05-26 21:25:36 -07:00
Jason Kulatunga 12e624a496 updating CONTRIBUTING.md guide. 2022-05-26 19:06:48 -07:00
Jason Kulatunga e95b44c690 make sure we use a reasonable number of decimal points for converted temps. 2022-05-26 14:21:46 -07:00
Jason Kulatunga 4ee947d55c trying to fix compilation/typing issues. 2022-05-26 13:49:59 -07:00
Jason Kulatunga 21212c0a1d add setting to change temperature between C and F.
fixes #175
2022-05-26 13:04:15 -07:00
Jason Kulatunga d1376a2200 serve all fonts locally
fixes #125
2022-05-26 10:12:59 -07:00
Jason Kulatunga 7d2daf4f6a add ability to sort devices by age (powered-on-hours)
fixes #100
2022-05-26 08:33:30 -07:00
Jason Kulatunga da4562d308 fixed UI issues related to deleting (component is now correctly removed from the dashboard device list).
fixes #69
2022-05-26 00:16:13 -07:00
Jason Kulatunga f51de52ff7 hide device dashboard component if deletion finishes successfully. 2022-05-25 19:19:11 -07:00
Jason Kulatunga 987632df39 working deletion code. 2022-05-25 19:02:30 -07:00
Jason Kulatunga 28a3c3e53f [WIP] Delete button for devices. 2022-05-25 17:55:23 -07:00
Jason Kulatunga 1bd86f5abd [WIP] Delete button for devices. 2022-05-25 14:59:55 -07:00
Jason Kulatunga 989fbc25f8 latest tag should consistently point to omnibus versions. 2022-05-25 09:52:17 -07:00
packagrio-bot 0f935ceb48 (v0.4.7) Automated packaging of release by Packagr 2022-05-25 15:04:55 +00:00
Jason Kulatunga f844a435fd fix error message. 2022-05-25 07:55:15 -07:00
Jason Kulatunga 3a970e7a27 Merge pull request #262 from AnalogJ/beta
pre-v0.4.7 release
2022-05-25 07:50:21 -07:00
Jason Kulatunga 307c2bcdef fix error message.
Simpler GormMigrateOptions.
2022-05-25 07:39:56 -07:00
Jason Kulatunga d62928aaae adding documentation for script based notifications. 2022-05-24 15:15:37 -07:00
Jason Kulatunga d08a1e3ef6 ignore retention policy errors during migration.
- fixes #256
2022-05-24 14:26:40 -07:00
Jason Kulatunga 2292041f9f never drop tables. 2022-05-24 10:00:41 -07:00
Jason Kulatunga 75e4bf1d6e added a helpful comment that the database migration might take a looong time. 2022-05-24 09:47:09 -07:00
Jason Kulatunga 97add04276 make sure the migration step runs with transactions, so that we can debug easier.
- related #256
2022-05-24 09:07:38 -07:00
Jason Kulatunga 1423f55d78 remove Power Cycle Count failure attribute for ATA drives. Unrealistic for consumer users (BackBlaze data is datacenter focused).
- fixed #31
2022-05-23 10:19:12 -07:00
Jason Kulatunga 46d0b70399 disable NVMe Scrutiny failures for "Numb Error Log Entries" attribute. More analysis needed for NVMe drives & their critical attributes.
- fixes #187
- fixes #247
2022-05-23 09:50:15 -07:00
Jason Kulatunga 168ca802d1 add support for specifying scheme for influxdb endpoint url (http vs https).
fixes #258
2022-05-23 09:34:16 -07:00
Jason Kulatunga 8c07e91f39 grey out and mark thresholds as not yet implemented. 2022-05-23 09:23:22 -07:00
Jason Kulatunga 7979950c3b fixing device sort and display title.
fixes #194
2022-05-23 08:49:51 -07:00
Jason Kulatunga 9846ba13e0 adding support for device sort in UI. 2022-05-23 08:19:58 -07:00
Jason Kulatunga 83839f7faf adding group by hostId support in dashboard.
fixes #151
2022-05-20 22:33:59 -07:00
Jason Kulatunga 85fa3b1f8f moved device summary info panel into isolated component. 2022-05-20 21:55:40 -07:00
Jason Kulatunga 4190f9a633 remove filter not implemented message. 2022-05-20 20:59:29 -07:00
Jason Kulatunga 743ce27d2e adding comment. 2022-05-20 20:59:29 -07:00
Jason Kulatunga 399a2450ff make sure we can change the temperature duration key for the chart. 2022-05-20 20:59:29 -07:00
Jason Kulatunga 934f16f0a5 persist settings across sessions (in local storage). 2022-05-20 20:59:29 -07:00
Jason Kulatunga 0aeb13c181 support custom display of devices by UUID/ID/Label & Scrutiny Name. (Does not persist).
Related #225
2022-05-20 20:59:29 -07:00
Jason Kulatunga 5899bf2026 started working on Dashboard UI sorting and naming 2022-05-20 20:59:29 -07:00
Jason Kulatunga 3b137964fc make sure we include the host id in the temp history label. 2022-05-20 20:59:29 -07:00
Jason Kulatunga 1bfdd0043f added a way to retrieve raw udev data. Can be used to retrieve disk label, UUID and "disk/by-id/*" device info.
Storing it in the database during device registration.
2022-05-20 20:59:29 -07:00
Jason Kulatunga 999c12748c added a way to retrieve raw udev data. Can be used to retrieve disk label, UUID and "disk/by-id/*" device info.
Storing it in the database during device registration.
2022-05-20 20:59:29 -07:00
Jason Kulatunga 6f283fd736 Update README.md 2022-05-20 10:25:02 -07:00
packagrio-bot 65d31046a0 (v0.4.6) Automated packaging of release by Packagr 2022-05-20 17:02:35 +00:00
Jason Kulatunga 601d632ae4 update xgo version. 2022-05-20 09:52:24 -07:00
Jason Kulatunga 8466c5e750 upgrade to v2.9.0 for influxdb sdk -- this includes the SetupWithToken method. 2022-05-20 09:18:01 -07:00
Jason Kulatunga aa786c0db8 upgrade to go 1.17 2022-05-18 09:40:52 -07:00
Jason Kulatunga f3faee389b trying to fix the docker builds. 2022-05-18 09:30:37 -07:00
Jason Kulatunga 5ac0aa8f74 Forked InfluxDB SDK and added support for using pre-generated admin token during setup. This ensures we no longer need to persist the token during startup.
fixes #248
2022-05-18 09:14:05 -07:00
Jason Kulatunga a589d11d01 update influxdb host default to localhost. 2022-05-17 09:39:03 -07:00
168 changed files with 17080 additions and 676 deletions
-1
View File
@@ -1,4 +1,3 @@
/dist
/vendor
/.idea
/.github
+7 -6
View File
@@ -22,20 +22,21 @@ See [/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](docs/TROUBLESHOOTING_DEVICE_COLL
```
docker run -it --rm -p 8080:8080 \
-v `pwd`/config:/opt/scrutiny/config \
-v /run/udev:/run/udev:ro \
--cap-add SYS_RAWIO \
--device=/dev/sda \
--device=/dev/sdb \
-e DEBUG=true \
-e COLLECTOR_LOG_FILE=/tmp/collector.log \
-e SCRUTINY_LOG_FILE=/tmp/web.log \
-e COLLECTOR_LOG_FILE=/opt/scrutiny/config/collector.log \
-e SCRUTINY_LOG_FILE=/opt/scrutiny/config/web.log \
--name scrutiny \
ghcr.io/analogj/scrutiny:master-omnibus
# in another terminal trigger the collector
docker exec scrutiny scrutiny-collector-metrics run
# then use docker cp to copy the log files out of the container.
docker cp scrutiny:/tmp/collector.log collector.log
docker cp scrutiny:/tmp/web.log web.log
```
The log files will be available on your host in the `config` directory. Please attach them to this issue.
Please also provide the output of `docker info`
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
container: techknowlogick/xgo:go-1.13.x
container: techknowlogick/xgo:go-1.17.x
# Service containers to run with `build` (Required for end-to-end testing)
services:
+42 -9
View File
@@ -20,9 +20,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Login against a Docker registry except on PR
@@ -40,6 +44,8 @@ jobs:
id: meta
uses: docker/metadata-action@v4
with:
flavor: |
latest=false
tags: |
type=ref,enable=true,event=branch,suffix=-collector
type=ref,enable=true,event=tag,suffix=-collector
@@ -56,8 +62,8 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# cache-from: type=gha
# cache-to: type=gha,mode=max
web:
runs-on: ubuntu-latest
@@ -68,8 +74,21 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: "Generate frontend"
uses: addnab/docker-run-action@v3
with:
image: node:lts
options: -v ${{ github.workspace }}:/work
run: |
cd /work
make frontend && echo "print contents of /work/dist" && ls -alt /work/dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Login against a Docker registry except on PR
@@ -87,11 +106,12 @@ jobs:
id: meta
uses: docker/metadata-action@v4
with:
flavor: |
latest=false
tags: |
type=ref,enable=true,event=branch,suffix=-web
type=ref,enable=true,event=tag,suffix=-web
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
@@ -103,8 +123,8 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# cache-from: type=gha
# cache-to: type=gha,mode=max
omnibus:
runs-on: ubuntu-latest
permissions:
@@ -114,8 +134,22 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: "Populate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: "Generate frontend & version information"
uses: addnab/docker-run-action@v3
with:
image: node:lts
options: -v ${{ github.workspace }}:/work
run: |
cd /work
make frontend && echo "print contents of /work/dist" && ls -alt /work/dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: 'arm64,arm'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Login against a Docker registry except on PR
@@ -137,7 +171,6 @@ jobs:
type=ref,enable=true,event=branch,suffix=-omnibus
type=ref,enable=true,event=tag,suffix=-omnibus
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
@@ -149,5 +182,5 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# cache-from: type=gha
# cache-to: type=gha,mode=max
+3 -1
View File
@@ -15,13 +15,15 @@ jobs:
uses: actions/checkout@v2
with:
ref: ${{github.event.release.tag_name}}
- name: "Generate frontend version information"
run: "cd webapp/frontend && ./git.version.sh"
- name: Build Frontend
run: |
cd webapp/frontend
npm install -g @angular/cli@9.1.4
npm install
mkdir -p dist
ng build --output-path=dist --deploy-url="/web/" --base-href="/web/" --prod
npm run build:prod -- --output-path=dist
tar -czf scrutiny-web-frontend.tar.gz dist
- name: Upload Frontend Asset
id: upload-release-asset3
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
container: techknowlogick/xgo:go-1.13.x
container: techknowlogick/xgo:go-1.17.x
# Service containers to run with `build` (Required for end-to-end testing)
services:
influxdb:
+125 -71
View File
@@ -1,75 +1,109 @@
# Contributing
There are multiple ways to develop on the scrutiny codebase locally. The two most popular are:
- Docker Development Container - only requires docker
- Run Components Locally - requires smartmontools, golang & nodejs installed locally
The Scrutiny repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) containing source code for:
- Scrutiny Backend Server (API)
- Scrutiny Frontend Angular SPA
- S.M.A.R.T Collector
## Docker Development
```
docker build -f docker/Dockerfile . -t chcr.io/analogj/scrutiny:master-omnibus
docker run -it --rm -p 8080:8080 \
-v /run/udev:/run/udev:ro \
--cap-add SYS_RAWIO \
--device=/dev/sda \
--device=/dev/sdb \
ghcr.io/analogj/scrutiny:master-omnibus
/opt/scrutiny/bin/scrutiny-collector-metrics run
```
Depending on the functionality you are adding, you may need to setup a development environment for 1 or more projects.
# Modifying the Scrutiny Backend Server (API)
## Local Development
1. install the [Go runtime](https://go.dev/doc/install) (v1.17+)
2. download the `scrutiny-web-frontend.tar.gz` for the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist`
3. create a `scrutiny.yaml` config file
```yaml
# config file for local development. store as scrutiny.yaml
version: 1
web:
listen:
port: 8080
host: 0.0.0.0
database:
# can also set absolute path here
location: ./scrutiny.db
src:
frontend:
path: ./dist
influxdb:
retention_policy: false
log:
file: 'web.log' #absolute or relative paths allowed, eg. web.log
level: DEBUG
### Frontend
The frontend is written in Angular.
If you're working on the frontend and can use mocked data rather than a real backend, you can use
```
cd webapp/frontend
npm install
ng serve --deploy-url="/web/" --base-href="/web/"
```
```
4. start a InfluxDB docker container.
```bash
docker run -p 8086:8086 --rm influxdb:2.2
```
5. start the scrutiny web server
```bash
go mod vendor
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
```
6. open your browser to [http://localhost:8080/web](http://localhost:8080/web)
However, if you need to also run the backend, and use real data, you'll need to run the following command:
```
cd webapp/frontend && ng build --watch --output-path=../../dist --prod
```
# Modifying the Scrutiny Frontend Angular SPA
> Note: if you do not add `--prod` flag, app will display mocked data for api calls.
The frontend is written in Angular. If you're working on the frontend and can use mocked data rather than a real backend, you can follow the instructions below:
### Backend
1. install [NodeJS](https://nodejs.org/en/download/)
2. start the Angular Frontend Application
```bash
cd webapp/frontend
npm install
npm run start -- --deploy-url="/web/" --base-href="/web/" --port 4200
```
3. open your browser and visit [http://localhost:4200/web](http://localhost:4200/web)
If you're using the `ng build` command above to generate your frontend, you'll need to create a custom config file and
override the `web.src.frontend.path` value.
# Modifying both Scrutiny Backend and Frontend Applications
If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data,
you'll need to follow the steps below:
```
# config file for local development. store as scrutiny.yaml
version: 1
1. install the [Go runtime](https://go.dev/doc/install) (v1.17+)
2. install [NodeJS](https://nodejs.org/en/download/)
3. create a `scrutiny.yaml` config file
```yaml
# config file for local development. store as scrutiny.yaml
version: 1
web:
listen:
port: 8080
host: 0.0.0.0
database:
# can also set absolute path here
location: ./scrutiny.db
src:
frontend:
path: ./dist
influxdb:
retention_policy: false
log:
file: 'web.log' #absolute or relative paths allowed, eg. web.log
level: DEBUG
web:
listen:
port: 8080
host: 0.0.0.0
database:
# can also set absolute path here
location: ./scrutiny.db
src:
frontend:
path: ./dist
influxdb:
retention_policy: false
log:
file: 'web.log' #absolute or relative paths allowed, eg. web.log
level: DEBUG
```
Once you've created a config file, you can pass it to the scrutiny binary during startup.
```
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
```
Now visit http://localhost:8080
```
4. start a InfluxDB docker container.
```bash
docker run -p 8086:8086 --rm influxdb:2.2
```
5. build the Angular Frontend Application
```bash
cd webapp/frontend
npm install
npm run build:prod -- --watch --output-path=../../dist
# Note: if you do not add `--prod` flag, app will display mocked data for api calls.
```
6. start the scrutiny web server
```bash
go mod vendor
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./scrutiny.yaml
```
7. open your browser to [http://localhost:8080/web](http://localhost:8080/web)
If you'd like to populate the database with some test data, you can run the following commands:
@@ -82,15 +116,6 @@ If you'd like to populate the database with some test data, you can run the fol
docker run -p 8086:8086 --rm influxdb:2.2
docker run --rm -p 8086:8086 \
-e DOCKER_INFLUXDB_INIT_MODE=setup \
-e DOCKER_INFLUXDB_INIT_USERNAME=admin \
-e DOCKER_INFLUXDB_INIT_PASSWORD=password12345 \
-e DOCKER_INFLUXDB_INIT_ORG=scrutiny \
-e DOCKER_INFLUXDB_INIT_BUCKET=metrics \
influxdb:2.2
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/web/testdata/register-devices-req.json localhost:8080/api/devices/register
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-ata.json localhost:8080/api/device/0x5000cca264eb01d7/smart
# curl -X POST -H "Content-Type: application/json" -d @webapp/backend/pkg/models/testdata/smart-ata-date.json localhost:8080/api/device/0x5000cca264eb01d7/smart
@@ -105,14 +130,14 @@ curl localhost:8080/api/summary
```
### Collector
# Modifying the Collector
```
brew install smartmontools
go run collector/cmd/collector-metrics/collector-metrics.go run --debug
```
## Debugging
# Debugging
If you need more verbose logs for debugging, you can use the following environmental variables:
@@ -131,3 +156,32 @@ Finally, you can copy the files from the scrutiny container to your host using t
docker cp scrutiny:/tmp/collector.log collector.log
docker cp scrutiny:/tmp/web.log web.log
```
# Docker Development
```
docker build -f docker/Dockerfile . -t chcr.io/analogj/scrutiny:master-omnibus
docker run -it --rm -p 8080:8080 \
-v /run/udev:/run/udev:ro \
--cap-add SYS_RAWIO \
--device=/dev/sda \
--device=/dev/sdb \
ghcr.io/analogj/scrutiny:master-omnibus
/opt/scrutiny/bin/scrutiny-collector-metrics run
```
# Running Tests
```bash
docker run -p 8086:8086 -d --rm \
-e DOCKER_INFLUXDB_INIT_MODE=setup \
-e DOCKER_INFLUXDB_INIT_USERNAME=admin \
-e DOCKER_INFLUXDB_INIT_PASSWORD=password12345 \
-e DOCKER_INFLUXDB_INIT_ORG=scrutiny \
-e DOCKER_INFLUXDB_INIT_BUCKET=metrics \
-e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token \
influxdb:2.2
go test ./...
```
+24
View File
@@ -9,6 +9,7 @@ BINARY=\
linux/arm-7 \
linux/arm64 \
.ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and provide it with the entire recipe, regardless of how many lines it contains.
.PHONY: all $(BINARY)
all: $(BINARY) windows/amd64
@@ -38,5 +39,28 @@ windows/amd64:
@echo "building collector binary (OS = $(OS), ARCH = $(ARCH))"
xgo -v --targets="$(OS)/$(ARCH)" -ldflags "-extldflags=-static -X main.goos=$(OS) -X main.goarch=$(ARCH)" -out scrutiny-collector-metrics -tags "static netgo" ${GO_WORKSPACE}/collector/cmd/collector-metrics/
docker-collector:
@echo "building collector docker image"
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile.collector -t analogj/scrutiny-dev:collector .
docker-web:
@echo "building web docker image"
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile.web -t analogj/scrutiny-dev:web .
docker-omnibus:
@echo "building omnibus docker image"
docker build --build-arg TARGETARCH=amd64 -f docker/Dockerfile -t analogj/scrutiny-dev:omnibus .
# reduce logging, disable angular-cli analytics for ci environment
frontend: export NPM_CONFIG_LOGLEVEL = warn
frontend: export NG_CLI_ANALYTICS = false
frontend:
cd webapp/frontend
npm install -g @angular/cli@9.1.4
mkdir -p $(CURDIR)/dist
npm ci
npm run build:prod -- --output-path=$(CURDIR)/dist
# clean:
# rm scrutiny-collector-metrics-* scrutiny-web-*
+6 -5
View File
@@ -17,8 +17,6 @@
WebUI for smartd S.M.A.R.T monitoring
> NOTE: Scrutiny is a Work-in-Progress and still has some rough edges.
>
> WARNING: Once the [InfluxDB](https://github.com/AnalogJ/scrutiny/tree/influxdb) branch is merged, Scrutiny will use both sqlite and InfluxDB for data storage. Unfortunately, this may not be backwards compatible with the database structures in the master (sqlite only) branch.
[![](docs/dashboard.png)](https://imgur.com/a/5k8qMzS)
@@ -60,11 +58,12 @@ Scrutiny uses `smartctl --scan` to detect devices/drives.
- All RAID controllers supported by `smartctl` are automatically supported by Scrutiny.
- While some RAID controllers support passing through the underlying SMART data to `smartctl` others do not.
- In some cases `--scan` does not correctly detect the device type, returning [incomplete SMART data](https://github.com/AnalogJ/scrutiny/issues/45).
Scrutiny will eventually support overriding detected device type via the config file.
Scrutiny supports overriding detected device type via the config file: see [example.collector.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml)
- If you use docker, you **must** pass though the RAID virtual disk to the container using `--device` (see below)
- This device may be in `/dev/*` or `/dev/bus/*`.
- If you're unsure, run `smartctl --scan` on your host, and pass all listed devices to the container.
See [docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md](./docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md) for help
## Docker
@@ -178,7 +177,9 @@ Scrutiny supports sending SMART device failure notifications via the following s
- Telegram
- Tulip
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for more information and documentation for service specific setup.
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for examples.
For more information and troubleshooting, see the [TROUBLESHOOTING_NOTIFICATIONS.md](./docs/TROUBLESHOOTING_NOTIFICATIONS.md) file
### Testing Notifications
@@ -239,7 +240,7 @@ scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
| arm-6 | :white_check_mark: | |
| arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
| arm64 | :white_check_mark: | :white_check_mark: |
| freebsd | :white_check_mark: | |
| freebsd | collector only. see [#238](https://github.com/AnalogJ/scrutiny/issues/238) | |
| macos-amd64 | | :white_check_mark: |
| macos-arm64 | | :white_check_mark: |
| windows-amd64 | :white_check_mark: | |
+4 -3
View File
@@ -116,12 +116,13 @@ func (mc *MetricsCollector) Collect(deviceWWN string, deviceName string, deviceT
}
mc.logger.Infof("Collecting smartctl results for %s\n", deviceName)
args := []string{"-x", "-j"}
fullDeviceName := fmt.Sprintf("%s%s", detect.DevicePrefix(), deviceName)
args := strings.Split(mc.config.GetCommandMetricsSmartArgs(fullDeviceName), " ")
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
if len(deviceType) > 0 && deviceType != "scsi" && deviceType != "ata" {
args = append(args, "-d", deviceType)
args = append(args, "--device", deviceType)
}
args = append(args, fmt.Sprintf("%s%s", detect.DevicePrefix(), deviceName))
args = append(args, fullDeviceName)
result, err := mc.shell.Command(mc.logger, "smartctl", args, "", os.Environ())
resultBytes := []byte(result)
+87 -5
View File
@@ -1,6 +1,7 @@
package config
import (
"fmt"
"github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/collector/pkg/errors"
"github.com/analogj/scrutiny/collector/pkg/models"
@@ -8,6 +9,8 @@ import (
"github.com/spf13/viper"
"log"
"os"
"sort"
"strings"
)
// When initializing this class the following methods must be called:
@@ -16,6 +19,8 @@ import (
// This is done automatically when created via the Factory.
type configuration struct {
*viper.Viper
deviceOverrides []models.ScanOverride
}
//Viper uses the following precedence order. Each item takes precedence over the item below it:
@@ -38,6 +43,10 @@ func (c *configuration) Init() error {
c.SetDefault("api.endpoint", "http://localhost:8080")
c.SetDefault("commands.metrics_scan_args", "--scan --json")
c.SetDefault("commands.metrics_info_args", "--info --json")
c.SetDefault("commands.metrics_smart_args", "--xall --json")
//c.SetDefault("collect.short.command", "-a -o on -S on")
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
@@ -90,16 +99,89 @@ func (c *configuration) ValidateConfig() error {
// check that device prefix matches OS
// check that schema of config file is valid
return nil
// check that the collector commands are valid
commandArgStrings := map[string]string{
"commands.metrics_scan_args": c.GetString("commands.metrics_scan_args"),
"commands.metrics_info_args": c.GetString("commands.metrics_info_args"),
"commands.metrics_smart_args": c.GetString("commands.metrics_smart_args"),
}
errorStrings := []string{}
for configKey, commandArgString := range commandArgStrings {
args := strings.Split(commandArgString, " ")
//ensure that the args string contains `--json` or `-j` flag
containsJsonFlag := false
containsDeviceFlag := false
for _, flag := range args {
if strings.HasPrefix(flag, "--json") || strings.HasPrefix(flag, "-j") {
containsJsonFlag = true
}
if strings.HasPrefix(flag, "--device") || strings.HasPrefix(flag, "-d") {
containsDeviceFlag = true
}
}
if !containsJsonFlag {
errorStrings = append(errorStrings, fmt.Sprintf("configuration key '%s' is missing '--json' flag", configKey))
}
if containsDeviceFlag {
errorStrings = append(errorStrings, fmt.Sprintf("configuration key '%s' must not contain '--device' or '-d' flag", configKey))
}
}
//sort(errorStrings)
sort.Strings(errorStrings)
if len(errorStrings) == 0 {
return nil
} else {
return errors.ConfigValidationError(strings.Join(errorStrings, ", "))
}
}
func (c *configuration) GetScanOverrides() []models.ScanOverride {
func (c *configuration) GetDeviceOverrides() []models.ScanOverride {
// we have to support 2 types of device types.
// - simple device type (device_type: 'sat')
// and list of device types (type: \n- 3ware,0 \n- 3ware,1 \n- 3ware,2)
// GetString will return "" if this is a list of device types.
overrides := []models.ScanOverride{}
c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true })
return overrides
if c.deviceOverrides == nil {
overrides := []models.ScanOverride{}
c.UnmarshalKey("devices", &overrides, func(c *mapstructure.DecoderConfig) { c.WeaklyTypedInput = true })
c.deviceOverrides = overrides
}
return c.deviceOverrides
}
func (c *configuration) GetCommandMetricsInfoArgs(deviceName string) string {
overrides := c.GetDeviceOverrides()
for _, deviceOverrides := range overrides {
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
//found matching device
if len(deviceOverrides.Commands.MetricsInfoArgs) > 0 {
return deviceOverrides.Commands.MetricsInfoArgs
} else {
return c.GetString("commands.metrics_info_args")
}
}
}
return c.GetString("commands.metrics_info_args")
}
func (c *configuration) GetCommandMetricsSmartArgs(deviceName string) string {
overrides := c.GetDeviceOverrides()
for _, deviceOverrides := range overrides {
if strings.ToLower(deviceName) == strings.ToLower(deviceOverrides.Device) {
//found matching device
if len(deviceOverrides.Commands.MetricsSmartArgs) > 0 {
return deviceOverrides.Commands.MetricsSmartArgs
} else {
return c.GetString("commands.metrics_smart_args")
}
}
}
return c.GetString("commands.metrics_smart_args")
}
+53 -3
View File
@@ -30,7 +30,7 @@ func TestConfiguration_GetScanOverrides_Simple(t *testing.T) {
//test
err := testConfig.ReadConfig(path.Join("testdata", "simple_device.yaml"))
require.NoError(t, err, "should correctly load simple device config")
scanOverrides := testConfig.GetScanOverrides()
scanOverrides := testConfig.GetDeviceOverrides()
//assert
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat"}, Ignore: false}}, scanOverrides)
@@ -45,7 +45,7 @@ func TestConfiguration_GetScanOverrides_Ignore(t *testing.T) {
//test
err := testConfig.ReadConfig(path.Join("testdata", "ignore_device.yaml"))
require.NoError(t, err, "should correctly load ignore device config")
scanOverrides := testConfig.GetScanOverrides()
scanOverrides := testConfig.GetDeviceOverrides()
//assert
require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}}, scanOverrides)
@@ -60,7 +60,7 @@ func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
//test
err := testConfig.ReadConfig(path.Join("testdata", "raid_device.yaml"))
require.NoError(t, err, "should correctly load ignore device config")
scanOverrides := testConfig.GetScanOverrides()
scanOverrides := testConfig.GetDeviceOverrides()
//assert
require.Equal(t, []models.ScanOverride{
@@ -75,3 +75,53 @@ func TestConfiguration_GetScanOverrides_Raid(t *testing.T) {
Ignore: false,
}}, scanOverrides)
}
func TestConfiguration_InvalidCommands_MissingJson(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "invalid_commands_missing_json.yaml"))
require.EqualError(t, err, `ConfigValidationError: "configuration key 'commands.metrics_scan_args' is missing '--json' flag"`, "should throw an error because json flag is missing")
}
func TestConfiguration_InvalidCommands_IncludesDevice(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "invalid_commands_includes_device.yaml"))
require.EqualError(t, err, `ConfigValidationError: "configuration key 'commands.metrics_info_args' must not contain '--device' or '-d' flag, configuration key 'commands.metrics_smart_args' must not contain '--device' or '-d' flag"`, "should throw an error because device flags detected")
}
func TestConfiguration_OverrideCommands(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "override_commands.yaml"))
require.NoError(t, err, "should not throw an error")
require.Equal(t, "--xall --json -T permissive", testConfig.GetString("commands.metrics_smart_args"))
}
func TestConfiguration_OverrideDeviceCommands_MetricsInfoArgs(t *testing.T) {
t.Parallel()
//setup
testConfig, _ := config.Create()
//test
err := testConfig.ReadConfig(path.Join("testdata", "override_device_commands.yaml"))
require.NoError(t, err, "should correctly override device command")
//assert
require.Equal(t, "--info --json -T permissive", testConfig.GetCommandMetricsInfoArgs("/dev/sda"))
require.Equal(t, "--info --json", testConfig.GetCommandMetricsInfoArgs("/dev/sdb"))
//require.Equal(t, []models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Commands: {MetricsInfoArgs: "--info --json -T "}}}, scanOverrides)
}
+3 -1
View File
@@ -22,5 +22,7 @@ type Interface interface {
GetStringSlice(key string) []string
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
GetScanOverrides() []models.ScanOverride
GetDeviceOverrides() []models.ScanOverride
GetCommandMetricsInfoArgs(deviceName string) string
GetCommandMetricsSmartArgs(deviceName string) string
}
+128 -99
View File
@@ -5,88 +5,37 @@
package mock_config
import (
reflect "reflect"
models "github.com/analogj/scrutiny/collector/pkg/models"
gomock "github.com/golang/mock/gomock"
viper "github.com/spf13/viper"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Init mocks base method
func (m *MockInterface) Init() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Init")
ret0, _ := ret[0].(error)
return ret0
}
// Init indicates an expected call of Init
func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
}
// ReadConfig mocks base method
func (m *MockInterface) ReadConfig(configFilePath string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadConfig", configFilePath)
ret0, _ := ret[0].(error)
return ret0
}
// ReadConfig indicates an expected call of ReadConfig
func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockInterface)(nil).ReadConfig), configFilePath)
}
// Set mocks base method
func (m *MockInterface) Set(key string, value interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Set", key, value)
}
// Set indicates an expected call of Set
func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockInterface)(nil).Set), key, value)
}
// SetDefault mocks base method
func (m *MockInterface) SetDefault(key string, value interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetDefault", key, value)
}
// SetDefault indicates an expected call of SetDefault
func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
}
// AllSettings mocks base method
// AllSettings mocks base method.
func (m *MockInterface) AllSettings() map[string]interface{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AllSettings")
@@ -94,27 +43,13 @@ func (m *MockInterface) AllSettings() map[string]interface{} {
return ret0
}
// AllSettings indicates an expected call of AllSettings
// AllSettings indicates an expected call of AllSettings.
func (mr *MockInterfaceMockRecorder) AllSettings() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllSettings", reflect.TypeOf((*MockInterface)(nil).AllSettings))
}
// IsSet mocks base method
func (m *MockInterface) IsSet(key string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsSet", key)
ret0, _ := ret[0].(bool)
return ret0
}
// IsSet indicates an expected call of IsSet
func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
}
// Get mocks base method
// Get mocks base method.
func (m *MockInterface) Get(key string) interface{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", key)
@@ -122,13 +57,13 @@ func (m *MockInterface) Get(key string) interface{} {
return ret0
}
// Get indicates an expected call of Get
// Get indicates an expected call of Get.
func (mr *MockInterfaceMockRecorder) Get(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), key)
}
// GetBool mocks base method
// GetBool mocks base method.
func (m *MockInterface) GetBool(key string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBool", key)
@@ -136,13 +71,55 @@ func (m *MockInterface) GetBool(key string) bool {
return ret0
}
// GetBool indicates an expected call of GetBool
// GetBool indicates an expected call of GetBool.
func (mr *MockInterfaceMockRecorder) GetBool(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockInterface)(nil).GetBool), key)
}
// GetInt mocks base method
// GetCommandMetricsInfoArgs mocks base method.
func (m *MockInterface) GetCommandMetricsInfoArgs(deviceName string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCommandMetricsInfoArgs", deviceName)
ret0, _ := ret[0].(string)
return ret0
}
// GetCommandMetricsInfoArgs indicates an expected call of GetCommandMetricsInfoArgs.
func (mr *MockInterfaceMockRecorder) GetCommandMetricsInfoArgs(deviceName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommandMetricsInfoArgs", reflect.TypeOf((*MockInterface)(nil).GetCommandMetricsInfoArgs), deviceName)
}
// GetCommandMetricsSmartArgs mocks base method.
func (m *MockInterface) GetCommandMetricsSmartArgs(deviceName string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCommandMetricsSmartArgs", deviceName)
ret0, _ := ret[0].(string)
return ret0
}
// GetCommandMetricsSmartArgs indicates an expected call of GetCommandMetricsSmartArgs.
func (mr *MockInterfaceMockRecorder) GetCommandMetricsSmartArgs(deviceName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommandMetricsSmartArgs", reflect.TypeOf((*MockInterface)(nil).GetCommandMetricsSmartArgs), deviceName)
}
// GetDeviceOverrides mocks base method.
func (m *MockInterface) GetDeviceOverrides() []models.ScanOverride {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDeviceOverrides")
ret0, _ := ret[0].([]models.ScanOverride)
return ret0
}
// GetDeviceOverrides indicates an expected call of GetDeviceOverrides.
func (mr *MockInterfaceMockRecorder) GetDeviceOverrides() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceOverrides", reflect.TypeOf((*MockInterface)(nil).GetDeviceOverrides))
}
// GetInt mocks base method.
func (m *MockInterface) GetInt(key string) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInt", key)
@@ -150,13 +127,13 @@ func (m *MockInterface) GetInt(key string) int {
return ret0
}
// GetInt indicates an expected call of GetInt
// GetInt indicates an expected call of GetInt.
func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
}
// GetString mocks base method
// GetString mocks base method.
func (m *MockInterface) GetString(key string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetString", key)
@@ -164,13 +141,13 @@ func (m *MockInterface) GetString(key string) string {
return ret0
}
// GetString indicates an expected call of GetString
// GetString indicates an expected call of GetString.
func (mr *MockInterfaceMockRecorder) GetString(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetString", reflect.TypeOf((*MockInterface)(nil).GetString), key)
}
// GetStringSlice mocks base method
// GetStringSlice mocks base method.
func (m *MockInterface) GetStringSlice(key string) []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStringSlice", key)
@@ -178,13 +155,79 @@ func (m *MockInterface) GetStringSlice(key string) []string {
return ret0
}
// GetStringSlice indicates an expected call of GetStringSlice
// GetStringSlice indicates an expected call of GetStringSlice.
func (mr *MockInterfaceMockRecorder) GetStringSlice(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStringSlice", reflect.TypeOf((*MockInterface)(nil).GetStringSlice), key)
}
// UnmarshalKey mocks base method
// Init mocks base method.
func (m *MockInterface) Init() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Init")
ret0, _ := ret[0].(error)
return ret0
}
// Init indicates an expected call of Init.
func (mr *MockInterfaceMockRecorder) Init() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInterface)(nil).Init))
}
// IsSet mocks base method.
func (m *MockInterface) IsSet(key string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsSet", key)
ret0, _ := ret[0].(bool)
return ret0
}
// IsSet indicates an expected call of IsSet.
func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
}
// ReadConfig mocks base method.
func (m *MockInterface) ReadConfig(configFilePath string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadConfig", configFilePath)
ret0, _ := ret[0].(error)
return ret0
}
// ReadConfig indicates an expected call of ReadConfig.
func (mr *MockInterfaceMockRecorder) ReadConfig(configFilePath interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockInterface)(nil).ReadConfig), configFilePath)
}
// Set mocks base method.
func (m *MockInterface) Set(key string, value interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Set", key, value)
}
// Set indicates an expected call of Set.
func (mr *MockInterfaceMockRecorder) Set(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockInterface)(nil).Set), key, value)
}
// SetDefault mocks base method.
func (m *MockInterface) SetDefault(key string, value interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetDefault", key, value)
}
// SetDefault indicates an expected call of SetDefault.
func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
}
// UnmarshalKey mocks base method.
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
m.ctrl.T.Helper()
varargs := []interface{}{key, rawVal}
@@ -196,23 +239,9 @@ func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts
return ret0
}
// UnmarshalKey indicates an expected call of UnmarshalKey
// UnmarshalKey indicates an expected call of UnmarshalKey.
func (mr *MockInterfaceMockRecorder) UnmarshalKey(key, rawVal interface{}, decoderOpts ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{key, rawVal}, decoderOpts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalKey", reflect.TypeOf((*MockInterface)(nil).UnmarshalKey), varargs...)
}
// GetScanOverrides mocks base method
func (m *MockInterface) GetScanOverrides() []models.ScanOverride {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetScanOverrides")
ret0, _ := ret[0].([]models.ScanOverride)
return ret0
}
// GetScanOverrides indicates an expected call of GetScanOverrides
func (mr *MockInterfaceMockRecorder) GetScanOverrides() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScanOverrides", reflect.TypeOf((*MockInterface)(nil).GetScanOverrides))
}
@@ -0,0 +1,4 @@
commands:
metrics_scan_args: '--scan --json' # used to detect devices
metrics_info_args: '--info --json --device=sat' # used to determine device unique ID & register device with Scrutiny
metrics_smart_args: '--xall --json -d sat' # used to retrieve smart data for each device.
@@ -0,0 +1,4 @@
commands:
metrics_scan_args: '--scan' # used to detect devices
metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny
metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.
+4
View File
@@ -0,0 +1,4 @@
commands:
metrics_scan_args: '--scan --json' # used to detect devices
metrics_info_args: '--info -j' # used to determine device unique ID & register device with Scrutiny
metrics_smart_args: '--xall --json -T permissive' # used to retrieve smart data for each device.
@@ -0,0 +1,5 @@
version: 1
devices:
- device: /dev/sda
commands:
metrics_info_args: "--info --json -T permissive"
+7 -6
View File
@@ -28,7 +28,8 @@ type Detect struct {
// models.Device returned from this function only contain the minimum data for smartctl to execute: device type and device name (device file).
func (d *Detect) SmartctlScan() ([]models.Device, error) {
//we use smartctl to detect all the drives available.
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, "smartctl", []string{"--scan", "-j"}, "", os.Environ())
args := strings.Split(d.Config.GetString("commands.metrics_scan_args"), " ")
detectedDeviceConnJson, err := d.Shell.Command(d.Logger, "smartctl", args, "", os.Environ())
if err != nil {
d.Logger.Errorf("Error scanning for devices: %v", err)
return nil, err
@@ -51,13 +52,13 @@ func (d *Detect) SmartctlScan() ([]models.Device, error) {
// - WWN is provided as component data, rather than a "string". We'll have to generate the WWN value ourselves
// - WWN from smartctl only provided for ATA protocol drives, NVMe and SCSI drives do not include WWN.
func (d *Detect) SmartCtlInfo(device *models.Device) error {
args := []string{"--info", "-j"}
fullDeviceName := fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName)
args := strings.Split(d.Config.GetCommandMetricsInfoArgs(fullDeviceName), " ")
//only include the device type if its a non-standard one. In some cases ata drives are detected as scsi in docker, and metadata is lost.
if len(device.DeviceType) > 0 && device.DeviceType != "scsi" && device.DeviceType != "ata" {
args = append(args, "-d", device.DeviceType)
args = append(args, "--device", device.DeviceType)
}
args = append(args, fmt.Sprintf("%s%s", DevicePrefix(), device.DeviceName))
args = append(args, fullDeviceName)
availableDeviceInfoJson, err := d.Shell.Command(d.Logger, "smartctl", args, "", os.Environ())
if err != nil {
@@ -138,7 +139,7 @@ func (d *Detect) TransformDetectedDevices(detectedDeviceConns models.Scan) []mod
//now tha we've "grouped" all the devices, lets override any groups specified in the config file.
for _, overrideDevice := range d.Config.GetScanOverrides() {
for _, overrideDevice := range d.Config.GetDeviceOverrides() {
overrideDeviceFile := strings.ToLower(overrideDevice.Device)
if overrideDevice.Ignore {
+16 -7
View File
@@ -18,7 +18,8 @@ func TestDetect_SmartctlScan(t *testing.T) {
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_simple.json")
@@ -45,7 +46,8 @@ func TestDetect_SmartctlScan_Megaraid(t *testing.T) {
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_megaraid.json")
@@ -75,7 +77,8 @@ func TestDetect_SmartctlScan_Nvme(t *testing.T) {
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeShell := mock_shell.NewMockInterface(mockCtrl)
testScanResults, err := ioutil.ReadFile("testdata/smartctl_scan_nvme.json")
@@ -104,7 +107,9 @@ func TestDetect_TransformDetectedDevices_Empty(t *testing.T) {
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
@@ -134,7 +139,9 @@ func TestDetect_TransformDetectedDevices_Ignore(t *testing.T) {
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: nil, Ignore: true}})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
@@ -163,7 +170,8 @@ func TestDetect_TransformDetectedDevices_Raid(t *testing.T) {
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{
{
Device: "/dev/bus/0",
DeviceType: []string{"megaraid,14", "megaraid,15", "megaraid,18", "megaraid,19", "megaraid,20", "megaraid,21"},
@@ -202,7 +210,8 @@ func TestDetect_TransformDetectedDevices_Simple(t *testing.T) {
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
fakeConfig.EXPECT().GetString("host.id").AnyTimes().Return("")
fakeConfig.EXPECT().GetScanOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
fakeConfig.EXPECT().GetString("commands.metrics_scan_args").AnyTimes().Return("--scan --json")
fakeConfig.EXPECT().GetDeviceOverrides().AnyTimes().Return([]models.ScanOverride{{Device: "/dev/sda", DeviceType: []string{"sat+megaraid"}}})
detectedDevices := models.Scan{
Devices: []models.ScanDevice{
{
+1 -1
View File
@@ -13,7 +13,7 @@ func DevicePrefix() string {
func (d *Detect) Start() ([]models.Device, error) {
d.Shell = shell.Create()
// call the base/common functionality to get a list of devicess
// call the base/common functionality to get a list of devices
detectedDevices, err := d.SmartctlScan()
if err != nil {
return nil, err
+52
View File
@@ -1,9 +1,12 @@
package detect
import (
"fmt"
"github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/models"
"github.com/jaypipes/ghw"
"io/ioutil"
"path/filepath"
"strings"
)
@@ -22,6 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) {
//inflate device info for detected devices.
for ndx, _ := range detectedDevices {
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
populateUdevInfo(&detectedDevices[ndx]) //ignore errors.
}
return detectedDevices, nil
@@ -49,3 +53,51 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
//wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
}
// as discussed in
// - https://github.com/AnalogJ/scrutiny/issues/225
// - https://github.com/jaypipes/ghw/issues/59#issue-361915216
// udev exposes its data in a standardized way under /run/udev/data/....
func populateUdevInfo(detectedDevice *models.Device) error {
// Get device major:minor numbers
// `cat /sys/class/block/sda/dev`
devNo, err := ioutil.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
if err != nil {
return err
}
// Look up block device in udev runtime database
// `cat /run/udev/data/b8:0`
udevID := "b" + strings.TrimSpace(string(devNo))
udevBytes, err := ioutil.ReadFile(filepath.Join("/run/udev/data/", udevID))
if err != nil {
return err
}
deviceMountPaths := []string{}
udevInfo := make(map[string]string)
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
if strings.HasPrefix(udevLine, "E:") {
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
udevInfo[s[0]] = s[1]
}
} else if strings.HasPrefix(udevLine, "S:") {
deviceMountPaths = append(deviceMountPaths, udevLine[2:])
}
}
//Set additional device information.
if deviceLabel, exists := udevInfo["ID_FS_LABEL"]; exists {
detectedDevice.DeviceLabel = deviceLabel
}
if deviceUUID, exists := udevInfo["ID_FS_UUID"]; exists {
detectedDevice.DeviceUUID = deviceUUID
}
if deviceSerialID, exists := udevInfo["ID_SERIAL"]; exists {
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
}
return nil
}
+9 -2
View File
@@ -1,10 +1,13 @@
package models
type Device struct {
WWN string `json:"wwn"`
HostId string `json:"host_id"`
WWN string `json:"wwn"`
DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
@@ -17,6 +20,10 @@ type Device struct {
SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`
}
type DeviceWrapper struct {
+4
View File
@@ -4,4 +4,8 @@ type ScanOverride struct {
Device string `mapstructure:"device"`
DeviceType []string `mapstructure:"type"`
Ignore bool `mapstructure:"ignore"`
Commands struct {
MetricsInfoArgs string `mapstructure:"metrics_info_args"`
MetricsSmartArgs string `mapstructure:"metrics_smart_args"`
} `mapstructure:"commands"`
}
+8 -23
View File
@@ -1,5 +1,5 @@
########
FROM golang:1.14.4-buster as backendbuild
FROM golang:1.17-bullseye as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
@@ -11,39 +11,24 @@ RUN go mod vendor && \
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
########
FROM node:lts-slim as frontendbuild
#reduce logging, disable angular-cli analytics for ci environment
ENV NPM_CONFIG_LOGLEVEL=warn NG_CLI_ANALYTICS=false
WORKDIR /opt/scrutiny/src
COPY webapp/frontend /opt/scrutiny/src
RUN npm install -g @angular/cli@9.1.4 && \
mkdir -p /scrutiny/dist && \
npm install && \
ng build --output-path=/opt/scrutiny/dist --prod
########
FROM ubuntu:bionic as runtime
FROM debian:bullseye-slim as runtime
ARG TARGETARCH
EXPOSE 8080
WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}"
ENV INFLUXD_CONFIG_PATH=/opt/scrutiny/influxdb
RUN apt-get update && apt-get install -y cron smartmontools=7.0-0ubuntu1~ubuntu18.04.1 ca-certificates curl tzdata \
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates curl tzdata \
&& update-ca-certificates \
&& case ${TARGETARCH} in \
"amd64") S6_ARCH=amd64 ;; \
"arm64") S6_ARCH=aarch64 ;; \
esac \
&& curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C /
ADD https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb /tmp/
RUN dpkg -i /tmp/influxdb2-2.2.0-${TARGETARCH}.deb && rm -rf /tmp/influxdb2-2.2.0-${TARGETARCH}.deb
&& tar xzf /tmp/s6-overlay-${S6_ARCH}.tar.gz -C / \
&& rm -rf /tmp/s6-overlay-${S6_ARCH}.tar.gz \
&& curl -L https://dl.influxdata.com/influxdb/releases/influxdb2-2.2.0-${TARGETARCH}.deb --output /tmp/influxdb2-2.2.0-${TARGETARCH}.deb \
&& dpkg -i --force-all /tmp/influxdb2-2.2.0-${TARGETARCH}.deb
COPY /rootfs /
@@ -51,7 +36,7 @@ COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /opt/scrutiny/bin/
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /opt/scrutiny/bin/
COPY --from=frontendbuild /opt/scrutiny/dist /opt/scrutiny/web
COPY dist /opt/scrutiny/web
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
chmod +x /opt/scrutiny/bin/scrutiny-collector-selftest && \
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics && \
+3 -3
View File
@@ -1,5 +1,5 @@
########
FROM golang:1.14.4-buster as backendbuild
FROM golang:1.17-bullseye as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
@@ -10,11 +10,11 @@ RUN go mod vendor && \
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
########
FROM ubuntu:bionic as runtime
FROM debian:bullseye-slim as runtime
WORKDIR /scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}"
RUN apt-get update && apt-get install -y cron smartmontools=7.0-0ubuntu1~ubuntu18.04.1 ca-certificates tzdata && update-ca-certificates
RUN apt-get update && apt-get install -y cron smartmontools ca-certificates tzdata && update-ca-certificates
COPY /docker/entrypoint-collector.sh /entrypoint-collector.sh
COPY /rootfs/etc/cron.d/scrutiny /etc/cron.d/scrutiny
+3 -18
View File
@@ -1,5 +1,5 @@
########
FROM golang:1.14.4-buster as backendbuild
FROM golang:1.17-bullseye as backendbuild
WORKDIR /go/src/github.com/analogj/scrutiny
@@ -9,22 +9,7 @@ RUN go mod vendor && \
go build -ldflags '-w -extldflags "-static"' -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go
########
FROM node:lts-slim as frontendbuild
#reduce logging, disable angular-cli analytics for ci environment
ENV NPM_CONFIG_LOGLEVEL=warn NG_CLI_ANALYTICS=false
WORKDIR /opt/scrutiny/src
COPY webapp/frontend /opt/scrutiny/src
RUN npm install -g @angular/cli@9.1.4 && \
mkdir -p /opt/scrutiny/dist && \
npm install && \
ng build --output-path=/opt/scrutiny/dist --prod
########
FROM ubuntu:bionic as runtime
FROM debian:bullseye-slim as runtime
EXPOSE 8080
WORKDIR /opt/scrutiny
ENV PATH="/opt/scrutiny/bin:${PATH}"
@@ -32,7 +17,7 @@ ENV PATH="/opt/scrutiny/bin:${PATH}"
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && update-ca-certificates
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /opt/scrutiny/bin/
COPY --from=frontendbuild /opt/scrutiny/dist /opt/scrutiny/web
COPY dist /opt/scrutiny/web
RUN chmod +x /opt/scrutiny/bin/scrutiny && \
mkdir -p /opt/scrutiny/web && \
mkdir -p /opt/scrutiny/config && \
+1 -1
View File
@@ -1,4 +1,4 @@
FROM techknowlogick/xgo:go-1.13.x
FROM techknowlogick/xgo:go-1.17.x
WORKDIR /go/src/github.com/analogj/scrutiny
+1 -1
View File
@@ -1 +1 @@
> See [docker/example.hubspoke.docker-compose.yml](./docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
> See [docker/example.hubspoke.docker-compose.yml](https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml) for a docker-compose file.
+5 -2
View File
@@ -57,7 +57,7 @@ web:
# and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide
# the `token`
influxdb:
host: 0.0.0.0
host: localhost
port: 8086
# token: 'my-token'
# org: 'my-org'
@@ -83,9 +83,11 @@ Now that we have downloaded the required files, let's prepare the filesystem.
chmod +x /opt/scrutiny/bin/scrutiny-web-linux-amd64
# Next, lets extract the frontend files.
# NOTE: after extraction, there **should not** be a `dist` subdirectory in `/opt/scrutiny/web` directory.
cd /opt/scrutiny/web
tar xvzf scrutiny-web-frontend.tar.gz --strip-components 1 -C .
# Cleanup
rm -rf scrutiny-web-frontend.tar.gz
```
@@ -113,7 +115,8 @@ Unlike the webapp, the collector does have some dependencies:
Unfortunately the version of `smartmontools` (which contains `smartctl`) available in some of the base OS repositories is ancient.
So you'll need to install the v7+ version using one of the following commands:
- **Ubuntu:** `apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1`
- **Ubuntu (22.04/Jammy/LTS):** `apt-get install -y smartmontools`
- **Ubuntu (18.04/Bionic):** `apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1`
- **Centos8:**
- `dnf install https://extras.getpagespeed.com/release-el8-latest.rpm`
- `dnf install smartmontools`
+72
View File
@@ -0,0 +1,72 @@
# pfsense Install
This bascially follows the [Manual collector instructions](https://github.com/AnalogJ/scrutiny/blob/master/docs/INSTALL_MANUAL.md#collector) and assumes you are running a hub and spoke deployment and already have the web app setup.
### Dependencies
SSH into pfsense, hit `8` for the shell and install the required dependencies.
```
pkg install smartmontools
```
Ensure smartmontools is v7+. This won't be a problem in pfsense 2.6.0+
### Directory Structure
Now let's create a directory structure to contain the Scrutiny collector binary.
```
mkdir -p /opt/scrutiny/bin
```
### Download Files
Next, we'll download the Scrutiny collector binary from the [latest Github release](https://github.com/analogj/scrutiny/releases).
> NOTE: Ensure you have the latest version in the below command
```
fetch -o /opt/scrutiny/bin https://github.com/AnalogJ/scrutiny/releases/download/vX.X.X/scrutiny-collector-metrics-freebsd-amd64
```
### Prepare Scrutiny
Now that we have downloaded the required files, let's prepare the filesystem.
```
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics-freebsd-amd64
```
### Start Scrutiny Collector, Populate Webapp
Next, we will manually trigger the collector, to populate the Scrutiny dashboard:
> NOTE: if you need to pass a config file to the scrutiny collector, you can provide it using the `--config` flag.
```
/opt/scrutiny/bin/scrutiny-collector-metrics-freebsd-amd64 run --api-endpoint "http://localhost:8080"
```
> NOTE: change the IP address to that of your web app
### Schedule Collector with Cron
Finally you need to schedule the collector to run periodically.
Login to the pfsense webGUI and head to `Services/Cron` add an entry with the following details:
```
Minute: */15
Hour: *
Day of the Month: *
Month of the Year: *
Day of the Week: *
User: root
Command: /opt/scrutiny/bin/scrutiny-collector-metrics-freebsd-amd64 run --api-endpoint "http://localhost:8080" >/dev/null 2>&1
```
> NOTE: `>/dev/null 2>&1` is used to stop cron confirmation emails being sent.
+3 -1
View File
@@ -4,11 +4,13 @@ These are the officially supported NAS OS's (with documentation and setup guides
Once a guide is created (in `docs/guides/`) it will be linked here.
- [ ] freenas/truenas
- [x] [unraid](https://github.com/AnalogJ/scrutiny/blob/master/docs/INSTALL_UNRAID.md)
- [x] [unraid](./INSTALL_UNRAID.md)
- [ ] ESXI
- [ ] Proxmox
- [ ] Synology
- [ ] OMV
- [ ] Amahi
- [ ] Running in a LXC container
- [x] [PFSense](./INSTALL_UNRAID.md)
- [ ] QNAP
+65 -1
View File
@@ -52,6 +52,8 @@ If the output is the same, your devices will be processed by Scrutiny.
In some cases `--scan` does not correctly detect the device type, returning [incomplete SMART data](https://github.com/AnalogJ/scrutiny/issues/45).
Scrutiny will supports overriding the detected device type via the config file.
[example.collector.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml)
### RAID Controllers (Megaraid/3ware/HBA/Adaptec/HPE/etc)
Smartctl has support for a large number of [RAID controllers](https://www.smartmontools.org/wiki/Supported_RAID-Controllers), however this
support is not automatic, and may require some additional device type hinting. You can provide this information to the Scrutiny collector
@@ -111,12 +113,60 @@ instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/Anal
### ATA
### Standby/Sleeping Disks
### Exit Codes
If you see an error message similar to `smartctl returned an error code (2) while processing /dev/sda`, this means that
`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you debug,
but you can look at the table (and associated links) below to debug `smartctl`.
> smartctl Return Values
> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of
> smartctl is 0 (all bits turned off). If a problem occurs, or an error, potential error, or fault is detected, then
> a non-zero status is returned. In this case, the eight different bits in the return value have the following meanings
> for ATA disks; some of these values may also be returned for SCSI disks.
>
> source: http://www.linuxguide.it/command_line/linux-manpage/do.php?file=smartctl#sect7
| Exit Code (Isolated) | Binary | Problem Message |
| --- | --- | --- |
| 1 | Bit 0 | Command line did not parse. |
| 2 | Bit 1 | Device open failed, or device did not return an IDENTIFY DEVICE structure. |
| 4 | Bit 2 | Some SMART command to the disk failed, or there was a checksum error in a SMART data structure (see В´-bВ´ option above). |
| 8 | Bit 3 | SMART status check returned “DISK FAILING". |
| 16 | Bit 4 | We found prefail Attributes <= threshold. |
| 32 | Bit 5 | SMART status check returned “DISK OK” but we found that some (usage or prefail) Attributes have been <= threshold at some time in the past. |
| 64 | Bit 6 | The device error log contains records of errors. |
| 128 | Bit 7 | The device self-test log contains records of errors. |
#### Standby/Sleeping Disks
Disks in Standby/Sleep can also cause `smartctl` to exit abnormally, usually with `exit code: 2`.
- https://github.com/AnalogJ/scrutiny/issues/221
- https://github.com/AnalogJ/scrutiny/issues/157
### Volume Mount All Devices (`/dev`) - Privileged
> WARNING: This is an insecure/dangerous workaround. Running Scrutiny (or any Docker image) with `--privileged` is equivalent to running it with root access.
If you have exhausted all other mechanisms to get your disks working with `smartctl` running within a container, you can try running the docker image with the following additional flags:
- `--privileged` (instead of `--cap-add`) - this gives the docker container full access to your system. Scrutiny does not require this permission, however it can be helpful for `smartctl`
- `-v /dev:/dev:ro` (instead of `--device`) - this mounts the `/dev` folder (containing all your device files) into the container, allowing `smartctl` to see your disks, exactly as if it were running on your host directly.
With this workaround your `docker run` command would look similar to the following:
```bash
docker run -it --rm -p 8080:8080 -p 8086:8086 \
-v `pwd`/scrutiny:/opt/scrutiny/config \
-v `pwd`/influxdb2:/opt/scrutiny/influxdb \
-v /run/udev:/run/udev:ro \
--privileged \
-v /dev:/dev \
--name scrutiny \
ghcr.io/analogj/scrutiny:master-omnibus
```
## Scrutiny detects Failure but SMART Passed?
@@ -138,3 +188,17 @@ Thankfully the collector has a special `--host-id` flag (or `COLLECTOR_HOST_ID`
See the [docs/INSTALL_HUB_SPOKE.md](/docs/INSTALL_HUB_SPOKE.md) guide for more information.
## Collector DEBUG mode
You can use environmental variables to enable debug logging and/or log files for the collector:
```bash
DEBUG=true
COLLECTOR_LOG_FILE=/tmp/collector.log
```
Or if you're not using docker, you can pass CLI arguments to the collector during startup:
```bash
scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
```
+25
View File
@@ -0,0 +1,25 @@
# Notifications
As documented in [example.scrutiny.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml#L59-L75)
there are multiple ways to configure notifications for Scrutiny.
Under the hood we use a library called [Shoutrrr](https://github.com/containrrr/shoutrrr) to send our notifications, and you should use their documentation if you run into
any issues: https://containrrr.dev/shoutrrr/services/overview/
# Script Notifications
While the Shoutrrr library supports many popular providers for sending notifications Scrutiny also supports a "script" based
notification system, allowing you to execute a custom script whenever a notification needs to be sent.
Data is provided to this script using the following environmental variables:
```
SCRUTINY_SUBJECT - eg. "Scrutiny SMART error (%s) detected on device: %s"
SCRUTINY_DATE
SCRUTINY_FAILURE_TYPE - EmailTest, SmartFail, ScrutinyFail
SCRUTINY_DEVICE_NAME - eg. /dev/sda
SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe
SCRUTINY_DEVICE_SERIAL - eg. WDDJ324KSO
SCRUTINY_MESSAGE - eg. "Scrutiny SMART error notification for device: %s\nFailure Type: %s\nDevice Name: %s\nDevice Serial: %s\nDevice Type: %s\nDate: %s"
```
+49 -1
View File
@@ -55,4 +55,52 @@ api:
You may also configure these values using the following environmental variables (both are required).
- `COLLECTOR_API_ENDPOINT=http://localhost:8080/custombasepath`
- `SCRUTINY_WEB_LISTEN_BASEPATH=/custombasepath`
- `SCRUTINY_WEB_LISTEN_BASEPATH=/custombasepath`
# Real Examples
## Caddy
1. Create a Caddyfile
```yaml
# Caddyfile
:9090
# The `scrutiny` text in this file must match the service name in the docker-compose file below.
# The `/custom/` text is the custom base path scrutiny will be availble on.
reverse_proxy /custom/* scrutiny:8080
```
2. Create a `docker-compose.yml` file
```yaml
# docker-compose.yml
version: '3.5'
services:
scrutiny:
container_name: scrutiny
image: ghcr.io/analogj/scrutiny:master-omnibus
cap_add:
- SYS_RAWIO
ports:
- "8086:8086" # influxDB admin
volumes:
- /run/udev:/run/udev:ro
- ./config:/opt/scrutiny/config
- ./influxdb:/opt/scrutiny/influxdb
devices:
- "/dev/sda"
- "/dev/sdb"
environment:
- SCRUTINY_WEB_LISTEN_BASEPATH=/custom
- COLLECTOR_API_ENDPOINT=http://localhost:8080/custom
caddy:
image: caddy
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
ports:
- "9090:9090"
```
3. run `docker-compose up`
4. visit [http://localhost:9090/custom/web](http://localhost:9090/custom/web) - access the scrutiny container via caddy reverse proxy
+13
View File
@@ -53,6 +53,13 @@ devices:
# - 3ware,3
# - 3ware,4
# - 3ware,5
#
# # example to show how to override the smartctl command args (per device), see below for how to override these globally.
# - device: /dev/sda
# commands:
# metrics_info_args: '--info --json -T permissive' # used to determine device unique ID & register device with Scrutiny
# metrics_smart_args: '--xall --json -T permissive' # used to retrieve smart data for each device.
#log:
# file: '' #absolute or relative paths allowed, eg. web.log
@@ -64,6 +71,12 @@ devices:
# if you need to use a custom base path (for a reverse proxy), you can add a suffix to the endpoint.
# See docs/TROUBLESHOOTING_REVERSE_PROXY.md for more info,
# example to show how to override the smartctl command args globally
#commands:
# metrics_scan_args: '--scan --json' # used to detect devices
# metrics_info_args: '--info --json' # used to determine device unique ID & register device with Scrutiny
# metrics_smart_args: '--xall --json' # used to retrieve smart data for each device.
########################################################################################################################
# FEATURES COMING SOON
+1
View File
@@ -40,6 +40,7 @@ web:
# and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide
# the `token`
influxdb:
# scheme: 'http'
host: 0.0.0.0
port: 8086
# token: 'my-token'
+61 -9
View File
@@ -1,31 +1,83 @@
module github.com/analogj/scrutiny
go 1.13
go 1.17
require (
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
github.com/citilinkru/libudev v1.0.0 // indirect
github.com/containrrr/shoutrrr v0.4.4
github.com/fatih/color v1.10.0
github.com/gin-gonic/gin v1.6.3
github.com/go-gormigrate/gormigrate/v2 v2.0.0
github.com/golang/mock v1.4.3
github.com/google/uuid v1.2.0 // indirect
github.com/influxdata/influxdb-client-go/v2 v2.2.3
github.com/influxdata/influxdb-client-go/v2 v2.9.0
github.com/jaypipes/ghw v0.6.1
github.com/jinzhu/gorm v1.9.16
github.com/klauspost/compress v1.12.1 // indirect
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // indirect
github.com/mattn/go-sqlite3 v1.14.4 // indirect
github.com/mitchellh/mapstructure v1.2.2
github.com/onsi/ginkgo v1.16.1 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.5.1
github.com/urfave/cli/v2 v2.2.0
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect
gorm.io/driver/sqlite v1.1.3
gorm.io/gorm v1.20.2
)
require (
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/citilinkru/libudev v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.8.2 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.2.0 // indirect
github.com/golang/protobuf v1.4.2 // indirect
github.com/google/uuid v1.2.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.1 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/klauspost/compress v1.12.1 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // 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.12 // indirect
github.com/mattn/go-sqlite3 v1.14.4 // 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/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.1 // indirect
github.com/pelletier/go-toml v1.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.23.0 // indirect
gopkg.in/ini.v1 v1.55.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
gosrc.io/xmpp v0.5.1 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
nhooyr.io/websocket v1.8.7 // indirect
)
+62 -18
View File
@@ -11,6 +11,7 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
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=
@@ -60,9 +61,10 @@ github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepmap/oapi-codegen v1.3.13 h1:9HKGCsdJqE4dnrQ8VerFS0/1ZOJPmAhN+g8xgp8y3K4=
github.com/deepmap/oapi-codegen v1.3.13/go.mod h1:WAmG5dWY8/PYHt4vKxlt90NsbHMAOCiteYKZMiIRfOo=
github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
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=
@@ -75,14 +77,14 @@ github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE
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/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
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=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
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-gormigrate/gormigrate/v2 v2.0.0 h1:e2A3Uznk4viUC4UuemuVgsNnvYZyOA8B3awlYk3UioU=
github.com/go-gormigrate/gormigrate/v2 v2.0.0/go.mod h1:YuVJ+D/dNt4HWrThTBnjgZuRbt7AuwINeg4q52ZE3Jw=
@@ -93,6 +95,9 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-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=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@@ -100,15 +105,20 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
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-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/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=
@@ -136,6 +146,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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=
@@ -149,9 +160,12 @@ github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.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/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -179,12 +193,14 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb-client-go/v2 v2.2.3 h1:082jdJ5t1CFeo0rpGQvKAK1mONVSbFhL4finWA5bRM8=
github.com/influxdata/influxdb-client-go/v2 v2.2.3/go.mod h1:fa/d1lAdUHxuc1jedx30ZfNG573oQTQmUni3N6pcW+0=
github.com/influxdata/influxdb-client-go/v2 v2.9.0 h1:1Ejxpt+cpWkadefxd5xvVx7pFgFaafdNp1ItfHzKRW4=
github.com/influxdata/influxdb-client-go/v2 v2.9.0/go.mod h1:x7Jo5UHHl+w8wu8UnGiNobDDHygojXwJX4mx7rXGKMk=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
@@ -192,18 +208,24 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.6.4 h1:S7T6cx5o2OqmxdHaXLH1ZeD1SbI8jBznyYE9Ec0RCQ8=
github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY=
github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
@@ -211,6 +233,7 @@ github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrU
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.4.2 h1:t+6LWm5eWPLX1H5Se702JSBcirq6uWa4jiG4wV1rAWY=
github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
@@ -218,11 +241,13 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.8.1 h1:SUbCLP2pXvf/Sr/25KsuI4aTxiFYIvpfk4l6aTSdyCw=
github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.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/jaypipes/ghw v0.6.1 h1:Ewt3mdpiyhWotGyzg1ursV/6SnToGcG4215X6rR2af8=
github.com/jaypipes/ghw v0.6.1/go.mod h1:QOXppNRCLGYR1H+hu09FxZPqjNt09bqUZUnOL3Rcero=
@@ -236,12 +261,14 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/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/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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@@ -255,13 +282,15 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 h1:3tLzEnUizyN9YLWFTT9loC30lSBvh2y70LTDcZOTs1s=
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
@@ -282,8 +311,8 @@ github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIG
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.4/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=
@@ -291,7 +320,6 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
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.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
@@ -331,6 +359,7 @@ github.com/onsi/ginkgo v1.16.1 h1:foqVmeWDD6yYpK+Yz3fHyNIxFYNxswxqNFjSKe+vI54=
github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -371,7 +400,9 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
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=
@@ -416,7 +447,7 @@ github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
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.1.0/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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -441,15 +472,15 @@ golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
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/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=
@@ -488,12 +519,13 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR
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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/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-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
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-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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=
@@ -531,27 +563,35 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20200826173525-f9321e4c35a6/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-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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/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 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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=
@@ -616,6 +656,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@@ -634,11 +675,14 @@ 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 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
gorm.io/driver/postgres v1.0.0/go.mod h1:wtMFcOzmuA5QigNsgEIb7O5lhvH1tHAF1RbWmLWV4to=
gorm.io/driver/sqlite v1.1.1/go.mod h1:hm2olEcl8Tmsc6eZyxYSeznnsDaMqamBvEXLNtBg4cI=
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
gorm.io/driver/sqlserver v1.0.2 h1:FzxAlw0/7hntMzSiNfotpYCo9Lz8dqWQGdmCGqIiFGo=
gorm.io/driver/sqlserver v1.0.2/go.mod h1:gb0Y9QePGgqjzrVyTQUZeh9zkd5v0iz71cM1B4ZycEY=
gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
+3 -1
View File
@@ -39,12 +39,14 @@ func (c *configuration) Init() error {
c.SetDefault("notify.urls", []string{})
c.SetDefault("web.influxdb.host", "0.0.0.0")
c.SetDefault("web.influxdb.scheme", "http")
c.SetDefault("web.influxdb.host", "localhost")
c.SetDefault("web.influxdb.port", "8086")
c.SetDefault("web.influxdb.org", "scrutiny")
c.SetDefault("web.influxdb.bucket", "metrics")
c.SetDefault("web.influxdb.init_username", "admin")
c.SetDefault("web.influxdb.init_password", "password12345")
c.SetDefault("web.influxdb.token", "scrutiny-default-admin-token")
c.SetDefault("web.influxdb.retention_policy", true)
//c.SetDefault("disks.include", []string{})
+27 -18
View File
@@ -4,25 +4,34 @@ const DeviceProtocolAta = "ATA"
const DeviceProtocolScsi = "SCSI"
const DeviceProtocolNvme = "NVMe"
const SmartAttributeStatusPassed = 0
const SmartAttributeStatusFailed = 1
const SmartAttributeStatusWarning = 2
const SmartWhenFailedFailingNow = "FAILING_NOW"
const SmartWhenFailedInThePast = "IN_THE_PAST"
//const SmartStatusPassed = "passed"
//const SmartStatusFailed = "failed"
type DeviceStatus int
type AttributeStatus uint8
const (
DeviceStatusPassed DeviceStatus = 0
DeviceStatusFailedSmart DeviceStatus = iota
DeviceStatusFailedScrutiny DeviceStatus = iota
// AttributeStatusPassed binary, 1,2,4,8,16,32,etc
AttributeStatusPassed AttributeStatus = 0
AttributeStatusFailedSmart AttributeStatus = 1
AttributeStatusWarningScrutiny AttributeStatus = 2
AttributeStatusFailedScrutiny AttributeStatus = 4
)
func Set(b, flag DeviceStatus) DeviceStatus { return b | flag }
func Clear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
func Toggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
func Has(b, flag DeviceStatus) bool { return b&flag != 0 }
const AttributeWhenFailedFailingNow = "FAILING_NOW"
const AttributeWhenFailedInThePast = "IN_THE_PAST"
func AttributeStatusSet(b, flag AttributeStatus) AttributeStatus { return b | flag }
func AttributeStatusClear(b, flag AttributeStatus) AttributeStatus { return b &^ flag }
func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^ flag }
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
type DeviceStatus uint8
const (
// DeviceStatusPassed binary, 1,2,4,8,16,32,etc
DeviceStatusPassed DeviceStatus = 0
DeviceStatusFailedSmart DeviceStatus = 1
DeviceStatusFailedScrutiny DeviceStatus = 2
)
func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | flag }
func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 }
+1
View File
@@ -19,6 +19,7 @@ type DeviceRepo interface {
UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error)
UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error)
GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error)
DeleteDevice(ctx context.Context, wwn string) error
SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error)
GetSmartAttributeHistory(ctx context.Context, wwn string, durationKey string, attributes []string) ([]measurements.Smart, error)
@@ -5,6 +5,7 @@ import (
"time"
)
// Deprecated: m20220503120000.Device is deprecated, only used by db migrations
type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time.Time
@@ -0,0 +1,41 @@
package m20220509170100
import (
"github.com/analogj/scrutiny/webapp/backend/pkg"
"time"
)
type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
InterfaceSpeed string `json:"interface_speed"`
SerialNumber string `json:"serial_number"`
Firmware string `json:"firmware"`
RotationSpeed int `json:"rotational_speed"`
Capacity int64 `json:"capacity"`
FormFactor string `json:"form_factor"`
SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`
// Data set by Scrutiny
DeviceStatus pkg.DeviceStatus `json:"device_status"`
}
@@ -2,6 +2,7 @@ package database
import (
"context"
"encoding/json"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
@@ -11,6 +12,9 @@ import (
"github.com/sirupsen/logrus"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"io/ioutil"
"net/http"
"net/url"
"time"
)
@@ -75,36 +79,38 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Create a new client using an InfluxDB server base URL and an authentication token
influxdbUrl := fmt.Sprintf("http://%s:%s", appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
influxdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.influxdb.scheme"), appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
globalLogger.Debugf("InfluxDB url: %s", influxdbUrl)
client := influxdb2.NewClient(influxdbUrl, appConfig.GetString("web.influxdb.token"))
if !appConfig.IsSet("web.influxdb.token") {
globalLogger.Debugf("No influxdb token found, running first-time setup...")
//if !appConfig.IsSet("web.influxdb.token") {
globalLogger.Debugf("Determine Influxdb setup status...")
influxSetupComplete, err := InfluxSetupComplete(influxdbUrl)
if err != nil {
return nil, fmt.Errorf("failed to check influxdb setup status - %w", err)
}
if !influxSetupComplete {
globalLogger.Debugf("Influxdb un-initialized, running first-time setup...")
// if no token is provided, but we have a valid server, we're going to assume this is the first setup of our server.
// we will initialize with a predetermined username & password, that you should change.
// metrics bucket will have a retention period of 8 days (since it will be down-sampled once a week)
// in seconds (60seconds * 60minutes * 24hours * 15 days) = 1_296_000 (see EnsureBucket() function)
onboardingResponse, err := client.Setup(
_, err := client.SetupWithToken(
backgroundContext,
appConfig.GetString("web.influxdb.init_username"),
appConfig.GetString("web.influxdb.init_password"),
appConfig.GetString("web.influxdb.org"),
appConfig.GetString("web.influxdb.bucket"),
0)
0,
appConfig.GetString("web.influxdb.token"),
)
if err != nil {
return nil, err
}
appConfig.Set("web.influxdb.token", *onboardingResponse.Auth.Token)
// we should write the config file out here. Ignore failures.
err = appConfig.WriteConfig()
if err != nil {
globalLogger.Infof("ignoring error while writing influxdb info to config: %v", err)
}
}
// Use blocking write client for writes to desired bucket
@@ -176,6 +182,37 @@ func (sr *scrutinyRepository) Close() error {
return nil
}
func InfluxSetupComplete(influxEndpoint string) (bool, error) {
influxUri, err := url.Parse(influxEndpoint)
if err != nil {
return false, err
}
influxUri, err = influxUri.Parse("/api/v2/setup")
if err != nil {
return false, err
}
res, err := http.Get(influxUri.String())
if err != nil {
return false, err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return false, err
}
type SetupStatus struct {
Allowed bool `json:"allowed"`
}
var data SetupStatus
err = json.Unmarshal(body, &data)
if err != nil {
return false, err
}
return !data.Allowed, nil
}
func (sr *scrutinyRepository) EnsureBuckets(ctx context.Context, org *domain.Organization) error {
var mainBucketRetentionRule domain.RetentionRule
@@ -7,6 +7,7 @@ import (
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"gorm.io/gorm/clause"
"time"
)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -18,7 +19,7 @@ import (
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "wwn"}},
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type"}),
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type", "device_uuid", "device_serial_id", "device_label"}),
}).Create(&dev).Error; err != nil {
return err
}
@@ -57,7 +58,7 @@ func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string
return device, fmt.Errorf("Could not get device from DB: %v", err)
}
device.DeviceStatus = pkg.Set(device.DeviceStatus, status)
device.DeviceStatus = pkg.DeviceStatusSet(device.DeviceStatus, status)
return device, sr.gormClient.Model(&device).Updates(device).Error
}
@@ -72,3 +73,33 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string)
return device, nil
}
func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) error {
if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).Delete(&models.Device{}).Error; err != nil {
return err
}
//delete data from influxdb.
buckets := []string{
sr.appConfig.GetString("web.influxdb.bucket"),
fmt.Sprintf("%s_weekly", sr.appConfig.GetString("web.influxdb.bucket")),
fmt.Sprintf("%s_monthly", sr.appConfig.GetString("web.influxdb.bucket")),
fmt.Sprintf("%s_yearly", sr.appConfig.GetString("web.influxdb.bucket")),
}
for _, bucket := range buckets {
sr.logger.Infof("Deleting data for %s in bucket: %s", wwn, bucket)
if err := sr.influxClient.DeleteAPI().DeleteWithName(
ctx,
sr.appConfig.GetString("web.influxdb.org"),
bucket,
time.Now().AddDate(-10, 0, 0),
time.Now(),
fmt.Sprintf(`device_wwn="%s"`, wwn),
); err != nil {
return err
}
}
return nil
}
@@ -2,14 +2,18 @@ package database
import (
"context"
"errors"
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
"github.com/go-gormigrate/gormigrate/v2"
"github.com/influxdata/influxdb-client-go/v2/api/http"
_ "github.com/jinzhu/gorm/dialects/sqlite"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"strconv"
"time"
@@ -22,9 +26,12 @@ import (
func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
sr.logger.Infoln("Database migration starting")
sr.logger.Infoln("Database migration starting. Please wait, this process may take a long time....")
m := gormigrate.New(sr.gormClient, gormigrate.DefaultOptions, []*gormigrate.Migration{
gormMigrateOptions := gormigrate.DefaultOptions
gormMigrateOptions.UseTransaction = true
m := gormigrate.New(sr.gormClient, gormMigrateOptions, []*gormigrate.Migration{
{
ID: "20201107210306", // v0.3.13 (pre-influxdb schema). 9fac3c6308dc6cb6cd5bbc43a68cd93e8fb20b87
Migrate: func(tx *gorm.DB) error {
@@ -38,16 +45,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
&m20201107210306.SmartNvmeAttribute{},
)
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable(
&m20201107210306.Device{},
&m20201107210306.Smart{},
&m20201107210306.SmartAtaAttribute{},
&m20201107210306.SmartNvmeAttribute{},
&m20201107210306.SmartNvmeAttribute{},
"self_tests",
)
},
},
{
ID: "20220503113100", // backwards compatible - influxdb schema
@@ -137,7 +134,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
smartTags,
smartFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
@@ -147,7 +144,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
tempTags,
tempFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
}
@@ -166,7 +163,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
smartFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
@@ -176,7 +173,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
tempTags,
tempFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
}
@@ -194,7 +191,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
smartTags,
smartFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
@@ -204,7 +201,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
tempTags,
tempFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
}
@@ -221,7 +218,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
smartTags,
smartFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
@@ -231,7 +228,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
tempTags,
tempFields,
postSmartResults.Date, ctx)
if err != nil {
if ignorePastRetentionPolicyError(err) != nil {
return err
}
}
@@ -257,20 +254,44 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return err
}
//migrate the device database to the current version
//migrate the device database
return tx.AutoMigrate(m20220503120000.Device{})
},
},
{
ID: "m20220509170100", // addl udev device data
Migrate: func(tx *gorm.DB) error {
//migrate the device database.
// adding addl columns (device_label, device_uuid, device_serial_id)
return tx.AutoMigrate(m20220509170100.Device{})
},
},
})
if err := m.Migrate(); err != nil {
sr.logger.Errorf("Database migration failed with error: %w", err)
sr.logger.Errorf("Database migration failed with error. \n Please open a github issue at https://github.com/AnalogJ/scrutiny and attach a copy of your scrutiny.db file. \n %v", err)
return err
}
sr.logger.Infoln("Database migration completed successfully")
return nil
}
// helpers
//When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy.
//This function will ignore retention policy errors, and allow the migration to continue.
func ignorePastRetentionPolicyError(err error) error {
var influxDbWriteError *http.Error
if errors.As(err, &influxDbWriteError) {
if influxDbWriteError.StatusCode == 422 {
log.Infoln("ignoring error: attempted to writePoint past retention period duration")
return nil
}
}
return err
}
// Deprecated
func m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp(preDevice m20201107210306.Device, preSmartResult m20201107210306.Smart) (error, measurements.SmartTemperature) {
//extract temperature data for every datapoint
+5 -1
View File
@@ -21,6 +21,10 @@ type Device struct {
WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
@@ -162,7 +166,7 @@ func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInfo) error {
dv.DeviceProtocol = info.Device.Protocol
if !info.SmartStatus.Passed {
dv.DeviceStatus = pkg.Set(dv.DeviceStatus, pkg.DeviceStatusFailedSmart)
dv.DeviceStatus = pkg.DeviceStatusSet(dv.DeviceStatus, pkg.DeviceStatusFailedSmart)
}
return nil
@@ -110,7 +110,7 @@ func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) er
sm.PowerCycleCount = info.PowerCycleCount
sm.PowerOnHours = info.PowerOnTime.Hours
if !info.SmartStatus.Passed {
sm.Status = pkg.DeviceStatusFailedSmart
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedSmart)
}
sm.DeviceProtocol = info.Device.Protocol
@@ -148,8 +148,9 @@ func (sm *Smart) ProcessAtaSmartInfo(tableItems []collector.AtaSmartAttributesTa
}
attrModel.PopulateAttributeStatus()
sm.Attributes[strconv.Itoa(collectorAttr.ID)] = &attrModel
if attrModel.Status == pkg.SmartAttributeStatusFailed {
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny)
if pkg.AttributeStatusHas(attrModel.Status, pkg.AttributeStatusFailedScrutiny) {
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
}
}
}
@@ -171,15 +172,15 @@ func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.Nv
"power_on_hours": (&SmartNvmeAttribute{AttributeId: "power_on_hours", Value: nvmeSmartHealthInformationLog.PowerOnHours, Threshold: -1}).PopulateAttributeStatus(),
"unsafe_shutdowns": (&SmartNvmeAttribute{AttributeId: "unsafe_shutdowns", Value: nvmeSmartHealthInformationLog.UnsafeShutdowns, Threshold: -1}).PopulateAttributeStatus(),
"media_errors": (&SmartNvmeAttribute{AttributeId: "media_errors", Value: nvmeSmartHealthInformationLog.MediaErrors, Threshold: 0}).PopulateAttributeStatus(),
"num_err_log_entries": (&SmartNvmeAttribute{AttributeId: "num_err_log_entries", Value: nvmeSmartHealthInformationLog.NumErrLogEntries, Threshold: 0}).PopulateAttributeStatus(),
"num_err_log_entries": (&SmartNvmeAttribute{AttributeId: "num_err_log_entries", Value: nvmeSmartHealthInformationLog.NumErrLogEntries, Threshold: -1}).PopulateAttributeStatus(),
"warning_temp_time": (&SmartNvmeAttribute{AttributeId: "warning_temp_time", Value: nvmeSmartHealthInformationLog.WarningTempTime, Threshold: -1}).PopulateAttributeStatus(),
"critical_comp_time": (&SmartNvmeAttribute{AttributeId: "critical_comp_time", Value: nvmeSmartHealthInformationLog.CriticalCompTime, Threshold: -1}).PopulateAttributeStatus(),
}
//find analyzed attribute status
for _, val := range sm.Attributes {
if val.GetStatus() == pkg.SmartAttributeStatusFailed {
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny)
if pkg.AttributeStatusHas(val.GetStatus(), pkg.AttributeStatusFailedScrutiny) {
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
}
}
}
@@ -204,8 +205,8 @@ func (sm *Smart) ProcessScsiSmartInfo(defectGrownList int64, scsiErrorCounterLog
//find analyzed attribute status
for _, val := range sm.Attributes {
if val.GetStatus() == pkg.SmartAttributeStatusFailed {
sm.Status = pkg.Set(sm.Status, pkg.DeviceStatusFailedScrutiny)
if pkg.AttributeStatusHas(val.GetStatus(), pkg.AttributeStatusFailedScrutiny) {
sm.Status = pkg.DeviceStatusSet(sm.Status, pkg.DeviceStatusFailedScrutiny)
}
}
}
@@ -18,13 +18,13 @@ type SmartAtaAttribute struct {
WhenFailed string `json:"when_failed"`
//Generated data
TransformedValue int64 `json:"transformed_value"`
Status int64 `json:"status"`
StatusReason string `json:"status_reason,omitempty"`
FailureRate float64 `json:"failure_rate,omitempty"`
TransformedValue int64 `json:"transformed_value"`
Status pkg.AttributeStatus `json:"status"`
StatusReason string `json:"status_reason,omitempty"`
FailureRate float64 `json:"failure_rate,omitempty"`
}
func (sa *SmartAtaAttribute) GetStatus() int64 {
func (sa *SmartAtaAttribute) GetStatus() pkg.AttributeStatus {
return sa.Status
}
@@ -43,7 +43,7 @@ func (sa *SmartAtaAttribute) Flatten() map[string]interface{} {
//Generated Data
fmt.Sprintf("attr.%s.transformed_value", idString): sa.TransformedValue,
fmt.Sprintf("attr.%s.status", idString): sa.Status,
fmt.Sprintf("attr.%s.status", idString): int64(sa.Status),
fmt.Sprintf("attr.%s.status_reason", idString): sa.StatusReason,
fmt.Sprintf("attr.%s.failure_rate", idString): sa.FailureRate,
}
@@ -77,7 +77,7 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
case "transformed_value":
sa.TransformedValue = val.(int64)
case "status":
sa.Status = val.(int64)
sa.Status = pkg.AttributeStatus(val.(int64))
case "status_reason":
sa.StatusReason = val.(string)
case "failure_rate":
@@ -89,16 +89,16 @@ func (sa *SmartAtaAttribute) Inflate(key string, val interface{}) {
//populate attribute status, using SMART Thresholds & Observed Metadata
// Chainable
func (sa *SmartAtaAttribute) PopulateAttributeStatus() *SmartAtaAttribute {
if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedFailingNow {
if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedFailingNow {
//this attribute has previously failed
sa.Status = pkg.SmartAttributeStatusFailed
sa.StatusReason = "Attribute is failing manufacturer SMART threshold"
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedSmart)
sa.StatusReason += "Attribute is failing manufacturer SMART threshold"
//if the Smart Status is failed, we should exit early, no need to look at thresholds.
return sa
} else if strings.ToUpper(sa.WhenFailed) == pkg.SmartWhenFailedInThePast {
sa.Status = pkg.SmartAttributeStatusWarning
sa.StatusReason = "Attribute has previously failed manufacturer SMART threshold"
} else if strings.ToUpper(sa.WhenFailed) == pkg.AttributeWhenFailedInThePast {
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
sa.StatusReason += "Attribute has previously failed manufacturer SMART threshold"
}
if smartMetadata, ok := thresholds.AtaMetadata[sa.AttributeId]; ok {
@@ -138,16 +138,16 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
if smartMetadata.Critical {
if obsThresh.AnnualFailureRate >= 0.10 {
sa.Status = pkg.SmartAttributeStatusFailed
sa.StatusReason = "Observed Failure Rate for Critical Attribute is greater than 10%"
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
sa.StatusReason += "Observed Failure Rate for Critical Attribute is greater than 10%"
}
} else {
if obsThresh.AnnualFailureRate >= 0.20 {
sa.Status = pkg.SmartAttributeStatusFailed
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 20%"
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
sa.StatusReason += "Observed Failure Rate for Non-Critical Attribute is greater than 20%"
} else if obsThresh.AnnualFailureRate >= 0.10 {
sa.Status = pkg.SmartAttributeStatusWarning
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 10%"
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
sa.StatusReason += "Observed Failure Rate for Non-Critical Attribute is greater than 10%"
}
}
@@ -157,7 +157,7 @@ func (sa *SmartAtaAttribute) ValidateThreshold(smartMetadata thresholds.AtaAttri
}
// no bucket found
if smartMetadata.Critical {
sa.Status = pkg.SmartAttributeStatusWarning
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusWarningScrutiny)
sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute"
}
@@ -1,7 +1,9 @@
package measurements
import "github.com/analogj/scrutiny/webapp/backend/pkg"
type SmartAttribute interface {
Flatten() (fields map[string]interface{})
Inflate(key string, val interface{})
GetStatus() int64
GetStatus() pkg.AttributeStatus
}
@@ -12,13 +12,13 @@ type SmartNvmeAttribute struct {
Value int64 `json:"value"`
Threshold int64 `json:"thresh"`
TransformedValue int64 `json:"transformed_value"`
Status int64 `json:"status"`
StatusReason string `json:"status_reason,omitempty"`
FailureRate float64 `json:"failure_rate,omitempty"`
TransformedValue int64 `json:"transformed_value"`
Status pkg.AttributeStatus `json:"status"`
StatusReason string `json:"status_reason,omitempty"`
FailureRate float64 `json:"failure_rate,omitempty"`
}
func (sa *SmartNvmeAttribute) GetStatus() int64 {
func (sa *SmartNvmeAttribute) GetStatus() pkg.AttributeStatus {
return sa.Status
}
@@ -30,7 +30,7 @@ func (sa *SmartNvmeAttribute) Flatten() map[string]interface{} {
//Generated Data
fmt.Sprintf("attr.%s.transformed_value", sa.AttributeId): sa.TransformedValue,
fmt.Sprintf("attr.%s.status", sa.AttributeId): sa.Status,
fmt.Sprintf("attr.%s.status", sa.AttributeId): int64(sa.Status),
fmt.Sprintf("attr.%s.status_reason", sa.AttributeId): sa.StatusReason,
fmt.Sprintf("attr.%s.failure_rate", sa.AttributeId): sa.FailureRate,
}
@@ -54,7 +54,7 @@ func (sa *SmartNvmeAttribute) Inflate(key string, val interface{}) {
case "transformed_value":
sa.TransformedValue = val.(int64)
case "status":
sa.Status = val.(int64)
sa.Status = pkg.AttributeStatus(val.(int64))
case "status_reason":
sa.StatusReason = val.(string)
case "failure_rate":
@@ -72,8 +72,8 @@ func (sa *SmartNvmeAttribute) PopulateAttributeStatus() *SmartNvmeAttribute {
//check what the ideal is. Ideal tells us if we our recorded value needs to be above, or below the threshold
if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) ||
(smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) {
sa.Status = pkg.SmartAttributeStatusFailed
sa.StatusReason = "Attribute is failing recommended SMART threshold"
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
sa.StatusReason += "Attribute is failing recommended SMART threshold"
}
}
}
@@ -12,13 +12,13 @@ type SmartScsiAttribute struct {
Value int64 `json:"value"`
Threshold int64 `json:"thresh"`
TransformedValue int64 `json:"transformed_value"`
Status int64 `json:"status"`
StatusReason string `json:"status_reason,omitempty"`
FailureRate float64 `json:"failure_rate,omitempty"`
TransformedValue int64 `json:"transformed_value"`
Status pkg.AttributeStatus `json:"status"`
StatusReason string `json:"status_reason,omitempty"`
FailureRate float64 `json:"failure_rate,omitempty"`
}
func (sa *SmartScsiAttribute) GetStatus() int64 {
func (sa *SmartScsiAttribute) GetStatus() pkg.AttributeStatus {
return sa.Status
}
@@ -30,7 +30,7 @@ func (sa *SmartScsiAttribute) Flatten() map[string]interface{} {
//Generated Data
fmt.Sprintf("attr.%s.transformed_value", sa.AttributeId): sa.TransformedValue,
fmt.Sprintf("attr.%s.status", sa.AttributeId): sa.Status,
fmt.Sprintf("attr.%s.status", sa.AttributeId): int64(sa.Status),
fmt.Sprintf("attr.%s.status_reason", sa.AttributeId): sa.StatusReason,
fmt.Sprintf("attr.%s.failure_rate", sa.AttributeId): sa.FailureRate,
}
@@ -54,7 +54,7 @@ func (sa *SmartScsiAttribute) Inflate(key string, val interface{}) {
case "transformed_value":
sa.TransformedValue = val.(int64)
case "status":
sa.Status = val.(int64)
sa.Status = pkg.AttributeStatus(val.(int64))
case "status_reason":
sa.StatusReason = val.(string)
case "failure_rate":
@@ -73,7 +73,7 @@ func (sa *SmartScsiAttribute) PopulateAttributeStatus() *SmartScsiAttribute {
//check what the ideal is. Ideal tells us if we our recorded value needs to be above, or below the threshold
if (smartMetadata.Ideal == "low" && sa.Value > sa.Threshold) ||
(smartMetadata.Ideal == "high" && sa.Value < sa.Threshold) {
sa.Status = pkg.SmartAttributeStatusFailed
sa.Status = pkg.AttributeStatusSet(sa.Status, pkg.AttributeStatusFailedScrutiny)
sa.StatusReason = "Attribute is failing recommended SMART threshold"
}
}
@@ -328,9 +328,12 @@ func TestFromCollectorSmartInfo(t *testing.T) {
require.Equal(t, 18, len(smartMdl.Attributes))
//check that temperature was correctly parsed
require.Equal(t, int64(163210330144), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).RawValue)
require.Equal(t, int64(32), smartMdl.Attributes["194"].(*measurements.SmartAtaAttribute).TransformedValue)
//ensure that Scrutiny warning for a non critical attribute does not set device status to failed.
require.Equal(t, pkg.AttributeStatusWarningScrutiny, smartMdl.Attributes["3"].GetStatus())
}
func TestFromCollectorSmartInfo_Fail_Smart(t *testing.T) {
@@ -402,7 +405,7 @@ func TestFromCollectorSmartInfo_Fail_ScrutinyNonCriticalFailed(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
require.Equal(t, int64(pkg.SmartAttributeStatusFailed), smartMdl.Attributes["199"].GetStatus(),
require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["199"].GetStatus(),
"scrutiny should detect that %d failed (status: %d, %s)",
smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).AttributeId,
smartMdl.Attributes["199"].GetStatus(), smartMdl.Attributes["199"].(*measurements.SmartAtaAttribute).StatusReason,
@@ -435,7 +438,7 @@ func TestFromCollectorSmartInfo_NVMe_Fail_Scrutiny(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "WWN-test", smartMdl.DeviceWWN)
require.Equal(t, pkg.DeviceStatusFailedScrutiny, smartMdl.Status)
require.Equal(t, int64(pkg.SmartAttributeStatusFailed), smartMdl.Attributes["media_errors"].GetStatus(),
require.Equal(t, pkg.AttributeStatusFailedScrutiny, smartMdl.Attributes["media_errors"].GetStatus(),
"scrutiny should detect that %s failed (status: %d, %s)",
smartMdl.Attributes["media_errors"].(*measurements.SmartNvmeAttribute).AttributeId,
smartMdl.Attributes["media_errors"].GetStatus(),
@@ -445,50 +445,6 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
Ideal: ObservedThresholdIdealLow,
Critical: false,
Description: "This attribute indicates the count of full hard disk power on/off cycles.",
ObservedThresholds: []ObservedThreshold{
{
Low: 0,
High: 13,
AnnualFailureRate: 0.019835987118930823,
ErrorInterval: []float64{0.016560870164523494, 0.023569242386797896},
},
{
Low: 13,
High: 26,
AnnualFailureRate: 0.038210930067894826,
ErrorInterval: []float64{0.03353859179329295, 0.0433520775718649},
},
{
Low: 26,
High: 39,
AnnualFailureRate: 0.11053528307302571,
ErrorInterval: []float64{0.09671061589521368, 0.1257816678419765},
},
{
Low: 39,
High: 52,
AnnualFailureRate: 0.16831189443375036,
ErrorInterval: []float64{0.1440976510675928, 0.19543066007594895},
},
{
Low: 52,
High: 65,
AnnualFailureRate: 0.20630344262550107,
ErrorInterval: []float64{0.1693965932069108, 0.2488633537247856},
},
{
Low: 65,
High: 78,
AnnualFailureRate: 0.1030972634140512,
ErrorInterval: []float64{0.06734655535304743, 0.15106137807407605},
},
{
Low: 78,
High: 91,
AnnualFailureRate: 0.12354840389522469,
ErrorInterval: []float64{0.06578432170016109, 0.21127153335749593},
},
},
},
13: {
ID: 13,
+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.4.5"
const VERSION = "0.4.10"
@@ -0,0 +1,22 @@
package handler
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
func DeleteDevice(c *gin.Context) {
logger := c.MustGet("LOGGER").(logrus.FieldLogger)
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
err := deviceRepo.DeleteDevice(c, c.Param("wwn"))
if err != nil {
logger.Errorln("An error occurred while deleting device", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
+8
View File
@@ -13,6 +13,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
)
type AppEngine struct {
@@ -47,6 +48,8 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data
api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests)
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
}
}
@@ -66,6 +69,11 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
}
func (ae *AppEngine) Start() error {
//set the gin mode
gin.SetMode(gin.ReleaseMode)
if strings.ToLower(ae.Config.GetString("log.level")) == "debug" {
gin.SetMode(gin.DebugMode)
}
logger := logrus.New()
//set default log level
+10 -1
View File
@@ -93,6 +93,7 @@ func (suite *ServerTestSuite) TestHealthRoute() {
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -132,6 +133,7 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -171,6 +173,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -219,6 +222,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -314,6 +318,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -352,6 +357,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -390,6 +396,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -428,6 +435,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -465,6 +473,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
@@ -509,5 +518,5 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
//assert
require.Equal(suite.T(), "a4c8e8ed-11a0-4c97-9bba-306440f1b944", deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.WWN)
require.Equal(suite.T(), pkg.DeviceStatusFailedScrutiny, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
require.Equal(suite.T(), pkg.DeviceStatusPassed, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
}
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
if [[ -z "${CI}" ]]; then
echo "running locally (not in Github Actions). generating version file from git client"
GIT_TAG=`git describe --tags`
GIT_BRANCH=`git rev-parse --abbrev-ref HEAD`
if [[ "$GIT_BRANCH" == "master" ]]; then
VERSION_INFO="${GIT_TAG}"
else
VERSION_INFO="${GIT_BRANCH}#${GIT_TAG}"
fi
else
echo "running in Github Actions, generating version file from environmental variables"
# https://docs.github.com/en/actions/learn-github-actions/environment-variables
VERSION_INFO="${GITHUB_REF_NAME}"
if [[ "$GITHUB_REF_TYPE" == "branch" ]]; then
VERSION_INFO="${VERSION_INFO}#${GITHUB_SHA::7}"
fi
fi
echo "writing version file (version: ${VERSION_INFO})"
cat <<EOT > src/environments/versions.ts
// this file is automatically generated by git.version.ts script
export const versionInfo = {
version: '${VERSION_INFO}',
};
EOT
+20 -20
View File
@@ -29,13 +29,13 @@
"@fullcalendar/rrule": "4.4.0",
"@fullcalendar/timegrid": "4.4.0",
"@types/humanize-duration": "^3.18.1",
"apexcharts": "3.19.0",
"apexcharts": "3.19.2",
"crypto-js": "3.3.0",
"highlight.js": "10.0.1",
"humanize-duration": "^3.24.0",
"lodash": "4.17.15",
"moment": "2.24.0",
"ng-apexcharts": "1.2.3",
"ng-apexcharts": "1.5.12",
"ngx-markdown": "9.0.0",
"ngx-quill": "9.1.0",
"perfect-scrollbar": "1.5.0",
@@ -2964,9 +2964,9 @@
}
},
"node_modules/apexcharts": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.19.0.tgz",
"integrity": "sha512-fzupCGVDvOoU6kEzguLAfgRgrlHynHM5fnkkyCL85tYf9U8bw1hCijs4A+kWXurC/SNytJrArBc21kA/2wuHYg==",
"version": "3.19.2",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.19.2.tgz",
"integrity": "sha512-hMFLRE2Lyx4WrN9pYfQLvBDcn+HOodZrqRwc+kucxM+hcUmI2NHY4z+GI14+VcSFmD4aKiMbS3z3Q2jiBxUrcg==",
"dependencies": {
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
@@ -9958,17 +9958,17 @@
"dev": true
},
"node_modules/ng-apexcharts": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.2.3.tgz",
"integrity": "sha512-4G+JRCWp8uSSBJKvYP9vKHEZIC0w6YuRLasumZS35fCCc7bzLY+L907n8khG9Xeoo4LBt7pVbmjb9P+lSWs/5g==",
"version": "1.5.12",
"resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.5.12.tgz",
"integrity": "sha512-k82AdWNbZs5yqGCjiX7PGS11Cy1+1Oo/RGt2lT89xReD9N9Vvo1t34p1dmzS+U6W5wOFlLEKKVLGNQqENW8cTQ==",
"dependencies": {
"tslib": "^1.9.0"
"tslib": "^1.10.0"
},
"peerDependencies": {
"@angular/common": "^8.0.0",
"@angular/core": "^8.0.0",
"apexcharts": "^3.11.2",
"rxjs": "^6.4.0"
"@angular/common": ">=9.0.0 <13.0.0",
"@angular/core": ">=9.0.0 <13.0.0",
"apexcharts": "^3.19.2",
"rxjs": "^6.5.5"
}
},
"node_modules/ngx-markdown": {
@@ -20708,9 +20708,9 @@
}
},
"apexcharts": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.19.0.tgz",
"integrity": "sha512-fzupCGVDvOoU6kEzguLAfgRgrlHynHM5fnkkyCL85tYf9U8bw1hCijs4A+kWXurC/SNytJrArBc21kA/2wuHYg==",
"version": "3.19.2",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.19.2.tgz",
"integrity": "sha512-hMFLRE2Lyx4WrN9pYfQLvBDcn+HOodZrqRwc+kucxM+hcUmI2NHY4z+GI14+VcSFmD4aKiMbS3z3Q2jiBxUrcg==",
"requires": {
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
@@ -26486,11 +26486,11 @@
"dev": true
},
"ng-apexcharts": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.2.3.tgz",
"integrity": "sha512-4G+JRCWp8uSSBJKvYP9vKHEZIC0w6YuRLasumZS35fCCc7bzLY+L907n8khG9Xeoo4LBt7pVbmjb9P+lSWs/5g==",
"version": "1.5.12",
"resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.5.12.tgz",
"integrity": "sha512-k82AdWNbZs5yqGCjiX7PGS11Cy1+1Oo/RGt2lT89xReD9N9Vvo1t34p1dmzS+U6W5wOFlLEKKVLGNQqENW8cTQ==",
"requires": {
"tslib": "^1.9.0"
"tslib": "^1.10.0"
}
},
"ngx-markdown": {
+2 -2
View File
@@ -40,13 +40,13 @@
"@fullcalendar/rrule": "4.4.0",
"@fullcalendar/timegrid": "4.4.0",
"@types/humanize-duration": "^3.18.1",
"apexcharts": "3.19.0",
"apexcharts": "3.19.2",
"crypto-js": "3.3.0",
"highlight.js": "10.0.1",
"humanize-duration": "^3.24.0",
"lodash": "4.17.15",
"moment": "2.24.0",
"ng-apexcharts": "1.2.3",
"ng-apexcharts": "1.5.12",
"ngx-markdown": "9.0.0",
"ngx-quill": "9.1.0",
"perfect-scrollbar": "1.5.0",
@@ -2,6 +2,9 @@ import { Inject, Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import * as _ from 'lodash';
import { TREO_APP_CONFIG } from '@treo/services/config/config.constants';
import { AppConfig } from 'app/core/config/app.config';
const SCRUTINY_CONFIG_LOCAL_STORAGE_KEY = 'scrutiny';
@Injectable({
providedIn: 'root'
@@ -14,10 +17,18 @@ export class TreoConfigService
/**
* Constructor
*/
constructor(@Inject(TREO_APP_CONFIG) config: any)
constructor(@Inject(TREO_APP_CONFIG) defaultConfig: any)
{
let currentScrutinyConfig = defaultConfig
let localConfigStr = localStorage.getItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY)
if (localConfigStr){
//check localstorage for a value
let localConfig = JSON.parse(localConfigStr)
currentScrutinyConfig = Object.assign({}, currentScrutinyConfig, localConfig) // make sure defaults are available if missing from localStorage.
}
// Set the private defaults
this._config = new BehaviorSubject(config);
this._config = new BehaviorSubject(currentScrutinyConfig);
}
// -----------------------------------------------------------------------------------------------------
@@ -27,20 +38,29 @@ export class TreoConfigService
/**
* Setter and getter for config
*/
//Setter
set config(value: any)
{
// Merge the new config over to the current config
const config = _.merge({}, this._config.getValue(), value);
let config = _.merge({}, this._config.getValue(), value);
//Store the config in localstorage
localStorage.setItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
// Execute the observable
this._config.next(config);
}
//Getter
get config$(): Observable<any>
{
return this._config.asObservable();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
@@ -1,7 +1,7 @@
import { Layout } from "app/layout/layout.types";
// Theme type
export type Theme = "light" | "dark";
export type Theme = "light" | "dark" | "system";
/**
* AppConfig interface. Update this interface to strictly type your config
@@ -11,6 +11,12 @@ export interface AppConfig
{
theme: Theme;
layout: Layout;
// Dashboard options
dashboardDisplay: string;
dashboardSort: string;
temperatureUnit: string;
}
/**
@@ -23,6 +29,11 @@ export interface AppConfig
*/
export const appConfig: AppConfig = {
theme : "light",
layout: "material"
layout: "material",
dashboardDisplay: "name",
dashboardSort: "status",
temperatureUnit: "celsius",
};
@@ -11,6 +11,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5000c500673e6b5f",
"device_name": "sdg",
"device_label": "14TB-WD-DRIVE2",
"device_uuid": "",
"device_serial_id": "ata-ST6000DX000-1H217Z-Z4DXXXXX",
"manufacturer": "ATA",
"model_name": "ST6000DX000-1H217Z",
"interface_type": "SCSI",
@@ -35,6 +38,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5000cca252c859cc",
"device_name": "sdd",
"device_label": "14TB-WD-DRIVE1",
"device_uuid": "806cf4bc-d160-4d96-8ee9-3ab7cf2a2e1f",
"device_serial_id": "ata-WDC_WD80EFAX-68LHPN0-7SGLXXXXX",
"manufacturer": "ATA",
"model_name": "WDC_WD80EFAX-68LHPN0",
"interface_type": "SCSI",
@@ -68,6 +74,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5000cca264eb01d7",
"device_name": "sdb",
"device_label": "14TB-WD-DRIVE5",
"device_uuid": "8125ec6d-a7e4-4950-ac84-72d6a4d67128",
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK1XXXXX",
"manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0",
"interface_type": "SCSI",
@@ -101,6 +110,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5000cca264ebc248",
"device_name": "sde",
"device_label": "14TB-WD-DRIVE3",
"device_uuid": "9eb60cde-d6d0-4172-b520-b241a6a5477f",
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK3XXXXX",
"manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0",
"interface_type": "SCSI",
@@ -125,6 +137,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5000cca264ec3183",
"device_name": "sdc",
"device_label": "14TB-WD-DRIVE6",
"device_uuid": "e1378723-7861-49b9-8e01-0bd063f0ecdd",
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK4XXXXX",
"manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0",
"interface_type": "SCSI",
@@ -138,7 +153,7 @@ export const summary = {
"device_protocol": "",
"device_type": "",
"label": "",
"host_id": "",
"host_id": "custom host id",
"device_status": 1
},
"smart": {
@@ -542,6 +557,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x50014ee20b2a72a9",
"device_name": "sdf",
"device_label": "8.0TB-WD-4",
"device_uuid": "fc684dcc-aa2f-44f3-a958-d302dc7dd46d",
"device_serial_id": "ata-WDC_WD60EFRX-68MYMN1-WXL1HXXXXX",
"manufacturer": "ATA",
"model_name": "WDC_WD60EFRX-68MYMN1",
"interface_type": "SCSI",
@@ -566,6 +584,9 @@ export const summary = {
"DeletedAt": null,
"wwn": "0x5002538e40a22954",
"device_name": "sda",
"device_label": "",
"device_uuid": "",
"device_serial_id": "ata-Samsung_SSD_860_EVO_500GB-S3YZNB0KBXXXXXX",
"manufacturer": "ATA",
"model_name": "Samsung_SSD_860_EVO_500GB",
"interface_type": "SCSI",
@@ -0,0 +1,10 @@
<h2 mat-dialog-title>Delete {{data.title}}?</h2>
<mat-dialog-content>This will delete all data associated with this device (including all historical data).</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button class="red-600" mat-button (click)="onDeleteClick()">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'delete_forever'"></mat-icon>
Delete</button>
</mat-dialog-actions>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardDeviceDeleteDialogComponent } from './dashboard-device-delete-dialog.component';
describe('DashboardDeviceDeleteDialogComponent', () => {
let component: DashboardDeviceDeleteDialogComponent;
let fixture: ComponentFixture<DashboardDeviceDeleteDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardDeviceDeleteDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardDeviceDeleteDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,30 @@
import { Component, OnInit, Inject } from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogService} from "./dashboard-device-delete-dialog.service";
import {Subject} from "rxjs";
@Component({
selector: 'app-dashboard-device-delete-dialog',
templateUrl: './dashboard-device-delete-dialog.component.html',
styleUrls: ['./dashboard-device-delete-dialog.component.scss']
})
export class DashboardDeviceDeleteDialogComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<DashboardDeviceDeleteDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string},
private _deleteService: DashboardDeviceDeleteDialogService,
) {
}
ngOnInit(): void {
}
onDeleteClick(): void {
this._deleteService.deleteDevice(this.data.wwn)
.subscribe((data) => {
this.dialogRef.close(data);
});
}
}
@@ -0,0 +1,52 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Overlay } from '@angular/cdk/overlay';
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'
import { MatButtonToggleModule} from "@angular/material/button-toggle";
import {MatTabsModule} from "@angular/material/tabs";
import {MatSliderModule} from "@angular/material/slider";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatTooltipModule} from "@angular/material/tooltip";
import {dashboardRoutes} from "../../../modules/dashboard/dashboard.routing";
import {MatDividerModule} from "@angular/material/divider";
import {MatMenuModule} from "@angular/material/menu";
import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatSortModule} from "@angular/material/sort";
import {MatTableModule} from "@angular/material/table";
import {NgApexchartsModule} from "ng-apexcharts";
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
DashboardDeviceDeleteDialogComponent
],
imports : [
RouterModule.forChild([]),
RouterModule.forChild(dashboardRoutes),
MatButtonModule,
MatDividerModule,
MatTooltipModule,
MatIconModule,
MatMenuModule,
MatProgressBarModule,
MatSortModule,
MatTableModule,
NgApexchartsModule,
SharedModule,
MatDialogModule
],
exports : [
DashboardDeviceDeleteDialogComponent,
],
providers : []
})
export class DashboardDeviceDeleteDialogModule
{
}
@@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { getBasePath } from 'app/app.routing';
@Injectable({
providedIn: 'root'
})
export class DashboardDeviceDeleteDialogService
{
/**
* Constructor
*
* @param {HttpClient} _httpClient
*/
constructor(
private _httpClient: HttpClient
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
deleteDevice(wwn: string): Observable<any>
{
return this._httpClient.delete( `${getBasePath()}/api/device/${wwn}`, {});
}
}
@@ -0,0 +1,67 @@
<div [ngClass]="{ 'border-green': deviceSummary.device.device_status == 0 && deviceSummary.smart,
'border-red': deviceSummary.device.device_status != 0 }"
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
<mat-icon class="icon-size-96 opacity-12 text-green"
*ngIf="deviceSummary.device.device_status == 0 && deviceSummary.smart"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-red"
*ngIf="deviceSummary.device.device_status != 0"
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-yellow"
*ngIf="!deviceSummary.smart"
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
</div>
<div class="flex items-center">
<div class="flex flex-col">
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboardDisplay}}</a>
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
</div>
</div>
<div class="ml-auto" *ngIf="deviceSummary.device">
<button mat-icon-button
[matMenuTriggerFor]="previousStatementMenu">
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
</button>
<mat-menu #previousStatementMenu="matMenu">
<a mat-menu-item [routerLink]="'/device/'+ deviceSummary.device.wwn">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'assessment'"></mat-icon>
<span>View Details</span>
</span>
</a>
<a mat-menu-item (click)="openDeleteDialog()">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'delete_forever'"></mat-icon>
<span>Delete Device</span>
</span>
</a>
</mat-menu>
</div>
</div>
<div class="flex flex-row flex-wrap mt-4 -mx-6">
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusString(deviceSummary.device.device_status) | titlecase}}</div>
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownTemp">{{ deviceSummary.smart?.temp | temperature:config.temperatureUnit:true }}</div>
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
<div class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize}}</div>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(deviceSummary.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
</div>
</div>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardDeviceComponent } from './dashboard-device.component';
describe('DashboardDeviceComponent', () => {
let component: DashboardDeviceComponent;
let fixture: ComponentFixture<DashboardDeviceComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardDeviceComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardDeviceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,93 @@
import { Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
import * as moment from "moment";
import {takeUntil} from "rxjs/operators";
import {AppConfig} from "app/core/config/app.config";
import {TreoConfigService} from "@treo/services/config";
import {Subject} from "rxjs";
import humanizeDuration from 'humanize-duration'
import {MatDialog} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogComponent} from "app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component";
import {DeviceTitlePipe} from "app/shared/device-title.pipe";
@Component({
selector: 'app-dashboard-device',
templateUrl: './dashboard-device.component.html',
styleUrls: ['./dashboard-device.component.scss']
})
export class DashboardDeviceComponent implements OnInit {
@Input() deviceSummary: any;
@Input() deviceWWN: string;
@Output() deviceDeleted = new EventEmitter<string>();
config: AppConfig;
private _unsubscribeAll: Subject<any>;
constructor(
private _configService: TreoConfigService,
public dialog: MatDialog,
) {
// Set the private defaults
this._unsubscribeAll = new Subject();
}
ngOnInit(): void {
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
this.config = config;
});
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
classDeviceLastUpdatedOn(deviceSummary){
if (deviceSummary.device.device_status !== 0) {
return 'text-red' // if the device has failed, always highlight in red
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last 2 weeks.
return 'text-green'
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last month
return 'text-yellow'
} else{
// last updated more than a month ago.
return 'text-red'
}
} else {
return ''
}
}
deviceStatusString(deviceStatus){
if(deviceStatus == 0){
return "passed"
} else {
return "failed"
}
}
readonly humanizeDuration = humanizeDuration;
openDeleteDialog(): void {
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
// width: '250px',
data: {wwn: this.deviceWWN, title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboardDisplay)}
});
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed', result);
if(result.success){
this.deviceDeleted.emit(this.deviceWWN)
}
});
}
}
@@ -0,0 +1,53 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Overlay } from '@angular/cdk/overlay';
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component'
import { MatDialogModule } from "@angular/material/dialog";
import { MatButtonToggleModule} from "@angular/material/button-toggle";
import {MatTabsModule} from "@angular/material/tabs";
import {MatSliderModule} from "@angular/material/slider";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatTooltipModule} from "@angular/material/tooltip";
import {dashboardRoutes} from "../../../modules/dashboard/dashboard.routing";
import {MatDividerModule} from "@angular/material/divider";
import {MatMenuModule} from "@angular/material/menu";
import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatSortModule} from "@angular/material/sort";
import {MatTableModule} from "@angular/material/table";
import {NgApexchartsModule} from "ng-apexcharts";
import {DashboardDeviceDeleteDialogModule} from "../dashboard-device-delete-dialog/dashboard-device-delete-dialog.module";
@NgModule({
declarations: [
DashboardDeviceComponent
],
imports : [
RouterModule.forChild([]),
RouterModule.forChild(dashboardRoutes),
MatButtonModule,
MatDividerModule,
MatTooltipModule,
MatIconModule,
MatMenuModule,
MatProgressBarModule,
MatSortModule,
MatTableModule,
NgApexchartsModule,
SharedModule,
DashboardDeviceDeleteDialogModule
],
exports : [
DashboardDeviceComponent,
],
providers : []
})
export class DashboardDeviceModule
{
}
@@ -1,77 +1,110 @@
<h2 mat-dialog-title>Scrutiny Settings</h2>
<mat-dialog-content class="mat-typography">
<form class="flex flex-col p-8 pb-0 overflow-hidden">
<div class="flex flex-col gt-xs:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3">
<mat-label>Sort By</mat-label>
<mat-select [value]="'status'">
<mat-option value="status">Status</mat-option>
<mat-option value="name" disabled>Name</mat-option>
<mat-option value="label" disabled>Label</mat-option>
<div class="flex flex-col p-8 pb-0 overflow-hidden">
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Dark Mode</mat-label>
<mat-select [(ngModel)]="theme">
<mat-option value="system">System</mat-option>
<mat-option value="dark">Dark</mat-option>
<mat-option value="light">Light</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Display Title</mat-label>
<mat-select [(ngModel)]="dashboardDisplay">
<mat-option value="name">Name</mat-option>
<mat-option value="serial_id">Serial ID</mat-option>
<mat-option value="uuid">UUID</mat-option>
<mat-option value="label">Label</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pl-3">
<mat-label>Sort By</mat-label>
<mat-select [(ngModel)]="dashboardSort">
<mat-option value="status">Status</mat-option>
<mat-option value="title">Title</mat-option>
<mat-option value="age">Age</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Temperature Display Unit</mat-label>
<mat-select [(ngModel)]="temperatureUnit">
<mat-option value="celsius">Celsius</mat-option>
<mat-option value="fahrenheit">Fahrenheit</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="flex">
<mat-tab-group mat-align-tabs="start">
<mat-tab label="Ata">
<div class="flex flex-col mt-5 gt-md:flex-row">
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Critical Error Threshold</mat-label>
<input matInput [value]="'10%'">
<mat-label class="text-hint">Critical Error Threshold</mat-label>
<input disabled matInput [value]="'10%'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Critical Warning Threshold</mat-label>
<input matInput>
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
<input disabled matInput>
</mat-form-field>
</div>
<div class="flex flex-col gt-md:flex-row">
<div matTooltip="not yet implemented" class="flex flex-col gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Error Threshold</mat-label>
<input matInput [value]="'20%'">
<mat-label class="text-hint">Error Threshold</mat-label>
<input disabled matInput [value]="'20%'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Warning Threshold</mat-label>
<input matInput [value]="'10%'">
<mat-label class="text-hint">Warning Threshold</mat-label>
<input disabled matInput [value]="'10%'">
</mat-form-field>
</div>
</mat-tab>
<mat-tab label="NVMe">
<div class="flex flex-col mt-5 gt-md:flex-row">
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Critical Error Threshold</mat-label>
<input matInput [value]="'enabled'">
<mat-label class="text-hint">Critical Error Threshold</mat-label>
<input disabled matInput [value]="'enabled'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Critical Warning Threshold</mat-label>
<input matInput>
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
<input disabled matInput>
</mat-form-field>
</div>
</mat-tab>
<mat-tab label="SCSI">
<div class="flex flex-col mt-5 gt-md:flex-row">
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Critical Error Threshold</mat-label>
<input matInput [value]="'enabled'">
<mat-label class="text-hint">Critical Error Threshold</mat-label>
<input disabled matInput [value]="'enabled'">
</mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Critical Warning Threshold</mat-label>
<input matInput>
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
<input disabled matInput>
</mat-form-field>
</div>
</mat-tab>
</mat-tab-group>
</div>
</form>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button matTooltip="not yet implemented" [mat-dialog-close]="true" cdkFocusInitial>Save</button>
<button mat-button mat-dialog-close (click)="saveSettings()" cdkFocusInitial>Save</button>
</mat-dialog-actions>
@@ -1,4 +1,8 @@
import { Component, OnInit } from '@angular/core';
import {AppConfig} from 'app/core/config/app.config';
import { TreoConfigService } from '@treo/services/config';
import {Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
@Component({
selector: 'app-dashboard-settings',
@@ -7,11 +11,51 @@ import { Component, OnInit } from '@angular/core';
})
export class DashboardSettingsComponent implements OnInit {
constructor() { }
dashboardDisplay: string;
dashboardSort: string;
temperatureUnit: string;
theme: string;
// Private
private _unsubscribeAll: Subject<any>;
constructor(
private _configService: TreoConfigService,
) {
// Set the private defaults
this._unsubscribeAll = new Subject();
}
ngOnInit(): void {
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
// Store the config
this.dashboardDisplay = config.dashboardDisplay;
this.dashboardSort = config.dashboardSort;
this.temperatureUnit = config.temperatureUnit;
this.theme = config.theme;
});
}
formatLabel(value: number) {
saveSettings(): void {
const newSettings = {
dashboardDisplay: this.dashboardDisplay,
dashboardSort: this.dashboardSort,
temperatureUnit: this.temperatureUnit,
theme: this.theme
}
this._configService.config = newSettings
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
}
formatLabel(value: number): number {
return value;
}
}
@@ -23,6 +23,7 @@ export class LayoutComponent implements OnInit, OnDestroy
// Private
private _unsubscribeAll: Subject<any>;
private systemPrefersDark: boolean;
/**
* Constructor
@@ -43,6 +44,9 @@ export class LayoutComponent implements OnInit, OnDestroy
{
// Set the private defaults
this._unsubscribeAll = new Subject();
this.systemPrefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
}
// -----------------------------------------------------------------------------------------------------
@@ -66,7 +70,7 @@ export class LayoutComponent implements OnInit, OnDestroy
this.theme = config.theme;
// Update the selected theme class name on body
const themeName = 'treo-theme-' + config.theme;
const themeName = 'treo-theme-' + this.determineTheme(config);
this._document.body.classList.forEach((className) => {
if ( className.startsWith('treo-theme-') && className !== themeName )
{
@@ -105,6 +109,17 @@ export class LayoutComponent implements OnInit, OnDestroy
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Checks if theme should be set to dark based on config & system settings
*/
private determineTheme(config:AppConfig): string {
if (config.theme === 'system') {
return this.systemPrefersDark ? 'dark' : 'light'
} else {
return config.theme
}
}
/**
* Update the selected layout
*/
@@ -39,7 +39,7 @@
<!-- Spacer -->
<div class="spacer"></div>
<code>{{appVersion}}</code>
<!-- Shortcuts -->
<!-- <shortcuts [shortcuts]="data.shortcuts"></shortcuts>-->
@@ -48,6 +48,7 @@
<!-- <notifications [notifications]="data.notifications"></notifications>-->
</div>
@@ -4,6 +4,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoMediaWatcherService } from '@treo/services/media-watcher';
import { TreoNavigationService } from '@treo/components/navigation';
import {versionInfo} from 'environments/versions';
@Component({
selector : 'material-layout',
@@ -13,6 +14,7 @@ import { TreoNavigationService } from '@treo/components/navigation';
})
export class MaterialLayoutComponent implements OnInit, OnDestroy
{
appVersion: string;
data: any;
isScreenSmall: boolean;
@@ -46,6 +48,8 @@ export class MaterialLayoutComponent implements OnInit, OnDestroy
// Set the defaults
this.fixedHeader = false;
this.fixedFooter = false;
this.appVersion = versionInfo.version
}
// -----------------------------------------------------------------------------------------------------
@@ -47,71 +47,15 @@
</div>
</div>
<div class="flex flex-wrap w-full">
<div *ngFor="let summary of data.data.summary | keyvalue" class="flex gt-sm:w-1/2 min-w-80 p-4">
<div [ngClass]="{ 'border-green': summary.value.device.device_status == 0 && summary.value.smart,
'border-red': summary.value.device.device_status != 0 }"
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
<mat-icon class="icon-size-96 opacity-12 text-green"
*ngIf="summary.value.device.device_status == 0 && summary.value.smart"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-red"
*ngIf="summary.value.device.device_status != 0"
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-yellow"
*ngIf="!summary.value.smart"
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
</div>
<div class="flex items-center">
<div class="flex flex-col">
<a [routerLink]="'/device/'+ summary.value.device.wwn"
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceTitle(summary.value.device)}}</a>
<div [ngClass]="classDeviceLastUpdatedOn(summary.value)" class="font-medium text-sm" *ngIf="summary.value.smart">
Last Updated on {{summary.value.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
</div>
</div>
<div class="ml-auto" *ngIf="summary.value.device">
<button mat-icon-button
[matMenuTriggerFor]="previousStatementMenu">
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
</button>
<mat-menu #previousStatementMenu="matMenu">
<a mat-menu-item [routerLink]="'/device/'+ summary.value.device.wwn">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'payment'"></mat-icon>
<span>View Details</span>
</span>
</a>
</mat-menu>
</div>
</div>
<div class="flex flex-row flex-wrap mt-4 -mx-6">
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownStatus">{{ deviceStatusString(summary.value.device.device_status) | titlecase}}</div>
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownTemp">{{ summary.value.smart?.temp }}°C</div>
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Capacity</div>
<div class="mt-2 font-medium text-3xl leading-none">{{ summary.value.device.capacity | fileSize}}</div>
</div>
<div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(summary.value.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
</div>
</div>
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
<div class="flex flex-wrap w-full">
<app-dashboard-device (deviceDeleted)="onDeviceDeleted($event)" class="flex gt-sm:w-1/2 min-w-80 p-4" *ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboardSort:config.dashboardDisplay )" [deviceWWN]="deviceSummary.device.wwn" [deviceSummary]="deviceSummary"></app-dashboard-device>
</div>
</div>
<!-- Drive Temperatures -->
<div class="flex flex-auto w-full min-w-80 h-90 p-4">
<div class="flex flex-col flex-auto bg-card shadow-md rounded overflow-hidden">
@@ -123,22 +67,22 @@
</div>
<div>
<button class="h-8 min-h-8 px-2"
matTooltip="not yet implemented"
mat-button
[matMenuTriggerFor]="tempRangeMenu">
<span class="font-medium text-sm text-hint">1 week</span>
<span class="font-medium text-sm text-hint">{{tempDurationKey}}</span>
</button>
<mat-menu #tempRangeMenu="matMenu">
<button mat-menu-item>1 month</button>
<button mat-menu-item>12 months</button>
<button mat-menu-item>all time</button>
<button (click)="changeSummaryTempDuration('forever')" mat-menu-item>forever</button>
<button (click)="changeSummaryTempDuration('year')" mat-menu-item>year</button>
<button (click)="changeSummaryTempDuration('month')" mat-menu-item>month</button>
<button (click)="changeSummaryTempDuration('week')" mat-menu-item>week</button>
</mat-menu>
</div>
</div>
</div>
<div class="flex flex-col flex-auto">
<apx-chart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
<apx-chart #tempChart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
[chart]="temperatureOptions.chart"
[colors]="temperatureOptions.colors"
[fill]="temperatureOptions.fill"
@@ -167,6 +111,4 @@
<code>scrutiny-collector-metrics run</code>
</div>
</ng-template>
@@ -3,12 +3,15 @@ import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ApexOptions } from 'ng-apexcharts';
import {ApexOptions, ChartComponent} from 'ng-apexcharts';
import { DashboardService } from 'app/modules/dashboard/dashboard.service';
import * as moment from "moment";
import {MatDialog} from '@angular/material/dialog';
import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component';
import humanizeDuration from 'humanize-duration'
import {AppConfig} from "app/core/config/app.config";
import {TreoConfigService} from "@treo/services/config";
import {Router} from "@angular/router";
import {TemperaturePipe} from "app/shared/temperature.pipe";
import {DeviceTitlePipe} from "app/shared/device-title.pipe";
@Component({
selector : 'example',
@@ -20,10 +23,14 @@ import humanizeDuration from 'humanize-duration'
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
{
data: any;
hostGroups: { [hostId: string]: string[] } = {}
temperatureOptions: ApexOptions;
tempDurationKey: string = "forever"
config: AppConfig;
// Private
private _unsubscribeAll: Subject<any>;
@ViewChild("tempChart", { static: false }) tempChart: ChartComponent;
/**
* Constructor
@@ -32,7 +39,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
*/
constructor(
private _smartService: DashboardService,
public dialog: MatDialog
private _configService: TreoConfigService,
public dialog: MatDialog,
private router: Router,
)
{
// Set the private defaults
@@ -49,6 +58,28 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
*/
ngOnInit(): void
{
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
//check if the old config and the new config do not match.
let oldConfig = JSON.stringify(this.config)
let newConfig = JSON.stringify(config)
if(oldConfig != newConfig){
console.log(`Configuration updated: ${newConfig} vs ${oldConfig}`)
// Store the config
this.config = config;
if(oldConfig){
console.log("reloading component...")
this.refreshComponent()
}
}
});
// Get the data
this._smartService.data$
.pipe(takeUntil(this._unsubscribeAll))
@@ -57,6 +88,15 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// Store the data
this.data = data;
//generate group data.
for(let wwn in this.data.data.summary){
let hostid = this.data.data.summary[wwn].device.host_id
let hostDeviceList = this.hostGroups[hostid] || []
hostDeviceList.push(wwn)
this.hostGroups[hostid] = hostDeviceList
}
console.log(this.hostGroups)
// Prepare the chart data
this._prepareChartData();
});
@@ -81,6 +121,14 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
private refreshComponent(){
let currentUrl = this.router.url;
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.router.navigate([currentUrl]);
}
private _deviceDataTemperatureSeries() {
var deviceTemperatureSeries = []
@@ -91,8 +139,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
if (!deviceSummary.temp_history){
continue
}
let deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboardDisplay)
var deviceSeriesMetadata = {
name: `/dev/${deviceSummary.device.device_name}`,
name: deviceName,
data: []
}
@@ -100,7 +151,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
let newDate = new Date(tempHistory.date);
deviceSeriesMetadata.data.push({
x: newDate,
y: tempHistory.temp
y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperatureUnit, false)
})
}
deviceTemperatureSeries.push(deviceSeriesMetadata)
@@ -149,8 +200,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
format: 'MMM dd, yyyy HH:mm:ss'
},
y : {
formatter: (value) => {
return value + '°C';
return TemperaturePipe.formatTemperature(value, this.config.temperatureUnit, true) as string;
}
}
},
@@ -164,6 +216,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// @ Public methods
// -----------------------------------------------------------------------------------------------------
deviceSummariesForHostGroup(hostGroupWWNs: string[]) {
let deviceSummaries = []
for(let wwn of hostGroupWWNs){
if(this.data.data.summary[wwn]){
deviceSummaries.push(this.data.data.summary[wwn])
}
}
return deviceSummaries
}
openDialog() {
const dialogRef = this.dialog.open(DashboardSettingsComponent);
@@ -172,48 +234,33 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
});
}
deviceTitle(disk){
let title = []
if (disk.host_id) title.push(disk.host_id)
title.push(`/dev/${disk.device_name}`)
if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){
title.push(disk.device_type)
}
title.push(disk.model_name)
return title.join(' - ')
onDeviceDeleted(wwn: string) {
delete this.data.data.summary[wwn] // remove the device from the summary list.
}
deviceStatusString(deviceStatus){
if(deviceStatus == 0){
return "passed"
} else {
return "failed"
}
}
/*
classDeviceLastUpdatedOn(deviceSummary){
if (deviceSummary.device.device_status !== 0) {
return 'text-red' // if the device has failed, always highlight in red
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last 2 weeks.
return 'text-green'
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last month
return 'text-yellow'
} else{
// last updated more than a month ago.
return 'text-red'
}
DURATION_KEY_WEEK = "week"
DURATION_KEY_MONTH = "month"
DURATION_KEY_YEAR = "year"
DURATION_KEY_FOREVER = "forever"
*/
} else {
return ''
}
changeSummaryTempDuration(durationKey: string){
this.tempDurationKey = durationKey
this._smartService.getSummaryTempData(durationKey)
.subscribe((data) => {
// given a list of device temp history, override the data in the "summary" object.
for(const wwn in this.data.data.summary) {
// console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`)
this.data.data.summary[wwn].temp_history = data.data.temp_history[wwn] || []
}
// Prepare the chart series data
this.tempChart.updateSeries(this._deviceDataTemperatureSeries())
});
}
/**
@@ -227,6 +274,4 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
return item.id || index;
}
readonly humanizeDuration = humanizeDuration;
}
@@ -13,12 +13,13 @@ import { MatTableModule } from '@angular/material/table';
import { NgApexchartsModule } from 'ng-apexcharts';
import { MatTooltipModule } from '@angular/material/tooltip'
import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/dashboard-settings.module";
import { DashboardDeviceModule } from "app/layout/common/dashboard-device/dashboard-device.module";
@NgModule({
declarations: [
DashboardComponent
],
imports : [
imports: [
RouterModule.forChild(dashboardRoutes),
MatButtonModule,
MatDividerModule,
@@ -30,7 +31,8 @@ import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/da
MatTableModule,
NgApexchartsModule,
SharedModule,
DashboardSettingsModule
DashboardSettingsModule,
DashboardDeviceModule
]
})
export class DashboardModule
@@ -31,6 +31,6 @@ export class DashboardResolver implements Resolve<any>
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
{
return this._dashboardService.getData();
return this._dashboardService.getSummaryData();
}
}
@@ -44,7 +44,7 @@ export class DashboardService
/**
* Get data
*/
getData(): Observable<any>
getSummaryData(): Observable<any>
{
return this._httpClient.get(getBasePath() + '/api/summary').pipe(
tap((response: any) => {
@@ -52,4 +52,14 @@ export class DashboardService
})
);
}
getSummaryTempData(durationKey: string): Observable<any>
{
let params = {}
if(durationKey){
params["duration_key"] = durationKey
}
return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params});
}
}
@@ -4,7 +4,7 @@
<div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0">
<div class="mr-6">
<h2 class="m-0">Drive Details</h2>
<h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboardDisplay}} </h2>
<div class="text-secondary tracking-tight">Dive into S.M.A.R.T data</div>
</div>
<!-- Action buttons -->
@@ -51,11 +51,8 @@
<!-- Card -->
<div class="flex flex-auto w-1/4 p-4 lt-md:w-full">
<treo-card class="flex flex-auto p-4 pt-6 flex-col flex-auto filter-list">
<div class="flex items-center justify-between">
<div class="text-2xl font-semibold leading-tight">/dev/{{device?.device_name}}</div>
</div>
<div class="flex flex-col my-2 grid grid-cols-2">
<treo-card class="flex flex-auto p-4 flex-col flex-auto filter-list">
<div class="flex flex-col grid grid-cols-2">
<div *ngIf="device" class="my-2 col-span-2 lt-md:col-span-1">
<div>
<span class="inline-flex items-center font-bold text-xs px-2 py-2px rounded-full tracking-wide uppercase"
@@ -64,7 +61,7 @@
<span class="w-2 h-2 rounded-full mr-2"
[ngClass]="{'bg-red': device?.device_status != 0,
'bg-green': device?.device_status == 0}"></span>
<span class="pr-2px leading-relaxed whitespace-no-wrap">{{device?.device_status == 0 ? 'passed' : 'failed'}}</span>
<span class="pr-2px leading-relaxed whitespace-no-wrap">{{device?.device_status | deviceStatus}}</span>
</span>
</div>
<div class="text-secondary text-md">Status</div>
@@ -74,6 +71,16 @@
<div>{{device?.host_id}}</div>
<div class="text-secondary text-md">Host ID</div>
</div>
<div *ngIf="device?.device_uuid" class="my-2 col-span-2 lt-md:col-span-1">
<div>{{device?.device_uuid}}</div>
<div class="text-secondary text-md">Device UUID</div>
</div>
<div *ngIf="device?.device_label" class="my-2 col-span-2 lt-md:col-span-1">
<div>{{device?.device_label}}</div>
<div class="text-secondary text-md">Device Label</div>
</div>
<div *ngIf="device?.device_type && device?.device_type != 'ata' && device?.device_type != 'scsi'" class="my-2 col-span-2 lt-md:col-span-1">
<div>{{device?.device_type | uppercase}}</div>
<div class="text-secondary text-md">Device Type</div>
@@ -119,7 +126,7 @@
<div class="text-secondary text-md">Powered On</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{smart_results[0]?.temp}}°C</div>
<div>{{smart_results[0]?.temp | temperature:config.temperatureUnit:true}}</div>
<div class="text-secondary text-md">Temperature</div>
</div>
</div>
@@ -9,6 +9,8 @@ import {fadeOut} from "../../../@treo/animations/fade";
import {DetailSettingsComponent} from "app/layout/common/detail-settings/detail-settings.component";
import {MatDialog} from "@angular/material/dialog";
import humanizeDuration from 'humanize-duration';
import {TreoConfigService} from "../../../@treo/services/config";
import {AppConfig} from "../../core/config/app.config";
@Component({
selector: 'detail',
@@ -18,6 +20,8 @@ import humanizeDuration from 'humanize-duration';
export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
config: AppConfig;
onlyCritical: boolean = true;
// data: any;
@@ -43,7 +47,9 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
*/
constructor(
private _detailService: DetailService,
public dialog: MatDialog
public dialog: MatDialog,
private _configService: TreoConfigService,
)
{
@@ -65,6 +71,14 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
*/
ngOnInit(): void
{
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
this.config = config;
});
// Get the data
this._detailService.data$
.pipe(takeUntil(this._unsubscribeAll))
@@ -107,25 +121,34 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
getAttributeStatusName(attribute_status){
if(attribute_status == 0){
return "passed"
} else if (attribute_status == 1){
return "failed"
} else if (attribute_status == 2){
return "warn"
getAttributeStatusName(attributeStatus: number): string {
// tslint:disable:no-bitwise
// from Constants.go
// AttributeStatusPassed AttributeStatus = 0
// AttributeStatusFailedSmart AttributeStatus = 1
// AttributeStatusWarningScrutiny AttributeStatus = 2
// AttributeStatusFailedScrutiny AttributeStatus = 4
if(attributeStatus === 0){
return 'passed'
} else if ((attributeStatus & 1) !== 0 || (attributeStatus & 4) !== 0 ){
return 'failed'
} else if ((attributeStatus & 2) !== 0){
return 'warn'
}
return
return ''
// tslint:enable:no-bitwise
}
getAttributeName(attribute_data){
getAttributeName(attribute_data): string {
let attribute_metadata = this.metadata[attribute_data.attribute_id]
if(!attribute_metadata){
return 'Unknown Attribute Name'
} else {
return attribute_metadata.display_name
}
return
}
getAttributeDescription(attribute_data){
let attribute_metadata = this.metadata[attribute_data.attribute_id]
@@ -1,33 +1,70 @@
import { Pipe, PipeTransform } from '@angular/core';
import {DeviceTitlePipe} from "./device-title.pipe";
@Pipe({
name: 'deviceSort'
})
export class DeviceSortPipe implements PipeTransform {
numericalStatus(device): number {
if(!device.smart_results[0]){
return 0
} else if (device.smart_results[0].smart_status == 'passed'){
return 1
} else {
return -1
statusCompareFn(a: any, b: any) {
function deviceStatus(deviceSummary): number {
if(!deviceSummary.smart){
return 0
} else if (deviceSummary.device.device_status == 0){
return 1
} else {
return deviceSummary.device.device_status * -1 // will return range from -1, -2, -3
}
}
let left = deviceStatus(a)
let right = deviceStatus(b)
return left - right;
}
titleCompareFn(dashboardDisplay: string) {
return function (a: any, b: any){
let _dashboardDisplay = dashboardDisplay
let left = DeviceTitlePipe.deviceTitleForType(a.device, _dashboardDisplay) || DeviceTitlePipe.deviceTitleForType(a.device, 'name')
let right = DeviceTitlePipe.deviceTitleForType(b.device, _dashboardDisplay) || DeviceTitlePipe.deviceTitleForType(b.device, 'name')
if( left < right )
return -1;
if( left > right )
return 1;
return 0;
}
}
ageCompareFn(a: any, b: any) {
const left = a.smart?.power_on_hours
const right = b.smart?.power_on_hours
transform(devices: Array<unknown>, ...args: unknown[]): Array<unknown> {
//failed, unknown/empty, passed
devices.sort((a: any, b: any) => {
let left = this.numericalStatus(a)
let right = this.numericalStatus(b)
return left - right;
});
return left - right;
}
return devices;
transform(deviceSummaries: Array<unknown>, sortBy = 'status', dashboardDisplay = 'name'): Array<unknown> {
let compareFn: any
switch (sortBy) {
case 'status':
compareFn = this.statusCompareFn
break;
case 'title':
compareFn = this.titleCompareFn(dashboardDisplay)
break;
case 'age':
compareFn = this.ageCompareFn
break;
}
// failed, unknown/empty, passed
deviceSummaries.sort(compareFn);
return deviceSummaries;
}
}
@@ -0,0 +1,8 @@
import { DeviceStatusPipe } from './device-status.pipe';
describe('DeviceStatusPipe', () => {
it('create an instance', () => {
const pipe = new DeviceStatusPipe();
expect(pipe).toBeTruthy();
});
});
@@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'deviceStatus'
})
export class DeviceStatusPipe implements PipeTransform {
transform(deviceStatusFlag: number): string {
if(deviceStatusFlag === 0){
return 'passed'
} else if(deviceStatusFlag === 3){
return 'failed: both'
} else if(deviceStatusFlag === 2) {
return 'failed: scrutiny'
} else if(deviceStatusFlag === 1) {
return 'failed: smart'
}
return 'unknown'
}
}
@@ -0,0 +1,8 @@
import { DeviceTitlePipe } from './device-title.pipe';
describe('DeviceTitlePipe', () => {
it('create an instance', () => {
const pipe = new DeviceTitlePipe();
expect(pipe).toBeTruthy();
});
});
@@ -0,0 +1,54 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'deviceTitle'
})
export class DeviceTitlePipe implements PipeTransform {
static deviceTitleForType(device: any, titleType: string): string {
const titleParts = []
switch(titleType){
case 'name':
titleParts.push(`/dev/${device.device_name}`)
if (device.device_type && device.device_type !== 'scsi' && device.device_type !== 'ata'){
titleParts.push(device.device_type)
}
titleParts.push(device.model_name)
break;
case 'serial_id':
if(!device.device_serial_id) return ''
titleParts.push(`/by-id/${device.device_serial_id}`)
break;
case 'uuid':
if(!device.device_uuid) return ''
titleParts.push(`/by-uuid/${device.device_uuid}`)
break;
case 'label':
if(device.label){
titleParts.push(device.label)
} else if(device.device_label){
titleParts.push(`/by-label/${device.device_label}`)
}
break;
}
return titleParts.join(' - ')
}
static deviceTitleWithFallback(device, titleType: string): string {
console.log(`Displaying Device ${device.wwn} with: ${titleType}`)
const titleParts = []
if (device.host_id) titleParts.push(device.host_id)
// add device identifier (fallback to generated device name)
titleParts.push(DeviceTitlePipe.deviceTitleForType(device, titleType) || DeviceTitlePipe.deviceTitleForType(device, 'name'))
return titleParts.join(' - ')
}
transform(device: any, titleType: string = 'name'): string {
return DeviceTitlePipe.deviceTitleWithFallback(device, titleType)
}
}
@@ -1,13 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import {FileSizePipe} from "./file-size.pipe";
import {FileSizePipe} from './file-size.pipe';
import { DeviceSortPipe } from './device-sort.pipe';
import { TemperaturePipe } from './temperature.pipe';
import { DeviceTitlePipe } from './device-title.pipe';
import { DeviceStatusPipe } from './device-status.pipe';
@NgModule({
declarations: [
FileSizePipe,
DeviceSortPipe
DeviceSortPipe,
TemperaturePipe,
DeviceTitlePipe,
DeviceStatusPipe
],
imports: [
CommonModule,
@@ -19,7 +25,10 @@ import { DeviceSortPipe } from './device-sort.pipe';
FormsModule,
ReactiveFormsModule,
FileSizePipe,
DeviceSortPipe
DeviceSortPipe,
DeviceTitlePipe,
DeviceStatusPipe,
TemperaturePipe
]
})
export class SharedModule
@@ -0,0 +1,8 @@
import { TemperaturePipe } from './temperature.pipe';
describe('TemperaturePipe', () => {
it('create an instance', () => {
const pipe = new TemperaturePipe();
expect(pipe).toBeTruthy();
});
});
@@ -0,0 +1,35 @@
import { Pipe, PipeTransform } from '@angular/core';
import {formatNumber} from "@angular/common";
@Pipe({
name: 'temperature'
})
export class TemperaturePipe implements PipeTransform {
static celsiusToFahrenheit(celsiusTemp: number): number {
return celsiusTemp * 9.0 / 5.0 + 32;
}
static formatTemperature(celsiusTemp: number, unit: string, includeUnits: boolean): number|string {
let convertedTemp
let convertedUnitSuffix
switch (unit) {
case 'celsius':
convertedTemp = celsiusTemp
convertedUnitSuffix = '°C'
break
case 'fahrenheit':
convertedTemp = TemperaturePipe.celsiusToFahrenheit(celsiusTemp)
convertedUnitSuffix = '°F'
break
}
if(includeUnits){
return formatNumber(convertedTemp, 'en-US') + convertedUnitSuffix
} else {
return formatNumber(convertedTemp, 'en-US',)
}
}
transform(celsiusTemp: number, unit = 'celsius', includeUnits = false): number|string {
return TemperaturePipe.formatTemperature(celsiusTemp, unit, includeUnits)
}
}
@@ -0,0 +1,329 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<defs >
<font id="IBMPlexMono" horiz-adv-x="600" ><font-face
font-family="IBM Plex Mono Medium"
units-per-em="1000"
panose-1="2 11 6 9 5 2 3 0 2 3"
ascent="1025"
descent="-275"
alphabetic="0" />
<glyph unicode=" " glyph-name="space" />
<glyph unicode="!" glyph-name="exclam" d="M281 237L240 495V698H360V495L319 237H281ZM300 -10Q254 -10 235 9T216 57V81Q216 110 235 129T300 148Q346 148 365 129T384 81V57Q384 28 365 9T300 -10Z" />
<glyph unicode="&quot;" glyph-name="quotedbl" d="M408 442V740H508V442H408ZM349 442V740H449V442H349Z" />
<glyph unicode="#" glyph-name="numbersign" d="M135 207H13V291H150L170 407H50V491H185L222 698H313L190 0H99L135 207ZM410 698H501L465 491H587V407H450L430 291H550V207H415L378 0H287L410 698Z" />
<glyph unicode="$" glyph-name="dollar" d="M268 -11Q188 -5 133 26T43 105L115 174Q149 133 187 111T273 83V312L250 316Q200 325 165 343T108 387T76 444T66 512Q66 597 119 647T268 708V811H352V708Q418 701 466 675T549 604L476 538Q452 569 421 588T347 612V400L374
396Q424 387 459 369T516 326T549 268T559 200Q559 114 506 59T352 -8V-113H268V-11ZM172 515Q172 473 195 450T273 414V614Q172 602 172 515ZM453 195Q453 240 429 263T347 298V84Q399 92 426 120T453 195Z" />
<glyph unicode="%" glyph-name="percent" d="M167 348Q98 348 57 394T15 529Q15 617 56 663T167 710Q236 710 277 664T319 529Q319 441 278 395T167 348ZM167 416Q200 416 218 439T236 505V553Q236 595 218 618T167 642Q134 642 116 619T98 553V505Q98 463 116
440T167 416ZM499 698H589L432 398H343L499 698ZM168 300H257L101 0H11L168 300ZM433 -12Q364 -12 323 34T281 169Q281 257 322 303T433 350Q502 350 543 304T585 169Q585 81 544 35T433 -12ZM433 56Q466 56 484 79T502 145V193Q502 235 484 258T433 282Q400 282
382 259T364 193V145Q364 103 382 80T433 56Z" />
<glyph unicode="&amp;" glyph-name="ampersand" d="M213 -12Q167 -12 131 3T71 46T33 108T20 185Q20 242 48 294T145 383Q115 422 98 459T80 538Q80 576 94 607T134 662T193 697T267 710Q302 710 331 700T382 674T420 638T443 596L362 554Q350 585 325 604T265
624Q227 624 204 601T180 540V533Q180 518 184 504T197 474T220 439T256 392L332 298L395 215H400Q407 252 409 300T412 390H569V305H496Q490 263 483 228T458 150L583 0H462L378 100H372Q361 49 320 19T213 -12ZM241 77Q275 77 302 90T347 133L192 324Q154 297
140 265T126 195V186Q126 135 157 106T241 77Z" />
<glyph unicode="&apos;" glyph-name="quotesingle" d="M250 442V740H350V442H250Z" />
<glyph unicode="(" glyph-name="parenleft" d="M192 311Q192 384 208 451T253 577T320 681T400 760H509Q464 728 424 685T354 590T306 480T288 357V265Q288 201 306 143T354 32T424 -63T509 -138H400Q358 -107 320 -60T254 45T209 170T192 311Z" />
<glyph unicode=")" glyph-name="parenright" d="M408 311Q408 238 392 171T347 45T280 -60T200 -138H91Q136 -106 176 -63T246 32T294 142T312 265V357Q312 421 294 479T246 590T176 685T91 760H200Q242 728 280 682T346 577T391 452T408 311Z" />
<glyph unicode="*" glyph-name="asterisk" d="M182 48L105 101L225 263L38 326L66 412L252 349V549H348V349L534 412L562 326L375 263L495 101L418 48L300 210L182 48Z" />
<glyph unicode="+" glyph-name="plus" d="M251 62V261H62V350H251V549H349V350H538V261H349V62H251Z" />
<glyph unicode="," glyph-name="comma" d="M244 152H399L275 -145H190L244 152Z" />
<glyph unicode="-" glyph-name="hyphen" d="M149 250V359H451V250H149Z" />
<glyph unicode="." glyph-name="period" d="M300 -10Q252 -10 233 10T213 59V85Q213 114 232 134T300 154Q348 154 367 134T387 85V59Q387 30 368 10T300 -10Z" />
<glyph unicode="/" glyph-name="slash" d="M83 -138L420 760H517L180 -138H83Z" />
<glyph unicode="0" glyph-name="zero" d="M300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300 -12ZM300 80Q376 80 408 136T441 289V409Q441 505
409 561T300 618Q262 618 235 604T191 562T167 496T159 409V289Q159 241 166 203T191 137T235 95T300 80ZM300 290Q265 290 252 304T238 338V360Q238 380 251 394T300 408Q335 408 348 394T362 360V338Q362 318 349 304T300 290Z" />
<glyph unicode="1" glyph-name="one" d="M88 0V93H284V616H276L114 443L45 505L223 698H396V93H565V0H88Z" />
<glyph unicode="2" glyph-name="two" d="M545 0H63V107L291 306Q345 354 374 397T404 488V499Q404 554 374 583T287 613Q229 613 198 582T152 503L54 540Q64 573 83 603T131 658T201 696T295 710Q349 710 391 695T463 652T507 588T522 505Q522 463 510 428T475
360T421 297T353 237L184 95H545V0Z" />
<glyph unicode="3" glyph-name="three" d="M271 410Q338 410 370 438T403 510V517Q403 565 372 590T287 616Q236 616 201 593T141 530L65 595Q81 618 102 639T150 675T212 700T291 710Q340 710 382 698T455 663T503 606T520 530Q520 497 510 470T481 423T437 389T384
370V365Q414 359 441 346T489 311T522 260T534 192Q534 146 516 109T465 44T384 3T279 -12Q230 -12 193 -2T126 26T76 66T38 113L121 177Q147 135 182 109T280 82Q346 82 381 112T416 197V205Q416 259 380 287T274 316H199V410H271Z" />
<glyph unicode="4" glyph-name="four" d="M362 0V136H30V232L322 698H469V224H567V136H469V0H362ZM125 224H362V595H356L125 224Z" />
<glyph unicode="5" glyph-name="five" d="M511 601H194L176 353H184Q208 394 242 418T335 443Q380 443 418 428T485 385T530 317T547 225Q547 173 530 130T481 55T402 6T295 -12Q248 -12 212 -2T147 26T97 66T60 113L142 177Q155 157 169 140T202 109T243 89T297
82Q362 82 396 117T430 214V222Q430 283 396 317T297 351Q250 351 223 334T176 296L83 309L109 698H511V601Z" />
<glyph unicode="6" glyph-name="six" d="M302 -12Q243 -12 197 7T119 61T70 147T53 260Q53 336 76 403T136 526T218 625T307 698H457Q397 655 349 615T266 533T206 445T169 341L176 339Q187 360 202 378T237 411T283 433T342 441Q387 441 425 426T491 383T534
314T550 224Q550 172 533 129T483 54T404 6T302 -12ZM301 78Q365 78 400 112T435 210V220Q435 283 400 317T301 352Q238 352 203 318T168 220V210Q168 147 203 113T301 78Z" />
<glyph unicode="7" glyph-name="seven" d="M169 0L428 605H156V476H59V698H540V600L288 0H169Z" />
<glyph unicode="8" glyph-name="eight" d="M300 -12Q238 -12 191 3T112 46T64 110T47 191Q47 261 86 303T188 362V370Q134 389 101 430T68 531Q68 612 128 661T300 710Q411 710 471 661T532 531Q532 471 499 430T412 370V362Q475 345 514 303T553 191Q553 147
537 110T488 46T409 4T300 -12ZM300 77Q366 77 401 106T437 188V209Q437 262 402 291T300 320Q234 320 199 291T163 209V188Q163 135 198 106T300 77ZM300 406Q360 406 391 432T423 507V520Q423 568 392 594T300 621Q240 621 209 595T177 520V507Q177 459 208 433T300
406Z" />
<glyph unicode="9" glyph-name="nine" d="M547 438Q547 362 524 295T464 172T382 72T293 0H143Q203 43 251 83T334 165T394 253T431 357L424 359Q413 338 398 320T363 287T317 265T258 257Q213 257 175 272T109 315T66 384T50 474Q50 526 67 569T117 644T196 692T298
710Q356 710 402 691T481 637T530 551T547 438ZM299 346Q362 346 397 380T432 478V488Q432 551 397 585T299 620Q235 620 200 586T165 488V478Q165 415 200 381T299 346Z" />
<glyph unicode=":" glyph-name="colon" d="M300 -10Q252 -10 233 10T213 59V85Q213 114 232 134T300 154Q348 154 367 134T387 85V59Q387 30 368 10T300 -10ZM300 362Q252 362 233 382T213 431V457Q213 486 232 506T300 526Q348 526 367 506T387 457V431Q387 402
368 382T300 362Z" />
<glyph unicode=";" glyph-name="semicolon" d="M244 152H399L275 -145H190L244 152ZM300 362Q252 362 233 382T213 431V457Q213 486 232 506T300 526Q348 526 367 506T387 457V431Q387 402 368 382T300 362Z" />
<glyph unicode="&lt;" glyph-name="less" d="M85 253V357L515 598V492L184 309V301L515 119V12L85 253Z" />
<glyph unicode="=" glyph-name="equal" d="M62 367V456H538V367H62ZM62 155V244H538V155H62Z" />
<glyph unicode="&gt;" glyph-name="greater" d="M85 118L416 301V309L85 491V598L515 357V253L85 12V118Z" />
<glyph unicode="?" glyph-name="question" d="M226 223V378Q307 381 354 410T401 503V517Q401 567 372 591T293 615Q241 615 209 586T165 511L70 549Q79 580 97 609T143 661T209 696T297 710Q347 710 387 696T457 657T501 595T517 514Q517 469 501 434T459 374T400
333T331 311V223H226ZM281 -10Q235 -10 216 9T197 57V81Q197 110 216 129T281 148Q327 148 346 129T365 81V57Q365 28 346 9T281 -10Z" />
<glyph unicode="@" glyph-name="at" d="M451 -112H335Q256 -112 202 -87T115 -10T67 120T52 302Q52 417 68 495T116 621T194 689T301 710Q363 710 407 691T481 639T523 561T537 463V91H451V150H445Q434 119 411 100T347 80Q288 80 253 131T218 294Q218 405 253
456T347 508Q388 508 411 488T445 438H451V463Q451 633 305 633Q264 633 233 619T182 571T152 486T142 358V229Q142 171 151 123T182 39T241 -15T335 -35H451V-112ZM381 150Q413 150 432 168T451 220V368Q451 402 432 420T381 438Q346 438 327 414T308 326V262Q308
199 327 175T381 150Z" />
<glyph unicode="A" glyph-name="A" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585Z" />
<glyph unicode="B" glyph-name="B" d="M83 698H324Q419 698 471 649T524 515Q524 454 494 418T413 367V362Q474 348 511 309T548 197Q548 154 534 118T494 56T432 15T351 0H83V698ZM317 86Q373 86 403 109T433 183V219Q433 270 403 293T317 316H193V86H317ZM301
398Q354 398 381 418T409 487V523Q409 571 382 591T301 612H193V398H301Z" />
<glyph unicode="C" glyph-name="C" d="M320 -12Q187 -12 123 82T58 349Q58 522 122 616T320 710Q371 710 408 697T473 660T518 605T549 537L448 503Q440 527 430 548T405 584T370 608T319 617Q245 617 211 561T177 408V290Q177 194 211 138T319 81Q349 81 369
90T405 114T430 150T448 195L549 161Q537 125 519 94T473 39T409 2T320 -12Z" />
<glyph unicode="D" glyph-name="D" d="M82 698H290Q424 698 488 609T553 349Q553 178 489 89T290 0H82V698ZM283 92Q360 92 397 143T435 291V407Q435 503 398 554T283 606H193V92H283Z" />
<glyph unicode="E" glyph-name="E" d="M86 0V698H523V604H198V401H511V307H198V94H523V0H86Z" />
<glyph unicode="F" glyph-name="F" d="M86 0V698H535V604H198V401H504V307H198V0H86Z" />
<glyph unicode="G" glyph-name="G" d="M430 96H423Q414 75 402 56T371 21T326 -3T266 -12Q157 -12 102 81T47 344Q47 522 110 616T303 710Q354 710 392 696T458 659T503 604T532 537L431 503Q423 526 413 546T389 583T354 608T304 617Q231 617 199 561T166 408V297Q166
249 173 209T196 141T239 96T304 80Q365 80 397 116T430 210V273H295V357H532V0H430V96Z" />
<glyph unicode="H" glyph-name="H" d="M416 307H184V0H72V698H184V401H416V698H528V0H416V307Z" />
<glyph unicode="I" glyph-name="I" d="M80 0V85H244V613H80V698H520V613H356V85H520V0H80Z" />
<glyph unicode="J" glyph-name="J" d="M493 698V187Q493 142 478 105T434 42T365 2T276 -12Q184 -12 130 34T59 161L165 183Q174 140 199 111T277 82Q326 82 353 111T381 202V604H138V698H493Z" />
<glyph unicode="K" glyph-name="K" d="M274 321L190 216V0H78V698H190V364H195L280 480L451 698H580L350 404L589 0H461L274 321Z" />
<glyph unicode="L" glyph-name="L" d="M108 0V698H220V94H538V0H108Z" />
<glyph unicode="M" glyph-name="M" d="M445 334L448 544H440L300 185L160 544H152L155 334V0H56V698H193L300 421H307L415 698H544V0H445V334Z" />
<glyph unicode="N" glyph-name="N" d="M179 537H170V0H72V698H216L421 161H430V698H528V0H384L179 537Z" />
<glyph unicode="O" glyph-name="O" d="M300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300 -12ZM300 81Q375 81 406 137T437 291V408Q437 504
406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81Z" />
<glyph unicode="P" glyph-name="P" d="M84 0V698H345Q445 698 497 642T550 488Q550 390 498 334T345 278H196V0H84ZM196 371H328Q432 371 432 467V510Q432 605 328 605H196V371Z" />
<glyph unicode="Q" glyph-name="Q" d="M506 -175H378Q310 -175 280 -140T250 -46V-8Q197 1 158 29T94 102T56 210T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 195 506 102T350 -8V-91H506V-175ZM300 81Q375 81
406 137T437 291V408Q437 504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81Z" />
<glyph unicode="R" glyph-name="R" d="M195 0H84V698H345Q445 698 497 642T550 488Q550 408 511 356T399 292L560 0H436L288 282H195V0ZM329 372Q433 372 433 467V510Q433 605 329 605H195V372H329Z" />
<glyph unicode="S" glyph-name="S" d="M294 -12Q203 -12 141 20T39 105L111 174Q151 126 196 104T298 81Q364 81 399 111T435 197Q435 242 409 266T320 302L244 314Q194 322 160 340T105 384T75 441T65 508Q65 607 129 658T305 710Q388 710 446 684T541 609L471
539Q442 574 403 595T305 617Q243 617 211 591T178 513Q178 470 203 446T294 410L368 397Q462 380 505 329T548 203Q548 155 532 115T483 47T403 4T294 -12Z" />
<glyph unicode="T" glyph-name="T" d="M356 604V0H244V604H25V698H575V604H356Z" />
<glyph unicode="U" glyph-name="U" d="M181 698V269Q181 226 183 191T198 132T234 94T300 81Q342 81 366 94T402 131T416 191T419 269V698H531V289Q531 214 523 158T489 64T419 7T300 -12Q227 -12 182 7T111 63T78 157T69 289V698H181Z" />
<glyph unicode="V" glyph-name="V" d="M222 0L25 698H146L238 346L299 109H305L366 346L459 698H575L378 0H222Z" />
<glyph unicode="W" glyph-name="W" d="M75 0L25 698H123L148 292L159 113H167L246 529H357L436 113H444L455 292L481 698H575L525 0H375L304 404H296L225 0H75Z" />
<glyph unicode="X" glyph-name="X" d="M582 0H455L376 138L302 270H296L220 138L138 0H19L233 358L32 698H159L230 573L302 444H307L379 573L453 698H572L370 358L582 0Z" />
<glyph unicode="Y" glyph-name="Y" d="M244 0V263L12 698H137L226 524L298 375H304L377 524L466 698H588L356 263V0H244Z" />
<glyph unicode="Z" glyph-name="Z" d="M552 0H48V99L415 604H62V698H538V599L171 94H552V0Z" />
<glyph unicode="[" glyph-name="bracketleft" d="M207 -138V760H506V682H297V-60H506V-138H207Z" />
<glyph unicode="\" glyph-name="backslash" d="M420 -138L83 760H180L517 -138H420Z" />
<glyph unicode="]" glyph-name="bracketright" d="M393 760V-138H94V-60H303V682H94V760H393Z" />
<glyph unicode="^" glyph-name="asciicircum" d="M475 267L301 601H294L120 267L37 307L241 698H359L563 307L475 267Z" />
<glyph unicode="_" glyph-name="underscore" d="M60 -179V-85H540V-179H60Z" />
<glyph unicode="`" glyph-name="grave" d="M171 745L265 791L356 611L289 579L171 745Z" />
<glyph unicode="a" glyph-name="a" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494 345V86H559V0H492ZM264
68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68Z" />
<glyph unicode="b" glyph-name="b" d="M79 740H188V428H194Q217 475 255 501T350 528Q441 528 495 460T549 258Q549 124 495 56T350 -12Q293 -12 255 14T194 88H188V0H79V740ZM305 77Q367 77 400 115T434 216V300Q434 363 401 401T305 439Q281 439 260 433T223
415T198 386T188 344V172Q188 148 197 131T223 101T260 83T305 77Z" />
<glyph unicode="c" glyph-name="c" d="M319 -12Q261 -12 216 7T139 61T91 146T74 258Q74 320 90 370T138 455T215 509T318 528Q398 528 446 494T519 406L434 360Q420 396 392 417T318 438Q256 438 222 401T188 301V215Q188 154 222 116T320 78Q368 78 398 100T447
160L527 111Q502 57 451 23T319 -12Z" />
<glyph unicode="d" glyph-name="d" d="M412 88H406Q383 41 345 15T250 -12Q159 -12 105 56T51 258Q51 392 105 460T250 528Q307 528 345 502T406 428H412V740H521V0H412V88ZM295 77Q319 77 340 83T377 101T402 130T412 172V344Q412 368 403 385T377 415T340 433T295
439Q233 439 200 401T166 300V216Q166 153 199 115T295 77Z" />
<glyph unicode="e" glyph-name="e" d="M311 -12Q252 -12 205 7T126 61T77 145T60 257Q60 319 77 369T126 454T203 509T303 528Q357 528 401 510T476 457T523 376T540 272V231H170V214Q170 153 208 115T313 76Q363 76 398 97T456 155L524 95Q498 50 445 19T311
-12ZM303 445Q274 445 250 435T208 407T180 363T170 308V301H429V311Q429 372 395 408T303 445Z" />
<glyph unicode="f" glyph-name="f" d="M78 88H243V428H68V516H243V609Q243 667 275 703T375 740H549V652H352V516H549V428H352V88H526V0H78V88Z" />
<glyph unicode="g" glyph-name="g" d="M570 -56Q570 -136 503 -174T299 -212Q232 -212 187 -204T113 -179T72 -140T59 -88Q59 -46 82 -23T149 12V22Q123 32 108 50T92 97Q92 135 118 154T185 184V189Q138 211 112 251T86 347Q86 388 101 421T143 478T209 515T296
528Q350 528 393 511V528Q393 558 410 578T462 598H556V512H436V488Q470 465 488 430T506 347Q506 306 491 273T449 216T383 180T296 167Q262 167 232 173Q214 168 196 156T178 123Q178 99 201 93T258 87H374Q478 87 524 48T570 -56ZM468 -63Q468 -37 447 -22T371
-6H192Q154 -24 154 -64Q154 -93 178 -114T260 -135H341Q403 -135 435 -117T468 -63ZM296 242Q349 242 374 267T399 334V361Q399 403 374 428T296 453Q243 453 218 428T193 361V334Q193 292 218 267T296 242Z" />
<glyph unicode="h" glyph-name="h" d="M84 740H193V428H198Q206 448 218 466T249 498T291 520T347 528Q425 528 473 477T521 332V0H412V316Q412 439 305 439Q284 439 264 434T228 417T203 389T193 350V0H84V740Z" />
<glyph unicode="i" glyph-name="i" d="M332 610Q290 610 274 627T257 669V690Q257 715 273 732T331 749Q373 749 389 732T406 690V669Q406 644 390 627T332 610ZM101 88H277V428H101V516H386V88H551V0H101V88Z" />
<glyph unicode="j" glyph-name="j" d="M98 -112H326V428H92V516H435V-69Q435 -127 403 -163T302 -200H98V-112ZM381 610Q339 610 323 627T306 669V690Q306 715 322 732T380 749Q422 749 438 732T455 690V669Q455 644 439 627T381 610Z" />
<glyph unicode="k" glyph-name="k" d="M88 740H197V293H202L283 376L428 516H557L348 313L580 0H449L269 251L197 184V0H88V740Z" />
<glyph unicode="l" glyph-name="l" d="M75 88H246V652H75V740H355V88H526V0H75V88Z" />
<glyph unicode="m" glyph-name="m" d="M44 0V516H137V449H142Q154 482 176 505T241 528Q282 528 304 506T333 444H337Q351 479 376 503T448 528Q508 528 531 486T555 363V0H462V349Q462 403 449 423T407 444Q381 444 364 427T346 374V0H253V349Q253 403 241 423T199
444Q172 444 155 427T137 374V0H44Z" />
<glyph unicode="n" glyph-name="n" d="M84 0V516H193V428H198Q206 448 218 466T249 498T291 520T347 528Q425 528 473 477T521 332V0H412V316Q412 439 305 439Q284 439 264 434T228 417T203 389T193 350V0H84Z" />
<glyph unicode="o" glyph-name="o" d="M300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196 528 146T480 61T403 7T300 -12ZM300 74Q360 74 395 110T431 219V297Q431 369 396
405T300 442Q240 442 205 406T169 297V219Q169 147 204 111T300 74Z" />
<glyph unicode="p" glyph-name="p" d="M79 516H188V428H194Q217 475 255 501T350 528Q441 528 495 460T549 258Q549 124 495 56T350 -12Q293 -12 255 14T194 88H188V-200H79V516ZM305 77Q367 77 400 115T434 216V300Q434 363 401 401T305 439Q281 439 260 433T223
415T198 386T188 344V172Q188 148 197 131T223 101T260 83T305 77Z" />
<glyph unicode="q" glyph-name="q" d="M412 88H406Q383 41 345 15T250 -12Q159 -12 105 56T51 258Q51 392 105 460T250 528Q307 528 345 502T406 428H412V516H521V-200H412V88ZM295 77Q319 77 340 83T377 101T402 130T412 172V344Q412 368 403 385T377 415T340
433T295 439Q233 439 200 401T166 300V216Q166 153 199 115T295 77Z" />
<glyph unicode="r" glyph-name="r" d="M76 88H213V428H76V516H322V379H328Q335 406 348 431T381 474T430 504T498 516H560V412H458Q398 412 360 377T322 281V88H509V0H76V88Z" />
<glyph unicode="s" glyph-name="s" d="M301 -12Q217 -12 157 15T59 87L126 147Q160 110 202 91T303 71Q356 71 388 89T421 145Q421 161 415 172T398 190T373 201T342 207L260 220Q230 224 199 233T144 258T104 301T88 367Q88 446 148 487T308 528Q380 528 432
507T520 447L459 383Q439 406 402 425T304 445Q195 445 195 376Q195 343 218 331T274 314L356 301Q387 296 417 288T472 264T512 221T528 155Q528 77 468 33T301 -12Z" />
<glyph unicode="t" glyph-name="t" d="M331 0Q263 0 231 36T199 131V428H40V516H153Q182 516 194 527T206 569V698H308V516H528V428H308V88H528V0H331Z" />
<glyph unicode="u" glyph-name="u" d="M407 88H402Q394 68 382 50T351 18T309 -4T253 -12Q175 -12 127 39T79 184V516H188V200Q188 77 295 77Q316 77 336 82T372 99T397 126T407 166V516H516V0H407V88Z" />
<glyph unicode="v" glyph-name="v" d="M233 0L48 516H161L232 298L299 91H305L372 298L443 516H552L367 0H233Z" />
<glyph unicode="w" glyph-name="w" d="M20 516H113L163 82H172L249 516H355L431 82H440L491 516H580L508 0H374L305 400H296L226 0H92L20 516Z" />
<glyph unicode="x" glyph-name="x" d="M45 0L237 262L53 516H180L248 418L302 340H308L361 418L429 516H547L362 266L556 0H428L351 111L298 187H292L240 111L164 0H45Z" />
<glyph unicode="y" glyph-name="y" d="M451 516H561L311 -100Q291 -150 259 -175T165 -200H78V-112H200L247 7L39 516H153L235 300L299 123H305L369 300L451 516Z" />
<glyph unicode="z" glyph-name="z" d="M77 0V96L391 428H88V516H513V420L199 88H523V0H77Z" />
<glyph unicode="{" glyph-name="braceleft" d="M324 -138Q271 -138 247 -112T222 -45V10Q222 36 227 56T240 91T259 119T278 142Q295 162 301 176T308 207Q308 272 183 272H100V350H183Q308 350 308 415Q308 431 302 445T278 480Q269 491 259 503T241 531T228
566T222 612V667Q222 707 246 733T324 760H500V682H312V618Q312 598 315 584T323 557T336 535T352 515Q369 494 384 470T399 414Q399 372 370 347T286 314V308Q340 301 369 276T399 208Q399 176 384 152T352 107Q344 97 337 87T324 65T315 39T312 4V-60H500V-138H324Z"
/>
<glyph unicode="|" glyph-name="bar" d="M253 -138V760H347V-138H253Z" />
<glyph unicode="}" glyph-name="braceright" d="M276 760Q329 760 353 734T378 667V612Q378 586 373 566T359 531T341 504T322 480Q305 460 299 446T292 415Q292 350 417 350H500V272H417Q292 272 292 207Q292 191 298 177T322 142Q331 130 341 118T359 91T373
56T378 10V-45Q378 -85 354 -111T276 -138H100V-60H288V4Q288 44 276 65T248 107Q231 128 216 152T201 208Q201 250 230 275T314 308V314Q260 321 231 346T201 414Q201 446 216 470T248 515Q264 535 276 556T288 618V682H100V760H276Z" />
<glyph unicode="~" glyph-name="asciitilde" d="M408 225Q377 225 350 235T293 260Q267 273 244 283T198 294Q171 294 155 276T125 225L45 256Q59 313 96 349T192 385Q223 385 250 375T307 350Q333 337 356 327T402 316Q429 316 445 334T475 385L555 354Q541 297
504 261T408 225Z" />
<glyph unicode="&#xa0;" glyph-name="uni00A0" />
<glyph unicode="&#xa1;" glyph-name="exclamdown" d="M240 -182V21L281 279H319L360 21V-182H240ZM300 368Q254 368 235 387T216 435V459Q216 488 235 507T300 526Q346 526 365 507T384 459V435Q384 406 365 387T300 368Z" />
<glyph unicode="&#xa2;" glyph-name="cent" d="M270 -114V-8Q176 7 125 77T74 258Q74 368 125 438T270 524V630H354V526Q417 518 457 486T519 406L437 362Q426 391 404 411T350 438V77Q385 84 410 105T450 158L527 111Q505 63 463 31T354 -10V-114H270ZM183 215Q183
164 206 128T275 80V436Q230 424 207 388T183 301V215Z" />
<glyph unicode="&#xa3;" glyph-name="sterling" d="M72 0V118Q114 135 134 167T154 245Q154 262 151 279H49V365H125Q113 397 103 429T93 501Q93 547 110 585T158 652T234 695T333 710Q411 710 465 681T554 600L476 535Q450 571 417 592T332 613Q275 613 241 583T207
488Q207 453 216 424T236 365H426V279H262Q263 272 263 266T264 253Q264 224 257 201T238 160T212 129T183 107V100H544V0H72Z" />
<glyph unicode="&#xa5;" glyph-name="yen" d="M67 87H247V241H67V328H192L13 698H132L298 328H305L471 698H587L408 328H533V241H353V87H533V0H67V87Z" />
<glyph unicode="&#xa6;" glyph-name="brokenbar" d="M253 401V760H347V401H253ZM253 -138V221H347V-138H253Z" />
<glyph unicode="&#xa7;" glyph-name="section" d="M497 3Q497 -35 482 -66T439 -119T373 -152T287 -164Q239 -164 191 -150T104 -101L162 -32Q187 -55 218 -67T288 -80Q339 -80 368 -59T398 0Q398 33 373 52T300 85L230 105Q146 128 110 168T74 258Q74 304 102
340T185 398V408Q145 433 127 467T108 543Q108 581 123 612T166 664T232 698T318 710Q366 710 414 696T501 647L443 578Q418 601 387 613T317 626Q266 626 237 605T207 546Q207 513 232 494T305 461L375 441Q459 418 495 378T531 288Q531 242 503 206T420 148V138Q460
113 478 79T497 3ZM433 258Q433 294 410 318T329 358L260 376Q251 378 243 381T227 387Q201 367 187 343T172 288Q172 252 195 228T276 188L345 170Q354 168 362 165T378 159Q404 179 418 203T433 258Z" />
<glyph unicode="&#xa8;" glyph-name="dieresis" d="M200 607Q163 607 148 622T133 660V679Q133 702 148 717T200 732Q237 732 252 717T267 679V660Q267 637 252 622T200 607ZM400 607Q363 607 348 622T333 660V679Q333 702 348 717T400 732Q437 732 452 717T467
679V660Q467 637 452 622T400 607Z" />
<glyph unicode="&#xa9;" glyph-name="copyright" d="M300 18Q237 18 183 41T88 107T24 211T0 349Q0 425 23 486T87 591T182 657T300 680Q363 680 417 657T512 591T576 487T600 349Q600 272 577 211T513 107T418 41T300 18ZM300 73Q351 73 394 92T469 144T519 223T537
319V379Q537 430 519 475T470 553T395 606T300 625Q249 625 206 606T131 554T81 475T63 379V319Q63 268 81 223T130 145T205 92T300 73ZM307 163Q229 163 185 213T140 349Q140 434 185 484T307 535Q361 535 394 510T444 445L378 410Q367 434 351 448T308 462Q273
462 253 440T233 380V317Q233 281 252 259T309 236Q338 236 355 251T385 289L450 253Q433 215 399 189T307 163Z" />
<glyph unicode="&#xaa;" glyph-name="ordfeminine" d="M429 350Q397 350 380 365T361 407H357Q346 379 319 361T246 342Q192 342 163 369T133 445Q133 499 173 525T288 552H355V575Q355 648 283 648Q249 648 227 634T192 599L144 640Q160 668 196 689T290 710Q358
710 396 677T435 579V414H474V350H429ZM288 501Q252 501 233 490T213 456V444Q213 421 228 411T270 400Q305 400 330 415T355 460V501H288Z" />
<glyph unicode="&#xab;" glyph-name="guillemotleft" d="M528 47L324 219V321L528 493L561 420L421 270L561 120L528 47ZM508 47L304 219V321L508 493L541 420L401 270L541 120L508 47Z" />
<glyph unicode="&#xac;" glyph-name="logicalnot" d="M431 68V261H62V350H522V68H431Z" />
<glyph unicode="&#xad;" glyph-name="uni00AD" d="M149 250V359H451V250H149Z" />
<glyph unicode="&#xae;" glyph-name="registered" d="M300 346Q262 346 229 360T170 398T131 455T117 528Q117 567 131 600T170 658T228 696T300 710Q338 710 371 696T429 658T468 601T483 528Q483 489 469 456T430 398T372 360T300 346ZM300 387Q329 387 353
396T396 423T424 464T435 518V538Q435 568 425 592T396 633T354 659T300 669Q271 669 247 660T204 633T176 592T165 538V518Q165 488 175 464T204 423T246 397T300 387ZM273 436H227V621H315Q347 621 363 605T379 561Q379 541 370 528T343 507L386 436H335L299
499H273V436ZM304 533Q330 533 330 555V563Q330 585 304 585H273V533H304Z" />
<glyph unicode="&#xaf;" glyph-name="overscore" d="M155 710H445V628H155V710Z" />
<glyph unicode="&#xb0;" glyph-name="degree" d="M300 354Q262 354 230 367T173 405T136 461T122 532Q122 570 135 602T173 659T229 696T300 710Q337 710 369 697T426 659T464 603T478 532Q478 494 464 462T426 405T370 368T300 354ZM300 431Q343 431 369 460T395
532Q395 575 369 604T300 633Q257 633 231 604T205 532Q205 489 231 460T300 431Z" />
<glyph unicode="&#xb1;" glyph-name="plusminus" d="M251 164V350H62V439H251V625H349V439H538V350H349V164H251ZM62 0V89H538V0H62Z" />
<glyph unicode="&#xb2;" glyph-name="twosuperior" d="M443 329H164V399L285 495Q318 521 333 541T349 586V590Q349 614 334 626T293 638Q263 638 248 623T225 585L156 611Q170 649 204 676T300 704Q364 704 399 673T434 593Q434 570 426 550T404 513T371 481T331
450L250 393H443V329Z" />
<glyph unicode="&#xb3;" glyph-name="threesuperior" d="M283 551Q317 551 332 563T348 594V598Q348 618 333 630T292 642Q240 642 210 598L159 642Q180 670 211 687T294 704Q357 704 394 678T432 606Q432 571 410 550T355 523V520Q390 514 414 492T438 432Q438
382 398 353T290 323Q231 323 198 344T146 393L205 437Q218 414 237 400T290 385Q322 385 338 398T355 436V440Q355 465 337 476T285 487H246V551H283Z" />
<glyph unicode="&#xb4;" glyph-name="acute" d="M311 579L244 611L335 791L429 745L311 579Z" />
<glyph unicode="&#xb5;" glyph-name="mu" d="M84 -200V516H193V204Q193 77 297 77Q318 77 338 82T373 99T397 126T407 166V516H516V0H407V88H402Q394 68 384 50T357 18T320 -4T272 -12Q241 -12 219 -1T181 36H176L193 -69V-200H84Z" />
<glyph unicode="&#xb6;" glyph-name="paragraph" d="M260 246Q213 246 173 263T101 310T53 381T35 472Q35 521 52 562T101 634T172 681T260 698H524V-149H432V612H352V-149H260V246Z" />
<glyph unicode="&#xb7;" glyph-name="middot" d="M300 222Q252 222 233 242T213 291V317Q213 346 232 366T300 386Q348 386 367 366T387 317V291Q387 262 368 242T300 222Z" />
<glyph unicode="&#xb8;" glyph-name="cedilla" d="M313 -209Q268 -209 242 -195T203 -167L245 -120Q254 -131 269 -139T308 -148Q327 -148 339 -141T352 -119Q352 -107 338 -96T281 -79L256 -76L276 24H333L316 -58L320 -62Q331 -59 341 -57T363 -55Q392 -55 412
-71T433 -123Q433 -146 423 -162T397 -188T359 -204T313 -209Z" />
<glyph unicode="&#xb9;" glyph-name="onesuperior" d="M181 329V391H283V628L189 581L158 637L278 698H362V391H453V329H181Z" />
<glyph unicode="&#xba;" glyph-name="ordmasculine" d="M300 342Q221 342 176 391T131 526Q131 612 176 661T300 710Q379 710 424 661T469 526Q469 440 424 391T300 342ZM300 405Q341 405 362 430T384 497V555Q384 597 363 622T300 647Q259 647 238 622T216 555V497Q216
455 237 430T300 405Z" />
<glyph unicode="&#xbb;" glyph-name="guillemotright" d="M60 120L200 270L60 420L93 493L297 321V219L93 47L60 120ZM296 120L436 270L296 420L329 493L533 321V219L329 47L296 120Z" />
<glyph unicode="&#xbc;" glyph-name="onequarter" d="M39 421H129V636L45 593L18 643L124 698H202V421H282V363H39V421ZM504 698H589L424 398H339L504 698ZM176 300H261L96 0H11L176 300ZM471 64H313V129L450 335H541V117H587V64H541V0H471V64ZM471 117V269H467L365
117H471Z" />
<glyph unicode="&#xbd;" glyph-name="onehalf" d="M39 421H129V636L45 593L18 643L124 698H202V421H282V363H39V421ZM504 698H589L424 398H339L504 698ZM176 300H261L96 0H11L176 300ZM326 65L436 151Q466 175 478 194T491 237V241Q491 259 478 270T443 281Q418
281 404 267T380 230L318 254Q324 271 334 286T359 314T397 333T448 341Q507 341 538 313T569 242Q569 221 562 204T541 170T510 138T472 108L405 58H578V0H326V65Z" />
<glyph unicode="&#xbe;" glyph-name="threequarters" d="M146 357Q92 357 62 378T15 426L69 463Q80 442 98 428T145 414Q172 414 186 426T201 458V465Q201 486 184 497T136 508H105V563H138Q168 563 182 574T196 601V609Q196 626 183 636T146 647Q99 647 71 606L25
646Q45 673 73 688T147 704Q204 704 238 680T272 615Q272 583 252 564T202 539V535Q235 530 256 511T278 456Q278 411 242 384T146 357ZM504 698H589L424 398H339L504 698ZM176 300H261L96 0H11L176 300ZM471 64H313V129L450 335H541V117H587V64H541V0H471V64ZM471
117V269H467L365 117H471Z" />
<glyph unicode="&#xbf;" glyph-name="questiondown" d="M303 -194Q253 -194 213 -180T143 -141T99 -79T83 2Q83 47 99 82T141 142T200 183T269 205V293H374V138Q293 135 246 106T199 13V-1Q199 -51 228 -75T307 -99Q359 -99 391 -70T435 5L530 -33Q521 -65 503
-94T457 -145T391 -180T303 -194ZM319 368Q273 368 254 387T235 435V459Q235 488 254 507T319 526Q365 526 384 507T403 459V435Q403 406 384 387T319 368Z" />
<glyph unicode="&#xc0;" glyph-name="Agrave" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM171 915L265 961L356 781L289 749L171 915Z" />
<glyph unicode="&#xc1;" glyph-name="Aacute" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM311 749L244 781L335 961L429 915L311 749Z" />
<glyph unicode="&#xc2;" glyph-name="Acircumflex" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM355 944L475 794L417 752L299 868L180 752L125 794L245 944H355Z" />
<glyph unicode="&#xc3;" glyph-name="Atilde" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM374 774Q349 774 331 781T296 797Q273 808 256 814T222 820Q205 820 192 814T164 794L123 846Q138 869 163 886T226 903Q251
903 269 896T304 880Q327 869 344 863T378 857Q395 857 408 863T436 883L477 831Q462 808 437 791T374 774Z" />
<glyph unicode="&#xc4;" glyph-name="Adieresis" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM200 777Q163 777 148 792T133 830V849Q133 872 148 887T200 902Q237 902 252 887T267 849V830Q267 807 252 792T200
777ZM400 777Q363 777 348 792T333 830V849Q333 872 348 887T400 902Q437 902 452 887T467 849V830Q467 807 452 792T400 777Z" />
<glyph unicode="&#xc5;" glyph-name="Aring" d="M462 0L409 186H187L134 0H20L224 698H377L581 0H462ZM302 585H294L208 279H388L302 585ZM300 735Q273 735 249 744T208 770T181 810T171 861Q171 889 181 912T208 952T249 978T300 988Q327 988 351 979T392 952T419
912T429 861Q429 834 419 811T392 771T351 745T300 735ZM300 795Q355 795 355 848V875Q355 928 300 928Q245 928 245 875V848Q245 795 300 795Z" />
<glyph unicode="&#xc6;" glyph-name="AE" d="M298 187H166L120 0H10L196 698H565V611H403V397H554V310H403V87H565V0H298V187ZM271 620L186 274H298V620H271Z" />
<glyph unicode="&#xc7;" glyph-name="Ccedilla" d="M319 617Q245 617 211 561T177 408V290Q177 242 185 204T211 138T255 96T319 81Q349 81 369 90T405 114T430 150T448 195L549 161Q537 126 520 95T476 41T415 4T331 -12L322 -58L326 -62Q337 -59 347 -57T368
-55Q398 -55 418 -71T439 -123Q439 -146 429 -162T403 -188T365 -204T319 -209Q273 -209 247 -195T209 -167L251 -120Q260 -131 275 -139T314 -148Q333 -148 345 -141T358 -119Q358 -107 344 -96T287 -79L262 -76L275 -9Q164 7 111 98T58 349Q58 522 122 616T320
710Q371 710 408 697T473 660T518 605T549 537L448 503Q440 527 430 548T405 584T370 608T319 617Z" />
<glyph unicode="&#xc8;" glyph-name="Egrave" d="M175 915L269 961L360 781L293 749L175 915ZM86 0V698H523V604H198V401H511V307H198V94H523V0H86Z" />
<glyph unicode="&#xc9;" glyph-name="Eacute" d="M86 0V698H523V604H198V401H511V307H198V94H523V0H86ZM315 749L248 781L339 961L433 915L315 749Z" />
<glyph unicode="&#xca;" glyph-name="Ecircumflex" d="M359 944L479 794L421 752L303 868L184 752L129 794L249 944H359ZM86 0V698H523V604H198V401H511V307H198V94H523V0H86Z" />
<glyph unicode="&#xcb;" glyph-name="Edieresis" d="M86 0V698H523V604H198V401H511V307H198V94H523V0H86ZM204 777Q167 777 152 792T137 830V849Q137 872 152 887T204 902Q241 902 256 887T271 849V830Q271 807 256 792T204 777ZM404 777Q367 777 352 792T337
830V849Q337 872 352 887T404 902Q441 902 456 887T471 849V830Q471 807 456 792T404 777Z" />
<glyph unicode="&#xcc;" glyph-name="Igrave" d="M80 0V85H244V613H80V698H520V613H356V85H520V0H80ZM171 915L265 961L356 781L289 749L171 915Z" />
<glyph unicode="&#xcd;" glyph-name="Iacute" d="M80 0V85H244V613H80V698H520V613H356V85H520V0H80ZM311 749L244 781L335 961L429 915L311 749Z" />
<glyph unicode="&#xce;" glyph-name="Icircumflex" d="M80 0V85H244V613H80V698H520V613H356V85H520V0H80ZM355 944L475 794L417 752L299 868L180 752L125 794L245 944H355Z" />
<glyph unicode="&#xcf;" glyph-name="Idieresis" d="M80 0V85H244V613H80V698H520V613H356V85H520V0H80ZM200 777Q163 777 148 792T133 830V849Q133 872 148 887T200 902Q237 902 252 887T267 849V830Q267 807 252 792T200 777ZM400 777Q363 777 348 792T333 830V849Q333
872 348 887T400 902Q437 902 452 887T467 849V830Q467 807 452 792T400 777Z" />
<glyph unicode="&#xd0;" glyph-name="Eth" d="M88 326H17V411H88V698H290Q424 698 488 609T553 349Q553 178 489 89T290 0H88V326ZM283 92Q360 92 397 143T435 291V407Q435 503 398 554T283 606H199V408H327V328H199V92H283Z" />
<glyph unicode="&#xd1;" glyph-name="Ntilde" d="M374 774Q349 774 331 781T296 797Q273 808 256 814T222 820Q205 820 192 814T164 794L123 846Q138 869 163 886T226 903Q251 903 269 896T304 880Q327 869 344 863T378 857Q395 857 408 863T436 883L477 831Q462
808 437 791T374 774ZM179 537H170V0H72V698H216L421 161H430V698H528V0H384L179 537Z" />
<glyph unicode="&#xd2;" glyph-name="Ograve" d="M171 915L265 961L356 781L289 749L171 915ZM300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300
-12ZM300 81Q375 81 406 137T437 291V408Q437 504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81Z" />
<glyph unicode="&#xd3;" glyph-name="Oacute" d="M311 749L244 781L335 961L429 915L311 749ZM300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300
-12ZM300 81Q375 81 406 137T437 291V408Q437 504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81Z" />
<glyph unicode="&#xd4;" glyph-name="Ocircumflex" d="M355 944L475 794L417 752L299 868L180 752L125 794L245 944H355ZM300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263
541 196T495 83T415 13T300 -12ZM300 81Q375 81 406 137T437 291V408Q437 504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81Z" />
<glyph unicode="&#xd5;" glyph-name="Otilde" d="M300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300 -12ZM300 81Q375 81 406 137T437 291V408Q437
504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81ZM374 774Q349 774 331 781T296 797Q273 808 256 814T222 820Q205 820 192 814T164 794L123 846Q138 869 163 886T226 903Q251 903 269 896T304 880Q327 869 344 863T378 857Q395 857 408
863T436 883L477 831Q462 808 437 791T374 774Z" />
<glyph unicode="&#xd6;" glyph-name="Odieresis" d="M300 -12Q234 -12 186 12T106 83T59 196T44 349Q44 434 59 501T105 615T185 685T300 710Q366 710 414 686T494 615T541 502T556 349Q556 263 541 196T495 83T415 13T300 -12ZM300 81Q375 81 406 137T437 291V408Q437
504 406 560T300 617Q225 617 194 561T163 408V290Q163 194 194 138T300 81ZM200 777Q163 777 148 792T133 830V849Q133 872 148 887T200 902Q237 902 252 887T267 849V830Q267 807 252 792T200 777ZM400 777Q363 777 348 792T333 830V849Q333 872 348 887T400
902Q437 902 452 887T467 849V830Q467 807 452 792T400 777Z" />
<glyph unicode="&#xd7;" glyph-name="multiply" d="M300 241L145 85L80 150L236 305L80 460L145 525L300 369L455 525L520 460L364 305L520 150L455 85L300 241Z" />
<glyph unicode="&#xd8;" glyph-name="Oslash" d="M300 -12Q213 -12 159 29L110 -53L35 -10L98 94Q70 140 57 204T44 349Q44 434 59 501T105 615T185 685T300 710Q387 710 441 669L490 751L565 708L502 603Q530 557 543 494T556 349Q556 263 541 196T495 83T415
13T300 -12ZM163 290Q163 266 164 245T171 203L392 580Q358 617 300 617Q225 617 194 561T163 408V290ZM300 81Q375 81 406 137T437 290V408Q437 432 436 453T429 495L208 118Q242 81 300 81Z" />
<glyph unicode="&#xd9;" glyph-name="Ugrave" d="M171 915L265 961L356 781L289 749L171 915ZM181 698V269Q181 226 183 191T198 132T234 94T300 81Q342 81 366 94T402 131T416 191T419 269V698H531V289Q531 214 523 158T489 64T419 7T300 -12Q227 -12 182 7T111
63T78 157T69 289V698H181Z" />
<glyph unicode="&#xda;" glyph-name="Uacute" d="M181 698V269Q181 226 183 191T198 132T234 94T300 81Q342 81 366 94T402 131T416 191T419 269V698H531V289Q531 214 523 158T489 64T419 7T300 -12Q227 -12 182 7T111 63T78 157T69 289V698H181ZM311 749L244
781L335 961L429 915L311 749Z" />
<glyph unicode="&#xdb;" glyph-name="Ucircumflex" d="M355 944L475 794L417 752L299 868L180 752L125 794L245 944H355ZM181 698V269Q181 226 183 191T198 132T234 94T300 81Q342 81 366 94T402 131T416 191T419 269V698H531V289Q531 214 523 158T489 64T419
7T300 -12Q227 -12 182 7T111 63T78 157T69 289V698H181Z" />
<glyph unicode="&#xdc;" glyph-name="Udieresis" d="M181 698V269Q181 226 183 191T198 132T234 94T300 81Q342 81 366 94T402 131T416 191T419 269V698H531V289Q531 214 523 158T489 64T419 7T300 -12Q227 -12 182 7T111 63T78 157T69 289V698H181ZM200 777Q163
777 148 792T133 830V849Q133 872 148 887T200 902Q237 902 252 887T267 849V830Q267 807 252 792T200 777ZM400 777Q363 777 348 792T333 830V849Q333 872 348 887T400 902Q437 902 452 887T467 849V830Q467 807 452 792T400 777Z" />
<glyph unicode="&#xdd;" glyph-name="Yacute" d="M244 0V263L12 698H137L226 524L298 375H304L377 524L466 698H588L356 263V0H244ZM311 749L244 781L335 961L429 915L311 749Z" />
<glyph unicode="&#xde;" glyph-name="Thorn" d="M84 0V698H196V563H343Q443 563 495 507T548 354Q548 258 496 202T343 146H196V0H84ZM196 239H327Q432 239 432 333V376Q432 470 327 470H196V239Z" />
<glyph unicode="&#xdf;" glyph-name="germandbls" d="M84 0V609Q84 667 116 703T217 740H363V654H191V516H531V426L373 212Q467 202 520 154T573 15Q573 -90 502 -145T311 -200H245V-114H307Q385 -114 423 -86T461 3V27Q461 87 421 112T294 138H275V224L427 430H191V0H84Z"
/>
<glyph unicode="&#xe0;" glyph-name="agrave" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM415 745L509 791L600 611L533 579L415 745Z" />
<glyph unicode="&#xe1;" glyph-name="aacute" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM555 579L488 611L579 791L673 745L555 579Z" />
<glyph unicode="&#xe2;" glyph-name="acircumflex" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM599 774L719 624L661 582L543 698L424 582L369 624L489 774H599Z" />
<glyph unicode="&#xe3;" glyph-name="atilde" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM618 604Q593 604 575 611T540 627Q517 638 500 644T466 650Q449 650 436 644T408 624L367 676Q382 699 407 716T470 733Q495 733 513 726T548 710Q571 699
588 693T622 687Q639 687 652 693T680 713L721 661Q706 638 681 621T618 604Z" />
<glyph unicode="&#xe4;" glyph-name="adieresis" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM444 607Q407 607 392 622T377 660V679Q377 702 392 717T444 732Q481 732 496 717T511 679V660Q511 637 496 622T444 607ZM644 607Q607 607 592 622T577 660V679Q577
702 592 717T644 732Q681 732 696 717T711 679V660Q711 637 696 622T644 607Z" />
<glyph unicode="&#xe5;" glyph-name="aring" d="M492 0Q445 0 422 24T393 87H388Q373 40 334 14T233 -12Q154 -12 107 29T60 144Q60 219 115 258T286 298H385V338Q385 443 274 443Q224 443 193 424T141 372L76 425Q98 467 150 497T284 528Q382 528 438 481T494
345V86H559V0H492ZM264 68Q317 68 351 92T385 154V229H288Q170 229 170 158V138Q170 103 195 86T264 68ZM544 565Q517 565 493 574T452 600T425 640T415 691Q415 719 425 742T452 782T493 808T544 818Q571 818 595 809T636 782T663 742T673 691Q673 664 663 641T636
601T595 575T544 565ZM544 625Q599 625 599 678V705Q599 758 544 758Q489 758 489 705V678Q489 625 544 625Z" />
<glyph unicode="&#xe6;" glyph-name="ae" d="M139 -12Q83 -12 45 26T7 139Q7 219 53 259T187 299H241V354Q241 407 223 430T169 454Q136 454 118 434T88 380L18 413Q35 464 73 496T171 528Q216 528 247 509T295 452H299Q319 493 350 510T421 528Q499 528 541 467T583
294V234H333V213Q333 147 355 107T419 66Q453 66 471 90T501 147L576 120Q568 94 555 71T523 29T477 -1T418 -12Q367 -12 328 17T268 103H264Q253 43 220 16T139 -12ZM161 62Q203 62 222 91T241 165V234H198Q150 234 125 215T99 154V137Q99 102 113 82T161 62ZM416
456Q375 456 354 426T333 338V299H498V338Q498 395 477 425T416 456Z" />
<glyph unicode="&#xe7;" glyph-name="ccedilla" d="M318 528Q398 528 446 494T519 406L434 360Q420 396 392 417T318 438Q256 438 222 401T188 301V215Q188 154 222 116T320 78Q368 78 398 100T447 160L527 111Q503 57 455 24T328 -12L318 -58L322 -62Q333 -59
343 -57T365 -55Q394 -55 414 -71T435 -123Q435 -146 425 -162T399 -188T361 -204T315 -209Q270 -209 244 -195T206 -167L247 -120Q256 -131 271 -139T310 -148Q329 -148 341 -141T354 -119Q354 -107 341 -96T283 -79L258 -76L272 -8Q178 6 126 77T74 258Q74 320
90 370T138 455T215 509T318 528Z" />
<glyph unicode="&#xe8;" glyph-name="egrave" d="M311 -12Q252 -12 205 7T126 61T77 145T60 257Q60 319 77 369T126 454T203 509T303 528Q357 528 401 510T476 457T523 376T540 272V231H170V214Q170 153 208 115T313 76Q363 76 398 97T456 155L524 95Q498 50 445
19T311 -12ZM303 445Q274 445 250 435T208 407T180 363T170 308V301H429V311Q429 372 395 408T303 445ZM172 745L266 791L357 611L290 579L172 745Z" />
<glyph unicode="&#xe9;" glyph-name="eacute" d="M312 579L245 611L336 791L430 745L312 579ZM311 -12Q252 -12 205 7T126 61T77 145T60 257Q60 319 77 369T126 454T203 509T303 528Q357 528 401 510T476 457T523 376T540 272V231H170V214Q170 153 208 115T313
76Q363 76 398 97T456 155L524 95Q498 50 445 19T311 -12ZM303 445Q274 445 250 435T208 407T180 363T170 308V301H429V311Q429 372 395 408T303 445Z" />
<glyph unicode="&#xea;" glyph-name="ecircumflex" d="M356 774L476 624L418 582L300 698L181 582L126 624L246 774H356ZM311 -12Q252 -12 205 7T126 61T77 145T60 257Q60 319 77 369T126 454T203 509T303 528Q357 528 401 510T476 457T523 376T540 272V231H170V214Q170
153 208 115T313 76Q363 76 398 97T456 155L524 95Q498 50 445 19T311 -12ZM303 445Q274 445 250 435T208 407T180 363T170 308V301H429V311Q429 372 395 408T303 445Z" />
<glyph unicode="&#xeb;" glyph-name="edieresis" d="M201 607Q164 607 149 622T134 660V679Q134 702 149 717T201 732Q238 732 253 717T268 679V660Q268 637 253 622T201 607ZM401 607Q364 607 349 622T334 660V679Q334 702 349 717T401 732Q438 732 453 717T468
679V660Q468 637 453 622T401 607ZM311 -12Q252 -12 205 7T126 61T77 145T60 257Q60 319 77 369T126 454T203 509T303 528Q357 528 401 510T476 457T523 376T540 272V231H170V214Q170 153 208 115T313 76Q363 76 398 97T456 155L524 95Q498 50 445 19T311 -12ZM303
445Q274 445 250 435T208 407T180 363T170 308V301H429V311Q429 372 395 408T303 445Z" />
<glyph unicode="&#xec;" glyph-name="igrave" d="M101 88H277V428H101V516H386V88H551V0H101V88ZM203 745L297 791L388 611L321 579L203 745Z" />
<glyph unicode="&#xed;" glyph-name="iacute" d="M343 579L276 611L367 791L461 745L343 579ZM101 88H277V428H101V516H386V88H551V0H101V88Z" />
<glyph unicode="&#xee;" glyph-name="icircumflex" d="M387 774L507 624L449 582L331 698L212 582L157 624L277 774H387ZM101 88H277V428H101V516H386V88H551V0H101V88Z" />
<glyph unicode="&#xef;" glyph-name="idieresis" d="M232 607Q195 607 180 622T165 660V679Q165 702 180 717T232 732Q269 732 284 717T299 679V660Q299 637 284 622T232 607ZM432 607Q395 607 380 622T365 660V679Q365 702 380 717T432 732Q469 732 484 717T499
679V660Q499 637 484 622T432 607ZM101 88H277V428H101V516H386V88H551V0H101V88Z" />
<glyph unicode="&#xf0;" glyph-name="eth" d="M469 702L387 648Q419 616 447 577T498 491T532 389T545 271Q545 199 528 146T480 57T403 5T303 -12Q244 -12 198 6T121 58T72 140T55 249Q55 307 70 353T113 433T179 484T264 502Q324 502 363 475T426 406L432 409Q412
464 380 510T306 597L210 534L167 585L254 642Q220 670 183 694T103 740H271Q286 730 302 719T336 694L426 753L469 702ZM301 74Q361 74 396 109T432 213V276Q432 345 396 380T300 415Q240 415 205 380T169 276V213Q169 144 205 109T301 74Z" />
<glyph unicode="&#xf1;" glyph-name="ntilde" d="M84 0V516H193V428H198Q206 448 218 466T249 498T291 520T347 528Q425 528 473 477T521 332V0H412V316Q412 439 305 439Q284 439 264 434T228 417T203 389T193 350V0H84ZM376 604Q351 604 333 611T298 627Q275
638 258 644T224 650Q207 650 194 644T166 624L125 676Q140 699 165 716T228 733Q253 733 271 726T306 710Q329 699 346 693T380 687Q397 687 410 693T438 713L479 661Q464 638 439 621T376 604Z" />
<glyph unicode="&#xf2;" glyph-name="ograve" d="M300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196 528 146T480 61T403 7T300 -12ZM300 74Q360 74 395 110T431 219V297Q431
369 396 405T300 442Q240 442 205 406T169 297V219Q169 147 204 111T300 74ZM171 745L265 791L356 611L289 579L171 745Z" />
<glyph unicode="&#xf3;" glyph-name="oacute" d="M311 579L244 611L335 791L429 745L311 579ZM300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196 528 146T480 61T403 7T300
-12ZM300 74Q360 74 395 110T431 219V297Q431 369 396 405T300 442Q240 442 205 406T169 297V219Q169 147 204 111T300 74Z" />
<glyph unicode="&#xf4;" glyph-name="ocircumflex" d="M355 774L475 624L417 582L299 698L180 582L125 624L245 774H355ZM300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196
528 146T480 61T403 7T300 -12ZM300 74Q360 74 395 110T431 219V297Q431 369 396 405T300 442Q240 442 205 406T169 297V219Q169 147 204 111T300 74Z" />
<glyph unicode="&#xf5;" glyph-name="otilde" d="M300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196 528 146T480 61T403 7T300 -12ZM300 74Q360 74 395 110T431 219V297Q431
369 396 405T300 442Q240 442 205 406T169 297V219Q169 147 204 111T300 74ZM374 604Q349 604 331 611T296 627Q273 638 256 644T222 650Q205 650 192 644T164 624L123 676Q138 699 163 716T226 733Q251 733 269 726T304 710Q327 699 344 693T378 687Q395 687 408
693T436 713L477 661Q462 638 437 621T374 604Z" />
<glyph unicode="&#xf6;" glyph-name="odieresis" d="M200 607Q163 607 148 622T133 660V679Q133 702 148 717T200 732Q237 732 252 717T267 679V660Q267 637 252 622T200 607ZM400 607Q363 607 348 622T333 660V679Q333 702 348 717T400 732Q437 732 452 717T467
679V660Q467 637 452 622T400 607ZM300 -12Q243 -12 198 7T121 61T72 146T55 258Q55 320 72 370T120 455T197 509T300 528Q357 528 402 509T479 455T528 370T545 258Q545 196 528 146T480 61T403 7T300 -12ZM300 74Q360 74 395 110T431 219V297Q431 369 396 405T300
442Q240 442 205 406T169 297V219Q169 147 204 111T300 74Z" />
<glyph unicode="&#xf7;" glyph-name="divide" d="M62 261V350H538V261H62ZM300 42Q260 42 244 58T227 99V122Q227 146 243 162T300 179Q340 179 356 163T373 122V99Q373 75 357 59T300 42ZM300 432Q260 432 244 448T227 489V512Q227 536 243 552T300 569Q340 569
356 553T373 512V489Q373 465 357 449T300 432Z" />
<glyph unicode="&#xf8;" glyph-name="oslash" d="M37 -3L105 81Q55 149 55 258Q55 320 72 370T120 455T197 509T300 528Q384 528 441 488L503 564L563 519L495 435Q545 367 545 258Q545 196 528 146T480 61T403 7T300 -12Q216 -12 159 28L97 -48L37 -3ZM300 442Q240
442 205 406T169 297V223Q169 206 170 190T177 161L385 415Q353 442 300 442ZM300 74Q360 74 395 110T431 219V293Q431 310 430 326T423 355L215 101Q231 87 252 81T300 74Z" />
<glyph unicode="&#xf9;" glyph-name="ugrave" d="M407 88H402Q394 68 382 50T351 18T309 -4T253 -12Q175 -12 127 39T79 184V516H188V200Q188 77 295 77Q316 77 336 82T372 99T397 126T407 166V516H516V0H407V88ZM426 745L520 791L611 611L544 579L426 745Z" />
<glyph unicode="&#xfa;" glyph-name="uacute" d="M566 579L499 611L590 791L684 745L566 579ZM407 88H402Q394 68 382 50T351 18T309 -4T253 -12Q175 -12 127 39T79 184V516H188V200Q188 77 295 77Q316 77 336 82T372 99T397 126T407 166V516H516V0H407V88Z" />
<glyph unicode="&#xfb;" glyph-name="ucircumflex" d="M610 774L730 624L672 582L554 698L435 582L380 624L500 774H610ZM407 88H402Q394 68 382 50T351 18T309 -4T253 -12Q175 -12 127 39T79 184V516H188V200Q188 77 295 77Q316 77 336 82T372 99T397 126T407
166V516H516V0H407V88Z" />
<glyph unicode="&#xfc;" glyph-name="udieresis" d="M455 607Q418 607 403 622T388 660V679Q388 702 403 717T455 732Q492 732 507 717T522 679V660Q522 637 507 622T455 607ZM655 607Q618 607 603 622T588 660V679Q588 702 603 717T655 732Q692 732 707 717T722
679V660Q722 637 707 622T655 607ZM407 88H402Q394 68 382 50T351 18T309 -4T253 -12Q175 -12 127 39T79 184V516H188V200Q188 77 295 77Q316 77 336 82T372 99T397 126T407 166V516H516V0H407V88Z" />
<glyph unicode="&#xfd;" glyph-name="yacute" d="M451 516H561L311 -100Q291 -150 259 -175T165 -200H78V-112H200L247 7L39 516H153L235 300L299 123H305L369 300L451 516ZM312 579L245 611L336 791L430 745L312 579Z" />
<glyph unicode="&#xfe;" glyph-name="thorn" d="M79 740H188V428H194Q217 475 255 501T350 528Q441 528 495 460T549 258Q549 124 495 56T350 -12Q293 -12 255 14T194 88H188V-200H79V740ZM305 77Q367 77 400 115T434 216V300Q434 363 401 401T305 439Q281 439
260 433T223 415T198 386T188 344V172Q188 148 197 131T223 101T260 83T305 77Z" />
<glyph unicode="&#xff;" glyph-name="ydieresis" d="M451 516H561L311 -100Q291 -150 259 -175T165 -200H78V-112H200L247 7L39 516H153L235 300L299 123H305L369 300L451 516ZM201 607Q164 607 149 622T134 660V679Q134 702 149 717T201 732Q238 732 253 717T268
679V660Q268 637 253 622T201 607ZM401 607Q364 607 349 622T334 660V679Q334 702 349 717T401 732Q438 732 453 717T468 679V660Q468 637 453 622T401 607Z" />
<glyph unicode="&#x2013;" glyph-name="endash" d="M60 257V351H540V257H60Z" />
<glyph unicode="&#x2014;" glyph-name="emdash" d="M0 257V351H600V257H0Z" />
<glyph unicode="&#x2018;" glyph-name="quoteleft" d="M306 740H390L336 442H181L306 740Z" />
<glyph unicode="&#x2019;" glyph-name="quoteright" d="M264 740H419L294 442H210L264 740Z" />
<glyph unicode="&#x201a;" glyph-name="quotesinglbase" d="M244 152H399L275 -145H190L244 152Z" />
<glyph unicode="&#x201c;" glyph-name="quotedblleft" d="M443 740H527L473 442H318L443 740ZM433 740H517L463 442H308L433 740Z" />
<glyph unicode="&#x201d;" glyph-name="quotedblright" d="M401 740H556L431 442H347L401 740ZM391 740H546L421 442H337L391 740Z" />
<glyph unicode="&#x201e;" glyph-name="quotedblbase" d="M393 152H548L424 -145H339L393 152ZM383 152H538L414 -145H329L383 152Z" />
<glyph unicode="&#x2022;" glyph-name="bullet" d="M300 173Q226 173 196 206T165 289V319Q165 344 172 365T195 401T237 426T300 435Q337 435 363 426T405 402T428 365T435 319V289Q435 239 405 206T300 173Z" />
<glyph unicode="&#x2039;" glyph-name="guilsinglleft" d="M375 47L171 219V321L375 493L408 420L268 270L408 120L375 47Z" />
<glyph unicode="&#x203a;" glyph-name="guilsinglright" d="M192 120L332 270L192 420L225 493L429 321V219L225 47L192 120Z" />
</font>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 51 KiB

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