diff --git a/Makefile b/Makefile index aaeecb83..f32416c0 100644 --- a/Makefile +++ b/Makefile @@ -112,7 +112,7 @@ clean: rm -rf var cd $(SSB) && ./gradlew clean -linux: +linux: gui rm -rf ./dist/$(DIST_LINUX) env GOOS=linux GOARCH=amd64 go build -ldflags "-X main.VERSION=$(STEAM_RELEASE_VERSION) -X main.BUILD_DATE=`date -u +%Y-%m-%dT%H:%M:%S%z`" cd $(SLA) && env GOOS=linux GOARCH=amd64 go build @@ -126,7 +126,7 @@ linux: cp -r $(SCRIPTS) ./dist/$(DIST_LINUX)/var/master/ tar czfC ./dist/$(DIST_LINUX).tar.gz dist $(DIST_LINUX) -darwin: +darwin: gui rm -rf ./dist/$(DIST_DARWIN) env GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.VERSION=$(STEAM_RELEASE_VERSION) -X main.BUILD_DATE=`date -u +%Y-%m-%dT%H:%M:%S%z`" cd $(SLA) && env GOOS=darwin GOARCH=amd64 go build @@ -140,7 +140,5 @@ darwin: cp -r $(SCRIPTS) ./dist/$(DIST_DARWIN)/var/master/ tar czfC ./dist/$(DIST_DARWIN).tar.gz dist $(DIST_DARWIN) -release: gui ssb db launcher linux - rm -rf ./dist/$(DIST_LINUX) - # rm -rf ./dist/$(DIST_DARWIN) +release: ssb db launcher linux diff --git a/ReleaseNotes.md b/ReleaseNotes.md index c24db292..3dbdd357 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -10,6 +10,13 @@ If you're an Open Source community member, you can contact H2O using one of the - Send an e-mail message directly to support@h2o.ai - Ask your question on the [H2O Community](https://community.h2o.ai/spaces/540/index.html) site (create an account if necessary) +## v1.1.5 Changes + +- [STEAM-613] The ``./steam add engine`` command has been replaced with ``./steam upload engine``. +- [STEAM-604] When adding clusters to Steam, node sizes are now only specified in GB. +- [STEAM-619] Adds a confirmation dialog on deletion of a cluster +- [STEAM-601] Optional ability to supply Python dependencies for Anaconda + ## v1.1.4 Changes - [STEAM-496] Early release support for LDAP basic authentication using the ``--authentication-provider`` and ``--authentication-config`` flags when starting Steam. diff --git a/cli2/cli.go b/cli2/cli.go index 8f5d6a87..accb003d 100644 --- a/cli2/cli.go +++ b/cli2/cli.go @@ -32,7 +32,6 @@ import ( func registerGeneratedCommands(c *context, cmd *cobra.Command) { cmd.AddCommand( activate(c), - add(c), build(c), check(c), create(c), @@ -101,56 +100,6 @@ func activateIdentity(c *context) *cobra.Command { return cmd } -var addHelp = ` -add [?] -Add entities -Commands: - - $ steam add engine ... -` - -func add(c *context) *cobra.Command { - cmd := newCmd(c, addHelp, nil) - - cmd.AddCommand(addEngine(c)) - return cmd -} - -var addEngineHelp = ` -engine [?] -Add Engine -Examples: - - Add an engine - $ steam add engine \ - --engine-name=? \ - --engine-path=? - -` - -func addEngine(c *context) *cobra.Command { - var engineName string // No description available - var enginePath string // No description available - - cmd := newCmd(c, addEngineHelp, func(c *context, args []string) { - - // Add an engine - engineId, err := c.remote.AddEngine( - engineName, // No description available - enginePath, // No description available - ) - if err != nil { - log.Fatalln(err) - } - fmt.Printf("EngineId:\t%v\n", engineId) - return - }) - - cmd.Flags().StringVar(&engineName, "engine-name", engineName, "No description available") - cmd.Flags().StringVar(&enginePath, "engine-path", enginePath, "No description available") - return cmd -} - var buildHelp = ` build [?] Build entities diff --git a/cli2/local.go b/cli2/local.go index dd55ae80..ccfd2fc3 100644 --- a/cli2/local.go +++ b/cli2/local.go @@ -139,53 +139,51 @@ Examples: func serveMaster(c *context) *cobra.Command { var ( - webAddress string - webTLSCertPath string - webTLSKeyPath string - authProvider string - authConfig string - workingDirectory string - clusterProxyAddress string - compilationServiceAddress string - scoringServiceHost string - scoringServicePortsString string - enableProfiler bool - yarnEnableKerberos bool - yarnUserName string - yarnKeytab string - dbName string - dbUserName string - dbPassword string - dbHost string - dbPort string - dbConnectionTimeout string - dbSSLMode string - dbSSLCertPath string - dbSSLKeyPath string - dbSSLRootCertPath string - superuserName string - superuserPassword string + webAddress string + webTLSCertPath string + webTLSKeyPath string + authProvider string + authConfig string + workingDirectory string + clusterProxyAddress string + compilationServiceAddress string + predictionServiceHost string + predictionServicePortsString string + enableProfiler bool + yarnEnableKerberos bool + dbName string + dbUserName string + dbPassword string + dbHost string + dbPort string + dbConnectionTimeout string + dbSSLMode string + dbSSLCertPath string + dbSSLKeyPath string + dbSSLRootCertPath string + superuserName string + superuserPassword string ) opts := master.DefaultOpts cmd := newCmd(c, serveMasterHelp, func(c *context, args []string) { - ports := strings.Split(scoringServicePortsString, ":") + ports := strings.Split(predictionServicePortsString, ":") if len(ports) != 2 { - log.Fatalln("Invalid usage of scoring service ports range. See 'steam help serve master'.") + log.Fatalln("Invalid usage of prediction service ports range. See 'steam help serve master'.") } - var scoringServicePorts [2]int + var predictionServicePorts [2]int for i, port := range ports { var err error - scoringServicePorts[i], err = strconv.Atoi(port) + predictionServicePorts[i], err = strconv.Atoi(port) if err != nil { - log.Fatalln("Invalid usage of scoring service ports range. See 'steam help serve master'.") + log.Fatalln("Invalid usage of prediction service ports range. See 'steam help serve master'.") } - if scoringServicePorts[i] < 1025 || scoringServicePorts[i] > 65535 { + if predictionServicePorts[i] < 1025 || predictionServicePorts[i] > 65535 { log.Fatalln("Invalid port range.") } } - if scoringServicePorts[0] > scoringServicePorts[1] { + if predictionServicePorts[0] > predictionServicePorts[1] { log.Fatalln("Invalid port range.") } @@ -198,13 +196,11 @@ func serveMaster(c *context) *cobra.Command { workingDirectory, clusterProxyAddress, compilationServiceAddress, - scoringServiceHost, - scoringServicePorts, + predictionServiceHost, + predictionServicePorts, enableProfiler, master.YarnOpts{ yarnEnableKerberos, - yarnUserName, - yarnKeytab, }, master.DBOpts{ data.Connection{ @@ -224,7 +220,6 @@ func serveMaster(c *context) *cobra.Command { }, }) }) - cmd.Flags().StringVar(&webAddress, "web-address", opts.WebAddress, "Web server address (\":\" or \":\").") cmd.Flags().StringVar(&webTLSCertPath, "web-tls-cert-path", opts.WebTLSCertPath, "Web server TLS certificate file path (optional).") cmd.Flags().StringVar(&webTLSKeyPath, "web-tls-key-path", opts.WebTLSKeyPath, "Web server TLS key file path (optional).") @@ -233,23 +228,24 @@ func serveMaster(c *context) *cobra.Command { cmd.Flags().StringVar(&workingDirectory, "working-directory", opts.WorkingDirectory, "Working directory for application files.") cmd.Flags().StringVar(&clusterProxyAddress, "cluster-proxy-address", opts.ClusterProxyAddress, "Cluster proxy address (\":\" or \":\")") cmd.Flags().StringVar(&compilationServiceAddress, "compilation-service-address", opts.CompilationServiceAddress, "Model compilation service address (\":\")") - cmd.Flags().StringVar(&scoringServiceHost, "scoring-service-address", opts.ScoringServiceHost, "Address to start scoring services on (\"\")") - // TODO: this uses a hardcoded port range, not the default const - cmd.Flags().StringVar(&scoringServicePortsString, "scoring-service-port-range", "1025:65535", "Specified port range to create scoring services on. (\":\")") + cmd.Flags().StringVar(&predictionServiceHost, "scoring-service-address", opts.PredictionServiceHost, "Hostname to start prediction services on (\"\")") + cmd.Flags().MarkDeprecated("scoring-service-address", "please use \"prediction-service-host\"") + cmd.Flags().StringVar(&predictionServiceHost, "prediction-service-host", opts.PredictionServiceHost, "Hostname to start prediction services on (\"\")") + cmd.Flags().StringVar(&predictionServicePortsString, "scoring-service-port-range", "1025:65535", "Specified port range to create prediction services on. (\":\")") + cmd.Flags().MarkDeprecated("scoring-service-port-range", "please use \"prediction-service-port-range\"") + cmd.Flags().StringVar(&predictionServicePortsString, "prediction-service-port-range", "1025:65535", "Specified port range to create prediction services on. (\":\")") cmd.Flags().BoolVar(&enableProfiler, "profile", opts.EnableProfiler, "Enable Go profiler") cmd.Flags().BoolVar(&yarnEnableKerberos, "yarn-enable-kerberos", opts.Yarn.KerberosEnabled, "Enable Kerberos authentication. Requires username and keytab.") // FIXME: Kerberos authentication is being passed by admin to all - cmd.Flags().StringVar(&yarnUserName, "yarn-username", opts.Yarn.Username, "Username to enable Kerberos") - cmd.Flags().StringVar(&yarnKeytab, "yarn-keytab", opts.Yarn.Keytab, "Keytab file to be used with Kerberos authentication") - cmd.Flags().StringVar(&dbName, "db-name", opts.DB.Connection.DbName, "Database name to use for application data storage (required)") - cmd.Flags().StringVar(&dbUserName, "db-username", opts.DB.Connection.User, "Database username (required)") - cmd.Flags().StringVar(&dbPassword, "db-password", opts.DB.Connection.Password, "Database password (optional)") - cmd.Flags().StringVar(&dbHost, "db-host", opts.DB.Connection.Host, "Database host (optional, defaults to localhost") - cmd.Flags().StringVar(&dbPort, "db-port", opts.DB.Connection.Port, "Database port (optional, defaults to 5432)") - cmd.Flags().StringVar(&dbConnectionTimeout, "db-connection-timeout", opts.DB.Connection.ConnectionTimeout, "Database connection timeout (optional)") - cmd.Flags().StringVar(&dbSSLMode, "db-ssl-mode", opts.DB.Connection.SSLMode, "Database connection SSL mode: one of 'disable', 'require', 'verify-ca', 'verify-full'") - cmd.Flags().StringVar(&dbSSLCertPath, "db-ssl-cert-path", opts.DB.Connection.SSLCert, "Database connection SSL certificate path (optional)") - cmd.Flags().StringVar(&dbSSLKeyPath, "db-ssl-key-path", opts.DB.Connection.SSLKey, "Database connection SSL key path (optional)") - cmd.Flags().StringVar(&dbSSLRootCertPath, "db-ssl-root-cert-path", opts.DB.Connection.SSLRootCert, "Database connection SSL root certificate path (optional)") + // cmd.Flags().StringVar(&dbName, "db-name", opts.DB.Connection.DbName, "Database name to use for application data storage (required)") + // cmd.Flags().StringVar(&dbUserName, "db-username", opts.DB.Connection.User, "Database username (required)") + // cmd.Flags().StringVar(&dbPassword, "db-password", opts.DB.Connection.Password, "Database password (optional)") + // cmd.Flags().StringVar(&dbHost, "db-host", opts.DB.Connection.Host, "Database host (optional, defaults to localhost") + // cmd.Flags().StringVar(&dbPort, "db-port", opts.DB.Connection.Port, "Database port (optional, defaults to 5432)") + // cmd.Flags().StringVar(&dbConnectionTimeout, "db-connection-timeout", opts.DB.Connection.ConnectionTimeout, "Database connection timeout (optional)") + // cmd.Flags().StringVar(&dbSSLMode, "db-ssl-mode", opts.DB.Connection.SSLMode, "Database connection SSL mode: one of 'disable', 'require', 'verify-ca', 'verify-full'") + // cmd.Flags().StringVar(&dbSSLCertPath, "db-ssl-cert-path", opts.DB.Connection.SSLCert, "Database connection SSL certificate path (optional)") + // cmd.Flags().StringVar(&dbSSLKeyPath, "db-ssl-key-path", opts.DB.Connection.SSLKey, "Database connection SSL key path (optional)") + // cmd.Flags().StringVar(&dbSSLRootCertPath, "db-ssl-root-cert-path", opts.DB.Connection.SSLRootCert, "Database connection SSL root certificate path (optional)") cmd.Flags().StringVar(&superuserName, "superuser-name", opts.DB.SuperuserName, "Set superuser username (required for first-time-use only)") cmd.Flags().StringVar(&superuserPassword, "superuser-password", opts.DB.SuperuserPassword, "Set superuser password (required for first-time-use only)") @@ -257,49 +253,6 @@ func serveMaster(c *context) *cobra.Command { } -var deployHelp = ` -deploy [resource-type] -Deploy a resource of the specified type. -Examples: - - $ steam deploy engine -` - -func deploy(c *context) *cobra.Command { - cmd := newCmd(c, deployHelp, nil) - cmd.AddCommand(deployEngine(c)) - return cmd -} - -var deployEngineHelp = ` -engine [enginePath] -Deploy an H2O engine to Steam. -Examples: - - $ steam deploy engine --file-path=path/to/engine -` - -func deployEngine(c *context) *cobra.Command { - var ( - filePath string - ) - cmd := newCmd(c, deployEngineHelp, func(c *context, args []string) { - attrs := map[string]string{ - "type": fs.KindEngine, - } - - if err := c.transmitFile(filePath, attrs); err != nil { - log.Fatalln(err) - } - - log.Println("Engine deployed:", path.Base(filePath)) - }) - - cmd.Flags().StringVar(&filePath, "file-path", "", "Path to engine") - - return cmd -} - var uploadHelp = ` upload [resource-type] Upload a resource of the specified type. @@ -311,15 +264,18 @@ Examples: func upload(c *context) *cobra.Command { cmd := newCmd(c, uploadHelp, nil) cmd.AddCommand(uploadFile(c)) + cmd.AddCommand(uploadEngine(c)) return cmd } var uploadFileHelp = ` file [path] -Upload an H2O engine to Steam. +Upload an asset to Steam. Examples: - $ steam upload engine path/to/engine + $ steam upload file \ + --file-path=? \ + --project-id=? ` func uploadFile(c *context) *cobra.Command { @@ -359,3 +315,32 @@ func uploadFile(c *context) *cobra.Command { return cmd } + +var uploadEngineHelp = ` +engine [path] +Upload an engine to Steam. +Examples: + + $ steam upload engine \ + --file-path=? +` + +func uploadEngine(c *context) *cobra.Command { + var ( + filePath string + ) + cmd := newCmd(c, uploadEngineHelp, func(c *context, args []string) { + attrs := map[string]string{ + "type": fs.KindEngine, + } + if err := c.transmitFile(filePath, attrs); err != nil { + log.Fatalln(err) + } + + log.Println("Engine uploaded:", path.Base(filePath)) + }) + + cmd.Flags().StringVar(&filePath, "file-path", "", "File to be uploaded") + + return cmd +} diff --git a/cli2/main.go b/cli2/main.go index 87dd5fcd..dbe10d88 100644 --- a/cli2/main.go +++ b/cli2/main.go @@ -114,7 +114,6 @@ func Steam(version, buildDate string, stdout, stderr, trace io.Writer) *cobra.Co login(c), reset(c), serve(c), - deploy(c), upload(c), ) registerGeneratedCommands(c, cmd) diff --git a/docs/CLIAppendix.rst b/docs/CLIAppendix.rst index af279f68..84bbfe13 100644 --- a/docs/CLIAppendix.rst +++ b/docs/CLIAppendix.rst @@ -3,36 +3,13 @@ Appendix B: CLI Command Reference This document describes the CLI commands available in Steam. In addition to this reference, you can view information about each command when you're in the CLI by typing ``./steam help``. ------ - -``add engine`` -~~~~~~~~~~~~~~ - -**Description** - -Adds a new engine to the Steam database. After an engine is successfully added, it can be specified when starting a cluster. (See `start cluster`_.) - -**Usage** +**Note**: To access the Steam CLI, open a terminal window and log in to Steam as the superuser: :: - ./steam add engine --engine-name="[name]" --engine-path="[path]" - -**Parameters** - -- ``--engine-name="[name]"``: Enter the name of the engine -- ``--engine-path="[path]"``: Enter the path for the engine - -**Example** - -The following example adds **h2o-genmodel.jar** to the list of available -engines. + ./steam login : --username=superuser --password=superuser -:: - - ./steam add engine --engine-name="h2o-genmodel.jar" --engine-path="../Desktop/engines" - --------------- +----- ``build model`` ~~~~~~~~~~~~~~~ @@ -984,7 +961,7 @@ None **Example** The following example retrieves a list of engines that have been -added. (Refer to `add engine`_.) +added. (Refer to `upload engine`_.) :: @@ -1854,3 +1831,59 @@ The following example changes the name of the production workgroup to be ./steam update workgroup production --desc="A deploy workgroup" --name="deploy" Successfully updated workgroup: production + +-------------- + +``upload engine`` +~~~~~~~~~~~~~~~~~ + +**Description** + +Adds a new engine to the Steam database. After an engine is successfully added, it can be specified when starting a cluster. (See `start cluster`_.) + +**Usage** + +:: + + ./steam upload engine --file-path="[path]" + +**Parameters** + +- ``--file-path="[path]"``: Enter the path for the engine that you want to upload + +**Example** + +The following example adds **h2o-genmodel.jar** to the list of available +engines. + +:: + + ./steam upload engine --file-path="../Desktop/engines/h2o.genmodel.jar" + +-------------- + +``upload file`` +~~~~~~~~~~~~~~~ + +Adds a new preprocessing file to the Steam database. + +**Usage** + +:: + + ./steam upload file --file-path="[path]" --project-id=[id] --package-name="[target_name]" --relative-path="[path_to_copy_to]" + +**Parameters** + +- ``--file-path="[path]"``: Enter the path for the preprocessing file that you want to upload +- ``--project-id=[id]``: Preprocessing files must be associated with a project. Enter the ID of the project that will have access to this file. +- ``--package-name="[target_name]"``: Specify the name for this package +- ``--relative-path="[path_to_copy_to]"``: Specify the relative path to copy this file to + +**Example** + +The following example adds a preprocessing file to a project whose ID is 5. The file will be copied to the Steam assets folder. + +:: + + /steam upload file --file-path="../preprocess/score.py" --package-name="score.py" --project-id=5 --relative-path="var/master/assets" diff --git a/docs/Clusters.rst b/docs/Clusters.rst index 1342469c..6446d492 100644 --- a/docs/Clusters.rst +++ b/docs/Clusters.rst @@ -18,7 +18,7 @@ You can connect to additional clusters that are running H2O by clicking the **La - Cluster Name - Number of Nodes - - Memory per Node (in MB or GB) - - H2O version (specifying a zip file) + - Memory per Node (in GB) + - H2O version (specify a zip file or upload an engine) 2. Click **Launch New Clusters** when you are done. diff --git a/docs/Installation.rst b/docs/Installation.rst index c225b0d4..89d5694b 100644 --- a/docs/Installation.rst +++ b/docs/Installation.rst @@ -64,7 +64,7 @@ the URL of the edge node and the superuser login credentials. The superuser can java -jar var/master/assets/jetty-runner.jar var/master/assets/ROOT.war -3. Open another terminal window and run the following command to start Steam. Be sure to include the ``--superuser-name=superuser`` and ``--superuser-password=superuser`` flags. (Or provide a more secure password.) This starts Steam on the edge node at port 9000 and creates a Steam superuser. The Steam superuser is responsible for creating roles, workgroups, and users and maintains the H2O cluster. Use ``./steam serve master --help`` or ``./steam serve master -h`` for information on how to start the compilation and/or prediction service on a different location and for additional flags that can be specified when starting Steam. +3. Open another terminal window and run the following command to start Steam. Be sure to include the ``--superuser-name=superuser`` and ``--superuser-password=superuser`` flags. (Or provide a more secure password.) This starts Steam on the edge node at port 9000 and creates a Steam superuser. The Steam superuser is responsible for creating roles, workgroups, and users and maintains the H2O cluster. Refer to the `Steam Start Flags`_ section or use ``./steam serve master --help`` or ``./steam serve master -h`` for information on how to start the compilation and/or prediction service on a different location and for additional flags that can be specified when starting Steam. :: @@ -116,16 +116,71 @@ After Steam is installed, the following steps describe how to start Steam. java -jar var/master/assets/jetty-runner.jar var/master/assets/ROOT.war 3. Open another terminal window and start Steam. Be sure to include the ``--superuser-name=superuser`` and - ``--superuser-password=superuser`` flags. (Or provide a more secure password.) This creates Steam superuser. A Steam superuser is responsible for creating roles,workgroups, and users. This also starts the Steam web service on ``localhost:9000``, the compilation service on ``localhost:8080`` (same as the Jetty server), and the prediction service on the external IP address of ``localhost``. You can change these using ``--compilation-service-address=`` and ``--prediction-service-address=``. Use ``./steam serve master --help`` or ``./steam serve master -h`` to view additional options. + ``--superuser-password=superuser`` flags. (Or provide a more secure password.) This creates Steam superuser. A Steam superuser is responsible for creating roles,workgroups, and users. This also starts the Steam web service on ``localhost:9000``, the compilation service on ``localhost:8080`` (same as the Jetty server), and the prediction service on the external IP address of ``localhost``. You can change these using ``--compilation-service-address=`` and ``--prediction-service-host=``. Refer to the `Steam Start Flags`_ section or use ``./steam serve master --help`` or ``./steam serve master -h`` to view additional options. :: ./steam serve master --superuser-name=superuser --superuser-password=superuser - **Note**: If you are demoing Steam and do not have an Internet connection, you can set the prediction service to point to localhost using ``--prediction-service-address=localhost``. + **Note**: If you are demoing Steam and do not have an Internet connection, you can set the prediction service to point to localhost using ``--prediction-service-host=localhost``. 4. Open a Chrome browser and navigate to http://localhost:9000. +Steam Start Flags +----------------- + +The following table lists the options/flags that can be added to the ``./steam serve master`` command when starting Steam. Use ``./steam serve master --help`` or ``./steam serve master -h`` for the most up-to-date list of available options. + ++-------------------------------------------+-----------------------------------------+ +| Flag | Description | ++===========================================+=========================================+ +| ``--authentication-config=`` | Specify a configuration file to use | +| | for authentication. | ++-------------------------------------------+-----------------------------------------+ +| ``--authentication-provider=`` | Specify either ``basic`` or ``digest`` | +| | as the authentication mechanism for | +| | client logins. | ++-------------------------------------------+-----------------------------------------+ +| ``--cluster-proxy-address=`` | Specify a proxy address. For example: | +| | ``:`` or ``:``. | ++-------------------------------------------+-----------------------------------------+ +| ``--compilation-service-address=`` | Specify an address to use for the | +| | compilation service. For example: | +| | ``:`` or ``:``. | ++-------------------------------------------+-----------------------------------------+ +| ``--prediction-service-host=`` | Specify the hostname to use for the | +| | prediction service. | ++-------------------------------------------+-----------------------------------------+ +| ``--prediction-service-port-range=`` | Specify a range of ports to create | +| | prediction services on. For example: | +| | ``:``. | ++-------------------------------------------+-----------------------------------------+ +| ``--profile=`` | Specify ``true`` to enable the Go | +| | profiler. | ++-------------------------------------------+-----------------------------------------+ +| ``--superuser-name=`` | Set the superuser username. This is | +| | required at first-time-use only. | ++-------------------------------------------+-----------------------------------------+ +| ``--superuser-password=`` | Set the superuser password. This is | +| | required at first-time-use only. | ++-------------------------------------------+-----------------------------------------+ +| ``--web-address=`` | Specify the web server address. For | +| | example: ``:`` or ``:``.| ++-------------------------------------------+-----------------------------------------+ +| ``--web-tls-cert-path=`` | Specify the web server TLS certificate | +| | path. | ++-------------------------------------------+-----------------------------------------+ +| ``--web-tls-key-path=`` | Specify the web server TLK key file | +| | path. | ++-------------------------------------------+-----------------------------------------+ +| ``--working-directory=`` | Specify the working directory for | +| | application files. | ++-------------------------------------------+-----------------------------------------+ +| ``--yarn-enable-kerberos=`` | Specify whether to enable Kerberos | +| | authentication. This requires a username| +| | and keytab. | ++-------------------------------------------+-----------------------------------------+ + Next Steps ---------- diff --git a/docs/Projects.rst b/docs/Projects.rst index f0a3644e..ccbbdd4e 100644 --- a/docs/Projects.rst +++ b/docs/Projects.rst @@ -143,7 +143,7 @@ Comparing Models Select model to compare 2. Select to compare the current model with any available model. This - exampel compares a GLM model with a GBM model. Once a model is + example compares a GLM model with a GBM model. Once a model is selected, the Model Details page immediately populates with the comparison information. The current model values are displayed in blue, and the selected comparison model displays in orange. diff --git a/docs/StopSteam.rst b/docs/StopSteam.rst index a201d564..b35a47b5 100644 --- a/docs/StopSteam.rst +++ b/docs/StopSteam.rst @@ -1,6 +1,4 @@ Stopping Steam -------------- -When you are finished using Steam, press ``Ctrl+C`` in each of the Steam, -Compilation Service, and postgres terminal windows to stop the services -end your session. +When you are finished using Steam, press ``Ctrl+C`` in the Steam and Compilation Services terminal windows to stop the services end your session. diff --git a/docs/conf.py b/docs/conf.py index 8980be00..137ae216 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -63,7 +63,7 @@ # built documents. # # The short X.Y version. -version = "Steam 1.1.4" +version = "Steam 1.1.5" # This commented out code is how the version is displayed in H2O. #if os.path.exists("project_version"): diff --git a/docs/images/launch_new_cluster.png b/docs/images/launch_new_cluster.png index 10615b6f..6a3974fd 100644 Binary files a/docs/images/launch_new_cluster.png and b/docs/images/launch_new_cluster.png differ diff --git a/etc/docker/centos-standalone/Dockerfile b/etc/docker/centos-standalone/Dockerfile index 57c1ebd3..6facec27 100644 --- a/etc/docker/centos-standalone/Dockerfile +++ b/etc/docker/centos-standalone/Dockerfile @@ -1,6 +1,6 @@ FROM centos:6.8 -MAINTAINER H2O.ai version: 1.1.4 +MAINTAINER H2O.ai version: 1.1.5 WORKDIR /steam diff --git a/etc/docker/hadoop/Dockerfile b/etc/docker/hadoop/Dockerfile index 16daaf03..11cdfef0 100644 --- a/etc/docker/hadoop/Dockerfile +++ b/etc/docker/hadoop/Dockerfile @@ -3,7 +3,7 @@ MAINTAINER H2O.ai USER root -ENV STEAM_VERSION 1.1.4 +ENV STEAM_VERSION 1.1.5 ENV PATH /steam/steam-${STEAM_VERSION}-linux-amd64:$PATH ENV PATH $PATH:/usr/local/hadoop/bin diff --git a/etc/docker/standalone/Dockerfile b/etc/docker/standalone/Dockerfile index 1b8065ff..39d6c3b0 100644 --- a/etc/docker/standalone/Dockerfile +++ b/etc/docker/standalone/Dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:latest -MAINTAINER H2O.ai version: 1.1.4 +MAINTAINER H2O.ai version: 1.1.5 WORKDIR /steam diff --git a/gui/package.json b/gui/package.json index 0727951c..f3129fa1 100644 --- a/gui/package.json +++ b/gui/package.json @@ -1,6 +1,6 @@ { "name": "steam", - "version": "1.1.4", + "version": "1.1.5", "description": "", "main": "src/main.tsx", "dependencies": { @@ -11,7 +11,7 @@ "h2oUIKit": "0.2.12", "jquery": "^3.0.0", "lodash": "^4.13.1", - "moment": "^2.14.1", + "moment": "2.16.0", "react": "^15.3.2", "react-dom": "^15.3.2", "react-motion": "^0.4.4", @@ -39,7 +39,7 @@ "ts-loader": "^0.8.2", "tslint": "^3.13.0", "tslint-loader": "^2.1.5", - "typescript": "^1.8.10", + "typescript": "^2.1.1", "typings": "^1.3.0", "url-loader": "^0.5.7", "webpack": "^1.13.1" diff --git a/gui/src/App/styles/app.scss b/gui/src/App/styles/app.scss index 7c13da5a..ab5d4f16 100644 --- a/gui/src/App/styles/app.scss +++ b/gui/src/App/styles/app.scss @@ -187,7 +187,6 @@ button, a { z-index: $body-index; display: flex; flex-direction: column; - box-shadow: 0 0 7px rgba(0,0,0,0.35); } } @@ -255,3 +254,51 @@ input[type="text"], textarea { .mar-bot-7 { margin-bottom: 7px; } + +@import '../../variables.scss'; + +.tooltip-launcher { + position: relative; +} +.tooltip { + position: absolute; + top: 29px; + left: -130px; + width: 300px; + + color: $h2o-charcoal-grey; + padding-left: 14px; + padding-right: 14px; + padding-bottom: 14px; + border-style: solid; + border-color: $h2o-charcoal-grey; + border-width: 2px; + background-color: white; + z-index: 20; +} + +.tooltip-question { + font-size: 10px; + font-weight: 300; + color: $h2o-charcoal-grey; + padding-top: 14px; +} + +.caret { + position: absolute; + width: 20px; + stroke-width: 20px; + top: -19px; + left: 125px; +} + +.caret-triangle { + fill:#FFFFFF; + stroke: $h2o-charcoal-grey; + stroke-miterlimit:10; +} +.caret-cover { + fill:#FFFFFF; + stroke:#FFFFFF; + stroke-miterlimit:10; +} diff --git a/gui/src/Clusters/Clusters.tsx b/gui/src/Clusters/Clusters.tsx index 40ddce8f..69ffb24b 100644 --- a/gui/src/Clusters/Clusters.tsx +++ b/gui/src/Clusters/Clusters.tsx @@ -33,6 +33,7 @@ import { connect } from 'react-redux'; import './styles/clusters.scss'; import { Link } from 'react-router'; import { getConfig } from './actions/clusters.actions'; +import ConfirmDeleteClusterDialog from "./components/ConfirmDeleteClusterDialog"; interface DispatchProps { fetchClusters: Function @@ -58,24 +59,15 @@ export class Clusters extends React.Component { super(props); this.state = { yarnClusterModalOpen: false, - newClusterRequested: false + newClusterRequested: false, + selectedCluster: null, + confirmDeleteClusterOpen: false }; } componentWillMount(): void { - if (_.isEmpty(this.props.clusters)) { - this.props.fetchClusters(); - this.props.getConfig(); - } - } - - removeCluster(cluster) { - if (cluster.type_id === 2) { - let keytabFilename = _.get((this.refs.keytabFilename as HTMLInputElement), 'value', ''); - this.props.stopClusterOnYarn(cluster.id, keytabFilename); - } else { - this.props.unregisterCluster(cluster.id); - } + this.props.fetchClusters(); + this.props.getConfig(); } openYarnClusterModal() { @@ -96,12 +88,45 @@ export class Clusters extends React.Component { this.setState({newClusterRequested: true}); } + removeCluster = (cluster) => { + if (cluster.type_id === 2) { + let keytabFilename = _.get((this.refs.keytabFilename as HTMLInputElement), 'value', ''); + this.props.stopClusterOnYarn(cluster.id, keytabFilename); + } else { + this.props.unregisterCluster(cluster.id); + } + }; + + onDeleteClusterClicked = (cluster) => { + this.setState({ + selectedCluster: cluster, + confirmDeleteClusterOpen: true + }); + }; + + onDeleteClusterConfirmed = () => { + this.removeCluster(this.state.selectedCluster); + this.setState({ + selectedCluster: null, + confirmDeleteClusterOpen: false + }); + }; + + onDeleteClusterCanceled = () => { + this.setState({ + selectedCluster: null, + confirmDeleteClusterOpen: false + }); + }; + render(): React.ReactElement { if (!this.props.clusters) { return
; } return (
+ + CLUSTERS { !this.state.newClusterRequested ?
@@ -134,11 +159,11 @@ export class Clusters extends React.Component {
{cluster.name} -- {cluster.status.total_cpu_count} nodes + rel="noopener" className="charcoal-grey semibold">{cluster.name} -- {cluster.status.total_cpu_count} cores - {_.get(this.props.config, 'kerberos_enabled', false) === true ? : null} + {_.get(this.props.config, 'kerberos_enabled', false) ? : null} -
diff --git a/gui/src/Clusters/components/ConfirmDeleteClusterDialog.tsx b/gui/src/Clusters/components/ConfirmDeleteClusterDialog.tsx new file mode 100644 index 00000000..f94d67af --- /dev/null +++ b/gui/src/Clusters/components/ConfirmDeleteClusterDialog.tsx @@ -0,0 +1,51 @@ + /* + Copyright (C) 2016 H2O.ai, Inc. + + 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 . +*/ + +import * as React from 'react'; +import '../styles/clusters.scss'; +import DefaultModal from '../../App/components/DefaultModal'; +import {Cluster} from "../../Proxy/Proxy"; + +interface Props { + clusterToDelete: Cluster + onDeleteConfirmed: Function + onCancel: Function + isOpen: boolean +} + +export default class ConfirmDeleteClusterDialog extends React.Component { + + onCancelClicked = () => { + this.props.onCancel(); + }; + + onConfirmClicked = () => { + this.props.onDeleteConfirmed(); + }; + + render(): React.ReactElement { + return ( + +

CONFIRM DELETE CLUSTER

  + {this.props.clusterToDelete ?

Are you sure you wish to delete cluster {this.props.clusterToDelete.name}?

: null} +
  +
Confirm
+
Cancel
+
+ ); + } +} diff --git a/gui/src/Clusters/components/LaunchCluster.tsx b/gui/src/Clusters/components/LaunchCluster.tsx index e0864f9a..088e9b55 100644 --- a/gui/src/Clusters/components/LaunchCluster.tsx +++ b/gui/src/Clusters/components/LaunchCluster.tsx @@ -54,7 +54,6 @@ export class LaunchCluster extends React.Component { constructor() { super(); this.state = { - memorySizeUnit: 'm', engineId: null }; } @@ -71,7 +70,7 @@ export class LaunchCluster extends React.Component { let size = (this.refs.clusterForm.querySelector('input[name="size"]') as HTMLInputElement).value; let memory = (this.refs.clusterForm.querySelector('input[name="memory"]') as HTMLInputElement).value; let keytab = _.get((this.refs.clusterForm.querySelector('input[name="keytab"]') as HTMLInputElement), 'value', ''); - this.props.startYarnCluster(clusterName, parseInt(engineId, 10), parseInt(size, 10), memory + this.state.memorySizeUnit, keytab); + this.props.startYarnCluster(clusterName, parseInt(engineId, 10), parseInt(size, 10), memory + 'g', keytab); } uploadEngine(event) { @@ -79,12 +78,6 @@ export class LaunchCluster extends React.Component { this.props.uploadEngine(this.refs.engine); } - onChangeMemory(event) { - this.setState({ - memorySizeUnit: event.target.value - }); - } - onChangeEngine(event) { this.setState({ engineId: event.target.value @@ -119,11 +112,7 @@ export class LaunchCluster extends React.Component { MEMORY PER NODE - - + GB @@ -143,7 +132,7 @@ export class LaunchCluster extends React.Component { - {_.get(this.props.config, 'kerberos_enabled', false) === true ? + {_.get(this.props.config, 'kerberos_enabled', false) ? Kerberos Keytab diff --git a/gui/src/Collaborators/components/projectMembers.tsx b/gui/src/Collaborators/components/projectMembers.tsx index 820f63f0..ae17500b 100644 --- a/gui/src/Collaborators/components/projectMembers.tsx +++ b/gui/src/Collaborators/components/projectMembers.tsx @@ -43,13 +43,17 @@ export class ProjectMembers extends React.Component this.props.fetchMembersForProject(); } + onLoadLabelsClicked = (e: React.MouseEvent) => { + this.props.loadLabelsTab(); + }; + render(): React.ReactElement { return (

Members

Theses are users who have access to this project, meaning they can see data, models and services associated with the project. Additionally, owners and collaborators can create new models, and new services based on those models.

-

Labels associated with projects have their own access controls, shown here.

+

Labels associated with projects have their own access controls, shown here.

USER diff --git a/gui/src/Configurations/components/CreateNewLabelModal.tsx b/gui/src/Configurations/components/CreateNewLabelModal.tsx index 5c6fc245..7b7b85fb 100644 --- a/gui/src/Configurations/components/CreateNewLabelModal.tsx +++ b/gui/src/Configurations/components/CreateNewLabelModal.tsx @@ -103,7 +103,7 @@ export default class CreateNewLabelModal extends React.Component {
- +
diff --git a/gui/src/Deployment/components/UploadPreProcessingModal.tsx b/gui/src/Deployment/components/UploadPreProcessingModal.tsx index ba4e4e8f..e0b7430e 100644 --- a/gui/src/Deployment/components/UploadPreProcessingModal.tsx +++ b/gui/src/Deployment/components/UploadPreProcessingModal.tsx @@ -47,8 +47,13 @@ export default class UploadPreProcessingModal extends React.Component { + this.setState({ + showTooltipMain: true + }); + }; + + onMainTooltipOut = () => { + this.setState({ + showTooltipMain: false + }); + }; + + onLibraryTooltipOver = () => { + this.setState({ + showTooltipLibrary: true + }); + }; + + onLibraryTooltipOut = () => { + this.setState({ + showTooltipLibrary: false + }); + }; + + onCondaTooltipOver = () => { + this.setState({ + showTooltipConda: true + }); + }; + + onCondaTooltipOut = () => { + this.setState({ + showTooltipConda: false + }); + }; + + onNameTooltipOver = () => { + this.setState({ + showTooltipName: true + }); + }; + + onNameTooltipOut = () => { + this.setState({ + showTooltipName: false + }); + }; + render(): React.ReactElement { let disableSubmit = false; if (this.state.libraryFiles.length < 1 || !this.state.mainFiles || !this.state.packageNamed) { @@ -113,8 +176,20 @@ export default class UploadPreProcessingModal extends React.Component -
Select a main Python file for pre-processing.
- The output from this Python file should be one row of an H2O data from that your model is expecting. +
+ Select a main Python file for pre-processing.  + +
@@ -134,8 +209,21 @@ export default class UploadPreProcessingModal extends React.Component -
Select a one or more Python files for your library.
- Any non-standard libraries called here should be installed into your deployment environment prior to launching services. +
+ Select a one or more Python files for your library.  + +
@@ -152,12 +240,60 @@ export default class UploadPreProcessingModal extends React.Component + + + + SELECT CONDA CONFIG + + +
+ Pick a .yaml file that defines your conda environment.  + +
+
+
+ + + + {this.state.condaFiles ? this.state.condaFiles.name : 'N/A'} + + + + +
+
+
+
+ NAME THE PACKAGE -
Pick a name for this pre-processing package. You will use it as a reference when deploying models.
+
Pick a name for this pre-processing package.  + +
Package name
diff --git a/gui/src/Deployment/styles/uploadpreprocessingmodal.scss b/gui/src/Deployment/styles/uploadpreprocessingmodal.scss index b55d73af..00efbc18 100644 --- a/gui/src/Deployment/styles/uploadpreprocessingmodal.scss +++ b/gui/src/Deployment/styles/uploadpreprocessingmodal.scss @@ -29,4 +29,5 @@ .package-name.error { border: 1px solid $brand-danger; } + } diff --git a/gui/src/ModelDetails/components/ExportModal.tsx b/gui/src/ModelDetails/components/ExportModal.tsx index a9fc9707..81f2ea91 100644 --- a/gui/src/ModelDetails/components/ExportModal.tsx +++ b/gui/src/ModelDetails/components/ExportModal.tsx @@ -70,6 +70,10 @@ export class ExportModal extends React.Component { } } + onCancelClicked = (e: React.MouseEvent) => { + this.props.onCancel(); + }; + render(): React.ReactElement { return ( @@ -128,7 +132,7 @@ export class ExportModal extends React.Component { Download - +
diff --git a/gui/src/Models/components/ModelLabelSelect.tsx b/gui/src/Models/components/ModelLabelSelect.tsx index 9167d299..05510e0d 100644 --- a/gui/src/Models/components/ModelLabelSelect.tsx +++ b/gui/src/Models/components/ModelLabelSelect.tsx @@ -41,14 +41,14 @@ export default class ModelLabelSelect extends React.Component { } render(): React.ReactElement { if (_.isUndefined(this.props.labels[this.props.projectId])) { - return ; + return ; } return ( - + {this.props.labels[this.props.projectId] ? this.props.labels[this.props.projectId].map((label: Label) => { return ( - + ); }) : } diff --git a/gui/src/Navigation/components/Navigation/navigation.scss b/gui/src/Navigation/components/Navigation/navigation.scss index ebc2a592..c421c99c 100644 --- a/gui/src/Navigation/components/Navigation/navigation.scss +++ b/gui/src/Navigation/components/Navigation/navigation.scss @@ -6,7 +6,6 @@ width: 228px; height: 100%; z-index: $body-index - 1; - box-shadow: inset -9px 0px 10px -5px rgba(0,0,0,0.35); } $navigation-width: $base-grid-unit * 5; diff --git a/gui/src/Projects/actions/projects.actions.ts b/gui/src/Projects/actions/projects.actions.ts index 9e22ada9..0210a7e9 100644 --- a/gui/src/Projects/actions/projects.actions.ts +++ b/gui/src/Projects/actions/projects.actions.ts @@ -444,6 +444,7 @@ export function registerCluster(address: string) { return (dispatch) => { Remote.registerCluster(address, (error, res) => { if (error) { + dispatch(openNotification(NotificationType.Error, 'Load error', error.toString(), null)); dispatch(registerClusterError(error.message)); return; } diff --git a/gui/src/Projects/components/MojoPojoSelector.tsx b/gui/src/Projects/components/MojoPojoSelector.tsx index 0b7a0e52..365f1783 100644 --- a/gui/src/Projects/components/MojoPojoSelector.tsx +++ b/gui/src/Projects/components/MojoPojoSelector.tsx @@ -51,9 +51,14 @@ export default class MojoPojoSelector extends React.Component { } return ( - + { this.state.showMenu ? -
+
+ + + + +

Choose Format

{mojoPojoSelection === "auto" ? : } Auto Select diff --git a/gui/src/Projects/components/Panel.tsx b/gui/src/Projects/components/Panel.tsx index 86385dee..6cfc164c 100644 --- a/gui/src/Projects/components/Panel.tsx +++ b/gui/src/Projects/components/Panel.tsx @@ -29,9 +29,16 @@ interface Props { } export default class Panel extends React.Component { + + onClick = (e: React.MouseEvent) => { + if (this.props.onClick) { + this.props.onClick(); + } + }; + render() { return ( -
+
{this.props.children}
); diff --git a/gui/src/Projects/components/ProgressBar.tsx b/gui/src/Projects/components/ProgressBar.tsx index d5e84661..e7248086 100644 --- a/gui/src/Projects/components/ProgressBar.tsx +++ b/gui/src/Projects/components/ProgressBar.tsx @@ -72,10 +72,14 @@ export default class ProgressBar extends React.Component { } } + onClick = (e: React.MouseEvent) => { + this.props.onClick(); + }; + render(): React.ReactElement { return (
+ className={classNames('progress-bar-container', {complete: this.state.progress === 100}, this.props.className)} onClick={this.onClick}>
void) => void - // Add an engine - addEngine: (engineName: string, enginePath: string, go: (error: Error, engineId: number) => void) => void - // Get engine details getEngine: (engineId: number, go: (error: Error, engine: Engine) => void) => void @@ -1675,20 +1672,6 @@ interface DeleteServiceOut { } -interface AddEngineIn { - - engine_name: string - - engine_path: string - -} - -interface AddEngineOut { - - engine_id: number - -} - interface GetEngineIn { engine_id: number @@ -3094,18 +3077,6 @@ export function deleteService(serviceId: number, go: (error: Error) => void): vo }); } -export function addEngine(engineName: string, enginePath: string, go: (error: Error, engineId: number) => void): void { - const req: AddEngineIn = { engine_name: engineName, engine_path: enginePath }; - Proxy.Call("AddEngine", req, function(error, data) { - if (error) { - return go(error, null); - } else { - const d: AddEngineOut = data; - return go(null, d.engine_id); - } - }); -} - export function getEngine(engineId: number, go: (error: Error, engine: Engine) => void): void { const req: GetEngineIn = { engine_id: engineId }; Proxy.Call("GetEngine", req, function(error, data) { diff --git a/gui/src/Users/components/CreateRole.tsx b/gui/src/Users/components/CreateRole.tsx index 2bc5c878..a4c89c4e 100644 --- a/gui/src/Users/components/CreateRole.tsx +++ b/gui/src/Users/components/CreateRole.tsx @@ -116,6 +116,10 @@ export class CreateRole extends React.Component { this.props.createRole(this.nameInput.value, this.descriptionInput.value, newRolePermissions); } + onCancelClicked = (e: React.MouseEvent) => { + this.props.cancelHandler(); + }; + render(): React.ReactElement { this.permissionInputs = []; return ( @@ -148,7 +152,7 @@ export class CreateRole extends React.Component { {this.state.validNameEntered && this.state.validDescriptionEntered ?
Create Role
:
Create Role
} -
Cancel
+
Cancel
); } diff --git a/gui/src/Users/components/CreateUser.tsx b/gui/src/Users/components/CreateUser.tsx index 00e51275..0475e0bd 100644 --- a/gui/src/Users/components/CreateUser.tsx +++ b/gui/src/Users/components/CreateUser.tsx @@ -29,7 +29,6 @@ import {createUser, NewUserDetails} from "../actions/users.actions"; import InputFeedback from "../../App/components/InputFeedback"; import {FeedbackType} from "../../App/components/InputFeedback"; - interface Props { workgroups: Array, roles: Array, @@ -166,6 +165,10 @@ export class CreateUser extends React.Component { )); }; + onCancelClicked = (e: React.MouseEvent) => { + this.props.cancelHandler(); + }; + render(): React.ReactElement { this.inputsWithRoles = []; this.inputsWithWorkgroups = []; @@ -239,7 +242,7 @@ export class CreateUser extends React.Component { {this.state.validNameEntered && this.state.validPasswordEntered && this.state.validPasswordConfirmEntered ?
Create User
:
Create User
} -
Cancel
+
Cancel
); } diff --git a/gui/src/Users/components/RolePermissions.tsx b/gui/src/Users/components/RolePermissions.tsx index 0a21ff90..493da09d 100644 --- a/gui/src/Users/components/RolePermissions.tsx +++ b/gui/src/Users/components/RolePermissions.tsx @@ -153,7 +153,7 @@ export class RolePermissions extends React.Component return {permissionSet.description} {permissionSet.flags.map((flag: any, flagIndex) => { - if (flagIndex === 0) { + if (flag.roleId === 1) { return this.registerInput(input, {value: true, roleId: flag.roleId}, flagIndex, permissionSet, permissionIndex)} type="checkbox" value="on" defaultChecked={true} readOnly={true} disabled={true}>; @@ -168,7 +168,7 @@ export class RolePermissions extends React.Component } deleteRolesCells = this.props.roles.map((role, rolesIndex) => { - if (rolesIndex !== 0) { + if (role.id !== 1) { return ; diff --git a/gui/src/Users/components/RolePermissionsConfirm.tsx b/gui/src/Users/components/RolePermissionsConfirm.tsx index 2000bf2d..49c832a1 100644 --- a/gui/src/Users/components/RolePermissionsConfirm.tsx +++ b/gui/src/Users/components/RolePermissionsConfirm.tsx @@ -116,6 +116,10 @@ export default class RolePermissionsConfirm extends React.Component }); }; + onCloseClicked = (e: React.MouseEvent) => { + this.props.closeHandler(); + }; + render(): React.ReactElement { let saveChangesEnabled = true; for (let requestedChange of this.state.requestedChanges) { @@ -184,7 +188,7 @@ export default class RolePermissionsConfirm extends React.Component
 
-
Close
+
Close
:

CONFIRMING PERMISSION CHANGES

diff --git a/gui/src/Users/components/UserAccess.tsx b/gui/src/Users/components/UserAccess.tsx index 432e21c1..69061f74 100644 --- a/gui/src/Users/components/UserAccess.tsx +++ b/gui/src/Users/components/UserAccess.tsx @@ -30,6 +30,8 @@ import { import {Role, Identity, Project, Workgroup} from "../../Proxy/Proxy"; import {fetchWorkgroups} from "../../Projects/actions/projects.actions"; import EditUserDialog from "./EditUserDialog"; +import InputFeedback from "../../App/components/InputFeedback"; +import {FeedbackType} from "../../App/components/InputFeedback"; interface Props { projects: Array, @@ -67,9 +69,9 @@ export class UserAccess extends React.Component { } } - onRoleCheckboxClicked(e) { + onRoleCheckboxClicked = (e) => { this.props.changeFilterSelections(parseInt((e.target.dataset as any).id, 10), e.target.checked); - } + }; checkIsRoleSelected(id): boolean { if (_.isEmpty(this.props.selectedRoles)) { @@ -86,6 +88,14 @@ export class UserAccess extends React.Component { } shouldRowBeShown(roles: Array): boolean { + if (roles.length === 0) { + for (let selectedRole of this.props.selectedRoles) { + if (selectedRole.id === -1) { + return selectedRole.selected; + } + } + } + for (let role of roles) { let index = _.findIndex(this.props.selectedRoles, (o) => { if ((o as any).id === role.id) { @@ -145,6 +155,7 @@ export class UserAccess extends React.Component { return {userWithRoleAndProject.user.name} { + userWithRoleAndProject.roles.length === 0 ?
this.onEditUserClicked(userWithRoleAndProject.user)}>Assign one or more roles
: userWithRoleAndProject.roles.map((role, index) => { return {role.name}
@@ -157,7 +168,27 @@ export class UserAccess extends React.Component { })); }; + onSelectNoneClicked = () => { + for (let selectedRole of this.props.selectedRoles) { + this.props.changeFilterSelections(selectedRole.id, false); + } + }; + onSelectAllClicked = () => { + for (let selectedRole of this.props.selectedRoles) { + this.props.changeFilterSelections(selectedRole.id, true); + } + }; + render(): React.ReactElement { + let numRolesSelected = 0; + if (this.props.selectedRoles) { + for (let selectedRole of this.props.selectedRoles) { + if (selectedRole.selected) { + numRolesSelected++; + } + } + } + return (
@@ -167,7 +198,37 @@ export class UserAccess extends React.Component {
- ROLES + ROLES
+
+ {numRolesSelected === 0 ? +
+ + Select None
+
+ :
+ + Select None
+
} + + {this.props.selectedRoles && numRolesSelected > 0 && numRolesSelected < this.props.selectedRoles.length ? +
+ Select Some
+
+ :
+ Select Some
+
+ } + + {this.props.selectedRoles && numRolesSelected === this.props.selectedRoles.length ? +
+ Select All +
+ :
+ Select All +
+ } + +
{this.props.roles ? @@ -180,6 +241,11 @@ export class UserAccess extends React.Component { } : null } + + + [No roles] + +
diff --git a/gui/src/Users/reducers/users.reducer.ts b/gui/src/Users/reducers/users.reducer.ts index 9a8e7003..5fc7eba7 100644 --- a/gui/src/Users/reducers/users.reducer.ts +++ b/gui/src/Users/reducers/users.reducer.ts @@ -110,11 +110,16 @@ export const usersReducer = (state: any = initialState, action: any) => { let toAppend: any = {roles}; if (!(state as any).selectedRoles) { - let selectedRoles = new Array(roles.length); + let selectedRoles = new Array(roles.length + 1); - for (let i = 0; i < selectedRoles.length; i++) { + selectedRoles[0] = { + id: -1, + selected: true + }; + + for (let i = 1; i < selectedRoles.length; i++) { selectedRoles[i] = { - id: roles[i].id, + id: roles[i - 1].id, selected: true }; } diff --git a/gui/src/Users/styles/users.scss b/gui/src/Users/styles/users.scss index 359bb579..ed8adc03 100644 --- a/gui/src/Users/styles/users.scss +++ b/gui/src/Users/styles/users.scss @@ -1,7 +1,12 @@ @import '../../variables.scss'; .user-access { - + .bulk-select { + font-family: "Open Sans", sans-serif; + font-size: 14px; + font-weight: normal; + text-transform: none; + } } .filter-and-list { display: flex; diff --git a/gui/src/variables.scss b/gui/src/variables.scss index 8220c5d2..4502b87a 100644 --- a/gui/src/variables.scss +++ b/gui/src/variables.scss @@ -65,3 +65,6 @@ $h2o-background-grey: #eeeeee; .red { color: $h2o-red-prime; } +.orange { + color: $h2o-orange-prime; +} diff --git a/master/main.go b/master/main.go index 9853d414..ffb011e1 100644 --- a/master/main.go +++ b/master/main.go @@ -38,14 +38,14 @@ import ( ) const ( - defaultWebAddress = ":9000" - defaultClusterProxyAddress = ":9001" - defaultCompilationAddress = ":8080" - defaultScoringServiceHost = "" - DefaultScoringServicePortsString = "1025:65535" + defaultWebAddress = ":9000" + defaultClusterProxyAddress = ":9001" + defaultCompilationAddress = ":8080" + defaultPredictionServiceHost = "" + DefaultPredictionServicePortsString = "1025:65535" ) -var defaultScoringServicePorts = [...]int{1025, 65535} +var defaultPredictionServicePorts = [...]int{1025, 65535} type DBOpts struct { Connection data.Connection @@ -55,8 +55,6 @@ type DBOpts struct { type YarnOpts struct { KerberosEnabled bool - Username string - Keytab string } type Opts struct { @@ -68,8 +66,8 @@ type Opts struct { WorkingDirectory string ClusterProxyAddress string CompilationServiceAddress string - ScoringServiceHost string - ScoringServicePorts [2]int + PredictionServiceHost string + PredictionServicePorts [2]int EnableProfiler bool Yarn YarnOpts DB DBOpts @@ -97,10 +95,10 @@ var DefaultOpts = &Opts{ path.Join(".", fs.VarDir, "master"), defaultClusterProxyAddress, defaultCompilationAddress, - defaultScoringServiceHost, - defaultScoringServicePorts, + defaultPredictionServiceHost, + defaultPredictionServicePorts, false, - YarnOpts{false, "", ""}, + YarnOpts{false}, DBOpts{DefaultConnection, "", ""}, } @@ -160,14 +158,14 @@ func Run(version, buildDate string, opts Opts) { authProvider = newBasicAuthProvider(defaultAz, webAddress) } - // --- set up scoring service launch host + // --- set up prediction service launch host - var scoringServiceHost string - if opts.ScoringServiceHost != "" { - scoringServiceHost = opts.ScoringServiceHost + var predictionServiceHost string + if opts.PredictionServiceHost != "" { + predictionServiceHost = opts.PredictionServiceHost } else { var err error - scoringServiceHost, err = fs.GetExternalHost() + predictionServiceHost, err = fs.GetExternalHost() if err != nil { log.Fatalln(err) } @@ -180,12 +178,10 @@ func Run(version, buildDate string, opts Opts) { wd, ds, opts.CompilationServiceAddress, - scoringServiceHost, + predictionServiceHost, opts.ClusterProxyAddress, - opts.ScoringServicePorts, + opts.PredictionServicePorts, opts.Yarn.KerberosEnabled, - opts.Yarn.Username, - opts.Yarn.Keytab, ) webServiceImpl := &srvweb.Impl{webService, defaultAz} diff --git a/master/upload.go b/master/upload.go index 43b5fccb..ee758094 100644 --- a/master/upload.go +++ b/master/upload.go @@ -134,7 +134,7 @@ func (s *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE, fs.FilePerm) + dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_TRUNC, fs.FilePerm) if err != nil { log.Println("Upload file open operation failed:", err) http.Error(w, fmt.Sprintf("Error writing uploaded file to disk: %s", err), http.StatusInternalServerError) @@ -199,7 +199,7 @@ func (s *UploadHandler) handleEngine(w http.ResponseWriter, pz az.Principal, fil } // Add Engine to datastore - if _, err := s.webService.AddEngine(pz, dstBase, dstPath); err != nil { + if _, err := s.ds.CreateEngine(pz, dstBase, dstPath); err != nil { http.Error(w, fmt.Sprintf("Error saving engine to datastore: %v", err), http.StatusInternalServerError) return errors.Wrap(err, "failed saving engine to datastore") } diff --git a/master/web/driver.go b/master/web/driver.go index f3fb1308..c6cd7e1c 100644 --- a/master/web/driver.go +++ b/master/web/driver.go @@ -44,8 +44,6 @@ type driverDBOpts struct { type driverYarnOpts struct { KerberosEnabled bool - Username string - Keytab string } func newService(opts driverOpts) (web.Service, az.Directory, error) { @@ -72,7 +70,5 @@ func newService(opts driverOpts) (web.Service, az.Directory, error) { opts.ClusterProxyAddress, opts.ScoringServicePorts, opts.Yarn.KerberosEnabled, - opts.Yarn.Username, - opts.Yarn.Keytab, ), ds, nil } diff --git a/master/web/service.go b/master/web/service.go index 0653a043..2a46c9b7 100644 --- a/master/web/service.go +++ b/master/web/service.go @@ -52,8 +52,6 @@ type Service struct { scoringServicePortMin int scoringServicePortMax int kerberosEnabled bool - username string - keytab string } func NewService( @@ -62,7 +60,6 @@ func NewService( compilationServiceAddress, scoringServiceAddress, clusterProxyAddress string, scoringServicePortsRange [2]int, kerberos bool, - username, keytab string, ) *Service { return &Service{ workingDir, @@ -70,7 +67,6 @@ func NewService( compilationServiceAddress, scoringServiceAddress, clusterProxyAddress, scoringServicePortsRange[0], scoringServicePortsRange[1], kerberos, - username, keytab, } } @@ -1676,15 +1672,6 @@ func (s *Service) DeleteService(pz az.Principal, serviceId int64) error { return nil } -// FIXME this should not be here - not an client-facing API -func (s *Service) AddEngine(pz az.Principal, engineName, enginePath string) (int64, error) { - if err := pz.CheckPermission(s.ds.Permissions.ManageEngine); err != nil { - return 0, err - } - - return s.ds.CreateEngine(pz, engineName, enginePath) -} - func (s *Service) GetEngine(pz az.Principal, engineId int64) (*web.Engine, error) { if err := pz.CheckPermission(s.ds.Permissions.ViewEngine); err != nil { return nil, err diff --git a/master/web/setup_test.go b/master/web/setup_test.go index 97ef77d7..b2d6192f 100644 --- a/master/web/setup_test.go +++ b/master/web/setup_test.go @@ -106,7 +106,7 @@ func newTest(t *testing.T) *test { compilationServiceAddress, "", [2]int{1025, 65535}, - driverYarnOpts{false, "", ""}, + driverYarnOpts{false}, dbOpts, } svc, dir, err := newService(opts) diff --git a/python/steam.py b/python/steam.py index 226e56dd..9ce20455 100644 --- a/python/steam.py +++ b/python/steam.py @@ -1187,24 +1187,6 @@ def delete_service(self, service_id): response = self.connection.call("DeleteService", request) return - def add_engine(self, engine_name, engine_path): - """ - Add an engine - - Parameters: - engine_name: No description available (string) - engine_path: No description available (string) - - Returns: - engine_id: No description available (int64) - """ - request = { - 'engine_name': engine_name, - 'engine_path': engine_path - } - response = self.connection.call("AddEngine", request) - return response['engine_id'] - def get_engine(self, engine_id): """ Get engine details diff --git a/srv/compiler/deepwater.go b/srv/compiler/deepwater.go index 2fa0e2a9..d1ecb376 100644 --- a/srv/compiler/deepwater.go +++ b/srv/compiler/deepwater.go @@ -32,9 +32,9 @@ type Deepwater struct { } // NewDeepWater returns a new instance of Deepwater. -func NewDeepwater(workingDirectory string, modelId int64, logicalName, modelType string, pythonFiles ...string) *Deepwater { +func NewDeepwater(workingDirectory string, modelId int64, logicalName, modelType string, pythonFiles pythonPackage) *Deepwater { return &Deepwater{ - Model: NewModel(workingDirectory, modelId, logicalName, modelType, pythonFiles...), + Model: NewModel(workingDirectory, modelId, logicalName, modelType, pythonFiles), deepwaterDep: fs.GetDeepwaterDepPath(workingDirectory, modelId), } } diff --git a/srv/compiler/model.go b/srv/compiler/model.go index 25ef8d05..64e971ac 100644 --- a/srv/compiler/model.go +++ b/srv/compiler/model.go @@ -29,10 +29,10 @@ type Model struct { modelType string javaDep string - pythonFilePaths []string + pythonFiles pythonPackage } -func NewModel(workingDirectory string, modelId int64, logicalName, modelType string, pythonFiles ...string) *Model { +func NewModel(workingDirectory string, modelId int64, logicalName, modelType string, pythonFiles pythonPackage) *Model { m := &Model{javaDep: fs.GetGenModelPath(workingDirectory, modelId)} switch modelType { @@ -48,16 +48,13 @@ func NewModel(workingDirectory string, modelId int64, logicalName, modelType str panic(fmt.Errorf("invalid model type %q", modelType)) } - pythonPaths := make([]string, len(pythonFiles)) - for i, path := range pythonFiles { - pythonPaths[i] = path - } - m.pythonFilePaths = pythonPaths + m.pythonFiles = pythonFiles return m } func (c *Model) AttachFiles(w *multipart.Writer) error { + // Attach Java files if err := attachFile(w, c.modelPath, c.modelType); err != nil { return errors.Wrap(err, "attaching model") } @@ -65,17 +62,21 @@ func (c *Model) AttachFiles(w *multipart.Writer) error { return errors.Wrap(err, "attaching java dependency") } - for i, file := range c.pythonFilePaths { - if i < 1 { - if err := attachFile(w, file, fileTypePythonMain); err != nil { - return errors.Wrap(err, "attaching Python main file") - } - } else { + // Attach Python Package Files + if c.pythonFiles.Main != "" { + if err := attachFile(w, c.pythonFiles.Main, fileTypePythonMain); err != nil { + return errors.Wrap(err, "attaching Python main file") + } + for _, file := range c.pythonFiles.Other { if err := attachFile(w, file, fileTypePythonOther); err != nil { return errors.Wrap(err, "attaching Python file") } } + if c.pythonFiles.Yaml != "" { + if err := attachFile(w, c.pythonFiles.Yaml, fileTypePythonEnv); err != nil { + return errors.Wrap(err, "attaching Python env file") + } + } } - return nil } diff --git a/srv/compiler/service.go b/srv/compiler/service.go index 86a95ff6..af3c5397 100644 --- a/srv/compiler/service.go +++ b/srv/compiler/service.go @@ -47,8 +47,15 @@ const ( fileTypeMOJO = "mojo" fileTypePythonMain = "python" fileTypePythonOther = "pythonextra" + fileTypePythonEnv = "envfile" ) +type pythonPackage struct { + Main string + Yaml string + Other []string +} + type ModelAsset interface { AttachFiles(w *multipart.Writer) error } @@ -69,7 +76,7 @@ func CompileModel(address, wd string, projectId, modelId int64, logicalName, mod return targetFile, nil } - var pythonFilePaths []string + var pythonFilePaths pythonPackage if artifact == ArtifactPythonWar { var err error pythonFilePaths, err = getPythonFilePaths(wd, packageName, projectId) @@ -80,9 +87,9 @@ func CompileModel(address, wd string, projectId, modelId int64, logicalName, mod var assets ModelAsset if algorithm == "Deep Water" { - assets = NewDeepwater(wd, modelId, logicalName, modelType, pythonFilePaths...) + assets = NewDeepwater(wd, modelId, logicalName, modelType, pythonFilePaths) } else { - assets = NewModel(wd, modelId, logicalName, modelType, pythonFilePaths...) + assets = NewModel(wd, modelId, logicalName, modelType, pythonFilePaths) } // ping to check if service is up @@ -154,39 +161,41 @@ func callCompiler(url, targetFile string, model ModelAsset) error { return nil } -func getPythonFilePaths(workingDirectory, packageName string, projectId int64) ([]string, error) { +func getPythonFilePaths(workingDirectory, packageName string, projectId int64) (pythonPackage, error) { packageName = strings.TrimSpace(packageName) if len(packageName) < 1 { - return nil, errors.New("package not set for PythonWar") + return pythonPackage{}, errors.New("package not set for PythonWar") } - var pythonMainFilePath string - var pythonOtherFilePaths []string + var ( + pythonMainFilePath, pythonYamlFilePath string + pythonOtherFilePaths []string + ) packagePath := fs.GetPackagePath(workingDirectory, projectId, packageName) if !fs.DirExists(packagePath) { - return nil, fmt.Errorf("Package %s does not exist") + return pythonPackage{}, fmt.Errorf("Package %s does not exist") } packageAttrsBytes, err := fs.GetPackageAttributes(workingDirectory, projectId, packageName) if err != nil { - return nil, fmt.Errorf("Failed reading package attributes: %s", err) + return pythonPackage{}, fmt.Errorf("Failed reading package attributes: %s", err) } packageAttrs, err := fs.JsonToMap(packageAttrsBytes) if err != nil { - return nil, fmt.Errorf("Failed parsing package attributes: %s", err) + return pythonPackage{}, fmt.Errorf("Failed parsing package attributes: %s", err) } pythonMain, ok := packageAttrs["main"] if !ok { - return nil, fmt.Errorf("Failed determining Python main file from package attributes") + return pythonPackage{}, fmt.Errorf("Failed determining Python main file from package attributes") } packageFileList, err := fs.ListFiles(packagePath) if err != nil { - return nil, fmt.Errorf("Failed reading package file list: %s", err) + return pythonPackage{}, fmt.Errorf("Failed reading package file list: %s", err) } // Filter .py files; separate ancillary files from the main one. @@ -199,14 +208,20 @@ func getPythonFilePaths(workingDirectory, packageName string, projectId int64) ( } else { pythonOtherFilePaths = append(pythonOtherFilePaths, p) } + } else if strings.ToLower(path.Ext(f)) == ".yaml" { + pythonYamlFilePath = path.Join(packagePath, f) } } if len(pythonMainFilePath) == 0 { - return nil, fmt.Errorf("Failed locating Python main file in package file listing") + return pythonPackage{}, fmt.Errorf("Failed locating Python main file in package file listing") } - return append([]string{pythonMainFilePath}, pythonOtherFilePaths...), nil + return pythonPackage{ + Main: pythonMainFilePath, + Yaml: pythonYamlFilePath, + Other: pythonOtherFilePaths, + }, nil } func toUrl(address, slug string) string { diff --git a/srv/web/api/api.go b/srv/web/api/api.go index 57d144b3..0f000a3e 100644 --- a/srv/web/api/api.go +++ b/srv/web/api/api.go @@ -341,7 +341,6 @@ type Service struct { GetServicesForProject GetServicesForProject `help:"List services for a project"` GetServicesForModel GetServicesForModel `help:"List services for a model"` DeleteService DeleteService `help:"Delete a service"` - AddEngine AddEngine `help:"Add an engine"` GetEngine GetEngine `help:"Get engine details"` GetEngines GetEngines `help:"List engines"` DeleteEngine DeleteEngine `help:"Delete an engine"` @@ -743,12 +742,6 @@ type GetServicesForModel struct { type DeleteService struct { ServiceId int64 } -type AddEngine struct { - EngineName string - EnginePath string - _ int - EngineId int64 -} type GetEngine struct { EngineId int64 _ int diff --git a/srv/web/service.go b/srv/web/service.go index 5679cb69..19d0fa85 100644 --- a/srv/web/service.go +++ b/srv/web/service.go @@ -352,7 +352,6 @@ type Service interface { GetServicesForProject(pz az.Principal, projectId int64, offset int64, limit int64) ([]*ScoringService, error) GetServicesForModel(pz az.Principal, modelId int64, offset int64, limit int64) ([]*ScoringService, error) DeleteService(pz az.Principal, serviceId int64) error - AddEngine(pz az.Principal, engineName string, enginePath string) (int64, error) GetEngine(pz az.Principal, engineId int64) (*Engine, error) GetEngines(pz az.Principal) ([]*Engine, error) DeleteEngine(pz az.Principal, engineId int64) error @@ -951,15 +950,6 @@ type DeleteServiceIn struct { type DeleteServiceOut struct { } -type AddEngineIn struct { - EngineName string `json:"engine_name"` - EnginePath string `json:"engine_path"` -} - -type AddEngineOut struct { - EngineId int64 `json:"engine_id"` -} - type GetEngineIn struct { EngineId int64 `json:"engine_id"` } @@ -2022,16 +2012,6 @@ func (this *Remote) DeleteService(serviceId int64) error { return nil } -func (this *Remote) AddEngine(engineName string, enginePath string) (int64, error) { - in := AddEngineIn{engineName, enginePath} - var out AddEngineOut - err := this.Proc.Call("AddEngine", &in, &out) - if err != nil { - return 0, err - } - return out.EngineId, nil -} - func (this *Remote) GetEngine(engineId int64) (*Engine, error) { in := GetEngineIn{engineId} var out GetEngineOut @@ -4755,41 +4735,6 @@ func (this *Impl) DeleteService(r *http.Request, in *DeleteServiceIn, out *Delet return nil } -func (this *Impl) AddEngine(r *http.Request, in *AddEngineIn, out *AddEngineOut) error { - const name = "AddEngine" - - guid := xid.New().String() - - pz, azerr := this.Az.Identify(r) - if azerr != nil { - return azerr - } - - req, merr := json.Marshal(in) - if merr != nil { - log.Println(guid, "REQ", pz, name, merr) - } else { - log.Println(guid, "REQ", pz, name, string(req)) - } - - val0, err := this.Service.AddEngine(pz, in.EngineName, in.EnginePath) - if err != nil { - log.Println(guid, "ERR", pz, name, err) - return err - } - - out.EngineId = val0 - - res, merr := json.Marshal(out) - if merr != nil { - log.Println(guid, "RES", pz, name, merr) - } else { - log.Println(guid, "RES", pz, name, string(res)) - } - - return nil -} - func (this *Impl) GetEngine(r *http.Request, in *GetEngineIn, out *GetEngineOut) error { const name = "GetEngine" diff --git a/tests/goh2orunner.sh b/tests/goh2orunner.sh index 775663c4..c942dcd4 100755 --- a/tests/goh2orunner.sh +++ b/tests/goh2orunner.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Start H2O and create a start log + store pid -java -jar ../var/temp/h2o-$H2OVERSION/h2o.jar --name steam-h2o > start.log 2>&1 & +java -jar ../var/temp/h2o-$H2OVERSION/h2o.jar --name steam-$(cat /dev/urandom | env LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) > start.log 2>&1 & H2OPID=$! echo "Started h2o with pid ${H2OPID}"