Init
Signed-off-by: Georg <georg@lysergic.dev>
This commit is contained in:
commit
c27753da86
1
README.md
Normal file
1
README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
Hosts configurations related to our POC shell service.
|
13
dockersh/install-custom.sh
Normal file
13
dockersh/install-custom.sh
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Original by https://github.com/sleeepyjack/dockersh
|
||||||
|
# Modified by georg@lysergic.dev
|
||||||
|
|
||||||
|
pip3 install --upgrade -r requirements.txt
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
activate-global-python-argcomplete
|
||||||
|
else
|
||||||
|
activate-global-python-argcomplete --dest=$1
|
||||||
|
fi
|
||||||
|
cp dockersh /opt/dockersh/bin/
|
||||||
|
chmod +x /opt/dockersh/bin/dockersh
|
||||||
|
cp -n dockersh.ini /opt/dockersh/etc/
|
29
etc/dockersh.ini
Normal file
29
etc/dockersh.ini
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[ADMIN]
|
||||||
|
command = admin
|
||||||
|
shell = /bin/bash
|
||||||
|
names =
|
||||||
|
cranberry-adm
|
||||||
|
mogad0n-adm
|
||||||
|
maintenance = off
|
||||||
|
maintenance_scp = on
|
||||||
|
maintenance_text = This Maschine is in Maintanence Mode. However, you can copy files with `scp`, `rsync`, `sftp` or list files with `ls` without connecting to the maschine. I.e.
|
||||||
|
ssh ${HOSTNAME} ls -la
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
image = ubuntu_user
|
||||||
|
suffix = _${USER}
|
||||||
|
homedir = /home/${USER}
|
||||||
|
shell = /bin/bash
|
||||||
|
greeting = Ready.
|
||||||
|
|
||||||
|
[test01]
|
||||||
|
image = test01
|
||||||
|
|
||||||
|
[test02]
|
||||||
|
image = test02
|
||||||
|
|
||||||
|
[test03]
|
||||||
|
image = test03
|
||||||
|
|
||||||
|
[cranberry-test]
|
||||||
|
image = cranberry-test
|
23
etc/motd
Normal file
23
etc/motd
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#################################################################
|
||||||
|
## ##
|
||||||
|
## --- https://liberta.casa --- ##
|
||||||
|
## ##
|
||||||
|
## - This is an EXPERIMENTAL, Proof Of Concept, environment. ##
|
||||||
|
## - You are being offered an isolated container which ##
|
||||||
|
## you are free to experiment in. ##
|
||||||
|
## - Sudo is not configured by default, but you may ##
|
||||||
|
## use root password ` freedom `. ##
|
||||||
|
## - Containers stay running/online for a while ##
|
||||||
|
## after `exit`ing. ##
|
||||||
|
## - The memory shown in common CLI tools is NOT accurate - ##
|
||||||
|
## if you exceed 512MB, processes may get killed. ##
|
||||||
|
## - At this stage, shells may be reset at any time. ##
|
||||||
|
## - If you encounter issues, please let us know. ##
|
||||||
|
## ##
|
||||||
|
## For your sanity: ##
|
||||||
|
## - Do NOT consider this a secure environment. ##
|
||||||
|
## - Do NOT consider this a reliable environment. ##
|
||||||
|
## - Be considerate, don't waste other users resources. ##
|
||||||
|
## - Help: Type ` help.sh ` or join #help on irc.liberta.casa ##
|
||||||
|
## ##
|
||||||
|
#################################################################
|
2
etc/sshd/sshd-banner
Normal file
2
etc/sshd/sshd-banner
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
You are connecting to the LibertaCasa public shell infrastructure.
|
||||||
|
If you do not have an account yet, please request one in #libcasa.info on irc.liberta.casa (Webchat: https://liberta.casa/gamja).
|
4
lcpubsh/bin/adduser.sh
Normal file
4
lcpubsh/bin/adduser.sh
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# georg@lysergic.dev
|
||||||
|
# The filename is flawed, however changing it would likely break scripts in three other places.
|
||||||
|
echo "$1:$2" | chpasswd
|
48
lcpubsh/bin/generate.sh
Normal file
48
lcpubsh/bin/generate.sh
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# georg@lysergic.dev
|
||||||
|
set -e
|
||||||
|
echo "Shell generation invoked." | nc -N 127.0.0.2 2424
|
||||||
|
if [ ! "$#" -eq 0 ]; then
|
||||||
|
user="$(echo "$1" |tr '[:upper:]' '[:lower:]')"
|
||||||
|
case "$2" in
|
||||||
|
"archlinux")
|
||||||
|
os="archlinux"
|
||||||
|
image="lc-archlinux-userbase-v2:sh0"
|
||||||
|
;;
|
||||||
|
"ubuntu")
|
||||||
|
os="ubuntu"
|
||||||
|
image="lcbase_ubuntu_14082021_2:sh0"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Choose between archlinux or ubuntu"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fingerprint_ecdsa="$(ssh-keygen -lf /etc/ssh/ssh_host_ecdsa_key.pub)"
|
||||||
|
if id "$1" &>/dev/null; then
|
||||||
|
echo "Aborted. Username is already taken."
|
||||||
|
echo "Aborted: $user is already taken." | nc -N 127.0.0.2 2424
|
||||||
|
else
|
||||||
|
echo "Hang on ..."
|
||||||
|
echo "Creating $user locally." | nc -N 127.0.0.2 2424
|
||||||
|
sudo useradd -mUs /opt/lcpubsh/bin/pubsh -G docker $user
|
||||||
|
pass=$(shuf -n2 /usr/share/dict/words | tr -d '\n')
|
||||||
|
echo "Appending to config." | nc -N 127.0.0.2 2424
|
||||||
|
echo "" >> /etc/dockersh.ini
|
||||||
|
echo "[$user]" >> /etc/dockersh.ini
|
||||||
|
echo "image = $user" >> /etc/dockersh.ini
|
||||||
|
echo "Forking Docker base image ($image)." | nc -N 127.0.0.2 2424
|
||||||
|
/opt/lcpubsh/bin/make_lc_user_image.sh $user $image | nc -N 127.0.0.2 2424
|
||||||
|
echo "Setting password." | nc -N 127.0.0.2 2424
|
||||||
|
sudo /opt/adduser.sh $user $pass
|
||||||
|
echo "@$user ssh -p 2222 $user@sh.lib.casa" | nc -N 127.0.0.2 2424
|
||||||
|
echo "@$user $fingerprint_ecdsa" | nc -N 127.0.0.2 2424
|
||||||
|
echo "@$user $pass" | nc -N 127.0.0.2 2424
|
||||||
|
echo "#universe $pass" | nc -N 127.0.0.2 2424
|
||||||
|
echo "Done." | nc -N 127.0.0.2 2424
|
||||||
|
echo "OK. Details sent to user and/or admins."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No argument supplied."
|
||||||
|
fi
|
||||||
|
|
12
lcpubsh/bin/make_lc_user_image.sh
Normal file
12
lcpubsh/bin/make_lc_user_image.sh
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Original by https://github.com/sleeepyjack/dockersh
|
||||||
|
# Modified by georg@lysergic.dev
|
||||||
|
|
||||||
|
if [ -z "$1" -o -z "$2" ]; then
|
||||||
|
echo "./make_user_image.sh [name] [source-image]"; exit 100
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i "1s/.*/FROM $2/" /opt/dockersh/dockersh-git/image_template/Dockerfile
|
||||||
|
cd /opt/dockersh/dockersh-git/image_template
|
||||||
|
docker build -t $1:sh0 .
|
||||||
|
|
274
lcpubsh/bin/pubsh
Executable file
274
lcpubsh/bin/pubsh
Executable file
@ -0,0 +1,274 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# PYTHON_ARGCOMPLETE_OK
|
||||||
|
# Original by https://github.com/sleeepyjack/dockersh
|
||||||
|
# Modified by georg@lysergic.dev
|
||||||
|
# POC / IN DEVELOPMENT
|
||||||
|
# Do NOT use this in insecure environments
|
||||||
|
import os
|
||||||
|
os.environ['TERM'] = 'xterm' # removes warning on non-tty commands
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
#import argcomplete
|
||||||
|
#from argcomplete.completers import ChoicesCompleter
|
||||||
|
from configparser import ConfigParser, ExtendedInterpolation
|
||||||
|
import docker
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
from pwd import getpwnam
|
||||||
|
import socket
|
||||||
|
import os
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
prog = 'dockersh'
|
||||||
|
version = prog + " v1.0"
|
||||||
|
config_file = "/etc/dockersh.ini"
|
||||||
|
|
||||||
|
user = getpass.getuser()
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
|
||||||
|
cli = docker.APIClient()
|
||||||
|
|
||||||
|
def containers(image_filter='', container_filter='', sort_by='Created', all=True):
|
||||||
|
cs = cli.containers(all=all, filters={'label': "user="+user})
|
||||||
|
cs.sort(key=lambda c: c[sort_by])
|
||||||
|
cs = [c for c in cs if str(c['Image']+':sh0').startswith(image_filter)]
|
||||||
|
cs = [c for c in cs if c['Names'][0][1:].startswith(container_filter)]
|
||||||
|
return cs
|
||||||
|
|
||||||
|
def random_string(length):
|
||||||
|
def random_char():
|
||||||
|
return random.choice(string.ascii_uppercase + string.digits)
|
||||||
|
return ''.join(random_char() for _ in range(length))
|
||||||
|
|
||||||
|
def strip(s, suffix=''):
|
||||||
|
for c in ['/', ':', '.', ' ']: #QUESTION does this suffice?
|
||||||
|
s = s.replace(c, '')
|
||||||
|
if s.endswith(suffix):
|
||||||
|
s = s[:len(s)-len(suffix)]
|
||||||
|
return s
|
||||||
|
|
||||||
|
def pull(image):
|
||||||
|
if not image in image_names:
|
||||||
|
s = image.split(':')
|
||||||
|
if len(s) > 1:
|
||||||
|
cli.pull(s[0], s[1])
|
||||||
|
else:
|
||||||
|
cli.pull(s[0])
|
||||||
|
|
||||||
|
def image_split(s):
|
||||||
|
sp = s.split(':')
|
||||||
|
if len(sp) == 1:
|
||||||
|
return sp[0], 'sh0'
|
||||||
|
else:
|
||||||
|
return sp[0], sp[1]
|
||||||
|
|
||||||
|
def selection_menu(choices):
|
||||||
|
if len(choices) == 1:
|
||||||
|
return 0
|
||||||
|
print("There are multiple matching containers running:")
|
||||||
|
for j, c in enumerate(choices):
|
||||||
|
print("[" + str(j+1) + "]\t" + c)
|
||||||
|
inp = input("select [1]: ")
|
||||||
|
if inp == "":
|
||||||
|
i = 0
|
||||||
|
else:
|
||||||
|
i = int(inp) - 1
|
||||||
|
assert(0 <= i < len(choices))
|
||||||
|
return i
|
||||||
|
|
||||||
|
#if __name__ == "__main__":
|
||||||
|
def parse_args():
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(prog=prog)
|
||||||
|
parser.add_argument('--version',
|
||||||
|
action='version',
|
||||||
|
version=version)
|
||||||
|
parser.add_argument('-i', '--image',
|
||||||
|
dest='image',
|
||||||
|
help="base image to be used",
|
||||||
|
default="") #.completer = ChoicesCompleter(tuple(images))
|
||||||
|
parser.add_argument('-n', '--name',
|
||||||
|
dest='name',
|
||||||
|
help="container name",
|
||||||
|
default="") #.completer = ChoicesCompleter(tuple(containers))
|
||||||
|
parser.add_argument('-t', '--temporary',
|
||||||
|
dest='temp',
|
||||||
|
action='store_true',
|
||||||
|
help="execute in temporary container",
|
||||||
|
default=False)
|
||||||
|
parser.add_argument('-c', '--command',
|
||||||
|
dest='cmd',
|
||||||
|
help="pass command to bash in container",
|
||||||
|
default="")
|
||||||
|
parser.add_argument('--home',
|
||||||
|
dest='home',
|
||||||
|
help="user home directory",
|
||||||
|
default=ini['homedir'])
|
||||||
|
#argcomplete.autocomplete(parser) #TODO make autocompletion work
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
args.suffix = ini['suffix']
|
||||||
|
args.greeting = ini['greeting']
|
||||||
|
args.ini = ini
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# load ini
|
||||||
|
cfg = ConfigParser({"USER": user, "HOSTNAME": hostname}, interpolation=ExtendedInterpolation())
|
||||||
|
cfg.read(config_file, encoding="utf-8")
|
||||||
|
|
||||||
|
if os.getenv("USER") and user != os.getenv('USER') and user in cfg["ADMIN"]["names"].splitlines():
|
||||||
|
user = os.getenv('USER')
|
||||||
|
|
||||||
|
# reread config
|
||||||
|
cfg = ConfigParser({"USER": user, "HOSTNAME": hostname}, interpolation=ExtendedInterpolation())
|
||||||
|
cfg.read(config_file, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
admin_cmd = "admin"
|
||||||
|
admin_shell = "/bin/bash"
|
||||||
|
if cfg.has_section("ADMIN") and "command" in cfg["ADMIN"]:
|
||||||
|
admin_cmd = cfg["ADMIN"]["command"]
|
||||||
|
if "admin_shell" in cfg["ADMIN"]:
|
||||||
|
admin_shell = cfg["ADMIN"]["admin_shell"]
|
||||||
|
ini = cfg[user] if cfg.has_section(user) else cfg['DEFAULT']
|
||||||
|
|
||||||
|
#if __name__ == "__main__":
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
if args.cmd == admin_cmd:
|
||||||
|
print("Trying to login into host: "+user)
|
||||||
|
if not sys.stdout.isatty():
|
||||||
|
print()
|
||||||
|
print("admin mode is only possible using pseudo tty-allocation.")
|
||||||
|
print("Try login using:")
|
||||||
|
print("ssh -t ...")
|
||||||
|
sys.exit(0)
|
||||||
|
os.system("sudo -u "+user+" sudo "+admin_shell)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if cfg.has_section("ADMIN") and "maintenance" in cfg["ADMIN"] and cfg["ADMIN"]["maintenance"] == "on" and (not "maintenance_scp" in cfg["ADMIN"] or cfg["ADMIN"]["maintenance_scp"] != "on"):
|
||||||
|
if "maintenance_text" in cfg["ADMIN"]:
|
||||||
|
print(cfg["ADMIN"]["maintenance_text"])
|
||||||
|
else:
|
||||||
|
print("This Maschine is in Maintanence Mode.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
is_scp_cmd = False
|
||||||
|
if args.cmd:
|
||||||
|
if os.path.basename(args.cmd).startswith(("scp","rsync --server","sftp-server","ls","*")):
|
||||||
|
is_scp_cmd = True
|
||||||
|
if args.cmd == "envir":
|
||||||
|
print(os.environ)
|
||||||
|
name_passed = (args.name != "")
|
||||||
|
image_passed = (args.image != "")
|
||||||
|
|
||||||
|
if not is_scp_cmd and cfg.has_section("ADMIN") and "maintenance" in cfg["ADMIN"] and cfg["ADMIN"]["maintenance"] == "on":
|
||||||
|
if "maintenance_text" in cfg["ADMIN"]:
|
||||||
|
print(cfg["ADMIN"]["maintenance_text"])
|
||||||
|
else:
|
||||||
|
print("This Maschine is in Maintanence Mode. However, you can copy files with scp, rsync, sftp or list files with ls without connecting to the maschine.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if args.temp:
|
||||||
|
if not image_passed:
|
||||||
|
args.image = args.ini['image']
|
||||||
|
args.image_base, args.image_tag = image_split(args.image)
|
||||||
|
args.image = args.image_base + ':' + args.image_tag
|
||||||
|
args.name = strip(args.image) + '_tmp' + random_string(4)
|
||||||
|
else:
|
||||||
|
if name_passed:
|
||||||
|
args.name = strip(args.name, args.suffix)
|
||||||
|
|
||||||
|
filtered_con = containers(image_filter=args.image, container_filter=args.name)
|
||||||
|
|
||||||
|
if len(filtered_con) > 0:
|
||||||
|
con_names = [c['Names'][0][1:] for c in filtered_con]
|
||||||
|
i = selection_menu(con_names)
|
||||||
|
args.name = strip(con_names[i], args.suffix)
|
||||||
|
else:
|
||||||
|
if not image_passed:
|
||||||
|
args.image = args.ini['image']
|
||||||
|
args.image_base, args.image_tag = image_split(args.image)
|
||||||
|
args.image = args.image_base + ':' + args.image_tag
|
||||||
|
|
||||||
|
if not name_passed:
|
||||||
|
args.name = strip(args.image)
|
||||||
|
|
||||||
|
if len(containers(container_filter=args.name)) != 0:
|
||||||
|
print("WARNING: container name already exists (ignoring --image)")
|
||||||
|
|
||||||
|
args.full_name = args.name + args.suffix
|
||||||
|
|
||||||
|
initing = False
|
||||||
|
if len(containers(container_filter=args.name)) == 0:
|
||||||
|
volumes = []
|
||||||
|
if "volumes" in args.ini:
|
||||||
|
volumes = volumes + args.ini["volumes"].split(",")
|
||||||
|
volumes = [v.split(":") for v in volumes]
|
||||||
|
binds = {v[0].strip():{"bind":v[1].strip(),"mode":v[2].strip()} for v in volumes}
|
||||||
|
volumes = [v[1] for v in volumes]
|
||||||
|
|
||||||
|
host_config = cli.create_host_config(
|
||||||
|
binds=binds,
|
||||||
|
restart_policy={'Name' : 'unless-stopped'})
|
||||||
|
|
||||||
|
#cli.pull(args.image)
|
||||||
|
userpwd = getpwnam(user)
|
||||||
|
cli.create_container(args.image,
|
||||||
|
stdin_open=True,
|
||||||
|
tty=True,
|
||||||
|
name=args.full_name,
|
||||||
|
hostname=args.name,
|
||||||
|
labels={'group': prog, 'user': user},
|
||||||
|
volumes=volumes,
|
||||||
|
working_dir=args.home,
|
||||||
|
environment={
|
||||||
|
"HOST_USER_ID": userpwd.pw_uid,
|
||||||
|
"HOST_USER_GID": userpwd.pw_gid,
|
||||||
|
"HOST_USER_NAME": user
|
||||||
|
},
|
||||||
|
host_config=host_config
|
||||||
|
)
|
||||||
|
initing=True
|
||||||
|
|
||||||
|
cli.start(args.full_name)
|
||||||
|
if initing:
|
||||||
|
print("")
|
||||||
|
print("Initializing...")
|
||||||
|
#os.popen('docker exec '+args.full_name + ' /bin/bash -c "if [ -e /init-user ]; then /init-user; else echo \"No Initialization skript found for container\"; fi; echo Initialization finished."').read().split(":")[-1]
|
||||||
|
init_cmd = 'docker exec '+args.full_name + ' /bin/bash -c "if [ -e /init-user ]; then /init-user; else echo \\\"Script...\\\"; fi; echo Initialization finished."'
|
||||||
|
print(os.popen(init_cmd).read())
|
||||||
|
#print("Please login again.")
|
||||||
|
#sys.exit(0)
|
||||||
|
if len(args.cmd) == 0:
|
||||||
|
try:
|
||||||
|
print(args.greeting.replace("```",""))
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
print(hostname)
|
||||||
|
user_bash = os.popen('docker exec -u root '+args.full_name + ' getent passwd '+user+'').read().split(":")[-1]
|
||||||
|
if user_bash == "":
|
||||||
|
user_bash = "/bin/bash"
|
||||||
|
cmd = args.cmd if args.cmd else user_bash
|
||||||
|
#custom
|
||||||
|
#user_bash = "/bin/sh"
|
||||||
|
#cmd = user_bash
|
||||||
|
cmd = "/bin/bash -c \"" + cmd + "\""
|
||||||
|
|
||||||
|
#custom
|
||||||
|
#os.system('docker exec -u '+user+' '+ args.full_name + ' ' + 'useradd -s /bin/bash user')
|
||||||
|
|
||||||
|
# a tty needs -it, scp needs -i
|
||||||
|
docker_arg = "-i" if not sys.stdout.isatty() or is_scp_cmd else "-it"
|
||||||
|
os.system('docker exec -u '+user+" " + docker_arg +' '+ args.full_name + ' ' + cmd+"")
|
||||||
|
|
||||||
|
if args.temp:
|
||||||
|
cli.remove_container(args.full_name, v=True, force=True)
|
||||||
|
|
||||||
|
cli.close()
|
9
lcpubsh/image_template/Dockerfile
Normal file
9
lcpubsh/image_template/Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Original by https://github.com/sleeepyjack/dockersh
|
||||||
|
# Modified by georg@lysergic.dev
|
||||||
|
# Note! This is a skeleton, it is being altered by the spawn process.
|
||||||
|
FROM lc-archlinux-userbase-v2:sh0
|
||||||
|
|
||||||
|
COPY user-mapping.sh /
|
||||||
|
RUN chmod u+x /user-mapping.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/user-mapping.sh"]
|
21
lcpubsh/image_template/user-mapping.sh
Normal file
21
lcpubsh/image_template/user-mapping.sh
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Original by https://github.com/sleeepyjack/dockersh
|
||||||
|
# Modified by georg@lysergic.dev
|
||||||
|
if [ -z "${HOST_USER_NAME}" -o -z "${HOST_USER_ID}" -o -z "${HOST_USER_GID}" ]; then
|
||||||
|
echo "HOST_USER_NAME, HOST_USER_ID & HOST_USER_GID needs to be set!"; exit 100
|
||||||
|
fi
|
||||||
|
|
||||||
|
useradd \
|
||||||
|
--uid ${HOST_USER_ID} \
|
||||||
|
--gid ${HOST_USER_GID} \
|
||||||
|
--create-home \
|
||||||
|
--shell /bin/bash \
|
||||||
|
${HOST_USER_NAME}
|
||||||
|
groupadd --gid "${HOST_USER_GID}" "${HOST_USER_NAME}"
|
||||||
|
usermod -aG sudo ${HOST_USER_NAME}
|
||||||
|
sleep 5s
|
||||||
|
|
||||||
|
echo ${HOST_USER_NAME}:${HOST_USER_NAME} | chpasswd
|
||||||
|
|
||||||
|
exec su - "${HOST_USER_NAME}"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user