snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
677
weed/plugin/worker/admin_script_handler.go
Normal file
677
weed/plugin/worker/admin_script_handler.go
Normal file
@@ -0,0 +1,677 @@
|
||||
package pluginworker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/shell"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
adminScriptJobType = "admin_script"
|
||||
maxAdminScriptOutputBytes = 16 * 1024
|
||||
defaultAdminScriptRunMins = 17
|
||||
adminScriptDetectTickMinutes = 17
|
||||
)
|
||||
|
||||
const defaultAdminScript = `fs.log.purge -daysAgo=7
|
||||
volume.deleteEmpty -quietFor=24h -apply
|
||||
volume.fix.replication -apply
|
||||
s3.clean.uploads -timeAgo=24h`
|
||||
|
||||
var adminScriptTokenRegex = regexp.MustCompile(`'.*?'|".*?"|\S+`)
|
||||
|
||||
func init() {
|
||||
RegisterHandler(HandlerFactory{
|
||||
JobType: "admin_script",
|
||||
Category: CategoryDefault,
|
||||
Aliases: []string{"admin-script", "admin.script", "script", "admin"},
|
||||
Build: func(opts HandlerBuildOptions) (JobHandler, error) {
|
||||
return NewAdminScriptHandler(opts.GrpcDialOption), nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type AdminScriptHandler struct {
|
||||
grpcDialOption grpc.DialOption
|
||||
}
|
||||
|
||||
func NewAdminScriptHandler(grpcDialOption grpc.DialOption) *AdminScriptHandler {
|
||||
return &AdminScriptHandler{grpcDialOption: grpcDialOption}
|
||||
}
|
||||
|
||||
func (h *AdminScriptHandler) Capability() *plugin_pb.JobTypeCapability {
|
||||
return &plugin_pb.JobTypeCapability{
|
||||
JobType: adminScriptJobType,
|
||||
CanDetect: true,
|
||||
CanExecute: true,
|
||||
MaxDetectionConcurrency: 1,
|
||||
MaxExecutionConcurrency: 1,
|
||||
DisplayName: "Admin Script",
|
||||
Description: "Execute custom admin shell scripts",
|
||||
Weight: 20,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AdminScriptHandler) Descriptor() *plugin_pb.JobTypeDescriptor {
|
||||
return &plugin_pb.JobTypeDescriptor{
|
||||
JobType: adminScriptJobType,
|
||||
DisplayName: "Admin Script",
|
||||
Description: "Run custom admin shell scripts not covered by built-in job types",
|
||||
Icon: "fas fa-terminal",
|
||||
DescriptorVersion: 1,
|
||||
AdminConfigForm: &plugin_pb.ConfigForm{
|
||||
FormId: "admin-script-admin",
|
||||
Title: "Admin Script Configuration",
|
||||
Description: "Define the admin shell script to execute.",
|
||||
Sections: []*plugin_pb.ConfigSection{
|
||||
{
|
||||
SectionId: "script",
|
||||
Title: "Script",
|
||||
Description: "Commands run sequentially by the admin script worker.",
|
||||
Fields: []*plugin_pb.ConfigField{
|
||||
{
|
||||
Name: "script",
|
||||
Label: "Script",
|
||||
Description: "Admin shell commands to execute (one per line).",
|
||||
HelpText: "Lock/unlock are handled by the admin server; omit explicit lock/unlock commands.",
|
||||
Placeholder: "volume.balance -apply\nvolume.fix.replication -apply",
|
||||
FieldType: plugin_pb.ConfigFieldType_CONFIG_FIELD_TYPE_STRING,
|
||||
Widget: plugin_pb.ConfigWidget_CONFIG_WIDGET_TEXTAREA,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "run_interval_minutes",
|
||||
Label: "Run Interval (minutes)",
|
||||
Description: "Minimum interval between successful admin script runs.",
|
||||
FieldType: plugin_pb.ConfigFieldType_CONFIG_FIELD_TYPE_INT64,
|
||||
Widget: plugin_pb.ConfigWidget_CONFIG_WIDGET_NUMBER,
|
||||
Required: true,
|
||||
MinValue: &plugin_pb.ConfigValue{Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: 1}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
DefaultValues: map[string]*plugin_pb.ConfigValue{
|
||||
"script": {
|
||||
Kind: &plugin_pb.ConfigValue_StringValue{StringValue: defaultAdminScript},
|
||||
},
|
||||
"run_interval_minutes": {
|
||||
Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: defaultAdminScriptRunMins},
|
||||
},
|
||||
},
|
||||
},
|
||||
AdminRuntimeDefaults: &plugin_pb.AdminRuntimeDefaults{
|
||||
Enabled: true,
|
||||
DetectionIntervalMinutes: adminScriptDetectTickMinutes,
|
||||
DetectionTimeoutSeconds: 300,
|
||||
MaxJobsPerDetection: 1,
|
||||
GlobalExecutionConcurrency: 1,
|
||||
PerWorkerExecutionConcurrency: 1,
|
||||
RetryLimit: 0,
|
||||
RetryBackoffSeconds: 30,
|
||||
JobTypeMaxRuntimeSeconds: 1800,
|
||||
ExecutionTimeoutSeconds: 1800,
|
||||
},
|
||||
WorkerDefaultValues: map[string]*plugin_pb.ConfigValue{},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AdminScriptHandler) Detect(ctx context.Context, request *plugin_pb.RunDetectionRequest, sender DetectionSender) error {
|
||||
if request == nil {
|
||||
return fmt.Errorf("run detection request is nil")
|
||||
}
|
||||
if sender == nil {
|
||||
return fmt.Errorf("detection sender is nil")
|
||||
}
|
||||
if request.JobType != "" && request.JobType != adminScriptJobType {
|
||||
return fmt.Errorf("job type %q is not handled by admin_script worker", request.JobType)
|
||||
}
|
||||
|
||||
script := normalizeAdminScript(ReadStringConfig(request.GetAdminConfigValues(), "script", ""))
|
||||
scriptName := strings.TrimSpace(ReadStringConfig(request.GetAdminConfigValues(), "script_name", ""))
|
||||
runIntervalMinutes := readAdminScriptRunIntervalMinutes(request.GetAdminConfigValues())
|
||||
if ShouldSkipDetectionByInterval(request.GetLastSuccessfulRun(), runIntervalMinutes*60) {
|
||||
_ = sender.SendActivity(BuildDetectorActivity(
|
||||
"skipped_by_interval",
|
||||
fmt.Sprintf("ADMIN SCRIPT: Detection skipped due to run interval (%dm)", runIntervalMinutes),
|
||||
map[string]*plugin_pb.ConfigValue{
|
||||
"run_interval_minutes": {
|
||||
Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: int64(runIntervalMinutes)},
|
||||
},
|
||||
},
|
||||
))
|
||||
if err := sender.SendProposals(&plugin_pb.DetectionProposals{
|
||||
JobType: adminScriptJobType,
|
||||
Proposals: []*plugin_pb.JobProposal{},
|
||||
HasMore: false,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return sender.SendComplete(&plugin_pb.DetectionComplete{
|
||||
JobType: adminScriptJobType,
|
||||
Success: true,
|
||||
TotalProposals: 0,
|
||||
})
|
||||
}
|
||||
|
||||
commands := parseAdminScriptCommands(script)
|
||||
execCount := countExecutableCommands(commands)
|
||||
if execCount == 0 {
|
||||
_ = sender.SendActivity(BuildDetectorActivity(
|
||||
"no_script",
|
||||
"ADMIN SCRIPT: No executable commands configured",
|
||||
map[string]*plugin_pb.ConfigValue{
|
||||
"command_count": {
|
||||
Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: int64(execCount)},
|
||||
},
|
||||
},
|
||||
))
|
||||
if err := sender.SendProposals(&plugin_pb.DetectionProposals{
|
||||
JobType: adminScriptJobType,
|
||||
Proposals: []*plugin_pb.JobProposal{},
|
||||
HasMore: false,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return sender.SendComplete(&plugin_pb.DetectionComplete{
|
||||
JobType: adminScriptJobType,
|
||||
Success: true,
|
||||
TotalProposals: 0,
|
||||
})
|
||||
}
|
||||
|
||||
proposal := buildAdminScriptProposal(script, scriptName, execCount)
|
||||
proposals := []*plugin_pb.JobProposal{proposal}
|
||||
hasMore := false
|
||||
maxResults := int(request.MaxResults)
|
||||
if maxResults > 0 && len(proposals) > maxResults {
|
||||
proposals = proposals[:maxResults]
|
||||
hasMore = true
|
||||
}
|
||||
|
||||
if err := sender.SendProposals(&plugin_pb.DetectionProposals{
|
||||
JobType: adminScriptJobType,
|
||||
Proposals: proposals,
|
||||
HasMore: hasMore,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sender.SendComplete(&plugin_pb.DetectionComplete{
|
||||
JobType: adminScriptJobType,
|
||||
Success: true,
|
||||
TotalProposals: 1,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminScriptHandler) Execute(ctx context.Context, request *plugin_pb.ExecuteJobRequest, sender ExecutionSender) error {
|
||||
if request == nil || request.Job == nil {
|
||||
return fmt.Errorf("execute job request is nil")
|
||||
}
|
||||
if sender == nil {
|
||||
return fmt.Errorf("execution sender is nil")
|
||||
}
|
||||
if request.Job.JobType != "" && request.Job.JobType != adminScriptJobType {
|
||||
return fmt.Errorf("job type %q is not handled by admin_script worker", request.Job.JobType)
|
||||
}
|
||||
|
||||
script := normalizeAdminScript(ReadStringConfig(request.Job.Parameters, "script", ""))
|
||||
scriptName := strings.TrimSpace(ReadStringConfig(request.Job.Parameters, "script_name", ""))
|
||||
if script == "" {
|
||||
script = normalizeAdminScript(ReadStringConfig(request.GetAdminConfigValues(), "script", ""))
|
||||
}
|
||||
if scriptName == "" {
|
||||
scriptName = strings.TrimSpace(ReadStringConfig(request.GetAdminConfigValues(), "script_name", ""))
|
||||
}
|
||||
|
||||
commands := parseAdminScriptCommands(script)
|
||||
execCommands := filterExecutableCommands(commands)
|
||||
if len(execCommands) == 0 {
|
||||
return sender.SendCompleted(&plugin_pb.JobCompleted{
|
||||
Success: false,
|
||||
ErrorMessage: "no executable admin script commands configured",
|
||||
})
|
||||
}
|
||||
|
||||
commandEnv, cancel, err := h.buildAdminScriptCommandEnv(ctx, request.ClusterContext)
|
||||
if err != nil {
|
||||
return sender.SendCompleted(&plugin_pb.JobCompleted{
|
||||
Success: false,
|
||||
ErrorMessage: err.Error(),
|
||||
})
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
if err := sender.SendProgress(&plugin_pb.JobProgressUpdate{
|
||||
JobId: request.Job.JobId,
|
||||
JobType: request.Job.JobType,
|
||||
State: plugin_pb.JobState_JOB_STATE_ASSIGNED,
|
||||
ProgressPercent: 0,
|
||||
Stage: "assigned",
|
||||
Message: "admin script job accepted",
|
||||
Activities: []*plugin_pb.ActivityEvent{
|
||||
BuildExecutorActivity("assigned", "admin script job accepted"),
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output := &limitedBuffer{maxBytes: maxAdminScriptOutputBytes}
|
||||
executed := 0
|
||||
errorMessages := make([]string, 0)
|
||||
executedCommands := make([]string, 0, len(execCommands))
|
||||
|
||||
for _, cmd := range execCommands {
|
||||
if ctx.Err() != nil {
|
||||
errorMessages = append(errorMessages, ctx.Err().Error())
|
||||
break
|
||||
}
|
||||
|
||||
commandLine := formatAdminScriptCommand(cmd)
|
||||
executedCommands = append(executedCommands, commandLine)
|
||||
_, _ = fmt.Fprintf(output, "$ %s\n", commandLine)
|
||||
|
||||
found := false
|
||||
sendBroken := false
|
||||
for _, command := range shell.Commands {
|
||||
if command.Name() != cmd.Name {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// The env is reused for every command in the script, and noLock
|
||||
// belongs to the invocation that set it. ForceNoLock already exempts
|
||||
// this path, so this changes nothing today -- it keeps the rule the
|
||||
// same in all three dispatchers rather than resting on that.
|
||||
commandEnv.SetNoLock(false)
|
||||
if err := command.Do(cmd.Args, commandEnv, output); err != nil {
|
||||
msg := fmt.Sprintf("%s: %v", cmd.Name, err)
|
||||
errorMessages = append(errorMessages, msg)
|
||||
if sendErr := sender.SendProgress(&plugin_pb.JobProgressUpdate{
|
||||
State: plugin_pb.JobState_JOB_STATE_RUNNING,
|
||||
ProgressPercent: percentProgress(executed+1, len(execCommands)),
|
||||
Stage: "error",
|
||||
Message: msg,
|
||||
Activities: []*plugin_pb.ActivityEvent{
|
||||
BuildExecutorActivity("error", msg),
|
||||
},
|
||||
}); sendErr != nil {
|
||||
sendBroken = true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if sendBroken {
|
||||
break
|
||||
}
|
||||
|
||||
if !found {
|
||||
msg := fmt.Sprintf("unknown admin command: %s", cmd.Name)
|
||||
errorMessages = append(errorMessages, msg)
|
||||
if sendErr := sender.SendProgress(&plugin_pb.JobProgressUpdate{
|
||||
State: plugin_pb.JobState_JOB_STATE_RUNNING,
|
||||
ProgressPercent: percentProgress(executed+1, len(execCommands)),
|
||||
Stage: "error",
|
||||
Message: msg,
|
||||
Activities: []*plugin_pb.ActivityEvent{
|
||||
BuildExecutorActivity("error", msg),
|
||||
},
|
||||
}); sendErr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
executed++
|
||||
progress := percentProgress(executed, len(execCommands))
|
||||
if sendErr := sender.SendProgress(&plugin_pb.JobProgressUpdate{
|
||||
State: plugin_pb.JobState_JOB_STATE_RUNNING,
|
||||
ProgressPercent: progress,
|
||||
Stage: "running",
|
||||
Message: fmt.Sprintf("executed %d/%d command(s)", executed, len(execCommands)),
|
||||
Activities: []*plugin_pb.ActivityEvent{
|
||||
BuildExecutorActivity("running", commandLine),
|
||||
},
|
||||
}); sendErr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
scriptHash := hashAdminScript(script)
|
||||
resultSummary := fmt.Sprintf("admin script executed (%d command(s))", executed)
|
||||
if scriptName != "" {
|
||||
resultSummary = fmt.Sprintf("admin script %q executed (%d command(s))", scriptName, executed)
|
||||
}
|
||||
|
||||
outputValues := map[string]*plugin_pb.ConfigValue{
|
||||
"command_count": {
|
||||
Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: int64(executed)},
|
||||
},
|
||||
"error_count": {
|
||||
Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: int64(len(errorMessages))},
|
||||
},
|
||||
"script_hash": {
|
||||
Kind: &plugin_pb.ConfigValue_StringValue{StringValue: scriptHash},
|
||||
},
|
||||
}
|
||||
if scriptName != "" {
|
||||
outputValues["script_name"] = &plugin_pb.ConfigValue{
|
||||
Kind: &plugin_pb.ConfigValue_StringValue{StringValue: scriptName},
|
||||
}
|
||||
}
|
||||
if len(executedCommands) > 0 {
|
||||
outputValues["commands"] = &plugin_pb.ConfigValue{
|
||||
Kind: &plugin_pb.ConfigValue_StringList{
|
||||
StringList: &plugin_pb.StringList{Values: executedCommands},
|
||||
},
|
||||
}
|
||||
}
|
||||
if out := strings.TrimSpace(output.String()); out != "" {
|
||||
outputValues["output"] = &plugin_pb.ConfigValue{
|
||||
Kind: &plugin_pb.ConfigValue_StringValue{StringValue: out},
|
||||
}
|
||||
}
|
||||
if output.truncated {
|
||||
outputValues["output_truncated"] = &plugin_pb.ConfigValue{
|
||||
Kind: &plugin_pb.ConfigValue_BoolValue{BoolValue: true},
|
||||
}
|
||||
}
|
||||
|
||||
success := len(errorMessages) == 0 && ctx.Err() == nil
|
||||
errorMessage := ""
|
||||
if !success {
|
||||
errorMessage = strings.Join(errorMessages, "; ")
|
||||
if ctx.Err() != nil {
|
||||
if errorMessage == "" {
|
||||
errorMessage = ctx.Err().Error()
|
||||
} else {
|
||||
errorMessage = fmt.Sprintf("%s; %s", errorMessage, ctx.Err().Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sender.SendCompleted(&plugin_pb.JobCompleted{
|
||||
Success: success,
|
||||
ErrorMessage: errorMessage,
|
||||
Result: &plugin_pb.JobResult{
|
||||
Summary: resultSummary,
|
||||
OutputValues: outputValues,
|
||||
},
|
||||
Activities: []*plugin_pb.ActivityEvent{
|
||||
BuildExecutorActivity("completed", resultSummary),
|
||||
},
|
||||
CompletedAt: timestamppb.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func readAdminScriptRunIntervalMinutes(values map[string]*plugin_pb.ConfigValue) int {
|
||||
runIntervalMinutes := int(ReadInt64Config(values, "run_interval_minutes", defaultAdminScriptRunMins))
|
||||
if runIntervalMinutes <= 0 {
|
||||
return defaultAdminScriptRunMins
|
||||
}
|
||||
return runIntervalMinutes
|
||||
}
|
||||
|
||||
type adminScriptCommand struct {
|
||||
Name string
|
||||
Args []string
|
||||
Raw string
|
||||
}
|
||||
|
||||
func normalizeAdminScript(script string) string {
|
||||
script = strings.ReplaceAll(script, "\r\n", "\n")
|
||||
return strings.TrimSpace(script)
|
||||
}
|
||||
|
||||
func parseAdminScriptCommands(script string) []adminScriptCommand {
|
||||
script = normalizeAdminScript(script)
|
||||
if script == "" {
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(script, "\n")
|
||||
commands := make([]adminScriptCommand, 0)
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
for _, chunk := range strings.Split(line, ";") {
|
||||
chunk = strings.TrimSpace(chunk)
|
||||
if chunk == "" {
|
||||
continue
|
||||
}
|
||||
parts := adminScriptTokenRegex.FindAllString(chunk, -1)
|
||||
if len(parts) == 0 {
|
||||
continue
|
||||
}
|
||||
args := make([]string, 0, len(parts)-1)
|
||||
for _, arg := range parts[1:] {
|
||||
args = append(args, strings.Trim(arg, "\"'"))
|
||||
}
|
||||
commands = append(commands, adminScriptCommand{
|
||||
Name: strings.TrimSpace(parts[0]),
|
||||
Args: args,
|
||||
Raw: chunk,
|
||||
})
|
||||
}
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func filterExecutableCommands(commands []adminScriptCommand) []adminScriptCommand {
|
||||
exec := make([]adminScriptCommand, 0, len(commands))
|
||||
for _, cmd := range commands {
|
||||
if cmd.Name == "" {
|
||||
continue
|
||||
}
|
||||
if isAdminScriptLockCommand(cmd.Name) {
|
||||
continue
|
||||
}
|
||||
exec = append(exec, cmd)
|
||||
}
|
||||
return exec
|
||||
}
|
||||
|
||||
func countExecutableCommands(commands []adminScriptCommand) int {
|
||||
count := 0
|
||||
for _, cmd := range commands {
|
||||
if cmd.Name == "" {
|
||||
continue
|
||||
}
|
||||
if isAdminScriptLockCommand(cmd.Name) {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func isAdminScriptLockCommand(name string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(name)) {
|
||||
case "lock", "unlock":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func buildAdminScriptProposal(script, scriptName string, commandCount int) *plugin_pb.JobProposal {
|
||||
scriptHash := hashAdminScript(script)
|
||||
summary := "Run admin script"
|
||||
if scriptName != "" {
|
||||
summary = fmt.Sprintf("Run admin script: %s", scriptName)
|
||||
}
|
||||
detail := fmt.Sprintf("Admin script with %d command(s)", commandCount)
|
||||
proposalID := fmt.Sprintf("admin-script-%s-%d", scriptHash[:8], time.Now().UnixNano())
|
||||
|
||||
labels := map[string]string{
|
||||
"script_hash": scriptHash,
|
||||
}
|
||||
if scriptName != "" {
|
||||
labels["script_name"] = scriptName
|
||||
}
|
||||
|
||||
return &plugin_pb.JobProposal{
|
||||
ProposalId: proposalID,
|
||||
DedupeKey: "admin-script:" + scriptHash,
|
||||
JobType: adminScriptJobType,
|
||||
Priority: plugin_pb.JobPriority_JOB_PRIORITY_NORMAL,
|
||||
Summary: summary,
|
||||
Detail: detail,
|
||||
Parameters: map[string]*plugin_pb.ConfigValue{
|
||||
"script": {
|
||||
Kind: &plugin_pb.ConfigValue_StringValue{StringValue: script},
|
||||
},
|
||||
"script_name": {
|
||||
Kind: &plugin_pb.ConfigValue_StringValue{StringValue: scriptName},
|
||||
},
|
||||
"script_hash": {
|
||||
Kind: &plugin_pb.ConfigValue_StringValue{StringValue: scriptHash},
|
||||
},
|
||||
"command_count": {
|
||||
Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: int64(commandCount)},
|
||||
},
|
||||
},
|
||||
Labels: labels,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AdminScriptHandler) buildAdminScriptCommandEnv(
|
||||
ctx context.Context,
|
||||
clusterContext *plugin_pb.ClusterContext,
|
||||
) (*shell.CommandEnv, context.CancelFunc, error) {
|
||||
options, err := h.buildAdminScriptShellOptions(clusterContext)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
commandEnv := shell.NewCommandEnv(&options)
|
||||
commandEnv.ForceNoLock()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
go commandEnv.MasterClient.KeepConnectedToMaster(ctx)
|
||||
|
||||
return commandEnv, cancel, nil
|
||||
}
|
||||
|
||||
// buildAdminScriptShellOptions maps a cluster context onto shell.ShellOptions.
|
||||
// It is split out from buildAdminScriptCommandEnv so the address handling is
|
||||
// testable without standing up a master client.
|
||||
func (h *AdminScriptHandler) buildAdminScriptShellOptions(clusterContext *plugin_pb.ClusterContext) (shell.ShellOptions, error) {
|
||||
if clusterContext == nil {
|
||||
return shell.ShellOptions{}, fmt.Errorf("cluster context is required")
|
||||
}
|
||||
|
||||
masters := normalizeAddressList(clusterContext.MasterGrpcAddresses)
|
||||
if len(masters) == 0 {
|
||||
return shell.ShellOptions{}, fmt.Errorf("missing master addresses for admin script")
|
||||
}
|
||||
|
||||
filerGroup := ""
|
||||
mastersValue := strings.Join(masters, ",")
|
||||
options := shell.ShellOptions{
|
||||
Masters: &mastersValue,
|
||||
GrpcDialOption: h.grpcDialOption,
|
||||
FilerGroup: &filerGroup,
|
||||
Directory: "/",
|
||||
}
|
||||
|
||||
// FilerAddresses are pb.ServerAddress strings (host:httpPort.grpcPort).
|
||||
// ShellOptions.FilerAddress is itself a ServerAddress; shell commands derive
|
||||
// the gRPC or HTTP port from it as needed, so store it verbatim.
|
||||
filers := normalizeAddressList(clusterContext.FilerAddresses)
|
||||
if len(filers) > 0 {
|
||||
options.FilerAddress = pb.ServerAddress(filers[0])
|
||||
} else {
|
||||
glog.V(1).Infof("admin script worker missing filer address; filer-dependent commands may fail")
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func normalizeAddressList(addresses []string) []string {
|
||||
normalized := make([]string, 0, len(addresses))
|
||||
seen := make(map[string]struct{}, len(addresses))
|
||||
for _, address := range addresses {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[address]; exists {
|
||||
continue
|
||||
}
|
||||
seen[address] = struct{}{}
|
||||
normalized = append(normalized, address)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func hashAdminScript(script string) string {
|
||||
sum := sha256.Sum256([]byte(script))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func formatAdminScriptCommand(cmd adminScriptCommand) string {
|
||||
if len(cmd.Args) == 0 {
|
||||
return cmd.Name
|
||||
}
|
||||
return fmt.Sprintf("%s %s", cmd.Name, strings.Join(cmd.Args, " "))
|
||||
}
|
||||
|
||||
func percentProgress(done, total int) float64 {
|
||||
if total <= 0 {
|
||||
return 0
|
||||
}
|
||||
if done < 0 {
|
||||
done = 0
|
||||
}
|
||||
if done > total {
|
||||
done = total
|
||||
}
|
||||
return float64(done) / float64(total) * 100
|
||||
}
|
||||
|
||||
type limitedBuffer struct {
|
||||
buf bytes.Buffer
|
||||
maxBytes int
|
||||
truncated bool
|
||||
}
|
||||
|
||||
func (b *limitedBuffer) Write(p []byte) (int, error) {
|
||||
if b == nil {
|
||||
return len(p), nil
|
||||
}
|
||||
if b.maxBytes <= 0 {
|
||||
b.truncated = true
|
||||
return len(p), nil
|
||||
}
|
||||
remaining := b.maxBytes - b.buf.Len()
|
||||
if remaining <= 0 {
|
||||
b.truncated = true
|
||||
return len(p), nil
|
||||
}
|
||||
if len(p) > remaining {
|
||||
_, _ = b.buf.Write(p[:remaining])
|
||||
b.truncated = true
|
||||
return len(p), nil
|
||||
}
|
||||
_, _ = b.buf.Write(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (b *limitedBuffer) String() string {
|
||||
if b == nil {
|
||||
return ""
|
||||
}
|
||||
return b.buf.String()
|
||||
}
|
||||
Reference in New Issue
Block a user