mirror of
https://github.com/compute-blade-community/compute-blade-agent.git
synced 2026-04-16 15:35:42 +02:00
Compare commits
124 Commits
v0.3.2
...
1161cf3ea2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1161cf3ea2 | ||
|
|
42faaf332e | ||
|
|
33458c075d | ||
|
|
d09abe14f2 | ||
|
|
baa7f813c1 | ||
|
|
bc86e413fc | ||
|
|
105d39be89 | ||
|
|
1f6fecfefe | ||
|
|
03541febb2 | ||
|
|
ed39f8320b | ||
|
|
6c1fb4aeb6 | ||
|
|
75c7df898f | ||
|
|
2b2aab20d4 | ||
|
|
e8b9d888fa | ||
|
|
84346089ca | ||
|
|
9477cc71c2 | ||
|
|
0c8efc3e54 | ||
|
|
d3018868c8 | ||
|
|
85a65ff4fc | ||
|
|
217a796fb9 | ||
|
|
4772faaa46 | ||
|
|
764a530c67 | ||
|
|
8459b80451 | ||
|
|
7508715397 | ||
|
|
659ef04d3f | ||
|
|
3b8362a640 | ||
|
|
0ae498cfd2 | ||
|
|
2e75867a2c | ||
|
|
da33d0767a | ||
|
|
940b8851f3 | ||
|
|
9d37e4d0e0 | ||
|
|
ee845c96da | ||
|
|
682d15abe1 | ||
|
|
c891384ed3 | ||
|
|
731de17934 | ||
|
|
20af75e36a | ||
|
|
bca920c510 | ||
|
|
4610f83e56 | ||
|
|
10839c5bf6 | ||
|
|
c6c7166a87 | ||
|
|
19c58fda60 | ||
|
|
551c3b50ba | ||
|
|
4f56e1a568 | ||
|
|
de8ca4c27b | ||
|
|
339b6881f2 | ||
|
|
7e2cb1d9c3 | ||
|
|
c30a64acd9 | ||
|
|
055fcb98b2 | ||
|
|
a4e4e468d1 | ||
|
|
a710b17abf | ||
|
|
eaf26cabd2 | ||
|
|
b0da67eaae | ||
|
|
0cfdb935b5 | ||
|
|
2679c21423 | ||
|
|
4f3bea45fe | ||
|
|
a2f06d99ec | ||
|
|
24cb5f0d4a | ||
|
|
19805dd5ae | ||
|
|
354c7b62c6 | ||
|
|
ddee9b2c14 | ||
|
|
1471ac9376 | ||
|
|
1865cc3163 | ||
|
|
97e9dc4e5e | ||
|
|
25758de65d | ||
|
|
c5c330ffa3 | ||
|
|
2d9fa62ac0 | ||
|
|
b176f86394 | ||
|
|
f6234b5a3d | ||
|
|
ebb492b71a | ||
|
|
062e36e33a | ||
|
|
4acfa27158 | ||
|
|
781ded8e43 | ||
|
|
7ec49ce05c | ||
|
|
ca5e9258fa | ||
|
|
ced33b0514 | ||
|
|
9e795d0a22 | ||
|
|
39b41e0cdd | ||
|
|
8fb8a0e100 | ||
|
|
2b01c25d0a | ||
|
|
56f72613b2 | ||
|
|
b022133860 | ||
|
|
0733dd594a | ||
|
|
f6a70fa6a3 | ||
|
|
631ddfedd4 | ||
|
|
27a87f3c0f | ||
|
|
501caeb750 | ||
|
|
6faf63c76f | ||
|
|
459ab10bec | ||
|
|
790ea2089a | ||
|
|
2ff46c62b7 | ||
|
|
ff6898f514 | ||
|
|
ac573c805f | ||
|
|
70541d86ba | ||
|
|
ec6229ad86 | ||
|
|
c5ff21d522 | ||
|
|
67b3411e32 | ||
|
|
485f5ac79b | ||
|
|
ca690d418f | ||
|
|
9048a4afca | ||
|
|
158e7fc1bd | ||
|
|
4a7c244cde | ||
|
|
3cbf7a8733 | ||
|
|
349c8b5199 | ||
|
|
d088a1ba0a | ||
|
|
4649d02952 | ||
|
|
3278678768 | ||
|
|
ae843db32c | ||
|
|
6421521bfc | ||
|
|
c93e561952 | ||
|
|
a8d470d4f9 | ||
|
|
99920370fb | ||
|
|
c6bba1339b | ||
|
|
e86b221aa8 | ||
|
|
f2cd029d83 | ||
|
|
780455e749 | ||
|
|
21d9942629 | ||
|
|
4691e2b3d7 | ||
|
|
6eef51b585 | ||
|
|
cee6912f57 | ||
|
|
9c82b60fd8 | ||
|
|
33dd6e5adf | ||
|
|
a793872bf2 | ||
|
|
5129bf6b33 | ||
|
|
0170f70cc0 |
70
.github/labeler.yml
vendored
Normal file
70
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
# API-related files
|
||||
api:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'api/**'
|
||||
- 'internal/api/**'
|
||||
- 'pkg/certificate/**'
|
||||
- 'buf.*'
|
||||
|
||||
# CLI-related files
|
||||
cli:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'api/**'
|
||||
- 'cmd/bladectl/**'
|
||||
|
||||
# Agent
|
||||
agent:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'api/**'
|
||||
- 'cmd/agent/**'
|
||||
- 'internal/agent/**'
|
||||
- 'pkg/agent/**'
|
||||
- 'pkg/events/**'
|
||||
|
||||
# Firmware-related files
|
||||
firmware:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'cmd/fanunit/**'
|
||||
- 'pkg/smartfanunit/**'
|
||||
|
||||
# Hardware-specific packages
|
||||
hardware:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'pkg/fancontroller/**'
|
||||
- 'pkg/hal/**'
|
||||
- 'pkg/ledengine/**'
|
||||
|
||||
# Utilities
|
||||
util:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'pkg/util/**'
|
||||
- 'pkg/log/**'
|
||||
|
||||
# Documentation (Markdown files or docs folders — optional here, kept for completeness)
|
||||
documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.md'
|
||||
|
||||
# Build system files
|
||||
build:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- '.github/**'
|
||||
- 'Makefile'
|
||||
|
||||
# Add 'enhancement' label to any PR where the head branch name starts with `feature`
|
||||
enhancement:
|
||||
- head-branch:
|
||||
- '^feature'
|
||||
|
||||
# Add 'bug' label to any PR where the head branch name starts with `feature`
|
||||
bug:
|
||||
- head-branch:
|
||||
- '^bug'
|
||||
- '^fix'
|
||||
192
.github/workflows/ci.yaml
vendored
192
.github/workflows/ci.yaml
vendored
@@ -1,36 +1,180 @@
|
||||
name: ci
|
||||
name: Go Build - CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags-ignore:
|
||||
- "v*"
|
||||
branches-ignore:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
name: Code Quality
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checkout code
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Set up Go environment
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache-dependency-path: "**/*.sum"
|
||||
cache: true
|
||||
|
||||
- name: GolangCI Lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Run format-check
|
||||
run: |
|
||||
UNFORMATTED=$(gofmt -l .)
|
||||
if [ -n "$UNFORMATTED" ]; then
|
||||
echo "The following files are not formatted according to gofmt:"
|
||||
echo "$UNFORMATTED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout code (full history)
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
# Checkout code
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Setup golang with caching
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v4
|
||||
# Set up Go environment
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: stable
|
||||
- id: go-cache-paths
|
||||
run: |
|
||||
echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
|
||||
echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
|
||||
- name: Go Build Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.go-cache-paths.outputs.go-build }}
|
||||
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
|
||||
- name: Go Mod Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.go-cache-paths.outputs.go-mod }}
|
||||
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
|
||||
go-version-file: "go.mod"
|
||||
cache: true
|
||||
cache-dependency-path: "**/*.sum"
|
||||
|
||||
# Run tests
|
||||
- name: Run tests
|
||||
run: make test
|
||||
run: go test -cover -coverprofile=coverage.txt ./...
|
||||
|
||||
- name: Archive code coverage results
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: code-coverage
|
||||
path: "coverage.txt"
|
||||
if-no-files-found: error
|
||||
|
||||
code_coverage:
|
||||
name: "Code coverage report"
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # to download code coverage results from "test" job
|
||||
pull-requests: write # write permission needed to comment on PR
|
||||
|
||||
steps:
|
||||
- uses: fgrosse/go-coverage-report@v1.3.0
|
||||
with:
|
||||
coverage-artifact-name: "code-coverage"
|
||||
coverage-file-name: "coverage.txt"
|
||||
github-baseline-workflow-ref: "release.yaml"
|
||||
|
||||
tinygo:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
- quality
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache-dependency-path: "**/*.sum"
|
||||
cache: true
|
||||
|
||||
# Setup tinygo
|
||||
- uses: acifani/setup-tinygo@v2
|
||||
with:
|
||||
tinygo-version: "0.37.0"
|
||||
|
||||
# Build fanunit firmware
|
||||
- name: Build FanUnit Firmware
|
||||
run: make build-fanunit
|
||||
|
||||
- name: Archive FanUnit Firmware
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: fanunit.uf2
|
||||
path: "fanunit.uf2"
|
||||
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- tinygo
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install cosign
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
# Install GoLang
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache-dependency-path: "**/*.sum"
|
||||
cache: true
|
||||
|
||||
# Setup docker buildx
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Download FanUnit Firmware
|
||||
- name: Download fanunit firmware
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: fanunit.uf2
|
||||
|
||||
# Run goreleaser
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: latest
|
||||
args: release --snapshot --clean --skip sign
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: compute-blade-agent
|
||||
path: dist/*-SNAPSHOT-*
|
||||
if-no-files-found: error
|
||||
|
||||
19
.github/workflows/conventional-commits.yaml
vendored
19
.github/workflows/conventional-commits.yaml
vendored
@@ -1,19 +0,0 @@
|
||||
name: Enforce conventional pr title
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: aslafy-z/conventional-pr-title-action@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
66
.github/workflows/goreleaser.yaml
vendored
66
.github/workflows/goreleaser.yaml
vendored
@@ -1,66 +0,0 @@
|
||||
name: gorelease
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout code (full history)
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup golang with caching
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: stable
|
||||
- id: go-cache-paths
|
||||
run: |
|
||||
echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
|
||||
echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
|
||||
- name: Go Build Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.go-cache-paths.outputs.go-build }}
|
||||
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
|
||||
- name: Go Mod Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.go-cache-paths.outputs.go-mod }}
|
||||
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
# Setup docker buildx
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: 'Login to GitHub Container Registry'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{github.actor}}
|
||||
password: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
# Install cosign
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
# Run goreleaser
|
||||
- name: Goreleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
COSIGN_YES: "true"
|
||||
KO_DOCKER_REPO: ghcr.io/${{ github.repository }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
77
.github/workflows/pr-hygiene.yaml
vendored
Normal file
77
.github/workflows/pr-hygiene.yaml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: GitHub Pull Request Hygiene
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
pr_title:
|
||||
name: "Validate PR Title"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
statuses: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v6
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: marocchino/sticky-pull-request-comment@v3
|
||||
# When the previous steps fail, the workflow would stop. By adding this
|
||||
# condition you can continue the execution with the populated error message.
|
||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Hey there and thank you for opening this pull request! 👋🏼
|
||||
|
||||
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||
|
||||
Details:
|
||||
|
||||
```
|
||||
${{ steps.lint_pr_title.outputs.error_message }}
|
||||
```
|
||||
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@v3
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
|
||||
labeler:
|
||||
name: "Add Labels to PR"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
id: labeler
|
||||
with:
|
||||
sync-labels: true
|
||||
dot: true
|
||||
|
||||
- shell: bash
|
||||
name: Write step-summary
|
||||
run: |
|
||||
echo "All Labels: ${{ steps.labeler.outputs.all-labels }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "New Labels for this iteration: ${{ steps.labeler.outputs.new-labels }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
144
.github/workflows/release.yaml
vendored
144
.github/workflows/release.yaml
vendored
@@ -1,19 +1,147 @@
|
||||
name: release-please
|
||||
name: Go Release - Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
# Release-please for auto-updated PRs
|
||||
release-please:
|
||||
test:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: google-github-actions/release-please-action@v3
|
||||
# Checkout code
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Set up Go environment
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: true
|
||||
cache-dependency-path: "**/*.sum"
|
||||
|
||||
- name: Run tests
|
||||
run: go test -cover -coverprofile=coverage.txt ./...
|
||||
|
||||
- name: Archive code coverage results
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: code-coverage
|
||||
path: "coverage.txt"
|
||||
if-no-files-found: error
|
||||
|
||||
# Release-please for auto-updated PRs
|
||||
release-please:
|
||||
name: Release Please
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
|
||||
steps:
|
||||
- uses: googleapis/release-please-action@v3
|
||||
id: release-please
|
||||
with:
|
||||
release-type: simple # actual releasing is handled by goreleaser
|
||||
package-name: computeblade-agent
|
||||
skip-github-release: true # GH release is created by goreleaser
|
||||
release-type: simple # actual releasing is handled by goreleaser
|
||||
package-name: compute-blade-agent
|
||||
bump-minor-pre-major: true
|
||||
bump-patch-for-minor-pre-major: true
|
||||
outputs:
|
||||
release_created: ${{ steps.release-please.outputs.release_created }}
|
||||
|
||||
tinygo:
|
||||
name: Build FanUnit Firmware
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release-please
|
||||
|
||||
if: needs.release-please.outputs.release_created
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache-dependency-path: "**/*.sum"
|
||||
cache: true
|
||||
|
||||
# Setup tinygo
|
||||
- uses: acifani/setup-tinygo@v2
|
||||
with:
|
||||
tinygo-version: "0.37.0"
|
||||
|
||||
# Build fanunit firmware
|
||||
- name: Build FanUnit Firmware
|
||||
run: make build-fanunit
|
||||
|
||||
- name: Archive FanUnit Firmware
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: fanunit.uf2
|
||||
path: "fanunit.uf2"
|
||||
|
||||
# Goreleaser for binary releases / GH release
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release-please
|
||||
- tinygo
|
||||
|
||||
if: needs.release-please.outputs.release_created
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install cosign
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
# Install GoLang
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache-dependency-path: "**/*.sum"
|
||||
cache: true
|
||||
|
||||
# Setup docker buildx
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: "Login to GitHub Container Registry"
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{github.actor}}
|
||||
password: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
# Download FanUnit Firmware
|
||||
- name: Download fanunit firmware
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: fanunit.uf2
|
||||
|
||||
# Run goreleaser
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: "~> v2"
|
||||
args: release --clean
|
||||
env:
|
||||
COSIGN_YES: "true"
|
||||
KO_DOCKER_REPO: ghcr.io/${{ github.repository }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
188
.gitignore
vendored
188
.gitignore
vendored
@@ -1,5 +1,193 @@
|
||||
bin/
|
||||
dist/
|
||||
|
||||
cover.cov
|
||||
fanunit.uf2
|
||||
.idea
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,goland+all,macos,linux
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=go,visualstudiocode,goland+all,macos,linux
|
||||
|
||||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### GoLand+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### GoLand+all Patch ###
|
||||
# Ignore everything but code style settings and run configurations
|
||||
# that are supposed to be shared within teams.
|
||||
|
||||
.idea/*
|
||||
|
||||
!.idea/codeStyles
|
||||
!.idea/runConfigurations
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,goland+all,macos,linux
|
||||
# Build artifacts
|
||||
agent
|
||||
bladectl
|
||||
compute-blade-agent
|
||||
|
||||
111
.goreleaser.yaml
111
.goreleaser.yaml
@@ -1,22 +1,52 @@
|
||||
project_name: computeblade-agent
|
||||
version: 2
|
||||
|
||||
project_name: compute-blade-agent
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- env: &env
|
||||
- CGO_ENABLED=0
|
||||
- id: agent
|
||||
env: &env
|
||||
- CGO_ENABLED=0
|
||||
goos: &goos
|
||||
- linux
|
||||
- linux
|
||||
goarch: &goarch
|
||||
- arm64
|
||||
binary: computeblade-agent
|
||||
id: agent
|
||||
- arm64
|
||||
binary: compute-blade-agent
|
||||
dir: ./cmd/agent/
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
ldflags: &ldflags
|
||||
- -X=main.Version={{.Version}}
|
||||
- -X=main.Commit={{.Commit}}
|
||||
- -X=main.Date={{ .CommitTimestamp }}
|
||||
|
||||
- env: *env
|
||||
- id: bladectl
|
||||
env: *env
|
||||
goos: *goos
|
||||
goarch: *goarch
|
||||
binary: bladectl
|
||||
id: bladectl
|
||||
dir: ./cmd/bladectl/
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
ldflags: *ldflags
|
||||
|
||||
- id: bladectl_other
|
||||
env: *env
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
binary: bladectl
|
||||
dir: ./cmd/bladectl/
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
ldflags: *ldflags
|
||||
|
||||
# Docker image including both agent and bladectl
|
||||
dockers:
|
||||
@@ -25,12 +55,12 @@ dockers:
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
ids:
|
||||
- agent
|
||||
- bladectl
|
||||
- agent
|
||||
- bladectl
|
||||
image_templates:
|
||||
- ghcr.io/uptime-lab/computeblade-agent:latest
|
||||
- ghcr.io/uptime-lab/computeblade-agent:{{ .Tag }}
|
||||
- ghcr.io/uptime-lab/computeblade-agent:v{{ .Major }}
|
||||
- ghcr.io/compute-blade-community/compute-blade-agent:latest
|
||||
- ghcr.io/compute-blade-community/compute-blade-agent:{{ .Tag }}
|
||||
- ghcr.io/compute-blade-community/compute-blade-agent:v{{ .Major }}
|
||||
build_flag_templates:
|
||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||
@@ -66,10 +96,15 @@ signs:
|
||||
|
||||
# Regular OS packages (for now only systemd based OSes)
|
||||
nfpms:
|
||||
- id: computeblade-agent
|
||||
- id: compute-blade-agent
|
||||
package_name: compute-blade-agent
|
||||
ids:
|
||||
- bladectl
|
||||
- agent
|
||||
maintainer: Matthias Riegler <me@xvzf.tech>
|
||||
description: Computeblade Agent
|
||||
homepage: https://github.com/uptime-lab/computeblade-agent
|
||||
description: compute-blade Agent
|
||||
homepage: https://github.com/compute-blade-community/compute-blade-agent
|
||||
vendor: Uptime Industries Inc.
|
||||
license: Apache 2.0
|
||||
formats:
|
||||
- deb
|
||||
@@ -77,9 +112,43 @@ nfpms:
|
||||
- archlinux
|
||||
bindir: /usr/bin
|
||||
contents:
|
||||
- src: ./hack/systemd/computeblade-agent.service
|
||||
dst: /etc/systemd/system/computeblade-agent.service
|
||||
type: config
|
||||
- src: ./hack/systemd/compute-blade-agent.service
|
||||
dst: /etc/systemd/system/compute-blade-agent.service
|
||||
- src: ./cmd/agent/default-config.yaml
|
||||
dst: /etc/computeblade-agent/config.yaml
|
||||
dst: /etc/compute-blade-agent/config.yaml
|
||||
type: config
|
||||
- src: ./fanunit.uf2
|
||||
dst: /usr/share/compute-blade-agent/fanunit.uf2
|
||||
|
||||
- id: bladectl
|
||||
package_name: bladectl
|
||||
ids:
|
||||
- bladectl
|
||||
- bladectl_other
|
||||
maintainer: Matthias Riegler <me@xvzf.tech>
|
||||
description: bladectl
|
||||
homepage: https://github.com/compute-blade-community/compute-blade-agent
|
||||
vendor: Uptime Industries Inc.
|
||||
license: Apache 2.0
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
- archlinux
|
||||
bindir: /usr/bin
|
||||
|
||||
archives:
|
||||
- id: compute-blade-agent
|
||||
ids:
|
||||
- agent
|
||||
- bladectl
|
||||
name_template: 'compute-blade-agent_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
|
||||
- id: bladectl
|
||||
ids:
|
||||
- bladectl
|
||||
- bladectl_other
|
||||
name_template: 'bladectl_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
|
||||
release:
|
||||
extra_files:
|
||||
- glob: ./fanunit.uf2
|
||||
|
||||
172
CHANGELOG.md
Normal file
172
CHANGELOG.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Changelog
|
||||
|
||||
## [0.11.2](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.11.1...v0.11.2) (2026-03-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **hal:** add BCM2712 (CM5/Pi 5) HAL support ([#154](https://github.com/compute-blade-community/compute-blade-agent/issues/154)) ([9477cc7](https://github.com/compute-blade-community/compute-blade-agent/commit/9477cc71c200cf193e467a8d543d8970e733a74f))
|
||||
* **hal:** add RK3588 (Radxa CM5) HAL with sysfs fan control ([#155](https://github.com/compute-blade-community/compute-blade-agent/issues/155)) ([03541fe](https://github.com/compute-blade-community/compute-blade-agent/commit/03541febb2c02ee5cf3d432128e756de4e68dfd4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **fancontroller:** support more than 2 steps in fan curve ([#156](https://github.com/compute-blade-community/compute-blade-agent/issues/156)) ([8434608](https://github.com/compute-blade-community/compute-blade-agent/commit/84346089caf7cb1b163e9279bdf9fe4cf0519651))
|
||||
|
||||
## [0.11.1](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.11.0...v0.11.1) (2025-07-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cmd_root.go:** change alias for --all flag from lowercase 'a' to uppercase 'A' for consistency with other flags ([#108](https://github.com/compute-blade-community/compute-blade-agent/issues/108)) ([ddee9b2](https://github.com/compute-blade-community/compute-blade-agent/commit/ddee9b2c14cf4c1855486c4683b6c241a8e47350))
|
||||
|
||||
## [0.11.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.10.0...v0.11.0) (2025-06-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **bladectl:** add server version information to output ([#100](https://github.com/compute-blade-community/compute-blade-agent/issues/100)) ([062e36e](https://github.com/compute-blade-community/compute-blade-agent/commit/062e36e33ad479677affa4773e620ca53be7e9fa))
|
||||
|
||||
## [0.10.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.9.1...v0.10.0) (2025-06-06)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **bladectl:** add more bladectl commands ([#91](https://github.com/compute-blade-community/compute-blade-agent/issues/91))
|
||||
|
||||
### Features
|
||||
|
||||
* **bladectl:** add more bladectl commands ([#91](https://github.com/compute-blade-community/compute-blade-agent/issues/91)) ([781ded8](https://github.com/compute-blade-community/compute-blade-agent/commit/781ded8e43d7115b334580a6ff18c2ab054e22cc))
|
||||
* **OpenTelemetry:** Integrate OpenTelemetry into agent ([#90](https://github.com/compute-blade-community/compute-blade-agent/issues/90)) ([7ec49ce](https://github.com/compute-blade-community/compute-blade-agent/commit/7ec49ce05ce4d428e5ee94858c01004cc1a2e40d))
|
||||
|
||||
## [0.9.1](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.9.0...v0.9.1) (2025-06-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **systemd:** Add User to systemd service ([b022133](https://github.com/compute-blade-community/compute-blade-agent/commit/b02213386036ac29a0f1a733395c44a87b3c00e2))
|
||||
|
||||
## [0.9.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.8.2...v0.9.0) (2025-06-06)
|
||||
|
||||
Re-Release of [v0.7.0](#070-2025-05-11), [v0.8.0](#080-2025-05-24), [v0.8.1](#081-2025-05-24), and , [v0.8.2](#082-2025-05-24) in the [compute-blade-community](https://github.com/compute-blade-community) GitHub Org
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **docker:** Docker Images are now available & published
|
||||
|
||||
### Documentation
|
||||
|
||||
* **release:** document release process ([f6a70fa](https://github.com/compute-blade-community/compute-blade-agent/commit/f6a70fa6a389d31a82dac9e340c1704053b198c0))
|
||||
|
||||
## [0.8.2](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.8.1...v0.8.2) (2025-05-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* auth to ghcr.io ([#63](https://github.com/compute-blade-community/compute-blade-agent/issues/63)) ([e600d32](https://github.com/compute-blade-community/compute-blade-agent/commit/e600d3245317eafe7df0090e7bc6f1dff45a5693))
|
||||
|
||||
## [0.8.1](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.8.0...v0.8.1) (2025-05-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* set goreleaser version to v2.x ([#61](https://github.com/compute-blade-community/compute-blade-agent/issues/61)) ([08a4e9b](https://github.com/compute-blade-community/compute-blade-agent/commit/08a4e9bca67f53e69fec3ce4cdf93344f2cf1327))
|
||||
|
||||
## [0.8.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.7.0...v0.8.0) (2025-05-24)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **go version:** Bump go version to 1.24 ([#58](https://github.com/compute-blade-community/compute-blade-agent/issues/58))
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **go version:** Bump go version to 1.24 ([#58](https://github.com/compute-blade-community/compute-blade-agent/issues/58)) ([bb7b8cd](https://github.com/compute-blade-community/compute-blade-agent/commit/bb7b8cd55d88954bb2632606e12b2c9eb057690a))
|
||||
|
||||
## [0.7.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.6...v0.7.0) (2025-05-11)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **agent:** add support for mTLS authentication in gRPC server ([#54](https://github.com/compute-blade-community/compute-blade-agent/issues/54))
|
||||
|
||||
### Features
|
||||
|
||||
* **agent:** add support for mTLS authentication in gRPC server ([#54](https://github.com/compute-blade-community/compute-blade-agent/issues/54)) ([70541d8](https://github.com/compute-blade-community/compute-blade-agent/commit/70541d86bad675a153daf8b5c80a92de204502ab))
|
||||
* **agent:** expose version, commit, and date information in logs for better tracking ([ec6229a](https://github.com/compute-blade-community/compute-blade-agent/commit/ec6229ad86b4eff06e40c805f8e4f216fe844c18))
|
||||
* **bladectl:** implement command structure for managing compute-blade features ([ec6229a](https://github.com/compute-blade-community/compute-blade-agent/commit/ec6229ad86b4eff06e40c805f8e4f216fe844c18))
|
||||
* **goreleaser:** add versioning information to builds for better traceability ([ec6229a](https://github.com/compute-blade-community/compute-blade-agent/commit/ec6229ad86b4eff06e40c805f8e4f216fe844c18))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **.gitignore:** add .idea directory to ignore list to prevent IDE files from being tracked ([ec6229a](https://github.com/compute-blade-community/compute-blade-agent/commit/ec6229ad86b4eff06e40c805f8e4f216fe844c18))
|
||||
* **bladectl:** improve error handling in identify command for better user feedback ([ec6229a](https://github.com/compute-blade-community/compute-blade-agent/commit/ec6229ad86b4eff06e40c805f8e4f216fe844c18))
|
||||
|
||||
## [0.6.6](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.5...v0.6.6) (2025-01-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* correct package name from computeblade-agent to compute-blade-agent ([#47](https://github.com/compute-blade-community/compute-blade-agent/issues/47)) ([67b3411](https://github.com/compute-blade-community/compute-blade-agent/commit/67b3411e32df10673c5f3bab8b76f31f366cf3ab))
|
||||
|
||||
## [0.6.5](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.4...v0.6.5) (2024-08-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pin golang/tinygo versions ([ca690d4](https://github.com/compute-blade-community/compute-blade-agent/commit/ca690d418f099881b6aafdb2ca4be3cee6ac73fc))
|
||||
|
||||
## [0.6.4](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.3...v0.6.4) (2024-08-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* finalize renaming ([158e7fc](https://github.com/compute-blade-community/compute-blade-agent/commit/158e7fc1bde46e66327d70f87743df39070c2753))
|
||||
|
||||
## [0.6.3](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.2...v0.6.3) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* oci reg typo ([3cbf7a8](https://github.com/compute-blade-community/compute-blade-agent/commit/3cbf7a8733dedde834f7392de0851c971a6e3a05))
|
||||
|
||||
## [0.6.2](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.1...v0.6.2) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cleanup uf2 files ([d088a1b](https://github.com/compute-blade-community/compute-blade-agent/commit/d088a1ba0a1adba7694a7d2d3b7d49bb9c72fe0c))
|
||||
|
||||
## [0.6.1](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.0...v0.6.1) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump tinygo release ([#39](https://github.com/compute-blade-community/compute-blade-agent/issues/39)) ([3278678](https://github.com/compute-blade-community/compute-blade-agent/commit/32786787683e2a0cd42b63b92fe7dd2c41bb6e8f))
|
||||
|
||||
## [0.6.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.5.0...v0.6.0) (2024-08-05)
|
||||
|
||||
### Features
|
||||
|
||||
* migrate to compute-blade-community gh org ([#37](https://github.com/compute-blade-community/compute-blade-agent/issues/37)) ([6421521](https://github.com/compute-blade-community/compute-blade-agent/commit/6421521bfc94a6211ed084bf8913f413e27e5b14))
|
||||
|
||||
## [0.5.0](https://github.com/github.com/compute-blade-community/compute-blade-agent/compare/v0.4.1...v0.5.0) (2023-11-25)
|
||||
|
||||
### Features
|
||||
|
||||
* add smart fan unit support ([#29](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/29)) ([9992037](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/99920370fba8176dc34243d28281aa343f437fc5))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* smart fan unit improvements ([#31](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/31)) ([a8d470d](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/a8d470d4f9ec2749e1067474805f67639cd24c09))
|
||||
|
||||
## [0.4.1](https://github.com/github.com/compute-blade-community/compute-blade-agent/compare/v0.4.0...v0.4.1) (2023-10-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ${ -> ${{ ... ([#27](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/27)) ([f2cd029](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/f2cd029d83329085354acb7ed68da390dfe9aee4))
|
||||
* add debug statement ([#25](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/25)) ([21d9942](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/21d99426293b724f53f0de594fce21e5c49724f8))
|
||||
* debug statement ([#26](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/26)) ([780455e](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/780455e749a6acd896ce862ac565f1d1f5467c20))
|
||||
* if statement? ([#23](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/23)) ([4691e2b](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/4691e2b3d71b9c28ebbed31b564c5356713b91f9))
|
||||
* rename release-please -> release workflow ([#28](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/28)) ([e86b221](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/e86b221aa886f11d6303521787ca4c755b114a6e))
|
||||
|
||||
## [0.4.0](https://github.com/github.com/compute-blade-community/compute-blade-agent/compare/v0.3.4...v0.4.0) (2023-10-05)
|
||||
|
||||
### Features
|
||||
|
||||
* switch to release-please ([#19](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/19)) ([33dd6e5](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/33dd6e5adf45d2b59c1af061c7e78c9426329f15))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* explicitly check for true before running goreleaser ([#21](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/21)) ([9c82b60](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/9c82b60fd88718ad90a9a0aa774ffc4bcdd18d3f))
|
||||
* if condition ([#22](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/22)) ([cee6912](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/cee6912f5768a310c2758c8755b9ed1985b10d23))
|
||||
2
CODEOWNERS
Normal file
2
CODEOWNERS
Normal file
@@ -0,0 +1,2 @@
|
||||
@xvzf
|
||||
@cedi
|
||||
52
CONTRIBUTING.md
Normal file
52
CONTRIBUTING.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Contributing
|
||||
|
||||
## Releases
|
||||
|
||||
This project uses [release-please](https://github.com/googleapis/release-please) and [goreleaser](https://goreleaser.com/) to automate releases based on conventional commits.
|
||||
|
||||
Releases are **semi-automated** and follow this flow:
|
||||
|
||||
### 1. Merge Code to `main`
|
||||
|
||||
All new features, fixes, and changes are merged into the `main` branch via pull requests using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
||||
|
||||
Examples:
|
||||
|
||||
- `feat: add new API endpoint`
|
||||
- `fix: correct off-by-one error`
|
||||
- `chore: update dependencies`
|
||||
|
||||
### 2. Release PR is Auto-Created
|
||||
|
||||
Once a commit is merged into `main`, a GitHub Action runs `release-please`, which:
|
||||
|
||||
- Calculates the next version (e.g., `v0.9.1`)
|
||||
- Creates a pull request (e.g., `chore: release v0.9.1`)
|
||||
- Includes a generated changelog in `CHANGELOG.md`
|
||||
|
||||
> 📌 Note:
|
||||
> This PR should **not be edited manually**. If something is wrong, fix the commit messages instead.
|
||||
|
||||
### 3. Merge the Release PR
|
||||
|
||||
Once the release PR is approved and merged:
|
||||
|
||||
- The changelog and version bump are committed to `main`
|
||||
- `release-please` pushes a new tag with the version-number the merged commit on `main`
|
||||
|
||||
### 4. Tag Triggers `goreleaser`
|
||||
|
||||
A GitHub Action watches for `v*` tags and runs `goreleaser`, which:
|
||||
|
||||
- Builds all binaries and artifacts
|
||||
- Publishes them to GitHub Releases
|
||||
- Optionally signs and pushes container images (if configured)
|
||||
- Attaches additional files (e.g., firmware, config) as release assets
|
||||
|
||||
Once complete, the new GitHub Release is available at: [github.com/compute-blade-community/compute-blade-agent/releases](https://github.com/compute-blade-community/compute-blade-agent/releases)
|
||||
|
||||
## Notes
|
||||
|
||||
- Never push tags manually.
|
||||
- Only edit the changelog through conventional commits and `release-please`.
|
||||
- You can retry failed releases by deleting the failed tag and re-merging the release PR or re-running the workflow.
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM cgr.dev/chainguard/wolfi-base
|
||||
LABEL org.opencontainers.image.source https://github.com/uptime-lab/computeblade-agent
|
||||
LABEL org.opencontainers.image.source="https://github.com/compute-blade-community/compute-blade-agent"
|
||||
|
||||
# Copy binaries generated by goreleaser
|
||||
COPY computeblade-agent bladectl /bin
|
||||
COPY compute-blade-agent bladectl /bin/
|
||||
|
||||
ENTRYPOINT ["/bin/computeblade-agent"]
|
||||
ENTRYPOINT ["/bin/compute-blade-agent"]
|
||||
|
||||
23
Makefile
23
Makefile
@@ -1,5 +1,6 @@
|
||||
FUZZ_TARGETS := ./pkg/smartfanunit/proto
|
||||
|
||||
all: lint
|
||||
all: lint test
|
||||
|
||||
.PHONY: run
|
||||
run:
|
||||
@@ -13,15 +14,29 @@ lint:
|
||||
test:
|
||||
go test ./... -v
|
||||
|
||||
|
||||
.PHONY: fuzz
|
||||
fuzz:
|
||||
@for target in $(FUZZ_TARGETS); do \
|
||||
go test -fuzz="Fuzz" -fuzztime=5s -fuzzminimizetime=10s $$target; \
|
||||
done
|
||||
|
||||
|
||||
.PHONY: generate
|
||||
generate: buf
|
||||
$(BUF) generate
|
||||
|
||||
release:
|
||||
goreleaser release --clean
|
||||
.PHONY: build-fanunit
|
||||
build-fanunit:
|
||||
tinygo build -target=pico -o fanunit.uf2 ./cmd/fanunit/
|
||||
|
||||
.PHONY: build-agent
|
||||
build-agent: generate
|
||||
goreleaser build --snapshot --clean
|
||||
|
||||
.PHONY: snapshot
|
||||
snapshot:
|
||||
goreleaser release --snapshot --skip-publish --clean
|
||||
goreleaser release --snapshot --skip=publish --clean
|
||||
|
||||
# Dependencies
|
||||
LOCALBIN ?= $(shell pwd)/bin
|
||||
|
||||
142
README.md
142
README.md
@@ -1,61 +1,133 @@
|
||||
# computeblade-agent
|
||||
> :warning: this is still a beta-release & configuration&APIs might see breaking changes!
|
||||
# compute-blade-agent
|
||||
|
||||
The `computeblade-agent` is an OS agent interfacing with the [ComputeBlade](http://computeblade.com) hardware.
|
||||
It controls fan speed, LEDs and handles common events e.g. to _identify_/find an individual blade in a server rack.
|
||||
In addition, it exposes hardware- and agent-related metrics on a [Prometheus](http://prometheus.io) endpoint.
|
||||
> :warning: **Beta Release**: This software is currently in beta, and both configurations and APIs may undergo breaking changes. It is not yet 100% feature complete, but it functions as intended.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Install the agent with the one-liner below:
|
||||
|
||||
**TL;DR, I just want it running on my blade script**:
|
||||
```bash
|
||||
curl -L -o /tmp/computeblade-agent-installer.sh https://raw.githubusercontent.com/Uptime-Lab/computeblade-agent/main/hack/autoinstall.sh
|
||||
chmod +x /tmp/computeblade-agent-installer.sh
|
||||
/tmp/computeblade-agent-installer.sh
|
||||
curl -L -o /tmp/compute-blade-agent-installer.sh https://raw.githubusercontent.com/compute-blade-community/compute-blade-agent/main/hack/autoinstall.sh
|
||||
chmod +x /tmp/compute-blade-agent-installer.sh
|
||||
/tmp/compute-blade-agent-installer.sh
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### computeblade-agent
|
||||
The agent is an event-loop handler that's reacting on system events such as button presses and temperature changes.
|
||||
It also exposes a prometheus endpoint allowing monitoring of core-metrics such as PoE status.
|
||||
### `compute-blade-agent`: Hardware Interaction & Monitoring
|
||||
|
||||
By default, the computeblade agent runs in _normal_ operation mode; the LEDs are static and fanspeed is set based on the configuration.
|
||||
In case the SoC temperature raises above a predefined level, the _critical_ mode is active and sets the fan-speed to 100% alongside changing the LED color (Red by default)
|
||||
The agent runs as a system service and monitors various hardware states and events:
|
||||
|
||||
Aside from the above mentioned normal and critical modes, the _identify_ action (independend of the mode), which lets the edge LED blink.
|
||||
This can be toggled using `bladectl` on the blade (`bladectl identify`) or by pressing the edge button.
|
||||
- Reacts to button presses and SoC temperature.
|
||||
- Automatically enters **critical mode** (fan 100%, red LED) when overheating.
|
||||
- Exposes system metrics via a Prometheus endpoint (`/metrics`).
|
||||
|
||||
The _identify_ function can be triggered via `bladectl` or a physical button press. It makes the edge LED blink to assist locating a blade in a rack.
|
||||
|
||||
### bladectl - interacting with the agent
|
||||
The bladectl interacts with the blade-local API exposed by the computeblade-agent.
|
||||
You can e.g. identify the blade in a rack using `bladectl identify --wait`, which will block & make the edge-LED blink until the button is pressed.
|
||||
### `bladectl`: User Command-Line Tool
|
||||
|
||||
`bladectl` is a CLI utility for remote or local interaction with the running agent. Example use cases:
|
||||
|
||||
## Install Options
|
||||
```bash
|
||||
bladectl set identify --wait # Blink LED until button is pressed
|
||||
bladectl set identify --confirm # Cancel identification
|
||||
bladectl unset identify # Cancel identification (alternative)
|
||||
```
|
||||
|
||||
The agent and bladectl are provided as package for Debian, RPM and ArchLinux or as OCI image to run within docker/Kubernetes.
|
||||
Packages ship with a systemd unit which can be enabled using `systemd enable computeblade-agent.service --now`.
|
||||
### `fanunit.uf2`: Smart Fan Unit Firmware
|
||||
|
||||
`bladectl` is available globally, but has to be executed as root since the socket (default `/tmp/computeblade-agent.sock`) does not have a user/group accessed due to privileged access on critical resources.
|
||||
This firmware runs on the fan unit microcontroller and:
|
||||
|
||||
**Kubernetes deployment**:
|
||||
A kustomize environment can be found in `hack/deploy`. A `kubectl -k hack/deploy` does the trick - or use a GitOps tool such as FluxCD.
|
||||
- Controls fan speed via UART commands from blade agents.
|
||||
- Reports RPM and airflow temperature back to the blade.
|
||||
- Forwards button events (1x = left blade, 2x = right blade).
|
||||
- Uses EMC2101 for optional advanced features like airflow-based fan control.
|
||||
|
||||
To install it, [download the `fanunit.uf2`](https://github.com/compute-blade-community/compute-blade-agent/releases/latest), and follow the firmware upgrade instructions [here](https://docs.computeblade.com/fan-unit/uart#update-firmware).
|
||||
|
||||
## Installation
|
||||
|
||||
Install the agent with the one-liner below:
|
||||
|
||||
```bash
|
||||
curl -L -o /tmp/compute-blade-agent-installer.sh https://raw.githubusercontent.com/compute-blade-community/compute-blade-agent/main/hack/autoinstall.sh
|
||||
chmod +x /tmp/compute-blade-agent-installer.sh
|
||||
/tmp/compute-blade-agent-installer.sh
|
||||
```
|
||||
|
||||
> Note: `bladectl` requires root privileges when used locally, due to restricted access to the Unix socket (`/tmp/compute-blade-agent.sock`).
|
||||
|
||||
## Configuration
|
||||
The configuration is driven by a config file or environment variables. Linux packages ship with the default configuration placed in `/etc/computeblade-agent/config.yaml`.
|
||||
Alternatively (specifically for running within Kubernetes), all parameters in the YAML configuration can be overwritten using environment variables, prefixed with `BLADE_`:
|
||||
|
||||
Changing the metric address defined in YAML like this:
|
||||
The default configuration file is located at:
|
||||
|
||||
```bash
|
||||
/etc/compute-blade-agent/config.yaml
|
||||
```
|
||||
|
||||
You can also override any config option via environment variables using the `BLADE_` prefix.
|
||||
|
||||
### Examples
|
||||
|
||||
#### YAML:
|
||||
```yaml
|
||||
# Listen configuration
|
||||
listen:
|
||||
metrics: ":9666"
|
||||
```
|
||||
is driven by the environment variable `BLADE_LISTEN_METRICS=":1234"`.
|
||||
|
||||
Some useful parameters:
|
||||
- `BLADE_STEALTH_MODE=false` Enables/disables stealth mode
|
||||
- `BLADE_FAN_SPEED_PERCENT=80` Sets static fan-speed (by default, there's a linear fan-curve of 40-80%
|
||||
- `BLADE_CRITICAL_TEMPERATURE_THRESHOLD=60` Configures critical temperature threshold of the agent
|
||||
- `BLADE_HAL_BCM2711_DISABLE_FANSPEED_MEASUREMENT=false` enables/disables fan speed measnurement (disabling it reduces CPU load of the agent)
|
||||
#### Environment variable override:
|
||||
|
||||
```bash
|
||||
BLADE_LISTEN_METRICS=":1234"
|
||||
```
|
||||
|
||||
### Common Overrides
|
||||
|
||||
| Variable | Description |
|
||||
|---------------------------------------------------|------------------------------------------|
|
||||
| `BLADE_STEALTH_MODE=false` | Enable/disable stealth mode |
|
||||
| `BLADE_FAN_SPEED_PERCENT=80` | Set static fan speed |
|
||||
| `BLADE_CRITICAL_TEMPERATURE_THRESHOLD=60` | Set critical temp threshold (°C) |
|
||||
| `BLADE_HAL_RPM_REPORTING_STANDARD_FAN_UNIT=false` | Disable RPM monitoring for lower CPU use |
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Endpoint for the OTLP exporter |
|
||||
|
||||
## Exposing the gRPC API for Remote Access
|
||||
|
||||
To allow secure remote use of `bladectl` over the network:
|
||||
|
||||
### 1. Update your config (`/etc/compute-blade-agent/config.yaml`):
|
||||
|
||||
```yaml
|
||||
listen:
|
||||
metrics: ":9666"
|
||||
grpc: ":8081"
|
||||
authenticated: true
|
||||
mode: tcp
|
||||
```
|
||||
|
||||
### 2. Restart the agent:
|
||||
|
||||
```bash
|
||||
systemctl restart compute-blade-agent
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Generate new mTLS server and client certificates in `/etc/compute-blade-agent/*.pem`
|
||||
- Write a new bladectl config to: `~/.config/bladectl/config.yaml` with the client certificates in place
|
||||
|
||||
## Using `bladectl` from your local machine
|
||||
|
||||
1. Copy the config from the blade:
|
||||
|
||||
```bash
|
||||
scp root@blade-pi1:~/.config/bladectl/config.yaml ~/.config/bladectl/config.yaml
|
||||
```
|
||||
|
||||
2. Fix the server address to point to the blade:
|
||||
|
||||
```bash
|
||||
yq e '.blades[] | select(.name == "blade-pi1") .blade.server = "blade-pi1.local:8081"' -i ~/.config/bladectl/config.yaml
|
||||
```
|
||||
|
||||
Your `bladectl` tool can now securely talk to the remote agent via gRPC over mTLS.
|
||||
|
||||
@@ -309,23 +309,146 @@ func (x *EmitEventRequest) GetEvent() Event {
|
||||
return Event_IDENTIFY
|
||||
}
|
||||
|
||||
type FanCurveStep struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Temperature int64 `protobuf:"varint,1,opt,name=temperature,proto3" json:"temperature,omitempty"`
|
||||
Percent uint32 `protobuf:"varint,2,opt,name=percent,proto3" json:"percent,omitempty"`
|
||||
}
|
||||
|
||||
func (x *FanCurveStep) Reset() {
|
||||
*x = FanCurveStep{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *FanCurveStep) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FanCurveStep) ProtoMessage() {}
|
||||
|
||||
func (x *FanCurveStep) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FanCurveStep.ProtoReflect.Descriptor instead.
|
||||
func (*FanCurveStep) Descriptor() ([]byte, []int) {
|
||||
return file_api_bladeapi_v1alpha1_blade_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *FanCurveStep) GetTemperature() int64 {
|
||||
if x != nil {
|
||||
return x.Temperature
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *FanCurveStep) GetPercent() uint32 {
|
||||
if x != nil {
|
||||
return x.Percent
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type VersionInfo struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"`
|
||||
Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"`
|
||||
Date int64 `protobuf:"varint,3,opt,name=date,proto3" json:"date,omitempty"`
|
||||
}
|
||||
|
||||
func (x *VersionInfo) Reset() {
|
||||
*x = VersionInfo{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *VersionInfo) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*VersionInfo) ProtoMessage() {}
|
||||
|
||||
func (x *VersionInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use VersionInfo.ProtoReflect.Descriptor instead.
|
||||
func (*VersionInfo) Descriptor() ([]byte, []int) {
|
||||
return file_api_bladeapi_v1alpha1_blade_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *VersionInfo) GetVersion() string {
|
||||
if x != nil {
|
||||
return x.Version
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *VersionInfo) GetCommit() string {
|
||||
if x != nil {
|
||||
return x.Commit
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *VersionInfo) GetDate() int64 {
|
||||
if x != nil {
|
||||
return x.Date
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type StatusResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
StealthMode bool `protobuf:"varint,1,opt,name=stealth_mode,json=stealthMode,proto3" json:"stealth_mode,omitempty"`
|
||||
IdentifyActive bool `protobuf:"varint,2,opt,name=identify_active,json=identifyActive,proto3" json:"identify_active,omitempty"`
|
||||
CriticalActive bool `protobuf:"varint,3,opt,name=critical_active,json=criticalActive,proto3" json:"critical_active,omitempty"`
|
||||
Temperature int64 `protobuf:"varint,4,opt,name=temperature,proto3" json:"temperature,omitempty"`
|
||||
FanRpm int64 `protobuf:"varint,5,opt,name=fan_rpm,json=fanRpm,proto3" json:"fan_rpm,omitempty"`
|
||||
PowerStatus PowerStatus `protobuf:"varint,6,opt,name=power_status,json=powerStatus,proto3,enum=api.bladeapi.v1alpha1.PowerStatus" json:"power_status,omitempty"`
|
||||
StealthMode bool `protobuf:"varint,1,opt,name=stealth_mode,json=stealthMode,proto3" json:"stealth_mode,omitempty"`
|
||||
IdentifyActive bool `protobuf:"varint,2,opt,name=identify_active,json=identifyActive,proto3" json:"identify_active,omitempty"`
|
||||
CriticalActive bool `protobuf:"varint,3,opt,name=critical_active,json=criticalActive,proto3" json:"critical_active,omitempty"`
|
||||
Temperature int64 `protobuf:"varint,4,opt,name=temperature,proto3" json:"temperature,omitempty"`
|
||||
FanRpm int64 `protobuf:"varint,5,opt,name=fan_rpm,json=fanRpm,proto3" json:"fan_rpm,omitempty"`
|
||||
PowerStatus PowerStatus `protobuf:"varint,6,opt,name=power_status,json=powerStatus,proto3,enum=api.bladeapi.v1alpha1.PowerStatus" json:"power_status,omitempty"`
|
||||
FanPercent uint32 `protobuf:"varint,7,opt,name=fan_percent,json=fanPercent,proto3" json:"fan_percent,omitempty"`
|
||||
FanSpeedAutomatic bool `protobuf:"varint,8,opt,name=fan_speed_automatic,json=fanSpeedAutomatic,proto3" json:"fan_speed_automatic,omitempty"`
|
||||
CriticalTemperatureThreshold int64 `protobuf:"varint,9,opt,name=critical_temperature_threshold,json=criticalTemperatureThreshold,proto3" json:"critical_temperature_threshold,omitempty"`
|
||||
FanCurveSteps []*FanCurveStep `protobuf:"bytes,10,rep,name=fan_curve_steps,json=fanCurveSteps,proto3" json:"fan_curve_steps,omitempty"`
|
||||
Version *VersionInfo `protobuf:"bytes,11,opt,name=version,proto3" json:"version,omitempty"`
|
||||
}
|
||||
|
||||
func (x *StatusResponse) Reset() {
|
||||
*x = StatusResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[3]
|
||||
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -338,7 +461,7 @@ func (x *StatusResponse) String() string {
|
||||
func (*StatusResponse) ProtoMessage() {}
|
||||
|
||||
func (x *StatusResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[3]
|
||||
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -351,7 +474,7 @@ func (x *StatusResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use StatusResponse.ProtoReflect.Descriptor instead.
|
||||
func (*StatusResponse) Descriptor() ([]byte, []int) {
|
||||
return file_api_bladeapi_v1alpha1_blade_proto_rawDescGZIP(), []int{3}
|
||||
return file_api_bladeapi_v1alpha1_blade_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *StatusResponse) GetStealthMode() bool {
|
||||
@@ -396,6 +519,41 @@ func (x *StatusResponse) GetPowerStatus() PowerStatus {
|
||||
return PowerStatus_POE_OR_USBC
|
||||
}
|
||||
|
||||
func (x *StatusResponse) GetFanPercent() uint32 {
|
||||
if x != nil {
|
||||
return x.FanPercent
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *StatusResponse) GetFanSpeedAutomatic() bool {
|
||||
if x != nil {
|
||||
return x.FanSpeedAutomatic
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *StatusResponse) GetCriticalTemperatureThreshold() int64 {
|
||||
if x != nil {
|
||||
return x.CriticalTemperatureThreshold
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *StatusResponse) GetFanCurveSteps() []*FanCurveStep {
|
||||
if x != nil {
|
||||
return x.FanCurveSteps
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StatusResponse) GetVersion() *VersionInfo {
|
||||
if x != nil {
|
||||
return x.Version
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_api_bladeapi_v1alpha1_blade_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_api_bladeapi_v1alpha1_blade_proto_rawDesc = []byte{
|
||||
@@ -414,66 +572,99 @@ var file_api_bladeapi_v1alpha1_blade_proto_rawDesc = []byte{
|
||||
0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x65, 0x76, 0x65,
|
||||
0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62,
|
||||
0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
|
||||
0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x87, 0x02,
|
||||
0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||
0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x5f, 0x6d, 0x6f, 0x64, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x4d,
|
||||
0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x5f,
|
||||
0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x64,
|
||||
0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x27, 0x0a, 0x0f,
|
||||
0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x41,
|
||||
0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61,
|
||||
0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x65, 0x6d, 0x70,
|
||||
0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x61, 0x6e, 0x5f, 0x72,
|
||||
0x70, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x66, 0x61, 0x6e, 0x52, 0x70, 0x6d,
|
||||
0x12, 0x45, 0x0a, 0x0c, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
|
||||
0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61,
|
||||
0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50,
|
||||
0x6f, 0x77, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0b, 0x70, 0x6f, 0x77, 0x65,
|
||||
0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2a, 0x4d, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74,
|
||||
0x12, 0x0c, 0x0a, 0x08, 0x49, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x46, 0x59, 0x10, 0x00, 0x12, 0x14,
|
||||
0x0a, 0x10, 0x49, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x46, 0x59, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49,
|
||||
0x52, 0x4d, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c,
|
||||
0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x5f, 0x52,
|
||||
0x45, 0x53, 0x45, 0x54, 0x10, 0x03, 0x2a, 0x21, 0x0a, 0x07, 0x46, 0x61, 0x6e, 0x55, 0x6e, 0x69,
|
||||
0x74, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x09,
|
||||
0x0a, 0x05, 0x53, 0x4d, 0x41, 0x52, 0x54, 0x10, 0x01, 0x2a, 0x2e, 0x0a, 0x0b, 0x50, 0x6f, 0x77,
|
||||
0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x4f, 0x45, 0x5f,
|
||||
0x4f, 0x52, 0x5f, 0x55, 0x53, 0x42, 0x43, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x50, 0x4f, 0x45,
|
||||
0x5f, 0x38, 0x30, 0x32, 0x5f, 0x41, 0x54, 0x10, 0x01, 0x32, 0xa8, 0x03, 0x0a, 0x11, 0x42, 0x6c,
|
||||
0x61, 0x64, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
|
||||
0x4e, 0x0a, 0x09, 0x45, 0x6d, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x2e, 0x61,
|
||||
0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c,
|
||||
0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6d, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12,
|
||||
0x4a, 0x0a, 0x16, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x66, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
||||
0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x0b, 0x53,
|
||||
0x65, 0x74, 0x46, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65, 0x64, 0x12, 0x29, 0x2e, 0x61, 0x70, 0x69,
|
||||
0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
|
||||
0x61, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x46, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65, 0x64, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12,
|
||||
0x55, 0x0a, 0x0e, 0x53, 0x65, 0x74, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x4d, 0x6f, 0x64,
|
||||
0x65, 0x12, 0x29, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69,
|
||||
0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x74,
|
||||
0x68, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67,
|
||||
0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x4a, 0x0a,
|
||||
0x0c, 0x46, 0x61, 0x6e, 0x43, 0x75, 0x72, 0x76, 0x65, 0x53, 0x74, 0x65, 0x70, 0x12, 0x20, 0x0a,
|
||||
0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12,
|
||||
0x18, 0x0a, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
|
||||
0x52, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x22, 0x53, 0x0a, 0x0b, 0x56, 0x65, 0x72,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61,
|
||||
0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x22, 0xa9,
|
||||
0x04, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x5f, 0x6d, 0x6f, 0x64,
|
||||
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68,
|
||||
0x4d, 0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79,
|
||||
0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69,
|
||||
0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x27, 0x0a,
|
||||
0x0f, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c,
|
||||
0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72,
|
||||
0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x65, 0x6d,
|
||||
0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x61, 0x6e, 0x5f,
|
||||
0x72, 0x70, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x66, 0x61, 0x6e, 0x52, 0x70,
|
||||
0x6d, 0x12, 0x45, 0x0a, 0x0c, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75,
|
||||
0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c,
|
||||
0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e,
|
||||
0x50, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0b, 0x70, 0x6f, 0x77,
|
||||
0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x61, 0x6e, 0x5f,
|
||||
0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x66,
|
||||
0x61, 0x6e, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x61, 0x6e,
|
||||
0x5f, 0x73, 0x70, 0x65, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x63,
|
||||
0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x66, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65, 0x64,
|
||||
0x41, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x12, 0x44, 0x0a, 0x1e, 0x63, 0x72, 0x69,
|
||||
0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72,
|
||||
0x65, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x1c, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x65, 0x6d, 0x70, 0x65,
|
||||
0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12,
|
||||
0x4b, 0x0a, 0x0f, 0x66, 0x61, 0x6e, 0x5f, 0x63, 0x75, 0x72, 0x76, 0x65, 0x5f, 0x73, 0x74, 0x65,
|
||||
0x70, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62,
|
||||
0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
|
||||
0x2e, 0x46, 0x61, 0x6e, 0x43, 0x75, 0x72, 0x76, 0x65, 0x53, 0x74, 0x65, 0x70, 0x52, 0x0d, 0x66,
|
||||
0x61, 0x6e, 0x43, 0x75, 0x72, 0x76, 0x65, 0x53, 0x74, 0x65, 0x70, 0x73, 0x12, 0x3c, 0x0a, 0x07,
|
||||
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e,
|
||||
0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61,
|
||||
0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66,
|
||||
0x6f, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2a, 0x4d, 0x0a, 0x05, 0x45, 0x76,
|
||||
0x65, 0x6e, 0x74, 0x12, 0x0c, 0x0a, 0x08, 0x49, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x46, 0x59, 0x10,
|
||||
0x00, 0x12, 0x14, 0x0a, 0x10, 0x49, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x46, 0x59, 0x5f, 0x43, 0x4f,
|
||||
0x4e, 0x46, 0x49, 0x52, 0x4d, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49,
|
||||
0x43, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41,
|
||||
0x4c, 0x5f, 0x52, 0x45, 0x53, 0x45, 0x54, 0x10, 0x03, 0x2a, 0x21, 0x0a, 0x07, 0x46, 0x61, 0x6e,
|
||||
0x55, 0x6e, 0x69, 0x74, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10,
|
||||
0x00, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x4d, 0x41, 0x52, 0x54, 0x10, 0x01, 0x2a, 0x2e, 0x0a, 0x0b,
|
||||
0x50, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0f, 0x0a, 0x0b, 0x50,
|
||||
0x4f, 0x45, 0x5f, 0x4f, 0x52, 0x5f, 0x55, 0x53, 0x42, 0x43, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a,
|
||||
0x50, 0x4f, 0x45, 0x5f, 0x38, 0x30, 0x32, 0x5f, 0x41, 0x54, 0x10, 0x01, 0x32, 0xed, 0x03, 0x0a,
|
||||
0x11, 0x42, 0x6c, 0x61, 0x64, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
|
||||
0x63, 0x65, 0x12, 0x4e, 0x0a, 0x09, 0x45, 0x6d, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12,
|
||||
0x27, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76,
|
||||
0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6d, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e,
|
||||
0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||
0x22, 0x00, 0x12, 0x4a, 0x0a, 0x16, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x49, 0x64, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x66, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x12, 0x16, 0x2e, 0x67,
|
||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
|
||||
0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x4c, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61,
|
||||
0x74, 0x75, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x25, 0x2e, 0x61, 0x70,
|
||||
0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70,
|
||||
0x68, 0x61, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x22, 0x00, 0x42, 0x48, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
|
||||
0x6f, 0x6d, 0x2f, 0x78, 0x76, 0x7a, 0x66, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x62,
|
||||
0x6c, 0x61, 0x64, 0x65, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x62,
|
||||
0x6c, 0x61, 0x64, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x62, 0x6c,
|
||||
0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x52,
|
||||
0x0a, 0x0b, 0x53, 0x65, 0x74, 0x46, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65, 0x64, 0x12, 0x29, 0x2e,
|
||||
0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61,
|
||||
0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x46, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65,
|
||||
0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||
0x22, 0x00, 0x12, 0x43, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x46, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65,
|
||||
0x64, 0x41, 0x75, 0x74, 0x6f, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x55, 0x0a, 0x0e, 0x53, 0x65, 0x74, 0x53, 0x74,
|
||||
0x65, 0x61, 0x6c, 0x74, 0x68, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x29, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||
0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
|
||||
0x31, 0x2e, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x4c,
|
||||
0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||
0x70, 0x74, 0x79, 0x1a, 0x25, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61,
|
||||
0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74,
|
||||
0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x57, 0x5a, 0x55,
|
||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x70, 0x74, 0x69, 0x6d,
|
||||
0x65, 0x2d, 0x69, 0x6e, 0x64, 0x75, 0x65, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x2f, 0x63, 0x6f,
|
||||
0x6d, 0x70, 0x75, 0x74, 0x65, 0x2d, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x2d, 0x61, 0x67, 0x65, 0x6e,
|
||||
0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c,
|
||||
0x70, 0x68, 0x61, 0x31, 0x3b, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x76, 0x31, 0x61,
|
||||
0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -489,7 +680,7 @@ func file_api_bladeapi_v1alpha1_blade_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_api_bladeapi_v1alpha1_blade_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
|
||||
var file_api_bladeapi_v1alpha1_blade_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||
var file_api_bladeapi_v1alpha1_blade_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_api_bladeapi_v1alpha1_blade_proto_goTypes = []interface{}{
|
||||
(Event)(0), // 0: api.bladeapi.v1alpha1.Event
|
||||
(FanUnit)(0), // 1: api.bladeapi.v1alpha1.FanUnit
|
||||
@@ -497,27 +688,33 @@ var file_api_bladeapi_v1alpha1_blade_proto_goTypes = []interface{}{
|
||||
(*StealthModeRequest)(nil), // 3: api.bladeapi.v1alpha1.StealthModeRequest
|
||||
(*SetFanSpeedRequest)(nil), // 4: api.bladeapi.v1alpha1.SetFanSpeedRequest
|
||||
(*EmitEventRequest)(nil), // 5: api.bladeapi.v1alpha1.EmitEventRequest
|
||||
(*StatusResponse)(nil), // 6: api.bladeapi.v1alpha1.StatusResponse
|
||||
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
|
||||
(*FanCurveStep)(nil), // 6: api.bladeapi.v1alpha1.FanCurveStep
|
||||
(*VersionInfo)(nil), // 7: api.bladeapi.v1alpha1.VersionInfo
|
||||
(*StatusResponse)(nil), // 8: api.bladeapi.v1alpha1.StatusResponse
|
||||
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
|
||||
}
|
||||
var file_api_bladeapi_v1alpha1_blade_proto_depIdxs = []int32{
|
||||
0, // 0: api.bladeapi.v1alpha1.EmitEventRequest.event:type_name -> api.bladeapi.v1alpha1.Event
|
||||
2, // 1: api.bladeapi.v1alpha1.StatusResponse.power_status:type_name -> api.bladeapi.v1alpha1.PowerStatus
|
||||
5, // 2: api.bladeapi.v1alpha1.BladeAgentService.EmitEvent:input_type -> api.bladeapi.v1alpha1.EmitEventRequest
|
||||
7, // 3: api.bladeapi.v1alpha1.BladeAgentService.WaitForIdentifyConfirm:input_type -> google.protobuf.Empty
|
||||
4, // 4: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeed:input_type -> api.bladeapi.v1alpha1.SetFanSpeedRequest
|
||||
3, // 5: api.bladeapi.v1alpha1.BladeAgentService.SetStealthMode:input_type -> api.bladeapi.v1alpha1.StealthModeRequest
|
||||
7, // 6: api.bladeapi.v1alpha1.BladeAgentService.GetStatus:input_type -> google.protobuf.Empty
|
||||
7, // 7: api.bladeapi.v1alpha1.BladeAgentService.EmitEvent:output_type -> google.protobuf.Empty
|
||||
7, // 8: api.bladeapi.v1alpha1.BladeAgentService.WaitForIdentifyConfirm:output_type -> google.protobuf.Empty
|
||||
7, // 9: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeed:output_type -> google.protobuf.Empty
|
||||
7, // 10: api.bladeapi.v1alpha1.BladeAgentService.SetStealthMode:output_type -> google.protobuf.Empty
|
||||
6, // 11: api.bladeapi.v1alpha1.BladeAgentService.GetStatus:output_type -> api.bladeapi.v1alpha1.StatusResponse
|
||||
7, // [7:12] is the sub-list for method output_type
|
||||
2, // [2:7] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
0, // 0: api.bladeapi.v1alpha1.EmitEventRequest.event:type_name -> api.bladeapi.v1alpha1.Event
|
||||
2, // 1: api.bladeapi.v1alpha1.StatusResponse.power_status:type_name -> api.bladeapi.v1alpha1.PowerStatus
|
||||
6, // 2: api.bladeapi.v1alpha1.StatusResponse.fan_curve_steps:type_name -> api.bladeapi.v1alpha1.FanCurveStep
|
||||
7, // 3: api.bladeapi.v1alpha1.StatusResponse.version:type_name -> api.bladeapi.v1alpha1.VersionInfo
|
||||
5, // 4: api.bladeapi.v1alpha1.BladeAgentService.EmitEvent:input_type -> api.bladeapi.v1alpha1.EmitEventRequest
|
||||
9, // 5: api.bladeapi.v1alpha1.BladeAgentService.WaitForIdentifyConfirm:input_type -> google.protobuf.Empty
|
||||
4, // 6: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeed:input_type -> api.bladeapi.v1alpha1.SetFanSpeedRequest
|
||||
9, // 7: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeedAuto:input_type -> google.protobuf.Empty
|
||||
3, // 8: api.bladeapi.v1alpha1.BladeAgentService.SetStealthMode:input_type -> api.bladeapi.v1alpha1.StealthModeRequest
|
||||
9, // 9: api.bladeapi.v1alpha1.BladeAgentService.GetStatus:input_type -> google.protobuf.Empty
|
||||
9, // 10: api.bladeapi.v1alpha1.BladeAgentService.EmitEvent:output_type -> google.protobuf.Empty
|
||||
9, // 11: api.bladeapi.v1alpha1.BladeAgentService.WaitForIdentifyConfirm:output_type -> google.protobuf.Empty
|
||||
9, // 12: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeed:output_type -> google.protobuf.Empty
|
||||
9, // 13: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeedAuto:output_type -> google.protobuf.Empty
|
||||
9, // 14: api.bladeapi.v1alpha1.BladeAgentService.SetStealthMode:output_type -> google.protobuf.Empty
|
||||
8, // 15: api.bladeapi.v1alpha1.BladeAgentService.GetStatus:output_type -> api.bladeapi.v1alpha1.StatusResponse
|
||||
10, // [10:16] is the sub-list for method output_type
|
||||
4, // [4:10] is the sub-list for method input_type
|
||||
4, // [4:4] is the sub-list for extension type_name
|
||||
4, // [4:4] is the sub-list for extension extendee
|
||||
0, // [0:4] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_api_bladeapi_v1alpha1_blade_proto_init() }
|
||||
@@ -563,6 +760,30 @@ func file_api_bladeapi_v1alpha1_blade_proto_init() {
|
||||
}
|
||||
}
|
||||
file_api_bladeapi_v1alpha1_blade_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*FanCurveStep); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_api_bladeapi_v1alpha1_blade_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*VersionInfo); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_api_bladeapi_v1alpha1_blade_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*StatusResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -581,7 +802,7 @@ func file_api_bladeapi_v1alpha1_blade_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_api_bladeapi_v1alpha1_blade_proto_rawDesc,
|
||||
NumEnums: 3,
|
||||
NumMessages: 4,
|
||||
NumMessages: 6,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
syntax = "proto4";
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
package api.bladeapi.v1alpha1;
|
||||
|
||||
option go_package = "github.com/xvzf/computeblade-agent/api/blade/v1alpha1;bladeapiv1alpha1";
|
||||
option go_package = "github.com/uptime-induestries/compute-blade-agent/api/blade/v1alpha1;bladeapiv1alpha1";
|
||||
|
||||
// Event is an event the agent reacts to
|
||||
enum Event {
|
||||
@@ -37,6 +37,17 @@ message EmitEventRequest {
|
||||
Event event = 1;
|
||||
}
|
||||
|
||||
message FanCurveStep {
|
||||
int64 temperature = 1;
|
||||
uint32 percent = 2;
|
||||
}
|
||||
|
||||
message VersionInfo {
|
||||
string version = 1;
|
||||
string commit = 2;
|
||||
int64 date = 3;
|
||||
}
|
||||
|
||||
message StatusResponse {
|
||||
bool stealth_mode = 1;
|
||||
bool identify_active = 2;
|
||||
@@ -44,6 +55,11 @@ message StatusResponse {
|
||||
int64 temperature = 4;
|
||||
int64 fan_rpm = 5;
|
||||
PowerStatus power_status = 6;
|
||||
uint32 fan_percent = 7;
|
||||
bool fan_speed_automatic = 8;
|
||||
int64 critical_temperature_threshold = 9;
|
||||
repeated FanCurveStep fan_curve_steps = 10;
|
||||
VersionInfo version = 11;
|
||||
}
|
||||
|
||||
service BladeAgentService {
|
||||
@@ -53,9 +69,17 @@ service BladeAgentService {
|
||||
// WaitForIdentifyConfirm blocks until the blades button is pressed
|
||||
rpc WaitForIdentifyConfirm(google.protobuf.Empty) returns (google.protobuf.Empty) {}
|
||||
|
||||
// Sets the fan speed to a specific value.
|
||||
rpc SetFanSpeed(SetFanSpeedRequest) returns (google.protobuf.Empty) {}
|
||||
|
||||
// Sets the fan speed to automatic mode.
|
||||
//
|
||||
// Internally, this is equivalent to calling SetFanSpeed with a nil/empty value.
|
||||
rpc SetFanSpeedAuto(google.protobuf.Empty) returns (google.protobuf.Empty) {}
|
||||
|
||||
// Sets the blade to stealth mode (disables all LEDs)
|
||||
rpc SetStealthMode(StealthModeRequest) returns (google.protobuf.Empty) {}
|
||||
|
||||
// Gets the current status of the blade
|
||||
rpc GetStatus(google.protobuf.Empty) returns (StatusResponse) {}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
BladeAgentService_EmitEvent_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/EmitEvent"
|
||||
BladeAgentService_WaitForIdentifyConfirm_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/WaitForIdentifyConfirm"
|
||||
BladeAgentService_SetFanSpeed_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/SetFanSpeed"
|
||||
BladeAgentService_SetFanSpeedAuto_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/SetFanSpeedAuto"
|
||||
BladeAgentService_SetStealthMode_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/SetStealthMode"
|
||||
BladeAgentService_GetStatus_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/GetStatus"
|
||||
)
|
||||
@@ -35,8 +36,15 @@ type BladeAgentServiceClient interface {
|
||||
EmitEvent(ctx context.Context, in *EmitEventRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// WaitForIdentifyConfirm blocks until the blades button is pressed
|
||||
WaitForIdentifyConfirm(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// Sets the fan speed to a specific value.
|
||||
SetFanSpeed(ctx context.Context, in *SetFanSpeedRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// Sets the fan speed to automatic mode.
|
||||
//
|
||||
// Internally, this is equivalent to calling SetFanSpeed with a nil/empty value.
|
||||
SetFanSpeedAuto(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// Sets the blade to stealth mode (disables all LEDs)
|
||||
SetStealthMode(ctx context.Context, in *StealthModeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// Gets the current status of the blade
|
||||
GetStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StatusResponse, error)
|
||||
}
|
||||
|
||||
@@ -75,6 +83,15 @@ func (c *bladeAgentServiceClient) SetFanSpeed(ctx context.Context, in *SetFanSpe
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *bladeAgentServiceClient) SetFanSpeedAuto(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, BladeAgentService_SetFanSpeedAuto_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *bladeAgentServiceClient) SetStealthMode(ctx context.Context, in *StealthModeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, BladeAgentService_SetStealthMode_FullMethodName, in, out, opts...)
|
||||
@@ -101,8 +118,15 @@ type BladeAgentServiceServer interface {
|
||||
EmitEvent(context.Context, *EmitEventRequest) (*emptypb.Empty, error)
|
||||
// WaitForIdentifyConfirm blocks until the blades button is pressed
|
||||
WaitForIdentifyConfirm(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
// Sets the fan speed to a specific value.
|
||||
SetFanSpeed(context.Context, *SetFanSpeedRequest) (*emptypb.Empty, error)
|
||||
// Sets the fan speed to automatic mode.
|
||||
//
|
||||
// Internally, this is equivalent to calling SetFanSpeed with a nil/empty value.
|
||||
SetFanSpeedAuto(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
// Sets the blade to stealth mode (disables all LEDs)
|
||||
SetStealthMode(context.Context, *StealthModeRequest) (*emptypb.Empty, error)
|
||||
// Gets the current status of the blade
|
||||
GetStatus(context.Context, *emptypb.Empty) (*StatusResponse, error)
|
||||
mustEmbedUnimplementedBladeAgentServiceServer()
|
||||
}
|
||||
@@ -120,6 +144,9 @@ func (UnimplementedBladeAgentServiceServer) WaitForIdentifyConfirm(context.Conte
|
||||
func (UnimplementedBladeAgentServiceServer) SetFanSpeed(context.Context, *SetFanSpeedRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetFanSpeed not implemented")
|
||||
}
|
||||
func (UnimplementedBladeAgentServiceServer) SetFanSpeedAuto(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetFanSpeedAuto not implemented")
|
||||
}
|
||||
func (UnimplementedBladeAgentServiceServer) SetStealthMode(context.Context, *StealthModeRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetStealthMode not implemented")
|
||||
}
|
||||
@@ -193,6 +220,24 @@ func _BladeAgentService_SetFanSpeed_Handler(srv interface{}, ctx context.Context
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _BladeAgentService_SetFanSpeedAuto_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(BladeAgentServiceServer).SetFanSpeedAuto(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: BladeAgentService_SetFanSpeedAuto_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(BladeAgentServiceServer).SetFanSpeedAuto(ctx, req.(*emptypb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _BladeAgentService_SetStealthMode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(StealthModeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -248,6 +293,10 @@ var BladeAgentService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "SetFanSpeed",
|
||||
Handler: _BladeAgentService_SetFanSpeed_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SetFanSpeedAuto",
|
||||
Handler: _BladeAgentService_SetFanSpeedAuto_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SetStealthMode",
|
||||
Handler: _BladeAgentService_SetStealthMode_Handler,
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
# Default configuration for the computeblade-agent
|
||||
|
||||
log:
|
||||
mode: production # production, development
|
||||
# Default configuration for the compute-blade-agent
|
||||
|
||||
# Listen configuration
|
||||
listen:
|
||||
metrics: ":9666"
|
||||
grpc: /tmp/computeblade-agent.sock
|
||||
grpc: /tmp/compute-blade-agent.sock
|
||||
authenticated: false
|
||||
mode: unix # tcp or unix
|
||||
|
||||
# Hardware abstraction layer configuration
|
||||
hal:
|
||||
bcm2711:
|
||||
# For the default fan unit, fanspeed measurement is causing a tiny bit of CPU laod.
|
||||
# Sometimes it might not be desired
|
||||
disable_fanspeed_measurement: false
|
||||
# For the default fan unit, fanspeed measurement is causing a tiny bit of CPU load.
|
||||
# Sometimes it might not be desired
|
||||
rpm_reporting_standard_fan_unit: true
|
||||
|
||||
# Idle LED color, values range from 0-255
|
||||
idle_led_color:
|
||||
idle_led_color:
|
||||
red: 0
|
||||
green: 16
|
||||
blue: 0
|
||||
@@ -36,14 +34,13 @@ criticalLedColor:
|
||||
# Enable/disable stealth mode; turns off all LEDs on the blade
|
||||
stealth_mode: false
|
||||
|
||||
|
||||
# Simple fan-speed controls based on the SoC temperature
|
||||
fan_controller:
|
||||
# For now, this is only supporting a two-step configuration.
|
||||
steps:
|
||||
- temperature: 45
|
||||
speed: 40
|
||||
percent: 40
|
||||
- temperature: 55
|
||||
speed: 80
|
||||
percent: 80
|
||||
|
||||
# Critical temperature threshold
|
||||
critical_temperature_threshold: 60
|
||||
|
||||
@@ -1,137 +1,239 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
||||
internal_agent "github.com/compute-blade-community/compute-blade-agent/internal/agent"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/agent"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spechtlabs/go-otel-utils/otelprovider"
|
||||
"github.com/spechtlabs/go-otel-utils/otelzap"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
bladeapiv1alpha1 "github.com/xvzf/computeblade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/xvzf/computeblade-agent/internal/agent"
|
||||
"github.com/xvzf/computeblade-agent/pkg/log"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// embed default configuration
|
||||
var (
|
||||
Version string
|
||||
Commit string
|
||||
// Date is the CommitTimestamp when the build was done as UNIX Timestamp
|
||||
Date string
|
||||
BuildTime time.Time
|
||||
)
|
||||
|
||||
//go:embed default-config.yaml
|
||||
var defaultConfig []byte
|
||||
var debug = pflag.BoolP("debug", "v", false, "enable verbose logging")
|
||||
|
||||
func main() {
|
||||
var wg sync.WaitGroup
|
||||
pflag.Parse()
|
||||
|
||||
// Setup configuration
|
||||
viper.SetConfigType("yaml")
|
||||
// auto-bind environment variables
|
||||
viper.SetEnvPrefix("AGENT")
|
||||
viper.SetEnvPrefix("BLADE")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath("/etc/compute-blade-agent")
|
||||
|
||||
// Load potential file configs
|
||||
if err := viper.ReadConfig(bytes.NewBuffer(defaultConfig)); err != nil {
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// setup logger
|
||||
var baseLogger *zap.Logger
|
||||
switch logMode := viper.GetString("log.mode"); logMode {
|
||||
case "development":
|
||||
|
||||
if debug != nil && *debug {
|
||||
baseLogger = zap.Must(zap.NewDevelopment())
|
||||
case "production":
|
||||
} else {
|
||||
baseLogger = zap.Must(zap.NewProduction())
|
||||
default:
|
||||
panic(fmt.Errorf("invalid log.mode: %s", logMode))
|
||||
}
|
||||
|
||||
zapLogger := baseLogger.With(zap.String("app", "computeblade-agent"))
|
||||
defer zapLogger.Sync()
|
||||
_ = zap.ReplaceGlobals(zapLogger.With(zap.String("scope", "global")))
|
||||
baseCtx := log.IntoContext(context.Background(), zapLogger)
|
||||
zapLogger := baseLogger.With(
|
||||
zap.String("app", "compute-blade-agent"),
|
||||
zap.String("version", Version),
|
||||
)
|
||||
defer func() {
|
||||
_ = zapLogger.Sync()
|
||||
}()
|
||||
|
||||
// Replace zap global
|
||||
undoZapGlobals := zap.ReplaceGlobals(zapLogger)
|
||||
|
||||
// Redirect stdlib log to zap
|
||||
undoStdLogRedirect := zap.RedirectStdLog(zapLogger)
|
||||
|
||||
// Create OpenTelemetry Log and Trace provider
|
||||
logProvider := otelprovider.NewLogger(
|
||||
otelprovider.WithLogAutomaticEnv(),
|
||||
)
|
||||
|
||||
traceProvider := otelprovider.NewTracer(
|
||||
otelprovider.WithTraceAutomaticEnv(),
|
||||
)
|
||||
|
||||
// Create otelLogger
|
||||
otelZapLogger := otelzap.New(zapLogger,
|
||||
otelzap.WithCaller(true),
|
||||
otelzap.WithMinLevel(zap.InfoLevel),
|
||||
otelzap.WithAnnotateLevel(zap.WarnLevel),
|
||||
otelzap.WithErrorStatusLevel(zap.ErrorLevel),
|
||||
otelzap.WithStackTrace(false),
|
||||
otelzap.WithLoggerProvider(logProvider),
|
||||
)
|
||||
|
||||
// Replace global otelZap logger
|
||||
undoOtelZapGlobals := otelzap.ReplaceGlobals(otelZapLogger)
|
||||
defer undoOtelZapGlobals()
|
||||
|
||||
// Cleanup Logging and Tracing
|
||||
defer func() {
|
||||
if err := traceProvider.ForceFlush(context.Background()); err != nil {
|
||||
otelzap.L().Warn("failed to flush traces")
|
||||
}
|
||||
|
||||
if err := logProvider.ForceFlush(context.Background()); err != nil {
|
||||
otelzap.L().Warn("failed to flush logs")
|
||||
}
|
||||
|
||||
if err := traceProvider.Shutdown(context.Background()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := logProvider.Shutdown(context.Background()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
undoStdLogRedirect()
|
||||
undoZapGlobals()
|
||||
}()
|
||||
|
||||
// Setup context
|
||||
baseCtx := log.IntoContext(context.Background(), otelZapLogger)
|
||||
ctx, cancelCtx := context.WithCancelCause(baseCtx)
|
||||
defer cancelCtx(context.Canceled)
|
||||
|
||||
// load configuration
|
||||
var cbAgentConfig agent.ComputeBladeAgentConfig
|
||||
if err := viper.Unmarshal(&cbAgentConfig); err != nil {
|
||||
log.FromContext(ctx).Error("Failed to load configuration", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
fmt.Printf("cbAgentConfig: %+v\n", cbAgentConfig)
|
||||
os.Exit(1)
|
||||
|
||||
computebladeAgent, err := agent.NewComputeBladeAgent(cbAgentConfig)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error("Failed to create agent", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
log.FromContext(ctx).WithError(err).Fatal("Failed to load configuration")
|
||||
}
|
||||
|
||||
// setup stop signal handlers
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
wg.Add(1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// Wait for context cancel or signal
|
||||
select {
|
||||
// Wait for context cancel
|
||||
case <-ctx.Done():
|
||||
|
||||
// Wait for signal
|
||||
case sig := <-sigs:
|
||||
// On signal, cancel context
|
||||
cancelCtx(fmt.Errorf("signal %s received", sig))
|
||||
switch sig {
|
||||
case syscall.SIGTERM:
|
||||
fallthrough
|
||||
case syscall.SIGINT:
|
||||
fallthrough
|
||||
case syscall.SIGQUIT:
|
||||
// On terminate signal, cancel context causing the program to terminate
|
||||
cancelCtx(fmt.Errorf("signal %s received", sig))
|
||||
|
||||
default:
|
||||
log.FromContext(ctx).Warn("Received unknown signal", zap.String("signal", sig.String()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if Date != "" {
|
||||
if unixTimestamp, err := strconv.ParseInt(Date, 10, 64); err == nil {
|
||||
BuildTime = time.Unix(unixTimestamp, 0)
|
||||
} else {
|
||||
BuildTime = time.Unix(0, 0)
|
||||
log.FromContext(context.Background()).WithError(err).Warn("Failed to parse build timestamp")
|
||||
}
|
||||
}
|
||||
|
||||
log.FromContext(ctx).Info("Bootstrapping compute-blade-agent",
|
||||
zap.String("version", Version),
|
||||
zap.String("commit", Commit),
|
||||
zap.String("date", BuildTime.Format(time.RFC3339)),
|
||||
)
|
||||
|
||||
cbAgentInfo := agent.ComputeBladeAgentInfo{
|
||||
Version: Version,
|
||||
Commit: Commit,
|
||||
BuildTime: BuildTime,
|
||||
}
|
||||
|
||||
computebladeAgent, err := internal_agent.NewComputeBladeAgent(ctx, cbAgentConfig, cbAgentInfo)
|
||||
if err != nil {
|
||||
cancelCtx(err)
|
||||
log.FromContext(ctx).WithError(err).Fatal("Failed to create agent")
|
||||
}
|
||||
|
||||
// Run agent
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := computebladeAgent.Run(ctx)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).Error("Failed to run agent", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Setup GRPC server
|
||||
// FIXME add logging middleware
|
||||
grpcServer := grpc.NewServer()
|
||||
bladeapiv1alpha1.RegisterBladeAgentServiceServer(grpcServer, agent.NewGrpcServiceFor(computebladeAgent))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
grpcListen, err := net.Listen("unix", viper.GetString("listen.grpc"))
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error("Failed to create grpc listener", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
return
|
||||
}
|
||||
log.FromContext(ctx).Info("Starting grpc server", zap.String("address", viper.GetString("listen.grpc")))
|
||||
if err := grpcServer.Serve(grpcListen); err != nil && err != grpc.ErrServerStopped {
|
||||
log.FromContext(ctx).Error("Failed to start grpc server", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-ctx.Done()
|
||||
log.FromContext(ctx).Info("Shutting down grpc server")
|
||||
grpcServer.GracefulStop()
|
||||
}()
|
||||
computebladeAgent.RunAsync(ctx, cancelCtx)
|
||||
|
||||
// setup prometheus endpoint
|
||||
promServer := runPrometheusEndpoint(ctx, cancelCtx, &cbAgentConfig.Listen)
|
||||
|
||||
// Wait for done
|
||||
<-ctx.Done()
|
||||
|
||||
// Since ctx is now done, we can no longer use it to get `log.FromContext(ctx)`
|
||||
// but we must use otelzap.L() to get a logger
|
||||
|
||||
// Shut down gRPC and Prom Servers async
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Shut-Down GRPC Server
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
log.FromContext(ctx).Info("Shutting down compute blade agent...")
|
||||
if err := computebladeAgent.GracefulStop(ctx); err != nil {
|
||||
log.FromContext(ctx).WithError(err).Error("Failed to close compute blade agent")
|
||||
}
|
||||
}()
|
||||
|
||||
// Shut-Down Prometheus Endpoint
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
shutdownCtx, shutdownCtxCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer shutdownCtxCancel()
|
||||
|
||||
otelzap.L().Info("Shutting down prometheus/pprof server")
|
||||
if err := promServer.Shutdown(shutdownCtx); err != nil {
|
||||
otelzap.L().WithError(err).Error("Failed to shutdown prometheus/pprof server")
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Terminate accordingly
|
||||
if err := ctx.Err(); !errors.Is(err, context.Canceled) {
|
||||
otelzap.L().WithError(err).Fatal("Exiting")
|
||||
} else {
|
||||
otelzap.L().Info("Exiting")
|
||||
}
|
||||
}
|
||||
|
||||
func runPrometheusEndpoint(ctx context.Context, cancel context.CancelCauseFunc, apiConfig *agent.ApiConfig) *http.Server {
|
||||
instrumentationHandler := http.NewServeMux()
|
||||
instrumentationHandler.Handle("/metrics", promhttp.Handler())
|
||||
instrumentationHandler.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
@@ -139,33 +241,17 @@ func main() {
|
||||
instrumentationHandler.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
instrumentationHandler.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
instrumentationHandler.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
server := &http.Server{Addr: ":9666", Handler: instrumentationHandler}
|
||||
wg.Add(1)
|
||||
|
||||
server := &http.Server{Addr: apiConfig.Metrics, Handler: instrumentationHandler}
|
||||
|
||||
// Run Prometheus Endpoint
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.FromContext(ctx).Error("Failed to start prometheus/pprof server", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-ctx.Done()
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err := server.Shutdown(shutdownCtx)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error("Failed to shutdown prometheus/pprof server", zap.Error(err))
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.FromContext(ctx).WithError(err).Error("Failed to start prometheus/pprof server")
|
||||
cancel(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for context cancel
|
||||
wg.Wait()
|
||||
if err := ctx.Err(); err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).Fatal("Exiting", zap.Error(err))
|
||||
} else {
|
||||
log.FromContext(ctx).Info("Exiting")
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
@@ -1,45 +1,209 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/spf13/cobra"
|
||||
bladeapiv1alpha1 "github.com/xvzf/computeblade-agent/api/bladeapi/v1alpha1"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var (
|
||||
percent int
|
||||
auto bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdFan.AddCommand(cmdFanSetPercent)
|
||||
rootCmd.AddCommand(cmdFan)
|
||||
cmdSetFan.Flags().IntVarP(&percent, "percent", "p", 40, "Fan speed in percent (Default: 40).")
|
||||
cmdSetFan.Flags().BoolVarP(&auto, "auto", "a", false, "Set fan speed to automatic mode.")
|
||||
|
||||
cmdSet.AddCommand(cmdSetFan)
|
||||
cmdGet.AddCommand(cmdGetFan)
|
||||
cmdRemove.AddCommand(cmdRmFan)
|
||||
cmdDescribe.AddCommand(cmdDescribeFan)
|
||||
}
|
||||
|
||||
var (
|
||||
cmdFan = &cobra.Command{
|
||||
Use: "fan",
|
||||
Short: "Fan-related commands for the compute blade",
|
||||
}
|
||||
fanAliases = []string{"fan_speed", "rpm"}
|
||||
|
||||
cmdFanSetPercent = &cobra.Command{
|
||||
Use: "set-percent <percent>",
|
||||
Example: "bladectl fan set-percent 50",
|
||||
Short: "Set the fan speed in percent",
|
||||
Args: cobra.ExactArgs(1),
|
||||
cmdSetFan = &cobra.Command{
|
||||
Use: "fan",
|
||||
Aliases: fanAliases,
|
||||
Short: "Control the fan behavior of the compute-blade",
|
||||
Example: "bladectl set fan --percent 50",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
autoSet := cmd.Flags().Changed("auto")
|
||||
percentSet := cmd.Flags().Changed("percent")
|
||||
|
||||
ctx := cmd.Context()
|
||||
client := clientFromContext(ctx)
|
||||
|
||||
// convert string to int
|
||||
percent, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
if autoSet && percentSet {
|
||||
return fmt.Errorf("only one of --auto or --percent can be specified")
|
||||
}
|
||||
|
||||
_, err = client.SetFanSpeed(ctx, &bladeapiv1alpha1.SetFanSpeedRequest{
|
||||
Percent: int64(percent),
|
||||
})
|
||||
if !autoSet && !percentSet {
|
||||
return fmt.Errorf("you must specify either --auto or --percent")
|
||||
}
|
||||
|
||||
return err
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
for _, client := range clients {
|
||||
var err error
|
||||
|
||||
if auto {
|
||||
_, err = client.SetFanSpeedAuto(ctx, &emptypb.Empty{})
|
||||
} else {
|
||||
_, err = client.SetFanSpeed(ctx, &bladeapiv1alpha1.SetFanSpeedRequest{
|
||||
Percent: int64(percent),
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmdRmFan = &cobra.Command{
|
||||
Use: "fan",
|
||||
Aliases: fanAliases,
|
||||
Short: "Remove the fan speed override of the compute-blade",
|
||||
Example: "bladectl unset fan",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
for _, client := range clients {
|
||||
if _, err := client.SetFanSpeedAuto(ctx, &emptypb.Empty{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmdGetFan = &cobra.Command{
|
||||
Use: "fan",
|
||||
Aliases: fanAliases,
|
||||
Short: "Get the fan speed of the compute-blade",
|
||||
Example: "bladectl get fan",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
for idx, client := range clients {
|
||||
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rpm := bladeStatus.FanRpm
|
||||
percent := bladeStatus.FanPercent
|
||||
rowPrefix := bladeNames[idx]
|
||||
if len(bladeNames) > 1 {
|
||||
rowPrefix += ": "
|
||||
} else {
|
||||
rowPrefix = ""
|
||||
}
|
||||
|
||||
fmt.Println(rpmStyle(rpm).Render(fmt.Sprint(rowPrefix + rpmLabel(rpm) + " (" + percentLabel(percent) + ")")))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmdDescribeFan = &cobra.Command{
|
||||
Use: "fan",
|
||||
Aliases: fanAliases,
|
||||
Short: "Get the fan speed curve of the compute-blade",
|
||||
Example: "bladectl describe fan",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
bladeFanCurves := make([][]*bladeapiv1alpha1.FanCurveStep, len(clients))
|
||||
criticalTemps := make([]int64, len(clients))
|
||||
for idx, client := range clients {
|
||||
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bladeFanCurves[idx] = bladeStatus.FanCurveSteps
|
||||
criticalTemps[idx] = bladeStatus.CriticalTemperatureThreshold
|
||||
}
|
||||
|
||||
printFanCurveTable(bladeFanCurves, criticalTemps)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func printFanCurveTable(bladeValues [][]*bladeapiv1alpha1.FanCurveStep, criticalTemps []int64) {
|
||||
bladeCount := len(bladeValues)
|
||||
|
||||
// Map blade index -> temperature -> step
|
||||
bladeTempMap := make([]map[int64]*bladeapiv1alpha1.FanCurveStep, bladeCount)
|
||||
allTempsSet := make(map[int64]struct{})
|
||||
|
||||
for bladeIdx, steps := range bladeValues {
|
||||
bladeTempMap[bladeIdx] = make(map[int64]*bladeapiv1alpha1.FanCurveStep)
|
||||
for _, step := range steps {
|
||||
temp := step.Temperature
|
||||
bladeTempMap[bladeIdx][temp] = step
|
||||
allTempsSet[temp] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Sorted temperature list
|
||||
var allTemps []int64
|
||||
for t := range allTempsSet {
|
||||
allTemps = append(allTemps, t)
|
||||
}
|
||||
|
||||
sort.Slice(allTemps, func(i, j int) bool {
|
||||
return allTemps[i] < allTemps[j]
|
||||
})
|
||||
|
||||
// Header: Blade | Temp1 | Temp2 | ...
|
||||
header := []string{"Blade"}
|
||||
for _, t := range allTemps {
|
||||
header = append(header, tempLabel(t))
|
||||
}
|
||||
|
||||
// Table writer setup
|
||||
tbl := tablewriter.NewTable(os.Stdout,
|
||||
tablewriter.WithHeader(header),
|
||||
tablewriter.WithHeaderAlignment(tw.AlignLeft),
|
||||
tablewriter.WithHeaderAutoFormat(tw.Off),
|
||||
)
|
||||
|
||||
// Rows: one per blade
|
||||
for bladeIdx, tempMap := range bladeTempMap {
|
||||
row := []string{bladeNames[bladeIdx]}
|
||||
for _, t := range allTemps {
|
||||
if step, ok := tempMap[t]; ok {
|
||||
style := tempStyle(step.Temperature, criticalTemps[bladeIdx])
|
||||
colored := style.Render(percentLabel(step.Percent))
|
||||
row = append(row, colored)
|
||||
} else {
|
||||
row = append(row, "")
|
||||
}
|
||||
}
|
||||
_ = tbl.Append(row)
|
||||
}
|
||||
|
||||
_ = tbl.Render()
|
||||
}
|
||||
|
||||
105
cmd/bladectl/cmd_get_misc.go
Normal file
105
cmd/bladectl/cmd_get_misc.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdGet.AddCommand(cmdGetTemp)
|
||||
cmdGet.AddCommand(cmdGetCritical)
|
||||
cmdGet.AddCommand(cmdGetPowerStatus)
|
||||
}
|
||||
|
||||
var (
|
||||
cmdGetTemp = &cobra.Command{
|
||||
Use: "temp",
|
||||
Aliases: []string{"temperature"},
|
||||
Short: "Get the temperature of the compute-blade",
|
||||
Example: "bladectl get temp",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
for idx, client := range clients {
|
||||
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
temp := bladeStatus.Temperature
|
||||
rowPrefix := bladeNames[idx]
|
||||
if len(bladeNames) > 1 {
|
||||
rowPrefix += ": "
|
||||
} else {
|
||||
rowPrefix = ""
|
||||
}
|
||||
|
||||
fmt.Println(tempStyle(temp, bladeStatus.CriticalTemperatureThreshold).Render(rowPrefix + tempLabel(temp)))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmdGetCritical = &cobra.Command{
|
||||
Use: "critical",
|
||||
Short: "Get the critical of the compute-blade",
|
||||
Example: "bladectl get critical",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
for idx, client := range clients {
|
||||
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowPrefix := bladeNames[idx]
|
||||
if len(bladeNames) > 1 {
|
||||
rowPrefix += ": "
|
||||
} else {
|
||||
rowPrefix = ""
|
||||
}
|
||||
|
||||
fmt.Println(activeStyle(bladeStatus.CriticalActive).Render(rowPrefix + activeLabel(bladeStatus.CriticalActive)))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmdGetPowerStatus = &cobra.Command{
|
||||
Use: "power_status",
|
||||
Aliases: []string{"powerstatus", "power"},
|
||||
Short: "Get the power status of the compute-blade",
|
||||
Example: "bladectl get power",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
for idx, client := range clients {
|
||||
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowPrefix := bladeNames[idx]
|
||||
if len(bladeNames) > 1 {
|
||||
rowPrefix += ": "
|
||||
} else {
|
||||
rowPrefix = ""
|
||||
}
|
||||
|
||||
fmt.Println(rowPrefix + hal.PowerStatus(bladeStatus.PowerStatus).String())
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1,58 +1,124 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
"github.com/spf13/cobra"
|
||||
bladeapiv1alpha1 "github.com/xvzf/computeblade-agent/api/bladeapi/v1alpha1"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var (
|
||||
confirm bool
|
||||
wait bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdIdentify.Flags().Bool("confirm", false, "confirm the identify state")
|
||||
cmdIdentify.Flags().Bool("wait", false, "Wait for the identify state to be confirmed (e.g. by a physical button press)")
|
||||
rootCmd.AddCommand(cmdIdentify)
|
||||
cmdSetIdentify.Flags().BoolVarP(&confirm, "confirm", "c", false, "confirm the identify state")
|
||||
cmdSetIdentify.Flags().BoolVarP(&wait, "wait", "w", false, "Wait for the identify state to be confirmed (e.g. by a physical button press)")
|
||||
cmdSet.AddCommand(cmdSetIdentify)
|
||||
cmdRemove.AddCommand(cmdRmIdentify)
|
||||
cmdGet.AddCommand(cmdGetIdentify)
|
||||
}
|
||||
|
||||
var cmdIdentify = &cobra.Command{
|
||||
Use: "identify",
|
||||
Short: "interact with the compute-blade identity LED",
|
||||
RunE: runIdentity,
|
||||
}
|
||||
var (
|
||||
cmdSetIdentify = &cobra.Command{
|
||||
Use: "identify",
|
||||
Example: "bladectl set identify --wait",
|
||||
Short: "interact with the compute-blade identity LED",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if len(bladeNames) > 1 && wait {
|
||||
return fmt.Errorf("cannot enable identify on multiple compute-blades at the same with the --wait flag")
|
||||
}
|
||||
|
||||
func runIdentity(cmd *cobra.Command, _ []string) error {
|
||||
var err error
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
ctx := cmd.Context()
|
||||
client := clientFromContext(ctx)
|
||||
for _, client := range clients {
|
||||
// Check if we should wait for the identify state to be confirmed
|
||||
event := bladeapiv1alpha1.Event_IDENTIFY
|
||||
if confirm {
|
||||
event = bladeapiv1alpha1.Event_IDENTIFY_CONFIRM
|
||||
}
|
||||
|
||||
// Get flags
|
||||
confirm, err := cmd.Flags().GetBool("confirm")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wait, err := cmd.Flags().GetBool("wait")
|
||||
if err != nil {
|
||||
return err
|
||||
// Emit the event to the compute-blade-agent
|
||||
_, err := client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: event})
|
||||
if err != nil {
|
||||
return errors.New(humane.Wrap(err,
|
||||
"failed to emit event",
|
||||
"ensure the compute-blade agent is running and responsive to requests",
|
||||
"check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'",
|
||||
).Display())
|
||||
}
|
||||
|
||||
// Check if we should wait for the identify state to be confirmed
|
||||
if wait {
|
||||
if _, err := client.WaitForIdentifyConfirm(ctx, &emptypb.Empty{}); err != nil {
|
||||
return errors.New(
|
||||
humane.Wrap(err, "unable to wait for confirmation",
|
||||
"ensure the compute-blade agent is running and responsive to requests",
|
||||
"check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'",
|
||||
).Display())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Check if we should wait for the identify state to be confirmed
|
||||
event := bladeapiv1alpha1.Event_IDENTIFY
|
||||
if confirm {
|
||||
event = bladeapiv1alpha1.Event_IDENTIFY_CONFIRM
|
||||
cmdRmIdentify = &cobra.Command{
|
||||
Use: "identify",
|
||||
Example: "bladectl unset identify",
|
||||
Short: "remove the identify state with the compute-blade identity LED",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
for _, client := range clients {
|
||||
// Emit the event to the compute-blade-agent
|
||||
_, err := client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: bladeapiv1alpha1.Event_IDENTIFY_CONFIRM})
|
||||
if err != nil {
|
||||
return errors.New(humane.Wrap(err,
|
||||
"failed to emit event",
|
||||
"ensure the compute-blade agent is running and responsive to requests",
|
||||
"check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'",
|
||||
).Display())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Emit the event to the computeblade-agent
|
||||
_, err = client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: event})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmdGetIdentify = &cobra.Command{
|
||||
Use: "identify",
|
||||
Example: "bladectl get identify",
|
||||
Short: "get the identify state of the compute-blade identity LED",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
// Check if we should wait for the identify state to be confirmed
|
||||
if wait {
|
||||
_, err := client.WaitForIdentifyConfirm(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for idx, client := range clients {
|
||||
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
rowPrefix := bladeNames[idx]
|
||||
if len(bladeNames) > 1 {
|
||||
rowPrefix += ": "
|
||||
} else {
|
||||
rowPrefix = ""
|
||||
}
|
||||
|
||||
fmt.Println(activeStyle(bladeStatus.IdentifyActive).Render(rowPrefix, activeLabel(bladeStatus.IdentifyActive)))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
169
cmd/bladectl/cmd_monitor.go
Normal file
169
cmd/bladectl/cmd_monitor.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
|
||||
ui "github.com/gizak/termui/v3"
|
||||
"github.com/gizak/termui/v3/widgets"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(cmdMonitor)
|
||||
}
|
||||
|
||||
var cmdMonitor = &cobra.Command{
|
||||
Use: "monitor",
|
||||
Aliases: fanAliases,
|
||||
Short: "Render a line-chart of the fan speed and temperature of the compute-blade",
|
||||
Example: "bladectl chart status",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(bladeNames) > 1 {
|
||||
return fmt.Errorf("cannot monitor multiple blades at once, please specify a single blade with --blade")
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
client := clientFromContext(ctx)
|
||||
|
||||
if err := ui.Init(); err != nil {
|
||||
return fmt.Errorf("failed to initialize UI: %w", err)
|
||||
}
|
||||
defer ui.Close()
|
||||
|
||||
events := ui.PollEvents()
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
labelBox := widgets.NewParagraph()
|
||||
labelBox.Title = fmt.Sprintf(" %s: Blade Status ", bladeNames[0])
|
||||
labelBox.Border = true
|
||||
labelBox.TextStyle = ui.NewStyle(ui.ColorWhite)
|
||||
|
||||
fanPlot := newPlot(fmt.Sprintf(" %s: Fan Speed (RPM) ", bladeNames[0]), ui.ColorGreen)
|
||||
tempPlot := newPlot(fmt.Sprintf(" %s: SoC Temperature (\u00b0C) ", bladeNames[0]), ui.ColorCyan)
|
||||
|
||||
fanData := []float64{math.NaN(), math.NaN()}
|
||||
tempData := []float64{math.NaN(), math.NaN()}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return ctx.Err()
|
||||
|
||||
case e := <-events:
|
||||
switch e.ID {
|
||||
case "q", "<C-c>":
|
||||
return nil
|
||||
case "<Resize>":
|
||||
renderCharts(nil, fanPlot, tempPlot, labelBox)
|
||||
ui.Clear()
|
||||
ui.Render(labelBox, fanPlot, tempPlot)
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
status, err := client.GetStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
labelBox.Text = "Error retrieving blade status: " + err.Error()
|
||||
ui.Render(labelBox)
|
||||
continue
|
||||
}
|
||||
|
||||
fanData = append(fanData, float64(status.FanRpm))
|
||||
tempData = append(tempData, float64(status.Temperature))
|
||||
|
||||
fanPlot.Data[0] = reversedFloats(fanData)
|
||||
tempPlot.Data[0] = reversedFloats(tempData)
|
||||
|
||||
renderCharts(status, fanPlot, tempPlot, labelBox)
|
||||
ui.Render(labelBox, fanPlot, tempPlot)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func reversedFloats(s []float64) []float64 {
|
||||
r := make([]float64, len(s))
|
||||
for i := range s {
|
||||
r[len(s)-1-i] = s[i]
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func newPlot(title string, color ui.Color) *widgets.Plot {
|
||||
plot := widgets.NewPlot()
|
||||
plot.Title = title
|
||||
plot.Data = [][]float64{{}}
|
||||
plot.LineColors = []ui.Color{color}
|
||||
plot.AxesColor = ui.ColorWhite
|
||||
plot.DrawDirection = widgets.DrawLeft
|
||||
plot.HorizontalScale = 2
|
||||
return plot
|
||||
}
|
||||
|
||||
func renderCharts(status *bladeapiv1alpha1.StatusResponse, fanPlot, tempPlot *widgets.Plot, labelBox *widgets.Paragraph) {
|
||||
width, height := ui.TerminalDimensions()
|
||||
labelHeight := 4
|
||||
|
||||
if status != nil {
|
||||
if status.CriticalActive {
|
||||
labelBox.Text = fmt.Sprintf(
|
||||
"Critical: %s | %s",
|
||||
activeLabel(status.CriticalActive),
|
||||
labelBox.Text,
|
||||
)
|
||||
}
|
||||
|
||||
labelBox.Text = fmt.Sprintf(
|
||||
"Temp: %d°C | Fan: %d RPM (%d%%)",
|
||||
status.Temperature,
|
||||
status.FanRpm,
|
||||
status.FanPercent,
|
||||
)
|
||||
|
||||
if !status.FanSpeedAutomatic {
|
||||
labelBox.Text = fmt.Sprintf(
|
||||
"%s | Fan Override: %s",
|
||||
labelBox.Text,
|
||||
fanSpeedOverrideLabel(status.FanSpeedAutomatic, status.FanPercent),
|
||||
)
|
||||
}
|
||||
|
||||
if status.StealthMode {
|
||||
labelBox.Text = fmt.Sprintf(
|
||||
"%s | Stealth: %s",
|
||||
labelBox.Text,
|
||||
activeLabel(status.StealthMode),
|
||||
)
|
||||
}
|
||||
|
||||
labelBox.Text = fmt.Sprintf(
|
||||
"%s | Identify: %s | Power: %s",
|
||||
labelBox.Text,
|
||||
activeLabel(status.IdentifyActive),
|
||||
hal.PowerStatus(status.PowerStatus).String(),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
labelBox.SetRect(0, 0, width, labelHeight)
|
||||
|
||||
if width >= 140 {
|
||||
fanPlot.SetRect(0, labelHeight, width/2, height)
|
||||
tempPlot.SetRect(width/2, labelHeight, width, height)
|
||||
} else {
|
||||
midY := (height-labelHeight)/2 + labelHeight
|
||||
fanPlot.SetRect(0, labelHeight, width, midY)
|
||||
tempPlot.SetRect(0, midY, width, height)
|
||||
}
|
||||
}
|
||||
187
cmd/bladectl/cmd_root.go
Normal file
187
cmd/bladectl/cmd_root.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/compute-blade-community/compute-blade-agent/cmd/bladectl/config"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var (
|
||||
allBlades bool
|
||||
bladeNames []string
|
||||
timeout time.Duration
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().BoolVarP(&allBlades, "all", "A", false, "control all compute-blades at the same time")
|
||||
rootCmd.PersistentFlags().StringArrayVar(&bladeNames, "blade", []string{""}, "Name of the compute-blade to control. If not provided, the compute-blade specified in `current-blade` will be used.")
|
||||
rootCmd.PersistentFlags().DurationVar(&timeout, "timeout", time.Minute, "timeout for gRPC requests")
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "bladectl",
|
||||
Short: "bladectl interacts with the compute-blade-agent and allows you to manage hardware-features of your compute blade(s)",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx, cancelCtx := context.WithCancelCause(cmd.Context())
|
||||
|
||||
// load configuration
|
||||
var bladectlCfg config.BladectlConfig
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
cancelCtx(err)
|
||||
return err
|
||||
}
|
||||
if err := viper.Unmarshal(&bladectlCfg); err != nil {
|
||||
cancelCtx(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// setup signal handlers for SIGINT and SIGTERM
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
go func() {
|
||||
select {
|
||||
// Wait for context cancel
|
||||
case <-ctx.Done():
|
||||
|
||||
// Wait for signal
|
||||
case sig := <-sigs:
|
||||
fmt.Println("Received signal", sig.String())
|
||||
|
||||
switch sig {
|
||||
case syscall.SIGTERM:
|
||||
fallthrough
|
||||
case syscall.SIGINT:
|
||||
fallthrough
|
||||
case syscall.SIGQUIT:
|
||||
// On terminate signal, cancel context causing the program to terminate
|
||||
cancelCtx(context.Canceled)
|
||||
|
||||
default:
|
||||
log.FromContext(ctx).Warn("Received unknown signal", zap.String("signal", sig.String()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Allow to easily select all blades
|
||||
if allBlades {
|
||||
bladeNames = make([]string, len(bladectlCfg.Blades))
|
||||
for idx, blade := range bladectlCfg.Blades {
|
||||
bladeNames[idx] = blade.Name
|
||||
}
|
||||
}
|
||||
|
||||
clients := make([]bladeapiv1alpha1.BladeAgentServiceClient, len(bladeNames))
|
||||
for idx, bladeName := range bladeNames {
|
||||
namedBlade, herr := bladectlCfg.FindBlade(bladeName)
|
||||
if herr != nil {
|
||||
cancelCtx(herr)
|
||||
return errors.New(herr.Display())
|
||||
}
|
||||
|
||||
bladeNames[idx] = namedBlade.Name
|
||||
|
||||
client, herr := buildClient(&namedBlade.Blade)
|
||||
if herr != nil {
|
||||
cancelCtx(herr)
|
||||
return errors.New(herr.Display())
|
||||
}
|
||||
|
||||
clients[idx] = client
|
||||
}
|
||||
|
||||
ctx = clientIntoContext(ctx, clients[0]) // Add the default client
|
||||
ctx = clientsIntoContext(ctx, clients) // Add all clients
|
||||
cmd.SetContext(ctx)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func loadTlsCredentials(server string, certData config.Certificate) (credentials.TransportCredentials, humane.Error) {
|
||||
// Decode base64 certificate, key, and CA
|
||||
certPEM, err := base64.StdEncoding.DecodeString(certData.ClientCertificateData)
|
||||
if err != nil {
|
||||
return nil, humane.Wrap(err, "invalid base64 client cert")
|
||||
}
|
||||
|
||||
keyPEM, err := base64.StdEncoding.DecodeString(certData.ClientKeyData)
|
||||
if err != nil {
|
||||
return nil, humane.Wrap(err, "invalid base64 client key")
|
||||
}
|
||||
|
||||
caPEM, err := base64.StdEncoding.DecodeString(certData.CertificateAuthorityData)
|
||||
if err != nil {
|
||||
return nil, humane.Wrap(err, "invalid base64 CA cert")
|
||||
}
|
||||
|
||||
// Load client cert/key pair
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, humane.Wrap(err, "failed to parse client cert/key pair")
|
||||
}
|
||||
|
||||
// Load CA into CertPool
|
||||
caPool := x509.NewCertPool()
|
||||
if !caPool.AppendCertsFromPEM(caPEM) {
|
||||
return nil, humane.Wrap(err, "failed to append CA certificate")
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
RootCAs: caPool,
|
||||
ServerName: server,
|
||||
}
|
||||
|
||||
return credentials.NewTLS(tlsConfig), nil
|
||||
}
|
||||
|
||||
func buildClient(blade *config.Blade) (bladeapiv1alpha1.BladeAgentServiceClient, humane.Error) {
|
||||
// Create our gRPC Transport Credentials
|
||||
creds := insecure.NewCredentials()
|
||||
certData := blade.Certificate
|
||||
|
||||
// If we're presented with certificate data in the config, we try to create a mTLS connection
|
||||
if len(certData.ClientCertificateData) > 0 && len(certData.ClientKeyData) > 0 && len(certData.CertificateAuthorityData) > 0 {
|
||||
serverName := blade.Server
|
||||
if strings.Contains(serverName, ":") {
|
||||
var err error
|
||||
if serverName, _, err = net.SplitHostPort(blade.Server); err != nil {
|
||||
return nil, humane.Wrap(err, "failed to parse server address")
|
||||
}
|
||||
}
|
||||
|
||||
var err humane.Error
|
||||
if creds, err = loadTlsCredentials(serverName, certData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(blade.Server, grpc.WithTransportCredentials(creds))
|
||||
if err != nil {
|
||||
return nil, humane.Wrap(err,
|
||||
"failed to dial grpc server",
|
||||
"ensure the gRPC server you are trying to connect to is running and the address is correct",
|
||||
)
|
||||
}
|
||||
|
||||
return bladeapiv1alpha1.NewBladeAgentServiceClient(conn), nil
|
||||
}
|
||||
77
cmd/bladectl/cmd_status.go
Normal file
77
cmd/bladectl/cmd_status.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdGet.AddCommand(cmdGetStatus)
|
||||
}
|
||||
|
||||
var cmdGetStatus = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Get in-depth information about the current state of the compute-blade",
|
||||
Example: "bladectl get status",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
bladeStatus := make([]*bladeapiv1alpha1.StatusResponse, len(clients))
|
||||
for idx, client := range clients {
|
||||
var err error
|
||||
if bladeStatus[idx], err = client.GetStatus(ctx, &emptypb.Empty{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
printStatusTable(bladeStatus)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func printStatusTable(bladeStatus []*bladeapiv1alpha1.StatusResponse) {
|
||||
// Header: Blade | Stat1 | Stat2 | ...
|
||||
header := []string{
|
||||
"Blade",
|
||||
"Temperature",
|
||||
"Fan Speed Override",
|
||||
"Fan Speed",
|
||||
"Stealth Mode",
|
||||
"Identify",
|
||||
"Critical Mode",
|
||||
"Power Status",
|
||||
}
|
||||
|
||||
// Table writer setup
|
||||
tbl := tablewriter.NewTable(os.Stdout,
|
||||
tablewriter.WithHeader(header),
|
||||
tablewriter.WithHeaderAlignment(tw.AlignLeft),
|
||||
tablewriter.WithHeaderAutoFormat(tw.Off),
|
||||
)
|
||||
|
||||
// Rows: one per blade
|
||||
for bladeIdx, status := range bladeStatus {
|
||||
row := []string{
|
||||
bladeNames[bladeIdx],
|
||||
tempStyle(status.Temperature, status.CriticalTemperatureThreshold).Render(tempLabel(status.Temperature)),
|
||||
speedOverrideStyle(status.FanSpeedAutomatic).Render(fanSpeedOverrideLabel(status.FanSpeedAutomatic, status.FanPercent)),
|
||||
rpmStyle(status.FanRpm).Render(rpmLabel(status.FanRpm) + " (" + percentLabel(status.FanPercent) + ")"),
|
||||
activeStyle(status.StealthMode).Render(activeLabel(status.StealthMode)),
|
||||
activeStyle(status.IdentifyActive).Render(activeLabel(status.IdentifyActive)),
|
||||
activeStyle(status.CriticalActive).Render(activeLabel(status.CriticalActive)),
|
||||
okStyle().Render(hal.PowerStatus(status.PowerStatus).String()),
|
||||
}
|
||||
|
||||
_ = tbl.Append(row)
|
||||
}
|
||||
|
||||
_ = tbl.Render()
|
||||
}
|
||||
88
cmd/bladectl/cmd_stealth.go
Normal file
88
cmd/bladectl/cmd_stealth.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var disable bool
|
||||
|
||||
func init() {
|
||||
cmdSetStealth.Flags().BoolVarP(&disable, "disable", "e", false, "disable stealth mode")
|
||||
|
||||
cmdSet.AddCommand(cmdSetStealth)
|
||||
cmdRemove.AddCommand(cmdRmStealth)
|
||||
cmdGet.AddCommand(cmdGetStealth)
|
||||
}
|
||||
|
||||
var (
|
||||
cmdSetStealth = &cobra.Command{
|
||||
Use: "stealth",
|
||||
Short: "Enable or disable stealth mode on the compute-blade",
|
||||
Example: "bladectl set stealth --disable",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
for _, client := range clients {
|
||||
_, err := client.SetStealthMode(ctx, &bladeapiv1alpha1.StealthModeRequest{Enable: !disable})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmdRmStealth = &cobra.Command{
|
||||
Use: "stealth",
|
||||
Short: "Disable stealth mode on the compute-blade",
|
||||
Example: "bladectl remove stealth",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
for _, client := range clients {
|
||||
_, err := client.SetStealthMode(ctx, &bladeapiv1alpha1.StealthModeRequest{Enable: false})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmdGetStealth = &cobra.Command{
|
||||
Use: "stealth",
|
||||
Short: "Get the stealth mode status of the compute-blade",
|
||||
Example: "bladectl get stealth",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
for idx, client := range clients {
|
||||
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowPrefix := bladeNames[idx]
|
||||
if len(bladeNames) > 1 {
|
||||
rowPrefix += ": "
|
||||
} else {
|
||||
rowPrefix = ""
|
||||
}
|
||||
|
||||
fmt.Println(activeStyle(bladeStatus.StealthMode).Render(rowPrefix, activeLabel(bladeStatus.StealthMode)))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
)
|
||||
39
cmd/bladectl/cmd_verbs.go
Normal file
39
cmd/bladectl/cmd_verbs.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(cmdGet)
|
||||
rootCmd.AddCommand(cmdSet)
|
||||
rootCmd.AddCommand(cmdRemove)
|
||||
rootCmd.AddCommand(cmdDescribe)
|
||||
}
|
||||
|
||||
var (
|
||||
cmdGet = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Display compute-blade related information",
|
||||
Long: "Prints information about compute-blade related information, e.g. fan speed, temperature, etc.",
|
||||
}
|
||||
|
||||
cmdDescribe = &cobra.Command{
|
||||
Use: "describe",
|
||||
Short: "Display compute-blade related information",
|
||||
Long: "Prints information about compute-blade related information, e.g. fan speed curve steps, etc.",
|
||||
}
|
||||
|
||||
cmdSet = &cobra.Command{
|
||||
Use: "set",
|
||||
Short: "Configure compute-blade",
|
||||
Long: "These commands allow you make changes to compute-blade related information.",
|
||||
}
|
||||
|
||||
cmdRemove = &cobra.Command{
|
||||
Use: "remove",
|
||||
Aliases: []string{"rm", "delete", "del", "unset"},
|
||||
Short: "Configure compute-blade",
|
||||
Long: "These commands allow you make changes to compute-blade related information.",
|
||||
}
|
||||
)
|
||||
80
cmd/bladectl/cmd_version.go
Normal file
80
cmd/bladectl/cmd_version.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(cmdVersion)
|
||||
}
|
||||
|
||||
var cmdVersion = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Shows version information",
|
||||
Example: "bladectl version",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clients := clientsFromContext(ctx)
|
||||
|
||||
header := []string{
|
||||
"Component",
|
||||
"Version",
|
||||
"Commit",
|
||||
"Build Time",
|
||||
}
|
||||
|
||||
// Table writer setup
|
||||
tbl := tablewriter.NewTable(os.Stdout,
|
||||
tablewriter.WithHeader(header),
|
||||
tablewriter.WithHeaderAlignment(tw.AlignLeft),
|
||||
tablewriter.WithHeaderAutoFormat(tw.Off),
|
||||
)
|
||||
|
||||
commit := Commit
|
||||
if len(commit) > 7 {
|
||||
commit = commit[:7]
|
||||
}
|
||||
|
||||
_ = tbl.Append([]string{"bladectl", Version, commit, BuildTime.Format(time.RFC3339)})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for idx, client := range clients {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if status, err := client.GetStatus(ctx, &emptypb.Empty{}); err == nil && status.Version != nil {
|
||||
commit := status.Version.Commit
|
||||
if len(commit) > 7 {
|
||||
commit = commit[:7]
|
||||
}
|
||||
|
||||
_ = tbl.Append([]string{
|
||||
fmt.Sprintf("api: %s", bladeNames[idx]),
|
||||
status.Version.Version,
|
||||
commit,
|
||||
time.Unix(status.Version.Date, 0).Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
log.Printf("Error (%s) getting status: %v", bladeNames[idx], err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
_ = tbl.Render()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
94
cmd/bladectl/config/config.go
Normal file
94
cmd/bladectl/config/config.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
"github.com/spechtlabs/go-otel-utils/otelzap"
|
||||
)
|
||||
|
||||
type BladectlConfig struct {
|
||||
Blades []NamedBlade `yaml:"blades" mapstructure:"blades"`
|
||||
CurrentBlade string `yaml:"current-blade" mapstructure:"current-blade"`
|
||||
}
|
||||
|
||||
type NamedBlade struct {
|
||||
Name string `yaml:"name" mapstructure:"name"`
|
||||
Blade Blade `yaml:"blade" mapstructure:"blade"`
|
||||
}
|
||||
|
||||
type Blade struct {
|
||||
Server string `yaml:"server" mapstructure:"server"`
|
||||
Certificate Certificate `yaml:"cert,omitempty" mapstructure:"cert,omitempty"`
|
||||
}
|
||||
|
||||
type Certificate struct {
|
||||
CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty" mapstructure:"certificate-authority-data,omitempty"`
|
||||
ClientCertificateData string `yaml:"client-certificate-data,omitempty" mapstructure:"client-certificate-data,omitempty"`
|
||||
ClientKeyData string `yaml:"client-key-data,omitempty" mapstructure:"client-key-data,omitempty"`
|
||||
}
|
||||
|
||||
func (c *BladectlConfig) FindBlade(name string) (*NamedBlade, humane.Error) {
|
||||
if len(name) == 0 {
|
||||
name = c.CurrentBlade
|
||||
}
|
||||
|
||||
for _, blade := range c.Blades {
|
||||
if blade.Name == name {
|
||||
return &blade, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, humane.New("current blade not found in configuration",
|
||||
"ensure you have a current-blade set in your configuration file, or use the --current-blade flag to specify one",
|
||||
"make sure you have a blade with the name you specified in the blades configuration",
|
||||
)
|
||||
}
|
||||
|
||||
func NewAuthenticatedBladectlConfig(server string, caPEM []byte, clientCertDER []byte, clientKeyDER []byte) *BladectlConfig {
|
||||
cfg := NewBladectlConfig(server)
|
||||
cfg.Blades[0].Blade.Certificate.CertificateAuthorityData = base64.StdEncoding.EncodeToString(caPEM)
|
||||
cfg.Blades[0].Blade.Certificate.ClientCertificateData = base64.StdEncoding.EncodeToString(clientCertDER)
|
||||
cfg.Blades[0].Blade.Certificate.ClientKeyData = base64.StdEncoding.EncodeToString(clientKeyDER)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func NewBladectlConfig(server string) *BladectlConfig {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
otelzap.L().WithError(err).Fatal("Failed to extract hostname")
|
||||
}
|
||||
|
||||
return &BladectlConfig{
|
||||
Blades: []NamedBlade{
|
||||
{
|
||||
Name: hostname,
|
||||
Blade: Blade{
|
||||
Server: server,
|
||||
},
|
||||
},
|
||||
},
|
||||
CurrentBlade: hostname,
|
||||
}
|
||||
}
|
||||
|
||||
func EnsureBladectlConfigHome() (string, humane.Error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", humane.Wrap(err, "Failed to extract home directory",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".config", "bladectl")
|
||||
if err := os.MkdirAll(configDir, 0700); err != nil {
|
||||
return "", humane.Wrap(err, "Failed to create config directory",
|
||||
"ensure the home-directory is writable by the agent user",
|
||||
)
|
||||
}
|
||||
|
||||
return configDir, nil
|
||||
}
|
||||
@@ -2,41 +2,37 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
bladeapiv1alpha1 "github.com/xvzf/computeblade-agent/api/bladeapi/v1alpha1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type grpcClientContextKey int
|
||||
|
||||
const (
|
||||
defaultGrpcClientContextKey grpcClientContextKey = 0
|
||||
defaultGrpcClientConnContextKey grpcClientContextKey = 1
|
||||
defaultGrpcClientContextKey grpcClientContextKey = 0
|
||||
defaultGrpcClientsContextKey grpcClientContextKey = 1
|
||||
)
|
||||
|
||||
var (
|
||||
grpcAddr string
|
||||
timeout time.Duration
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
BuildTime time.Time
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().
|
||||
StringVar(&grpcAddr, "addr", "unix:///tmp/computeblade-agent.sock", "address of the computeblade-agent gRPC server")
|
||||
rootCmd.PersistentFlags().DurationVar(&timeout, "timeout", time.Minute, "timeout for gRPC requests")
|
||||
}
|
||||
|
||||
func clientIntoContext(ctx context.Context, client bladeapiv1alpha1.BladeAgentServiceClient) context.Context {
|
||||
return context.WithValue(ctx, defaultGrpcClientContextKey, client)
|
||||
}
|
||||
|
||||
func clientsIntoContext(ctx context.Context, clients []bladeapiv1alpha1.BladeAgentServiceClient) context.Context {
|
||||
return context.WithValue(ctx, defaultGrpcClientsContextKey, clients)
|
||||
}
|
||||
|
||||
func clientFromContext(ctx context.Context) bladeapiv1alpha1.BladeAgentServiceClient {
|
||||
client, ok := ctx.Value(defaultGrpcClientContextKey).(bladeapiv1alpha1.BladeAgentServiceClient)
|
||||
if !ok {
|
||||
@@ -45,59 +41,30 @@ func clientFromContext(ctx context.Context) bladeapiv1alpha1.BladeAgentServiceCl
|
||||
return client
|
||||
}
|
||||
|
||||
func grpcConnIntoContext(ctx context.Context, grpcConn *grpc.ClientConn) context.Context {
|
||||
return context.WithValue(ctx, defaultGrpcClientConnContextKey, grpcConn)
|
||||
}
|
||||
|
||||
func grpcConnFromContext(ctx context.Context) *grpc.ClientConn {
|
||||
grpcConn, ok := ctx.Value(defaultGrpcClientContextKey).(*grpc.ClientConn)
|
||||
func clientsFromContext(ctx context.Context) []bladeapiv1alpha1.BladeAgentServiceClient {
|
||||
clients, ok := ctx.Value(defaultGrpcClientsContextKey).([]bladeapiv1alpha1.BladeAgentServiceClient)
|
||||
if !ok {
|
||||
panic("grpc client connection not found in context")
|
||||
panic("grpc client not found in context")
|
||||
}
|
||||
return grpcConn
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "bladectl",
|
||||
Short: "bladectl interacts with the computeblade-agent and allows you to manage hardware-features of your compute blade(s)",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
origCtx := cmd.Context()
|
||||
|
||||
// setup signal handlers for SIGINT and SIGTERM
|
||||
ctx, cancelCtx := context.WithTimeout(origCtx, timeout)
|
||||
|
||||
// setup signal handler channels
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
// Wait for context cancel or signal
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-sigs:
|
||||
// On signal, cancel context
|
||||
cancelCtx()
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := grpc.Dial(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to dial grpc server: %w", err)
|
||||
}
|
||||
client := bladeapiv1alpha1.NewBladeAgentServiceClient(conn)
|
||||
|
||||
cmd.SetContext(
|
||||
grpcConnIntoContext(clientIntoContext(ctx, client), conn),
|
||||
)
|
||||
return nil
|
||||
},
|
||||
|
||||
// Ensure we're closing the grpc connection on exit
|
||||
PersistentPostRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return grpcConnFromContext(cmd.Context()).Close()
|
||||
},
|
||||
return clients
|
||||
}
|
||||
|
||||
func main() {
|
||||
if Date != "" {
|
||||
if unixTimestamp, err := strconv.ParseInt(Date, 10, 64); err == nil {
|
||||
BuildTime = time.Unix(unixTimestamp, 0)
|
||||
} else {
|
||||
BuildTime = time.Unix(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup configuration
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath("$HOME/.config/bladectl")
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
83
cmd/bladectl/util.go
Normal file
83
cmd/bladectl/util.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
const (
|
||||
ColorCritical = lipgloss.Color("#cc0000")
|
||||
ColorWarning = lipgloss.Color("#e69138")
|
||||
ColorOk = lipgloss.Color("#04B575")
|
||||
)
|
||||
|
||||
func fanSpeedOverrideLabel(automatic bool, percent uint32) string {
|
||||
if automatic {
|
||||
return "Not set"
|
||||
}
|
||||
return fmt.Sprintf("%d%%", percent)
|
||||
}
|
||||
|
||||
func tempLabel(temp int64) string {
|
||||
return fmt.Sprintf("%d°C", temp)
|
||||
}
|
||||
|
||||
func percentLabel(percent uint32) string {
|
||||
return fmt.Sprintf("%d%%", percent)
|
||||
}
|
||||
|
||||
func rpmLabel(rpm int64) string {
|
||||
return fmt.Sprintf("%d RPM", rpm)
|
||||
}
|
||||
|
||||
func activeLabel(b bool) string {
|
||||
if b {
|
||||
return "Active"
|
||||
}
|
||||
return "Off"
|
||||
}
|
||||
|
||||
func speedOverrideStyle(automaticMode bool) lipgloss.Style {
|
||||
if automaticMode {
|
||||
return lipgloss.NewStyle().Foreground(ColorOk)
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().Foreground(ColorCritical)
|
||||
}
|
||||
|
||||
func activeStyle(active bool) lipgloss.Style {
|
||||
if active {
|
||||
return lipgloss.NewStyle().Foreground(ColorCritical)
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().Foreground(ColorOk)
|
||||
}
|
||||
|
||||
func tempStyle(temp int64, criticalTemp int64) lipgloss.Style {
|
||||
color := ColorOk
|
||||
|
||||
if temp >= criticalTemp {
|
||||
color = ColorCritical
|
||||
} else if temp >= criticalTemp-10 {
|
||||
color = ColorWarning
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().Foreground(color)
|
||||
}
|
||||
|
||||
func rpmStyle(rpm int64) lipgloss.Style {
|
||||
color := ColorOk
|
||||
|
||||
if rpm > 6000 {
|
||||
color = ColorCritical
|
||||
} else if rpm > 5250 {
|
||||
color = ColorWarning
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().Foreground(color)
|
||||
}
|
||||
|
||||
func okStyle() lipgloss.Style {
|
||||
return lipgloss.NewStyle().Foreground(ColorOk)
|
||||
}
|
||||
262
cmd/fanunit/controller.go
Normal file
262
cmd/fanunit/controller.go
Normal file
@@ -0,0 +1,262 @@
|
||||
//go:build tinygo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"machine"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/emc2101"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/ws2812"
|
||||
)
|
||||
|
||||
const (
|
||||
leftBladeTopicIn = "left:in"
|
||||
leftBladeTopicOut = "left:out"
|
||||
rightBladeTopicIn = "right:in"
|
||||
rightBladeTopicOut = "right:out"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
DefaultFanSpeed uint8
|
||||
LEDs ws2812.Device
|
||||
FanController emc2101.EMC2101
|
||||
ButtonPin machine.Pin
|
||||
|
||||
LeftUART drivers.UART
|
||||
RightUART drivers.UART
|
||||
|
||||
eb events.EventBus
|
||||
leftLed led.Color
|
||||
rightLed led.Color
|
||||
leftReqFanSpeed uint8
|
||||
rightReqFanSpeed uint8
|
||||
|
||||
buttonPressed int
|
||||
}
|
||||
|
||||
func (c *Controller) Run(parentCtx context.Context) error {
|
||||
c.eb = events.New()
|
||||
|
||||
c.FanController.Init()
|
||||
c.FanController.SetFanPercent(c.DefaultFanSpeed)
|
||||
c.LEDs.Write([]byte{0, 0, 0, 0, 0, 0})
|
||||
|
||||
group, ctx := errgroup.WithContext(parentCtx)
|
||||
|
||||
// LED Update events
|
||||
println("[+] Starting LED update loop")
|
||||
group.Go(func() error {
|
||||
return c.updateLEDs(ctx)
|
||||
})
|
||||
|
||||
// Fan speed update events
|
||||
println("[+] Starting fan update loop")
|
||||
group.Go(func() error {
|
||||
return c.updateFanSpeed(ctx)
|
||||
})
|
||||
|
||||
// Metric reporting events
|
||||
println("[+] Starting metric reporting loop")
|
||||
group.Go(func() error {
|
||||
return c.metricReporter(ctx)
|
||||
})
|
||||
|
||||
// Left blade events
|
||||
println("[+] Starting event listener (left)")
|
||||
group.Go(func() error {
|
||||
return c.listenEvents(ctx, c.LeftUART, leftBladeTopicIn)
|
||||
})
|
||||
println("[+] Starting event dispatcher (left)")
|
||||
group.Go(func() error {
|
||||
return c.dispatchEvents(ctx, c.LeftUART, leftBladeTopicOut)
|
||||
})
|
||||
|
||||
// right blade events
|
||||
println("[+] Starting event listener (right)")
|
||||
group.Go(func() error {
|
||||
return c.listenEvents(ctx, c.RightUART, rightBladeTopicIn)
|
||||
})
|
||||
println("[+] Starting event dispatcher (right)")
|
||||
group.Go(func() error {
|
||||
return c.dispatchEvents(ctx, c.RightUART, rightBladeTopicOut)
|
||||
})
|
||||
|
||||
// Button Press events
|
||||
println("[+] Starting button interrupt handler")
|
||||
c.ButtonPin.SetInterrupt(machine.PinFalling, func(machine.Pin) {
|
||||
c.buttonPressed += 1
|
||||
})
|
||||
|
||||
group.Go(func() error {
|
||||
ticker := time.NewTicker(20 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
btnPressed := smartfanunit.ButtonPressPacket{}
|
||||
if c.buttonPressed > 0 {
|
||||
// Allow up to 600ms for a 2nc button press
|
||||
time.Sleep(600 * time.Millisecond)
|
||||
}
|
||||
|
||||
if c.buttonPressed == 1 {
|
||||
println("[ ] Button pressed once")
|
||||
c.eb.Publish(leftBladeTopicOut, btnPressed.Packet())
|
||||
}
|
||||
if c.buttonPressed == 2 {
|
||||
println("[ ] Button pressed twice")
|
||||
c.eb.Publish(rightBladeTopicOut, btnPressed.Packet())
|
||||
}
|
||||
c.buttonPressed = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
// listenEvents reads events from the UART interface and dispatches them to the events
|
||||
func (c *Controller) listenEvents(ctx context.Context, uart drivers.UART, targetTopic string) error {
|
||||
for {
|
||||
// Read packet from UART; blocks until packet is received
|
||||
pkt, err := proto.ReadPacket(ctx, uart)
|
||||
if err != nil {
|
||||
println("[!] failed to read packet, continuing..", err.Error())
|
||||
continue
|
||||
}
|
||||
println("[ ] received packet from UART publishing to topic", targetTopic)
|
||||
c.eb.Publish(targetTopic, pkt)
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchEvents reads events from the events and writes them to the UART interface
|
||||
func (c *Controller) dispatchEvents(ctx context.Context, uart drivers.UART, sourceTopic string) error {
|
||||
sub := c.eb.Subscribe(sourceTopic, 4, events.MatchAll)
|
||||
defer sub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case msg := <-sub.C():
|
||||
println("[ ] dispatching event to UART from topic", sourceTopic)
|
||||
pkt := msg.(proto.Packet)
|
||||
err := proto.WritePacket(ctx, uart, pkt)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) metricReporter(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
airFlowTempRight := smartfanunit.AirFlowTemperaturePacket{}
|
||||
airFlowTempLeft := smartfanunit.AirFlowTemperaturePacket{}
|
||||
fanRpm := smartfanunit.FanSpeedRPMPacket{}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
}
|
||||
|
||||
airFlowTempLeft.Temperature, err = c.FanController.InternalTemperature()
|
||||
if err != nil {
|
||||
println("[!] failed to read internal temperature:", err.Error())
|
||||
}
|
||||
airFlowTempRight.Temperature, err = c.FanController.ExternalTemperature()
|
||||
if err != nil {
|
||||
println("[!] failed to read external temperature:", err.Error())
|
||||
}
|
||||
fanRpm.RPM, err = c.FanController.FanRPM()
|
||||
if err != nil {
|
||||
println("[!] failed to read fan RPM:", err.Error())
|
||||
}
|
||||
|
||||
// FIXME: This is a workaround for an i2c lockup issue.
|
||||
if err != nil {
|
||||
println("[!] resetting CPU")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
machine.CPUReset()
|
||||
}
|
||||
|
||||
// Publish metrics
|
||||
c.eb.Publish(leftBladeTopicOut, airFlowTempLeft.Packet())
|
||||
c.eb.Publish(rightBladeTopicOut, airFlowTempRight.Packet())
|
||||
c.eb.Publish(leftBladeTopicOut, fanRpm.Packet())
|
||||
c.eb.Publish(rightBladeTopicOut, fanRpm.Packet())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateFanSpeed(ctx context.Context) error {
|
||||
var pkt smartfanunit.SetFanSpeedPercentPacket
|
||||
|
||||
subLeft := c.eb.Subscribe(leftBladeTopicIn, 1, events.MatchAll)
|
||||
defer subLeft.Unsubscribe()
|
||||
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, events.MatchAll)
|
||||
defer subRight.Unsubscribe()
|
||||
|
||||
for {
|
||||
// Update LED color depending on blade
|
||||
select {
|
||||
case msg := <-subLeft.C():
|
||||
pkt.FromPacket(msg.(proto.Packet))
|
||||
c.leftReqFanSpeed = pkt.Percent
|
||||
case msg := <-subRight.C():
|
||||
pkt.FromPacket(msg.(proto.Packet))
|
||||
c.rightReqFanSpeed = pkt.Percent
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update fan speed with the max requested speed
|
||||
if c.leftReqFanSpeed > c.rightReqFanSpeed {
|
||||
c.FanController.SetFanPercent(c.leftReqFanSpeed)
|
||||
} else {
|
||||
c.FanController.SetFanPercent(c.rightReqFanSpeed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateLEDs(ctx context.Context) error {
|
||||
subLeft := c.eb.Subscribe(leftBladeTopicIn, 1, smartfanunit.MatchCmd(smartfanunit.CmdSetLED))
|
||||
defer subLeft.Unsubscribe()
|
||||
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, smartfanunit.MatchCmd(smartfanunit.CmdSetLED))
|
||||
defer subRight.Unsubscribe()
|
||||
|
||||
var pkt smartfanunit.SetLEDPacket
|
||||
for {
|
||||
// Update LED color depending on blade
|
||||
select {
|
||||
case msg := <-subLeft.C():
|
||||
pkt.FromPacket(msg.(proto.Packet))
|
||||
c.leftLed = pkt.Color
|
||||
case msg := <-subRight.C():
|
||||
pkt.FromPacket(msg.(proto.Packet))
|
||||
c.rightLed = pkt.Color
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
// Write to LEDs (they are in a chain -> we always have to update both)
|
||||
_, err := c.LEDs.Write([]byte{
|
||||
c.rightLed.Blue, c.rightLed.Green, c.rightLed.Red,
|
||||
c.leftLed.Blue, c.leftLed.Green, c.leftLed.Red,
|
||||
})
|
||||
if err != nil {
|
||||
println("[!] failed to update LEDs", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
88
cmd/fanunit/main.go
Normal file
88
cmd/fanunit/main.go
Normal file
@@ -0,0 +1,88 @@
|
||||
//go:build tinygo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"machine"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/emc2101"
|
||||
"tinygo.org/x/drivers/ws2812"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var controller *Controller
|
||||
var emc emc2101.EMC2101
|
||||
var bgrLeds ws2812.Device
|
||||
var err error
|
||||
|
||||
// Configure status LED
|
||||
machine.LED.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.LED.Set(false)
|
||||
|
||||
// Configure UARTs
|
||||
err = machine.UART0.Configure(machine.UARTConfig{TX: machine.UART0_TX_PIN, RX: machine.UART0_RX_PIN})
|
||||
if err != nil {
|
||||
println("[!] Failed to initialize UART0:", err.Error())
|
||||
goto errPrint
|
||||
}
|
||||
machine.UART0.SetBaudRate(smartfanunit.BaudRate)
|
||||
err = machine.UART1.Configure(machine.UARTConfig{TX: machine.UART1_TX_PIN, RX: machine.UART1_RX_PIN})
|
||||
if err != nil {
|
||||
println("[!] Failed to initialize UART1:", err.Error())
|
||||
goto errPrint
|
||||
}
|
||||
machine.UART1.SetBaudRate(smartfanunit.BaudRate)
|
||||
|
||||
// Enables fan, DO NOT CHANGE
|
||||
machine.GP16.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.GP16.Set(true)
|
||||
|
||||
// WS2812 LEDs
|
||||
machine.GP15.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
bgrLeds = ws2812.New(machine.GP15)
|
||||
|
||||
// Configure button
|
||||
machine.GP12.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
|
||||
// Setup emc2101
|
||||
machine.I2C0.Configure(machine.I2CConfig{
|
||||
Frequency: 100 * machine.KHz,
|
||||
SDA: machine.I2C0_SDA_PIN,
|
||||
SCL: machine.I2C0_SCL_PIN,
|
||||
})
|
||||
emc = emc2101.New(machine.I2C0)
|
||||
err = emc.Init()
|
||||
if err != nil {
|
||||
println("[!] Failed to initialize emc2101:", err.Error())
|
||||
goto errPrint
|
||||
}
|
||||
|
||||
println("[+] IO initialized, starting controller...")
|
||||
|
||||
// Run controller
|
||||
controller = &Controller{
|
||||
DefaultFanSpeed: 40,
|
||||
LEDs: bgrLeds,
|
||||
FanController: emc,
|
||||
ButtonPin: machine.GP12,
|
||||
LeftUART: machine.UART0,
|
||||
RightUART: machine.UART1,
|
||||
}
|
||||
|
||||
err = controller.Run(context.Background())
|
||||
|
||||
// Blinking -> something went wrong
|
||||
errPrint:
|
||||
ledState := false
|
||||
for {
|
||||
ledState = !ledState
|
||||
machine.LED.Set(ledState)
|
||||
// Repeat error message
|
||||
println("[FATAL] controller exited with error:", err)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
118
go.mod
118
go.mod
@@ -1,46 +1,98 @@
|
||||
module github.com/xvzf/computeblade-agent
|
||||
module github.com/compute-blade-community/compute-blade-agent
|
||||
|
||||
go 1.20
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.8.3
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/gizak/termui/v3 v3.1.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3
|
||||
github.com/olekukonko/tablewriter v1.1.4
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/sierrasoftworks/humane-errors-go v0.0.0-20260226124905-a7be4ffe4f32
|
||||
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.15
|
||||
github.com/spechtlabs/go-otel-utils/otelzap v0.0.15
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/warthog618/gpiod v0.8.1
|
||||
go.uber.org/zap v1.24.0
|
||||
google.golang.org/grpc v1.56.2
|
||||
google.golang.org/protobuf v1.31.0
|
||||
go.bug.st/serial v1.6.4
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/grpc v1.80.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
tinygo.org/x/drivers v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/smithy-go v1.22.5 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
|
||||
github.com/creack/goselect v0.1.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.2.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.6 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.13.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||
)
|
||||
|
||||
951
go.sum
951
go.sum
@@ -1,529 +1,462 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
|
||||
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
|
||||
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/clipperhouse/displaywidth v0.3.1 h1:k07iN9gD32177o1y4O1jQMzbLdCrsGJh+blirVYybsk=
|
||||
github.com/clipperhouse/displaywidth v0.3.1/go.mod h1:tgLJKKyaDOCadywag3agw4snxS5kYEuYR6Y9+qWDDYM=
|
||||
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
|
||||
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc=
|
||||
github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
|
||||
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
|
||||
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
|
||||
github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/ll v0.1.2 h1:lkg/k/9mlsy0SxO5aC+WEpbdT5K83ddnNhAepz7TQc0=
|
||||
github.com/olekukonko/ll v0.1.2/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
|
||||
github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg=
|
||||
github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
|
||||
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
|
||||
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
|
||||
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
|
||||
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
|
||||
github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
|
||||
github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
|
||||
github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
|
||||
github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
|
||||
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
|
||||
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
|
||||
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/olekukonko/tablewriter v1.1.1 h1:b3reP6GCfrHwmKkYwNRFh2rxidGHcT6cgxj/sHiDDx0=
|
||||
github.com/olekukonko/tablewriter v1.1.1/go.mod h1:De/bIcTF+gpBDB3Alv3fEsZA+9unTsSzAg/ZGADCtn4=
|
||||
github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc=
|
||||
github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg=
|
||||
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
|
||||
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
|
||||
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
|
||||
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
|
||||
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
|
||||
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250507223502-4bb667dc1e16 h1:9vtY3febGroV+aPR5OlI3fekkesi+lMVsVWyxBp/rfk=
|
||||
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250507223502-4bb667dc1e16/go.mod h1:CbJLj9L1qHdzLg4YRh2Lzr0noe9pR6QrVEqfLbITRKw=
|
||||
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250811205537-5f14a04ebff5 h1:nlfxPheTxwOE5hEq9iVurbo83/Wie52V5lKIhi73mRw=
|
||||
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250811205537-5f14a04ebff5/go.mod h1:CbJLj9L1qHdzLg4YRh2Lzr0noe9pR6QrVEqfLbITRKw=
|
||||
github.com/sierrasoftworks/humane-errors-go v0.0.0-20251121131909-6b4ca9dd07a7 h1:mD7GjWbIAdxIvDwTXs2uidulnIAfi06h7SZBQvDZga8=
|
||||
github.com/sierrasoftworks/humane-errors-go v0.0.0-20251121131909-6b4ca9dd07a7/go.mod h1:AyTGN4SGjbmEHvVAnBy8SpCnG3hskAfZ7TJW1AzMXgk=
|
||||
github.com/sierrasoftworks/humane-errors-go v0.0.0-20260226124905-a7be4ffe4f32 h1:6qDjIFnVHw6zdtW0t3RuN1PWcVSL6kXZiiDYR/P7id8=
|
||||
github.com/sierrasoftworks/humane-errors-go v0.0.0-20260226124905-a7be4ffe4f32/go.mod h1:AyTGN4SGjbmEHvVAnBy8SpCnG3hskAfZ7TJW1AzMXgk=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.10 h1:Q5p+5KGA587GfzR6FdXGje4XBfxhi1u4NSu6lSnWCGA=
|
||||
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.10/go.mod h1:sFuJXEBbNq/pQx9pP5OnVtx9yGJnH4fXi7x3qihW0ak=
|
||||
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.13 h1:DyTnvzLrhGowglrmDO8GFzbC/ZnzHRkMlFCk7p/pEmc=
|
||||
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.13/go.mod h1:63JphMzIY2vGTZ9OMjntS/XUM2sVwLCydSsjVE+M5QU=
|
||||
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.15 h1:LRx9EFzD4bI+kH3NNn6GtqwRrGbJgmyau4IOJcfBTYE=
|
||||
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.15/go.mod h1:ACquqruOxYFRL5H7TCxXKXIJTVj+6eqlEJR6WQAEBeE=
|
||||
github.com/spechtlabs/go-otel-utils/otelzap v0.0.10 h1:RR/WS4b+ABxNL7xzlK4FTvnuXRbGk3yyggvvLnQ+FeM=
|
||||
github.com/spechtlabs/go-otel-utils/otelzap v0.0.10/go.mod h1:IhsBuW+sZwLxX1Ww5LmTlIonBP8GiyhsiZkIRq+ySE0=
|
||||
github.com/spechtlabs/go-otel-utils/otelzap v0.0.11 h1:D3jzku3MMLMt/CgI++Zk3TE/mnQWz9dI9y/sCBb60OA=
|
||||
github.com/spechtlabs/go-otel-utils/otelzap v0.0.11/go.mod h1:Bk/uRNkU1/MsMc2J1NUxF+0lRNDbUUIulioJevLYM74=
|
||||
github.com/spechtlabs/go-otel-utils/otelzap v0.0.13 h1:eKrf3Jd4WurABA+iw4TAZJRF2/Ke1cIzsX1wSUOZZT0=
|
||||
github.com/spechtlabs/go-otel-utils/otelzap v0.0.13/go.mod h1:Bk/uRNkU1/MsMc2J1NUxF+0lRNDbUUIulioJevLYM74=
|
||||
github.com/spechtlabs/go-otel-utils/otelzap v0.0.15 h1:LPo3vmPRVTQDG/ic2r4q1t9irptnffRRTEpaLYjbguo=
|
||||
github.com/spechtlabs/go-otel-utils/otelzap v0.0.15/go.mod h1:0LC7Tzo53EdZ0FY6dUMDDkDmimphYDbjqIg+BY5MJjk=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
||||
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/warthog618/gpiod v0.8.1 h1:+8iHpHd3fljAd6l4AT8jPbMDQNKdvBIpW/hmLgAcHiM=
|
||||
github.com/warthog618/gpiod v0.8.1/go.mod h1:A7v1hGR2eTsnkN+e9RoAPYgJG9bLJWtwyIIK+pgqC7s=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 h1:HMUytBT3uGhPKYY/u/G5MR9itrlSO2SMOsSD3Tk3k7A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0/go.mod h1:hdDXsiNLmdW/9BF2jQpnHHlhFajpWCEYfM6e5m2OAZg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 h1:z6lNIajgEBVtQZHjfw2hAccPEBDs+nx58VemmXWa2ec=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0/go.mod h1:+kyc3bRx/Qkq05P6OCu3mTEIOxYRYzoIg+JsUp5X+PM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 h1:zUfYw8cscHHLwaY8Xz3fiJu+R59xBnkgq2Zr1lwmK/0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0/go.mod h1:514JLMCcFLQFS8cnTepOk6I09cKWJ5nGHBxHrMJ8Yfg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
||||
go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y=
|
||||
go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc=
|
||||
go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls=
|
||||
go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c=
|
||||
go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY=
|
||||
go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ=
|
||||
go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
|
||||
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo=
|
||||
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY=
|
||||
google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA=
|
||||
google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
tinygo.org/x/drivers v0.31.0 h1:Q2RpvTRMtdmjHD2Xyn4e8WXsJZKpIny3Lg4hzG1dLu4=
|
||||
tinygo.org/x/drivers v0.31.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
|
||||
tinygo.org/x/drivers v0.32.0 h1:qz7MRR1ZBIUhWC6kc4XuVNPr2+mUT8m7QJwA+Jji4IU=
|
||||
tinygo.org/x/drivers v0.32.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
|
||||
tinygo.org/x/drivers v0.33.0 h1:5r8Ab0IxjWQi7LzYLNWpya6U4nedo9ZtxeMaAzrJTG8=
|
||||
tinygo.org/x/drivers v0.33.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
|
||||
tinygo.org/x/drivers v0.34.0 h1:lw8ePJeUSn9oICKBvQXHC9TIE+J00OfXfkGTrpXM9Iw=
|
||||
tinygo.org/x/drivers v0.34.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
|
||||
|
||||
@@ -5,7 +5,7 @@ set -eu
|
||||
tmp_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
# Function to detect the Linux package manager
|
||||
# Function to detect the package suffix based on the available package manager
|
||||
detect_package_suffix() {
|
||||
if [ -x "$(command -v dpkg)" ]; then
|
||||
echo "deb"
|
||||
@@ -25,13 +25,16 @@ get_latest_release() {
|
||||
curl -s "https://api.github.com/repos/$repo/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
|
||||
}
|
||||
|
||||
github_repo="uptime-lab/computeblade-agent"
|
||||
github_repo="compute-blade-community/compute-blade-agent"
|
||||
package_suffix=$(detect_package_suffix)
|
||||
latest_release=$(get_latest_release "$github_repo")
|
||||
|
||||
# Create a temporary directory for the download
|
||||
tmp_dir=$(mktemp -d)
|
||||
|
||||
# Construct the download URL and filename based on the detected package manager and latest release
|
||||
download_url="https://github.com/$github_repo/releases/download/$latest_release/${github_repo##*/}_${latest_release#v}_linux_arm64.$package_suffix"
|
||||
target_file="$tmp_dir/computeblade-agent.$package_suffix"
|
||||
download_url="https://github.com/$github_repo/releases/download/$latest_release/compute-blade-agent_${latest_release#v}_linux_arm64.$package_suffix"
|
||||
target_file="$tmp_dir/compute-blade-agent.$package_suffix"
|
||||
|
||||
# Download the package
|
||||
echo "Downloading $download_url"
|
||||
@@ -49,8 +52,12 @@ case "$package_suffix" in
|
||||
pkg.tar.zst)
|
||||
sudo pacman -U --noconfirm "$target_file"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported package format" >> /dev/stderr
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Enable and start the service
|
||||
echo "Enabling and starting computeblade-agent"
|
||||
sudo systemctl enable computeblade-agent --now
|
||||
echo "Enabling and starting compute-blade-agent"
|
||||
sudo systemctl enable compute-blade-agent --now
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[Unit]
|
||||
Description=ComputeBlade Agent
|
||||
Documentation=https://github.com/xvzf/computeblade-agent
|
||||
Documentation=https://github.com/compute-blade-community/compute-blade-agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Restart=on-failure
|
||||
ExecStart=/usr/bin/computeblade-agent
|
||||
ExecStart=/usr/bin/compute-blade-agent
|
||||
TimeoutStopSec=20s
|
||||
|
||||
[Install]
|
||||
@@ -1,18 +1,25 @@
|
||||
package agent
|
||||
package internal_agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/agent"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/ledengine"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/xvzf/computeblade-agent/pkg/fancontroller"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal"
|
||||
"github.com/xvzf/computeblade-agent/pkg/ledengine"
|
||||
"github.com/xvzf/computeblade-agent/pkg/log"
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -20,415 +27,277 @@ var (
|
||||
eventCounter = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "computeblade_agent",
|
||||
Name: "events_count",
|
||||
Help: "ComputeBlade Agent internal event handler statistics (handled events)",
|
||||
Help: "ComputeBlade agent internal event handler statistics (handled events)",
|
||||
}, []string{"type"})
|
||||
|
||||
// droppedEventCounter is a prometheus counter that counts the number of events dropped by the agent
|
||||
droppedEventCounter = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "computeblade_agent",
|
||||
Name: "events_dropped_count",
|
||||
Help: "ComputeBlade Agent internal event handler statistics (dropped events)",
|
||||
Help: "ComputeBlade agent internal event handler statistics (dropped events)",
|
||||
}, []string{"type"})
|
||||
)
|
||||
|
||||
type Event int
|
||||
|
||||
const (
|
||||
NoopEvent = iota
|
||||
IdentifyEvent
|
||||
IdentifyConfirmEvent
|
||||
CriticalEvent
|
||||
CriticalResetEvent
|
||||
EdgeButtonEvent
|
||||
)
|
||||
|
||||
func (e Event) String() string {
|
||||
switch e {
|
||||
case NoopEvent:
|
||||
return "noop"
|
||||
case IdentifyEvent:
|
||||
return "identify"
|
||||
case IdentifyConfirmEvent:
|
||||
return "identify_confirm"
|
||||
case CriticalEvent:
|
||||
return "critical"
|
||||
case CriticalResetEvent:
|
||||
return "critical_reset"
|
||||
case EdgeButtonEvent:
|
||||
return "edge_button"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type ComputeBladeAgentConfig struct {
|
||||
// IdleLedColor is the color of the edge LED when the blade is idle mode
|
||||
IdleLedColor hal.LedColor `mapstructure:"idle_led_color"`
|
||||
// IdentifyLedColor is the color of the edge LED when the blade is in identify mode
|
||||
IdentifyLedColor hal.LedColor `mapstructure:"identify_led_color"`
|
||||
// CriticalLedColor is the color of the top(!) LED when the blade is in critical mode.
|
||||
// In the circumstance when >1 blades are in critical mode, the identidy function can be used to find the right blade
|
||||
CriticalLedColor hal.LedColor `mapstructure:"critical_led_color"`
|
||||
|
||||
// StealthModeEnabled indicates whether stealth mode is enabled
|
||||
StealthModeEnabled bool `mapstructure:"stealth_mode"`
|
||||
|
||||
// Critical temperature of the compute blade (used to trigger critical mode)
|
||||
CriticalTemperatureThreshold uint `mapstructure:"critical_temperature_threshold"`
|
||||
|
||||
// FanSpeed allows to set a fixed fan speed (in percent)
|
||||
FanSpeed *fancontroller.FanOverrideOpts `mapstructure:"fan_speed"`
|
||||
// FanControllerConfig is the configuration of the fan controller
|
||||
FanControllerConfig fancontroller.FanControllerConfig `mapstructure:"fan_controller"`
|
||||
}
|
||||
|
||||
// ComputeBladeAgent implements the core-logic of the agent. It is responsible for handling events and interfacing with the hardware.
|
||||
type ComputeBladeAgent interface {
|
||||
// Run dispatches the agent and blocks until the context is canceled or an error occurs
|
||||
Run(ctx context.Context) error
|
||||
// EmitEvent emits an event to the agent
|
||||
EmitEvent(ctx context.Context, event Event) error
|
||||
// SetFanSpeed sets the fan speed in percent
|
||||
SetFanSpeed(_ context.Context, speed uint8) error
|
||||
// SetStealthMode sets the stealth mode
|
||||
SetStealthMode(_ context.Context, enabled bool) error
|
||||
|
||||
// WaitForIdentifyConfirm blocks until the user confirms the identify mode
|
||||
WaitForIdentifyConfirm(ctx context.Context) error
|
||||
}
|
||||
|
||||
// computeBladeAgentImpl is the implementation of the ComputeBladeAgent interface
|
||||
type computeBladeAgentImpl struct {
|
||||
opts ComputeBladeAgentConfig
|
||||
// computeBladeAgent manages the operation and coordination of hardware components and services for a compute blade agent.
|
||||
type computeBladeAgent struct {
|
||||
bladeapiv1alpha1.UnimplementedBladeAgentServiceServer
|
||||
config agent.ComputeBladeAgentConfig
|
||||
blade hal.ComputeBladeHal
|
||||
state ComputebladeState
|
||||
state agent.ComputebladeState
|
||||
edgeLedEngine ledengine.LedEngine
|
||||
topLedEngine ledengine.LedEngine
|
||||
|
||||
fanController fancontroller.FanController
|
||||
|
||||
eventChan chan Event
|
||||
eventChan chan events.Event
|
||||
server *grpc.Server
|
||||
agentInfo agent.ComputeBladeAgentInfo
|
||||
}
|
||||
|
||||
func NewComputeBladeAgent(opts ComputeBladeAgentConfig) (ComputeBladeAgent, error) {
|
||||
var err error
|
||||
|
||||
// blade, err := hal.NewCm4Hal(hal.ComputeBladeHalOpts{
|
||||
blade, err := hal.NewCm4Hal(hal.ComputeBladeHalOpts{
|
||||
FanUnit: hal.FanUnitStandard, // FIXME: support smart fan unit
|
||||
})
|
||||
// NewComputeBladeAgent creates and initializes a new ComputeBladeAgent, including gRPC server setup and hardware interfaces.
|
||||
func NewComputeBladeAgent(ctx context.Context, config agent.ComputeBladeAgentConfig, agentInfo agent.ComputeBladeAgentInfo) (agent.ComputeBladeAgent, error) {
|
||||
blade, err := hal.NewHal(ctx, config.ComputeBladeHalOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edgeLedEngine := ledengine.NewLedEngine(ledengine.LedEngineOpts{
|
||||
LedIdx: hal.LedEdge,
|
||||
Hal: blade,
|
||||
})
|
||||
fanController, err := fancontroller.NewLinearFanController(config.FanControllerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
topLedEngine := ledengine.NewLedEngine(ledengine.LedEngineOpts{
|
||||
LedIdx: hal.LedTop,
|
||||
Hal: blade,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fanController, err := fancontroller.NewLinearFanController(opts.FanControllerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &computeBladeAgentImpl{
|
||||
opts: opts,
|
||||
a := &computeBladeAgent{
|
||||
config: config,
|
||||
blade: blade,
|
||||
edgeLedEngine: edgeLedEngine,
|
||||
topLedEngine: topLedEngine,
|
||||
edgeLedEngine: ledengine.New(blade, hal.LedEdge),
|
||||
topLedEngine: ledengine.New(blade, hal.LedTop),
|
||||
fanController: fanController,
|
||||
state: NewComputeBladeState(),
|
||||
eventChan: make(
|
||||
chan Event,
|
||||
10,
|
||||
), // backlog of 10 events. They should process fast but we e.g. don't want to miss button presses
|
||||
}, nil
|
||||
state: agent.NewComputeBladeState(),
|
||||
eventChan: make(chan events.Event, 10),
|
||||
agentInfo: agentInfo,
|
||||
}
|
||||
|
||||
if err := a.setupGrpcServer(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bladeapiv1alpha1.RegisterBladeAgentServiceServer(a.server, a)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *computeBladeAgentImpl) Run(origCtx context.Context) error {
|
||||
var wg sync.WaitGroup
|
||||
// RunAsync starts the agent in a separate goroutine and handles errors, allowing cancellation through the provided context.
|
||||
func (a *computeBladeAgent) RunAsync(ctx context.Context, cancel context.CancelCauseFunc) {
|
||||
go func() {
|
||||
log.FromContext(ctx).Info("Starting agent")
|
||||
err := a.Run(ctx)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.FromContext(ctx).WithError(err).Error("Failed to run agent")
|
||||
cancel(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Run initializes and starts the compute blade agent, setting up necessary components and processes, and waits for termination.
|
||||
func (a *computeBladeAgent) Run(origCtx context.Context) error {
|
||||
ctx, cancelCtx := context.WithCancelCause(origCtx)
|
||||
defer a.cleanup(ctx)
|
||||
defer cancelCtx(fmt.Errorf("cancel"))
|
||||
|
||||
log.FromContext(ctx).Info("Starting ComputeBlade agent")
|
||||
|
||||
// Ingest noop event to initialise metrics
|
||||
a.state.RegisterEvent(NoopEvent)
|
||||
a.state.RegisterEvent(events.NoopEvent)
|
||||
|
||||
// Set defaults
|
||||
if err := a.blade.SetStealthMode(a.opts.StealthModeEnabled); err != nil {
|
||||
if err := a.blade.SetStealthMode(a.config.StealthModeEnabled); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run HAL
|
||||
|
||||
go a.runHal(ctx, cancelCtx)
|
||||
|
||||
// Start edge button event handler
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.FromContext(ctx).Info("Starting edge button event handler")
|
||||
for {
|
||||
err := a.blade.WaitForEdgeButtonPress(ctx)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).Error("Edge button event handler failed", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
} else if err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case a.eventChan <- Event(EdgeButtonEvent):
|
||||
default:
|
||||
log.FromContext(ctx).Warn("Edge button press event dropped due to backlog")
|
||||
droppedEventCounter.WithLabelValues(Event(EdgeButtonEvent).String()).Inc()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go a.runEdgeButtonHandler(ctx, cancelCtx)
|
||||
|
||||
// Start top LED engine
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.FromContext(ctx).Info("Starting top LED engine")
|
||||
err := a.runTopLedEngine(ctx)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).Error("Top LED engine failed", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
}()
|
||||
go a.runTopLedEngine(ctx, cancelCtx)
|
||||
|
||||
// Start edge LED engine
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.FromContext(ctx).Info("Starting edge LED engine")
|
||||
err := a.runEdgeLedEngine(ctx)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).Error("Edge LED engine failed", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
}()
|
||||
go a.runEdgeLedEngine(ctx, cancelCtx)
|
||||
|
||||
// Start fan controller
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.FromContext(ctx).Info("Starting fan controller")
|
||||
err := a.runFanController(ctx)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).Error("Fan Controller Failed", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
}()
|
||||
go a.runFanController(ctx, cancelCtx)
|
||||
|
||||
// Start event handler
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.FromContext(ctx).Info("Starting event handler")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case event := <-a.eventChan:
|
||||
err := a.handleEvent(ctx, event)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).Error("Event handler failed", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
go a.runEventHandler(ctx, cancelCtx)
|
||||
|
||||
// Start gRPC API
|
||||
go a.runGRpcApi(ctx, cancelCtx)
|
||||
|
||||
// wait till we're done
|
||||
<-ctx.Done()
|
||||
|
||||
wg.Wait()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// cleanup restores sane defaults before exiting. Ignores canceled context!
|
||||
func (a *computeBladeAgentImpl) cleanup(ctx context.Context) {
|
||||
// GracefulStop gracefully stops the gRPC server, ensuring all in-progress RPCs are completed before shutting down.
|
||||
func (a *computeBladeAgent) GracefulStop(ctx context.Context) error {
|
||||
a.server.GracefulStop()
|
||||
|
||||
log.FromContext(ctx).Info("Exiting, restoring safe settings")
|
||||
if err := a.blade.SetFanSpeed(100); err != nil {
|
||||
log.FromContext(ctx).Error("Failed to set fan speed to 100%", zap.Error(err))
|
||||
log.FromContext(ctx).WithError(err).Error("Failed to set fan speed to 100%")
|
||||
}
|
||||
if err := a.blade.SetLed(hal.LedEdge, hal.LedColor{}); err != nil {
|
||||
log.FromContext(ctx).Error("Failed to set edge LED to off", zap.Error(err))
|
||||
if err := a.blade.SetLed(hal.LedEdge, led.Color{}); err != nil {
|
||||
log.FromContext(ctx).WithError(err).Error("Failed to set edge LED to off")
|
||||
}
|
||||
if err := a.blade.SetLed(hal.LedTop, hal.LedColor{}); err != nil {
|
||||
log.FromContext(ctx).Error("Failed to set edge LED to off", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *computeBladeAgentImpl) handleEvent(ctx context.Context, event Event) error {
|
||||
log.FromContext(ctx).Info("Handling event", zap.String("event", event.String()))
|
||||
eventCounter.WithLabelValues(event.String()).Inc()
|
||||
|
||||
// register event in state
|
||||
a.state.RegisterEvent(event)
|
||||
|
||||
// Dispatch incoming events to the right handler(s)
|
||||
switch event {
|
||||
case CriticalEvent:
|
||||
// Handle critical event
|
||||
return a.handleCriticalActive(ctx)
|
||||
case CriticalResetEvent:
|
||||
// Handle critical event
|
||||
return a.handleCriticalReset(ctx)
|
||||
case IdentifyEvent:
|
||||
// Handle identify event
|
||||
return a.handleIdentifyActive(ctx)
|
||||
case IdentifyConfirmEvent:
|
||||
// Handle identify event
|
||||
return a.handleIdentifyConfirm(ctx)
|
||||
case EdgeButtonEvent:
|
||||
// Handle edge button press to toggle identify mode
|
||||
event := Event(IdentifyEvent)
|
||||
if a.state.IdentifyActive() {
|
||||
event = Event(IdentifyConfirmEvent)
|
||||
}
|
||||
select {
|
||||
case a.eventChan <- Event(event):
|
||||
default:
|
||||
log.FromContext(ctx).Warn("Edge button press event dropped due to backlog")
|
||||
droppedEventCounter.WithLabelValues(event.String()).Inc()
|
||||
}
|
||||
if err := a.blade.SetLed(hal.LedTop, led.Color{}); err != nil {
|
||||
log.FromContext(ctx).WithError(err).Error("Failed to set edge LED to off")
|
||||
}
|
||||
|
||||
return nil
|
||||
return a.blade.Close()
|
||||
}
|
||||
|
||||
func (a *computeBladeAgentImpl) handleIdentifyActive(ctx context.Context) error {
|
||||
log.FromContext(ctx).Info("Identify active")
|
||||
return a.edgeLedEngine.SetPattern(ledengine.NewBurstPattern(hal.LedColor{}, a.opts.IdentifyLedColor))
|
||||
}
|
||||
|
||||
func (a *computeBladeAgentImpl) handleIdentifyConfirm(ctx context.Context) error {
|
||||
log.FromContext(ctx).Info("Identify confirmed/cleared")
|
||||
return a.edgeLedEngine.SetPattern(ledengine.NewStaticPattern(a.opts.IdleLedColor))
|
||||
}
|
||||
|
||||
func (a *computeBladeAgentImpl) handleCriticalActive(ctx context.Context) error {
|
||||
log.FromContext(ctx).Warn("Blade in critical state, setting fan speed to 100% and turning on LEDs")
|
||||
|
||||
// Set fan speed to 100%
|
||||
a.fanController.Override(&fancontroller.FanOverrideOpts{Percent: 100})
|
||||
|
||||
// Disable stealth mode (turn on LEDs)
|
||||
setStealthModeError := a.blade.SetStealthMode(false)
|
||||
|
||||
// Set critical pattern for top LED
|
||||
setPatternTopLedErr := a.topLedEngine.SetPattern(
|
||||
ledengine.NewSlowBlinkPattern(hal.LedColor{}, a.opts.CriticalLedColor),
|
||||
)
|
||||
// Combine errors, but don't stop execution flow for now
|
||||
return errors.Join(setStealthModeError, setPatternTopLedErr)
|
||||
}
|
||||
|
||||
func (a *computeBladeAgentImpl) handleCriticalReset(ctx context.Context) error {
|
||||
log.FromContext(ctx).Info("Critical state cleared, setting fan speed to default and restoring LEDs to default state")
|
||||
// Reset fan controller overrides
|
||||
a.fanController.Override(nil)
|
||||
|
||||
// Reset stealth mode
|
||||
if err := a.blade.SetStealthMode(a.opts.StealthModeEnabled); err != nil {
|
||||
return err
|
||||
// runHal initializes and starts the HAL service within the given context, handling errors and supporting graceful cancellation.
|
||||
func (a *computeBladeAgent) runHal(ctx context.Context, cancel context.CancelCauseFunc) {
|
||||
log.FromContext(ctx).Info("Starting HAL")
|
||||
if err := a.blade.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.FromContext(ctx).WithError(err).Error("HAL failed")
|
||||
cancel(err)
|
||||
}
|
||||
|
||||
// Set top LED off
|
||||
if err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(hal.LedColor{})); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *computeBladeAgentImpl) Close() error {
|
||||
return errors.Join(a.blade.Close())
|
||||
}
|
||||
|
||||
// runTopLedEngine runs the top LED engine
|
||||
func (a *computeBladeAgentImpl) runTopLedEngine(ctx context.Context) error {
|
||||
// FIXME the top LED is only used to indicate emergency situations
|
||||
err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(hal.LedColor{}))
|
||||
if err != nil {
|
||||
return err
|
||||
// FIXME the top LED is only used to indicate emergency situations
|
||||
func (a *computeBladeAgent) runTopLedEngine(ctx context.Context, cancel context.CancelCauseFunc) {
|
||||
log.FromContext(ctx).Info("Starting top LED engine")
|
||||
if err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(led.Color{})); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.FromContext(ctx).WithError(err).Error("Top LED engine failed")
|
||||
cancel(err)
|
||||
}
|
||||
|
||||
if err := a.topLedEngine.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.FromContext(ctx).WithError(err).Error("Top LED engine failed")
|
||||
cancel(err)
|
||||
}
|
||||
return a.topLedEngine.Run(ctx)
|
||||
}
|
||||
|
||||
// runEdgeLedEngine runs the edge LED engine
|
||||
func (a *computeBladeAgentImpl) runEdgeLedEngine(ctx context.Context) error {
|
||||
err := a.edgeLedEngine.SetPattern(ledengine.NewStaticPattern(a.opts.IdleLedColor))
|
||||
if err != nil {
|
||||
return err
|
||||
func (a *computeBladeAgent) runEdgeLedEngine(ctx context.Context, cancel context.CancelCauseFunc) {
|
||||
log.FromContext(ctx).Info("Starting edge LED engine")
|
||||
|
||||
if err := a.edgeLedEngine.SetPattern(ledengine.NewStaticPattern(a.config.IdleLedColor)); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.FromContext(ctx).WithError(err).Error("Edge LED engine failed")
|
||||
cancel(err)
|
||||
}
|
||||
|
||||
if err := a.edgeLedEngine.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.FromContext(ctx).WithError(err).Error("Edge LED engine failed")
|
||||
cancel(err)
|
||||
}
|
||||
return a.edgeLedEngine.Run(ctx)
|
||||
}
|
||||
|
||||
func (a *computeBladeAgentImpl) runFanController(ctx context.Context) error {
|
||||
// runFanController initializes and manages a periodic task to control fan speed based on temperature readings.
|
||||
// The method uses a ticker to execute fan speed adjustments and handles context cancellation for cleanup.
|
||||
// If obtaining temperature or setting fan speed fails, appropriate error logs are recorded.
|
||||
func (a *computeBladeAgent) runFanController(ctx context.Context, cancel context.CancelCauseFunc) {
|
||||
log.FromContext(ctx).Info("Starting fan controller")
|
||||
|
||||
// Update fan speed periodically
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
||||
for {
|
||||
|
||||
// Wait for the next tick
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
return ctx.Err()
|
||||
|
||||
if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.FromContext(ctx).WithError(err).Error("Fan Controller Failed")
|
||||
cancel(err)
|
||||
}
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
|
||||
// Get temperature
|
||||
temp, err := a.blade.GetTemperature()
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error("Failed to get temperature", zap.Error(err))
|
||||
log.FromContext(ctx).WithError(err).Error("Failed to get temperature")
|
||||
temp = 100 // set to a high value to trigger the maximum speed defined by the fan curve
|
||||
}
|
||||
// Derive fan speed from temperature
|
||||
speed := a.fanController.GetFanSpeed(temp)
|
||||
speed := a.fanController.GetFanSpeedPercent(temp)
|
||||
// Set fan speed
|
||||
if err := a.blade.SetFanSpeed(speed); err != nil {
|
||||
log.FromContext(ctx).Error("Failed to set fan speed", zap.Error(err))
|
||||
log.FromContext(ctx).WithError(err).Error("Failed to set fan speed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EmitEvent dispatches an event to the event handler
|
||||
func (a *computeBladeAgentImpl) EmitEvent(ctx context.Context, event Event) error {
|
||||
select {
|
||||
case a.eventChan <- event:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
// runEdgeButtonHandler initializes and handles edge button press events in a loop until the context is canceled.
|
||||
// It waits for edge button presses and sends corresponding events to the event channel, logging errors and warnings.
|
||||
// If an unrecoverable error occurs, the cancel function is triggered to terminate the operation.
|
||||
func (a *computeBladeAgent) runEdgeButtonHandler(ctx context.Context, cancel context.CancelCauseFunc) {
|
||||
log.FromContext(ctx).Info("Starting edge button event handler")
|
||||
for {
|
||||
if err := a.blade.WaitForEdgeButtonPress(ctx); err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
log.FromContext(ctx).WithError(err).Error("Edge button event handler failed")
|
||||
cancel(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case a.eventChan <- events.Event(events.EdgeButtonEvent):
|
||||
default:
|
||||
log.FromContext(ctx).Warn("Edge button press event dropped due to backlog")
|
||||
droppedEventCounter.WithLabelValues(events.Event(events.EdgeButtonEvent).String()).Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetFanSpeed sets the fan speed
|
||||
func (a *computeBladeAgentImpl) SetFanSpeed(_ context.Context, speed uint8) error {
|
||||
if a.state.CriticalActive() {
|
||||
return errors.New("cannot set fan speed while the blade is in a critical state")
|
||||
// runEventHandler processes events from the agent's event channel, handles them, and cancels on critical failure or context cancellation.
|
||||
func (a *computeBladeAgent) runEventHandler(ctx context.Context, cancel context.CancelCauseFunc) {
|
||||
log.FromContext(ctx).Info("Starting event handler")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case event := <-a.eventChan:
|
||||
err := a.handleEvent(ctx, event)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.FromContext(ctx).WithError(err).Error("Event handler failed")
|
||||
cancel(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
a.fanController.Override(&fancontroller.FanOverrideOpts{Percent: speed})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetStealthMode enables/disables the stealth mode
|
||||
func (a *computeBladeAgentImpl) SetStealthMode(_ context.Context, enabled bool) error {
|
||||
if a.state.CriticalActive() {
|
||||
return errors.New("cannot set stealth mode while the blade is in a critical state")
|
||||
// runGRpcApi starts the gRPC server for the agent based on the configuration and gracefully handles errors or cancellation.
|
||||
func (a *computeBladeAgent) runGRpcApi(ctx context.Context, cancel context.CancelCauseFunc) {
|
||||
if len(a.config.Listen.Grpc) == 0 {
|
||||
err := humane.New("no listen address provided",
|
||||
"ensure you are passing a valid listen config to the grpc server",
|
||||
)
|
||||
log.FromContext(ctx).Error("no listen address provided, not starting gRPC server", humane.Zap(err)...)
|
||||
cancel(err)
|
||||
}
|
||||
return a.blade.SetStealthMode(enabled)
|
||||
}
|
||||
|
||||
// WaitForIdentifyConfirm waits for the identify confirm event
|
||||
func (a *computeBladeAgentImpl) WaitForIdentifyConfirm(ctx context.Context) error {
|
||||
return a.state.WaitForIdentifyConfirm(ctx)
|
||||
grpcListen, err := net.Listen(a.config.Listen.GrpcListenMode, a.config.Listen.Grpc)
|
||||
if err != nil {
|
||||
err := humane.Wrap(err, "failed to create grpc listener",
|
||||
"ensure the gRPC server you are trying to serve to is not already running and the address is not bound by another process",
|
||||
)
|
||||
log.FromContext(ctx).Error("failed to create grpc listener, not starting gRPC server", humane.Zap(err)...)
|
||||
cancel(err)
|
||||
}
|
||||
|
||||
log.FromContext(ctx).Info("Starting grpc server", zap.String("address", a.config.Listen.Grpc))
|
||||
if err := a.server.Serve(grpcListen); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
|
||||
log.FromContext(ctx).Error("failed to start grpc server", humane.Zap(err)...)
|
||||
cancel(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +1,156 @@
|
||||
package agent
|
||||
package internal_agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
bladeapiv1alpha1 "github.com/xvzf/computeblade-agent/api/bladeapi/v1alpha1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
grpczap "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
"github.com/spechtlabs/go-otel-utils/otelzap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// ComputeBladeAgent implementing the BladeAgentServiceServer
|
||||
type agentGrpcService struct {
|
||||
bladeapiv1alpha1.UnimplementedBladeAgentServiceServer
|
||||
// setupGrpcServer initializes and configures the gRPC server with authentication, logging, and server options.
|
||||
func (a *computeBladeAgent) setupGrpcServer(ctx context.Context) error {
|
||||
listenMode, err := ListenModeFromString(a.config.Listen.GrpcListenMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Agent ComputeBladeAgent
|
||||
var grpcOpts []grpc.ServerOption
|
||||
|
||||
if listenMode == ModeTcp && a.config.Listen.GrpcAuthenticated {
|
||||
tlsCfg, err := createServerTLSConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grpcOpts = append(grpcOpts, grpc.Creds(credentials.NewTLS(tlsCfg)))
|
||||
|
||||
if err := EnsureAuthenticatedBladectlConfig(ctx, a.config.Listen.Grpc, listenMode); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := EnsureUnauthenticatedBladectlConfig(ctx, a.config.Listen.Grpc, listenMode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logger := log.InterceptorLogger(otelzap.L())
|
||||
grpcOpts = append(grpcOpts,
|
||||
grpc.ChainUnaryInterceptor(grpczap.UnaryServerInterceptor(logger)),
|
||||
grpc.ChainStreamInterceptor(grpczap.StreamServerInterceptor(logger)),
|
||||
)
|
||||
|
||||
a.server = grpc.NewServer(grpcOpts...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewGrpcServiceFor creates a new gRPC service for a given agent
|
||||
func NewGrpcServiceFor(agent ComputeBladeAgent) *agentGrpcService {
|
||||
return &agentGrpcService{
|
||||
Agent: agent,
|
||||
// createServerTLSConfig creates and returns a TLS configuration for a server, enforcing client authentication.
|
||||
// It generates or loads the necessary certificates and certificate pools, logging fatal errors if certificate loading fails.
|
||||
func createServerTLSConfig(ctx context.Context) (*tls.Config, error) {
|
||||
cert, certPool, err := EnsureServerCertificate(ctx)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).WithError(err).Fatal("failed to load server key pair")
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
ClientCAs: certPool,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EmitEvent dispatches an event to the event handler
|
||||
func (a *computeBladeAgent) EmitEvent(ctx context.Context, req *bladeapiv1alpha1.EmitEventRequest) (*emptypb.Empty, error) {
|
||||
event, err := fromProto(req.GetEvent())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
select {
|
||||
case a.eventChan <- event:
|
||||
return &emptypb.Empty{}, nil
|
||||
case <-ctx.Done():
|
||||
return &emptypb.Empty{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// EmitEvent emits an event to the agent runtime
|
||||
func (service *agentGrpcService) EmitEvent(
|
||||
ctx context.Context,
|
||||
req *bladeapiv1alpha1.EmitEventRequest,
|
||||
) (*emptypb.Empty, error) {
|
||||
switch req.GetEvent() {
|
||||
case bladeapiv1alpha1.Event_IDENTIFY:
|
||||
return &emptypb.Empty{}, service.Agent.EmitEvent(ctx, IdentifyEvent)
|
||||
case bladeapiv1alpha1.Event_IDENTIFY_CONFIRM:
|
||||
return &emptypb.Empty{}, service.Agent.EmitEvent(ctx, IdentifyConfirmEvent)
|
||||
case bladeapiv1alpha1.Event_CRITICAL:
|
||||
return &emptypb.Empty{}, service.Agent.EmitEvent(ctx, CriticalEvent)
|
||||
case bladeapiv1alpha1.Event_CRITICAL_RESET:
|
||||
return &emptypb.Empty{}, service.Agent.EmitEvent(ctx, CriticalResetEvent)
|
||||
default:
|
||||
return &emptypb.Empty{}, status.Errorf(codes.InvalidArgument, "invalid event type")
|
||||
// SetFanSpeed sets the fan speed
|
||||
func (a *computeBladeAgent) SetFanSpeed(_ context.Context, req *bladeapiv1alpha1.SetFanSpeedRequest) (*emptypb.Empty, error) {
|
||||
if a.state.CriticalActive() {
|
||||
return &emptypb.Empty{}, humane.New("cannot set fan speed while the blade is in a critical state", "improve cooling on your blade before attempting to overwrite the fan speed")
|
||||
}
|
||||
|
||||
a.fanController.Override(&fancontroller.FanOverrideOpts{Percent: uint8(req.GetPercent())})
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (service *agentGrpcService) WaitForIdentifyConfirm(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return &emptypb.Empty{}, service.Agent.WaitForIdentifyConfirm(ctx)
|
||||
// SetFanSpeedAuto sets the fan speed to automatic mode
|
||||
func (a *computeBladeAgent) SetFanSpeedAuto(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
a.fanController.Override(nil)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// SetFanSpeed sets the fan speed of the blade
|
||||
func (service *agentGrpcService) SetFanSpeed(
|
||||
ctx context.Context,
|
||||
req *bladeapiv1alpha1.SetFanSpeedRequest,
|
||||
) (*emptypb.Empty, error) {
|
||||
return &emptypb.Empty{}, service.Agent.SetFanSpeed(ctx, uint8(req.GetPercent()))
|
||||
}
|
||||
|
||||
// SetStealthMode enables/disables stealth mode on the blade
|
||||
func (service *agentGrpcService) SetStealthMode(ctx context.Context, req *bladeapiv1alpha1.StealthModeRequest) (*emptypb.Empty, error) {
|
||||
return &emptypb.Empty{}, service.Agent.SetStealthMode(ctx, req.GetEnable())
|
||||
// SetStealthMode enables/disables the stealth mode
|
||||
func (a *computeBladeAgent) SetStealthMode(_ context.Context, req *bladeapiv1alpha1.StealthModeRequest) (*emptypb.Empty, error) {
|
||||
if a.state.CriticalActive() {
|
||||
return &emptypb.Empty{}, humane.New("cannot set stealth mode while the blade is in a critical state", "improve cooling on your blade before attempting to enable stealth mode again")
|
||||
}
|
||||
return &emptypb.Empty{}, a.blade.SetStealthMode(req.GetEnable())
|
||||
}
|
||||
|
||||
// GetStatus aggregates the status of the blade
|
||||
func (service *agentGrpcService) GetStatus(context.Context, *emptypb.Empty) (*bladeapiv1alpha1.StatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetStatus not implemented")
|
||||
func (a *computeBladeAgent) GetStatus(_ context.Context, _ *emptypb.Empty) (*bladeapiv1alpha1.StatusResponse, error) {
|
||||
rpm, err := a.blade.GetFanRPM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
temp, err := a.blade.GetTemperature()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
powerStatus, err := a.blade.GetPowerStatus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
steps := a.fanController.Steps()
|
||||
fanCurveSteps := make([]*bladeapiv1alpha1.FanCurveStep, len(steps))
|
||||
for idx, step := range steps {
|
||||
fanCurveSteps[idx] = &bladeapiv1alpha1.FanCurveStep{
|
||||
Temperature: int64(step.Temperature),
|
||||
Percent: uint32(step.Percent),
|
||||
}
|
||||
}
|
||||
|
||||
versionInfo := &bladeapiv1alpha1.VersionInfo{
|
||||
Version: a.agentInfo.Version,
|
||||
Commit: a.agentInfo.Commit,
|
||||
Date: a.agentInfo.BuildTime.Unix(),
|
||||
}
|
||||
|
||||
return &bladeapiv1alpha1.StatusResponse{
|
||||
StealthMode: a.blade.StealthModeActive(),
|
||||
IdentifyActive: a.state.IdentifyActive(),
|
||||
CriticalActive: a.state.CriticalActive(),
|
||||
Temperature: int64(temp),
|
||||
FanRpm: int64(rpm),
|
||||
FanPercent: uint32(a.fanController.GetFanSpeedPercent(temp)),
|
||||
FanSpeedAutomatic: a.fanController.IsAutomaticSpeed(),
|
||||
PowerStatus: bladeapiv1alpha1.PowerStatus(powerStatus),
|
||||
FanCurveSteps: fanCurveSteps,
|
||||
CriticalTemperatureThreshold: int64(a.config.CriticalTemperatureThreshold),
|
||||
Version: versionInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WaitForIdentifyConfirm blocks until the identify confirmation process is completed or an error occurs.
|
||||
func (a *computeBladeAgent) WaitForIdentifyConfirm(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return &emptypb.Empty{}, a.state.WaitForIdentifyConfirm(ctx)
|
||||
}
|
||||
|
||||
348
internal/agent/api_certificates.go
Normal file
348
internal/agent/api_certificates.go
Normal file
@@ -0,0 +1,348 @@
|
||||
package internal_agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/cmd/bladectl/config"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/certificate"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const certDir = "/etc/compute-blade-agent"
|
||||
|
||||
var (
|
||||
caPath = filepath.Join(certDir, "ca.pem")
|
||||
caKeyPath = filepath.Join(certDir, "ca-key.pem")
|
||||
serverCertPath = filepath.Join(certDir, "server.pem")
|
||||
serverKeyPath = filepath.Join(certDir, "server-key.pem")
|
||||
)
|
||||
|
||||
// GenerateClientCert creates a client certificate signed by a CA with the specified common name.
|
||||
// It validates the CA certificate and private key before generating the client certificate.
|
||||
// Returns CA certificate, client certificate, private key in PEM format, and any error encountered.
|
||||
func GenerateClientCert(commonName string) (caPEM, certPEM, keyPEM []byte, herr humane.Error) {
|
||||
caCert, caKey, herr := certificate.LoadAndValidateCertificate(caPath, caKeyPath)
|
||||
if herr != nil {
|
||||
return nil, nil, nil, humane.Wrap(herr, "No valid CA found to sign the client certificate")
|
||||
}
|
||||
|
||||
certDER, keyDER, herr := certificate.GenerateCertificate(
|
||||
commonName,
|
||||
certificate.WithClientUsage(),
|
||||
certificate.WithCaCert(caCert),
|
||||
certificate.WithCaKey(caKey),
|
||||
)
|
||||
if herr != nil {
|
||||
return nil, nil, nil, humane.Wrap(herr, "failed to generate client certificate")
|
||||
}
|
||||
|
||||
// Load CA PEM
|
||||
caPEM, err := os.ReadFile(caPath)
|
||||
if err != nil {
|
||||
return nil, nil, nil, humane.Wrap(err, "failed to read CA",
|
||||
fmt.Sprintf("ensure the certificate file %s exists and is readable by the agent user", caPath),
|
||||
)
|
||||
}
|
||||
|
||||
// Convert DER to PEM
|
||||
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
|
||||
|
||||
return caPEM, certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
func EnsureAuthenticatedBladectlConfig(ctx context.Context, serverAddr string, serverMode ListenMode) humane.Error {
|
||||
configDir, herr := config.EnsureBladectlConfigHome()
|
||||
if herr != nil {
|
||||
return herr
|
||||
}
|
||||
|
||||
configPath := filepath.Join(configDir, "config.yaml")
|
||||
|
||||
if util.FileExists(configPath) {
|
||||
// Load and decode bladectl config
|
||||
configBytes, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return humane.Wrap(err, "failed to read bladectl config",
|
||||
fmt.Sprintf("ensure the config file %s exists and is readable by the agent user", configPath),
|
||||
)
|
||||
}
|
||||
|
||||
var bladectlConfig config.BladectlConfig
|
||||
if err := yaml.Unmarshal(configBytes, &bladectlConfig); err != nil {
|
||||
return humane.Wrap(err, "failed to parse bladectl config",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
"ensure your config file is valid YAML",
|
||||
)
|
||||
}
|
||||
|
||||
blade, herr := bladectlConfig.FindBlade("")
|
||||
if herr != nil {
|
||||
return herr
|
||||
}
|
||||
|
||||
certPEM, err := base64.StdEncoding.DecodeString(blade.Blade.Certificate.ClientCertificateData)
|
||||
if err != nil {
|
||||
return humane.Wrap(err, fmt.Sprintf("failed to decode client certificate data for blade %s", blade.Name),
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
"ensure your config file is valid YAML",
|
||||
)
|
||||
}
|
||||
|
||||
keyPEM, err := base64.StdEncoding.DecodeString(blade.Blade.Certificate.ClientKeyData)
|
||||
if err != nil {
|
||||
return humane.Wrap(err, fmt.Sprintf("failed to decode client certificate key data for blade %s", blade.Name),
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
"ensure your config file is valid YAML",
|
||||
)
|
||||
}
|
||||
|
||||
if _, _, err := certificate.ValidateCertificate(certPEM, keyPEM); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate localhost keys
|
||||
log.FromContext(ctx).Debug("Generating new local client certificate...")
|
||||
|
||||
caPEM, clientCertDER, clientKeyDER, herr := GenerateClientCert("localhost")
|
||||
if herr != nil {
|
||||
return herr
|
||||
}
|
||||
|
||||
if serverMode == ModeTcp {
|
||||
_, grpcApiPort, err := net.SplitHostPort(serverAddr)
|
||||
if err != nil {
|
||||
return humane.Wrap(err, "failed to extract port from gRPC address",
|
||||
"check your gRPC address is correct in your agent config",
|
||||
)
|
||||
}
|
||||
|
||||
serverAddr = fmt.Sprintf("localhost:%s", grpcApiPort)
|
||||
}
|
||||
|
||||
bladectlConfig := config.NewAuthenticatedBladectlConfig(serverAddr, caPEM, clientCertDER, clientKeyDER)
|
||||
data, err := yaml.Marshal(&bladectlConfig)
|
||||
if err != nil {
|
||||
return humane.Wrap(err, "Failed to marshal YAML config",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||||
return humane.Wrap(err, "Failed to write bladectl config file",
|
||||
"ensure the home-directory is writable by the agent user",
|
||||
)
|
||||
}
|
||||
|
||||
log.FromContext(ctx).Info("Generated new local bladectl config",
|
||||
zap.String("path", configPath),
|
||||
zap.String("server", serverAddr),
|
||||
zap.Bool("authenticated", true),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureUnauthenticatedBladectlConfig(ctx context.Context, serverAddr string, serverMode ListenMode) humane.Error {
|
||||
configDir, herr := config.EnsureBladectlConfigHome()
|
||||
if herr != nil {
|
||||
return herr
|
||||
}
|
||||
|
||||
configPath := filepath.Join(configDir, "config.yaml")
|
||||
if util.FileExists(configPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate localhost keys
|
||||
log.FromContext(ctx).Debug("Generating new local bladectl config...")
|
||||
|
||||
if serverMode == ModeTcp {
|
||||
_, grpcApiPort, err := net.SplitHostPort(serverAddr)
|
||||
if err != nil {
|
||||
return humane.Wrap(err, "failed to extract port from gRPC address",
|
||||
"check your gRPC address is correct in your agent config",
|
||||
)
|
||||
}
|
||||
|
||||
serverAddr = fmt.Sprintf("localhost:%s", grpcApiPort)
|
||||
}
|
||||
|
||||
bladectlConfig := config.NewBladectlConfig(serverAddr)
|
||||
data, err := yaml.Marshal(&bladectlConfig)
|
||||
if err != nil {
|
||||
return humane.Wrap(err, "Failed to marshal YAML config",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||||
return humane.Wrap(err, "Failed to write bladectl config file",
|
||||
"ensure the home-directory is writable by the agent user",
|
||||
)
|
||||
}
|
||||
|
||||
log.FromContext(ctx).Info("Generated new local bladectl config",
|
||||
zap.String("path", configPath),
|
||||
zap.String("server", serverAddr),
|
||||
zap.Bool("authenticated", false),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureServerCertificate ensures the presence of a valid server certificate and CA, generating them if necessary.
|
||||
func EnsureServerCertificate(ctx context.Context) (tls.Certificate, *x509.CertPool, humane.Error) {
|
||||
// If Keys already exist, there is nothing to do :)
|
||||
if util.FileExists(caPath) && util.FileExists(caKeyPath) && util.FileExists(serverCertPath) && util.FileExists(serverKeyPath) {
|
||||
if _, _, err := certificate.LoadAndValidateCertificate(caPath, caKeyPath); err != nil {
|
||||
return tls.Certificate{}, nil, err
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, humane.Wrap(err, "failed to load existing server cert",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
pool, herr := certificate.GetCertPoolFrom(caPath)
|
||||
if herr != nil {
|
||||
return tls.Certificate{}, nil, herr
|
||||
}
|
||||
|
||||
return cert, pool, nil
|
||||
}
|
||||
|
||||
// We need a CA
|
||||
if err := ensureCA(ctx); err != nil {
|
||||
return tls.Certificate{}, nil, err
|
||||
}
|
||||
|
||||
// But more importantly: a valid CA
|
||||
caCert, caKey, herr := certificate.LoadAndValidateCertificate(caPath, caKeyPath)
|
||||
if herr != nil {
|
||||
return tls.Certificate{}, nil, herr
|
||||
}
|
||||
|
||||
// Generate Server Keys
|
||||
log.FromContext(ctx).Debug("Generating new server certificate...")
|
||||
serverCertDER, serverKeyDER, herr := certificate.GenerateCertificate(
|
||||
"Compute Blade Agent",
|
||||
certificate.WithServerUsage(),
|
||||
certificate.WithCaCert(caCert),
|
||||
certificate.WithCaKey(caKey),
|
||||
)
|
||||
if herr != nil {
|
||||
return tls.Certificate{}, nil, herr
|
||||
}
|
||||
|
||||
if err := certificate.WriteCertificate(serverCertPath, serverKeyPath, serverCertDER, serverKeyDER); err != nil {
|
||||
return tls.Certificate{}, nil, err
|
||||
}
|
||||
|
||||
log.FromContext(ctx).Info("Generated new server certificates",
|
||||
zap.String("cert", serverCertPath),
|
||||
zap.String("key", serverKeyPath),
|
||||
zap.String("ca", caPath),
|
||||
)
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, humane.Wrap(err, "failed to parse generated server certificate",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
pool, herr := certificate.GetCertPoolFrom(caPath)
|
||||
if herr != nil {
|
||||
return tls.Certificate{}, nil, herr
|
||||
}
|
||||
|
||||
return cert, pool, nil
|
||||
}
|
||||
|
||||
// ensureCA ensures that a valid Certificate Authority (CA) certificate and private key exist or generates new ones.
|
||||
func ensureCA(ctx context.Context) humane.Error {
|
||||
if util.FileExists(caPath) && util.FileExists(caKeyPath) {
|
||||
_, _, err := certificate.LoadAndValidateCertificate(caPath, caKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
log.FromContext(ctx).Info("Generating new CA for compute-blade-agent")
|
||||
|
||||
caKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
if err != nil {
|
||||
return humane.Wrap(err, "failed to generate CA key")
|
||||
}
|
||||
|
||||
caTemplate := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{Organization: []string{"Compute Blade CA"}, CommonName: "Compute Blade Agent Root CA"},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
|
||||
IsCA: true,
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
|
||||
if err != nil {
|
||||
return humane.Wrap(err, "failed to create CA certificate",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
caKeyBytes, err := x509.MarshalECPrivateKey(caKey)
|
||||
if err != nil {
|
||||
return humane.Wrap(err, "failed to marshal CA private key",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(certDir, 0600); err != nil {
|
||||
return humane.Wrap(err, "failed to create cert directory",
|
||||
"ensure the directory you are trying to create exists and is writable by the agent user",
|
||||
)
|
||||
}
|
||||
|
||||
if err := certificate.WriteCertificate(caPath, caKeyPath, caCertDER, caKeyBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
104
internal/agent/handler.go
Normal file
104
internal/agent/handler.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package internal_agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/ledengine"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// handleEvent processes an incoming event, updates state, and dispatches it to the appropriate handler based on the event type.
|
||||
func (a *computeBladeAgent) handleEvent(ctx context.Context, event events.Event) error {
|
||||
log.FromContext(ctx).Info("Handling event", zap.String("event", event.String()))
|
||||
eventCounter.WithLabelValues(event.String()).Inc()
|
||||
|
||||
// register event in state
|
||||
a.state.RegisterEvent(event)
|
||||
|
||||
// Dispatch incoming events to the right handler(s)
|
||||
switch event {
|
||||
case events.CriticalEvent:
|
||||
// Handle critical event
|
||||
return a.handleCriticalActive(ctx)
|
||||
case events.CriticalResetEvent:
|
||||
// Handle critical event
|
||||
return a.handleCriticalReset(ctx)
|
||||
case events.IdentifyEvent:
|
||||
// Handle identify event
|
||||
return a.handleIdentifyActive(ctx)
|
||||
case events.IdentifyConfirmEvent:
|
||||
// Handle identify event
|
||||
return a.handleIdentifyConfirm(ctx)
|
||||
case events.EdgeButtonEvent:
|
||||
// Handle edge button press to toggle identify mode
|
||||
event := events.Event(events.IdentifyEvent)
|
||||
if a.state.IdentifyActive() {
|
||||
event = events.Event(events.IdentifyConfirmEvent)
|
||||
}
|
||||
select {
|
||||
case a.eventChan <- event:
|
||||
default:
|
||||
log.FromContext(ctx).Warn("Edge button press event dropped due to backlog")
|
||||
droppedEventCounter.WithLabelValues(event.String()).Inc()
|
||||
}
|
||||
case events.NoopEvent:
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleIdentifyActive is responsible for handling the identify event by setting a burst LED pattern based on the configuration.
|
||||
func (a *computeBladeAgent) handleIdentifyActive(ctx context.Context) error {
|
||||
log.FromContext(ctx).Info("Identify active")
|
||||
return a.edgeLedEngine.SetPattern(ledengine.NewBurstPattern(led.Color{}, a.config.IdentifyLedColor))
|
||||
}
|
||||
|
||||
// handleIdentifyConfirm handles the confirmation of an identify event by updating the LED engine with a static idle pattern.
|
||||
func (a *computeBladeAgent) handleIdentifyConfirm(ctx context.Context) error {
|
||||
log.FromContext(ctx).Info("Identify confirmed/cleared")
|
||||
return a.edgeLedEngine.SetPattern(ledengine.NewStaticPattern(a.config.IdleLedColor))
|
||||
}
|
||||
|
||||
// handleCriticalActive handles the system's response to a critical state by adjusting fan speed and LED indications.
|
||||
// It sets the fan speed to 100%, disables stealth mode, and applies a critical LED pattern.
|
||||
// Returns any errors encountered during the process as a combined error.
|
||||
func (a *computeBladeAgent) handleCriticalActive(ctx context.Context) error {
|
||||
log.FromContext(ctx).Warn("Blade in critical state, setting fan speed to 100% and turning on LEDs")
|
||||
|
||||
// Set fan speed to 100%
|
||||
a.fanController.Override(&fancontroller.FanOverrideOpts{Percent: 100})
|
||||
|
||||
// Disable stealth mode (turn on LEDs)
|
||||
setStealthModeError := a.blade.SetStealthMode(false)
|
||||
|
||||
// Set critical pattern for top LED
|
||||
setPatternTopLedErr := a.topLedEngine.SetPattern(
|
||||
ledengine.NewSlowBlinkPattern(led.Color{}, a.config.CriticalLedColor),
|
||||
)
|
||||
// Combine errors, but don't stop execution flow for now
|
||||
return errors.Join(setStealthModeError, setPatternTopLedErr)
|
||||
}
|
||||
|
||||
// handleCriticalReset handles the reset of a critical state by restoring default hardware settings for fans and LEDs.
|
||||
func (a *computeBladeAgent) handleCriticalReset(ctx context.Context) error {
|
||||
log.FromContext(ctx).Info("Critical state cleared, setting fan speed to default and restoring LEDs to default state")
|
||||
// Reset fan controller overrides
|
||||
a.fanController.Override(nil)
|
||||
|
||||
// Reset stealth mode
|
||||
if err := a.blade.SetStealthMode(a.config.StealthModeEnabled); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set top LED off
|
||||
if err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(led.Color{})); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
30
internal/agent/options.go
Normal file
30
internal/agent/options.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package internal_agent
|
||||
|
||||
import (
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
)
|
||||
|
||||
type ListenMode string
|
||||
|
||||
const (
|
||||
ModeTcp ListenMode = "tcp"
|
||||
ModeUnix ListenMode = "unix"
|
||||
)
|
||||
|
||||
func ListenModeFromString(s string) (ListenMode, humane.Error) {
|
||||
switch s {
|
||||
case string(ModeTcp):
|
||||
return ModeTcp, nil
|
||||
case string(ModeUnix):
|
||||
return ModeUnix, nil
|
||||
default:
|
||||
return "", humane.New("invalid listen mode",
|
||||
"ensure you are passing a valid listen mode to the grpc server",
|
||||
"valid modes are: [tcp, unix]",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (l ListenMode) String() string {
|
||||
return string(l)
|
||||
}
|
||||
25
internal/agent/utils.go
Normal file
25
internal/agent/utils.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package internal_agent
|
||||
|
||||
import (
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// fromProto converts a `bladeapiv1alpha1.Event` into a corresponding `events.Event` type.
|
||||
// Returns an error if the event type is invalid.
|
||||
func fromProto(event bladeapiv1alpha1.Event) (events.Event, error) {
|
||||
switch event {
|
||||
case bladeapiv1alpha1.Event_IDENTIFY:
|
||||
return events.IdentifyEvent, nil
|
||||
case bladeapiv1alpha1.Event_IDENTIFY_CONFIRM:
|
||||
return events.IdentifyConfirmEvent, nil
|
||||
case bladeapiv1alpha1.Event_CRITICAL:
|
||||
return events.CriticalEvent, nil
|
||||
case bladeapiv1alpha1.Event_CRITICAL_RESET:
|
||||
return events.CriticalResetEvent, nil
|
||||
default:
|
||||
return events.NoopEvent, status.Errorf(codes.InvalidArgument, "invalid event type")
|
||||
}
|
||||
}
|
||||
19
pkg/agent/agent.go
Normal file
19
pkg/agent/agent.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
)
|
||||
|
||||
// ComputeBladeAgent implements the core-logic of the agent. It is responsible for handling events and interfacing with the hardware.
|
||||
// any ComputeBladeAgent must also be a bladeapiv1alpha1.BladeAgentServiceServer to handle the gRPC API requests.
|
||||
type ComputeBladeAgent interface {
|
||||
bladeapiv1alpha1.BladeAgentServiceServer
|
||||
// RunAsync dispatches the agent until the context is canceled or an error occurs
|
||||
RunAsync(ctx context.Context, cancel context.CancelCauseFunc)
|
||||
// Run dispatches the agent and blocks until the context is canceled or an error occurs
|
||||
Run(ctx context.Context) error
|
||||
// GracefulStop gracefully stops the gRPC server, ensuring all in-progress RPCs are completed before shutting down.
|
||||
GracefulStop(ctx context.Context) error
|
||||
}
|
||||
62
pkg/agent/config.go
Normal file
62
pkg/agent/config.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
)
|
||||
|
||||
type LogConfiguration struct {
|
||||
Mode string `mapstructure:"mode"`
|
||||
}
|
||||
|
||||
type ApiConfig struct {
|
||||
Metrics string `mapstructure:"metrics"`
|
||||
Grpc string `mapstructure:"grpc"`
|
||||
GrpcAuthenticated bool `mapstructure:"authenticated"`
|
||||
GrpcListenMode string `mapstructure:"mode"`
|
||||
}
|
||||
|
||||
type ComputeBladeAgentConfig struct {
|
||||
// Log is the logging configuration
|
||||
Log LogConfiguration `mapstructure:"log"`
|
||||
|
||||
// Listen is the listen configuration for the server
|
||||
Listen ApiConfig `mapstructure:"listen"`
|
||||
|
||||
// Hal is the hardware abstraction layer configuration
|
||||
Hal hal.Config `mapstructure:"hal"`
|
||||
|
||||
// IdleLedColor is the color of the edge LED when the blade is idle mode
|
||||
IdleLedColor led.Color `mapstructure:"idle_led_color"`
|
||||
|
||||
// IdentifyLedColor is the color of the edge LED when the blade is in identify mode
|
||||
IdentifyLedColor led.Color `mapstructure:"identify_led_color"`
|
||||
|
||||
// CriticalLedColor is the color of the top(!) LED when the blade is in critical mode.
|
||||
// In the circumstance when >1 blades are in critical mode, the identify function can be used to find the right blade
|
||||
CriticalLedColor led.Color `mapstructure:"critical_led_color"`
|
||||
|
||||
// StealthModeEnabled indicates whether stealth mode is enabled
|
||||
StealthModeEnabled bool `mapstructure:"stealth_mode"`
|
||||
|
||||
// Critical temperature of the compute blade (used to trigger critical mode)
|
||||
CriticalTemperatureThreshold uint `mapstructure:"critical_temperature_threshold"`
|
||||
|
||||
// FanSpeed allows to set a fixed fan speed (in percent)
|
||||
FanSpeed *fancontroller.FanOverrideOpts `mapstructure:"fan_speed"`
|
||||
|
||||
// FanControllerConfig is the configuration of the fan controller
|
||||
FanControllerConfig fancontroller.Config `mapstructure:"fan_controller"`
|
||||
|
||||
ComputeBladeHalOpts hal.ComputeBladeHalOpts `mapstructure:"hal"`
|
||||
}
|
||||
|
||||
// ComputeBladeAgentInfo represents metadata information about a compute blade agent, including version, commit, and build time.
|
||||
type ComputeBladeAgentInfo struct {
|
||||
Version string
|
||||
Commit string
|
||||
BuildTime time.Time
|
||||
}
|
||||
@@ -4,8 +4,11 @@ import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/spechtlabs/go-otel-utils/otelzap"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -17,7 +20,7 @@ var (
|
||||
)
|
||||
|
||||
type ComputebladeState interface {
|
||||
RegisterEvent(event Event)
|
||||
RegisterEvent(event events.Event)
|
||||
IdentifyActive() bool
|
||||
WaitForIdentifyConfirm(ctx context.Context) error
|
||||
CriticalActive() bool
|
||||
@@ -28,10 +31,10 @@ type computebladeStateImpl struct {
|
||||
mutex sync.Mutex
|
||||
|
||||
// identifyActive indicates whether the blade is currently in identify mode
|
||||
identifyActive bool
|
||||
identifyActive bool
|
||||
identifyConfirmChan chan struct{}
|
||||
// criticalActive indicates whether the blade is currently in critical mode
|
||||
criticalActive bool
|
||||
criticalActive bool
|
||||
criticalConfirmChan chan struct{}
|
||||
}
|
||||
|
||||
@@ -42,23 +45,26 @@ func NewComputeBladeState() ComputebladeState {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *computebladeStateImpl) RegisterEvent(event Event) {
|
||||
func (s *computebladeStateImpl) RegisterEvent(event events.Event) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
switch event {
|
||||
case IdentifyEvent:
|
||||
case events.IdentifyEvent:
|
||||
s.identifyActive = true
|
||||
case IdentifyConfirmEvent:
|
||||
case events.IdentifyConfirmEvent:
|
||||
s.identifyActive = false
|
||||
close(s.identifyConfirmChan)
|
||||
s.identifyConfirmChan = make(chan struct{})
|
||||
case CriticalEvent:
|
||||
case events.CriticalEvent:
|
||||
s.criticalActive = true
|
||||
s.identifyActive = false
|
||||
case CriticalResetEvent:
|
||||
case events.CriticalResetEvent:
|
||||
s.criticalActive = false
|
||||
close(s.criticalConfirmChan)
|
||||
s.criticalConfirmChan = make(chan struct{})
|
||||
|
||||
default:
|
||||
otelzap.L().Warn("Unknown event", zap.String("event", event.String()))
|
||||
}
|
||||
|
||||
// Set identify state metric
|
||||
@@ -6,8 +6,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/agent"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xvzf/computeblade-agent/internal/agent"
|
||||
)
|
||||
|
||||
func TestNewComputeBladeState(t *testing.T) {
|
||||
@@ -23,9 +24,9 @@ func TestComputeBladeState_RegisterEventIdentify(t *testing.T) {
|
||||
state := agent.NewComputeBladeState()
|
||||
|
||||
// Identify event
|
||||
state.RegisterEvent(agent.IdentifyEvent)
|
||||
state.RegisterEvent(events.IdentifyEvent)
|
||||
assert.True(t, state.IdentifyActive())
|
||||
state.RegisterEvent(agent.IdentifyConfirmEvent)
|
||||
state.RegisterEvent(events.IdentifyConfirmEvent)
|
||||
assert.False(t, state.IdentifyActive())
|
||||
}
|
||||
|
||||
@@ -35,9 +36,9 @@ func TestComputeBladeState_RegisterEventCritical(t *testing.T) {
|
||||
state := agent.NewComputeBladeState()
|
||||
|
||||
// critical event
|
||||
state.RegisterEvent(agent.CriticalEvent)
|
||||
state.RegisterEvent(events.CriticalEvent)
|
||||
assert.True(t, state.CriticalActive())
|
||||
state.RegisterEvent(agent.CriticalResetEvent)
|
||||
state.RegisterEvent(events.CriticalResetEvent)
|
||||
assert.False(t, state.CriticalActive())
|
||||
}
|
||||
|
||||
@@ -47,15 +48,15 @@ func TestComputeBladeState_RegisterEventMixed(t *testing.T) {
|
||||
state := agent.NewComputeBladeState()
|
||||
|
||||
// Send a bunch of events
|
||||
state.RegisterEvent(agent.CriticalEvent)
|
||||
state.RegisterEvent(agent.CriticalResetEvent)
|
||||
state.RegisterEvent(agent.NoopEvent)
|
||||
state.RegisterEvent(agent.CriticalEvent)
|
||||
state.RegisterEvent(agent.NoopEvent)
|
||||
state.RegisterEvent(agent.IdentifyEvent)
|
||||
state.RegisterEvent(agent.IdentifyEvent)
|
||||
state.RegisterEvent(agent.CriticalResetEvent)
|
||||
state.RegisterEvent(agent.IdentifyEvent)
|
||||
state.RegisterEvent(events.CriticalEvent)
|
||||
state.RegisterEvent(events.CriticalResetEvent)
|
||||
state.RegisterEvent(events.NoopEvent)
|
||||
state.RegisterEvent(events.CriticalEvent)
|
||||
state.RegisterEvent(events.NoopEvent)
|
||||
state.RegisterEvent(events.IdentifyEvent)
|
||||
state.RegisterEvent(events.IdentifyEvent)
|
||||
state.RegisterEvent(events.CriticalResetEvent)
|
||||
state.RegisterEvent(events.IdentifyEvent)
|
||||
|
||||
assert.False(t, state.CriticalActive())
|
||||
assert.True(t, state.IdentifyActive())
|
||||
@@ -68,7 +69,7 @@ func TestComputeBladeState_WaitForIdentifyConfirm_NoTimeout(t *testing.T) {
|
||||
|
||||
// send identify event
|
||||
t.Log("Setting identify event")
|
||||
state.RegisterEvent(agent.IdentifyEvent)
|
||||
state.RegisterEvent(events.IdentifyEvent)
|
||||
assert.True(t, state.IdentifyActive())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -87,7 +88,7 @@ func TestComputeBladeState_WaitForIdentifyConfirm_NoTimeout(t *testing.T) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// confirm event
|
||||
state.RegisterEvent(agent.IdentifyConfirmEvent)
|
||||
state.RegisterEvent(events.IdentifyConfirmEvent)
|
||||
t.Log("Identify event confirmed")
|
||||
|
||||
wg.Wait()
|
||||
@@ -100,7 +101,7 @@ func TestComputeBladeState_WaitForIdentifyConfirm_Timeout(t *testing.T) {
|
||||
|
||||
// send identify event
|
||||
t.Log("Setting identify event")
|
||||
state.RegisterEvent(agent.IdentifyEvent)
|
||||
state.RegisterEvent(events.IdentifyEvent)
|
||||
assert.True(t, state.IdentifyActive())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
|
||||
@@ -120,7 +121,7 @@ func TestComputeBladeState_WaitForIdentifyConfirm_Timeout(t *testing.T) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// confirm event
|
||||
state.RegisterEvent(agent.IdentifyConfirmEvent)
|
||||
state.RegisterEvent(events.IdentifyConfirmEvent)
|
||||
t.Log("Identify event confirmed")
|
||||
|
||||
wg.Wait()
|
||||
@@ -133,7 +134,7 @@ func TestComputeBladeState_WaitForCriticalClear_NoTimeout(t *testing.T) {
|
||||
|
||||
// send critical event
|
||||
t.Log("Setting critical event")
|
||||
state.RegisterEvent(agent.CriticalEvent)
|
||||
state.RegisterEvent(events.CriticalEvent)
|
||||
assert.True(t, state.CriticalActive())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -152,7 +153,7 @@ func TestComputeBladeState_WaitForCriticalClear_NoTimeout(t *testing.T) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// confirm event
|
||||
state.RegisterEvent(agent.CriticalResetEvent)
|
||||
state.RegisterEvent(events.CriticalResetEvent)
|
||||
t.Log("critical event confirmed")
|
||||
|
||||
wg.Wait()
|
||||
@@ -165,7 +166,7 @@ func TestComputeBladeState_WaitForCriticalClear_Timeout(t *testing.T) {
|
||||
|
||||
// send critical event
|
||||
t.Log("Setting critical event")
|
||||
state.RegisterEvent(agent.CriticalEvent)
|
||||
state.RegisterEvent(events.CriticalEvent)
|
||||
assert.True(t, state.CriticalActive())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
|
||||
@@ -185,7 +186,7 @@ func TestComputeBladeState_WaitForCriticalClear_Timeout(t *testing.T) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// confirm event
|
||||
state.RegisterEvent(agent.CriticalResetEvent)
|
||||
state.RegisterEvent(events.CriticalResetEvent)
|
||||
t.Log("critical event confirmed")
|
||||
|
||||
wg.Wait()
|
||||
237
pkg/certificate/certificate.go
Normal file
237
pkg/certificate/certificate.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
)
|
||||
|
||||
// LoadAndValidateCertificate loads and validates a certificate and its private key from the provided file paths.
|
||||
// It reads, decodes, and parses the certificate and private key, ensuring the public key matches the private key.
|
||||
// Returns the parsed X.509 certificate, ECDSA private key, and a humane.Error if any error occurs during processing.
|
||||
func LoadAndValidateCertificate(certPath, keyPath string) (cert *x509.Certificate, key *ecdsa.PrivateKey, herr humane.Error) {
|
||||
// Load and decode CA cert
|
||||
certPEM, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, nil, humane.Wrap(err, "failed to read certificate",
|
||||
fmt.Sprintf("ensure the certificate file %s exists and is readable by the agent user", certPath),
|
||||
)
|
||||
}
|
||||
|
||||
// Load and decode CA key
|
||||
keyPEM, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, nil, humane.Wrap(err, "failed to read private key",
|
||||
fmt.Sprintf("ensure the key file %s exists and is readable by the agent user", keyPath),
|
||||
)
|
||||
}
|
||||
|
||||
return ValidateCertificate(certPEM, keyPEM)
|
||||
}
|
||||
|
||||
// ValidateCertificate validates a PEM-encoded certificate and private key, ensuring the private key matches the certificate.
|
||||
// Returns a parsed *x509.Certificate, *ecdsa.PrivateKey, or a humane.Error if any issue occurs during validation or parsing.
|
||||
func ValidateCertificate(certPEM []byte, keyPEM []byte) (cert *x509.Certificate, key *ecdsa.PrivateKey, herr humane.Error) {
|
||||
certBlock, _ := pem.Decode(certPEM)
|
||||
if certBlock == nil {
|
||||
return nil, nil, humane.New("failed to decode certificate",
|
||||
"Verify if the certificate is valid by run the following command:",
|
||||
"openssl x509 -in /path/to/certificate.pem -text -noout",
|
||||
)
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(certBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, humane.New("failed to parse certificate",
|
||||
"Verify if the certificate is valid by run the following command:",
|
||||
"openssl x509 -in /path/to/certificate.pem -text -noout",
|
||||
)
|
||||
}
|
||||
|
||||
keyBlock, _ := pem.Decode(keyPEM)
|
||||
if keyBlock == nil {
|
||||
return nil, nil, humane.New("failed to decode certificate",
|
||||
"Verify if the key-file is valid by run the following command:",
|
||||
"openssl ec -in /path/to/keyfile.pem -check",
|
||||
)
|
||||
}
|
||||
key, err = x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, humane.Wrap(err, "failed to parse private key",
|
||||
"Verify if the key-file is valid by run the following command:",
|
||||
"openssl ec -in /path/to/keyfile.pem -check",
|
||||
)
|
||||
}
|
||||
|
||||
// Compare public keys
|
||||
certPub, ok := cert.PublicKey.(*ecdsa.PublicKey)
|
||||
if !ok || certPub.X.Cmp(key.X) != 0 || certPub.Y.Cmp(key.Y) != 0 {
|
||||
return nil, nil, humane.New("private key does not match certificate",
|
||||
"Verify the certificate and private key match.",
|
||||
"To verify on the CLI, use:",
|
||||
fmt.Sprintf("cmp <(openssl x509 -in %s -pubkey -noout -outform PEM) <(openssl ec -in %s -pubout -outform PEM) && echo \"✅ Certificate and key match\" || echo \"❌ Mismatch\"",
|
||||
"/path/to/certificate.pem",
|
||||
"/path/to/keyfile.pem",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return cert, key, nil
|
||||
}
|
||||
|
||||
// GenerateCertificate generates a certificate and private key based on provided options and outputs them in DER format.
|
||||
// It supports client and server certificates, returning the certificate, private key, and an error if generation fails.
|
||||
func GenerateCertificate(commonName string, opts ...Option) (certDER, keyDER []byte, herr humane.Error) {
|
||||
options := &options{
|
||||
Usage: UsageClient,
|
||||
CaCert: nil,
|
||||
CaKey: nil,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return nil, nil, humane.Wrap(err, "failed to extract hostname",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
var extKeyUsage []x509.ExtKeyUsage
|
||||
var hostIps []net.IP
|
||||
|
||||
// If we generate server certificates
|
||||
switch options.Usage {
|
||||
case UsageClient:
|
||||
extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
|
||||
|
||||
case UsageServer:
|
||||
// make sure to use the correct key-usage
|
||||
extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
|
||||
|
||||
// And add all the host-ips
|
||||
if hostIps, err = util.GetHostIPs(); err != nil {
|
||||
return nil, nil, humane.Wrap(err, "failed to extract server IPs",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, nil, humane.New(fmt.Sprintf("invalid certificate usage %s", options.Usage.String()),
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
certTemplate := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{
|
||||
CommonName: commonName,
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: extKeyUsage,
|
||||
DNSNames: []string{"localhost", hostname, fmt.Sprintf("%s.local", hostname)},
|
||||
IPAddresses: hostIps,
|
||||
}
|
||||
|
||||
clientKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, humane.Wrap(err, "failed to generate client key",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
// prevent nil pointer exceptions by using the cert key as signing key and generate a
|
||||
// self-signed certificate, if no CA is provided
|
||||
signingCert := certTemplate
|
||||
signingKey := clientKey
|
||||
if options.CaCert != nil && options.CaKey != nil {
|
||||
signingCert = options.CaCert
|
||||
signingKey = options.CaKey
|
||||
}
|
||||
|
||||
certDER, err = x509.CreateCertificate(rand.Reader, certTemplate, signingCert, &clientKey.PublicKey, signingKey)
|
||||
if err != nil {
|
||||
return nil, nil, humane.Wrap(err, "failed to create client certificate",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
clientKeyBytes, err := x509.MarshalECPrivateKey(clientKey)
|
||||
if err != nil {
|
||||
return nil, nil, humane.Wrap(err, "failed to marshal client private key",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
)
|
||||
}
|
||||
|
||||
return certDER, clientKeyBytes, nil
|
||||
}
|
||||
|
||||
// WriteCertificate writes a certificate and its private key to the specified file paths in PEM format.
|
||||
// certPath specifies the file path to write the certificate PEM data.
|
||||
// keyPath specifies the file path to write the private key PEM data.
|
||||
// certDataDER is the DER-encoded certificate data to be written.
|
||||
// keyDataDER is the DER-encoded private key data to be written.
|
||||
// Returns a humane.Error if writing to the files fails.
|
||||
func WriteCertificate(certPath, keyPath string, certDataDER []byte, keyDataDER []byte) humane.Error {
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDataDER})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDataDER})
|
||||
|
||||
if err := os.WriteFile(certPath, certPEM, 0600); err != nil {
|
||||
return humane.Wrap(err, "failed to write certificate file",
|
||||
"ensure the directory you are trying to create exists and is writable by the agent user",
|
||||
)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
|
||||
return humane.Wrap(err, "failed to write key file",
|
||||
"ensure the directory you are trying to create exists and is writable by the agent user",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCertPoolFrom reads a CA certificate from a given path and initializes a x509.CertPool with its contents.
|
||||
// Returns the initialized certificate pool or a descriptive error if reading or appending the certificate fails.
|
||||
func GetCertPoolFrom(caPath string) (pool *x509.CertPool, herr humane.Error) {
|
||||
caCert, err := os.ReadFile(caPath)
|
||||
if err != nil {
|
||||
return nil, humane.Wrap(err, "failed to read CA certificate",
|
||||
"ensure the directory you are trying to create exists and is writable by the agent user",
|
||||
)
|
||||
}
|
||||
|
||||
pool = x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caCert) {
|
||||
return nil, humane.New("failed to append CA certificate to pool",
|
||||
"this should never happen",
|
||||
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
|
||||
"Verify if the CA certificate is valid by run the following command:",
|
||||
fmt.Sprintf("openssl x509 -in %s -text -noout", caPath),
|
||||
)
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
40
pkg/certificate/options.go
Normal file
40
pkg/certificate/options.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
CaCert *x509.Certificate
|
||||
CaKey *ecdsa.PrivateKey
|
||||
Usage Usage
|
||||
}
|
||||
|
||||
type Option func(*options)
|
||||
|
||||
func WithUsage(usage Usage) Option {
|
||||
return func(o *options) {
|
||||
o.Usage = usage
|
||||
}
|
||||
}
|
||||
|
||||
func WithClientUsage() Option {
|
||||
return WithUsage(UsageClient)
|
||||
}
|
||||
|
||||
func WithServerUsage() Option {
|
||||
return WithUsage(UsageServer)
|
||||
}
|
||||
|
||||
func WithCaCert(cert *x509.Certificate) Option {
|
||||
return func(o *options) {
|
||||
o.CaCert = cert
|
||||
}
|
||||
}
|
||||
|
||||
func WithCaKey(key *ecdsa.PrivateKey) Option {
|
||||
return func(o *options) {
|
||||
o.CaKey = key
|
||||
}
|
||||
}
|
||||
45
pkg/certificate/types.go
Normal file
45
pkg/certificate/types.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package certificate
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Usage defines the intended purpose of a certificate, such as client or server usage.
|
||||
type Usage int
|
||||
|
||||
func (c Usage) String() string {
|
||||
switch c {
|
||||
case UsageClient:
|
||||
return "client"
|
||||
|
||||
case UsageServer:
|
||||
return "server"
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("CertificateUsage(%d)", c)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
UsageClient Usage = iota // Certificate is for Client
|
||||
UsageServer // Certificate is for Server
|
||||
)
|
||||
|
||||
// Format represents the encoding format of a certificate, such as PEM or DER.
|
||||
type Format int
|
||||
|
||||
func (c Format) String() string {
|
||||
switch c {
|
||||
case FormatPEM:
|
||||
return "pem"
|
||||
|
||||
case FormatDER:
|
||||
return "der"
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("CertificateFormat(%d)", c)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
FormatPEM Format = iota // PEM Encoded Certificate
|
||||
FormatDER // DER Encoded Certificate
|
||||
)
|
||||
31
pkg/events/event.go
Normal file
31
pkg/events/event.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package events
|
||||
|
||||
type Event int
|
||||
|
||||
const (
|
||||
NoopEvent = iota
|
||||
IdentifyEvent
|
||||
IdentifyConfirmEvent
|
||||
CriticalEvent
|
||||
CriticalResetEvent
|
||||
EdgeButtonEvent
|
||||
)
|
||||
|
||||
func (e Event) String() string {
|
||||
switch e {
|
||||
case NoopEvent:
|
||||
return "noop"
|
||||
case IdentifyEvent:
|
||||
return "identify"
|
||||
case IdentifyConfirmEvent:
|
||||
return "identify_confirm"
|
||||
case CriticalEvent:
|
||||
return "critical"
|
||||
case CriticalResetEvent:
|
||||
return "critical_reset"
|
||||
case EdgeButtonEvent:
|
||||
return "edge_button"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
102
pkg/events/eventbus.go
Normal file
102
pkg/events/eventbus.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// EventBus is a simple event bus with topic-based publish/subscribe.
|
||||
// This is, by no means, a performant or complete implementation but for the scope of this project more than sufficient
|
||||
type EventBus interface {
|
||||
Publish(topic string, message any)
|
||||
Subscribe(topic string, bufSize int, filter func(any) bool) Subscriber
|
||||
}
|
||||
|
||||
type Subscriber interface {
|
||||
C() <-chan any
|
||||
Unsubscribe()
|
||||
}
|
||||
|
||||
type eventBus struct {
|
||||
subscribers map[string]map[*subscriber]func(any) bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type subscriber struct {
|
||||
mu sync.Mutex
|
||||
ch chan any
|
||||
closed bool
|
||||
}
|
||||
|
||||
func MatchAll(_ any) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// New returns an initialized EventBus.
|
||||
func New() EventBus {
|
||||
return &eventBus{
|
||||
subscribers: make(map[string]map[*subscriber]func(any) bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Publish a message to a topic (best-effort). Subscribers with a full receive queue are dropped.
|
||||
func (eb *eventBus) Publish(topic string, message any) {
|
||||
eb.mu.Lock()
|
||||
defer eb.mu.Unlock()
|
||||
|
||||
if eb.subscribers[topic] == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if subs, ok := eb.subscribers[topic]; ok {
|
||||
for sub, filter := range subs {
|
||||
sub.mu.Lock()
|
||||
// Clean up closed subscribers
|
||||
if sub.closed {
|
||||
delete(eb.subscribers[topic], sub)
|
||||
continue
|
||||
}
|
||||
|
||||
if filter(message) {
|
||||
// Try to send message, but don't block
|
||||
select {
|
||||
case sub.ch <- message:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
sub.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to a topic with a filter function. Returns a channel with given buffer size.
|
||||
func (eb *eventBus) Subscribe(topic string, bufSize int, filter func(any) bool) Subscriber {
|
||||
eb.mu.Lock()
|
||||
defer eb.mu.Unlock()
|
||||
|
||||
ch := make(chan any, bufSize)
|
||||
|
||||
sub := &subscriber{
|
||||
ch: ch,
|
||||
closed: false,
|
||||
}
|
||||
|
||||
if _, ok := eb.subscribers[topic]; !ok {
|
||||
eb.subscribers[topic] = make(map[*subscriber]func(any) bool)
|
||||
}
|
||||
|
||||
eb.subscribers[topic][sub] = filter
|
||||
|
||||
return sub
|
||||
}
|
||||
|
||||
func (s *subscriber) C() <-chan any {
|
||||
return s.ch
|
||||
}
|
||||
|
||||
func (s *subscriber) Unsubscribe() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
close(s.ch)
|
||||
s.closed = true
|
||||
}
|
||||
74
pkg/events/eventbus_test.go
Normal file
74
pkg/events/eventbus_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEventBusManySubscribers(t *testing.T) {
|
||||
eb := events.New()
|
||||
|
||||
// Create a channel and subscribe to a topic without a filter
|
||||
sub0 := eb.Subscribe("topic0", 2, events.MatchAll)
|
||||
assert.Equal(t, cap(sub0.C()), 2)
|
||||
assert.Equal(t, len(sub0.C()), 0)
|
||||
defer sub0.Unsubscribe()
|
||||
|
||||
// Create a channel and subscribe to a topic with a filter
|
||||
sub1 := eb.Subscribe("topic0", 2, func(msg any) bool {
|
||||
return msg.(int) > 5
|
||||
})
|
||||
assert.Equal(t, cap(sub1.C()), 2)
|
||||
assert.Equal(t, len(sub1.C()), 0)
|
||||
defer sub1.Unsubscribe()
|
||||
|
||||
// Create a channel and subscribe to another topic
|
||||
sub2 := eb.Subscribe("topic1", 1, events.MatchAll)
|
||||
assert.Equal(t, cap(sub2.C()), 1)
|
||||
assert.Equal(t, len(sub2.C()), 0)
|
||||
defer sub2.Unsubscribe()
|
||||
|
||||
sub3 := eb.Subscribe("topic1", 0, events.MatchAll)
|
||||
assert.Equal(t, cap(sub3.C()), 0)
|
||||
assert.Equal(t, len(sub3.C()), 0)
|
||||
defer sub3.Unsubscribe()
|
||||
|
||||
// Publish some messages
|
||||
eb.Publish("topic0", 10)
|
||||
eb.Publish("topic0", 4)
|
||||
eb.Publish("topic1", "Hello, World!")
|
||||
|
||||
// Assert received messages
|
||||
assert.Equal(t, len(sub0.C()), 2)
|
||||
assert.Equal(t, <-sub0.C(), 10)
|
||||
assert.Equal(t, <-sub0.C(), 4)
|
||||
|
||||
assert.Equal(t, len(sub1.C()), 1)
|
||||
assert.Equal(t, <-sub1.C(), 10)
|
||||
|
||||
assert.Equal(t, len(sub2.C()), 1)
|
||||
assert.Equal(t, <-sub2.C(), "Hello, World!")
|
||||
|
||||
// sub3 has no buffer, so it should be empty as there's been no consumer at time of publishing
|
||||
assert.Equal(t, len(sub3.C()), 0)
|
||||
}
|
||||
|
||||
func TestUnsubscribe(t *testing.T) {
|
||||
eb := events.New()
|
||||
|
||||
// Create a channel and subscribe to a topic
|
||||
sub := eb.Subscribe("topic", 2, events.MatchAll)
|
||||
|
||||
// Unsubscribe from the topic
|
||||
sub.Unsubscribe()
|
||||
|
||||
// Try to publish a message after unsubscribing
|
||||
eb.Publish("topic", "This message should not be received")
|
||||
|
||||
// Assert that the channel is closed
|
||||
_, ok := <-sub.C()
|
||||
assert.False(t, ok, "Unsubscribed channel should be closed")
|
||||
|
||||
}
|
||||
18
pkg/fancontroller/config.go
Normal file
18
pkg/fancontroller/config.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package fancontroller
|
||||
|
||||
type FanOverrideOpts struct {
|
||||
Percent uint8 `mapstructure:"speed"`
|
||||
}
|
||||
|
||||
type Step struct {
|
||||
// Temperature is the temperature to react to
|
||||
Temperature float64 `mapstructure:"temperature"`
|
||||
// Percent is the fan speed in percent
|
||||
Percent uint8 `mapstructure:"percent"`
|
||||
}
|
||||
|
||||
// Config configures a fan controller for the computeblade
|
||||
type Config struct {
|
||||
// Steps defines the temperature/speed steps for the fan controller
|
||||
Steps []Step `mapstructure:"steps"`
|
||||
}
|
||||
@@ -2,53 +2,64 @@ package fancontroller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
)
|
||||
|
||||
type FanController interface {
|
||||
Override(opts *FanOverrideOpts)
|
||||
GetFanSpeed(temperature float64) uint8
|
||||
}
|
||||
// GetFanSpeedPercent returns the fan speed in percent based on the current temperature
|
||||
GetFanSpeedPercent(temperature float64) uint8
|
||||
// IsAutomaticSpeed returns true if the FanSpeed is determined by the fan controller logic, or false if determined
|
||||
// by an FanOverrideOpts
|
||||
IsAutomaticSpeed() bool
|
||||
|
||||
type FanOverrideOpts struct {
|
||||
Percent uint8 `mapstructure:"speed"`
|
||||
}
|
||||
|
||||
type FanControllerStep struct {
|
||||
// Temperature is the temperature to react to
|
||||
Temperature float64 `mapstructure:"temperature"`
|
||||
// Percent is the fan speed in percent
|
||||
Percent uint8 `mapstructure:"percent"`
|
||||
}
|
||||
|
||||
// FanController configures a fan controller for the computeblade
|
||||
type FanControllerConfig struct {
|
||||
// Steps defines the temperature/speed steps for the fan controller
|
||||
Steps []FanControllerStep `mapstructure:"steps"`
|
||||
// Steps returns the list of temperature and fan speed steps configured for the fan controller.
|
||||
Steps() []Step
|
||||
}
|
||||
|
||||
// FanController is a simple fan controller that reacts to temperature changes with a linear function
|
||||
type fanControllerLinear struct {
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
overrideOpts *FanOverrideOpts
|
||||
config FanControllerConfig
|
||||
config Config
|
||||
}
|
||||
|
||||
// NewFanControllerLinear creates a new FanControllerLinear
|
||||
func NewLinearFanController(config FanControllerConfig) (FanController, error) {
|
||||
// NewLinearFanController creates a new FanControllerLinear
|
||||
func NewLinearFanController(config Config) (FanController, humane.Error) {
|
||||
steps := config.Steps
|
||||
|
||||
// Validate config for a very simple linear fan controller
|
||||
if len(config.Steps) != 2 {
|
||||
return nil, fmt.Errorf("exactly two steps must be defined")
|
||||
// Sort steps by temperature
|
||||
sort.Slice(steps, func(i, j int) bool {
|
||||
return steps[i].Temperature < steps[j].Temperature
|
||||
})
|
||||
|
||||
for i := 0; i < len(steps)-1; i++ {
|
||||
curr := steps[i]
|
||||
next := steps[i+1]
|
||||
|
||||
if curr.Temperature >= next.Temperature {
|
||||
return nil, humane.New("steps must have strictly increasing temperatures",
|
||||
"Ensure that the temperatures are in ascending order and the ranges do not overlap",
|
||||
fmt.Sprintf("Ensure defined temperature stepd %.2f is >= %.2f", curr.Temperature, next.Temperature),
|
||||
)
|
||||
}
|
||||
if curr.Percent > next.Percent {
|
||||
return nil, humane.New("fan percent must not decrease",
|
||||
"Ensure that the fan percentages are not decreasing for higher temperatures",
|
||||
fmt.Sprintf("Temperature %.2f is defined at %d%% and must be >= %d%% defined for temperature %.2f", curr.Temperature, curr.Percent, next.Percent, next.Temperature),
|
||||
)
|
||||
}
|
||||
}
|
||||
if config.Steps[0].Temperature > config.Steps[1].Temperature {
|
||||
return nil, fmt.Errorf("step 1 temperature must be lower than step 2 temperature")
|
||||
}
|
||||
if config.Steps[0].Percent > config.Steps[1].Percent {
|
||||
return nil, fmt.Errorf("step 1 speed must be lower than step 2 speed")
|
||||
}
|
||||
if config.Steps[0].Percent > 100 || config.Steps[1].Percent > 100 {
|
||||
return nil, fmt.Errorf("speed must be between 0 and 100")
|
||||
|
||||
for _, step := range steps {
|
||||
if step.Percent > 100 {
|
||||
return nil, humane.New("fan percent must be between 0 and 100",
|
||||
fmt.Sprintf("Ensure your fan percentage is 0 < %d < 100", step.Percent),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return &fanControllerLinear{
|
||||
@@ -56,14 +67,18 @@ func NewLinearFanController(config FanControllerConfig) (FanController, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fanControllerLinear) Steps() []Step {
|
||||
return f.config.Steps
|
||||
}
|
||||
|
||||
func (f *fanControllerLinear) Override(opts *FanOverrideOpts) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.overrideOpts = opts
|
||||
}
|
||||
|
||||
// GetFanSpeed returns the fan speed in percent based on the current temperature
|
||||
func (f *fanControllerLinear) GetFanSpeed(temperature float64) uint8 {
|
||||
// GetFanSpeedPercent returns the fan speed in percent based on the current temperature
|
||||
func (f *fanControllerLinear) GetFanSpeedPercent(temperature float64) uint8 {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
@@ -71,18 +86,33 @@ func (f *fanControllerLinear) GetFanSpeed(temperature float64) uint8 {
|
||||
return f.overrideOpts.Percent
|
||||
}
|
||||
|
||||
if temperature <= f.config.Steps[0].Temperature {
|
||||
return f.config.Steps[0].Percent
|
||||
}
|
||||
if temperature >= f.config.Steps[1].Temperature {
|
||||
return f.config.Steps[1].Percent
|
||||
steps := f.config.Steps
|
||||
|
||||
// Below minimum temperature: use minimum fan speed
|
||||
if temperature <= steps[0].Temperature {
|
||||
return steps[0].Percent
|
||||
}
|
||||
|
||||
// Calculate slope
|
||||
slope := float64(f.config.Steps[1].Percent-f.config.Steps[0].Percent) / (f.config.Steps[1].Temperature - f.config.Steps[0].Temperature)
|
||||
// Above maximum temperature: use maximum fan speed
|
||||
lastIdx := len(steps) - 1
|
||||
if temperature >= steps[lastIdx].Temperature {
|
||||
return steps[lastIdx].Percent
|
||||
}
|
||||
|
||||
// Calculate speed
|
||||
speed := float64(f.config.Steps[0].Percent) + slope*(temperature-f.config.Steps[0].Temperature)
|
||||
// Find the bracket where steps[i].Temperature <= temperature < steps[i+1].Temperature
|
||||
for i := 0; i < lastIdx; i++ {
|
||||
if temperature >= steps[i].Temperature && temperature < steps[i+1].Temperature {
|
||||
// Linear interpolation between steps[i] and steps[i+1]
|
||||
slope := float64(steps[i+1].Percent-steps[i].Percent) / (steps[i+1].Temperature - steps[i].Temperature)
|
||||
speed := float64(steps[i].Percent) + slope*(temperature-steps[i].Temperature)
|
||||
return uint8(speed)
|
||||
}
|
||||
}
|
||||
|
||||
return uint8(speed)
|
||||
// Fallback (should not reach here due to above checks)
|
||||
return steps[lastIdx].Percent
|
||||
}
|
||||
|
||||
func (f *fanControllerLinear) IsAutomaticSpeed() bool {
|
||||
return f.overrideOpts == nil
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// fancontroller_test.go
|
||||
package fancontroller_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xvzf/computeblade-agent/pkg/fancontroller"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFanControllerLinear_GetFanSpeed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config := fancontroller.FanControllerConfig{
|
||||
Steps: []fancontroller.FanControllerStep{
|
||||
config := fancontroller.Config{
|
||||
Steps: []fancontroller.Step{
|
||||
{Temperature: 20, Percent: 30},
|
||||
{Temperature: 30, Percent: 60},
|
||||
},
|
||||
@@ -31,15 +31,65 @@ func TestFanControllerLinear_GetFanSpeed(t *testing.T) {
|
||||
{35, 60}, // Should use the maximum speed
|
||||
}
|
||||
|
||||
assert.Equal(t, controller.Steps(), config.Steps)
|
||||
|
||||
for _, tc := range testCases {
|
||||
expected := tc.expected
|
||||
temperature := tc.temperature
|
||||
t.Run("", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
speed := controller.GetFanSpeed(temperature)
|
||||
if speed != expected {
|
||||
t.Errorf("For temperature %.2f, expected speed %d but got %d", temperature, expected, speed)
|
||||
}
|
||||
speed := controller.GetFanSpeedPercent(temperature)
|
||||
assert.Equal(t, expected, speed)
|
||||
assert.True(t, controller.IsAutomaticSpeed(), "Expected fan speed to be automatic, but it was not")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFanControllerLinear_GetFanSpeedMultipleSteps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Typical 5-step fan curve configuration
|
||||
config := fancontroller.Config{
|
||||
Steps: []fancontroller.Step{
|
||||
{Temperature: 40, Percent: 30},
|
||||
{Temperature: 50, Percent: 50},
|
||||
{Temperature: 60, Percent: 70},
|
||||
{Temperature: 70, Percent: 90},
|
||||
{Temperature: 75, Percent: 100},
|
||||
},
|
||||
}
|
||||
|
||||
controller, err := fancontroller.NewLinearFanController(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create fan controller: %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
temperature float64
|
||||
expected uint8
|
||||
}{
|
||||
{"below minimum", 30, 30}, // Below 40°C: use minimum 30%
|
||||
{"at step 0", 40, 30}, // At 40°C: 30%
|
||||
{"between step 0-1", 45, 40}, // Midpoint 40-50°C: 40%
|
||||
{"at step 1", 50, 50}, // At 50°C: 50%
|
||||
{"between step 1-2", 55, 60}, // Midpoint 50-60°C: 60%
|
||||
{"at step 2", 60, 70}, // At 60°C: 70%
|
||||
{"between step 2-3", 65, 80}, // Midpoint 60-70°C: 80%
|
||||
{"at step 3", 70, 90}, // At 70°C: 90%
|
||||
{"between step 3-4", 72, 94}, // 70 + (100-90)*(72-70)/(75-70) = 90 + 4 = 94%
|
||||
{"at step 4", 75, 100}, // At 75°C: 100%
|
||||
{"above maximum", 80, 100}, // Above 75°C: use maximum 100%
|
||||
{"well above maximum", 90, 100}, // Well above: still 100%
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
expected := tc.expected
|
||||
temperature := tc.temperature
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
speed := controller.GetFanSpeedPercent(temperature)
|
||||
assert.Equal(t, expected, speed, "Temperature %.1f°C should yield %d%% fan speed", temperature, expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -47,8 +97,8 @@ func TestFanControllerLinear_GetFanSpeed(t *testing.T) {
|
||||
func TestFanControllerLinear_GetFanSpeedWithOverride(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config := fancontroller.FanControllerConfig{
|
||||
Steps: []fancontroller.FanControllerStep{
|
||||
config := fancontroller.Config{
|
||||
Steps: []fancontroller.Step{
|
||||
{Temperature: 20, Percent: 30},
|
||||
{Temperature: 30, Percent: 60},
|
||||
},
|
||||
@@ -76,10 +126,9 @@ func TestFanControllerLinear_GetFanSpeedWithOverride(t *testing.T) {
|
||||
temperature := tc.temperature
|
||||
t.Run("", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
speed := controller.GetFanSpeed(temperature)
|
||||
if speed != expected {
|
||||
t.Errorf("For temperature %.2f, expected speed %d but got %d", temperature, expected, speed)
|
||||
}
|
||||
speed := controller.GetFanSpeedPercent(temperature)
|
||||
assert.Equal(t, expected, speed)
|
||||
assert.False(t, controller.IsAutomaticSpeed(), "Expected fan speed to be overridden, but it was not")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -87,47 +136,38 @@ func TestFanControllerLinear_GetFanSpeedWithOverride(t *testing.T) {
|
||||
func TestFanControllerLinear_ConstructionErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config fancontroller.FanControllerConfig
|
||||
config fancontroller.Config
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "InvalidStepCount",
|
||||
config: fancontroller.FanControllerConfig{
|
||||
Steps: []fancontroller.FanControllerStep{
|
||||
name: "Overlapping Step Temperatures",
|
||||
config: fancontroller.Config{
|
||||
Steps: []fancontroller.Step{
|
||||
{Temperature: 20, Percent: 60},
|
||||
{Temperature: 20, Percent: 30},
|
||||
},
|
||||
},
|
||||
errMsg: "exactly two steps must be defined",
|
||||
errMsg: "steps must have strictly increasing temperatures",
|
||||
},
|
||||
{
|
||||
name: "InvalidStepTemperatures",
|
||||
config: fancontroller.FanControllerConfig{
|
||||
Steps: []fancontroller.FanControllerStep{
|
||||
{Temperature: 30, Percent: 60},
|
||||
{Temperature: 20, Percent: 30},
|
||||
},
|
||||
},
|
||||
errMsg: "step 1 temperature must be lower than step 2 temperature",
|
||||
},
|
||||
{
|
||||
name: "InvalidStepSpeeds",
|
||||
config: fancontroller.FanControllerConfig{
|
||||
Steps: []fancontroller.FanControllerStep{
|
||||
name: "Percentages must not decrease",
|
||||
config: fancontroller.Config{
|
||||
Steps: []fancontroller.Step{
|
||||
{Temperature: 20, Percent: 60},
|
||||
{Temperature: 30, Percent: 30},
|
||||
},
|
||||
},
|
||||
errMsg: "step 1 speed must be lower than step 2 speed",
|
||||
errMsg: "fan percent must not decrease",
|
||||
},
|
||||
{
|
||||
name: "InvalidSpeedRange",
|
||||
config: fancontroller.FanControllerConfig{
|
||||
Steps: []fancontroller.FanControllerStep{
|
||||
config: fancontroller.Config{
|
||||
Steps: []fancontroller.Step{
|
||||
{Temperature: 20, Percent: 10},
|
||||
{Temperature: 30, Percent: 200},
|
||||
},
|
||||
},
|
||||
errMsg: "speed must be between 0 and 100",
|
||||
errMsg: "fan percent must be between 0 and 100",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -137,11 +177,9 @@ func TestFanControllerLinear_ConstructionErrors(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := fancontroller.NewLinearFanController(config)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with message '%s', but got no error", expectedErrMsg)
|
||||
} else if err.Error() != expectedErrMsg {
|
||||
t.Errorf("Expected error message '%s', but got '%s'", expectedErrMsg, err.Error())
|
||||
}
|
||||
|
||||
assert.NotNil(t, err, "Expected error with message '%s', but got no error", expectedErrMsg)
|
||||
assert.EqualError(t, err, expectedErrMsg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
5
pkg/hal/config.go
Normal file
5
pkg/hal/config.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package hal
|
||||
|
||||
type Config struct {
|
||||
RpmReporting bool `mapstructure:"rpm_reporting_standard_fan_unit"`
|
||||
}
|
||||
49
pkg/hal/example_smartfanunit_test.go
Normal file
49
pkg/hal/example_smartfanunit_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
//go:build !tinygo
|
||||
|
||||
package hal_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
)
|
||||
|
||||
func ExampleNewSmartFanUnit() {
|
||||
ctx := context.Background()
|
||||
|
||||
client, err := hal.NewSmartFanUnit("/dev/tty.usbmodem11102")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go func() {
|
||||
err := client.Run(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Set LED color for the blade to red
|
||||
err = client.SetLed(ctx, led.Color{Red: 100, Green: 0, Blue: 0})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Set fan speed to 20%
|
||||
err = client.SetFanSpeedPercent(ctx, 20)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tmp, err := client.AirFlowTemperature(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Println("AirflowTemp", tmp)
|
||||
rpm, err := client.FanSpeedRPM(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Println("RPM", rpm)
|
||||
}
|
||||
109
pkg/hal/hal.go
109
pkg/hal/hal.go
@@ -3,62 +3,13 @@ package hal
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
)
|
||||
|
||||
type FanUnit uint8
|
||||
type FanUnitKind uint8
|
||||
type ComputeModule uint8
|
||||
type PowerStatus uint8
|
||||
|
||||
var (
|
||||
fanSpeedTargetPercent = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_speed_target_percent",
|
||||
Help: "Target fanspeed in percent",
|
||||
})
|
||||
fanSpeed = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_speed",
|
||||
Help: "Fan speed in RPM",
|
||||
})
|
||||
socTemperature = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "soc_temperature",
|
||||
Help: "SoC temperature in °C",
|
||||
})
|
||||
computeModule = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "compute_modul_present",
|
||||
Help: "Compute module type",
|
||||
}, []string{"type"})
|
||||
ledColorChangeEventCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "led_color_change_event_count",
|
||||
Help: "Led color change event_count",
|
||||
})
|
||||
powerStatus = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "power_status",
|
||||
Help: "Power status of the blade",
|
||||
}, []string{"type"})
|
||||
stealthModeEnabled = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "stealth_mode_enabled",
|
||||
Help: "Stealth mode enabled",
|
||||
})
|
||||
fanUnit = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_unit",
|
||||
Help: "Fan unit",
|
||||
}, []string{"type"})
|
||||
edgeButtonEventCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "edge_button_event_count",
|
||||
Help: "Number of edge button presses",
|
||||
})
|
||||
)
|
||||
|
||||
func (p PowerStatus) String() string {
|
||||
switch p {
|
||||
case PowerPoe802at:
|
||||
@@ -71,8 +22,9 @@ func (p PowerStatus) String() string {
|
||||
}
|
||||
|
||||
const (
|
||||
FanUnitStandard = iota
|
||||
FanUnitSmart
|
||||
FanUnitKindStandard = iota
|
||||
FanUnitKindStandardNoRPM
|
||||
FanUnitKindSmart
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -80,36 +32,63 @@ const (
|
||||
PowerPoe802at
|
||||
)
|
||||
|
||||
type LedIndex uint8
|
||||
|
||||
const (
|
||||
LedTop = iota
|
||||
LedTop LedIndex = iota
|
||||
LedEdge
|
||||
)
|
||||
|
||||
type LedColor struct {
|
||||
Red uint8 `mapstructure:"red"`
|
||||
Green uint8 `mapstructure:"green"`
|
||||
Blue uint8 `mapstructure:"blue"`
|
||||
}
|
||||
|
||||
type ComputeBladeHalOpts struct {
|
||||
FanUnit FanUnit
|
||||
RpmReportingStandardFanUnit bool `mapstructure:"rpm_reporting_standard_fan_unit"`
|
||||
}
|
||||
|
||||
// ComputeBladeHal abstracts hardware details of the Compute Blade and provides a simple interface
|
||||
type ComputeBladeHal interface {
|
||||
// Run starts background tasks and returns when the context is cancelled or an error occurs
|
||||
Run(ctx context.Context) error
|
||||
// Close closes the ComputeBladeHal
|
||||
Close() error
|
||||
// SetFanSpeed sets the fan speed in percent
|
||||
SetFanSpeed(speed uint8) error
|
||||
// GetFanSpeed returns the current fan speed in percent (based on moving average)
|
||||
// GetFanRPM returns the current fan speed in percent (based on moving average)
|
||||
GetFanRPM() (float64, error)
|
||||
// SetStealthMode enables/disables stealth mode of the blade (turning on/off the LEDs)
|
||||
SetStealthMode(enabled bool) error
|
||||
// SetLEDs sets the color of the LEDs
|
||||
SetLed(idx uint, color LedColor) error
|
||||
// StealthModeActive returns if stealth mode of the blade is currently active
|
||||
StealthModeActive() bool
|
||||
// SetLed sets the color of the LEDs
|
||||
SetLed(idx LedIndex, color led.Color) error
|
||||
// GetPowerStatus returns the current power status of the blade
|
||||
GetPowerStatus() (PowerStatus, error)
|
||||
// GetTemperature returns the current temperature of the SoC in °C
|
||||
GetTemperature() (float64, error)
|
||||
// GetEdgeButtonPressChan returns a channel emitting edge button press events
|
||||
// WaitForEdgeButtonPress returns a channel emitting edge button press events
|
||||
WaitForEdgeButtonPress(ctx context.Context) error
|
||||
}
|
||||
|
||||
// FanUnit abstracts the fan unit
|
||||
type FanUnit interface {
|
||||
// Kind returns the kind of the fan FanUnit
|
||||
Kind() FanUnitKind
|
||||
|
||||
// Run the client with event loop
|
||||
Run(context.Context) error
|
||||
|
||||
// SetFanSpeedPercent sets the fan speed in percent.
|
||||
SetFanSpeedPercent(context.Context, uint8) error
|
||||
|
||||
// SetLed sets the LED color. Noop if the LED is not available.
|
||||
SetLed(context.Context, led.Color) error
|
||||
|
||||
// FanSpeedRPM returns the current fan speed in rotations per minute.
|
||||
FanSpeedRPM(context.Context) (float64, error)
|
||||
|
||||
// WaitForButtonPress blocks until the button is pressed. Noop if the button is not available.
|
||||
WaitForButtonPress(context.Context) error
|
||||
|
||||
// AirFlowTemperature returns the temperature of the air flow. Noop if the sensor is not available.
|
||||
AirFlowTemperature(context.Context) (float32, error)
|
||||
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build linux
|
||||
//go:build linux && !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
@@ -14,8 +14,12 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"github.com/warthog618/gpiod"
|
||||
"github.com/warthog618/gpiod/device/rpi"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,9 +30,10 @@ const (
|
||||
bcm2711ClkManagerPwd = (0x5A << 24) //(31 - 24) on CM_GP0CTL/CM_GP1CTL/CM_GP2CTL regs
|
||||
bcm2711PageSize = 4096 // theoretical page size
|
||||
|
||||
bcm2711FrontButtonPin = 20
|
||||
bcm2711StealthPin = 21
|
||||
bcm2711RegPwmTachPin = 13
|
||||
// FIXME: no dead code
|
||||
//bcm2711FrontButtonPin = 20
|
||||
//bcm2711StealthPin = 21
|
||||
//bcm2711RegPwmTachPin = 13
|
||||
|
||||
bcm2711RegGpfsel1 = 0x01
|
||||
|
||||
@@ -52,6 +57,8 @@ const (
|
||||
bcm2711DebounceInterval = 100 * time.Millisecond
|
||||
|
||||
bcm2711ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
|
||||
|
||||
smartFanUnitDev = "/dev/ttyAMA5" // UART5
|
||||
)
|
||||
|
||||
type bcm2711 struct {
|
||||
@@ -73,7 +80,7 @@ type bcm2711 struct {
|
||||
gpioChip0 *gpiod.Chip
|
||||
|
||||
// Save LED colors so the pixels can be updated individually
|
||||
leds [2]LedColor
|
||||
leds [2]led.Color
|
||||
|
||||
// Stealth mode output
|
||||
stealthModeLine *gpiod.Line
|
||||
@@ -86,13 +93,11 @@ type bcm2711 struct {
|
||||
// PoE detection input
|
||||
poeLine *gpiod.Line
|
||||
|
||||
// Fan tach input
|
||||
fanEdgeLine *gpiod.Line
|
||||
lastFanEdgeEvent *gpiod.LineEvent
|
||||
fanRpm float64
|
||||
// Fan unit
|
||||
fanUnit FanUnit
|
||||
}
|
||||
|
||||
func NewCm4Hal(opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
func newBcm2711Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
// /dev/gpiomem doesn't allow complex operations for PWM fan control or WS281x
|
||||
devmem, err := os.OpenFile("/dev/mem", os.O_RDWR|os.O_SYNC, os.ModePerm)
|
||||
if err != nil {
|
||||
@@ -134,84 +139,33 @@ func NewCm4Hal(opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
|
||||
computeModule.WithLabelValues("cm4").Set(1)
|
||||
|
||||
return bcm, bcm.setup()
|
||||
log.FromContext(ctx).Info("starting hal setup", zap.String("hal", "bcm2711"))
|
||||
err = bcm.setup(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bcm, nil
|
||||
}
|
||||
|
||||
// Close cleans all memory mappings
|
||||
func (bcm *bcm2711) Close() error {
|
||||
errs := errors.Join(
|
||||
bcm.fanUnit.Close(),
|
||||
syscall.Munmap(bcm.gpioMem8),
|
||||
syscall.Munmap(bcm.pwmMem8),
|
||||
syscall.Munmap(bcm.clkMem8),
|
||||
bcm.devmem.Close(),
|
||||
bcm.gpioChip0.Close(),
|
||||
bcm.edgeButtonLine.Close(),
|
||||
bcm.poeLine.Close(),
|
||||
bcm.stealthModeLine.Close(),
|
||||
)
|
||||
|
||||
if bcm.fanEdgeLine != nil {
|
||||
return errors.Join(errs, bcm.fanEdgeLine.Close())
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// handleFanEdge handles an edge event on the fan tach input for the standard fan unite.
|
||||
// Exponential moving average is used to smooth out the fan speed.
|
||||
func (bcm *bcm2711) handleFanEdge(evt gpiod.LineEvent) {
|
||||
// Ensure we're always storing the last event
|
||||
defer func() {
|
||||
bcm.lastFanEdgeEvent = &evt
|
||||
}()
|
||||
|
||||
// First event, we cannot extrapolate the fan speed yet
|
||||
if bcm.lastFanEdgeEvent == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate time delta between events
|
||||
delta := evt.Timestamp - bcm.lastFanEdgeEvent.Timestamp
|
||||
ticksPerSecond := 1000.0 / float64(delta.Milliseconds())
|
||||
rpm := (ticksPerSecond * 60.0) / 2.0 // 2 ticks per revolution
|
||||
|
||||
// Simple moving average to smooth out the fan speed
|
||||
bcm.fanRpm = (rpm * 0.1) + (bcm.fanRpm * 0.9)
|
||||
fanSpeed.Set(bcm.fanRpm)
|
||||
}
|
||||
|
||||
func (bcm *bcm2711) handleEdgeButtonEdge(evt gpiod.LineEvent) {
|
||||
// Despite the debounce, we still get multiple events for a single button press
|
||||
// -> This is an in-software debounce to ensure we only get one event per button press
|
||||
select {
|
||||
case bcm.edgeButtonDebounceChan <- struct{}{}:
|
||||
go func() {
|
||||
// Manually debounce the button
|
||||
<-bcm.edgeButtonDebounceChan
|
||||
time.Sleep(bcm2711DebounceInterval)
|
||||
edgeButtonEventCount.Inc()
|
||||
close(bcm.edgeButtonWatchChan)
|
||||
bcm.edgeButtonWatchChan = make(chan struct{})
|
||||
}()
|
||||
default:
|
||||
// noop
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForEdgeButtonPress blocks until the edge button has been pressed
|
||||
func (bcm *bcm2711) WaitForEdgeButtonPress(ctx context.Context) error {
|
||||
// Either wait for the context to be cancelled or the edge button to be pressed
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-bcm.edgeButtonWatchChan:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Init initialises GPIOs and sets sane defaults
|
||||
func (bcm *bcm2711) setup() error {
|
||||
var err error = nil
|
||||
func (bcm *bcm2711) setup(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// Register edge event handler for edge button
|
||||
bcm.edgeButtonLine, err = bcm.gpioChip0.RequestLine(
|
||||
@@ -233,29 +187,97 @@ func (bcm *bcm2711) setup() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// standard fan unit
|
||||
if bcm.opts.FanUnit == FanUnitStandard {
|
||||
fanUnit.WithLabelValues("standard").Set(1)
|
||||
// FAN PWM output for standard fan unit (GPIO 12)
|
||||
// -> bcm2711RegGpfsel1 8:6, alt0
|
||||
bcm.gpioMem[bcm2711RegGpfsel1] = (bcm.gpioMem[bcm2711RegGpfsel1] &^ (0b111 << 6)) | (0b100 << 6)
|
||||
// Register edge event handler for fan tach input
|
||||
bcm.fanEdgeLine, err = bcm.gpioChip0.RequestLine(
|
||||
rpi.GPIO13,
|
||||
gpiod.WithEventHandler(bcm.handleFanEdge),
|
||||
gpiod.WithFallingEdge,
|
||||
gpiod.WithPullUp,
|
||||
)
|
||||
// Setup correct fan unit
|
||||
log.FromContext(ctx).Info("detecting fan unit")
|
||||
detectCtx, cancel := context.WithTimeout(ctx, 3*time.Second) // temp events are sent every 2 seconds
|
||||
defer cancel()
|
||||
|
||||
if smartFanUnitPresent, err := SmartFanUnitPresent(detectCtx, smartFanUnitDev); err == nil && smartFanUnitPresent {
|
||||
log.FromContext(ctx).Info("detected smart fan unit")
|
||||
bcm.fanUnit, err = NewSmartFanUnit(smartFanUnitDev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.FromContext(ctx).WithError(err).Info("no smart fan unit detected, assuming standard fan unit")
|
||||
// FAN PWM output for standard fan unit (GPIO 12)
|
||||
// -> bcm2711RegGpfsel1 8:6, alt0
|
||||
bcm.gpioMem[bcm2711RegGpfsel1] = (bcm.gpioMem[bcm2711RegGpfsel1] &^ (0b111 << 6)) | (0b100 << 6)
|
||||
bcm.fanUnit = &standardFanUnitBcm2711{
|
||||
GpioChip0: bcm.gpioChip0,
|
||||
DisableRpmReporting: !bcm.opts.RpmReportingStandardFanUnit,
|
||||
SetFanSpeedPwmFunc: func(speed uint8) error {
|
||||
bcm.setFanSpeedPWM(speed)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bcm2711 *bcm2711) GetFanRPM() (float64, error) {
|
||||
return bcm2711.fanRpm, nil
|
||||
func (bcm *bcm2711) Run(parentCtx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
defer cancel()
|
||||
|
||||
group := errgroup.Group{}
|
||||
|
||||
group.Go(func() error {
|
||||
defer cancel()
|
||||
return bcm.fanUnit.Run(ctx)
|
||||
})
|
||||
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
func (bcm *bcm2711) handleEdgeButtonEdge(evt gpiod.LineEvent) {
|
||||
// Despite debouncing, we still get multiple events for a single button press
|
||||
// -> This is an in-software debounce to ensure we only get one event per button press
|
||||
select {
|
||||
case bcm.edgeButtonDebounceChan <- struct{}{}:
|
||||
go func() {
|
||||
// Manually debounce the button
|
||||
<-bcm.edgeButtonDebounceChan
|
||||
time.Sleep(bcm2711DebounceInterval)
|
||||
edgeButtonEventCount.Inc()
|
||||
close(bcm.edgeButtonWatchChan)
|
||||
bcm.edgeButtonWatchChan = make(chan struct{})
|
||||
}()
|
||||
default:
|
||||
// noop
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForEdgeButtonPress blocks until the edge button has been pressed
|
||||
func (bcm *bcm2711) WaitForEdgeButtonPress(parentCtx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
defer cancel()
|
||||
|
||||
fanUnitChan := make(chan struct{})
|
||||
go func() {
|
||||
err := bcm.fanUnit.WaitForButtonPress(ctx)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).WithError(err).Error("failed to wait for button press")
|
||||
} else {
|
||||
close(fanUnitChan)
|
||||
}
|
||||
}()
|
||||
|
||||
// Either wait for the context to be cancelled or the edge button to be pressed
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-bcm.edgeButtonWatchChan:
|
||||
return nil
|
||||
case <-fanUnitChan:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (bcm *bcm2711) GetFanRPM() (float64, error) {
|
||||
rpm, err := bcm.fanUnit.FanSpeedRPM(context.TODO())
|
||||
return float64(rpm), err
|
||||
}
|
||||
|
||||
func (bcm *bcm2711) GetPowerStatus() (PowerStatus, error) {
|
||||
@@ -316,9 +338,8 @@ func (bcm *bcm2711) setPwm0Freq(targetFrequency uint64) error {
|
||||
|
||||
// SetFanSpeed sets the fanspeed of a blade in percent (standard fan unit)
|
||||
func (bcm *bcm2711) SetFanSpeed(speed uint8) error {
|
||||
fanSpeedTargetPercent.Set(float64(speed))
|
||||
bcm.setFanSpeedPWM(speed)
|
||||
return nil
|
||||
fanTargetPercent.Set(float64(speed))
|
||||
return bcm.fanUnit.SetFanSpeedPercent(context.TODO(), speed)
|
||||
}
|
||||
|
||||
func (bcm *bcm2711) setFanSpeedPWM(speed uint8) {
|
||||
@@ -365,6 +386,14 @@ func (bcm *bcm2711) SetStealthMode(enable bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (bcm *bcm2711) StealthModeActive() bool {
|
||||
val, err := bcm.stealthModeLine.Value()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return val > 0
|
||||
}
|
||||
|
||||
// serializePwmDataFrame converts a byte to a 24 bit PWM data frame for WS281x LEDs
|
||||
func serializePwmDataFrame(data uint8) uint32 {
|
||||
var result uint32 = 0
|
||||
@@ -381,11 +410,18 @@ func serializePwmDataFrame(data uint8) uint32 {
|
||||
return result
|
||||
}
|
||||
|
||||
func (bcm *bcm2711) SetLed(idx uint, color LedColor) error {
|
||||
func (bcm *bcm2711) SetLed(idx LedIndex, color led.Color) error {
|
||||
if idx >= 2 {
|
||||
return fmt.Errorf("invalid led index %d, supported: [0, 1]", idx)
|
||||
}
|
||||
|
||||
// Update the fan unit LED if the index is the same as the fan unit LED index
|
||||
if idx == LedEdge {
|
||||
if err := bcm.fanUnit.SetLed(context.TODO(), color); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
bcm.leds[idx] = color
|
||||
|
||||
return bcm.updateLEDs()
|
||||
@@ -399,8 +435,10 @@ func (bcm *bcm2711) updateLEDs() error {
|
||||
ledColorChangeEventCount.Inc()
|
||||
|
||||
// Set frequency to 3*800khz.
|
||||
// we'll bit-bang the data, so we'll need to send 3 bits per bit of data.
|
||||
bcm.setPwm0Freq(3 * 800000)
|
||||
// we'll bit-bang the data, so we'll need to send 3 bits per one bit of data.
|
||||
if err := bcm.setPwm0Freq(3 * 800000); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// WS281x Output (GPIO 18)
|
||||
|
||||
@@ -6,36 +6,45 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/spechtlabs/go-otel-utils/otelzap"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// fails if SimulatedHal does not implement ComputeBladeHal
|
||||
var _ ComputeBladeHal = &SimulatedHal{}
|
||||
|
||||
// ComputeBladeMock implements a mock for the ComputeBladeHal interface
|
||||
// SimulatedHal implements a mock for the ComputeBladeHal interface
|
||||
type SimulatedHal struct {
|
||||
logger *zap.Logger
|
||||
logger *zap.Logger
|
||||
isStealthMode bool
|
||||
}
|
||||
|
||||
func NewCm4Hal(_ ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
logger := zap.L().Named("hal").Named("simulated-cm4")
|
||||
func NewHal(_ context.Context, _ ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
logger := otelzap.L().Named("hal").Named("simulated-cm4")
|
||||
logger.Warn("Using simulated hal")
|
||||
|
||||
computeModule.WithLabelValues("simulated").Set(1)
|
||||
fanUnit.WithLabelValues("simulated").Set(1)
|
||||
|
||||
socTemperature.Set(42)
|
||||
|
||||
return &SimulatedHal{
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *SimulatedHal) Run(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (m *SimulatedHal) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SimulatedHal) SetFanSpeed(percent uint8) error {
|
||||
m.logger.Info("SetFanSpeed", zap.Uint8("percent", percent))
|
||||
fanSpeedTargetPercent.Set(float64(percent))
|
||||
fanSpeed.Set(2500 * (float64(percent) / 100))
|
||||
return nil
|
||||
}
|
||||
@@ -50,10 +59,16 @@ func (m *SimulatedHal) SetStealthMode(enabled bool) error {
|
||||
} else {
|
||||
stealthModeEnabled.Set(0)
|
||||
}
|
||||
|
||||
m.isStealthMode = enabled
|
||||
m.logger.Info("SetStealthMode", zap.Bool("enabled", enabled))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SimulatedHal) StealthModeActive() bool {
|
||||
return m.isStealthMode
|
||||
}
|
||||
|
||||
func (m *SimulatedHal) GetPowerStatus() (PowerStatus, error) {
|
||||
m.logger.Info("GetPowerStatus")
|
||||
powerStatus.WithLabelValues("simulated").Set(1)
|
||||
@@ -71,9 +86,9 @@ func (m *SimulatedHal) WaitForEdgeButtonPress(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SimulatedHal) SetLed(idx uint, color LedColor) error {
|
||||
func (m *SimulatedHal) SetLed(idx LedIndex, color led.Color) error {
|
||||
ledColorChangeEventCount.Inc()
|
||||
m.logger.Info("SetLed", zap.Uint("idx", idx), zap.Any("color", color))
|
||||
m.logger.Info("SetLed", zap.Uint("idx", uint(idx)), zap.Any("color", color))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
106
pkg/hal/hal_bcm2711_standardfanunit.go
Normal file
106
pkg/hal/hal_bcm2711_standardfanunit.go
Normal file
@@ -0,0 +1,106 @@
|
||||
//go:build linux && !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"github.com/warthog618/gpiod"
|
||||
"github.com/warthog618/gpiod/device/rpi"
|
||||
)
|
||||
|
||||
type standardFanUnitBcm2711 struct {
|
||||
GpioChip0 *gpiod.Chip
|
||||
SetFanSpeedPwmFunc func(speed uint8) error
|
||||
DisableRpmReporting bool
|
||||
|
||||
// Fan tachometer input
|
||||
fanEdgeLine *gpiod.Line
|
||||
lastFanEdgeEvent *gpiod.LineEvent
|
||||
fanRpm float64
|
||||
}
|
||||
|
||||
func (fu standardFanUnitBcm2711) Kind() FanUnitKind {
|
||||
if fu.DisableRpmReporting {
|
||||
return FanUnitKindStandardNoRPM
|
||||
}
|
||||
return FanUnitKindStandard
|
||||
}
|
||||
|
||||
func (fu standardFanUnitBcm2711) Run(ctx context.Context) error {
|
||||
var err error
|
||||
fanUnit.WithLabelValues("standard").Set(1)
|
||||
|
||||
// Register edge event handler for fan tachometer input
|
||||
if !fu.DisableRpmReporting {
|
||||
fu.fanEdgeLine, err = fu.GpioChip0.RequestLine(
|
||||
rpi.GPIO13,
|
||||
gpiod.WithEventHandler(fu.handleFanEdge),
|
||||
gpiod.WithFallingEdge,
|
||||
gpiod.WithPullUp,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(fanEdgeLine *gpiod.Line) {
|
||||
err := fanEdgeLine.Close()
|
||||
if err != nil {
|
||||
log.FromContext(ctx).WithError(err).Error("failed to close fanEdgeLine")
|
||||
}
|
||||
}(fu.fanEdgeLine)
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// handleFanEdge handles an edge event on the fan tachometer input for the standard fan unite.
|
||||
// Exponential moving average is used to smooth out the fan speed.
|
||||
func (fu *standardFanUnitBcm2711) handleFanEdge(evt gpiod.LineEvent) {
|
||||
// Ensure we're always storing the last event
|
||||
defer func() {
|
||||
fu.lastFanEdgeEvent = &evt
|
||||
}()
|
||||
|
||||
// First event, we cannot extrapolate the fan speed yet
|
||||
if fu.lastFanEdgeEvent == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate time delta between events
|
||||
delta := evt.Timestamp - fu.lastFanEdgeEvent.Timestamp
|
||||
ticksPerSecond := 1000.0 / float64(delta.Milliseconds())
|
||||
rpm := (ticksPerSecond * 60.0) / 2.0 // 2 ticks per revolution
|
||||
|
||||
// Simple moving average to smooth out the fan speed
|
||||
fu.fanRpm = (rpm * 0.1) + (fu.fanRpm * 0.9)
|
||||
fanSpeed.Set(fu.fanRpm)
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) SetFanSpeedPercent(_ context.Context, percent uint8) error {
|
||||
return fu.SetFanSpeedPwmFunc(percent)
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) SetLed(_ context.Context, _ led.Color) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) FanSpeedRPM(_ context.Context) (float64, error) {
|
||||
return fu.fanRpm, nil
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) WaitForButtonPress(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) AirFlowTemperature(_ context.Context) (float32, error) {
|
||||
return -1 * math.MaxFloat32, nil
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) Close() error {
|
||||
return nil
|
||||
}
|
||||
653
pkg/hal/hal_bcm2712.go
Normal file
653
pkg/hal/hal_bcm2712.go
Normal file
@@ -0,0 +1,653 @@
|
||||
//go:build linux && !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"github.com/warthog618/gpiod"
|
||||
"github.com/warthog618/gpiod/device/rpi"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
// RP1 southbridge is connected via PCIe on BCM2712.
|
||||
// The BAR base address is fixed by firmware at 0x1f00000000.
|
||||
rp1BarBase int64 = 0x1f00000000
|
||||
rp1GpioBase int64 = rp1BarBase + 0xd0000
|
||||
rp1Pwm0Base int64 = rp1BarBase + 0x98000
|
||||
rp1PageSize = 4096
|
||||
|
||||
// RP1 PWM input clock is 50 MHz (from device tree assigned-clock-rates)
|
||||
rp1PwmClockHz = 50_000_000
|
||||
|
||||
// RP1 GPIO register layout: each GPIO has 8 bytes (STATUS + CTRL)
|
||||
rp1GpioCtrlOffset = 0x04 // CTRL register offset within each GPIO's 8-byte block
|
||||
rp1GpioRegSize = 0x08
|
||||
rp1GpioFuncselMask = 0x1f // CTRL bits [4:0]
|
||||
|
||||
// GPIO function select values (confirmed via `pinctrl set/get` on CM5)
|
||||
// GPIO 12: funcsel 0 (a0) = PWM0_CHAN0 (fan PWM)
|
||||
// GPIO 18: funcsel 3 (a3) = PWM0_CHAN2 (WS281x LEDs)
|
||||
rp1Gpio12FuncselPwm = 0
|
||||
rp1Gpio18FuncselPwm = 3
|
||||
rp1GpioFuncselSio = 5 // Software IO (standard GPIO)
|
||||
rp1GpioFuncselNull = 0x1f // Null function (disabled)
|
||||
|
||||
// RP1 PWM register offsets (byte offsets, divide by 4 for []uint32 index)
|
||||
rp1PwmGlobalCtrl = 0x00
|
||||
rp1PwmFifoCtrl = 0x04
|
||||
rp1PwmFifoPush = 0x08
|
||||
rp1PwmFifoLevel = 0x0c
|
||||
|
||||
// Per-channel registers: base = 0x14 + channel * 0x10
|
||||
// From kernel pwm-rp1.c: CTRL(x)=0x14+x*16, RANGE(x)=0x18+x*16, DUTY(x)=0x20+x*16
|
||||
rp1PwmChanCtrlOff = 0x00 // relative to channel base
|
||||
rp1PwmChanRangeOff = 0x04
|
||||
rp1PwmChanPhaseOff = 0x08 // undocumented in kernel driver, possibly COUNT
|
||||
rp1PwmChanDutyOff = 0x0c
|
||||
rp1PwmChanSize = 0x10
|
||||
rp1PwmChanBase = 0x14 // first channel base offset (NOT 0x10)
|
||||
|
||||
// PWM global ctrl bits (from kernel pwm-rp1.c)
|
||||
rp1PwmGlobalChanEnBit = 0 // bits [3:0] = per-channel enable, BIT(x)
|
||||
rp1PwmGlobalSetUpdate = 31 // BIT(31) = global set_update trigger
|
||||
|
||||
// PWM channel ctrl bits
|
||||
rp1PwmChanCtrlModeBit = 0 // bits [1:0]
|
||||
rp1PwmChanCtrlInvertBit = 2
|
||||
rp1PwmChanCtrlUseFifoBit = 4
|
||||
rp1PwmChanCtrlFifoPopMaskBit = 7 // bits [8:7]
|
||||
|
||||
// PWM modes
|
||||
rp1PwmModeTrailingEdge = 0
|
||||
rp1PwmModeMarkSpace = 1
|
||||
rp1PwmModeSerializer = 3
|
||||
|
||||
// FIFO ctrl bits
|
||||
rp1PwmFifoFlushBit = 5
|
||||
|
||||
// Fan PWM: 25 kHz for Noctua fans
|
||||
// RANGE = 50 MHz / 25 kHz = 2000
|
||||
rp1FanPwmRange = rp1PwmClockHz / 25000
|
||||
|
||||
bcm2712SmartFanUnitDev = "/dev/ttyAMA4" // UART4 on BCM2712 (same GPIO 12/13 pins as UART5 on BCM2711)
|
||||
bcm2712ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
|
||||
bcm2712DebounceInterval = 100 * time.Millisecond
|
||||
|
||||
// WS281x LED encoding for RP1 at 50MHz with RANGE=32 serializer mode.
|
||||
// Each WS281x data bit is encoded as 2 × 32-bit FIFO words (1280ns per bit ≈ 781kHz).
|
||||
// Channel 2 on GPIO 18 is used independently from fan PWM on channel 0.
|
||||
rp1Ws281xChan = 2 // PWM0 channel 2 on GPIO 18
|
||||
rp1Ws281xRange = 32 // full 32-bit word in serializer mode
|
||||
|
||||
// "0" bit: T0H=400ns (20 high bits), T0L=880ns (44 low bits)
|
||||
rp1Ws281xBit0Word0 uint32 = 0xFFFFF000
|
||||
rp1Ws281xBit0Word1 uint32 = 0x00000000
|
||||
// "1" bit: T1H=800ns (40 high bits), T1L=480ns (24 low bits)
|
||||
rp1Ws281xBit1Word0 uint32 = 0xFFFFFFFF
|
||||
rp1Ws281xBit1Word1 uint32 = 0xFF000000
|
||||
|
||||
// Reset: >50μs of low. At 640ns/word, 80 words = 51.2μs.
|
||||
rp1Ws281xResetWords = 80
|
||||
// Conservative FIFO depth assumption (BCM2835 has 16, RP1 may differ)
|
||||
rp1Ws281xFifoMax uint32 = 8
|
||||
// Safety timeout for FIFO operations
|
||||
rp1Ws281xTimeout = 5 * time.Millisecond
|
||||
)
|
||||
|
||||
// pwmChanRegIdx returns the []uint32 index for a per-channel register.
|
||||
func pwmChanRegIdx(channel, regOffset int) int {
|
||||
return (rp1PwmChanBase + channel*rp1PwmChanSize + regOffset) / 4
|
||||
}
|
||||
|
||||
type bcm2712 struct {
|
||||
opts ComputeBladeHalOpts
|
||||
|
||||
wrMutex sync.Mutex
|
||||
|
||||
currFanSpeed uint8
|
||||
|
||||
devmem *os.File
|
||||
gpioMem8 []uint8
|
||||
gpioMem []uint32
|
||||
pwmMem8 []uint8
|
||||
pwmMem []uint32
|
||||
|
||||
gpioChip0 *gpiod.Chip
|
||||
|
||||
// WS281x LED colors (top + edge)
|
||||
leds [2]led.Color
|
||||
|
||||
// Stealth mode output
|
||||
stealthModeLine *gpiod.Line
|
||||
|
||||
// Edge button input
|
||||
edgeButtonLine *gpiod.Line
|
||||
edgeButtonDebounceChan chan struct{}
|
||||
edgeButtonWatchChan chan struct{}
|
||||
|
||||
// PoE detection input
|
||||
poeLine *gpiod.Line
|
||||
|
||||
// Fan unit
|
||||
fanUnit FanUnit
|
||||
}
|
||||
|
||||
func newBcm2712Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
devmem, err := os.OpenFile("/dev/mem", os.O_RDWR|os.O_SYNC, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open /dev/mem: %w", err)
|
||||
}
|
||||
|
||||
gpioChip0, err := gpiod.NewChip("gpiochip0")
|
||||
if err != nil {
|
||||
_ = devmem.Close()
|
||||
return nil, fmt.Errorf("failed to open gpiochip0: %w", err)
|
||||
}
|
||||
|
||||
// Memory-map RP1 GPIO bank 0 (GPIOs 0-27)
|
||||
gpioMem, gpioMem8, err := mmap(devmem, rp1GpioBase, rp1PageSize)
|
||||
if err != nil {
|
||||
_ = gpioChip0.Close()
|
||||
_ = devmem.Close()
|
||||
return nil, fmt.Errorf("failed to mmap RP1 GPIO at 0x%x: %w", rp1GpioBase, err)
|
||||
}
|
||||
|
||||
// Memory-map RP1 PWM0
|
||||
pwmMem, pwmMem8, err := mmap(devmem, rp1Pwm0Base, rp1PageSize)
|
||||
if err != nil {
|
||||
_ = syscall.Munmap(gpioMem8)
|
||||
_ = gpioChip0.Close()
|
||||
_ = devmem.Close()
|
||||
return nil, fmt.Errorf("failed to mmap RP1 PWM0 at 0x%x: %w", rp1Pwm0Base, err)
|
||||
}
|
||||
|
||||
bcm := &bcm2712{
|
||||
devmem: devmem,
|
||||
gpioMem: gpioMem,
|
||||
gpioMem8: gpioMem8,
|
||||
pwmMem: pwmMem,
|
||||
pwmMem8: pwmMem8,
|
||||
gpioChip0: gpioChip0,
|
||||
opts: opts,
|
||||
edgeButtonDebounceChan: make(chan struct{}, 1),
|
||||
edgeButtonWatchChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
computeModule.WithLabelValues("cm5").Set(1)
|
||||
|
||||
log.FromContext(ctx).Info("starting hal setup", zap.String("hal", "bcm2712"))
|
||||
if err := bcm.setup(ctx); err != nil {
|
||||
_ = bcm.Close()
|
||||
return nil, err
|
||||
}
|
||||
return bcm, nil
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) Close() error {
|
||||
errs := errors.Join(
|
||||
bcm.closeFanUnit(),
|
||||
bcm.unmapMem(),
|
||||
bcm.closeDevmem(),
|
||||
bcm.closeGpio(),
|
||||
)
|
||||
return errs
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) closeFanUnit() error {
|
||||
if bcm.fanUnit != nil {
|
||||
return bcm.fanUnit.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) unmapMem() error {
|
||||
return errors.Join(
|
||||
munmapIfNonNil(bcm.gpioMem8),
|
||||
munmapIfNonNil(bcm.pwmMem8),
|
||||
)
|
||||
}
|
||||
|
||||
func munmapIfNonNil(mem []uint8) error {
|
||||
if mem != nil {
|
||||
return syscall.Munmap(mem)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) closeDevmem() error {
|
||||
if bcm.devmem != nil {
|
||||
return bcm.devmem.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) closeGpio() error {
|
||||
var errs []error
|
||||
if bcm.gpioChip0 != nil {
|
||||
errs = append(errs, bcm.gpioChip0.Close())
|
||||
}
|
||||
if bcm.poeLine != nil {
|
||||
errs = append(errs, bcm.poeLine.Close())
|
||||
}
|
||||
if bcm.stealthModeLine != nil {
|
||||
errs = append(errs, bcm.stealthModeLine.Close())
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// setGpioFuncsel sets the function select for a GPIO pin via direct register write.
|
||||
func (bcm *bcm2712) setGpioFuncsel(gpio int, funcsel uint32) {
|
||||
ctrlIdx := (gpio*rp1GpioRegSize + rp1GpioCtrlOffset) / 4
|
||||
ctrl := bcm.gpioMem[ctrlIdx]
|
||||
ctrl = (ctrl &^ uint32(rp1GpioFuncselMask)) | (funcsel & uint32(rp1GpioFuncselMask))
|
||||
bcm.gpioMem[ctrlIdx] = ctrl
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) setup(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// Register edge event handler for edge button (GPIO 20)
|
||||
bcm.edgeButtonLine, err = bcm.gpioChip0.RequestLine(
|
||||
rpi.GPIO20, gpiod.WithEventHandler(bcm.handleEdgeButtonEdge),
|
||||
gpiod.WithFallingEdge, gpiod.WithPullUp, gpiod.WithDebounce(50*time.Millisecond))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to request GPIO20 (edge button): %w", err)
|
||||
}
|
||||
|
||||
// Register input for PoE detection (GPIO 23)
|
||||
bcm.poeLine, err = bcm.gpioChip0.RequestLine(rpi.GPIO23, gpiod.AsInput, gpiod.WithPullUp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to request GPIO23 (PoE detect): %w", err)
|
||||
}
|
||||
|
||||
// Register output for stealth mode (GPIO 21)
|
||||
bcm.stealthModeLine, err = bcm.gpioChip0.RequestLine(rpi.GPIO21, gpiod.AsOutput(1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to request GPIO21 (stealth mode): %w", err)
|
||||
}
|
||||
|
||||
// Detect fan unit type
|
||||
log.FromContext(ctx).Info("detecting fan unit")
|
||||
detectCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if smartFanUnitPresent, err := SmartFanUnitPresent(detectCtx, bcm2712SmartFanUnitDev); err == nil && smartFanUnitPresent {
|
||||
log.FromContext(ctx).Info("detected smart fan unit")
|
||||
bcm.fanUnit, err = NewSmartFanUnit(bcm2712SmartFanUnitDev)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create smart fan unit: %w", err)
|
||||
}
|
||||
} else {
|
||||
log.FromContext(ctx).WithError(err).Info("no smart fan unit detected, assuming standard fan unit")
|
||||
|
||||
// Set GPIO 12 to PWM0_CHAN0 function
|
||||
bcm.setGpioFuncsel(12, rp1Gpio12FuncselPwm)
|
||||
|
||||
// Initialize PWM0 channel 0 for fan control (25 kHz mark-space)
|
||||
bcm.initFanPwm()
|
||||
|
||||
bcm.fanUnit = &standardFanUnitBcm2711{
|
||||
GpioChip0: bcm.gpioChip0,
|
||||
DisableRpmReporting: !bcm.opts.RpmReportingStandardFanUnit,
|
||||
SetFanSpeedPwmFunc: func(speed uint8) error {
|
||||
bcm.setFanSpeedPWM(speed)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initFanPwm configures PWM0 channel 0 in mark-space mode at 25 kHz.
|
||||
func (bcm *bcm2712) initFanPwm() {
|
||||
ch := 0
|
||||
|
||||
// Disable channel 0
|
||||
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Configure channel 0: mark-space mode, no FIFO, no invert
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanCtrlOff)] = uint32(rp1PwmModeMarkSpace) << rp1PwmChanCtrlModeBit
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Set range (period) for 25 kHz
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanRangeOff)] = rp1FanPwmRange
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Set initial duty to 0 (fan off)
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanDutyOff)] = 0
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Phase = 0
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanPhaseOff)] = 0
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Trigger update for channel 0
|
||||
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Enable channel 0
|
||||
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl |= (1 << (rp1PwmGlobalChanEnBit + ch))
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) Run(parentCtx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
defer cancel()
|
||||
|
||||
group := errgroup.Group{}
|
||||
|
||||
group.Go(func() error {
|
||||
defer cancel()
|
||||
return bcm.fanUnit.Run(ctx)
|
||||
})
|
||||
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) handleEdgeButtonEdge(evt gpiod.LineEvent) {
|
||||
select {
|
||||
case bcm.edgeButtonDebounceChan <- struct{}{}:
|
||||
go func() {
|
||||
<-bcm.edgeButtonDebounceChan
|
||||
time.Sleep(bcm2712DebounceInterval)
|
||||
edgeButtonEventCount.Inc()
|
||||
close(bcm.edgeButtonWatchChan)
|
||||
bcm.edgeButtonWatchChan = make(chan struct{})
|
||||
}()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) WaitForEdgeButtonPress(parentCtx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
defer cancel()
|
||||
|
||||
fanUnitChan := make(chan struct{})
|
||||
go func() {
|
||||
err := bcm.fanUnit.WaitForButtonPress(ctx)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).WithError(err).Error("failed to wait for button press")
|
||||
} else {
|
||||
close(fanUnitChan)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-bcm.edgeButtonWatchChan:
|
||||
return nil
|
||||
case <-fanUnitChan:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) GetFanRPM() (float64, error) {
|
||||
rpm, err := bcm.fanUnit.FanSpeedRPM(context.TODO())
|
||||
return float64(rpm), err
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) GetPowerStatus() (PowerStatus, error) {
|
||||
val, err := bcm.poeLine.Value()
|
||||
if err != nil {
|
||||
return PowerPoeOrUsbC, err
|
||||
}
|
||||
|
||||
if val > 0 {
|
||||
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(1)
|
||||
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(0)
|
||||
return PowerPoe802at, nil
|
||||
}
|
||||
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(0)
|
||||
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(1)
|
||||
return PowerPoeOrUsbC, nil
|
||||
}
|
||||
|
||||
// SetFanSpeed sets the fan speed in percent.
|
||||
func (bcm *bcm2712) SetFanSpeed(speed uint8) error {
|
||||
fanTargetPercent.Set(float64(speed))
|
||||
return bcm.fanUnit.SetFanSpeedPercent(context.TODO(), speed)
|
||||
}
|
||||
|
||||
// setFanSpeedPWM sets the fan PWM duty cycle using RP1 PWM0 channel 0 in mark-space mode.
|
||||
func (bcm *bcm2712) setFanSpeedPWM(speed uint8) {
|
||||
ch := 0
|
||||
|
||||
var duty uint32
|
||||
if speed == 0 {
|
||||
duty = 0
|
||||
} else if speed <= 100 {
|
||||
duty = uint32(float64(rp1FanPwmRange) * float64(speed) / 100.0)
|
||||
} else {
|
||||
duty = rp1FanPwmRange
|
||||
}
|
||||
|
||||
// Update duty cycle
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanDutyOff)] = duty
|
||||
|
||||
// Trigger update
|
||||
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
|
||||
bcm.currFanSpeed = speed
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) SetStealthMode(enable bool) error {
|
||||
if enable {
|
||||
stealthModeEnabled.Set(1)
|
||||
return bcm.stealthModeLine.SetValue(1)
|
||||
}
|
||||
stealthModeEnabled.Set(0)
|
||||
return bcm.stealthModeLine.SetValue(0)
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) StealthModeActive() bool {
|
||||
val, err := bcm.stealthModeLine.Value()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return val > 0
|
||||
}
|
||||
|
||||
// SetLed sets the WS281x LED color via RP1 PWM0 channel 2 serializer mode.
|
||||
// Unlike BCM2711 which shares PWM0 between fan and LEDs, RP1 has independent channels:
|
||||
// channel 0 (GPIO 12) handles fan PWM, channel 2 (GPIO 18) handles WS281x LEDs.
|
||||
func (bcm *bcm2712) SetLed(idx LedIndex, color led.Color) error {
|
||||
if idx >= 2 {
|
||||
return fmt.Errorf("invalid led index %d, supported: [0, 1]", idx)
|
||||
}
|
||||
|
||||
// Update the fan unit LED if this is the edge LED
|
||||
if idx == LedEdge {
|
||||
if err := bcm.fanUnit.SetLed(context.TODO(), color); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
bcm.leds[idx] = color
|
||||
|
||||
return bcm.updateLEDs()
|
||||
}
|
||||
|
||||
// updateLEDs sends WS281x data via RP1 PWM0 channel 2 serializer mode.
|
||||
// Uses the shared FIFO (channel 0 fan PWM uses mark-space mode without FIFO, so no conflict).
|
||||
// The RP1 PWM clock is fixed at 50MHz, so we use 2 FIFO words per WS281x data bit
|
||||
// at RANGE=32 to achieve ~1280ns per bit (within WS281x tolerance).
|
||||
func (bcm *bcm2712) updateLEDs() error {
|
||||
bcm.wrMutex.Lock()
|
||||
defer bcm.wrMutex.Unlock()
|
||||
|
||||
ledColorChangeEventCount.Inc()
|
||||
|
||||
ch := rp1Ws281xChan
|
||||
|
||||
// Build complete FIFO data stream
|
||||
data := bcm.buildWs281xStream()
|
||||
|
||||
// Set GPIO 18 to PWM0_CH2 function
|
||||
bcm.setGpioFuncsel(18, rp1Gpio18FuncselPwm)
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Disable channel 2
|
||||
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Configure channel 2: serializer mode, use FIFO, SBIT=0 (low when idle)
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanCtrlOff)] =
|
||||
(rp1PwmModeSerializer << rp1PwmChanCtrlModeBit) | (1 << rp1PwmChanCtrlUseFifoBit)
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanRangeOff)] = rp1Ws281xRange
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanPhaseOff)] = 0
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Flush FIFO
|
||||
bcm.pwmMem[rp1PwmFifoCtrl/4] = (1 << rp1PwmFifoFlushBit)
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Trigger update for channel 2
|
||||
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Pre-fill FIFO before enabling channel
|
||||
idx := 0
|
||||
for idx < len(data) && uint32(idx) < rp1Ws281xFifoMax {
|
||||
bcm.pwmMem[rp1PwmFifoPush/4] = data[idx]
|
||||
idx++
|
||||
}
|
||||
|
||||
// Lock OS thread for tight FIFO feeding
|
||||
runtime.LockOSThread()
|
||||
|
||||
// Enable channel 2
|
||||
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl |= (1 << (rp1PwmGlobalChanEnBit + ch))
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
|
||||
// Push remaining data, polling FIFO level to avoid overflow
|
||||
fifoLevelReg := rp1PwmFifoLevel / 4
|
||||
fifoPushReg := rp1PwmFifoPush / 4
|
||||
deadline := time.Now().Add(rp1Ws281xTimeout)
|
||||
|
||||
for idx < len(data) {
|
||||
if time.Now().After(deadline) {
|
||||
break
|
||||
}
|
||||
if bcm.pwmMem[fifoLevelReg] < rp1Ws281xFifoMax {
|
||||
bcm.pwmMem[fifoPushReg] = data[idx]
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for FIFO to drain
|
||||
for bcm.pwmMem[fifoLevelReg] > 0 {
|
||||
if time.Now().After(deadline) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
runtime.UnlockOSThread()
|
||||
|
||||
// Wait for last word to finish shifting out
|
||||
time.Sleep(200 * time.Microsecond)
|
||||
|
||||
// Disable channel 2
|
||||
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
|
||||
// Disconnect GPIO 18 from PWM to prevent residual noise on the data line
|
||||
bcm.setGpioFuncsel(18, rp1GpioFuncselNull)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildWs281xStream constructs the complete FIFO word stream for both WS281x LEDs.
|
||||
// Format: [reset padding] [top LED RGB] [edge LED RGB] [trailing zeros]
|
||||
func (bcm *bcm2712) buildWs281xStream() []uint32 {
|
||||
// Pre-allocate: 80 reset + 96 data (2 LEDs × 3 bytes × 16 words) + 2 trailing
|
||||
words := make([]uint32, 0, rp1Ws281xResetWords+96+2)
|
||||
|
||||
// Reset padding (>50μs of low signal)
|
||||
for i := 0; i < rp1Ws281xResetWords; i++ {
|
||||
words = append(words, 0)
|
||||
}
|
||||
|
||||
// Top LED (index 0) - RGB order (matches BCM2711 upstream)
|
||||
words = appendByteWs281x(words, bcm.leds[0].Red)
|
||||
words = appendByteWs281x(words, bcm.leds[0].Green)
|
||||
words = appendByteWs281x(words, bcm.leds[0].Blue)
|
||||
|
||||
// Edge LED (index 1)
|
||||
words = appendByteWs281x(words, bcm.leds[1].Red)
|
||||
words = appendByteWs281x(words, bcm.leds[1].Green)
|
||||
words = appendByteWs281x(words, bcm.leds[1].Blue)
|
||||
|
||||
// Trailing zeros for clean end-of-frame
|
||||
words = append(words, 0, 0)
|
||||
|
||||
return words
|
||||
}
|
||||
|
||||
// appendByteWs281x encodes one byte as 16 FIFO words (2 per data bit, MSB first) for RP1 WS281x.
|
||||
func appendByteWs281x(words []uint32, b uint8) []uint32 {
|
||||
for i := 7; i >= 0; i-- {
|
||||
if (b>>uint(i))&1 == 0 {
|
||||
words = append(words, rp1Ws281xBit0Word0, rp1Ws281xBit0Word1)
|
||||
} else {
|
||||
words = append(words, rp1Ws281xBit1Word0, rp1Ws281xBit1Word1)
|
||||
}
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
// GetTemperature returns the SoC temperature in degrees Celsius.
|
||||
func (bcm *bcm2712) GetTemperature() (float64, error) {
|
||||
f, err := os.Open(bcm2712ThermalZonePath)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
raw, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
cpuTemp, err := strconv.Atoi(strings.TrimSpace(string(raw)))
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
temp := float64(cpuTemp) / 1000.0
|
||||
socTemperature.Set(temp)
|
||||
|
||||
return temp, nil
|
||||
}
|
||||
@@ -3,17 +3,23 @@ package hal
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// fails if ComputeBladeHalMock does not implement ComputeBladeHal
|
||||
var _ ComputeBladeHal = &ComputeBladeHalMock{}
|
||||
|
||||
// ComputeBladeMock implements a mock for the ComputeBladeHal interface
|
||||
// ComputeBladeHalMock implements a mock for the ComputeBladeHal interface
|
||||
type ComputeBladeHalMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *ComputeBladeHalMock) Run(ctx context.Context) error {
|
||||
args := m.Called(ctx)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *ComputeBladeHalMock) Close() error {
|
||||
args := m.Called()
|
||||
return args.Error(0)
|
||||
@@ -34,6 +40,11 @@ func (m *ComputeBladeHalMock) SetStealthMode(enabled bool) error {
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *ComputeBladeHalMock) StealthModeActive() bool {
|
||||
args := m.Called()
|
||||
return args.Bool(0)
|
||||
}
|
||||
|
||||
func (m *ComputeBladeHalMock) GetPowerStatus() (PowerStatus, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(PowerStatus), args.Error(1)
|
||||
@@ -44,7 +55,7 @@ func (m *ComputeBladeHalMock) WaitForEdgeButtonPress(ctx context.Context) error
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *ComputeBladeHalMock) SetLed(idx uint, color LedColor) error {
|
||||
func (m *ComputeBladeHalMock) SetLed(idx LedIndex, color led.Color) error {
|
||||
args := m.Called(idx, color)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
178
pkg/hal/hal_rk3588.go
Normal file
178
pkg/hal/hal_rk3588.go
Normal file
@@ -0,0 +1,178 @@
|
||||
//go:build linux && !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
rk3588ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
|
||||
rk3588PwmFanHwmonName = "pwmfan"
|
||||
)
|
||||
|
||||
// rk3588 implements the ComputeBladeHal interface for the Rockchip RK3588 (Radxa CM5).
|
||||
// Fan control uses the kernel's pwmfan driver via sysfs. GPIO-dependent features
|
||||
// (button, stealth hardware, PoE detection, LEDs, tachometer) are stubbed until the
|
||||
// Rockchip-to-B2B-connector pin mapping is determined.
|
||||
type rk3588 struct {
|
||||
opts ComputeBladeHalOpts
|
||||
pwmPath string // e.g. /sys/class/hwmon/hwmon8/pwm1
|
||||
pwmEnablePath string // e.g. /sys/class/hwmon/hwmon8/pwm1_enable
|
||||
stealthMode bool
|
||||
}
|
||||
|
||||
// Compile-time interface check
|
||||
var _ ComputeBladeHal = &rk3588{}
|
||||
|
||||
func newRk3588Hal(ctx context.Context, opts ComputeBladeHalOpts) (*rk3588, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
pwmPath, err := findHwmonPwm(rk3588PwmFanHwmonName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find pwmfan hwmon device: %w", err)
|
||||
}
|
||||
|
||||
enablePath := pwmPath + "_enable"
|
||||
|
||||
// Set manual control mode (1 = manual PWM control)
|
||||
if err := os.WriteFile(enablePath, []byte("1"), 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to set pwm1_enable to manual mode: %w", err)
|
||||
}
|
||||
|
||||
computeModule.WithLabelValues("radxa-cm5").Set(1)
|
||||
|
||||
logger.Info("starting hal setup", zap.String("hal", "rk3588"))
|
||||
logger.Warn("GPIO pin mapping unknown for RK3588 B2B connector — button, stealth hardware, PoE detection, LEDs, and tachometer are stubbed")
|
||||
|
||||
return &rk3588{
|
||||
opts: opts,
|
||||
pwmPath: pwmPath,
|
||||
pwmEnablePath: enablePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rk *rk3588) Run(ctx context.Context) error {
|
||||
fanUnit.WithLabelValues("sysfs").Set(1)
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (rk *rk3588) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFanSpeed sets the fan speed via sysfs pwm1 (0-100% mapped to 0-255).
|
||||
func (rk *rk3588) SetFanSpeed(speed uint8) error {
|
||||
fanTargetPercent.Set(float64(speed))
|
||||
|
||||
var pwmVal uint8
|
||||
if speed == 0 {
|
||||
pwmVal = 0
|
||||
} else if speed >= 100 {
|
||||
pwmVal = 255
|
||||
} else {
|
||||
pwmVal = uint8(float64(speed) * 255.0 / 100.0)
|
||||
}
|
||||
|
||||
return os.WriteFile(rk.pwmPath, []byte(strconv.Itoa(int(pwmVal))), 0644)
|
||||
}
|
||||
|
||||
// GetFanRPM returns 0 — no tachometer GPIO is mapped on the RK3588.
|
||||
func (rk *rk3588) GetFanRPM() (float64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// GetTemperature returns the SoC temperature in degrees Celsius.
|
||||
func (rk *rk3588) GetTemperature() (float64, error) {
|
||||
f, err := os.Open(rk3588ThermalZonePath)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
raw, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
cpuTemp, err := strconv.Atoi(strings.TrimSpace(string(raw)))
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
temp := float64(cpuTemp) / 1000.0
|
||||
socTemperature.Set(temp)
|
||||
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
// SetStealthMode tracks stealth mode in software only (no GPIO mapped).
|
||||
func (rk *rk3588) SetStealthMode(enabled bool) error {
|
||||
rk.stealthMode = enabled
|
||||
if enabled {
|
||||
stealthModeEnabled.Set(1)
|
||||
} else {
|
||||
stealthModeEnabled.Set(0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rk *rk3588) StealthModeActive() bool {
|
||||
return rk.stealthMode
|
||||
}
|
||||
|
||||
// SetLed is a no-op — WS281x LED GPIO pin mapping is unknown on RK3588.
|
||||
func (rk *rk3588) SetLed(idx LedIndex, color led.Color) error {
|
||||
ledColorChangeEventCount.Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPowerStatus returns PowerPoeOrUsbC as a safe default (no PoE detection GPIO mapped).
|
||||
func (rk *rk3588) GetPowerStatus() (PowerStatus, error) {
|
||||
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(0)
|
||||
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(1)
|
||||
return PowerPoeOrUsbC, nil
|
||||
}
|
||||
|
||||
// WaitForEdgeButtonPress blocks until context cancellation (no button GPIO mapped).
|
||||
func (rk *rk3588) WaitForEdgeButtonPress(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// findHwmonPwm scans /sys/class/hwmon/hwmon*/name for a device matching the given name
|
||||
// and returns the path to its pwm1 file.
|
||||
func findHwmonPwm(name string) (string, error) {
|
||||
matches, err := filepath.Glob("/sys/class/hwmon/hwmon*/name")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to glob hwmon devices: %w", err)
|
||||
}
|
||||
|
||||
for _, namePath := range matches {
|
||||
raw, err := os.ReadFile(namePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(string(raw)) == name {
|
||||
dir := filepath.Dir(namePath)
|
||||
pwmPath := filepath.Join(dir, "pwm1")
|
||||
if _, err := os.Stat(pwmPath); err != nil {
|
||||
return "", fmt.Errorf("found %s hwmon at %s but pwm1 does not exist: %w", name, dir, err)
|
||||
}
|
||||
return pwmPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no hwmon device found with name %q", name)
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
//go:build linux
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func mmap(file *os.File, base int64, lenght int) ([]uint32, []uint8, error) {
|
||||
func mmap(file *os.File, base int64, length int) ([]uint32, []uint8, error) {
|
||||
mem8, err := syscall.Mmap(
|
||||
int(file.Fd()),
|
||||
base,
|
||||
lenght,
|
||||
length,
|
||||
syscall.PROT_READ|syscall.PROT_WRITE,
|
||||
syscall.MAP_SHARED,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// We'll have to work with 32 bit registers, so let's convert it.
|
||||
header := *(*reflect.SliceHeader)(unsafe.Pointer(&mem8))
|
||||
header.Len /= (32 / 8)
|
||||
header.Cap /= (32 / 8)
|
||||
mem32 := *(*[]uint32)(unsafe.Pointer(&header))
|
||||
|
||||
// Convert []uint8 to []uint32 using unsafe.Slice
|
||||
ptr := unsafe.Pointer(&mem8[0])
|
||||
mem32 := unsafe.Slice((*uint32)(ptr), len(mem8)/4)
|
||||
|
||||
return mem32, mem8, nil
|
||||
}
|
||||
|
||||
7
pkg/hal/led/types.go
Normal file
7
pkg/hal/led/types.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package led
|
||||
|
||||
type Color struct {
|
||||
Red uint8 `mapstructure:"red"`
|
||||
Green uint8 `mapstructure:"green"`
|
||||
Blue uint8 `mapstructure:"blue"`
|
||||
}
|
||||
61
pkg/hal/metrics.go
Normal file
61
pkg/hal/metrics.go
Normal file
@@ -0,0 +1,61 @@
|
||||
//go:build !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
fanTargetPercent = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_target_percent",
|
||||
Help: "Target fan speed in percent",
|
||||
})
|
||||
fanSpeed = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_speed",
|
||||
Help: "Fan speed in RPM",
|
||||
})
|
||||
socTemperature = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "soc_temperature",
|
||||
Help: "SoC temperature in °C",
|
||||
})
|
||||
airFlowTemperature = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "airflow_temperature",
|
||||
Help: "airflow temperature in °C",
|
||||
})
|
||||
computeModule = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "compute_module_present",
|
||||
Help: "Compute module type",
|
||||
}, []string{"type"})
|
||||
ledColorChangeEventCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "led_color_change_event_count",
|
||||
Help: "Led color change event_count",
|
||||
})
|
||||
powerStatus = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "power_status",
|
||||
Help: "Power status of the blade",
|
||||
}, []string{"type"})
|
||||
stealthModeEnabled = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "stealth_mode_enabled",
|
||||
Help: "Stealth mode enabled",
|
||||
})
|
||||
fanUnit = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_unit",
|
||||
Help: "Fan unit",
|
||||
}, []string{"type"})
|
||||
edgeButtonEventCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "edge_button_event_count",
|
||||
Help: "Number of edge button presses",
|
||||
})
|
||||
)
|
||||
39
pkg/hal/platform_linux.go
Normal file
39
pkg/hal/platform_linux.go
Normal file
@@ -0,0 +1,39 @@
|
||||
//go:build linux && !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const deviceTreeCompatiblePath = "/sys/firmware/devicetree/base/compatible"
|
||||
|
||||
// NewHal creates the appropriate HAL implementation based on the detected platform.
|
||||
// It reads the device tree compatible string to determine whether the SoC is a
|
||||
// BCM2711 (CM4/Pi 4) or BCM2712 (CM5/Pi 5).
|
||||
func NewHal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
compatible, err := os.ReadFile(deviceTreeCompatiblePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read device tree compatible string: %w", err)
|
||||
}
|
||||
|
||||
compatStr := string(compatible)
|
||||
log.FromContext(ctx).Info("detected platform", zap.String("compatible", strings.ReplaceAll(compatStr, "\x00", ", ")))
|
||||
|
||||
switch {
|
||||
case strings.Contains(compatStr, "bcm2712"):
|
||||
return newBcm2712Hal(ctx, opts)
|
||||
case strings.Contains(compatStr, "bcm2711"):
|
||||
return newBcm2711Hal(ctx, opts)
|
||||
case strings.Contains(compatStr, "rockchip,rk3588"):
|
||||
return newRk3588Hal(ctx, opts)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported platform: %s", strings.ReplaceAll(compatStr, "\x00", ", "))
|
||||
}
|
||||
}
|
||||
211
pkg/hal/smartfanunit.go
Normal file
211
pkg/hal/smartfanunit.go
Normal file
@@ -0,0 +1,211 @@
|
||||
//go:build !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
|
||||
"go.bug.st/serial"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func SmartFanUnitPresent(ctx context.Context, portName string) (bool, error) {
|
||||
// Open the serial port.
|
||||
log.FromContext(ctx).Info("Opening serial port")
|
||||
|
||||
rwc, err := serial.Open(portName, &serial.Mode{
|
||||
BaudRate: smartfanunit.BaudRate,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
log.FromContext(ctx).Info("Opened serial port")
|
||||
defer func(rwc serial.Port) {
|
||||
err := rwc.Close()
|
||||
if err != nil {
|
||||
log.FromContext(ctx).WithError(err).Warn("Error while closing serial port")
|
||||
}
|
||||
}(rwc)
|
||||
|
||||
// Close reader after context is done
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
log.FromContext(ctx).Warn("Closing serial port")
|
||||
err := rwc.Close()
|
||||
if err != nil {
|
||||
log.FromContext(ctx).WithError(err).Warn("Error while closing serial port")
|
||||
}
|
||||
}()
|
||||
|
||||
// read byte after byte, matching it to the SOF header used by the smart fan unit protocol.
|
||||
// -> if that's present, we have a smart fanunit connected.
|
||||
for {
|
||||
b := make([]byte, 1)
|
||||
log.FromContext(ctx).Info("Waiting for next byte from serial port")
|
||||
_, err := rwc.Read(b)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b[0] == proto.SOF {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewSmartFanUnit(portName string) (FanUnit, error) {
|
||||
// Open the serial port.
|
||||
rwc, err := serial.Open(portName, &serial.Mode{
|
||||
BaudRate: smartfanunit.BaudRate,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &smartFanUnit{
|
||||
rwc: rwc,
|
||||
eb: events.New(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
//var ErrCommunicationFailed = errors.New("communication failed") // FIXME: still required or dead code?
|
||||
|
||||
const (
|
||||
inboundTopic = "smartfanunit:inbound"
|
||||
//outboundTopic = "smartfanunit:outbound" // FIXME: still required or dead code?
|
||||
)
|
||||
|
||||
type smartFanUnit struct {
|
||||
rwc io.ReadWriteCloser
|
||||
mu sync.Mutex // write mutex
|
||||
|
||||
speed smartfanunit.FanSpeedRPMPacket
|
||||
airflow smartfanunit.AirFlowTemperaturePacket
|
||||
|
||||
eb events.EventBus
|
||||
}
|
||||
|
||||
func (fuc *smartFanUnit) Kind() FanUnitKind {
|
||||
return FanUnitKindSmart
|
||||
}
|
||||
|
||||
// Run the client with event loop
|
||||
func (fuc *smartFanUnit) Run(parentCtx context.Context) error {
|
||||
fanUnit.WithLabelValues("smart").Set(1)
|
||||
|
||||
ctx, cancel := context.WithCancelCause(parentCtx)
|
||||
defer cancel(nil)
|
||||
|
||||
wg := errgroup.Group{}
|
||||
|
||||
// Start read loop
|
||||
wg.Go(func() error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
pkt, err := proto.ReadPacket(ctx, fuc.rwc)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).WithError(err).Error("Failed to read packet from serial port")
|
||||
continue
|
||||
}
|
||||
fuc.eb.Publish(inboundTopic, pkt)
|
||||
}
|
||||
})
|
||||
|
||||
// Subscribe to fan speed updates
|
||||
wg.Go(func() error {
|
||||
sub := fuc.eb.Subscribe(inboundTopic, 1, smartfanunit.MatchCmd(smartfanunit.NotifyFanSpeedRPM))
|
||||
defer sub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case pktAny := <-sub.C():
|
||||
rawPkt := pktAny.(proto.Packet)
|
||||
if err := fuc.speed.FromPacket(rawPkt); err != nil && !errors.Is(err, proto.ErrChecksumMismatch) {
|
||||
return err
|
||||
}
|
||||
fanSpeed.Set(float64(fuc.speed.RPM))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Subscribe to air flow temperature updates
|
||||
wg.Go(func() error {
|
||||
sub := fuc.eb.Subscribe(inboundTopic, 1, smartfanunit.MatchCmd(smartfanunit.NotifyAirFlowTemperature))
|
||||
defer sub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case pktAny := <-sub.C():
|
||||
rawPkt := pktAny.(proto.Packet)
|
||||
if err := fuc.airflow.FromPacket(rawPkt); err != nil && !errors.Is(err, proto.ErrChecksumMismatch) {
|
||||
return err
|
||||
}
|
||||
airFlowTemperature.Set(float64(fuc.airflow.Temperature))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return wg.Wait()
|
||||
}
|
||||
|
||||
func (fuc *smartFanUnit) write(ctx context.Context, pktGen smartfanunit.PacketGenerator) error {
|
||||
fuc.mu.Lock()
|
||||
defer fuc.mu.Unlock()
|
||||
return proto.WritePacket(ctx, fuc.rwc, pktGen.Packet())
|
||||
}
|
||||
|
||||
// SetFanSpeedPercent sets the fan speed in percent.
|
||||
func (fuc *smartFanUnit) SetFanSpeedPercent(ctx context.Context, percent uint8) error {
|
||||
return fuc.write(ctx, &smartfanunit.SetFanSpeedPercentPacket{Percent: percent})
|
||||
}
|
||||
|
||||
// SetLed sets the LED color.
|
||||
func (fuc *smartFanUnit) SetLed(ctx context.Context, color led.Color) error {
|
||||
return fuc.write(ctx, &smartfanunit.SetLEDPacket{Color: color})
|
||||
}
|
||||
|
||||
// FanSpeedRPM returns the current fan speed in rotations per minute.
|
||||
func (fuc *smartFanUnit) FanSpeedRPM(_ context.Context) (float64, error) {
|
||||
return float64(fuc.speed.RPM), nil
|
||||
}
|
||||
|
||||
// WaitForButtonPress blocks until the button is pressed.
|
||||
func (fuc *smartFanUnit) WaitForButtonPress(ctx context.Context) error {
|
||||
sub := fuc.eb.Subscribe(inboundTopic, 1, smartfanunit.MatchCmd(smartfanunit.NotifyButtonPress))
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case pktAny := <-sub.C():
|
||||
rawPkt := pktAny.(proto.Packet)
|
||||
if rawPkt.Command != smartfanunit.NotifyButtonPress {
|
||||
return errors.New("unexpected packet")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AirFlowTemperature returns the temperature of the air flow.
|
||||
func (fuc *smartFanUnit) AirFlowTemperature(_ context.Context) (float32, error) {
|
||||
return fuc.airflow.Temperature, nil
|
||||
}
|
||||
|
||||
func (fuc *smartFanUnit) Close() error {
|
||||
return fuc.rwc.Close()
|
||||
}
|
||||
@@ -5,8 +5,9 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal"
|
||||
"github.com/xvzf/computeblade-agent/pkg/util"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
|
||||
)
|
||||
|
||||
// LedEngine is the interface for controlling effects on the computeblade RGB LEDs
|
||||
@@ -19,7 +20,7 @@ type LedEngine interface {
|
||||
|
||||
// ledEngineImpl is the implementation of the LedEngine interface
|
||||
type ledEngineImpl struct {
|
||||
ledIdx uint
|
||||
ledIdx hal.LedIndex
|
||||
restart chan struct{}
|
||||
pattern BlinkPattern
|
||||
hal hal.ComputeBladeHal
|
||||
@@ -28,43 +29,43 @@ type ledEngineImpl struct {
|
||||
|
||||
type BlinkPattern struct {
|
||||
// BaseColor is the color is the color shown when the pattern starts (-> before the first blink)
|
||||
BaseColor hal.LedColor
|
||||
BaseColor led.Color
|
||||
// ActiveColor is the color shown when the pattern is active (-> during the blink)
|
||||
ActiveColor hal.LedColor
|
||||
ActiveColor led.Color
|
||||
// Delays is a list of delays between changes -> (base) -> 0.5s(active) -> 1s(base) -> 0.5s (active) -> 1s (base)
|
||||
Delays []time.Duration
|
||||
}
|
||||
|
||||
func mapBrighnessUint8(brightness float64) uint8 {
|
||||
func mapBrightnessUint8(brightness float64) uint8 {
|
||||
return uint8(255.0 * brightness)
|
||||
}
|
||||
|
||||
func LedColorPurple(brightness float64) hal.LedColor {
|
||||
return hal.LedColor{
|
||||
Red: mapBrighnessUint8(brightness),
|
||||
func LedColorPurple(brightness float64) led.Color {
|
||||
return led.Color{
|
||||
Red: mapBrightnessUint8(brightness),
|
||||
Green: 0,
|
||||
Blue: mapBrighnessUint8(brightness),
|
||||
Blue: mapBrightnessUint8(brightness),
|
||||
}
|
||||
}
|
||||
|
||||
func LedColorRed(brightness float64) hal.LedColor {
|
||||
return hal.LedColor{
|
||||
Red: mapBrighnessUint8(brightness),
|
||||
func LedColorRed(brightness float64) led.Color {
|
||||
return led.Color{
|
||||
Red: mapBrightnessUint8(brightness),
|
||||
Green: 0,
|
||||
Blue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func LedColorGreen(brightness float64) hal.LedColor {
|
||||
return hal.LedColor{
|
||||
func LedColorGreen(brightness float64) led.Color {
|
||||
return led.Color{
|
||||
Red: 0,
|
||||
Green: mapBrighnessUint8(brightness),
|
||||
Green: mapBrightnessUint8(brightness),
|
||||
Blue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// NewStaticPattern creates a new static pattern (no color changes)
|
||||
func NewStaticPattern(color hal.LedColor) BlinkPattern {
|
||||
func NewStaticPattern(color led.Color) BlinkPattern {
|
||||
return BlinkPattern{
|
||||
BaseColor: color,
|
||||
ActiveColor: color,
|
||||
@@ -73,23 +74,23 @@ func NewStaticPattern(color hal.LedColor) BlinkPattern {
|
||||
}
|
||||
|
||||
// NewBurstPattern creates a new burst pattern (~1s cycle duration with 3x 50ms bursts)
|
||||
func NewBurstPattern(baseColor hal.LedColor, burstColor hal.LedColor) BlinkPattern {
|
||||
func NewBurstPattern(baseColor led.Color, burstColor led.Color) BlinkPattern {
|
||||
return BlinkPattern{
|
||||
BaseColor: baseColor,
|
||||
ActiveColor: burstColor,
|
||||
Delays: []time.Duration{
|
||||
750 * time.Millisecond, // 750ms off
|
||||
50 * time.Millisecond, // 50ms on
|
||||
50 * time.Millisecond, // 50ms off
|
||||
50 * time.Millisecond, // 50ms on
|
||||
50 * time.Millisecond, // 50ms off
|
||||
50 * time.Millisecond, // 50ms on
|
||||
500 * time.Millisecond, // 750ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewSlowBlinkPattern creates a new slow blink pattern (~2s cycle duration with 1s off and 1s on)
|
||||
func NewSlowBlinkPattern(baseColor hal.LedColor, activeColor hal.LedColor) BlinkPattern {
|
||||
func NewSlowBlinkPattern(baseColor led.Color, activeColor led.Color) BlinkPattern {
|
||||
return BlinkPattern{
|
||||
BaseColor: baseColor,
|
||||
ActiveColor: activeColor,
|
||||
@@ -100,17 +101,14 @@ func NewSlowBlinkPattern(baseColor hal.LedColor, activeColor hal.LedColor) Blink
|
||||
}
|
||||
}
|
||||
|
||||
// LedEngineOpts are the options for the LedEngine
|
||||
type LedEngineOpts struct {
|
||||
// LedIdx is the index of the LED to control
|
||||
LedIdx uint
|
||||
// Hal is the computeblade hardware abstraction layer
|
||||
Hal hal.ComputeBladeHal
|
||||
// Clock is the clock used for timing
|
||||
Clock util.Clock
|
||||
func New(hal hal.ComputeBladeHal, ledIdx hal.LedIndex) LedEngine {
|
||||
return NewLedEngine(Options{
|
||||
Hal: hal,
|
||||
LedIdx: ledIdx,
|
||||
})
|
||||
}
|
||||
|
||||
func NewLedEngine(opts LedEngineOpts) *ledEngineImpl {
|
||||
func NewLedEngine(opts Options) LedEngine {
|
||||
clock := opts.Clock
|
||||
if clock == nil {
|
||||
clock = util.RealClock{}
|
||||
@@ -118,8 +116,8 @@ func NewLedEngine(opts LedEngineOpts) *ledEngineImpl {
|
||||
return &ledEngineImpl{
|
||||
ledIdx: opts.LedIdx,
|
||||
hal: opts.Hal,
|
||||
restart: make(chan struct{}), // restart channel controls cancelation of any pattern
|
||||
pattern: NewStaticPattern(hal.LedColor{}), // Turn off LEDs by default
|
||||
restart: make(chan struct{}), // restart channel controls cancellation of any pattern
|
||||
pattern: NewStaticPattern(led.Color{}), // Turn off LEDs by default
|
||||
clock: clock,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,18 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/ledengine"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal"
|
||||
"github.com/xvzf/computeblade-agent/pkg/ledengine"
|
||||
"github.com/xvzf/computeblade-agent/pkg/util"
|
||||
)
|
||||
|
||||
func TestNewStaticPattern(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type args struct {
|
||||
color hal.LedColor
|
||||
color led.Color
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -27,19 +28,19 @@ func TestNewStaticPattern(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"Green",
|
||||
args{hal.LedColor{Green: 255}},
|
||||
args{led.Color{Green: 255}},
|
||||
ledengine.BlinkPattern{
|
||||
BaseColor: hal.LedColor{Green: 255},
|
||||
ActiveColor: hal.LedColor{Green: 255},
|
||||
BaseColor: led.Color{Green: 255},
|
||||
ActiveColor: led.Color{Green: 255},
|
||||
Delays: []time.Duration{time.Hour},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Red",
|
||||
args{hal.LedColor{Red: 255}},
|
||||
args{led.Color{Red: 255}},
|
||||
ledengine.BlinkPattern{
|
||||
BaseColor: hal.LedColor{Red: 255},
|
||||
ActiveColor: hal.LedColor{Red: 255},
|
||||
BaseColor: led.Color{Red: 255},
|
||||
ActiveColor: led.Color{Red: 255},
|
||||
Delays: []time.Duration{time.Hour},
|
||||
},
|
||||
},
|
||||
@@ -56,8 +57,8 @@ func TestNewStaticPattern(t *testing.T) {
|
||||
func TestNewBurstPattern(t *testing.T) {
|
||||
t.Parallel()
|
||||
type args struct {
|
||||
baseColor hal.LedColor
|
||||
burstColor hal.LedColor
|
||||
baseColor led.Color
|
||||
burstColor led.Color
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -67,38 +68,38 @@ func TestNewBurstPattern(t *testing.T) {
|
||||
{
|
||||
"Green <-> Red",
|
||||
args{
|
||||
baseColor: hal.LedColor{Green: 255},
|
||||
burstColor: hal.LedColor{Red: 255},
|
||||
baseColor: led.Color{Green: 255},
|
||||
burstColor: led.Color{Red: 255},
|
||||
},
|
||||
ledengine.BlinkPattern{
|
||||
BaseColor: hal.LedColor{Green: 255},
|
||||
ActiveColor: hal.LedColor{Red: 255},
|
||||
BaseColor: led.Color{Green: 255},
|
||||
ActiveColor: led.Color{Red: 255},
|
||||
Delays: []time.Duration{
|
||||
750 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
500 * time.Millisecond, // 750ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Green <-> Green (valid, but no visual effect)",
|
||||
args{
|
||||
baseColor: hal.LedColor{Green: 255},
|
||||
burstColor: hal.LedColor{Green: 255},
|
||||
baseColor: led.Color{Green: 255},
|
||||
burstColor: led.Color{Green: 255},
|
||||
},
|
||||
ledengine.BlinkPattern{
|
||||
BaseColor: hal.LedColor{Green: 255},
|
||||
ActiveColor: hal.LedColor{Green: 255},
|
||||
BaseColor: led.Color{Green: 255},
|
||||
ActiveColor: led.Color{Green: 255},
|
||||
Delays: []time.Duration{
|
||||
750 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
500 * time.Millisecond, // 750ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -112,33 +113,9 @@ func TestNewBurstPattern(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSlowBlinkPattern(t *testing.T) {
|
||||
type args struct {
|
||||
baseColor hal.LedColor
|
||||
activeColor hal.LedColor
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want ledengine.BlinkPattern
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ledengine.NewSlowBlinkPattern(tt.args.baseColor, tt.args.activeColor); !reflect.DeepEqual(
|
||||
got,
|
||||
tt.want,
|
||||
) {
|
||||
t.Errorf("NewSlowledengine.BlinkPattern() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLedEngine(t *testing.T) {
|
||||
t.Parallel()
|
||||
engine := ledengine.LedEngineOpts{
|
||||
engine := ledengine.Options{
|
||||
Clock: util.RealClock{},
|
||||
LedIdx: 0,
|
||||
Hal: &hal.ComputeBladeHalMock{},
|
||||
@@ -146,6 +123,17 @@ func TestNewLedEngine(t *testing.T) {
|
||||
assert.NotNil(t, engine)
|
||||
}
|
||||
|
||||
func TestLedEngine_NewLedEngineWithoutClock(t *testing.T) {
|
||||
opts := ledengine.Options{
|
||||
Clock: nil,
|
||||
LedIdx: 0,
|
||||
Hal: &hal.ComputeBladeHalMock{},
|
||||
}
|
||||
|
||||
engine := ledengine.NewLedEngine(opts)
|
||||
assert.NotNil(t, engine)
|
||||
}
|
||||
|
||||
func Test_LedEngine_SetPattern_WhileRunning(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -154,10 +142,10 @@ func Test_LedEngine_SetPattern_WhileRunning(t *testing.T) {
|
||||
clk.On("After", time.Hour).Times(2).Return(clkAfterChan)
|
||||
|
||||
cbMock := hal.ComputeBladeHalMock{}
|
||||
cbMock.On("SetLed", uint(0), hal.LedColor{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
|
||||
cbMock.On("SetLed", uint(0), hal.LedColor{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
|
||||
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
|
||||
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
|
||||
|
||||
opts := ledengine.LedEngineOpts{
|
||||
opts := ledengine.Options{
|
||||
Hal: &cbMock,
|
||||
Clock: &clk,
|
||||
LedIdx: 0,
|
||||
@@ -182,7 +170,7 @@ func Test_LedEngine_SetPattern_WhileRunning(t *testing.T) {
|
||||
|
||||
// Set pattern
|
||||
t.Log("Setting pattern")
|
||||
err := engine.SetPattern(ledengine.NewStaticPattern(hal.LedColor{Red: 255}))
|
||||
err := engine.SetPattern(ledengine.NewStaticPattern(led.Color{Red: 255}))
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Log("Canceling context")
|
||||
@@ -201,9 +189,9 @@ func Test_LedEngine_SetPattern_BeforeRun(t *testing.T) {
|
||||
clk.On("After", time.Hour).Once().Return(clkAfterChan)
|
||||
|
||||
cbMock := hal.ComputeBladeHalMock{}
|
||||
cbMock.On("SetLed", uint(0), hal.LedColor{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
|
||||
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
|
||||
|
||||
opts := ledengine.LedEngineOpts{
|
||||
opts := ledengine.Options{
|
||||
Hal: &cbMock,
|
||||
Clock: &clk,
|
||||
LedIdx: 0,
|
||||
@@ -212,7 +200,7 @@ func Test_LedEngine_SetPattern_BeforeRun(t *testing.T) {
|
||||
engine := ledengine.NewLedEngine(opts)
|
||||
// We want to change the pattern BEFORE the engine is started
|
||||
t.Log("Setting pattern")
|
||||
err := engine.SetPattern(ledengine.NewStaticPattern(hal.LedColor{Red: 255}))
|
||||
err := engine.SetPattern(ledengine.NewStaticPattern(led.Color{Red: 255}))
|
||||
assert.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
@@ -243,10 +231,10 @@ func Test_LedEngine_SetPattern_SetLedFailureInPattern(t *testing.T) {
|
||||
clk.On("After", time.Hour).Once().Return(clkAfterChan)
|
||||
|
||||
cbMock := hal.ComputeBladeHalMock{}
|
||||
call0 := cbMock.On("SetLed", uint(0), hal.LedColor{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
|
||||
cbMock.On("SetLed", uint(0), hal.LedColor{Green: 0, Blue: 0, Red: 0}).Once().Return(errors.New("failure")).NotBefore(call0)
|
||||
call0 := cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
|
||||
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(errors.New("failure")).NotBefore(call0)
|
||||
|
||||
opts := ledengine.LedEngineOpts{
|
||||
opts := ledengine.Options{
|
||||
Hal: &cbMock,
|
||||
Clock: &clk,
|
||||
LedIdx: 0,
|
||||
@@ -277,3 +265,29 @@ func Test_LedEngine_SetPattern_SetLedFailureInPattern(t *testing.T) {
|
||||
clk.AssertExpectations(t)
|
||||
cbMock.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func Test_LedEngine_SetPattern_NoDelay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clk := util.MockClock{}
|
||||
clkAfterChan := make(chan time.Time)
|
||||
clk.On("After", time.Hour).Once().Return(clkAfterChan)
|
||||
|
||||
cbMock := hal.ComputeBladeHalMock{}
|
||||
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
|
||||
|
||||
opts := ledengine.Options{
|
||||
Hal: &cbMock,
|
||||
Clock: &clk,
|
||||
LedIdx: 0,
|
||||
}
|
||||
|
||||
engine := ledengine.NewLedEngine(opts)
|
||||
invalidPattern := ledengine.NewStaticPattern(led.Color{Red: 255})
|
||||
invalidPattern.Delays = []time.Duration{}
|
||||
// We want to change the pattern BEFORE the engine is started
|
||||
t.Log("Setting pattern")
|
||||
err := engine.SetPattern(invalidPattern)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "pattern must have at least one delay")
|
||||
}
|
||||
|
||||
16
pkg/ledengine/options.go
Normal file
16
pkg/ledengine/options.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package ledengine
|
||||
|
||||
import (
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
|
||||
)
|
||||
|
||||
// Options are the options for the LedEngine
|
||||
type Options struct {
|
||||
// LedIdx is the index of the LED to control
|
||||
LedIdx hal.LedIndex
|
||||
// Hal is the computeblade hardware abstraction layer
|
||||
Hal hal.ComputeBladeHal
|
||||
// Clock is the clock used for timing
|
||||
Clock util.Clock
|
||||
}
|
||||
50
pkg/log/interceptor_logger.go
Normal file
50
pkg/log/interceptor_logger.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
|
||||
"github.com/spechtlabs/go-otel-utils/otelzap"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// InterceptorLogger adapts zap logger to interceptor logger.
|
||||
// This code is simple enough to be copied and not imported.
|
||||
func InterceptorLogger(l *otelzap.Logger) logging.Logger {
|
||||
return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) {
|
||||
f := make([]zap.Field, 0, len(fields)/2)
|
||||
|
||||
for i := 0; i < len(fields); i += 2 {
|
||||
key := fields[i]
|
||||
value := fields[i+1]
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
f = append(f, zap.String(key.(string), v))
|
||||
case int:
|
||||
f = append(f, zap.Int(key.(string), v))
|
||||
case bool:
|
||||
f = append(f, zap.Bool(key.(string), v))
|
||||
case zap.Field:
|
||||
f = append(f, v)
|
||||
default:
|
||||
f = append(f, zap.Any(key.(string), v))
|
||||
}
|
||||
}
|
||||
|
||||
logger := l.WithOptions(zap.AddCallerSkip(4)).With(f...)
|
||||
|
||||
switch lvl {
|
||||
case logging.LevelDebug:
|
||||
logger.Debug(msg)
|
||||
case logging.LevelInfo:
|
||||
logger.Info(msg)
|
||||
case logging.LevelWarn:
|
||||
logger.Warn(msg)
|
||||
case logging.LevelError:
|
||||
logger.Error(msg)
|
||||
default:
|
||||
logger.Warn(msg, zap.String("error", "unknown level"), zap.Int("level", int(lvl)))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -3,20 +3,22 @@ package log
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spechtlabs/go-otel-utils/otelzap"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type logCtxKey int
|
||||
|
||||
func IntoContext(ctx context.Context, logger *zap.Logger) context.Context {
|
||||
func IntoContext(ctx context.Context, logger *otelzap.Logger) context.Context {
|
||||
return context.WithValue(ctx, logCtxKey(0), logger)
|
||||
}
|
||||
|
||||
func FromContext(ctx context.Context) *zap.Logger {
|
||||
func FromContext(ctx context.Context) *otelzap.Logger {
|
||||
val := ctx.Value(logCtxKey(0))
|
||||
if val != nil {
|
||||
return val.(*zap.Logger)
|
||||
return val.(*otelzap.Logger)
|
||||
}
|
||||
zap.L().Warn("No logger in context, passing default")
|
||||
return zap.L()
|
||||
|
||||
otelzap.L().WithOptions(zap.AddCallerSkip(1)).Warn("No logger in context, passing default")
|
||||
return otelzap.L()
|
||||
}
|
||||
|
||||
135
pkg/smartfanunit/commands.go
Normal file
135
pkg/smartfanunit/commands.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package smartfanunit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
|
||||
)
|
||||
|
||||
// Blade -> FanUnit communication
|
||||
const (
|
||||
// CmdSetFanSpeedPercent sets the fan speed as a percentage, sent from the blade to the fan unit.
|
||||
CmdSetFanSpeedPercent proto.Command = 0x01
|
||||
|
||||
// CmdSetLED represents the command to set the LED color, sent from the blade to the fan unit.
|
||||
CmdSetLED proto.Command = 0x02
|
||||
)
|
||||
|
||||
// FanUnit -> Blade, sent in regular intervals
|
||||
const (
|
||||
// NotifyButtonPress represents a command sent from the fan unit to indicate a button press event.
|
||||
NotifyButtonPress proto.Command = 0xa1
|
||||
|
||||
// NotifyAirFlowTemperature represents a command sent from the fan unit to report the current air flow temperature.
|
||||
NotifyAirFlowTemperature proto.Command = 0xa2
|
||||
|
||||
// NotifyFanSpeedRPM is a command used to report the current fan speed in RPM from the fan unit to the blade.
|
||||
NotifyFanSpeedRPM proto.Command = 0xa3
|
||||
)
|
||||
|
||||
var ErrInvalidCommand = errors.New("invalid command")
|
||||
|
||||
type PacketGenerator interface {
|
||||
Packet() proto.Packet
|
||||
}
|
||||
|
||||
// SetFanSpeedPercentPacket is sent from the blade to the fan unit to set the fan speed in percent.
|
||||
type SetFanSpeedPercentPacket struct {
|
||||
Percent uint8
|
||||
}
|
||||
|
||||
func (p *SetFanSpeedPercentPacket) Packet() proto.Packet {
|
||||
return proto.Packet{
|
||||
Command: CmdSetFanSpeedPercent,
|
||||
Data: proto.Data{p.Percent, 0, 0},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SetFanSpeedPercentPacket) FromPacket(packet proto.Packet) error {
|
||||
if packet.Command != CmdSetFanSpeedPercent {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
p.Percent = packet.Data[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLEDPacket is sent from the blade to the fan unit to set the LED color.
|
||||
type SetLEDPacket struct {
|
||||
Color led.Color
|
||||
}
|
||||
|
||||
func (p *SetLEDPacket) Packet() proto.Packet {
|
||||
return proto.Packet{
|
||||
Command: CmdSetLED,
|
||||
Data: proto.Data{p.Color.Blue, p.Color.Green, p.Color.Red},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SetLEDPacket) FromPacket(packet proto.Packet) error {
|
||||
if packet.Command != CmdSetLED {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
p.Color = led.Color{
|
||||
Blue: packet.Data[0],
|
||||
Green: packet.Data[1],
|
||||
Red: packet.Data[2],
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ButtonPressPacket is sent from the fan unit to the blade when the button is pressed.
|
||||
type ButtonPressPacket struct{}
|
||||
|
||||
func (p *ButtonPressPacket) Packet() proto.Packet {
|
||||
return proto.Packet{
|
||||
Command: NotifyButtonPress,
|
||||
Data: proto.Data{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ButtonPressPacket) FromPacket(packet proto.Packet) error {
|
||||
if packet.Command != NotifyButtonPress {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AirFlowTemperaturePacket is sent from the fan unit to the blade to report the current air flow temperature.
|
||||
type AirFlowTemperaturePacket struct {
|
||||
Temperature float32
|
||||
}
|
||||
|
||||
func (p *AirFlowTemperaturePacket) Packet() proto.Packet {
|
||||
return proto.Packet{
|
||||
Command: NotifyAirFlowTemperature,
|
||||
Data: float32To24Bit(p.Temperature),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AirFlowTemperaturePacket) FromPacket(packet proto.Packet) error {
|
||||
if packet.Command != NotifyAirFlowTemperature {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
p.Temperature = float32From24Bit(packet.Data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FanSpeedRPMPacket is sent from the fan unit to the blade to report the current fan speed in RPM.
|
||||
type FanSpeedRPMPacket struct {
|
||||
RPM float32
|
||||
}
|
||||
|
||||
func (p *FanSpeedRPMPacket) Packet() proto.Packet {
|
||||
return proto.Packet{
|
||||
Command: NotifyFanSpeedRPM,
|
||||
Data: float32To24Bit(p.RPM),
|
||||
}
|
||||
}
|
||||
func (p *FanSpeedRPMPacket) FromPacket(packet proto.Packet) error {
|
||||
if packet.Command != NotifyFanSpeedRPM {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
p.RPM = float32From24Bit(packet.Data)
|
||||
return nil
|
||||
}
|
||||
131
pkg/smartfanunit/emc2101/emc2101.go
Normal file
131
pkg/smartfanunit/emc2101/emc2101.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// Package emc2101 is a driver for the EMC2101 fan controller
|
||||
// Based on https://ww1.microchip.com/downloads/en/DeviceDoc/2101.pdf
|
||||
package emc2101
|
||||
|
||||
import (
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
type emc2101 struct {
|
||||
Address uint16
|
||||
bus drivers.I2C
|
||||
}
|
||||
|
||||
// EMC2101 is a driver for the EMC2101 fan controller
|
||||
type EMC2101 interface {
|
||||
// Init initializes the EMC2101
|
||||
Init() error
|
||||
// InternalTemperature returns the internal temperature of the EMC2101
|
||||
InternalTemperature() (float32, error)
|
||||
// ExternalTemperature returns the external temperature of the EMC2101
|
||||
ExternalTemperature() (float32, error)
|
||||
// SetFanPercent sets the fan speed as a percentage of max
|
||||
SetFanPercent(percent uint8) error
|
||||
// FanRPM returns the current fan speed in RPM
|
||||
FanRPM() (float32, error)
|
||||
}
|
||||
|
||||
const (
|
||||
// Address is the default I2C address for the EMC2101
|
||||
Address = 0x4C
|
||||
ConfigReg = 0x03
|
||||
FanConfigReg = 0x4a
|
||||
FanSpinUpReg = 0x4b
|
||||
FanSettingReg = 0x4c
|
||||
FanTachReadingLowReg = 0x46
|
||||
FanTachReadingHighReg = 0x47
|
||||
ExternalTempReg = 0x01
|
||||
InternalTempReg = 0x00
|
||||
)
|
||||
|
||||
func New(bus drivers.I2C) EMC2101 {
|
||||
return &emc2101{bus: bus, Address: Address}
|
||||
}
|
||||
|
||||
// updateReg updates a register with the given set and clear masks
|
||||
func (e *emc2101) updateReg(regAddr, setMask, clearMask uint8) error {
|
||||
buf := make([]uint8, 1)
|
||||
err := e.bus.Tx(e.Address, []byte{regAddr}, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toWrite := buf[0]
|
||||
toWrite |= setMask
|
||||
toWrite &= ^clearMask
|
||||
|
||||
if toWrite == buf[0] {
|
||||
return nil
|
||||
}
|
||||
|
||||
return e.bus.Tx(e.Address, []byte{regAddr, toWrite}, nil)
|
||||
}
|
||||
|
||||
func (e *emc2101) Init() error {
|
||||
// set pwm mode
|
||||
// bit 4: 0 = PWM mode
|
||||
// bit 2: 1 = TACH input
|
||||
if err := e.updateReg(ConfigReg, (1 << 2), (1 << 4)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.updateReg(FanConfigReg, (1 << 5), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
0x3 0b100
|
||||
0x4b 0b11111
|
||||
0x4a 0b100000
|
||||
0x4a 0b100000
|
||||
*/
|
||||
|
||||
// Configure fan spin up to ignore tach input
|
||||
// bit 5: 1 = Ignore tach input for spin up procedure
|
||||
if err := e.updateReg(FanSpinUpReg, 0, (1 << 5)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *emc2101) InternalTemperature() (float32, error) {
|
||||
buf := make([]byte, 1)
|
||||
if err := e.bus.Tx(e.Address, []byte{InternalTempReg}, buf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float32(buf[0]), nil
|
||||
}
|
||||
|
||||
func (e *emc2101) ExternalTemperature() (float32, error) {
|
||||
buf := make([]byte, 1)
|
||||
if err := e.bus.Tx(e.Address, []byte{ExternalTempReg}, buf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float32(buf[0]), nil
|
||||
}
|
||||
|
||||
func (e *emc2101) SetFanPercent(percent uint8) error {
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
val := uint8(uint32(percent) * 63 / 100)
|
||||
return e.bus.Tx(e.Address, []byte{FanSettingReg, val}, nil)
|
||||
}
|
||||
|
||||
func (e *emc2101) FanRPM() (float32, error) {
|
||||
high := make([]byte, 1)
|
||||
low := make([]byte, 1)
|
||||
|
||||
err := e.bus.Tx(e.Address, []byte{FanTachReadingHighReg}, high)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = e.bus.Tx(e.Address, []byte{FanTachReadingLowReg}, low)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var tachCount = int(high[0])<<8 | int(low[0])
|
||||
|
||||
return float32(5400000) / float32(tachCount), nil
|
||||
}
|
||||
21
pkg/smartfanunit/helpers.go
Normal file
21
pkg/smartfanunit/helpers.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package smartfanunit
|
||||
|
||||
import "github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
|
||||
|
||||
func float32To24Bit(val float32) proto.Data {
|
||||
// Convert float32 to number with 3 bytes (0.1 precision)
|
||||
tmp := uint32(val * 10)
|
||||
if tmp > 0xffffff {
|
||||
tmp = 0xffffff // cap
|
||||
}
|
||||
return proto.Data{
|
||||
uint8((tmp >> 16) & 0xFF),
|
||||
uint8((tmp >> 8) & 0xFF),
|
||||
uint8(tmp & 0xFF),
|
||||
}
|
||||
}
|
||||
|
||||
func float32From24Bit(data proto.Data) float32 {
|
||||
tmp := uint32(data[0])<<16 | uint32(data[1])<<8 | uint32(data[2])
|
||||
return float32(tmp) / 10
|
||||
}
|
||||
35
pkg/smartfanunit/helpers_test.go
Normal file
35
pkg/smartfanunit/helpers_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
//go:build !tinygo
|
||||
|
||||
package smartfanunit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFloat32ToAndFrom24Bit(t *testing.T) {
|
||||
tests := []struct {
|
||||
input float32
|
||||
expected float32
|
||||
}{
|
||||
{0.0, 0.0},
|
||||
{1.0, 1.0},
|
||||
{0.123, 0.1},
|
||||
{10.0, 10.0},
|
||||
{100.0, 100.0},
|
||||
{1677721.5, 1677721.5},
|
||||
{2000000.0, 1677721.5}, // Should be capped at 0xFFFFFF
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("Input: %f", test.input), func(t *testing.T) {
|
||||
data := float32To24Bit(test.input)
|
||||
result := float32From24Bit(data)
|
||||
|
||||
// Check if the result is approximately equal within a small delta
|
||||
if result < test.expected-0.01 || result > test.expected+0.01 {
|
||||
t.Errorf("Expected %f, but got %f", test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
148
pkg/smartfanunit/proto/proto.go
Normal file
148
pkg/smartfanunit/proto/proto.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
// Simple P2P protocol for communicating over a serial port.
|
||||
// All commands are 4 bytes long, the first byte is the command, the remaining bytes are data
|
||||
// This allows encoding of 256 commands, with a payload of 3 bytes each.
|
||||
// Includes SOF/EOF framing and a checksum. Colliding bytes in the payload are escaped.
|
||||
|
||||
var (
|
||||
ErrChecksumMismatch = errors.New("checksum mismatch")
|
||||
ErrInvalidFramingByte = errors.New("invalid framing byte")
|
||||
)
|
||||
|
||||
const (
|
||||
SOF = 0x7E // Start of Frame
|
||||
ESC = 0x7D // Escape character
|
||||
XOR = 0x20 // XOR value for escaping
|
||||
EOF = 0x7F // End of Frame
|
||||
)
|
||||
|
||||
// Command represents the command byte.
|
||||
type Command uint8
|
||||
|
||||
// Data represents the three data bytes.
|
||||
type Data [3]uint8
|
||||
|
||||
// Packet represents a serial packet with command and data.
|
||||
type Packet struct {
|
||||
Command Command
|
||||
Data Data
|
||||
}
|
||||
|
||||
// Checksum calculates the Checksum for a packet.
|
||||
func (packet *Packet) Checksum() uint8 {
|
||||
crc := uint8(0)
|
||||
crc ^= uint8(packet.Command)
|
||||
for _, d := range packet.Data {
|
||||
crc ^= d
|
||||
}
|
||||
return crc
|
||||
}
|
||||
|
||||
// WritePacket writes a packet to an io.Writer with escaping.
|
||||
func WritePacket(_ context.Context, w io.Writer, packet Packet) error {
|
||||
checksum := packet.Checksum()
|
||||
|
||||
buf := []uint8{uint8(packet.Command), packet.Data[0], packet.Data[1], packet.Data[2], checksum}
|
||||
|
||||
_, err := w.Write([]uint8{SOF})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, b := range buf {
|
||||
if b == SOF || b == EOF || b == ESC {
|
||||
_, err := w.Write([]uint8{ESC, b ^ XOR})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_, err := w.Write([]uint8{b})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
_, err = w.Write([]uint8{EOF})
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadPacket reads a packet from an io.Reader with escaping.
|
||||
// This is blocking and drops invalid bytes until a valid packet is received.
|
||||
func ReadPacket(ctx context.Context, r io.Reader) (Packet, error) {
|
||||
var buffer []uint8
|
||||
|
||||
started := false
|
||||
escaped := false
|
||||
|
||||
uart, isUart := r.(drivers.UART)
|
||||
|
||||
for {
|
||||
|
||||
// Check if context is done before reading
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return Packet{}, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
if isUart && uart.Buffered() == 0 {
|
||||
// Allows TinyGo to switch to other goroutines
|
||||
time.Sleep(time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
b := make([]uint8, 1)
|
||||
_, err := r.Read(b)
|
||||
if err != nil {
|
||||
return Packet{}, err
|
||||
}
|
||||
|
||||
if b[0] == SOF && !started {
|
||||
started = true
|
||||
} else if !started {
|
||||
continue
|
||||
}
|
||||
|
||||
if escaped {
|
||||
buffer = append(buffer, b[0]^XOR)
|
||||
escaped = false
|
||||
} else if b[0] == ESC {
|
||||
escaped = true
|
||||
} else {
|
||||
buffer = append(buffer, b[0])
|
||||
}
|
||||
|
||||
if b[0] == EOF && !escaped {
|
||||
if len(buffer) == 7 { // Packet size
|
||||
break
|
||||
} else {
|
||||
buffer = []uint8{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if buffer[0] != SOF || buffer[len(buffer)-1] != EOF {
|
||||
return Packet{}, ErrInvalidFramingByte
|
||||
}
|
||||
|
||||
command := Command(buffer[1])
|
||||
data := Data{buffer[2], buffer[3], buffer[4]}
|
||||
checksum := buffer[5]
|
||||
pkt := Packet{command, data}
|
||||
expectedChecksum := pkt.Checksum()
|
||||
|
||||
if checksum != expectedChecksum {
|
||||
return Packet{}, ErrChecksumMismatch
|
||||
}
|
||||
|
||||
return pkt, nil
|
||||
}
|
||||
206
pkg/smartfanunit/proto/proto_test.go
Normal file
206
pkg/smartfanunit/proto/proto_test.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package proto_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWritePacket(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
packet proto.Packet
|
||||
expected []uint8
|
||||
}{
|
||||
{
|
||||
name: "Simple packet",
|
||||
packet: proto.Packet{
|
||||
Command: proto.Command(0x01),
|
||||
Data: proto.Data{0x11, 0x12, 0x13},
|
||||
},
|
||||
expected: []uint8{proto.SOF, 0x01, 0x11, 0x12, 0x13, 0x11, proto.EOF},
|
||||
},
|
||||
{
|
||||
name: "ESC in payload and checksum == ESC",
|
||||
packet: proto.Packet{
|
||||
Command: proto.Command(0x01),
|
||||
Data: proto.Data{proto.ESC, 0x12, 0x13},
|
||||
// Checksup: 0x7d -> proto.ESC as well
|
||||
},
|
||||
expected: []uint8{
|
||||
// Start of frame
|
||||
proto.SOF,
|
||||
0x01,
|
||||
// Escaped data
|
||||
proto.ESC,
|
||||
proto.XOR ^ proto.ESC,
|
||||
// continuing non-escaped data
|
||||
0x12, 0x13,
|
||||
// escape checksum
|
||||
proto.ESC,
|
||||
proto.XOR ^ proto.ESC,
|
||||
// end of frame
|
||||
proto.EOF,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EOF, SOF and ESC in payload",
|
||||
packet: proto.Packet{
|
||||
// 0x01, 0x7e, 0x7f, 0x7d
|
||||
Command: proto.Command(0xff),
|
||||
Data: proto.Data{proto.SOF, proto.EOF, proto.ESC},
|
||||
// Checksup: 0x7d -> proto.ESC as well
|
||||
},
|
||||
expected: []uint8{
|
||||
// Start of frame
|
||||
proto.SOF,
|
||||
0xff,
|
||||
// Escaped SOF
|
||||
proto.ESC,
|
||||
proto.XOR ^ proto.SOF,
|
||||
// Escaped EOF
|
||||
proto.ESC,
|
||||
proto.XOR ^ proto.EOF,
|
||||
// Escaped ESC
|
||||
proto.ESC,
|
||||
proto.XOR ^ proto.ESC,
|
||||
// Checksum
|
||||
0x83,
|
||||
// end of frame
|
||||
proto.EOF,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tcl := range testcases {
|
||||
tc := tcl
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var buffer bytes.Buffer
|
||||
err := proto.WritePacket(context.TODO(), &buffer, tc.packet)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, buffer.Bytes())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzPacketReadWrite(f *testing.F) {
|
||||
f.Add(uint8(0x01), uint8(0x02), uint8(0x03), uint8(0x04))
|
||||
|
||||
// Fuzz function
|
||||
f.Fuzz(func(t *testing.T, cmd, d0, d1, d2 uint8) {
|
||||
pkt := proto.Packet{
|
||||
Command: proto.Command(cmd),
|
||||
Data: proto.Data([]uint8{d0, d1, d2}),
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
err := proto.WritePacket(context.TODO(), &buffer, pkt)
|
||||
assert.NoError(t, err)
|
||||
|
||||
readPkt, err := proto.ReadPacket(context.TODO(), &buffer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, pkt, readPkt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPacketReadWrite(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
packet proto.Packet
|
||||
}{
|
||||
{
|
||||
name: "Simple packet",
|
||||
packet: proto.Packet{
|
||||
Command: proto.Command(0x01),
|
||||
Data: proto.Data{0x11, 0x12, 0x13},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EOF, SOF and ESC in payload",
|
||||
packet: proto.Packet{
|
||||
Command: proto.Command(0xff),
|
||||
Data: proto.Data{proto.SOF, proto.EOF, proto.ESC},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tcl := range testcases {
|
||||
tc := tcl
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var buffer bytes.Buffer
|
||||
err := proto.WritePacket(context.TODO(), &buffer, tc.packet)
|
||||
assert.NoError(t, err)
|
||||
|
||||
packet, err := proto.ReadPacket(context.TODO(), &buffer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.packet, packet)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPacketChecksumError(t *testing.T) {
|
||||
// Create a simple packet with an invalid Checksum
|
||||
var buffer bytes.Buffer
|
||||
invalidPacket := []uint8{
|
||||
proto.SOF,
|
||||
0x01,
|
||||
0x11,
|
||||
0x22,
|
||||
0x33,
|
||||
0x00,
|
||||
proto.EOF,
|
||||
} // 0x00 as checksum is invalid here
|
||||
|
||||
// Write invalid packet to buffer
|
||||
for _, b := range invalidPacket {
|
||||
_, err := buffer.Write([]uint8{b})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to buffer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to read the packet with a Checksum error
|
||||
_, err := proto.ReadPacket(context.TODO(), &buffer)
|
||||
assert.ErrorIs(t, err, proto.ErrChecksumMismatch)
|
||||
}
|
||||
|
||||
func TestReadPacketDirtyReader(t *testing.T) {
|
||||
// Create a simple packet with an invalid Checksum
|
||||
var buffer bytes.Buffer
|
||||
invalidPacket := []uint8{
|
||||
// Incomplete previous packet
|
||||
0x01,
|
||||
0x12,
|
||||
0x13,
|
||||
0x11,
|
||||
proto.EOF,
|
||||
// Actual packet
|
||||
proto.SOF,
|
||||
0x01,
|
||||
0x11,
|
||||
0x12,
|
||||
0x13,
|
||||
0x11,
|
||||
proto.EOF,
|
||||
}
|
||||
|
||||
// Write invalid packet to buffer
|
||||
for _, b := range invalidPacket {
|
||||
_, err := buffer.Write([]uint8{b})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to buffer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to read the packet with a Checksum error
|
||||
pkt, err := proto.ReadPacket(context.TODO(), &buffer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proto.Packet{Command: proto.Command(0x01), Data: proto.Data{0x11, 0x12, 0x13}}, pkt)
|
||||
}
|
||||
22
pkg/smartfanunit/smartfanunit.go
Normal file
22
pkg/smartfanunit/smartfanunit.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package smartfanunit
|
||||
|
||||
import (
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
BaudRate = 115200
|
||||
)
|
||||
|
||||
func MatchCmd(cmd proto.Command) func(any) bool {
|
||||
return func(pktAny any) bool {
|
||||
pkt, ok := pktAny.(proto.Packet)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if pkt.Command == cmd {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
72
pkg/util/clock_test.go
Normal file
72
pkg/util/clock_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package util_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestRealClock_Now ensures that RealClock.Now() returns a time close to the actual time.
|
||||
func TestRealClock_Now(t *testing.T) {
|
||||
rc := util.RealClock{}
|
||||
before := time.Now()
|
||||
got := rc.Now()
|
||||
after := time.Now()
|
||||
|
||||
if got.Before(before) || got.After(after) {
|
||||
t.Errorf("RealClock.Now() = %v, want between %v and %v", got, before, after)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRealClock_After ensures that RealClock.After() returns a channel that sends after the given duration.
|
||||
func TestRealClock_After(t *testing.T) {
|
||||
rc := util.RealClock{}
|
||||
delay := 50 * time.Millisecond
|
||||
|
||||
start := time.Now()
|
||||
ch := rc.After(delay)
|
||||
<-ch
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if elapsed < delay {
|
||||
t.Errorf("RealClock.After(%v) triggered too early after %v", delay, elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMockClock_Now tests that MockClock.Now() returns the expected time and records the call.
|
||||
func TestMockClock_Now(t *testing.T) {
|
||||
mockClock := new(util.MockClock)
|
||||
expectedTime := time.Date(2025, time.June, 6, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
mockClock.On("Now").Return(expectedTime)
|
||||
|
||||
actualTime := mockClock.Now()
|
||||
assert.Equal(t, expectedTime, actualTime)
|
||||
mockClock.AssertCalled(t, "Now")
|
||||
mockClock.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// TestMockClock_After tests that MockClock.After() returns the expected channel and records the call.
|
||||
func TestMockClock_After(t *testing.T) {
|
||||
mockClock := new(util.MockClock)
|
||||
duration := 100 * time.Millisecond
|
||||
expectedChan := make(chan time.Time, 1)
|
||||
expectedTime := time.Now().Add(duration)
|
||||
expectedChan <- expectedTime
|
||||
|
||||
mockClock.On("After", duration).Return(expectedChan)
|
||||
|
||||
resultChan := mockClock.After(duration)
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
assert.WithinDuration(t, expectedTime, result, time.Second)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timeout waiting for result from MockClock.After")
|
||||
}
|
||||
|
||||
mockClock.AssertCalled(t, "After", duration)
|
||||
mockClock.AssertExpectations(t)
|
||||
}
|
||||
25
pkg/util/file_exist_test.go
Normal file
25
pkg/util/file_exist_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package util_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFileExists(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "fileexists-test")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// It should exist
|
||||
assert.True(t, util.FileExists(tmpFile.Name()), "Expected file to exist")
|
||||
|
||||
// Close and remove the file
|
||||
assert.NoError(t, tmpFile.Close())
|
||||
assert.NoError(t, os.Remove(tmpFile.Name()))
|
||||
|
||||
// It should not exist anymore
|
||||
assert.False(t, util.FileExists(tmpFile.Name()), "Expected file not to exist")
|
||||
}
|
||||
9
pkg/util/file_exists.go
Normal file
9
pkg/util/file_exists.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package util
|
||||
|
||||
import "os"
|
||||
|
||||
// FileExists checks if a file exists at the given path and returns true if it does, false otherwise.
|
||||
func FileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
43
pkg/util/host_ips.go
Normal file
43
pkg/util/host_ips.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package util
|
||||
|
||||
import "net"
|
||||
|
||||
func GetHostIPs() ([]net.IP, error) {
|
||||
var ips []net.IP
|
||||
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
// Skip down or loopback interfaces
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue // skip interfaces we can't read
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
|
||||
// Skip loopback or unspecified
|
||||
if ip == nil || ip.IsLoopback() || ip.IsUnspecified() {
|
||||
continue
|
||||
}
|
||||
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
18
pkg/util/host_ips_test.go
Normal file
18
pkg/util/host_ips_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package util_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetHostIPs_ReturnsNonLoopbackIPs(t *testing.T) {
|
||||
ips, err := util.GetHostIPs()
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, ip := range ips {
|
||||
assert.False(t, ip.IsLoopback(), "Should not return loopback IPs")
|
||||
assert.False(t, ip.IsUnspecified(), "Should not return unspecified IPs")
|
||||
}
|
||||
}
|
||||
22
renovate.json
Normal file
22
renovate.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"commitMessagePrefix": "chore(deps):",
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Automatically merge minor and patch-level updates",
|
||||
"matchUpdateTypes": [
|
||||
"patch",
|
||||
"bump",
|
||||
"minor"
|
||||
],
|
||||
"automerge": true,
|
||||
"automergeType": "branch"
|
||||
}
|
||||
],
|
||||
"ignoreDeps": [
|
||||
"github.com/warthog618/gpiod"
|
||||
]
|
||||
}
|
||||
115
scripts/find-tach-gpio.sh
Executable file
115
scripts/find-tach-gpio.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
# find-tach-gpio.sh - Safely probe GPIOs to find fan tachometer signal
|
||||
# Run on Radxa node with fan at 100%: sudo ./find-tach-gpio.sh
|
||||
#
|
||||
# The tachometer generates 2 pulses per revolution. At 5000 RPM:
|
||||
# 5000 RPM / 60 = 83.33 RPS * 2 pulses = ~167 Hz
|
||||
# At 3000 RPM: ~100 Hz
|
||||
# We look for any GPIO showing periodic edge events.
|
||||
|
||||
set -e
|
||||
|
||||
PROBE_DURATION=1 # seconds to monitor each GPIO
|
||||
MIN_EVENTS=10 # minimum events to consider "active" (10 events in 1s = 600 RPM minimum)
|
||||
DELAY_BETWEEN=0.5 # seconds between probes to let system settle
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo "=== Fan Tachometer GPIO Finder ==="
|
||||
echo ""
|
||||
|
||||
# Check if running as root (needed for gpiomon)
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}Please run as root (sudo)${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check fan speed
|
||||
FAN_PWM=$(cat /sys/class/hwmon/hwmon8/pwm1 2>/dev/null || echo "unknown")
|
||||
echo "Current fan PWM: $FAN_PWM (should be 255 for best detection)"
|
||||
if [ "$FAN_PWM" != "255" ]; then
|
||||
echo -e "${YELLOW}Setting fan to 100% for detection...${NC}"
|
||||
echo 255 > /sys/class/hwmon/hwmon8/pwm1
|
||||
sleep 2 # Let fan spin up
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Get list of gpiochips
|
||||
CHIPS=$(ls /dev/gpiochip* | sed 's|/dev/||')
|
||||
|
||||
echo "Scanning GPIO chips: $CHIPS"
|
||||
echo "Probe duration: ${PROBE_DURATION}s per line, minimum ${MIN_EVENTS} events to flag"
|
||||
echo ""
|
||||
|
||||
FOUND_CANDIDATES=""
|
||||
|
||||
for chip in $CHIPS; do
|
||||
# Get number of lines for this chip
|
||||
NUM_LINES=$(gpioinfo $chip 2>/dev/null | wc -l)
|
||||
NUM_LINES=$((NUM_LINES - 1)) # Subtract header line
|
||||
|
||||
if [ "$NUM_LINES" -le 0 ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}=== Scanning $chip ($NUM_LINES lines) ===${NC}"
|
||||
|
||||
for line in $(seq 0 $((NUM_LINES - 1))); do
|
||||
# Check if line is already in use
|
||||
LINE_INFO=$(gpioinfo $chip 2>/dev/null | grep "line *$line:" || true)
|
||||
if echo "$LINE_INFO" | grep -q "\[used\]"; then
|
||||
# Skip lines that are in use
|
||||
continue
|
||||
fi
|
||||
|
||||
# Probe the line
|
||||
printf " Line %2d: " "$line"
|
||||
|
||||
# Run gpiomon with timeout, count events
|
||||
EVENTS=$(timeout ${PROBE_DURATION}s gpiomon --num-events=100 $chip $line 2>&1 | wc -l || echo "0")
|
||||
|
||||
if [ "$EVENTS" -ge "$MIN_EVENTS" ]; then
|
||||
# Calculate approximate frequency
|
||||
FREQ=$((EVENTS / PROBE_DURATION))
|
||||
RPM_ESTIMATE=$((FREQ * 60 / 2)) # 2 pulses per revolution
|
||||
echo -e "${GREEN}ACTIVE! $EVENTS events (~${FREQ} Hz, ~${RPM_ESTIMATE} RPM)${NC}"
|
||||
FOUND_CANDIDATES="$FOUND_CANDIDATES\n $chip line $line: $EVENTS events (~${RPM_ESTIMATE} RPM)"
|
||||
elif [ "$EVENTS" -gt 0 ]; then
|
||||
echo "$EVENTS events (noise?)"
|
||||
else
|
||||
echo "no events"
|
||||
fi
|
||||
|
||||
# Small delay to let system settle
|
||||
sleep $DELAY_BETWEEN
|
||||
done
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "=== Scan Complete ==="
|
||||
if [ -n "$FOUND_CANDIDATES" ]; then
|
||||
echo -e "${GREEN}Candidate tachometer GPIOs found:${NC}"
|
||||
echo -e "$FOUND_CANDIDATES"
|
||||
echo ""
|
||||
echo "To verify, try monitoring the candidate with varying fan speeds:"
|
||||
echo " gpiomon --num-events=50 <chip> <line>"
|
||||
echo ""
|
||||
echo "Then add to device tree or HAL configuration."
|
||||
else
|
||||
echo -e "${YELLOW}No active GPIOs found.${NC}"
|
||||
echo "Possible reasons:"
|
||||
echo " - Fan tachometer not connected on this carrier board"
|
||||
echo " - Tachometer uses a different interface (I2C, ADC, etc.)"
|
||||
echo " - Fan doesn't have tachometer wire connected"
|
||||
fi
|
||||
|
||||
# Restore fan to auto if we changed it
|
||||
if [ "$FAN_PWM" != "255" ] && [ "$FAN_PWM" != "unknown" ]; then
|
||||
echo ""
|
||||
echo "Restoring fan PWM to $FAN_PWM"
|
||||
echo "$FAN_PWM" > /sys/class/hwmon/hwmon8/pwm1
|
||||
fi
|
||||
Reference in New Issue
Block a user