diff --git a/doc/manual/installation.xml b/doc/manual/installation.xml index a1c410d5..4d1c6b2a 100644 --- a/doc/manual/installation.xml +++ b/doc/manual/installation.xml @@ -163,15 +163,16 @@ hydra-init - To add a user root with - admin privileges, execute: - -echo "INSERT INTO Users(userName, emailAddress, password) VALUES ('root', 'some@email.adress.com', '$(echo -n foobar | sha1sum | cut -c1-40)');" | psql hydra -echo "INSERT INTO UserRoles(userName, role) values('root', 'admin');" | psql hydra + To create projects, you need to create a user with + admin privileges. This can be done using + the command hydra-create-user: - For SQLite the same commands can be used, with psql - hydra replaced by sqlite3 - /path/to/hydra.sqlite. + +$ hydra-create-user alice --full-name 'Alice Q. User' \ + --email-address 'alice@example.org' --password foobar --role admin + + + Additional users can be created through the web interface. diff --git a/hydra-module.nix b/hydra-module.nix index 1b8d8663..8034ef77 100644 --- a/hydra-module.nix +++ b/hydra-module.nix @@ -9,28 +9,31 @@ let hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig; - env = - { NIX_REMOTE = "daemon"; - HYDRA_DBI = cfg.dbi; + hydraEnv = + { HYDRA_DBI = cfg.dbi; HYDRA_CONFIG = "${baseDir}/data/hydra.conf"; HYDRA_DATA = "${baseDir}/data"; - HYDRA_PORT = "${toString cfg.port}"; + }; + + env = + { NIX_REMOTE = "daemon"; OPENSSL_X509_CERT_FILE = "/etc/ssl/certs/ca-bundle.crt"; GIT_SSL_CAINFO = "/etc/ssl/certs/ca-bundle.crt"; - }; - + } // hydraEnv; + serverEnv = env // - { HYDRA_LOGO = if cfg.logo != null then cfg.logo else ""; - HYDRA_TRACKER = cfg.tracker; + { HYDRA_TRACKER = cfg.tracker; } // (optionalAttrs cfg.debugServer { DBIC_TRACE = 1; }); in { ###### interface options = { + services.hydra = rec { enable = mkOption { + type = types.bool; default = false; description = '' Whether to run Hydra services. @@ -38,27 +41,29 @@ in }; dbi = mkOption { - default = "dbi:Pg:dbname=hydra;host=localhost;user=hydra;"; - example = "dbi:SQLite:/home/hydra/db/hydra.sqlite"; + type = types.string; + default = "dbi:Pg:dbname=hydra;user=hydra;"; + example = "dbi:Pg:dbname=hydra;host=postgres.example.org;user=foo;"; description = '' The DBI string for Hydra database connection. ''; }; - hydra = mkOption { + package = mkOption { + type = types.path; #default = pkgs.hydra; - description = '' - Location of hydra - ''; + description = "The Hydra package."; }; hydraURL = mkOption { + type = types.str; description = '' The base URL for the Hydra webserver instance. Used for links in emails. ''; }; listenHost = mkOption { + type = types.str; default = "*"; example = "localhost"; description = '' @@ -68,6 +73,7 @@ in }; port = mkOption { + type = types.int; default = 3000; description = '' TCP port the web server should listen to. @@ -75,26 +81,30 @@ in }; minimumDiskFree = mkOption { + type = types.int; default = 5; description = '' - Threshold of minimum disk space (G) to determine if queue runner should run or not. + Threshold of minimum disk space (GiB) to determine if queue runner should run or not. ''; }; minimumDiskFreeEvaluator = mkOption { + type = types.int; default = 2; description = '' - Threshold of minimum disk space (G) to determine if evaluator should run or not. + Threshold of minimum disk space (GiB) to determine if evaluator should run or not. ''; }; notificationSender = mkOption { + type = types.str; description = '' Sender email address used for email notifications. ''; }; tracker = mkOption { + type = types.str; default = ""; description = '' Piece of HTML that is included on all pages. @@ -102,22 +112,16 @@ in }; logo = mkOption { + type = types.nullOr types.path; default = null; description = '' File name of an alternate logo to be displayed on the web pages. ''; }; - useWAL = mkOption { - default = true; - description = '' - Whether to use SQLite's Write-Ahead Logging, which may improve performance. - ''; - }; - debugServer = mkOption { - default = false; type = types.bool; + default = false; description = "Whether to run the server in debug mode"; }; @@ -134,15 +138,21 @@ in ###### implementation config = mkIf cfg.enable { + services.hydra.extraConfig = '' using_frontend_proxy 1 base_uri ${cfg.hydraURL} notification_sender ${cfg.notificationSender} max_servers 25 + ${optionalString (cfg.logo != null) '' + hydra_logo ${cfg.logo} + ''} ''; - environment.systemPackages = [ cfg.hydra ]; + environment.systemPackages = [ cfg.package ]; + + environment.variables = hydraEnv; users.extraUsers.hydra = { description = "Hydra"; @@ -163,12 +173,10 @@ in build-cache-failure = true build-poll-interval = 10 - + # Online log compression makes it impossible to get the tail of # builds that are in progress. build-compress-log = false - - use-sqlite-wal = ${if cfg.useWAL then "true" else "false"} ''; systemd.services."hydra-init" = @@ -177,41 +185,32 @@ in after = [ "postgresql.service" ]; environment = env; script = '' - mkdir -p ${baseDir}/data + mkdir -m 0700 -p ${baseDir}/data chown hydra ${baseDir}/data ln -sf ${hydraConf} ${baseDir}/data/hydra.conf - pass=$(HOME=/root ${pkgs.openssl}/bin/openssl rand -base64 32) - if [ ! -f ${baseDir}/.pgpass ]; then - ${config.services.postgresql.package}/bin/psql postgres << EOF - CREATE USER hydra PASSWORD '$pass'; - EOF + ${optionalString (cfg.dbi == "dbi:Pg:dbname=hydra;user=hydra;") '' + if ! [ -e ${baseDir}/.db-created ]; then + ${config.services.postgresql.package}/bin/createuser hydra ${config.services.postgresql.package}/bin/createdb -O hydra hydra - cat > ${baseDir}/.pgpass-tmp << EOF - localhost:*:hydra:hydra:$pass - EOF - chown hydra ${baseDir}/.pgpass-tmp - chmod 600 ${baseDir}/.pgpass-tmp - mv ${baseDir}/.pgpass-tmp ${baseDir}/.pgpass - fi - ${pkgs.shadow}/bin/su hydra -c ${cfg.hydra}/bin/hydra-init - ${config.services.postgresql.package}/bin/psql hydra << EOF - BEGIN; - INSERT INTO Users(userName, emailAddress, password) VALUES ('admin', '${cfg.notificationSender}', '$(echo -n $pass | sha1sum | cut -c1-40)'); - INSERT INTO UserRoles(userName, role) values('admin', 'admin'); - COMMIT; - EOF + touch ${baseDir}/.db-created + fi + ''} + ${pkgs.shadow}/bin/su hydra -c ${cfg.package}/bin/hydra-init ''; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; }; - + systemd.services."hydra-server" = { wantedBy = [ "multi-user.target" ]; - wants = [ "hydra-init.service" ]; + requires = [ "hydra-init.service" ]; after = [ "hydra-init.service" ]; environment = serverEnv; serviceConfig = - { ExecStart = "@${cfg.hydra}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' --max_spare_servers 5 --max_servers 25 --max_requests 100${optionalString cfg.debugServer " -d"}"; + { ExecStart = + "@${cfg.package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' " + + "-p ${toString cfg.port} --max_spare_servers 5 --max_servers 25 " + + "--max_requests 100 ${optionalString cfg.debugServer "-d"}"; User = "hydra"; Restart = "always"; }; @@ -219,13 +218,13 @@ in systemd.services."hydra-queue-runner" = { wantedBy = [ "multi-user.target" ]; - wants = [ "hydra-init.service" ]; + requires = [ "hydra-init.service" ]; after = [ "hydra-init.service" "network.target" ]; path = [ pkgs.nettools ]; environment = env; serviceConfig = - { ExecStartPre = "${cfg.hydra}/bin/hydra-queue-runner --unlock"; - ExecStart = "@${cfg.hydra}/bin/hydra-queue-runner hydra-queue-runner"; + { ExecStartPre = "${cfg.package}/bin/hydra-queue-runner --unlock"; + ExecStart = "@${cfg.package}/bin/hydra-queue-runner hydra-queue-runner"; User = "hydra"; Restart = "always"; }; @@ -233,25 +232,26 @@ in systemd.services."hydra-evaluator" = { wantedBy = [ "multi-user.target" ]; - wants = [ "hydra-init.service" ]; + requires = [ "hydra-init.service" ]; after = [ "hydra-init.service" "network.target" ]; path = [ pkgs.nettools ]; environment = env; serviceConfig = - { ExecStart = "@${cfg.hydra}/bin/hydra-evaluator hydra-evaluator"; + { ExecStart = "@${cfg.package}/bin/hydra-evaluator hydra-evaluator"; User = "hydra"; Restart = "always"; }; }; systemd.services."hydra-update-gc-roots" = - { wants = [ "hydra-init.service" ]; + { requires = [ "hydra-init.service" ]; after = [ "hydra-init.service" ]; environment = env; serviceConfig = - { ExecStart = "@${cfg.hydra}/bin/hydra-update-gc-roots hydra-update-gc-roots"; + { ExecStart = "@${cfg.package}/bin/hydra-update-gc-roots hydra-update-gc-roots"; User = "hydra"; }; + startAt = "02:15"; }; services.cron.systemCronJobs = @@ -278,7 +278,6 @@ in in [ "*/5 * * * * root ${checkSpace} &> ${baseDir}/data/checkspace.log" "15 5 * * * root ${compressLogs} &> ${baseDir}/data/compress.log" - "15 2 * * * root ${pkgs.systemd}/bin/systemctl start hydra-update-gc-roots.service" ]; }; } diff --git a/release.nix b/release.nix index 2be29519..6004c88d 100644 --- a/release.nix +++ b/release.nix @@ -8,6 +8,23 @@ let genAttrs' = pkgs.lib.genAttrs [ "x86_64-linux" ]; + hydraServer = hydraPkg: + { config, pkgs, ... }: + { imports = [ ./hydra-module.nix ]; + + virtualisation.memorySize = 1024; + + services.hydra.enable = true; + services.hydra.package = hydraPkg; + services.hydra.hydraURL = "http://hydra.example.org"; + services.hydra.notificationSender = "admin@hydra.example.org"; + + services.postgresql.enable = true; + services.postgresql.package = pkgs.postgresql92; + + environment.systemPackages = [ pkgs.perlPackages.LWP pkgs.perlPackages.JSON ]; + }; + in rec { tarball = @@ -150,67 +167,45 @@ in rec { tests.install = genAttrs' (system: with import { inherit system; }; - let hydra = builtins.getAttr system build; in # build.${system} simpleTest { - machine = - { config, pkgs, ... }: - { services.postgresql.enable = true; - services.postgresql.package = pkgs.postgresql92; - environment.systemPackages = [ hydra ]; - }; - + machine = hydraServer (builtins.getAttr system build); # build.${system} testScript = '' - $machine->waitForJob("postgresql"); - - # Initialise the database and the state. - $machine->mustSucceed - ( "createdb -O root hydra", - , "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql" - , "mkdir /var/lib/hydra" - ); - - # Start the web interface. - $machine->mustSucceed("HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=hydra;' hydra-server >&2 &"); + $machine->waitForJob("hydra-init"); + $machine->waitForJob("hydra-server"); + $machine->waitForJob("hydra-evaluator"); + $machine->waitForJob("hydra-queue-runner"); $machine->waitForOpenPort("3000"); + $machine->succeed("curl --fail http://localhost:3000/"); ''; }); tests.api = genAttrs' (system: with import { inherit system; }; - let hydra = builtins.getAttr system build; in # build."${system}" simpleTest { - machine = - { config, pkgs, ... }: - { services.postgresql.enable = true; - services.postgresql.package = pkgs.postgresql92; - environment.systemPackages = [ hydra pkgs.perlPackages.LWP pkgs.perlPackages.JSON ]; - virtualisation.memorySize = 2047; - boot.kernelPackages = pkgs.linuxPackages_3_10; - }; - + machine = hydraServer (builtins.getAttr system build); # build.${system} testScript = + let dbi = "dbi:Pg:dbname=hydra;user=root;"; in '' - $machine->waitForJob("postgresql"); + $machine->waitForJob("hydra-init"); - # Initialise the database and the state. - $machine->mustSucceed - ( "createdb -O root hydra" - , "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql" - , "mkdir /var/lib/hydra" - , "echo \"insert into Users(userName, emailAddress, password) values('root', 'e.dolstra\@tudelft.nl', '\$(echo -n foobar | sha1sum | cut -c1-40)');\" | psql hydra" - , "echo \"insert into UserRoles(userName, role) values('root', 'admin');\" | psql hydra" - , "mkdir /run/jobset" - , "chmod 755 /run/jobset" + # Create an admin account and some other state. + $machine->succeed + ( "su hydra -c \"hydra-create-user root --email-address 'e.dolstra\@tudelft.nl' --password foobar --role admin\"" + , "mkdir /run/jobset /tmp/nix" + , "chmod 755 /run/jobset /tmp/nix" , "cp ${./tests/api-test.nix} /run/jobset/default.nix" , "chmod 644 /run/jobset/default.nix" + , "chown -R hydra /run/jobset /tmp/nix" ); - # Start the web interface. - $machine->mustSucceed("NIX_STORE_DIR=/run/nix NIX_LOG_DIR=/run/nix/var/log/nix NIX_STATE_DIR=/run/nix/var/nix HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' LOGNAME=root DBIC_TRACE=1 hydra-server -d >&2 &"); + # Start the web interface with some weird settings. + $machine->succeed("systemctl stop hydra-server hydra-evaluator hydra-queue-runner"); + $machine->mustSucceed("su hydra -c 'NIX_STORE_DIR=/tmp/nix/store NIX_LOG_DIR=/tmp/nix/var/log/nix NIX_STATE_DIR=/tmp/nix/var/nix DBIC_TRACE=1 hydra-server -d' >&2 &"); $machine->waitForOpenPort("3000"); - $machine->mustSucceed("perl ${./tests/api-test.pl} >&2"); + # Run the API tests. + $machine->mustSucceed("su hydra -c 'perl ${./tests/api-test.pl}' >&2"); ''; }); @@ -236,7 +231,7 @@ in rec { $machine->waitForJob("postgresql"); # Initialise the database and the state. - $machine->mustSucceed + $machine->succeed ( "createdb -O root hydra" , "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql" , "mkdir /var/lib/hydra" @@ -246,10 +241,10 @@ in rec { ); # start fakes3 - $machine->mustSucceed("fakes3 --root /tmp/s3 --port 80 &>/dev/null &"); + $machine->succeed("fakes3 --root /tmp/s3 --port 80 &>/dev/null &"); $machine->waitForOpenPort("80"); - $machine->mustSucceed("cd /tmp && LOGNAME=root AWS_ACCESS_KEY_ID=foo AWS_SECRET_ACCESS_KEY=bar HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' HYDRA_CONFIG=${./tests/s3-backup-test.config} perl -I ${hydra}/libexec/hydra/lib -I ${hydra.perlDeps}/lib/perl5/site_perl ./s3-backup-test.pl >&2"); + $machine->succeed("cd /tmp && LOGNAME=root AWS_ACCESS_KEY_ID=foo AWS_SECRET_ACCESS_KEY=bar HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' HYDRA_CONFIG=${./tests/s3-backup-test.config} perl -I ${hydra}/libexec/hydra/lib -I ${hydra.perlDeps}/lib/perl5/site_perl ./s3-backup-test.pl >&2"); ''; }); } diff --git a/src/lib/Hydra.pm b/src/lib/Hydra.pm index 42da5f65..6bef5cd8 100644 --- a/src/lib/Hydra.pm +++ b/src/lib/Hydra.pm @@ -53,8 +53,9 @@ __PACKAGE__->config( expose_stash => 'json' }, 'Plugin::Session' => { - expires => 3600 * 24 * 2, - storage => Hydra::Model::DB::getHydraPath . "/session_data" + expires => 3600 * 24 * 7, + storage => Hydra::Model::DB::getHydraPath . "/session_data", + unlink_on_exit => 0 }, 'Plugin::AccessLog' => { formatter => { diff --git a/src/lib/Hydra/Controller/Root.pm b/src/lib/Hydra/Controller/Root.pm index fc88d16c..ad34ae40 100644 --- a/src/lib/Hydra/Controller/Root.pm +++ b/src/lib/Hydra/Controller/Root.pm @@ -20,10 +20,11 @@ sub begin :Private { $c->stash->{version} = $ENV{"HYDRA_RELEASE"} || ""; $c->stash->{nixVersion} = $ENV{"NIX_RELEASE"} || ""; $c->stash->{curTime} = time; - $c->stash->{logo} = $ENV{"HYDRA_LOGO"} ? "/logo" : ""; + $c->stash->{logo} = ($c->config->{hydra_logo} // $ENV{"HYDRA_LOGO"}) ? "/logo" : ""; $c->stash->{tracker} = $ENV{"HYDRA_TRACKER"}; $c->stash->{flashMsg} = $c->flash->{flashMsg}; $c->stash->{successMsg} = $c->flash->{successMsg}; + $c->stash->{personaEnabled} = $c->config->{enable_persona} // "0" eq "1"; if (scalar(@args) == 0 || $args[0] ne "static") { $c->stash->{nrRunningBuilds} = $c->model('DB::Builds')->search({ finished => 0, busy => 1 }, {})->count(); @@ -75,20 +76,6 @@ sub queue_GET { } -sub timeline :Local { - my ($self, $c) = @_; - my $pit = time(); - $c->stash->{pit} = $pit; - $pit = $pit-(24*60*60)-1; - - $c->stash->{template} = 'timeline.tt'; - $c->stash->{builds} = [ $c->model('DB::Builds')->search - ( { finished => 1, stoptime => { '>' => $pit } } - , { order_by => ["starttime"] } - ) ]; -} - - sub status :Local :Args(0) :ActionClass('REST') { } sub status_GET { @@ -117,7 +104,7 @@ sub machines :Local Args(0) { { order_by => 'stoptime desc', rows => 1 }); ${$machines}{$m}{'idle'} = $idle ? $idle->stoptime : 0; } - + $c->stash->{machines} = $machines; $c->stash->{steps} = [ $c->model('DB::BuildSteps')->search( { finished => 0, 'me.busy' => 1, 'build.busy' => 1, }, @@ -273,7 +260,7 @@ sub narinfo :LocalRegex('^([a-z0-9]+).narinfo$') :Args(0) { sub logo :Local { my ($self, $c) = @_; - my $path = $ENV{"HYDRA_LOGO"} or die("Logo not set!"); + my $path = $c->config->{hydra_logo} // $ENV{"HYDRA_LOGO"} // die("Logo not set!"); $c->serve_static_file($path); } diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index 1ee214d0..6a1a8552 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -8,65 +8,83 @@ use Crypt::RandPasswd; use Digest::SHA1 qw(sha1_hex); use Hydra::Helper::Nix; use Hydra::Helper::CatalystUtils; +use LWP::UserAgent; +use JSON; +use HTML::Entities; __PACKAGE__->config->{namespace} = ''; -sub login :Local :Args(0) :ActionClass('REST::ForBrowsers') { } - -sub login_GET { - my ($self, $c) = @_; - - my $baseurl = $c->uri_for('/'); - my $referer = $c->request->referer; - $c->session->{referer} = $referer if defined $referer && $referer =~ m/^($baseurl)/; - - $c->stash->{template} = 'login.tt'; -} +sub login :Local :Args(0) :ActionClass('REST') { } sub login_POST { my ($self, $c) = @_; - my $username; - my $password; + my $username = $c->stash->{params}->{username} // ""; + my $password = $c->stash->{params}->{password} // ""; - $username = $c->stash->{params}->{username}; - $password = $c->stash->{params}->{password}; + error($c, "You must specify a user name.") if $username eq ""; + error($c, "You must specify a password.") if $password eq ""; - if ($username && $password) { - if ($c->authenticate({username => $username, password => $password})) { - if ($c->request->looks_like_browser) { - backToReferer($c); - } else { - currentUser_GET($self, $c); - } - } else { - $self->status_forbidden($c, message => "Bad username or password."); - if ($c->request->looks_like_browser) { - login_GET($self, $c); - } - } - } + accessDenied($c, "Bad username or password.") + if !$c->authenticate({username => $username, password => $password}); + + currentUser_GET($self, $c); } -sub logout :Local :Args(0) :ActionClass('REST::ForBrowsers') { } +sub logout :Local :Args(0) :ActionClass('REST') { } sub logout_POST { my ($self, $c) = @_; + $c->flash->{flashMsg} = "You are no longer signed in." if $c->user_exists(); $c->logout; - if ($c->request->looks_like_browser) { - $c->response->redirect($c->request->referer || $c->uri_for('/')); - } else { - $self->status_no_content($c); - } + $self->status_no_content($c); } -sub logout_GET { - # Probably a better way to do this + +sub persona_login :Path('/persona-login') Args(0) { my ($self, $c) = @_; - logout_POST($self, $c); + requirePost($c); + + error($c, "Persona support is not enabled.") unless $c->stash->{personaEnabled}; + + my $assertion = $c->req->params->{assertion} or die; + + my $ua = new LWP::UserAgent; + my $response = $ua->post( + 'https://verifier.login.persona.org/verify', + { assertion => $assertion, + audience => $c->uri_for('/') + }); + error($c, "Did not get a response from Persona.") unless $response->is_success; + + my $d = decode_json($response->decoded_content) or die; + error($c, "Persona says: $d->{reason}") if $d->{status} ne "okay"; + + my $email = $d->{email} or die; + + # Be paranoid about the email address format, since we do use it + # in URLs. + die "Illegal email address." unless $email =~ /^[a-zA-Z0-9\.\-\_]+@[a-zA-Z0-9\.\-\_]+$/; + + my $user = $c->find_user({ username => $email }); + + if (!$user) { + $c->model('DB::Users')->create( + { username => $email + , password => "!" + , emailaddress => $email, + , type => "persona" + }); + $user = $c->find_user({ username => $email }) or die; + } + + $c->set_authenticated($user); + + $self->status_no_content($c); + $c->flash->{successMsg} = "You are now signed in as " . encode_entities($email) . "."; } @@ -91,57 +109,81 @@ sub setPassword { sub register :Local Args(0) { my ($self, $c) = @_; - die "Not implemented!\n"; + accessDenied($c, "User registration is currently not implemented.") unless isAdmin($c); - $c->stash->{template} = 'user.tt'; - $c->stash->{create} = 1; - return if $c->request->method ne "POST"; - - my $userName = trim $c->req->params->{username}; - my $fullName = trim $c->req->params->{fullname}; - my $password = trim $c->req->params->{password}; - $c->stash->{username} = $userName; - $c->stash->{fullname} = $fullName; - - sub fail { - my ($c, $msg) = @_; - $c->stash->{errorMsg} = $msg; + if ($c->request->method eq "GET") { + $c->stash->{template} = 'user.tt'; + $c->stash->{create} = 1; + return; } - return fail($c, "You did not enter the correct digits from the security image.") - unless $c->validate_captcha($c->req->param('captcha')); + die unless $c->request->method eq "PUT"; - return fail($c, "Your user name is invalid. It must start with a lower-case letter followed by lower-case letters, digits, dots or underscores.") + my $userName = trim $c->req->params->{username}; + $c->stash->{username} = $userName; + + error($c, "You did not enter the correct digits from the security image.") + unless isAdmin($c) || $c->validate_captcha($c->req->param('captcha')); + + error($c, "Your user name is invalid. It must start with a lower-case letter followed by lower-case letters, digits, dots or underscores.") if $userName !~ /^$userNameRE$/; - return fail($c, "Your user name is already taken.") + error($c, "Your user name is already taken.") if $c->find_user({ username => $userName }); - return fail($c, "Your must specify your full name.") if $fullName eq ""; - - return fail($c, "You must specify a password of at least 6 characters.") - unless isValidPassword($password); - - return fail($c, "The passwords you specified did not match.") - if $password ne trim $c->req->params->{password2}; - txn_do($c->model('DB')->schema, sub { my $user = $c->model('DB::Users')->create( { username => $userName - , fullname => $fullName , password => "!" , emailaddress => "", + , type => "hydra" }); - setPassword($user, $password); + updatePreferences($c, $user); }); unless ($c->user_exists) { - $c->authenticate({username => $userName, password => $password}) + $c->set_authenticated({username => $userName}) or error($c, "Unable to authenticate the new user!"); } $c->flash->{successMsg} = "User $userName has been created."; - backToReferer($c); + $self->status_no_content($c); +} + + +sub updatePreferences { + my ($c, $user) = @_; + + my $fullName = trim($c->req->params->{fullname} // ""); + error($c, "Your must specify your full name.") if $fullName eq ""; + + my $password = trim($c->req->params->{password} // ""); + if ($user->type eq "hydra" && ($user->password eq "!" || $password ne "")) { + error($c, "You must specify a password of at least 6 characters.") + unless isValidPassword($password); + + error($c, "The passwords you specified did not match.") + if $password ne trim $c->req->params->{password2}; + + setPassword($user, $password); + } + + my $emailAddress = trim($c->req->params->{emailaddress} // ""); + # FIXME: validate email address? + + $user->update( + { fullname => $fullName + , emailonerror => $c->stash->{params}->{"emailonerror"} ? 1 : 0 + }); + + if (isAdmin($c)) { + $user->update({ emailaddress => $emailAddress }) + if $user->type eq "hydra"; + + $user->userroles->delete; + $user->userroles->create({ role => $_ }) + foreach paramToList($c, "roles"); + } } @@ -152,8 +194,7 @@ sub currentUser_GET { requireUser($c); - $self->status_ok( - $c, + $self->status_ok($c, entity => $c->model("DB::Users")->find($c->user->username) ); } @@ -172,96 +213,69 @@ sub user :Chained('/') PathPart('user') CaptureArgs(1) { } -sub deleteUser { - my ($self, $c, $user) = @_; - my ($project) = $c->model('DB::Projects')->search({ owner => $user->username }); - error($c, "User " . $user->username . " is still owner of project " . $project->name . ".") - if defined $project; - $c->logout() if $user->username eq $c->user->username; - $user->delete; -} - - -sub edit :Chained('user') :Args(0) :ActionClass('REST::ForBrowsers') { } +sub edit :Chained('user') :PathPart('') :Args(0) :ActionClass('REST::ForBrowsers') { } sub edit_GET { my ($self, $c) = @_; - - my $user = $c->stash->{user}; - $c->stash->{template} = 'user.tt'; - - $c->session->{referer} = $c->request->referer if !defined $c->session->{referer}; - - $c->stash->{fullname} = $user->fullname; - - $c->stash->{emailonerror} = $user->emailonerror; } -sub edit_POST { +sub edit_PUT { my ($self, $c) = @_; - my $user = $c->stash->{user}; - $c->stash->{template} = 'user.tt'; - - $c->session->{referer} = $c->request->referer if !defined $c->session->{referer}; - - if (($c->stash->{params}->{submit} // "") eq "delete") { - deleteUser($self, $c, $user); - backToReferer($c); - } - if (($c->stash->{params}->{submit} // "") eq "reset-password") { - $c->stash->{json} = {}; - error($c, "No email address is set for this user.") - unless $user->emailaddress; - my $password = Crypt::RandPasswd->word(8,10); - setPassword($user, $password); - sendEmail($c, - $user->emailaddress, - "Hydra password reset", - "Hi,\n\n". - "Your password has been reset. Your new password is '$password'.\n\n". - "You can change your password at " . $c->uri_for($self->action_for('edit'), [$user->username]) . ".\n\n". - "With regards,\n\nHydra.\n" - ); return; } - my $fullName = trim $c->stash->{params}->{fullname}; - txn_do($c->model('DB')->schema, sub { - - error($c, "Your must specify your full name.") if $fullName eq ""; - - $user->update( - { fullname => $fullName - , emailonerror => $c->stash->{params}->{"emailonerror"} ? 1 : 0 - }); - - my $password = $c->stash->{params}->{password} // ""; - if ($password ne "") { - error($c, "You must specify a password of at least 6 characters.") - unless isValidPassword($password); - error($c, "The passwords you specified did not match.") - if $password ne trim $c->stash->{params}->{password2}; - setPassword($user, $password); - } - - if (isAdmin($c)) { - $user->userroles->delete; - $user->userroles->create({ role => $_}) - foreach paramToList($c, "roles"); - } - + updatePreferences($c, $user); }); - if ($c->request->looks_like_browser) { - backToReferer($c); - } else { - $self->status_no_content($c); - } + $c->flash->{successMsg} = "Your preferences have been updated."; + $self->status_no_content($c); +} + +sub edit_DELETE { + my ($self, $c) = @_; + my $user = $c->stash->{user}; + + my ($project) = $c->model('DB::Projects')->search({ owner => $user->username }); + error($c, "User " . $user->username . " is still owner of project " . $project->name . ".") + if defined $project; + + $c->logout() if $user->username eq $c->user->username; + + $user->delete; + + $c->flash->{successMsg} = "The user has been deleted."; + $self->status_no_content($c); +} + + +sub reset_password :Chained('user') :PathPart('reset-password') :Args(0) { + my ($self, $c) = @_; + my $user = $c->stash->{user}; + + requirePost($c); + + error($c, "This user's password cannot be reset.") if $user->type ne "hydra"; + error($c, "No email address is set for this user.") + unless $user->emailaddress; + + my $password = Crypt::RandPasswd->word(8,10); + setPassword($user, $password); + sendEmail($c, + $user->emailaddress, + "Hydra password reset", + "Hi,\n\n". + "Your password has been reset. Your new password is '$password'.\n\n". + "You can change your password at " . $c->uri_for($self->action_for('edit'), [$user->username]) . ".\n\n". + "With regards,\n\nHydra.\n" + ); + + $c->flash->{successMsg} = "A new password has been sent to ${\$user->emailaddress}."; + $self->status_no_content($c); } @@ -280,4 +294,39 @@ sub dashboard :Chained('user') :Args(0) { } +sub my_jobs_tab :Chained('user') :PathPart('my-jobs-tab') :Args(0) { + my ($self, $c) = @_; + $c->stash->{template} = 'dashboard-my-jobs-tab.tt'; + + die unless $c->stash->{user}->emailaddress; + + # Get all current builds of which this user is a maintainer. + $c->stash->{builds} = [$c->model('DB::Builds')->search( + { iscurrent => 1 + , maintainers => { ilike => "%" . $c->stash->{user}->emailaddress . "%" } + , "project.enabled" => 1 + , "jobset.enabled" => 1 + }, + { order_by => ["project", "jobset", "job"] + , join => ["project", "jobset"] + })]; +} + + +sub my_jobsets_tab :Chained('user') :PathPart('my-jobsets-tab') :Args(0) { + my ($self, $c) = @_; + $c->stash->{template} = 'dashboard-my-jobsets-tab.tt'; + + my $jobsets = $c->model('DB::Jobsets')->search( + { "project.enabled" => 1, "me.enabled" => 1, + , owner => $c->stash->{user}->username + }, + { order_by => ["project", "name"] + , join => ["project"] + }); + + $c->stash->{jobsets} = [jobsetOverview_($c, $jobsets)]; +} + + 1; diff --git a/src/lib/Hydra/Helper/CatalystUtils.pm b/src/lib/Hydra/Helper/CatalystUtils.pm index 83413e2d..e83bd0ea 100644 --- a/src/lib/Hydra/Helper/CatalystUtils.pm +++ b/src/lib/Hydra/Helper/CatalystUtils.pm @@ -90,7 +90,8 @@ sub getPreviousSuccessfulBuild { sub error { - my ($c, $msg) = @_; + my ($c, $msg, $status) = @_; + $c->response->status($status) if defined $status; $c->error($msg); $c->detach; # doesn't return } @@ -98,15 +99,13 @@ sub error { sub notFound { my ($c, $msg) = @_; - $c->response->status(404); - error($c, $msg); + error($c, $msg, 404); } sub accessDenied { my ($c, $msg) = @_; - $c->response->status(403); - error($c, $msg); + error($c, $msg, 403); } @@ -121,8 +120,7 @@ sub backToReferer { sub forceLogin { my ($c) = @_; $c->session->{referer} = $c->request->uri; - $c->response->redirect($c->uri_for('/login')); - $c->detach; # doesn't return + accessDenied($c, "This page requires you to sign in."); } diff --git a/src/lib/Hydra/Helper/Nix.pm b/src/lib/Hydra/Helper/Nix.pm index a7fa06ed..76e10b47 100644 --- a/src/lib/Hydra/Helper/Nix.pm +++ b/src/lib/Hydra/Helper/Nix.pm @@ -17,7 +17,8 @@ our @EXPORT = qw( getPrimaryBuildsForView getPrimaryBuildTotal getViewResult getLatestSuccessfulViewResult - jobsetOverview removeAsciiEscapes getDrvLogPath findLog logContents + jobsetOverview jobsetOverview_ + removeAsciiEscapes getDrvLogPath findLog logContents getMainOutput getEvals getMachines pathIsInsidePrefix @@ -173,9 +174,9 @@ sub findLastJobForBuilds { } -sub jobsetOverview { - my ($c, $project) = @_; - return $project->jobsets->search( isProjectOwner($c, $project) ? {} : { hidden => 0 }, +sub jobsetOverview_ { + my ($c, $jobsets) = @_; + return $jobsets->search({}, { order_by => "name" , "+select" => [ "(select count(*) from Builds as a where a.finished = 0 and me.project = a.project and me.name = a.jobset and a.isCurrent = 1)" @@ -188,6 +189,13 @@ sub jobsetOverview { } +sub jobsetOverview { + my ($c, $project) = @_; + my $jobsets = $project->jobsets->search(isProjectOwner($c, $project) ? {} : { hidden => 0 }); + return jobsetOverview_($c, $jobsets); +} + + sub getViewResult { my ($primaryBuild, $jobs, $finished) = @_; diff --git a/src/lib/Hydra/Schema/Users.pm b/src/lib/Hydra/Schema/Users.pm index 245f44ee..a16370d3 100644 --- a/src/lib/Hydra/Schema/Users.pm +++ b/src/lib/Hydra/Schema/Users.pm @@ -61,6 +61,12 @@ __PACKAGE__->table("Users"); default_value: 0 is_nullable: 0 +=head2 type + + data_type: 'text' + default_value: 'hydra' + is_nullable: 0 + =cut __PACKAGE__->add_columns( @@ -74,6 +80,8 @@ __PACKAGE__->add_columns( { data_type => "text", is_nullable => 0 }, "emailonerror", { data_type => "integer", default_value => 0, is_nullable => 0 }, + "type", + { data_type => "text", default_value => "hydra", is_nullable => 0 }, ); =head1 PRIMARY KEY @@ -176,8 +184,8 @@ Composing rels: L -> project __PACKAGE__->many_to_many("projects", "projectmembers", "project"); -# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Hv9Ukqud0d3uIUot0ErKeg +# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-11-05 10:22:03 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Gd8KwFcnVShZ/WihvwfgQw my %hint = ( columns => [ diff --git a/src/root/common.tt b/src/root/common.tt index 48366567..056c9f7e 100644 --- a/src/root/common.tt +++ b/src/root/common.tt @@ -452,7 +452,7 @@ BLOCK makePopover %] BLOCK menuItem %]
  • - + uri) %] [%+ IF modal %]data-toggle="modal"[% END %]> [% IF icon %] [%+ END %] [% title %] @@ -464,4 +464,65 @@ BLOCK makeStar %] [% IF starred; "★"; ELSE; "☆"; END %] [% END; + +BLOCK renderJobsetOverview %] + + + + + + + + + + + + [% FOREACH j IN jobsets %] + [% successrate = 0 %] + + + + + + [% IF j.get_column('nrtotal') > 0 %] + [% successrate = ( j.get_column('nrsucceeded') / j.get_column('nrtotal') )*100 %] + [% IF j.get_column('nrscheduled') > 0 %] + [% class = 'label' %] + [% ELSIF successrate < 25 %] + [% class = 'label label-important' %] + [% ELSIF successrate < 75 %] + [% class = 'label label-warning' %] + [% ELSIF successrate <= 100 %] + [% class = 'label label-success' %] + [% END %] + [% END %] + + + + [% END %] + +
    NameDescriptionLast evaluatedSuccess
    + [% IF j.get_column('nrscheduled') > 0 %] + Scheduled + [% ELSIF j.get_column('nrfailed') == 0 %] + Succeeded + [% ELSIF j.get_column('nrfailed') > 0 && j.get_column('nrsucceeded') > 0 %] + Some Failed + [% ELSE %] + All Failed + [% END %] + [% IF showProject; INCLUDE renderFullJobsetName project=j.get_column('project') jobset=j.name inRow=1; ELSE; INCLUDE renderJobsetName project=j.get_column('project') jobset=j.name inRow=1; END %][% HTML.escape(j.description) %][% IF j.lastcheckedtime; INCLUDE renderDateTime timestamp = j.lastcheckedtime; ELSE; "-"; END %][% successrate FILTER format('%d') %]% + [% IF j.get_column('nrsucceeded') > 0 %] + [% j.get_column('nrsucceeded') %] + [% END %] + [% IF j.get_column('nrfailed') > 0 %] + [% j.get_column('nrfailed') %] + [% END %] + [% IF j.get_column('nrscheduled') > 0 %] + [% j.get_column('nrscheduled') %] + [% END %] +
    +[% END; + + %] diff --git a/src/root/dashboard-my-jobs-tab.tt b/src/root/dashboard-my-jobs-tab.tt new file mode 100644 index 00000000..a1e82612 --- /dev/null +++ b/src/root/dashboard-my-jobs-tab.tt @@ -0,0 +1,17 @@ +[% PROCESS common.tt %] + +[% IF builds.size == 0 %] + +
    You are not the maintainer of any + job. You can become a maintainer by setting the + meta.maintainer field of a job to [% + HTML.escape(user.emailaddress) %].
    + +[% ELSE %] + +

    Below are the most recent builds of the [% builds.size %] jobs of which you + ([% HTML.escape(user.emailaddress) %]) are a maintainer.

    + + [% INCLUDE renderBuildList %] + +[% END %] diff --git a/src/root/dashboard-my-jobsets-tab.tt b/src/root/dashboard-my-jobsets-tab.tt new file mode 100644 index 00000000..ab4fff0c --- /dev/null +++ b/src/root/dashboard-my-jobsets-tab.tt @@ -0,0 +1,12 @@ +[% PROCESS common.tt %] + +[% IF jobsets.size == 0 %] + +
    You are not the owner of any + jobset.
    + +[% ELSE %] + + [% INCLUDE renderJobsetOverview showProject=1 %] + +[% END %] diff --git a/src/root/dashboard.tt b/src/root/dashboard.tt index d33eb887..07e48eb5 100644 --- a/src/root/dashboard.tt +++ b/src/root/dashboard.tt @@ -3,6 +3,8 @@
    @@ -31,12 +33,17 @@ [% ELSE %] -
    You have no starred jobs. You can add them by visiting a job page and clicking on the ☆ icon.
    +
    You have no starred jobs. You + can add them by visiting a job page and clicking on the ☆ + icon.
    [% END %]
    + [% INCLUDE makeLazyTab tabName="tabs-my-jobs" uri=c.uri_for(c.controller('User').action_for('my_jobs_tab'), [user.username]) %] + [% INCLUDE makeLazyTab tabName="tabs-my-jobsets" uri=c.uri_for(c.controller('User').action_for('my_jobsets_tab'), [user.username]) %] + [% END %] diff --git a/src/root/layout.tt b/src/root/layout.tt index 0fd1397f..bee4d8a7 100644 --- a/src/root/layout.tt +++ b/src/root/layout.tt @@ -8,7 +8,8 @@ Hydra - [% HTML.escape(title) %] - + + @@ -64,28 +65,26 @@
    +
    [% IF flashMsg %] -
    [% flashMsg %]
    [% END %] [% IF successMsg %] -
    [% successMsg %]
    [% END %] [% IF errorMsg %] -
    Error: [% errorMsg %]
    [% END %] [% IF !hideHeader %] [% ELSE %] -
    + [% IF first %]
    [% first = 0; END; %] [% END %] [% content %] @@ -95,13 +94,96 @@ Hydra [% HTML.escape(version) %] (using [% HTML.escape(nixVersion) %]). [% IF c.user_exists %] - You are logged in as [% c.user.username %]. + You are signed in as [% HTML.escape(c.user.username) %][% IF c.user.type == 'persona' %] via Persona[% END %]. [% END %]
    + + + [% IF c.user_exists && c.user.type == 'hydra' %] + + [% ELSE %] + + + + [% END %] + + [% IF !c.user_exists %] + + + + [% END %] + diff --git a/src/root/login.tt b/src/root/login.tt deleted file mode 100644 index 4e57077f..00000000 --- a/src/root/login.tt +++ /dev/null @@ -1,44 +0,0 @@ -[% WRAPPER layout.tt title="Sign in" %] -[% PROCESS common.tt %] - -[% IF c.user_exists %] -

    -You are already logged in as [% c.user.username %]. -You can logout here. -

    -[% ELSE %] - - - -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -[% END %] - -[% END %] diff --git a/src/root/overview.tt b/src/root/overview.tt index 9b31aeb5..ebc4cea7 100644 --- a/src/root/overview.tt +++ b/src/root/overview.tt @@ -13,7 +13,7 @@ [% END %] -

    Projects

    + [% IF projects.size != 0 %] diff --git a/src/root/project.tt b/src/root/project.tt index d88474dd..4e2dbd8f 100644 --- a/src/root/project.tt +++ b/src/root/project.tt @@ -30,64 +30,7 @@
    [% IF project.jobsets %]

    This project has the following jobsets:

    - - - - - - - - - - - - - [% FOREACH j IN jobsets %] - [% successrate = 0 %] - - - - - - [% IF j.get_column('nrtotal') > 0 %] - [% successrate = ( j.get_column('nrsucceeded') / j.get_column('nrtotal') )*100 %] - [% IF j.get_column('nrscheduled') > 0 %] - [% class = 'label' %] - [% ELSIF successrate < 25 %] - [% class = 'label label-important' %] - [% ELSIF successrate < 75 %] - [% class = 'label label-warning' %] - [% ELSIF successrate <= 100 %] - [% class = 'label label-success' %] - [% END %] - [% END %] - - - - [% END %] - -
    IdDescriptionLast evaluatedSuccess
    - [% IF j.get_column('nrscheduled') > 0 %] - Scheduled - [% ELSIF j.get_column('nrfailed') == 0 %] - Succeeded - [% ELSIF j.get_column('nrfailed') > 0 && j.get_column('nrsucceeded') > 0 %] - Some Failed - [% ELSE %] - All Failed - [% END %] - [% INCLUDE renderJobsetName project=project.name jobset=j.name inRow=1 %][% HTML.escape(j.description) %][% IF j.lastcheckedtime; INCLUDE renderDateTime timestamp = j.lastcheckedtime; ELSE; "-"; END %][% successrate FILTER format('%d') %]% - [% IF j.get_column('nrsucceeded') > 0 %] - [% j.get_column('nrsucceeded') %] - [% END %] - [% IF j.get_column('nrfailed') > 0 %] - [% j.get_column('nrfailed') %] - [% END %] - [% IF j.get_column('nrscheduled') > 0 %] - [% j.get_column('nrscheduled') %] - [% END %] -
    - + [% INCLUDE renderJobsetOverview %] [% ELSE %]

    No jobsets have been defined yet.

    [% END %] diff --git a/src/root/static/css/hydra.css b/src/root/static/css/hydra.css index 4fac2c3c..59ef91c1 100644 --- a/src/root/static/css/hydra.css +++ b/src/root/static/css/hydra.css @@ -1,6 +1,12 @@ -@media (min-width: 768px) { - body { - padding-top: 40px; +div.skip-topbar { + padding-top: 40px; + margin-bottom: 1.5em; +} + +@media (max-width: 979px) { + div.skip-topbar { + padding-top: 0; + margin-bottom: 0; } } @@ -68,16 +74,13 @@ h3 { } div.page-header { - margin-top: 0em; + margin-top: 1em; margin-bottom: 1em; -} - -div.page-header h1 { - margin-bottom: 0em; -} - -div.page-header h1 small { - font-size: 45%; + padding-top: 0em; + padding-bottom: 0.5em; + color: #999999; + font-size: 123.809%; + font-weight: normal; } .shell-prompt { @@ -107,4 +110,4 @@ td.nowrap { .star:hover { cursor: pointer; -} \ No newline at end of file +} diff --git a/src/root/static/js/common.js b/src/root/static/js/common.js index 891fd4e6..68a65d2a 100644 --- a/src/root/static/js/common.js +++ b/src/root/static/js/common.js @@ -135,6 +135,7 @@ function requestJSON(args) { bootbox.alert("Server error: " + escapeHTML(data.responseText)); else bootbox.alert("Unknown server error!"); + if (args.postError) args.postError(data); }; return $.ajax(args); }; @@ -145,3 +146,9 @@ function redirectJSON(args) { }; return requestJSON(args); }; + +function backToReferrer() { + // FIXME: should only do this if the referrer is another Hydra + // page. + window.location = document.referrer; +} diff --git a/src/root/timeline.tt b/src/root/timeline.tt deleted file mode 100644 index 2f665ad0..00000000 --- a/src/root/timeline.tt +++ /dev/null @@ -1,64 +0,0 @@ -[% USE date %] - -[% WRAPPER layout.tt title="Timeline" %] - -[% PROCESS common.tt %] - - - - - - - -
    - - -[% END %] diff --git a/src/root/topbar.tt b/src/root/topbar.tt index 642e40d2..6e3cd264 100644 --- a/src/root/topbar.tt +++ b/src/root/topbar.tt @@ -119,9 +119,27 @@ [% IF c.user_exists %] [% INCLUDE menuItem uri = c.uri_for(c.controller('User').action_for('edit'), [c.user.username]) title = "Preferences" %] - [% INCLUDE menuItem uri = c.uri_for(c.controller('Root').action_for('logout')) title = "Sign out" %] +
  • + Sign out +
  • [% ELSE %] - [% INCLUDE menuItem uri = c.uri_for(c.controller('Root').action_for('login')) title = "Sign in" %] + [% IF personaEnabled %] + [% WRAPPER makeSubMenu title="Sign in" %] +
  • + + Sign in with Persona + +
  • +
  • +
  • + Sign in with a Hydra account +
  • + [% END %] + [% ELSE %] +
  • + Sign in +
  • + [% END %] [% END %] diff --git a/src/root/user.tt b/src/root/user.tt index 18de0ebb..8f577929 100644 --- a/src/root/user.tt +++ b/src/root/user.tt @@ -1,4 +1,4 @@ -[% WRAPPER layout.tt title=(create ? "Register new user" : "Editing user $user.username") %] +[% WRAPPER layout.tt title=(create ? "Add new user" : "Editing user $user.username") %] [% PROCESS common.tt %] [% BLOCK roleoption %] @@ -14,7 +14,7 @@ >[% role %] [% END %] -
    +
    @@ -30,10 +30,11 @@
    - fullname) %]/> + create ? '' : user.fullname) %]/>
    + [% IF create || user.type == 'hydra' %]
    @@ -47,31 +48,28 @@
    + [% END %] - - [% IF !create %] -
    -
    - -
    +
    +
    +
    - [% END %] +
    - [% IF !create && c.check_user_roles('admin') %] + [% IF !create || c.check_user_roles('admin') %]
    - [% INCLUDE roleoption role="admin" %] [% INCLUDE roleoption role="create-projects" %] @@ -79,7 +77,7 @@
    [% END %] - [% IF create %] + [% IF create && !c.check_user_roles('admin') %]
    CAPTCHA @@ -95,44 +93,21 @@ [% END %]
    - - [% IF !create && c.check_user_roles('admin') %] - - [% END %] [% IF !create %] - - [% END %]
    @@ -140,4 +115,48 @@ + + [% END %] diff --git a/src/root/users.tt b/src/root/users.tt index f9178b8e..45f5e5e7 100644 --- a/src/root/users.tt +++ b/src/root/users.tt @@ -4,7 +4,7 @@ - + @@ -14,9 +14,9 @@ [% FOREACH u IN users %] - - - + + + diff --git a/src/script/Makefile.am b/src/script/Makefile.am index 3994684c..c05c9a12 100644 --- a/src/script/Makefile.am +++ b/src/script/Makefile.am @@ -1,6 +1,5 @@ EXTRA_DIST = \ $(distributable_scripts) \ - hydra-control \ hydra-eval-guile-jobs.in distributable_scripts = \ @@ -11,6 +10,7 @@ distributable_scripts = \ hydra-server \ hydra-update-gc-roots \ hydra-s3-backup-collect-garbage \ + hydra-create-user \ nix-prefetch-git \ nix-prefetch-bzr \ nix-prefetch-hg diff --git a/src/script/hydra-control b/src/script/hydra-control deleted file mode 100755 index 7349e303..00000000 --- a/src/script/hydra-control +++ /dev/null @@ -1,42 +0,0 @@ -#! /bin/sh - -action="$1" - -if test -z "$HYDRA_DATA"; then - echo "Error: \$HYDRA_DATA is not set"; - exit 1 -fi - -if test "$action" = "start"; then - - hydra-server > $HYDRA_DATA/server.log 2>&1 & - echo $! > $HYDRA_DATA/server.pid - - hydra-evaluator > $HYDRA_DATA/evaluator.log 2>&1 & - echo $! > $HYDRA_DATA/evaluator.pid - - hydra-queue-runner > $HYDRA_DATA/queue-runner.log 2>&1 & - echo $! > $HYDRA_DATA/queue_runner.pid - -elif test "$action" = "stop"; then - - kill $(cat $HYDRA_DATA/server.pid) - kill $(cat $HYDRA_DATA/evaluator.pid) - kill $(cat $HYDRA_DATA/queue_runner.pid) - -elif test "$action" = "status"; then - - echo -n "Hydra web server... " - (kill -0 $(cat $HYDRA_DATA/server.pid) 2> /dev/null && echo "ok") || echo "not running" - - echo -n "Hydra evaluator... " - (kill -0 $(cat $HYDRA_DATA/evaluator.pid) 2> /dev/null && echo "ok") || echo "not running" - - echo -n "Hydra queue runner... " - (kill -0 $(cat $HYDRA_DATA/queue_runner.pid) 2> /dev/null && echo "ok") || echo "not running" - - -else - echo "Syntax: $0 [start|stop|status]" - exit 1 -fi diff --git a/src/script/hydra-create b/src/script/hydra-create deleted file mode 100755 index d05c0c6c..00000000 --- a/src/script/hydra-create +++ /dev/null @@ -1,60 +0,0 @@ -#! @perl@ - -use strict; -use warnings; - -use Catalyst::ScriptRunner; -Catalyst::ScriptRunner->run('Hydra', 'Create'); - -1; - -=head1 NAME - -hydra_create.pl - Create a new Catalyst Component - -=head1 SYNOPSIS - -hydra_create.pl [options] model|view|controller name [helper] [options] - - Options: - --force don't create a .new file where a file to be created exists - --mechanize use Test::WWW::Mechanize::Catalyst for tests if available - --help display this help and exits - - Examples: - hydra_create.pl controller My::Controller - hydra_create.pl -mechanize controller My::Controller - hydra_create.pl view My::View - hydra_create.pl view HTML TT - hydra_create.pl model My::Model - hydra_create.pl model SomeDB DBIC::Schema MyApp::Schema create=dynamic\ - dbi:SQLite:/tmp/my.db - hydra_create.pl model AnotherDB DBIC::Schema MyApp::Schema create=static\ - [Loader opts like db_schema, naming] dbi:Pg:dbname=foo root 4321 - [connect_info opts like quote_char, name_sep] - - See also: - perldoc Catalyst::Manual - perldoc Catalyst::Manual::Intro - perldoc Catalyst::Helper::Model::DBIC::Schema - perldoc Catalyst::Model::DBIC::Schema - perldoc Catalyst::View::TT - -=head1 DESCRIPTION - -Create a new Catalyst Component. - -Existing component files are not overwritten. If any of the component files -to be created already exist the file will be written with a '.new' suffix. -This behavior can be suppressed with the C<-force> option. - -=head1 AUTHORS - -Catalyst Contributors, see Catalyst.pm - -=head1 COPYRIGHT - -This library is free software. You can redistribute it and/or modify -it under the same terms as Perl itself. - -=cut diff --git a/src/script/hydra-create-user b/src/script/hydra-create-user new file mode 100755 index 00000000..1fcf4728 --- /dev/null +++ b/src/script/hydra-create-user @@ -0,0 +1,88 @@ +#! /var/run/current-system/sw/bin/perl -w + +use strict; +use Hydra::Schema; +use Hydra::Helper::Nix; +use Hydra::Model::DB; +use Getopt::Long qw(:config gnu_getopt); +use Digest::SHA1 qw(sha1_hex); + +sub showHelp { + print < \$renameFrom, + "type=s" => \$type, + "full-name=s" => \$fullName, + "email-address=s" => \$emailAddress, + "password=s" => \$password, + "wipe-roles" => \$wipeRoles, + "role=s" => \@roles, + "help" => sub { showHelp() } + ) or exit 1; + +die "$0: one user name required\n" if scalar @ARGV != 1; +my $userName = $ARGV[0]; + +die "$0: type must be `hydra' or `persona'\n" + if defined $type && $type ne "hydra" && $type ne "persona"; + +my $db = Hydra::Model::DB->new(); + +txn_do($db, sub { + my $user = $db->resultset('Users')->find({ username => $renameFrom // $userName }); + if ($renameFrom) { + die "$0: user `$renameFrom' does not exist\n" unless $user; + $user->update({ username => $userName }); + } elsif ($user) { + print STDERR "updating existing user `$userName'\n"; + } else { + print STDERR "creating new user `$userName'\n"; + $user = $db->resultset('Users')->create( + { username => $userName, type => "hydra", emailaddress => "", password => "!" }); + } + + die "$0: Persona user names must be email addresses\n" + if $user->type eq "persona" && $userName !~ /\@/; + + $user->update({ type => $type }) if defined $type; + + $user->update({ fullname => $fullName eq "" ? undef : $fullName }) if defined $fullName; + + if ($user->type eq "persona") { + die "$0: Persona accounts do not have an explicitly set email address.\n" + if defined $emailAddress; + die "$0: Persona accounts do not have a password.\n" + if defined $password; + $user->update({ emailaddress => $userName, password => "!" }); + } else { + $user->update({ emailaddress => $emailAddress }) if defined $emailAddress; + $user->update({ password => sha1_hex($password) }) if defined $password; + } + + $user->userroles->delete if $wipeRoles; + $user->userroles->update_or_create({ role => $_ }) foreach @roles; +}); diff --git a/src/sql/hydra.sql b/src/sql/hydra.sql index 8e5f3093..a91013c8 100644 --- a/src/sql/hydra.sql +++ b/src/sql/hydra.sql @@ -9,7 +9,8 @@ create table Users ( fullName text, emailAddress text not null, password text not null, -- sha256 hash - emailOnError integer not null default 0 + emailOnError integer not null default 0, + type text not null default 'hydra' -- either "hydra" or "persona" ); diff --git a/src/sql/upgrade-25.sql b/src/sql/upgrade-25.sql new file mode 100644 index 00000000..d191d4fb --- /dev/null +++ b/src/sql/upgrade-25.sql @@ -0,0 +1 @@ +alter table Users add column type text not null default 'hydra'; diff --git a/tests/Makefile.am b/tests/Makefile.am index ab848d85..f5f88488 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -27,7 +27,7 @@ TESTS = \ clean: chmod -R a+w nix || true - rm -rf db.sqlite data nix git-repo hg-repo svn-repo svn-checkout svn-checkout-repo bzr-repo bzr-checkout-repo + rm -rf db.sqlite data nix git-repo hg-repo svn-repo svn-checkout svn-checkout-repo bzr-repo bzr-checkout-repo darcs-repo rm -f .*-state check_SCRIPTS = db.sqlite repos diff --git a/tests/api-test.pl b/tests/api-test.pl index f9068dd4..c71f7909 100644 --- a/tests/api-test.pl +++ b/tests/api-test.pl @@ -1,6 +1,6 @@ use LWP::UserAgent; use JSON; -use Test::Simple tests => 16; +use Test::Simple tests => 17; my $ua = LWP::UserAgent->new; $ua->cookie_jar({}); @@ -17,6 +17,9 @@ sub request_json { return $res; } +my $result = request_json({ uri => "/login", method => "POST", data => { username => "root", password => "wrong" } }); +ok($result->code() == 403, "Incorrect password rejected."); + my $result = request_json({ uri => "/login", method => "POST", data => { username => "root", password => "foobar" } }); my $user = decode_json($result->content()); @@ -46,7 +49,7 @@ ok(exists $jobset->{jobsetinputs}->{"my-src"}, "The new jobset has a 'my-src' in ok($jobset->{jobsetinputs}->{"my-src"}->{jobsetinputalts}->[0] eq "/run/jobset", "The 'my-src' input is in /run/jobset"); -system("LOGNAME=root NIX_STORE_DIR=/run/nix/store NIX_LOG_DIR=/run/nix/var/log/nix NIX_STATE_DIR=/run/nix/var/nix HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' hydra-evaluator sample default"); +system("NIX_STORE_DIR=/tmp/nix/store NIX_LOG_DIR=/tmp/nix/var/log/nix NIX_STATE_DIR=/tmp/nix/var/nix hydra-evaluator sample default"); $result = request_json({ uri => '/jobset/sample/default/evals' }); ok($result->code() == 200, "Can get evals of a jobset"); my $evals = decode_json($result->content())->{evals}; @@ -55,11 +58,11 @@ ok($eval->{hasnewbuilds} == 1, "The first eval of a jobset has new builds"); # Ugh, cached for 30s sleep 30; -system("echo >> /run/jobset/default.nix; LOGNAME=root NIX_STORE_DIR=/run/nix/store NIX_LOG_DIR=/run/nix/var/log/nix NIX_STATE_DIR=/run/nix/var/nix HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' hydra-evaluator sample default"); +system("echo >> /run/jobset/default.nix; NIX_STORE_DIR=/tmp/nix/store NIX_LOG_DIR=/tmp/nix/var/log/nix NIX_STATE_DIR=/tmp/nix/var/nix hydra-evaluator sample default"); my $evals = decode_json(request_json({ uri => '/jobset/sample/default/evals' })->content())->{evals}; ok($evals->[0]->{jobsetevalinputs}->{"my-src"}->{revision} != $evals->[1]->{jobsetevalinputs}->{"my-src"}->{revision}, "Changing a jobset source changes its revision"); my $build = decode_json(request_json({ uri => "/build/" . $evals->[0]->{builds}->[0] })->content()); ok($build->{job} eq "job", "The build's job name is job"); ok($build->{finished} == 0, "The build isn't finished yet"); -ok($build->{buildoutputs}->{out}->{path} =~ /^\/run\/nix\/store\/[a-zA-Z0-9]{32}-job$/, "The build's outpath is in the nix store and named 'job'"); +ok($build->{buildoutputs}->{out}->{path} =~ /^\/tmp\/nix\/store\/[a-zA-Z0-9]{32}-job$/, "The build's outpath is in the nix store and named 'job'");
    UsernameUser name Name Email Roles
    [% u.username %][% u.fullname %][% u.emailaddress %][% HTML.escape(u.username) %][% HTML.escape(u.fullname) %][% HTML.escape(u.emailaddress) %] [% FOREACH r IN u.userroles %][% r.role %] [% END %] [% IF u.emailonerror %]Yes[% ELSE %]No[% END %]