Created
January 10, 2017 17:07
-
-
Save aeris/26add07aef31a887237e3ba8530f49cc to your computer and use it in GitHub Desktop.
LXC management
This file contains hidden or 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 python | |
# License : AGPLv3+ | |
import subprocess | |
import logging | |
import re | |
import os.path | |
import tempfile | |
from datetime import date, datetime, timedelta | |
import shutil | |
from glob import glob | |
import yaml | |
import fcntl | |
import argparse | |
import sys | |
from contextlib import contextmanager | |
class ColorizedFormatter(logging.Formatter): | |
COLORS = { | |
logging.FATAL: "\x1b[0;37;41m", | |
logging.ERROR: "\x1b[0;31;49m", | |
logging.WARNING: "\x1b[0;33;49m", | |
logging.INFO: "\x1b[0;94;49m", | |
logging.DEBUG: "\x1b[0;90;49m" | |
} | |
def format(self, record): | |
level = record.levelno | |
color = ColorizedFormatter.COLORS[level] | |
level = record.levelname | |
message = record.getMessage() | |
return "[%(color)s%(level)8s\x1b[0m] %(message)s" % {"color": color, | |
"level": level, | |
"message": message} | |
console = logging.StreamHandler() | |
console.setFormatter(ColorizedFormatter()) | |
LOGGER = logging.getLogger("lxc") | |
LOGGER.addHandler(console) | |
LOGGER.setLevel(os.environ.get("LOG") or logging.DEBUG) | |
def fatal(*args, **kwargs): | |
LOGGER.fatal(*args, **kwargs) | |
exit(-1) | |
def call(*cmd, **kwargs): | |
LOGGER.debug(cmd) | |
stdin = kwargs.get("stdin") | |
if stdin: | |
process = subprocess.Popen(cmd, stdin=subprocess.PIPE) | |
process.communicate(stdin) | |
process.wait() | |
retcode = process.returncode | |
if retcode: | |
raise subprocess.CalledProcessError(retcode, cmd) | |
else: | |
stdout = kwargs.get("stdout") | |
if stdout: | |
return subprocess.check_output(cmd) | |
else: | |
subprocess.check_call(cmd) | |
@contextmanager | |
def lock(lockfile): | |
LOGGER.debug("Locking %s", lockfile) | |
with open(lockfile, "w") as file: | |
try: | |
fcntl.flock(file, fcntl.LOCK_EX | fcntl.LOCK_NB) | |
try: | |
yield | |
finally: | |
fcntl.flock(file, fcntl.LOCK_UN) | |
finally: | |
os.unlink(lockfile) | |
LOGGER.debug("Unlocking %s", lockfile ) | |
@contextmanager | |
def tmp_dir(): | |
dir = tempfile.mkdtemp() | |
try: | |
yield dir | |
finally: | |
os.rmdir(dir) | |
@contextmanager | |
def activate_lvm(lvm): | |
LOGGER.info("Activating LVM %s", lvm) | |
call("lvchange", "-ay", "-Ky", lvm) | |
try: | |
yield | |
finally: | |
LOGGER.info("Deactivating LVM %s", lvm) | |
call("lvchange", "-an", lvm) | |
@contextmanager | |
def lvm_snapshot(dev, snapshot): | |
LOGGER.info("Creating LVM snapshot of %s to %s", dev, snapshot) | |
call("lvcreate", "-n", snapshot, "-s", dev) | |
try: | |
with activate_lvm(snapshot): | |
yield | |
finally: | |
LOGGER.info("Removing LVM snapshot %s", snapshot) | |
call("lvremove", snapshot) | |
@contextmanager | |
def mount(dev): | |
with tmp_dir() as dir: | |
LOGGER.info("Mounting %s to %s", dev, dir) | |
call("mount", dev, dir) | |
try: | |
yield dir | |
finally: | |
LOGGER.info("Unmounting %s", dir) | |
call("umount", dir) | |
class LXCGuest: | |
DATE_FORMAT = "%Y-%m-%d" | |
BACKUP_DATE = date.today().strftime(DATE_FORMAT) | |
BACKUP_DIR = "/var/backups/lxc" | |
BACKUP_TODAY_DIR = os.path.join(BACKUP_DIR, BACKUP_DATE) | |
LXC_DIR = "/var/lib/lxc" | |
# EXCLUDES = ["/dev/*", "/proc/*", "/sys/*", "/tmp/*"] | |
EXCLUDES = ["/tmp/*"] | |
RETENTION = timedelta(days=7) | |
BACKUP_CONFIG = "/etc/lxc/backup.yml" | |
def __init__(self, name, state=None): | |
self.__name = name | |
self.__state = state | |
def name(self): | |
return self.__name | |
def state(self): | |
return self.__state | |
def __call(self, *args): | |
cmd = ["lxc-attach", "-n", self.__name] + list(args) | |
call(*cmd) | |
def __call_bash(self, cmd): | |
cmd = ["--", "bash", "-c", cmd] | |
self.__call(*cmd) | |
def __rootfs(self): | |
rootfs = call("lxc-info", "-n", self.__name, "-c", "lxc.rootfs", | |
stdout=True) | |
rootfs = rootfs.strip() | |
rootfs = re.split("\\s+=\\s+", rootfs) | |
rootfs = rootfs[1] | |
return rootfs | |
def backup(self): | |
LOGGER.info("Backuping %s", self.__name) | |
LOGGER.info(" Running backup scripts inside the container") | |
self.__call_bash("[ -d /etc/backup ] && run-parts /etc/backup || true") | |
rootfs = self.__rootfs() | |
snapshot = rootfs + "-snap" | |
with lvm_snapshot(rootfs, snapshot): | |
with mount(snapshot) as mountpoint: | |
archive = os.path.join(LXCGuest.BACKUP_TODAY_DIR, | |
self.__name + ".tar.xz") | |
LOGGER.info(" Backuping %s to %s", mountpoint, archive) | |
tar = ["tar", "-I", "pxz -9e -k -T8", "--numeric-owner", | |
"-cf", archive, "-C", mountpoint, "."] | |
tar += ["--exclude=.%s" % exclude for exclude in | |
LXCGuest.EXCLUDES] | |
call(*tar) | |
lxc_config = os.path.join(LXCGuest.LXC_DIR, self.__name, | |
"config") | |
config = os.path.join(LXCGuest.BACKUP_TODAY_DIR, | |
self.__name + ".config") | |
LOGGER.info(" Backuping %s to %s", lxc_config, config) | |
shutil.copy(lxc_config, config) | |
@staticmethod | |
def list(running=True, stopped=False): | |
ls = call("lxc-ls", "-f", stdout=True) | |
ls = ls.split("\n")[2:] # Skip headers | |
containers = [] | |
for l in ls: | |
l = re.split("\\s+", l) | |
if len(l) != 6: | |
continue | |
name, state = l[0], l[1] | |
if (running and state == "RUNNING") or \ | |
(stopped and state == "STOPPED"): | |
container = LXCGuest(name, state) | |
containers.append(container) | |
return containers | |
@staticmethod | |
def delete_old(): | |
LOGGER.info("Purging old backups") | |
backups = os.path.join(LXCGuest.BACKUP_DIR, "*") | |
backups = glob(backups) | |
for backup in sorted(backups): | |
d = os.path.basename(backup) | |
d = datetime.strptime(d, LXCGuest.DATE_FORMAT).date() | |
if date.today() - d >= LXCGuest.RETENTION: | |
LOGGER.info(" Purging %s backup", d) | |
shutil.rmtree(backup) | |
else: | |
LOGGER.debug(" Keeping %s backup", d) | |
@staticmethod | |
def is_backup_enabled(config, container): | |
if container.state() != "RUNNING": | |
return False | |
included = config.get("include", []) | |
excluded = config.get("exclude", []) | |
name = container.name() | |
if excluded and name in excluded: | |
return False | |
if included: | |
if name in included: | |
return True | |
return False | |
return True | |
@staticmethod | |
def backup_all(hosts=None): | |
if not os.path.isdir(LXCGuest.BACKUP_TODAY_DIR): | |
os.makedirs(LXCGuest.BACKUP_TODAY_DIR) | |
if os.path.isfile(LXCGuest.BACKUP_CONFIG): | |
config = open(LXCGuest.BACKUP_CONFIG, "r") | |
config = yaml.load(config) or {} | |
else: | |
config = {} | |
if hosts: | |
containers = [LXCGuest(host) for host in hosts] | |
else: | |
containers = LXCGuest.list() | |
containers = filter( | |
lambda c: LXCGuest.is_backup_enabled(config, c), | |
containers | |
) | |
for container in containers: | |
container.backup() | |
# Don't purge old backup if not all guests backuped ! | |
if not hosts: | |
LXCGuest.delete_old() | |
@staticmethod | |
def restore(host, size=None, dir=None): | |
if not size: | |
size = "5G" | |
if not dir: | |
dir = os.getcwd() | |
LOGGER.info("Restoring %s from %s", host, dir, size) | |
LOGGER.info(" Creating LVM %s with %i size", host, size) | |
call("lvcreate", "--virtualsize", size, "--thin", "--name", host, | |
"lxc/lxc") | |
dev = os.path.join("/dev/lxc", host) | |
LOGGER.info(" Formatting %s to ext4", dev) | |
call("mkfs.ext4", dev) | |
lxc_dir = os.path.join("/var/lib/lxc", host) | |
if not os.path.isdir(lxc_dir): | |
os.makedirs(lxc_dir) | |
config_from = os.path.join(dir, host + ".config") | |
config_to = os.path.join(lxc_dir, "config") | |
LOGGER.info(" Restoring config %s to %s", config_from, config_to) | |
shutil.copy(config_from, config_to) | |
with mount(dev) as mountpoint: | |
tar = os.path.join(dir, host + ".tar.xz") | |
LOGGER.info(" Desarchiving archive %s to %s", tar, mountpoint) | |
call("tar", "-C", mountpoint, "-xf", tar) | |
class LXCHost: | |
BACKUP_DIR = "/var/backups/lxc" | |
BACKUP_CONFIG = "/etc/lxc/backup.yml" | |
RSYNC = ["rsync", "-axh", "--delete"] | |
CONFIG = None | |
MONITORING = { | |
"AGED": timedelta(days=2), | |
"OBSOLETED": timedelta(days=3) | |
} | |
@staticmethod | |
def config(): | |
if LXCHost.CONFIG: | |
return LXCHost.CONFIG | |
try: | |
config = open(LXCHost.BACKUP_CONFIG, "r") | |
config = yaml.load(config) | |
LXCHost.CONFIG = config | |
except: | |
LXCHost.CONFIG = {} | |
return LXCHost.CONFIG | |
def __init__(self, name): | |
self.__name = name | |
def backup(self): | |
lockfile = os.path.join(LXCHost.BACKUP_DIR, self.__name + ".lock") | |
with lock(lockfile): | |
# LOGGER.info("Launch LXC backup on %s", self.__name) | |
# call("ssh", self.__name, "lxc-backup") | |
backup_dir = os.path.join(LXCHost.BACKUP_DIR, self.__name) | |
target = "%s:%s/" % (self.__name, LXCGuest.BACKUP_DIR) | |
dest = "%s/" % (backup_dir) | |
LOGGER.info("Backuping %s (%s -> %s)", self.__name, target, dest) | |
rsync = LXCHost.RSYNC + [target, dest] | |
call(*rsync) | |
@staticmethod | |
def backup_all(hosts=None): | |
if not os.path.isdir(LXCHost.BACKUP_DIR): | |
os.makedirs(LXCHost.BACKUP_DIR) | |
if not hosts: | |
hosts = LXCHost.config().get("hosts", {}).keys() | |
for host in hosts: | |
host = LXCHost(host) | |
host.backup() | |
@staticmethod | |
def age(host, guest): | |
path = os.path.join(LXCHost.BACKUP_DIR, host, "*", "%s.tar.xz" % guest) | |
backups = glob(path) | |
age_min = timedelta.max | |
for backup in backups: | |
parent = os.path.dirname(backup) | |
d = os.path.basename(parent) | |
d = datetime.strptime(d, LXCGuest.DATE_FORMAT).date() | |
age = date.today() - d | |
if age < age_min: | |
age_min = age | |
return age_min | |
@staticmethod | |
def check_all(): | |
hosts = LXCHost.config().get("hosts", {}) | |
obsoleted = [] | |
aged = [] | |
for host, guests in hosts.iteritems(): | |
for guest in guests: | |
age = LXCHost.age(host, guest) | |
result = (host, guest, age) | |
if age > LXCHost.MONITORING["OBSOLETED"]: | |
obsoleted.append(result) | |
elif age > LXCHost.MONITORING["AGED"]: | |
aged.append(result) | |
if obsoleted: | |
print("Obsoleted : %i backups" % len(obsoleted)) | |
for o in obsoleted: | |
print(" %s @ %s : %s" % o) | |
if aged: | |
print("Aged : %i backups" % len(aged)) | |
for a in aged: | |
print(" %s @ %s : %s" % a) | |
if obsoleted: | |
exit(2) | |
if aged: | |
exit(1) | |
parser = argparse.ArgumentParser(description="Backup LXC host or guests") | |
parser.add_argument("-b", "--backend", | |
help="Set this flag if run on the backup backend", | |
action="store_true") | |
parser.add_argument("-c", "--check", | |
help="Check the backups state", | |
action="store_true") | |
parser.add_argument("-r", "--restore", | |
help="Restore container from backup", | |
action="store_true") | |
parser.add_argument("args", nargs="*") | |
args = parser.parse_args() | |
if args.check: | |
if args.backend: | |
LXCHost.check_all() | |
elif args.restore: | |
# Python list has no .get :'( | |
args = dict(enumerate(args.args)) | |
host = args[0] | |
size = args.get(1) | |
dir = args.get(2) | |
LXCGuest.restore(host, size=size, dir=dir) | |
else: | |
if args.backend: | |
LXCHost.backup_all(args.args) | |
else: | |
LXCGuest.backup_all(args.args) |
This file contains hidden or 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 python | |
# License : AGPLv3+ | |
import argparse | |
import base64 | |
import glob | |
import hashlib | |
import logging | |
import os | |
import random | |
import re | |
import socket | |
import subprocess | |
import textwrap | |
from contextlib import contextmanager | |
from grp import getgrnam | |
from pwd import getpwnam | |
from time import sleep | |
from jinja2 import Template | |
class ColorizedFormatter(logging.Formatter): | |
COLORS = { | |
logging.FATAL: "\x1b[0;37;41m", | |
logging.ERROR: "\x1b[0;31;49m", | |
logging.WARNING: "\x1b[0;33;49m", | |
logging.INFO: "\x1b[0;94;49m", | |
logging.DEBUG: "\x1b[0;90;49m" | |
} | |
def format(self, record): | |
level = record.levelno | |
color = ColorizedFormatter.COLORS[level] | |
level = record.levelname | |
message = record.getMessage() | |
return "[%(color)s%(level)8s\x1b[0m] %(message)s" % {"color": color, | |
"level": level, | |
"message": message} | |
console = logging.StreamHandler() | |
console.setFormatter(ColorizedFormatter()) | |
LOGGER = logging.getLogger("lxc") | |
LOGGER.addHandler(console) | |
LOGGER.setLevel(logging.DEBUG) | |
def fatal(*args, **kwargs): | |
LOGGER.fatal(*args, **kwargs) | |
exit(-1) | |
def call(*cmd, **kwargs): | |
LOGGER.debug(cmd) | |
stdin = kwargs.get("stdin") | |
if stdin: | |
process = subprocess.Popen(cmd, stdin=subprocess.PIPE) | |
process.communicate(stdin) | |
process.wait() | |
retcode = process.returncode | |
if retcode: | |
raise subprocess.CalledProcessError(retcode, cmd) | |
else: | |
stdout = kwargs.get("stdout") | |
if stdout: | |
return subprocess.check_output(cmd) | |
else: | |
subprocess.check_call(cmd) | |
@contextmanager | |
def do_in_mount(dev, dir): | |
LOGGER.info("Mounting %s to %s", dev, dir) | |
call("mount", dev, dir) | |
try: | |
yield | |
finally: | |
LOGGER.info("Unmounting %s", dir) | |
call("umount", dir) | |
def chroot_call(directory, *cmd, **kwargs): | |
cmd = ["chroot", directory] + list(cmd) | |
call(*cmd, **kwargs) | |
def file_content(content, dst, user="root", group="root", chmod="644"): | |
with open(dst, "w") as file: | |
file.write(content) | |
uid = getpwnam(user).pw_uid | |
gid = getgrnam(group).gr_gid | |
os.chown(dst, uid, gid) | |
chmod = int(chmod, 8) | |
os.chmod(dst, chmod) | |
def file_template(src, dst, user="root", group="root", chmod="644", **kwargs): | |
template = Template(src) | |
content = template.render(**kwargs) | |
file_content(content, dst, user=user, group=group, chmod=chmod) | |
class LXCGuest: | |
LXC_DIR = "/var/lib/lxc" | |
def __init__(self, name): | |
self.__name = name | |
self.__dir = self.__path() | |
self.__dev = os.path.join("/dev/lxc", self.__name) | |
self.__root = self.__path("rootfs") | |
def __path(self, *args): | |
path = [LXCGuest.LXC_DIR, self.__name] | |
if args: | |
path += args | |
return os.path.join(*path) | |
def __rootfs_path(self, *args): | |
return self.__path("rootfs", *args) | |
def __is_mounted(self): | |
return os.path.ismount(self.__root) | |
@contextmanager | |
def mount(self): | |
if self.__is_mounted(): | |
fatal("Rootfs is already mounted [%s]", self.__root) | |
with do_in_mount(self.__dev, self.__root): | |
yield | |
def create_container_from_file_template(self, template, size): | |
LOGGER.info("Creating LXC container %s from template %s", | |
self.__name, template) | |
call("lxc-create", "--bdev", "lvm", "--fssize", size, | |
"--template", template, "--name", self.__name) | |
def clone_container(self, clone, size): | |
LOGGER.info("Cloning LXC container %s from %s", self.__name, clone) | |
call("lxc-copy", "-B", "lvm", "--fssize", size, | |
"--name", clone, "--newname", self.__name) | |
def create_container_from_archive(self, tar, size): | |
LOGGER.info("Creating LXC container %s from archive %s", | |
self.__name, tar) | |
if not os.path.exists(tar): | |
fatal("Unable to find archive %s", tar) | |
LOGGER.debug(" Creating LXC container directory %s", self.__dir) | |
os.mkdir(self.__dir, 0o770) | |
LOGGER.debug(" Creating LXC container rootfs directory %s", | |
self.__root) | |
os.mkdir(self.__root, 0o755) | |
LOGGER.debug( | |
" Creating thin logical volume %s on lxc/lxc pool with %s size", | |
self.__dir, size) | |
call("lvcreate", "--virtualsize", size, "--thin", "--name", self.__name, | |
"lxc/lxc") | |
LOGGER.debug(" Formatting %s to ext4", self.__dev) | |
call("mkfs.ext4", self.__dev) | |
with self.mount(): | |
LOGGER.debug(" Desarchiving %s to %s", tar, self.__root) | |
call("tar", "-C", self.__root, "-xf", tar) | |
def create_container(self, args): | |
if os.path.exists(self.__dir): | |
if not args.force: | |
fatal("Guest already exists [%s]", self.__dir) | |
else: | |
size = args.size | |
clone = args.clone | |
tar = args.tar | |
if clone: | |
self.clone_container(clone, size) | |
elif tar: | |
self.create_container_from_archive(tar, size) | |
else: | |
self.create_container_from_file_template(args.template, size) | |
def create_fstab(self): | |
fstab = self.__path("fstab") | |
LOGGER.info("Creating empty fstab [%s]", fstab) | |
file_content("", fstab) | |
@staticmethod | |
def __random_mac(prefix="02:00:00"): | |
mac = prefix.replace(":", "") | |
while len(mac) < 12: | |
digit = random.randint(0x0, 0xf) | |
digit = "{0:x}".format(digit) | |
mac += digit | |
mac = textwrap.wrap(mac, 2) | |
mac = ":".join(mac) | |
mac = mac.lower() | |
return mac | |
def create_config(self, args): | |
mac = LXCGuest.__random_mac() | |
veth = self.__name | |
# Interface name is limited to 15 characters | |
if len(veth) > 15: | |
hash = hashlib.sha256() | |
hash.update(veth) | |
veth = hash.hexdigest() | |
veth = "lxc-{0}".format(veth) | |
veth = veth[:15] | |
config = self.__path("config") | |
LOGGER.info( | |
"Creating container config (bridge: %s, veth: %s, mac: %s) [%s]", | |
args.bridge, veth, mac, config) | |
file_template("""\ | |
lxc.start.auto = 1 | |
lxc.network.type = veth | |
lxc.network.flags = up | |
lxc.network.link = {{bridge}} | |
lxc.network.name = eth0 | |
lxc.network.veth.pair = {{veth}} | |
lxc.network.hwaddr = {{mac}} | |
lxc.rootfs = {{dev}} | |
# Common configuration | |
lxc.include = /usr/share/lxc/config/debian.common.conf | |
# Container specific configuration | |
lxc.mount = {{dir}}/fstab | |
lxc.utsname = {{name}} | |
lxc.arch = amd64 | |
{% if memory %} | |
lxc.cgroup.memory.limit_in_bytes = {{memory}} | |
{% endif %} | |
{% if swap %} | |
lxc.cgroup.memory.memsw.limit_in_bytes = {{swap}} | |
{% endif %} | |
""", config, name=self.__name, dev=self.__dev, dir=self.__dir, root=self.__root, | |
veth=veth, mac=mac, bridge=args.bridge, | |
memory=args.memory, swap=args.swap) | |
def configure_network(self, args): | |
interfaces = self.__rootfs_path("etc/network/interfaces") | |
LOGGER.info("Enabling interfaces.d [%s]", interfaces) | |
file_content("source-directory /etc/network/interfaces.d\n", interfaces) | |
lo = self.__rootfs_path("etc/network/interfaces.d/lo") | |
LOGGER.info("Configuring loopback interface [%s]", lo) | |
file_content("""\ | |
auto lo | |
iface lo inet loopback | |
""", lo) | |
if args.gateway is True: | |
ips = call("ip", "addr", "show", "dev", args.bridge, stdout=True) | |
ips = re.search("\\s+inet ([\\d.]+)/\\d+ brd .* scope global .*", | |
ips) | |
gateway = ips.group(1) | |
args.gateway = gateway | |
eth0 = self.__rootfs_path("etc/network/interfaces.d/eth0") | |
LOGGER.info("Configuring eth0 interface (ip: %s, gw: %s) [%s]", | |
args.ip, args.gateway, eth0) | |
file_template("""\ | |
auto eth0 | |
iface eth0 inet static | |
address {{ip}} | |
gateway {{gw}} | |
""", eth0, ip=args.ip, gw=args.gateway) | |
resolvconf = self.__rootfs_path("etc/resolv.conf") | |
LOGGER.info("Configuring DNS resolver (domain: %s, gw: %s) [%s]", | |
args.domain, args.gateway, resolvconf) | |
file_template("""\ | |
domain {{domain}} | |
search {{domain}} | |
nameserver {{gw}} | |
""", resolvconf, domain=args.domain, gw=args.gateway) | |
hostname = self.__rootfs_path("etc/hostname") | |
LOGGER.info("Configuring hostname (hostname: %s) [%s]", | |
args.hostname, hostname) | |
file_template("{{hostname}}\n", hostname, hostname=args.hostname) | |
fqdn = "{name}.{domain}".format(name=args.hostname, domain=args.domain) | |
hosts = self.__rootfs_path("etc/hosts") | |
LOGGER.info("Configuring host names (hostname: %s, FQDN: %s) [%s]", | |
args.hostname, fqdn, hosts) | |
file_template("""\ | |
127.0.0.1 {{fqdn}} {{name}} | |
127.0.0.1 localhost.localdomain localhost | |
# The following lines are desirable for IPv6 capable hosts | |
::1 ip6-localhost ip6-loopback | |
fe00::0 ip6-localnet | |
ff00::0 ip6-mcastprefix | |
ff02::1 ip6-allnodes | |
ff02::2 ip6-allrouters | |
""", hosts, name=args.hostname, fqdn=fqdn) | |
def __chroot(self, *cmd, **kwargs): | |
chroot_call(self.__root, *cmd, **kwargs) | |
def __lxc(self, *cmd, **kwargs): | |
cmd = ["lxc-attach", "-n", self.__name] + list(cmd) | |
call(*cmd, **kwargs) | |
def __start(self): | |
LOGGER.info("Starting guest") | |
call("lxc-start", "-dn", self.__name) | |
sleep(5) | |
def __stop(self): | |
LOGGER.info("Stopping guest") | |
call("lxc-stop", "-n", self.__name) | |
def set_root_password(self, password): | |
if password is None: | |
return | |
if password: | |
LOGGER.info("Setting root password") | |
self.__chroot("chpasswd", | |
stdin="root:{password}\n".format(password=password)) | |
else: | |
LOGGER.info("Disabling root password") | |
self.__chroot("passwd", "--delete", "--lock", "root") | |
def deploy_ssh_keys(self, keys): | |
authorized_keys = self.__rootfs_path("root/.ssh/authorized_keys") | |
dir = os.path.dirname(authorized_keys) | |
if not os.path.isdir(dir): | |
os.mkdir(dir, 0o700) | |
with open(authorized_keys, "w") as authorized_keys: | |
for key in keys: | |
if os.path.isfile(key): | |
with open(key, "r") as file: | |
key = file.readlines() | |
else: | |
key = [key] | |
for key in key: | |
key = key.strip() | |
LOGGER.info("Adding SSH keys %s", key) | |
authorized_keys.write(key) | |
@contextmanager | |
def disable_services(self): | |
policy = self.__rootfs_path("usr/sbin/policy-rc.d") | |
LOGGER.info("Disabling service start [%s]", policy) | |
file_content("#!/bin/sh\nexit 101\n", policy, chmod="755") | |
try: | |
yield | |
finally: | |
LOGGER.info("Enabling service start [%s]", policy) | |
os.unlink(policy) | |
def regenerate_ssh_keys(self): | |
LOGGER.info("Removing old SSH host keys") | |
keys = self.__rootfs_path("etc/ssh/ssh_host*") | |
keys = glob.glob(keys) | |
for file in keys: | |
LOGGER.debug(" Remove SSH host key %s", file) | |
os.unlink(file) | |
LOGGER.info("Creating new SSH host keys") | |
self.__chroot("ssh-keygen", "-A") | |
def configure_apt(self, args): | |
resolvconf = self.__rootfs_path("etc/resolv.conf") | |
LOGGER.info( | |
"Configuring DNS resolver to localhost (we will be in a chroot!) [%s]", | |
resolvconf) | |
# Needed to have connectivity inside chroot | |
file_content("nameserver 127.0.0.1\n", resolvconf) | |
recommends = self.__rootfs_path("etc/apt/apt.conf.d/60recommends") | |
LOGGER.info("Disabling APT recommends & suggests [%s]", recommends) | |
file_content("""\ | |
APT::Install-Recommends "0"; | |
APT::Install-Suggests "0"; | |
""", recommends) | |
if args.apt_proxy: | |
if args.apt_proxy is True: | |
args.apt_proxy = "apt.{domain}".format(domain=args.domain) | |
proxy = self.__rootfs_path("etc/apt/apt.conf.d/60proxy") | |
LOGGER.info( | |
"Configuring APT proxy to %s [%s]", args.apt_proxy, proxy) | |
file_template( | |
"Acquire::http::Proxy \"http://{{host}}:3142\";\n", | |
proxy, host=args.apt_proxy) | |
self.__chroot("apt", "update") | |
self.__chroot("apt", "install", "-y", "apt-utils") | |
def clean(self): | |
LOGGER.info("Cleaning apt cache") | |
self.__chroot("apt-get", "clean") | |
LOGGER.info("Removing logs") | |
self.__chroot("find", "/var/log", "-type", "f", "-delete") | |
@staticmethod | |
def create(args): | |
name = args.name | |
guest = LXCGuest(name) | |
guest.create_container(args) | |
guest.create_fstab() | |
guest.create_config(args) | |
with guest.mount(): | |
guest.set_root_password(args.password) | |
guest.deploy_ssh_keys(args.keys) | |
with guest.disable_services(): | |
guest.regenerate_ssh_keys() | |
guest.configure_apt(args) | |
# Reset resolv.conf, must be call at the end | |
guest.configure_network(args) | |
guest.clean() | |
local_name = socket.gethostname() | |
local_name = re.escape(local_name + ".") | |
local_fqdn = socket.getfqdn() | |
local_domain = re.sub("^" + local_name, "", local_fqdn) | |
parser = argparse.ArgumentParser(description="Create LXC guest") | |
parser.add_argument("-f", "--force", | |
help="In case of error, force creation if possible", | |
action="store_true") | |
parser.add_argument("-n", "--name", help="Guest name", required=True) | |
parser.add_argument("-H", "--hostname", help="Hostname (default to $name)") | |
parser.add_argument("-d", "--domain", help="Domain (default to host domain)", | |
default=local_domain) | |
parser.add_argument("-T", "--template", help="LXC template", nargs="?", | |
const="debian") | |
parser.add_argument("-s", "--size", help="Filesystem size", default="5G") | |
parser.add_argument("-c", "--clone", help="Clone from existing LXC guest") | |
parser.add_argument("-t", "--tar", help="Clone from archive") | |
parser.add_argument("-m", "--memory", help="Cap memory") | |
parser.add_argument("-w", "--swap", help="Cap swap") | |
parser.add_argument("-b", "--bridge", help="Bridge to use (default br1)", | |
default="br1") | |
parser.add_argument("-i", "--ip", help="IP address (CIDR notation IP/netmask)", | |
required=True) | |
parser.add_argument("-g", "--gateway", help="Gateway IP address", default=True) | |
parser.add_argument("-p", "--password", nargs="?", const=False, | |
help="Root password (set to empty to disable root login)") | |
parser.add_argument("-k", "--keys", nargs="+", help="SSH keys to deploy") | |
parser.add_argument("-P", "--apt-proxy", | |
help="APT proxy to use (default apt.$domain)", | |
nargs="?", const=True) | |
args = parser.parse_args() | |
if not args.hostname: | |
args.hostname = args.name | |
LXCGuest.create(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment