summaryrefslogtreecommitdiffstats
path: root/bin/generate-mirror-mail.pl
blob: a5966e9c4d58e866f63c35f0a410450265e9d7d6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
#!/usr/bin/perl
use warnings;
use strict;

use Config::Tiny;
use Date::Format;
use Date::Parse;
use File::Basename;
use HTTP::Cookies;
use JSON;
use List::Util qw(uniq);
use Text::Template;
use Try::Tiny;
use WWW::Mechanize;

use Data::Dumper;

=head1 NAME

generate-mirror-mail.pl - Generate notification mails for broken mirrors

=head1 DESCRIPTION

Run the script and pass it URLs to the archweb mirror page (e.g.
<https://www.archlinux.org/mirrors/melbourneitmirror.net/>) via STDIN. If the
mirror has a problem the script will generate an appropriate mail and run
`compose-mail-from-stdin` which should be a script that starts your favourite
mail client with the mail. You can also let the script send the mail directly
if you do not want to review it.

=cut

# TODO: put this in a config file

#$ENV{HTTPS_CA_FILE}    = '/etc/ssl/certs/ca-certificates.crt';

my %templates = (
	'out-of-sync' => {
		'subject' => '[{$mirror_name}] Arch Linux mirror out of sync',
		'template' => 'Hi,

Your mirror seems to be out of sync since {$out_of_sync{last_sync}}, could you please
investigate?

{$mirror_url}
{$out_of_sync{mirror_urls}}

Thanks,
{$mail_from_name}
',
	},
	'connection-failed' => {
		'subject' => '[{$mirror_name}] Arch Linux mirror not accessible{$OUT = ", ".join("/", @{$connection_failed{protocols}}) if @{$connection_failed{protocols}} > 0;}',
		'template' => 'Hi,

We\'re having trouble connecting to and/or verifying the content of your mirror{$OUT = " via ".join(", ", @{$connection_failed{protocols}}) if @{$connection_failed{protocols}} > 0;}. Could you
please check what\'s going on?

{$mirror_url}
{$connection_failed{mirror_urls}}

Thanks,
{$mail_from_name}
',
	},
	'multiple-issues' => {
		'subject' => '[{$mirror_name}] Multiple issues with your Arch Linux mirror',
		'template' => 'Hi,

We are seeing multiple issues with your Arch Linux mirror {$mirror_name}
{$mirror_url}

 - We are unable to reach and/or verify the content of your mirror{$OUT = " via ".join(", ", @{$connection_failed{protocols}}) if @{$connection_failed{protocols}} > 0;}:
{$connection_failed{mirror_urls}}

 - Your mirror seems to be out of sync since {$out_of_sync{last_sync}}:
{$out_of_sync{mirror_urls}}

Could you please check what is wrong?

Thanks,
{$mail_from_name}
',
	},
);

my $Config = Config::Tiny->new();
$Config = Config::Tiny->read(dirname($0) . "/../settings.conf");

my $cookie_jar = HTTP::Cookies->new(file => dirname($0) . "/../cookie_jar", autosave => 1);
my $mech = WWW::Mechanize->new(agent => "arch-mirror-tools", cookie_jar => $cookie_jar);

sub login {
	$mech->get("https://www.archlinux.org/login/");
	my $res = $mech->submit_form(
		form_id => "dev-login-form",
		fields => {
			username => $Config->{account}->{username},
			password => $Config->{account}->{password}
		}
	);
}

sub send_mail {
	my $to = shift;
	my $subject = shift;
	my $body = shift;

	open my $fh, "|compose-mail-from-stdin" or die "Failed to run mailer: $!";
	print $fh "To: $to\n";
	printf $fh "From: %s\n", $Config->{misc}->{email_from};
	print $fh "Subject: $subject\n";
	print $fh "\n";
	print $fh "$body";
	close $fh;
}

sub send_template_mail {
	my $to = shift;
	my $subject = shift;
	my $body = shift;
	my $values = shift;

	send_mail($to, fill_template($subject, $values), fill_template($body, $values));
}

sub fill_template {
	my $template = shift;
	my $values = shift;
	my $result = Text::Template::fill_in_string($template, HASH => $values)
		or die "Failed to fill in template: $Text::Template::ERROR";

	return $result;
}

sub get_mirror_json {
	my ($url) = @_;

	$mech->get($url."/json/");
	return JSON::decode_json($mech->content());
}

while (<STDIN>) {
	try {
		my $url = $_;
		chomp($url);
		die "Skipping non-mirror detail URL" if $url =~ m/\/[0-9]+(\/|$)/;
		die "Skipping non-mirror detail URL" if $url eq "https://www.archlinux.org/mirrors/status/";

		my ($mirror_name) = ($url =~ m#/([^/]+)/?$#);
		my $json = get_mirror_json($url);

		if (not defined $json->{admin_email}) {
			login();
			$json = get_mirror_json($url);
			die "Admin email not set in mirror json. Login problem?" unless defined $json->{admin_email};
		}

		my $issues;

		for my $mirror (@{$json->{urls}}) {
			next if not $mirror->{active};
			if ($mirror->{last_sync}) {
				my $time = str2time($mirror->{last_sync});
				if ($time < time() - 60*60*24*3) {
					push @{$issues->{out_of_sync}}, {
						time => $time,
						url => $mirror->{url},
						details_link => $mirror->{details},
					};
				}
			} else {
			#if ($mirror->{last_sync} and $mirror->{completion_pct} < 0.9 and $mirror->{completion_pct} > 0) {
				push @{$issues->{connection_failed}}, {
					url => $mirror->{url},
					details_link => $mirror->{details},
					protocol => $mirror->{protocol},
				};
			}
		}

		# extract and deduplicate sync times
		my @last_sync = keys %{{ map { ${$_}{time} => 1 } @{$issues->{out_of_sync}} }};
		my $sent_mail = 0;

		my $to = $json->{admin_email};
		$to .= ",".$json->{alternate_email} if $json->{alternate_email} ne "";

		my %values = (
			out_of_sync => {
				last_sync => join(", ", map {time2str("%Y-%m-%d", $_)} @last_sync),
				mirror_urls => join("\n", map {${$_}{details_link}} @{$issues->{out_of_sync}}),
			},
			connection_failed => {
				mirror_urls => join("\n", map {${$_}{details_link}} @{$issues->{connection_failed}}),
			},
			mirror_name => $mirror_name,
			mirror_url => $url,
			mail_from_name => $Config->{misc}->{name} // die "misc.name not set in config",
		);

		my @protocols = uniq map {${$_}{protocol}} @{$issues->{connection_failed}};
		my @active_urls = grep { $_->{active} } @{$json->{urls}};
		if (scalar(@protocols) != scalar(@active_urls)) {
			$values{connection_failed}->{protocols} = \@protocols;
		}

		my $issue_type_count = grep {@{$issues->{$_}} > 0} keys %$issues;
		if ($issue_type_count > 1) {
			send_template_mail($to, $templates{"multiple-issues"}{"subject"}, $templates{"multiple-issues"}{"template"}, \%values);
			$sent_mail = 1;
		} elsif (@{$issues->{out_of_sync}}) {
			send_template_mail($to, $templates{"out-of-sync"}{"subject"}, $templates{"out-of-sync"}{"template"}, \%values);
			$sent_mail = 1;
		} elsif (@{$issues->{connection_failed}}) {
			send_template_mail($to, $templates{"connection-failed"}{"subject"}, $templates{"connection-failed"}{"template"}, \%values);
			$sent_mail = 1;
		}

		if (!$sent_mail) {
			say STDERR "No issue detected for mirror $mirror_name";
		}

	} catch {
		warn "ignoring error: $_";
	}
}