snippetpythonMinor
LXC Bootstrap - A wrapper script around lxc utility scripts to create and set up an LXC container based on settings
Viewed 0 times
scriptaroundscriptssettingscontainercreatebootstraplxcutilitywrapper
Problem
So some background here.
I recently discovered the ease of LXC containers on Ubuntu. I've been building some 'test' containers locally for testing things I develop on Ubuntu, but the evil thing is that creating these containers then getting all the software I want in the container and everything set up the way I want it to be set up is not the most trivial of processes. First you have to create the container, then start the container, then run a long
Well, I found this quite annoying, as I need a 'base container' with Ubuntu Server in a userspace-usable format, which there is no 'base template' for. So, I wrote this, what I call
And it's got a set of arguments that can be parsed to override standard bootstrap settings, such as different OSes, different release versions, different architectures, different packages or users to "bootstrap" the system with, etc. It can also run the bootstrapping process on existing containers, without creating a new one, with a specific flag.
Any suggestions for improvement are welcome, though I'm definitely using a lot of code that can probably be condensed or otherwise optimized, so such suggestions are most welcome above most other code suggestions. The code is here below.
Oh, and since I'm so picky about PEP8 style guidelines, I've got Flake8 running in CI from the repository, so it catches most evils. Note that per PEP8, a team/individual can choose to use a longer line length than 80 but no more than 120; I'm using a max line length of 120 in this case, so please
I recently discovered the ease of LXC containers on Ubuntu. I've been building some 'test' containers locally for testing things I develop on Ubuntu, but the evil thing is that creating these containers then getting all the software I want in the container and everything set up the way I want it to be set up is not the most trivial of processes. First you have to create the container, then start the container, then run a long
lxc-attach -n CONTAINERNAME -- [command] string depending on the command you want to execute (unless you want to drop into a shell, in which case you omit the -- [command] part), then you have to set up the things by hand with commands in the container.Well, I found this quite annoying, as I need a 'base container' with Ubuntu Server in a userspace-usable format, which there is no 'base template' for. So, I wrote this, what I call
lxc_bootstrap.py, which creates the container, starts the container, installs the packages I want on the image, sets up users per my needs, and then is done, and the container is running and ready for use.And it's got a set of arguments that can be parsed to override standard bootstrap settings, such as different OSes, different release versions, different architectures, different packages or users to "bootstrap" the system with, etc. It can also run the bootstrapping process on existing containers, without creating a new one, with a specific flag.
Any suggestions for improvement are welcome, though I'm definitely using a lot of code that can probably be condensed or otherwise optimized, so such suggestions are most welcome above most other code suggestions. The code is here below.
Oh, and since I'm so picky about PEP8 style guidelines, I've got Flake8 running in CI from the repository, so it catches most evils. Note that per PEP8, a team/individual can choose to use a longer line length than 80 but no more than 120; I'm using a max line length of 120 in this case, so please
Solution
I think you are using classes wrong. You use them only to have nice namespaces and do all the stuff you would normally do as methods outside of the class. And then you suppress the warnings about non-existing constructors...
In my opinion, you should have a
Going from top to bottom, here are the changes I made to your code:
I defined a
The
Next, the most important part, the
I also used
Almost all stand-alone functions of your code are now methods of this class. It can create and/or start a container, create the users and install packages.
The
The
Bug:
Your code has the subtle bug that all users will be admins. This is because in Python all non-empty strings are truthy:
In my original code I had this turned around, by calling
To fix this, I now compare the
Final code:
```
#!/usr/bin/python
# lxc_bootstrap
# Copyright (C) 2017 Thomas Ward
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
# LXC Bootstrapper, around the lxc-create 'Download' template for userspace
# containers; creates then modifies the container based on specifications.
#
# Designed for Ubuntu / Debian systems.
# import os
import sys
import argparse
import crypt
import subprocess as sp
import random
import platform
from string import ascii_letters, digits
SHADOW_SALT_CHARSET = ascii_letters + digits
RANDOM_ENGINE = random.SystemRandom()
class TransparentDict(dict):
def __missing__(self, key):
return key
ARCHITECTURE_MAP = TransparentDict({
'x86_64': 'amd64',
'x86': 'i386',
'armv7l': 'armhf',
'armv8l': 'arm64'
})
class User:
def __init__(self, name, password, salt, admin):
self.name = name
self.password = password
self.salt = salt
if not self.salt or len(self.salt) != 8:
# Create a random salt
self.salt = "".join(RANDOM_ENGINE.choice(SHADOW_SALT_CHARSET) for _ in range(8))
self.admin = admin in (True, "True")
@property
def shadow_password(self):
return crypt.crypt(self.password, ('$6${}$'.format(self.salt)))
class Container:
create_cmd = "/usr/bin/lxc-create -t download -n {0.name} -- -d {0.distribution} -r {0.release} -a {0.architecture}"
start_cmd = "lxc-start -n {0.name}"
attach_cmd = "/usr/bin/lxc-attach -n {0.name} -- "
def __init__(self, name, architecture, distribution, release):
self.name = name
self.architecture = architecture
self.distribution = distribution
self.release = release
self.attach = self.attach_cmd.format(self).split()
def __call__(self, cmd, error_msg="", attach=True):
cmd = cmd.split()
if attach:
cmd = self.attach + cmd
try:
sp.check_call(cmd, stdout=sys.stdout, stderr=sys.stderr)
except sp.CalledProcessError as e:
if error_msg:
print error_msg
print e
sys.exit()
raise e
def create(self):
cmd = self.create_cmd.format(self)
self(cmd, "Something went wrong when creating the container.", attach=False)
def start(self):
In my opinion, you should have a
User class that stores the info of a user and has a @property that returns the shadow password. Then, there is a Container class, that actually does the stuff. You don't need the Bootstrap, Customization, Packages classes at all.Going from top to bottom, here are the changes I made to your code:
I defined a
TransparentDict class that passes undefined keys right through, but returns the specified value for the defined keys.The
User class started life as a collections.namedtuple, but is a full class now, so it can have the shadow_password property.Next, the most important part, the
Container class. It has a __call__ method defined, which allows running arbitrary commands in the container and possibly giving it a fail string.I also used
str.format instead of the old % formatting throughout.Almost all stand-alone functions of your code are now methods of this class. It can create and/or start a container, create the users and install packages.
The
_parse_arguments does now some operation on the parsed args before returning them, so we have sensible things in there.The
run function is now slightly longer, because some stuff is in there, but in my opinion more readable now.Bug:
Your code has the subtle bug that all users will be admins. This is because in Python all non-empty strings are truthy:
>>> bool("True")
True
>>> bool("False")
TrueIn my original code I had this turned around, by calling
int and not bool, thinking you had a 0 or 1 in your userfile. However, you have a string "True" or "False" there. This will raise a ValueError for all users except the default user:>>> int("True")
ValueError: invalid literal for int() with base 10: 'True'To fix this, I now compare the
admin parameter with both True and "True".Final code:
```
#!/usr/bin/python
# lxc_bootstrap
# Copyright (C) 2017 Thomas Ward
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
# LXC Bootstrapper, around the lxc-create 'Download' template for userspace
# containers; creates then modifies the container based on specifications.
#
# Designed for Ubuntu / Debian systems.
# import os
import sys
import argparse
import crypt
import subprocess as sp
import random
import platform
from string import ascii_letters, digits
SHADOW_SALT_CHARSET = ascii_letters + digits
RANDOM_ENGINE = random.SystemRandom()
class TransparentDict(dict):
def __missing__(self, key):
return key
ARCHITECTURE_MAP = TransparentDict({
'x86_64': 'amd64',
'x86': 'i386',
'armv7l': 'armhf',
'armv8l': 'arm64'
})
class User:
def __init__(self, name, password, salt, admin):
self.name = name
self.password = password
self.salt = salt
if not self.salt or len(self.salt) != 8:
# Create a random salt
self.salt = "".join(RANDOM_ENGINE.choice(SHADOW_SALT_CHARSET) for _ in range(8))
self.admin = admin in (True, "True")
@property
def shadow_password(self):
return crypt.crypt(self.password, ('$6${}$'.format(self.salt)))
class Container:
create_cmd = "/usr/bin/lxc-create -t download -n {0.name} -- -d {0.distribution} -r {0.release} -a {0.architecture}"
start_cmd = "lxc-start -n {0.name}"
attach_cmd = "/usr/bin/lxc-attach -n {0.name} -- "
def __init__(self, name, architecture, distribution, release):
self.name = name
self.architecture = architecture
self.distribution = distribution
self.release = release
self.attach = self.attach_cmd.format(self).split()
def __call__(self, cmd, error_msg="", attach=True):
cmd = cmd.split()
if attach:
cmd = self.attach + cmd
try:
sp.check_call(cmd, stdout=sys.stdout, stderr=sys.stderr)
except sp.CalledProcessError as e:
if error_msg:
print error_msg
print e
sys.exit()
raise e
def create(self):
cmd = self.create_cmd.format(self)
self(cmd, "Something went wrong when creating the container.", attach=False)
def start(self):
Code Snippets
>>> bool("True")
True
>>> bool("False")
True>>> int("True")
ValueError: invalid literal for int() with base 10: 'True'#!/usr/bin/python
# lxc_bootstrap
# Copyright (C) 2017 Thomas Ward <teward@ubuntu.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# LXC Bootstrapper, around the lxc-create 'Download' template for userspace
# containers; creates then modifies the container based on specifications.
#
# Designed for Ubuntu / Debian systems.
# import os
import sys
import argparse
import crypt
import subprocess as sp
import random
import platform
from string import ascii_letters, digits
SHADOW_SALT_CHARSET = ascii_letters + digits
RANDOM_ENGINE = random.SystemRandom()
class TransparentDict(dict):
def __missing__(self, key):
return key
ARCHITECTURE_MAP = TransparentDict({
'x86_64': 'amd64',
'x86': 'i386',
'armv7l': 'armhf',
'armv8l': 'arm64'
})
class User:
def __init__(self, name, password, salt, admin):
self.name = name
self.password = password
self.salt = salt
if not self.salt or len(self.salt) != 8:
# Create a random salt
self.salt = "".join(RANDOM_ENGINE.choice(SHADOW_SALT_CHARSET) for _ in range(8))
self.admin = admin in (True, "True")
@property
def shadow_password(self):
return crypt.crypt(self.password, ('$6${}$'.format(self.salt)))
class Container:
create_cmd = "/usr/bin/lxc-create -t download -n {0.name} -- -d {0.distribution} -r {0.release} -a {0.architecture}"
start_cmd = "lxc-start -n {0.name}"
attach_cmd = "/usr/bin/lxc-attach -n {0.name} -- "
def __init__(self, name, architecture, distribution, release):
self.name = name
self.architecture = architecture
self.distribution = distribution
self.release = release
self.attach = self.attach_cmd.format(self).split()
def __call__(self, cmd, error_msg="", attach=True):
cmd = cmd.split()
if attach:
cmd = self.attach + cmd
try:
sp.check_call(cmd, stdout=sys.stdout, stderr=sys.stderr)
except sp.CalledProcessError as e:
if error_msg:
print error_msg
print e
sys.exit()
raise e
def create(self):
cmd = self.create_cmd.format(self)
self(cmd, "Something went wrong when creating the container.", attach=False)
def start(self):
cmd = self.start_cmd.format(self)
self(cmd,Context
StackExchange Code Review Q#155295, answer score: 2
Revisions (0)
No revisions yet.