#! /usr/perl5/bin/perl -w
#
# ident   "@(#)Analyzer.pm 1.1     03/12/03 SMI"
# 
# Copyright 2003 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#

# 
# Module: ScSnapshot::Analyzer
# 

package ScSnapshot::Analyzer;
use strict;

#
# Constructor
# Arguments: 
#   1) Metadata read from the repository. 
#      Cannot be undefined.
#
sub new {
  my $proto = shift;
  my $class = ref($proto) || $proto;
  my $self  = {};
  $self->{METADATA} = shift;
  bless ($self, $class);
  return $self;
}

#
# Performs a diff between an old and a new configuration.
# If old configuration is empty, invokes $self->create(new configuration).
# Otherwise, invokes $self->delta(old configuration, new configuration).
# Arguments: 
#   1) old configuration, as read by ScSnapshot::XMLReader
#      May be undefined.
#   2) new configuration, as read by ScSnapshot::ConfigDataParser or 
#      ScSnapshot::XMLReader Cannot be undefined.
#
sub diff {
  my $self = shift;
  my $olddata = shift;
  my $newdata = shift;
  
  if ($olddata) {
    # Perform a delta between old and new configurations
    return $self->delta($olddata, $newdata);
  } else {
    # Re-create the current configuration
    return $self->create($newdata);
  }
}

#
# Generates a view used to re-create the given configuration.
# Arguments:
#   1) Configuration to be re-created.
#      Cannot be undefined.
#
sub create {
  my $self = shift;
  my $data = shift;
  my $view;

  # add actions for creating resource types
  foreach my $rt (@{$data->{rtdefs}}) {
    # Don't generate action for SunCluster built-in Resources
    if(    ($rt->{name} ne "SUNW.LogicalHostname") 
       and ($rt->{name} ne "SUNW.SharedAddress") ) {
      
      # Path the inconsistency in scrgadm -pvv output. Sometimes
      # Resource Type name are displayed with version 
      my $name = $rt->{name};
      if($name =~ /:/) {
	my @elt = split(':',$name);
	$name=$elt[0];
      }

      push(@{$view}, 
	   { 'Type' => 'CREATE_RESOURCE_TYPE',
	     'Parameters' => { 
			      'RESOURCE_TYPE_NAME'=>$name,
			      'RESOURCE_TYPE_VERSION' => $rt->{version},
			      'INSTALLED_ON_NODES'=>$rt->{installed_on_nodes} 
			     } 
	   });
    }
  }

  # Treat resource groups according to their dependencies
  my $originalRGRefList = [];
  my $resourceGroupRefs = [];

  if(defined($data->{rgdefs})) {

    # Build a list of reference on all defined RG
    foreach my $rg (@{$data->{rgdefs}}) {
      my $rgName = $rg->{name};
      my $found =0;

      my @array = @{$originalRGRefList};
      for(my $i=0; $i<=$#array and not $found; $i++) {
	my $queuedRef = $array[$i];
	if( $$queuedRef->{name} eq $rgName ) {
	  $found=1;
	}
      }

      if(not $found) {
	push(@{$originalRGRefList}, \$rg);
      }
    }

  RG_LOOP: while (@{$originalRGRefList}) {
      # Pop each resource group from the orignal list
      my $resourceGroupRef = pop(@{$originalRGRefList});
    
      # Enqueued resource group if it has no dependency ...
      if(  not $$resourceGroupRef->{rg_dependencies} 
	   and not $$resourceGroupRef->{rg_affinities}) {
	push(@{$resourceGroupRefs}, $resourceGroupRef);
      } else {
	# ... or of if all dependencies are already enqueued
	my $depString = "";
	if($$resourceGroupRef->{rg_dependencies}) {
	  $depString .= $$resourceGroupRef->{rg_dependencies}
	}
	if($$resourceGroupRef->{rg_affinities}) {
	  $depString .= " " . _extractRGFromAffinities($$resourceGroupRef->{rg_affinities});
	}
	$depString =~ s/[\t,]|[ ]+/ /g;
	$depString =~ s/^[ ]+|[ ]+$//g;
      RG_DEP_LOOP: foreach my $rgDep ( split(/ /, $depString ) ) {
          foreach my $queuedRGRef (@{$resourceGroupRefs}) {
	    if ($$queuedRGRef->{name} eq $rgDep) {
	      next RG_DEP_LOOP;
	    }
	  }
	  # Send resource group back to the botto; of the original list 
	  # otherwise
	  unshift(@{$originalRGRefList}, $resourceGroupRef);
	  next RG_LOOP;
	}
	push(@{$resourceGroupRefs}, $resourceGroupRef);
      }
    }
  }

  # add actions for creating resource groups
  foreach my $rgRef (@{$resourceGroupRefs}) {
    push(@{$view}, 
	 { 'Type' => 'CREATE_RESOURCE_GROUP',
	   'Parameters' => {
			    'RESOURCE_GROUP_NAME' => $$rgRef->{name},
			    'PROPERTIES' => my $properties = {}
			   }
	 });
    my $metadata = $self->{METADATA}->resourcegroups();
    foreach my $property (keys(%{$$rgRef})) { 
      # ignore properties which are not defined in the metadata 
      # repository
      my $defined = 0;
      for (keys(%{$metadata})) {
	if ($property =~ /$_/) # do not use eq here as property name 
	  # in the metadata repository may be a
	  # regexp
	  {
	    $defined = 1;
	    last;
	  }
      }
      if ($defined) {
	my $default = $metadata->{$property};
	my $value = $$rgRef->{$property};
	# add properties only when the value is different from the 
	# default value or when there is no default value
	if (!defined($default) || 
	    lc($value) ne lc($default)) # perform a case-insensitive 
	  # comparison in order to avoid
	  # problems like "true != True"
	  {
	    $properties->{$property} = $value;
	  }
      }
    }
  }

  # Resources treatment
  
  # Build a complete list of the resource references cross all the resource
  # groups
  my $resourceRefs = [];
  foreach my $rgRef (@{$resourceGroupRefs}) {
    if(defined($$rgRef->{resources})) {
      foreach my $res (@{$$rgRef->{resources}}) {
	my $resName = $res->{name};
	my $found =0;
	my @array = @{$resourceRefs};
	for(my $i=0; $i<=$#array and not $found; $i++) {
	  my $queuedRef = $array[$i];
	  if( $$queuedRef->{name} eq $resName ) {
	    $found=1;
	  }
	}
	if(not $found) {
	  push(@{$resourceRefs}, \$res);
	}
      }
    }
  }

  # Order resources following their dependencies
  my $orderedResourceRefs = [];

  if(defined($resourceRefs)) {
  
  RES_LOOP: while (@{$resourceRefs}) {
    
      # pop each resources reference from the start list
      my $resRef = pop(@{$resourceRefs});

      # enqueue resource if is has no dependency ...
      if(  not  $$resRef->{resource_dependencies} 
	   and not $$resRef->{resource_dependencies_weak}
	   and not $$resRef->{resource_dependencies_restart}) {
	push(@{$orderedResourceRefs}, $resRef);
      } else {
	# ... or if all its dependencies are already enqueued
	my $depString = "";
	if($$resRef->{resource_dependencies}) {
	  $depString .= $$resRef->{resource_dependencies};
	}
	if($$resRef->{resource_dependencies_weak}) {
	  $depString .= " " . $$resRef->{resource_dependencies_weak};
	}
	if($$resRef->{resource_dependencies_restart}) {
	  $depString .= " " . $$resRef->{resource_dependencies_restart};
	}
	$depString =~ s/[\t,]|[ ]+/ /g;
	$depString =~ s/^[ ]+|[ ]+$//g;

      DEP_LOOP: foreach my $dependency (  split( / /, $depString ) ) {
	  foreach my $queuedRef (@{$orderedResourceRefs}) {
	    if ($$queuedRef->{name} eq $dependency) {
	      next DEP_LOOP;
	    }
	  }
	  # send resource reference back to the bottom of the original list 
	  # otherwise
	  unshift(@{$resourceRefs}, $resRef);
	  next RES_LOOP;
	}
	push(@{$orderedResourceRefs}, $resRef);
      } 
    }
  }

  # Make a second pass and to resolve implicit dependencies
  # SharedAddress and LogicalAddress come first.
  my $finalResourceRefs = [];
  my $restResourceRefs = [];
  
  if(defined($orderedResourceRefs)) {
    
    while (@{$orderedResourceRefs}) {
      my $curResRef = pop(@{$orderedResourceRefs});
      
      # If it is a SharedAddress or a LogicalAddress, enqued it in the 
      # final list
      if( ($$curResRef->{resource_type} eq "SUNW.LogicalHostname") or
	  ($$curResRef->{resource_type} eq "SUNW.SharedAddress") ) {
	unshift(@{$finalResourceRefs}, $curResRef);
      } else {
	unshift(@{$restResourceRefs}, $curResRef);
      }
    }
    
    if(defined($restResourceRefs)) {
      while (@{$restResourceRefs}) {
	push(@{$finalResourceRefs}, shift(@{$restResourceRefs}));
      }
    }
    # add actions for creating resources
    foreach my $resRef (@{$finalResourceRefs}) {
      my $stdproperties;
      my $extproperties;
      
      if ($$resRef->{resource_type} eq "SUNW.LogicalHostname") {
	# action for creating a logical hostname resource
	push(@{$view}, 
	     my $action={ 
			 'Type'=>'CREATE_LOGICAL_HOSTNAME_RESOURCE',
			 'Parameters'=>{ 
					'RESOURCE_NAME'=>$$resRef->{name},
					'RESOURCE_GROUP_NAME'=>$$resRef->{resource_group_name},
					'HOSTNAME_LIST'=>undef,
					'NETIF_LIST'=>undef,
					'STANDARD_PROPERTIES'=>$stdproperties = {}
				       } 
			});
	# process extension properties, which must be
	# { HostnameList, NetIfList }
	foreach my $property (@{$$resRef->{properties}}) {
	  if ($property->{class} eq "extension") {
	    if (lc($property->{name}) eq "hostnamelist") {
	      my $value=$property->{value};
	      $value =~ s/ /,/g;
	      $action->{Parameters}->{HOSTNAME_LIST} = $value;
	    } elsif (lc($property->{name}) eq "netiflist") {
	      my $value=$property->{value};
	      $value =~ s/ /,/g;
	      $action->{Parameters}->{NETIF_LIST} = $value;
	    }
	  }
	}
	
      } elsif ($$resRef->{resource_type} eq "SUNW.SharedAddress") {
	# action for creating a shared address resource
	push(@{$view}, 
	     my $action = {'Type' => 'CREATE_SHARED_ADDRESS_RESOURCE',
			   'Parameters' => { 'RESOURCE_NAME'=>$$resRef->{name},
					     'RESOURCE_GROUP_NAME'=>$$resRef->{resource_group_name},
					     'HOSTNAME_LIST' => undef,
					     'NETIF_LIST' => undef,
					     'AUXNODE_LIST' => undef,
					     'STANDARD_PROPERTIES'=>$stdproperties = {}
					   } 
			  });
	
	# process extension properties, which must be
	# { HostnameList, NetIfList, AuxNodeList }
	foreach my $property (@{$$resRef->{properties}}) {
	  if ($property->{class} eq "extension") {
	    if (lc($property->{name}) eq "hostnamelist") {
	      my $value=$property->{value};
	      $value =~ s/ /,/g;
	      $action->{Parameters}->{HOSTNAME_LIST} = $value;
	    } elsif (lc($property->{name}) eq "netiflist") {
	      my $value=$property->{value};
	      $value =~ s/ /,/g;
	      $action->{Parameters}->{NETIF_LIST} = $value;
	    } elsif (lc($property->{name}) eq "auxnodelist") {
	      my $value=$property->{value};
	      $value =~ s/ /,/g;
	      $action->{Parameters}->{AUXNODE_LIST} = $value;
	    }
	  }
	}
      } else {
	# action for creating a general resource
	push(@{$view},    
	     { 'Type' => 'CREATE_RESOURCE',
	       'Parameters' => { 'RESOURCE_NAME' => $$resRef->{name},
				 'RESOURCE_GROUP_NAME' => $$resRef->{resource_group_name},
				 'RESOURCE_TYPE_NAME' => $$resRef->{resource_type},
				 'STANDARD_PROPERTIES' => $stdproperties = {},
				 'EXTENSION_PROPERTIES' => $extproperties = {}
			       } 
	     });
      }

      # Add the following resource attribute to the STANDARD_PROPERTIES list
      my @specialAtts = ("resource_dependencies", "resource_dependencies_restart", "resource_dependencies_weak" , "r_description", "resource_project_name");
      my @specialProperties = ();
      
      foreach my $specialAtt (@specialAtts) {
	
	# No matter of the 'type field' not used.
	push(@specialProperties, { 'name' => $specialAtt,
				   'class' => 'standard',
				   'type' => 'string',
				   'value' => $$resRef->{$specialAtt} });
      }
      
      my $metadata = $self->{METADATA}->resources();
      foreach my $property ((@{$$resRef->{properties}}, @specialProperties)) {
	
	# ignore standard properties which are not defined in the
	# metadata repository
	my $defined = 0;
	for (keys(%{$metadata})) {
	  if ($property->{name} =~ /$_/)# do not use eq here as 
	    # property name in the 
	    # metadata repository may be
	    # a regexp
	    {
	      $defined = 1;
	      last;
	    }
	} 
	
	if ($defined || $property->{class} eq "extension") {
	  # select the right list of properties in the action
	  my $propertylist = ($property->{class} eq 
			      "standard")?$stdproperties:$extproperties;
	  my $value = $property->{value};
	  if (defined $propertylist) # false when resource is a 
	    # LogicalHostName or a 
	    # SharedAddress and property is
	    # HostnameList, NetIfList or 
	    # AuxNodeList
	    {
	      # default value for the property provided by the
	      # resource type
	      my $rtdefault;
	      foreach my $rt (@{$data->{rtdefs}}) {
		if ($rt->{name} eq $$resRef->{resource_type}) {
		  foreach my $param (@{$rt->{params}}) {
		    if ($param->{name} eq $property->{name}) {
		      $rtdefault = $param->{default};
		      last;
		    }
		  }
		  last;
		}
	      }
	      # default value for the property provided by the
	      # metadata repository
	      my $repdefault = $metadata->{$property->{name}};
	      # default value used as a comparison
	      my $default = (defined($rtdefault))?$rtdefault:$repdefault;
	      # add properties only when the value is different
	      # from the default value or when there is no default 
	      # value
	      if (!defined($default) || 
		  lc($value) ne lc($default))
		
		# perform a case-insensitive comparison
		#in order to avoid problems like "true != True"
		{
		  $propertylist->{$property->{name}} = $value;
		}
	    }
	}
      }
    }
  }
  
  push(@{$view}, 
       { 'Type' => 'METADATA',
	 'Parameters' => {
			  'REQUESTED_SOFTWARE_VERSION'=>$data->{scsoftvers},
			  'REQUESTED_OS_VERSION' => $data->{osversion},
			  'REQUESTED_ARCH' => $data->{arch}
			 }
       });
  
  return defined($view)?$view:[];
} 

#
# Generates a view used to delete the given configuration.
# Arguments:
#   1) Configuration to be re-created.
#      Cannot be undefined.
#
sub remove {
  my $self = shift;
  my $data = shift;
  my $view;
  
  # Resource Groups should be treated from most dependant to less one.
  my $resourceGroupRefs = [];
  my $initialRGRefList = [];
  
  # Reorder the resource groups reverse according to their dependencies, 
  if(defined($data->{rgdefs})) {

    # Build a list of reference on all defined RG
    foreach my $rg (@{$data->{rgdefs}}) {
      my $rgName = $rg->{name};
      my $found =0;
      my @array = @{$initialRGRefList};

      for(my $i=0; $i<=$#array and not $found; $i++) {
	my $queuedRef = $array[$i];
	if( $$queuedRef->{name} eq $rgName ) {
	  $found=1;
	}
      }
      
      if(not $found) {
	push(@{$initialRGRefList}, \$rg);
      }
    }

  RG_LOOP: while (@{$initialRGRefList}) {

      # Pop each resource group from the orignal list
      my $resourceGroupRef = pop(@{$initialRGRefList});
      
      # Enqueued resource group if it has no dependency ...
      if( not $$resourceGroupRef->{rg_dependencies}
	  and not $$resourceGroupRef->{rg_affinities}) {
	unshift(@{$resourceGroupRefs}, $resourceGroupRef);
      }
      # ... or of if all dependencies are already enqueued
      else {
	my $depString = "";
	if($$resourceGroupRef->{rg_dependencies}) {
	  $depString .= $$resourceGroupRef->{rg_dependencies}
	}
	if($$resourceGroupRef->{rg_affinities}) {
	  $depString .= " " . _extractRGFromAffinities($$resourceGroupRef->{rg_affinities});
	}
	$depString =~ s/[\t,]|[ ]+/ /g;
	$depString =~ s/^[ ]+|[ ]+$//g;
      RG_DEP_LOOP: foreach my $rgDep (split(/ /, $depString ) ) { 
	  
	  foreach my $queuedRGRef (@{$resourceGroupRefs}) {
	    if ($$queuedRGRef->{name} eq $rgDep) {
	      next RG_DEP_LOOP;
	    }
	  }
	  # Send resource group back to the bottom of the original list
	  # otherwise
	  unshift(@{$initialRGRefList}, $resourceGroupRef);
	  next RG_LOOP;
	}
	unshift(@{$resourceGroupRefs}, $resourceGroupRef);
      }
    }
  }
  
  # Build a list of all resource references to remove regardless their 
  # container RG
  my $resourceRefs = [];
  my $orderedResRefs = [];
  
  foreach my $rgRef (@{$resourceGroupRefs}) {
    if(defined($$rgRef->{resources})) {
      foreach my $res (@{$$rgRef->{resources}}) {
	my $resName = $res->{name};
	my $found=0;
	
	my @array = @{$resourceRefs};
	for(my $i=0; $i<=$#array and not $found; $i++) {
	  my $queuedRef = $array[$i];
	  if( $$queuedRef->{name} eq $resName ) {
	    $found=1;
	  }
	}
	if(not $found) {
	  push(@{$resourceRefs}, \$res);
	}
      }
    }
  }

  # Resources should be treated reversely against their dependencies
  if(defined($resourceRefs)) {
    
  RES_LOOP: while (@{$resourceRefs}) {

      # pop each resource reference from the original list
      my $resourceRef = pop(@{$resourceRefs});
      # enqueue resource if is has no dependency ...
      if (not $$resourceRef->{resource_dependencies}
	  and not $$resourceRef->{resource_dependencies_weak}
	  and not $$resourceRef->{resource_dependencies_restart} ) {
	unshift(@{$orderedResRefs}, $resourceRef);
      }
      # ... or if all its dependencies are already enqueued
      else {
	my $depString = "";
	if($$resourceRef->{resource_dependencies}) {
	  $depString .= $$resourceRef->{resource_dependencies};
	}
	if($$resourceRef->{resource_dependencies_weak}) {
	  $depString .= " " . $$resourceRef->{resource_dependencies_weak};
	}
	if($$resourceRef->{resource_dependencies_restart}) {
	  $depString .= " " .$$resourceRef->{resource_dependencies_restart};
	}
	$depString =~ s/[\t,]|[ ]+/ /g;
	$depString =~ s/^[ ]+|[ ]+$//g;
      DEP_LOOP: foreach my $dependency ( split(/ /, $depString)) {
	  foreach my $queued (@{$orderedResRefs}) {
	    if ($$queued->{name} eq $dependency) {
	      next DEP_LOOP;
	    }
	  }
	  # send resource back to the bottom of the original list 
	  # otherwise
	  unshift(@{$resourceRefs}, $resourceRef);
	  next RES_LOOP;
	}
	unshift(@{$orderedResRefs}, $resourceRef);
      }
    }
    
    # add action to remove resources
    foreach my $resourceRef (@{$orderedResRefs}) {
      push(@{$view},
	   { 'Type' => 'DELETE_RESOURCE',
	     'Parameters' => { 'RESOURCE_NAME' => $$resourceRef->{name} }
	   });
    }
  }

  foreach my $rgRef (@{$resourceGroupRefs}) {
    push(@{$view},
	 { 'Type' => 'DELETE_RESOURCE_GROUP',
	   'Parameters' => {
			    'RESOURCE_GROUP_NAME' => $$rgRef->{name}
			   }
	 });
  }

  # remove action for deleting resource types
  foreach my $rt (@{$data->{rtdefs}}) {
    # Don't generate action for Sun Cluster built-in Resource Types
    if(    ($rt->{name} ne "SUNW.LogicalHostname") 
	   and ($rt->{name} ne "SUNW.SharedAddress") ) {
      
      # Patch the inconsistency in scrgadm -pvv output. Sometimes
      # Resource Type name are displayed with version 
      my $name = $rt->{name};
      if($name =~ /:/) {
	my @elt = split(':',$name);
	$name=$elt[0];
      }
      
      push(@{$view},
	   { 'Type' => 'DELETE_RESOURCE_TYPE',
	     'Parameters' => { 'RESOURCE_TYPE_NAME' => $rt->{name},
			       'RESOURCE_TYPE_VERSION' => $rt->{version} }
	   });
    }
  }

  return defined($view)?$view:[];
}

# 
# This subroutine, remove the delete/create actions couple in a passed
# view for the same Resource Type.
#
# Arguments:
#   1) view
#      Cannot be undefined
#
sub optimize {
  my $self = shift;
  my $view = shift;
  my $newView = [];
  
  if(defined($view)) {
  ACTION_LOOP: while (@{$view})
    {
      my $action = shift(@{$view});
      
      if ($action->{Type} eq "DELETE_RESOURCE_TYPE") {
	my $rtname = $action->{Parameters}->{RESOURCE_TYPE_NAME};
	my $rtversion = $action->{Parameters}->{RESOURCE_TYPE_VERSION};
	foreach my $act (@{$view}) {
	  if ( ($act->{Type} eq "CREATE_RESOURCE_TYPE") && 
	       ((my $actname = $act->{Parameters}->{RESOURCE_TYPE_NAME} eq
		 $rtname) 
		&& (my $actversion = $act->{Parameters}->{RESOURCE_TYPE_VERSION} eq
		    $rtversion))) {
	    shift(@{$view});
	    next ACTION_LOOP;
	  }
	}
      }
      
      push(@{$newView}, $action);
    }
  }
  
  return $newView;
}

#
# Generates a view used to migrate from a given old configuration to a given
# new configuration.
# Arguments:
#   1) Old configuration.
#      Cannot be undefined.
#   2) New configuration.
#      Cannot be undefined.
#
sub delta {
  my $self = shift;
  my $olddata = shift;
  my $newdata = shift;
  my $view = [];
  
  my $removedView = $self->remove($olddata);
  my $addedView = $self->create($newdata);
  
  foreach my $action (@{$removedView}) {
    push(@{$view}, $action);
  }
  foreach my $action (@{$addedView}) {
    push(@{$view}, $action);
  }
  optimize($view);
  return $view;
} 

#
# Extract a list of Resource Group name from a string that express Resource Group affinities
#
# Arguments:
#  1) RG_affinities property value
#
# Return:
#   A string containing the list of affinities
#
sub _extractRGFromAffinities {
  
  my $string = shift;
  
  # Remove '+' and '-' and multiple occurence of blank character character 
  # from the passed string.
  if($string) {
    $string =~ s/[+-]//g;
  }

  return $string;
}

# Return success for module load
1; 
