169 lines
4.4 KiB
Go
169 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
"github.com/letsencrypt/boulder/unpause"
|
|
)
|
|
|
|
// subcommandUnpauseAccount encapsulates the "admin unpause-account" command.
|
|
type subcommandUnpauseAccount struct {
|
|
accountID int64
|
|
batchFile string
|
|
parallelism uint
|
|
}
|
|
|
|
var _ subcommand = (*subcommandUnpauseAccount)(nil)
|
|
|
|
func (u *subcommandUnpauseAccount) Desc() string {
|
|
return "Administratively unpause an account to allow certificate issuance attempts"
|
|
}
|
|
|
|
func (u *subcommandUnpauseAccount) Flags(flag *flag.FlagSet) {
|
|
flag.Int64Var(&u.accountID, "account", 0, "A single account ID to unpause")
|
|
flag.StringVar(&u.batchFile, "batch-file", "", "Path to a file containing multiple account IDs where each is separated by a newline")
|
|
flag.UintVar(&u.parallelism, "parallelism", 10, "The maximum number of concurrent unpause requests to send to the SA (default: 10)")
|
|
}
|
|
|
|
func (u *subcommandUnpauseAccount) Run(ctx context.Context, a *admin) error {
|
|
// This is a map of all input-selection flags to whether or not they were set
|
|
// to a non-default value. We use this to ensure that exactly one input
|
|
// selection flag was given on the command line.
|
|
setInputs := map[string]bool{
|
|
"-account": u.accountID != 0,
|
|
"-batch-file": u.batchFile != "",
|
|
}
|
|
activeFlag, err := findActiveInputMethodFlag(setInputs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var regIDs []int64
|
|
switch activeFlag {
|
|
case "-account":
|
|
regIDs = []int64{u.accountID}
|
|
case "-batch-file":
|
|
regIDs, err = a.readUnpauseAccountFile(u.batchFile)
|
|
default:
|
|
return errors.New("no recognized input method flag set (this shouldn't happen)")
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("collecting serials to revoke: %w", err)
|
|
}
|
|
|
|
_, err = a.unpauseAccounts(ctx, regIDs, u.parallelism)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type unpauseCount struct {
|
|
accountID int64
|
|
count int64
|
|
}
|
|
|
|
// unpauseAccount concurrently unpauses all identifiers for each account using
|
|
// up to `parallelism` workers. It returns a count of the number of identifiers
|
|
// unpaused for each account and any accumulated errors.
|
|
func (a *admin) unpauseAccounts(ctx context.Context, accountIDs []int64, parallelism uint) ([]unpauseCount, error) {
|
|
if len(accountIDs) <= 0 {
|
|
return nil, errors.New("no account IDs provided for unpausing")
|
|
}
|
|
slices.Sort(accountIDs)
|
|
accountIDs = slices.Compact(accountIDs)
|
|
|
|
countChan := make(chan unpauseCount, len(accountIDs))
|
|
work := make(chan int64)
|
|
|
|
var wg sync.WaitGroup
|
|
var errCount atomic.Uint64
|
|
for i := uint(0); i < parallelism; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for accountID := range work {
|
|
totalCount := int64(0)
|
|
for {
|
|
response, err := a.sac.UnpauseAccount(ctx, &sapb.RegistrationID{Id: accountID})
|
|
if err != nil {
|
|
errCount.Add(1)
|
|
a.log.Errf("error unpausing accountID %d: %v", accountID, err)
|
|
break
|
|
}
|
|
totalCount += response.Count
|
|
if response.Count < unpause.RequestLimit {
|
|
// All identifiers have been unpaused.
|
|
break
|
|
}
|
|
}
|
|
countChan <- unpauseCount{accountID: accountID, count: totalCount}
|
|
}
|
|
}()
|
|
}
|
|
|
|
go func() {
|
|
for _, accountID := range accountIDs {
|
|
work <- accountID
|
|
}
|
|
close(work)
|
|
}()
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(countChan)
|
|
}()
|
|
|
|
var unpauseCounts []unpauseCount
|
|
for count := range countChan {
|
|
unpauseCounts = append(unpauseCounts, count)
|
|
}
|
|
|
|
if errCount.Load() > 0 {
|
|
return unpauseCounts, fmt.Errorf("encountered %d errors while unpausing; see logs above for details", errCount.Load())
|
|
}
|
|
|
|
return unpauseCounts, nil
|
|
}
|
|
|
|
// readUnpauseAccountFile parses the contents of a file containing one account
|
|
// ID per into a slice of int64s. It will skip malformed records and continue
|
|
// processing until the end of file marker.
|
|
func (a *admin) readUnpauseAccountFile(filePath string) ([]int64, error) {
|
|
fp, err := os.Open(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening paused account data file: %w", err)
|
|
}
|
|
defer fp.Close()
|
|
|
|
var unpauseAccounts []int64
|
|
lineCounter := 0
|
|
scanner := bufio.NewScanner(fp)
|
|
for scanner.Scan() {
|
|
lineCounter++
|
|
regID, err := strconv.ParseInt(scanner.Text(), 10, 64)
|
|
if err != nil {
|
|
a.log.Infof("skipping: malformed account ID entry on line %d\n", lineCounter)
|
|
continue
|
|
}
|
|
unpauseAccounts = append(unpauseAccounts, regID)
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, scanner.Err()
|
|
}
|
|
|
|
return unpauseAccounts, nil
|
|
}
|