Merge branch 'persona'

This commit is contained in:
Eelco Dolstra 2013-11-06 18:14:52 +01:00
commit 80267bcbb1
31 changed files with 734 additions and 636 deletions

View file

@ -163,15 +163,16 @@ hydra-init</screen>
</para>
<para>
To add a user <emphasis>root</emphasis> with
<emphasis>admin</emphasis> privileges, execute:
<screen>
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</screen>
To create projects, you need to create a user with
<emphasis>admin</emphasis> privileges. This can be done using
the command <command>hydra-create-user</command>:
For SQLite the same commands can be used, with <command>psql
hydra</command> replaced by <command>sqlite3
/path/to/hydra.sqlite</command>.
<screen>
$ hydra-create-user alice --full-name 'Alice Q. User' \
--email-address 'alice@example.org' --password foobar --role admin
</screen>
Additional users can be created through the web interface.
</para>
</section>

View file

@ -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}";
OPENSSL_X509_CERT_FILE = "/etc/ssl/certs/ca-bundle.crt";
GIT_SSL_CAINFO = "/etc/ssl/certs/ca-bundle.crt";
};
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";
@ -167,8 +177,6 @@ in
# 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,29 +185,17 @@ 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
touch ${baseDir}/.db-created
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
''}
${pkgs.shadow}/bin/su hydra -c ${cfg.package}/bin/hydra-init
'';
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
@ -207,11 +203,14 @@ in
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"
];
};
}

View file

@ -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 <nixpkgs/nixos/lib/testing.nix> { 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 <nixpkgs/nixos/lib/testing.nix> { 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");
'';
});
}

View file

@ -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 => {

View file

@ -20,10 +20,11 @@ sub begin :Private {
$c->stash->{version} = $ENV{"HYDRA_RELEASE"} || "<devel>";
$c->stash->{nixVersion} = $ENV{"NIX_RELEASE"} || "<devel>";
$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 {
@ -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);
}

View file

@ -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 "";
accessDenied($c, "Bad username or password.")
if !$c->authenticate({username => $username, password => $password});
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);
}
}
}
}
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);
}
sub persona_login :Path('/persona-login') Args(0) {
my ($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;
}
sub logout_GET {
# Probably a better way to do this
my ($self, $c) = @_;
logout_POST($self, $c);
$c->set_authenticated($user);
$self->status_no_content($c);
$c->flash->{successMsg} = "You are now signed in as <tt>" . encode_entities($email) . "</tt>.";
}
@ -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);
if ($c->request->method eq "GET") {
$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;
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 <tt>$userName</tt> 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,50 +213,56 @@ 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} = {};
return;
}
txn_do($c->model('DB')->schema, sub {
updatePreferences($c, $user);
});
$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,
@ -226,43 +273,10 @@ sub edit_POST {
"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");
}
});
if ($c->request->looks_like_browser) {
backToReferer($c);
} else {
$c->flash->{successMsg} = "A new password has been sent to ${\$user->emailaddress}.";
$self->status_no_content($c);
}
}
sub dashboard :Chained('user') :Args(0) {
@ -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;

View file

@ -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.");
}

View file

@ -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) = @_;

View file

@ -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</projectmembers> -> 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 => [

View file

@ -452,7 +452,7 @@ BLOCK makePopover %]
BLOCK menuItem %]
<li class="[% IF "${root}${curUri}" == uri %]active[% END %]" [% IF confirmmsg %]onclick="javascript:return confirm('[% confirmmsg %]')"[% END %]>
<a href="[% uri %]" [% IF modal %]data-toggle="modal"[% END %]>
<a [% HTML.attributes(href => uri) %] [%+ IF modal %]data-toggle="modal"[% END %]>
[% IF icon %]<i class="[% icon %] icon-black"></i> [%+ END %]
[% title %]
</a>
@ -464,4 +464,65 @@ BLOCK makeStar %]
<span class="star" data-post="[% starUri %]">[% IF starred; "★"; ELSE; "☆"; END %]</span>
[% END;
BLOCK renderJobsetOverview %]
<table class="table table-striped table-condensed clickable-rows">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Description</th>
<th>Last evaluated</th>
<th colspan="2">Success</th>
</tr>
</thead>
<tbody>
[% FOREACH j IN jobsets %]
[% successrate = 0 %]
<tr>
<td>
[% IF j.get_column('nrscheduled') > 0 %]
<img src="[% c.uri_for("/static/images/help_16.png") %]" alt="Scheduled" />
[% ELSIF j.get_column('nrfailed') == 0 %]
<img src="[% c.uri_for("/static/images/checkmark_16.png") %]" alt="Succeeded" />
[% ELSIF j.get_column('nrfailed') > 0 && j.get_column('nrsucceeded') > 0 %]
<img src="[% c.uri_for("/static/images/error_some_16.png") %]" alt="Some Failed" />
[% ELSE %]
<img src="[% c.uri_for("/static/images/error_16.png") %]" alt="All Failed" />
[% END %]
</td>
<td><span class="[% IF !j.enabled %]disabled-jobset[% END %] [%+ IF j.hidden %]hidden-jobset[% 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 %]</span></td>
<td>[% HTML.escape(j.description) %]</td>
<td>[% IF j.lastcheckedtime; INCLUDE renderDateTime timestamp = j.lastcheckedtime; ELSE; "-"; END %]</td>
[% 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 %]
<td><span class="[% class %]">[% successrate FILTER format('%d') %]%</span></td>
<td>
[% IF j.get_column('nrsucceeded') > 0 %]
<span class="label label-success">[% j.get_column('nrsucceeded') %]</span>
[% END %]
[% IF j.get_column('nrfailed') > 0 %]
<span class="label label-important">[% j.get_column('nrfailed') %]</span>
[% END %]
[% IF j.get_column('nrscheduled') > 0 %]
<span class="label label">[% j.get_column('nrscheduled') %]</span>
[% END %]
</td>
</tr>
[% END %]
</tbody>
</table>
[% END;
%]

View file

@ -0,0 +1,17 @@
[% PROCESS common.tt %]
[% IF builds.size == 0 %]
<div class="alert alert-warning">You are not the maintainer of any
job. You can become a maintainer by setting the
<tt>meta.maintainer</tt> field of a job to <tt>[%
HTML.escape(user.emailaddress) %]</tt>.</div>
[% ELSE %]
<p>Below are the most recent builds of the [% builds.size %] jobs of which you
(<tt>[% HTML.escape(user.emailaddress) %]</tt>) are a maintainer.</p>
[% INCLUDE renderBuildList %]
[% END %]

View file

@ -0,0 +1,12 @@
[% PROCESS common.tt %]
[% IF jobsets.size == 0 %]
<div class="alert alert-warning">You are not the owner of any
jobset.</div>
[% ELSE %]
[% INCLUDE renderJobsetOverview showProject=1 %]
[% END %]

View file

@ -3,6 +3,8 @@
<ul class="nav nav-tabs">
<li class="active"><a href="#tabs-starred-jobs" data-toggle="tab">Starred jobs</a></li>
<li><a href="#tabs-my-jobs" data-toggle="tab">My jobs</a></li>
<li><a href="#tabs-my-jobsets" data-toggle="tab">My jobsets</a></li>
</ul>
<div id="generic-tabs" class="tab-content">
@ -31,12 +33,17 @@
[% ELSE %]
<div class="alert alert-warning">You have no starred jobs. You can add them by visiting a job page and clicking on the ☆ icon.</div>
<div class="alert alert-warning">You have no starred jobs. You
can add them by visiting a job page and clicking on the ☆
icon.</div>
[% END %]
</div>
[% 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]) %]
</div>
[% END %]

View file

@ -8,7 +8,8 @@
<head>
<title>Hydra - [% HTML.escape(title) %]</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/jquery-ui.min.js"></script>
@ -64,28 +65,26 @@
</div>
<div class="container">
<div class="skip-topbar"></div>
[% IF flashMsg %]
<br />
<div class="alert alert-info">[% flashMsg %]</div>
[% END %]
[% IF successMsg %]
<br />
<div class="alert alert-success">[% successMsg %]</div>
[% END %]
[% IF errorMsg %]
<br />
<div class="alert alert-warning">Error: [% errorMsg %]</div>
[% END %]
[% IF !hideHeader %]
<div class="page-header">
<h1><small>[% IF c.user_exists && starUri; INCLUDE makeStar; " "; END; HTML.escape(title) %]</small></h1>
[% IF c.user_exists && starUri; INCLUDE makeStar; " "; END; HTML.escape(title) %]
</div>
[% ELSE %]
<br />
[% IF first %]<br />[% first = 0; END; %]
[% END %]
[% content %]
@ -95,13 +94,96 @@
<small>
<em><a href="http://nixos.org/hydra" target="_blank">Hydra</a> [% HTML.escape(version) %] (using [% HTML.escape(nixVersion) %]).</em>
[% IF c.user_exists %]
You are logged in as <tt>[% c.user.username %]</tt>.
You are signed in as <tt>[% HTML.escape(c.user.username) %]</tt>[% IF c.user.type == 'persona' %] via Persona[% END %].
[% END %]
</small>
</footer>
</div>
<script>
function doLogout() {
[% IF c.user_exists %]
$.post("[% c.uri_for('/logout') %]")
.done(function(data) {
window.location.reload();
})
.fail(function() { bootbox.alert("Server request failed!"); });
[% END %]
}
</script>
[% IF c.user_exists && c.user.type == 'hydra' %]
<script>
$("#persona-signout").click(doLogout);
</script>
[% ELSE %]
<script src="https://login.persona.org/include.js"></script>
<script>
navigator.id.watch({
loggedInUser: [% c.user_exists ? '"' _ HTML.escape(c.user.username) _ '"' : "null" %],
onlogin: function(assertion) {
requestJSON({
url: "[% c.uri_for('/persona-login') %]",
data: "assertion=" + assertion,
type: 'POST',
success: function(data) { window.location.reload(); },
postError: function() { navigator.id.logout(); }
});
},
onlogout: doLogout
});
$("#persona-signin").click(function() {
navigator.id.request({ siteName: 'Hydra' });
});
$("#persona-signout").click(function() {
navigator.id.logout();
});
</script>
[% END %]
[% IF !c.user_exists %]
<div id="hydra-signin" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
<form class="form-horizontal">
<div class="modal-body">
<div class="control-group">
<label class="control-label">User name</label>
<div class="controls">
<input type="text" class="span3" name="username" value=""/>
</div>
</div>
<div class="control-group">
<label class="control-label">Password</label>
<div class="controls">
<input type="password" class="span3" name="password" value=""/>
</div>
</div>
</div>
<div class="modal-footer">
<button id="do-signin" class="btn btn-primary">Sign in</button>
<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
</div>
</form>
</div>
<script>
$("#do-signin").click(function() {
requestJSON({
url: "[% c.uri_for('/login') %]",
data: $(this).parents("form").serialize(),
type: 'POST',
success: function(data) {
window.location.reload();
}
});
return false;
});
</script>
[% END %]
</body>
</html>

View file

@ -1,44 +0,0 @@
[% WRAPPER layout.tt title="Sign in" %]
[% PROCESS common.tt %]
[% IF c.user_exists %]
<p class="btn-info btn-large">
You are already logged in as <tt>[% c.user.username %]</tt>.
You can <a href="[% c.uri_for('/logout') %]">logout</a> here.
</p>
[% ELSE %]
<!--
<p>Don't have an account yet? Please <a href="[%
c.uri_for('/register') %]">register</a> first.</p>
-->
<br/>
<form class="form-horizontal" method="post" action="[% c.uri_for('/login') %]">
<fieldset>
<div class="control-group">
<label class="control-label">User name</label>
<div class="controls">
<input type="text" class="span3" name="username" value=""/>
</div>
</div>
<div class="control-group">
<label class="control-label">Password</label>
<div class="controls">
<input type="password" class="span3" name="password" value=""/>
</div>
</div>
<div class="form-actions">
<input type="submit" name="login" value="Sign in" class="btn btn-primary" />
</div>
</fieldset>
</form>
[% END %]
[% END %]

View file

@ -13,7 +13,7 @@
</div>
[% END %]
<h2>Projects</h2>
<div class="page-header">Projects</div>
[% IF projects.size != 0 %]

View file

@ -30,64 +30,7 @@
<div id="tabs-project" class="tab-pane active">
[% IF project.jobsets %]
<p>This project has the following jobsets:</p>
<table class="table table-striped table-condensed clickable-rows">
<thead>
<tr>
<th></th>
<th>Id</th>
<th>Description</th>
<th>Last evaluated</th>
<th colspan="2">Success</th>
</tr>
</thead>
<tbody>
[% FOREACH j IN jobsets %]
[% successrate = 0 %]
<tr>
<td>
[% IF j.get_column('nrscheduled') > 0 %]
<img src="[% c.uri_for("/static/images/help_16.png") %]" alt="Scheduled" />
[% ELSIF j.get_column('nrfailed') == 0 %]
<img src="[% c.uri_for("/static/images/checkmark_16.png") %]" alt="Succeeded" />
[% ELSIF j.get_column('nrfailed') > 0 && j.get_column('nrsucceeded') > 0 %]
<img src="[% c.uri_for("/static/images/error_some_16.png") %]" alt="Some Failed" />
[% ELSE %]
<img src="[% c.uri_for("/static/images/error_16.png") %]" alt="All Failed" />
[% END %]
</td>
<td><span class="[% IF !j.enabled %]disabled-jobset[% END %] [%+ IF j.hidden %]hidden-jobset[% END %]">[% INCLUDE renderJobsetName project=project.name jobset=j.name inRow=1 %]</span></td>
<td>[% HTML.escape(j.description) %]</td>
<td>[% IF j.lastcheckedtime; INCLUDE renderDateTime timestamp = j.lastcheckedtime; ELSE; "-"; END %]</td>
[% 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 %]
<td><span class="[% class %]">[% successrate FILTER format('%d') %]%</span></td>
<td>
[% IF j.get_column('nrsucceeded') > 0 %]
<span class="label label-success">[% j.get_column('nrsucceeded') %]</span>
[% END %]
[% IF j.get_column('nrfailed') > 0 %]
<span class="label label-important">[% j.get_column('nrfailed') %]</span>
[% END %]
[% IF j.get_column('nrscheduled') > 0 %]
<span class="label label">[% j.get_column('nrscheduled') %]</span>
[% END %]
</td>
</tr>
[% END %]
</tbody>
</table>
[% INCLUDE renderJobsetOverview %]
[% ELSE %]
<p>No jobsets have been defined yet.</p>
[% END %]

View file

@ -1,6 +1,12 @@
@media (min-width: 768px) {
body {
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 {

View file

@ -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;
}

View file

@ -1,64 +0,0 @@
[% USE date %]
[% WRAPPER layout.tt title="Timeline" %]
[% PROCESS common.tt %]
<script type="text/javascript">
Timeline_urlPrefix="http://simile.mit.edu/timeline/api/";
</script>
<script src="http://simile.mit.edu/timeline/api/timeline-api.js" type="text/javascript"></script>
<script type="text/javascript">
$(function() {
doItNow()
});
var tl;
function doItNow() {
var eventSource = new Timeline.DefaultEventSource();
var bandInfos = [
Timeline.createBandInfo({
eventSource: eventSource,
width: "100%",
intervalUnit: Timeline.DateTime.HOUR,
intervalPixels: 200
})
];
tl = Timeline.create(document.getElementById("my-timeline"), bandInfos);
var centerd = Timeline.DateTime.parseIso8601DateTime("[% date.format(pit, '%Y-%m-%dT%H:%M:%S') %]");
tl.getBand(0).setCenterVisibleDate( centerd );
var event_data =
{ "dateTimeFormat": "iso8601", "events":[
{ "start": "[% date.format(pit, '%Y-%m-%dT%H:%M:%S') %]",
"end": "[% date.format(pit, '%Y-%m-%dT%H:%M:%S') %]",
"title": "Now"
}
[% FOREACH build IN builds %]
, { "start": "[% date.format(build.get_column("starttime"), '%Y-%m-%dT%H:%M:%S') %]",
"end": "[% date.format(build.get_column("stoptime"), '%Y-%m-%dT%H:%M:%S') %]",
"isDuration": "true",
"title": "[% build.id %]",
"link": "[% c.uri_for('/build' build.id) %]",
"color": "[% IF build.get_column("buildstatus") == 0 %]green[%ELSE%]red[% END%]"
}
[% END %]
]};
eventSource.loadJSON(event_data, document.location.href);
}
</script>
<div id="my-timeline" style="height: 700px; width: 100%;border: 1px solid #aaa"></div>
<noscript>
This page uses Javascript to show you a Timeline. Please enable Javascript in your browser to see the full page. Thank you.
</noscript>
[% END %]

View file

@ -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" %]
<li>
<a href="#" id="persona-signout">Sign out</a>
</li>
[% ELSE %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('Root').action_for('login')) title = "Sign in" %]
[% IF personaEnabled %]
[% WRAPPER makeSubMenu title="Sign in" %]
<li>
<a href="#" id="persona-signin">
<img src="https://developer.mozilla.org/files/3963/persona_sign_in_blue.png" alt="Sign in with Persona" />
</a>
</li>
<li class="divider"></li>
<li>
<a href="#hydra-signin" data-toggle="modal">Sign in with a Hydra account</a>
</li>
[% END %]
[% ELSE %]
<li>
<a href="#hydra-signin" data-toggle="modal">Sign in</a>
</li>
[% END %]
[% END %]
</ul>

View file

@ -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 %]</option>
[% END %]
<form class="form-horizontal" method="post">
<form class="form-horizontal">
<fieldset>
@ -30,10 +30,11 @@
<div class="control-group">
<label class="control-label">Full name</label>
<div class="controls">
<input type="text" class="span3" name="fullname" [% HTML.attributes(value => fullname) %]/>
<input type="text" class="span3" name="fullname" [% HTML.attributes(value => create ? '' : user.fullname) %]/>
</div>
</div>
[% IF create || user.type == 'hydra' %]
<div class="control-group">
<label class="control-label">Password</label>
<div class="controls">
@ -47,31 +48,28 @@
<input type="password" class="span3" name="password2" value=""/>
</div>
</div>
[% END %]
<!--
<div class="control-group">
<label class="control-label">Email</label>
<div class="controls">
<input type="text" class="span3" name="emailaddress" [% HTML.attributes(value => user.emailaddress) %]/>
<input type="text" class="span3" name="emailaddress" [% IF !create && user.type == 'persona' %]disabled="disabled"[% END %] [%+ HTML.attributes(value => user.emailaddress) %]/>
</div>
</div>
-->
[% IF !create %]
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="emailonerror" [% IF emailonerror; 'checked="checked"'; END %]/>Receive evaluation error notifications
<input type="checkbox" name="emailonerror" [% IF !create && user.emailonerror; 'checked="checked"'; END %]/>Receive evaluation error notifications
</label>
</div>
</div>
[% END %]
[% IF !create && c.check_user_roles('admin') %]
[% IF !create || c.check_user_roles('admin') %]
<div class="control-group">
<label class="control-label">Roles</label>
<div class="controls">
<select multiple="multiple" name="roles" class="span3">
<select multiple="multiple" name="roles" class="span3" [% IF !c.check_user_roles('admin') %]disabled="disabled"[% END %]>
[% INCLUDE roleoption role="admin" %]
[% INCLUDE roleoption role="create-projects" %]
</select>
@ -79,7 +77,7 @@
</div>
[% END %]
[% IF create %]
[% IF create && !c.check_user_roles('admin') %]
<div class="control-group">
<div class="controls">
<img src="[% c.uri_for('/captcha') %]" alt="CAPTCHA"/>
@ -95,44 +93,21 @@
[% END %]
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<button id="submit-user" class="btn btn-primary">
<i class="icon-ok icon-white"></i>
[%IF create %]Create[% ELSE %]Apply changes[% END %]
</button>
[% IF !create && c.check_user_roles('admin') %]
<button id="reset-password" type="submit" class="btn btn-warning" name="submit" value="reset-password">
[% IF !create && c.check_user_roles('admin') && user.type == 'hydra' %]
<button id="reset-password" class="btn btn-warning">
<i class="icon-trash icon-white"></i>
Reset password
</button>
<script type="text/javascript">
$("#reset-password").click(function() {
bootbox.confirm(
'Are you sure you want to reset the password for this user?',
function(c) {
if (!c) return;
$.post("[% c.uri_for(c.controller('User').action_for('edit'), [user.username]) %]", { submit: 'reset-password' })
.done(function(data) {
if (data.error)
bootbox.alert("Unable to reset password: " + data.error);
else
bootbox.alert("An email containing the new password has been sent to the user.");
})
.fail(function() { bootbox.alert("Server request failed!"); });
});
return false;
});
</script>
[% END %]
[% IF !create %]
<button id="delete-user" type="submit" class="btn btn-danger" name="submit" value="delete">
<button id="delete-user" class="btn btn-danger">
<i class="icon-trash icon-white"></i>
Delete this user
</button>
<script type="text/javascript">
$("#delete-user").click(function() {
return confirm("Are you sure you want to delete this user?");
});
</script>
[% END %]
</div>
@ -140,4 +115,48 @@
</form>
<script>
$("#submit-user").click(function() {
requestJSON({
[% IF create %]
url: "[% c.uri_for(c.controller('User').action_for('register')) %]",
[% ELSE %]
url: "[% c.uri_for(c.controller('User').action_for('edit'), c.req.captures) %]",
[% END %]
data: $(this).parents("form").serialize(),
type: 'PUT',
success: backToReferrer
});
return false;
});
$("#reset-password").click(function() {
bootbox.confirm(
'Are you sure you want to reset the password for this user?',
function(c) {
if (!c) return;
requestJSON({
url: "[% c.uri_for(c.controller('User').action_for('reset_password'), [user.username]) %]",
type: 'POST',
success: backToReferrer
});
});
return false;
});
$("#delete-user").click(function() {
bootbox.confirm(
'Are you sure you want to delete this user?',
function(c) {
if (!c) return;
requestJSON({
url: "[% c.uri_for(c.controller('User').action_for('edit'), c.req.captures) %]",
type: 'DELETE',
success: backToReferrer
});
});
return false;
});
</script>
[% END %]

View file

@ -14,9 +14,9 @@
<tbody>
[% FOREACH u IN users %]
<tr>
<td><a class="row-link" href="[% c.uri_for(c.controller('User').action_for('edit'), [u.username]) %]">[% u.username %]</a></td>
<td>[% u.fullname %]</td>
<td>[% u.emailaddress %]</td>
<td><a class="row-link" href="[% c.uri_for(c.controller('User').action_for('edit'), [u.username]) %]">[% HTML.escape(u.username) %]</a></td>
<td>[% HTML.escape(u.fullname) %]</td>
<td>[% HTML.escape(u.emailaddress) %]</td>
<td>[% FOREACH r IN u.userroles %]<i>[% r.role %]</i> [% END %]</td>
<td>[% IF u.emailonerror %]Yes[% ELSE %]No[% END %]</td>
</tr>

View file

@ -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

View file

@ -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

View file

@ -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

88
src/script/hydra-create-user Executable file
View file

@ -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 <<EOF;
Usage: $0 NAME
[--rename-from NAME]
[--type hydra|persona]
[--full-name FULLNAME]
[--email-address EMAIL-ADDRESS]
[--password PASSWORD]
[--wipe-roles]
[--role ROLE]...
Create a new Hydra user account, or update or an existing one. The
--role flag can be given multiple times. If the account already
exists, roles are added to the existing roles unless --wipe-roles is
specified. If --rename-from is given, the specified account is
renamed.
Example:
\$ hydra-create-user alice --password foobar --role admin
EOF
exit 0;
}
my ($renameFrom, $type, $fullName, $emailAddress, $password);
my $wipeRoles = 0;
my @roles;
GetOptions("rename-from=s" => \$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;
});

View file

@ -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"
);

1
src/sql/upgrade-25.sql Normal file
View file

@ -0,0 +1 @@
alter table Users add column type text not null default 'hydra';

View file

@ -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

View file

@ -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'");