Merge branch 'WIP-partitioning' into python

This commit is contained in:
Anders Ingemann 2013-11-06 22:36:37 +01:00
commit f33cc3ab69
91 changed files with 1784 additions and 1273 deletions

View file

@ -14,10 +14,17 @@ Pull requests are also welcome!
Dependencies Dependencies
------------ ------------
You will need to run debian wheezy with **python 2.7** and **debootstrap** installed. You will need to run debian wheezy with **python 2.7** and **debootstrap** installed.
Other depencies include:
* qemu-utils
* parted
* grub2
* euca2ools
* xfsprogs (If you want to use XFS as a filesystem)
Also the following python libraries are required: Also the following python libraries are required:
* **boto** * **boto**
* **jsomschema** ([version 2.0.0](https://pypi.python.org/pypi/jsonschema), only available through pip) * **jsomschema** ([version 2.0.0](https://pypi.python.org/pypi/jsonschema), only available through pip)
* **termcolor** * **termcolor**
* **fysom**
Bootstrapping instance store AMIs requires **euca2ools** to be installed. Bootstrapping instance store AMIs requires **euca2ools** to be installed.

View file

@ -3,6 +3,11 @@
class BootstrapInformation(object): class BootstrapInformation(object):
def __init__(self, manifest=None, debug=False): def __init__(self, manifest=None, debug=False):
self.manifest = manifest self.manifest = manifest
from fs import load_volume
self.volume = load_volume(self.manifest.volume)
self.debug = debug self.debug = debug
import random import random
self.run_id = random.randrange(16 ** 8) self.run_id = random.randrange(16 ** 8)
import os.path
workspace_dirname = '{id:x}'.format(id=self.run_id)
self.workspace = os.path.join(manifest.bootstrapper['workspace'], workspace_dirname)

20
base/fs/__init__.py Normal file
View file

@ -0,0 +1,20 @@
def load_volume(data):
from common.fs.loopbackvolume import LoopbackVolume
from providers.ec2.ebsvolume import EBSVolume
from providers.virtualbox.volume import VirtualBoxVolume
from partitionmaps.gpt import GPTPartitionMap
from partitionmaps.mbr import MBRPartitionMap
from partitionmaps.none import NoPartitions
partition_maps = {'none': NoPartitions,
'gpt': GPTPartitionMap,
'mbr': MBRPartitionMap,
}
partition_map = partition_maps.get(data['partitions']['type'])(data['partitions'])
volume_backings = {'raw': LoopbackVolume,
's3': LoopbackVolume,
'vdi': VirtualBoxVolume,
'ebs': EBSVolume
}
return volume_backings.get(data['backing'])(partition_map)

8
base/fs/exceptions.py Normal file
View file

@ -0,0 +1,8 @@
class VolumeError(Exception):
pass
class PartitionError(Exception):
pass

View file

@ -0,0 +1,78 @@
from abc import ABCMeta
from abc import abstractmethod
from common.tools import log_check_call
from common.fsm_proxy import FSMProxy
from ..exceptions import PartitionError
class AbstractPartitionMap(FSMProxy):
__metaclass__ = ABCMeta
events = [{'name': 'create', 'src': 'nonexistent', 'dst': 'unmapped'},
{'name': 'map', 'src': 'unmapped', 'dst': 'mapped'},
{'name': 'unmap', 'src': 'mapped', 'dst': 'unmapped'},
]
def __init__(self):
cfg = {'initial': 'nonexistent', 'events': self.events, 'callbacks': {}}
super(AbstractPartitionMap, self).__init__(cfg)
def is_blocking(self):
return self.fsm.current == 'mapped'
def get_total_size(self):
return sum(p.size for p in self.partitions)
def create(self, volume):
self.fsm.create(volume=volume)
@abstractmethod
def _before_create(self, event):
pass
def map(self, volume):
self.fsm.map(volume=volume)
def _before_map(self, event):
volume = event.volume
try:
mappings = log_check_call(['/sbin/kpartx', '-l', volume.device_path])
import re
regexp = re.compile('^(?P<name>.+[^\d](?P<p_idx>\d+)) : '
'(?P<start_blk>\d) (?P<num_blks>\d+) '
'{device_path} (?P<blk_offset>\d+)$'
.format(device_path=volume.device_path))
log_check_call(['/sbin/kpartx', '-a', volume.device_path])
import os.path
for mapping in mappings:
match = regexp.match(mapping)
if match is None:
raise PartitionError('Unable to parse kpartx output: {line}'.format(line=mapping))
partition_path = os.path.join('/dev/mapper', match.group('name'))
p_idx = int(match.group('p_idx'))-1
self.partitions[p_idx].map(partition_path)
for idx, partition in enumerate(self.partitions):
if partition.fsm.current not in ['mapped', 'formatted']:
raise PartitionError('kpartx did not map partition #{idx}'.format(idx=idx+1))
except PartitionError as e:
for partition in self.partitions:
if not partition.fsm.can('unmap'):
partition.unmap()
log_check_call(['/sbin/kpartx', '-d', volume.device_path])
raise e
def unmap(self, volume):
self.fsm.unmap(volume=volume)
def _before_unmap(self, event):
volume = event.volume
for partition in self.partitions:
if partition.fsm.cannot('unmap'):
msg = 'The partition {partition} prevents the unmap procedure'.format(partition=partition)
raise PartitionError(msg)
log_check_call(['/sbin/kpartx', '-d', volume.device_path])
for partition in self.partitions:
partition.unmap()

View file

@ -0,0 +1,36 @@
from abstract import AbstractPartitionMap
from ..partitions.gpt import GPTPartition
from ..partitions.gpt_swap import GPTSwapPartition
from common.tools import log_check_call
class GPTPartitionMap(AbstractPartitionMap):
def __init__(self, data):
self.partitions = []
if 'boot' in data:
self.boot = GPTPartition(data['boot']['size'], data['boot']['filesystem'], 'boot', None)
self.partitions.append(self.boot)
self.root = GPTPartition(data['root']['size'], data['root']['filesystem'], 'root',
getattr(self, 'boot', None))
self.partitions.append(self.root)
if 'swap' in data:
self.swap = GPTSwapPartition(data['swap']['size'], self.root)
self.partitions.append(self.swap)
super(GPTPartitionMap, self).__init__()
def _before_create(self, event):
volume = event.volume
log_check_call(['/sbin/parted', '--script', '--align', 'none', volume.device_path,
'--', 'mklabel', 'gpt'])
for partition in self.partitions:
partition.create(volume)
boot_idx = self.root.get_index()
if hasattr(self, 'boot'):
boot_idx = self.boot.get_index()
log_check_call(['/sbin/parted', '--script', volume.device_path,
'--', 'set ' + str(boot_idx) + ' boot on'])
log_check_call(['/sbin/parted', '--script', volume.device_path,
'--', 'set ' + str(boot_idx) + ' bios_grub on'])

View file

@ -0,0 +1,37 @@
from abstract import AbstractPartitionMap
from ..partitions.mbr import MBRPartition
from ..partitions.mbr_swap import MBRSwapPartition
from common.tools import log_check_call
class MBRPartitionMap(AbstractPartitionMap):
def __init__(self, data):
self.partitions = []
if 'boot' in data:
self.boot = MBRPartition(data['boot']['size'], data['boot']['filesystem'], None)
self.partitions.append(self.boot)
self.root = MBRPartition(data['root']['size'], data['root']['filesystem'],
getattr(self, 'boot', None))
self.partitions.append(self.root)
if 'swap' in data:
self.swap = MBRSwapPartition(data['swap']['size'], self.root)
self.partitions.append(self.swap)
super(MBRPartitionMap, self).__init__()
def get_total_size(self):
return sum(p.size for p in self.partitions) + 1 # Post-MBR gap for embedding grub
def _before_create(self, event):
volume = event.volume
log_check_call(['/sbin/parted', '--script', '--align', 'none', volume.device_path,
'--', 'mklabel', 'msdos'])
for partition in self.partitions:
partition.create(volume)
boot_idx = self.root.get_index()
if hasattr(self, 'boot'):
boot_idx = self.boot.get_index()
log_check_call(['/sbin/parted', '--script', volume.device_path,
'--', 'set ' + str(boot_idx) + ' boot on'])

View file

@ -0,0 +1,15 @@
from ..partitions.single import SinglePartition
class NoPartitions(object):
def __init__(self, data):
root = data['root']
self.root = SinglePartition(root['size'], root['filesystem'])
self.partitions = [self.root]
def is_blocking(self):
return self.root.fsm == 'mounted'
def get_total_size(self):
return self.root.size

View file

View file

@ -0,0 +1,41 @@
from abc import ABCMeta
from common.tools import log_check_call
from common.fsm_proxy import FSMProxy
class AbstractPartition(FSMProxy):
__metaclass__ = ABCMeta
events = [{'name': 'create', 'src': 'nonexistent', 'dst': 'created'},
{'name': 'format', 'src': 'created', 'dst': 'formatted'},
{'name': 'mount', 'src': 'formatted', 'dst': 'mounted'},
{'name': 'unmount', 'src': 'mounted', 'dst': 'formatted'},
]
def __init__(self, size, filesystem):
self.size = size
self.filesystem = filesystem
self.device_path = None
cfg = {'initial': 'nonexistent', 'events': self.events, 'callbacks': {}}
super(AbstractPartition, self).__init__(cfg)
def get_uuid(self):
[uuid] = log_check_call(['/sbin/blkid', '-s', 'UUID', '-o', 'value', self.device_path])
return uuid
def _before_format(self, e):
mkfs = '/sbin/mkfs.{fs}'.format(fs=self.filesystem)
log_check_call([mkfs, self.device_path])
def mount(self, destination):
self.fsm.mount(destination=destination)
def _before_mount(self, e):
log_check_call(['/bin/mount', '--types', self.filesystem, self.device_path, e.destination])
self.mount_dir = e.destination
def _before_unmount(self, e):
log_check_call(['/bin/umount', self.mount_dir])
del self.mount_dir

View file

@ -0,0 +1,43 @@
from abstract import AbstractPartition
class BasePartition(AbstractPartition):
events = [{'name': 'create', 'src': 'nonexistent', 'dst': 'unmapped'},
{'name': 'map', 'src': 'unmapped', 'dst': 'mapped'},
{'name': 'format', 'src': 'mapped', 'dst': 'formatted'},
{'name': 'mount', 'src': 'formatted', 'dst': 'mounted'},
{'name': 'unmount', 'src': 'mounted', 'dst': 'formatted'},
{'name': 'unmap', 'src': 'formatted', 'dst': 'unmapped_fmt'},
{'name': 'map', 'src': 'unmapped_fmt', 'dst': 'formatted'},
{'name': 'unmap', 'src': 'mapped', 'dst': 'unmapped'},
]
def __init__(self, size, filesystem, previous):
self.previous = previous
super(BasePartition, self).__init__(size, filesystem)
def create(self, volume):
self.fsm.create(volume=volume)
def get_index(self):
if self.previous is None:
return 1
else:
return self.previous.get_index()+1
def get_start(self):
if self.previous is None:
return 0
else:
return self.previous.get_start() + self.previous.size
def map(self, device_path):
self.fsm.map(device_path=device_path)
def _before_map(self, e):
self.device_path = e.device_path
def _before_unmap(self, e):
self.device_path = None

19
base/fs/partitions/gpt.py Normal file
View file

@ -0,0 +1,19 @@
from common.tools import log_check_call
from base import BasePartition
class GPTPartition(BasePartition):
def __init__(self, size, filesystem, name, previous):
self.name = name
super(GPTPartition, self).__init__(size, filesystem, previous)
def _before_create(self, e):
start = self.get_start()
# {name} only works for gpt, for msdos that becomes the part-type (primary, extended, logical)
parted_command = ('mkpart primary {start}MiB {end}MiB'
.format(name=self.name,
start=str(start),
end=str(start + self.size)))
log_check_call(['/sbin/parted', '--script', '--align', 'none', e.volume.device_path,
'--', parted_command])

View file

@ -0,0 +1,11 @@
from common.tools import log_check_call
from gpt import GPTPartition
class GPTSwapPartition(GPTPartition):
def __init__(self, size, previous):
super(GPTSwapPartition, self).__init__(size, 'swap', 'swap', previous)
def _before_format(self, e):
log_check_call(['/sbin/mkswap', self.device_path])

19
base/fs/partitions/mbr.py Normal file
View file

@ -0,0 +1,19 @@
from common.tools import log_check_call
from base import BasePartition
class MBRPartition(BasePartition):
def get_start(self):
if self.previous is None:
return 1 # Post-MBR gap for embedding grub
else:
return self.previous.get_start() + self.previous.size
def _before_create(self, e):
start = self.get_start()
parted_command = ('mkpart primary {start}MiB {end}MiB'
.format(start=str(start),
end=str(start + self.size)))
log_check_call(['/sbin/parted', '--script', '--align', 'none', e.volume.device_path,
'--', parted_command])

View file

@ -0,0 +1,11 @@
from common.tools import log_check_call
from mbr import MBRPartition
class MBRSwapPartition(MBRPartition):
def __init__(self, size, previous):
super(MBRSwapPartition, self).__init__(size, 'swap', previous)
def _before_format(self, e):
log_check_call(['/sbin/mkswap', self.device_path])

View file

@ -0,0 +1,5 @@
from abstract import AbstractPartition
class SinglePartition(AbstractPartition):
pass

64
base/fs/volume.py Normal file
View file

@ -0,0 +1,64 @@
from abc import ABCMeta
from common.fsm_proxy import FSMProxy
from common.tools import log_check_call
from exceptions import VolumeError
from partitionmaps.none import NoPartitions
class Volume(FSMProxy):
__metaclass__ = ABCMeta
events = [{'name': 'create', 'src': 'nonexistent', 'dst': 'detached'},
{'name': 'attach', 'src': 'detached', 'dst': 'attached'},
{'name': 'detach', 'src': 'attached', 'dst': 'detached'},
{'name': 'delete', 'src': 'detached', 'dst': 'deleted'},
]
def __init__(self, partition_map):
self.device_path = None
self.specials_mounted = False
self.partition_map = partition_map
self.size = self.partition_map.get_total_size()
callbacks = {'onbeforedetach': self._check_blocking}
if isinstance(self.partition_map, NoPartitions):
def set_dev_path(e):
self.partition_map.root.device_path = self.device_path
callbacks['onafterattach'] = set_dev_path
cfg = {'initial': 'nonexistent', 'events': self.events, 'callbacks': callbacks}
super(Volume, self).__init__(cfg)
def _after_create(self, e):
if isinstance(self.partition_map, NoPartitions):
self.partition_map.root.create()
def can_mount_specials(self):
return self.fsm.current == 'attached'
def mount_specials(self):
if self.specials_mounted:
raise VolumeError('The special devices are already mounted')
root = self.partition_map.root.mount_dir
log_check_call(['/bin/mount', '--bind', '/dev', '{root}/dev'.format(root=root)])
log_check_call(['/usr/sbin/chroot', root, '/bin/mount', '--types', 'proc', 'none', '/proc'])
log_check_call(['/usr/sbin/chroot', root, '/bin/mount', '--types', 'sysfs', 'none', '/sys'])
log_check_call(['/usr/sbin/chroot', root, '/bin/mount', '--types', 'devpts', 'none', '/dev/pts'])
self.specials_mounted = True
def unmount_specials(self):
if not self.specials_mounted:
raise VolumeError('The special devices are not mounted')
root = self.partition_map.root.mount_dir
log_check_call(['/usr/sbin/chroot', root, '/bin/umount', '/dev/pts'])
log_check_call(['/usr/sbin/chroot', root, '/bin/umount', '/sys'])
log_check_call(['/usr/sbin/chroot', root, '/bin/umount', '/proc'])
log_check_call(['/bin/umount', '{root}/dev'.format(root=root)])
self.specials_mounted = False
def _check_blocking(self, e):
if self.partition_map.is_blocking():
raise VolumeError('The partitionmap prevents the detach procedure')
if self.specials_mounted:
raise VolumeError('The special devices are mounted and prevent the detaching procedure')

View file

@ -15,6 +15,10 @@ def get_args():
parser = ArgumentParser(description='Bootstrap Debian for the cloud.') parser = ArgumentParser(description='Bootstrap Debian for the cloud.')
parser.add_argument('--debug', action='store_true', parser.add_argument('--debug', action='store_true',
help='Print debugging information') help='Print debugging information')
parser.add_argument('--pause-on-error', action='store_true',
help='Pause on error, before rollback')
parser.add_argument('--dry-run', action='store_true',
help='Dont\'t actually run the tasks')
parser.add_argument('manifest', help='Manifest file to use for bootstrapping', metavar='MANIFEST') parser.add_argument('manifest', help='Manifest file to use for bootstrapping', metavar='MANIFEST')
return parser.parse_args() return parser.parse_args()
@ -33,10 +37,12 @@ def run(args):
bootstrap_info = BootstrapInformation(manifest=manifest, debug=args.debug) bootstrap_info = BootstrapInformation(manifest=manifest, debug=args.debug)
try: try:
tasklist.run(bootstrap_info) tasklist.run(info=bootstrap_info, dry_run=args.dry_run)
log.info('Successfully completed bootstrapping') log.info('Successfully completed bootstrapping')
except (Exception, KeyboardInterrupt) as e: except (Exception, KeyboardInterrupt) as e:
log.exception(e) log.exception(e)
if args.pause_on_error:
raw_input("Press Enter to commence rollback")
log.error('Rolling back') log.error('Rolling back')
rollback_tasklist = TaskList() rollback_tasklist = TaskList()
provider.rollback_tasks(rollback_tasklist, tasklist.tasks_completed, manifest) provider.rollback_tasks(rollback_tasklist, tasklist.tasks_completed, manifest)
@ -44,5 +50,5 @@ def run(args):
rollback_tasks = getattr(plugin, 'rollback_tasks', None) rollback_tasks = getattr(plugin, 'rollback_tasks', None)
if callable(rollback_tasks): if callable(rollback_tasks):
plugin.rollback_tasks(rollback_tasklist, tasklist.tasks_completed, manifest) plugin.rollback_tasks(rollback_tasklist, tasklist.tasks_completed, manifest)
rollback_tasklist.run(bootstrap_info) rollback_tasklist.run(info=bootstrap_info, dry_run=args.dry_run)
log.info('Successfully completed rollback') log.info('Successfully completed rollback')

View file

@ -9,39 +9,36 @@
"bootstrapper": { "bootstrapper": {
"type": "object", "type": "object",
"properties": { "properties": {
"mount_dir": { "type": "string" }, "workspace": { "$ref": "#/definitions/path" },
"mirror": { "type": "string" }, "mirror": { "type": "string", "format": "uri" },
"tarball": { "type": "boolean" }, "tarball": { "type": "boolean" }
"tarball_dir": { "type": "string" }
}, },
"required": ["mount_dir"] "required": ["workspace"]
}, },
"system": { "system": {
"type": "object", "type": "object",
"properties": { "properties": {
"release": { "release": { "enum": ["wheezy"] },
"type": "string", "architecture": { "enum": ["i386", "amd64"] },
"enum": ["wheezy"] "timezone": { "type": "string" },
}, "locale": { "type": "string" },
"architecture": { "charmap": { "type": "string" }
"type": "string",
"enum": ["i386", "amd64"]
},
"timezone": { "type": "string" },
"locale": { "type": "string" },
"charmap": { "type": "string" }
}, },
"required": ["release", "architecture", "timezone", "locale", "charmap"] "required": ["release", "architecture", "timezone", "locale", "charmap"]
}, },
"volume": { "volume": {
"type": "object", "type": "object",
"properties": { "properties": {
"size": { "backing": { "type": "string" },
"type": "integer", "partitions": {
"minimum": 1 "type": "object",
"oneOf": [
{ "$ref": "#/definitions/no_partitions" },
{ "$ref": "#/definitions/partition_table" }
]
} }
}, },
"required": ["size"] "required": ["partitions"]
}, },
"plugins": { "plugins": {
"type": "object", "type": "object",
@ -59,5 +56,44 @@
"additionalProperties": false "additionalProperties": false
} }
}, },
"required": ["provider", "bootstrapper", "volume", "system"] "required": ["provider", "bootstrapper", "volume", "system"],
"definitions": {
"path": {
"type": "string",
"pattern": "^[^\\0]+$"
},
"no_partitions": {
"type": "object",
"properties": {
"type": { "enum": ["none"] },
"root": { "$ref": "#/definitions/partition" }
},
"required": ["root"],
"additionalProperties": false
},
"partition_table": {
"type": "object",
"properties": {
"type": { "enum": ["mbr", "gpt"] },
"boot": { "$ref": "#/definitions/partition" },
"root": { "$ref": "#/definitions/partition" },
"swap": {
"type": "object",
"properties": { "size": { "type": "integer", "minimum": 1 } },
"required": ["size"]
}
},
"required": ["root"],
"additionalProperties": false
},
"partition": {
"type": "object",
"properties": {
"size": { "type": "integer", "minimum": 1 },
"filesystem": { "enum": ["ext2", "ext3", "ext4", "xfs"] }
},
"required": ["size", "filesystem"]
}
},
"required": ["provider", "bootstrapper", "system", "volume"]
} }

View file

@ -49,10 +49,6 @@ class Manifest(object):
self.bootstrapper = data['bootstrapper'] self.bootstrapper = data['bootstrapper']
if 'mirror' not in self.bootstrapper: if 'mirror' not in self.bootstrapper:
self.bootstrapper['mirror'] = 'http://http.debian.net/debian' self.bootstrapper['mirror'] = 'http://http.debian.net/debian'
if 'tarball' not in self.bootstrapper:
self.bootstrapper['tarball'] = False
if 'tarball_dir' not in self.bootstrapper and self.bootstrapper['tarball']:
self.bootstrapper['tarball_dir'] = '/tmp'
self.volume = data['volume'] self.volume = data['volume']
self.system = data['system'] self.system = data['system']
self.plugins = data['plugins'] if 'plugins' in data else {} self.plugins = data['plugins'] if 'plugins' in data else {}

View file

@ -1,4 +1,3 @@
from common.exceptions import TaskListError
class Task(object): class Task(object):
@ -6,27 +5,8 @@ class Task(object):
before = [] before = []
after = [] after = []
def __init__(self):
self._check_ordering()
def __str__(self): def __str__(self):
return '{module}.{task}'.format(module=self.__module__, task=self.__class__.__name__) return '{module}.{task}'.format(module=self.__module__, task=self.__class__.__name__)
def __repr__(self): def __repr__(self):
return self.__str__() return self.__str__()
def _check_ordering(self):
def name(ref):
return '{module}.{task}'.format(module=ref.__module__, task=ref.__class__.__name__)
for task in self.before:
if self.phase > task.phase:
msg = ("The task {self} is specified as running before {other}, "
"but its phase '{phase}' lies after the phase '{other_phase}'"
.format(self=type(self), other=task, phase=self.phase, other_phase=task.phase))
raise TaskListError(msg)
for task in self.after:
if self.phase < task.phase:
msg = ("The task {self} is specified as running after {other}, "
"but its phase '{phase}' lies before the phase '{other_phase}'"
.format(self=type(self), other=task, phase=self.phase, other_phase=task.phase))
raise TaskListError(msg)

View file

@ -13,40 +13,34 @@ class TaskList(object):
self.tasks.update(args) self.tasks.update(args)
def remove(self, *args): def remove(self, *args):
for task_type in args: for task in args:
task = self.get(task_type) self.tasks.discard(task)
if task is not None:
self.tasks.discard(task)
def replace(self, task, replacement): def run(self, info={}, dry_run=False):
self.remove(task)
self.add(replacement)
def get(self, ref):
return next((task for task in self.tasks if type(task) is ref), None)
def run(self, bootstrap_info):
task_list = self.create_list(self.tasks) task_list = self.create_list(self.tasks)
log.debug('Tasklist:\n\t{list}'.format(list='\n\t'.join(repr(task) for task in task_list))) log.debug('Tasklist:\n\t{list}'.format(list='\n\t'.join(repr(task) for task in task_list)))
for task in task_list: for task_type in task_list:
task = task_type()
if hasattr(task, 'description'): if hasattr(task, 'description'):
log.info(task.description) log.info(task.description)
else: else:
log.info('Running {task}'.format(task=task)) log.info('Running {task}'.format(task=task))
task.run(bootstrap_info) if not dry_run:
task.run(info)
self.tasks_completed.append(task) self.tasks_completed.append(task)
def create_list(self, tasks): def create_list(self, tasks):
from common.phases import order from common.phases import order
graph = {} graph = {}
for task in tasks: for task in tasks:
successors = [] self.check_ordering(task)
successors.extend([self.get(succ) for succ in task.before]) successors = set()
successors.extend(filter(lambda succ: type(task) in succ.after, tasks)) successors.update(task.before)
successors.update(filter(lambda succ: task in succ.after, tasks))
succeeding_phases = order[order.index(task.phase) + 1:] succeeding_phases = order[order.index(task.phase) + 1:]
successors.extend(filter(lambda succ: succ.phase in succeeding_phases, tasks)) successors.update(filter(lambda succ: succ.phase in succeeding_phases, tasks))
graph[task] = filter(lambda succ: succ in self.tasks, successors) graph[task] = filter(lambda succ: succ in tasks, successors)
components = self.strongly_connected_components(graph) components = self.strongly_connected_components(graph)
cycles_found = 0 cycles_found = 0
@ -63,6 +57,20 @@ class TaskList(object):
return sorted_tasks return sorted_tasks
def check_ordering(self, task):
for successor in task.before:
if successor.phase > successor.phase:
msg = ("The task {task} is specified as running before {other}, "
"but its phase '{phase}' lies after the phase '{other_phase}'"
.format(task=task, other=successor, phase=task.phase, other_phase=successor.phase))
raise TaskListError(msg)
for predecessor in task.after:
if task.phase < predecessor.phase:
msg = ("The task {task} is specified as running after {other}, "
"but its phase '{phase}' lies before the phase '{other_phase}'"
.format(task=task, other=predecessor, phase=task.phase, other_phase=predecessor.phase))
raise TaskListError(msg)
def strongly_connected_components(self, graph): def strongly_connected_components(self, graph):
# Source: http://www.logarithmic.net/pfh-files/blog/01208083168/sort.py # Source: http://www.logarithmic.net/pfh-files/blog/01208083168/sort.py
# Find the strongly connected components in a graph using Tarjan's algorithm. # Find the strongly connected components in a graph using Tarjan's algorithm.

View file

@ -13,7 +13,7 @@
prog=$(basename $0) prog=$(basename $0)
logger="logger -t $prog" logger="logger -t $prog"
device_path="/dev/xvda1" device_path="/dev/xvda"
filesystem=`blkid | grep $device_path | sed 's#\(.*\):.*TYPE="\(.*\)".*#\2#'` filesystem=`blkid | grep $device_path | sed 's#\(.*\):.*TYPE="\(.*\)".*#\2#'`

39
common/fs/__init__.py Normal file
View file

@ -0,0 +1,39 @@
def get_partitions():
import re
regexp = re.compile('^ *(?P<major>\d+) *(?P<minor>\d+) *(?P<num_blks>\d+) (?P<dev_name>\S+)$')
matches = {}
path = '/proc/partitions'
with open(path) as partitions:
next(partitions)
next(partitions)
for line in partitions:
match = regexp.match(line)
if match is None:
raise RuntimeError('Unable to parse {line} in {path}'.format(line=line, path=path))
matches[match.group('dev_name')] = match.groupdict()
return matches
def remount(volume, fn):
from base.fs.partitionmaps.none import NoPartitions
p_map = volume.partition_map
volume.unmount_specials()
if hasattr(p_map, 'boot'):
boot_dir = p_map.boot.mount_dir
p_map.boot.unmount()
root_dir = p_map.root.mount_dir
p_map.root.unmount()
if not isinstance(p_map, NoPartitions):
p_map.unmap(volume)
result = fn()
p_map.map(volume)
else:
result = fn()
p_map.root.mount(root_dir)
if hasattr(p_map, 'boot'):
p_map.boot.mount(boot_dir)
volume.mount_specials()
return result

View file

@ -0,0 +1,80 @@
from base.fs.volume import Volume
from common.tools import log_check_call
from base.fs.exceptions import VolumeError
# QEMU formats:
# raw, host_device, qcow2, qcow, cow, vdi, vmdk, vpc, cloop
class LoopbackVolume(Volume):
events = [{'name': 'create', 'src': 'nonexistent', 'dst': 'detached'},
{'name': 'attach', 'src': 'detached', 'dst': 'attached'},
{'name': 'link_dm_node', 'src': 'attached', 'dst': 'linked'},
{'name': 'unlink_dm_node', 'src': 'linked', 'dst': 'attached'},
{'name': 'detach', 'src': 'attached', 'dst': 'detached'},
{'name': 'delete', 'src': 'detached', 'dst': 'deleted'},
]
extension = 'raw'
def can_mount_specials(self):
return self.fsm.current in ['attached', 'linked']
def create(self, image_path):
self.fsm.create(image_path=image_path)
def _before_create(self, e):
self.image_path = e.image_path
log_check_call(['/usr/bin/qemu-img', 'create', '-f', 'raw', self.image_path, str(self.size) + 'M'])
def _before_attach(self, e):
[self.loop_device_path] = log_check_call(['/sbin/losetup', '--show', '--find', self.image_path])
self.device_path = self.loop_device_path
def _before_link_dm_node(self, e):
import os.path
from . import get_partitions
proc_partitions = get_partitions()
loop_device_name = os.path.basename(self.loop_device_path)
loop_device_partition = proc_partitions[loop_device_name]
sectors = self.size*1024*1024/512
table = ('{log_start_sec} {sectors} linear {major}:{minor} {start_sec}'
.format(log_start_sec=0,
sectors=sectors,
major=loop_device_partition['major'],
minor=loop_device_partition['minor'],
start_sec=0))
import string
import os.path
for letter in string.ascii_lowercase:
dev_name = 'vd' + letter
dev_path = os.path.join('/dev/mapper', dev_name)
if not os.path.exists(dev_path):
self.dm_node_name = dev_name
self.dm_node_path = dev_path
break
if not hasattr(self, 'dm_node_name'):
raise VolumeError('Unable to find a free block device path for mounting the bootstrap volume')
log_check_call(['/sbin/dmsetup', 'create', self.dm_node_name], table)
self.device_path = self.dm_node_path
def _before_unlink_dm_node(self, e):
log_check_call(['/sbin/dmsetup', 'remove', self.dm_node_name])
del self.dm_node_name
del self.dm_node_path
self.device_path = self.loop_device_path
def _before_detach(self, e):
log_check_call(['/sbin/losetup', '--detach', self.loop_device_path])
del self.loop_device_path
del self.device_path
def _before_delete(self, e):
from os import remove
remove(self.image_path)
del self.image_path

42
common/fsm_proxy.py Normal file
View file

@ -0,0 +1,42 @@
class FSMProxy(object):
def __init__(self, cfg):
from fysom import Fysom
events = set([event['name'] for event in cfg['events']])
cfg['callbacks'] = self.collect_event_listeners(events, cfg['callbacks'])
self.fsm = Fysom(cfg)
self.attach_proxy_methods(self.fsm, events)
def collect_event_listeners(self, events, callbacks):
callbacks = callbacks.copy()
callback_names = []
for event in events:
callback_names.append(('_before_' + event, 'onbefore' + event))
callback_names.append(('_after_' + event, 'onafter' + event))
for fn_name, listener in callback_names:
fn = getattr(self, fn_name, None)
if callable(fn):
if listener in callbacks:
old_fn = callbacks[listener]
def wrapper(e, old_fn=old_fn, fn=fn):
old_fn(e)
fn(e)
callbacks[listener] = wrapper
else:
callbacks[listener] = fn
return callbacks
def attach_proxy_methods(self, fsm, events):
def make_proxy(fsm, event):
fn = getattr(fsm, event)
def proxy():
fn()
return proxy
for event in events:
if not hasattr(self, event):
setattr(self, event, make_proxy(fsm, event))

74
common/task_sets.py Normal file
View file

@ -0,0 +1,74 @@
from common.tasks import workspace
from common.tasks import packages
from common.tasks import host
from common.tasks import bootstrap
from common.tasks import volume
from common.tasks import filesystem
from common.tasks import partitioning
from common.tasks import cleanup
from common.tasks import apt
from common.tasks import security
from common.tasks import locale
base_set = [workspace.CreateWorkspace,
packages.HostPackages,
packages.ImagePackages,
host.CheckPackages,
bootstrap.Bootstrap,
workspace.DeleteWorkspace,
]
volume_set = [volume.Attach,
volume.Detach,
filesystem.Format,
filesystem.FStab,
]
partitioning_set = [partitioning.PartitionVolume,
partitioning.MapPartitions,
partitioning.UnmapPartitions,
]
boot_partition_set = [filesystem.CreateBootMountDir,
filesystem.MountBoot,
filesystem.UnmountBoot,
]
mounting_set = [filesystem.CreateMountDir,
filesystem.MountRoot,
filesystem.MountSpecials,
filesystem.UnmountSpecials,
filesystem.UnmountRoot,
filesystem.DeleteMountDir,
]
ssh_set = [security.DisableSSHPasswordAuthentication,
security.DisableSSHDNSLookup,
cleanup.ShredHostkeys,
]
apt_set = [apt.DisableDaemonAutostart,
apt.AptSources,
apt.AptUpgrade,
apt.PurgeUnusedPackages,
apt.AptClean,
apt.EnableDaemonAutostart,
]
locale_set = [locale.GenerateLocale,
locale.SetTimezone,
]
def get_fs_specific_set(partitions):
task_set = {'ext2': [filesystem.TuneVolumeFS],
'ext3': [filesystem.TuneVolumeFS],
'ext4': [filesystem.TuneVolumeFS],
'xfs': [filesystem.AddXFSProgs],
}
tasks = set()
if 'boot' in partitions:
tasks.update(task_set.get(partitions['boot']['filesystem'], []))
if 'root' in partitions:
tasks.update(task_set.get(partitions['root']['filesystem'], []))
return tasks

View file

@ -29,7 +29,7 @@ class MakeTarball(Task):
hash_args = [arg for arg in arguments if arg != info.root] hash_args = [arg for arg in arguments if arg != info.root]
tarball_id = sha1(repr(frozenset(options + hash_args))).hexdigest()[0:8] tarball_id = sha1(repr(frozenset(options + hash_args))).hexdigest()[0:8]
tarball_filename = 'debootstrap-{id}.tar'.format(id=tarball_id) tarball_filename = 'debootstrap-{id}.tar'.format(id=tarball_id)
info.tarball = os.path.join(info.manifest.bootstrapper['tarball_dir'], tarball_filename) info.tarball = os.path.join(info.manifest.bootstrapper['workspace'], tarball_filename)
if os.path.isfile(info.tarball): if os.path.isfile(info.tarball):
log.debug('Found matching tarball, skipping download') log.debug('Found matching tarball, skipping download')
else: else:

View file

@ -1,29 +1,30 @@
from base import Task from base import Task
from common import phases from common import phases
from common.exceptions import TaskError
from common.tools import log_check_call from common.tools import log_check_call
from bootstrap import Bootstrap from bootstrap import Bootstrap
import volume
class FormatVolume(Task): class Format(Task):
description = 'Formatting the volume' description = 'Formatting the volume'
phase = phases.volume_preparation phase = phases.volume_preparation
def run(self, info): def run(self, info):
dev_path = info.bootstrap_device['path'] for partition in info.volume.partition_map.partitions:
mkfs = '/sbin/mkfs.{fs}'.format(fs=info.manifest.volume['filesystem']) partition.format()
log_check_call([mkfs, dev_path])
info.bootstrap_device['partitions'] = {'root_path': info.bootstrap_device['path']}
class TuneVolumeFS(Task): class TuneVolumeFS(Task):
description = 'Tuning the bootstrap volume filesystem' description = 'Tuning the bootstrap volume filesystem'
phase = phases.volume_preparation phase = phases.volume_preparation
after = [FormatVolume] after = [Format]
def run(self, info): def run(self, info):
import re
# Disable the time based filesystem check # Disable the time based filesystem check
log_check_call(['/sbin/tune2fs', '-i', '0', info.bootstrap_device['partitions']['root_path']]) for partition in info.volume.partition_map.partitions:
if re.match('^ext[2-4]$', partition.filesystem) is not None:
log_check_call(['/sbin/tune2fs', '-i', '0', partition.device_path])
class AddXFSProgs(Task): class AddXFSProgs(Task):
@ -36,33 +37,43 @@ class AddXFSProgs(Task):
class CreateMountDir(Task): class CreateMountDir(Task):
description = 'Creating mountpoint for the bootstrap volume' description = 'Creating mountpoint for the root partition'
phase = phases.volume_mounting phase = phases.volume_mounting
def run(self, info): def run(self, info):
import os import os
mount_dir = info.manifest.bootstrapper['mount_dir'] info.root = os.path.join(info.workspace, 'root')
info.root = '{mount_dir}/{id:x}'.format(mount_dir=mount_dir, id=info.run_id)
# Works recursively, fails if last part exists, which is exaclty what we want.
os.makedirs(info.root) os.makedirs(info.root)
class MountVolume(Task): class MountRoot(Task):
description = 'Mounting the bootstrap volume' description = 'Mounting the root partition'
phase = phases.volume_mounting phase = phases.volume_mounting
after = [CreateMountDir] after = [CreateMountDir]
def run(self, info): def run(self, info):
with open('/proc/mounts') as mounts: info.volume.partition_map.root.mount(info.root)
for mount in mounts:
if info.root in mount:
msg = 'Something is already mounted at {root}'.format(root=info.root)
raise TaskError(msg)
log_check_call(['/bin/mount',
'--types', info.manifest.volume['filesystem'], class MountBoot(Task):
info.bootstrap_device['partitions']['root_path'], description = 'Mounting the boot partition'
info.root]) phase = phases.volume_mounting
after = [MountRoot]
def run(self, info):
info.volume.partition_map.boot.mount(info.boot_dir)
class CreateBootMountDir(Task):
description = 'Creating mountpoint for the boot partition'
phase = phases.volume_mounting
after = [MountRoot]
before = [MountBoot]
def run(self, info):
import os
info.boot_dir = os.path.join(info.root, 'boot')
os.makedirs(info.boot_dir)
class MountSpecials(Task): class MountSpecials(Task):
@ -71,36 +82,40 @@ class MountSpecials(Task):
after = [Bootstrap] after = [Bootstrap]
def run(self, info): def run(self, info):
log_check_call(['/bin/mount', '--bind', '/dev', '{root}/dev'.format(root=info.root)]) info.volume.mount_specials()
log_check_call(['/usr/sbin/chroot', info.root, '/bin/mount', '--types', 'proc', 'none', '/proc'])
log_check_call(['/usr/sbin/chroot', info.root, '/bin/mount', '--types', 'sysfs', 'none', '/sys'])
log_check_call(['/usr/sbin/chroot', info.root, '/bin/mount', '--types', 'devpts', 'none', '/dev/pts']) class UnmountRoot(Task):
description = 'Unmounting the bootstrap volume'
phase = phases.volume_unmounting
before = [volume.Detach]
def run(self, info):
info.volume.partition_map.root.unmount()
class UnmountBoot(Task):
description = 'Unmounting the boot partition'
phase = phases.volume_unmounting
before = [UnmountRoot]
def run(self, info):
info.volume.partition_map.boot.unmount()
class UnmountSpecials(Task): class UnmountSpecials(Task):
description = 'Unmunting special block devices' description = 'Unmunting special block devices'
phase = phases.volume_unmounting phase = phases.volume_unmounting
before = [UnmountRoot]
def run(self, info): def run(self, info):
log_check_call(['/usr/sbin/chroot', info.root, '/bin/umount', '/dev/pts']) info.volume.unmount_specials()
log_check_call(['/usr/sbin/chroot', info.root, '/bin/umount', '/sys'])
log_check_call(['/usr/sbin/chroot', info.root, '/bin/umount', '/proc'])
log_check_call(['/bin/umount', '{root}/dev'.format(root=info.root)])
class UnmountVolume(Task):
description = 'Unmounting the bootstrap volume'
phase = phases.volume_unmounting
after = [UnmountSpecials]
def run(self, info):
log_check_call(['/bin/umount', info.root])
class DeleteMountDir(Task): class DeleteMountDir(Task):
description = 'Deleting mountpoint for the bootstrap volume' description = 'Deleting mountpoint for the bootstrap volume'
phase = phases.volume_unmounting phase = phases.volume_unmounting
after = [UnmountVolume] after = [UnmountRoot]
def run(self, info): def run(self, info):
import os import os
@ -108,24 +123,44 @@ class DeleteMountDir(Task):
del info.root del info.root
class ModifyFstab(Task): class FStab(Task):
description = 'Adding root volume to the fstab' description = 'Adding partitions to the fstab'
phase = phases.system_modification phase = phases.system_modification
def run(self, info): def run(self, info):
import os.path import os.path
mount_opts = ['defaults'] p_map = info.volume.partition_map
if info.manifest.volume['filesystem'].lower() in ['ext2', 'ext3', 'ext4']: mount_points = [{'path': '/',
mount_opts.append('barrier=0') 'partition': p_map.root,
if info.manifest.volume['filesystem'].lower() == 'xfs': 'dump': '1',
mount_opts.append('nobarrier') 'pass_num': '1',
fstab_path = os.path.join(info.root, 'etc/fstab') }]
if hasattr(p_map, 'boot'):
mount_points.append({'path': '/boot',
'partition': p_map.boot,
'dump': '1',
'pass_num': '2',
})
if hasattr(p_map, 'swap'):
mount_points.append({'path': 'none',
'partition': p_map.swap,
'dump': '1',
'pass_num': '0',
})
device = '/dev/sda1' fstab_lines = []
if info.manifest.virtualization == 'pvm': for mount_point in mount_points:
device = '/dev/xvda1' partition = mount_point['partition']
with open(fstab_path, 'a') as fstab: mount_opts = ['defaults']
fstab.write('{device} / {filesystem} {mount_opts} 1 1\n' fstab_lines.append('UUID={uuid} {mountpoint} {filesystem} {mount_opts} {dump} {pass_num}'
.format(device=device, .format(uuid=partition.get_uuid(),
filesystem=info.manifest.volume['filesystem'].lower(), mountpoint=mount_point['path'],
mount_opts=','.join(mount_opts))) filesystem=partition.filesystem,
mount_opts=','.join(mount_opts),
dump=mount_point['dump'],
pass_num=mount_point['pass_num']))
fstab_path = os.path.join(info.root, 'etc/fstab')
with open(fstab_path, 'w') as fstab:
fstab.write('\n'.join(fstab_lines))
fstab.write('\n')

View file

@ -14,7 +14,7 @@ class CheckPackages(Task):
from subprocess import CalledProcessError from subprocess import CalledProcessError
for package in info.host_packages: for package in info.host_packages:
try: try:
log_check_call(['/usr/bin/dpkg', '--status', package]) log_check_call(['/usr/bin/dpkg-query', '-s', package])
except CalledProcessError: except CalledProcessError:
msg = "The package ``{0}\'\' is not installed".format(package) msg = "The package ``{0}\'\' is not installed".format(package)
raise TaskError(msg) raise TaskError(msg)

View file

@ -1,5 +1,6 @@
from base import Task from base import Task
from common import phases from common import phases
from common.tools import log_check_call
import os.path import os.path
@ -8,11 +9,18 @@ class ResolveInitScripts(Task):
phase = phases.system_modification phase = phases.system_modification
def run(self, info): def run(self, info):
init_scripts = {'expand-volume': 'expand-volume'} init_scripts = {}
init_scripts['expand-volume'] = 'expand-volume'
init_scripts['generate-ssh-hostkeys'] = 'generate-ssh-hostkeys' from subprocess import CalledProcessError
if info.manifest.system['release'] == 'squeeze': try:
init_scripts['generate-ssh-hostkeys'] = 'squeeze/generate-ssh-hostkeys' log_check_call(['/usr/sbin/chroot', info.root,
'/usr/bin/dpkg-query', '-W', 'openssh-server'])
init_scripts['generate-ssh-hostkeys'] = 'generate-ssh-hostkeys'
if info.manifest.system['release'] == 'squeeze':
init_scripts['generate-ssh-hostkeys'] = 'squeeze/generate-ssh-hostkeys'
except CalledProcessError:
pass
disable_scripts = ['hwclock.sh'] disable_scripts = ['hwclock.sh']
if info.manifest.system['release'] == 'squeeze': if info.manifest.system['release'] == 'squeeze':
@ -37,7 +45,6 @@ class InstallInitScripts(Task):
stat.S_IRGRP | stat.S_IXGRP | stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH) stat.S_IROTH | stat.S_IXOTH)
from shutil import copy from shutil import copy
from common.tools import log_check_call
for name, src in info.initd['install'].iteritems(): for name, src in info.initd['install'].iteritems():
dst = os.path.join(info.root, 'etc/init.d', name) dst = os.path.join(info.root, 'etc/init.d', name)
copy(src, dst) copy(src, dst)

View file

@ -1,60 +1,29 @@
from base import Task from base import Task
from common import phases from common import phases
from filesystem import UnmountVolume import volume
from common.tools import log_check_call
class Create(Task): class Create(Task):
description = 'Creating a loopback volume' description = 'Creating a loopback volume'
phase = phases.volume_creation phase = phases.volume_creation
before = [volume.Attach]
def run(self, info): def run(self, info):
loopback_filename = 'loopback-{id:x}.img'.format(id=info.run_id)
import os.path import os.path
info.loopback_file = os.path.join(info.manifest.volume['loopback_dir'], loopback_filename) image_path = os.path.join(info.workspace, 'volume.{ext}'.format(ext=info.volume.extension))
log_check_call(['/bin/dd', info.volume.create(image_path)
'if=/dev/zero', 'of=' + info.loopback_file,
'bs=1M', 'seek=' + str(info.manifest.volume['size']), 'count=0'])
class CreateQemuImg(Task): class MoveImage(Task):
description = 'Creating a loopback volume with qemu' description = 'Moving volume image'
phase = phases.volume_creation phase = phases.image_registration
def run(self, info): def run(self, info):
loopback_filename = 'loopback-{id:x}.img'.format(id=info.run_id)
import os.path import os.path
info.loopback_file = os.path.join(info.manifest.volume['loopback_dir'], loopback_filename) filename = 'volume-{id:x}.{ext}'.format(id=info.run_id, ext=info.volume.extension)
log_check_call(['/usr/bin/qemu-img', 'create', '-f', 'raw', destination = os.path.join(info.manifest.bootstrapper['workspace'], filename)
info.loopback_file, str(info.manifest.volume['size']) + 'M']) import shutil
shutil.move(info.volume.image_path, destination)
import logging
class Attach(Task): log = logging.getLogger(__name__)
description = 'Attaching the loopback volume' log.info('The volume image has been moved to {image_path}'.format(image_path=destination))
phase = phases.volume_creation
after = [Create, CreateQemuImg]
def run(self, info):
info.bootstrap_device = {}
command = ['/sbin/losetup', '--show', '--find', info.loopback_file]
[info.bootstrap_device['path']] = log_check_call(command)
class Detach(Task):
description = 'Detaching the loopback volume'
phase = phases.volume_unmounting
after = [UnmountVolume]
def run(self, info):
log_check_call(['/sbin/losetup', '--detach', info.bootstrap_device['path']])
del info.bootstrap_device
class Delete(Task):
description = 'Deleting the loopback volume'
phase = phases.cleaning
def run(self, info):
from os import remove
remove(info.loopback_file)
del info.loopback_file

View file

@ -12,6 +12,15 @@ class RemoveDNSInfo(Task):
remove(os.path.join(info.root, 'etc/resolv.conf')) remove(os.path.join(info.root, 'etc/resolv.conf'))
class RemoveHostname(Task):
description = 'Removing the hostname file'
phase = phases.system_modification
def run(self, info):
from os import remove
remove(os.path.join(info.root, 'etc/hostname'))
class ConfigureNetworkIF(Task): class ConfigureNetworkIF(Task):
description = 'Configuring network interfaces' description = 'Configuring network interfaces'
phase = phases.system_modification phase = phases.system_modification
@ -26,13 +35,3 @@ class ConfigureNetworkIF(Task):
'iface eth0 inet dhcp\n'} 'iface eth0 inet dhcp\n'}
with open(interfaces_path, 'a') as interfaces: with open(interfaces_path, 'a') as interfaces:
interfaces.write(if_config.get(info.manifest.system['release'])) interfaces.write(if_config.get(info.manifest.system['release']))
class ConfigureDHCP(Task):
description = 'Configuring the DHCP client'
phase = phases.system_modification
def run(self, info):
from common.tools import sed_i
dhcpcd = os.path.join(info.root, 'etc/default/dhcpcd')
sed_i(dhcpcd, '^#*SET_DNS=.*', 'SET_DNS=\'yes\'')

View file

@ -7,8 +7,19 @@ class HostPackages(Task):
phase = phases.preparation phase = phases.preparation
def run(self, info): def run(self, info):
packages = set(['debootstrap']) info.host_packages = set()
info.host_packages = packages info.host_packages.add('debootstrap')
from common.fs.loopbackvolume import LoopbackVolume
if isinstance(info.volume, LoopbackVolume):
info.host_packages.add('qemu-utils')
if 'xfs' in (p.filesystem for p in info.volume.partition_map.partitions):
info.host_packages.add('xfsprogs')
from base.fs.partitionmaps.none import NoPartitions
if not isinstance(info.volume.partition_map, NoPartitions):
info.host_packages.update(['parted', 'kpartx'])
class ImagePackages(Task): class ImagePackages(Task):
@ -16,12 +27,7 @@ class ImagePackages(Task):
phase = phases.preparation phase = phases.preparation
def run(self, info): def run(self, info):
# Add some basic packages we are going to need info.img_packages = set(), set()
include = set(['udev', include, exclude = info.img_packages
'openssh-server', # We could bootstrap without locales, but things just suck without them, error messages etc.
# We could bootstrap without locales, but things just suck without them, error messages etc. include.add('locales')
'locales',
])
exclude = set()
info.img_packages = include, exclude

View file

@ -1,53 +0,0 @@
from base import Task
from common import phases
from common.tools import log_check_call
import filesystem
import loopback
class PartitionVolume(Task):
description = 'Partitioning the volume'
phase = phases.volume_preparation
def run(self, info):
# parted
log_check_call(['parted', '--align', 'optimal', '--script', info.bootstrap_device['path'],
'--', 'mklabel', 'msdos'])
log_check_call(['parted', '--align', 'optimal', '--script', info.bootstrap_device['path'],
'--', 'mkpart', 'primary', 'ext4', '32k', '-1'])
log_check_call(['parted', '--script', info.bootstrap_device['path'],
'--', 'set', '1', 'boot', 'on'])
class MapPartitions(Task):
description = 'Mapping volume partitions'
phase = phases.volume_preparation
after = [PartitionVolume]
def run(self, info):
root_partition_path = info.bootstrap_device['path'].replace('/dev', '/dev/mapper') + 'p1'
log_check_call(['kpartx', '-a', '-v', info.bootstrap_device['path']])
info.bootstrap_device['partitions'] = {'root_path': root_partition_path}
class FormatPartitions(Task):
description = 'Formatting the partitions'
phase = phases.volume_preparation
before = [filesystem.TuneVolumeFS]
after = [MapPartitions]
def run(self, info):
# These params will fail for mkfs.xfs
log_check_call(['/sbin/mkfs.{fs}'.format(fs=info.manifest.volume['filesystem']),
'-m', '1', '-v', info.bootstrap_device['partitions']['root_path']])
class UnmapPartitions(Task):
description = 'Removing volume partitions mapping'
phase = phases.volume_unmounting
before = [loopback.Detach]
after = [filesystem.UnmountVolume]
def run(self, info):
log_check_call(['kpartx', '-d', info.bootstrap_device['path']])
del info.bootstrap_device['partitions']['root_path']

View file

@ -0,0 +1,32 @@
from base import Task
from common import phases
import filesystem
import volume
class PartitionVolume(Task):
description = 'Partitioning the volume'
phase = phases.volume_preparation
def run(self, info):
info.volume.partition_map.create(info.volume)
class MapPartitions(Task):
description = 'Mapping volume partitions'
phase = phases.volume_preparation
before = [filesystem.Format]
after = [PartitionVolume]
def run(self, info):
info.volume.partition_map.map(info.volume)
class UnmapPartitions(Task):
description = 'Removing volume partitions mapping'
phase = phases.volume_unmounting
before = [volume.Detach]
after = [filesystem.UnmountRoot]
def run(self, info):
info.volume.partition_map.unmap(info.volume)

28
common/tasks/volume.py Normal file
View file

@ -0,0 +1,28 @@
from base import Task
from common import phases
from common.tasks import workspace
class Attach(Task):
description = 'Attaching the volume'
phase = phases.volume_creation
def run(self, info):
info.volume.attach()
class Detach(Task):
description = 'Detaching the volume'
phase = phases.volume_unmounting
def run(self, info):
info.volume.detach()
class Delete(Task):
description = 'Deleting the volume'
phase = phases.cleaning
before = [workspace.DeleteWorkspace]
def run(self, info):
info.volume.delete()

20
common/tasks/workspace.py Normal file
View file

@ -0,0 +1,20 @@
from base import Task
from common import phases
class CreateWorkspace(Task):
description = 'Creating workspace'
phase = phases.preparation
def run(self, info):
import os
os.makedirs(info.workspace)
class DeleteWorkspace(Task):
description = 'Deleting workspace'
phase = phases.cleaning
def run(self, info):
import os
os.rmdir(info.workspace)

View file

@ -16,6 +16,7 @@ def log_call(command, stdin=None):
from os.path import realpath from os.path import realpath
command_log = realpath(command[0]).replace('/', '.') command_log = realpath(command[0]).replace('/', '.')
log = logging.getLogger(__name__ + command_log) log = logging.getLogger(__name__ + command_log)
log.debug('Executing: {command}'.format(command=' '.join(command)))
if stdin is not None: if stdin is not None:
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@ -31,13 +32,11 @@ def log_call(command, stdin=None):
ret = select.select(reads, [], []) ret = select.select(reads, [], [])
for fd in ret[0]: for fd in ret[0]:
if fd == process.stdout.fileno(): if fd == process.stdout.fileno():
line = process.stdout.readline() for line in iter(process.stdout.readline, ''):
if line != '':
log.debug(line.strip()) log.debug(line.strip())
stdout.append(line.strip()) stdout.append(line.strip())
if fd == process.stderr.fileno(): if fd == process.stderr.fileno():
line = process.stderr.readline() for line in iter(process.stderr.readline, ''):
if line != '':
log.error(line.strip()) log.error(line.strip())
stderr.append(line.strip()) stderr.append(line.strip())
if process.poll() is not None: if process.poll() is not None:

View file

@ -2,21 +2,16 @@
"provider": "ec2", "provider": "ec2",
"virtualization": "pvm", "virtualization": "pvm",
"credentials": { "credentials": {
"access-key": null, // "access-key": null,
"secret-key": null, // "secret-key": null
"certificate": null,
"private-key": null,
"user-id": null
}, },
"bootstrapper": { "bootstrapper": {
"mount_dir": "/target", "workspace": "/target"
"mirror": "http://http.debian.net/debian"
}, },
"image": { "image": {
"name": "debian-{release}-{architecture}-{virtualization}-{%Y}{%m}{%d}", "name": "debian-{release}-{architecture}-{virtualization}-{%y}{%m}{%d}",
"description": "Debian {release} {architecture} AMI ({virtualization})" "description": "Debian {release} {architecture} AMI ({virtualization})"
"bucket": ""
}, },
"system": { "system": {
"release": "wheezy", "release": "wheezy",
@ -26,18 +21,19 @@
"charmap": "UTF-8" "charmap": "UTF-8"
}, },
"volume": { "volume": {
"backing": "s3", "backing": "ebs",
"filesystem": "ext4", "partitions": {
"size": 1024 "type": "none",
"root": {
"size": 1024,
"filesystem": "ext4"
}
}
}, },
"plugins": { "plugins": {
"admin_user": { "admin_user": {
"enabled": true, "enabled": true,
"username": "admin" "username": "admin"
},
"prebootstrapped": {
"enabled": false,
"image": null
} }
} }
} }

View file

@ -0,0 +1,33 @@
{
"provider": "ec2",
"virtualization": "pvm",
"credentials": {
// "access-key": null,
// "secret-key": null
},
"bootstrapper": {
"workspace": "/target"
},
"image": {
"name": "debian-{release}-{architecture}-{virtualization}-{%y}{%m}{%d}",
"description": "Debian {release} {architecture} AMI ({virtualization})"
},
"system": {
"release": "wheezy",
"architecture": "amd64",
"timezone": "UTC",
"locale": "en_US",
"charmap": "UTF-8"
},
"volume": {
"backing": "ebs",
"partitions": {
"type": "mbr",
"root": {
"size": 1023,
"filesystem": "ext4"
}
}
}
}

View file

@ -1,47 +0,0 @@
{
"provider": "ec2",
"virtualization": "pvm",
"credentials": {
"access-key": null,
"secret-key": null
},
"bootstrapper": {
"mount_dir": "/target",
"mirror": "http://http.debian.net/debian"
},
"image": {
"name": "debian-{release}-{architecture}-{virtualization}-{%Y}{%m}{%d}",
"description": "Debian {release} {architecture} AMI ({virtualization})"
},
"system": {
"release": "wheezy",
"architecture": "amd64",
"timezone": "UTC",
"locale": "en_US",
"charmap": "UTF-8"
},
"volume": {
"backing": "ebs",
"filesystem": "ext4",
"size": 8192
},
"plugins": {
"admin_user": {
"enabled": true,
"username": "admin"
},
"backports": {
"enabled": true,
"packages": [ "cloud-init" ]
},
"build_metadata": {
"enabled": false,
"path": "/root/build-metadata-{ami_name}"
},
"prebootstrapped": {
"enabled": false,
"snapshot": ""
}
}
}

View file

@ -0,0 +1,33 @@
{
"provider": "ec2",
"virtualization": "pvm",
"credentials": {
// "access-key": null,
// "secret-key": null
},
"bootstrapper": {
"workspace": "/target"
},
"image": {
"name": "debian-{release}-{architecture}-{virtualization}-{%y}{%m}{%d}",
"description": "Debian {release} {architecture} AMI ({virtualization})"
},
"system": {
"release": "wheezy",
"architecture": "amd64",
"timezone": "UTC",
"locale": "en_US",
"charmap": "UTF-8"
},
"volume": {
"backing": "ebs",
"partitions": {
"type": "none",
"root": {
"size": 1024,
"filesystem": "ext4"
}
}
}
}

View file

@ -0,0 +1,37 @@
{
"provider": "ec2",
"virtualization": "pvm",
"credentials": {
// "access-key": null,
// "secret-key": null,
// "certificate": null,
// "private-key": null,
// "user-id": null
},
"bootstrapper": {
"workspace": "/target"
},
"image": {
"name": "debian-{release}-{architecture}-{virtualization}-{%y}{%m}{%d}",
"description": "Debian {release} {architecture} AMI",
"bucket": "debian-amis"
},
"system": {
"release": "wheezy",
"architecture": "amd64",
"timezone": "UTC",
"locale": "en_US",
"charmap": "UTF-8"
},
"volume": {
"backing": "s3",
"partitions": {
"type": "none",
"root": {
"size": 1024,
"filesystem": "ext4"
}
}
}
}

View file

@ -1,40 +0,0 @@
{
"provider" : "kvm",
"virtualization": "virtio",
"bootstrapper": {
"mount_dir": "/mnt/target",
"mirror" : "http://ftp.fr.debian.org/debian/"
},
"image": {
"name" : "debian-{release}-{architecture}-{virtualization}-{%y}{%m}{%d}",
"description": "Debian {release} {architecture} ({virtualization})"
},
"system": {
"release" : "wheezy",
"architecture": "amd64",
"timezone" : "UTC",
"locale" : "en_US",
"charmap" : "UTF-8"
},
"volume": {
"backing" : "raw",
"filesystem": "ext4",
"size" : 1024,
"loopback_dir" : "/tmp"
},
"plugins": {
"user_packages": {
"enabled": true,
"repo": [ "apache2" ],
"local": []
},
"root_password": {
"enabled": true,
"password": "test"
},
"opennebula": {
"enabled": true
}
}
}

View file

@ -1,31 +0,0 @@
{
"provider" : "one",
"virtualization": "ide",
"bootstrapper": {
"mount_dir": "/target"
},
"image": {
"name" : "debian-{release}-{architecture}-{virtualization}-{%y}{%m}{%d}",
"description": "Debian {release} {architecture} ({virtualization})"
},
"system": {
"release" : "wheezy",
"architecture": "amd64",
"timezone" : "UTC",
"locale" : "en_US",
"charmap" : "UTF-8"
},
"volume": {
"backing" : "raw",
"filesystem": "ext4",
"size" : 1024
},
"plugins": {
"user_packages": {
"enabled": true,
"repo": [ "apache2" ],
"local": []
}
}
}

View file

@ -1,31 +0,0 @@
{
"provider" : "one",
"virtualization": "virtio",
"bootstrapper": {
"mount_dir": "/target"
},
"image": {
"name" : "debian-{release}-{architecture}-{virtualization}-{%y}{%m}{%d}",
"description": "Debian {release} {architecture} ({virtualization})"
},
"system": {
"release" : "wheezy",
"architecture": "amd64",
"timezone" : "UTC",
"locale" : "en_US",
"charmap" : "UTF-8"
},
"volume": {
"backing" : "raw",
"filesystem": "ext4",
"size" : 1024
},
"plugins": {
"user_packages": {
"enabled": true,
"repo": [ "apache2" ],
"local": []
}
}
}

View file

@ -1,41 +0,0 @@
{
"provider" : "virtualbox",
"virtualization": "ide",
"bootstrapper": {
"mount_dir": "/mnt/target",
"mirror" : "http://ftp.fr.debian.org/debian/"
},
"image": {
"name" : "debian-{release}-{architecture}-{virtualization}-{%y}{%m}{%d}",
"description": "Debian {release} {architecture} ({virtualization})"
},
"system": {
"release" : "wheezy",
"architecture": "amd64",
"timezone" : "UTC",
"locale" : "en_US",
"charmap" : "UTF-8"
},
"volume": {
"backing" : "raw",
"filesystem": "ext4",
"size" : 1024,
"loopback_dir" : "/tmp"
},
"plugins": {
"user_packages": {
"enabled": true,
"repo": [ "apache2" ],
"local": []
},
"root_password": {
"enabled": true,
"password": "test"
},
"convert_image": {
"enabled": true,
"format": "vdi"
}
}
}

View file

@ -0,0 +1,33 @@
{
"provider" : "virtualbox",
"bootstrapper": {
"workspace": "/target",
"tarball": true
},
"image": {
"name" : "debian-{release}-{architecture}-{%y}{%m}{%d}",
"description": "Debian {release} {architecture}"
},
"system": {
"release" : "wheezy",
"architecture": "amd64",
"timezone" : "UTC",
"locale" : "en_US",
"charmap" : "UTF-8"
},
"volume": {
"backing": "vdi",
"partitions": {
"type": "mbr",
"boot": {
"size": 32,
"filesystem": "ext2"
},
"root": {
"size": 991,
"filesystem": "ext4"
},
"swap": {"size": 128}
}
}
}

View file

@ -2,11 +2,11 @@
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
import tasks import tasks
tasklist.add(tasks.AddSudoPackage()) tasklist.add(tasks.AddSudoPackage.
tasklist.add(tasks.CreateAdminUser()) tasks.CreateAdminUser,
tasklist.add(tasks.PasswordlessSudo()) tasks.PasswordlessSudo,
tasklist.add(tasks.AdminUserCredentials()) tasks.AdminUserCredentials,
tasklist.add(tasks.DisableRootLogin()) tasks.DisableRootLogin)
def validate_manifest(data, schema_validate): def validate_manifest(data, schema_validate):

View file

@ -2,8 +2,8 @@
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
import tasks import tasks
tasklist.add(tasks.AptSourcesBackports()) tasklist.add(tasks.AptSourcesBackports,
tasklist.add(tasks.AddBackportsPackages()) tasks.AddBackportsPackages)
def validate_manifest(data, schema_validate): def validate_manifest(data, schema_validate):

View file

@ -2,4 +2,4 @@
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
from tasks import WriteMetadata from tasks import WriteMetadata
tasklist.add(WriteMetadata()) tasklist.add(WriteMetadata

View file

@ -2,7 +2,7 @@
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
from tasks import ConvertImage from tasks import ConvertImage
tasklist.add(ConvertImage()) tasklist.add(ConvertImage)
def validate_manifest(data, schema_validate): def validate_manifest(data, schema_validate):

View file

@ -1,10 +1,12 @@
from base import Task from base import Task
from common import phases from common import phases
from common.tasks import loopback
class ConvertImage(Task): class ConvertImage(Task):
description = 'Converting raw image' description = 'Converting raw image'
phase = phases.image_registration phase = phases.image_registration
before = [loopback.MoveImage]
def run(self, info): def run(self, info):
from common.tools import log_check_call from common.tools import log_check_call

View file

@ -2,4 +2,4 @@
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
import tasks import tasks
tasklist.add(tasks.OpenNebulaContext()) tasklist.add(tasks.OpenNebulaContext)

View file

@ -2,39 +2,43 @@ from tasks import Snapshot
from tasks import CopyImage from tasks import CopyImage
from tasks import CreateFromSnapshot from tasks import CreateFromSnapshot
from tasks import CreateFromImage from tasks import CreateFromImage
from tasks import SetBootMountDir
from providers.ec2.tasks import ebs from providers.ec2.tasks import ebs
from common.tasks import loopback from common.tasks import loopback
from common.tasks import volume
from common.tasks import bootstrap from common.tasks import bootstrap
from common.tasks import filesystem from common.tasks import filesystem
from common.tasks import parted from common.tasks import partitioning
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
settings = manifest.plugins['prebootstrapped'] settings = manifest.plugins['prebootstrapped']
skip_tasks = [ebs.Create,
loopback.Create,
filesystem.Format,
partitioning.PartitionVolume,
filesystem.TuneVolumeFS,
filesystem.AddXFSProgs,
filesystem.CreateBootMountDir,
bootstrap.MakeTarball,
bootstrap.Bootstrap]
if manifest.volume['backing'] == 'ebs': if manifest.volume['backing'] == 'ebs':
if 'snapshot' in settings and settings['snapshot'] is not None: if 'snapshot' in settings and settings['snapshot'] is not None:
tasklist.replace(ebs.Create, CreateFromSnapshot()) tasklist.add(CreateFromSnapshot)
tasklist.remove(filesystem.FormatVolume, tasklist.remove(*skip_tasks)
filesystem.TuneVolumeFS, if 'boot' in manifest.volume['partitions']:
filesystem.AddXFSProgs, tasklist.add(SetBootMountDir)
bootstrap.MakeTarball,
bootstrap.Bootstrap)
else: else:
tasklist.add(Snapshot()) tasklist.add(Snapshot)
else: else:
if 'image' in settings and settings['image'] is not None: if 'image' in settings and settings['image'] is not None:
tasklist.add(CreateFromImage()) tasklist.add(CreateFromImage)
tasklist.remove(loopback.Create, tasklist.remove(*skip_tasks)
loopback.CreateQemuImg, if 'boot' in manifest.volume['partitions']:
parted.PartitionVolume, tasklist.add(SetBootMountDir)
parted.FormatPartitions,
filesystem.FormatVolume,
filesystem.TuneVolumeFS,
filesystem.AddXFSProgs,
bootstrap.MakeTarball,
bootstrap.Bootstrap)
else: else:
tasklist.add(CopyImage()) tasklist.add(CopyImage)
def rollback_tasks(tasklist, tasks_completed, manifest): def rollback_tasks(tasklist, tasks_completed, manifest):
@ -42,12 +46,12 @@ def rollback_tasks(tasklist, tasks_completed, manifest):
def counter_task(task, counter): def counter_task(task, counter):
if task in completed and counter not in completed: if task in completed and counter not in completed:
tasklist.add(counter()) tasklist.add(counter)
if manifest.volume['backing'] == 'ebs': if manifest.volume['backing'] == 'ebs':
counter_task(CreateFromSnapshot, ebs.Delete) counter_task(CreateFromSnapshot, volume.Delete)
else: else:
counter_task(CreateFromImage, loopback.Delete) counter_task(CreateFromImage, volume.Delete)
def validate_manifest(data, schema_validate): def validate_manifest(data, schema_validate):

View file

@ -1,64 +1,96 @@
from base import Task from base import Task
from common import phases from common import phases
from providers.ec2.tasks import ebs from common.tasks import volume
from common.tasks import loopback
from common.tasks import bootstrap from common.tasks import bootstrap
from common.tasks import filesystem
from common.fs import remount
from shutil import copyfile
import os.path
import time import time
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Snapshot(ebs.Snapshot): class Snapshot(Task):
description = 'Creating a snapshot of the bootstrapped volume' description = 'Creating a snapshot of the bootstrapped volume'
phase = phases.os_installation phase = phases.os_installation
after = [bootstrap.Bootstrap] after = [bootstrap.Bootstrap, filesystem.MountSpecials]
def run(self, info): def run(self, info):
super(Snapshot, self).run(info) def mk_snapshot():
msg = 'A snapshot of the bootstrapped volume was created. ID: {id}'.format(id=info.snapshot.id) return info.volume.snapshot()
snapshot = remount(info.volume, mk_snapshot)
msg = 'A snapshot of the bootstrapped volume was created. ID: {id}'.format(id=snapshot.id)
log.info(msg) log.info(msg)
class CreateFromSnapshot(Task): class CreateFromSnapshot(Task):
description = 'Creating EBS volume from a snapshot' description = 'Creating EBS volume from a snapshot'
phase = phases.volume_creation phase = phases.volume_creation
before = [ebs.Attach] before = [volume.Attach]
def run(self, info): def run(self, info):
volume_size = int(info.manifest.volume['size'] / 1024) volume_size = int(info.manifest.volume['size'] / 1024)
snapshot = info.manifest.plugins['prebootstrapped']['snapshot'] snapshot = info.manifest.plugins['prebootstrapped']['snapshot']
info.volume = info.connection.create_volume(volume_size, info.volume.volume = info.connection.create_volume(volume_size,
info.host['availabilityZone'], info.host['availabilityZone'],
snapshot=snapshot) snapshot=snapshot)
while info.volume.volume_state() != 'available': while info.volume.volume_state() != 'available':
time.sleep(5) time.sleep(5)
info.volume.update() info.volume.update()
set_fs_states(info.volume)
class CopyImage(Task): class CopyImage(Task):
description = 'Creating a snapshot of the bootstrapped volume' description = 'Creating a snapshot of the bootstrapped volume'
phase = phases.os_installation phase = phases.os_installation
after = [bootstrap.Bootstrap] after = [bootstrap.Bootstrap, filesystem.MountSpecials]
def run(self, info): def run(self, info):
import os.path loopback_backup_name = 'volume-{id:x}.{ext}.backup'.format(id=info.run_id, ext=info.volume.extension)
from shutil import copyfile destination = os.path.join(info.manifest.bootstrapper['workspace'], loopback_backup_name)
loopback_backup_name = 'loopback-{id:x}.img.backup'.format(id=info.run_id)
image_copy_path = os.path.join('/tmp', loopback_backup_name) def mk_snapshot():
copyfile(info.loopback_file, image_copy_path) copyfile(info.volume.image_path, destination)
msg = 'A copy of the bootstrapped volume was created. Path: {path}'.format(path=image_copy_path) remount(info.volume, mk_snapshot)
msg = 'A copy of the bootstrapped volume was created. Path: {path}'.format(path=destination)
log.info(msg) log.info(msg)
class CreateFromImage(Task): class CreateFromImage(Task):
description = 'Creating loopback image from a copy' description = 'Creating loopback image from a copy'
phase = phases.volume_creation phase = phases.volume_creation
before = [loopback.Attach] before = [volume.Attach]
def run(self, info): def run(self, info):
loopback_filename = 'loopback-{id:x}.img'.format(id=info.run_id) info.volume.image_path = os.path.join(info.workspace, 'volume.{ext}'.format(ext=info.volume.extension))
import os.path
info.loopback_file = os.path.join(info.manifest.volume['loopback_dir'], loopback_filename)
loopback_backup_path = info.manifest.plugins['prebootstrapped']['image'] loopback_backup_path = info.manifest.plugins['prebootstrapped']['image']
from shutil import copyfile copyfile(loopback_backup_path, info.volume.image_path)
copyfile(loopback_backup_path, info.loopback_file)
set_fs_states(info.volume)
class SetBootMountDir(Task):
description = 'Setting mountpoint for the boot partition'
phase = phases.volume_mounting
after = [filesystem.MountRoot]
before = [filesystem.MountBoot]
def run(self, info):
info.boot_dir = os.path.join(info.root, 'boot')
def set_fs_states(volume):
volume.fsm.current = 'detached'
p_map = volume.partition_map
partitions_state = 'attached'
from base.fs.partitionmaps.none import NoPartitions
if isinstance(p_map, NoPartitions):
partitions_state = 'formatted'
else:
p_map.fsm.current = 'unmapped'
partitions_state = 'unmapped_fmt'
for partition in p_map.partitions:
partition.fsm.current = partitions_state

View file

@ -3,7 +3,8 @@
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
from common.tasks.security import DisableSSHPasswordAuthentication from common.tasks.security import DisableSSHPasswordAuthentication
from tasks import SetRootPassword from tasks import SetRootPassword
tasklist.replace(DisableSSHPasswordAuthentication, SetRootPassword()) tasklist.remove(DisableSSHPasswordAuthentication)
tasklist.add(SetRootPassword)
def validate_manifest(data, schema_validate): def validate_manifest(data, schema_validate):

View file

@ -2,8 +2,8 @@
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
import tasks import tasks
tasklist.add(tasks.AddUnattendedUpgradesPackage()) tasklist.add(tasks.AddUnattendedUpgradesPackage,
tasklist.add(tasks.EnablePeriodicUpgrades()) tasks.EnablePeriodicUpgrades)
def validate_manifest(data, schema_validate): def validate_manifest(data, schema_validate):

View file

@ -2,5 +2,5 @@
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
from user_packages import AddUserPackages, AddLocalUserPackages from user_packages import AddUserPackages, AddLocalUserPackages
tasklist.add(AddUserPackages()) tasklist.add(AddUserPackages,
tasklist.add(AddLocalUserPackages()) AddLocalUserPackages)

View file

@ -1,24 +1,25 @@
from manifest import Manifest from manifest import Manifest
import logging import logging
from tasks import packages from tasks import packages
from common.tasks import packages as common_packages
from tasks import connection from tasks import connection
from tasks import host from tasks import host
from common.tasks import host as common_host
from tasks import ami from tasks import ami
from common.tasks import volume as volume_tasks
from tasks import ebs from tasks import ebs
from common.tasks import partitioning
from common.tasks import loopback from common.tasks import loopback
from common.tasks import filesystem from common.tasks import filesystem as common_filesystem
from tasks import filesystem
from common.tasks import bootstrap from common.tasks import bootstrap
from common.tasks import locale
from common.tasks import apt
from tasks import boot from tasks import boot
from common.tasks import boot as common_boot from common.tasks import boot as common_boot
from common.tasks import security from common.tasks import security
from common.tasks import network from tasks import network
from common.tasks import network as common_network
from tasks import initd from tasks import initd
from common.tasks import initd as common_initd from common.tasks import initd as common_initd
from common.tasks import cleanup from common.tasks import cleanup
from common.tasks import workspace
def initialize(): def initialize():
@ -27,74 +28,68 @@ def initialize():
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
tasklist.add(packages.HostPackages(), from common.task_sets import base_set
common_packages.HostPackages(), from common.task_sets import mounting_set
packages.ImagePackages(), from common.task_sets import apt_set
common_packages.ImagePackages(), from common.task_sets import locale_set
common_host.CheckPackages(), from common.task_sets import ssh_set
connection.GetCredentials(), tasklist.add(*base_set)
host.GetInfo(), tasklist.add(*mounting_set)
ami.AMIName(), tasklist.add(*apt_set)
connection.Connect(), tasklist.add(*locale_set)
tasklist.add(*ssh_set)
filesystem.FormatVolume(), if manifest.volume['partitions']['type'] != 'none':
filesystem.CreateMountDir(), from common.task_sets import partitioning_set
filesystem.MountVolume(), tasklist.add(*partitioning_set)
bootstrap.Bootstrap(), tasklist.add(packages.HostPackages,
filesystem.MountSpecials(), packages.ImagePackages,
locale.GenerateLocale(), connection.GetCredentials,
locale.SetTimezone(), host.GetInfo,
apt.DisableDaemonAutostart(), ami.AMIName,
apt.AptSources(), connection.Connect,
apt.AptUpgrade(),
boot.ConfigureGrub(),
filesystem.ModifyFstab(),
common_boot.BlackListModules(),
common_boot.DisableGetTTYs(),
security.EnableShadowConfig(),
security.DisableSSHPasswordAuthentication(),
security.DisableSSHDNSLookup(),
network.RemoveDNSInfo(),
network.ConfigureNetworkIF(),
network.ConfigureDHCP(),
common_initd.ResolveInitScripts(),
initd.AddEC2InitScripts(),
common_initd.InstallInitScripts(),
cleanup.ClearMOTD(),
cleanup.ShredHostkeys(),
cleanup.CleanTMP(),
apt.PurgeUnusedPackages(),
apt.AptClean(),
apt.EnableDaemonAutostart(),
filesystem.UnmountSpecials(),
filesystem.UnmountVolume(), boot.ConfigureGrub,
filesystem.DeleteMountDir(), common_boot.BlackListModules,
ami.RegisterAMI()) common_boot.DisableGetTTYs,
security.EnableShadowConfig,
common_network.RemoveDNSInfo,
common_network.ConfigureNetworkIF,
network.EnableDHCPCDDNS,
common_initd.ResolveInitScripts,
initd.AddEC2InitScripts,
common_initd.InstallInitScripts,
initd.AdjustExpandVolumeScript,
cleanup.ClearMOTD,
cleanup.CleanTMP,
if manifest.bootstrapper['tarball']: ami.RegisterAMI)
tasklist.add(bootstrap.MakeTarball())
backing_specific_tasks = {'ebs': [ebs.Create(), backing_specific_tasks = {'ebs': [ebs.Create,
ebs.Attach(), ebs.Attach,
ebs.Detach(), common_filesystem.FStab,
ebs.Snapshot(), ebs.Snapshot],
ebs.Delete()], 's3': [loopback.Create,
's3': [loopback.Create(), volume_tasks.Attach,
loopback.Attach(), filesystem.S3FStab,
loopback.Detach(), ami.BundleImage,
ami.BundleImage(), ami.UploadImage,
ami.UploadImage(), ami.RemoveBundle]}
loopback.Delete(),
ami.RemoveBundle()]}
tasklist.add(*backing_specific_tasks.get(manifest.volume['backing'].lower())) tasklist.add(*backing_specific_tasks.get(manifest.volume['backing'].lower()))
tasklist.add(common_filesystem.Format,
volume_tasks.Detach,
volume_tasks.Delete)
filesystem_specific_tasks = {'xfs': [filesystem.AddXFSProgs()], if manifest.bootstrapper.get('tarball', False):
'ext2': [filesystem.TuneVolumeFS()], tasklist.add(bootstrap.MakeTarball)
'ext3': [filesystem.TuneVolumeFS()],
'ext4': [filesystem.TuneVolumeFS()]} from common.task_sets import get_fs_specific_set
tasklist.add(*filesystem_specific_tasks.get(manifest.volume['filesystem'].lower())) tasklist.add(*get_fs_specific_set(manifest.volume['partitions']))
if 'boot' in manifest.volume['partitions']:
from common.task_sets import boot_partition_set
tasklist.add(*boot_partition_set)
def rollback_tasks(tasklist, tasks_completed, manifest): def rollback_tasks(tasklist, tasks_completed, manifest):
@ -102,14 +97,20 @@ def rollback_tasks(tasklist, tasks_completed, manifest):
def counter_task(task, counter): def counter_task(task, counter):
if task in completed and counter not in completed: if task in completed and counter not in completed:
tasklist.add(counter()) tasklist.add(counter)
if manifest.volume['backing'].lower() == 'ebs': counter_task(ebs.Create, volume_tasks.Delete)
counter_task(ebs.Create, ebs.Delete) counter_task(ebs.Attach, volume_tasks.Detach)
counter_task(ebs.Attach, ebs.Detach)
if manifest.volume['backing'].lower() == 's3': counter_task(loopback.Create, volume_tasks.Delete)
counter_task(loopback.Create, loopback.Delete) counter_task(volume_tasks.Attach, volume_tasks.Detach)
counter_task(loopback.Attach, loopback.Detach)
counter_task(filesystem.CreateMountDir, filesystem.DeleteMountDir) counter_task(partitioning.MapPartitions, partitioning.UnmapPartitions)
counter_task(filesystem.MountVolume, filesystem.UnmountVolume) counter_task(common_filesystem.CreateMountDir, common_filesystem.DeleteMountDir)
counter_task(filesystem.MountSpecials, filesystem.UnmountSpecials) counter_task(common_filesystem.MountSpecials, common_filesystem.UnmountSpecials)
counter_task(common_filesystem.MountRoot, common_filesystem.UnmountRoot)
counter_task(common_filesystem.MountBoot, common_filesystem.UnmountBoot)
counter_task(volume_tasks.Attach, volume_tasks.Detach)
counter_task(workspace.CreateWorkspace, workspace.DeleteWorkspace)
counter_task(ami.BundleImage, ami.RemoveBundle)

View file

@ -13,7 +13,7 @@ libdir=${exec_prefix}/lib
export TEXTDOMAIN=grub export TEXTDOMAIN=grub
export TEXTDOMAINDIR=${prefix}/share/locale export TEXTDOMAINDIR=${prefix}/share/locale
GRUB_DEVICE=/dev/xvda1 GRUB_DEVICE=/dev/xvda
cat << EOF cat << EOF

View file

@ -0,0 +1,57 @@
from base.fs.volume import Volume
from base.fs.exceptions import VolumeError
import time
class EBSVolume(Volume):
def create(self, conn, zone):
self.fsm.create(connection=conn, zone=zone)
def _before_create(self, e):
conn = e.connection
zone = e.zone
import math
size = int(math.ceil(self.partition_map.get_total_size() / 1024))
self.volume = conn.create_volume(size, zone)
while self.volume.volume_state() != 'available':
time.sleep(5)
self.volume.update()
def attach(self, instance_id):
self.fsm.attach(instance_id=instance_id)
def _before_attach(self, e):
instance_id = e.instance_id
import os.path
import string
for letter in string.ascii_lowercase[5:]:
dev_path = os.path.join('/dev', 'xvd' + letter)
if not os.path.exists(dev_path):
self.device_path = dev_path
self.ec2_device_path = os.path.join('/dev', 'sd' + letter)
break
if self.device_path is None:
raise VolumeError('Unable to find a free block device path for mounting the bootstrap volume')
self.volume.attach(instance_id, self.ec2_device_path)
while self.volume.attachment_state() != 'attached':
time.sleep(2)
self.volume.update()
def _before_detach(self, e):
self.volume.detach()
while self.volume.attachment_state() is not None:
time.sleep(2)
self.volume.update()
def _before_delete(self, e):
self.volume.delete()
def snapshot(self):
snapshot = self.volume.create_snapshot()
while snapshot.status != 'completed':
time.sleep(2)
snapshot.update()
return snapshot

View file

@ -25,6 +25,17 @@
} }
}, },
"required": ["bucket"] "required": ["bucket"]
},
"volume": {
"type": "object",
"properties": {
"partitions": {
"type": "object",
"properties": {
"type": { "enum": ["none"] }
}
}
}
} }
}, },
"required": ["image"] "required": ["image"]

View file

@ -3,6 +3,17 @@
"title": "EC2 manifest", "title": "EC2 manifest",
"type": "object", "type": "object",
"properties": { "properties": {
"image": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
}
},
"credentials": { "credentials": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -17,17 +28,16 @@
"volume": { "volume": {
"type": "object", "type": "object",
"properties": { "properties": {
"backing": { "backing": { "enum": ["ebs", "s3"] },
"type": "string", "partitions": {
"enum": ["ebs", "s3"] "type": "object",
}, "properties": {
"filesystem": { "type": { "enum": ["none", "mbr"] }
"type": "string", }
"enum": ["ext2", "ext3", "ext4", "xfs"]
} }
}, },
"required": ["backing", "filesystem"] "required": ["backing"]
} }
}, },
"required": ["volume"] "required": ["image"]
} }

View file

@ -9,8 +9,10 @@ class Manifest(base.Manifest):
schema_path = path.join(path.dirname(__file__), 'manifest-schema.json') schema_path = path.join(path.dirname(__file__), 'manifest-schema.json')
self.schema_validate(data, schema_path) self.schema_validate(data, schema_path)
if data['volume']['backing'] == 'ebs': if data['volume']['backing'] == 'ebs':
if data['volume']['size'] % 1024 != 0: volume_size = self._calculate_volume_size(data['volume']['partitions'])
msg = 'The volume size must be a multiple of 1024 when using EBS backing' if volume_size % 1024 != 0:
msg = ('The volume size must be a multiple of 1024 when using EBS backing '
'(MBR partitioned volumes are 1MB larger than specified, for the post-mbr gap)')
raise ManifestError(msg, self) raise ManifestError(msg, self)
else: else:
schema_path = path.join(path.dirname(__file__), 'manifest-schema-s3.json') schema_path = path.join(path.dirname(__file__), 'manifest-schema-s3.json')
@ -21,9 +23,15 @@ class Manifest(base.Manifest):
self.credentials = data['credentials'] self.credentials = data['credentials']
self.virtualization = data['virtualization'] self.virtualization = data['virtualization']
self.image = data['image'] self.image = data['image']
if data['volume']['backing'] == 'ebs':
self.ebs_volume_size = data['volume']['size'] / 1024 def _calculate_volume_size(self, partitions):
if 'loopback_dir' not in self.volume and self.volume['backing'].lower() == 's3': if partitions['type'] == 'mbr':
self.volume['loopback_dir'] = '/tmp' size = 1
if 'bundle_dir' not in self.image and self.volume['backing'].lower() == 's3': else:
self.image['bundle_dir'] = '/tmp' size = 0
if 'boot' in partitions:
size += partitions['boot']['size']
size += partitions['root']['size']
if 'swap' in partitions:
size += partitions['swap']['size']
return size

View file

@ -3,6 +3,7 @@ from common import phases
from common.exceptions import TaskError from common.exceptions import TaskError
from common.tools import log_check_call from common.tools import log_check_call
from ebs import Snapshot from ebs import Snapshot
from common.tasks import workspace
from connection import Connect from connection import Connect
import os.path import os.path
@ -45,9 +46,9 @@ class BundleImage(Task):
def run(self, info): def run(self, info):
bundle_name = 'bundle-{id:x}'.format(id=info.run_id) bundle_name = 'bundle-{id:x}'.format(id=info.run_id)
info.bundle_path = os.path.join(info.manifest.image['bundle_dir'], bundle_name) info.bundle_path = os.path.join(info.workspace, bundle_name)
log_check_call(['/usr/bin/euca-bundle-image', log_check_call(['/usr/bin/euca-bundle-image',
'--image', info.loopback_file, '--image', info.volume.image_path,
'--user', info.credentials['user-id'], '--user', info.credentials['user-id'],
'--privatekey', info.credentials['private-key'], '--privatekey', info.credentials['private-key'],
'--cert', info.credentials['certificate'], '--cert', info.credentials['certificate'],
@ -80,6 +81,7 @@ class UploadImage(Task):
class RemoveBundle(Task): class RemoveBundle(Task):
description = 'Removing the bundle files' description = 'Removing the bundle files'
phase = phases.cleaning phase = phases.cleaning
before = [workspace.DeleteWorkspace]
def run(self, info): def run(self, info):
from shutil import rmtree from shutil import rmtree
@ -92,46 +94,109 @@ class RegisterAMI(Task):
phase = phases.image_registration phase = phases.image_registration
after = [Snapshot, UploadImage] after = [Snapshot, UploadImage]
kernel_mapping = {'us-east-1': {'amd64': 'aki-88aa75e1', # Source: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/UserProvidedKernels.html#AmazonKernelImageIDs
'i386': 'aki-b6aa75df'}, kernel_mapping = {'ap-northeast-1': { # Asia Pacific (Tokyo) Region
'us-west-1': {'amd64': 'aki-f77e26b2', 'hd0': {'i386': 'aki-136bf512', # pv-grub-hd0_1.04-i386.gz
'i386': 'aki-f57e26b0'}, 'amd64': 'aki-176bf516'}, # pv-grub-hd0_1.04-x86_64.gz
'us-west-2': {'amd64': 'aki-fc37bacc', 'hd00': {'i386': 'aki-196bf518', # pv-grub-hd00_1.04-i386.gz
'i386': 'aki-fa37baca'}, 'amd64': 'aki-1f6bf51e'} # pv-grub-hd00_1.04-x86_64.gz
'eu-west-1': {'amd64': 'aki-71665e05', },
'i386': 'aki-75665e01'}, 'ap-southeast-1': { # Asia Pacific (Singapore) Region
'ap-southeast-1': {'amd64': 'aki-fe1354ac', 'hd0': {'i386': 'aki-ae3973fc', # pv-grub-hd0_1.04-i386.gz
'i386': 'aki-f81354aa'}, 'amd64': 'aki-503e7402'}, # pv-grub-hd0_1.04-x86_64.gz
'ap-southeast-2': {'amd64': 'aki-31990e0b', 'hd00': {'i386': 'aki-563e7404', # pv-grub-hd00_1.04-i386.gz
'i386': 'aki-33990e09'}, 'amd64': 'aki-5e3e740c'} # pv-grub-hd00_1.04-x86_64.gz
'ap-northeast-1': {'amd64': 'aki-44992845', },
'i386': 'aki-42992843'}, 'ap-southeast-2': { # Asia Pacific (Sydney) Region
'sa-east-1': {'amd64': 'aki-c48f51d9', 'hd0': {'i386': 'aki-cd62fff7', # pv-grub-hd0_1.04-i386.gz
'i386': 'aki-ca8f51d7'}, 'amd64': 'aki-c362fff9'}, # pv-grub-hd0_1.04-x86_64.gz
'us-gov-west-1': {'amd64': 'aki-79a4c05a', 'hd00': {'i386': 'aki-c162fffb', # pv-grub-hd00_1.04-i386.gz
'i386': 'aki-7ba4c058'}} 'amd64': 'aki-3b1d8001'} # pv-grub-hd00_1.04-x86_64.gz
},
'eu-west-1': { # EU (Ireland) Region
'hd0': {'i386': 'aki-68a3451f', # pv-grub-hd0_1.04-i386.gz
'amd64': 'aki-52a34525'}, # pv-grub-hd0_1.04-x86_64.gz
'hd00': {'i386': 'aki-5ea34529', # pv-grub-hd00_1.04-i386.gz
'amd64': 'aki-58a3452f'} # pv-grub-hd00_1.04-x86_64.gz
},
'sa-east-1': { # South America (Sao Paulo) Region
'hd0': {'i386': 'aki-5b53f446', # pv-grub-hd0_1.04-i386.gz
'amd64': 'aki-5553f448'}, # pv-grub-hd0_1.04-x86_64.gz
'hd00': {'i386': 'aki-5753f44a', # pv-grub-hd00_1.04-i386.gz
'amd64': 'aki-5153f44c'} # pv-grub-hd00_1.04-x86_64.gz
},
'us-east-1': { # US East (Northern Virginia) Region
'hd0': {'i386': 'aki-8f9dcae6', # pv-grub-hd0_1.04-i386.gz
'amd64': 'aki-919dcaf8'}, # pv-grub-hd0_1.04-x86_64.gz
'hd00': {'i386': 'aki-659ccb0c', # pv-grub-hd00_1.04-i386.gz
'amd64': 'aki-499ccb20'} # pv-grub-hd00_1.04-x86_64.gz
},
'us-gov-west-1': { # AWS GovCloud (US)
'hd0': {'i386': 'aki-1fe98d3c', # pv-grub-hd0_1.04-i386.gz
'amd64': 'aki-1de98d3e'}, # pv-grub-hd0_1.04-x86_64.gz
'hd00': {'i386': 'aki-63e98d40', # pv-grub-hd00_1.04-i386.gz
'amd64': 'aki-61e98d42'} # pv-grub-hd00_1.04-x86_64.gz
},
'us-west-1': { # US West (Northern California) Region
'hd0': {'i386': 'aki-8e0531cb', # pv-grub-hd0_1.04-i386.gz
'amd64': 'aki-880531cd'}, # pv-grub-hd0_1.04-x86_64.gz
'hd00': {'i386': 'aki-960531d3', # pv-grub-hd00_1.04-i386.gz
'amd64': 'aki-920531d7'} # pv-grub-hd00_1.04-x86_64.gz
},
'us-west-2': { # US West (Oregon) Region
'hd0': {'i386': 'aki-f08f11c0', # pv-grub-hd0_1.04-i386.gz
'amd64': 'aki-fc8f11cc'}, # pv-grub-hd0_1.04-x86_64.gz
'hd00': {'i386': 'aki-e28f11d2', # pv-grub-hd00_1.04-i386.gz
'amd64': 'aki-e68f11d6'} # pv-grub-hd00_1.04-x86_64.gz
}}
def run(self, info): def run(self, info):
arch = {'i386': 'i386', 'amd64': 'x86_64'}.get(info.manifest.system['architecture'])
kernel_id = self.kernel_mapping.get(info.host['region']).get(info.manifest.system['architecture'])
if info.manifest.volume['backing'] == 'ebs': if info.manifest.volume['backing'] == 'ebs':
from boto.ec2.blockdevicemapping import BlockDeviceType self.run_ebs(info)
from boto.ec2.blockdevicemapping import BlockDeviceMapping
block_device = BlockDeviceType(snapshot_id=info.snapshot.id, delete_on_termination=True,
size=info.manifest.ebs_volume_size)
block_device_map = BlockDeviceMapping()
block_device_map['/dev/sda1'] = block_device
info.image = info.connection.register_image(name=info.ami_name, description=info.ami_description,
architecture=arch, kernel_id=kernel_id,
root_device_name='/dev/sda1',
block_device_map=block_device_map)
if info.manifest.volume['backing'] == 's3': if info.manifest.volume['backing'] == 's3':
image_location = ('{bucket}/{ami_name}.manifest.xml' self.run_s3(info)
.format(bucket=info.manifest.image['bucket'],
ami_name=info.ami_name)) def run_ebs(self, info):
info.image = info.connection.register_image(description=info.ami_description, arch = {'i386': 'i386', 'amd64': 'x86_64'}.get(info.manifest.system['architecture'])
architecture=arch, kernel_id=kernel_id,
root_device_name='/dev/sda1', from base.fs.partitionmaps.none import NoPartitions
image_location=image_location) if isinstance(info.volume.partition_map, NoPartitions):
grub_boot_device = 'hd0'
root_device_name = '/dev/sda'
else:
grub_boot_device = 'hd00'
root_idx = info.volume.partition_map.root.get_index()
root_device_name = '/dev/sda{idx}'.format(idx=root_idx)
kernel_id = (self.kernel_mapping
.get(info.host['region'])
.get(grub_boot_device)
.get(info.manifest.system['architecture']))
from boto.ec2.blockdevicemapping import BlockDeviceType
from boto.ec2.blockdevicemapping import BlockDeviceMapping
block_device = BlockDeviceType(snapshot_id=info.snapshot.id, delete_on_termination=True,
size=info.volume.partition_map.get_total_size()/1024)
block_device_map = BlockDeviceMapping()
block_device_map['/dev/sda'] = block_device
info.image = info.connection.register_image(name=info.ami_name, description=info.ami_description,
architecture=arch, kernel_id=kernel_id,
root_device_name=root_device_name,
block_device_map=block_device_map)
def run_s3(self, info):
arch = {'i386': 'i386', 'amd64': 'x86_64'}.get(info.manifest.system['architecture'])
kernel_id = (self.kernel_mapping
.get(info.host['region'])
.get('hd0')
.get(info.manifest.system['architecture']))
image_manifest = ('{bucket}/{ami_name}.manifest.xml'
.format(bucket=info.manifest.image['bucket'],
ami_name=info.ami_name))
info.image = info.connection.register_image(description=info.ami_description,
architecture=arch, kernel_id=kernel_id,
root_device_name='dev/sda1',
image_location=image_manifest)

View file

@ -24,6 +24,19 @@ class ConfigureGrub(Task):
copy(script_src, script_dst) copy(script_src, script_dst)
os.chmod(script_dst, rwxr_xr_x) os.chmod(script_dst, rwxr_xr_x)
from base.fs.partitionmaps.none import NoPartitions
if not isinstance(info.volume.partition_map, NoPartitions):
from common.tools import sed_i
root_idx = info.volume.partition_map.root.get_index()
grub_device = 'GRUB_DEVICE=/dev/xvda{idx}'.format(idx=root_idx)
sed_i(script_dst, '^GRUB_DEVICE=/dev/xvda$', grub_device)
grub_root = '\troot (hd0,{idx})'.format(idx=root_idx-1)
sed_i(script_dst, '^\troot \(hd0\)$', grub_root)
if info.manifest.volume['backing'] == 's3':
from common.tools import sed_i
sed_i(script_dst, '^GRUB_DEVICE=/dev/xvda$', 'GRUB_DEVICE=/dev/xvda1')
from common.tools import sed_i from common.tools import sed_i
grub_def = os.path.join(info.root, 'etc/default/grub') grub_def = os.path.join(info.root, 'etc/default/grub')
sed_i(grub_def, '^GRUB_TIMEOUT=[0-9]+', 'GRUB_TIMEOUT=0\n' sed_i(grub_def, '^GRUB_TIMEOUT=[0-9]+', 'GRUB_TIMEOUT=0\n'

View file

@ -20,9 +20,12 @@ class GetCredentials(Task):
for key in keys: for key in keys:
creds[key] = manifest.credentials[key] creds[key] = manifest.credentials[key]
return creds return creds
if all(getenv(key) is not None for key in keys):
def env_key(key):
return ('aws-'+key).upper().replace('-', '_')
if all(getenv(env_key(key)) is not None for key in keys):
for key in keys: for key in keys:
creds[key] = getenv(key) creds[key] = getenv(env_key(key))
return creds return creds
raise RuntimeError(('No ec2 credentials found, they must all be specified ' raise RuntimeError(('No ec2 credentials found, they must all be specified '
'exclusively via environment variables or through the manifest.')) 'exclusively via environment variables or through the manifest.'))

View file

@ -1,60 +1,22 @@
from base import Task from base import Task
from common import phases from common import phases
from common.exceptions import TaskError
from common.tasks.filesystem import UnmountVolume
import time
class Create(Task): class Create(Task):
description = 'Creating an EBS volume for bootstrapping' description = 'Creating the EBS volume'
phase = phases.volume_creation phase = phases.volume_creation
def run(self, info): def run(self, info):
info.volume = info.connection.create_volume(info.manifest.ebs_volume_size, info.volume.create(info.connection, info.host['availabilityZone'])
info.host['availabilityZone'])
while info.volume.volume_state() != 'available':
time.sleep(5)
info.volume.update()
class Attach(Task): class Attach(Task):
description = 'Attaching the EBS volume' description = 'Attaching the volume'
phase = phases.volume_creation phase = phases.volume_creation
after = [Create] after = [Create]
def run(self, info): def run(self, info):
def char_range(c1, c2): info.volume.attach(info.host['instanceId'])
"""Generates the characters from `c1` to `c2`, inclusive."""
for c in xrange(ord(c1), ord(c2) + 1):
yield chr(c)
import os.path
info.bootstrap_device = {}
for letter in char_range('f', 'z'):
dev_path = os.path.join('/dev', 'xvd' + letter)
if not os.path.exists(dev_path):
info.bootstrap_device['path'] = dev_path
info.bootstrap_device['ec2_path'] = os.path.join('/dev', 'sd' + letter)
break
if 'path' not in info.bootstrap_device:
raise VolumeError('Unable to find a free block device path for mounting the bootstrap volume')
info.volume.attach(info.host['instanceId'], info.bootstrap_device['ec2_path'])
while info.volume.attachment_state() != 'attached':
time.sleep(2)
info.volume.update()
class Detach(Task):
description = 'Detaching the EBS volume'
phase = phases.volume_unmounting
after = [UnmountVolume]
def run(self, info):
info.volume.detach()
while info.volume.attachment_state() is not None:
time.sleep(2)
info.volume.update()
class Snapshot(Task): class Snapshot(Task):
@ -62,20 +24,4 @@ class Snapshot(Task):
phase = phases.image_registration phase = phases.image_registration
def run(self, info): def run(self, info):
info.snapshot = info.volume.create_snapshot() info.snapshot = info.volume.snapshot()
while info.snapshot.status != 'completed':
time.sleep(2)
info.snapshot.update()
class Delete(Task):
description = 'Deleting the EBS volume'
phase = phases.cleaning
def run(self, info):
info.volume.delete()
del info.volume
class VolumeError(TaskError):
pass

View file

@ -0,0 +1,27 @@
from base import Task
from common import phases
class S3FStab(Task):
description = 'Adding the S3 root partition to the fstab'
phase = phases.system_modification
def run(self, info):
import os.path
root = info.volume.partition_map.root
fstab_lines = []
mount_opts = ['defaults']
fstab_lines.append('{device_path}{idx} {mountpoint} {filesystem} {mount_opts} {dump} {pass_num}'
.format(device_path='/dev/xvda',
idx=1,
mountpoint='/',
filesystem=root.filesystem,
mount_opts=','.join(mount_opts),
dump='1',
pass_num='1'))
fstab_path = os.path.join(info.root, 'etc/fstab')
with open(fstab_path, 'w') as fstab:
fstab.write('\n'.join(fstab_lines))
fstab.write('\n')

View file

@ -1,5 +1,6 @@
from base import Task from base import Task
from common import phases from common import phases
from common.exceptions import TaskError
from common.tasks import initd from common.tasks import initd
import os.path import os.path
@ -17,3 +18,22 @@ class AddEC2InitScripts(Task):
init_scripts_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), '../assets/init.d')) init_scripts_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), '../assets/init.d'))
for name, path in init_scripts.iteritems(): for name, path in init_scripts.iteritems():
info.initd['install'][name] = os.path.join(init_scripts_dir, path) info.initd['install'][name] = os.path.join(init_scripts_dir, path)
class AdjustExpandVolumeScript(Task):
description = 'Adjusting the expand-volume script'
phase = phases.system_modification
after = [initd.InstallInitScripts]
def run(self, info):
if 'expand-volume' not in info.initd['install']:
raise TaskError('The expand-volume script was not installed')
from base.fs.partitionmaps.none import NoPartitions
if not isinstance(info.volume.partition_map, NoPartitions):
import os.path
from common.tools import sed_i
script = os.path.join(info.root, 'etc/init.d.expand-volume')
root_idx = info.volume.partition_map.root.get_index()
device_path = 'device_path="/dev/xvda{idx}"'.format(idx=root_idx)
sed_i(script, '^device_path="/dev/xvda$', device_path)

View file

@ -0,0 +1,15 @@
from base import Task
from common import phases
import os.path
class EnableDHCPCDDNS(Task):
description = 'Configuring the DHCP client to set the nameservers'
phase = phases.system_modification
def run(self, info):
# The dhcp client that ships with debian sets the DNS servers per default.
# For dhcpcd we need to configure it to do that.
from common.tools import sed_i
dhcpcd = os.path.join(info.root, 'etc/default/dhcpcd')
sed_i(dhcpcd, '^#*SET_DNS=.*', 'SET_DNS=\'yes\'')

View file

@ -11,8 +11,6 @@ class HostPackages(Task):
after = [packages.HostPackages] after = [packages.HostPackages]
def run(self, info): def run(self, info):
if info.manifest.volume['filesystem'] == 'xfs':
info.host_packages.add('xfsprogs')
if info.manifest.volume['backing'] == 's3': if info.manifest.volume['backing'] == 's3':
info.host_packages.add('euca2ools') info.host_packages.add('euca2ools')
@ -25,6 +23,7 @@ class ImagePackages(Task):
def run(self, info): def run(self, info):
manifest = info.manifest manifest = info.manifest
include, exclude = info.img_packages include, exclude = info.img_packages
include.add('openssh-server')
include.add('file') # Needed for the init scripts include.add('file') # Needed for the init scripts
include.add('dhcpcd') # isc-dhcp-client doesn't work properly with ec2 include.add('dhcpcd') # isc-dhcp-client doesn't work properly with ec2
if manifest.virtualization == 'pvm': if manifest.virtualization == 'pvm':

View file

@ -1,9 +0,0 @@
this provider creates images for KVM.
It is possible to add opennebula plugin for OpenNebula images.
Disk virtuzalition is specified by the virtualization field in the manifest.
Allowed values are:
* ide: basic disk emulation on /dev/sda1
* virtio: enhanced performance on /dev/vda1. It adds virtio modules in the kernel and needs configuration update in VM template: <target dev='vda' bus='virtio'/>

View file

@ -1,92 +0,0 @@
from manifest import Manifest
from tasks import packages
from common.tasks import packages as common_packages
from common.tasks import host
from common.tasks import loopback
from common.tasks import parted
from common.tasks import filesystem
from common.tasks import bootstrap
from common.tasks import locale
from common.tasks import apt
from tasks import boot
from common.tasks import boot as common_boot
from common.tasks import security
from common.tasks import network
from common.tasks import initd
from common.tasks import cleanup
from common.tasks import loopback
def initialize():
pass
def tasks(tasklist, manifest):
tasklist.add(packages.HostPackages(),
common_packages.HostPackages(),
packages.ImagePackages(),
common_packages.ImagePackages(),
host.CheckPackages(),
loopback.CreateQemuImg(),
loopback.Attach(),
parted.PartitionVolume(),
parted.MapPartitions(),
parted.FormatPartitions(),
filesystem.CreateMountDir(),
filesystem.MountVolume(),
bootstrap.Bootstrap(),
filesystem.MountSpecials(),
locale.GenerateLocale(),
locale.SetTimezone(),
apt.DisableDaemonAutostart(),
apt.AptSources(),
apt.AptUpgrade(),
boot.ConfigureGrub(),
filesystem.ModifyFstab(),
common_boot.BlackListModules(),
common_boot.DisableGetTTYs(),
security.EnableShadowConfig(),
security.DisableSSHPasswordAuthentication(),
security.DisableSSHDNSLookup(),
network.RemoveDNSInfo(),
network.ConfigureNetworkIF(),
network.ConfigureDHCP(),
initd.ResolveInitScripts(),
initd.InstallInitScripts(),
cleanup.ClearMOTD(),
cleanup.ShredHostkeys(),
cleanup.CleanTMP(),
apt.PurgeUnusedPackages(),
apt.AptClean(),
apt.EnableDaemonAutostart(),
filesystem.UnmountSpecials(),
filesystem.UnmountVolume(),
parted.UnmapPartitions(),
loopback.Detach(),
filesystem.DeleteMountDir())
if manifest.bootstrapper['tarball']:
tasklist.add(bootstrap.MakeTarball())
filesystem_specific_tasks = {'xfs': [filesystem.AddXFSProgs()],
'ext2': [filesystem.TuneVolumeFS()],
'ext3': [filesystem.TuneVolumeFS()],
'ext4': [filesystem.TuneVolumeFS()]}
tasklist.add(*filesystem_specific_tasks.get(manifest.volume['filesystem'].lower()))
def rollback_tasks(tasklist, tasks_completed, manifest):
completed = [type(task) for task in tasks_completed]
def counter_task(task, counter):
if task in completed and counter not in completed:
tasklist.add(counter())
counter_task(filesystem.CreateMountDir, filesystem.DeleteMountDir)
counter_task(parted.MapPartitions, parted.UnmapPartitions)
counter_task(filesystem.MountVolume, filesystem.UnmountVolume)
counter_task(filesystem.MountSpecials, filesystem.UnmountSpecials)
counter_task(loopback.Attach, loopback.Detach)

View file

@ -1,4 +0,0 @@
#! /bin/sh
set -e
# nothing to do, skip grub mkconfig for this

View file

@ -1,93 +0,0 @@
#!/bin/sh
# This file generates the old menu.lst configuration with grub2
# It was copied from tomheadys github repo:
# https://github.com/tomheady/ec2debian/blob/master/src/root/etc/grub.d/40_custom
prefix=/usr
exec_prefix=${prefix}
bindir=${exec_prefix}/bin
libdir=${exec_prefix}/lib
. ${libdir}/grub/grub-mkconfig_lib
export TEXTDOMAIN=grub
export TEXTDOMAINDIR=${prefix}/share/locale
GRUB_DEVICE=/dev/sda1
cat << EOF
set default=${GRUB_DEFAULT}
set timeout=${GRUB_TIMEOUT}
insmod part_msdos
insmod ext2
insmod gettext
set menu_color_normal=cyan/blue
set menu_color_highlight=white/blue
set root='(hd0,msdos1)'
EOF
if ${GRUB_HIDDEN_TIMEOUT:-false}; then
printf "hiddenmenu\n"
fi
linux_entry ()
{
os="$1"
version="$2"
args="$4"
title="$(gettext_quoted "%s, with Linux %s")"
cat << EOF
menuentry 'Debian GNU/Linux, ${version}' --class debian --class gnu-linux --class os {
insmod part_msdos
insmod ext2
set timeout=${GRUB_TIMEOUT}
set root='(hd0,msdos1)'
echo 'Loading Linux ${version}'
linux ${rel_dirname}/${basename} root=${GRUB_DEVICE} ro ${args}
echo 'Loading initial ramdisk ...'
initrd ${rel_dirname}/${initrd}
}
EOF
}
list=`for i in /boot/vmlinuz-* /boot/vmlinux-* /vmlinuz-* /vmlinux-* ; do
if grub_file_is_not_garbage "$i" ; then echo -n "$i " ; fi
done`
prepare_boot_cache=
while [ "x$list" != "x" ] ; do
linux=`version_find_latest $list`
basename=`basename $linux`
dirname=`dirname $linux`
rel_dirname=`make_system_path_relative_to_its_root $dirname`
version=`echo $basename | sed -e "s,^[^0-9]*-,,g"`
alt_version=`echo $version | sed -e "s,\.old$,,g"`
linux_root_device_thisversion="${LINUX_ROOT_DEVICE}"
initrd=
for i in "initrd.img-${version}" "initrd-${version}.img" \
"initrd-${version}" "initramfs-${version}.img" \
"initrd.img-${alt_version}" "initrd-${alt_version}.img" \
"initrd-${alt_version}" "initramfs-${alt_version}.img"; do
if test -e "${dirname}/${i}" ; then
initrd="$i"
break
fi
done
initramfs=
for i in "config-${version}" "config-${alt_version}"; do
if test -e "${dirname}/${i}" ; then
initramfs=`grep CONFIG_INITRAMFS_SOURCE= "${dirname}/${i}" | cut -f2 -d= | tr -d \"`
break
fi
done
linux_entry "${OS}" "${version}" \
"${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}"
list=`echo $list | tr ' ' '\n' | grep -vx $linux | tr '\n' ' '`
done

View file

@ -1,22 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Kvm manifest",
"type": "object",
"properties": {
"volume": {
"type": "object",
"properties": {
"backing": {
"type": "string",
"enum": ["raw", "qcow2"]
},
"filesystem": {
"type": "string",
"enum": ["ext2", "ext3", "ext4", "xfs"]
}
},
"required": ["backing", "filesystem"]
}
},
"required": ["volume"]
}

View file

@ -1,16 +0,0 @@
import base
class Manifest(base.Manifest):
def validate(self, data):
super(Manifest, self).validate(data)
from os import path
schema_path = path.join(path.dirname(__file__), 'manifest-schema.json')
self.schema_validate(data, schema_path)
def parse(self, data):
super(Manifest, self).parse(data)
self.image = data['image']
self.virtualization = data['virtualization']
if 'loopback_dir' not in self.volume:
self.volume['loopback_dir'] = '/tmp'

View file

@ -1,55 +0,0 @@
from base import Task
from common import phases
class ConfigureGrub(Task):
description = 'Configuring grub for KVM'
phase = phases.system_modification
def run(self, info):
import stat
rwxr_xr_x = (stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH)
x_all = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
import os.path
device_map_path = os.path.join(info.root, 'boot/grub/device.map')
with open(device_map_path, 'w') as device_map:
device_map.write('(hd0) /dev/sda\n')
from common.tools import log_check_call
from shutil import copy
script_src = os.path.normpath(os.path.join(os.path.dirname(__file__), '../assets/grub.d/10_linux'))
script_dst = os.path.join(info.root, 'etc/grub.d/10_linux')
copy(script_src, script_dst)
os.chmod(script_dst, rwxr_xr_x)
script_src = os.path.normpath(os.path.join(os.path.dirname(__file__), '../assets/grub.d/00_header'))
script_dst = os.path.join(info.root, 'etc/grub.d/00_header')
copy(script_src, script_dst)
os.chmod(script_dst, rwxr_xr_x)
if info.manifest.virtualization == 'virtio':
print "Using virtio"
modules_path = os.path.join(info.root,
'etc/initramfs-tools/modules')
with open(modules_path, 'a') as modules:
modules.write("\nvirtio_pci\nvirtio_blk\n")
log_check_call(['/usr/sbin/chroot', info.root, '/usr/sbin/update-initramfs', '-u'])
# Install grub in mbr
log_check_call(['/usr/sbin/grub-install', '--boot-directory='+info.root+"/boot/", info.bootstrap_device['path']])
log_check_call(['/usr/sbin/chroot', info.root, '/usr/sbin/update-grub'])
if info.manifest.virtualization == 'virtio':
from common.tools import sed_i
grub_cfg = os.path.join(info.root, 'boot/grub/grub.cfg')
sed_i(grub_cfg, 'sda', 'vda')
device_map = os.path.join(info.root,
'boot/grub/device.map')
sed_i(device_map, 'sda', 'vda')
fstab_file = os.path.join(info.root,
'etc/fstab')
sed_i(fstab_file, 'sda', 'vda')

View file

@ -1,48 +0,0 @@
from base import Task
from common import phases
from common.tasks import packages
from common.tasks.host import CheckPackages
class HostPackages(Task):
description = 'Determining required host packages'
phase = phases.preparation
before = [CheckPackages]
after = [packages.HostPackages]
def run(self, info):
info.host_packages.update(['qemu-utils', 'parted', 'grub2', 'sysv-rc', 'kpartx'])
if info.manifest.volume['filesystem'] == 'xfs':
info.host_packages.add('xfsprogs')
class ImagePackages(Task):
description = 'Determining required image packages'
phase = phases.preparation
after = [packages.ImagePackages]
def run(self, info):
manifest = info.manifest
include, exclude = info.img_packages
# Add some basic packages we are going to need
include.update(['parted',
'kpartx',
# Needed for the init scripts
'file',
# isc-dhcp-client doesn't work properly with ec2
'dhcpcd',
'chkconfig',
'openssh-client',
'grub2'
])
exclude.update(['isc-dhcp-client',
'isc-dhcp-common',
])
# In squeeze, we need a special kernel flavor for xen
kernels = {'squeeze': {'amd64': 'linux-image-amd64',
'i386': 'linux-image-686', },
'wheezy': {'amd64': 'linux-image-amd64',
'i386': 'linux-image-686', }, }
include.add(kernels.get(manifest.system['release']).get(manifest.system['architecture']))

View file

@ -1,20 +1,17 @@
from manifest import Manifest from manifest import Manifest
from tasks import packages from tasks import packages
from common.tasks import packages as common_packages from common.tasks import volume as volume_tasks
from common.tasks import host
from common.tasks import loopback from common.tasks import loopback
from common.tasks import parted from common.tasks import partitioning
from common.tasks import filesystem from common.tasks import filesystem
from common.tasks import bootstrap from common.tasks import bootstrap
from common.tasks import locale
from common.tasks import apt
from tasks import boot from tasks import boot
from common.tasks import boot as common_boot from common.tasks import boot as common_boot
from common.tasks import security from common.tasks import security
from common.tasks import network from common.tasks import network
from common.tasks import initd from common.tasks import initd
from common.tasks import cleanup from common.tasks import cleanup
from common.tasks import loopback from common.tasks import workspace
def initialize(): def initialize():
@ -22,60 +19,48 @@ def initialize():
def tasks(tasklist, manifest): def tasks(tasklist, manifest):
tasklist.add(packages.HostPackages(), from common.task_sets import base_set
common_packages.HostPackages(), from common.task_sets import volume_set
packages.ImagePackages(), from common.task_sets import mounting_set
common_packages.ImagePackages(), from common.task_sets import apt_set
host.CheckPackages(), from common.task_sets import locale_set
tasklist.add(*base_set)
tasklist.add(*volume_set)
tasklist.add(*mounting_set)
tasklist.add(*apt_set)
tasklist.add(*locale_set)
loopback.CreateQemuImg(), if manifest.volume['partitions']['type'] != 'none':
loopback.Attach(), from common.task_sets import partitioning_set
parted.PartitionVolume(), tasklist.add(*partitioning_set)
parted.MapPartitions(),
parted.FormatPartitions(),
filesystem.CreateMountDir(),
filesystem.MountVolume(),
bootstrap.Bootstrap(), tasklist.add(packages.ImagePackages,
filesystem.MountSpecials(),
locale.GenerateLocale(),
locale.SetTimezone(),
apt.DisableDaemonAutostart(),
apt.AptSources(),
apt.AptUpgrade(),
boot.ConfigureGrub(),
filesystem.ModifyFstab(),
common_boot.BlackListModules(),
common_boot.DisableGetTTYs(),
security.EnableShadowConfig(),
security.DisableSSHPasswordAuthentication(),
security.DisableSSHDNSLookup(),
network.RemoveDNSInfo(),
network.ConfigureNetworkIF(),
network.ConfigureDHCP(),
initd.ResolveInitScripts(),
initd.InstallInitScripts(),
cleanup.ClearMOTD(),
cleanup.ShredHostkeys(),
cleanup.CleanTMP(),
apt.PurgeUnusedPackages(),
apt.AptClean(),
apt.EnableDaemonAutostart(),
filesystem.UnmountSpecials(),
filesystem.UnmountVolume(), loopback.Create,
parted.UnmapPartitions(),
loopback.Detach(),
filesystem.DeleteMountDir())
if manifest.bootstrapper['tarball']: boot.ConfigureGrub,
tasklist.add(bootstrap.MakeTarball()) common_boot.BlackListModules,
common_boot.DisableGetTTYs,
security.EnableShadowConfig,
network.RemoveDNSInfo,
network.ConfigureNetworkIF,
network.RemoveHostname,
initd.ResolveInitScripts,
initd.InstallInitScripts,
cleanup.ClearMOTD,
cleanup.CleanTMP,
filesystem_specific_tasks = {'xfs': [filesystem.AddXFSProgs()], loopback.MoveImage)
'ext2': [filesystem.TuneVolumeFS()],
'ext3': [filesystem.TuneVolumeFS()], if manifest.bootstrapper.get('tarball', False):
'ext4': [filesystem.TuneVolumeFS()]} tasklist.add(bootstrap.MakeTarball)
tasklist.add(*filesystem_specific_tasks.get(manifest.volume['filesystem'].lower()))
from common.task_sets import get_fs_specific_set
tasklist.add(*get_fs_specific_set(manifest.volume['partitions']))
if 'boot' in manifest.volume['partitions']:
from common.task_sets import boot_partition_set
tasklist.add(*boot_partition_set)
def rollback_tasks(tasklist, tasks_completed, manifest): def rollback_tasks(tasklist, tasks_completed, manifest):
@ -83,10 +68,13 @@ def rollback_tasks(tasklist, tasks_completed, manifest):
def counter_task(task, counter): def counter_task(task, counter):
if task in completed and counter not in completed: if task in completed and counter not in completed:
tasklist.add(counter()) tasklist.add(counter)
counter_task(loopback.Create, volume_tasks.Delete)
counter_task(filesystem.CreateMountDir, filesystem.DeleteMountDir) counter_task(filesystem.CreateMountDir, filesystem.DeleteMountDir)
counter_task(parted.MapPartitions, parted.UnmapPartitions) counter_task(partitioning.MapPartitions, partitioning.UnmapPartitions)
counter_task(filesystem.MountVolume, filesystem.UnmountVolume) counter_task(filesystem.MountRoot, filesystem.UnmountRoot)
counter_task(filesystem.MountBoot, filesystem.UnmountBoot)
counter_task(filesystem.MountSpecials, filesystem.UnmountSpecials) counter_task(filesystem.MountSpecials, filesystem.UnmountSpecials)
counter_task(loopback.Attach, loopback.Detach) counter_task(volume_tasks.Attach, volume_tasks.Detach)
counter_task(workspace.CreateWorkspace, workspace.DeleteWorkspace)

View file

@ -1,4 +0,0 @@
#! /bin/sh
set -e
# nothing to do, skip grub mkconfig for this

View file

@ -1,93 +0,0 @@
#!/bin/sh
# This file generates the old menu.lst configuration with grub2
# It was copied from tomheadys github repo:
# https://github.com/tomheady/ec2debian/blob/master/src/root/etc/grub.d/40_custom
prefix=/usr
exec_prefix=${prefix}
bindir=${exec_prefix}/bin
libdir=${exec_prefix}/lib
. ${libdir}/grub/grub-mkconfig_lib
export TEXTDOMAIN=grub
export TEXTDOMAINDIR=${prefix}/share/locale
GRUB_DEVICE=/dev/sda1
cat << EOF
set default=${GRUB_DEFAULT}
set timeout=${GRUB_TIMEOUT}
insmod part_msdos
insmod ext2
insmod gettext
set menu_color_normal=cyan/blue
set menu_color_highlight=white/blue
set root='(hd0,msdos1)'
EOF
if ${GRUB_HIDDEN_TIMEOUT:-false}; then
printf "hiddenmenu\n"
fi
linux_entry ()
{
os="$1"
version="$2"
args="$4"
title="$(gettext_quoted "%s, with Linux %s")"
cat << EOF
menuentry 'Debian GNU/Linux, ${version}' --class debian --class gnu-linux --class os {
insmod part_msdos
insmod ext2
set timeout=${GRUB_TIMEOUT}
set root='(hd0,msdos1)'
echo 'Loading Linux ${version}'
linux ${rel_dirname}/${basename} root=${GRUB_DEVICE} ro ${args}
echo 'Loading initial ramdisk ...'
initrd ${rel_dirname}/${initrd}
}
EOF
}
list=`for i in /boot/vmlinuz-* /boot/vmlinux-* /vmlinuz-* /vmlinux-* ; do
if grub_file_is_not_garbage "$i" ; then echo -n "$i " ; fi
done`
prepare_boot_cache=
while [ "x$list" != "x" ] ; do
linux=`version_find_latest $list`
basename=`basename $linux`
dirname=`dirname $linux`
rel_dirname=`make_system_path_relative_to_its_root $dirname`
version=`echo $basename | sed -e "s,^[^0-9]*-,,g"`
alt_version=`echo $version | sed -e "s,\.old$,,g"`
linux_root_device_thisversion="${LINUX_ROOT_DEVICE}"
initrd=
for i in "initrd.img-${version}" "initrd-${version}.img" \
"initrd-${version}" "initramfs-${version}.img" \
"initrd.img-${alt_version}" "initrd-${alt_version}.img" \
"initrd-${alt_version}" "initramfs-${alt_version}.img"; do
if test -e "${dirname}/${i}" ; then
initrd="$i"
break
fi
done
initramfs=
for i in "config-${version}" "config-${alt_version}"; do
if test -e "${dirname}/${i}" ; then
initramfs=`grep CONFIG_INITRAMFS_SOURCE= "${dirname}/${i}" | cut -f2 -d= | tr -d \"`
break
fi
done
linux_entry "${OS}" "${version}" \
"${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}"
list=`echo $list | tr ' ' '\n' | grep -vx $linux | tr '\n' ' '`
done

View file

@ -8,15 +8,16 @@
"properties": { "properties": {
"backing": { "backing": {
"type": "string", "type": "string",
"enum": ["raw", "qcow2"] "enum": ["raw", "vdi", "qcow2"]
}, },
"filesystem": { "partitions": {
"type": "string", "type": "object",
"enum": ["ext2", "ext3", "ext4", "xfs"] "properties": {
"type": { "enum": ["none", "mbr"] }
}
} }
}, },
"required": ["backing", "filesystem"] "required": ["backing"]
} }
}, }
"required": ["volume"]
} }

View file

@ -12,5 +12,3 @@ class Manifest(base.Manifest):
super(Manifest, self).parse(data) super(Manifest, self).parse(data)
self.virtualization = None self.virtualization = None
self.image = data['image'] self.image = data['image']
if 'loopback_dir' not in self.volume:
self.volume['loopback_dir'] = '/tmp'

View file

@ -1,34 +1,65 @@
from base import Task from base import Task
from common import phases from common import phases
from common.tasks import apt
from common.fs.loopbackvolume import LoopbackVolume
class ConfigureGrub(Task): class ConfigureGrub(Task):
description = 'Configuring grub' description = 'Configuring grub'
phase = phases.system_modification phase = phases.system_modification
after = [apt.AptUpgrade]
def run(self, info): def run(self, info):
import stat import os
rwxr_xr_x = (stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH)
import os.path
device_map_path = os.path.join(info.root, 'boot/grub/device.map')
with open(device_map_path, 'w') as device_map:
device_map.write('(hd0) /dev/sda\n')
from common.tools import log_check_call from common.tools import log_check_call
from shutil import copy boot_dir = os.path.join(info.root, 'boot')
script_src = os.path.normpath(os.path.join(os.path.dirname(__file__), '../assets/grub.d/10_linux')) grub_dir = os.path.join(boot_dir, 'grub')
script_dst = os.path.join(info.root, 'etc/grub.d/10_linux')
copy(script_src, script_dst) from base.fs.partitionmaps.none import NoPartitions
os.chmod(script_dst, rwxr_xr_x) from base.fs.partitionmaps.gpt import GPTPartitionMap
script_src = os.path.normpath(os.path.join(os.path.dirname(__file__), '../assets/grub.d/00_header')) from common.fs import remount
script_dst = os.path.join(info.root, 'etc/grub.d/00_header') p_map = info.volume.partition_map
copy(script_src, script_dst)
os.chmod(script_dst, rwxr_xr_x) def mk_remount_fn(fn):
log_check_call(['/usr/sbin/chroot', info.root, '/usr/sbin/update-initramfs', '-u']) def set_device_path():
# Install grub in mbr fn()
log_check_call(['/usr/sbin/grub-install', '--boot-directory=' + info.root + "/boot/", if isinstance(p_map, NoPartitions):
info.bootstrap_device['path']]) p_map.root.device_path = info.volume.device_path
log_check_call(['/usr/sbin/chroot', info.root, '/usr/sbin/update-grub']) return set_device_path
link_fn = mk_remount_fn(info.volume.link_dm_node)
unlink_fn = mk_remount_fn(info.volume.unlink_dm_node)
# GRUB cannot deal with installing to loopback devices
# so we fake a real harddisk with dmsetup.
# Guide here: http://ebroder.net/2009/08/04/installing-grub-onto-a-disk-image/
if isinstance(info.volume, LoopbackVolume):
remount(info.volume, link_fn)
try:
[device_path] = log_check_call(['readlink', '-f', info.volume.device_path])
device_map_path = os.path.join(grub_dir, 'device.map')
partition_prefix = 'msdos'
if isinstance(p_map, GPTPartitionMap):
partition_prefix = 'gpt'
with open(device_map_path, 'w') as device_map:
device_map.write('(hd0) {device_path}\n'.format(device_path=device_path))
if not isinstance(p_map, NoPartitions):
for idx, partition in enumerate(info.volume.partition_map.partitions):
[partition_path] = log_check_call(['readlink', '-f', partition.device_path])
device_map.write('(hd0,{prefix}{idx}) {device_path}\n'
.format(device_path=partition_path, prefix=partition_prefix, idx=idx+1))
# Install grub
log_check_call(['/usr/sbin/chroot', info.root,
'/usr/sbin/grub-install',
# '--root-directory=' + info.root,
# '--boot-directory=' + boot_dir,
device_path])
log_check_call(['/usr/sbin/chroot', info.root, '/usr/sbin/update-grub'])
except Exception as e:
if isinstance(info.volume, LoopbackVolume):
remount(info.volume, unlink_fn)
raise e
if isinstance(info.volume, LoopbackVolume):
remount(info.volume, unlink_fn)

View file

@ -1,19 +1,6 @@
from base import Task from base import Task
from common import phases from common import phases
from common.tasks import packages from common.tasks import packages
from common.tasks.host import CheckPackages
class HostPackages(Task):
description = 'Determining required host packages'
phase = phases.preparation
before = [CheckPackages]
after = [packages.HostPackages]
def run(self, info):
info.host_packages.update(['qemu-utils', 'parted', 'grub2', 'sysv-rc', 'kpartx'])
if info.manifest.volume['filesystem'] == 'xfs':
info.host_packages.add('xfsprogs')
class ImagePackages(Task): class ImagePackages(Task):
@ -25,20 +12,7 @@ class ImagePackages(Task):
manifest = info.manifest manifest = info.manifest
include, exclude = info.img_packages include, exclude = info.img_packages
# Add some basic packages we are going to need # Add some basic packages we are going to need
include.update(['parted', include.add('grub2')
'kpartx',
# Needed for the init scripts
'file',
# isc-dhcp-client doesn't work properly with ec2
'dhcpcd',
'chkconfig',
'openssh-client',
'grub2'
])
exclude.update(['isc-dhcp-client',
'isc-dhcp-common',
])
# In squeeze, we need a special kernel flavor for xen # In squeeze, we need a special kernel flavor for xen
kernels = {'squeeze': {'amd64': 'linux-image-amd64', kernels = {'squeeze': {'amd64': 'linux-image-amd64',

View file

@ -0,0 +1,77 @@
from common.fs.loopbackvolume import LoopbackVolume
from base.fs.exceptions import VolumeError
from common.tools import log_check_call
from common.fs import get_partitions
class VirtualBoxVolume(LoopbackVolume):
extension = 'vdi'
def _before_create(self, e):
self.image_path = e.image_path
log_check_call(['/usr/bin/qemu-img', 'create', '-f', 'vdi', self.image_path, str(self.size) + 'M'])
def _check_nbd_module(self):
from base.fs.partitionmaps.none import NoPartitions
if isinstance(self.partition_map, NoPartitions):
if not self._module_loaded('nbd'):
raise VolumeError('The kernel module `nbd\' must be loaded '
'(`modprobe nbd\') to attach .vdi images')
else:
num_partitions = len(self.partition_map.partitions)
if not self._module_loaded('nbd'):
msg = ('The kernel module `nbd\' must be loaded '
'(`modprobe nbd max_part={num_partitions}\') to attach .vdi images'
.format(num_partitions=num_partitions))
raise VolumeError(msg)
nbd_max_part = int(self._module_param('nbd', 'max_part'))
if nbd_max_part < num_partitions:
# Found here: http://bethesignal.org/blog/2011/01/05/how-to-mount-virtualbox-vdi-image/
msg = ('The kernel module `nbd\' was loaded with the max_part '
'parameter set to {max_part}, which is below '
'the amount of partitions for this volume ({num_partitions}). '
'Reload the nbd kernel module with max_part set to at least {num_partitions} '
'(`rmmod nbd; modprobe nbd max_part={num_partitions}\').'
.format(max_part=nbd_max_part, num_partitions=num_partitions))
raise VolumeError(msg)
def _before_attach(self, e):
self._check_nbd_module()
self.loop_device_path = self._find_free_nbd_device()
log_check_call(['/usr/bin/qemu-nbd', '--connect', self.loop_device_path, self.image_path])
self.device_path = self.loop_device_path
def _before_detach(self, e):
log_check_call(['/usr/bin/qemu-nbd', '--disconnect', self.loop_device_path])
del self.loop_device_path
del self.device_path
def _module_loaded(self, module):
import re
regexp = re.compile('^{module} +'.format(module=module))
with open('/proc/modules') as loaded_modules:
for line in loaded_modules:
match = regexp.match(line)
if match is not None:
return True
return False
def _module_param(self, module, param):
import os.path
param_path = os.path.join('/sys/module', module, 'parameters', param)
with open(param_path) as param:
return param.read().strip()
# From http://lists.gnu.org/archive/html/qemu-devel/2011-11/msg02201.html
# Apparently it's not in the current qemu-nbd shipped with wheezy
def _is_nbd_used(self, device_name):
return device_name in get_partitions()
def _find_free_nbd_device(self):
import os.path
for i in xrange(0, 15):
device_name = 'nbd' + str(i)
if not self._is_nbd_used(device_name):
return os.path.join('/dev', device_name)
raise VolumeError('Unable to find free nbd device.')