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 }