package PPM::Repository::PPM3Server; use strict; use SOAP::Lite 0.51; use Data::Dumper; use File::Basename qw(basename); use Digest::MD5 (); use PPM::Config; use PPM::Sysinfo; use PPM::Result qw(Ok Error Warning List); use PPM::PPD; use base qw(PPM::Repository); our $VERSION = '3.06'; our $PROTOCOL_VERSION = 3; #============================================================================== # Note: the server exports this interface: # describe('language', 'package name'); # getppd('language', 'package name'); # search('language', 'query', 'case-insensitive'); # uptodate('language', 'package name', 'osd-version'); # ppm_protocol(); # # Note: definition of package_entity: # package_entity { # repository_url # target_type # target_name # package_name # package_version # } # # This part of the interface is for profile management: # profile_create('name'); # profile_delete('name'); # profile_save('name', package_entity(s)); # profile_info('name'); # profile_target_rename('name', 'oldtarget', 'newtarget'); # profile_rename('oldname', 'newname'); # XXX: emulated on client-side! # # This part is for profile tracking: # installed(package_entity); # removed(package_entity); # upgraded(package_entity); #============================================================================== sub init { my $o = shift; my $file = PPM::Config::get_license_file(); $o->{licfile} = $file; $o->{mtime} = 0; $o->{license} = undef; 1; } sub check_license { my $o = $_[0]; my $file = $o->{licfile}; # Reset state unless the license file exists goto &init unless -f $file; # Used the cached license unless the license file has changed on disk my $f_mtime = ((stat($file))[9]); my $l_mtime = $o->{mtime}; return unless ($f_mtime > $l_mtime or $file ne $o->{liclast}); # Update the cache from the disk if (open (my $LICENSE, $file)) { $o->{mtime} = $f_mtime; $o->{license} = do { local $/; <$LICENSE> }; $o->{liclast} = $file; close ($LICENSE) or die "can't close license file $file: $!"; } } sub search { my $o = shift; my $target = shift; my $query = shift; my $casei = shift; # Get all my arguments together: my $target_t = $target->config_get("TARGET_TYPE")->result; my @headers = $o->mkheaders(target => $target); my $response = eval { $o->{client}->search($target_t, $query, $casei, @headers)->result; }; my $err = $o->errors($response); return $err unless $err->ok; my @results; for my $res (@{$response->{'results'}}) { my $h = { name => $res->[0], version => $res->[1], abstract => $res->[2], }; my $ppd = PPM::Repository::PPM3Server::PPD->new($h, 0); @$ppd{qw(rep id from)} = ($o, $ppd->name, 'repository'); push @results, $ppd; } List(@results); } sub describe { my $o = shift; my $target = shift; my $pkg = shift; my $target_t = $target->config_get("TARGET_TYPE")->result; my @headers = $o->mkheaders(target => $target); my $response = eval { $o->{client}->describe($target_t, $pkg, @headers)->result; }; my $err = $o->errors($response); return $err unless $err->ok; my $ppd = PPM::Repository::PPM3Server::PPD->new($response->{'results'}, 1); @$ppd{qw(rep id from)} = ($o, $ppd->name, 'repository'); Ok($ppd); } sub getppd_obj { my $o = shift; my $target = shift; my $pkg = shift; my $ppd_txt = $o->getppd($target, $pkg); return $ppd_txt unless $ppd_txt->ok; my $obj = PPM::PPD::->new($ppd_txt->result, $o, $pkg); $obj->{from} = 'repository'; Ok($obj); } sub getppd { my $o = shift; my $target = shift; my $pkg = shift; my $target_t = $target->config_get("TARGET_TYPE")->result; my @headers = $o->mkheaders(target => $target); my $response = eval { $o->{client}->getppd($target_t, $pkg, @headers)->result; }; my $err = $o->errors($response); return $err unless $err->ok; Ok($response->{'results'}); } sub uptodate { my $o = shift; my $target = shift; my $pkg = shift; my $version = shift; my $target_t = $target->config_get("TARGET_TYPE")->result; my @headers = $o->mkheaders(target => $target); my $response = eval { # call the more efficient uptodate2 method. $o->{client}->uptodate2($target_t, $pkg, $version, @headers)->result; }; my $err = $o->errors($response); return $err unless $err->ok; List($response->{results}[0], $response->{results}[1]{version}); # # If the status is false (it's out of date) return the version on the # # server as the new version. # my $newversion = ''; # unless ($response->{'results'}) { # my $ppd = $o->describe($target, $pkg)->result; # $newversion = $ppd->version; # } # List($response->{'results'}, $newversion); } sub batch { my $o = shift; # The batch() method was introduced in PPM protocol version 303. return $o->SUPER::batch(@_) unless $o->protocol >= 303; my $targ = shift; my @tasks = @_; my $targ_t = $targ->config_get("TARGET_TYPE")->result; # Every task needs the target as the first argument, so add it. for my $task (@tasks) { splice @$task, 1, 0, $targ_t; } my @headers = $o->mkheaders($targ => $targ); my $response = eval { $o->{client}->batch($targ_t, @tasks, @headers)->result; }; my $err = $o->errors($response); return $err unless $err->ok; my @responses = @{$response->{results}}; for my $i (0 .. $#tasks) { my $resp = $responses[$i]; my $meth = $tasks[$i][0]; my $err = $o->errors($resp); if ($err->ok) { my $clean_method = "cleanup_$meth"; $responses[$i] = $o->can($clean_method) ? $o->$clean_method($resp->{results}) : Ok($resp->{results}); } else { $responses[$i] = $err; } } List(@responses); } #============================================================================= # Cleanup functions for the batch() method. #============================================================================= sub cleanup_uptodate2 { my ($o, $result) = @_; my $ppd = PPM::Repository::PPM3Server::PPD->new($result->[1], 0); @$ppd{qw(rep id from)} = ($o, $ppd->name, 'repository'); $result->[1] = $ppd; Ok($result); } #============================================================================= # Profiles #============================================================================= sub profile_list { my $o = shift; my @headers = $o->mkheaders; my $response = eval { $o->{client}->profile_list(@headers)->result; }; my $err = $o->errors($response); return $err unless $err->ok; my @profiles = @{$response->{'results'}}; List(@profiles); } sub profile_add { my $o = shift; my $name = shift; my @headers = $o->mkheaders; my $response = eval { $o->{client}->profile_create($name, @headers)->result; }; my $err = $o->errors($response,"profile_create"); return $err unless $err->ok; Ok(); } sub profile_del { my $o = shift; my $name = shift; my @headers = $o->mkheaders; my $response = eval { $o->{client}->profile_delete($name, @headers)->result; }; my $err = $o->errors($response,"profile_delete"); return $err unless $err->ok; Ok(); } sub profile_save { my $o = shift; my $name = shift; my @entries = @_; my @headers = $o->mkheaders; my $response = eval { $o->{client}->profile_save($name, \@entries, @headers)->result; }; my $err = $o->errors($response); return $err unless $err->ok; Ok(); } sub profile_info { my $o = shift; my $name = shift; my @headers = $o->mkheaders; my $response = eval { $o->{client}->profile_info($name, @headers)->result; }; my $err = $o->errors($response); return $err unless $err->ok; my $entries = $response->{'results'}; List(@$entries); } sub profile_target_rename { my $o = shift; my $profile = shift; my $oldname = shift; my $newname = shift; my @headers = $o->mkheaders; my $response = eval { my @args = ($profile, $oldname, $newname); $o->{client}->profile_target_rename(@args, @headers)->result; }; my $err = $o->errors($response); return $err unless $err->ok; Ok(); } # This is a bit of a temporary hack: the server doesn't actually expose a # profile_rename() method, so I emulate it by retrieving the doomed profile, # saving it as the new profile, and deleting the other. I suspect this will be # moved over to the server in future, because it can be lightening-fast if # done directly in the database. sub profile_rename { my $o = shift; my $oldprof = shift; my $newprof = shift; # Retrieve the old profile: my $info = $o->profile_info($oldprof); return $info unless $info->ok; # Delete the new one, but don't croak if it returns an error. This allows # us to rename over old profiles. my $purge = $o->profile_del($newprof); # Create the new one: my $new = $o->profile_add($newprof); return $new unless $new->ok; # Save the new one: my $save = $o->profile_save($newprof, $info->result_l); return $save unless $save->ok; # Delete the old one: my $del = $o->profile_del($oldprof); return $del unless $del->ok; Ok(); } #============================================================================= # Profile Tracking: #============================================================================= sub installed { my $o = shift; my $profile = shift; my @l = @_; my @headers = $o->mkheaders; my $response = eval { $o->{client}->profile_pkgs_installed($profile, \@l, @headers)->result; }; $o->errors($response); } sub upgraded { my $o = shift; my $profile = shift; my @l = @_; my @headers = $o->mkheaders; my $response = eval { $o->{client}->profile_pkgs_upgraded($profile, \@l, @headers)->result; }; $o->errors($response); } sub removed { my $o = shift; my $profile = shift; my @l = @_; my @headers = $o->mkheaders; my $response = eval { $o->{client}->profile_pkgs_removed($profile, \@l, @headers)->result; }; $o->errors($response); } # Calculate a hash of the current user, plus the host and the install time. # This is useful for tracking how many "users" are using each installation. my $userhash = PPM::Sysinfo::generate_user_key(); my $insthash = PPM::Sysinfo::inst_key(); # This little helper builds SOAP Headers we can use to send along with the # SOAP request. The license and other information is sent along with it. sub mkheaders { my $o = shift; my %args = @_; my @headers; # By checking the license each time, we can auto-detect new licenses # without a re-start of PPM: $o->check_license; push @headers, SOAP::Header->name('license', $o->{license}); # Push on the ID of this installation of PPM: push @headers, SOAP::Header->name('ppm_install_id', $insthash); # Push on a hash of the current user plus hostname & install time. Note # that we specifically don't want to use all the same elements that went # into the install_id of this host (hostname, insttime, os, ip_addr). We # want to have each user get a unique string, but to munge enough extra # uniqueness that the usernames can't be guessed just by a simple # dictionary attack against someone sniffing the MD5 keys. push @headers, SOAP::Header->name('user_hash', $userhash); # Push on the client's protocol version and "real" version: push @headers, SOAP::Header->name('client_version', $VERSION); push @headers, SOAP::Header->name('ppm_protocol', $PROTOCOL_VERSION); # This information has to be grabbed from the piece of software actually # interacting with the user. Currently, there's no way to do that cleanly. push @headers, SOAP::Header->name('useragent', 'PPM'); push @headers, SOAP::Header->name('useragent_vers', '3.0'); # Push on target-specific stuff: for my $k (keys %args) { if ($k eq 'target') { my $t = $args{$k}; push(@headers, SOAP::Header->name('archname', $t->config_get("ARCHITECTURE")->result), SOAP::Header->name('os', $t->config_get("OSVALUE")->result), SOAP::Header->name('osvers', $t->config_get("OSVERSION")->result), ); } } @headers; } sub type_printable { "PPMServer 3.0" } sub errors { my $o = shift; my $response = shift; # assuming that method name here and method name on server are usually # equivalent. if not, use an optional second argument to supply method # name. my $method = shift || (split '::', (caller(1))[3])[-1]; if ($@) { chomp $@; return PPM::Repository::Result::->new("$method exception: $@.", $o->location, ); } elsif (not defined $response) { return PPM::Repository::Result::->new( "$method returned undefined results.", $o->location, ); } elsif ($response->{'status'} != 0) { return PPM::Repository::Result::->new($response->{'message'}, $o->location, $response->{'status'} ); } Ok(); } package PPM::Repository::PPM3Server::PPD; our @ISA = qw(PPM::PPD); sub new { my $this = shift; my $class = ref($this) || $this; my $self = bless {}, $class; my $server_ppd = shift; my $complete = shift; $self->{is_complete} = $complete; # Author: "authorname (authoremail)" if (defined $server_ppd->{'authorname'}) { $self->{parsed}{AUTHOR} = $server_ppd->{'authorname'}; if (defined $server_ppd->{'authoremail'}) { $self->{parsed}{AUTHOR} .= " ($server_ppd->{'authoremail'})"; } } # Name, title, version, abstract: for my $field (qw(title abstract version name)) { $self->{parsed}{"\U$field"} = $server_ppd->{$field}; } # Implementations: for my $impl (@{$server_ppd->{implementation}}) { my $i = bless { ARCHITECTURE => $impl }, 'PPM::Repository::PPM3Server::PPD::Implementation'; push @{$self->{parsed}{IMPLEMENTATION}}, $i; # Dependencies: for my $dep (@{$server_ppd->{dependency}}) { my $dep = bless { NAME => $dep->{name}, VERSION => $dep->{version} }, 'PPM::Repository::PPM3Server::PPD::Dependency'; push @{$i->{DEPENDENCY}}, $dep; } } return $self; } sub version { my $o = shift; $o->version_osd; } package PPM::Repository::PPM3Server::PPD::Implementation; our @ISA = qw(PPM::PPD::Implementation); package PPM::Repository::PPM3Server::PPD::Dependency; our @ISA = qw(PPM::PPD::Dependency); sub version { my $o = shift; $o->version_osd; }