Skip to content

Instantly share code, notes, and snippets.

@Roadmaster
Created February 2, 2015 21:14
Show Gist options
  • Save Roadmaster/df0778c93a716ae96193 to your computer and use it in GitHub Desktop.
Save Roadmaster/df0778c93a716ae96193 to your computer and use it in GitHub Desktop.
develop-in-lxc.py
#!/usr/bin/env python3
#
# Copyright 2015 Canonical Ltd.
#
# Authors:
# Daniel Manrique <[email protected]>
#
# develop-in-lxc is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as
# published by the Free Software Foundation.
#
# develop-in-lxc 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
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with develop-in-lxc. If not, see
# <http://www.gnu.org/licenses/>.
from argparse import ArgumentParser
import configparser
import logging
import lxc
import os
import random
import shlex
import string
import textwrap
import unittest
class DLXCtests(unittest.TestCase):
def setUp(self):
self.cont_name = "".join([random.choice(string.ascii_lowercase)
for i in range(16)])
cont = lxc.Container(self.cont_name)
create_result = create_container(cont)
self.assertTrue(create_result)
self.cont = cont
def tearDown(self):
stop_result = self.cont.stop()
self.cont.wait("STOPPED", 10)
destroy_result = self.cont.destroy()
self.assertTrue(stop_result)
self.assertTrue(destroy_result)
def test_create_project_mount(self):
# "touch" a file
with open("test-file", "w") as testfile:
testfile.write("codeword")
create_project_mount(self.cont, "/in-container")
self.cont.start()
self.cont.wait("RUNNING", 10)
run_result = self.cont.attach_wait(lxc.attach_run_command,
['grep', 'codeword',
'/in-container/test-file'])
os.unlink("test-file")
self.assertEqual(0, run_result)
def test_provision_container_ok(self):
"""
Test that provisioning script runs as expected, by having
it return a weird exit code and then checking that.
"""
with open("test-script", "w") as testscript:
testscript.write("#!/bin/bash\n")
testscript.write("exit 149\n")
os.chmod("test-script", 0o755)
create_project_mount(self.cont, "/in-container")
self.cont.start()
self.cont.wait("RUNNING", 10)
run_result = self.cont.attach_wait(lxc.attach_run_command,
['/in-container/test-script'])
# The run_result has to be shifted bitwise 8 bits to the left
# to get the actual return code
os.unlink("test-script")
self.assertEqual(149, run_result >> 8)
def create_container(devcontainer, verbose=False):
logging.info("Creating container, this may take a few minutes.")
if verbose:
quiet_level = 0
else:
quiet_level = lxc.LXC_CREATE_QUIET
if not devcontainer.create("ubuntu", quiet_level,
{"release": "trusty",
"arch": "amd64"}):
print("Failed to create container")
return False
return True
def create_project_mount(devcontainer, container_mount):
dest_mount = os.path.join(devcontainer.get_config_item('lxc.rootfs'),
container_mount.lstrip("/"))
src_mount = os.getcwd()
try:
os.mkdir(dest_mount)
except OSError as err:
logging.error("Unable to create %s: %s ", dest_mount, err)
return False
mount_entry = "{src} {dest} none bind 0 0".format(src=src_mount,
dest=dest_mount)
devcontainer.append_config_item('lxc.mount.entry', mount_entry)
devcontainer.save_config()
logging.info("Contents of {src} will be "
"available in the container in "
"{dest}".format(src=src_mount,
dest=container_mount))
return True
def provision_container(container, provision_scripts, container_mount):
for script in provision_scripts:
command = shlex.split(os.path.join(container_mount, script))
logging.info("Running %s", " ".join(command))
result = container.attach_wait(lxc.attach_run_command, command)
logging.debug("Return code from command was %s", result)
if result != 0:
return False
return True
def start_container(container):
if not container.start():
return False
if not container.wait("RUNNING", 5):
return False
ips = container.get_ips(timeout=30)
if not ips:
return False
else:
print("Container '{}' is up,"
"access with ssh ubuntu@{}".format(
container.name, ips[0]))
return True
def get_container_name(configfile):
config = configparser.ConfigParser()
try:
config.read(configfile)
except (configparser.DuplicateOptionError,
configparser.MissingSectionHeaderError) as err:
logging.error("Unable to read state file: %s", err)
name = ""
if 'develop-in-lxc' in config:
if 'container-name' in config['develop-in-lxc']:
name = config['develop-in-lxc']['container-name']
return name
def store_container_name(configfile, container_name):
with open(configfile, "w") as cfile:
config = configparser.ConfigParser()
config['develop-in-lxc'] = {'container-name': str(container_name)}
config.write(cfile)
def report_container_ip(container):
if container.defined and container.running:
print("IP for {} is {}".format(container.name,
container.get_ips(timeout=10)[0]))
def parse_args():
parser = ArgumentParser("Create a development container")
parser.add_argument("-c", "--configfile", type=str,
default='develop-in-lxc.cfg',
help="""Name of the config file to read. Defaults
to %(default)s.""")
parser.add_argument('-d', '--debug', dest='log_level',
action="store_const", const=logging.DEBUG,
default=logging.INFO, help="Show debugging messages")
parser.add_argument('-n', '--container-name',
help="""Name of the container to create. If not
specified, a default name will
be generated.""")
return parser.parse_args()
def main():
"""
Develop-in-lxc
==============
This script helps you set up a development environment under LXC
to work with your project. You can also conveniently use the same script to
start the container up once it's been provisioned, or you can
lxc-start the container in the traditional way.
What it does
------------
0- First it sees if the container was already created or is running.
If so, it tells you it's running or starts the existing container.
1- Creates the container.
2- Configures it so that the directory containing the source tree
(the $PWD) is mounted in the container.
3- Starts the container and runs (using lxc-attach) the scripts
enumerated in provision_scripts, in the given order.
The container's name, project name, Ubuntu series for the container and
mount point are defined in a config file (develop-in-lxc.cfg by
default, or point the script to your own using the -c parameter).
Example config file (standard Python .ini format)
-------------------------------------------------
[develop-in-lxc]
project_name = project
# Which Ubuntu series to use to deploy the development container.
container_series = trusty
# Where in the container to mount the current working tree
container_mount = /project
# Scripts to run, in order, once container is created for the first time.
# This can be a single value or a multi-line value, in which case each
# line is a script to run.
# Each line can contain parameters for the script as well.
provision_scripts = provision-script /project-mount
second-provision-cript /project-mount something
Multi-container/multi-config behavior
-------------------------------------
Complex projects may require more than one container for development, for
instance, one to run the application server and another to run the
database. This can be handled by shipping multiple config files, with
different project names and possibly different provisioning scripts.
You can specify the -c parameter to decide which config file to read for
setting up a container.
Furthermore, developers may want to have different branches of the same
project/code base simultaneously. Because the container mounts a specific
directory from the host system, the same container can't be reused if these
branches exist in different directories (it would only mount the one from
which the container was first created). If you try to use develop-in-lxc
from a branch of the same project, the script will detect that the existing
container was created from another branch, and ask you to create a
container with a different name. For this, simply specify the -n parameter
and give any name you like for the container. On subsequent invocations,
the script will remember the custom name and correctly start the container
that matches the current branch.
The above behavior is consistent even if you use a different config file
(-c) because the project name from the config file is taken into account
when storing container state.
This should accomodate the 3 main use cases:
1- Casual developer wanting to fix a single issue in project. Simply uses
develop-in-lxc.py with no arguments to get a single container. Could
be using bzr but will not be expected to have more than one
branch at a time.
2- Developer using bzr. Can use develop-in-lxc.py with -n to create
a container for each branch in progress (bugfixes, features, etc).
3- Developer using git or git-lp. Can use develop-in-lxc.py with
no arguments to reuse a single container and simply use git's in-place
branch switching to decide what to work on.
"""
options = parse_args()
logging.basicConfig(level=options.log_level)
try:
config = configparser.ConfigParser()
logging.info("Reading config from %s", options.configfile)
config.read(options.configfile)
except (configparser.DuplicateOptionError,
configparser.MissingSectionHeaderError) as err:
logging.error("Unable to read configuration file: %s", err)
container_series = config['develop-in-lxc'].get('container_series',
'trusty')
project_name = config['develop-in-lxc'].get('project_name', 'project')
container_mount = config['develop-in-lxc'].get('container_mount',
'/development')
# Where to store container name data for this project.
container_name_file = ".develop-in-lxc-{}".format(project_name)
# Determine container name. First, we assume default:
container_name = "{project}-{series}-development".format(
project=project_name,
series=container_series)
# Next, if we already had created a container, assume we should
# use that:
if get_container_name(container_name_file):
container_name = get_container_name(container_name_file)
# Finally, if user forced a new container name, use *that*
if options.container_name:
container_name = options.container_name
provision_scripts = [script.lstrip("/") for script in
config['develop-in-lxc'].get('provision_scripts',
'').splitlines()]
logging.info("File to keep container name: %s", container_name_file)
logging.info("Will create a %s container with %s", container_name,
container_series)
logging.info("Directory %s will be mounted "
"in %s in container", os.getcwd(),
container_mount)
logging.info("Container provisioning scripts "
"(relative to container's %s): %s",
container_mount, provision_scripts)
if not os.getuid() == 0:
print("I need root privileges to create and manage LXC containers.")
print("run me with sudo.")
return 1
# Is the container name valid? If a container by that name already
# exists BUT I didn't create it, it means I can't use it.
if (container_name in lxc.list_containers() and
container_name != get_container_name(container_name_file)):
m = ("A container by that name already exists but I didn't create it. "
"This means it's probably mounting a different source directory, "
"so it will likely not work as expected. Please specify a "
"diffferent container name with -n. You only have to do this "
" once, "
"afterwards I'll remember the container name and you can use "
"this script to start it when needed.")
print("\nATTENTION:\n\n{}".format(textwrap.fill(m, 72)))
raise SystemExit(1)
# OK, we're going to create the container, so note the name that
# has been chosen
store_container_name(container_name_file, container_name)
devcontainer = lxc.Container(container_name)
if not devcontainer.defined:
if not (create_container(devcontainer) and
create_project_mount(devcontainer, container_mount) and
start_container(devcontainer)):
print("Can't create/configure/start container :(")
else:
if provision_container(devcontainer,
provision_scripts, container_mount):
print("Container successfully provisioned!")
report_container_ip(devcontainer)
else:
print("Can't provision the container for development :(")
else:
if not devcontainer.state == 'STOPPED':
print("Container seems to be running already")
report_container_ip(devcontainer)
else:
print("Starting existing container")
start_container(devcontainer)
report_container_ip(devcontainer)
if __name__ == '__main__':
raise SystemExit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment