1package Ubic::Admin::Setup;
2{
3  $Ubic::Admin::Setup::VERSION = '1.58';
4}
5
6# ABSTRACT: this module handles ubic setup: asks user some questions and configures your system
7
8
9use strict;
10use warnings;
11
12use Getopt::Long 2.33;
13use Carp;
14use IPC::Open3;
15use File::Path;
16use File::Which;
17use File::Spec;
18
19use Ubic::AtomicFile;
20use Ubic::Settings;
21use Ubic::Settings::ConfigFile;
22
23my $batch_mode;
24my $quiet;
25
26sub _defaults {
27    if ($^O eq 'freebsd') {
28        return (
29            config => '/usr/local/etc/ubic/ubic.cfg',
30            data_dir => '/var/db/ubic',
31            service_dir => '/usr/local/etc/ubic/service',
32            log_dir => '/var/log/ubic',
33            example => '/usr/local/etc, /var',
34        );
35    }
36    else {
37        # fhs
38        return (
39            config => '/etc/ubic/ubic.cfg',
40            data_dir => '/var/lib/ubic',
41            service_dir => '/etc/ubic/service',
42            log_dir => '/var/log/ubic',
43            example => '/etc, /var',
44        );
45    }
46};
47
48sub print_tty {
49    print @_ unless $quiet or $batch_mode;
50}
51
52sub prompt ($;$) {
53    my($mess, $def) = @_;
54    Carp::confess("prompt function called without an argument")
55        unless defined $mess;
56
57    return $def if $batch_mode;
58
59    my $isa_tty = -t STDIN && (-t STDOUT || !(-f STDOUT || -c STDOUT));
60    Carp::confess("tty not found") if not $isa_tty;
61
62    $def = defined $def ? $def : "";
63
64    local $| = 1;
65    local $\ = undef;
66    print "$mess ";
67
68    my $ans;
69    if (not $isa_tty and eof STDIN) {
70        print "$def\n";
71    }
72    else {
73        $ans = <STDIN>;
74        if( defined $ans ) {
75            chomp $ans;
76        }
77        else { # user hit ctrl-D
78            print "\n";
79        }
80    }
81
82    return (!defined $ans || $ans eq '') ? $def : $ans;
83}
84
85sub prompt_str {
86    my ($description, $default) = @_;
87    return prompt("$description [$default]", $default);
88}
89
90sub prompt_bool {
91    my ($description, $default) = @_;
92    my $yn = ($default ? 'y' : 'n');
93    my $yn_hint = ($default ? 'Y/n' : 'y/N');
94    my $result = prompt("$description [$yn_hint]", $yn);
95    if ($result =~ /^y/i) {
96        return 1;
97    }
98    return;
99}
100
101sub xsystem {
102    local $! = local $? = 0;
103    return if system(@_) == 0;
104
105    my @msg;
106    if ($!) {
107        push @msg, "error ".int($!)." '$!'";
108    }
109    if ($? > 0) {
110        push @msg, "kill by signal ".($? & 127) if ($? & 127);
111        push @msg, "core dumped" if ($? & 128);
112        push @msg, "exit code ".($? >> 8) if $? >> 8;
113    }
114    die join ", ", @msg;
115}
116
117sub slurp {
118    my ($file) = @_;
119    open my $fh, '<', $file or die "Can't open $file: $!";
120    my $result = join '', <$fh>;
121    close $fh;
122    return $result;
123}
124
125sub setup {
126
127    my $opt_reconfigure;
128    my $opt_service_dir;
129    my $opt_data_dir;
130    my $opt_log_dir;
131    my $opt_default_user = 'root';
132    my $opt_sticky_777 = 1;
133    my $opt_install_services = 1;
134    my $opt_crontab = 1;
135    my $opt_local;
136
137    # These options are documented in ubic-admin script POD.
138    # Don't forget to update their description if you change them.
139    GetOptions(
140        'local!' => \$opt_local,
141        'batch-mode' => \$batch_mode,
142        'quiet' => \$quiet,
143        'reconfigure!' => \$opt_reconfigure,
144        'service-dir=s' => \$opt_service_dir,
145        'data-dir=s' => \$opt_data_dir,
146        'log-dir=s' => \$opt_log_dir,
147        'default-user=s' => \$opt_default_user,
148        'sticky-777!' => \$opt_sticky_777,
149        'install-services!' => \$opt_install_services,
150        'crontab!' => \$opt_crontab,
151    ) or die "Getopt failed";
152
153    die "Unexpected arguments '@ARGV'" if @ARGV;
154
155    my %defaults = _defaults();
156
157    eval { Ubic::Settings->check_settings };
158    unless ($@) {
159        my $go = prompt_bool("Looks like ubic is already configured, do you want to reconfigure?", $opt_reconfigure);
160        return unless $go;
161        print_tty "\n";
162    }
163
164    print_tty "Ubic can be installed either in your home dir or into standard system paths ($defaults{example}).\n";
165    print_tty "You need to be root to install it into system.\n";
166
167    unless ($batch_mode) {
168        $batch_mode = prompt_bool("Would you like to configure as much as possible automatically?", 1);
169    }
170
171    # ideally, we want is_root option and local option to be orthogonal
172    # it's not completely true by now, though
173    my $is_root = ( $> ? 0 : 1 );
174    my $local = $opt_local;
175    unless (defined $local) {
176        if ($is_root) {
177            my $ok = prompt_bool("You are root, install into system?", 1);
178            $local = 1 unless $ok; # root can install locally
179        }
180        else {
181            my $ok = prompt_bool("You are not root, install locally?", 1);
182            return unless $ok; # non-root user can't install into system
183            $local = 1;
184        }
185    }
186
187    my $local_dir;
188    if ($local) {
189        $local_dir = $ENV{HOME};
190        unless (defined $local_dir) {
191            die "Can't find your home!";
192        }
193        unless (-d $local_dir) {
194            die "Can't find your home dir '$local_dir'!";
195        }
196    }
197
198    print_tty "\nService dir is a directory with descriptions of your services.\n";
199    my $default_service_dir = (
200        defined($local_dir)
201        ? "$local_dir/ubic/service"
202        : $defaults{service_dir}
203    );
204    $default_service_dir = $opt_service_dir if defined $opt_service_dir;
205    my $service_dir = prompt_str("Service dir?", $default_service_dir);
206
207    print_tty "\nData dir is where ubic stores all of its data: locks,\n";
208    print_tty "status files, tmp files.\n";
209    my $default_data_dir = (
210        defined($local_dir)
211        ? "$local_dir/ubic/data"
212        : $defaults{data_dir}
213    );
214    $default_data_dir = $opt_data_dir if defined $opt_data_dir;
215    my $data_dir = prompt_str("Data dir?", $default_data_dir);
216
217    print_tty "\nLog dir is where ubic.watchdog will write its logs.\n";
218    print_tty "(Your own services are free to write logs wherever they want.)\n";
219    my $default_log_dir = (
220        defined($local_dir)
221        ? "$local_dir/ubic/log"
222        : $defaults{log_dir}
223    );
224    $default_log_dir = $opt_log_dir if defined $opt_log_dir;
225    my $log_dir = prompt_str("Log dir?", $default_log_dir);
226
227    # TODO - sanity checks?
228
229    my $default_user;
230    if ($is_root) {
231        print_tty "\nUbic services can be started from any user.\n";
232        print_tty "Some services don't specify the user from which they must be started.\n";
233        print_tty "Default user will be used in this case.\n";
234        $default_user = prompt_str("Default user?", $opt_default_user);
235    }
236    else {
237        print_tty "\n";
238        $default_user = getpwuid($>);
239        unless (defined $default_user) {
240            die "Can't get login (uid '$>')";
241        }
242        print_tty "You're using local installation, so default service user will be set to '$default_user'.\n";
243    }
244
245    my $enable_1777;
246    if ($is_root) {
247        print_tty "\nSystem-wide installations usually need to store service-related data\n";
248        print_tty "into data dir for different users. For non-root services to work\n";
249        print_tty "1777 grants for some data dir subdirectories is required.\n";
250        print_tty "(1777 grants means that everyone is able to write to the dir,\n";
251        print_tty "but only file owners are able to modify and remove their files.)\n";
252        print_tty "There are no known security issues with this approach, but you have\n";
253        print_tty "to decide for yourself if that's ok for you.\n";
254
255        $enable_1777 = prompt_bool("Enable 1777 grants for data dir?", $opt_sticky_777);
256    }
257
258    my $install_services;
259    {
260        print_tty "There are three standard services in ubic service tree:\n";
261        print_tty " - ubic.watchdog (universal watchdog)\n";
262        print_tty " - ubic.ping (http service status reporter)\n";
263        print_tty " - ubic.update (helper process which updates service portmap, used by ubic.ping service)\n";
264        print_tty "If you'll choose to install them, ubic.watchdog will be started automatically\n";
265        print_tty "and two other services will be initially disabled.\n";
266        $install_services = prompt_bool("Do you want to install standard services?", $opt_install_services);
267    }
268
269    my $enable_crontab;
270    {
271        print_tty "\n'ubic.watchdog' is a service which checks all services and restarts them if\n";
272        print_tty "there are any problems with their statuses.\n";
273        print_tty "It is very simple and robust, but since it's important that watchdog never\n";
274        print_tty "goes down, we recommended to install the cron job which checks watchdog itself.\n";
275        print_tty "Also, this cron job will bring watchdog and all other services online on host reboots.\n";
276        $enable_crontab = prompt_bool("Install watchdog's watchdog as a cron job?", $opt_crontab);
277    }
278
279    my $crontab_env_fix = '';
280    my $crontab_wrap_bash;
281    my $ubic_watchdog_full_name = which('ubic-watchdog') or die "ubic-watchdog script not found in your current PATH";
282    {
283        my @path = split /:/, $ENV{PATH};
284        my @perls = grep { -x $_ } map { "$_/perl" } @path;
285        if ($perls[0] =~ /perlbrew/) {
286            print_tty "\nYou're using perlbrew.\n";
287
288            my $HOME = $ENV{ORIGINAL_HOME} || $ENV{HOME}; # ORIGINAL_HOME is set in ubic tests
289            unless ($HOME) {
290                die "HOME env variable not defined";
291            }
292            my $perlbrew_config = File::Spec->catfile($ENV{PERLBREW_ROOT} || "$HOME/perl5/perlbrew", "etc/bashrc");
293            if (not -e $perlbrew_config) {
294                die "Can't find perlbrew config (assumed $perlbrew_config)";
295            }
296            print_tty "I'll source your perlbrew config in ubic crontab entry to start the watchdog in the correct environment.\n";
297            $crontab_env_fix .= "source $perlbrew_config && ";
298            $crontab_wrap_bash = 1;
299        }
300        elsif (@perls > 1) {
301            print_tty "\nYou're using custom perl and it's not from perlbrew.\n";
302            print_tty "I'll add your current PATH to ubic crontab entry.\n";
303
304            # TODO - what if PATH contains " quotes? hopefully nobody is that crazy...
305            $crontab_env_fix .= qq[PATH="$ENV{PATH}" ];
306        }
307
308        if ($ENV{PERL5LIB}) {
309            print_tty "\nYou're using custom PERL5LIB.\n";
310            print_tty "I'll add your current PERL5LIB to ubic crontab entry.\n";
311            print_tty "Feel free to edit your crontab manually after installation if necessary.\n";
312            $crontab_env_fix .= qq[PERL5LIB="$ENV{PERL5LIB}" ];
313        }
314    }
315
316    my $config_file = (
317        defined($local_dir)
318        ?  "$local_dir/.ubic.cfg"
319        : $defaults{config}
320    );
321
322    {
323        print_tty "\nThat's all I need to know.\n";
324        print_tty "If you proceed, all necessary directories will be created,\n";
325        print_tty "and configuration file will be stored into $config_file.\n";
326        my $run = prompt_bool("Complete setup?", 1);
327        return unless $run;
328    }
329
330    print "Installing dirs...\n";
331
332    mkpath($_) for ($service_dir, $data_dir, $log_dir);
333
334    for my $subdir (qw[
335        status simple-daemon/pid lock ubic-daemon tmp watchdog/lock watchdog/status
336    ]) {
337        mkpath("$data_dir/$subdir");
338        chmod(01777, "$data_dir/$subdir") or die "chmod failed: $!";
339    }
340
341    mkpath("$service_dir/ubic");
342
343    if ($install_services) {
344        my $add_service = sub {
345            my ($name, $content) = @_;
346            print "Installing ubic.$name service...\n";
347
348            my $file = "$service_dir/ubic/$name";
349            Ubic::AtomicFile::store($content => $file);
350        };
351
352        $add_service->(
353            'ping',
354            "use Ubic::Ping::Service;\n"
355            ."Ubic::Ping::Service->new;\n"
356        );
357
358        $add_service->(
359            'watchdog',
360            "use Ubic::Service::SimpleDaemon;\n"
361            ."Ubic::Service::SimpleDaemon->new(\n"
362            ."bin => [ 'ubic-periodic', '--rotate-logs', '--period=60', '--stdout=$log_dir/watchdog.log', '--stderr=$log_dir/watchdog.err.log', 'ubic-watchdog' ],\n"
363            .");\n"
364        );
365
366        $add_service->(
367            'update',
368            "use Ubic::Service::SimpleDaemon;\n"
369            ."Ubic::Service::SimpleDaemon->new(\n"
370            ."bin => [ 'ubic-periodic', '--rotate-logs', '--period=60', '--stdout=$log_dir/update.log', '--stderr=$log_dir/update.err.log', 'ubic-update' ],\n"
371            .");\n"
372        );
373    }
374
375    if ($enable_crontab) {
376        print "Installing cron jobs...\n";
377
378        system("crontab -l >$data_dir/tmp/crontab.stdout 2>$data_dir/tmp/crontab.stderr");
379        my $old_crontab = slurp("$data_dir/tmp/crontab.stdout");
380        my $stderr = slurp("$data_dir/tmp/crontab.stderr");
381        unlink "$data_dir/tmp/crontab.stdout";
382        unlink "$data_dir/tmp/crontab.stderr";
383        if ($stderr and $stderr !~ /no crontab/) {
384            die "crontab -l failed";
385        }
386
387        my @crontab_lines = split /\n/, $old_crontab;
388        @crontab_lines = grep { $_ !~ /\Qubic-watchdog ubic.watchdog\E/ } @crontab_lines;
389
390        open my $fh, '|-', 'crontab -' or die "Can't run 'crontab -': $!";
391        my $printc = sub {
392            print {$fh} @_ or die "Can't write to pipe: $!";
393        };
394
395        my $crontab_command = "$crontab_env_fix$ubic_watchdog_full_name ubic.watchdog";
396        if ($crontab_wrap_bash) {
397            $crontab_command = "bash -c '$crontab_command'";
398        }
399        push @crontab_lines, "* * * * * $crontab_command    >>$log_dir/watchdog.log 2>>$log_dir/watchdog.err.log";
400        $printc->("$_\n") for @crontab_lines;
401        close $fh or die "Can't close pipe: $!";
402    }
403
404    print "Installing $config_file...\n";
405    Ubic::Settings::ConfigFile->write($config_file, {
406        service_dir => $service_dir,
407        data_dir => $data_dir,
408        default_user => $default_user,
409    });
410
411    if ($install_services) {
412        print "Starting ubic.watchdog...\n";
413        xsystem('ubic start ubic.watchdog');
414    }
415
416    print "Installation complete.\n";
417}
418
419
4201;
421
422__END__
423
424=pod
425
426=head1 NAME
427
428Ubic::Admin::Setup - this module handles ubic setup: asks user some questions and configures your system
429
430=head1 VERSION
431
432version 1.58
433
434=head1 DESCRPITION
435
436This module guides user through ubic configuration process.
437
438=head1 INTERFACE SUPPORT
439
440This is considered to be a non-public class. Its interface is subject to change without notice.
441
442=head1 FUNCTIONS
443
444=over
445
446=item B<< print_tty(@) >>
447
448Print something to terminal unless quiet mode or batch mode is enabled.
449
450=item B<< prompt($description, $default) >>
451
452Ask user a question, assuming C<$default> as a default.
453
454This function is stolen from ExtUtils::MakeMaker with some modifications.
455
456=item B<< prompt_str($description, $default) >>
457
458Ask user for a string.
459
460=item B<< prompt_bool($description, $default) >>
461
462Ask user a yes/no question.
463
464=item B<< xsystem(@command) >>
465
466Invoke C<system> command, throwing exception on errors.
467
468=item B<< slurp($file) >>
469
470Read file contents.
471
472=item B<< setup() >>
473
474Perform setup.
475
476=back
477
478=head1 SEE ALSO
479
480L<ubic-admin> - command-line script which calls this module
481
482=head1 AUTHOR
483
484Vyacheslav Matyukhin <mmcleric@yandex-team.ru>
485
486=head1 COPYRIGHT AND LICENSE
487
488This software is copyright (c) 2015 by Yandex LLC.
489
490This is free software; you can redistribute it and/or modify it under
491the same terms as the Perl 5 programming language system itself.
492
493=cut
494