github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cli/demo.go (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package cli 12 13 import ( 14 "context" 15 gosql "database/sql" 16 "fmt" 17 "strings" 18 "time" 19 20 "github.com/cockroachdb/cockroach/pkg/cli/cliflags" 21 "github.com/cockroachdb/cockroach/pkg/geo/geos" 22 "github.com/cockroachdb/cockroach/pkg/roachpb" 23 "github.com/cockroachdb/cockroach/pkg/security" 24 "github.com/cockroachdb/cockroach/pkg/settings/cluster" 25 "github.com/cockroachdb/cockroach/pkg/util" 26 "github.com/cockroachdb/cockroach/pkg/util/log" 27 "github.com/cockroachdb/cockroach/pkg/util/uuid" 28 "github.com/cockroachdb/cockroach/pkg/workload" 29 "github.com/cockroachdb/errors" 30 "github.com/spf13/cobra" 31 "github.com/spf13/pflag" 32 ) 33 34 var demoCmd = &cobra.Command{ 35 Use: "demo", 36 Short: "open a demo sql shell", 37 Long: ` 38 Start an in-memory, standalone, single-node CockroachDB instance, and open an 39 interactive SQL prompt to it. Various datasets are available to be preloaded as 40 subcommands: e.g. "cockroach demo startrek". See --help for a full list. 41 42 By default, the 'movr' dataset is pre-loaded. You can also use --empty 43 to avoid pre-loading a dataset. 44 45 cockroach demo attempts to connect to a Cockroach Labs server to obtain a 46 temporary enterprise license for demoing enterprise features and enable 47 telemetry back to Cockroach Labs. In order to disable this behavior, set the 48 environment variable "COCKROACH_SKIP_ENABLING_DIAGNOSTIC_REPORTING" to true. 49 `, 50 Example: ` cockroach demo`, 51 Args: cobra.NoArgs, 52 RunE: MaybeDecorateGRPCError(func(cmd *cobra.Command, _ []string) error { 53 return runDemo(cmd, nil /* gen */) 54 }), 55 } 56 57 const demoOrg = "Cockroach Demo" 58 59 const defaultGeneratorName = "movr" 60 61 const defaultRootPassword = "admin" 62 63 var defaultGenerator workload.Generator 64 65 // maxNodeInitTime is the maximum amount of time to wait for nodes to be connected. 66 const maxNodeInitTime = 30 * time.Second 67 68 var defaultLocalities = demoLocalityList{ 69 // Default localities for a 3 node cluster 70 {Tiers: []roachpb.Tier{{Key: "region", Value: "us-east1"}, {Key: "az", Value: "b"}}}, 71 {Tiers: []roachpb.Tier{{Key: "region", Value: "us-east1"}, {Key: "az", Value: "c"}}}, 72 {Tiers: []roachpb.Tier{{Key: "region", Value: "us-east1"}, {Key: "az", Value: "d"}}}, 73 // Default localities for a 6 node cluster 74 {Tiers: []roachpb.Tier{{Key: "region", Value: "us-west1"}, {Key: "az", Value: "a"}}}, 75 {Tiers: []roachpb.Tier{{Key: "region", Value: "us-west1"}, {Key: "az", Value: "b"}}}, 76 {Tiers: []roachpb.Tier{{Key: "region", Value: "us-west1"}, {Key: "az", Value: "c"}}}, 77 // Default localities for a 9 node cluster 78 {Tiers: []roachpb.Tier{{Key: "region", Value: "europe-west1"}, {Key: "az", Value: "b"}}}, 79 {Tiers: []roachpb.Tier{{Key: "region", Value: "europe-west1"}, {Key: "az", Value: "c"}}}, 80 {Tiers: []roachpb.Tier{{Key: "region", Value: "europe-west1"}, {Key: "az", Value: "d"}}}, 81 } 82 83 var demoNodeCacheSizeValue = newBytesOrPercentageValue( 84 &demoCtx.cacheSize, 85 memoryPercentResolver, 86 ) 87 var demoNodeSQLMemSizeValue = newBytesOrPercentageValue( 88 &demoCtx.sqlPoolMemorySize, 89 memoryPercentResolver, 90 ) 91 92 type regionPair struct { 93 regionA string 94 regionB string 95 } 96 97 var regionToRegionToLatency map[string]map[string]int 98 99 func insertPair(pair regionPair, latency int) { 100 regionToLatency, ok := regionToRegionToLatency[pair.regionA] 101 if !ok { 102 regionToLatency = make(map[string]int) 103 regionToRegionToLatency[pair.regionA] = regionToLatency 104 } 105 regionToLatency[pair.regionB] = latency 106 } 107 108 func init() { 109 regionToRegionToLatency = make(map[string]map[string]int) 110 // Latencies collected from http://cloudping.co on 2019-09-11. 111 for pair, latency := range map[regionPair]int{ 112 {regionA: "us-east1", regionB: "us-west1"}: 66, 113 {regionA: "us-east1", regionB: "europe-west1"}: 64, 114 {regionA: "us-west1", regionB: "europe-west1"}: 146, 115 } { 116 insertPair(pair, latency) 117 insertPair(regionPair{ 118 regionA: pair.regionB, 119 regionB: pair.regionA, 120 }, latency) 121 } 122 } 123 124 func init() { 125 for _, meta := range workload.Registered() { 126 gen := meta.New() 127 128 if meta.Name == defaultGeneratorName { 129 // Save the default for use in the top-level 'demo' command 130 // without argument. 131 defaultGenerator = gen 132 } 133 134 var genFlags *pflag.FlagSet 135 if f, ok := gen.(workload.Flagser); ok { 136 genFlags = f.Flags().FlagSet 137 } 138 139 genDemoCmd := &cobra.Command{ 140 Use: meta.Name, 141 Short: meta.Description, 142 Args: cobra.ArbitraryArgs, 143 RunE: MaybeDecorateGRPCError(func(cmd *cobra.Command, _ []string) error { 144 return runDemo(cmd, gen) 145 }), 146 } 147 if !meta.PublicFacing { 148 genDemoCmd.Hidden = true 149 } 150 demoCmd.AddCommand(genDemoCmd) 151 genDemoCmd.Flags().AddFlagSet(genFlags) 152 } 153 } 154 155 // GetAndApplyLicense is not implemented in order to keep OSS/BSL builds successful. 156 // The cliccl package sets this function if enterprise features are available to demo. 157 var GetAndApplyLicense func(dbConn *gosql.DB, clusterID uuid.UUID, org string) (bool, error) 158 159 func incrementTelemetryCounters(cmd *cobra.Command) { 160 incrementDemoCounter(demo) 161 if flagSetForCmd(cmd).Lookup(cliflags.DemoNodes.Name).Changed { 162 incrementDemoCounter(nodes) 163 } 164 if demoCtx.localities != nil { 165 incrementDemoCounter(demoLocality) 166 } 167 if demoCtx.runWorkload { 168 incrementDemoCounter(withLoad) 169 } 170 if demoCtx.geoPartitionedReplicas { 171 incrementDemoCounter(geoPartitionedReplicas) 172 } 173 } 174 175 func checkDemoConfiguration( 176 cmd *cobra.Command, gen workload.Generator, 177 ) (workload.Generator, error) { 178 if gen == nil && !demoCtx.useEmptyDatabase { 179 // Use a default dataset unless prevented by --empty. 180 gen = defaultGenerator 181 } 182 183 // Make sure that the user didn't request a workload and an empty database. 184 if demoCtx.runWorkload && demoCtx.useEmptyDatabase { 185 return nil, errors.New("cannot run a workload against an empty database") 186 } 187 188 // Make sure the number of nodes is valid. 189 if demoCtx.nodes <= 0 { 190 return nil, errors.Newf("--nodes has invalid value (expected positive, got %d)", demoCtx.nodes) 191 } 192 193 // If artificial latencies were requested, then the user cannot supply their own localities. 194 if demoCtx.simulateLatency && demoCtx.localities != nil { 195 return nil, errors.New("--global cannot be used with --demo-locality") 196 } 197 198 demoCtx.disableTelemetry = cluster.TelemetryOptOut() 199 // disableLicenseAcquisition can also be set by the the user as an 200 // input flag, so make sure it include it when considering the final 201 // value of disableLicenseAcquisition. 202 demoCtx.disableLicenseAcquisition = 203 demoCtx.disableTelemetry || (GetAndApplyLicense == nil) || demoCtx.disableLicenseAcquisition 204 205 if demoCtx.geoPartitionedReplicas { 206 geoFlag := "--" + cliflags.DemoGeoPartitionedReplicas.Name 207 if demoCtx.disableLicenseAcquisition { 208 return nil, errors.Newf("enterprise features are needed for this demo (%s)", geoFlag) 209 } 210 211 // Make sure that the user didn't request to have a topology and an empty database. 212 if demoCtx.useEmptyDatabase { 213 return nil, errors.New("cannot setup geo-partitioned replicas topology on an empty database") 214 } 215 216 // Make sure that the Movr database is selected when automatically partitioning. 217 if gen == nil || gen.Meta().Name != "movr" { 218 return nil, errors.Newf("%s must be used with the Movr dataset", geoFlag) 219 } 220 221 // If the geo-partitioned replicas flag was given and the demo localities have changed, throw an error. 222 if demoCtx.localities != nil { 223 return nil, errors.Newf("--demo-locality cannot be used with %s", geoFlag) 224 } 225 226 // If the geo-partitioned replicas flag was given and the nodes have changed, throw an error. 227 if flagSetForCmd(cmd).Lookup(cliflags.DemoNodes.Name).Changed { 228 if demoCtx.nodes != 9 { 229 return nil, errors.Newf("--nodes with a value different from 9 cannot be used with %s", geoFlag) 230 } 231 } else { 232 const msg = `# 233 # --geo-partitioned replicas operates on a 9 node cluster. 234 # The cluster size has been changed from the default to 9 nodes.` 235 fmt.Println(msg) 236 demoCtx.nodes = 9 237 } 238 239 // If geo-partition-replicas is requested, make sure the workload has a Partitioning step. 240 configErr := errors.Newf( 241 "workload %s is not configured to have a partitioning step", gen.Meta().Name) 242 hookser, ok := gen.(workload.Hookser) 243 if !ok { 244 return nil, configErr 245 } 246 if hookser.Hooks().Partition == nil { 247 return nil, configErr 248 } 249 } 250 251 return gen, nil 252 } 253 254 func runDemo(cmd *cobra.Command, gen workload.Generator) (err error) { 255 if gen, err = checkDemoConfiguration(cmd, gen); err != nil { 256 return err 257 } 258 // Record some telemetry about what flags are being used. 259 incrementTelemetryCounters(cmd) 260 261 ctx := context.Background() 262 263 if err := checkTzDatabaseAvailability(ctx); err != nil { 264 return err 265 } 266 267 loc, err := geos.EnsureInit(geos.EnsureInitErrorDisplayPrivate, demoCtx.geoLibsDir) 268 if err != nil { 269 log.Infof(ctx, "could not initialize GEOS - geospatial functions may not be available: %v", err) 270 } else { 271 log.Infof(ctx, "GEOS initialized at %s", loc) 272 } 273 274 c, err := setupTransientCluster(ctx, cmd, gen) 275 defer c.cleanup(ctx) 276 if err != nil { 277 return checkAndMaybeShout(err) 278 } 279 demoCtx.transientCluster = &c 280 281 checkInteractive() 282 283 if cliCtx.isInteractive { 284 fmt.Printf(`# 285 # Welcome to the CockroachDB demo database! 286 # 287 # You are connected to a temporary, in-memory CockroachDB cluster of %d node%s. 288 `, demoCtx.nodes, util.Pluralize(int64(demoCtx.nodes))) 289 290 if demoCtx.disableTelemetry { 291 fmt.Println("#\n# Telemetry and automatic license acquisition disabled by configuration.") 292 } else if demoCtx.disableLicenseAcquisition { 293 fmt.Println("#\n# Enterprise features disabled by OSS-only build.") 294 } else { 295 fmt.Println("#\n# This demo session will attempt to enable enterprise features\n" + 296 "# by acquiring a temporary license from Cockroach Labs in the background.\n" + 297 "# To disable this behavior, set the environment variable\n" + 298 "# COCKROACH_SKIP_ENABLING_DIAGNOSTIC_REPORTING=true.") 299 } 300 } 301 302 // Start license acquisition in the background. 303 licenseDone, err := c.acquireDemoLicense(ctx) 304 if err != nil { 305 return checkAndMaybeShout(err) 306 } 307 308 // Initialize the workload, if requested. 309 if err := c.setupWorkload(ctx, gen, licenseDone); err != nil { 310 return checkAndMaybeShout(err) 311 } 312 313 if cliCtx.isInteractive { 314 if gen != nil { 315 fmt.Printf("#\n# The cluster has been preloaded with the %q dataset\n# (%s).\n", 316 gen.Meta().Name, gen.Meta().Description) 317 } 318 319 fmt.Println(`# 320 # Reminder: your changes to data stored in the demo session will not be saved! 321 # 322 # Connection parameters:`) 323 var nodeList strings.Builder 324 c.listDemoNodes(&nodeList, true /* justOne */) 325 fmt.Println("#", strings.ReplaceAll(nodeList.String(), "\n", "\n# ")) 326 327 if !demoCtx.insecure { 328 fmt.Printf( 329 "# The user %q with password %q has been created. Use it to access the Web UI!\n#\n", 330 security.RootUser, 331 defaultRootPassword, 332 ) 333 } 334 // If we didn't launch a workload, we still need to inform the 335 // user if the license check fails. Do this asynchronously and print 336 // the final error if any. 337 338 // It's ok to do this twice (if workload setup already waited) because 339 // then the error return is guaranteed to be nil. 340 go func() { 341 if err := waitForLicense(licenseDone); err != nil { 342 _ = checkAndMaybeShout(err) 343 } 344 }() 345 } else { 346 // If we are not running an interactive shell, we need to wait to ensure 347 // that license acquisition is successful. If license acquisition is 348 // disabled, then a read on this channel will return immediately. 349 if err := waitForLicense(licenseDone); err != nil { 350 return checkAndMaybeShout(err) 351 } 352 } 353 354 conn := makeSQLConn(c.connURL) 355 defer conn.Close() 356 357 return runClient(cmd, conn) 358 } 359 360 func waitForLicense(licenseDone <-chan error) error { 361 err := <-licenseDone 362 return err 363 }