Skip to content

Instantly share code, notes, and snippets.

@craigphicks
Last active March 26, 2019 23:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save craigphicks/90f6af52dad54b978fd0cea562434f09 to your computer and use it in GitHub Desktop.
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
#!/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