initial version
This commit is contained in:
commit
79a9b1d8eb
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
#
|
|
@ -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()
|
|
@ -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)
|
||||
|
|
@ -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)
|
|
@ -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)
|
Loading…
Reference in New Issue