github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/license-register.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"fmt"
    22  	"net"
    23  	"net/url"
    24  	"os"
    25  
    26  	"github.com/fatih/color"
    27  	"github.com/minio/cli"
    28  	json "github.com/minio/colorjson"
    29  	"github.com/minio/madmin-go/v3"
    30  	"github.com/minio/mc/pkg/probe"
    31  	"github.com/minio/minio-go/v7/pkg/set"
    32  	"github.com/minio/pkg/v2/console"
    33  )
    34  
    35  const (
    36  	licRegisterMsgTag  = "licenseRegisterMessage"
    37  	licRegisterLinkTag = "licenseRegisterLink"
    38  )
    39  
    40  var licenseRegisterFlags = append([]cli.Flag{
    41  	cli.StringFlag{
    42  		Name:  "name",
    43  		Usage: "Specify the name to associate to this MinIO cluster in SUBNET",
    44  	},
    45  	cli.StringFlag{
    46  		Name:  "license",
    47  		Usage: "license of the account on SUBNET",
    48  	},
    49  }, subnetCommonFlags...)
    50  
    51  var licenseRegisterCmd = cli.Command{
    52  	Name:         "register",
    53  	Usage:        "register with MinIO Subscription Network",
    54  	OnUsageError: onUsageError,
    55  	Action:       mainLicenseRegister,
    56  	Before:       setGlobalsFromContext,
    57  	Flags:        licenseRegisterFlags,
    58  	CustomHelpTemplate: `NAME:
    59    {{.HelpName}} - {{.Usage}}
    60  
    61  USAGE:
    62    {{.HelpName}} TARGET
    63  
    64  FLAGS:
    65    {{range .VisibleFlags}}{{.}}
    66    {{end}}
    67  EXAMPLES:
    68    1. Register MinIO cluster at alias 'play' on SUBNET, using api key for auth
    69       {{.Prompt}} {{.HelpName}} play --api-key 08efc836-4289-dbd4-ad82-b5e8b6d25577
    70  
    71    2. Register MinIO cluster at alias 'play' on SUBNET, using license file ./minio.license
    72       {{.Prompt}} {{.HelpName}} play --license ./minio.license
    73  
    74    3. Register MinIO cluster at alias 'play' on SUBNET, using api key for auth,
    75       and "play-cluster" as the preferred name for the cluster on SUBNET.
    76       {{.Prompt}} {{.HelpName}} play --api-key 08efc836-4289-dbd4-ad82-b5e8b6d25577 --name play-cluster
    77  
    78    4. Register MinIO cluster at alias 'play' on SUBNET in an airgapped environment
    79       {{.Prompt}} {{.HelpName}} play --airgap
    80  
    81    5. Register MinIO cluster at alias 'play' on SUBNET, using alias as the cluster name.
    82       This asks for SUBNET credentials if the cluster is not already registered.
    83       {{.Prompt}} {{.HelpName}} play
    84  `,
    85  }
    86  
    87  type licRegisterMessage struct {
    88  	Status string `json:"status"`
    89  	Alias  string `json:"-"`
    90  	Action string `json:"action,omitempty"`
    91  	Type   string `json:"type"`
    92  	URL    string `json:"url,omitempty"`
    93  }
    94  
    95  // String colorized license register message
    96  func (li licRegisterMessage) String() string {
    97  	var msg string
    98  	switch li.Type {
    99  	case "online":
   100  		msg = console.Colorize(licRegisterMsgTag, fmt.Sprintf("%s %s successfully.", li.Alias, li.Action))
   101  	case "offline":
   102  		msg = fmt.Sprintln("Open the following URL in the browser to register", li.Alias, "on SUBNET:")
   103  		msg = console.Colorize(licRegisterMsgTag, msg) + console.Colorize(licRegisterLinkTag, li.URL)
   104  	}
   105  	return msg
   106  }
   107  
   108  // JSON jsonified license register message
   109  func (li licRegisterMessage) JSON() string {
   110  	jsonBytes, e := json.MarshalIndent(li, "", " ")
   111  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   112  
   113  	return string(jsonBytes)
   114  }
   115  
   116  // checkLicenseRegisterSyntax - validate arguments passed by a user
   117  func checkLicenseRegisterSyntax(ctx *cli.Context) {
   118  	if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 {
   119  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
   120  	}
   121  }
   122  
   123  // ClusterRegistrationReq - JSON payload of the subnet api for cluster registration
   124  // Contains a registration token created by base64 encoding  of the registration info
   125  type ClusterRegistrationReq struct {
   126  	Token string `json:"token"`
   127  }
   128  
   129  // ClusterRegistrationInfo - Information stored in the cluster registration token
   130  type ClusterRegistrationInfo struct {
   131  	DeploymentID string      `json:"deployment_id"`
   132  	ClusterName  string      `json:"cluster_name"`
   133  	UsedCapacity uint64      `json:"used_capacity"`
   134  	Info         ClusterInfo `json:"info"`
   135  }
   136  
   137  // ClusterInfo - The "info" sub-node of the cluster registration information struct
   138  // Intended to be extensible i.e. more fields will be added as and when required
   139  type ClusterInfo struct {
   140  	MinioVersion    string `json:"minio_version"`
   141  	NoOfServerPools int    `json:"no_of_server_pools"`
   142  	NoOfServers     int    `json:"no_of_servers"`
   143  	NoOfDrives      int    `json:"no_of_drives"`
   144  	NoOfBuckets     uint64 `json:"no_of_buckets"`
   145  	NoOfObjects     uint64 `json:"no_of_objects"`
   146  	TotalDriveSpace uint64 `json:"total_drive_space"`
   147  	UsedDriveSpace  uint64 `json:"used_drive_space"`
   148  }
   149  
   150  // SubnetLoginReq - JSON payload of the SUBNET login api
   151  type SubnetLoginReq struct {
   152  	Username string `json:"username"`
   153  	Password string `json:"password"`
   154  }
   155  
   156  // SubnetMFAReq - JSON payload of the SUBNET mfa api
   157  type SubnetMFAReq struct {
   158  	Username string `json:"username"`
   159  	OTP      string `json:"otp"`
   160  	Token    string `json:"token"`
   161  }
   162  
   163  func isPlay(endpoint url.URL) (bool, error) {
   164  	playEndpoint := "https://play.min.io"
   165  	if globalAirgapped {
   166  		return endpoint.String() == playEndpoint, nil
   167  	}
   168  
   169  	aliasIPs, e := net.LookupHost(endpoint.Hostname())
   170  	if e != nil {
   171  		return false, e
   172  	}
   173  	aliasIPSet := set.CreateStringSet(aliasIPs...)
   174  
   175  	playURL, e := url.Parse(playEndpoint)
   176  	if e != nil {
   177  		return false, e
   178  	}
   179  
   180  	playIPs, e := net.LookupHost(playURL.Hostname())
   181  	if e != nil {
   182  		return false, e
   183  	}
   184  
   185  	playIPSet := set.CreateStringSet(playIPs...)
   186  	return !aliasIPSet.Intersection(playIPSet).IsEmpty(), nil
   187  }
   188  
   189  func validateNotPlay(aliasedURL string) {
   190  	client := getClient(aliasedURL)
   191  	endpoint := client.GetEndpointURL()
   192  	if endpoint == nil {
   193  		fatal(errDummy().Trace(), "invalid endpoint on alias "+aliasedURL)
   194  		return
   195  	}
   196  
   197  	isplay, e := isPlay(*endpoint)
   198  	fatalIf(probe.NewError(e), "error checking if endpoint is play:")
   199  
   200  	if isplay {
   201  		fatal(errDummy().Trace(), "play is a public demo cluster; cannot be registered")
   202  	}
   203  }
   204  
   205  func mainLicenseRegister(ctx *cli.Context) error {
   206  	console.SetColor(licRegisterMsgTag, color.New(color.FgGreen, color.Bold))
   207  	console.SetColor(licRegisterLinkTag, color.New(color.FgWhite, color.Bold))
   208  	checkLicenseRegisterSyntax(ctx)
   209  
   210  	// Get the alias parameter from cli
   211  	aliasedURL := ctx.Args().Get(0)
   212  	validateNotPlay(aliasedURL)
   213  
   214  	licFile := ctx.String("license")
   215  
   216  	var alias, accAPIKey string
   217  	if len(licFile) > 0 {
   218  		licBytes, e := os.ReadFile(licFile)
   219  		fatalIf(probe.NewError(e), fmt.Sprintf("Unable to read license file %s", licFile))
   220  		alias, _ = url2Alias(aliasedURL)
   221  		accAPIKey = validateAndSaveLic(string(licBytes), alias, true)
   222  	} else {
   223  		alias, accAPIKey = initSubnetConnectivity(ctx, aliasedURL, false)
   224  	}
   225  
   226  	clusterName := ctx.String("name")
   227  	if len(clusterName) == 0 {
   228  		clusterName = alias
   229  	} else {
   230  		if globalAirgapped {
   231  			fatalIf(errInvalidArgument(), "'--name' is not allowed in airgapped mode")
   232  		}
   233  	}
   234  
   235  	regInfo := GetClusterRegInfo(getAdminInfo(aliasedURL), clusterName)
   236  
   237  	lrm := licRegisterMessage{Status: "success", Alias: alias}
   238  	if !globalAirgapped {
   239  		alreadyRegistered := false
   240  		if len(accAPIKey) == 0 {
   241  			apiKey, _, e := getSubnetCreds(alias)
   242  			fatalIf(probe.NewError(e), "Error in fetching subnet API Key")
   243  			if len(apiKey) > 0 {
   244  				alreadyRegistered = true
   245  				accAPIKey = apiKey
   246  			}
   247  		} else {
   248  			apiKey := getSubnetAPIKeyFromConfig(alias)
   249  			if len(apiKey) > 0 {
   250  				alreadyRegistered = true
   251  			}
   252  		}
   253  
   254  		lrm.Type = "online"
   255  		_, _, e := registerClusterOnSubnet(regInfo, alias, accAPIKey)
   256  		if e == nil {
   257  			lrm.Action = "registered"
   258  			if alreadyRegistered {
   259  				lrm.Action = "updated"
   260  			}
   261  			printMsg(lrm)
   262  			return nil
   263  		}
   264  
   265  		console.Println("Could not register cluster with SUBNET: ", e.Error())
   266  	}
   267  
   268  	// Airgapped mode OR online mode with registration failure
   269  	lrm.Type = "offline"
   270  
   271  	regToken, e := generateRegToken(regInfo)
   272  	fatalIf(probe.NewError(e), "Unable to generate registration token")
   273  
   274  	lrm.URL = subnetOfflineRegisterURL(regToken)
   275  	printMsg(lrm)
   276  	return nil
   277  }
   278  
   279  func getAdminInfo(aliasedURL string) madmin.InfoMessage {
   280  	// Create a new MinIO Admin Client
   281  	client := getClient(aliasedURL)
   282  
   283  	// Fetch info of all servers (cluster or single server)
   284  	admInfo, e := client.ServerInfo(globalContext)
   285  	fatalIf(probe.NewError(e), "Unable to fetch cluster info")
   286  
   287  	return admInfo
   288  }