forked from lix-project/hydra
Merge branch 'persona'
This commit is contained in:
commit
80267bcbb1
|
@ -163,15 +163,16 @@ hydra-init</screen>
|
||||||
</para>
|
</para>
|
||||||
|
|
||||||
<para>
|
<para>
|
||||||
To add a user <emphasis>root</emphasis> with
|
To create projects, you need to create a user with
|
||||||
<emphasis>admin</emphasis> privileges, execute:
|
<emphasis>admin</emphasis> privileges. This can be done using
|
||||||
<screen>
|
the command <command>hydra-create-user</command>:
|
||||||
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>
|
|
||||||
|
|
||||||
For SQLite the same commands can be used, with <command>psql
|
<screen>
|
||||||
hydra</command> replaced by <command>sqlite3
|
$ hydra-create-user alice --full-name 'Alice Q. User' \
|
||||||
/path/to/hydra.sqlite</command>.
|
--email-address 'alice@example.org' --password foobar --role admin
|
||||||
|
</screen>
|
||||||
|
|
||||||
|
Additional users can be created through the web interface.
|
||||||
</para>
|
</para>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
117
hydra-module.nix
117
hydra-module.nix
|
@ -9,28 +9,31 @@ let
|
||||||
|
|
||||||
hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig;
|
hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig;
|
||||||
|
|
||||||
env =
|
hydraEnv =
|
||||||
{ NIX_REMOTE = "daemon";
|
{ HYDRA_DBI = cfg.dbi;
|
||||||
HYDRA_DBI = cfg.dbi;
|
|
||||||
HYDRA_CONFIG = "${baseDir}/data/hydra.conf";
|
HYDRA_CONFIG = "${baseDir}/data/hydra.conf";
|
||||||
HYDRA_DATA = "${baseDir}/data";
|
HYDRA_DATA = "${baseDir}/data";
|
||||||
HYDRA_PORT = "${toString cfg.port}";
|
};
|
||||||
|
|
||||||
|
env =
|
||||||
|
{ NIX_REMOTE = "daemon";
|
||||||
OPENSSL_X509_CERT_FILE = "/etc/ssl/certs/ca-bundle.crt";
|
OPENSSL_X509_CERT_FILE = "/etc/ssl/certs/ca-bundle.crt";
|
||||||
GIT_SSL_CAINFO = "/etc/ssl/certs/ca-bundle.crt";
|
GIT_SSL_CAINFO = "/etc/ssl/certs/ca-bundle.crt";
|
||||||
};
|
} // hydraEnv;
|
||||||
|
|
||||||
serverEnv = env //
|
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; });
|
} // (optionalAttrs cfg.debugServer { DBIC_TRACE = 1; });
|
||||||
in
|
in
|
||||||
|
|
||||||
{
|
{
|
||||||
###### interface
|
###### interface
|
||||||
options = {
|
options = {
|
||||||
|
|
||||||
services.hydra = rec {
|
services.hydra = rec {
|
||||||
|
|
||||||
enable = mkOption {
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description = ''
|
description = ''
|
||||||
Whether to run Hydra services.
|
Whether to run Hydra services.
|
||||||
|
@ -38,27 +41,29 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
dbi = mkOption {
|
dbi = mkOption {
|
||||||
default = "dbi:Pg:dbname=hydra;host=localhost;user=hydra;";
|
type = types.string;
|
||||||
example = "dbi:SQLite:/home/hydra/db/hydra.sqlite";
|
default = "dbi:Pg:dbname=hydra;user=hydra;";
|
||||||
|
example = "dbi:Pg:dbname=hydra;host=postgres.example.org;user=foo;";
|
||||||
description = ''
|
description = ''
|
||||||
The DBI string for Hydra database connection.
|
The DBI string for Hydra database connection.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hydra = mkOption {
|
package = mkOption {
|
||||||
|
type = types.path;
|
||||||
#default = pkgs.hydra;
|
#default = pkgs.hydra;
|
||||||
description = ''
|
description = "The Hydra package.";
|
||||||
Location of hydra
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
hydraURL = mkOption {
|
hydraURL = mkOption {
|
||||||
|
type = types.str;
|
||||||
description = ''
|
description = ''
|
||||||
The base URL for the Hydra webserver instance. Used for links in emails.
|
The base URL for the Hydra webserver instance. Used for links in emails.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
listenHost = mkOption {
|
listenHost = mkOption {
|
||||||
|
type = types.str;
|
||||||
default = "*";
|
default = "*";
|
||||||
example = "localhost";
|
example = "localhost";
|
||||||
description = ''
|
description = ''
|
||||||
|
@ -68,6 +73,7 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
port = mkOption {
|
port = mkOption {
|
||||||
|
type = types.int;
|
||||||
default = 3000;
|
default = 3000;
|
||||||
description = ''
|
description = ''
|
||||||
TCP port the web server should listen to.
|
TCP port the web server should listen to.
|
||||||
|
@ -75,26 +81,30 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
minimumDiskFree = mkOption {
|
minimumDiskFree = mkOption {
|
||||||
|
type = types.int;
|
||||||
default = 5;
|
default = 5;
|
||||||
description = ''
|
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 {
|
minimumDiskFreeEvaluator = mkOption {
|
||||||
|
type = types.int;
|
||||||
default = 2;
|
default = 2;
|
||||||
description = ''
|
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 {
|
notificationSender = mkOption {
|
||||||
|
type = types.str;
|
||||||
description = ''
|
description = ''
|
||||||
Sender email address used for email notifications.
|
Sender email address used for email notifications.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
tracker = mkOption {
|
tracker = mkOption {
|
||||||
|
type = types.str;
|
||||||
default = "";
|
default = "";
|
||||||
description = ''
|
description = ''
|
||||||
Piece of HTML that is included on all pages.
|
Piece of HTML that is included on all pages.
|
||||||
|
@ -102,22 +112,16 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
logo = mkOption {
|
logo = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
File name of an alternate logo to be displayed on the web pages.
|
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 {
|
debugServer = mkOption {
|
||||||
default = false;
|
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
description = "Whether to run the server in debug mode";
|
description = "Whether to run the server in debug mode";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -134,15 +138,21 @@ in
|
||||||
###### implementation
|
###### implementation
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
|
|
||||||
services.hydra.extraConfig =
|
services.hydra.extraConfig =
|
||||||
''
|
''
|
||||||
using_frontend_proxy 1
|
using_frontend_proxy 1
|
||||||
base_uri ${cfg.hydraURL}
|
base_uri ${cfg.hydraURL}
|
||||||
notification_sender ${cfg.notificationSender}
|
notification_sender ${cfg.notificationSender}
|
||||||
max_servers 25
|
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 =
|
users.extraUsers.hydra =
|
||||||
{ description = "Hydra";
|
{ description = "Hydra";
|
||||||
|
@ -163,12 +173,10 @@ in
|
||||||
build-cache-failure = true
|
build-cache-failure = true
|
||||||
|
|
||||||
build-poll-interval = 10
|
build-poll-interval = 10
|
||||||
|
|
||||||
# Online log compression makes it impossible to get the tail of
|
# Online log compression makes it impossible to get the tail of
|
||||||
# builds that are in progress.
|
# builds that are in progress.
|
||||||
build-compress-log = false
|
build-compress-log = false
|
||||||
|
|
||||||
use-sqlite-wal = ${if cfg.useWAL then "true" else "false"}
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
systemd.services."hydra-init" =
|
systemd.services."hydra-init" =
|
||||||
|
@ -177,41 +185,32 @@ in
|
||||||
after = [ "postgresql.service" ];
|
after = [ "postgresql.service" ];
|
||||||
environment = env;
|
environment = env;
|
||||||
script = ''
|
script = ''
|
||||||
mkdir -p ${baseDir}/data
|
mkdir -m 0700 -p ${baseDir}/data
|
||||||
chown hydra ${baseDir}/data
|
chown hydra ${baseDir}/data
|
||||||
ln -sf ${hydraConf} ${baseDir}/data/hydra.conf
|
ln -sf ${hydraConf} ${baseDir}/data/hydra.conf
|
||||||
pass=$(HOME=/root ${pkgs.openssl}/bin/openssl rand -base64 32)
|
${optionalString (cfg.dbi == "dbi:Pg:dbname=hydra;user=hydra;") ''
|
||||||
if [ ! -f ${baseDir}/.pgpass ]; then
|
if ! [ -e ${baseDir}/.db-created ]; then
|
||||||
${config.services.postgresql.package}/bin/psql postgres << EOF
|
${config.services.postgresql.package}/bin/createuser hydra
|
||||||
CREATE USER hydra PASSWORD '$pass';
|
|
||||||
EOF
|
|
||||||
${config.services.postgresql.package}/bin/createdb -O hydra hydra
|
${config.services.postgresql.package}/bin/createdb -O hydra hydra
|
||||||
cat > ${baseDir}/.pgpass-tmp << EOF
|
touch ${baseDir}/.db-created
|
||||||
localhost:*:hydra:hydra:$pass
|
fi
|
||||||
EOF
|
''}
|
||||||
chown hydra ${baseDir}/.pgpass-tmp
|
${pkgs.shadow}/bin/su hydra -c ${cfg.package}/bin/hydra-init
|
||||||
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
|
|
||||||
'';
|
'';
|
||||||
serviceConfig.Type = "oneshot";
|
serviceConfig.Type = "oneshot";
|
||||||
serviceConfig.RemainAfterExit = true;
|
serviceConfig.RemainAfterExit = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services."hydra-server" =
|
systemd.services."hydra-server" =
|
||||||
{ wantedBy = [ "multi-user.target" ];
|
{ wantedBy = [ "multi-user.target" ];
|
||||||
wants = [ "hydra-init.service" ];
|
requires = [ "hydra-init.service" ];
|
||||||
after = [ "hydra-init.service" ];
|
after = [ "hydra-init.service" ];
|
||||||
environment = serverEnv;
|
environment = serverEnv;
|
||||||
serviceConfig =
|
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";
|
User = "hydra";
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
};
|
};
|
||||||
|
@ -219,13 +218,13 @@ in
|
||||||
|
|
||||||
systemd.services."hydra-queue-runner" =
|
systemd.services."hydra-queue-runner" =
|
||||||
{ wantedBy = [ "multi-user.target" ];
|
{ wantedBy = [ "multi-user.target" ];
|
||||||
wants = [ "hydra-init.service" ];
|
requires = [ "hydra-init.service" ];
|
||||||
after = [ "hydra-init.service" "network.target" ];
|
after = [ "hydra-init.service" "network.target" ];
|
||||||
path = [ pkgs.nettools ];
|
path = [ pkgs.nettools ];
|
||||||
environment = env;
|
environment = env;
|
||||||
serviceConfig =
|
serviceConfig =
|
||||||
{ ExecStartPre = "${cfg.hydra}/bin/hydra-queue-runner --unlock";
|
{ ExecStartPre = "${cfg.package}/bin/hydra-queue-runner --unlock";
|
||||||
ExecStart = "@${cfg.hydra}/bin/hydra-queue-runner hydra-queue-runner";
|
ExecStart = "@${cfg.package}/bin/hydra-queue-runner hydra-queue-runner";
|
||||||
User = "hydra";
|
User = "hydra";
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
};
|
};
|
||||||
|
@ -233,25 +232,26 @@ in
|
||||||
|
|
||||||
systemd.services."hydra-evaluator" =
|
systemd.services."hydra-evaluator" =
|
||||||
{ wantedBy = [ "multi-user.target" ];
|
{ wantedBy = [ "multi-user.target" ];
|
||||||
wants = [ "hydra-init.service" ];
|
requires = [ "hydra-init.service" ];
|
||||||
after = [ "hydra-init.service" "network.target" ];
|
after = [ "hydra-init.service" "network.target" ];
|
||||||
path = [ pkgs.nettools ];
|
path = [ pkgs.nettools ];
|
||||||
environment = env;
|
environment = env;
|
||||||
serviceConfig =
|
serviceConfig =
|
||||||
{ ExecStart = "@${cfg.hydra}/bin/hydra-evaluator hydra-evaluator";
|
{ ExecStart = "@${cfg.package}/bin/hydra-evaluator hydra-evaluator";
|
||||||
User = "hydra";
|
User = "hydra";
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services."hydra-update-gc-roots" =
|
systemd.services."hydra-update-gc-roots" =
|
||||||
{ wants = [ "hydra-init.service" ];
|
{ requires = [ "hydra-init.service" ];
|
||||||
after = [ "hydra-init.service" ];
|
after = [ "hydra-init.service" ];
|
||||||
environment = env;
|
environment = env;
|
||||||
serviceConfig =
|
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";
|
User = "hydra";
|
||||||
};
|
};
|
||||||
|
startAt = "02:15";
|
||||||
};
|
};
|
||||||
|
|
||||||
services.cron.systemCronJobs =
|
services.cron.systemCronJobs =
|
||||||
|
@ -278,7 +278,6 @@ in
|
||||||
in
|
in
|
||||||
[ "*/5 * * * * root ${checkSpace} &> ${baseDir}/data/checkspace.log"
|
[ "*/5 * * * * root ${checkSpace} &> ${baseDir}/data/checkspace.log"
|
||||||
"15 5 * * * root ${compressLogs} &> ${baseDir}/data/compress.log"
|
"15 5 * * * root ${compressLogs} &> ${baseDir}/data/compress.log"
|
||||||
"15 2 * * * root ${pkgs.systemd}/bin/systemctl start hydra-update-gc-roots.service"
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
85
release.nix
85
release.nix
|
@ -8,6 +8,23 @@ let
|
||||||
|
|
||||||
genAttrs' = pkgs.lib.genAttrs [ "x86_64-linux" ];
|
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 {
|
in rec {
|
||||||
|
|
||||||
tarball =
|
tarball =
|
||||||
|
@ -150,67 +167,45 @@ in rec {
|
||||||
|
|
||||||
tests.install = genAttrs' (system:
|
tests.install = genAttrs' (system:
|
||||||
with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
|
with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
|
||||||
let hydra = builtins.getAttr system build; in # build.${system}
|
|
||||||
simpleTest {
|
simpleTest {
|
||||||
machine =
|
machine = hydraServer (builtins.getAttr system build); # build.${system}
|
||||||
{ config, pkgs, ... }:
|
|
||||||
{ services.postgresql.enable = true;
|
|
||||||
services.postgresql.package = pkgs.postgresql92;
|
|
||||||
environment.systemPackages = [ hydra ];
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript =
|
testScript =
|
||||||
''
|
''
|
||||||
$machine->waitForJob("postgresql");
|
$machine->waitForJob("hydra-init");
|
||||||
|
$machine->waitForJob("hydra-server");
|
||||||
# Initialise the database and the state.
|
$machine->waitForJob("hydra-evaluator");
|
||||||
$machine->mustSucceed
|
$machine->waitForJob("hydra-queue-runner");
|
||||||
( "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->waitForOpenPort("3000");
|
$machine->waitForOpenPort("3000");
|
||||||
|
$machine->succeed("curl --fail http://localhost:3000/");
|
||||||
'';
|
'';
|
||||||
});
|
});
|
||||||
|
|
||||||
tests.api = genAttrs' (system:
|
tests.api = genAttrs' (system:
|
||||||
with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
|
with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
|
||||||
let hydra = builtins.getAttr system build; in # build."${system}"
|
|
||||||
simpleTest {
|
simpleTest {
|
||||||
machine =
|
machine = hydraServer (builtins.getAttr system build); # build.${system}
|
||||||
{ 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript =
|
testScript =
|
||||||
|
let dbi = "dbi:Pg:dbname=hydra;user=root;"; in
|
||||||
''
|
''
|
||||||
$machine->waitForJob("postgresql");
|
$machine->waitForJob("hydra-init");
|
||||||
|
|
||||||
# Initialise the database and the state.
|
# Create an admin account and some other state.
|
||||||
$machine->mustSucceed
|
$machine->succeed
|
||||||
( "createdb -O root hydra"
|
( "su hydra -c \"hydra-create-user root --email-address 'e.dolstra\@tudelft.nl' --password foobar --role admin\""
|
||||||
, "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql"
|
, "mkdir /run/jobset /tmp/nix"
|
||||||
, "mkdir /var/lib/hydra"
|
, "chmod 755 /run/jobset /tmp/nix"
|
||||||
, "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"
|
|
||||||
, "cp ${./tests/api-test.nix} /run/jobset/default.nix"
|
, "cp ${./tests/api-test.nix} /run/jobset/default.nix"
|
||||||
, "chmod 644 /run/jobset/default.nix"
|
, "chmod 644 /run/jobset/default.nix"
|
||||||
|
, "chown -R hydra /run/jobset /tmp/nix"
|
||||||
);
|
);
|
||||||
|
|
||||||
# Start the web interface.
|
# Start the web interface with some weird settings.
|
||||||
$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 &");
|
$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->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");
|
$machine->waitForJob("postgresql");
|
||||||
|
|
||||||
# Initialise the database and the state.
|
# Initialise the database and the state.
|
||||||
$machine->mustSucceed
|
$machine->succeed
|
||||||
( "createdb -O root hydra"
|
( "createdb -O root hydra"
|
||||||
, "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql"
|
, "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql"
|
||||||
, "mkdir /var/lib/hydra"
|
, "mkdir /var/lib/hydra"
|
||||||
|
@ -246,10 +241,10 @@ in rec {
|
||||||
);
|
);
|
||||||
|
|
||||||
# start fakes3
|
# 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->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");
|
||||||
'';
|
'';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,8 +53,9 @@ __PACKAGE__->config(
|
||||||
expose_stash => 'json'
|
expose_stash => 'json'
|
||||||
},
|
},
|
||||||
'Plugin::Session' => {
|
'Plugin::Session' => {
|
||||||
expires => 3600 * 24 * 2,
|
expires => 3600 * 24 * 7,
|
||||||
storage => Hydra::Model::DB::getHydraPath . "/session_data"
|
storage => Hydra::Model::DB::getHydraPath . "/session_data",
|
||||||
|
unlink_on_exit => 0
|
||||||
},
|
},
|
||||||
'Plugin::AccessLog' => {
|
'Plugin::AccessLog' => {
|
||||||
formatter => {
|
formatter => {
|
||||||
|
|
|
@ -20,10 +20,11 @@ sub begin :Private {
|
||||||
$c->stash->{version} = $ENV{"HYDRA_RELEASE"} || "<devel>";
|
$c->stash->{version} = $ENV{"HYDRA_RELEASE"} || "<devel>";
|
||||||
$c->stash->{nixVersion} = $ENV{"NIX_RELEASE"} || "<devel>";
|
$c->stash->{nixVersion} = $ENV{"NIX_RELEASE"} || "<devel>";
|
||||||
$c->stash->{curTime} = time;
|
$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->{tracker} = $ENV{"HYDRA_TRACKER"};
|
||||||
$c->stash->{flashMsg} = $c->flash->{flashMsg};
|
$c->stash->{flashMsg} = $c->flash->{flashMsg};
|
||||||
$c->stash->{successMsg} = $c->flash->{successMsg};
|
$c->stash->{successMsg} = $c->flash->{successMsg};
|
||||||
|
$c->stash->{personaEnabled} = $c->config->{enable_persona} // "0" eq "1";
|
||||||
|
|
||||||
if (scalar(@args) == 0 || $args[0] ne "static") {
|
if (scalar(@args) == 0 || $args[0] ne "static") {
|
||||||
$c->stash->{nrRunningBuilds} = $c->model('DB::Builds')->search({ finished => 0, busy => 1 }, {})->count();
|
$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 :Local :Args(0) :ActionClass('REST') { }
|
||||||
|
|
||||||
sub status_GET {
|
sub status_GET {
|
||||||
|
@ -117,7 +104,7 @@ sub machines :Local Args(0) {
|
||||||
{ order_by => 'stoptime desc', rows => 1 });
|
{ order_by => 'stoptime desc', rows => 1 });
|
||||||
${$machines}{$m}{'idle'} = $idle ? $idle->stoptime : 0;
|
${$machines}{$m}{'idle'} = $idle ? $idle->stoptime : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$c->stash->{machines} = $machines;
|
$c->stash->{machines} = $machines;
|
||||||
$c->stash->{steps} = [ $c->model('DB::BuildSteps')->search(
|
$c->stash->{steps} = [ $c->model('DB::BuildSteps')->search(
|
||||||
{ finished => 0, 'me.busy' => 1, 'build.busy' => 1, },
|
{ finished => 0, 'me.busy' => 1, 'build.busy' => 1, },
|
||||||
|
@ -273,7 +260,7 @@ sub narinfo :LocalRegex('^([a-z0-9]+).narinfo$') :Args(0) {
|
||||||
|
|
||||||
sub logo :Local {
|
sub logo :Local {
|
||||||
my ($self, $c) = @_;
|
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);
|
$c->serve_static_file($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,65 +8,83 @@ use Crypt::RandPasswd;
|
||||||
use Digest::SHA1 qw(sha1_hex);
|
use Digest::SHA1 qw(sha1_hex);
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
use LWP::UserAgent;
|
||||||
|
use JSON;
|
||||||
|
use HTML::Entities;
|
||||||
|
|
||||||
|
|
||||||
__PACKAGE__->config->{namespace} = '';
|
__PACKAGE__->config->{namespace} = '';
|
||||||
|
|
||||||
|
|
||||||
sub login :Local :Args(0) :ActionClass('REST::ForBrowsers') { }
|
sub login :Local :Args(0) :ActionClass('REST') { }
|
||||||
|
|
||||||
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_POST {
|
sub login_POST {
|
||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
my $username;
|
my $username = $c->stash->{params}->{username} // "";
|
||||||
my $password;
|
my $password = $c->stash->{params}->{password} // "";
|
||||||
|
|
||||||
$username = $c->stash->{params}->{username};
|
error($c, "You must specify a user name.") if $username eq "";
|
||||||
$password = $c->stash->{params}->{password};
|
error($c, "You must specify a password.") if $password eq "";
|
||||||
|
|
||||||
if ($username && $password) {
|
accessDenied($c, "Bad username or password.")
|
||||||
if ($c->authenticate({username => $username, password => $password})) {
|
if !$c->authenticate({username => $username, password => $password});
|
||||||
if ($c->request->looks_like_browser) {
|
|
||||||
backToReferer($c);
|
currentUser_GET($self, $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 {
|
sub logout_POST {
|
||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
|
$c->flash->{flashMsg} = "You are no longer signed in." if $c->user_exists();
|
||||||
$c->logout;
|
$c->logout;
|
||||||
if ($c->request->looks_like_browser) {
|
$self->status_no_content($c);
|
||||||
$c->response->redirect($c->request->referer || $c->uri_for('/'));
|
|
||||||
} else {
|
|
||||||
$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) = @_;
|
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 <tt>" . encode_entities($email) . "</tt>.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,57 +109,81 @@ sub setPassword {
|
||||||
sub register :Local Args(0) {
|
sub register :Local Args(0) {
|
||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
die "Not implemented!\n";
|
accessDenied($c, "User registration is currently not implemented.") unless isAdmin($c);
|
||||||
|
|
||||||
$c->stash->{template} = 'user.tt';
|
if ($c->request->method eq "GET") {
|
||||||
$c->stash->{create} = 1;
|
$c->stash->{template} = 'user.tt';
|
||||||
return if $c->request->method ne "POST";
|
$c->stash->{create} = 1;
|
||||||
|
return;
|
||||||
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 fail($c, "You did not enter the correct digits from the security image.")
|
die unless $c->request->method eq "PUT";
|
||||||
unless $c->validate_captcha($c->req->param('captcha'));
|
|
||||||
|
|
||||||
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$/;
|
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 });
|
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 {
|
txn_do($c->model('DB')->schema, sub {
|
||||||
my $user = $c->model('DB::Users')->create(
|
my $user = $c->model('DB::Users')->create(
|
||||||
{ username => $userName
|
{ username => $userName
|
||||||
, fullname => $fullName
|
|
||||||
, password => "!"
|
, password => "!"
|
||||||
, emailaddress => "",
|
, emailaddress => "",
|
||||||
|
, type => "hydra"
|
||||||
});
|
});
|
||||||
setPassword($user, $password);
|
updatePreferences($c, $user);
|
||||||
});
|
});
|
||||||
|
|
||||||
unless ($c->user_exists) {
|
unless ($c->user_exists) {
|
||||||
$c->authenticate({username => $userName, password => $password})
|
$c->set_authenticated({username => $userName})
|
||||||
or error($c, "Unable to authenticate the new user!");
|
or error($c, "Unable to authenticate the new user!");
|
||||||
}
|
}
|
||||||
|
|
||||||
$c->flash->{successMsg} = "User <tt>$userName</tt> has been created.";
|
$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);
|
requireUser($c);
|
||||||
|
|
||||||
$self->status_ok(
|
$self->status_ok($c,
|
||||||
$c,
|
|
||||||
entity => $c->model("DB::Users")->find($c->user->username)
|
entity => $c->model("DB::Users")->find($c->user->username)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -172,96 +213,69 @@ sub user :Chained('/') PathPart('user') CaptureArgs(1) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub deleteUser {
|
sub edit :Chained('user') :PathPart('') :Args(0) :ActionClass('REST::ForBrowsers') { }
|
||||||
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_GET {
|
sub edit_GET {
|
||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
my $user = $c->stash->{user};
|
|
||||||
|
|
||||||
$c->stash->{template} = 'user.tt';
|
$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 ($self, $c) = @_;
|
||||||
|
|
||||||
my $user = $c->stash->{user};
|
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") {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
my $fullName = trim $c->stash->{params}->{fullname};
|
|
||||||
|
|
||||||
txn_do($c->model('DB')->schema, sub {
|
txn_do($c->model('DB')->schema, sub {
|
||||||
|
updatePreferences($c, $user);
|
||||||
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) {
|
$c->flash->{successMsg} = "Your preferences have been updated.";
|
||||||
backToReferer($c);
|
$self->status_no_content($c);
|
||||||
} else {
|
}
|
||||||
$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;
|
1;
|
||||||
|
|
|
@ -90,7 +90,8 @@ sub getPreviousSuccessfulBuild {
|
||||||
|
|
||||||
|
|
||||||
sub error {
|
sub error {
|
||||||
my ($c, $msg) = @_;
|
my ($c, $msg, $status) = @_;
|
||||||
|
$c->response->status($status) if defined $status;
|
||||||
$c->error($msg);
|
$c->error($msg);
|
||||||
$c->detach; # doesn't return
|
$c->detach; # doesn't return
|
||||||
}
|
}
|
||||||
|
@ -98,15 +99,13 @@ sub error {
|
||||||
|
|
||||||
sub notFound {
|
sub notFound {
|
||||||
my ($c, $msg) = @_;
|
my ($c, $msg) = @_;
|
||||||
$c->response->status(404);
|
error($c, $msg, 404);
|
||||||
error($c, $msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub accessDenied {
|
sub accessDenied {
|
||||||
my ($c, $msg) = @_;
|
my ($c, $msg) = @_;
|
||||||
$c->response->status(403);
|
error($c, $msg, 403);
|
||||||
error($c, $msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,8 +120,7 @@ sub backToReferer {
|
||||||
sub forceLogin {
|
sub forceLogin {
|
||||||
my ($c) = @_;
|
my ($c) = @_;
|
||||||
$c->session->{referer} = $c->request->uri;
|
$c->session->{referer} = $c->request->uri;
|
||||||
$c->response->redirect($c->uri_for('/login'));
|
accessDenied($c, "This page requires you to sign in.");
|
||||||
$c->detach; # doesn't return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,8 @@ our @EXPORT = qw(
|
||||||
getPrimaryBuildsForView
|
getPrimaryBuildsForView
|
||||||
getPrimaryBuildTotal
|
getPrimaryBuildTotal
|
||||||
getViewResult getLatestSuccessfulViewResult
|
getViewResult getLatestSuccessfulViewResult
|
||||||
jobsetOverview removeAsciiEscapes getDrvLogPath findLog logContents
|
jobsetOverview jobsetOverview_
|
||||||
|
removeAsciiEscapes getDrvLogPath findLog logContents
|
||||||
getMainOutput
|
getMainOutput
|
||||||
getEvals getMachines
|
getEvals getMachines
|
||||||
pathIsInsidePrefix
|
pathIsInsidePrefix
|
||||||
|
@ -173,9 +174,9 @@ sub findLastJobForBuilds {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub jobsetOverview {
|
sub jobsetOverview_ {
|
||||||
my ($c, $project) = @_;
|
my ($c, $jobsets) = @_;
|
||||||
return $project->jobsets->search( isProjectOwner($c, $project) ? {} : { hidden => 0 },
|
return $jobsets->search({},
|
||||||
{ order_by => "name"
|
{ order_by => "name"
|
||||||
, "+select" =>
|
, "+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)"
|
[ "(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 {
|
sub getViewResult {
|
||||||
my ($primaryBuild, $jobs, $finished) = @_;
|
my ($primaryBuild, $jobs, $finished) = @_;
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,12 @@ __PACKAGE__->table("Users");
|
||||||
default_value: 0
|
default_value: 0
|
||||||
is_nullable: 0
|
is_nullable: 0
|
||||||
|
|
||||||
|
=head2 type
|
||||||
|
|
||||||
|
data_type: 'text'
|
||||||
|
default_value: 'hydra'
|
||||||
|
is_nullable: 0
|
||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
||||||
__PACKAGE__->add_columns(
|
__PACKAGE__->add_columns(
|
||||||
|
@ -74,6 +80,8 @@ __PACKAGE__->add_columns(
|
||||||
{ data_type => "text", is_nullable => 0 },
|
{ data_type => "text", is_nullable => 0 },
|
||||||
"emailonerror",
|
"emailonerror",
|
||||||
{ data_type => "integer", default_value => 0, is_nullable => 0 },
|
{ data_type => "integer", default_value => 0, is_nullable => 0 },
|
||||||
|
"type",
|
||||||
|
{ data_type => "text", default_value => "hydra", is_nullable => 0 },
|
||||||
);
|
);
|
||||||
|
|
||||||
=head1 PRIMARY KEY
|
=head1 PRIMARY KEY
|
||||||
|
@ -176,8 +184,8 @@ Composing rels: L</projectmembers> -> project
|
||||||
__PACKAGE__->many_to_many("projects", "projectmembers", "project");
|
__PACKAGE__->many_to_many("projects", "projectmembers", "project");
|
||||||
|
|
||||||
|
|
||||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
|
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-11-05 10:22:03
|
||||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Hv9Ukqud0d3uIUot0ErKeg
|
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Gd8KwFcnVShZ/WihvwfgQw
|
||||||
|
|
||||||
my %hint = (
|
my %hint = (
|
||||||
columns => [
|
columns => [
|
||||||
|
|
|
@ -452,7 +452,7 @@ BLOCK makePopover %]
|
||||||
|
|
||||||
BLOCK menuItem %]
|
BLOCK menuItem %]
|
||||||
<li class="[% IF "${root}${curUri}" == uri %]active[% END %]" [% IF confirmmsg %]onclick="javascript:return confirm('[% confirmmsg %]')"[% END %]>
|
<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 %]
|
[% IF icon %]<i class="[% icon %] icon-black"></i> [%+ END %]
|
||||||
[% title %]
|
[% title %]
|
||||||
</a>
|
</a>
|
||||||
|
@ -464,4 +464,65 @@ BLOCK makeStar %]
|
||||||
<span class="star" data-post="[% starUri %]">[% IF starred; "★"; ELSE; "☆"; END %]</span>
|
<span class="star" data-post="[% starUri %]">[% IF starred; "★"; ELSE; "☆"; END %]</span>
|
||||||
[% END;
|
[% 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;
|
||||||
|
|
||||||
|
|
||||||
%]
|
%]
|
||||||
|
|
17
src/root/dashboard-my-jobs-tab.tt
Normal file
17
src/root/dashboard-my-jobs-tab.tt
Normal 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 %]
|
12
src/root/dashboard-my-jobsets-tab.tt
Normal file
12
src/root/dashboard-my-jobsets-tab.tt
Normal 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 %]
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li class="active"><a href="#tabs-starred-jobs" data-toggle="tab">Starred jobs</a></li>
|
<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>
|
</ul>
|
||||||
|
|
||||||
<div id="generic-tabs" class="tab-content">
|
<div id="generic-tabs" class="tab-content">
|
||||||
|
@ -31,12 +33,17 @@
|
||||||
|
|
||||||
[% ELSE %]
|
[% 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 %]
|
[% END %]
|
||||||
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
<head>
|
<head>
|
||||||
<title>Hydra - [% HTML.escape(title) %]</title>
|
<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/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>
|
<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>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<div class="skip-topbar"></div>
|
||||||
|
|
||||||
[% IF flashMsg %]
|
[% IF flashMsg %]
|
||||||
<br />
|
|
||||||
<div class="alert alert-info">[% flashMsg %]</div>
|
<div class="alert alert-info">[% flashMsg %]</div>
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
||||||
[% IF successMsg %]
|
[% IF successMsg %]
|
||||||
<br />
|
|
||||||
<div class="alert alert-success">[% successMsg %]</div>
|
<div class="alert alert-success">[% successMsg %]</div>
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
||||||
[% IF errorMsg %]
|
[% IF errorMsg %]
|
||||||
<br />
|
|
||||||
<div class="alert alert-warning">Error: [% errorMsg %]</div>
|
<div class="alert alert-warning">Error: [% errorMsg %]</div>
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
||||||
[% IF !hideHeader %]
|
[% IF !hideHeader %]
|
||||||
<div class="page-header">
|
<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>
|
</div>
|
||||||
[% ELSE %]
|
[% ELSE %]
|
||||||
<br />
|
[% IF first %]<br />[% first = 0; END; %]
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
||||||
[% content %]
|
[% content %]
|
||||||
|
@ -95,13 +94,96 @@
|
||||||
<small>
|
<small>
|
||||||
<em><a href="http://nixos.org/hydra" target="_blank">Hydra</a> [% HTML.escape(version) %] (using [% HTML.escape(nixVersion) %]).</em>
|
<em><a href="http://nixos.org/hydra" target="_blank">Hydra</a> [% HTML.escape(version) %] (using [% HTML.escape(nixVersion) %]).</em>
|
||||||
[% IF c.user_exists %]
|
[% 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 %]
|
[% END %]
|
||||||
</small>
|
</small>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</div>
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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 %]
|
|
|
@ -13,7 +13,7 @@
|
||||||
</div>
|
</div>
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
||||||
<h2>Projects</h2>
|
<div class="page-header">Projects</div>
|
||||||
|
|
||||||
[% IF projects.size != 0 %]
|
[% IF projects.size != 0 %]
|
||||||
|
|
||||||
|
|
|
@ -30,64 +30,7 @@
|
||||||
<div id="tabs-project" class="tab-pane active">
|
<div id="tabs-project" class="tab-pane active">
|
||||||
[% IF project.jobsets %]
|
[% IF project.jobsets %]
|
||||||
<p>This project has the following jobsets:</p>
|
<p>This project has the following jobsets:</p>
|
||||||
|
[% INCLUDE renderJobsetOverview %]
|
||||||
<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>
|
|
||||||
|
|
||||||
[% ELSE %]
|
[% ELSE %]
|
||||||
<p>No jobsets have been defined yet.</p>
|
<p>No jobsets have been defined yet.</p>
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
@media (min-width: 768px) {
|
div.skip-topbar {
|
||||||
body {
|
padding-top: 40px;
|
||||||
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 {
|
div.page-header {
|
||||||
margin-top: 0em;
|
margin-top: 1em;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
padding-top: 0em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
div.page-header h1 {
|
color: #999999;
|
||||||
margin-bottom: 0em;
|
font-size: 123.809%;
|
||||||
}
|
font-weight: normal;
|
||||||
|
|
||||||
div.page-header h1 small {
|
|
||||||
font-size: 45%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell-prompt {
|
.shell-prompt {
|
||||||
|
@ -107,4 +110,4 @@ td.nowrap {
|
||||||
|
|
||||||
.star:hover {
|
.star:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,6 +135,7 @@ function requestJSON(args) {
|
||||||
bootbox.alert("Server error: " + escapeHTML(data.responseText));
|
bootbox.alert("Server error: " + escapeHTML(data.responseText));
|
||||||
else
|
else
|
||||||
bootbox.alert("Unknown server error!");
|
bootbox.alert("Unknown server error!");
|
||||||
|
if (args.postError) args.postError(data);
|
||||||
};
|
};
|
||||||
return $.ajax(args);
|
return $.ajax(args);
|
||||||
};
|
};
|
||||||
|
@ -145,3 +146,9 @@ function redirectJSON(args) {
|
||||||
};
|
};
|
||||||
return requestJSON(args);
|
return requestJSON(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function backToReferrer() {
|
||||||
|
// FIXME: should only do this if the referrer is another Hydra
|
||||||
|
// page.
|
||||||
|
window.location = document.referrer;
|
||||||
|
}
|
||||||
|
|
|
@ -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 %]
|
|
|
@ -119,9 +119,27 @@
|
||||||
|
|
||||||
[% IF c.user_exists %]
|
[% 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('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 %]
|
[% 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 %]
|
[% END %]
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
107
src/root/user.tt
107
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 %]
|
[% PROCESS common.tt %]
|
||||||
|
|
||||||
[% BLOCK roleoption %]
|
[% BLOCK roleoption %]
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
>[% role %]</option>
|
>[% role %]</option>
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
||||||
<form class="form-horizontal" method="post">
|
<form class="form-horizontal">
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
||||||
|
@ -30,10 +30,11 @@
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label">Full name</label>
|
<label class="control-label">Full name</label>
|
||||||
<div class="controls">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
[% IF create || user.type == 'hydra' %]
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label">Password</label>
|
<label class="control-label">Password</label>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
@ -47,31 +48,28 @@
|
||||||
<input type="password" class="span3" name="password2" value=""/>
|
<input type="password" class="span3" name="password2" value=""/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
[% END %]
|
||||||
|
|
||||||
<!--
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label">Email</label>
|
<label class="control-label">Email</label>
|
||||||
<div class="controls">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
|
|
||||||
[% IF !create %]
|
<div class="control-group">
|
||||||
<div class="control-group">
|
<div class="controls">
|
||||||
<div class="controls">
|
<label class="checkbox">
|
||||||
<label class="checkbox">
|
<input type="checkbox" name="emailonerror" [% IF !create && user.emailonerror; 'checked="checked"'; END %]/>Receive evaluation error notifications
|
||||||
<input type="checkbox" name="emailonerror" [% IF emailonerror; 'checked="checked"'; END %]/>Receive evaluation error notifications
|
</label>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
[% END %]
|
</div>
|
||||||
|
|
||||||
[% IF !create && c.check_user_roles('admin') %]
|
[% IF !create || c.check_user_roles('admin') %]
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label">Roles</label>
|
<label class="control-label">Roles</label>
|
||||||
<div class="controls">
|
<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="admin" %]
|
||||||
[% INCLUDE roleoption role="create-projects" %]
|
[% INCLUDE roleoption role="create-projects" %]
|
||||||
</select>
|
</select>
|
||||||
|
@ -79,7 +77,7 @@
|
||||||
</div>
|
</div>
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
||||||
[% IF create %]
|
[% IF create && !c.check_user_roles('admin') %]
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<img src="[% c.uri_for('/captcha') %]" alt="CAPTCHA"/>
|
<img src="[% c.uri_for('/captcha') %]" alt="CAPTCHA"/>
|
||||||
|
@ -95,44 +93,21 @@
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
||||||
<div class="form-actions">
|
<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>
|
<i class="icon-ok icon-white"></i>
|
||||||
[%IF create %]Create[% ELSE %]Apply changes[% END %]
|
[%IF create %]Create[% ELSE %]Apply changes[% END %]
|
||||||
</button>
|
</button>
|
||||||
[% IF !create && c.check_user_roles('admin') %]
|
[% IF !create && c.check_user_roles('admin') && user.type == 'hydra' %]
|
||||||
<button id="reset-password" type="submit" class="btn btn-warning" name="submit" value="reset-password">
|
<button id="reset-password" class="btn btn-warning">
|
||||||
<i class="icon-trash icon-white"></i>
|
<i class="icon-trash icon-white"></i>
|
||||||
Reset password
|
Reset password
|
||||||
</button>
|
</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 %]
|
[% END %]
|
||||||
[% IF !create %]
|
[% 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>
|
<i class="icon-trash icon-white"></i>
|
||||||
Delete this user
|
Delete this user
|
||||||
</button>
|
</button>
|
||||||
<script type="text/javascript">
|
|
||||||
$("#delete-user").click(function() {
|
|
||||||
return confirm("Are you sure you want to delete this user?");
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
[% END %]
|
[% END %]
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -140,4 +115,48 @@
|
||||||
|
|
||||||
</form>
|
</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 %]
|
[% END %]
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<table class="table table-striped table-condensed clickable-rows">
|
<table class="table table-striped table-condensed clickable-rows">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>User name</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Roles</th>
|
<th>Roles</th>
|
||||||
|
@ -14,9 +14,9 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
[% FOREACH u IN users %]
|
[% FOREACH u IN users %]
|
||||||
<tr>
|
<tr>
|
||||||
<td><a class="row-link" href="[% c.uri_for(c.controller('User').action_for('edit'), [u.username]) %]">[% u.username %]</a></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>[% u.fullname %]</td>
|
<td>[% HTML.escape(u.fullname) %]</td>
|
||||||
<td>[% u.emailaddress %]</td>
|
<td>[% HTML.escape(u.emailaddress) %]</td>
|
||||||
<td>[% FOREACH r IN u.userroles %]<i>[% r.role %]</i> [% END %]</td>
|
<td>[% FOREACH r IN u.userroles %]<i>[% r.role %]</i> [% END %]</td>
|
||||||
<td>[% IF u.emailonerror %]Yes[% ELSE %]No[% END %]</td>
|
<td>[% IF u.emailonerror %]Yes[% ELSE %]No[% END %]</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
EXTRA_DIST = \
|
EXTRA_DIST = \
|
||||||
$(distributable_scripts) \
|
$(distributable_scripts) \
|
||||||
hydra-control \
|
|
||||||
hydra-eval-guile-jobs.in
|
hydra-eval-guile-jobs.in
|
||||||
|
|
||||||
distributable_scripts = \
|
distributable_scripts = \
|
||||||
|
@ -11,6 +10,7 @@ distributable_scripts = \
|
||||||
hydra-server \
|
hydra-server \
|
||||||
hydra-update-gc-roots \
|
hydra-update-gc-roots \
|
||||||
hydra-s3-backup-collect-garbage \
|
hydra-s3-backup-collect-garbage \
|
||||||
|
hydra-create-user \
|
||||||
nix-prefetch-git \
|
nix-prefetch-git \
|
||||||
nix-prefetch-bzr \
|
nix-prefetch-bzr \
|
||||||
nix-prefetch-hg
|
nix-prefetch-hg
|
||||||
|
|
|
@ -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
|
|
|
@ -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
88
src/script/hydra-create-user
Executable 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;
|
||||||
|
});
|
|
@ -9,7 +9,8 @@ create table Users (
|
||||||
fullName text,
|
fullName text,
|
||||||
emailAddress text not null,
|
emailAddress text not null,
|
||||||
password text not null, -- sha256 hash
|
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
1
src/sql/upgrade-25.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
alter table Users add column type text not null default 'hydra';
|
|
@ -27,7 +27,7 @@ TESTS = \
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
chmod -R a+w nix || true
|
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
|
rm -f .*-state
|
||||||
|
|
||||||
check_SCRIPTS = db.sqlite repos
|
check_SCRIPTS = db.sqlite repos
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use LWP::UserAgent;
|
use LWP::UserAgent;
|
||||||
use JSON;
|
use JSON;
|
||||||
use Test::Simple tests => 16;
|
use Test::Simple tests => 17;
|
||||||
|
|
||||||
my $ua = LWP::UserAgent->new;
|
my $ua = LWP::UserAgent->new;
|
||||||
$ua->cookie_jar({});
|
$ua->cookie_jar({});
|
||||||
|
@ -17,6 +17,9 @@ sub request_json {
|
||||||
return $res;
|
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 $result = request_json({ uri => "/login", method => "POST", data => { username => "root", password => "foobar" } });
|
||||||
|
|
||||||
my $user = decode_json($result->content());
|
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");
|
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' });
|
$result = request_json({ uri => '/jobset/sample/default/evals' });
|
||||||
ok($result->code() == 200, "Can get evals of a jobset");
|
ok($result->code() == 200, "Can get evals of a jobset");
|
||||||
my $evals = decode_json($result->content())->{evals};
|
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
|
# Ugh, cached for 30s
|
||||||
sleep 30;
|
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};
|
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");
|
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());
|
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->{job} eq "job", "The build's job name is job");
|
||||||
ok($build->{finished} == 0, "The build isn't finished yet");
|
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'");
|
||||||
|
|
Loading…
Reference in a new issue