diff --git a/utils/Config.pm b/utils/Config.pm new file mode 100644 index 000000000..1694d80ed --- /dev/null +++ b/utils/Config.pm @@ -0,0 +1,117 @@ +# ---------------------------------------------------------------------- +# Copyright (c) 2006 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 Immunix::Config; + +use strict; +use warnings; + +use Carp; +use Cwd qw(cwd realpath); +use File::Basename; +use File::Temp qw/ tempfile tempdir /; +use Data::Dumper; +use Locale::gettext; +use POSIX; + +require Exporter; +our @ISA = qw(Exporter); +our @EXPORT = qw( + read_config + write_config + find_first_file + find_first_dir +); + +our $confdir = "/etc/apparmor"; + +# config vars +our $cfg; +our $repo_cfg; + +sub read_config { + my $filename = shift; + my $config; + + if (open(CONF, "$confdir/$filename")) { + my $which; + while () { + chomp; + # ignore comments + next if /^\s*#/; + if (m/^\[(\S+)\]/) { + $which = $1; + } elsif (m/^\s*(\S+)\s*=\s*(.*)\s*$/) { + my ($key, $value) = ($1, $2); + $config->{$which}{$key} = $value; + } + } + close(CONF); + } + + return $config; +} + +sub write_config { + my ($filename, $config) = @_; + if (open(my $CONF, ">$confdir/$filename")) { + for my $section (sort keys %$config) { + print $CONF "[$section]\n"; + + for my $key (sort keys %{$config->{$section}}) { + print $CONF " $key = $config->{$section}{$key}\n" + if ($config->{$section}{$key}); + } + } + chmod(0600, $CONF); + close($CONF); + } else { + die "Can't write config file $filename: $!"; + } +} + +sub find_first_file { + my $list = shift; + return if ( not defined $list ); + my $filename; + for my $f (split(/\s+/, $list)) { + if (-f $f) { + $filename = $f; + last; + } + } + + return $filename; +} + +sub find_first_dir { + my $list = shift; + return if ( not defined $list ); + my $dirname; + for my $f (split(/\s+/, $list)) { + if (-d $f) { + $dirname = $f; + last; + } + } + + return $dirname; +} + +1; diff --git a/utils/Repository.pm b/utils/Repository.pm new file mode 100644 index 000000000..1c99e25da --- /dev/null +++ b/utils/Repository.pm @@ -0,0 +1,347 @@ +# ---------------------------------------------------------------------- +# Copyright (c) 2008 Dominic Reynolds +# +# 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. +# +# +# ---------------------------------------------------------------------- + +package Immunix::Repository; + +use strict; +use warnings; + +use Carp; +use Cwd qw(cwd realpath); +use Data::Dumper; +use File::Basename; +use File::Temp qw/ tempfile tempdir /; +use Immunix::Config; +use Locale::gettext; +use POSIX; +use RPC::XML; +use RPC::XML::Client; + + +require Exporter; +our @ISA = qw(Exporter); +our @EXPORT = qw( + get_repo_client + did_result_succeed + get_result_error + user_login + user_register + upload_profile + fetch_profile_by_id + fetch_profiles_by_user + fetch_profiles_by_name + fetch_profiles_by_name_and_user + fetch_newer_profile + get_repo_config + set_repo_config +); + +our %clients; +our %uid2login; +our $DEBUGGING = 0; +our $repo_cfg; +our $aa_cfg; + +sub get_repo_client ($) { + my $repo_url = shift; + unless ( $clients{$repo_url} ) { + $clients{$repo_url} = new RPC::XML::Client $repo_url; + } + return $clients{$repo_url}; +} + +sub did_result_succeed { + my $result = shift; + + my $ref = ref $result; + return ($ref && $ref ne "RPC::XML::fault") ? 1 : 0; +} + +sub get_result_error { + my $result = shift; + + if (ref $result) { + if (ref $result eq "RPC::XML::fault") { + $result = $result->string; + } else { + $result = $$result; + } + } + return $result; +} + +sub user_login ($$$) { + my ($repo_url,$user,$pass) = @_; + my ($status,$detail); + my $repo_client = get_repo_client( $repo_url ); + if ( $repo_client ) { + my $res = $repo_client->send_request('LoginConfirm', $user, $pass); + if (did_result_succeed($res)) { + $status = 1; + $detail = ""; + } else { + $status = 0; + $detail = get_result_error($res); + } + } + return $status,$detail; +} + + +sub user_register ($$$$) { + my ($repo_url,$user,$pass,$email) = @_; + my $repo_client = get_repo_client( $repo_url ); + my ($status,$detail); + if ( $repo_client ) { + my $res = $repo_client->send_request('Signup', $user, $pass, $email); + if (did_result_succeed($res)) { + $status = 1; + $detail = ""; + } else { + $status = 0; + $detail = get_result_error($res); + } + } + return $status,$detail; +} + +sub upload_profile ($$$$$$$) { + my ($repo_url,$user,$pass,$distro,$pname,$profile,$changelog) = @_; + my ($status,$detail); + my $repo_client = get_repo_client( $repo_url ); + my $res = $repo_client->send_request( 'Create', $user, $pass, $distro, + $pname, $profile, $changelog); + if (did_result_succeed($res)) { + $detail = $res->value; + $status = 1; + } else { + $detail = get_result_error($res); + $status = 0; + } + return $status,$detail; +} + +sub fetch_profile_by_id ($$) { + my ($repo_url,$id) = @_; + my $repo_client = get_repo_client( $repo_url ); + my $repo_profile; + my ($status,$detail); + my $res = $repo_client->send_request('Show', $id); + if (did_result_succeed($res)) { + $status = 1; + $detail = $res->value(); + } else { + $status = 0; + $detail = get_result_error($res); + } + + return $status, $detail; +} + + +sub fetch_profiles ($$$$) { + my ($repo_url,$distro,$username,$fqdn) = @_; + my $p_hash = {}; + my ($status,$detail); + my $repo_client = get_repo_client( $repo_url ); + my $res = + $repo_client->send_request('FindProfiles', $distro, $fqdn, $username); + if (did_result_succeed($res)) { + $status = 1; + for my $p ( @$res ) { + my $p_repo = $p->{profile}->value(); + $p_repo =~ s/flags=\(complain\)// if ( $p_repo ); #strip complain flag + $p->{profile} = $p_repo; + $p->{user_id} = $p->{user_id}->value(); + $p->{id} = $p->{id}->value(); + $p->{name} = $p->{name}->value(); + $p->{created_at} = $p->{created_at}->value(); + $p->{downloaded_count} = $p->{downloaded_count}->value(); + } + $detail = $res; + } else { + $status = 0; + $detail = get_result_error($res); + } + return $status,$detail; +} + +sub fetch_profiles_by_user ($$$) { + my ($repo_url,$distro,$username) = @_; + my $p_hash = {}; + my ($status,$detail) = fetch_profiles( $repo_url, $distro, $username, "" ); + if ( $status ) { + for my $p ( @$detail ) { + my $p_repo = $p->{profile}; + if ($p_repo ne "") { + $p->{username} = $username; + $p_hash->{$p->{name}} = $p; + } + } + } else { + return ($status,$detail); + } + return($status,$p_hash); +} + + +sub fetch_profiles_by_name_and_user ($$$$) { + my ($repo_url,$distro,$fqdbin, $username) = @_; + my $p_hash = {}; + my ($status,$detail) = fetch_profiles( $repo_url, $distro, $username, $fqdbin ); + if ( $status ) { + for my $p ( @$detail ) { + my $p_repo = $p->{profile}?$p->{profile}:""; + $p_hash->{$p->{name}} = $p if ($p_repo ne ""); + } + } else { + return ($status,$detail); + } + return($status,$p_hash); +} + + +sub fetch_profiles_by_name ($$$) { + my ($repo_url,$distro,$fqdbin) = @_; + my ($status,$detail,$data); + $detail = {}; + ($status,$data) = fetch_profiles( $repo_url, $distro, "", $fqdbin); + if ($status) { + my @uids; + for my $p (@$data) { + push @uids, $p->{user_id}; + } + my ($status_unames,$unames) = fetch_usernames_from_uids($repo_url, @uids); + if ( $status_unames ) { + for my $p (@$data) { + if ( $unames->{$p->{user_id}} ) { + $p->{username} = $unames->{$p->{user_id}}; + } else { + $p->{username} = "unkown-" . $p->{user_id}; + } + } + + } else { + print STDOUT "ERROR UID\n"; + } + for my $p (@$data) { + $p->{profile_type} = "REPOSITORY"; + $detail->{$p->{username}} = $p; + } + } else { + $detail = $data; + } + return $status,$detail; +} + + +sub fetch_newer_profile ($$$$$) { + my ($repo_url,$distro,$user,$id,$profile) = @_; + my $repo_client = get_repo_client( $repo_url ); + my $p; + my ($status,$detail); + + if ($repo_client) { + my $res = + $repo_client->send_request('FindProfiles', $distro, $profile, $user); + if (did_result_succeed($res)) { + my @profiles; + my @profile_list = @{$res->value}; + $status = 1; + + if (@profile_list) { + if ($profile_list[0]->{id} > $id) { + $p = $profile_list[0]; + } + } + $detail = $p; + } else { + $status = 0; + $detail = get_result_error($res); + } + } + return $status,$detail; +} + +sub fetch_usernames_from_uids ($) { + my ($repo_url,@searchuids) = @_; + my ($status,$result) = (1,{}); + my @uids; + + for my $uid ( @searchuids ) { + if ( $uid2login{$uid} ) { + $result->{$uid} = $uid2login{$uid}; + } else { + push @uids, $uid; + } + } + if (@uids) { + my $repo_client = get_repo_client( $repo_url ); + my $res = $repo_client->send_request('LoginNamesFromUserIds', [@uids]); + if (did_result_succeed($res)) { + my @usernames = @{ $res->value }; + for my $uid (@uids) { + my $username = shift @usernames; + $uid2login{$uid} = $username; + $result->{$uid} = $uid2login{$uid}; + } + } else { + $status = 0; + $result = get_result_error($res); + } + } + return $status,$result; +} + +sub get_repo_config { + unless ( $repo_cfg ) { + $repo_cfg = Immunix::Config::read_config("repository.conf"); + } + unless ( $aa_cfg ) { + $aa_cfg = Immunix::Config::read_config("logprof.conf"); + } + return { + "url" => $aa_cfg->{repository}{url}, + "distro" => $aa_cfg->{repository}{distro}, + "enabled" => $repo_cfg->{repository}{enabled}, + "upload" => $repo_cfg->{repository}{upload}, + "user" => $repo_cfg->{repository}{user}, + "password" => $repo_cfg->{repository}{pass}, + "email" => $repo_cfg->{repository}{email} + }; +} + +sub set_repo_config ($) { + my $cfg = shift; + my ($url,$distro,$enabled,$upload,$user,$pass); + unless ( $repo_cfg ) { + $repo_cfg = Immunix::Config::read_config("repository.conf"); + } + unless ( $aa_cfg ) { + $aa_cfg = Immunix::Config::read_config("logprof.conf"); + } + $repo_cfg->{repository}{enabled} = $cfg->{enabled} if ( $cfg->{enabled} ); + $repo_cfg->{repository}{upload} = $cfg->{upload} if ( $cfg->{upload} ); + $repo_cfg->{repository}{user} = $cfg->{user} if ( $cfg->{user} ); + $repo_cfg->{repository}{pass} = $cfg->{password}if ( $cfg->{password} ); + $repo_cfg->{repository}{email} = $cfg->{email} if ( $cfg->{email} ); + $aa_cfg->{repository}{distro} = $cfg->{distro} if ( $cfg->{distro} ); + $aa_cfg->{repository}{url} = $cfg->{url} if ( $cfg->{url} ); + write_config("repository.conf", $repo_cfg); + write_config("logprof.conf", $aa_cfg); +} + + +1; diff --git a/utils/aa-repo.pl b/utils/aa-repo.pl new file mode 100644 index 000000000..9d41d80f1 --- /dev/null +++ b/utils/aa-repo.pl @@ -0,0 +1,824 @@ +#!/usr/bin/perl +# +# $Id: $ +# +# ---------------------------------------------------------------------- +# Copyright (c) 2008 Dominic Reynolds. 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. +# +# +# ---------------------------------------------------------------------- + +# ---------------------------------------------------------------------- +# +my $usage = +"aa-repo.pl --command args\n"; + +my $usage_search = +" --search [author=XXX] [prog=XXX] [id=XXX] + Search the repository for profiles matching the search criteria + and return the results. + NOTE: One --search switch per option + + --verbose|v + Verbosity level. Supply either one or two switches. Two switches + adds full profile text in returned search results.\n"; + +my $usage_push = + +" --push [--profile=XXX|all] [--changelog=XXX] + Push local profiles to repository, uses configured user and upon + overwrite of an existing profile in the repository then prompt + user with a diff for confirmation XXX the name of the application + whose profile should be uploaded or \"all\" to upload all + profiles. Multiple --profile switches may be passed to supply + multiple profile names + + e.g. --push --profile /usr/sbin/mdnsd --profile /usr/sbin/ftp + e.g. --push --profile all\n"; + +my $usage_pull = +" --pull [--author=XXX] [--profile=XXX] or [--id=XXX] [--mode=complain] + pull remote profiles and install on local system + If operation will change local profiles then prompt user with + diff for confirmation + NOTE: One --pull switch per option and there are three acceptable + combinations + + --pull --author=XXX + * pull all profiles in the repo for the author + + --pull --author=XXX --profile=XXXX + * pull the profile for prog owned by author + + --pull --id=XXXX + * pull the profile with id + + --pull --mode=complain + * set the profile(s) to complain mode when installed + + Profiles are checked for conflicts with currently installed + profiles and presented as a list to the user to confirm and view.\n"; + + +my $usage_sync = +" --sync [--up] [--down] [--noconfirm] + Synchronize local profile set with the repository - showing + changes and allowing prompting the user with the diffs and + suggest the newest version to be activated. If the --all option + is passed then treat profiles not marked as remote as new + profiles that will be uploaded to the repository.\n"; + +my $usage_stat = +" --status + Show the current status of the local profile set. This operation + is similar to sync but does not prompt the user to up|down load + changes\n"; + +my $usage_getconfig = +" --getconfig|c + Print the current configuration for the repsository\n"; + + +my $usage_setconfig = +" --setconfig [url=xxx] [username=xxxx] [password=xxxx] [enabled=(yes|no)] + [upload=(yes|no)] + Set the configuration options for the repository. + NOTE: One --setconfig switch per option\n"; + +my $usage_bottom = +" --quiet|q Don't prompt user - assume that all changes should be made. + + ISSUES: + o Should changes made to the system be recorded somehow? An audit event? + o Should the tool allow a repo/distro to be passed for each operation? + +"; + +use strict; +use Getopt::Long; + +use Immunix::SubDomain; +use Immunix::Repository; +use Data::Dumper; + +use Locale::gettext; +use POSIX; + +# force $PATH to be sane +$ENV{PATH} = "/bin:/sbin:/usr/bin:/usr/sbin"; + +# initialize the local poo +setlocale(LC_MESSAGES, ""); +textdomain("apparmor-utils"); + +# options variables +my $help = ''; +my $verbose = ''; + +my ( $id, $author, $mode, %search, $sync, $getconfig, $push, + $pull, %setconfig, @profiles, $all, $changelog, $stat ); + +GetOptions( + 'search=s%' => \%search, + 'sync=s' => \$sync, + 'status' => \$stat, + 'getconfig|c' => \$getconfig, + 'setconfig=s%' => \%setconfig, + 'push' => \$push, + 'id=s' => \$id, + 'author=s' => \$author, + 'profile=s' => \@profiles, + 'changelog=s' => \$changelog, + 'pull' => \$pull, + 'all|a' => \$all, + 'help|h' => \$help, + 'verbose|v+' => \$verbose +); + +# +# Root privs required to run the repo tool +# +if ( geteuid() != 0 ) { + print STDERR gettext( +"You must be logged in with root user privileges to use this program.\n" + ); + exit; +} + +# --help +# tell 'em how to use it... +&usage && exit if $help; + +my $config = get_repo_config(); + +# +# --getconfig operation +# +&config && exit if $getconfig; + +my $sd_mountpoint = check_for_subdomain(); +unless ($sd_mountpoint) { + fatal_error(gettext( +"AppArmor does not appear to be started. Please enable AppArmor and try again." + ) + ); +} + +# +# --setconfig operation +# +if ( keys %setconfig ) { + $config->{url} = $setconfig{url} if ( $setconfig{url} ); + $config->{distro} = $setconfig{distro} if ( $setconfig{distro} ); + $config->{enabled} = $setconfig{enabled} if ( $setconfig{enabled} ); + $config->{email} = $setconfig{email} if ( $setconfig{email} ); + $config->{user} = $setconfig{username} if ( $setconfig{username} ); + $config->{password} = $setconfig{password} if ( $setconfig{password} ); + $config->{upload} = $setconfig{upload} if ( $setconfig{upload} ); + set_repo_config( $config ); +} + +# +# --push operation +# +if ( $push ) { + my ($conflicts, $repo_profiles, $local_profiles, @overrides); + if ( ! @profiles ) { + print STDERR gettext( +"Must supply at least one profile using \"--profile XXX\" to --push\n" + ); + exit 1; + } else { + print STDERR Data::Dumper->Dump([@profiles], [qw(*profiles)]); + } + my $changelog = $changelog?$changelog:"none"; + push_profiles( \@profiles, $changelog, 1 ); +} + + +# +# --pull operation +# +if ( $pull ) { + my $type = ""; + if ( $id ) { + if ( $author || @profiles ) { + print STDERR gettext( +"Option --id=XX is only allowed by itself and not in combination to +other options for the --pull command.\n" + ); + exit 1; + } + $type = "id"; + } + if ( @profiles && ! $author ) { + print STDERR gettext( +"Option --profile=XX requires that the --author=XX option be supplied +to distinguish a specific profile.\n" + ); + exit 1; + } else { + $type = "profile"; + } + + my $mode = $mode eq "complain"?1:0; + pull_profiles( \@profiles, $type, $mode, 1 ); +} + +# +# --search operation +# +if ( keys %search ) { + if ( $search{id} ) { + my($status,$result) = fetch_profile_by_id( $config->{url}, + $search{id} ); + if ($status) { + my $title = sprintf(gettext( "Profile ID %s\n"), $search{id}); + console_print_search_results( $title, + "profile", + { $result->{name} => $result } + ); + + } else { + print STDERR "ERROR $result\n"; + } + } elsif ( $search{author} && $search{prog} ) { + my($status,$result) = + fetch_profiles_by_name_and_user( $config->{url}, + $config->{distro}, + $search{prog}, + $search{author} + ); + if ( $status ) { + my $title = + sprintf(gettext("Profiles matching user: %s and program: %s\n"), + $search{author}, + $search{prog} + ); + console_print_search_results( $title, "profile", $result ); + } else { + print STDERR "ERROR $result\n"; + } + } elsif ( $search{author} ) { + my($status,$result) = fetch_profiles_by_user( $config->{url}, + $config->{distro}, + $search{author} + ); + if ( $status ) { + my $title = sprintf(gettext( "Profiles for %s\n"), $search{author}); + console_print_search_results( $title, "profile", $result ); + } else { + print STDERR "ERROR $result\n"; + } + } elsif ( $search{prog} ) { + my($status,$result) = fetch_profiles_by_name( $config->{url}, + $config->{distro}, + $search{prog}, + ); + if ( $status ) { + my $title = sprintf(gettext("Profiles matching program: %s\n"), + $search{prog}); + console_print_search_results( $title, "user", $result ); + } else { + print STDERR "ERROR $result\n"; + } + } else { + print STDERR +"Unsupported search criteria. Please specify at least one of +author=XXX prog=XXX id=XXX\n"; + } +} + +if ( $stat ) { + my ( $local_profiles, $remote_profiles ); + my $msg = +" The following profiles are stored in the repository but + are not synchronized with the copy in the repository\n"; + + my ($status, $result) = fetch_profiles_by_user( $config->{url}, + $config->{distro}, + $config->{user} + ); + if ( $status ) { + $remote_profiles = $result; + } else { + print STDERR sprintf(gettext("ERROR connecting to repository: %s\n"), + $result); + exit; + } + + readprofiles(); + $local_profiles = serialize_local_profiles( \%sd ); + my ($local_only,$unsynched,$synched,$conflicts) = ({}, {}, {}); + $unsynched = find_profile_conflicts($remote_profiles, $local_profiles); + for my $p ( keys %$local_profiles ) { + if ( ! $remote_profiles->{$p} ) { + $local_only->{$p} = $local_profiles->{$p}; + } + } + + for my $p ( keys %$remote_profiles ) { + $synched->{$p} = + $remote_profiles->{$p}->{profile} if ( ! %$unsynched->{$p} ); + } + UI_status($synched, $unsynched, $local_only); +} + +###################### +# Helper functions +###################### + +# +# Compare the local profile set with the remote profile set. +# Return a list of the conflicting profiles as a list +# { PROFILE_NAME => [LOCAL_PROFILE, REMOTE_PROFILE] ] +# +# +# remote_profiles = repository profiles as returned by one of the +# Immunix::Repository::fetch... functions +# local_profiles = hash ref containing +# { name => serialized local profile } +# +# + +sub find_profile_conflicts ($$) { + my ($remote_profiles,$local_profiles) = @_; + my $conflicts = {}; + for my $p ( keys(%$local_profiles) ) { + if ( $local_profiles->{$p} and $remote_profiles->{$p} ) { + my $p_local = $local_profiles->{$p}; + my $p_remote = $remote_profiles->{$p}->{profile}; + chomp($p_local); + chomp($p_remote); + if ( $p_remote ne $p_local ) { + $conflicts->{$p} = [ $p_local, $p_remote ]; + } + } + } + return( $conflicts ); +} + +sub serialize_local_profiles ($) { + my $profiles = shift; + my $local_profiles = {}; + for my $p ( keys %$profiles ) { + my $serialize_opts = {}; + $serialize_opts->{NO_FLAGS} = 1; + my $p_local = serialize_profile( $profiles->{$p}, + $p, + $serialize_opts ); + $local_profiles->{$p} = $p_local; + } + return $local_profiles; +} + + +sub console_print_search_results ($$$) { + my ($title, $type,$result) = @_; + open(PAGER, "| less") or die "Can't open pager"; + print PAGER $title; + print PAGER "Found " . values(%$result) . " profiles \n"; + for my $p ( values(%$result) ) { + if ( $verbose ) { + if ( $type eq "user" ) { + print PAGER " Author [ " . $p->{username} . " ]\n"; + } elsif ( $type eq "profile" ) { + print PAGER " Name [ " . $p->{name} . " ]\n"; + } + print PAGER " Created [ " . $p->{created_at} . " ]\n"; + print PAGER " Downloads [ " . $p->{downloaded_count} . " ]\n"; + print PAGER " ID [ " . $p->{id} . " ]\n"; + if ( $verbose > 1 ) { + print PAGER " Profile [ \n" . $p->{profile} . " ]\n\n"; + } else { + print PAGER "\n"; + } + } else { + my $data = $type eq "user"?$p->{username}:$p->{name}; + print PAGER " " . $data . "\n"; + } + } + close PAGER; +} + +sub UI_resolve_profile_conflicts { + + my ($explanation, $conflict_hash) = @_; + my $url = $config->{url}; + my @conflicts = map { [ $_, + $conflict_hash->{$_}->[0], + $conflict_hash->{$_}->[1] + ] } + keys %$conflict_hash; + my @commits = []; + my $title = "Profile conflicts"; + my %resolution = (); + my $q = {}; + $q->{title} = $title; + $q->{headers} = [ "Repository", $url, ]; + + $q->{explanation} = $explanation; + + $q->{functions} = [ "CMD_OVERWRITE", + "CMD_KEEP", + "CMD_VIEW_CHANGES", + "CMD_ABORT", + "CMD_CONTINUE", ]; + + $q->{default} = "CMD_OVERWRITE"; + $q->{options} = [ map { $_->[0] } @conflicts ]; + $q->{selected} = 0; + + my ($ans, $arg); + do { + ($ans, $arg) = UI_PromptUser($q); + + if ($ans eq "CMD_VIEW_CHANGES") { + display_changes($conflicts[$arg]->[2], $conflicts[$arg]->[1]); + } + if ( $ans eq "CMD_OVERWRITE") { + $q->{options} = + [ map { $_ =~ /$conflicts[$arg]->[0]( K| O)?$/? + $conflicts[$arg]->[0] . " O": + $_ } + @{$q->{options}} + ]; + $resolution{$conflicts[$arg]->[0]} = "O"; + } + if ( $ans eq "CMD_KEEP") { + $q->{options} = + [ map { $_ =~ /$conflicts[$arg]->[0]( K| O)?$/? + $conflicts[$arg]->[0] . " K": + $_ } + @{$q->{options}} + ]; + $resolution{$conflicts[$arg]->[0]} = "K"; + } + $q->{selected} = ($arg+1) % @conflicts; + } until $ans =~ /^CMD_CONTINUE/; + if ($ans eq "CMD_CONTINUE") { + my @results = (); + for my $p ( keys %resolution ) { + if ( $resolution{$p} eq "O" ) { + push @results, $p; + } + } + return @results; + } +} + +sub UI_display_profiles { + my ($explanation, $profile_hash) = @_; + my $url = $config->{url}; + my @profiles = map { [ $_, $profile_hash->{$_} ] } keys %$profile_hash; + my $title = gettext("Profiles"); + my $q = {}; + $q->{title} = $title; + $q->{headers} = [ "Repository", $url, ]; + + $q->{explanation} = $explanation; + + $q->{functions} = [ "CMD_VIEW", + "CMD_CONTINUE", ]; + + $q->{default} = "CMD_CONTINUE"; + $q->{options} = [ map { $_->[0] } @profiles ]; + $q->{selected} = 0; + + my ($ans, $arg); + do { + ($ans, $arg) = UI_PromptUser($q); + + if ($ans eq "CMD_VIEW") { + my $pager = get_pager(); + open ( PAGER, "| $pager" ) or die "Can't open $pager"; + print PAGER gettext("Profile: ") . $profiles[$arg]->[0] . "\n"; + print PAGER $profiles[$arg]->[1]; + close PAGER; + } + $q->{selected} = ($arg+1) % @profiles; + } until $ans =~ /^CMD_CONTINUE/; + return; +} + +sub UI_display_profile_conflicts { + my ($explanation, $conflict_hash) = @_; + my $url = $config->{url}; + my @conflicts = map { [ $_, + $conflict_hash->{$_}->[0], + $conflict_hash->{$_}->[1] + ] } + keys %$conflict_hash; + my @commits = []; + my $title = gettext("Profile conflicts"); + my $q = {}; + $q->{title} = $title; + $q->{headers} = [ "Repository", $url, ]; + + $q->{explanation} = $explanation; + + $q->{functions} = [ "CMD_VIEW_CHANGES", + "CMD_CONTINUE", ]; + + $q->{default} = "CMD_CONTINUE"; + $q->{options} = [ map { $_->[0] } @conflicts ]; + $q->{selected} = 0; + + my ($ans, $arg); + do { + ($ans, $arg) = UI_PromptUser($q); + + if ($ans eq "CMD_VIEW_CHANGES") { + display_changes($conflicts[$arg]->[2], $conflicts[$arg]->[1]); + } + $q->{selected} = ($arg+1) % @conflicts; + } until $ans =~ /^CMD_CONTINUE/; + return; +} + +sub usage { + if ( $help eq "push" ) { + print STDERR $usage . $usage_push ."\n"; + } elsif ( $help eq "pull" ) { + print STDERR $usage . $usage_pull ."\n"; + } elsif ( $help eq "sync" ) { + print STDERR $usage . $usage_sync ."\n"; + } elsif ( $help eq "getconfig" ) { + print STDERR $usage . $usage_getconfig ."\n"; + } elsif ( $help eq "setconfig" ) { + print STDERR $usage . $usage_setconfig ."\n"; + } elsif ( $help eq "status" ) { + print STDERR $usage . $usage_stat ."\n"; + } elsif ( $help eq "search" ) { + print STDERR $usage . $usage_search ."\n"; + } else { + open(PAGER, "| less") or die "Can't open pager"; + print PAGER $usage . + $usage_search . + $usage_push . + $usage_pull . + $usage_sync . + $usage_stat . + $usage_setconfig . + $usage_getconfig . + $usage_bottom . "\n"; + close PAGER; + } +} + +# +# --getconfig helper function +# +sub config { + my $configstr = gettext("Current config\n"); + my $config = get_repo_config(); + $configstr .= "\turl:\t\t$config->{url}\n"; + $configstr .= "\tdistro:\t\t$config->{distro}\n"; + $configstr .= "\tenabled:\t$config->{enabled}\n"; + $configstr .= "\temail:\t\t$config->{email}\n"; + $configstr .= "\tusername:\t$config->{user}\n"; + $configstr .= "\tpassword:\t$config->{password}\n"; + $configstr .= "\tupload:\t\t$config->{upload}\n"; + print STDERR $configstr . "\n"; +} + +# +# helper function to push profiles to the repository +# used by --push and --sync options +# +sub push_profiles($$$) { + my ( $p_ref, $changelog, $confirm ) = @_; + my ( $conflicts, $remote_profiles, $local_profiles, @overrides ); + my @profiles = @$p_ref; + + my $conflict_msg = +" The following profile(s) selected for upload conflicts with a profile already + stored in the repository for your account. Please choose whether to keep the + current version or overwrite it.\n"; + $all = 0; + + readprofiles(); + my ($status, $result) = fetch_profiles_by_user( $config->{url}, + $config->{distro}, + $config->{user} + ); + if ( $status ) { + $remote_profiles = $result; + } else { + print STDERR sprintf(gettext("ERROR connecting to repository: %s\n"), + $result); + exit; + } + + $all = 1 if ( grep(/^all$/, @profiles) ); + + if ( $all ) { + $local_profiles = serialize_local_profiles( \%sd ); + } else { + my $local_sd = {}; + for my $p ( @profiles ) { + if ( !$sd{$p} ) { + print STDERR + sprintf(gettext("Profile for [%s] does not exist\n"), $p); + exit; + } + $local_sd->{$p} = $sd{$p}; + } + $local_profiles = serialize_local_profiles( $local_sd ); + } + + $conflicts = find_profile_conflicts($remote_profiles, $local_profiles); + + if ( keys %$conflicts ) { + @overrides = UI_resolve_profile_conflicts( $conflict_msg, $conflicts ); + } + + if ( $local_profiles ) { + my @uploads; + for my $p ( keys %$local_profiles ) { + unless ( $conflicts->{$p} and !grep(/^$p$/, @overrides) ) { + print STDERR gettext("Uploading ") . $p . "... "; + my ($status,$result) = upload_profile( $config->{url}, + $config->{user}, + $config->{password}, + $config->{distro}, + $p, + $local_profiles->{$p}, + $changelog + ); + print STDERR gettext("done") . "\n"; + } + if ( $status ) { + push @uploads, $p; + } else { + print STDERR gettext("Error uploading") . "$p: $result\n"; + } + } + if ( @uploads ) { + # + # Currently the upload API with the repository returns the + # the current users profile set before the update so we have + # to refetch to obtain the metadata to update the local profiles + # + my $repo_p = []; + print STDERR gettext("Updating local profile metedata....\n"); + my ($status,$result) = fetch_profiles_by_user( $config->{url}, + $config->{distro}, + $config->{user} ); + if ( $status ) { + for my $p ( @uploads ) { + push( @$repo_p, [$p, $result->{$p}] ) if ( $result->{$p} ); + } + activate_repo_profiles( $config->{url}, $repo_p, 0 ); + print STDERR gettext(" done\n"); + } else { + print STDERR gettext( +"Failed to retrieve updated profiles from the repository. Error: " + ) . $result . "\n"; + } + } + } +} + +# +# Helper function for pulling profiles from the repository +# used by --pull and --sync options +# +sub pull_profiles($$$$) { + my ( $p_ref, $mode, $confirm, $opts ) = @_; + my @profiles = @$p_ref; + my ( $conflicts, $commit_list, $remote_profiles, + $local_profiles, @overrides ); + + my $conflict_msg = +" The following profiles selected for download conflict with profiles + already deployed on the system. Please choose whether to keep the local + version or overwrite with the version from the repository\n"; + + readprofiles(); + + if ( $opts->{id} ) { + my ($status,$newp) = fetch_profile_by_id( $config->{url}, $opts->{id} ); + if ( ! $status ) { + print STDERR gettext( + sprintf("Error occured during operation\n\t[%s]\n", + $newp + ) + ); + exit 1; + } else { + $remote_profiles = { $newp->{name} => $newp->{profile} }; + } + } elsif ( @profiles && $opts->{author} ) { + $remote_profiles = {}; + for my $p ( @profiles ) { + my ($status,$profiles) = + fetch_profiles_by_name_and_user( $config->{url}, + $config->{distro}, + $p, + $opts->{author} ); + if ( ! $status ) { + print STDERR gettext(sprintf( + "Error occured during operation\n\t[%s]\n", + $profiles + ) + ); + exit 1; + } else { + $remote_profiles->{$p} = $profiles->{$p}; + } + } + } elsif ( $opts->{author} ) { + my ($status,$profiles) = fetch_profiles_by_user( $config->{url}, + $config->{distro}, + $opts->{author} ); + if ( ! $status ) { + print STDERR gettext(sprintf( + "Error occured during operation\n\t[%s]\n", + $profiles + ) + ); + exit 1; + } else { + $remote_profiles = $profiles; + } + } + $local_profiles = serialize_local_profiles( \%sd ); + $conflicts = find_profile_conflicts( $remote_profiles, $local_profiles ); + if ( keys %$conflicts ) { + @overrides = UI_resolve_profile_conflicts( $conflict_msg, $conflicts ); + } + for my $p ( keys %$remote_profiles ) { + unless ( $conflicts->{$p} and !grep(/^$p$/, @overrides) ) { + $remote_profiles->{$p}->{username} = $opts->{author}; + push @$commit_list, [$p, $remote_profiles->{$p}]; + } + } + + if ( $commit_list and @$commit_list ) { + activate_repo_profiles( $config->{url}, $commit_list, $mode ); + system("rcapparmor reload"); + } else { + UI_Info(gettext("No changes to make")); + } +} + +sub UI_status { + + my ($synched, $unsynched, $local) = @_; + my $url = $config->{url}; + my $synched_text = gettext("Synchronized repository profiles:\t\t") . + keys %$synched; + my $unsynched_text = gettext("Unsynchronized repository profiles:\t") . + keys %$unsynched; + my $local_text = gettext("Local only profiles :\t\t\t") . keys %$local; + my $options = [ $synched_text, $unsynched_text, $local_text ]; + my $title = gettext("Profile Status"); + my $explanation = gettext( +" This is the current status of active profiles on the system. + To view the profiles or unsyncronized changes select VIEW\n" + ); + my $q = {}; + $q->{title} = $title; + $q->{headers} = [ "Repository", $url, ]; + $q->{explanation} = $explanation; + $q->{functions} = [ "CMD_VIEW", "CMD_FINISHED", ]; + $q->{default} = "CMD_FINISHED"; + $q->{options} = $options; + $q->{selected} = 0; + + my ($ans, $arg); + do { + ($ans, $arg) = UI_PromptUser($q); + + if ($ans eq "CMD_VIEW") { + if ( $arg == 0 ) { + UI_display_profiles( + gettext("Profiles stored in the repository"), + $synched + ); + } elsif ( $arg == 1 ) { + UI_display_profile_conflicts( + gettext("Unsyncronised profile changes"), + $unsynched + ); + } elsif ( $arg == 2 ) { + UI_display_profiles( + gettext("Profiles stored in the repository"), + $local + ); + } + } + } until $ans =~ /^CMD_FINSHED/; +} + +