--- /dev/null
+Web GUI for Varnish, very limited christmas edition
+===================================================
+
+This is a preview of the upcoming web GUI for Varnish, which will be released with version 2.1 It has most of the features intended for the final version, but a number of things are still missing, so it is just inteded to get feedback. The following known things are _not_ implemented
+ - saving of state: if the web server goes down, the information is lost.
+ - smart data collection: it stores ALL the polled data with the given poll intervlal. It does not truncate data used for the hour, day, week and month graph. So eventually you will go out of memory.
+ - simple configuration: currently, you must edit the start.pl script by hand to set the configuration, and edit Varnish/RequestHandler.pl to add graphs and summary statistics.
+ - security: there is no form for access control or security implemented.
+
+So I wouldn't use it for anything with 'prod' in its name, but as a preview of what is to come. It should be noted that when adding nodes to an existing group, all the parameters and VCL of that node are replaced with the default parameters and VCL of the group. So if you add more nodes to a group, be aware of this.
+
+Any feedback is most welcome, so just post it to varnish-misc@projects.linpro.no.
+
+So what can you actually do with this thing?
+
+
+Overview
+--------
+
+The web GUI is written in Perl and runs in its own server container, so a web server is not required, only the necessary Perl modules. The GUI consists of five sections: 'View stats', 'Configure parameters', 'Edit VCL', 'Node management' and 'Management console'. As the names might suggest, the GUI let you
+
+- view statistics from the Varnish nodes
+- configure the parameters of the Varnish nodes and clusters
+- edit the VCL for each cluster, which will be shared between all the nodes
+- perform node management, like adding clusters and nodes, get the state of the nodes and backend healths
+- get access to the management console of each node
+
+Requirements
+------------
+
+The web GUI is written in Perl, and uses some modules that might not be installed by default:
+
+- HTTP::Daemon (libwww-perl)
+- HTML::Template (libhtml-template-perl)
+- GD::Graph (libgd-graph-perl)
+- LWP::UserAgent (libwww-perl)
+
+The name in the paranthesis are the package name on a standard Ubuntu system. The rest of the modules should be pretty standard.
+
+As the management port of the Varnish instances are used for collecting data, this must be enaled when starting varnish. This is done with the -T option to varnish, e.g. like this
+
+$ /opt/varnish/sbin/varnishd -a :80 -b :8080 -T :9003
+
+Configuration
+-------------
+
+The configuration is done directly in the Perl code for this very limited christmas edition, but will be more userfriendly in the final version. As this version doesn't save your state, e.g. the nodes and clusters added, I would recommend adding this in the start.pl. The files needing customisation are
+
+- start.pl: early in the file you'll see '# Configuration starts here', and this is where to set the config. It is all commented, so should be fairly straight forward.
+- Varnish/RequestHandler.pl: this is the main enging of the whole web GUI, including the parts creating the summary stats and graphs. This is well documented in the code, so look at line 380 for adding values to the summary statistics and 826 for adding custom graphs (and removing the dummy 'Missing graph').
+
+That is configuration for this version. Remember that when creating and editing VCLs, the information is stored on the node, so if the web server is restarted, the nodes will still have the VCLs (unless they are restarted too, of course).
+
+Starting it
+-----------
+
+'perl start.pl' shoud do it, after reading the Configuration section and setting the values to something reasonable for your setup. If the shebang matches your perl, and the script is executable, './start.pl' should also do it.
+
+The GUI
+======
+
+View stats
+----------
+
+In 'View stats' you will see statistics gathered from the nodes. The 'Summary statistics' is/will be customisable (see Configuration) and shows the most important statistics. If you want to see all the statistics from the Varnish nodes you turn 'Raw statistics' on. If you want the page to be refreshed automatically in order to follow the graph 'live', you can turn 'Auto refresh' on. The refresh rate is the same as the rate the statistics are collected from the nodes.
+
+Configure parameters
+--------------------
+
+'Configure' parameters let you configure the parameters for a group (cluster) or a single node. For this version, the parameters for the groups are a copy of the parameters of the first node added to the group, so if a group has not been populated, it will not contain any values. Changing a value in a group will change the same value of all the nodes in that group. Changing a value for node only changes that node, naturally.
+
+Edit VCL
+--------
+
+'Edit VCL' lets you edit the VCL of the group, and any changes made here will be reflected on all the nodes of the group. You can add, edit, save and discard VCLs as well as making a VCL active. It discards without warning (isn't baby safe yet), so be carefull.
+
+A note about the editor: it is a simple text are, with <tab> giving you an indent as expected. If you save a VCL with error, the errors are listed and you can click on the 'Line X Pos Y' information in the error list to jump to that position in the editor.
+
+
+Node management
+---------------
+
+From 'Node management' you can manage groups and nodes. It will let you add, remove, stop and start the nodes and groups. It also displays information about the state of the node:
+ - the V column is the state of Varnish. Green means it responds with a 200 message to a probe (it probes http://varnish-host/), a yellow light means it anwers with a non-200 status code and a red light means it does not respond at all.
+ - the M columns is the state of the management port, with green meaning OK and red meaning it is not reachable. As almost all functionality is dependent on the management port, it should never have a red light.
+
+ If a health probe is defined in the VCL, a list of backend healths for the running VCL is shown as well.
+
+ Since this version does not let you save the configuration, it is recommended that you add groups and nodes in the stat.pl file as explained in Configuration.
+
+ WARNING: as noted earlier, adding nodes to a group will replace that nodes parameter and VCL.
+
+Management console
+------------------
+
+'Management console' gives you access to the management console of each node. The input field has magical tab completion for the CLI commands and a command history accessable by arrow up/down. In addition to the standard CLI commands, 'cls' can be issued to clear the console.
+
+The color and size of the console can be changed, but this information is unfortunately not stored when switching nodes.
+
+
+Road ahead
+==========
+
+This is a sneak preview of the web GUI for 2.1, and as such is not complete. It is mostly feature complete (minus things mentioned in the start of this file), so any comments on what is working, what is lacking etc. is most appreciated. Please use the varnish-misc@projects.linpro.no mailing list for discussion.
+
+
+Merry christmas!
--- /dev/null
+package Varnish::Management;
+
+use strict;
+use IO::Socket::INET;
+use Exporter;
+use List::Util qw(first);
+
+{
+ my %hostname_of;
+ my %port_of;
+ my %error_of;
+ my %socket_of;
+
+ sub new {
+ my ($class, $hostname, $port) = @_;
+
+ my $new_object = bless \do{ my $anon_scalar; }, $class;
+
+ $hostname_of{$new_object} = $hostname;
+ $port_of{$new_object} = $port;
+ $error_of{$new_object} = "";
+
+ return $new_object;
+ }
+
+ sub _send_command {
+ my ($self, $command) = @_;
+
+ if (!$socket_of{$self} || !$socket_of{$self}->connected ) {
+ my $socket = new IO::Socket::INET->new(
+ PeerPort => $port_of{$self},
+ Proto => 'tcp',
+ PeerAddr => $hostname_of{$self}
+ );
+ return ("666", "Could not connect to node") if (!$socket);
+ $socket_of{$self} = $socket;
+ }
+ my $socket = $socket_of{$self};
+
+ print $socket "$command\n";
+ my ($status_code, $response_size) = <$socket> =~ m/^(\d+) (\d+)/;
+ my $response;
+ my $remaining_bytes = $response_size;
+ while ($remaining_bytes > 0 ) {
+ my $data;
+ my $read = read $socket, $data, $remaining_bytes;
+ $response .= $data;
+ $remaining_bytes -= $read;
+ }
+ my $eat_newline = <$socket>;
+ return ($status_code, $response);
+ }
+
+ sub send_command {
+ my ($self, $command) = @_;
+
+ my ($status_code, $response) = _send_command($self, $command);
+
+ return no_error($self, $response) if $status_code eq "200";
+ return set_error($self, $response);
+ }
+
+ sub get_parameters {
+ my ($self) = @_;
+
+ my %param;
+ my $current_param;
+
+ my ($status_code, $response) = _send_command($self, "param.show -l");
+ return set_error($self, $response) if ($status_code ne "200");
+ for my $line (split( '\n', $response)) {
+
+ if ($line =~ /^(\w+)\s+(\w+) (.*)$/) {
+ my %param_info = (
+ value => $2,
+ unit => $3
+ );
+
+ $current_param = $1;
+ $param{$1} = \%param_info;
+ }
+ elsif ($line =~ /^\s+(.+)$/) {
+# The first comment line contains no . and describes the default value.
+ if (!$param{$current_param}->{'description'}) {
+ $param{$current_param}->{'description'} = "$1. ";
+ }
+ else {
+ $param{$current_param}->{'description'} .= "$1 ";
+ }
+ }
+ }
+
+ return \%param;
+ }
+
+ sub get_parameter($) {
+ my ($self, $parameter) = @_;
+
+ my ($status_code, $response) = _send_command($self, "param.show $parameter");
+
+ return no_error($self, $1) if ($response =~ /^(?:\w+)\s+(\w+)/);
+ return set_error($self, $response);
+ }
+
+ sub set_parameter {
+ my ($self, $parameter, $value) = @_;
+
+ my ($status_code, $response) = _send_command($self, "param.set $parameter $value");
+
+ return no_error($self) if ($status_code eq "200");
+ return set_error($self, $response);
+ }
+
+ sub get_vcl_names {
+ my ($self) = @_;
+
+ my ($status_code, $response) = _send_command($self, "vcl.list");
+ return set_error($self, $response) if ($status_code ne "200");
+
+ my @vcl_infos = ($response =~ /^(\w+)\s+\d+\s+(\w+)$/gm);
+ my $vcl_names_ref = [];
+ my $active_vcl_name = "";
+ while (my ($status, $name) = splice @vcl_infos, 0, 2) {
+ next if ($status eq "discarded");
+
+ if ($status eq "active") {
+ $active_vcl_name = $name;
+ }
+ push @$vcl_names_ref, $name;
+ }
+
+ unshift @$vcl_names_ref, $active_vcl_name;
+ return no_error($self, $vcl_names_ref) if ($status_code eq "200");
+ }
+
+ sub get_vcl {
+ my ($self, $vcl_name) = @_;
+
+ my ($status_code, $response) = _send_command($self, "vcl.show $vcl_name");
+
+ return no_error($self, $response) if ($status_code eq "200");
+ return set_error($self, $response);
+ }
+
+ sub set_vcl {
+ my ($self, $vcl_name, $vcl) = @_;
+ $vcl =~ s/"/\\"/g;
+ $vcl =~ s/\r//g;
+ $vcl =~ s/\n/\\n/g;
+
+ my $need_restart = 0;
+ my ($active_vcl_name, @vcl_names) = @{get_vcl_names($self)};
+ my $editing_active_vcl = $vcl_name eq $active_vcl_name;
+
+ # try to compile the new vcl
+ my ($status_code, $response) = _send_command($self, "vcl.inline _new_vcl \"$vcl\"");
+ if ($status_code ne "200") {
+ _send_command($self, "vcl.discard _new_vcl");
+ return set_error($self, $response);
+ }
+
+ if ($editing_active_vcl) {
+ ($status_code, $response) = _send_command($self, "vcl.use _new_vcl");
+ }
+
+ if (grep { $_ eq $vcl_name } @vcl_names) {
+ ($status_code, $response) = _send_command($self, "vcl.discard $vcl_name");
+ if ($status_code ne "200") {
+ _send_command($self, "vcl.use $vcl_name");
+ _send_command($self, "vcl.discard _new_vcl");
+ return set_error($self, $response);
+ }
+ }
+ ($status_code, $response) = _send_command($self, "vcl.inline $vcl_name \"$vcl\"");
+
+ if ($editing_active_vcl) {
+ ($status_code, $response) = _send_command($self, "vcl.use $vcl_name");
+ }
+ _send_command($self, "vcl.discard _new_vcl");
+
+ return no_error($self) if ($status_code eq "200");
+ return set_error($self, $response);
+ }
+
+ sub discard_vcl {
+ my ($self, $vcl_name) = @_;
+
+ my ($status_code, $response) = _send_command($self, "vcl.discard $vcl_name");
+
+ return no_error($self) if ($status_code eq "200");
+ return set_error($self, $response);
+ }
+
+ sub make_vcl_active {
+ my ($self, $vcl_name) = @_;
+
+ my ($status_code, $response) = _send_command($self, "vcl.use $vcl_name");
+
+ return no_error($self) if ($status_code eq "200");
+ return set_error($self, $response);
+ }
+
+ sub get_stats {
+ my ($self) = @_;
+
+ my ($status_code, $response) = _send_command($self, "stats");
+
+ my %stat_counter = map {
+ /^\s*(\d+)\s+(.*?)$/;
+ $2 => $1
+ } split /\n/, $response;
+
+ return no_error($self, \%stat_counter) if ($status_code eq "200");
+ return set_error($self, $response);
+ }
+
+ sub ping {
+ my ($self) = @_;
+
+ my ($status_code, $response) = _send_command($self, "stats");
+
+ return no_error($self) if ($status_code eq "200");
+ return set_error($self, $response);
+ }
+
+ sub start {
+ my ($self) = @_;
+
+ my ($status_code, $response) = _send_command($self, "start");
+
+ return no_error($self) if ($status_code eq "200");
+ return set_error($self, $response);
+ }
+
+ sub stop {
+ my ($self) = @_;
+
+ my ($status_code, $response) = _send_command($self, "stop");
+
+ return no_error($self) if ($status_code eq "200");
+ return set_error($self, $response);
+ }
+
+ sub set_error {
+ my ($self, $error) = @_;
+
+ $error_of{$self} = $error;
+
+ return;
+ }
+
+ sub get_error {
+ my ($self) = @_;
+
+ return $error_of{$self};
+ }
+
+ sub no_error {
+ my ($self, $return_value) = @_;
+
+ $error_of{$self} = "";
+
+ return defined($return_value) ? $return_value : 1;
+ }
+
+ sub close {
+ my ($self) = @_;
+
+ if ($socket_of{$self} && $socket_of{$self}->connected) {
+ $socket_of{$self}->close();
+ }
+ }
+
+ sub get_backend_health {
+ my ($self) = @_;
+
+ my ($status_code, $response) = _send_command($self, "debug.health");
+ return set_error($self, $response) if ($status_code ne "200");
+
+ my %backend_health = ($response =~ /^Backend (\w+) is (\w+)$/gm);
+
+ return no_error($self, \%backend_health);
+ }
+}
+
+1;
--- /dev/null
+package Varnish::Node;
+
+use strict;
+use LWP::UserAgent;
+use Varnish::Management;
+
+{
+ my %name_of;
+ my %address_of;
+ my %port_of;
+ my %group_of;
+ my %management_of;
+ my %management_port_of;
+ my %is_master_of;
+ my %node_id_of;
+
+ my $next_node_id = 1;
+
+ sub new {
+ my ($class, $arg_ref) = @_;
+
+ my $new_object = bless \do{ my $anon_scalar; }, $class;
+
+ $name_of{$new_object} = $arg_ref->{'name'};
+ $address_of{$new_object} = $arg_ref->{'address'};
+ $port_of{$new_object} = $arg_ref->{'port'};
+ $group_of{$new_object} = $arg_ref->{'group'};
+ $management_port_of{$new_object} = $arg_ref->{'management_port'};
+ $management_of{$new_object} = Varnish::Management->new($arg_ref->{'address'},
+ $arg_ref->{'management_port'});
+ $is_master_of{$new_object} = 0;
+ $node_id_of{$new_object} = $next_node_id++;
+
+ return $new_object;
+ }
+
+ sub get_id {
+ my ($self) = @_;
+
+ return $node_id_of{$self};
+ }
+
+ sub get_name {
+ my ($self) = @_;
+
+ return $name_of{$self};
+ }
+
+ sub get_address {
+ my ($self) = @_;
+
+ return $address_of{$self};
+ }
+
+ sub get_port {
+ my ($self) = @_;
+
+ return $port_of{$self};
+ }
+
+ sub get_group {
+ my ($self) = @_;
+
+ return $group_of{$self};
+ }
+
+ sub get_management {
+ my ($self) = @_;
+
+ return $management_of{$self};
+ }
+
+ sub get_management_port {
+ my ($self) = @_;
+
+ return $management_port_of{$self};
+ }
+
+ sub is_master {
+ my ($self) = @_;
+
+ return $is_master_of{$self};
+ }
+
+ sub set_master {
+ my ($self, $master) = @_;
+
+ $is_master_of{$self} = $master;
+ }
+
+ sub is_running_ok {
+ my ($self) = @_;
+
+ my $user_agent = LWP::UserAgent->new;
+ $user_agent->timeout(1);
+
+ my $url = 'http://' . get_address($self) . ':' . get_port($self);
+ my $response = $user_agent->head($url);
+ return $response->is_success;
+ }
+
+ sub is_running {
+ my ($self) = @_;
+
+ my $user_agent = LWP::UserAgent->new;
+ $user_agent->timeout(1);
+
+ my $url = 'http://' . get_address($self) . ':' . get_port($self);
+ my $response = $user_agent->head($url);
+ return $response->code != 500;
+ }
+
+
+ sub is_management_running {
+ my ($self) = @_;
+
+ my $management = get_management($self);
+ if ($management) {
+ my $ping = $management->ping();
+ return defined($ping) && $ping;
+ }
+ else {
+ return 0;
+ }
+ }
+}
+
+1;
--- /dev/null
+package Varnish::NodeManager;
+use strict;
+use warnings;
+use Varnish::Node;
+use List::Util qw(first);
+
+{
+ my @groups = ();
+ my @nodes = ();
+ my %group_parameters = ();
+
+ my $error = "";
+
+ sub add_node {
+ my ($self, $node) = @_;
+
+ my $group = $node->get_group();
+ if (! grep { $_->get_group eq $group } @nodes ) {
+ $node->set_master(1);
+ my %node_parameters = %{$node->get_management()->get_parameters()};
+ while (my ($parameter, $value) = each %node_parameters) {
+ $group_parameters{$group}->{$parameter} = $value;
+ }
+ }
+ else {
+# inherit the VCL and the parameters of the group
+ my %group_parameters = %{$group_parameters{$group}};
+ my $management = $node->get_management();
+ while (my ($parameter, $value) = each %group_parameters) {
+ $management->set_parameter($parameter, $value->{'value'});
+ }
+
+ my $vcl_names_ref = $management->get_vcl_names();
+ my $active_vcl_name;
+ my @vcl_names;
+ if ($vcl_names_ref) {
+ @vcl_names = @{$vcl_names_ref};
+ $active_vcl_name = shift @vcl_names;
+ }
+
+ for my $vcl_name (@vcl_names) {
+ if ($vcl_name ne $active_vcl_name) {
+ $management->discard_vcl($vcl_name);
+ }
+ }
+
+ my $discard_active_vcl = 1;
+ my $group_master = first {
+ $_->get_group() eq $group
+ && $_->is_master()
+ } @nodes;
+ my $master_management = $group_master->get_management();
+ my $master_active_vcl_name;
+ my @master_vcl_names;
+ my $master_vcl_names_ref = $group_master->get_management()->get_vcl_names();
+ if ($master_vcl_names_ref) {
+ @master_vcl_names = @{$master_vcl_names_ref};
+ $master_active_vcl_name = shift @master_vcl_names;
+ }
+
+ for my $vcl_name (@master_vcl_names) {
+ my $vcl = $master_management->get_vcl($vcl_name);
+ $management->set_vcl($vcl_name, $vcl);
+
+ if ($vcl_name eq $master_active_vcl_name) {
+ $management->make_vcl_active($vcl_name);
+ }
+ if ($vcl_name eq $active_vcl_name) {
+ $discard_active_vcl = 0;
+ }
+ }
+
+ if ($discard_active_vcl) {
+ $management->discard_vcl($active_vcl_name);
+ }
+ }
+
+ push @nodes, $node;
+ }
+
+ sub remove_node {
+ my ($self, $node) = @_;
+
+ if ($node) {
+ @nodes = grep { $_ != $node } @nodes;
+
+ if ($node->is_master()) {
+ my $new_master = first {
+ $_->is_master
+ && $_->get_group() eq $node->get_group()
+ } @nodes;
+ if ($new_master) {
+ $new_master->set_master(1);
+ }
+ }
+ }
+ }
+
+ sub get_node {
+ my ($self, $node_id) = @_;
+
+ my $node = first {
+ $_->get_id() == $node_id
+ } @nodes;
+
+ return $node;
+ }
+
+ sub add_group {
+ my ($self, $name) = @_;
+
+ push @groups, $name;
+ }
+
+ sub remove_group {
+ my ($self, $name) = @_;
+
+ @groups = grep { $_ ne $name } @groups;
+ my @nodes_to_remove = grep { $_->get_group() eq $name } @nodes;
+ for my $node (@nodes_to_remove) {
+ remove_node($self, $node);
+ }
+ }
+
+ sub get_groups {
+
+ return @groups;
+ }
+
+ sub get_nodes {
+
+ return @nodes;
+ }
+
+ sub get_nodes_for_group {
+ my ($self, $group) = @_;
+
+ return grep { $_->get_group() eq $group } @nodes;
+ }
+
+ sub get_group_masters {
+ my ($self) = @_;
+
+ return grep { $_->is_master() } @nodes;
+ }
+
+ sub load {
+
+
+ }
+
+ sub save {
+ my ($self) = @_;
+
+ }
+
+ sub quit {
+ my ($self) = @_;
+
+ for my $node (@nodes) {
+ my $management = $node->get_management();
+ if ($management) {
+ $management->close();
+ }
+ }
+
+ save($self);
+ }
+
+ sub set_error {
+ my ($self, $new_error) = @_;
+
+ $error = $new_error;
+
+ return;
+ }
+
+ sub get_error {
+ my ($self) = @_;
+
+ return $error;
+ }
+
+ sub no_error {
+ my ($self, $return_value) = @_;
+
+ $error = "";
+
+ return defined($return_value) ? $return_value : 1;
+ }
+
+ sub set_group_parameter {
+ my ($self, $group, $parameter, $value) = @_;
+
+ my $error;
+
+ $group_parameters{$group}->{$parameter}->{'value'} = $value;
+ my @nodes_in_group = grep { $_->get_group() eq $group } @nodes;
+ for my $node (@nodes_in_group) {
+ my $management = $node->get_management();
+ if (!$management->set_parameter($parameter, $value)) {
+ $error .= $management->get_error() . "\n";
+ }
+ }
+
+ if ($error) {
+ return set_error($self, $error);
+ }
+ else {
+ return no_error();
+ }
+ }
+
+ sub get_group_parameters {
+ my ($self, $group) = @_;
+
+ return $group_parameters{$group};
+ }
+}
+
+1;
--- /dev/null
+package Varnish::RequestHandler;
+
+use strict;
+use warnings;
+use HTML::Template;
+use HTTP::Request;
+use Varnish::Util;
+use Varnish::Management;
+use Varnish::NodeManager;
+use Varnish::Node;
+use Varnish::Statistics;
+use URI::Escape;
+use GD::Graph::lines;
+use POSIX qw(strftime);
+use List::Util qw(first);
+
+{
+ my %request_ref_of;
+ my %response_content_ref_of;
+ my %response_header_ref_of;
+ my %master_tmpl_var_of;
+
+ sub new {
+ my ($class, $request_ref, $connection) = @_;
+
+ my $new_object = bless \do{ my $anon_scalar; }, $class;
+
+ $request_ref_of{$new_object} = $request_ref;
+ $response_content_ref_of{$new_object} = \"";
+ $response_header_ref_of{$new_object} = {};
+
+ $master_tmpl_var_of{$new_object}->{'server_host'} = $connection->sockhost();
+ $master_tmpl_var_of{$new_object}->{'server_port'} = $connection->sockport();
+
+ return $new_object;
+ }
+
+ sub DESTROY {
+ my ($self) = @_;
+
+ delete $request_ref_of{$self};
+
+ return;
+ }
+
+ sub get_response_header {
+ my ($self) = @_;
+
+ return $response_header_ref_of{$self};
+ }
+ sub get_response_content {
+ my ($self) = @_;
+
+ return ${$response_content_ref_of{$self}};
+ }
+
+ sub _parse_request_parameters {
+ my ($content_ref) = @_;
+
+ my %parameter = ();
+ for my $pair (split /&/,$$content_ref) {
+ my ($key,$value) = split /=/,$pair;
+ $value = uri_unescape($value);
+ $value =~ s/\+/ /g;
+ $parameter{$key} = $value;
+ }
+
+ return %parameter;
+ }
+
+ sub process {
+ my ($self) = @_;
+
+ my $request = ${$request_ref_of{$self}};
+ my $operation = $request->uri();
+ my $content = $request->content();
+
+ $operation =~ s:^/::;
+ if ($operation =~ /^(.*?)\?(.*)/) {
+ $operation = $1;
+ $content = $2;
+ }
+
+ my $content_template;
+ my $response_content;
+ my %request_parameter = _parse_request_parameters(\$content);
+
+# while (my ($k, $v) = each %request_parameter) {
+# print "$k => $v\n";
+# }
+
+ my $param;
+ if ($operation eq 'view_stats' || $operation eq '') {
+ ($content_template, $param) = view_stats(\%request_parameter);
+ }
+ elsif ($operation eq 'configure_parameters') {
+ ($content_template, $param) = configure_parameters(\%request_parameter);
+ }
+ elsif ($operation eq 'edit_vcl') {
+ ($content_template, $param) = edit_vcl(\%request_parameter);
+ }
+ elsif ($operation eq 'node_management') {
+ ($content_template, $param) = node_management(\%request_parameter);
+ }
+ elsif ($operation eq 'management_console') {
+ ($content_template, $param) = management_console(\%request_parameter);
+ }
+ elsif ($operation eq 'send_management_command') {
+ $response_content = send_management_command(\%request_parameter);
+ }
+ elsif ($operation eq 'generate_graph') {
+ $response_header_ref_of{$self}->{'Content-Type'} = "image/png";
+ $response_content = generate_graph(\%request_parameter);
+ if (!$response_content) {
+ $response_content = read_file('images/nograph.png');
+ }
+ }
+ elsif ($operation eq 'collect_data') {
+ Varnish::Statistics->collect_data();
+ $response_content = "Ok";
+ }
+ else {
+ return;
+ }
+
+ if ($content_template) {
+ my $template_text = read_file("templates/master.tmpl");
+ $template_text =~ s/CONTENT_TEMPLATE/$content_template/;
+
+ my $template = HTML::Template->new_scalar_ref( \$template_text,
+ die_on_bad_params => 0);
+
+ my $tmpl_var = $master_tmpl_var_of{$self};
+ if ($param) {
+ while (my ($parameter, $value) = each %{$param}) {
+ $tmpl_var->{$parameter} = $value;
+ }
+ }
+ $template->param($tmpl_var);
+ $response_content = $template->output;
+ }
+ $response_content_ref_of{$self} = \$response_content;
+ }
+
+ sub edit_vcl {
+ my ($parameter_ref) = @_;
+
+ my %param = %{$parameter_ref};
+ $param{'vcl'} ||= "";
+ $param{'operation'} ||= "load";
+ $param{'group_name'} ||= "";
+ $param{'vcl_name'} ||= "";
+ $param{'new_vcl_name'} ||= "";
+
+
+ my $template = "edit_vcl.tmpl";
+ my %tmpl_var;
+ $tmpl_var{'error'} = "";
+ $tmpl_var{'vcl_name'} = $param{'vcl_name'};
+ $tmpl_var{'status'} = "";
+ $tmpl_var{'vcl_infos'} = [];
+ $tmpl_var{'group_infos'} = [];
+ $tmpl_var{'vcl_error'} = "";
+ $tmpl_var{'vcl'} = "";
+
+ my $successfull_save = 0;
+ my $editing_new_vcl = 0;
+
+ my @group_masters = Varnish::NodeManager->get_group_masters();
+ if ($param{'operation'} eq "make_active") {
+ my $group_master = first {
+ $_->get_group() eq $param{'group_name'};
+ } @group_masters;
+
+ if ($group_master) {
+ my $management = $group_master->get_management();
+ if ($management->make_vcl_active($param{'vcl_name'})) {
+ my @nodes = Varnish::NodeManager->get_nodes();
+ for my $node (@nodes) {
+ if ($node != $group_master && $node->get_group() eq $param{'group_name'}) {
+ $node->get_management()->make_vcl_active($param{'vcl_name'});
+ }
+ }
+ $tmpl_var{'status'} = "VCL activated successfully";
+ }
+ else {
+ $tmpl_var{'error'} .= "Error activating configuration:\n" . $management->get_error() . "\n";
+ }
+ }
+ }
+ elsif ($param{'operation'} eq 'new') {
+ if ($param{'new_vcl_name'}) {
+ $tmpl_var{'vcl_name'} = $param{'new_vcl_name'};
+ $tmpl_var{'vcl'} = "";
+ push @{$tmpl_var{'vcl_infos'}}, {
+ name => $param{'new_vcl_name'},
+ selected => 1,
+ active => 0
+ };
+ $editing_new_vcl = 1;
+ }
+ }
+ elsif ($param{'operation'} eq 'save') {
+ my $group_master = first {
+ $_->get_group() eq $param{'group_name'}
+ } @group_masters;
+
+ if ($group_master && $param{'vcl'} ne "" && $param{'vcl_name'} ne "") {
+ my $master_management = $group_master->get_management();
+ if ($master_management->set_vcl($param{'vcl_name'}, $param{'vcl'})) {
+ my @nodes = Varnish::NodeManager->get_nodes();
+ for my $node (@nodes) {
+ if ($node != $group_master && $node->get_group() eq $param{'group_name'}) {
+ my $management = $node->get_management();
+ if (!$management->set_vcl($param{'vcl_name'}, $param{'vcl'})) {
+ $tmpl_var{'error'} .= "Error saving configuration for " . $node->get_name()
+ . ":\n" . $management->get_error() . "\n";
+ }
+ }
+ }
+ $successfull_save = 1;
+ $tmpl_var{'status'} = "VCL saved successfully";
+ }
+ else {
+ push @{$tmpl_var{'vcl_infos'}}, {
+ name => $param{'vcl_name'},
+ selected => 1,
+ active => 0
+ };
+ $editing_new_vcl = 1;
+ $tmpl_var{'vcl'} = $param{'vcl'};
+ my $vcl_error = $master_management->get_error();
+ # it is bad bad bad mixing presentation and code, I know, but sometimes you have to
+ $vcl_error =~ s/Line (\d+) Pos (\d+)/<a href="javascript:goToPosition($1,$2)">$&<\/a>/g;
+ $vcl_error =~ s/\n/<br\/>/g;
+ $tmpl_var{'vcl_error'} = $vcl_error;
+ }
+ }
+ }
+ elsif ($param{'operation'} eq "discard") {
+ my ($group_master) = grep {
+ $_->get_group() eq $param{'group_name'}
+ } @group_masters;
+ if ($group_master && $param{'vcl_name'} ne "") {
+ my $management = $group_master->get_management();
+ if ($management->discard_vcl($param{'vcl_name'})) {
+ my @nodes = Varnish::NodeManager->get_nodes();
+ for my $node (@nodes) {
+ if ($node != $group_master && $node->get_group() eq $param{'group_name'}) {
+ $node->get_management()->discard_vcl($param{'vcl_name'});
+ }
+ }
+
+ $tmpl_var{'vcl_name'} = "";
+ $tmpl_var{'status'} = "VCL discarded successfully";
+ }
+ else {
+ $tmpl_var{'error'} .= "Error discarding configuration:\n" . $management->get_error() . "\n";
+ }
+ }
+ }
+
+ my $selected_group_master;
+ for my $group_master (@group_masters) {
+ my %group_info = (
+ name => $group_master->get_group(),
+ selected => 0,
+ );
+ if ($param{'group_name'} eq $group_master->get_group()) {
+ $group_info{'selected'} = '1';
+ $selected_group_master = $group_master;
+ }
+ push @{$tmpl_var{'group_infos'}}, \%group_info;
+ }
+ if (!$selected_group_master && @group_masters > 0) {
+ $selected_group_master = $group_masters[0];
+ $tmpl_var{'group_infos'}->[0]->{'selected'} = 1;
+ }
+
+ if ($selected_group_master) {
+ my $active_vcl_name;
+ my @vcl_names;
+ my $vcl_names_ref = $selected_group_master->get_management()->get_vcl_names();
+ if ($vcl_names_ref) {
+ @vcl_names = @{$vcl_names_ref};
+ $active_vcl_name = shift @vcl_names;
+
+ for my $vcl_name (@vcl_names) {
+ my %vcl_info = (
+ name => $vcl_name,
+ selected => 0,
+ active => 0,
+ );
+ if ($vcl_name eq $tmpl_var{'vcl_name'}) {
+ $vcl_info{'selected'} = 1;
+ $tmpl_var{'vcl_name'} = $vcl_name;
+ }
+ if ($vcl_name eq $active_vcl_name) {
+ $vcl_info{'active'} = 1;
+ }
+ push @{$tmpl_var{'vcl_infos'}}, \%vcl_info;
+ }
+ if ($tmpl_var{'vcl_name'} eq "") {
+ FIND_ACTIVE_VCL:
+ for my $vcl_info (@{$tmpl_var{'vcl_infos'}}) {
+ if ($vcl_info->{'active'}) {
+ $tmpl_var{'vcl_name'} = $vcl_info->{'name'};
+ $vcl_info->{'selected'} = 1;
+ last FIND_ACTIVE_VCL;
+ }
+ }
+ if ($tmpl_var{'vcl_name'} eq "") {
+ $tmpl_var{'vcl_name'} = $tmpl_var{'vcl_infos'}->[0]->{'name'};
+ $tmpl_var{'vcl_infos'}->[0]->{'selected'} = 1;
+ }
+ }
+
+ if (!(($param{'operation'} eq 'save' && !$successfull_save
+ || $param{'operation'} eq 'new'))) {
+ my $vcl = $selected_group_master->get_management()->get_vcl($tmpl_var{'vcl_name'});
+ if ($vcl) {
+ $tmpl_var{'vcl'} = $vcl;
+ }
+ else {
+ $tmpl_var{'error'} .= "Error retrieving VCL: " . $selected_group_master->get_management()->get_error() . "\n";
+ }
+ }
+ }
+ else {
+ $tmpl_var{'error'} .= "Error retrieving the VCLs: " . $selected_group_master->get_management()->get_error();
+ }
+ }
+
+ $tmpl_var{'editing_new_vcl'} = $editing_new_vcl;
+ $tmpl_var{'successfull_save'} = $successfull_save;
+
+ return ($template, \%tmpl_var);
+ }
+
+
+ sub view_stats {
+ my ($parameter_ref) = @_;
+
+ my $template = "view_stats.tmpl";
+
+ my %param = %{$parameter_ref};
+ $param{'view_raw_stats'} ||= 0;
+ $param{'auto_refresh'} ||= 0;
+
+ my %tmpl_var;
+ $tmpl_var{'error'} = "";
+ $tmpl_var{'stat_time'} = 0;
+ $tmpl_var{'node_infos'} = [];
+ $tmpl_var{'summary_stats'} = [];
+ $tmpl_var{'raw_stats'} = [];
+ $tmpl_var{'auto_refresh'} = $param{'toggle_auto_refresh'} ? 1 - $param{'auto_refresh'} : $param{'auto_refresh'};
+ $tmpl_var{'auto_refresh_interval'} = $tmpl_var{'auto_refresh'} ? get_config_value('poll_interval') : 0;
+ $tmpl_var{'view_raw_stats'} = $param{'view_raw_stats'};
+
+ my $error = "";
+
+ my ($stat_time, $stat_ref) = Varnish::Statistics->get_last_measure();
+ my @nodes = Varnish::NodeManager->get_nodes();
+
+ if ($stat_time) {
+ $stat_time = strftime("%a %b %e %H:%M:%S %Y", localtime($stat_time));
+ }
+
+ my %summary_stat_list;
+ my %raw_stat_list;
+ for my $node (@nodes) {
+ push @{$tmpl_var{'node_infos'}}, {
+ name => $node->get_name(),
+ };
+
+ my $node_stat_ref = $stat_ref->{$node};
+ my $node_id = $node->get_id();
+ my $time_span = 'minute';
+
+ # example of adding graph the graph ID must match that of a predefind graph
+ # which is created in generate_graph found around line 826
+ push @{$summary_stat_list{'Hit ratio'}}, {
+ is_graph => 1,
+ node_id => $node_id,
+ graph_id => 'cache_hit_ratio',
+ };
+ push @{$summary_stat_list{'Connect requests'}}, {
+ is_graph => 1,
+ node_id => $node_id,
+ graph_id => 'connect_rate',
+ };
+
+ # example of missing graph_id
+ push @{$summary_stat_list{'Missing graph'}}, {
+ is_graph => 1,
+ node_id => $node_id,
+ graph_id => 'missing_graph',
+ };
+
+ # to add custom values, just add values by adding it to the list. The
+ # get_formatted_bytes() function is usefull for displaying byte values
+ # as it will convert to MB, GB etc as needed.
+ push @{$summary_stat_list{'% of requests served from cache'}}, {
+ value => get_formatted_percentage($$node_stat_ref{'Cache hits'}
+ , $$node_stat_ref{'Client requests received'})
+ };
+
+ # these are examples of adding plain values from the raw stats
+ push @{$summary_stat_list{'Client connections accepted'}}, {
+ value => $$node_stat_ref{'Client connections accepted'}
+ };
+ push @{$summary_stat_list{'Client requests received'}}, {
+ value => $$node_stat_ref{'Client requests received'}
+ };
+
+ my $total_bytes_served;
+ if ($$node_stat_ref{'Total header bytes'}
+ && $$node_stat_ref{'Total body bytes'}) {
+ $total_bytes_served = $$node_stat_ref{'Total header bytes'} + $$node_stat_ref{'Total header bytes'};
+ }
+ push @{$summary_stat_list{'Total bytes served'}}, {
+ 'value'
+ => get_formatted_bytes($total_bytes_served)
+ };
+
+ if ($param{'view_raw_stats'}) {
+ while (my ($stat_name, $value) = each %{$node_stat_ref}) {
+ push @{$raw_stat_list{$stat_name}}, {
+ value => $value,
+ };
+ }
+ }
+ }
+
+ my $row = 1;
+ while (my ($stat_name, $values_ref) = each %raw_stat_list) {
+ push @{$tmpl_var{'raw_stats'}}, {
+ name => $stat_name,
+ values => $values_ref,
+ odd_row => $row++ % 2,
+ }
+ }
+
+ $row = 1;
+ my $graph_row = 0;
+ while (my ($stat_name, $values_ref) = each %summary_stat_list) {
+ if ($values_ref->[0]->{'is_graph'}) {
+ unshift @{$tmpl_var{'summary_stats'}}, {
+ name => $stat_name,
+ values => $values_ref,
+ odd_row => $graph_row++ % 2,
+ }
+ }
+ else {
+ push @{$tmpl_var{'summary_stats'}}, {
+ name => $stat_name,
+ values => $values_ref,
+ odd_row => $row++ % 2,
+ }
+ }
+ }
+
+ $tmpl_var{'error'} = $error;
+ $tmpl_var{'stat_time'} = $stat_time;
+
+ return ($template, \%tmpl_var);
+ }
+
+ sub configure_parameters {
+ my ($parameter_ref) = @_;
+
+ my %param = %{$parameter_ref};
+ $param{'node_id'} = $$parameter_ref{'node_id'} || "";
+ $param{'group'} = $$parameter_ref{'group'} || "";
+
+ my $template = "configure_parameters.tmpl";
+ my %tmpl_var;
+ $tmpl_var{'error'} = "";
+ $tmpl_var{'status'} = "";
+ $tmpl_var{'unit_infos'} = [];
+ $tmpl_var{'parameter_infos'} = [];
+
+ my $unit_parameter_ref = {};
+ my $error = "";
+
+ my %changed_parameters;
+ while (my ($parameter, $value) = each %$parameter_ref) {
+ if ($parameter =~ /^new_(.*?)$/ &&
+ $$parameter_ref{"old_$1"} ne $value) {
+ $changed_parameters{$1} = $value;
+ }
+ }
+
+ my @nodes = Varnish::NodeManager->get_nodes();
+ my @groups = Varnish::NodeManager->get_groups();
+ if (%changed_parameters) {
+ my $node = first { $_->get_id() eq $param{'node_id'} } @nodes;
+ if ($node) {
+ my $management = $node->get_management();
+ while (my ($parameter, $value) = each %changed_parameters) {
+ if (!$management->set_parameter($parameter, $value)) {
+ $error .= "Could not set parameter $parameter: ". $node->get_management()->get_error() . "\n";
+ }
+ }
+ }
+ else {
+ my $group = first { $_ eq $param{'group'} } @groups;
+ if ($group ne "") {
+ while (my ($parameter, $value) = each %changed_parameters) {
+ if (!Varnish::NodeManager->set_group_parameter($group, $parameter, $value)) {
+ $error .= "Could not set parameter $parameter for group $group: "
+ . Varnish::NodeManager->get_error() . "\n";
+ }
+ }
+ }
+ }
+ if ($error eq "") {
+ my @changed_parameters = keys %changed_parameters;
+ my $status = "Parameter" . (@changed_parameters > 1 ? "s " : " ");
+
+ $status .= shift @changed_parameters;
+ for my $parameter (@changed_parameters) {
+ $status .= ", $parameter";
+ }
+ $status .= " configured successfully";
+ $tmpl_var{'status'} = $status;
+ }
+ }
+
+ for my $group (@groups) {
+ my %unit_info = (
+ name => $group,
+ id => $group,
+ is_node => 0,
+ selected => 0,
+ );
+ if ($group eq $param{'group'}) {
+ $unit_info{'selected'} = 1;
+ $unit_parameter_ref = Varnish::NodeManager->get_group_parameters($group);
+ if (!$unit_parameter_ref) {
+ $error .= "Could not get parameters for group $group. You need to have added a node to set these.\n";
+ }
+ }
+ push @{$tmpl_var{'unit_infos'}}, \%unit_info;
+ }
+
+ for my $node (@nodes) {
+ my %unit_info = (
+ name => $node->get_name(),
+ id => $node->get_id(),
+ is_node => 1,
+ selected => 0,
+ );
+ if ($node->get_id() eq $param{'node_id'}) {
+ $unit_info{'selected'} = 1;
+ $unit_parameter_ref = $node->get_management()->get_parameters();
+ if (!$unit_parameter_ref) {
+ $error .= "Could not get parameters for node " . $node->get_name() . "\n";
+ }
+ }
+ push @{$tmpl_var{'unit_infos'}}, \%unit_info;
+ }
+
+ if ($param{'group'} eq "" && $param{'node_id'} eq ""
+ && @{$tmpl_var{'unit_infos'}} > 0) {
+ $tmpl_var{'unit_infos'}->[0]->{'selected'} = 1;
+ my $group = $tmpl_var{'unit_infos'}->[0]->{'name'};
+ $unit_parameter_ref = Varnish::NodeManager->get_group_parameters($group);
+ if (!$unit_parameter_ref) {
+ $error .= "Could not get parameters for group $group\n";
+ }
+ }
+
+ my $row = 0;
+ while (my ($parameter, $info) = each %{$unit_parameter_ref} ) {
+ my $value = $info->{'value'};
+ my $unit = $info->{'unit'};
+ my $is_boolean = $unit eq "[bool]";
+ if ($is_boolean) {
+ $value = $value eq "on";
+ $unit = '';
+ }
+
+ push @{$tmpl_var{'parameter_infos'}}, {
+ name => $parameter,
+ value => $value,
+ unit => $unit,
+ description => $info->{'description'},
+ is_boolean => $is_boolean,
+ odd_row => $row++ % 2,
+ };
+ }
+
+ $tmpl_var{'error'} = $error;
+
+ return ($template, \%tmpl_var);
+ }
+
+
+ sub node_management {
+ my ($parameter_ref) = @_;
+
+ my %param = %{$parameter_ref};
+ $param{'node_id'} ||= "";
+ $param{'group'} ||= "";
+ $param{'operation'} ||= "";
+ $param{'name'} ||= "";
+ $param{'address'} = $$parameter_ref{'address'} || "";
+ $param{'port'} ||= "";
+ $param{'management_port'} ||= "";
+
+ my $template = "node_management.tmpl";
+ my %tmpl_var = ();
+ $tmpl_var{'error'} = "";
+ $tmpl_var{'status'} = "";
+ $tmpl_var{'add_group'} = 0;
+ $tmpl_var{'group_infos'} = [];
+ $tmpl_var{'group'} = $param{'group'} || "";
+ $tmpl_var{'node_infos'} = [];
+ $tmpl_var{'default_managment_port'} = 9001;
+ $tmpl_var{'backend_health_infos'} = [];
+
+ my $error = "";
+ my $status = "";
+
+ if ($param{'operation'} eq "add_group") {
+ if ($param{'group'}) {
+ Varnish::NodeManager->add_group($param{'group'});
+ $status .= "Group " . $param{'group'} . " added successfully.";
+ }
+ else {
+ $tmpl_var{'add_group'} = 1;
+ }
+ }
+ elsif ($param{'operation'} eq "remove_group") {
+ if ($param{'group'}) {
+ Varnish::NodeManager->remove_group($param{'group'});
+ $status = "Group " . $param{'group'} . " removed successfully";
+ $tmpl_var{'group'} = "";
+ }
+ }
+ elsif ($param{'operation'} eq "start_group") {
+ my $node_errors = "";
+ for my $node (Varnish::NodeManager->get_nodes()) {
+ if ($node->get_group() eq $param{'group'}
+ && !$node->is_running()) {
+ my $management = $node->get_management();
+ if (!$management->start()) {
+ $node_errors .= "Could not start " . $node->get_name() . ": "
+ . $management->get_error() . ". ";
+ }
+ }
+ }
+
+ if ($node_errors eq "") {
+ $status .= "Group " . $param{'group'} . " started successfully.";
+ }
+ else {
+ $status .= "Group " . $param{'group'} . " started with errors: $node_errors.";
+ }
+ }
+ elsif ($param{'operation'} eq "stop_group") {
+ my $node_errors = "";
+ for my $node (Varnish::NodeManager->get_nodes()) {
+ if ($node->get_group() eq $param{'group'}
+ && $node->is_running()) {
+ my $management = $node->get_management();
+ if (!$management->stop()) {
+ $node_errors .= "Could not stop " . $node->get_name() . ": "
+ . $management->get_error() . ". ";
+ }
+ }
+ }
+ if ($node_errors eq "") {
+ $status .= "Group " . $param{'group'} . " stopped successfully.";
+ }
+ else {
+ $status .= "Group " . $param{'group'} . " stopped with errors: $node_errors.";
+ }
+ }
+ elsif ($param{'operation'} eq 'add_node') {
+ if ($param{'name'} && $param{'address'} && $param{'port'}
+ && $param{'group'} && $param{'management_port'}) {
+ my $node = Varnish::Node->new({
+ name => $param{'name'},
+ address => $param{'address'},
+ port => $param{'port'},
+ group => $param{'group'},
+ management_port => $param{'management_port'}
+ });
+ Varnish::NodeManager->add_node($node);
+ $status .= "Node " . $node->get_name() . " added successfully.";
+ }
+ else {
+ $error .= "Not enough information to add node:\n";
+ $error .= "Name: " . $param{'name'} . ":\n";
+ $error .= "Address: " . $param{'address'} . ":\n";
+ $error .= "Port: " . $param{'port'} . ":\n";
+ $error .= "Group: " . $param{'group'} . ":\n";
+ $error .= "Management port: " . $param{'management_port'} . ":\n";
+ }
+ }
+ elsif ($param{'operation'} eq "remove_node") {
+ if ($param{'node_id'}) {
+ my $node = Varnish::NodeManager->get_node($param{'node_id'});
+ if ($node) {
+ $tmpl_var{'group'} = $node->get_group();
+ Varnish::NodeManager->remove_node($node);
+ $status .= "Node " . $node->get_name() . " removed successfully.";
+ }
+ }
+ else {
+ $error .= "Could not remove node: Missing node ID\n";
+ }
+ }
+ elsif ($param{'operation'} eq "start_node") {
+ if ($param{'node_id'}) {
+ my $node = Varnish::NodeManager->get_node($param{'node_id'});
+ if ($node) {
+ my $management = $node->get_management();
+ if ($node->is_running()) {
+ $status .= "Node " . $node->get_name() . " already running.";
+ }
+ elsif ($management->start() ) {
+ $status .= "Node " . $node->get_name() . " started successfully.";
+ }
+ else {
+ $error .= "Could not start " . $node->get_name()
+ . ": " . $management->get_error() . "\n";
+ }
+ $tmpl_var{'group'} = $node->get_group();
+ }
+ }
+ else {
+ $error .= "Could not start node: Missing node ID\n";
+ }
+ }
+ elsif ($param{'operation'} eq "stop_node") {
+ if ($param{'node_id'}) {
+ my $node = Varnish::NodeManager->get_node($param{'node_id'});
+ if ($node) {
+ my $management = $node->get_management();
+ if (!$node->is_running()) {
+ $status .= "Node " . $node->get_name() . " already stopped.";
+ }
+ elsif ($management->stop()) {
+ $status .= "Node " . $node->get_name() . " stopped successfully.";
+ }
+ else {
+ $error .= "Could not stop " . $node->get_name()
+ . ": " . $management->get_error() . "\n";
+ }
+ }
+ $tmpl_var{'group'} = $node->get_group();
+ }
+ else {
+ $error .= "Could not stop node: Missing node ID\n";
+ }
+ }
+
+ # Populate the node table
+ my @groups = Varnish::NodeManager->get_groups();
+ if (@groups) {
+ if (!$tmpl_var{'group'} && !$tmpl_var{'add_group'}) {
+ $tmpl_var{'group'} = $groups[0];
+ }
+ my @group_infos = map {
+ {
+ name => $_,
+ selected => $_ eq $tmpl_var{'group'},
+ }
+ } @groups;
+ $tmpl_var{'group_infos'} = \@group_infos;
+
+ my @nodes = Varnish::NodeManager->get_nodes();
+ for my $node (@nodes) {
+ next if ($node->get_group() ne $tmpl_var{'group'});
+
+ push @{$tmpl_var{'node_infos'}}, {
+ id => $node->get_id(),
+ is_running_ok => $node->is_running_ok(),
+ is_running => $node->is_running(),
+ is_management_running => $node->is_management_running(),
+ name => $node->get_name(),
+ address => $node->get_address(),
+ port => $node->get_port(),
+ management_port => $node->get_management_port(),
+ group => $node->get_group(),
+ };
+
+ if (@{$tmpl_var{'backend_health_infos'}} == 0) {
+ my $backend_health = $node->get_management()->get_backend_health();
+ if ($backend_health) {
+ while (my ($backend, $health) = each %{$backend_health}) {
+ push @{$tmpl_var{'backend_health_infos'}}, {
+ name => $backend,
+ health => $health,
+ };
+ }
+ }
+ }
+ }
+ }
+ else {
+ $tmpl_var{'add_group'} = 1;
+ }
+
+ $tmpl_var{'error'} = $error;
+ $tmpl_var{'status'} = $status;
+
+ return ($template, \%tmpl_var);
+ }
+
+sub generate_graph {
+ my ($parameter_ref) = @_;
+
+ my %param = %{$parameter_ref};
+ $param{'width'} ||= 250;
+ $param{'height'} ||= 150;
+ $param{'time_span'} ||= "minute";
+ $param{'type'} ||= "";
+ $param{'node_id'} ||= 0;
+
+ my $interval = get_config_value('poll_interval');
+
+ # this hash holds available graphs which can be added to the summary stats in the view_stats
+ # function.
+ my %graph_info = (
+ # the name of the graph
+ cache_hit_ratio => {
+ # the parameters to GD::Graph. y_number_format should be noted, as it let you format
+ # the presentation, like multiplying with 100 to get the percentage as shown here
+ graph_parameter => {
+ y_label => '%',
+ title => "Cache hit ratio last " . $param{'time_span'},
+ y_max_value => 1,
+ y_min_value => 0,
+ y_number_format => sub { return $_[0] * 100 }
+ },
+ # the divisors and dividends are lists of names of the statistics to
+ # use when calculating the values in the graph. The names can be obtained
+ # by turning 'Raw statistics' on in the GUI. The value in the graph is calculated
+ # by taking the sum of divisors and divide witht the sum of the dividends, i.e.
+ # value = (divisor1 + divisor2 + divisor3 ...) / (dividend1 + dividend 2 +..)
+ # if divisor or dividend is emitted, the value of 1 is used instead
+ divisors => [ 'Cache hits' ],
+ dividends => [ 'Cache hits', 'Cache misses' ],
+ },
+ connect_rate => {
+ graph_parameter => {
+ y_label => 'Reqs',
+ title => "Reqs / $interval s last " . $param{'time_span'},
+ y_min_value => 0,
+ },
+ # here we have no dividends as we only want to plot 'Client requests received'
+ divisors => [ 'Client requests received' ],
+ # if use_delta is set to 1, the derived value is used, i.e. the difference
+ # in value between two measurements. This is usefull for graphs showing rates
+ # like this connect rate
+ use_delta => 1,
+ },
+ );
+ my %time_span_graph_parameters = (
+ minute => {
+ x_label => 'Time',
+ x_tick_number => 6, # need to be set to make x_number_format work
+ x_number_format => sub { return strftime(":%S", localtime($_[0])); },
+ x_max_value => time
+ },
+ hour => {
+ x_label => 'Time',
+ x_tick_number => 6, # need to be set to make x_number_format work
+ x_number_format => sub { return strftime("%H:%M", localtime($_[0])); },
+ },
+ day => {
+ x_label => 'Time',
+ x_tick_number => 4, # need to be set to make x_number_format work
+ x_number_format => sub { return strftime("%H", localtime($_[0])); },
+ },
+ week => {
+ x_label => 'Time',
+ x_tick_number => 7, # need to be set to make x_number_format work
+ x_number_format => sub { return strftime("%d", localtime($_[0])); },
+ },
+ month => {
+ x_label => 'Time',
+ x_tick_number => 4, # need to be set to make x_number_format work
+ x_number_format => sub { return strftime("%d.%m", localtime($_[0])); },
+ },
+ );
+
+
+ if ( !$graph_info{$param{'type'}}
+ || !$time_span_graph_parameters{$param{'time_span'}}) {
+ #print "Error: Missing data";
+ return;
+ }
+ my $data_ref = Varnish::Statistics->generate_graph_data(
+ $param{'node_id'},
+ $param{'time_span'},
+ $graph_info{$param{'type'}}->{'divisors'},
+ $graph_info{$param{'type'}}->{'dividends'},
+ $graph_info{$param{'type'}}->{'use_delta'}
+ );
+ if (!$data_ref) {
+ #print "Error generating graph data\n";
+ return;
+ }
+ my $graph = GD::Graph::lines->new($param{'width'}, $param{'height'});
+ $graph->set((%{$graph_info{$param{'type'}}->{'graph_parameter'}},
+ %{$time_span_graph_parameters{$param{'time_span'}}}),
+ dclrs => ["#990200"]);
+
+ my $graph_image = $graph->plot($data_ref);
+
+ if (!$graph_image) {
+ return;
+ }
+
+ return $graph_image->png;
+ }
+
+ sub management_console {
+ my ($parameter_ref) = @_;
+
+ my %param = %{$parameter_ref};
+ $param{'node_id'} = $$parameter_ref{'node_id'} || 0;
+
+ my $template = "management_console.tmpl";
+ my %tmpl_var;
+ $tmpl_var{'error'} = "";
+ $tmpl_var{'unit_infos'} = [];
+ $tmpl_var{'parameter_infos'} = [];
+ $tmpl_var{'default_console_font_size'} = '1.1em',
+ $tmpl_var{'default_console_cols'} = 80,
+ $tmpl_var{'default_console_rows'} = 30,
+
+ my @nodes = Varnish::NodeManager->get_nodes();
+ if (@nodes) {
+ my $node_id = $param{'node_id'} ? $param{'node_id'} : $nodes[0]->get_id();
+ for my $node (@nodes) {
+ my $selected = $node->get_id() == $node_id;
+ push @{$tmpl_var{'node_infos'}}, {
+ id => $node->get_id(),
+ name => $node->get_name(),
+ selected => $selected,
+ };
+
+ if ($selected) {
+ $tmpl_var{'current_node_name'} = $node->get_name();
+ }
+ }
+ }
+
+ $tmpl_var{'console_themes'} = [
+ {
+ name => 'Grey on black',
+ foreground => '#bbb',
+ background => 'black',
+ },
+ {
+ name => 'Black on white',
+ foreground => 'black',
+ background => 'white',
+ },
+ {
+ name => 'Retro',
+ foreground => 'green',
+ background => 'black',
+ }
+ ];
+ $tmpl_var{'default_console_foreground'} = $tmpl_var{'console_themes'}->[0]->{'foreground'},
+ $tmpl_var{'default_console_background'} = $tmpl_var{'console_themes'}->[0]->{'background'},
+
+ return ($template, \%tmpl_var);
+ }
+
+ sub send_management_command {
+ my ($parameter_ref) = @_;
+
+ my $node_id = $$parameter_ref{'node_id'};
+ my $command = $$parameter_ref{'command'};
+
+ if ($node_id && $command) {
+ my $node = Varnish::NodeManager->get_node($node_id);
+ return "Error: Node not found." if (!$node);
+
+ my $management = $node->get_management();
+ my $response = $management->send_command($command);
+ if ($response) {
+ return $response;
+ }
+ else {
+ return "Error: " . $management->get_error();
+ }
+ }
+ else {
+ return "Error: Not valid input";
+ }
+ }
+
+}
+
+
+1;
--- /dev/null
+package Varnish::Statistics;
+
+use strict;
+use POSIX qw(strftime);
+use List::Util qw(first);
+
+use Varnish::Util;
+use Varnish::NodeManager;
+
+{
+ my %data;
+ my $last_measure_ref;
+ my $last_measure_time;
+
+ sub collect_data {
+ my $time_stamp = time();
+ my %measure;
+ my $good_stat_ref;
+ my @nodes = Varnish::NodeManager->get_nodes();
+
+ my @bad_nodes;
+ for my $node (@nodes) {
+ my $management = $node->get_management();
+ my $stat_ref;
+ if ($management) {
+ $stat_ref = $management->get_stats();
+ }
+ if ($stat_ref) {
+ $measure{$node} = $stat_ref;
+ if (!$good_stat_ref) {
+ $good_stat_ref = $stat_ref;
+ }
+ }
+ else {
+ push @bad_nodes, $node;
+ }
+ }
+
+ if (@bad_nodes && $good_stat_ref) {
+ for my $bad_node (@bad_nodes) {
+ $measure{$bad_node}->{'missing data'} = 1;
+ for my $key (keys %$good_stat_ref) {
+ $measure{$bad_node}->{$key} = -1;
+ }
+ }
+ }
+
+ $data{$time_stamp} = \%measure;
+ $last_measure_ref = \%measure;
+ $last_measure_time = $time_stamp;
+ }
+
+
+ sub get_last_measure {
+
+ return ($last_measure_time, $last_measure_ref);
+ }
+
+ sub generate_graph_data {
+ my ($self, $node_id, $time_span, $divisors_ref, $dividends_ref, $use_delta) = @_;
+
+ my %seconds_for = (
+ minute => 60,
+ hour => 3600,
+ day => 86400,
+ week => 604800,
+ month => 18144000, # 30 days
+ );
+ my $start_time = time() - $seconds_for{$time_span};
+ if ($use_delta) {
+ $start_time -= get_config_value('poll_interval');
+ }
+ my $node = first {
+ $_->get_id() == $node_id
+ } Varnish::NodeManager->get_nodes();
+
+ my @measures = grep {
+ $_ > $start_time
+ } (sort keys %data);
+ my @values;
+ my $last_value;
+ GENERATE_DATA:
+ for my $measure (@measures) {
+ my $value;
+ if (!$data{$measure}->{$node}->{'missing data'}) {
+ my $divisor_value = 0;
+ my $dividend_value = 0;
+
+ if ($divisors_ref && @$divisors_ref) {
+ for my $divisor (@$divisors_ref) {
+ $divisor_value += $data{$measure}->{$node}->{$divisor};
+ }
+ }
+ else {
+ $divisor_value = 1;
+ }
+ if ($dividends_ref && @$dividends_ref) {
+ for my $dividend (@$dividends_ref) {
+ $dividend_value += $data{$measure}->{$node}->{$dividend};
+ }
+ }
+ else {
+ $dividend_value = 1;
+ }
+ if ($dividend_value) {
+ $value = $divisor_value / $dividend_value;
+ }
+
+ if ($use_delta) {
+ if (!$last_value) {
+ $last_value = $value;
+ next GENERATE_DATA;
+ }
+ my $delta_value = $value - $last_value;
+ $last_value = $value;
+ # if the value is negative, then we have had restart and don't plot it.
+ if ($delta_value < 0) {
+ $value = undef;
+ }
+ else {
+ $value = $delta_value;
+ }
+ }
+ }
+ push @values, $value;
+ }
+
+ return [ \@measures, \@values ];
+ }
+}
+
+1;
--- /dev/null
+package Varnish::Util;
+
+use strict;
+use Exporter;
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ set_config
+ get_config_value
+ read_file
+ get_formatted_percentage
+ get_formatted_bytes
+ );
+
+{
+ my %config;
+
+ sub set_config {
+ my ($config_ref) = @_;
+
+ %config = %{$config_ref};
+ }
+
+ sub get_config_value {
+ my ($key) = @_;
+
+ return $config{$key};
+ }
+
+ sub read_file($) {
+ my ($filename) = @_;
+
+ open(my $fh, "<$filename" );
+ my $content = do { local($/); <$fh> };
+ close($fh);
+
+ return $content;
+ }
+
+ sub get_formatted_percentage {
+ my ($divisor, $dividend) = @_;
+
+ return $dividend > 0 ? sprintf( "%.2f", 100 * ($divisor / $dividend)) : "inf";
+ }
+
+ # thanks to foxdie at #varnish for php snippet
+ sub get_formatted_bytes {
+ my ($bytes) = @_;
+
+ if ($bytes > 1099511627776) {
+ return sprintf( "%.3f TB", $bytes / 1099511627776);
+ }
+ elsif ($bytes > 1073741824) {
+ return sprintf( "%.3f GB", $bytes / 1073741824);
+ }
+ elsif ($bytes > 1048576) {
+ return sprintf( "%.3f MB", $bytes / 1048576);
+ }
+ elsif ($bytes > 1024) {
+ return sprintf( "%.3f KB", $bytes / 1024);
+ }
+ else {
+ return $bytes . " B";
+ }
+ }
+}
+
+1;
--- /dev/null
+* {
+ padding: 0px;
+ margin: 0px;
+}
+
+a {
+ color: black;
+}
+
+body {
+ background-color: #d7d7d7;
+ padding: 10px;
+}
+
+#header {
+ background-color: #990200;
+ border-style: solid;
+ padding-bottom: 3px;
+ border-width: 0px 0px 2px 0px;
+}
+
+img#headerLogo {
+ background-color: #d7d7d7;
+}
+
+#menu {
+ padding: 5px 3px 5px 3px;
+ text-align: center;
+}
+
+a.menu {
+ color: #fff;
+ padding-right: 20px;
+}
+
+#error {
+ font-size: 1.1em;
+ padding: 5px;
+ border-width: 3px;
+ border-style: solid;
+ border-color: #6c0000;
+ background-color: #a21616;
+ margin-bottom: 10px;
+}
+
+#content {
+ background-color: #f2f2f2;
+ padding: 10px;
+}
+
+td.header {
+ font-weight: bold;
+ border-style: solid;
+ border-width: 0px 0px 1px 0px;
+}
+
+td.footer {
+ font-size: 0.7em;
+ padding-top: 10px;
+ border-style: solid;
+ border-width: 1px 0px 0px 0px;
+}
+
+
+table#parameters {
+ padding: 2px;
+}
+
+td.parameterLabel {
+ padding: 0px 5px 0px 5px;
+}
+
+td.parameterValue {
+ padding: 0px 5px 0px 5px;
+ text-align: right;
+}
+
+td.parameterUnit {
+ font-style: italic;
+ padding: 0px 5px 0px 5px;
+}
+
+td.parameterDescription {
+ padding: 0px 5px 0px 5px;
+}
+
+td.parameterEdit {
+ padding: 0px 5px 0px 5px;
+ text-align: center;
+}
+
+tr.evenRow {
+ background-color:#fff;
+ vertical-align: top;
+}
+
+tr.oddRow {
+ background-color:#eee;
+ vertical-align: top;
+}
+
+textarea#vclEditor {
+ margin: 5px 0px 10px 0px;
+}
+
+span.tab {
+ padding: 0px 10px 0px 10px;
+ width: 10em;
+ background-color: #ddd;
+ border-style: solid;
+ border-width: 1px 1px 1px 1px;
+}
+
+span.selectedTab {
+ padding: 1px 10px 1px 10px;
+ border-style: solid;
+ background-color: #fff;
+ border-width: 1px 1px 0px 1px;
+}
+
+span.space {
+ border-style: solid;
+ border-width: 0 0 1px 0;
+}
+
+div.tabArea {
+ background-color: #fff;
+ padding: 5px;
+ border-style: solid;
+ border-width: 0px 1px 1px 1px;
+}
+
+table#statsTable {
+ background-color: #fff;
+ border-style: solid;
+ border-width: 1px;
+}
+
+option {
+ padding-right: 10px;
+}
+
+#groupControls {
+ padding-top: 30px;
+}
+
+td {
+ padding: 1px 2px 1px 2px;
+}
+
+textarea#vclConfig {
+ font-size: 1.1em;
+ border-style: solid;
+ border-width: 2px;
+ border-color: #990200;
+ padding: 2px 0 0 5px;
+}
+
+#themeSelection {
+ padding: 3px;
+}
+
+li.consoleSetting {
+ margin-left: 30px;
+ margin-top: 10px;
+}
+
+input.removeNode {
+ margin-left: 50px;
+}
+
+#status {
+ border-color: #2e6c00;
+ border-style: solid;
+ border-width: 3px;
+ background-color: #419a00;
+ padding: 5px;
+ margin-bottom: 10px;
+}
+
+#vclError {
+ font-family: monospace;
+ font-size: 1.1em;
+ border-color: black;
+ border-style: solid;
+ border-width: 3px;
+ background-color: red;
+ padding: 5px;
+}
--- /dev/null
+#!/usr/bin/perl
+use threads;
+use strict;
+use warnings;
+use HTTP::Daemon;
+use HTTP::Status;
+use HTTP::Request;
+use LWP::UserAgent;
+use Varnish::Util;
+use Varnish::RequestHandler;
+use Varnish::NodeManager;
+use Varnish::Node;
+use Varnish::Statistics;
+
+
+# Configuration starts here
+my %config = (
+# 'address' is the IP to bind to. If not set, it listens on all.
+# address => localhost,
+
+# 'port' is the port of the web server
+ port => 8000,
+
+# 'poll_intervall' is the polling interval for the statistics
+ poll_interval => 5,
+);
+
+# create some default groups
+my @groups = qw(default images);
+
+# create some default nodes
+my @node_params = (
+ {
+ name => 'varnish-1',
+ address => 'localhost',
+ port => '80',
+ group => 'default',
+ management_port => 9001,
+ },
+ {
+ name => 'varnish-2',
+ address => 'localhost',
+ port => '8181',
+ group => 'default',
+ management_port => 9002,
+ },
+ {
+ name => 'varnish-1',
+ address => 'localhost',
+ port => '8888',
+ group => 'images',
+ management_port => 9003,
+ },
+);
+
+# End of configuration
+
+set_config(\%config);
+
+for my $group (@groups) {
+ Varnish::NodeManager->add_group($group);
+}
+
+for my $node_param_ref (@node_params) {
+ my $group_exists = grep {
+ $_ eq $node_param_ref->{'group'}
+ } @groups;
+ if ($group_exists) {
+ my $node = Varnish::Node->new($node_param_ref);
+ Varnish::NodeManager->add_node($node);
+ }
+ else {
+ print "Node " . $node_param_ref->{'name'} . " has an invalid group "
+ . $node_param_ref->{'group'} . ". Skipping.";
+ }
+}
+
+# catch interupt to stop the daemon
+$SIG{'INT'} = sub {
+ print "Interrupt detected.\n";
+};
+
+# ignore the occational sigpipe
+$SIG{'PIPE'} = sub {
+# print "Pipe ignored\n";
+};
+
+my $daemon = HTTP::Daemon->new( LocalPort => $config{'port'},
+ LocalAddr => $config{'address'},
+ ReuseAddr => 1 ) || die "Could not start web server";
+print "Web server started with URL: " . $daemon->url, "\n";
+my $data_collector_handle = threads->create('data_collector_thread');
+while (my $connection = $daemon->accept) {
+ REQUEST:
+ while (my $request = $connection->get_request) {
+ $connection->force_last_request;
+ if ($request->uri =~ m{/(.*?\.png)} ||
+ $request->uri =~ m{/(.*?\.css)} ||
+ $request->uri =~ m{/(.*?\.ico)}) {
+ my $filename = $1;
+
+ $connection->send_file($filename);
+ next REQUEST;
+ }
+
+ my $request_handler = Varnish::RequestHandler->new(\$request, $connection);
+ $request_handler->process();
+
+ my $response = HTTP::Response->new(200);
+ $response->header( $request_handler->get_response_header() );
+ $response->content( $request_handler->get_response_content() );
+ $connection->send_response($response);
+ }
+ $connection->close();
+ undef($connection);
+}
+print "Shutting down!\n";
+$daemon->close();
+Varnish::NodeManager->quit();
+print "Stopping data collector thread\n";
+$data_collector_handle->join();
+
+sub data_collector_thread {
+ my $url = $daemon->url . "collect_data";
+ my $interval = $config{'poll_interval'};
+ print "Data collector thread started. Polling URL $url at $interval seconds interval\n";
+
+ sleep 1; # wait for the server to come up
+ while (1) {
+ my $user_agent = LWP::UserAgent->new;
+ $user_agent->timeout(6);
+ my $response = $user_agent->get($url);
+
+ last if ($response->code eq "500");
+ sleep $interval;
+ }
+ print "Data collector thread stopped.\n";
+}
--- /dev/null
+<script type="text/javascript">
+function toggleHelp(sectionId) {
+
+ var sectionStyle = document.getElementById(sectionId).style;
+ if (sectionStyle.display == "none") {
+ sectionStyle.display = "block";
+ }
+ else {
+ sectionStyle.display = "none";
+ }
+}
+</script>
+
+<TMPL_IF NAME=UNIT_INFOS>
+<form action="configure_parameters" method="POST">
+<div class="tabHeader">
+<TMPL_LOOP NAME=UNIT_INFOS>
+<TMPL_IF NAME=SELECTED>
+<span class="selectedTab">
+<TMPL_IF NAME=IS_NODE>
+<input type="hidden" name="node_id" value="<TMPL_VAR NAME=ID>"/>
+<TMPL_ELSE>
+<input type="hidden" name="group" value="<TMPL_VAR NAME=ID>"/>
+</TMPL_IF>
+<TMPL_ELSE>
+<span class="tab">
+</TMPL_IF>
+<TMPL_IF NAME=IS_NODE>
+<a href="configure_parameters?node_id=<TMPL_VAR NAME=ID>"><TMPL_VAR NAME=NAME> (node)</a>
+<TMPL_ELSE>
+<a href="configure_parameters?group=<TMPL_VAR NAME=ID>"><TMPL_VAR NAME=NAME> (group)</a>
+</TMPL_IF>
+</span>
+</TMPL_LOOP>
+</div>
+<div class="tabArea">
+<table id="parameters">
+<tr><td class="header">Name</td><td class="header">Value</td><td class="header">Unit</td><td class="header"></td></tr>
+<TMPL_LOOP NAME=PARAMETER_INFOS>
+<TMPL_IF NAME=ODD_ROW>
+<tr class="oddRow">
+<TMPL_ELSE>
+<tr class="evenRow">
+</TMPL_IF>
+<td class="parameterLabel"><TMPL_VAR NAME=NAME></td>
+<td class="parameterValue">
+<TMPL_IF NAME=IS_BOOLEAN>
+<select name="new_<TMPL_VAR NAME=NAME>">
+<TMPL_IF NAME=VALUE>
+<option value="on" selected>on</option>
+<option value="off">off</option>
+<TMPL_ELSE>
+<option value="on">on</option>
+<option value="off" selected>off</option>
+</TMPL_IF>
+</select>
+<TMPL_ELSE>
+<input type="text" name="new_<TMPL_VAR NAME=NAME>" value="<TMPL_VAR NAME=VALUE>"/>
+</TMPL_IF>
+
+<TMPL_IF NAME=IS_BOOLEAN>
+<TMPL_IF NAME=VALUE>
+<input type="hidden" name="old_<TMPL_VAR NAME=NAME>" value="on"/>
+<TMPL_ELSE>
+<input type="hidden" name="old_<TMPL_VAR NAME=NAME>" value="off"/>
+</TMPL_IF>
+<TMPL_ELSE>
+<input type="hidden" name="old_<TMPL_VAR NAME=NAME>" value="<TMPL_VAR NAME=VALUE>"/>
+</TMPL_IF>
+
+</td>
+<td class="parameterUnit"><TMPL_VAR NAME=UNIT></td>
+<td class="parameterDescription">
+<a href="javascript:toggleHelp('<TMPL_VAR NAME=NAME>');">?</a>
+<div id="<TMPL_VAR NAME=NAME>" style="display: none">
+<TMPL_VAR NAME=DESCRIPTION>
+</div>
+</td>
+</tr>
+</TMPL_LOOP>
+<tr><td class="footer" colspan="4">
+<input type="submit" value="Update"/>
+</td></tr>
+</form>
+</table>
+</div>
+<TMPL_ELSE>
+No nodes available
+</TMPL_IF>
--- /dev/null
+<script type="text/javascript">
+
+// The following is lended from http://www.webdeveloper.com/forum/archive/index.php/t-74982.html in a post by m2c
+/*
+** Returns the caret (cursor) position of the specified text field.
+** Return value range is 0-oField.length.
+*/
+function getCaretPosition (oField) {
+ // Initialize
+ var iCaretPos = 0;
+ // IE Support
+ if (document.selection) {
+ // Set focus on the element
+ oField.focus ();
+ // To get cursor position, get empty selection range
+ var oSel = document.selection.createRange ();
+ // Move selection start to 0 position
+ oSel.moveStart ('character', -oField.value.length);
+ // The caret position is selection length
+ iCaretPos = oSel.text.length;
+ }
+ // Firefox support
+ else if (oField.selectionStart || oField.selectionStart == '0')
+ iCaretPos = oField.selectionStart;
+ // Return results
+ return (iCaretPos);
+}
+
+/*
+ ** Sets the caret (cursor) position of the specified text field.
+ ** Valid positions are 0-oField.length.
+ */
+function setCaretPosition (oField, iCaretPos) {
+ // IE Support
+ if (document.selection) {
+ // Set focus on the element
+ oField.focus ();
+ // Create empty selection range
+ var oSel = document.selection.createRange ();
+ // Move selection start and end to 0 position
+ oSel.moveStart ('character', -oField.value.length);
+ // Move selection start and end to desired position
+ oSel.moveStart ('character', iCaretPos);
+ oSel.moveEnd ('character', 0);
+ oSel.select ();
+ }
+ // Firefox support
+ else if (oField.selectionStart || oField.selectionStart == '0') {
+ oField.selectionStart = iCaretPos;
+ oField.selectionEnd = iCaretPos;
+ oField.focus ();
+ }
+}
+// End of code lending
+
+
+var indentString = " ";
+
+function onEditorKeyDown(e) {
+ var keyCode = (window.Event) ? e.which : e.keyCode;
+ var charCode = e.charCode;
+ var editor = document.getElementById('editor');
+ var currentPosition = getCaretPosition(editor);
+
+// trap the tab
+ if (keyCode == 9) {
+ var indent = indentString;
+ var newValue = editor.value.substring(0, currentPosition) + indent
+ + editor.value.substring(currentPosition);
+ editor.value = newValue;
+ setCaretPosition(editor, currentPosition + indent.length);
+ return false;
+ }
+
+ return true;
+}
+
+function showNewEntry() {
+ document.getElementById('newVclCell').style['display'] = 'block';
+ document.getElementById('newVclEntry').focus();
+}
+
+function onNewVclEntryDown(e, form) {
+ var keyCode = (window.Event) ? e.which : e.keyCode;
+
+ if (keyCode == 13) {
+ form.operation.value = 'new';
+ form.submit();
+ }
+}
+
+function goToPosition(row, column) {
+ var editor = document.getElementById('editor');
+ var text = editor.value;
+ var pos = 0;
+
+ while (row > 1) {
+ if (text.charAt(pos) == '\n')
+ row--;
+ pos++;
+ }
+
+ pos += column -1;
+ setCaretPosition(editor, pos);
+}
+
+</script>
+<TMPL_IF NAME=GROUP_INFOS>
+<div class="tabArea">
+<div class="tabHeader">
+<TMPL_LOOP NAME=GROUP_INFOS>
+<TMPL_IF NAME=SELECTED>
+<span class="selectedTab">
+<TMPL_ELSE>
+<span class="tab">
+</TMPL_IF>
+<a href="edit_vcl?group_name=<TMPL_VAR NAME=NAME>"><TMPL_VAR NAME=NAME></a>
+</span>
+</TMPL_LOOP>
+</div>
+<form action="edit_vcl" method="POST">
+<textarea cols=80 rows=30 onkeydown="return onEditorKeyDown(event);" id="editor" name="vcl"><TMPL_VAR NAME=VCL></textarea>
+<br/>
+<table>
+<tr>
+<td>
+<input type="button" onclick="showNewEntry();" value="New"/>
+</td>
+<td id="newVclCell" style="display: none;">
+<input type="text" name="new_vcl_name" id="newVclEntry" onkeydown="onNewVclEntryDown(event, this.form)">
+<input type="button" onclick="this.form.operation.value='new'; this.form.submit();" value="OK">
+</td>
+<td>
+|
+<select name="vcl_name">
+<TMPL_LOOP NAME=VCL_INFOS>
+<option
+<TMPL_IF NAME=SELECTED>
+selected
+</TMPL_IF>
+value="<TMPL_VAR NAME=NAME>"><TMPL_VAR NAME=NAME>
+<TMPL_IF NAME=ACTIVE>
+(active)
+</TMPL_IF>
+</option>
+</TMPL_LOOP>
+</select>
+</td>
+<td>
+<input type="hidden" value="load" name="operation"/>
+<input type="submit" value="Load"/>
+<TMPL_IF NAME=EDITING_NEW_VCL>
+<input type="button" disabled value="Make active"/>
+<TMPL_ELSE>
+<input type="button" onclick="this.form.operation.value='make_active'; this.form.submit();" value="Make active"/>
+</TMPL_IF>
+<TMPL_LOOP NAME=GROUP_INFOS>
+<TMPL_IF NAME=SELECTED>
+<input type="hidden" name="group_name" value="<TMPL_VAR NAME=NAME>"/>
+</TMPL_IF>
+</TMPL_LOOP>
+<input type="button" onclick="this.form.operation.value='save'; this.form.submit();" value="Save"/>
+| <input type="button" onclick="this.form.operation.value='discard'; this.form.submit();" value="Discard"/>
+</td>
+</tr>
+</table>
+</form>
+<TMPL_IF NAME=EDITING_NEW_VCL>
+<script type="text/javascript">
+document.getElementById('editor').focus();
+</script>
+</TMPL_IF>
+<TMPL_IF NAME=VCL_ERROR>
+<b>Errors found in the VCL:</b>
+<div id="vclError">
+<TMPL_VAR NAME=VCL_ERROR>
+</div>
+</TMPL_IF>
+</div>
+<TMPL_ELSE>
+No groups available
+</TMPL_IF>
--- /dev/null
+<script type="text/javascript">
+<TMPL_LOOP NAME=NODE_INFOS>
+<TMPL_IF NAME=SELECTED>
+var currentNodeId = <TMPL_VAR NAME=ID>;
+</TMPL_IF>
+</TMPL_LOOP>
+
+var commandHistoryMaxSize = 10;
+var commandHistoryIndex = 0;
+var commandHistorySize = 1;
+var commandHistory = new Array(10);
+
+var lastMatchIndex = -1;
+var lastPartialCommand;
+var managementCommands = [
+ "help",
+ "param.set",
+ "param.show",
+ "ping",
+ "purge.hash",
+ "purge.list",
+ "purge.url",
+ "quit",
+ "start",
+ "stats",
+ "status",
+ "stop",
+ "vcl.discard",
+ "vcl.inline",
+ "vcl.list",
+ "vcl.load",
+ "vcl.show",
+ "vcl.use"
+ ];
+
+function consolePrint(text) {
+ var console = document.getElementById('console');
+ console.value += text + "\n";
+ console.scrollTop = console.scrollHeight;
+}
+
+// taken from http://www.jibbering.com/2002/4/httprequest.html
+function doHttpRequest(url) {
+ var xmlhttp=false;
+ /*@cc_on @*/
+ /*@if (@_jscript_version >= 5)
+ // JScript gives us Conditional compilation, we can cope with old IE versions.
+ // and security blocked creation of the objects.
+ try {
+ xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
+ } catch (e) {
+ try {
+ xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
+ } catch (E) {
+ xmlhttp = false;
+ }
+ }
+ @end @*/
+ if (!xmlhttp && typeof XMLHttpRequest!='undefined') {
+ try {
+ xmlhttp = new XMLHttpRequest();
+ } catch (e) {
+ xmlhttp=false;
+ }
+ }
+ if (!xmlhttp && window.createRequest) {
+ try {
+ xmlhttp = window.createRequest();
+ } catch (e) {
+ xmlhttp=false;
+ }
+ }
+ xmlhttp.open("GET", url, true);
+ xmlhttp.onreadystatechange=function() {
+ if (xmlhttp.readyState == 4)
+ consolePrint(xmlhttp.responseText);
+ }
+ xmlhttp.send(null)
+}
+
+function runCommand(command) {
+ if (command == "cls")
+ document.getElementById('console').value = "";
+ else {
+ consolePrint(command);
+ var url = "http://<TMPL_VAR NAME=SERVER_HOST>:<TMPL_VAR NAME=SERVER_PORT>/send_management_command?node_id=" + escape(currentNodeId) + "&command=" + escape(command);
+ doHttpRequest(url);
+ }
+}
+
+function onInputKeyDown(e) {
+ var keyCode = (window.Event) ? e.which : e.keyCode;
+
+ if (keyCode == 13) {
+ var input = document.getElementById('input');
+ runCommand(input.value);
+ commandHistory[0] = input.value;
+
+ if (commandHistorySize < commandHistoryMaxSize)
+ commandHistorySize++;
+ for (var i = commandHistorySize - 1; i > 0; i--)
+ commandHistory[i] = commandHistory[i-1];
+ commandHistory[0] = input.value = "";
+ commandHistoryIndex = 0;
+ }
+ else if (keyCode == 9) {
+ var firstMatchIndex = -1;
+ var matchIndex = -1;
+ var partialCommandPattern = new RegExp("^" + lastPartialCommand);
+ for (var i = 0; i < managementCommands.length; i++) {
+ if (managementCommands[i].match(partialCommandPattern)) {
+ if (i > lastMatchIndex) {
+ matchIndex = i;
+ break;
+ }
+
+ if (firstMatchIndex == -1)
+ firstMatchIndex = i;
+ }
+ }
+
+ if (firstMatchIndex != -1 && matchIndex == -1 )
+ matchIndex = firstMatchIndex;
+
+ if (matchIndex != -1) {
+ lastMatchIndex = matchIndex;
+ document.getElementById('input').value = managementCommands[matchIndex];
+ }
+
+ // trap tab from escaping the input field
+ return false;
+ }
+ else if (keyCode == 38) {
+
+ if (commandHistoryIndex + 1 < commandHistorySize) {
+ commandHistoryIndex++;
+ document.getElementById('input').value = commandHistory[commandHistoryIndex];
+ }
+
+ return false;
+ }
+ else if (keyCode == 40) {
+
+ if (commandHistoryIndex > 0 ) {
+ commandHistoryIndex--;
+ document.getElementById('input').value = commandHistory[commandHistoryIndex];
+ }
+
+ return false;
+ }
+
+}
+
+function onInputKeyUp(e) {
+ var keyCode = (window.Event) ? e.which : e.keyCode;
+ var inputValue = document.getElementById('input').value;
+
+ if (keyCode != 10 && keyCode != 9)
+ lastPartialCommand = inputValue;
+ if (keyCode != 38 && keyCode != 40)
+ commandHistory[0] = inputValue;
+}
+
+function setConsoleColors(foreground, background) {
+ var console = document.getElementById('console');
+ var input = document.getElementById('input');
+ console.style['color'] = foreground;
+ console.style['backgroundColor'] = background;
+ input.style['color'] = foreground;
+ input.style['backgroundColor'] = background;
+
+ document.getElementById('input').focus();
+}
+
+function adjustConsoleSettings() {
+ var console = document.getElementById('console');
+ console.rows = document.getElementById('consoleRows').value;
+ console.cols = document.getElementById('consoleCols').value;
+}
+</script>
+
+<TMPL_IF NAME=NODE_INFOS>
+<div class="tabHeader">
+<TMPL_LOOP NAME=NODE_INFOS>
+<TMPL_IF NAME=SELECTED>
+<span class="selectedTab">
+<TMPL_ELSE>
+<span class="tab">
+</TMPL_IF>
+<a href="management_console?node_id=<TMPL_VAR NAME=ID>"><TMPL_VAR NAME=NAME></a>
+</span>
+</TMPL_LOOP>
+</div>
+<div class="tabArea">
+<textarea id="console" cols="<TMPL_VAR NAME=DEFAULT_CONSOLE_COLS>" rows="<TMPL_VAR NAME=DEFAULT_CONSOLE_ROWS>" style="font-size: <TMPL_VAR NAME=DEFAULT_CONSOLE_FONT_SIZE>;" readonly>
+
+ oooo$$$$$$$$$$$$oooo
+ oo$$$$$$$$$$$$$$$$$$$$$$$$o
+ oo$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o o$ $$ o$
+ o $ oo o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o $$ $$ $$o$
+ oo $ $ "$ o$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$o $$$o$$o$
+ "$$$$$$o$ o$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$o $$$$$$$$
+ $$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$
+ $$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$$$$$$ """$$$
+ "$$$""""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$
+ $$$ o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$o
+ o$$" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$o
+ $$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" "$$$$$$ooooo$$$$o
+ o$$$oooo$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o$$$$$$$$$$$$$$$$$
+ $$$$$$$$"$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$""""""""
+ """" $$$$ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" o$$$
+ "$$$o """$$$$$$$$$$$$$$$$$$"$$" $$$
+ $$$o "$$""$$$$$$"""" o$$$ Welcome to the
+ $$$$o o$$$"
+ "$$$$o o$$$$$$o"$$$$o o$$$$ Varnish
+ "$$$$$oo ""$$$$o$$$$$o o$$$$""
+ ""$$$$$oooo "$$$o$$$$$$$$$""" management console
+ ""$$$$$$$oo $$$$$$$$$$
+ """"$$$$$$$$$$$ for
+ $$$$$$$$$$$$
+ $$$$$$$$$$" <TMPL_VAR NAME=CURRENT_NODE_NAME>
+ "$$$""""
+</textarea><br/>
+><input size="60" type="text" id="input" onkeydown="return onInputKeyDown(event);" onkeyup="onInputKeyUp(event);"/>
+<br/>
+Console settings:
+<ul>
+<li class="consoleSetting">
+Color theme:
+<TMPL_LOOP NAME=CONSOLE_THEMES>
+<a style="font-size: <TMPL_VAR NAME=DEFAULT_CONSOLE_FONT_SIZE>; margin-left: 3px; padding: 5px; background-color: <TMPL_VAR NAME=BACKGROUND>; color: <TMPL_VAR NAME=FOREGROUND>;"
+ href="javascript:setConsoleColors('<TMPL_VAR NAME=FOREGROUND>', '<TMPL_VAR NAME=BACKGROUND>')">
+<TMPL_VAR NAME=NAME>
+</a>
+</TMPL_LOOP>
+</li>
+<li class="consoleSetting">Size:
+<input type="text" id="consoleCols" value="<TMPL_VAR NAME=DEFAULT_CONSOLE_COLS>" onchange="adjustConsoleSettings();"> x
+ <input type="text" id="consoleRows" value="<TMPL_VAR NAME=DEFAULT_CONSOLE_ROWS>" onchange="adjustConsoleSettings();">
+ </li>
+</div>
+<script type="text/javascript">
+document.getElementById('input').focus();
+setConsoleColors('<TMPL_VAR NAME=DEFAULT_CONSOLE_FOREGROUND>', '<TMPL_VAR NAME=DEFAULT_CONSOLE_BACKGROUND>');
+</script>
+<TMPL_ELSE>
+No nodes to manage.
+</TMPL_IF>
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<link rel="stylesheet" type="text/css" href="css/web.css" />
+<link rel="icon" type="image/png" href="/images/favicon.png">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<TMPL_IF NAME=AUTO_REFRESH_INTERVAL>
+<meta http-equiv="refresh" content="<TMPL_VAR NAME=AUTO_REFRESH_INTERVAL>">
+</TMPL_IF>
+<title>Varnish - Lust controller christmas edition</title>
+</head>
+<body>
+<div id="canvas">
+<div id="header">
+<img id="headerLogo" src="images/varnish-logo-christmas.png" id="logo"/>
+<span id="menu">
+<a href="/view_stats" class="menu">View stats</a>
+<a href="/configure_parameters" class="menu">Configure parameters</a>
+<a href="/edit_vcl" class="menu">Edit VCL</a>
+<a href="/node_management" class="menu">Node management</a>
+<a href="/management_console" class="menu">Management console</a>
+</span>
+</div>
+<div id="content">
+<TMPL_IF NAME=ERROR>
+<div id="error">
+An error occured:<br/>
+<pre>
+<TMPL_VAR NAME=ERROR>
+</pre>
+</div>
+</TMPL_IF>
+<TMPL_IF NAME=STATUS>
+<div id="status">
+<TMPL_VAR NAME=STATUS>
+</div>
+</TMPL_IF>
+<TMPL_INCLUDE NAME=templates/CONTENT_TEMPLATE>
+</div>
+</div>
+</body>
+</html>
--- /dev/null
+<div class="tabHeader">
+<TMPL_LOOP NAME=GROUP_INFOS>
+<TMPL_IF NAME=SELECTED>
+<span class="selectedTab">
+<TMPL_ELSE>
+<span class="tab">
+</TMPL_IF>
+<a href="node_management?group=<TMPL_VAR NAME=NAME>"><TMPL_VAR NAME=NAME></a>
+</span>
+</TMPL_LOOP>
+<TMPL_IF NAME=ADD_GROUP>
+<span class="selectedTab">
+<TMPL_ELSE>
+<span class="tab">
+</TMPL_IF>
+<a href="node_management?operation=add_group">Add..</a>
+</span>
+</div>
+<div class="tabArea">
+<TMPL_IF NAME=ADD_GROUP>
+<form action="node_management" method="post">
+<input type="hidden" name="operation" value="add_group"/>
+<table>
+<tr><td>Name:</td><td><input name="group"/></td></tr>
+<tr><td><input type="submit" value="Add"/></td></tr>
+</table>
+<TMPL_ELSE>
+<table>
+<tr>
+<td class="header">V</td>
+<td class="header">M</td>
+<td class="header">Name</td>
+<td class="header">Address</td>
+<td class="header">Port</td>
+<td class="header">Management port</td>
+<td></td></tr>
+<TMPL_LOOP NAME=NODE_INFOS>
+<tr>
+<td>
+<TMPL_IF NAME=IS_RUNNING_OK>
+<img src="images/running.png"/>
+<TMPL_ELSE>
+<TMPL_IF NAME=IS_RUNNING>
+<img src="images/running_nok.png"/>
+<TMPL_ELSE>
+<img src="images/stopped.png"/>
+</TMPL_IF>
+</TMPL_IF>
+</td>
+<td>
+<TMPL_IF NAME=IS_MANAGEMENT_RUNNING>
+<img src="images/running.png"/>
+<TMPL_ELSE>
+<img src="images/stopped.png"/>
+</TMPL_IF>
+</td>
+<td><TMPL_VAR NAME=NAME></td>
+<td><TMPL_VAR NAME=ADDRESS></td>
+<td><TMPL_VAR NAME=PORT></td>
+<td><TMPL_VAR NAME=MANAGEMENT_PORT></td>
+<td>
+<form action="node_management" method="POST">
+<input type="hidden" name="node_id" value="<TMPL_VAR NAME=ID>">
+<input type="hidden" name="operation" value="start_node">
+<input type="submit" value="Start">
+</form>
+</td>
+<td>
+<form action="node_management" method="POST">
+<input type="hidden" name="node_id" value="<TMPL_VAR NAME=ID>">
+<input type="hidden" name="operation" value="stop_node">
+<input type="submit" value="Stop">
+</form>
+</td>
+<td>
+<form action="node_management" method="POST">
+<input type="hidden" name="node_id" value="<TMPL_VAR NAME=ID>">
+<input type="hidden" name="operation" value="remove_node">
+<input type="submit" value="Remove" class="removeNode">
+</form>
+</td>
+</tr>
+</TMPL_LOOP>
+</td>
+<tr>
+<form action="node_management" method="post">
+<input type="hidden" name="operation" value="add_node"/>
+<td></td>
+<td></td>
+<td><input type="text" name="name"/></td>
+<td><input type="text" name="address"/></td>
+<td><input type="text" name="port"/></td>
+<td><input type="text" name="management_port" value="<TMPL_VAR NAME=DEFAULT_MANAGEMENT_PORT>"/></td>
+<td>
+<input type="hidden" name="group" value="<TMPL_VAR NAME=GROUP>"/>
+<input type="submit" value="Add"/></td>
+</tr>
+</form>
+</table>
+<TMPL_IF NAME=BACKEND_HEALTH_INFOS>
+<table>
+<tr><td class="header">Backend</td><td class="header">Health</td></tr>
+<TMPL_LOOP NAME=BACKEND_HEALTH_INFOS>
+<tr><td><TMPL_VAR NAME=NAME></td><td><TMPL_VAR NAME=HEALTH></td></tr>
+</TMPL_LOOP>
+</table>
+<TMPL_ELSE>
+<i>No backend health information available</i>
+</TMPL_IF>
+<div id="groupControls">
+<table>
+<tr>
+<td>
+<form action="node_management" method="POST">
+<input type="hidden" name="group" value="<TMPL_VAR NAME=GROUP>">
+<input type="hidden" name="operation" value="start_group">
+<input type="submit" value="Start group">
+</form>
+</td>
+<td>
+<form action="node_management" method="POST">
+<input type="hidden" name="group" value="<TMPL_VAR NAME=GROUP>">
+<input type="hidden" name="operation" value="stop_group">
+<input type="submit" value="Stop group">
+</form>
+</td>
+<td>
+<form action="node_management" method="POST">
+<input type="hidden" name="group" value="<TMPL_VAR NAME=GROUP>">
+<input type="hidden" name="operation" value="remove_group">
+<input type="submit" value="Remove group" class="removeNode">
+</form>
+</td>
+</tr>
+</table>
+</div>
+</TMPL_IF>
+</div>
--- /dev/null
+<script type="text/javascript">
+
+function loadGraph(nodeId, graphId, timeSpan) {
+ var id= "img_" + nodeId + "_" + graphId;
+ var graphUrl = "/generate_graph?time_span=" + timeSpan + "&node_id=" + nodeId + "&type=" + graphId;
+
+ var graphImage = document.getElementById(id);
+ if (graphImage)
+ graphImage.src = graphUrl;
+}
+
+</script>
+
+<TMPL_IF NAME=NODE_INFOS>
+<h2>
+<TMPL_IF NAME=STAT_TIME>
+Statistics collected at <TMPL_VAR NAME=STAT_TIME>
+<TMPL_IF NAME=AUTO_REFRESH>
+<span style="font-size:0.5em">(auto-refreshing)</span>
+</TMPL_IF>
+<TMPL_ELSE>
+No statistics yet collected
+</TMPL_IF>
+</h2>
+<table id="statsTable">
+<tr>
+<td>
+<h2>Summary</h2>
+</td>
+</tr>
+<tr class="header">
+<td></td>
+<TMPL_LOOP NAME=NODE_INFOS>
+<td><TMPL_VAR NAME=NAME></td>
+</TMPL_LOOP>
+</tr>
+<tr>
+<td>
+<TMPL_LOOP NAME=SUMMARY_STATS>
+<TMPL_IF NAME=ODD_ROW>
+<tr class="oddRow">
+<TMPL_ELSE>
+<tr class="evenRow">
+</TMPL_IF>
+<td><TMPL_VAR NAME=NAME></td>
+<TMPL_LOOP NAME=VALUES>
+<td>
+<TMPL_IF NAME=IS_GRAPH>
+<div class="graph">
+<img id="img_<TMPL_VAR NAME=NODE_ID>_<TMPL_VAR NAME=GRAPH_ID>" src=""/><br/>
+Last:
+<a class="timeSpan" id="link_<TMPL_VAR NAME=NODE_ID>_<TMPL_VAR NAME=GRAPH_ID>" href="javascript:loadGraph('<TMPL_VAR NAME=NODE_ID>', '<TMPL_VAR NAME=GRAPH_ID>', 'minute')">Mi</a>
+<a class="timeSpan" id="link_<TMPL_VAR NAME=NODE_ID>_<TMPL_VAR NAME=GRAPH_ID>_hour" href="javascript:loadGraph('<TMPL_VAR NAME=NODE_ID>', '<TMPL_VAR NAME=GRAPH_ID>', 'hour')">H</a>
+<a class="timeSpan" id="link_<TMPL_VAR NAME=NODE_ID>_<TMPL_VAR NAME=GRAPH_ID>_hour" href="javascript:loadGraph('<TMPL_VAR NAME=NODE_ID>', '<TMPL_VAR NAME=GRAPH_ID>', 'day')">D</a>
+<a class="timeSpan" id="link_<TMPL_VAR NAME=NODE_ID>_<TMPL_VAR NAME=GRAPH_ID>_hour" href="javascript:loadGraph('<TMPL_VAR NAME=NODE_ID>', '<TMPL_VAR NAME=GRAPH_ID>', 'week')">W</a>
+<a class="timeSpan" id="link_<TMPL_VAR NAME=NODE_ID>_<TMPL_VAR NAME=GRAPH_ID>_hour" href="javascript:loadGraph('<TMPL_VAR NAME=NODE_ID>', '<TMPL_VAR NAME=GRAPH_ID>', 'month')">Mo</a>
+</div>
+<script type="text/javascript">
+loadGraph('<TMPL_VAR NAME=NODE_ID>', '<TMPL_VAR NAME=GRAPH_ID>', 'minute');
+</script>
+<TMPL_ELSE>
+<TMPL_VAR NAME=VALUE>
+</TMPL_IF>
+</td>
+</TMPL_LOOP>
+<tr>
+</TMPL_LOOP>
+<TMPL_IF NAME=VIEW_RAW_STATS>
+<tr><td><h2>Raw statistics</h2>
+</td></tr>
+<tr>
+<td>
+<TMPL_LOOP NAME=RAW_STATS>
+<TMPL_IF NAME=ODD_ROW>
+<tr class="oddRow">
+<TMPL_ELSE>
+<tr class="evenRow">
+</TMPL_IF>
+<td><TMPL_VAR NAME=NAME></td>
+<TMPL_LOOP NAME=VALUES>
+<td><TMPL_VAR NAME=VALUE></td>
+</TMPL_LOOP>
+</tr>
+</TMPL_LOOP>
+</table>
+Raw statistics: On <a href="/view_stats?view_raw_stats=0&auto_refresh=<TMPL_VAR NAME=AUTO_REFRESH>">Off</a>
+<TMPL_ELSE>
+</table>
+Raw statistics: <a href="/view_stats?view_raw_stats=1&auto_refresh=<TMPL_VAR NAME=AUTO_REFRESH>">On</a> Off
+</TMPL_IF>
+| Auto refresh:
+<TMPL_IF NAME=AUTO_REFRESH>
+On <a href="/view_stats?view_raw_stats=<TMPL_VAR NAME=VIEW_RAW_STATS>&auto_refresh=0">Off</a>
+<TMPL_ELSE>
+<a href="/view_stats?view_raw_stats=<TMPL_VAR NAME=VIEW_RAW_STATS>&auto_refresh=1">On</a> Off
+</TMPL_IF>
+<TMPL_ELSE>
+No nodes available
+</TMPL_IF>
--- /dev/null
+use strict;
+use warnings;
+use Varnish::Management;
+
+my $console = Varnish::Management->new("localhost", "9001");
+
+my $status = $console->set_config("testing", "backend b0 { .host = \"localhost\"; .port = \"8080\"; }");
+
+if ($status ne "") {
+ print "Error:\n$status\n";
+}
+
+for my $config ($console->get_config_names()) {
+ print "Config: $config\n";
+ print "-" x 80 . "\n";
+ print $console->get_config($config) . "\n";
+}
+
+
+my %stats_counter = $console->get_stats();
+while (my ($stat, $value) = each %stats_counter) {
+ print "$stat = $value\n";
+}
+
+if ($console->ping()) {
+ print "I am alive!\n";
+}
+else {
+ print "I am dead: " . $console->get_error() . "\n";
+}
+
+my $console2 = Varnish::Management->new("localhost", "9002");
+
+
+if ($console2->ping()) {
+ print "I am alive!\n";
+}
+else {
+ print "I am dead: " . $console2->get_error() . "\n";
+}
+