File SERVICES.pm of Package webyast-services-ws

#--
# Copyright (c) 2009-2010 Novell, Inc.
# 
# All Rights Reserved.
# 
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License
# as published by the Free Software Foundation.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Novell, Inc.
# 
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#++

package YaPI::SERVICES;

use strict;
use YaST::YCP qw(:LOGGING);
use YaPI;
use Data::Dumper;

# ------------------- imported modules
YaST::YCP::Import ("Directory");
YaST::YCP::Import ("FileUtils");
YaST::YCP::Import ("Package");
YaST::YCP::Import ("Progress");
YaST::YCP::Import ("Service");
YaST::YCP::Import ("SCR");
YaST::YCP::Import ("Report");
YaST::YCP::Import ("RunlevelEd");
# -------------------------------------

our $VERSION            = '1.0.0';
our @CAPABILITIES       = ('SLES11');
our %TYPEINFO;

my $custom_services_dir		= "/etc/webyast";
my $custom_services_file	= "custom_services.yml";

my $error_message		= "";

# check for key presence in given list
sub contains {
    my ( $list, $key, $ignorecase ) = @_;
    if ( $ignorecase ) {
        if ( grep /^$key$/i, @{$list} ) {
            return 1;
        }
    } else {
        if ( grep /^$key$/, @{$list} ) {
            return 1;
        }
    }
    return 0;
}

# log error message and fill it into $error_message variable
sub report_error {
  $error_message	= shift;
  y2error ($error_message);
}

# parse the file with custom services and return the hash describing the file
sub parse_custom_services {

  my $custom_services_path	= "$custom_services_dir/vendor/$custom_services_file";

  unless (FileUtils->Exists ($custom_services_path)) {
    y2milestone ("$custom_services_path not available, using default");
    $custom_services_path		= "$custom_services_dir/$custom_services_file" 
  }

  if (!FileUtils->Exists ($custom_services_path)) {
    report_error ("$custom_services_path file not present");
    return {};
  }

  if (!Package->Installed ("yast2-ruby-bindings")) {
    report_error ("yast2-ruby-bindings not installed, cannot read custom services");
    return {};
  }

  if (!FileUtils->Exists (Directory->moduledir()."/YML.rb")) {
    report_error ("YML.rb not present, cannot parse config file");
    return {};
  }
  
  YaST::YCP::Import ("YML");

  my $parsed = YML->parse ($custom_services_path);

  if (!defined $parsed || ref ($parsed) ne "HASH") {
    report_error ("custom services file could not be read");
    return {};
  }
  return $parsed;
}

# read the list of custom services and return the information about them
# if requested, read the status of services
sub read_custom_services {

  my $args	= shift;
  my @ret	= ();
  my $services	= parse_custom_services ();
  foreach my $name (keys %$services) {
    my $s      = {
	"name"		=> $name
    };
    $s->{"description"}	= ($services->{$name}{"description"} || "") if $args->{"description"} || 0;
    $s->{"shortdescription"}= ($services->{$name}{"shortdescription"} || "") if $args->{"shortdescription"} || 0;

    # read list of available commands, it may be limited for 'custom service'
    my @commands	= ();
    foreach my $key (keys %{$services->{$name}}) {
	if (contains (["start","stop","restart","reload","try-restart"], $key, 1)) {
	    push @commands, $key;
	}
    }
    $s->{"commands"}	= \@commands;

    if ($args->{"read_status"} || 0)
    {
	my $cmd	= $services->{$name}{"status"};
	if (!$cmd) {
	    report_error ("status script for $name not defined or empty");
	    next;
	}
	my $out     = SCR->Execute (".target.bash_output", $cmd);
	$s->{"status"}	= $out->{"exit"};
    }

    push @ret, $s;
  }
  return \@ret;
}

# read infomation about custom service and execute given command with it
sub execute_custom_script {

  my $name	= shift;
  my $action	= shift;
  my $services	= parse_custom_services ();
  my $ret	= {
      "stdout"	=> "",
      "stderr"	=> "failure",
      "exit"	=> 255
  };

  if (%$services) {
    my $service	= $services->{$name};
    if (!defined $service || ref ($service) ne "HASH" || ! %$service) {
	report_error ("service $name not defined or empty in config file");
	$ret->{"stderr"}	= $error_message;
	return $ret;
    }
    my $cmd	= $services->{$name}{$action};
    if (!$cmd) {
	report_error ("'$action' script for $name not defined or empty");
	$ret->{"stderr"}	= $error_message;
	return $ret;
    }
    $ret	= SCR->Execute (".target.bash_output", $cmd);
  }
  return $ret;
}

# Return the list of services enabled in given runlevel, or even all available.
#
# Parameter is an argument map with possible keys:
#	"service"	: if defined, only the status of _this given service_ will be returned (= list with one item)
# 	"runlevel" 	: integer; if not defined, current runlevel will be used
#	"read_status"	: if true, service status will be queried and returned for each service
#	"custom"	: if true, custom services (defined in config file) will be read (otherwise list of init.d services)
#	"description"	: if true, read the description of each service
#	"only_enabled"	: if true, return only list of services enabled in given runlevel
#		- neither "start_runlevels", nor "enabled" key will be part of resulting maps
#	"start_runlevels" if true, each service's result map will contain list of runlevels where it is started
#		- if not present (or false), "enabled" key with boolean value will be returned instead
#	"filter"	: list of strings; defines filtered list of services that should be returned
# @returns array of hashes
BEGIN{$TYPEINFO{Read} = ["function",
    ["list", [ "map", "string", "any"]],
    ["map", "string", "any"]];
}
sub Read {

  my $self	= shift;
  my $args	= shift;
  my @ret	= ();
  my $runlevel	= $args->{"runlevel"} || "";

  $runlevel	= SCR->Read (".init.scripts.current_runlevel") unless ($runlevel);

  unless ($runlevel) {
    $runlevel  = SCR->Read (".init.scripts.default_runlevel");
    y2warning ("current runlevel not available, using default ('$runlevel')");
  }

  my @filter	= ();
  @filter	= @{$args->{"filter"}} if defined $args->{"filter"};
  my $filter_map= {};
  foreach my $s (@filter) {
      $filter_map->{$s}	= 1;
  }

  # only read status of one service if the name was given
  if ($args->{"service"} || "") {
    my $name	= $args->{"service"} || "";
    my $exec	= $self->Execute ({
	"name" 		=> $name,
	"action"	=> "status",
	"only_execute"	=> 1,
	"only_this"	=> 1,
	"custom"	=> $args->{"custom"} || 0
    });
    my $s	= {
	"name"  	=> $name,
	"status"	=> $exec->{"exit"} || 0,
	# custom service is always 'enabled' (in fact, we can't check)
	"enabled"	=> ($args->{"custom"} || 0) || YaST::YCP::Boolean (Service->Enabled ($name))
    };
    push @ret, $s;
    return \@ret;
  }

  # read only custom services
  if ($args->{"custom"} || 0) {
    return read_custom_services ($args);
  }

  if ($args->{"only_enabled"}) {
    # generate the output list
    foreach my $name (@{Service->EnabledServices ($runlevel)}) {
	next if (@filter && !defined $filter_map->{$name}); # should not be returned
	my $s      = {
	    "name"		=> $name,
	};
	$s->{"status"}	= Service->Status ($name) if ($args->{"read_status"} || 0);
	if (($args->{"description"} || 0) || ($args->{"shortdescription"} || 0)) {
	    my $info	= Service->Info ($name);
	    $s->{"description"}	= ($info->{"description"} || "") if $args->{"description"} || 0;
	    $s->{"shortdescription"}= ($info->{"shortdescription"} || "") if $args->{"shortdescription"} || 0;
	}
	push @ret, $s;
    }
  }
  else {
    my $progress_orig   = Progress->set (0);
    Report->DisplayErrors (0, 0);
    RunlevelEd->Read ();
    my $full_services	= RunlevelEd->services ();
    while (my ($name, $info) = each %$full_services) {

	next if (@filter && !defined $filter_map->{$name}); # should not be returned
	next if (contains ($info->{"defstart"} || [], "B", 1));

	my $s      = {
	    "name"		=> $name
	};

	if ($args->{"start_runlevels"} || 0) {
	    $s->{"start_runlevels"}	= $info->{"start"} || [];
	}
	else {
	    my $start		= $info->{"start"} || [];
	    # for "B" check, see RunlevelEd::StartContainsImplicitly
	    $s->{"enabled"}	= YaST::YCP::Boolean (contains ($start, $runlevel, 1) || contains ($start, "B", 1));
	}
	# return start and stop dependencies for each service
	if ($args->{"dependencies"} || 0) {
	    my @required_for_start	= ();
	    # filter out services started on boot by default:
	    foreach my $rq (@{RunlevelEd->ServiceDependencies ($name, 1)}) {
		my $start	= $full_services->{$rq}{"start"} || [];
		push @required_for_start, $rq unless contains ($start, "B", 1);
	    }
	    $s->{"required_for_start"}	= \@required_for_start;
	    $s->{"required_for_stop"}	= RunlevelEd->ServiceDependencies ($name, 0);
	}
	$s->{"status"}		= Service->Status ($name) if ($args->{"read_status"} || 0);
	$s->{"description"}	= ($info->{"description"} || "") if $args->{"description"} || 0;
	$s->{"shortdescription"}= ($info->{"shortdescription"} || "") if $args->{"shortdescription"} || 0;
	push @ret, $s;
    }
    Progress->set ($progress_orig);
  }

  return \@ret;
}

# Return the status of given service 
# return value is the exit code of status function
BEGIN{$TYPEINFO{Get} = ["function",
    "integer", "string" ];
}
sub Get {

  my $self	= shift;
  my $name	= shift;

  return Service->Status ($name);
}

# Executes an action (e.g. "restart") with given service
# If the action is start or stop, it will also enable (resp. disable)
# the service for current runlevel.
#
# parameter is a map where "name" is service name, "action" means what to do
# - if "only_execute" key is present, do not continue with enabling/disabling
# - if action is "enable" or "disable", only enables/disables service
# - if "custom" key is present (with true value), indicates custom service, which
# has special handling. Also, custom service will not be enabled/disabled.
#
# return value is map with "exit", "stdout" and "stderr" keys
BEGIN{$TYPEINFO{Execute} = ["function",
    [ "map", "string", "any"],
    [ "map", "string", "any"]];
}
sub Execute {

  my $self	= shift;

  my $args	= shift;
  my $name	= $args->{"name"} || "";
  my $action	= $args->{"action"} || "";
  my $ret	= {};

  y2debug ("Execute args: ", Dumper ($args));

  # no enable/disable
  my $only_execute 	= $args->{"only_execute"} || 0;
  # do not solve dependencies
  my $only_this 	= $args->{"only_this"} || 0;

  # just a shurtcut, so Execute function can be used for Enable only
  return $self->Enable ($args) if ($action eq "enable" || $action eq "disable");

  # custom service has special handling
  if ($args->{"custom"} || 0) {
    return execute_custom_script ($name, $action);
  }
  # only handle given service, not dependencies
  elsif ($only_this) {
    $ret = Service->RunInitScriptOutput ($name, $action);
    unless ($only_execute) {
	if (($ret->{"exit"} || 0) ne 0) {
	    y2error ("action '$action' failed");
	    return $ret;
	}
	if ($action eq "start") {
	    $args->{"action"}	= "enable";
	}
	else {
	    $args->{"action"}	= "disable";
	}
	return $self->Enable ($args);
    }
  }
  # full action: start/stop and enable/disable required service
  else {
    my $progress_orig   = Progress->set (0);
    RunlevelEd->Read ();
    Progress->set ($progress_orig);

    my $full_services	= RunlevelEd->services ();
    my $runlevel	= RunlevelEd->GetCurrentRunlevel ();

    if ($runlevel eq "unknown") {
	$runlevel        = RunlevelEd->GetDefaultRunlevel ();
	y2warning ("current runlevel not available, using default ('$runlevel')");
    }

    # in fact, this may mean "start & enable" (depends on $only_execute)
    my $start		= ($action eq "start") || ($action eq "restart");

    # list of runlevels where the service should be enabled
    my $rls = $start? ($full_services->{$name}{"defstart"} || []) : undef;

    # list of dependencies
    my $dep_s	= RunlevelEd->ServiceDependencies ($name, $start);

    # filtered list; unfortunatelly it does not really check for current status
    $dep_s	= RunlevelEd->FilterAlreadyDoneServices ($dep_s, $rls, $start, 1, 1);

    my $enable_args	= {
	"action"	=> ($action eq "start") ? "enable" : "disable",
	# we're solving dependencies here, so no need to do it in Enable call again
	"only_this"	=> 1
    };

    foreach my $s (@$dep_s) {
	# check if service is not already running
	my $status	= Service->Status ($s);
	# action for required service: when restarting selected, only start required ones
	my $req_action	= ($action eq "restart") ? "start" : $action;
	if (($start && $status != 0) || ($status == 0 && !$start)) {
	    # RunInitScriptWithTimeOut would be better, but does not return stderr
	    $ret	= Service->RunInitScriptOutput ($s, $req_action);
	}
	if (($ret->{"exit"} || 0) ne 0) {
	    y2error ("action '$req_action' for service '$s' failed");
	    return $ret;
	}
	next if $only_execute;

	my $startlist		= $full_services->{$s}{"start"} || [];
	my $service_enabled	= contains ($startlist, $runlevel, 1) || contains ($startlist, "B", 1);
	if (($start && !$service_enabled) || (!$start && $service_enabled)) {
	    $enable_args->{"name"}	= $s;
	    $ret	= $self->Enable ($enable_args);
	}
	if (($ret->{"exit"} || 0) ne 0) {
	    y2error ("insserv call for service '$s' failed");
	    return $ret;
	}
    }
    # now, finally start/stop our service...
    $ret	= Service->RunInitScriptOutput ($name, $action);
    if (($ret->{"exit"} || 0) ne 0) {
	y2error ("action '$action' for service '$name' failed");
	return $ret;
    }
    return $ret if $only_execute;
    # ... and enable/disable it
    $enable_args->{"name"}      = $name;
    $ret	= $self->Enable ($enable_args);
  }
  return $ret;
}

# Enable/Disable given service in current runlevel
# parameter is a map where "name" is service name, "action" means what to do
# return value is map with "exit", "stdout" and "stderr" keys
BEGIN{$TYPEINFO{Enable} = ["function",
    [ "map", "string", "any"],
    [ "map", "string", "any"]];
}
sub Enable {

  my $self	= shift;
  my $args	= shift;
  my $name	= $args->{"name"} || "";
  my $action	= $args->{"action"} || "";
  my $ret	= {
      "stdout"	=> "",
      "stderr"	=> "",
      "exit"	=> 0
  };
  # do not solve dependencies
  my $only_this 	= $args->{"only_this"} || 0;

  y2debug ("Enable args: ", Dumper ($args));

  # enable/disable with dependencies
  unless ($only_this) {
    my $progress_orig   = Progress->set (0);
    Report->DisplayErrors (0, 0);
    RunlevelEd->Read ();
    my $exit	= 0;
    if ($action eq "enable") {
	$exit	= RunlevelEd->ServiceInstall ($name, undef);
	if ($exit == 1) {
	    $ret->{"stderr"}	= "Failed to enable service $name.";
	    $ret->{"stdout"}	= $name;
	    $ret->{"exit"}	= 1000;
	}
    } elsif ($action eq "disable") {
	$exit	= RunlevelEd->ServiceRemove ($name, undef);
	if ($exit == 1) {
	    $ret->{"stderr"}	= "Failed to disable service $name.";
	    $ret->{"stdout"}	= $name;
	    $ret->{"exit"}	= 2000;
	}
    }
    unless (RunlevelEd->Write ()) {
	$ret->{"stderr"}	= "Failed during writing runlevel settings.";
	$ret->{"exit"}		= 3000;
    }
    Progress->set ($progress_orig);
    return $ret;
  }

  if ($action eq "enable") {
    unless (Service->Enable ($name)) {
	$ret->{"stderr"}	= "Failed to enable service $name.";
	$ret->{"stdout"}	= $name;
	$ret->{"exit"}		= 1000;
    }
  }
  elsif ($action eq "disable") {
    unless (Service->Disable ($name)) {
	$ret->{"stderr"}	= "Failed to disable service $name.";
	$ret->{"stdout"}	= $name;
	$ret->{"exit"}		= 2000;
    }
  }
  else {
    $ret->{"stderr"}	= "Unknown action '$action'";
    $ret->{"exit"}		= 3;
  }
  return  $ret;
}

1;
openSUSE Build Service is sponsored by