From b93956fde450ef97e3777923587f9533fbb6dcf7 Mon Sep 17 00:00:00 2001 From: Michael Buchmann Date: Wed, 2 Jul 2025 16:04:52 -0500 Subject: [PATCH 1/2] Added JAWS PDU Support Signed-off-by: Michael Buchmann --- CHANGELOG.md | 1 + Dockerfile | 5 + Dockerfile.integration.Dockerfile | 5 + Dockerfile.pprof | 5 + cmd/power-control/main.go | 29 +++++ internal/domain/jaws.go | 184 ++++++++++++++++++++++++++++++ internal/domain/power-status.go | 4 + internal/domain/transitions.go | 11 +- 8 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 internal/domain/jaws.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 29afe981..13bd1eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Support for JAWS PDU devices - Added support for pprof builds ### Changes diff --git a/Dockerfile b/Dockerfile index af0b5aea..d7b409c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,6 +49,11 @@ ENV API_URL="http://cray-power" ENV API_SERVER_PORT=":28007" ENV API_BASE_PATH="/v1" +ENV GODEBUG="tlsrsakex=1" + +ENV PCS_JAWS_MONITOR="true" +ENV PCS_JAWS_MONITOR_INTERVAL="20" + COPY power-control /usr/local/bin/ COPY configs configs diff --git a/Dockerfile.integration.Dockerfile b/Dockerfile.integration.Dockerfile index 6bed807c..670ab69c 100644 --- a/Dockerfile.integration.Dockerfile +++ b/Dockerfile.integration.Dockerfile @@ -80,6 +80,11 @@ ENV CRAY_VAULT_AUTH_PATH="auth/token/create" ENV CRAY_VAULT_ROLE_FILE="/configs/namespace" ENV CRAY_VAULT_JWT_FILE="/configs/token" +ENV GODEBUG="tlsrsakex=1" + +ENV PCS_JAWS_MONITOR="true" +ENV PCS_JAWS_MONITOR_INTERVAL="20" + #nobody 65534:65534 USER 65534:65534 diff --git a/Dockerfile.pprof b/Dockerfile.pprof index af08c649..d09f6900 100644 --- a/Dockerfile.pprof +++ b/Dockerfile.pprof @@ -80,6 +80,11 @@ ENV CRAY_VAULT_AUTH_PATH="auth/token/create" ENV CRAY_VAULT_ROLE_FILE="/configs/namespace" ENV CRAY_VAULT_JWT_FILE="/configs/token" +ENV GODEBUG="tlsrsakex=1" + +ENV PCS_JAWS_MONITOR="true" +ENV PCS_JAWS_MONITOR_INTERVAL="20" + #nobody 65534:65534 USER 65534:65534 diff --git a/cmd/power-control/main.go b/cmd/power-control/main.go index 578ac3be..79e96114 100644 --- a/cmd/power-control/main.go +++ b/cmd/power-control/main.go @@ -493,6 +493,35 @@ func main() { logger.Log, (time.Duration(pwrSampleInterval) * time.Second), statusTimeout, statusHttpRetries, maxIdleConns, maxIdleConnsPerHost) + // Start up Monitoring of JAWS PDU devices + jawsMonitor := false + envstr = os.Getenv("PCS_JAWS_MONITOR") + if envstr != "" { + yn, _ := strconv.ParseBool(envstr) + if yn == true { + logger.Log.Infof("Monitoring JAWS devices.") + jawsMonitor = true + } + } else { + logger.Log.Infof("JAWS Monitoring set to False") + } + + if jawsMonitor { + jawsMonitorInterval := 30 + envstr = os.Getenv("PCS_JAWS_MONITOR_INTERVAL") + if envstr != "" { + jmi, err := strconv.Atoi(envstr) + if err != nil { + logger.Log.Errorf("Invalid value of PCS_JAWS_MONITOR_INTERVAL, defaulting to %d", + jawsMonitorInterval) + } else { + logger.Log.Infof("Using PCS_JAWS_MONITOR_INTERVAL: %v", jmi) + jawsMonitorInterval = jmi + } + } + go domain.JawsMonitor(jawsMonitorInterval) + } + domain.StartRecordsReaper() /////////////////////////////// diff --git a/internal/domain/jaws.go b/internal/domain/jaws.go new file mode 100644 index 00000000..43765eef --- /dev/null +++ b/internal/domain/jaws.go @@ -0,0 +1,184 @@ +// MIT License +// +// (C) Copyright [2025] Hewlett Packard Enterprise Development LP +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +package domain + +import ( + "context" + "crypto/tls" + "encoding/json" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + pcsmodel "github.com/OpenCHAMI/power-control/v2/internal/model" + "github.com/sirupsen/logrus" + + "github.com/OpenCHAMI/power-control/v2/internal/logger" + "github.com/hashicorp/go-retryablehttp" +) + +type JawsEndpointStatus struct { + Id string `json:"id"` + Name string `json:"name"` + Active_power int `json:"active_power"` + Active_power_status string `json:"active_power_status"` + Apparent_power int `json:"apparent_power"` + Branch_id string `json:"branch_id"` + Control_state string `json:"control_state"` + Current float32 `json:"current"` + Current_capacity int `json:"current_capacity"` + Current_status string `json:"current_status"` + Current_utilized float32 `json:"current_utilized"` + Energy int `json:"energy"` + Ocp_id string `json:"ocp_id"` + Phase_id string `json:"phase_id"` + Power_capacity int `json:"power_capacity"` + Power_factor_status string `json:"power_factor_status"` + Socket_adapter string `json:"socket_adapter"` + Socket_type string `json:"socket_type"` + State string `json:"state"` + Status string `json:"status"` + Voltage float32 `json:"voltage"` +} + +// Load JAWS outlets and store +func JawsLoad(xname string, FQDN string, authUser string, authPass string) { + timeout := 20 + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := retryablehttp.NewClient() + client.HTTPClient.Transport = transport + + var req *retryablehttp.Request + + // jaws/monitor/outlets + jurl, _ := url.Parse("https://" + FQDN + "/jaws/monitor/outlets") + req, err := retryablehttp.NewRequest("GET", jurl.String(), nil) + if err != nil { + logger.Log.Error(err) + return + } + + reqContext, reqCtxCancel := context.WithTimeout(context.Background(), time.Second*time.Duration(timeout)) + req = req.WithContext(reqContext) + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("cache-control", "no-cache") + + if !(authUser == "" && authPass == "") { + req.SetBasicAuth(authUser, authPass) + } + + resp, err := client.Do(req) + defer drainAndCloseBodyWithCtxCancel(resp, reqCtxCancel) + if err != nil { + return + } + + body, err := io.ReadAll(resp.Body) + var eps []JawsEndpointStatus + if err != nil { + logger.Log.Error(err) + } else { + err = json.Unmarshal(body, &eps) + + // Store power state for each outlet + for _, jep2 := range eps { + epxname := jaws2xname(xname, jep2.Id) + + powerState := pcsmodel.PowerStateFilter_Undefined + if strings.EqualFold(jep2.State, "on") { + powerState = pcsmodel.PowerStateFilter_On + } else if strings.EqualFold(jep2.State, "off") { + powerState = pcsmodel.PowerStateFilter_Off + } + + updateHWState(epxname, powerState, pcsmodel.ManagementStateFilter_available, "") + } + } + return +} + +// Convert JAWS outlet name to an xname +// example: x3000m0p0v17 +func jaws2xname(xname string, id string) string { + nxname := xname + nxname = nxname + "p" + strconv.Itoa(int(id[0])-int('A')) + nxname = nxname + "v" + id[2:] + return nxname +} + +// JAWS loop to monitor PDUs +func JawsMonitor(looptime int) { + logger.Log.Info("In JAWS Monitor") + if looptime == 0 { + looptime = 20 + } + // Loop forever + for { + xnameList := []string{} + // Find PDUs + compMap, err := (*hsmHandle).FillHSMData([]string{"all"}) + if err != nil { + logger.Log.Error("JAWS FillHMSDATA ERROR: ", err) + } else { + for _, xname := range compMap { + if xname.BaseData.Type == "CabinetPDUController" { + if xname.PowerStatusURI == "/jaws" { + xnameList = append(xnameList, xname.BaseData.ID) + } + } + } + } + for _, xname := range xnameList { + logger.Log.Info("JAWS Load: " + xname) + var user, pw string + if GLOB.VaultEnabled { + user, pw, err = (*GLOB.CS).GetControllerCredentials(xname) + if err != nil { + logger.Log.WithFields(logrus.Fields{"ERROR": err}).Error("Unable to get credentials for " + xname) + } + } + JawsLoad(xname, xname, user, pw) + } + // Wait for a bit + time.Sleep(time.Duration(looptime) * time.Second) + } +} + +func drainAndCloseBodyWithCtxCancel(resp *http.Response, ctxCancel context.CancelFunc) { + // Must always drain and close response bodies + if resp != nil && resp.Body != nil { + _, _ = io.Copy(io.Discard, resp.Body) // ok even if already drained + resp.Body.Close() + } + // Call context cancel function, if supplied. This must be done after + // draining and closing the response body + if ctxCancel != nil { + ctxCancel() + } +} diff --git a/internal/domain/power-status.go b/internal/domain/power-status.go index 99a31b4a..3d5377a0 100644 --- a/internal/domain/power-status.go +++ b/internal/domain/power-status.go @@ -536,6 +536,10 @@ func getHWStatesFromHW() error { glogger.Warnf("%s: Missing FQDN or power status URI for %s", fname, k) taskList[taskIX].Ignore = true taskIX++ + // PDU JAWS Device, handled in own monitoring loop + } else if strings.Contains(v.HSMData.PowerStatusURI, "/jaws") { + taskList[taskIX].Ignore = true + taskIX++ } else { url = "https://" + v.HSMData.RfFQDN + v.HSMData.PowerStatusURI taskList[taskIX].Request, _ = http.NewRequest(http.MethodGet, url, nil) diff --git a/internal/domain/transitions.go b/internal/domain/transitions.go index 46ed795e..d46ecdd1 100644 --- a/internal/domain/transitions.go +++ b/internal/domain/transitions.go @@ -784,7 +784,12 @@ func doTransition(transitionID uuid.UUID) { comp.Task.Operation = powerActionOp trsTaskMap[trsTaskList[trsTaskIdx].GetID()] = comp trsTaskList[trsTaskIdx].CPolicy.Retry.Retries = 3 - trsTaskList[trsTaskIdx].Request, _ = http.NewRequest("POST", "https://"+comp.HSMData.RfFQDN+comp.HSMData.PowerActionURI, bytes.NewBuffer([]byte(payload))) + method := "POST" + // JAWS Update uses the PATCH method + if strings.Contains(comp.HSMData.PowerActionURI, "/jaws") { + method = "PATCH" + } + trsTaskList[trsTaskIdx].Request, _ = http.NewRequest(method, "https://"+comp.HSMData.RfFQDN+comp.HSMData.PowerActionURI, bytes.NewBuffer([]byte(payload))) trsTaskList[trsTaskIdx].Request.Header.Set("Content-Type", "application/json") trsTaskList[trsTaskIdx].Request.Header.Add("HMS-Service", GLOB.BaseTRSTask.ServiceName) // Vault enabled? @@ -1600,6 +1605,10 @@ func generateTransitionPayload(comp *TransitionComponent, action string) (string } else { body = fmt.Sprintf(`{"PowerState": "%s"}`, resetType) } + // If we have a JAWS PDU + if strings.Contains(comp.HSMData.PowerActionURI, "jaws") { + body = fmt.Sprintf(`{"control_action": "%s"}`, resetType) + } } else { body = fmt.Sprintf(`{"ResetType": "%s"}`, resetType) } From 07ce71d68b4316b884405b127978d7b4ba5e49dc Mon Sep 17 00:00:00 2001 From: Michael Buchmann Date: Thu, 31 Jul 2025 10:56:36 -0500 Subject: [PATCH 2/2] Removed Dockerfile.integration.Dockerfile --- Dockerfile.integration.Dockerfile | 91 ------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 Dockerfile.integration.Dockerfile diff --git a/Dockerfile.integration.Dockerfile b/Dockerfile.integration.Dockerfile deleted file mode 100644 index 670ab69c..00000000 --- a/Dockerfile.integration.Dockerfile +++ /dev/null @@ -1,91 +0,0 @@ -# MIT License -# -# (C) Copyright [2022-2024] Hewlett Packard Enterprise Development LP -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. - -# Dockerfile for building power-control. - -# Build base just has the packages installed we need. -FROM docker.io/library/golang:1.24-alpine AS build-base - -RUN set -ex \ - && apk -U upgrade \ - && apk add build-base - -# Base copies in the files we need to test/build. -FROM build-base AS base - -RUN go env -w GO111MODULE=auto - -# Copy all the necessary files to the image. -COPY cmd $GOPATH/src/github.com/OpenCHAMI/power-control/v2/cmd -COPY go.mod $GOPATH/src/github.com/OpenCHAMI/power-control/v2/go.mod -COPY go.sum $GOPATH/src/github.com/OpenCHAMI/power-control/v2/go.sum -COPY internal $GOPATH/src/github.com/OpenCHAMI/power-control/v2/internal - -### Build Stage ### -FROM base AS builder - -RUN set -ex && go build -C $GOPATH/src/github.com/OpenCHAMI/power-control/v2/cmd/power-control -v -tags musl -o /usr/local/bin/power-control - -### Final Stage ### - -FROM docker.io/alpine:3 -LABEL maintainer="Hewlett Packard Enterprise" -EXPOSE 28007 -STOPSIGNAL SIGTERM - -RUN set -ex \ - && apk -U upgrade \ - && apk add curl jq - -# Get the power-control from the builder stage. -COPY --from=builder /usr/local/bin/power-control /usr/local/bin/. -COPY configs configs - -# Setup environment variables. -ENV SMS_SERVER="http://cray-smd:27779" -ENV LOG_LEVEL="INFO" -ENV SERVICE_RESERVATION_VERBOSITY="INFO" -ENV TRS_IMPLEMENTATION="LOCAL" -ENV STORAGE="ETCD" -ENV ETCD_HOST="etcd" -ENV ETCD_PORT="2379" -ENV HSMLOCK_ENABLED="true" -ENV VAULT_ENABLED="true" -ENV VAULT_ADDR="http://vault:8200" -ENV VAULT_KEYPATH="secret/hms-creds" - -#DONT USES IN PRODUCTION; MOST WILL BREAK PROD!!! -ENV VAULT_SKIP_VERIFY="true" -ENV VAULT_TOKEN="hms" -ENV CRAY_VAULT_AUTH_PATH="auth/token/create" -ENV CRAY_VAULT_ROLE_FILE="/configs/namespace" -ENV CRAY_VAULT_JWT_FILE="/configs/token" - -ENV GODEBUG="tlsrsakex=1" - -ENV PCS_JAWS_MONITOR="true" -ENV PCS_JAWS_MONITOR_INTERVAL="20" - -#nobody 65534:65534 -USER 65534:65534 - -CMD ["sh", "-c", "power-control"]