2016-01-09 02:21:02 +00:00
|
|
|
package vault
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2018-01-19 06:44:44 +00:00
|
|
|
"context"
|
2016-01-09 02:21:02 +00:00
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
|
2018-04-05 15:49:21 +00:00
|
|
|
"github.com/hashicorp/errwrap"
|
2016-01-09 02:21:02 +00:00
|
|
|
"github.com/hashicorp/go-uuid"
|
2017-02-16 20:15:02 +00:00
|
|
|
"github.com/hashicorp/vault/helper/consts"
|
2016-01-09 02:21:02 +00:00
|
|
|
"github.com/hashicorp/vault/helper/pgpkeys"
|
|
|
|
"github.com/hashicorp/vault/helper/xor"
|
|
|
|
"github.com/hashicorp/vault/shamir"
|
|
|
|
)
|
|
|
|
|
2017-11-10 18:19:42 +00:00
|
|
|
const coreDROperationTokenPath = "core/dr-operation-token"
|
|
|
|
|
|
|
|
var (
|
|
|
|
// GenerateStandardRootTokenStrategy is the strategy used to generate a
|
|
|
|
// typical root token
|
|
|
|
GenerateStandardRootTokenStrategy GenerateRootStrategy = generateStandardRootToken{}
|
|
|
|
)
|
|
|
|
|
|
|
|
// GenerateRootStrategy allows us to swap out the strategy we want to use to
|
|
|
|
// create a token upon completion of the generate root process.
|
|
|
|
type GenerateRootStrategy interface {
|
2018-01-19 06:44:44 +00:00
|
|
|
generate(context.Context, *Core) (string, func(), error)
|
2017-11-10 18:19:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// generateStandardRootToken implements the GenerateRootStrategy and is in
|
|
|
|
// charge of creating standard root tokens.
|
|
|
|
type generateStandardRootToken struct{}
|
|
|
|
|
2018-01-19 06:44:44 +00:00
|
|
|
func (g generateStandardRootToken) generate(ctx context.Context, c *Core) (string, func(), error) {
|
|
|
|
te, err := c.tokenStore.rootToken(ctx)
|
2017-11-10 18:19:42 +00:00
|
|
|
if err != nil {
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Error("root token generation failed", "error", err)
|
2017-11-10 18:19:42 +00:00
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
if te == nil {
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Error("got nil token entry back from root generation")
|
2017-11-10 18:19:42 +00:00
|
|
|
return "", nil, fmt.Errorf("got nil token entry back from root generation")
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanupFunc := func() {
|
2018-05-10 19:50:02 +00:00
|
|
|
c.tokenStore.revokeOrphan(ctx, te.ID)
|
2017-11-10 18:19:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return te.ID, cleanupFunc, nil
|
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
// GenerateRootConfig holds the configuration for a root generation
|
2016-01-09 02:21:02 +00:00
|
|
|
// command.
|
2016-01-15 15:55:35 +00:00
|
|
|
type GenerateRootConfig struct {
|
2016-01-09 02:21:02 +00:00
|
|
|
Nonce string
|
|
|
|
PGPKey string
|
|
|
|
PGPFingerprint string
|
|
|
|
OTP string
|
2017-11-10 18:19:42 +00:00
|
|
|
Strategy GenerateRootStrategy
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
// GenerateRootResult holds the result of a root generation update
|
2016-01-09 02:21:02 +00:00
|
|
|
// command
|
2016-01-15 15:55:35 +00:00
|
|
|
type GenerateRootResult struct {
|
2017-11-13 20:44:26 +00:00
|
|
|
Progress int
|
|
|
|
Required int
|
|
|
|
EncodedToken string
|
|
|
|
PGPFingerprint string
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2017-11-13 20:44:26 +00:00
|
|
|
// GenerateRootProgress is used to return the root generation progress (num shares)
|
2016-01-15 15:55:35 +00:00
|
|
|
func (c *Core) GenerateRootProgress() (int, error) {
|
2016-01-09 02:21:02 +00:00
|
|
|
c.stateLock.RLock()
|
|
|
|
defer c.stateLock.RUnlock()
|
|
|
|
if c.sealed {
|
2017-02-16 20:15:02 +00:00
|
|
|
return 0, consts.ErrSealed
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
if c.standby {
|
2017-02-16 20:15:02 +00:00
|
|
|
return 0, consts.ErrStandby
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
c.generateRootLock.Lock()
|
|
|
|
defer c.generateRootLock.Unlock()
|
2016-01-09 02:21:02 +00:00
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
return len(c.generateRootProgress), nil
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2017-11-13 20:44:26 +00:00
|
|
|
// GenerateRootConfiguration is used to read the root generation configuration
|
2016-01-09 02:21:02 +00:00
|
|
|
// It stubbornly refuses to return the OTP if one is there.
|
2016-01-15 15:55:35 +00:00
|
|
|
func (c *Core) GenerateRootConfiguration() (*GenerateRootConfig, error) {
|
2016-01-09 02:21:02 +00:00
|
|
|
c.stateLock.RLock()
|
|
|
|
defer c.stateLock.RUnlock()
|
|
|
|
if c.sealed {
|
2017-02-16 20:15:02 +00:00
|
|
|
return nil, consts.ErrSealed
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
if c.standby {
|
2017-02-16 20:15:02 +00:00
|
|
|
return nil, consts.ErrStandby
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
c.generateRootLock.Lock()
|
|
|
|
defer c.generateRootLock.Unlock()
|
2016-01-09 02:21:02 +00:00
|
|
|
|
|
|
|
// Copy the config if any
|
2016-01-15 15:55:35 +00:00
|
|
|
var conf *GenerateRootConfig
|
|
|
|
if c.generateRootConfig != nil {
|
|
|
|
conf = new(GenerateRootConfig)
|
|
|
|
*conf = *c.generateRootConfig
|
2016-01-09 02:21:02 +00:00
|
|
|
conf.OTP = ""
|
2017-11-10 18:19:42 +00:00
|
|
|
conf.Strategy = nil
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
return conf, nil
|
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
// GenerateRootInit is used to initialize the root generation settings
|
2017-11-10 18:19:42 +00:00
|
|
|
func (c *Core) GenerateRootInit(otp, pgpKey string, strategy GenerateRootStrategy) error {
|
2016-01-09 02:21:02 +00:00
|
|
|
var fingerprint string
|
|
|
|
switch {
|
|
|
|
case len(otp) > 0:
|
|
|
|
otpBytes, err := base64.StdEncoding.DecodeString(otp)
|
|
|
|
if err != nil {
|
2018-04-05 15:49:21 +00:00
|
|
|
return errwrap.Wrapf("error decoding base64 OTP value: {{err}}", err)
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
if otpBytes == nil || len(otpBytes) != 16 {
|
|
|
|
return fmt.Errorf("decoded OTP value is invalid or wrong length")
|
|
|
|
}
|
|
|
|
|
|
|
|
case len(pgpKey) > 0:
|
|
|
|
fingerprints, err := pgpkeys.GetFingerprints([]string{pgpKey}, nil)
|
|
|
|
if err != nil {
|
2018-04-05 15:49:21 +00:00
|
|
|
return errwrap.Wrapf("error parsing PGP key: {{err}}", err)
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
if len(fingerprints) != 1 || fingerprints[0] == "" {
|
|
|
|
return fmt.Errorf("could not acquire PGP key entity")
|
|
|
|
}
|
|
|
|
fingerprint = fingerprints[0]
|
|
|
|
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unreachable condition")
|
|
|
|
}
|
|
|
|
|
|
|
|
c.stateLock.RLock()
|
|
|
|
defer c.stateLock.RUnlock()
|
|
|
|
if c.sealed {
|
2017-02-16 20:15:02 +00:00
|
|
|
return consts.ErrSealed
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
if c.standby {
|
2017-02-16 20:15:02 +00:00
|
|
|
return consts.ErrStandby
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
c.generateRootLock.Lock()
|
|
|
|
defer c.generateRootLock.Unlock()
|
2016-01-09 02:21:02 +00:00
|
|
|
|
|
|
|
// Prevent multiple concurrent root generations
|
2016-01-15 15:55:35 +00:00
|
|
|
if c.generateRootConfig != nil {
|
2016-01-09 02:21:02 +00:00
|
|
|
return fmt.Errorf("root generation already in progress")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy the configuration
|
|
|
|
generationNonce, err := uuid.GenerateUUID()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
c.generateRootConfig = &GenerateRootConfig{
|
2016-01-09 02:21:02 +00:00
|
|
|
Nonce: generationNonce,
|
|
|
|
OTP: otp,
|
|
|
|
PGPKey: pgpKey,
|
|
|
|
PGPFingerprint: fingerprint,
|
2017-11-10 18:19:42 +00:00
|
|
|
Strategy: strategy,
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2016-08-19 20:45:17 +00:00
|
|
|
if c.logger.IsInfo() {
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Info("root generation initialized", "nonce", c.generateRootConfig.Nonce)
|
2016-08-19 20:45:17 +00:00
|
|
|
}
|
2016-01-09 02:21:02 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
// GenerateRootUpdate is used to provide a new key part
|
2018-01-19 06:44:44 +00:00
|
|
|
func (c *Core) GenerateRootUpdate(ctx context.Context, key []byte, nonce string, strategy GenerateRootStrategy) (*GenerateRootResult, error) {
|
2016-01-09 02:21:02 +00:00
|
|
|
// Verify the key length
|
|
|
|
min, max := c.barrier.KeyLength()
|
|
|
|
max += shamir.ShareOverhead
|
|
|
|
if len(key) < min {
|
|
|
|
return nil, &ErrInvalidKey{fmt.Sprintf("key is shorter than minimum %d bytes", min)}
|
|
|
|
}
|
|
|
|
if len(key) > max {
|
|
|
|
return nil, &ErrInvalidKey{fmt.Sprintf("key is longer than maximum %d bytes", max)}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the seal configuration
|
2016-04-04 14:44:22 +00:00
|
|
|
var config *SealConfig
|
|
|
|
var err error
|
2018-01-19 08:44:06 +00:00
|
|
|
if c.seal.RecoveryKeySupported() {
|
2018-01-19 06:44:44 +00:00
|
|
|
config, err = c.seal.RecoveryConfig(ctx)
|
2016-04-04 14:44:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else {
|
2018-01-19 06:44:44 +00:00
|
|
|
config, err = c.seal.BarrierConfig(ctx)
|
2016-04-04 14:44:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the barrier is initialized
|
|
|
|
if config == nil {
|
|
|
|
return nil, ErrNotInit
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure we are already unsealed
|
|
|
|
c.stateLock.RLock()
|
|
|
|
defer c.stateLock.RUnlock()
|
|
|
|
if c.sealed {
|
2017-02-16 20:15:02 +00:00
|
|
|
return nil, consts.ErrSealed
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
if c.standby {
|
2017-02-16 20:15:02 +00:00
|
|
|
return nil, consts.ErrStandby
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
c.generateRootLock.Lock()
|
|
|
|
defer c.generateRootLock.Unlock()
|
2016-01-09 02:21:02 +00:00
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
// Ensure a generateRoot is in progress
|
|
|
|
if c.generateRootConfig == nil {
|
2016-01-09 02:21:02 +00:00
|
|
|
return nil, fmt.Errorf("no root generation in progress")
|
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
if nonce != c.generateRootConfig.Nonce {
|
2018-04-05 15:49:21 +00:00
|
|
|
return nil, fmt.Errorf("incorrect nonce supplied; nonce for this root generation operation is %q", c.generateRootConfig.Nonce)
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2017-11-10 18:19:42 +00:00
|
|
|
if strategy != c.generateRootConfig.Strategy {
|
2018-03-20 18:54:10 +00:00
|
|
|
return nil, fmt.Errorf("incorrect strategy supplied; a generate root operation of another type is already in progress")
|
2017-11-10 18:19:42 +00:00
|
|
|
}
|
|
|
|
|
2016-01-09 02:21:02 +00:00
|
|
|
// Check if we already have this piece
|
2016-01-15 15:55:35 +00:00
|
|
|
for _, existing := range c.generateRootProgress {
|
2016-01-09 02:21:02 +00:00
|
|
|
if bytes.Equal(existing, key) {
|
2016-09-01 21:40:01 +00:00
|
|
|
return nil, fmt.Errorf("given key has already been provided during this generation operation")
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store this key
|
2016-01-15 15:55:35 +00:00
|
|
|
c.generateRootProgress = append(c.generateRootProgress, key)
|
|
|
|
progress := len(c.generateRootProgress)
|
2016-01-09 02:21:02 +00:00
|
|
|
|
|
|
|
// Check if we don't have enough keys to unlock
|
2016-01-15 15:55:35 +00:00
|
|
|
if len(c.generateRootProgress) < config.SecretThreshold {
|
2016-08-19 20:45:17 +00:00
|
|
|
if c.logger.IsDebug() {
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Debug("cannot generate root, not enough keys", "keys", progress, "threshold", config.SecretThreshold)
|
2016-08-19 20:45:17 +00:00
|
|
|
}
|
2016-01-15 15:55:35 +00:00
|
|
|
return &GenerateRootResult{
|
2016-01-09 02:21:02 +00:00
|
|
|
Progress: progress,
|
|
|
|
Required: config.SecretThreshold,
|
2016-01-15 15:55:35 +00:00
|
|
|
PGPFingerprint: c.generateRootConfig.PGPFingerprint,
|
2016-01-09 02:21:02 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Recover the master key
|
|
|
|
var masterKey []byte
|
|
|
|
if config.SecretThreshold == 1 {
|
2016-01-15 15:55:35 +00:00
|
|
|
masterKey = c.generateRootProgress[0]
|
|
|
|
c.generateRootProgress = nil
|
2016-01-09 02:21:02 +00:00
|
|
|
} else {
|
2016-01-15 15:55:35 +00:00
|
|
|
masterKey, err = shamir.Combine(c.generateRootProgress)
|
|
|
|
c.generateRootProgress = nil
|
2016-01-09 02:21:02 +00:00
|
|
|
if err != nil {
|
2018-04-05 15:49:21 +00:00
|
|
|
return nil, errwrap.Wrapf("failed to compute master key: {{err}}", err)
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify the master key
|
2018-01-19 08:44:06 +00:00
|
|
|
if c.seal.RecoveryKeySupported() {
|
2018-01-19 06:44:44 +00:00
|
|
|
if err := c.seal.VerifyRecoveryKey(ctx, masterKey); err != nil {
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Error("root generation aborted, recovery key verification failed", "error", err)
|
2016-04-04 14:44:22 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if err := c.barrier.VerifyMaster(masterKey); err != nil {
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Error("root generation aborted, master key verification failed", "error", err)
|
2016-04-04 14:44:22 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2017-11-10 18:19:42 +00:00
|
|
|
// Run the generate strategy
|
2018-01-19 06:44:44 +00:00
|
|
|
tokenUUID, cleanupFunc, err := strategy.generate(ctx, c)
|
2016-01-20 00:44:33 +00:00
|
|
|
if err != nil {
|
2016-01-09 02:21:02 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-11-10 18:19:42 +00:00
|
|
|
uuidBytes, err := uuid.ParseUUID(tokenUUID)
|
2016-01-09 02:21:02 +00:00
|
|
|
if err != nil {
|
2017-11-10 18:19:42 +00:00
|
|
|
cleanupFunc()
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Error("error getting generated token bytes", "error", err)
|
2016-01-09 02:21:02 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2016-01-20 00:44:33 +00:00
|
|
|
if uuidBytes == nil {
|
2017-11-10 18:19:42 +00:00
|
|
|
cleanupFunc()
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Error("got nil parsed UUID bytes")
|
2016-01-20 00:44:33 +00:00
|
|
|
return nil, fmt.Errorf("got nil parsed UUID bytes")
|
|
|
|
}
|
2016-01-09 02:21:02 +00:00
|
|
|
|
|
|
|
var tokenBytes []byte
|
|
|
|
// Get the encoded value first so that if there is an error we don't create
|
|
|
|
// the root token.
|
|
|
|
switch {
|
2016-01-15 15:55:35 +00:00
|
|
|
case len(c.generateRootConfig.OTP) > 0:
|
2016-01-09 02:21:02 +00:00
|
|
|
// This function performs decoding checks so rather than decode the OTP,
|
|
|
|
// just encode the value we're passing in.
|
2016-01-20 00:44:33 +00:00
|
|
|
tokenBytes, err = xor.XORBase64(c.generateRootConfig.OTP, base64.StdEncoding.EncodeToString(uuidBytes))
|
2016-01-09 02:21:02 +00:00
|
|
|
if err != nil {
|
2017-11-10 18:19:42 +00:00
|
|
|
cleanupFunc()
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Error("xor of root token failed", "error", err)
|
2016-01-09 02:21:02 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
case len(c.generateRootConfig.PGPKey) > 0:
|
2017-11-10 18:19:42 +00:00
|
|
|
_, tokenBytesArr, err := pgpkeys.EncryptShares([][]byte{[]byte(tokenUUID)}, []string{c.generateRootConfig.PGPKey})
|
2016-01-09 02:21:02 +00:00
|
|
|
if err != nil {
|
2017-11-10 18:19:42 +00:00
|
|
|
cleanupFunc()
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Error("error encrypting new root token", "error", err)
|
2016-01-09 02:21:02 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
tokenBytes = tokenBytesArr[0]
|
|
|
|
|
|
|
|
default:
|
2017-11-10 18:19:42 +00:00
|
|
|
cleanupFunc()
|
2016-01-09 02:21:02 +00:00
|
|
|
return nil, fmt.Errorf("unreachable condition")
|
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
results := &GenerateRootResult{
|
2017-11-13 20:44:26 +00:00
|
|
|
Progress: progress,
|
|
|
|
Required: config.SecretThreshold,
|
|
|
|
EncodedToken: base64.StdEncoding.EncodeToString(tokenBytes),
|
|
|
|
PGPFingerprint: c.generateRootConfig.PGPFingerprint,
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2016-08-19 20:45:17 +00:00
|
|
|
if c.logger.IsInfo() {
|
2018-04-03 00:46:59 +00:00
|
|
|
c.logger.Info("root generation finished", "nonce", c.generateRootConfig.Nonce)
|
2016-08-19 20:45:17 +00:00
|
|
|
}
|
2016-01-09 02:21:02 +00:00
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
c.generateRootProgress = nil
|
|
|
|
c.generateRootConfig = nil
|
2016-01-09 02:21:02 +00:00
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
// GenerateRootCancel is used to cancel an in-progress root generation
|
|
|
|
func (c *Core) GenerateRootCancel() error {
|
2016-01-09 02:21:02 +00:00
|
|
|
c.stateLock.RLock()
|
|
|
|
defer c.stateLock.RUnlock()
|
|
|
|
if c.sealed {
|
2017-02-16 20:15:02 +00:00
|
|
|
return consts.ErrSealed
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
if c.standby {
|
2017-02-16 20:15:02 +00:00
|
|
|
return consts.ErrStandby
|
2016-01-09 02:21:02 +00:00
|
|
|
}
|
|
|
|
|
2016-01-15 15:55:35 +00:00
|
|
|
c.generateRootLock.Lock()
|
|
|
|
defer c.generateRootLock.Unlock()
|
2016-01-09 02:21:02 +00:00
|
|
|
|
|
|
|
// Clear any progress or config
|
2016-01-15 15:55:35 +00:00
|
|
|
c.generateRootConfig = nil
|
|
|
|
c.generateRootProgress = nil
|
2016-01-09 02:21:02 +00:00
|
|
|
return nil
|
|
|
|
}
|