diff --git a/api/bladeapi/v1alpha1/blade.pb.go b/api/bladeapi/v1alpha1/blade.pb.go index 4728228..bad2074 100644 --- a/api/bladeapi/v1alpha1/blade.pb.go +++ b/api/bladeapi/v1alpha1/blade.pb.go @@ -309,23 +309,82 @@ 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 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"` } 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[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -338,7 +397,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[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -351,7 +410,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{4} } func (x *StatusResponse) GetStealthMode() bool { @@ -396,6 +455,34 @@ 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 +} + var File_api_bladeapi_v1alpha1_blade_proto protoreflect.FileDescriptor var file_api_bladeapi_v1alpha1_blade_proto_rawDesc = []byte{ @@ -414,24 +501,43 @@ 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, + 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, 0xeb, 0x03, 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, 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, @@ -441,7 +547,7 @@ var file_api_bladeapi_v1alpha1_blade_proto_rawDesc = []byte{ 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, + 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, @@ -458,22 +564,27 @@ var file_api_bladeapi_v1alpha1_blade_proto_rawDesc = []byte{ 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, - 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, + 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 +600,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, 5) 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 +608,31 @@ 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 + (*StatusResponse)(nil), // 7: api.bladeapi.v1alpha1.StatusResponse + (*emptypb.Empty)(nil), // 8: 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 + 6, // 2: api.bladeapi.v1alpha1.StatusResponse.fan_curve_steps:type_name -> api.bladeapi.v1alpha1.FanCurveStep + 5, // 3: api.bladeapi.v1alpha1.BladeAgentService.EmitEvent:input_type -> api.bladeapi.v1alpha1.EmitEventRequest + 8, // 4: api.bladeapi.v1alpha1.BladeAgentService.WaitForIdentifyConfirm:input_type -> google.protobuf.Empty + 4, // 5: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeed:input_type -> api.bladeapi.v1alpha1.SetFanSpeedRequest + 8, // 6: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeedAuto:input_type -> google.protobuf.Empty + 3, // 7: api.bladeapi.v1alpha1.BladeAgentService.SetStealthMode:input_type -> api.bladeapi.v1alpha1.StealthModeRequest + 8, // 8: api.bladeapi.v1alpha1.BladeAgentService.GetStatus:input_type -> google.protobuf.Empty + 8, // 9: api.bladeapi.v1alpha1.BladeAgentService.EmitEvent:output_type -> google.protobuf.Empty + 8, // 10: api.bladeapi.v1alpha1.BladeAgentService.WaitForIdentifyConfirm:output_type -> google.protobuf.Empty + 8, // 11: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeed:output_type -> google.protobuf.Empty + 8, // 12: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeedAuto:output_type -> google.protobuf.Empty + 8, // 13: api.bladeapi.v1alpha1.BladeAgentService.SetStealthMode:output_type -> google.protobuf.Empty + 7, // 14: api.bladeapi.v1alpha1.BladeAgentService.GetStatus:output_type -> api.bladeapi.v1alpha1.StatusResponse + 9, // [9:15] is the sub-list for method output_type + 3, // [3:9] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_api_bladeapi_v1alpha1_blade_proto_init() } @@ -563,6 +678,18 @@ 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.(*StatusResponse); i { case 0: return &v.state @@ -581,7 +708,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: 5, NumExtensions: 0, NumServices: 1, }, diff --git a/api/bladeapi/v1alpha1/blade.proto b/api/bladeapi/v1alpha1/blade.proto index 64e46fc..6958ce2 100644 --- a/api/bladeapi/v1alpha1/blade.proto +++ b/api/bladeapi/v1alpha1/blade.proto @@ -1,4 +1,4 @@ -syntax = "proto4"; +syntax = "proto3"; import "google/protobuf/empty.proto"; package api.bladeapi.v1alpha1; @@ -37,6 +37,11 @@ message EmitEventRequest { Event event = 1; } +message FanCurveStep { + int64 temperature = 1; + uint32 percent = 2; +} + message StatusResponse { bool stealth_mode = 1; bool identify_active = 2; @@ -44,6 +49,10 @@ 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; } service BladeAgentService { @@ -53,9 +62,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) {} } diff --git a/api/bladeapi/v1alpha1/blade_grpc.pb.go b/api/bladeapi/v1alpha1/blade_grpc.pb.go index 6e6cddb..011281f 100644 --- a/api/bladeapi/v1alpha1/blade_grpc.pb.go +++ b/api/bladeapi/v1alpha1/blade_grpc.pb.go @@ -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, diff --git a/cmd/agent/main.go b/cmd/agent/main.go index b01ac04..67a1c72 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -13,8 +13,8 @@ import ( "syscall" "time" - "github.com/compute-blade-community/compute-blade-agent/internal/agent" - "github.com/compute-blade-community/compute-blade-agent/internal/api" + 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" @@ -153,8 +153,8 @@ func main() { } }() - log.FromContext(ctx).Info("Bootstrapping compute-blade-agent") - computebladeAgent, err := agent.NewComputeBladeAgent(ctx, cbAgentConfig) + log.FromContext(ctx).Info("Bootstrapping compute-blade-agent", zap.String("version", Version), zap.String("commit", Commit)) + computebladeAgent, err := internal_agent.NewComputeBladeAgent(ctx, cbAgentConfig) if err != nil { cancelCtx(err) log.FromContext(ctx).WithError(err).Fatal("Failed to create agent") @@ -163,17 +163,6 @@ func main() { // Run agent computebladeAgent.RunAsync(ctx, cancelCtx) - // Setup GRPC server - 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) @@ -190,8 +179,11 @@ func main() { wg.Add(1) go func() { defer wg.Done() - otelzap.L().Info("Shutting down grpc server") - grpcServer.GracefulStop() + + 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 @@ -218,7 +210,7 @@ func main() { } } -func runPrometheusEndpoint(ctx context.Context, cancel context.CancelCauseFunc, apiConfig *api.Config) *http.Server { +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) diff --git a/cmd/bladectl/cmd_fan.go b/cmd/bladectl/cmd_fan.go index 686123a..4840441 100644 --- a/cmd/bladectl/cmd_fan.go +++ b/cmd/bladectl/cmd_fan.go @@ -1,38 +1,209 @@ package main import ( + "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" + "google.golang.org/protobuf/types/known/emptypb" ) var ( percent int + auto bool ) func init() { - cmdFan.Flags().IntVarP(&percent, "percent", "p", 40, "Fan speed in percent (Default: 40).") - _ = cmdFan.MarkFlagRequired("percent") + 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(cmdFan) + cmdSet.AddCommand(cmdSetFan) + cmdGet.AddCommand(cmdGetFan) + cmdRemove.AddCommand(cmdRmFan) + cmdDescribe.AddCommand(cmdDescribeFan) } var ( - cmdFan = &cobra.Command{ + fanAliases = []string{"fan_speed", "rpm"} + + 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") + + if autoSet && percentSet { + return fmt.Errorf("only one of --auto or --percent can be specified") + } + + if !autoSet && !percentSet { + return fmt.Errorf("you must specify either --auto or --percent") + } + + 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() - client := clientFromContext(ctx) + clients := clientsFromContext(ctx) - _, err = client.SetFanSpeed(ctx, &bladeapiv1alpha1.SetFanSpeedRequest{ - Percent: int64(percent), - }) + 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 + } - 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() +} diff --git a/cmd/bladectl/cmd_get_misc.go b/cmd/bladectl/cmd_get_misc.go new file mode 100644 index 0000000..c96eeff --- /dev/null +++ b/cmd/bladectl/cmd_get_misc.go @@ -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 + }, + } +) diff --git a/cmd/bladectl/cmd_identify.go b/cmd/bladectl/cmd_identify.go index 436def5..b1f82a7 100644 --- a/cmd/bladectl/cmd_identify.go +++ b/cmd/bladectl/cmd_identify.go @@ -2,6 +2,7 @@ package main import ( "errors" + "fmt" bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1" "github.com/sierrasoftworks/humane-errors-go" @@ -19,69 +20,105 @@ func init() { 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 cmdSetIdentify = &cobra.Command{ - Use: "identify", - Example: "bladectl set identify --wait", - Short: "interact with the compute-blade identity LED", - RunE: runSetIdentify, -} +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") + } -var cmdRmIdentify = &cobra.Command{ - Use: "identify", - Example: "bladectl unset identify", - Short: "remove the identify state with the compute-blade identity LED", - RunE: runRemoveIdentify, -} + ctx := cmd.Context() + clients := clientsFromContext(ctx) -func runSetIdentify(cmd *cobra.Command, _ []string) error { - 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 + } - // Check if we should wait for the identify state to be confirmed - event := bladeapiv1alpha1.Event_IDENTIFY - if confirm { - event = bladeapiv1alpha1.Event_IDENTIFY_CONFIRM - } + // 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()) + } - // 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()) + } + } + } - // Check if we should wait for the identify state to be confirmed - if !wait { - return nil + 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'") - } + 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) - return nil -} + 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()) + } + } -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 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 + }, } - return nil -} + 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) + + 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.IdentifyActive).Render(rowPrefix, activeLabel(bladeStatus.IdentifyActive))) + } + + return nil + }, + } +) diff --git a/cmd/bladectl/cmd_monitor.go b/cmd/bladectl/cmd_monitor.go new file mode 100644 index 0000000..7cb6fa2 --- /dev/null +++ b/cmd/bladectl/cmd_monitor.go @@ -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", "": + return nil + case "": + 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) + } +} diff --git a/cmd/bladectl/cmd_root.go b/cmd/bladectl/cmd_root.go index 656646c..ffb4457 100644 --- a/cmd/bladectl/cmd_root.go +++ b/cmd/bladectl/cmd_root.go @@ -27,12 +27,14 @@ import ( ) var ( - bladeName string - timeout time.Duration + allBlades bool + bladeNames []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().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") } @@ -40,30 +42,20 @@ 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() + ctx, cancelCtx := context.WithCancelCause(cmd.Context()) - // Load potential file configs + // load configuration + var bladectlCfg config.BladectlConfig if err := viper.ReadInConfig(); err != nil { + cancelCtx(err) return err } - - // load configuration - var bladectlCfg config.BladectlConfig if err := viper.Unmarshal(&bladectlCfg); err != nil { + cancelCtx(err) return err } - var blade *config.Blade - - blade, herr := bladectlCfg.FindBlade(bladeName) - if herr != nil { - return errors.New(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, syscall.SIGQUIT) go func() { @@ -73,6 +65,8 @@ var rootCmd = &cobra.Command{ // Wait for signal case sig := <-sigs: + fmt.Println("Received signal", sig.String()) + switch sig { case syscall.SIGTERM: fallthrough @@ -80,7 +74,7 @@ var rootCmd = &cobra.Command{ fallthrough case syscall.SIGQUIT: // On terminate signal, cancel context causing the program to terminate - cancelCtx() + cancelCtx(context.Canceled) default: log.FromContext(ctx).Warn("Received unknown signal", zap.String("signal", sig.String())) @@ -88,69 +82,66 @@ var rootCmd = &cobra.Command{ } }() - // 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 + // Allow to easily select all blades + if allBlades { + bladeNames = make([]string, len(bladectlCfg.Blades)) + for idx, blade := range bladectlCfg.Blades { + bladeNames[idx] = blade.Name + } + } - 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) - } + clients := make([]bladeapiv1alpha1.BladeAgentServiceClient, len(bladeNames)) + for idx, bladeName := range bladeNames { + var blade *config.Blade + blade, herr := bladectlCfg.FindBlade(bladeName) + if herr != nil { + cancelCtx(herr) + return errors.New(herr.Display()) } - if credentials, err = loadTlsCredentials(serverName, certData); err != nil { - return err + client, herr := buildClient(blade) + if herr != nil { + cancelCtx(herr) + return errors.New(herr.Display()) } - } - conn, err := grpc.NewClient(blade.Server, grpc.WithTransportCredentials(credentials)) - if err != nil { - return errors.New( - 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(), - ) + clients[idx] = client } - client := bladeapiv1alpha1.NewBladeAgentServiceClient(conn) - cmd.SetContext(clientIntoContext(ctx, 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, error) { +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, fmt.Errorf("invalid base64 client cert: %w", err) + return nil, humane.Wrap(err, "invalid base64 client cert") } keyPEM, err := base64.StdEncoding.DecodeString(certData.ClientKeyData) if err != nil { - return nil, fmt.Errorf("invalid base64 client key: %w", err) + return nil, humane.Wrap(err, "invalid base64 client key") } caPEM, err := base64.StdEncoding.DecodeString(certData.CertificateAuthorityData) if err != nil { - return nil, fmt.Errorf("invalid base64 CA cert: %w", err) + 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, fmt.Errorf("failed to parse client cert/key pair: %w", err) + 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, fmt.Errorf("failed to append CA certificate") + return nil, humane.Wrap(err, "failed to append CA certificate") } tlsConfig := &tls.Config{ @@ -161,3 +152,35 @@ func loadTlsCredentials(server string, certData config.Certificate) (credentials 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 +} diff --git a/cmd/bladectl/cmd_status.go b/cmd/bladectl/cmd_status.go new file mode 100644 index 0000000..84ce82d --- /dev/null +++ b/cmd/bladectl/cmd_status.go @@ -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() +} diff --git a/cmd/bladectl/cmd_stealth.go b/cmd/bladectl/cmd_stealth.go new file mode 100644 index 0000000..b0f1005 --- /dev/null +++ b/cmd/bladectl/cmd_stealth.go @@ -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 + }, + } +) diff --git a/cmd/bladectl/cmd_verbs.go b/cmd/bladectl/cmd_verbs.go index 52e20ee..de6a699 100644 --- a/cmd/bladectl/cmd_verbs.go +++ b/cmd/bladectl/cmd_verbs.go @@ -7,6 +7,8 @@ import ( func init() { rootCmd.AddCommand(cmdGet) rootCmd.AddCommand(cmdSet) + rootCmd.AddCommand(cmdRemove) + rootCmd.AddCommand(cmdDescribe) } var ( @@ -16,6 +18,12 @@ var ( 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", diff --git a/cmd/bladectl/main.go b/cmd/bladectl/main.go index e9264d8..19ba58f 100644 --- a/cmd/bladectl/main.go +++ b/cmd/bladectl/main.go @@ -12,7 +12,8 @@ import ( type grpcClientContextKey int const ( - defaultGrpcClientContextKey grpcClientContextKey = 0 + defaultGrpcClientContextKey grpcClientContextKey = 0 + defaultGrpcClientsContextKey grpcClientContextKey = 1 ) var ( @@ -25,6 +26,10 @@ func clientIntoContext(ctx context.Context, client bladeapiv1alpha1.BladeAgentSe 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 { @@ -33,6 +38,14 @@ func clientFromContext(ctx context.Context) bladeapiv1alpha1.BladeAgentServiceCl return client } +func clientsFromContext(ctx context.Context) []bladeapiv1alpha1.BladeAgentServiceClient { + clients, ok := ctx.Value(defaultGrpcClientsContextKey).([]bladeapiv1alpha1.BladeAgentServiceClient) + if !ok { + panic("grpc client not found in context") + } + return clients +} + func main() { // Setup configuration viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) diff --git a/cmd/bladectl/util.go b/cmd/bladectl/util.go new file mode 100644 index 0000000..3797222 --- /dev/null +++ b/cmd/bladectl/util.go @@ -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) +} diff --git a/go.mod b/go.mod index 4b23683..3606209 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module github.com/compute-blade-community/compute-blade-agent go 1.24.0 require ( + 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.2 + github.com/olekukonko/tablewriter v1.0.7 github.com/prometheus/client_golang v1.22.0 github.com/sierrasoftworks/humane-errors-go v0.0.0-20250507223502-4bb667dc1e16 github.com/spechtlabs/go-otel-utils/otelprovider v0.0.10 @@ -12,7 +15,7 @@ require ( github.com/spf13/pflag v1.0.6 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 - github.com/warthog618/gpiod v0.9.1 + github.com/warthog618/gpiod v0.8.1 go.bug.st/serial v1.6.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 go.uber.org/zap v1.27.0 @@ -25,11 +28,17 @@ require ( require ( github.com/aws/smithy-go v1.22.3 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // 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/creack/goselect v0.1.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -38,18 +47,29 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // 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/errors v0.0.0-20250405072817-4e6d85265da6 // indirect + github.com/olekukonko/ll v0.0.8 // 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.6.2 // indirect github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.8.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.1.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 // indirect diff --git a/go.sum b/go.sum index 0a5e083..ece40cb 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,36 @@ 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/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/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/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/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/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/frankban/quicktest v1.14.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= @@ -44,8 +60,30 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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-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/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/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo= +github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/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/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw= +github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs= 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= @@ -60,6 +98,9 @@ github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 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= @@ -91,6 +132,8 @@ 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/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= @@ -129,10 +172,14 @@ 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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= diff --git a/internal/agent/agent.go b/internal/agent/agent.go index e652bb2..fa1ecb7 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -1,13 +1,14 @@ -package agent +package internal_agent import ( "context" "errors" "fmt" - "sync" + "net" "time" - agent2 "github.com/compute-blade-community/compute-blade-agent/pkg/agent" + 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" @@ -16,7 +17,9 @@ import ( "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/sierrasoftworks/humane-errors-go" "go.uber.org/zap" + "google.golang.org/grpc" ) var ( @@ -35,56 +38,51 @@ var ( }, []string{"type"}) ) -// 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 agent2.ComputebladeState + state agent.ComputebladeState edgeLedEngine ledengine.LedEngine topLedEngine ledengine.LedEngine fanController fancontroller.FanController eventChan chan events.Event + server *grpc.Server } -func NewComputeBladeAgent(ctx context.Context, opts ComputeBladeAgentConfig) (agent2.ComputeBladeAgent, error) { - var err error - - // blade, err := hal.NewCm4Hal(hal.ComputeBladeHalOpts{ - blade, err := hal.NewCm4Hal(ctx, opts.ComputeBladeHalOpts) +// NewComputeBladeAgent creates and initializes a new ComputeBladeAgent, including gRPC server setup and hardware interfaces. +func NewComputeBladeAgent(ctx context.Context, config agent.ComputeBladeAgentConfig) (agent.ComputeBladeAgent, error) { + blade, err := hal.NewCm4Hal(ctx, config.ComputeBladeHalOpts) if err != nil { return nil, err } - edgeLedEngine := ledengine.NewLedEngine(ledengine.Options{ - LedIdx: hal.LedEdge, - Hal: blade, - }) - - topLedEngine := ledengine.NewLedEngine(ledengine.Options{ - LedIdx: hal.LedTop, - Hal: blade, - }) - - fanController, err := fancontroller.NewLinearFanController(opts.FanControllerConfig) + fanController, err := fancontroller.NewLinearFanController(config.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: agent2.NewComputeBladeState(), - eventChan: make( - chan events.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), + } + + if err := a.setupGrpcServer(ctx); err != nil { + return nil, err + } + + bladeapiv1alpha1.RegisterBladeAgentServiceServer(a.server, a) + return a, nil } -func (a *computeBladeAgentImpl) RunAsync(ctx context.Context, cancel context.CancelCauseFunc) { +// 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) @@ -95,11 +93,10 @@ func (a *computeBladeAgentImpl) RunAsync(ctx context.Context, cancel context.Can }() } -func (a *computeBladeAgentImpl) Run(origCtx context.Context) error { - var wg sync.WaitGroup +// 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 cancelCtx(fmt.Errorf("cancel")) - defer a.cleanup(ctx) log.FromContext(ctx).Info("Starting ComputeBlade agent") @@ -107,104 +104,43 @@ func (a *computeBladeAgentImpl) Run(origCtx context.Context) error { 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 - wg.Add(1) - go func() { - defer wg.Done() - 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") - cancelCtx(err) - } - }() + + 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 && !errors.Is(err, context.Canceled) { - log.FromContext(ctx).WithError(err).Error("Edge button event handler failed") - cancelCtx(err) - } else if err != nil { - 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() - } - } - }() + + 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 && !errors.Is(err, context.Canceled) { - log.FromContext(ctx).WithError(err).Error("Top LED engine failed") - 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 && !errors.Is(err, context.Canceled) { - log.FromContext(ctx).WithError(err).Error("Edge LED engine failed") - 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 && !errors.Is(err, context.Canceled) { - log.FromContext(ctx).WithError(err).Error("Fan Controller Failed") - 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 && !errors.Is(err, context.Canceled) { - log.FromContext(ctx).WithError(err).Error("Event handler failed") - 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).WithError(err).Error("Failed to set fan speed to 100%") @@ -215,162 +151,69 @@ func (a *computeBladeAgentImpl) cleanup(ctx context.Context) { if err := a.blade.SetLed(hal.LedTop, led.Color{}); err != nil { log.FromContext(ctx).WithError(err).Error("Failed to set edge LED to off") } - if err := a.Close(); err != nil { - log.FromContext(ctx).WithError(err).Error("Failed to close blade") - } -} -// EmitEvent dispatches an event to the event handler -func (a *computeBladeAgentImpl) EmitEvent(ctx context.Context, event events.Event) error { - select { - case a.eventChan <- event: - return nil - case <-ctx.Done(): - return ctx.Err() - } + return a.blade.Close() } -// 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") +// 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) } - 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") +// runTopLedEngine runs the top LED engine +// 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) } - 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) -} -// Close shuts down the underlying blade instance and releases any associated resources, returning a combined error if any. -func (a *computeBladeAgentImpl) Close() error { - return errors.Join(a.blade.Close()) -} - -func (a *computeBladeAgentImpl) 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: + 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 nil -} - -func (a *computeBladeAgentImpl) handleIdentifyActive(ctx context.Context) error { - log.FromContext(ctx).Info("Identify active") - return a.edgeLedEngine.SetPattern(ledengine.NewBurstPattern(led.Color{}, 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(led.Color{}, 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) +// runEdgeLedEngine runs the edge LED engine +func (a *computeBladeAgent) runEdgeLedEngine(ctx context.Context, cancel context.CancelCauseFunc) { + log.FromContext(ctx).Info("Starting edge LED engine") - // Reset stealth mode - if err := a.blade.SetStealthMode(a.opts.StealthModeEnabled); err != nil { - return err + 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) } - // Set top LED off - if err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(led.Color{})); err != nil { - return err - } - - return nil -} - -// 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(led.Color{})) - if err != nil { - return 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.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 - } - return a.edgeLedEngine.Run(ctx) -} +// 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") -func (a *computeBladeAgentImpl) runFanController(ctx context.Context) error { // Update fan speed periodically 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: } @@ -381,10 +224,78 @@ func (a *computeBladeAgentImpl) runFanController(ctx context.Context) error { 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).WithError(err).Error("Failed to set fan speed") } } } + +// 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() + } + } +} + +// 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) + } + } + } +} + +// 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) + } + + 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) + } +} diff --git a/internal/agent/api.go b/internal/agent/api.go new file mode 100644 index 0000000..2c158da --- /dev/null +++ b/internal/agent/api.go @@ -0,0 +1,149 @@ +package internal_agent + +import ( + "context" + "crypto/tls" + + 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" +) + +// 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 + } + + 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 +} + +// 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() + } +} + +// 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 +} + +// 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 +} + +// 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 (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), + } + } + + 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), + }, 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) +} diff --git a/internal/api/api_certificates.go b/internal/agent/api_certificates.go similarity index 99% rename from internal/api/api_certificates.go rename to internal/agent/api_certificates.go index 962ccdd..3a2dc66 100644 --- a/internal/api/api_certificates.go +++ b/internal/agent/api_certificates.go @@ -1,4 +1,4 @@ -package api +package internal_agent import ( "context" diff --git a/internal/agent/handler.go b/internal/agent/handler.go new file mode 100644 index 0000000..5f37678 --- /dev/null +++ b/internal/agent/handler.go @@ -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 +} diff --git a/internal/agent/options.go b/internal/agent/options.go new file mode 100644 index 0000000..fdef151 --- /dev/null +++ b/internal/agent/options.go @@ -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) +} diff --git a/internal/agent/utils.go b/internal/agent/utils.go new file mode 100644 index 0000000..8ee919c --- /dev/null +++ b/internal/agent/utils.go @@ -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") + } +} diff --git a/internal/api/api.go b/internal/api/api.go deleted file mode 100644 index e40d39d..0000000 --- a/internal/api/api.go +++ /dev/null @@ -1,191 +0,0 @@ -package api - -import ( - "context" - "crypto/tls" - "errors" - "net" - - bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1" - agent2 "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/log" - grpczap "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" - "github.com/sierrasoftworks/humane-errors-go" - "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" - "go.uber.org/zap" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/emptypb" -) - -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) -} - -// AgentGrpcService represents a gRPC server implementation for managing compute blade agents. -// It embeds UnimplementedBladeAgentServiceServer for forward compatibility and integrates ComputeBladeAgent logic. -// The type allows for serving gRPC requests and gracefully shutting down the server. -type AgentGrpcService struct { - bladeapiv1alpha1.UnimplementedBladeAgentServiceServer - agent agent2.ComputeBladeAgent - server *grpc.Server - authenticated bool - listenAddr string - listenMode ListenMode -} - -// NewGrpcApiServer creates a new gRPC service -func NewGrpcApiServer(ctx context.Context, options ...GrpcApiServiceOption) *AgentGrpcService { - service := &AgentGrpcService{} - - for _, option := range options { - option(service) - } - - grpcOpts := make([]grpc.ServerOption, 0) - - // If we run our gRPC Server TLS with authentication enabled - if service.listenMode == ModeTcp && service.authenticated { - // Load server's certificate and private key - cert, certPool, err := EnsureServerCertificate(ctx) - if err != nil { - log.FromContext(ctx).WithError(err).Fatal("failed to load server key pair") - } - - // Create the TLS config that enforces mTLS for client authentication - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - ClientAuth: tls.RequireAndVerifyClientCert, - ClientCAs: certPool, - } - - // Append the mTLS credentials to our gRPC Options to enable authenticated clients - grpcOpts = append(grpcOpts, grpc.Creds(credentials.NewTLS(tlsConfig))) - - // Make sure we have a local bladectl config with authentication enabled - if err := EnsureAuthenticatedBladectlConfig(ctx, service.listenAddr, service.listenMode); err != nil { - log.FromContext(ctx).WithError(err).Fatal("failed to ensure proper local bladectl config") - } - } else { - // Make sure we have a local bladectl config with no authentication enabled - if err := EnsureUnauthenticatedBladectlConfig(ctx, service.listenAddr, service.listenMode); err != nil { - log.FromContext(ctx).WithError(err).Fatal("failed to ensure proper local bladectl config") - } - } - - // Add Logging Middleware - grpcOpts = append(grpcOpts, grpc.ChainUnaryInterceptor(grpczap.UnaryServerInterceptor(log.InterceptorLogger(log.FromContext(ctx))))) - grpcOpts = append(grpcOpts, grpc.ChainStreamInterceptor(grpczap.StreamServerInterceptor(log.InterceptorLogger(log.FromContext(ctx))))) - grpcOpts = append(grpcOpts, grpc.StatsHandler(otelgrpc.NewServerHandler())) - - // Make server - service.server = grpc.NewServer(grpcOpts...) - bladeapiv1alpha1.RegisterBladeAgentServiceServer(service.server, service) - - return service -} - -// ServeAsync starts the gRPC server asynchronously in a new goroutine and cancels the context if an error occurs. -func (s *AgentGrpcService) ServeAsync(ctx context.Context, cancel context.CancelCauseFunc) { - go func() { - err := s.Serve(ctx) - if err != nil { - log.FromContext(ctx).WithError(err).Error("Failed to start grpc server") - - cancel(err.Cause()) - } - }() -} - -// Serve starts the gRPC server using the configured listen address and mode, returning an error if it fails. -func (s *AgentGrpcService) Serve(ctx context.Context) humane.Error { - if len(s.listenAddr) == 0 { - return humane.New("no listen address provided", - "ensure you are passing a valid listen config to the grpc server", - ) - } - - grpcListen, err := net.Listen(s.listenMode.String(), s.listenAddr) - if err != nil { - return 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).Info("Starting grpc server", zap.String("address", s.listenAddr)) - if err := s.server.Serve(grpcListen); err != nil && !errors.Is(err, grpc.ErrServerStopped) { - return humane.Wrap(err, "failed to start grpc server", - "ensure the gRPC server you are trying to serve to is not already running and the address is not bound by another process", - ) - } - - return nil -} - -// GracefulStop gracefully stops the gRPC server, ensuring all in-progress RPCs are completed before shutting down. -func (s *AgentGrpcService) GracefulStop() { - s.server.GracefulStop() -} - -// EmitEvent emits an event to the agent runtime -func (s *AgentGrpcService) EmitEvent(ctx context.Context, req *bladeapiv1alpha1.EmitEventRequest) (*emptypb.Empty, error) { - switch req.GetEvent() { - case bladeapiv1alpha1.Event_IDENTIFY: - return &emptypb.Empty{}, s.agent.EmitEvent(ctx, events.IdentifyEvent) - case bladeapiv1alpha1.Event_IDENTIFY_CONFIRM: - return &emptypb.Empty{}, s.agent.EmitEvent(ctx, events.IdentifyConfirmEvent) - case bladeapiv1alpha1.Event_CRITICAL: - return &emptypb.Empty{}, s.agent.EmitEvent(ctx, events.CriticalEvent) - case bladeapiv1alpha1.Event_CRITICAL_RESET: - return &emptypb.Empty{}, s.agent.EmitEvent(ctx, events.CriticalResetEvent) - default: - return &emptypb.Empty{}, status.Errorf(codes.InvalidArgument, "invalid event type") - } -} - -// WaitForIdentifyConfirm blocks until the identify confirmation process is completed or an error occurs. -func (s *AgentGrpcService) WaitForIdentifyConfirm(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { - return &emptypb.Empty{}, s.agent.WaitForIdentifyConfirm(ctx) -} - -// SetFanSpeed sets the fan speed of the blade -func (s *AgentGrpcService) SetFanSpeed( - ctx context.Context, - req *bladeapiv1alpha1.SetFanSpeedRequest, -) (*emptypb.Empty, error) { - return &emptypb.Empty{}, s.agent.SetFanSpeed(ctx, uint8(req.GetPercent())) -} - -// SetStealthMode enables/disables stealth mode on the blade -func (s *AgentGrpcService) SetStealthMode(ctx context.Context, req *bladeapiv1alpha1.StealthModeRequest) (*emptypb.Empty, error) { - return &emptypb.Empty{}, s.agent.SetStealthMode(ctx, req.GetEnable()) -} - -// GetStatus aggregates the status of the blade -func (s *AgentGrpcService) GetStatus(context.Context, *emptypb.Empty) (*bladeapiv1alpha1.StatusResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetStatus not implemented") -} diff --git a/internal/api/config.go b/internal/api/config.go deleted file mode 100644 index 2a1ba68..0000000 --- a/internal/api/config.go +++ /dev/null @@ -1,8 +0,0 @@ -package api - -type Config struct { - Metrics string `mapstructure:"metrics"` - Grpc string `mapstructure:"grpc"` - GrpcAuthenticated bool `mapstructure:"authenticated"` - GrpcListenMode string `mapstructure:"mode"` -} diff --git a/internal/api/options.go b/internal/api/options.go deleted file mode 100644 index f72c96a..0000000 --- a/internal/api/options.go +++ /dev/null @@ -1,46 +0,0 @@ -package api - -import ( - "github.com/compute-blade-community/compute-blade-agent/pkg/agent" - "github.com/spechtlabs/go-otel-utils/otelzap" - "go.uber.org/zap" -) - -// GrpcApiServiceOption defines a functional option for configuring an AgentGrpcService instance. -type GrpcApiServiceOption func(*AgentGrpcService) - -// WithComputeBladeAgent sets the ComputeBladeAgent implementation for the AgentGrpcService. -func WithComputeBladeAgent(agent agent.ComputeBladeAgent) GrpcApiServiceOption { - return func(service *AgentGrpcService) { - service.agent = agent - } -} - -// WithAuthentication configures the authentication requirement for the gRPC service by enabling or disabling it. -func WithAuthentication(enabled bool) GrpcApiServiceOption { - return func(service *AgentGrpcService) { - service.authenticated = enabled - } -} - -// WithListenAddr sets the server's listen address on an AgentGrpcService instance. -func WithListenAddr(server string) GrpcApiServiceOption { - return func(service *AgentGrpcService) { - service.listenAddr = server - } -} - -// WithListenMode configures the listen mode for the AgentGrpcService using the provided mode string. -func WithListenMode(mode string) GrpcApiServiceOption { - return func(service *AgentGrpcService) { - lMode, err := ListenModeFromString(mode) - if err != nil { - otelzap.L().Fatal(err.Error(), - zap.String("mode", mode), - zap.Strings("advice", err.Advice()), - ) - } - - service.listenMode = lMode - } -} diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index d964034..421eb6d 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -3,21 +3,17 @@ package agent import ( "context" - "github.com/compute-blade-community/compute-blade-agent/pkg/events" + 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 - // EmitEvent emits an event to the agent - EmitEvent(ctx context.Context, event events.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 + // GracefulStop gracefully stops the gRPC server, ensuring all in-progress RPCs are completed before shutting down. + GracefulStop(ctx context.Context) error } diff --git a/internal/agent/config.go b/pkg/agent/config.go similarity index 86% rename from internal/agent/config.go rename to pkg/agent/config.go index a475238..78a3da0 100644 --- a/internal/agent/config.go +++ b/pkg/agent/config.go @@ -1,7 +1,6 @@ package agent import ( - "github.com/compute-blade-community/compute-blade-agent/internal/api" "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" @@ -11,12 +10,19 @@ 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 api.Config `mapstructure:"listen"` + Listen ApiConfig `mapstructure:"listen"` // Hal is the hardware abstraction layer configuration Hal hal.Config `mapstructure:"hal"` diff --git a/pkg/fancontroller/fancontroller.go b/pkg/fancontroller/fancontroller.go index 072c44b..299ef08 100644 --- a/pkg/fancontroller/fancontroller.go +++ b/pkg/fancontroller/fancontroller.go @@ -10,7 +10,14 @@ import ( 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 + + // 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 @@ -60,14 +67,18 @@ func NewLinearFanController(config Config) (FanController, humane.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() @@ -90,3 +101,7 @@ func (f *fanControllerLinear) GetFanSpeed(temperature float64) uint8 { return uint8(speed) } + +func (f *fanControllerLinear) IsAutomaticSpeed() bool { + return f.overrideOpts == nil +} diff --git a/pkg/fancontroller/fancontroller_test.go b/pkg/fancontroller/fancontroller_test.go index 3a647d5..d534767 100644 --- a/pkg/fancontroller/fancontroller_test.go +++ b/pkg/fancontroller/fancontroller_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller" + "github.com/stretchr/testify/assert" ) func TestFanControllerLinear_GetFanSpeed(t *testing.T) { @@ -30,15 +31,16 @@ 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") }) } } @@ -75,10 +77,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") }) } } @@ -127,11 +128,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) }) } } diff --git a/pkg/hal/hal.go b/pkg/hal/hal.go index 280ed56..617388f 100644 --- a/pkg/hal/hal.go +++ b/pkg/hal/hal.go @@ -32,8 +32,10 @@ const ( PowerPoe802at ) +type LedIndex uint8 + const ( - LedTop = iota + LedTop LedIndex = iota LedEdge ) @@ -53,8 +55,10 @@ type ComputeBladeHal interface { GetFanRPM() (float64, error) // SetStealthMode enables/disables stealth mode of the blade (turning on/off the LEDs) SetStealthMode(enabled bool) error + // StealthModeActive returns if stealth mode of the blade is currently active + StealthModeActive() bool // SetLed sets the color of the LEDs - SetLed(idx uint, color led.Color) error + 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 @@ -65,7 +69,6 @@ type ComputeBladeHal interface { // FanUnit abstracts the fan unit type FanUnit interface { - // Kind returns the kind of the fan FanUnit Kind() FanUnitKind diff --git a/pkg/hal/hal_bcm2711.go b/pkg/hal/hal_bcm2711.go index ff66f7e..ef75881 100644 --- a/pkg/hal/hal_bcm2711.go +++ b/pkg/hal/hal_bcm2711.go @@ -386,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 @@ -402,7 +410,7 @@ func serializePwmDataFrame(data uint8) uint32 { return result } -func (bcm *bcm2711) SetLed(idx uint, color led.Color) 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) } diff --git a/pkg/hal/hal_bcm2711_simulated.go b/pkg/hal/hal_bcm2711_simulated.go index f8a32c2..de0d27a 100644 --- a/pkg/hal/hal_bcm2711_simulated.go +++ b/pkg/hal/hal_bcm2711_simulated.go @@ -16,7 +16,8 @@ var _ ComputeBladeHal = &SimulatedHal{} // SimulatedHal implements a mock for the ComputeBladeHal interface type SimulatedHal struct { - logger *zap.Logger + logger *zap.Logger + isStealthMode bool } func NewCm4Hal(_ context.Context, _ ComputeBladeHalOpts) (ComputeBladeHal, error) { @@ -58,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) @@ -79,9 +86,9 @@ func (m *SimulatedHal) WaitForEdgeButtonPress(ctx context.Context) error { } } -func (m *SimulatedHal) SetLed(idx uint, color led.Color) 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 } diff --git a/pkg/hal/hal_bcm2711_standardfanunit.go b/pkg/hal/hal_bcm2711_standardfanunit.go index 08e26af..70d8827 100644 --- a/pkg/hal/hal_bcm2711_standardfanunit.go +++ b/pkg/hal/hal_bcm2711_standardfanunit.go @@ -6,10 +6,8 @@ import ( "context" "math" - "github.com/compute-blade-community/compute-blade-agent/pkg/log" - "go.uber.org/zap" - "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" ) @@ -50,7 +48,7 @@ func (fu standardFanUnitBcm2711) Run(ctx context.Context) error { defer func(fanEdgeLine *gpiod.Line) { err := fanEdgeLine.Close() if err != nil { - log.FromContext(ctx).Error("failed to close fanEdgeLine", zap.Error(err)) + log.FromContext(ctx).WithError(err).Error("failed to close fanEdgeLine") } }(fu.fanEdgeLine) } diff --git a/pkg/hal/hal_mock.go b/pkg/hal/hal_mock.go index 98d9ecf..d9c031d 100644 --- a/pkg/hal/hal_mock.go +++ b/pkg/hal/hal_mock.go @@ -40,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) @@ -50,7 +55,7 @@ func (m *ComputeBladeHalMock) WaitForEdgeButtonPress(ctx context.Context) error return args.Error(0) } -func (m *ComputeBladeHalMock) SetLed(idx uint, color led.Color) error { +func (m *ComputeBladeHalMock) SetLed(idx LedIndex, color led.Color) error { args := m.Called(idx, color) return args.Error(0) } diff --git a/pkg/ledengine/ledengine.go b/pkg/ledengine/ledengine.go index 7d06ef7..32d9b8e 100644 --- a/pkg/ledengine/ledengine.go +++ b/pkg/ledengine/ledengine.go @@ -20,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 @@ -101,6 +101,13 @@ func NewSlowBlinkPattern(baseColor led.Color, activeColor led.Color) BlinkPatter } } +func New(hal hal.ComputeBladeHal, ledIdx hal.LedIndex) LedEngine { + return NewLedEngine(Options{ + Hal: hal, + LedIdx: ledIdx, + }) +} + func NewLedEngine(opts Options) LedEngine { clock := opts.Clock if clock == nil { diff --git a/pkg/ledengine/ledengine_test.go b/pkg/ledengine/ledengine_test.go index 93b9c1e..a3aff45 100644 --- a/pkg/ledengine/ledengine_test.go +++ b/pkg/ledengine/ledengine_test.go @@ -123,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() @@ -131,8 +142,8 @@ 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), led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil) - cbMock.On("SetLed", uint(0), led.Color{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.Options{ Hal: &cbMock, @@ -178,7 +189,7 @@ func Test_LedEngine_SetPattern_BeforeRun(t *testing.T) { clk.On("After", time.Hour).Once().Return(clkAfterChan) cbMock := hal.ComputeBladeHalMock{} - cbMock.On("SetLed", uint(0), led.Color{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.Options{ Hal: &cbMock, @@ -220,8 +231,8 @@ 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), led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil) - cbMock.On("SetLed", uint(0), led.Color{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.Options{ Hal: &cbMock, @@ -254,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") +} diff --git a/pkg/ledengine/options.go b/pkg/ledengine/options.go index 5a64fec..2060429 100644 --- a/pkg/ledengine/options.go +++ b/pkg/ledengine/options.go @@ -8,7 +8,7 @@ import ( // Options are the options for the LedEngine type Options struct { // LedIdx is the index of the LED to control - LedIdx uint + LedIdx hal.LedIndex // Hal is the computeblade hardware abstraction layer Hal hal.ComputeBladeHal // Clock is the clock used for timing diff --git a/pkg/util/clock_test.go b/pkg/util/clock_test.go new file mode 100644 index 0000000..024616c --- /dev/null +++ b/pkg/util/clock_test.go @@ -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) +} diff --git a/pkg/util/file_exist_test.go b/pkg/util/file_exist_test.go new file mode 100644 index 0000000..d0ea6b6 --- /dev/null +++ b/pkg/util/file_exist_test.go @@ -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") +} diff --git a/pkg/util/host_ips_test.go b/pkg/util/host_ips_test.go new file mode 100644 index 0000000..7a1439d --- /dev/null +++ b/pkg/util/host_ips_test.go @@ -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") + } +} diff --git a/renovate.json b/renovate.json index 140f2ee..4ecdda7 100644 --- a/renovate.json +++ b/renovate.json @@ -15,5 +15,8 @@ "automerge": true, "automergeType": "branch" } + ], + "ignoreDeps": [ + "github.com/warthog618/gpiod" ] } \ No newline at end of file