晋太元中,武陵人捕鱼为业。缘溪行,忘路之远近。忽逢桃花林,夹岸数百步,中无杂树,芳草鲜美,落英缤纷。渔人甚异之,复前行,欲穷其林。 林尽水源,便得一山,山有小口,仿佛若有光。便舍船,从口入。初极狭,才通人。复行数十步,豁然开朗。土地平旷,屋舍俨然,有良田、美池、桑竹之属。阡陌交通,鸡犬相闻。其中往来种作,男女衣着,悉如外人。黄发垂髫,并怡然自乐。 见渔人,乃大惊,问所从来。具答之。便要还家,设酒杀鸡作食。村中闻有此人,咸来问讯。自云先世避秦时乱,率妻子邑人来此绝境,不复出焉,遂与外人间隔。问今是何世,乃不知有汉,无论魏晋。此人一一为具言所闻,皆叹惋。余人各复延至其家,皆出酒食。停数日,辞去。此中人语云:“不足为外人道也。”(间隔 一作:隔绝) 既出,得其船,便扶向路,处处志之。及郡下,诣太守,说如此。太守即遣人随其往,寻向所志,遂迷,不复得路。 南阳刘子骥,高尚士也,闻之,欣然规往。未果,寻病终。后遂无问津者。
| DIR:/proc/thread-self/root/proc/thread-self/root/proc/self/root/opt/cpanel/ea-podman/bin/ |
| Current File : //proc/thread-self/root/proc/thread-self/root/proc/self/root/opt/cpanel/ea-podman/bin/ea-podman.pl |
#!/usr/local/cpanel/3rdparty/bin/perl
# cpanel - ea-podman Copyright 2022 cPanel, L.L.C.
# All rights Reserved.
# copyright@cpanel.net http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
use strict;
use warnings;
package scripts::ea_podman;
BEGIN {
# I cannot get this to work using FindBin in 4 different environments, this works in all
# 4 enviroments.
#
# The environments:
#
# Testing, in the ea-podman repo dir
# Script, in /opt/cpanel/ea-podman/bin/ea-podman
# Script, in /usr/local/cpanel/scripts/ea-podman
# AdminBin, in /usr/cpanel/local/bin/admin/Cpanel
if ( -e '/opt/cpanel/ea-podman/lib' ) { # it has been installed on the machine
require '/opt/cpanel/ea-podman/lib/ea_podman/util.pm';
require '/opt/cpanel/ea-podman/lib/ea_podman/subids.pm';
}
else { # this is for testing
if ( -d 'SOURCES' ) {
require './SOURCES/util.pm';
require './SOURCES/subids.pm';
}
else {
require '/root/git/ea-podman/SOURCES/util.pm';
require '/root/git/ea-podman/SOURCES/subids.pm';
}
}
}
use Cpanel::Config::Users ();
use Cpanel::JSON ();
use Cpanel::AccessIds ();
use Whostmgr::Accounts::Shell ();
use Cpanel::Shell ();
use Term::ReadLine ();
use App::CmdDispatch ();
use Try::Tiny;
run(@ARGV) unless caller;
sub run {
my @args = @_;
local $Term::ReadLine::termcap_nowarn = 1;
my $user = getpwuid($>);
# A restricted-shell (jailshell) account cannot run rootless podman from
# inside the jail chroot. Rather than refuse, transparently route the
# supported verbs through the EAPodman UAPI: cpsrvd executes that request as
# this cpuser OUTSIDE the cage, so it "just works" the same as the CLI does
# for an unrestricted user. root and unrestricted-shell users keep the
# direct path below (and thus the full verb set). See CPANEL-54037.
if ( $> != 0 && !_has_unrestricted_shell($user) ) {
return delegate_to_uapi(@args);
}
if ( $ENV{'OPENSSL_NO_DEFAULT_ZLIB'} && $ENV{'OPENSSL_NO_DEFAULT_ZLIB'} == 1 ) {
# This is a special case where they are trying to run ea-podman from
# inside cPanel Terminal.
#
# We cannot allow it, they instead should ssh $USER@localhost and
# perform the operations.
print "You cannot run the /scripts/ea-podman script directly from the cPanel and WHM terminal.\n";
print " To use this script, you must first log in via ssh with the following command:\n";
print " ssh $user\@localhost\n\n";
exit 1;
}
# We are on the direct CLI path: only root or an unrestricted-shell user
# reaches here (restricted shells were routed to delegate_to_uapi above).
# Stay silent about the cgroup config for them; the UAPI/restricted path
# never runs through here and keeps the CloudLinux + cgroup v2 advisory.
$ea_podman::util::EMIT_CGROUP_ADVISORY = 0;
# An account can also be unreachable directly for a reason the shell check
# above can't see: CageFS. A CageFS-caged account can have an unrestricted
# shell (so it isn't routed to delegate_to_uapi above), but a real login
# still runs inside the cage's own mount namespace, which does not expose
# /run/user. The root-privileged bootstrap (ensure_user(), just above
# ensure_su_login() in init_user()) still succeeds — it runs via the
# ENSURE_USER adminbin, outside the cage — so the account ends up fully
# bootstrapped on the host while this process still can't see the result.
# ensure_su_login() (util.pm) surfaces that as a specific, distinctive die.
# Rather than inventing cage-detection, catch that exact symptom and fall
# back to the same UAPI bridge jailshell already uses (cpsrvd also runs
# outside the cage, so it can see what we just bootstrapped). See CPANEL-54672.
if ( $> != 0 ) {
my $ok = eval {
App::CmdDispatch->new( get_dispatch_args() )->run(@args);
1;
};
if ( !$ok ) {
my $err = $@;
if ( $err =~ /rootless runtime directory .* does not exist/ ) {
warn "ea-podman: could not see this account's rootless runtime directory directly " . "(typical of a CageFS-enabled account) - retrying through the EAPodman UAPI...\n";
return delegate_to_uapi(@args);
}
die $err;
}
return;
}
return App::CmdDispatch->new( get_dispatch_args() )->run(@args);
}
sub _has_unrestricted_shell {
my ($user) = @_;
if ( defined &Whostmgr::Accounts::Shell::has_unrestricted_shell ) {
return Whostmgr::Accounts::Shell::has_unrestricted_shell($user);
}
return Cpanel::Shell::has_unrestricted_shell($user);
}
#####################################
#### restricted-shell UAPI bridge ###
#####################################
#
# For a restricted-shell account the CLI cannot drive podman directly, so it
# delegates to the EAPodman UAPI over localhost HTTPS. Auth: a cpuser shell has
# no ambient cpsrvd credential, so the (root) ea-podman adminbin mints a
# short-lived API token for the caller; the CLI makes one authenticated request
# (`Authorization: cpanel user:token`) to /execute/EAPodman/<verb> and then has
# the adminbin revoke the token. See CPANEL-54037 and docs/uapi.md.
sub delegate_to_uapi {
my (@args) = @_;
# Built locally (not file-scoped) so they are populated regardless of where
# `run(@ARGV)` sits relative to a file-scope initializer.
#
# Verbs the EAPodman UAPI implements — the only ones that can be delegated.
my %uapi_verb = map { $_ => 1 } qw(install upgrade list start stop restart uninstall status cmd);
# CLI aliases (subset of the dispatcher's table) that resolve to a UAPI verb.
my %uapi_alias = (
in => 'install',
up => 'upgrade',
li => 'list',
running => 'list',
st => 'start',
sp => 'stop',
re => 'restart',
un => 'uninstall',
stat => 'status',
);
my $verb = shift(@args) // '';
$verb = $uapi_alias{$verb} if exists $uapi_alias{$verb};
my $supported = join( ", ", sort keys %uapi_verb );
# No verb (or `help`): show what a restricted account can do rather than
# erroring, so the bare `ea-podman` invocation is still friendly.
if ( $verb eq '' || $verb eq 'help' ) {
print "Your account has a restricted shell (jailshell) or CageFS, so ea-podman routes\n" . "these commands through the EAPodman UAPI: $supported.\n" . "Usage: ea-podman <" . join( "|", sort keys %uapi_verb ) . "> [args]\n";
return 1;
}
if ( !$uapi_verb{$verb} ) {
die "The “$verb” command is not available for accounts with a restricted shell (jailshell) or CageFS.\n" . "Those accounts can use: $supported.\n" . "(These route through the EAPodman UAPI, which works from inside the jail/cage; the remaining ea-podman subcommands require an unrestricted shell.)\n";
}
my $params = _cli_args_to_uapi( $verb, @args );
my $result = _uapi_call( $verb, $params );
my $status = ref($result) eq 'HASH' ? $result->{status} : undef;
if ( !$status ) {
my $errors = ref($result) eq 'HASH' ? $result->{errors} : undef;
my $msg = ( ref($errors) eq 'ARRAY' && @{$errors} ) ? join( "\n", @{$errors} ) : "EAPodman $verb failed";
die "$msg\n";
}
_render_uapi_result( $verb, ref($result) eq 'HASH' ? $result->{data} : undef );
return 1;
}
# Translate the CLI argv for a verb into UAPI key/value params. Mirrors the
# reverse mapping in Cpanel::API::EAPodman (cpuser_port/env/risk-flag + image).
sub _cli_args_to_uapi {
my ( $verb, @args ) = @_;
return {} if $verb eq 'list';
if ( $verb eq 'install' ) {
my %p;
my ( @ports, @envs, @positional );
for ( my $i = 0; $i < @args; $i++ ) {
my $a = $args[$i];
if ( $a =~ /^--cpuser-port=(.+)$/ ) {
push @ports, $1;
}
elsif ( $a =~ /^(?:-e|--env)=(.+)$/ ) {
push @envs, $1;
}
elsif ( ( $a eq '-e' || $a eq '--env' ) && defined $args[ $i + 1 ] ) {
push @envs, $args[ ++$i ];
}
elsif ( $a eq '--i-understand-the-risks-do-it-anyway' ) {
$p{accept_arbitrary_image_risk} = 1;
}
else {
push @positional, $a;
}
}
die "install requires a package or container name\n" if !@positional;
$p{name} = shift @positional;
$p{image} = pop @positional if @positional; # non-package form: trailing IMAGE
$p{cpuser_port} = \@ports if @ports;
$p{env} = \@envs if @envs;
return \%p;
}
if ( $verb eq 'cmd' ) {
my ( $container_name, $cd, @cmd_argv ) = _parse_cmd_args(@args);
my %p = ( container_name => $container_name, arg => \@cmd_argv );
$p{cd} = $cd if length( $cd // '' );
return \%p;
}
# upgrade / start / stop / restart / uninstall / status: a single
# container_name positional (ignore the CLI's --verify; UAPI uninstall has
# no interactive gate).
my ($container_name) = grep { defined && length && $_ ne '--verify' } @args;
die "$verb requires a container name\n" if !defined $container_name;
return { container_name => $container_name };
}
# Shared by the direct-CLI `cmd` verb and its UAPI delegation: parses
# `<CONTAINER_NAME> [--cd DIR] -- <CMD> [ARGS...]`. The `--` is mandatory so
# ea-podman's own flags can never be confused with the exec'd command's own
# argv (which may legitimately contain "--cd" or "--" tokens of its own).
sub _parse_cmd_args {
my (@args) = @_;
my $container_name = shift @args;
die "cmd requires a container name\n" if !length( $container_name // '' );
my $cd;
if ( @args && $args[0] eq '--cd' ) {
shift @args;
$cd = shift @args;
die "--cd requires a directory\n" if !length( $cd // '' );
}
die "cmd requires a “--” before the command, e.g. cmd <CONTAINER_NAME> [--cd DIR] -- <CMD> [ARGS...]\n"
if !@args || $args[0] ne '--';
shift @args;
die "cmd requires a command to run after “--”\n" if !@args;
return ( $container_name, $cd, @args );
}
sub _uapi_call {
my ( $verb, $params ) = @_;
require Cpanel::AdminBin::Call;
# A shell login has no ambient cpsrvd credential, so the (root) adminbin
# mints a short-lived full-access API token for us; we authenticate the one
# UAPI request with it (`Authorization: cpanel user:token`) and revoke it
# immediately after, success or not. See CPANEL-54037.
my $cred = Cpanel::AdminBin::Call::call( 'Cpanel', 'ea_podman', 'MINT_API_TOKEN' );
die "Could not obtain an API token to reach the EAPodman UAPI\n"
if ref($cred) ne 'HASH' || !$cred->{token};
my $user = scalar getpwuid($>);
require HTTP::Tiny;
my $http = HTTP::Tiny->new( verify_SSL => 0, timeout => 120 ); # localhost cert; installs can pull an image
my $path = "/execute/EAPodman/$verb";
my $qs = _uapi_query_string($params);
my $resp = $http->get(
"https://127.0.0.1:2083$path" . ( length $qs ? "?$qs" : "" ),
{ headers => { 'Authorization' => "cpanel $user:$cred->{token}" } },
);
eval { Cpanel::AdminBin::Call::call( 'Cpanel', 'ea_podman', 'REVOKE_API_TOKEN', $cred->{name} ); 1 };
if ( !$resp->{success} ) {
die "EAPodman UAPI request failed: $resp->{status} $resp->{reason} ($path)\n" . _http_snippet($resp);
}
my $decoded = eval { Cpanel::JSON::Load( $resp->{content} ) };
if ( !$decoded ) {
die "Could not parse the EAPodman UAPI response (status $resp->{status}, " . "content-type " . ( $resp->{headers}{'content-type'} // '?' ) . "):\n" . _http_snippet($resp);
}
return $decoded->{result} // $decoded;
}
# A short, single-line excerpt of a response body, for legible error messages.
sub _http_snippet {
my ($resp) = @_;
my $body = $resp->{content} // '';
$body =~ s/\s+/ /g;
$body =~ s/^\s+//;
return length($body) > 300 ? substr( $body, 0, 300 ) . " …\n" : "$body\n";
}
sub _uapi_query_string {
my ($params) = @_;
require Cpanel::Encoder::URI;
# A repeatable arg is just the same name given more than once
# (key=a&key=b&key=c). Cpanel::Form stores the duplicates and
# $args->get_multiple() recovers them in order, so there is no need to
# number them key-1, key-2, … ourselves.
my @pairs;
for my $key ( sort keys %{$params} ) {
my $val = $params->{$key};
for my $v ( ref($val) eq 'ARRAY' ? @{$val} : $val ) {
push @pairs, Cpanel::Encoder::URI::uri_encode_str($key) . '=' . Cpanel::Encoder::URI::uri_encode_str($v);
}
}
return join( '&', @pairs );
}
sub _render_uapi_result {
my ( $verb, $data ) = @_;
if ( $verb eq 'install' ) {
my $name = ref($data) eq 'HASH' ? $data->{container_name} : undef;
print "Done, installed: " . ( $name // '?' ) . "\n";
}
elsif ( $verb eq 'list' || $verb eq 'status' ) {
print Cpanel::JSON::pretty_canonical_dump( $data || {} );
}
elsif ( $verb eq 'cmd' ) {
my $exit_code = ref($data) eq 'HASH' ? $data->{exit_code} // 0 : 0;
print STDOUT ref($data) eq 'HASH' ? $data->{stdout} // '' : '';
print STDERR ref($data) eq 'HASH' ? $data->{stderr} // '' : '';
exit($exit_code);
}
else { # upgrade / start / stop / restart / uninstall
print "Done: $verb\n";
}
return;
}
sub get_dispatch_args {
my $hint_blurb = "This tool supports the following commands (i.e. $0 {command} …):";
my %opts = (
'default_commands' => 'help', # shell is probably not useful here and potentially confusing
'help:pre_hint' => $hint_blurb,
'help:pre_help' => "Various EA4 user-container based service/app/etc management\n\n$hint_blurb",
alias => { stat => "status", in => "install", up => "upgrade", un => "uninstall", li => "list", re => "restart", st => "start", sp => "stop", sid => "subids", si => "subids", registered => "containers", running => "list", available => "avail", av => "avail" },
);
if ( $> == 0 ) {
$opts{"help:post_help"} = "To manage containers for a user use `su - USER -c '$0 …'` or similar.";
$opts{"help:post_hint"} = $opts{"help:post_help"};
}
my %cmds = (
testbin => {
clue => "testbin",
abstract => "Verify binary is stable",
help => "If it exits clean the binary is ok. If it exits unclean it should be recompiled with `/opt/cpanel/ea-podman/bin/compile.sh`",
code => sub {
printf "$0 is running under perl v%vd\n", $^V;
},
},
subids => {
clue => "subids [--ensure]",
abstract => "Check and report on sub id config",
help => "Checks that use name spaces are enabled or not and if so what sub uids and sub gids are allocated\nOptional --ensure flag, makes sure the subids are setup for this user.",
code => sub {
my ( $app, @other_args ) = @_;
ea_podman::util::init_user();
subids( $app, @other_args );
},
},
install => {
clue => "install <PKG> [`run` flags]|install <NON-PKG-NAME> [--cpuser-port=<CONTAINER PORT|0> [--cpuser-port=<ANOTHER CONTAINER PORT|0> …]] [`run` flags] <IMAGE>",
abstract => "Install a container",
help => "Has two modes:\n\t<PKG> - An EA4 container based package.\n\t\tNeeds no other arguments or setup as that is all provided by the package. It can take some additional start up arguments.\n\t<NON-PKG-NAME> - manage an arbitrary image as if it where an EA4 container based package.\n\t\tSee https://github.com/CpanelInc/ea-podman/blob/master/README.md for details",
code => sub {
my ( $app, $name, @start_args ) = @_;
# The one verb that legitimately needs a rootless session for an
# account with no containers yet.
ea_podman::util::init_user( creating => 1 );
my $container_name = ea_podman::util::install_container( $name, @start_args );
print "Done, installed: $container_name\n";
},
},
upgrade => {
clue => "upgrade <CONTAINER_NAME>",
abstract => "Upgrade a container",
help => "Upgrade the container named CONTAINER_NAME",
code => sub {
my ( $app, $container_name ) = @_;
ea_podman::util::init_user();
ea_podman::util::upgrade_container($container_name);
},
},
uninstall => {
clue => "uninstall <CONTAINER_NAME>",
abstract => "Uninstall a container",
help => "Uninstall the container named CONTAINER_NAME",
code => sub {
my ( $app, $container_name, $verify ) = @_;
if ( !length($verify) || $verify ne "--verify" ) {
print "This operation can not be undone! Please pass `--verify` to verify you really want to do this.\n";
return;
}
ea_podman::util::init_user();
ea_podman::util::remove_container_by_name($container_name);
},
},
list => {
clue => "list",
abstract => "Show container information",
help => "Dumps the information about user’s running containers in human readable JSON",
code => sub {
my ($app) = @_;
ea_podman::util::init_user();
print Cpanel::JSON::pretty_canonical_dump( ea_podman::util::get_containers() );
},
},
start => {
clue => "start <CONTAINER_NAME>",
abstract => "Start a container",
help => "Start the container named CONTAINER_NAME",
code => sub {
my ( $app, $container_name ) = @_;
ea_podman::util::validate_user_container_name($container_name);
ea_podman::util::init_user();
my $service_name = ea_podman::util::get_container_service_name($container_name);
# else a container that used up its restarts cannot start for five minutes
ea_podman::util::reset_container_unit_failure($container_name);
ea_podman::util::sysctl( start => $service_name );
}
},
stop => {
clue => "stop <CONTAINER_NAME>",
abstract => "Stop a container",
help => "Stop the container named CONTAINER_NAME",
code => sub {
my ( $app, $container_name ) = @_;
ea_podman::util::validate_user_container_name($container_name);
ea_podman::util::init_user();
my $service_name = ea_podman::util::get_container_service_name($container_name);
ea_podman::util::sysctl( stop => $service_name );
# a PID 1 that ignores SIGTERM is SIGKILLed and exits 137, recorded as a failure
ea_podman::util::reset_container_unit_failure($container_name);
}
},
restart => {
clue => "restart <CONTAINER_NAME>",
abstract => "Restart a container",
help => "Restart the container named CONTAINER_NAME",
code => sub {
my ( $app, $container_name ) = @_;
ea_podman::util::validate_user_container_name($container_name);
ea_podman::util::init_user();
my $service_name = ea_podman::util::get_container_service_name($container_name);
ea_podman::util::reset_container_unit_failure($container_name);
ea_podman::util::sysctl( restart => $service_name );
}
},
status => {
clue => "status <CONTAINER_NAME>",
abstract => "Get status of a container",
help => "Get status of the container named CONTAINER_NAME",
code => sub {
my ( $app, $container_name ) = @_;
ea_podman::util::validate_user_container_name($container_name);
ea_podman::util::init_user();
my $service_name = ea_podman::util::get_container_service_name($container_name);
ea_podman::util::sysctl( status => $service_name );
}
},
bash => {
clue => "bash <CONTAINER_NAME> [CMD]",
abstract => "get into a shell/run commands inside the container",
help => "If the container has bash: get an interactive shell inside the container, or run the (optional) CMD. Interactive access needs a TTY, so it is only for root and unrestricted-shell accounts. For a non-interactive command that also works for jailshell/CageFS accounts and on hidepid=2 hosts, use `cmd`.",
code => sub {
my ( $app, $container_name, $cmd ) = @_;
ea_podman::util::validate_user_container_name($container_name);
ea_podman::util::init_user();
if ($cmd) {
ea_podman::util::podman( exec => "-it", $container_name, "/bin/bash", "-c" => $cmd );
}
else {
ea_podman::util::podman( exec => "-it", $container_name, "/bin/bash" );
}
},
},
cmd => {
clue => "cmd <CONTAINER_NAME> [--cd DIR] -- <CMD> [ARGS...]",
abstract => "run a one-shot, non-interactive command inside the container",
help => "Runs CMD (with ARGS) directly inside the container (no shell, no TTY) and prints its stdout/stderr, exiting with its exit code. Does not assume the container has bash. Optional --cd DIR runs the command from that working directory. Works even where /proc is mounted hidepid=2, and is available to restricted-shell (jailshell) and CageFS accounts through the EAPodman UAPI.",
code => sub {
my ( $app, @args ) = @_;
my ( $container_name, $cd, @cmd_argv ) = _parse_cmd_args(@args);
ea_podman::util::validate_user_container_name($container_name);
ea_podman::util::init_user();
my $state = ea_podman::util::exec_in_container( $container_name, \@cmd_argv, cd => $cd );
print STDOUT $state->{stdout} // '';
print STDERR $state->{stderr} // '';
exit( $state->{exit_code} // 0 );
},
},
containers => {
clue => "containers [--all]",
abstract => "List containers",
help => "List your ea-podman registered containers. root can additionally pass --all to list everyone’s ea-podman registered containers.",
code => sub {
my ( $app, $all ) = @_;
die "Unknown argument “$all”\n" if defined $all && $all ne '--all';
my $user = getpwuid($>);
ea_podman::util::init_user();
my $containers_hr = ea_podman::util::load_known_containers();
my %user_containers;
foreach my $container ( grep { $_->{user} eq $user } values %{$containers_hr} ) {
$user_containers{ $container->{container_name} } = $container;
}
if ( $> == 0 ) {
if ($all) {
print Cpanel::JSON::pretty_canonical_dump($containers_hr);
}
else {
print Cpanel::JSON::pretty_canonical_dump( \%user_containers );
}
}
else {
if ($all) {
die "Only root can specifically --all\n";
}
else {
print Cpanel::JSON::pretty_canonical_dump( \%user_containers );
}
}
},
},
remove_containers => {
clue => "remove_containers [<PKG|NON-PKG-NAME>|--all]",
abstract => "Remove containers",
help => qq{Remove ea-podman registered containers by EA4 package, an arbitrary non-package name, or all via `--all`.
- as non-root will only affect only the user
- as root this will effect all users
This is intended to make it easier for a user to purge their ea-podman based containers and to facilitate cleanup when uninstalling packages that the containers need to run.
},
code => sub {
my ( $app, $pkg ) = @_;
die "Please provide a package name or the flag `--all`\n" if ( !$pkg );
ea_podman::util::init_user();
# TODO ZC-9746: have them verify they want to do this destructive thing
my $user = getpwuid($>);
my $containers_hr = ea_podman::util::load_known_containers();
my @containers = values %{$containers_hr};
@containers = grep { $_->{user} eq $user } @containers if ( $user ne "root" );
if ( $pkg ne '--all' ) {
@containers = grep {
( defined $_->{pkg} && $_->{pkg} eq $pkg ) # <PKG> form …
|| # … OR …
( !defined $_->{pkg} && $_->{container_name} =~ m/^\Q$pkg\E\.$user\.[0-9][0-9]$/ ) # … <NON-PKG-NAME> form
} @containers;
}
@containers = sort { $a->{user} cmp $b->{user} } @containers;
if ( @containers == 0 ) {
print "There are no containers\n";
exit 0;
}
if ( $user eq "root" ) {
my %user_breakdown;
foreach my $container (@containers) {
my $c_user = $container->{user};
push( @{ $user_breakdown{$c_user} }, $container );
}
foreach my $c_user ( keys %user_breakdown ) {
if ( $c_user eq "root" ) {
ea_podman::util::remove_containers_for_a_user( @{ $user_breakdown{$c_user} } );
}
else {
try {
Cpanel::AccessIds::do_as_user_with_exception(
$c_user,
sub {
my $homedir = ( getpwuid($>) )[7];
local $ENV{HOME} = $homedir;
local $ENV{USER} = $c_user;
chdir($homedir);
ea_podman::util::init_user();
ea_podman::util::remove_containers_for_a_user( @{ $user_breakdown{$c_user} } );
}
);
}
catch {
my $err = $_;
# Handles cases where users are not removed cleanly (with the use of a cPanel script/API), therefore it tries to manage containers as the deleted user
# which causes unistall of containerized packages to fail (see ZC-10958)
if ( $err->isa("Cpanel::Exception::UserNotFound") ) {
ea_podman::util::remove_containers_for_a_deleted_user( @{ $user_breakdown{$c_user} } );
return;
}
# Rethrow any other exception type
die $err;
};
}
}
}
else {
ea_podman::util::init_user();
ea_podman::util::remove_containers_for_a_user(@containers);
}
},
},
upgrade_containers => {
clue => "upgrade_containers [<PKG|NON-PKG-NAME>|--all]",
abstract => "Upgrade containers",
help => qq{Upgrade ea-podman registered containers by EA4 package, an arbitrary non-package name, or all via `--all`.
- as non-root will only affect only the user
- as root this will effect all users
},
code => sub {
my ( $app, $pkg ) = @_;
die "Please provide a package name or the flag `--all`\n" if ( !$pkg );
my $user = getpwuid($>);
my $containers_hr = ea_podman::util::load_known_containers();
my @containers = values %{$containers_hr};
@containers = grep { $_->{user} eq $user } @containers if ( $user ne "root" );
if ( $pkg ne '--all' ) {
@containers = grep {
( defined $_->{pkg} && $_->{pkg} eq $pkg ) # <PKG> form …
|| # … OR …
( !defined $_->{pkg} && $_->{container_name} =~ m/^\Q$pkg\E\.$user\.[0-9][0-9]$/ ) # … <NON-PKG-NAME> form
} @containers;
}
@containers = sort { $a->{user} cmp $b->{user} } @containers;
if ( @containers == 0 ) {
print "There are no containers\n";
exit 0;
}
if ( $user eq "root" ) {
my %user_breakdown;
foreach my $container (@containers) {
my $c_user = $container->{user};
push( @{ $user_breakdown{$c_user} }, $container );
}
foreach my $c_user ( keys %user_breakdown ) {
if ( $c_user eq "root" ) {
ea_podman::util::upgrade_containers_for_a_user( @{ $user_breakdown{$c_user} } );
}
else {
Cpanel::AccessIds::do_as_user_with_exception(
$c_user,
sub {
my $homedir = ( getpwuid($>) )[7];
local $ENV{HOME} = $homedir;
local $ENV{USER} = $c_user;
chdir($homedir);
ea_podman::util::init_user();
ea_podman::util::upgrade_containers_for_a_user( @{ $user_breakdown{$c_user} } );
}
);
}
}
}
else {
ea_podman::util::init_user();
ea_podman::util::upgrade_containers_for_a_user(@containers);
}
},
},
backup => {
clue => "backup",
abstract => "Backup containers",
help => qq{Backup all ea-podman registered containers for a user.
Outputs a file ea_podman_backup_<USER>.json
},
code => sub {
my ($app) = @_;
die "Backup is not allowed for the root user at this time.\n" if ( $> == 0 );
ea_podman::util::perform_user_backup();
},
},
restore => {
clue => "restore <BACKUP_FILE_PATH> [--verify]",
abstract => "Restore containers that have been backed up.",
help => qq{Will restore containers that bave been backed up.
NOTE:
* Will remove existing containers
* Will destroy the ea-podman.d directory
* This is a destructive operation, you are required to pass ”--verify”
},
code => sub {
my ( $app, $backup_file, $verify ) = @_;
die "Restore is not allowed for the root user at this time.\n" if ( $> == 0 );
die "Please pass in the path to the backup file you want to restore.\n" if ( !$backup_file );
die "Backup file cannot be read\n" if ( !-r $backup_file );
if ( !length($verify) || $verify ne "--verify" ) {
print "This operation can not be undone! Please pass `--verify` to verify you really want to do this.\n";
return;
}
ea_podman::util::perform_user_restore($backup_file);
},
},
avail => {
clue => "avail",
abstract => "list available EA4 container based packages",
help => "lists available EA4 container based packages and, for each one, shows if its installed locally or not and has a URL to its documentation",
code => sub {
my ($app) = @_;
my $e4m = Cpanel::JSON::LoadFile("/etc/cpanel/ea4/ea4-metainfo.json");
if ( !exists $e4m->{container_based_packages} ) {
die "Container based packages list not found (need to upgrade ea-cpanel-tools?)\n";
}
my %avail;
for my $pkg ( @{ $e4m->{container_based_packages} } ) {
$avail{$pkg}{installed_locally} = -e "/opt/cpanel/$pkg/pkg-version" ? 1 : 0;
$avail{$pkg}{url} = "https://github.com/CpanelInc/$pkg/blob/master/SOURCES/README.md";
}
print Cpanel::JSON::pretty_canonical_dump( \%avail );
return 1;
},
},
ensure_user_sessions => {
clue => "ensure_user_sessions [--quiet]",
abstract => "Bring up the per-user systemd managers every account with containers needs",
help =>
"root only. For each account the container registry says has containers, makes sure it lingers and that its user systemd manager is running — lifting a masked `user\@.service` for as long as that takes, exactly as any other ea-podman command does.\n\nRun at boot by ea-podman-user-managers.service. Nothing else starts these managers at boot on a host where `user\@.service` is masked (CageFS 7.6.39+ / CloudLinux CLOS-4517), so without it an account's containers stay down until its next ea-podman command. Safe and near-free to run by hand at any time: an account whose manager is already up is skipped without opening a window.\n\nReports what it did per account and exits non-zero if any account's manager could not be started.\n\n--quiet prints only failures, for unattended runs.",
code => sub {
my ( $app, @other_args ) = @_;
die "ensure_user_sessions can only be run by root\n" if $> != 0;
my $quiet = 0;
for my $arg (@other_args) {
if ( $arg eq "--quiet" ) { $quiet = 1 }
else { die "Unknown argument “$arg”\n" }
}
my @users = ea_podman::util::users_with_containers_as_root();
if ( !@users ) {
print "No accounts have ea-podman containers, so there are no user systemd managers to start.\n" if !$quiet;
return 1;
}
my $result = ea_podman::subids::ensure_user_sessions(@users);
# The per-account warnings have already gone to STDERR from
# ensure_user_sessions(); this is the summary.
my @failed = sort grep { $result->{$_} eq "failed" } keys %{$result};
if ( !$quiet ) {
for my $user ( sort keys %{$result} ) {
print "$user: $result->{$user}\n";
}
}
if (@failed) {
warn "ea-podman: could not start the user systemd manager for: " . join( ", ", @failed ) . "\n";
exit 1;
}
return 1;
},
},
rootbackupofuser => {
clue => "rootbackupofuser - internal use only",
abstract => "internal use only",
help => "internal use only",
code => sub {
my ( $app, $user ) = @_;
die "rootbackupofuser can only be run by root\n" if $> != 0;
# The same guard as the PkgAcct hook that calls us, since this
# is a reachable entry point in its own right: do not stand up a
# rootless session (subids + linger) for an account that has no
# containers to back up. (CPANEL-55309)
return 1 if !ea_podman::util::user_has_containers_as_root($user);
require Cpanel::AccessIds;
Cpanel::AccessIds::do_as_user_with_exception(
$user,
sub {
my $homedir = ( getpwuid($<) )[7];
local $ENV{HOME} = $homedir;
local $ENV{USER} = $user;
chdir($homedir);
ea_podman::util::init_user();
ea_podman::util::perform_user_backup();
}
);
return 1;
},
},
);
return ( \%cmds, \%opts );
}
####################
#### sub commands ##
####################
sub subids {
my ( $app, @other ) = @_;
ea_podman::subids::assert_has_user_namespaces(1);
if ( @other == 1 && $other[0] eq "--ensure" ) {
ea_podman::util::ensure_user(); # do not need init_user because we don’t care aboue the su login stuff here
}
else {
die "Too many arguments" if @other > 1;
die "--ensure is the only allowed argument" if @other && $other[0] ne "--ensure";
}
my $subuid_lu = ea_podman::subids::get_subuids();
my $subgid_lu = ea_podman::subids::get_subgids();
# Having a range and having one nobody else has are different things, and
# only the second is isolation. Worked out once for the whole file.
my $subuid_problems = ea_podman::subids::get_subuid_problems();
my $subgid_problems = ea_podman::subids::get_subgid_problems();
if ( $> == 0 ) {
for my $user ( "root", Cpanel::Config::Users::getcpusers() ) {
_check_output_user( $user, $subuid_lu, $subgid_lu, $subuid_problems, $subgid_problems );
}
}
else {
my $user = getpwuid($>);
_check_output_user( $user, $subuid_lu, $subgid_lu, $subuid_problems, $subgid_problems );
}
}
sub _check_output_user {
my ( $user, $subuid_lu, $subgid_lu, $subuid_problems, $subgid_problems ) = @_;
_output_user_range( $user, "subuids", $subuid_lu, $subuid_problems );
_output_user_range( $user, "subgids", $subgid_lu, $subgid_problems );
}
sub _output_user_range {
my ( $user, $label, $lu, $problems ) = @_;
if ( !exists $lu->{$user} ) {
print "$ea_podman::subids::bad “$user” does not have $label\n";
return;
}
# ea-podman refuses to run this account’s containers while its range is not
# exclusively its own, so that does not get a checkmark here either.
if ( my $problem = $problems->{$user} ) {
print "$ea_podman::subids::bad “$user” has $label ($lu->{$user}) but $problem\n";
return;
}
print "$ea_podman::subids::good “$user” has $label ($lu->{$user})\n";
}
1;
|