Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions broker/dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ func (d *BrokerDialer) UseBroker(serverType server_structs.ServerType, name, bro
}, ttlcache.DefaultTTL)
}

// HasBrokerEndpoint returns true if the dialer knows about a broker endpoint for the given address.
func (d *BrokerDialer) HasBrokerEndpoint(addr string) bool {
return d.brokerEndpoints.Get(addr) != nil
}

// DialContext dials a connection to the given network and address using the broker.
func (d *BrokerDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
info := d.brokerEndpoints.Get(addr)
Expand Down
12 changes: 7 additions & 5 deletions client/handle_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,15 @@ func TestNewTransferDetailsEnv(t *testing.T) {
}

func TestSlowTransfers(t *testing.T) {
t.Cleanup(func() {
goleak.VerifyNone(t,
// Ignore the progress bars
goleak.IgnoreTopFunction("github.com/vbauerster/mpb/v8.(*Progress).serve"),
goleak.IgnoreTopFunction("github.com/vbauerster/mpb/v8.heapManager.run"),
)
})
t.Cleanup(test_utils.SetupTestLogging(t))

defer goleak.VerifyNone(t,
// Ignore the progress bars
goleak.IgnoreTopFunction("github.com/vbauerster/mpb/v8.(*Progress).serve"),
goleak.IgnoreTopFunction("github.com/vbauerster/mpb/v8.heapManager.run"),
)
ctx, _, _ := test_utils.TestContext(context.Background(), t)
// Adjust down some timeouts to speed up the test
test_utils.InitClient(t, map[string]any{
Expand Down
2 changes: 2 additions & 0 deletions cmd/fed_serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ func TestFedServePosixOrigin(t *testing.T) {
require.NoError(t, err)

require.NoError(t, param.Set("ConfigDir", tmpPath))
// Set RuntimeDir to avoid race conditions with parallel tests using shared /run/pelican
require.NoError(t, param.Set(param.RuntimeDir.GetName(), tmpPath))
require.NoError(t, param.Set("Origin.RunLocation", filepath.Join(tmpPath, "xrd")))
t.Cleanup(func() {
if err := os.RemoveAll(tmpPath); err != nil {
Expand Down
200 changes: 200 additions & 0 deletions cmd/logging_set_level.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/***************************************************************
*
* Copyright (C) 2025, Pelican Project, Morgridge Institute for Research
*
* Licensed under the Apache License, Version 2.0 (the "License"); you
* may not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************/

package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"time"

"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/pelicanplatform/pelican/config"
"github.com/pelicanplatform/pelican/param"
)

var (
loggingParameterName string

serverSetLoggingLevelCmd = &cobra.Command{
Use: "set-logging-level <level> <duration>",
Short: "Temporarily change the server's log level",
Long: `Temporarily change the server's log level for a specified duration.
The log level will automatically revert to the configured level after the duration expires.

Valid log levels: debug, info, warn, error, fatal, panic
Duration should be specified as a Go duration string (e.g., 5m, 1h30m, 300s)

Examples:
pelican server set-logging-level debug 5m -s https://my-origin.com:8447
pelican server set-logging-level info 30m -s https://my-cache.com:8447 -t /path/to/token
pelican server set-logging-level debug 2m -s https://my-origin.com:8447 --param Logging.Origin.Xrootd`,
Args: cobra.ExactArgs(2),
RunE: setLogLevel,
}
)

func init() {
serverCmd.AddCommand(serverSetLoggingLevelCmd)
serverSetLoggingLevelCmd.Flags().StringVarP(&loggingParameterName, "param", "p", "Logging.Level", "Target parameter for the log level (e.g., Logging.Level, Logging.Origin.Xrootd, Logging.Cache.Xrootd)")
serverSetLoggingLevelCmd.Flags().StringVarP(&serverURLStr, "server", "s", "", "Web URL of the Pelican server (e.g. https://my-origin.com:8447)")
serverSetLoggingLevelCmd.Flags().StringVarP(&tokenLocation, "token", "t", "", "Path to the admin token file")
}

func setLogLevel(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if ctx == nil {
ctx = context.Background()
}

// Initialize config to load configuration file
// InitClient will compute Server.ExternalWebUrl from Server.Hostname and Server.WebPort
if err := config.InitClient(); err != nil {
log.Errorln("Failed to initialize client:", err)
}

level := args[0]
durationStr := args[1]

// Parse duration as Go duration string (e.g., "5m", "1h30m", "300s")
duration, err := time.ParseDuration(durationStr)
if err != nil {
return errors.Wrap(err, "Duration must be a valid Go duration string (e.g., 5m, 1h30m, 300s)")
}
if duration <= 0 {
return errors.New("Duration must be positive")
}
durationSeconds := int(duration.Seconds())

parameterName := strings.TrimSpace(loggingParameterName)
if parameterName == "" {
parameterName = "Logging.Level"
}

// Construct API URL - use config if server URL not provided
srvURL := serverURLStr
if srvURL == "" {
// Try to get Server.ExternalWebUrl from config (computed or explicit)
srvURL = param.Server_ExternalWebUrl.GetString()
if srvURL == "" {
return errors.New("Server URL must be provided via --server flag or Server.ExternalWebUrl config")
}
}

targetURL, err := constructLoggingApiURL(srvURL)
if err != nil {
return err
}

// Build request payload
payload := map[string]interface{}{
"level": level,
"duration": durationSeconds,
"parameterName": parameterName,
}

payloadBytes, err := json.Marshal(payload)
if err != nil {
return errors.Wrap(err, "Failed to marshal request payload")
}

// Get admin token - use config for server URL if not provided
srvURL = serverURLStr
if srvURL == "" {
srvURL = param.Server_ExternalWebUrl.GetString()
}

tok, err := fetchOrGenerateWebAPIAdminToken(srvURL, tokenLocation)
if err != nil {
return err
}

// Prepare and send the HTTP request
req, err := http.NewRequestWithContext(ctx, "POST", targetURL.String(), bytes.NewBuffer(payloadBytes))
if err != nil {
return errors.Wrap(err, "Failed to create HTTP request")
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tok)
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "pelican-client/"+config.GetVersion())

httpClient := &http.Client{Transport: config.GetTransport()}
resp, err := httpClient.Do(req)
if err != nil {
return errors.Wrap(err, "HTTP request failed")
}
defer resp.Body.Close()

bodyBytes, err := handleAdminApiResponse(resp)
if err != nil {
return errors.Wrap(err, "Server request failed")
}

// Parse response
type LogLevelChangeResponse struct {
ChangeID string `json:"changeId"`
Level string `json:"level"`
ParameterName string `json:"parameterName"`
EndTime time.Time `json:"endTime"`
Remaining int `json:"remainingSeconds"`
}

var response LogLevelChangeResponse
if err := json.Unmarshal(bodyBytes, &response); err != nil {
return errors.Wrap(err, "Failed to parse server response")
}

fmt.Printf("Log level for %s successfully changed to '%s' for %d seconds\n", response.ParameterName, response.Level, response.Remaining)
fmt.Printf("Change ID: %s\n", response.ChangeID)
fmt.Printf("Will revert at: %s\n", response.EndTime.Format(time.RFC3339))
return nil
}

func constructLoggingApiURL(serverURLStr string) (*url.URL, error) {
if serverURLStr == "" {
return nil, errors.New("The --server flag providing the server's web URL is required")
}
serverURLStr = strings.TrimSuffix(serverURLStr, "/") // Normalize URL
baseURL, err := url.Parse(serverURLStr)
if err != nil {
return nil, errors.Wrapf(err, "Invalid server URL format: %s", serverURLStr)
}
// A Pelican server must use HTTPS scheme
if baseURL.Scheme != "https" {
return nil, errors.Errorf("Server URL must have an https scheme: %s", serverURLStr)
}
if baseURL.Host == "" {
return nil, errors.Errorf("Server URL must include a hostname: %s", serverURLStr)
}
// Construct the full API endpoint URL
targetURL, err := baseURL.Parse(path.Join("/api/v1.0/logging/level"))
if err != nil {
return nil, errors.Wrap(err, "Failed to construct logging API URL")
}
return targetURL, nil
}
2 changes: 2 additions & 0 deletions cmd/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ func (f *FedTest) Spinup() {
require.NoError(f.T, err)

require.NoError(f.T, param.Set("ConfigDir", tmpPath))
// Set RuntimeDir to avoid race conditions with parallel tests using shared /run/pelican
require.NoError(f.T, param.Set(param.RuntimeDir.GetName(), tmpPath))

// Create a file to capture output from commands
output, err := os.CreateTemp(f.T.TempDir(), "output")
Expand Down
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func init() {
rootCmd.AddCommand(generateCmd)
rootCmd.AddCommand(keyCmd)
rootCmd.AddCommand(downtimeCmd)
rootCmd.AddCommand(serverCmd)
rootCmd.AddCommand(config_printer.ConfigCmd)
preferredPrefix := config.GetPreferredPrefix()
rootCmd.Use = strings.ToLower(preferredPrefix.String())
Expand Down
32 changes: 32 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/***************************************************************
*
* Copyright (C) 2025, Pelican Project, Morgridge Institute for Research
*
* Licensed under the Apache License, Version 2.0 (the "License"); you
* may not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************/

package main

import (
"github.com/spf13/cobra"
)

var (
serverCmd = &cobra.Command{
Use: "server",
Short: "Manage server operations",
Long: `Provide commands to manage and interact with Pelican server operations.
These commands allow administrators to interact with server administrative APIs.`,
}
)
Loading
Loading