-
-
Save chenhan1218/aa6e3eb2865256e48540 to your computer and use it in GitHub Desktop.
develop-in-lxc.py
This file contains 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 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