mirror of
https://github.com/compute-blade-community/compute-blade-agent.git
synced 2026-04-21 17:45:43 +02:00
feat(agent)!: add support for mTLS authentication in gRPC server (#54)
* refactor(fancontroller): improve fan controller validation logic and error handling for temperature steps * refactor(agent): restructure gRPC server implementation by moving it to a new api package for better organization and maintainability * feat(agent): implement gRPC server for managing compute blade agents and add graceful shutdown support refactor(agent): restructure agent code by moving API logic to a dedicated file and improving error handling fix(agent): update logging messages for clarity and consistency across the agent's operations chore(agent): remove unused API code and consolidate event handling logic for better maintainability style(agent): improve code formatting and organization for better readability and adherence to conventions * feat(agent): add support for TLS configuration in gRPC server * feat(api): add gRPC server authentication * fix * feat(config): add listen mode configuration to support tcp or unix sockets feat(agent): implement listen mode in gRPC service to allow flexible socket types feat(bladectl): enhance configuration loading and add support for TLS credentials fix(bladectl): improve error handling for gRPC connection and event emission style(logging): change log level from Warn to Info for better clarity in logs * add logging middleware + fixes * fix remote-connection to gRPC API Server debugging the SAN issues took the soul out of me... And then the stupid mistake in cmd_root where I didn't construct the TLS credentials correctly... Oh dear... * cleanup * cleanup * cleanup commands * cleanup * make README.md nicer * Update cmd/agent/main.go Co-authored-by: Matthias Riegler <github@m4tbit.de> * Update cmd/bladectl/cmd_root.go Co-authored-by: Matthias Riegler <github@m4tbit.de> * move bladectl config into correct directory * fix bugs * // FIXME: No dead code * nit: code style * nit(YAGNI): you aint gonna need it. Don't make life harder than it needs to be * nit(YAGNI): you aint gonna need it. Don't make life harder than it needs to be * nit(YAGNI): you aint gonna need it. Don't make life harder than it needs to be * nit(cmd_identify) --------- Co-authored-by: Matthias Riegler <github@m4tbit.de>
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
# Default configuration for the compute-blade-agent
|
||||
|
||||
log:
|
||||
mode: production # production, development
|
||||
|
||||
# Listen configuration
|
||||
listen:
|
||||
metrics: ":9666"
|
||||
grpc: /tmp/compute-blade-agent.sock
|
||||
authenticated: false
|
||||
mode: unix # tcp or unix
|
||||
|
||||
# Hardware abstraction layer configuration
|
||||
hal:
|
||||
# For the default fan unit, fanspeed measurement is causing a tiny bit of CPU laod.
|
||||
# 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
|
||||
|
||||
@@ -35,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
|
||||
percent: 40
|
||||
- temperature: 55
|
||||
percent: 80
|
||||
|
||||
# Critical temperature threshold
|
||||
critical_temperature_threshold: 60
|
||||
|
||||
@@ -2,8 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/uptime-induestries/compute-blade-agent/internal/agent"
|
||||
"github.com/uptime-induestries/compute-blade-agent/pkg/log"
|
||||
"github.com/uptime-industries/compute-blade-agent/internal/agent"
|
||||
"github.com/uptime-industries/compute-blade-agent/internal/api"
|
||||
"github.com/uptime-industries/compute-blade-agent/pkg/log"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -28,13 +28,12 @@ var (
|
||||
Date string
|
||||
)
|
||||
|
||||
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("BLADE")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
@@ -49,13 +48,11 @@ func main() {
|
||||
|
||||
// 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", "compute-blade-agent"))
|
||||
@@ -71,73 +68,96 @@ func main() {
|
||||
// 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)
|
||||
log.FromContext(ctx).Fatal("Failed to load configuration", zap.Error(err))
|
||||
}
|
||||
|
||||
// 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()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.FromContext(ctx).Info("Bootstrapping compute-blade-agent", zap.String("version", Version), zap.String("commit", Commit), zap.String("date", Date))
|
||||
computebladeAgent, err := agent.NewComputeBladeAgent(ctx, cbAgentConfig)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error("Failed to create agent", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
os.Exit(1)
|
||||
log.FromContext(ctx).Fatal("Failed to create agent", zap.Error(err))
|
||||
}
|
||||
|
||||
// Run agent
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.FromContext(ctx).Info("Starting agent")
|
||||
err := computebladeAgent.Run(ctx)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).Error("Failed to run agent", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
}()
|
||||
computebladeAgent.RunAsync(ctx, cancelCtx)
|
||||
|
||||
// Setup GRPC server
|
||||
// FIXME add logging middleware
|
||||
grpcServer := grpc.NewServer()
|
||||
bladeapiv1alpha1.RegisterBladeAgentServiceServer(grpcServer, agent.NewGrpcServiceFor(computebladeAgent))
|
||||
grpcServer := api.NewGrpcApiServer(ctx,
|
||||
api.WithComputeBladeAgent(computebladeAgent),
|
||||
api.WithAuthentication(cbAgentConfig.Listen.GrpcAuthenticated),
|
||||
api.WithListenAddr(cbAgentConfig.Listen.Grpc),
|
||||
api.WithListenMode(cbAgentConfig.Listen.GrpcListenMode),
|
||||
)
|
||||
|
||||
// Run gRPC API
|
||||
grpcServer.ServeAsync(ctx, cancelCtx)
|
||||
|
||||
// setup prometheus endpoint
|
||||
promServer := runPrometheusEndpoint(ctx, cancelCtx, &cbAgentConfig.Listen)
|
||||
|
||||
// Wait for done
|
||||
<-ctx.Done()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Shut-Down GRPC Server
|
||||
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()
|
||||
}()
|
||||
|
||||
// setup prometheus endpoint
|
||||
// Shut-Down Prometheus Endpoint
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
shutdownCtx, shutdownCtxCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer shutdownCtxCancel()
|
||||
|
||||
if err := promServer.Shutdown(shutdownCtx); err != nil {
|
||||
log.FromContext(ctx).Error("Failed to shutdown prometheus/pprof server", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Wait for context cancel
|
||||
if err := ctx.Err(); !errors.Is(err, context.Canceled) {
|
||||
log.FromContext(ctx).Fatal("Exiting", zap.Error(err))
|
||||
} else {
|
||||
log.FromContext(ctx).Info("Exiting")
|
||||
}
|
||||
}
|
||||
|
||||
func runPrometheusEndpoint(ctx context.Context, cancel context.CancelCauseFunc, apiConfig *api.Config) *http.Server {
|
||||
instrumentationHandler := http.NewServeMux()
|
||||
instrumentationHandler.Handle("/metrics", promhttp.Handler())
|
||||
instrumentationHandler.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
@@ -145,33 +165,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 {
|
||||
if err != nil && !errors.Is(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))
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
bladeapiv1alpha1 "github.com/uptime-industries/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
"github.com/spf13/cobra"
|
||||
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
bladeapiv1alpha1 "github.com/uptime-industries/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
@@ -13,21 +15,27 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdIdentify.Flags().BoolVarP(&confirm, "confirm", "c", false, "confirm the identify state")
|
||||
cmdIdentify.Flags().BoolVarP(&wait, "wait", "w", false, "Wait for the identify state to be confirmed (e.g. by a physical button press)")
|
||||
cmdSet.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)
|
||||
}
|
||||
|
||||
var cmdIdentify = &cobra.Command{
|
||||
var cmdSetIdentify = &cobra.Command{
|
||||
Use: "identify",
|
||||
Example: "bladectl set identify --wait",
|
||||
Short: "interact with the compute-blade identity LED",
|
||||
RunE: runIdentity,
|
||||
RunE: runSetIdentify,
|
||||
}
|
||||
|
||||
func runIdentity(cmd *cobra.Command, _ []string) error {
|
||||
var err error
|
||||
var cmdRmIdentify = &cobra.Command{
|
||||
Use: "identify",
|
||||
Example: "bladectl unset identify",
|
||||
Short: "remove the identify state with the compute-blade identity LED",
|
||||
RunE: runRemoveIdentify,
|
||||
}
|
||||
|
||||
func runSetIdentify(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
client := clientFromContext(ctx)
|
||||
|
||||
@@ -38,17 +46,41 @@ func runIdentity(cmd *cobra.Command, _ []string) error {
|
||||
}
|
||||
|
||||
// Emit the event to the compute-blade-agent
|
||||
_, err = client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: event})
|
||||
_, err := client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: event})
|
||||
if err != nil {
|
||||
return 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'")
|
||||
return fmt.Errorf(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 {
|
||||
_, err := client.WaitForIdentifyConfirm(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return 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'")
|
||||
}
|
||||
if !wait {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := client.WaitForIdentifyConfirm(ctx, &emptypb.Empty{}); err != nil {
|
||||
return 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'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runRemoveIdentify(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
client := clientFromContext(ctx)
|
||||
|
||||
// Emit the event to the compute-blade-agent
|
||||
_, err := client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: bladeapiv1alpha1.Event_IDENTIFY_CONFIRM})
|
||||
if err != nil {
|
||||
return fmt.Errorf(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
|
||||
|
||||
@@ -2,45 +2,161 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
humane "github.com/sierrasoftworks/humane-errors-go"
|
||||
"github.com/spf13/cobra"
|
||||
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sierrasoftworks/humane-errors-go"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
bladeapiv1alpha1 "github.com/uptime-industries/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/uptime-industries/compute-blade-agent/cmd/bladectl/config"
|
||||
"github.com/uptime-industries/compute-blade-agent/pkg/log"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var (
|
||||
bladeName string
|
||||
timeout time.Duration
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVar(&bladeName, "blade", "", "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 {
|
||||
origCtx := cmd.Context()
|
||||
|
||||
// Load potential file configs
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// load configuration
|
||||
var bladectlCfg config.BladectlConfig
|
||||
if err := viper.Unmarshal(&bladectlCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var blade *config.Blade
|
||||
|
||||
blade, herr := bladectlCfg.FindBlade(bladeName)
|
||||
if herr != nil {
|
||||
return fmt.Errorf(herr.Display())
|
||||
}
|
||||
|
||||
// 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)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
go func() {
|
||||
// Wait for context cancel or signal
|
||||
select {
|
||||
// Wait for context cancel
|
||||
case <-ctx.Done():
|
||||
case <-sigs:
|
||||
// On signal, cancel context
|
||||
cancelCtx()
|
||||
|
||||
// Wait for signal
|
||||
case sig := <-sigs:
|
||||
switch sig {
|
||||
case syscall.SIGTERM:
|
||||
fallthrough
|
||||
case syscall.SIGINT:
|
||||
fallthrough
|
||||
case syscall.SIGQUIT:
|
||||
// On terminate signal, cancel context causing the program to terminate
|
||||
cancelCtx()
|
||||
|
||||
default:
|
||||
log.FromContext(ctx).Warn("Received unknown signal", zap.String("signal", sig.String()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := grpc.Dial(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return 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")
|
||||
}
|
||||
client := bladeapiv1alpha1.NewBladeAgentServiceClient(conn)
|
||||
// Create our gRPC Transport Credentials
|
||||
credentials := 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 {
|
||||
var err error
|
||||
|
||||
serverName := blade.Server
|
||||
if strings.Contains(serverName, ":") {
|
||||
if serverName, _, err = net.SplitHostPort(blade.Server); err != nil {
|
||||
return fmt.Errorf("failed to parse server address: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if credentials, err = loadTlsCredentials(serverName, certData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(blade.Server, grpc.WithTransportCredentials(credentials))
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
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",
|
||||
).Display(),
|
||||
)
|
||||
}
|
||||
|
||||
client := bladeapiv1alpha1.NewBladeAgentServiceClient(conn)
|
||||
cmd.SetContext(clientIntoContext(ctx, client))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func loadTlsCredentials(server string, certData config.Certificate) (credentials.TransportCredentials, error) {
|
||||
// Decode base64 certificate, key, and CA
|
||||
certPEM, err := base64.StdEncoding.DecodeString(certData.ClientCertificateData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid base64 client cert: %w", err)
|
||||
}
|
||||
|
||||
keyPEM, err := base64.StdEncoding.DecodeString(certData.ClientKeyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid base64 client key: %w", err)
|
||||
}
|
||||
|
||||
caPEM, err := base64.StdEncoding.DecodeString(certData.CertificateAuthorityData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid base64 CA cert: %w", err)
|
||||
}
|
||||
|
||||
// Load client cert/key pair
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse client cert/key pair: %w", err)
|
||||
}
|
||||
|
||||
// Load CA into CertPool
|
||||
caPool := x509.NewCertPool()
|
||||
if !caPool.AppendCertsFromPEM(caPEM) {
|
||||
return nil, fmt.Errorf("failed to append CA certificate")
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
RootCAs: caPool,
|
||||
ServerName: server,
|
||||
}
|
||||
|
||||
return credentials.NewTLS(tlsConfig), nil
|
||||
}
|
||||
|
||||
@@ -21,4 +21,11 @@ var (
|
||||
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.",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
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"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
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) (*Blade, humane.Error) {
|
||||
if len(name) == 0 {
|
||||
name = c.CurrentBlade
|
||||
}
|
||||
|
||||
for _, blade := range c.Blades {
|
||||
if blade.Name == name {
|
||||
return &blade.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 {
|
||||
zap.L().Fatal("Failed to extract hostname", zap.Error(err))
|
||||
}
|
||||
|
||||
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/uptime-industries/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
|
||||
}
|
||||
@@ -3,33 +3,24 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
"github.com/spf13/viper"
|
||||
bladeapiv1alpha1 "github.com/uptime-industries/compute-blade-agent/api/bladeapi/v1alpha1"
|
||||
)
|
||||
|
||||
type grpcClientContextKey int
|
||||
|
||||
const (
|
||||
defaultGrpcClientContextKey grpcClientContextKey = 0
|
||||
defaultGrpcClientConnContextKey grpcClientContextKey = 1
|
||||
defaultGrpcClientContextKey grpcClientContextKey = 0
|
||||
)
|
||||
|
||||
var (
|
||||
grpcAddr string
|
||||
timeout time.Duration
|
||||
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().
|
||||
StringVar(&grpcAddr, "addr", "unix:///tmp/compute-blade-agent.sock", "address of the compute-blade-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)
|
||||
}
|
||||
@@ -43,6 +34,13 @@ func clientFromContext(ctx context.Context) bladeapiv1alpha1.BladeAgentServiceCl
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"github.com/uptime-induestries/compute-blade-agent/pkg/eventbus"
|
||||
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit"
|
||||
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/emc2101"
|
||||
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/proto"
|
||||
"machine"
|
||||
|
||||
"github.com/uptime-industries/compute-blade-agent/pkg/events"
|
||||
"github.com/uptime-industries/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/uptime-industries/compute-blade-agent/pkg/smartfanunit"
|
||||
"github.com/uptime-industries/compute-blade-agent/pkg/smartfanunit/emc2101"
|
||||
"github.com/uptime-industries/compute-blade-agent/pkg/smartfanunit/proto"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/ws2812"
|
||||
@@ -33,7 +34,7 @@ type Controller struct {
|
||||
LeftUART drivers.UART
|
||||
RightUART drivers.UART
|
||||
|
||||
eb eventbus.EventBus
|
||||
eb events.EventBus
|
||||
leftLed led.Color
|
||||
rightLed led.Color
|
||||
leftReqFanSpeed uint8
|
||||
@@ -43,7 +44,7 @@ type Controller struct {
|
||||
}
|
||||
|
||||
func (c *Controller) Run(parentCtx context.Context) error {
|
||||
c.eb = eventbus.New()
|
||||
c.eb = events.New()
|
||||
|
||||
c.FanController.Init()
|
||||
c.FanController.SetFanPercent(c.DefaultFanSpeed)
|
||||
@@ -80,7 +81,7 @@ func (c *Controller) Run(parentCtx context.Context) error {
|
||||
})
|
||||
|
||||
// right blade events
|
||||
println("[+] Starting event listener (righ)")
|
||||
println("[+] Starting event listener (right)")
|
||||
group.Go(func() error {
|
||||
return c.listenEvents(ctx, c.RightUART, rightBladeTopicIn)
|
||||
})
|
||||
@@ -123,7 +124,7 @@ func (c *Controller) Run(parentCtx context.Context) error {
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
// listenEvents reads events from the UART interface and dispatches them to the eventbus
|
||||
// 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
|
||||
@@ -137,9 +138,9 @@ func (c *Controller) listenEvents(ctx context.Context, uart drivers.UART, target
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchEvents reads events from the eventbus and writes them to the UART interface
|
||||
// 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, eventbus.MatchAll)
|
||||
sub := c.eb.Subscribe(sourceTopic, 4, events.MatchAll)
|
||||
defer sub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
@@ -201,9 +202,9 @@ func (c *Controller) metricReporter(ctx context.Context) error {
|
||||
func (c *Controller) updateFanSpeed(ctx context.Context) error {
|
||||
var pkt smartfanunit.SetFanSpeedPercentPacket
|
||||
|
||||
subLeft := c.eb.Subscribe(leftBladeTopicIn, 1, eventbus.MatchAll)
|
||||
subLeft := c.eb.Subscribe(leftBladeTopicIn, 1, events.MatchAll)
|
||||
defer subLeft.Unsubscribe()
|
||||
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, eventbus.MatchAll)
|
||||
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, events.MatchAll)
|
||||
defer subRight.Unsubscribe()
|
||||
|
||||
for {
|
||||
|
||||
@@ -4,11 +4,12 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit"
|
||||
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/emc2101"
|
||||
"machine"
|
||||
|
||||
"github.com/uptime-industries/compute-blade-agent/pkg/smartfanunit"
|
||||
"github.com/uptime-industries/compute-blade-agent/pkg/smartfanunit/emc2101"
|
||||
"tinygo.org/x/drivers/ws2812"
|
||||
)
|
||||
|
||||
@@ -26,15 +27,15 @@ func main() {
|
||||
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
|
||||
goto errPrint
|
||||
}
|
||||
machine.UART0.SetBaudRate(smartfanunit.Baudrate)
|
||||
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
|
||||
goto errPrint
|
||||
}
|
||||
machine.UART1.SetBaudRate(smartfanunit.Baudrate)
|
||||
machine.UART1.SetBaudRate(smartfanunit.BaudRate)
|
||||
|
||||
// Enables fan, DO NOT CHANGE
|
||||
machine.GP16.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
@@ -57,7 +58,7 @@ func main() {
|
||||
err = emc.Init()
|
||||
if err != nil {
|
||||
println("[!] Failed to initialize emc2101:", err.Error())
|
||||
goto errprint
|
||||
goto errPrint
|
||||
}
|
||||
|
||||
println("[+] IO initialized, starting controller...")
|
||||
@@ -75,7 +76,7 @@ func main() {
|
||||
err = controller.Run(context.Background())
|
||||
|
||||
// Blinking -> something went wrong
|
||||
errprint:
|
||||
errPrint:
|
||||
ledState := false
|
||||
for {
|
||||
ledState = !ledState
|
||||
|
||||
Reference in New Issue
Block a user