diff --git a/Netdisco/Changes b/Netdisco/Changes index df5dc405..454c69d8 100644 --- a/Netdisco/Changes +++ b/Netdisco/Changes @@ -1,5 +1,11 @@ 2.025000 - + [NEW FEATURES] + + * Web and Backend daemons will restart when deployment.yml is updated + * Web and Backend daemons will drop privilege to same uid/gid as their + on-disk files (to allow run-control symlink as non-root) + [ENHANCEMENTS] * Use daterange for IP Subnets (same as IP Inventory) diff --git a/Netdisco/bin/netdisco-daemon b/Netdisco/bin/netdisco-daemon index 8dbfc6e6..9767c551 100755 --- a/Netdisco/bin/netdisco-daemon +++ b/Netdisco/bin/netdisco-daemon @@ -3,12 +3,13 @@ use strict; use warnings FATAL => 'all'; -our $home; +our $home = ($ENV{NETDISCO_HOME} || $ENV{HOME}); BEGIN { - # try to find a localenv if one isn't already in place. - $home = ($ENV{NETDISCO_HOME} || $ENV{HOME}); + use FindBin; + FindBin::again(); + # try to find a localenv if one isn't already in place. if (!exists $ENV{PERL_LOCAL_LIB_ROOT}) { use File::Spec; my $localenv = File::Spec->catfile($FindBin::RealBin, 'localenv'); @@ -19,12 +20,20 @@ BEGIN { die "Sorry, can't find libs required for App::Netdisco.\n" if !exists $ENV{PERLBREW_PERL}; } + + use Path::Class; + + # stuff useful locations into @INC and $PATH + unshift @INC, + dir($FindBin::RealBin)->parent->subdir('lib')->stringify, + dir($FindBin::RealBin, 'lib')->stringify; } -use FindBin; -FindBin::again(); -use Path::Class; use Daemon::Control; +use Filesys::Notify::Simple; + +use App::Netdisco::Environment; +my $config = ($ENV{PLACK_ENV} || $ENV{DANCER_ENVIRONMENT}) .'.yml'; my $netdisco = file($FindBin::RealBin, 'netdisco-daemon-fg'); my @args = (scalar @ARGV > 1 ? @ARGV[1 .. $#ARGV] : ()); @@ -32,20 +41,73 @@ my @args = (scalar @ARGV > 1 ? @ARGV[1 .. $#ARGV] : ()); my $log_dir = dir($home, 'logs'); mkdir $log_dir if ! -d $log_dir; -my $uid = stat($netdisco)[4] || 0; -my $gid = stat($netdisco)[5] || 0; +my $uid = (stat($netdisco->stringify))[4] || 0; +my $gid = (stat($netdisco->stringify))[5] || 0; Daemon::Control->new({ name => 'Netdisco Daemon', - program => $netdisco, + program => \&restarter, program_args => [@args], pid_file => file($home, 'netdisco-daemon.pid'), stderr_file => file($log_dir, 'netdisco-daemon.log'), stdout_file => file($log_dir, 'netdisco-daemon.log'), - uid => $uid, - gid => $gid, + uid => $uid, gid => $gid, })->run; +# the guts of this are borrowed from Plack::Loader::Restarter - many thanks!! + +sub restarter { + my ($daemon, @program_args) = @_; + + my $child = fork_and_start(@program_args); + exit(1) unless $child; + + my $watcher = Filesys::Notify::Simple->new([$ENV{DANCER_ENVDIR}]); + warn "config watcher: watching $ENV{DANCER_ENVDIR} for updates.\n"; + + local $SIG{TERM} = sub { signal_child('TERM', $child); exit(0); }; + + while (1) { + my @restart; + + # this is blocking + $watcher->wait(sub { + my @events = @_; + @events = grep {file($_->{path})->basename eq $config} @events; + return unless @events; + @restart = @events; + }); + + next unless @restart; + warn "-- $_->{path} updated.\n" for @restart; + + signal_child('TERM', $child); + $child = fork_and_start(@program_args); + exit(1) unless $child; + } +} + +sub fork_and_start { + my @daemon_args = @_; + my $pid = fork; + die "Can't fork: $!" unless defined $pid; + + if ($pid == 0) { # child + exec( $netdisco->stringify, @daemon_args ); + } + else { + return $pid; + } +} + +sub signal_child { + my ($signal, $pid) = @_; + return unless $signal and $pid; + warn "config watcher: sending $signal to the server (pid:$pid)...\n"; + kill $signal => $pid; + waitpid($pid, 0); +} + =head1 NAME netdisco-daemon - Job Control Daemon for Netdisco diff --git a/Netdisco/bin/netdisco-web b/Netdisco/bin/netdisco-web index 5484931a..f1c6e693 100755 --- a/Netdisco/bin/netdisco-web +++ b/Netdisco/bin/netdisco-web @@ -3,12 +3,13 @@ use strict; use warnings FATAL => 'all'; -our $home; +our $home = ($ENV{NETDISCO_HOME} || $ENV{HOME}); BEGIN { - # try to find a localenv if one isn't already in place. - $home = ($ENV{NETDISCO_HOME} || $ENV{HOME}); + use FindBin; + FindBin::again(); + # try to find a localenv if one isn't already in place. if (!exists $ENV{PERL_LOCAL_LIB_ROOT}) { use File::Spec; my $localenv = File::Spec->catfile($FindBin::RealBin, 'localenv'); @@ -19,33 +20,97 @@ BEGIN { die "Sorry, can't find libs required for App::Netdisco.\n" if !exists $ENV{PERLBREW_PERL}; } + + use Path::Class; + + # stuff useful locations into @INC and $PATH + unshift @INC, + dir($FindBin::RealBin)->parent->subdir('lib')->stringify, + dir($FindBin::RealBin, 'lib')->stringify; } -use FindBin; -FindBin::again(); -use Path::Class; use Daemon::Control; +use Filesys::Notify::Simple; + +use App::Netdisco::Environment; +my $config = ($ENV{PLACK_ENV} || $ENV{DANCER_ENVIRONMENT}) .'.yml'; my $netdisco = file($FindBin::RealBin, 'netdisco-web-fg'); my @args = (scalar @ARGV > 1 ? @ARGV[1 .. $#ARGV] : ()); +my $uid = (stat($netdisco->stringify))[4] || 0; +my $gid = (stat($netdisco->stringify))[5] || 0; + my $log_dir = dir($home, 'logs'); mkdir $log_dir if ! -d $log_dir; -my $uid = stat($netdisco)[4] || 0; -my $gid = stat($netdisco)[5] || 0; - Daemon::Control->new({ name => 'Netdisco Web', - program => 'starman', - program_args => ['--disable-keepalive', @args, $netdisco->stringify], + program => \&restarter, + program_args => [ + '--disable-keepalive', + '--user', $uid, '--group', $gid, + @args, $netdisco->stringify + ], pid_file => file($home, 'netdisco-web.pid'), stderr_file => file($log_dir, 'netdisco-web.log'), stdout_file => file($log_dir, 'netdisco-web.log'), - uid => $uid, - gid => $gid, })->run; +# the guts of this are borrowed from Plack::Loader::Restarter - many thanks!! + +sub restarter { + my ($daemon, @program_args) = @_; + + my $child = fork_and_start(@program_args); + exit(1) unless $child; + + my $watcher = Filesys::Notify::Simple->new([$ENV{DANCER_ENVDIR}]); + warn "config watcher: watching $ENV{DANCER_ENVDIR} for updates.\n"; + + # TODO: starman also supports TTIN,TTOU,INT,QUIT + local $SIG{HUP} = sub { signal_child('HUP', $child); }; + local $SIG{TERM} = sub { signal_child('TERM', $child); exit(0); }; + + while (1) { + my @restart; + + # this is blocking + $watcher->wait(sub { + my @events = @_; + @events = grep {file($_->{path})->basename eq $config} @events; + return unless @events; + @restart = @events; + }); + + next unless @restart; + warn "-- $_->{path} updated.\n" for @restart; + + signal_child('HUP', $child); + } +} + +sub fork_and_start { + my @starman_args = @_; + my $pid = fork; + die "Can't fork: $!" unless defined $pid; + + if ($pid == 0) { # child + exec( 'starman', @starman_args ); + } + else { + return $pid; + } +} + +sub signal_child { + my ($signal, $pid) = @_; + return unless $signal and $pid; + warn "config watcher: sending $signal to the server (pid:$pid)...\n"; + kill $signal => $pid; + waitpid($pid, 0); +} + =head1 NAME netdisco-web - Web Application Server for Netdisco diff --git a/Netdisco/lib/App/Netdisco.pm b/Netdisco/lib/App/Netdisco.pm index 8fc64787..bb7c8abf 100644 --- a/Netdisco/lib/App/Netdisco.pm +++ b/Netdisco/lib/App/Netdisco.pm @@ -4,45 +4,12 @@ use strict; use warnings FATAL => 'all'; use 5.010_000; -use File::ShareDir 'dist_dir'; -use Path::Class; - our $VERSION = '2.024004'; -BEGIN { - if (not ($ENV{DANCER_APPDIR} || '') - or not -f file($ENV{DANCER_APPDIR}, 'config.yml')) { - - my $auto = dir(dist_dir('App-Netdisco'))->absolute; - my $home = ($ENV{NETDISCO_HOME} || $ENV{HOME}); - - $ENV{DANCER_APPDIR} ||= $auto->stringify; - $ENV{DANCER_CONFDIR} ||= $auto->stringify; - - my $test_envdir = dir($home, 'environments')->stringify; - $ENV{DANCER_ENVDIR} ||= (-d $test_envdir - ? $test_envdir : $auto->subdir('environments')->stringify); - - $ENV{DANCER_ENVIRONMENT} ||= 'deployment'; - $ENV{PLACK_ENV} ||= $ENV{DANCER_ENVIRONMENT}; - - $ENV{DANCER_PUBLIC} ||= $auto->subdir('public')->stringify; - $ENV{DANCER_VIEWS} ||= $auto->subdir('views')->stringify; - } - - { - # Dancer 1 uses the broken YAML.pm module - # This is a global sledgehammer - could just apply to Dancer::Config - use YAML; - use YAML::XS; - no warnings 'redefine'; - *YAML::LoadFile = sub { goto \&YAML::XS::LoadFile }; - } -} - -# set up database schema config from simple config vars +use App::Netdisco::Environment; use Dancer ':script'; +# set up database schema config from simple config vars if (ref {} eq ref setting('database')) { my $name = (setting('database')->{name} || 'netdisco'); my $host = setting('database')->{host}; diff --git a/Netdisco/lib/App/Netdisco/Environment.pm b/Netdisco/lib/App/Netdisco/Environment.pm new file mode 100644 index 00000000..210b2b5e --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Environment.pm @@ -0,0 +1,40 @@ +package App::Netdisco::Environment; + +use strict; +use warnings FATAL => 'all'; + +use File::ShareDir 'dist_dir'; +use Path::Class; + +BEGIN { + if (not ($ENV{DANCER_APPDIR} || '') + or not -f file($ENV{DANCER_APPDIR}, 'config.yml')) { + + my $auto = dir(dist_dir('App-Netdisco'))->absolute; + my $home = ($ENV{NETDISCO_HOME} || $ENV{HOME}); + + $ENV{DANCER_APPDIR} ||= $auto->stringify; + $ENV{DANCER_CONFDIR} ||= $auto->stringify; + + my $test_envdir = dir($home, 'environments')->stringify; + $ENV{DANCER_ENVDIR} ||= (-d $test_envdir + ? $test_envdir : $auto->subdir('environments')->stringify); + + $ENV{DANCER_ENVIRONMENT} ||= 'deployment'; + $ENV{PLACK_ENV} ||= $ENV{DANCER_ENVIRONMENT}; + + $ENV{DANCER_PUBLIC} ||= $auto->subdir('public')->stringify; + $ENV{DANCER_VIEWS} ||= $auto->subdir('views')->stringify; + } + + { + # Dancer 1 uses the broken YAML.pm module + # This is a global sledgehammer - could just apply to Dancer::Config + use YAML; + use YAML::XS; + no warnings 'redefine'; + *YAML::LoadFile = sub { goto \&YAML::XS::LoadFile }; + } +} + +1; diff --git a/Netdisco/lib/App/Netdisco/Manual/ReleaseNotes.pod b/Netdisco/lib/App/Netdisco/Manual/ReleaseNotes.pod index 42867975..8dfcbfa8 100644 --- a/Netdisco/lib/App/Netdisco/Manual/ReleaseNotes.pod +++ b/Netdisco/lib/App/Netdisco/Manual/ReleaseNotes.pod @@ -36,6 +36,18 @@ but they are backwards compatible. =back +=head1 2.025000 + +=head2 General Changes + +The Web and Backend daemons (C and C +respectively) will now watch your C configuration file, and +restart themselves whenever it is changed. + +The Web and Backend daemons will also now drop privilege to the same user and +group as their files on disk. This allows you to symlink the programs as +run-control scripts, yet maintain non-root privilege status. + =head1 2.023000 =head2 Incompatible Changes