From 52a540eab51b06896fc75e4164424ae81ab05e0f Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Fri, 12 Feb 2016 13:31:55 -0500 Subject: [PATCH 001/241] use Getopt::Std to parse command line options --- bootstrap.pl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bootstrap.pl b/bootstrap.pl index 5e64cacd..e8a27227 100755 --- a/bootstrap.pl +++ b/bootstrap.pl @@ -2,6 +2,7 @@ use strict; use FileHandle; +use Getopt::Std; # Bootstrap CloudCoder on an Ubuntu server @@ -12,8 +13,11 @@ #print "program=$program\n"; #exit 0; +my %opts = (); +getopts('n', \%opts); + my $dryRun = 0; -if (scalar(@ARGV) > 0 && $ARGV[0] eq '-n') { +if (exists $opts{'n'}) { print ">>> Dry run <<<\n"; shift @ARGV; $dryRun = 1; From ea88af031150aca44648cb05d7aa9bbd9ce3045e Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Fri, 12 Feb 2016 14:36:01 -0500 Subject: [PATCH 002/241] store all config props in a hash This will make it easy to support non-interactive configuration, e.g., for building a Docker image with hard-coded config values. --- bootstrap.pl | 91 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/bootstrap.pl b/bootstrap.pl index e8a27227..7898fd5f 100755 --- a/bootstrap.pl +++ b/bootstrap.pl @@ -13,6 +13,10 @@ #print "program=$program\n"; #exit 0; +# Configuration properties +my %props = (); + +# Parse command line options my %opts = (); getopts('n', \%opts); @@ -25,10 +29,19 @@ my $mode = 'start'; +# See if the mode was specified explicitly if (scalar(@ARGV) > 0) { $mode = shift @ARGV; } +# Assume that any remaining command line option is +# stringified config properties (which is what should +# happen if step2 is being executed, or if start is +# being executed noninteractively) +if (scalar(@ARGV) > 0) { + %props = UnstringifyProps(shift @ARGV); +} + if ($mode eq 'start') { Start(); } elsif ($mode eq 'step2') { @@ -62,16 +75,16 @@ sub Start { print "\nFirst, please enter some configuration information...\n\n"; # Get minimal required configuration information - my $ccUser = ask("What username do you want for your CloudCoder account?"); - my $ccPasswd = ask("What password do you want for your CloudCoder account?"); - my $ccFirstName = ask("What is your first name?"); - my $ccLastName = ask("What is your last name?"); - my $ccEmail = ask("What is your email address?"); - my $ccWebsite = ask("What is the URL of your personal website?"); - my $ccInstitutionName = ask("What is the name of your institution?"); - my $ccMysqlRootPasswd = ask("What password do you want for the MySQL root user?"); - my $ccMysqlCCPasswd = ask("What password do you want for the MySQL cloudcoder user?"); - my $ccHostname = ask("What is the hostname of this server?"); + $props{'ccUser'} = ask("What username do you want for your CloudCoder account?"); + $props{'ccPasswd'} = ask("What password do you want for your CloudCoder account?"); + $props{'ccFirstName'} = ask("What is your first name?"); + $props{'ccLastName'} = ask("What is your last name?"); + $props{'ccEmail'} = ask("What is your email address?"); + $props{'ccWebsite'} = ask("What is the URL of your personal website?"); + $props{'ccInstitutionName'} = ask("What is the name of your institution?"); + $props{'ccMysqlRootPasswd'} = ask("What password do you want for the MySQL root user?"); + $props{'ccMysqlCCPasswd'} = ask("What password do you want for the MySQL cloudcoder user?"); + $props{'ccHostname'} = ask("What is the hostname of this server?"); print "\n"; my $startInstall = ask("Are you ready to start the installation? (yes/no)"); @@ -94,8 +107,8 @@ sub Start { # Configure mysql root password so that no user interaction # will be required when installing packages. - DebconfSetSelections("mysql-server-$mysqlVersion", "mysql-server/root_password", "password $ccMysqlRootPasswd"); - DebconfSetSelections("mysql-server-$mysqlVersion", "mysql-server/root_password_again", "password $ccMysqlRootPasswd"); + DebconfSetSelections("mysql-server-$mysqlVersion", "mysql-server/root_password", "password $props{'ccMysqlRootPasswd'}"); + DebconfSetSelections("mysql-server-$mysqlVersion", "mysql-server/root_password_again", "password $props{'ccMysqlRootPasswd'}"); # Install packages. Note that because cloudcoderApp.jar is self-configuring, # we can install the JRE rather than the full JDK, as we won't need @@ -111,10 +124,10 @@ sub Start { # ---------------------------------------------------------------------- section("Configuring MySQL..."); print "Creating cloudcoder user...\n"; - Run("mysql", "--user=root", "--pass=$ccMysqlRootPasswd", - "--execute=create user 'cloudcoder'\@'localhost' identified by '$ccMysqlCCPasswd'"); + Run("mysql", "--user=root", "--pass=$props{'ccMysqlRootPasswd'}", + "--execute=create user 'cloudcoder'\@'localhost' identified by '$props{'ccMysqlCCPasswd'}'"); print "Granting permissions on cloudcoderdb to cloudcoder...\n"; - Run("mysql", "--user=root", "--pass=$ccMysqlRootPasswd", + Run("mysql", "--user=root", "--pass=$props{'ccMysqlRootPasswd'}", "--execute=grant all on cloudcoderdb.* to 'cloudcoder'\@'localhost'"); # ---------------------------------------------------------------------- @@ -130,7 +143,7 @@ sub Start { # ---------------------------------------------------------------------- section("Configuring apache2..."); print "Generating SSL configuration...\n"; - EditApache2DefaultSsl($ccHostname); + EditApache2DefaultSsl($props{'ccHostname'}); print "Enabling modules...\n"; RunAdmin(cmd => ['a2enmod', 'proxy']); RunAdmin(cmd => ['a2enmod', 'proxy_http']); @@ -145,13 +158,7 @@ sub Start { section("Continuing as cloud user..."); Run("cp", $program, "/tmp/bootstrap.pl"); Run("chmod", "a+x", "/tmp/bootstrap.pl"); - RunAdmin( - asUser => 'cloud', - cmd => ["/tmp/bootstrap.pl", "step2", - "ccUser=$ccUser,ccPassword=$ccPasswd,ccFirstName=$ccFirstName," . - "ccLastName=$ccLastName,ccEmail=$ccEmail,ccWebsite=$ccWebsite," . - "ccInstitutionName=$ccInstitutionName," . - "ccMysqlCCPasswd=$ccMysqlCCPasswd,ccHostname=$ccHostname"]); + RunAdmin(asUser => 'cloud', cmd => ["/tmp/bootstrap.pl", "step2", StringifyProps("\a", "\a")]); # ---------------------------------------------------------------------- # Copy the configured builder jarfile into the home directory of the current user. @@ -174,7 +181,7 @@ sub Start { You should be able to test your new installation by opening the following web page: - https://$ccHostname/cloudcoder + https://$props{'ccHostname'}/cloudcoder Note that no builders are running, so you won't be able to test submissions yet. The builder jar file ($builderJar) @@ -191,12 +198,6 @@ sub Step2 { print "Step2: running as $whoami\n"; chdir "/home/cloud" || die "Couldn't change directory to /home/cloud: $!\n"; - # Get configuration properties passed from start step - my %props = split(/,|=/, $ARGV[0]); - foreach my $name (keys %props) { - print "$name=$props{$name}\n"; - } - # Create webapp directory and change to it Run("mkdir", "-p", "webapp"); chdir "webapp" || die "Couldn't change directory to webapp directory: $!\n"; @@ -224,16 +225,14 @@ sub Step2 { # Generate cloudcoder.properties print "Creating cloudcoder.properties...\n"; my $pfh = new FileHandle(">cloudcoder.properties"); - my $ccMysqlCCPasswd = $props{ccMysqlCCPasswd}; - my $ccHostname = $props{ccHostname}; print $pfh <<"ENDPROPERTIES"; cloudcoder.db.user=cloudcoder -cloudcoder.db.passwd=$ccMysqlCCPasswd +cloudcoder.db.passwd=$props{'ccMysqlCCPasswd'} cloudcoder.db.databaseName=cloudcoderdb cloudcoder.db.host=localhost cloudcoder.db.portStr= cloudcoder.login.service=database -cloudcoder.submitsvc.oop.host=$ccHostname +cloudcoder.submitsvc.oop.host=$props{'ccHostname'} cloudcoder.submitsvc.oop.numThreads=2 cloudcoder.submitsvc.oop.port=47374 cloudcoder.submitsvc.oop.easysandbox.enable=true @@ -279,7 +278,7 @@ sub Step2 { # Create the cloudcoderdb database # ---------------------------------------------------------------------- section("Creating cloudcoderdb database..."); - Run("java", "-jar", $appJar, "createdb", "--props=$ARGV[0],ccRepoUrl=https://cloudcoder.org/repo"); + Run("java", "-jar", $appJar, "createdb", "--props=" . StringifyProps(',', '=') . ",ccRepoUrl=https://cloudcoder.org/repo"); # ---------------------------------------------------------------------- # Start the webapp! @@ -289,6 +288,28 @@ sub Step2 { } +# Encode %props as a string. +sub StringifyProps { + my ($pairSep,$keyValSep) = @_; + my $s = ''; + + for my $key (sort keys %props) { + if ($s ne '') { + $s .= $pairSep; + } + $s .= "$key$keyValSep$props{$key}"; + } + + return $s; +} + +# Decode a string containing %props: note that this is hard-coded +# to assume that \a (ASCII/Unicode BEL) is used as the separator. +sub UnstringifyProps { + my ($s) = @_; + return split(/\a/, $s); +} + sub ask { my ($question, $defval) = @_; From 63f1ad7562f60fc038b744f3157cf63737d23cd1 Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Sat, 13 Feb 2016 14:44:23 -0500 Subject: [PATCH 003/241] use Getopt::Long, use upper-case idents for subs --- bootstrap.pl | 72 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/bootstrap.pl b/bootstrap.pl index 7898fd5f..6ff5d99e 100755 --- a/bootstrap.pl +++ b/bootstrap.pl @@ -2,7 +2,7 @@ use strict; use FileHandle; -use Getopt::Std; +use Getopt::Long qw{:config bundling no_ignore_case no_auto_abbrev}; # Bootstrap CloudCoder on an Ubuntu server @@ -18,10 +18,18 @@ # Parse command line options my %opts = (); -getopts('n', \%opts); +GetOptions(\%opts, + qw(dry-run|n! + help|h!) +) or (Usage() && exit 0); + +if (exists $opts{'help'}) { + Usage(); + exit 0; +} my $dryRun = 0; -if (exists $opts{'n'}) { +if (exists $opts{'dry-run'}) { print ">>> Dry run <<<\n"; shift @ARGV; $dryRun = 1; @@ -69,31 +77,31 @@ sub Start { concerned if you don't see the prompt. GREET - my $readyToStart = ask("\nReady to start? (yes/no)"); + my $readyToStart = Ask("\nReady to start? (yes/no)"); exit 0 if ((lc $readyToStart) ne 'yes'); print "\nFirst, please enter some configuration information...\n\n"; # Get minimal required configuration information - $props{'ccUser'} = ask("What username do you want for your CloudCoder account?"); - $props{'ccPasswd'} = ask("What password do you want for your CloudCoder account?"); - $props{'ccFirstName'} = ask("What is your first name?"); - $props{'ccLastName'} = ask("What is your last name?"); - $props{'ccEmail'} = ask("What is your email address?"); - $props{'ccWebsite'} = ask("What is the URL of your personal website?"); - $props{'ccInstitutionName'} = ask("What is the name of your institution?"); - $props{'ccMysqlRootPasswd'} = ask("What password do you want for the MySQL root user?"); - $props{'ccMysqlCCPasswd'} = ask("What password do you want for the MySQL cloudcoder user?"); - $props{'ccHostname'} = ask("What is the hostname of this server?"); + $props{'ccUser'} = Ask("What username do you want for your CloudCoder account?"); + $props{'ccPasswd'} = Ask("What password do you want for your CloudCoder account?"); + $props{'ccFirstName'} = Ask("What is your first name?"); + $props{'ccLastName'} = Ask("What is your last name?"); + $props{'ccEmail'} = Ask("What is your email address?"); + $props{'ccWebsite'} = Ask("What is the URL of your personal website?"); + $props{'ccInstitutionName'} = Ask("What is the name of your institution?"); + $props{'ccMysqlRootPasswd'} = Ask("What password do you want for the MySQL root user?"); + $props{'ccMysqlCCPasswd'} = Ask("What password do you want for the MySQL cloudcoder user?"); + $props{'ccHostname'} = Ask("What is the hostname of this server?"); print "\n"; - my $startInstall = ask("Are you ready to start the installation? (yes/no)"); + my $startInstall = Ask("Are you ready to start the installation? (yes/no)"); exit 0 if ((lc $startInstall) ne 'yes'); # ---------------------------------------------------------------------- # Install/configure required packages # ---------------------------------------------------------------------- - section("Installing required packages..."); + Section("Installing required packages..."); # Run apt-get update so that repository metadata is current RunAdmin( @@ -122,7 +130,7 @@ sub Start { # ---------------------------------------------------------------------- # Configure MySQL # ---------------------------------------------------------------------- - section("Configuring MySQL..."); + Section("Configuring MySQL..."); print "Creating cloudcoder user...\n"; Run("mysql", "--user=root", "--pass=$props{'ccMysqlRootPasswd'}", "--execute=create user 'cloudcoder'\@'localhost' identified by '$props{'ccMysqlCCPasswd'}'"); @@ -133,7 +141,7 @@ sub Start { # ---------------------------------------------------------------------- # Create cloud user # ---------------------------------------------------------------------- - section("Creating cloud user account..."); + Section("Creating cloud user account..."); RunAdmin( cmd => [ 'adduser', '--disabled-password', '--home', '/home/cloud', '--gecos', '', 'cloud' ] ); @@ -141,7 +149,7 @@ sub Start { # ---------------------------------------------------------------------- # Configure apache2 # ---------------------------------------------------------------------- - section("Configuring apache2..."); + Section("Configuring apache2..."); print "Generating SSL configuration...\n"; EditApache2DefaultSsl($props{'ccHostname'}); print "Enabling modules...\n"; @@ -155,7 +163,7 @@ sub Start { # Continue as the cloud user to download and configure # webapp and builder jarfiles. # ---------------------------------------------------------------------- - section("Continuing as cloud user..."); + Section("Continuing as cloud user..."); Run("cp", $program, "/tmp/bootstrap.pl"); Run("chmod", "a+x", "/tmp/bootstrap.pl"); RunAdmin(asUser => 'cloud', cmd => ["/tmp/bootstrap.pl", "step2", StringifyProps("\a", "\a")]); @@ -174,7 +182,7 @@ sub Start { # ---------------------------------------------------------------------- # We're done! # ---------------------------------------------------------------------- - section("CloudCoder installation successful!"); + Section("CloudCoder installation successful!"); print <<"SUCCESS"; It looks like CloudCoder was installed successfully. @@ -212,7 +220,7 @@ sub Step2 { my $builderJar = "cloudcoderBuilder-v$version.jar"; # Download webapp and builder release jarfiles - section("Downloading $appJar and $builderJar..."); + Section("Downloading $appJar and $builderJar..."); Run("wget", "$DOWNLOAD_SITE/$appJar"); Run("wget", "$DOWNLOAD_SITE/$builderJar"); @@ -220,7 +228,7 @@ sub Step2 { # Configure webapp distribution jarfile with # generated cloudcoder.properties and keystore # ---------------------------------------------------------------------- - section("Configuring $appJar and $builderJar..."); + Section("Configuring $appJar and $builderJar..."); # Generate cloudcoder.properties print "Creating cloudcoder.properties...\n"; @@ -277,17 +285,27 @@ sub Step2 { # ---------------------------------------------------------------------- # Create the cloudcoderdb database # ---------------------------------------------------------------------- - section("Creating cloudcoderdb database..."); + Section("Creating cloudcoderdb database..."); Run("java", "-jar", $appJar, "createdb", "--props=" . StringifyProps(',', '=') . ",ccRepoUrl=https://cloudcoder.org/repo"); # ---------------------------------------------------------------------- # Start the webapp! # ---------------------------------------------------------------------- - section("Starting the CloudCoder web application"); + Section("Starting the CloudCoder web application"); Run("java", "-jar", $appJar, "start"); } +sub Usage { + print << "USAGE"; +./bootstrap.pl [options] [mode [config props]] + +Options: + -n|--dry-run Do a dry run without executing any commands + -h|--help Print usage information +USAGE +} + # Encode %props as a string. sub StringifyProps { my ($pairSep,$keyValSep) = @_; @@ -310,7 +328,7 @@ sub UnstringifyProps { return split(/\a/, $s); } -sub ask { +sub Ask { my ($question, $defval) = @_; print "$question\n"; @@ -329,7 +347,7 @@ sub ask { return $value; } -sub section { +sub Section { my ($name) = @_; print "\n"; print "#" x 72, "\n"; From dd6f042b88f6e1bab00e530a07295d7819f2e1ba Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Sat, 13 Feb 2016 15:43:16 -0500 Subject: [PATCH 004/241] make apache installation optional Also, fixed bug where ccPassword property wasn't passed to the createdb command (I had renamed it ccPasswd, but CreateWebappDatabase expects it to be called ccPassword.) --- bootstrap.pl | 103 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 19 deletions(-) diff --git a/bootstrap.pl b/bootstrap.pl index 6ff5d99e..383e7b61 100755 --- a/bootstrap.pl +++ b/bootstrap.pl @@ -6,6 +6,15 @@ # Bootstrap CloudCoder on an Ubuntu server +#################################################################### +# Global data +#################################################################### + +# Selectable features (all are enabled by default) +my %features = ( + 'apache' => 1, +); + # Download site my $DOWNLOAD_SITE = 'https://s3.amazonaws.com/cloudcoder-binaries'; @@ -18,9 +27,15 @@ # Parse command line options my %opts = (); + +#################################################################### +# Parse command line, execute +#################################################################### + GetOptions(\%opts, qw(dry-run|n! - help|h!) + help|h! + disable=s) ) or (Usage() && exit 0); if (exists $opts{'help'}) { @@ -35,6 +50,13 @@ $dryRun = 1; } +# See if any features are being disabled +if (exists $opts{'disable'}) { + for my $feature (split(',', $opts{'disable'})) { + $features{$feature} = 0; + } +} + my $mode = 'start'; # See if the mode was specified explicitly @@ -58,6 +80,12 @@ die "Unknown mode: $mode\n"; } +#################################################################### +# Subroutines +#################################################################### + +# Start does all of the sudo commands to install and configure +# software, create the cloud user, etc. sub Start { print <<"GREET"; Welcome to the CloudCoder bootstrap script. @@ -84,7 +112,7 @@ sub Start { # Get minimal required configuration information $props{'ccUser'} = Ask("What username do you want for your CloudCoder account?"); - $props{'ccPasswd'} = Ask("What password do you want for your CloudCoder account?"); + $props{'ccPassword'} = Ask("What password do you want for your CloudCoder account?"); $props{'ccFirstName'} = Ask("What is your first name?"); $props{'ccLastName'} = Ask("What is your last name?"); $props{'ccEmail'} = Ask("What is your email address?"); @@ -118,13 +146,20 @@ sub Start { DebconfSetSelections("mysql-server-$mysqlVersion", "mysql-server/root_password", "password $props{'ccMysqlRootPasswd'}"); DebconfSetSelections("mysql-server-$mysqlVersion", "mysql-server/root_password_again", "password $props{'ccMysqlRootPasswd'}"); - # Install packages. Note that because cloudcoderApp.jar is self-configuring, - # we can install the JRE rather than the full JDK, as we won't need - # the jar tool. + # Install packages. + # We need a full JDK because we use keytool, + # but it can be headless. + my @packages = ("openjdk-7-jre-headless", "mysql-client-$mysqlVersion", "mysql-server-$mysqlVersion"); + if ($features{'apache'}) { + push @packages, 'apache2'; + } + + my @cmd = ("apt-get", "-y", "install"); + push @cmd, @packages; + RunAdmin( env => { 'DEBIAN_FRONTEND' => 'noninteractive' }, - cmd => ["apt-get", "-y", "install", "openjdk-7-jre-headless", "mysql-client-$mysqlVersion", - "mysql-server-$mysqlVersion", "apache2"] + cmd => \@cmd ); # ---------------------------------------------------------------------- @@ -149,15 +184,17 @@ sub Start { # ---------------------------------------------------------------------- # Configure apache2 # ---------------------------------------------------------------------- - Section("Configuring apache2..."); - print "Generating SSL configuration...\n"; - EditApache2DefaultSsl($props{'ccHostname'}); - print "Enabling modules...\n"; - RunAdmin(cmd => ['a2enmod', 'proxy']); - RunAdmin(cmd => ['a2enmod', 'proxy_http']); - RunAdmin(cmd => ['a2enmod', 'ssl']); - print "Restarting...\n"; - RunAdmin(cmd => ['service', 'apache2', 'restart']); + if ($features{'apache'}) { + Section("Configuring apache2..."); + print "Generating SSL configuration...\n"; + EditApache2DefaultSsl($props{'ccHostname'}); + print "Enabling modules...\n"; + RunAdmin(cmd => ['a2enmod', 'proxy']); + RunAdmin(cmd => ['a2enmod', 'proxy_http']); + RunAdmin(cmd => ['a2enmod', 'ssl']); + print "Restarting...\n"; + RunAdmin(cmd => ['service', 'apache2', 'restart']); + } # ---------------------------------------------------------------------- # Continue as the cloud user to download and configure @@ -183,22 +220,45 @@ sub Start { # We're done! # ---------------------------------------------------------------------- Section("CloudCoder installation successful!"); - print <<"SUCCESS"; + print <<"SUCCESS1"; It looks like CloudCoder was installed successfully. +SUCCESS1 + + # If apache was installed, the webapp should be reachable + # via https. Otherwise, only unencrypted HTTP on port + # 8081 is available. + if ($features{'apache'}) { + print <<"SUCCESS2a"; You should be able to test your new installation by opening the following web page: https://$props{'ccHostname'}/cloudcoder +SUCCESS2a + } else { + print <<"SUCCESS2b"; + +You did not install apache (for SSL support). +CloudCoder is listening for unencrypted connections on +port 8081, on localhost only (so connections from outside +will not be accepted.) You should use a proxy server +supporting secure HTTP to make CloudCoder publicly +reachable. +SUCCESS2b + } + + print <<"SUCCESS3"; Note that no builders are running, so you won't be able to test submissions yet. The builder jar file ($builderJar) is in the $home directory: you will need to copy it to the server(s) which will be responsible for building and testing submissions. -SUCCESS +SUCCESS3 } +# Step2 does all of the setup as the cloud user, specifically +# downloading and configuring the CloudCoder webapp and builder. sub Step2 { # Complete the installation running as the cloud user my $whoami = `whoami`; @@ -293,7 +353,6 @@ sub Step2 { # ---------------------------------------------------------------------- Section("Starting the CloudCoder web application"); Run("java", "-jar", $appJar, "start"); - } sub Usage { @@ -303,7 +362,13 @@ sub Usage { Options: -n|--dry-run Do a dry run without executing any commands -h|--help Print usage information + --disable= Disable specified features (comma-separated) + +Selectable features (all enabled by default) are: USAGE + for my $feature (sort keys %features) { + print " $feature\n"; + } } # Encode %props as a string. From ec741a164786c57644558cb85e99b7e7e200c76b Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Sun, 14 Feb 2016 16:34:59 -0500 Subject: [PATCH 005/241] allow noninteractive configuration --- bootstrap.pl | 111 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 40 deletions(-) diff --git a/bootstrap.pl b/bootstrap.pl index 383e7b61..6fa4129c 100755 --- a/bootstrap.pl +++ b/bootstrap.pl @@ -35,8 +35,10 @@ GetOptions(\%opts, qw(dry-run|n! help|h! - disable=s) -) or (Usage() && exit 0); + disable=s + config=s + ) +) or (Usage() && exit 1); if (exists $opts{'help'}) { Usage(); @@ -87,44 +89,11 @@ # Start does all of the sudo commands to install and configure # software, create the cloud user, etc. sub Start { - print <<"GREET"; -Welcome to the CloudCoder bootstrap script. - -By running this script, you will create a basic CloudCoder -installation on a server running Ubuntu Linux. - -Make sure to run this script from a user account that has -permission to run the "sudo" command. If you see the -following prompt: - - sudo password>> - -then you will need to type the account password and press -enter. On some Ubuntu systems, such as Ubuntu server on -Amazon EC2, no password is required for sudo, so don't be -concerned if you don't see the prompt. -GREET - - my $readyToStart = Ask("\nReady to start? (yes/no)"); - exit 0 if ((lc $readyToStart) ne 'yes'); - - print "\nFirst, please enter some configuration information...\n\n"; - - # Get minimal required configuration information - $props{'ccUser'} = Ask("What username do you want for your CloudCoder account?"); - $props{'ccPassword'} = Ask("What password do you want for your CloudCoder account?"); - $props{'ccFirstName'} = Ask("What is your first name?"); - $props{'ccLastName'} = Ask("What is your last name?"); - $props{'ccEmail'} = Ask("What is your email address?"); - $props{'ccWebsite'} = Ask("What is the URL of your personal website?"); - $props{'ccInstitutionName'} = Ask("What is the name of your institution?"); - $props{'ccMysqlRootPasswd'} = Ask("What password do you want for the MySQL root user?"); - $props{'ccMysqlCCPasswd'} = Ask("What password do you want for the MySQL cloudcoder user?"); - $props{'ccHostname'} = Ask("What is the hostname of this server?"); - - print "\n"; - my $startInstall = Ask("Are you ready to start the installation? (yes/no)"); - exit 0 if ((lc $startInstall) ne 'yes'); + if (exists $opts{'config'}) { + LoadConfigProperties($opts{'config'}); + } else { + ConfigureInteractively(); + } # ---------------------------------------------------------------------- # Install/configure required packages @@ -363,12 +332,15 @@ sub Usage { -n|--dry-run Do a dry run without executing any commands -h|--help Print usage information --disable= Disable specified features (comma-separated) + --config= Load configuration from specified properties file + (for noninteractive configuration) Selectable features (all enabled by default) are: USAGE for my $feature (sort keys %features) { print " $feature\n"; } + return 1; } # Encode %props as a string. @@ -393,6 +365,65 @@ sub UnstringifyProps { return split(/\a/, $s); } +# Load %props from a properties file +sub LoadConfigProperties { + my ($fname) = @_; + my $fh = new FileHandle("<$fname") || die "Couldn't open configuration properties file $fname: $!\n"; + while (<$fh>) { + chomp; + next if (/^\s*$/ || /^\s*#/); + if (/^\s*([^=]+)\s*=\s*(.*)$/) { + my $key = $1; + my $val = $2; + $val =~ s/\s+$//g; # trim trailing whitespace, if any + $props{$key} = $val; + } + } + $fh->close(); +} + +# Read %props interactively +sub ConfigureInteractively { + print <<"GREET"; +Welcome to the CloudCoder bootstrap script. + +By running this script, you will create a basic CloudCoder +installation on a server running Ubuntu Linux. + +Make sure to run this script from a user account that has +permission to run the "sudo" command. If you see the +following prompt: + + sudo password>> + +then you will need to type the account password and press +enter. On some Ubuntu systems, such as Ubuntu server on +Amazon EC2, no password is required for sudo, so don't be +concerned if you don't see the prompt. +GREET + + my $readyToStart = Ask("\nReady to start? (yes/no)"); + exit 0 if ((lc $readyToStart) ne 'yes'); + + print "\nFirst, please enter some configuration information...\n\n"; + + # Get minimal required configuration information + $props{'ccUser'} = Ask("What username do you want for your CloudCoder account?"); + $props{'ccPassword'} = Ask("What password do you want for your CloudCoder account?"); + $props{'ccFirstName'} = Ask("What is your first name?"); + $props{'ccLastName'} = Ask("What is your last name?"); + $props{'ccEmail'} = Ask("What is your email address?"); + $props{'ccWebsite'} = Ask("What is the URL of your personal website?"); + $props{'ccInstitutionName'} = Ask("What is the name of your institution?"); + $props{'ccMysqlRootPasswd'} = Ask("What password do you want for the MySQL root user?"); + $props{'ccMysqlCCPasswd'} = Ask("What password do you want for the MySQL cloudcoder user?"); + $props{'ccHostname'} = Ask("What is the hostname of this server?"); + + print "\n"; + my $startInstall = Ask("Are you ready to start the installation? (yes/no)"); + exit 0 if ((lc $startInstall) ne 'yes'); +} + sub Ask { my ($question, $defval) = @_; From 28c88c9a54d33ec0c1f07fda4f86f57e1d320fa8 Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Sun, 14 Feb 2016 16:35:29 -0500 Subject: [PATCH 006/241] generic configuration properties for docker image --- dockerconfig.properties | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 dockerconfig.properties diff --git a/dockerconfig.properties b/dockerconfig.properties new file mode 100644 index 00000000..e5802028 --- /dev/null +++ b/dockerconfig.properties @@ -0,0 +1,15 @@ +# bootstrap.pl configuration properties for building +# a Docker image. These are reasonable defaults, +# but many/most of them will be overridden during +# installation/configuration. + +ccUser=admin +ccPassword=abc123 +ccFirstName=Super +ccLastName=User +ccEmail=super@localhost +ccWebsite=http://localhost/~super +ccInstitutionName=CloudCoder Docker image +ccMysqlRootPasswd=abc123 +ccMysqlCCPasswd=abc123 +ccHostname=localhost From 0b9d23b9609dfb13a4916edfdfc531cfb901ba6d Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Sun, 14 Feb 2016 17:33:01 -0500 Subject: [PATCH 007/241] a couple of changes for building docker image --- bootstrap.pl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bootstrap.pl b/bootstrap.pl index 6fa4129c..a7ee4ae5 100755 --- a/bootstrap.pl +++ b/bootstrap.pl @@ -117,8 +117,9 @@ sub Start { # Install packages. # We need a full JDK because we use keytool, - # but it can be headless. - my @packages = ("openjdk-7-jre-headless", "mysql-client-$mysqlVersion", "mysql-server-$mysqlVersion"); + # but it can be headless. "wget" isn't installed by + # default in the ubuntu docker image. + my @packages = ("wget", "openjdk-7-jre-headless", "mysql-client-$mysqlVersion", "mysql-server-$mysqlVersion"); if ($features{'apache'}) { push @packages, 'apache2'; } @@ -130,6 +131,12 @@ sub Start { env => { 'DEBIAN_FRONTEND' => 'noninteractive' }, cmd => \@cmd ); + + # For some reason, mysqld doesn't seem to start automatically + # when running in a docker container. Kick it. + # This shouldn't cause any harm if it's already running. + RunAdmin(cmd => ['service', 'mysql', 'start']); + Run("sleep", "5"); # ---------------------------------------------------------------------- # Configure MySQL From 7189ea4433747489a8ce1d11ff4830be718491e5 Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Sun, 14 Feb 2016 17:35:03 -0500 Subject: [PATCH 008/241] initial support for building a docker image Not quite there, but we're pretty close to having the bootstrap script execute cleanly from within a docker container. --- .dockerignore | 8 ++++++++ Dockerfile | 15 +++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..2ef62a60 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +# ignore all of the actual CloudCoder source code +CloudCoder*/** + +# ignore downloaded dependencies +deps/** + +# ignore .git repository +.git/** diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..61e4fece --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +# Docker image for running CloudCoder and its dependencies + +FROM ubuntu:trusty +MAINTAINER David Hovemeyer + +# Run from the root user's home directory +WORKDIR /root + +# Use the bootstrap script to install software and configure +# stuff. A generic set of configuration properties is used +# noninteractively. +ADD bootstrap.pl . +ADD dockerconfig.properties . + +#RUN ./bootstrap.pl --disable=apache --config=dockerconfig.properties From 72992767872994da13130f2c446777458bc808e0 Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Sun, 14 Feb 2016 18:14:05 -0500 Subject: [PATCH 009/241] stringified props aren't used for noninteractive config --- bootstrap.pl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bootstrap.pl b/bootstrap.pl index a7ee4ae5..6f6a1095 100755 --- a/bootstrap.pl +++ b/bootstrap.pl @@ -68,8 +68,7 @@ # Assume that any remaining command line option is # stringified config properties (which is what should -# happen if step2 is being executed, or if start is -# being executed noninteractively) +# happen if step2 is being executed). if (scalar(@ARGV) > 0) { %props = UnstringifyProps(shift @ARGV); } From 9ed7abce89a8f5a1e04c02bef38000dbf96cc3a3 Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Sun, 14 Feb 2016 19:26:11 -0500 Subject: [PATCH 010/241] don't shift @ARGV on a dry run This is no longer necessary now that we are using Getopt. --- bootstrap.pl | 1 - 1 file changed, 1 deletion(-) diff --git a/bootstrap.pl b/bootstrap.pl index 6f6a1095..aac3f612 100755 --- a/bootstrap.pl +++ b/bootstrap.pl @@ -48,7 +48,6 @@ my $dryRun = 0; if (exists $opts{'dry-run'}) { print ">>> Dry run <<<\n"; - shift @ARGV; $dryRun = 1; } From 3cf2ebbdfca7ea410777ce0c121cd1cfcbb426b1 Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Mon, 15 Feb 2016 10:07:57 -0500 Subject: [PATCH 011/241] more changes for building docker image --no-localhost-only option to allow webapp to accept connections other than from localhost. Fix for issue where $USER is not set for the root user. Make sure options are passed to step2. --- bootstrap.pl | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/bootstrap.pl b/bootstrap.pl index aac3f612..8879ff4e 100755 --- a/bootstrap.pl +++ b/bootstrap.pl @@ -28,15 +28,23 @@ # Parse command line options my %opts = (); +# Original options +my @origOpts; + #################################################################### # Parse command line, execute #################################################################### +# Save the original argument list. +@origOpts = @ARGV; + GetOptions(\%opts, qw(dry-run|n! help|h! disable=s config=s + no-start! + no-localhost-only! ) ) or (Usage() && exit 1); @@ -58,6 +66,16 @@ } } +# Preserve original options (so that they can be passed to step2.) +# We assume that anything that was not consumed by GetOptions +# is not an option. +my $npop = scalar(@ARGV); +while ($npop-- > 0) { + pop @origOpts; +} +#print "Original options: ", join(' ', @origOpts), "\n"; +#exit 0; + my $mode = 'start'; # See if the mode was specified explicitly @@ -130,7 +148,7 @@ sub Start { cmd => \@cmd ); - # For some reason, mysqld doesn't seem to start automatically + # Mysqld doesn't start automatically # when running in a docker container. Kick it. # This shouldn't cause any harm if it's already running. RunAdmin(cmd => ['service', 'mysql', 'start']); @@ -177,7 +195,7 @@ sub Start { Section("Continuing as cloud user..."); Run("cp", $program, "/tmp/bootstrap.pl"); Run("chmod", "a+x", "/tmp/bootstrap.pl"); - RunAdmin(asUser => 'cloud', cmd => ["/tmp/bootstrap.pl", "step2", StringifyProps("\a", "\a")]); + RunAdmin(asUser => 'cloud', cmd => ["/tmp/bootstrap.pl", @origOpts, "step2", StringifyProps("\a", "\a")]); # ---------------------------------------------------------------------- # Copy the configured builder jarfile into the home directory of the current user. @@ -185,11 +203,20 @@ sub Start { my $version = GetLatestVersion(); my $builderJar = "cloudcoderBuilder-v$version.jar"; my $home = $ENV{'HOME'}; - my $user = $ENV{'USER'}; + my $user = `whoami`; + chomp $user; print "Copying configured builder jarfile into $home...\n"; RunAdmin(cmd => ["cp", "/home/cloud/webapp/$builderJar", $home]); RunAdmin(cmd => ["chown", $user, "$builderJar"]); + # ---------------------------------------------------------------------- + # Shut down mysqld if --no-start was used. + # (Typically, this is because we're building a docker image.) + # ---------------------------------------------------------------------- + if (exists $opts{'no-start'}) { + RunAdmin(cmd => ["service", "mysql", "stop"]); + } + # ---------------------------------------------------------------------- # We're done! # ---------------------------------------------------------------------- @@ -266,6 +293,7 @@ sub Step2 { # Generate cloudcoder.properties print "Creating cloudcoder.properties...\n"; + my $localhostOnly = (exists $opts{'no-localhost-only'}) ? 'false' : 'true'; my $pfh = new FileHandle(">cloudcoder.properties"); print $pfh <<"ENDPROPERTIES"; cloudcoder.db.user=cloudcoder @@ -284,7 +312,7 @@ sub Step2 { cloudcoder.submitsvc.ssl.keystore.password=changeit cloudcoder.webserver.port=8081 cloudcoder.webserver.contextpath=/cloudcoder -cloudcoder.webserver.localhostonly=true +cloudcoder.webserver.localhostonly=$localhostOnly ENDPROPERTIES $pfh->close(); @@ -325,8 +353,10 @@ sub Step2 { # ---------------------------------------------------------------------- # Start the webapp! # ---------------------------------------------------------------------- - Section("Starting the CloudCoder web application"); - Run("java", "-jar", $appJar, "start"); + if (!exists $opts{'no-start'}) { + Section("Starting the CloudCoder web application"); + Run("java", "-jar", $appJar, "start"); + } } sub Usage { @@ -339,6 +369,9 @@ sub Usage { --disable= Disable specified features (comma-separated) --config= Load configuration from specified properties file (for noninteractive configuration) + --no-start Don't start the webapp + --no-localhost-only Allow webapp to accept unencrypted HTTP connections + from anywhere (not just localhost) Selectable features (all enabled by default) are: USAGE @@ -484,6 +517,7 @@ sub RunAdmin { print "cmd: ", join(' ', @cmd), "\n"; $result = 1; } else { + print "Running admin command: ", join(' ', @cmd), "\n"; $result = system(@cmd)/256 == 0; } From 3700fb105f7046fcb5a97428f037f1ba924eb5ae Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Mon, 15 Feb 2016 11:40:39 -0500 Subject: [PATCH 012/241] Don't start the webapp, allow outside connections --- Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 61e4fece..91ad53f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,4 +12,8 @@ WORKDIR /root ADD bootstrap.pl . ADD dockerconfig.properties . -#RUN ./bootstrap.pl --disable=apache --config=dockerconfig.properties +RUN ./bootstrap.pl \ + --disable=apache \ + --config=dockerconfig.properties \ + --no-start \ + --no-localhost-only From b8b3b2b78eccd5bfea74edf484d5c171631860b3 Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Mon, 15 Feb 2016 15:32:05 -0500 Subject: [PATCH 013/241] added dockerrun.pl entry point script The entry point script starts mysqld and the CloudCoder webapp, waits for SIGTERM, and then shuts down the CloudCoder webapp and mysqld cleanly. At this point we seem to be getting fairly close to having a useful containerized CloudCoder webapp. More work needs to be done to allow post-deployment customization. --- Dockerfile | 25 +++++++++++++++++++------ dockerrun.pl | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 6 deletions(-) create mode 100755 dockerrun.pl diff --git a/Dockerfile b/Dockerfile index 91ad53f4..b9a41728 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,17 @@ -# Docker image for running CloudCoder and its dependencies +# Docker image for running CloudCoder and MySQL. +# The webapp will listen for unencrypted HTTP connections +# on port 8081. So, you should run it something +# like the following: +# +# docker run -d -p 8081:8081 -p 47374:47374 cloudcoder FROM ubuntu:trusty MAINTAINER David Hovemeyer +# Webapp will listen for HTTP connections on port 8081, +# and builder connections on port 47374. +EXPOSE 8081 47374 + # Run from the root user's home directory WORKDIR /root @@ -11,9 +20,13 @@ WORKDIR /root # noninteractively. ADD bootstrap.pl . ADD dockerconfig.properties . - +ADD dockerrun.pl . RUN ./bootstrap.pl \ - --disable=apache \ - --config=dockerconfig.properties \ - --no-start \ - --no-localhost-only + --disable=apache \ + --config=dockerconfig.properties \ + --no-start \ + --no-localhost-only + +# The dockerrun.pl script starts mysql and the CloudCoder webapp, +# and shuts them down cleanly when a SIGTERM is received. +ENTRYPOINT ["./dockerrun.pl"] diff --git a/dockerrun.pl b/dockerrun.pl new file mode 100755 index 00000000..5c8c5e80 --- /dev/null +++ b/dockerrun.pl @@ -0,0 +1,49 @@ +#! /usr/bin/perl -w + +use strict; +use POSIX qw(pause); +use IO::Handle; + +STDOUT->autoflush(1); +STDERR->autoflush(1); + +print "Starting CloudCoder docker container...\n"; + +# Run CloudCoder (and mysql) in Docker, attempting to shut +# both down gracefully when a SIGTERM is received. + +my $done = 0; + +$SIG{TERM} = \&SigtermHandler; + +# Start mysqld and CloudCoder. +Run("service", "mysql", "start"); +Run("sudo", "-u", "cloud", "/bin/bash", "-c", + "cd /home/cloud/webapp && java -jar cloudcoderApp-v*.jar start"); + +print "mysqld and CloudCoder webapp are running...\n"; + +# Wait for SIGTERM. +# Note that there is a race here if SIGTERM arrives +# after $done is checked but before pause() is executed. +if (!$done) { + pause(); +} +print "SIGTERM received, shutting down...\n"; + +# Shut down CloudCoder and mysqld. +Run("sudo", "-u", "cloud", "/bin/bash", "-c", + "cd /home/cloud/webapp && java -jar cloudcoderApp-v*.jar shutdown"); +Run("service", "mysql", "stop"); + +print "Done\n"; + +sub SigtermHandler { + $done = 1; +} + +sub Run { + system(@_)/256 == 0 || die "Command $_[0] failed\n"; +} + +# vim:ts=2: From 72f96333ee4f8b54ac4c0e8627e935962a60f956 Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Wed, 17 Feb 2016 10:14:05 -0500 Subject: [PATCH 014/241] started admin UI for editing ConfigurationSettings --- .../client/page/CoursesAndProblemsPage3.java | 15 +- .../app/client/page/SessionUtil.java | 80 +++++++++++ .../view/EditConfigurationSettingsPanel.java | 131 ++++++++++++++++++ 3 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 CloudCoder/src/org/cloudcoder/app/client/view/EditConfigurationSettingsPanel.java diff --git a/CloudCoder/src/org/cloudcoder/app/client/page/CoursesAndProblemsPage3.java b/CloudCoder/src/org/cloudcoder/app/client/page/CoursesAndProblemsPage3.java index e2a616a6..beeaf007 100644 --- a/CloudCoder/src/org/cloudcoder/app/client/page/CoursesAndProblemsPage3.java +++ b/CloudCoder/src/org/cloudcoder/app/client/page/CoursesAndProblemsPage3.java @@ -30,6 +30,7 @@ import org.cloudcoder.app.client.view.CourseSelectionListBox; import org.cloudcoder.app.client.view.CreateCoursePanel; import org.cloudcoder.app.client.view.DebugPopupPanel; +import org.cloudcoder.app.client.view.EditConfigurationSettingsPanel; import org.cloudcoder.app.client.view.ExerciseAdminPanel; import org.cloudcoder.app.client.view.ExerciseSummaryView; import org.cloudcoder.app.client.view.ISelectableComposite; @@ -126,6 +127,7 @@ private class UI extends Composite implements SessionObserver, Subscriber { private TabLayoutPanel tabLayoutPanel; private List tabIdList; private CreateCoursePanel createCoursePanel; + private EditConfigurationSettingsPanel editConfigurationSettingsPanel; private boolean manageUsersTabCreated; private boolean manageExercisesTabCreated; private ModuleListBox moduleListBox; @@ -369,7 +371,17 @@ public void run() { }); accordionPanel.add(createCoursePanel, "Create course"); - // Could put other widgets in the accordion panel here... + // Add edit configuration settings panel + this.editConfigurationSettingsPanel = new EditConfigurationSettingsPanel(CoursesAndProblemsPage3.this); + editConfigurationSettingsPanel.setOnUpdateCallback(new Runnable() { + @Override + public void run() { + if (editConfigurationSettingsPanel.validate()) { + getSession().add(StatusMessage.information("Should be updating configuration settings")); + } + } + }); + accordionPanel.add(editConfigurationSettingsPanel, "Edit configuration settings"); return accordionPanel; } @@ -522,6 +534,7 @@ public void activate(Session session, SubscriptionRegistrar subscriptionRegistra if (user.isSuperuser()) { addTab(createAdminTab(), "Admin", TabId.ADMIN); createCoursePanel.activate(session, subscriptionRegistrar); + editConfigurationSettingsPanel.activate(session, subscriptionRegistrar); } // Load courses diff --git a/CloudCoder/src/org/cloudcoder/app/client/page/SessionUtil.java b/CloudCoder/src/org/cloudcoder/app/client/page/SessionUtil.java index 68c2fd68..c2ff5def 100644 --- a/CloudCoder/src/org/cloudcoder/app/client/page/SessionUtil.java +++ b/CloudCoder/src/org/cloudcoder/app/client/page/SessionUtil.java @@ -17,10 +17,16 @@ package org.cloudcoder.app.client.page; +import java.util.ArrayList; +import java.util.List; + import org.cloudcoder.app.client.model.Session; import org.cloudcoder.app.client.model.StatusMessage; +import org.cloudcoder.app.client.rpc.ConfigurationSettingService; import org.cloudcoder.app.client.rpc.RPC; import org.cloudcoder.app.shared.model.CloudCoderAuthenticationException; +import org.cloudcoder.app.shared.model.ConfigurationSetting; +import org.cloudcoder.app.shared.model.ConfigurationSettingName; import org.cloudcoder.app.shared.model.Course; import org.cloudcoder.app.shared.model.CourseAndCourseRegistration; import org.cloudcoder.app.shared.model.CourseRegistration; @@ -581,4 +587,78 @@ public void onSuccess(Boolean result) { } }.execute(); } + + /** + * Get all {@link ConfigurationSetting}s and add them to the {@link Session}. + * + * @author David Hovemeyer + */ + private static class ConfigurationSettingsGetter { + private int index; + private List values; + private Session session; + private static final ConfigurationSettingName[] names = ConfigurationSettingName.values(); + + public ConfigurationSettingsGetter(Session session) { + this(0, new ArrayList(), session); + } + + public ConfigurationSettingsGetter(int index, List values, Session session) { + this.index = index; + this.values = values; + this.session = session; + } + + public void execute() { + if (index >= names.length) { + // All configuration settings have been received, + // so we can add the results to the session. + ConfigurationSetting[] result = new ConfigurationSetting[names.length]; + for (int i = 0; i < names.length; i++) { + result[i] = new ConfigurationSetting(); + result[i].setName(names[i]); + result[i].setValue(values.get(i)); + } + session.add(result); + } else { + GWT.log("Loading configuration setting " + names[index]); + RPC.configurationSettingService.getConfigurationSettingValue(names[index], new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + // Hmm... + session.add(StatusMessage.error("Could not load configuration property " + names[index], caught)); + values.add(null); + loadNext(); + } + + @Override + public void onSuccess(String result) { + GWT.log("Configuration setting " + names[index] + "=" + result); + values.add(result); + loadNext(); + } + }); + } + } + + protected void loadNext() { + new ConfigurationSettingsGetter(index + 1, values, session).execute(); + } + } + + private static final RunOnce loadAllConfigurationSettingsRunner = new RunOnce(); + + /** + * Load all {@link ConfigurationSetting}s from the server. + * + * @param page the {@link CloudCoderPage} requesting the {@link ConfigurationSetting}s + */ + public static void loadAllConfigurationSettings(final CloudCoderPage page) { + new OneTimeRunnable(loadAllConfigurationSettingsRunner) { + @Override + public void run() { + new ConfigurationSettingsGetter(page.getSession()).execute(); + } + }.execute(); + } } diff --git a/CloudCoder/src/org/cloudcoder/app/client/view/EditConfigurationSettingsPanel.java b/CloudCoder/src/org/cloudcoder/app/client/view/EditConfigurationSettingsPanel.java new file mode 100644 index 00000000..9b066f0f --- /dev/null +++ b/CloudCoder/src/org/cloudcoder/app/client/view/EditConfigurationSettingsPanel.java @@ -0,0 +1,131 @@ +// CloudCoder - a web-based pedagogical programming environment +// Copyright (C) 2011-2016, Jaime Spacco +// Copyright (C) 2011-2016, David H. Hovemeyer +// +// 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 . + +package org.cloudcoder.app.client.view; + +import java.util.ArrayList; +import java.util.List; + +import org.cloudcoder.app.client.model.Session; +import org.cloudcoder.app.client.model.StatusMessage; +import org.cloudcoder.app.client.page.CloudCoderPage; +import org.cloudcoder.app.client.page.SessionObserver; +import org.cloudcoder.app.client.page.SessionUtil; +import org.cloudcoder.app.client.validator.NoopFieldValidator; +import org.cloudcoder.app.client.validator.TextBoxNonemptyValidator; +import org.cloudcoder.app.shared.model.ConfigurationSetting; +import org.cloudcoder.app.shared.model.ConfigurationSettingName; +import org.cloudcoder.app.shared.model.ICallback; +import org.cloudcoder.app.shared.util.Publisher; +import org.cloudcoder.app.shared.util.Subscriber; +import org.cloudcoder.app.shared.util.SubscriptionRegistrar; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.TextBox; + +/** + * Admin UI for editing {@link ConfigurationSetting}s. + * This is useful for setting, e.g., the institution name. + * + * @author David Hovemeyer + */ +public class EditConfigurationSettingsPanel extends ValidatedFormUI implements SessionObserver, Subscriber { + private CloudCoderPage page; + private ConfigurationSettingName[] names; + private List currentValues; + private List textBoxes; + private Button updateButton; + private Runnable onUpdateCallback; + + public EditConfigurationSettingsPanel(CloudCoderPage page) { + this.page = page; + + setWidth("100%"); + setHeight("200px"); + + double y = 10.0; + + this.names = ConfigurationSettingName.values(); + this.currentValues = new ArrayList(); + this.textBoxes = new ArrayList(); + for (ConfigurationSettingName name : names) { + TextBox textBox = new TextBox(); + textBoxes.add(textBox); + y = addWidget(y, textBox, name.toString(), new TextBoxNonemptyValidator()); + + // All textboxes will be disabled until the current values + // are loaded successfully + textBox.setEnabled(false); + } + + this.updateButton = new Button("Update settings"); + y = addWidget(y, updateButton, "", new NoopFieldValidator()); + this.updateButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (onUpdateCallback != null) { + onUpdateCallback.run(); + } + } + }); + } + + /** + * Set the callback to run when the "Update settings" button is clicked. + * + * @param onUpdateCallback the onUpdateCallback to set + */ + public void setOnUpdateCallback(Runnable onUpdateCallback) { + this.onUpdateCallback = onUpdateCallback; + } + + @Override + public void activate(Session session, SubscriptionRegistrar subscriptionRegistrar) { + session.subscribe(Session.Event.ADDED_OBJECT, this, subscriptionRegistrar); + + // Force configuration settings to be loaded + SessionUtil.loadAllConfigurationSettings(page); + } + + @Override + public void eventOccurred(Object key, Publisher publisher, Object hint) { + if (hint instanceof ConfigurationSetting[]) { + currentValues.clear(); + // These are guaranteed to be ordered in the order of + // the ConfigurationSettingName enum, which is how the + // textboxes are ordered as well. + ConfigurationSetting[] settings = (ConfigurationSetting[]) hint; + for (int i = 0; i < names.length; i++) { + boolean valid = settings[i] != null; + String value = valid ? settings[i].getValue() : ""; + TextBox textBox = textBoxes.get(i); + currentValues.add(value); + textBox.setText(value); + textBox.setEnabled(valid); + } + } + } + + @Override + public void clear() { + // Do nothing - in general, there is no harm in leaving the values + // in the UI + } + +} From 0f0937554df2f5e9e13e9b8b76a1f606c5875083 Mon Sep 17 00:00:00 2001 From: David Hovemeyer Date: Wed, 17 Feb 2016 10:14:27 -0500 Subject: [PATCH 015/241] updated copyright year in header comment template --- CloudCoder/.settings/org.eclipse.jdt.ui.prefs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CloudCoder/.settings/org.eclipse.jdt.ui.prefs b/CloudCoder/.settings/org.eclipse.jdt.ui.prefs index 57bb96ee..ed24dfc8 100644 --- a/CloudCoder/.settings/org.eclipse.jdt.ui.prefs +++ b/CloudCoder/.settings/org.eclipse.jdt.ui.prefs @@ -1,3 +1,3 @@ eclipse.preferences.version=1 org.eclipse.jdt.ui.javadoc=true -org.eclipse.jdt.ui.text.custom_code_templates=