initial version

This commit is contained in:
Christophe Siraut 2018-02-12 17:22:20 +01:00
commit 79a9b1d8eb
6 changed files with 291 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
*.pyc
*.egg-info
*.dsc
*.deb
*.changes
*.buildinfo
*.tar.gz
*.upload
*/debian/debhelper-build-stamp
*/debian/*.debhelper.log
*/debian/*.substvars
*/debian/dspawn/
*/debian/files

1
dspawn/__init__.py Normal file
View File

@ -0,0 +1 @@
#

53
dspawn/cli.py Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python
import os
import sys
from dspawn.container import Machine
import argparse
machinectl_actions = ['list', 'list-images', 'start', 'stop', 'show',
'shell', 'remove', 'login', 'enable', 'disable',
'kill', 'reboot']
def main():
parser = argparse.ArgumentParser(
description='manage nspawn containers with debootstrap and caching')
parser.add_argument('action',
choices=['create', 'config'] + machinectl_actions,
help='which task shall we perform?')
parser.add_argument('name',
nargs='*',
help='container name(s)')
parser.add_argument('-r', '--release',
choices=['stable', 'jessie', 'stretch', 'buster'],
default='stable',
help='which release model shall we use; default: stable')
parser.add_argument('-a', '--address', metavar="x.x.y.z",
help='static ip address')
parser.add_argument('-m', '--mode',
default='zone',
choices=['zone', 'bridge', 'private'],
help='configure nspawn network; default: '
'containers in a common zone')
parser.add_argument('-c', '--macaddress',
help='specify container mac address'),
args = parser.parse_args()
if args.action == 'create' or args.action == 'config':
if not args.name:
parser.print_help()
sys.exit()
m = Machine(**vars(args))
getattr(m, args.action)()
if args.action in machinectl_actions:
if args.name:
ar = ' '.join(args.name)
else:
ar = ''
os.system('machinectl %s %s' % (args.action, ar))
if __name__ == '__main__':
main()

119
dspawn/container.py Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env python
import os
import glob
import psutil
from dspawn.netconf import MachineConfig
defaults = {
'arch': os.uname()[4],
'basepath': '/var/lib/machines',
'mirror': 'http://deb.debian.org/debian/'}
default_configuration = [
'apt-get install -y openssh-server dbus locales',
'test -d /root/.ssh || mkdir /root/.ssh',
'systemctl enable systemd-networkd']
jessie_fix = [
'echo "deb http://ftp.debian.org/debian jessie-backports main" >'
'/etc/apt/sources.list.d/backports.list',
'apt update; apt install -y -t jessie-backports dbus systemd systemd-sysv']
local_rsa_keys = glob.glob('/home/*/.ssh/id_rsa.pub') + \
glob.glob('/root/.ssh/id_rsa.pub')
local_authorized_keys = glob.glob('/home/*/.ssh/authorized_keys')
def use_aptcacherng():
return "apt-cacher-ng" in (p.name() for p in psutil.process_iter())
class Machine(object):
def __init__(self, name, release, mode='zone', address=None,
proxy=False, macaddress=None, **kwargs):
if isinstance(name, list):
self.name = name[0]
else:
self.name = name
for k, v in defaults.items():
setattr(self, k, v)
self.path = os.path.join(self.basepath, self.name)
self.release = release
self.address = address
self.macaddress = macaddress
self.proxy = proxy
self.mode = mode
self.modelpath = ".%s-%s" % (release, defaults['arch'])
self.config_file = '/etc/systemd/nspawn/%s.nspawn' % self.name
self.init = default_configuration
if self.release == 'jessie':
self.init = jessie_fix + self.init
self.configuration = MachineConfig(self)
if not os.system('cd %s' % self.basepath) == 0:
raise(Exception(
'You do not have permission to access %s, please use sudo'
% self.basepath))
config_dir = os.path.dirname(self.config_file)
if not os.path.isdir(config_dir):
os.mkdir(config_dir)
def exist(self):
if os.path.isdir(self.path):
return True
def create(self):
if self.exist():
print('container "%s" already exists' % self.name)
return
model = Machine(self.modelpath, self.release)
if not model.exist():
model._create()
print('copying base model %s to %s' % (model.path, self.path))
# shutil.copytree(model.path, self.path, symlinks=True)
os.system('cp -r %s %s' % (model.path, self.path))
for c in self.init:
self.chroot(c)
self.authorize()
self.config()
self.start()
def _create(self):
print('creating base model %s' % self.path)
if use_aptcacherng:
env = 'http_proxy=http://localhost:3142 '
print('Found apt-cacher-ng process, using it (%s)' % env)
else:
env = ''
bootstrap = '%sdebootstrap %s %s %s' % (
env, self.release, self.path, self.mirror)
os.system(bootstrap)
os.system('rm -r %s/var/lib/apt/lists/*')
def chroot(self, command):
print('in chroot: %s' % command)
cmd = "chroot %s /bin/bash -c '%s'" % (self.path, command)
os.system(cmd)
def authorize(self):
ah = os.path.join(self.path, 'root/.ssh/authorized_keys')
fh = open(ah, 'a')
rsa_lines = []
for rsa_file in local_rsa_keys + local_authorized_keys:
rsa_lines += open(rsa_file).readlines()
for rsa in set(rsa_lines):
fh.write('%s\n' % rsa)
def config(self):
self.configuration.realize()
def start(self):
os.system('machinectl start %s' % self.name)

87
dspawn/netconf.py Normal file
View File

@ -0,0 +1,87 @@
#!/usr/bin/env python
nameserver = '9.9.9.9'
modes = {'zone': '[Network]\nZone=Containers',
'bridge': '[Network]\nBridge=br0',
'private': ''}
nettpl_bridge = '''[Match]
Name=host0
[Network]
Address=%s/32
Gateway=%s
DNS=%s
[Link]
MACAddress=%s
[Route]
Destination=0.0.0.0/0'''
nettpl_zone = '''[Match]
Virtualization=container
Name=host0
[Network]
Address=%s
Gateway=%s'''
def get_default_gateway(macaddress):
import socket
import struct
"""Read the default gateway directly from /proc."""
with open("/proc/net/route") as fh:
for line in fh:
fields = line.strip().split()
if fields[1] != '00000000' or not int(fields[3], 16) & 2:
continue
return socket.inet_ntoa(struct.pack("<L", int(fields[2], 16)))
class MachineConfig:
def __init__(self, machine):
self.machine = machine
self.mode = self.machine.mode
self.address = self.machine.address
self.macaddress = self.machine.macaddress
def realize(self):
with open(self.machine.config_file, 'w') as cf:
cf.write(modes[self.mode])
def netconf(self):
self.set_hostname()
if self.address:
self.netconf()
self.resolvconf()
def netconf(self):
cf = os.path.join(
self.path, 'etc/systemd/network/80-container-host0.network')
with open(cf, 'w') as fh:
if self.mode == 'bridge':
gw = get_default_gateway(self.macaddress)
fh.write(nettpl % (self.address, gw, nameserver,
self.macaddress))
else:
gw = '10.0.0.1'
fh.write(nettpl_simple % (self.address, gw))
def resolvconf(self):
rc = os.path.join(self.path, 'etc/resolv.conf')
with open(rc, 'w') as rch:
rch.write("nameserver %s" % nameserver)
def set_hostname(self):
if self.name.count('.') <= 1:
hostname = self.machine.name
elif self.name.count('.') > 1:
hostname = self.name.partition('.')[0]
# domainname = self.name.partition('.')[2]
self.chroot('echo "127.0.1.1 %s %s" >> /etc/hosts' % (
self.name, hostname))
self.chroot('echo %s > /etc/hostname' % hostname)

16
setup.py Normal file
View File

@ -0,0 +1,16 @@
from setuptools import setup
setup(name='dspawn',
version='0.1',
description='systemd-nspawn container manager',
author='Christophe Siraut',
author_email='csiraut@entrouvert.com',
license='GPL',
packages=['dspawn'],
install_requires=[
'psutil',
],
entry_points = {
'console_scripts': ['dspawn=dspawn.cli:main'],
},
zip_safe=False)