Last active
March 26, 2019 23:00
-
-
Save craigphicks/90f6af52dad54b978fd0cea562434f09 to your computer and use it in GitHub Desktop.
python3 Script to create instance of LDX container with debian stretch from linuxcontainers and configure it up to ssh-login-ability
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import argparse | |
import textwrap | |
import subprocess | |
import os | |
import datetime | |
#import pdb | |
#import random | |
import time | |
import sys | |
# this is necessary when using zfs filesystem and when deletion was not complete | |
#os.system("sudo rm /var/snap/lxd/common/lxd/storage-pools/default/containers/stretch-cc/backup.yaml") | |
from pylxd import Client | |
############################## | |
### CHANGEABLE PARAMETERS | |
#ctr_name='stretch-cc' | |
#user_name='cc' | |
#ctr_name='test' | |
#user_name='tester' | |
#host_user_name=os.environ['USER'] | |
log_dir="./log/" | |
log_name=log_dir+"script.log" | |
image_source={ | |
'alias': 'debian/stretch', | |
'mode': 'pull', | |
'protocol': 'simplestreams', | |
'server': 'https://images.linuxcontainers.org', | |
'type': 'image'} | |
############################## | |
def start_log(): | |
try: | |
os.stat(log_dir) | |
except: | |
os.mkdir(log_dir) | |
with open(log_name,'w') as f: | |
f.write("log start {}\n".format(str(datetime.datetime.now()))) | |
def write_log(s,doprint=False): | |
with open(log_name,'a') as f: | |
f.write("----------------- {}\n".format(str(datetime.datetime.now()))) | |
f.write(s+'\n') | |
if doprint is True: | |
print(s) | |
def container_exec(ctr,cmd,n_excep_fails=30,n_exit_fails=10, sleep_sec=0.1): | |
''' | |
There are communication problems which sometimes result in exceptions | |
or errors from [container].execute(). Especially the first calls | |
to "apt -y update/install". Just give it a couple of tries and it | |
always seems to work. This is a that workaround. | |
It should be noted that calling 'apt -y ...' from CLI has the same problem. | |
However calling interactive 'apt ....' (no -y) doesn't have this problem. | |
https://discuss.linuxcontainers.org/t/when-calling-unattened-apt-update-install-sometimes-error/4390?u=craigphicks | |
''' | |
n=0 | |
p=0 | |
while n<n_excep_fails and p<n_exit_fails: | |
try: | |
cer=ctr.execute(cmd) | |
except Exception as e: | |
write_log("ERROR: {}: {}".format(" ".join(cmd), str(e))) | |
time.sleep(sleep_sec) | |
n=n+1 | |
continue | |
if cer.exit_code != 0: | |
write_log("ERROR: {}: exit_code: {}, stdout: {}, stderr: {}".format( | |
" ".join(cmd), cer.exit_code, cer.stdout, cer.stderr)) | |
time.sleep(0.1) | |
p=p+1 | |
continue | |
else: | |
write_log("SUCCESS:(n={},p={}): {}: exit_code: {}, stdout: {}, stderr: {}".format( | |
n,p," ".join(cmd), cer.exit_code, cer.stdout, cer.stderr)) | |
return True | |
raise Exception("container_exec") | |
def container_stop(c): | |
''' | |
[container].stop always hangs, when the container is running systemd on Debian Stretch. | |
The CLI 'lxc stop [container]' also hangs. | |
'lxc stop [container] --force' works. | |
'lxc exec [container] -- poweroff' also works, but it more gentle. See: | |
https://discuss.linuxcontainers.org/t/cant-stop-debian-8-9-and-10-containers/4294?u=craigphicks | |
''' | |
(exit_code, stdout, stderr)=c.execute(["poweroff"]) | |
write_log("container_stop::") | |
write_log("exit_code: "+ str(exit_code)) | |
write_log("stdout: "+stdout) | |
write_log("stderr: "+stderr) | |
assert exit_code is 0, stderr | |
def get_ipaddr(ctr): | |
addresses = ctr.state().network['eth0']['addresses'] | |
for a in addresses: | |
if(a['scope'] == 'global'): | |
return str(a['address']) | |
def part1(ctr_name, snapshot_name): | |
client = Client() | |
ctrs=client.containers | |
if ctrs.exists(ctr_name): | |
ctr=ctrs.get(ctr_name) | |
if ctr.status != "Stopped": | |
container_stop(ctr) | |
ctr.delete(wait=True) | |
# ctr=ctrs.create( | |
# {'name': ctr_name, 'source': | |
# {'alias': 'debian/stretch', | |
# 'mode': 'pull', | |
# 'protocol': 'simplestreams', | |
# 'server': 'https://images.linuxcontainers.org', | |
# 'type': 'image'}}, wait=True) | |
ctr=ctrs.create( | |
{'name': ctr_name, 'source': image_source}, | |
wait=True) | |
write_log(ctr_name+" created",True) | |
ctr.start() | |
write_log(ctr_name+" started",True) | |
container_exec(ctr,"apt-get -y update".split()) | |
container_exec(ctr,"apt-get -y install apt-utils".split()) | |
container_exec(ctr,"apt-get -y install sudo openssh-client openssh-server sshfs".split()) | |
try: ss=ctr.snapshots.get(snapshot_name) | |
except: pass | |
else: ss.delete(wait=True) | |
ss = ctr.snapshots.create(snapshot_name, stateful=False, wait=True) | |
write_log("SNAPSHOT: {} {}".format(ctr_name, snapshot_name),True) | |
def part2(ctr_name, user_name, host_user_name, | |
authorized_keys_file, | |
snapshot_name): | |
client = Client() | |
ctrs=client.containers | |
assert ctrs.exists(ctr_name) | |
ctr=ctrs.get(ctr_name) | |
write_log("Status:{}".format(ctr.status)) | |
script_str=''' | |
#!/bin/bash | |
echo '$user_name ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/nopw || exit 10 | |
sed "/^[^#]*PubkeyAuthentication/d" -i /etc/ssh/sshd_config || exit 20 | |
sed "/^[^#]*PasswordAuthentication/d" -i /etc/ssh/sshd_config || exit 30 | |
sed "/^[^#]*ChallengeResponseAuthentication/d" -i /etc/ssh/sshd_config || exit 40 | |
echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config || exit 50 | |
echo "PasswordAuthentication no" >> /etc/ssh/sshd_config || exit 60 | |
echo "ChallengeResponseAuthentication no" >> /etc/ssh/sshd_config || exit 70 | |
echo "AllowUsers $user_name" >> /etc/ssh/sshd_config || exit 80 | |
exit 0 | |
'''[1:-1].replace('$user_name',user_name) | |
ctr.files.put("/root/ssh-setup.sh",script_str) | |
container_exec(ctr,'chmod +x /root/ssh-setup.sh'.split()) | |
cmd = ['/root/ssh-setup.sh'] | |
container_exec(ctr,cmd) | |
content = ctr.files.get("/etc/ssh/sshd_config").decode('utf-8') | |
assert "AllowUsers "+user_name in content, "ssh setup failed" | |
write_log("successfully modified "+ctr_name+" /etc/ssh/sshd_config",True) | |
container_exec(ctr, ("useradd -m -d /home/"+user_name+" -U -s /bin/bash "+user_name).split()) | |
write_log("user account for "+user_name+" added",True) | |
container_exec(ctr, ("mkdir -p /home/"+user_name+"/.ssh").split()) | |
container_exec(ctr, ("chmod 700 /home/"+user_name+"/.ssh").split()) | |
write_log("/home/"+user_name+"/.ssh dir create and permissions 700 set") | |
# if do_key and key_name and len(key_name) | |
# ### add key to HOST .ssh, for ssh login into container | |
# KEYBASENAME="id-lxd-"+ctr_name+"-"+user_name | |
# KEY="/home/"+host_user_name+"/.ssh/"+KEYBASENAME | |
# PUBKEY=KEY+'.pub' | |
# try: os.stat(KEY) | |
# except: pass | |
# else: os.remove(KEY) | |
# try: os.stat(PUBKEY) | |
# except: pass | |
# else: os.remove(PUBKEY) | |
# #if os.is_file(PUBKEY): | |
# # os.remove(PUBKEY) | |
# cmd=['ssh-keygen', '-f', KEY, '-N', '', '-C', host_user_name+'@host'] | |
# cp = subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,errors="") | |
# write_log("subprocess.run({}), code: {}, stdout: {}, stderr: {}".format( | |
# " ".join(cmd), cp.returncode, cp.stdout, cp.stderr)) | |
# assert cp.returncode == 0, Exception("ERROR subprocess.run") | |
# ### set up the hostside host-to-key mapping in .ssh/confin | |
# ipaddr=get_ipaddr(ctr) | |
# config_filename = "/home/"+host_user_name+"/.ssh/config" | |
# config_str=textwrap.dedent(''' | |
# Host ${ipaddr} | |
# User ${user_name} | |
# IdentityFile ~/.ssh/${KEYBASENAME} | |
# ''')[1:].replace( | |
# '${ipaddr}',ipaddr).replace( | |
# '${user_name}',user_name).replace( | |
# '${KEYBASENAME}',KEYBASENAME) | |
# with open(config_filename,'a') as f: | |
# f.write(config_str) | |
### copy the pubkey to the container | |
if authorized_keys_file: | |
with open(authorized_keys_file,'r') as f: | |
content=f.read() | |
ctr.files.put("/home/"+user_name+"/.ssh/authorized_keys",content) | |
container_exec(ctr,("chmod 640 /home/"+user_name+"/.ssh/authorized_keys").split()) | |
container_exec(ctr,( | |
"chown -R ${user_name}:${user_name} /home/${user_name}/.ssh") | |
.replace("${user_name}",user_name).split()) | |
write_log("contents of "+ authorized_keys_file +" were written to container's ~/.ssh/authorized_keys ",True) | |
try: ss=ctr.snapshots.get(snapshot_name) | |
except: pass | |
else: ss.delete(wait=True) | |
ss = ctr.snapshots.create(snapshot_name, stateful=False, wait=True) | |
write_log("SNAPSHOT: {} {}".format(ctr_name, snapshot_name),True) | |
# write_log("You should now be able to ssh into the container with:". True) | |
# write_log("ssh {}@{}".format(user_name, ipaddr), True) | |
def main(): | |
parser = argparse.ArgumentParser( | |
formatter_class=argparse.RawDescriptionHelpFormatter, | |
description=textwrap.dedent(''' | |
The program has two sequential parts: | |
part1: Download a debian/stretch image from linuxcontainers.org, | |
create an unprivileged container <CONTAINER_NAME>, | |
perform basic "apt update" and "apt install" | |
of packages apt-utils, sudo, openssh-server. | |
Finally a snapshot of the container named "ss1" is created. | |
part2: On the container, | |
- create a user account <CONTAINER-USER-NAME> belonging to sudo | |
and admin groups, | |
- modify the /etc/ssh/sshd_config file to allow ssh login to | |
that account, and dissallow password login. | |
- create ~/.ssh directory with correct permissions | |
- optionally set contents of ~/.ssh/authorized_keys | |
- finally, a snapshot of the container named "ss2" is created. | |
After part1 and part2 and completed, the host user can(*) ssh into the container with | |
# ssh my-user@<container ip address> | |
where <container ip address> can be seen with | |
# lxc info my-cont | |
or running the project progam | |
# ./get_ip.py my-cont | |
(* may require setting hosts ~/.ssh/config file to show the container->key mapping) | |
Typical use of "--skip-part2" | |
- reset the container to snapshot "ss1" | |
# lxc restore my-cont ss1 | |
- call part2 | |
# PROG -c my-cont -u my-user --skip-part1 | |
''')) | |
#parser.add_argument("-v", "--verbose", help="increase output verbosity") | |
parser.add_argument("-c", "--container-name", | |
help="lxc container name (MANDATORY)") | |
parser.add_argument("-u", "--container-user-name", | |
help="name of user account inside container (MANDATORY for part2)") | |
parser.add_argument("-a", "--authorized-keys-file", | |
help=textwrap.dedent(''' | |
The contents of the specified file will be copied to ~/.ssh/authorized_keys on the container. | |
Typically this would just be the public part of an existing host key, e.g. "~/.ssh/id_rsa.pub". | |
Don't specify a private key by mistake. | |
(optional) | |
''')) | |
parser.add_argument("--skip-part1",action="store_true", | |
help="skip part1 (optional)") | |
parser.add_argument("--skip-part2",action="store_true", | |
help="skip part2 (optional)") | |
pargs=parser.parse_args() | |
start_log() | |
if not pargs.container_name: | |
raise Exception("container-name required") | |
if not pargs.skip_part2 and not pargs.container_user_name: | |
raise Exception("container-user-name required") | |
if not pargs.skip_part1: | |
part1(ctr_name=pargs.container_name,snapshot_name="ss1") | |
if not pargs.skip_part2: | |
host_user_name=os.environ['USER'] | |
part2(ctr_name=pargs.container_name, | |
user_name=pargs.container_user_name, | |
host_user_name=host_user_name, | |
authorized_keys_file=pargs.authorized_keys_file, | |
snapshot_name="ss2") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment