1
0
mirror of https://github.com/minio/mc.git synced 2025-04-18 10:04:03 +03:00
mc/cmd/support-inspect.go
Klaus Post 2c4e3ad46b
Report errors and better file names for mc support inspect (#5062)
Check the returned stream for errors and show them. Don't upload/keep pointless data

Use the request alias to create a better file name...

Example:

```
λ mc support inspect play/testbucket/testdat/**
mc: <ERROR> Unable to download file data. GetRawData: No files matched the given pattern.
```

Previously the error would just be ignored.

```
λ mc support inspect --airgap play/testbucket/testdata/**
File data successfully downloaded as inspect-play_testbucket_testdata.enc
```

Previously all files would be called `inspect-data.enc`, which would just be difficult to distinguish.
2024-10-15 08:15:39 -07:00

302 lines
9.0 KiB
Go

// Copyright (c) 2015-2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"context"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"hash/crc32"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/fatih/color"
"github.com/minio/cli"
json "github.com/minio/colorjson"
"github.com/minio/madmin-go/v3"
"github.com/minio/madmin-go/v3/estream"
"github.com/minio/mc/pkg/probe"
"github.com/minio/pkg/v3/console"
)
const (
defaultPublicKey = "MIIBCgKCAQEAs/128UFS9A8YSJY1XqYKt06dLVQQCGDee69T+0Tip/1jGAB4z0/3QMpH0MiS8Wjs4BRWV51qvkfAHzwwdU7y6jxU05ctb/H/WzRj3FYdhhHKdzear9TLJftlTs+xwj2XaADjbLXCV1jGLS889A7f7z5DgABlVZMQd9BjVAR8ED3xRJ2/ZCNuQVJ+A8r7TYPGMY3wWvhhPgPk3Lx4WDZxDiDNlFs4GQSaESSsiVTb9vyGe/94CsCTM6Cw9QG6ifHKCa/rFszPYdKCabAfHcS3eTr0GM+TThSsxO7KfuscbmLJkfQev1srfL2Ii2RbnysqIJVWKEwdW05ID8ryPkuTuwIDAQAB"
)
var supportInspectFlags = append(subnetCommonFlags,
cli.BoolFlag{
Name: "legacy",
Usage: "use the older inspect format",
},
)
var supportInspectCmd = cli.Command{
Name: "inspect",
Usage: "upload raw object contents for analysis",
Action: mainSupportInspect,
OnUsageError: onUsageError,
Before: setGlobalsFromContext,
Flags: supportInspectFlags,
HideHelpCommand: true,
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Upload 'xl.meta' of a specific object from all the drives
{{.Prompt}} {{.HelpName}} myminio/bucket/test*/xl.meta
2. Upload recursively all objects at a prefix. NOTE: This can be an expensive operation use it with caution.
{{.Prompt}} {{.HelpName}} myminio/bucket/test/**
3. Download 'xl.meta' of a specific object from all the drives locally, and upload to SUBNET manually
{{.Prompt}} {{.HelpName}} myminio/bucket/test*/xl.meta --airgap
`,
}
type inspectMessage struct {
Status string `json:"status"`
AliasedURL string `json:"aliasedURL,omitempty"`
File string `json:"file,omitempty"`
Key string `json:"key,omitempty"`
}
// Colorized message for console printing.
func (t inspectMessage) String() string {
var msg string
if globalAirgapped || t.File != "" {
if t.Key == "" {
msg = fmt.Sprintf("File data successfully downloaded as %s", console.Colorize("File", t.File))
} else {
msg = fmt.Sprintf("Encrypted file data successfully downloaded as %s\n", console.Colorize("File", t.File))
msg += fmt.Sprintf("Decryption key: %s\n\n", console.Colorize("Key", t.Key))
msg += "The decryption key will ONLY be shown here. It cannot be recovered.\n"
msg += "The encrypted file can safely be shared without the decryption key.\n"
msg += "Even with the decryption key, data stored with encryption cannot be accessed."
}
} else {
msg = fmt.Sprintf("Object inspection data for '%s' uploaded to SUBNET successfully", t.AliasedURL)
}
return console.Colorize(supportSuccessMsgTag, msg)
}
func (t inspectMessage) JSON() string {
t.Status = "success"
jsonMessageBytes, e := json.MarshalIndent(t, "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(jsonMessageBytes)
}
func checkSupportInspectSyntax(ctx *cli.Context) {
if len(ctx.Args()) != 1 {
showCommandHelpAndExit(ctx, 1) // last argument is exit code
}
}
// mainSupportInspect - the entry function of inspect command
func mainSupportInspect(ctx *cli.Context) error {
// Check for command syntax
checkSupportInspectSyntax(ctx)
setSuccessMessageColor()
// Get the alias parameter from cli
args := ctx.Args()
aliasedURL := args.Get(0)
alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true)
if len(apiKey) == 0 {
// api key not passed as flag. Check that the cluster is registered.
apiKey = validateClusterRegistered(alias, true)
}
console.SetColor("File", color.New(color.FgWhite, color.Bold))
console.SetColor("Key", color.New(color.FgHiRed, color.Bold))
// Create a new MinIO Admin Client
client, err := newAdminClient(aliasedURL)
if err != nil {
fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.")
return nil
}
// Compute bucket and object from the aliased URL
aliasedURL = filepath.ToSlash(aliasedURL)
splits := splitStr(aliasedURL, "/", 3)
bucket, prefix := splits[1], splits[2]
shellName, _ := getShellName()
if runtime.GOOS != "windows" && shellName != "bash" && strings.Contains(prefix, "*") {
console.Infoln("Your shell is auto determined as '" + shellName + "', wildcard patterns are only supported with 'bash' SHELL.")
}
var publicKey []byte
if !ctx.Bool("legacy") {
var e error
publicKey, e = os.ReadFile(filepath.Join(mustGetMcConfigDir(), "support_public.pem"))
if e != nil && !os.IsNotExist(e) {
fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to inspect file.")
} else if len(publicKey) > 0 {
if !globalJSON && !globalQuiet {
console.Infoln("Using public key from ", filepath.Join(mustGetMcConfigDir(), "support_public.pem"))
}
}
// Fall back to MinIO public key.
if len(publicKey) == 0 {
// Public key for MinIO confidential information.
publicKey, _ = base64.StdEncoding.DecodeString(defaultPublicKey)
}
}
key, r, e := client.Inspect(context.Background(), madmin.InspectOptions{
Volume: bucket,
File: prefix,
PublicKey: publicKey,
})
fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to inspect file.")
// Download the inspect data in a temporary file first
tmpFile, e := os.CreateTemp("", "mc-inspect-")
fatalIf(probe.NewError(e), "Unable to download file data.")
copied := false
// Validate stream, report errors on stream.
if len(key) == 0 {
pr, pw := io.Pipe()
copied = true
var aErr error
var wg sync.WaitGroup
wg.Add(1)
// Validate stream...
go func() {
defer wg.Done()
er, err := estream.NewReader(pr)
if err != nil {
// Ignore if header is non-parsable...
io.Copy(io.Discard, pr)
return
}
// We don't care about decrypting at this point.
er.SkipEncrypted(true)
for {
var st *estream.Stream
st, aErr = er.NextStream()
if errors.Is(aErr, io.EOF) {
aErr = nil
pr.CloseWithError(nil)
return
}
if aErr != nil {
pr.CloseWithError(aErr)
return
}
aErr = st.Skip()
if aErr != nil {
fmt.Println("Skip:", aErr)
pr.CloseWithError(aErr)
return
}
}
}()
_, e = io.Copy(io.MultiWriter(tmpFile, pw), r)
pw.CloseWithError(e)
wg.Wait()
if e == nil && aErr != nil {
e = aErr
}
}
if !copied {
_, e = io.Copy(tmpFile, r)
}
fatalIf(probe.NewError(e), "Unable to download file data.")
r.Close()
tmpFile.Close()
wantFileName := "inspect-" + conservativeFileName(strings.Join(splits, "_")) + ".enc"
if globalAirgapped {
saveInspectDataFile(wantFileName, key, tmpFile)
return nil
}
uploadURL := SubnetUploadURL("inspect")
reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey)
tmpFileName := tmpFile.Name()
_, e = (&SubnetFileUploader{
alias: alias,
FilePath: tmpFileName,
filename: wantFileName,
ReqURL: reqURL,
Headers: headers,
DeleteAfterUpload: true,
}).UploadFileToSubnet()
if e != nil {
console.Errorln("Unable to upload inspect data to SUBNET portal: " + e.Error())
saveInspectDataFile(wantFileName, key, tmpFile)
return nil
}
printMsg(inspectMessage{AliasedURL: aliasedURL})
return nil
}
func saveInspectDataFile(dstFileName string, key []byte, tmpFile *os.File) {
var keyHex string
// Choose a name and move the inspect data to its final destination
if key != nil {
// Create an id that is also crc.
var id [4]byte
binary.LittleEndian.PutUint32(id[:], crc32.ChecksumIEEE(key[:]))
// We use 4 bytes of the 32 bytes to identify they file.
dstFileName = fmt.Sprintf("inspect-data.%s.enc", hex.EncodeToString(id[:]))
keyHex = hex.EncodeToString(id[:]) + hex.EncodeToString(key[:])
}
fi, e := os.Stat(dstFileName)
if e == nil && !fi.IsDir() {
e = moveFile(dstFileName, dstFileName+"."+time.Now().Format(dateTimeFormatFilename))
fatalIf(probe.NewError(e), "Unable to create a backup of "+dstFileName)
} else {
if !os.IsNotExist(e) {
fatal(probe.NewError(e), "Unable to download file data")
}
}
fatalIf(probe.NewError(moveFile(tmpFile.Name(), dstFileName)), "Unable to rename downloaded data, file exists at %s", tmpFile.Name())
printMsg(inspectMessage{
File: dstFileName,
Key: keyHex,
})
}