Merge "Add state object, rename "results", add unit tests"
This commit is contained in:
commit
09543cf52b
@ -16,18 +16,87 @@ import codecs
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pprint
|
||||
import shutil
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
from diskimage_builder.block_device.config import config_tree_to_graph
|
||||
from diskimage_builder.block_device.config import create_graph
|
||||
from diskimage_builder.block_device.exception import \
|
||||
BlockDeviceSetupException
|
||||
from diskimage_builder.block_device.utils import exec_sudo
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_json(file_name):
|
||||
"""Load file from .json file on disk, return None if not existing"""
|
||||
if os.path.exists(file_name):
|
||||
with codecs.open(file_name, encoding="utf-8", mode="r") as fd:
|
||||
return json.load(fd)
|
||||
return None
|
||||
|
||||
|
||||
class BlockDeviceState(object):
|
||||
"""The global state singleton
|
||||
|
||||
An reference to an instance of this object is passed between nodes
|
||||
as a global repository. It contains a single dictionary "state"
|
||||
and a range of helper functions.
|
||||
|
||||
This is used in two contexts:
|
||||
|
||||
- The state is built by the :func:`NodeBase.create` commands as
|
||||
called during :func:`BlockDevice.cmd_create`. It is then
|
||||
persisted to disk by :func:`save_state`
|
||||
|
||||
- Later calls (cleanup, umount, etc) load the state dictionary
|
||||
from disk and are thus passed the full state.
|
||||
"""
|
||||
# XXX:
|
||||
# - might it make sense to make state implement MutableMapping,
|
||||
# so that callers treat it as a dictionary?
|
||||
# - we could implement getters/setters such that if loaded from
|
||||
# disk, the state is read-only? or make it append-only
|
||||
# (i.e. you can't overwrite existing keys)
|
||||
def __init__(self, filename=None):
|
||||
"""Initialise state
|
||||
|
||||
:param filename: if :param:`filename` is passed and exists, it
|
||||
will be loaded as the state. If it does not exist an
|
||||
exception is raised. If :param:`filename` is not
|
||||
passed, state will be initalised to a blank dictionary.
|
||||
"""
|
||||
if filename:
|
||||
if not os.path.exists(filename):
|
||||
raise BlockDeviceSetupException("State dump not found")
|
||||
else:
|
||||
self.state = _load_json(filename)
|
||||
assert self.state is not None
|
||||
else:
|
||||
self.state = {}
|
||||
|
||||
def save_state(self, filename):
|
||||
"""Persist the state to disk
|
||||
|
||||
:param filename: The file to persist state to
|
||||
"""
|
||||
logger.debug("Writing state to: %s", filename)
|
||||
self.debug_dump()
|
||||
with open(filename, "w") as fd:
|
||||
json.dump(self.state, fd)
|
||||
|
||||
def debug_dump(self):
|
||||
"""Log state to debug"""
|
||||
# This is pretty good for human consumption, but maybe a bit
|
||||
# verbose.
|
||||
nice_output = pprint.pformat(self.state, width=40)
|
||||
for line in nice_output.split('\n'):
|
||||
logger.debug(" " + line)
|
||||
|
||||
|
||||
class BlockDevice(object):
|
||||
"""Handles block devices.
|
||||
|
||||
@ -116,13 +185,6 @@ class BlockDevice(object):
|
||||
else:
|
||||
v['label'] = "cloudimg-rootfs"
|
||||
|
||||
@staticmethod
|
||||
def _load_json(file_name):
|
||||
if os.path.exists(file_name):
|
||||
with codecs.open(file_name, encoding="utf-8", mode="r") as fd:
|
||||
return json.load(fd)
|
||||
return None
|
||||
|
||||
def __init__(self, params):
|
||||
"""Create BlockDevice object
|
||||
|
||||
@ -142,9 +204,7 @@ class BlockDevice(object):
|
||||
self.config_json_file_name \
|
||||
= os.path.join(self.state_dir, "config.json")
|
||||
|
||||
self.config = self._load_json(self.config_json_file_name)
|
||||
self.state = self._load_json(self.state_json_file_name)
|
||||
logger.debug("Using state [%s]", self.state)
|
||||
self.config = _load_json(self.config_json_file_name)
|
||||
|
||||
# This needs to exists for the state and config files
|
||||
try:
|
||||
@ -152,16 +212,6 @@ class BlockDevice(object):
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def write_state(self, state):
|
||||
logger.debug("Write state [%s]", self.state_json_file_name)
|
||||
with open(self.state_json_file_name, "w") as fd:
|
||||
json.dump(state, fd)
|
||||
|
||||
def create(self, result, rollback):
|
||||
dg, call_order = create_graph(self.config, self.params)
|
||||
for node in call_order:
|
||||
node.create(result, rollback)
|
||||
|
||||
def cmd_init(self):
|
||||
"""Initialize block device setup
|
||||
|
||||
@ -234,16 +284,23 @@ class BlockDevice(object):
|
||||
# mountpoints list
|
||||
print("%s" % "|".join(mount_points))
|
||||
return 0
|
||||
|
||||
# the following symbols all come from the global state
|
||||
# dictionary. They can only be accessed after the state has
|
||||
# been dumped; i.e. after cmd_create() called.
|
||||
state = BlockDeviceState(self.state_json_file_name)
|
||||
state = state.state
|
||||
|
||||
if symbol == 'image-block-partition':
|
||||
# If there is no partition needed, pass back directly the
|
||||
# image.
|
||||
if 'root' in self.state['blockdev']:
|
||||
print("%s" % self.state['blockdev']['root']['device'])
|
||||
if 'root' in state['blockdev']:
|
||||
print("%s" % state['blockdev']['root']['device'])
|
||||
else:
|
||||
print("%s" % self.state['blockdev']['image0']['device'])
|
||||
print("%s" % state['blockdev']['image0']['device'])
|
||||
return 0
|
||||
if symbol == 'image-path':
|
||||
print("%s" % self.state['blockdev']['image0']['image'])
|
||||
print("%s" % state['blockdev']['image0']['image'])
|
||||
return 0
|
||||
|
||||
logger.error("Invalid symbol [%s] for getval", symbol)
|
||||
@ -253,28 +310,33 @@ class BlockDevice(object):
|
||||
"""Creates the fstab"""
|
||||
logger.info("Creating fstab")
|
||||
|
||||
# State should have been created by prior calls; we only need
|
||||
# the dict
|
||||
state = BlockDeviceState(self.state_json_file_name)
|
||||
state = state.state
|
||||
|
||||
tmp_fstab = os.path.join(self.state_dir, "fstab")
|
||||
with open(tmp_fstab, "wt") as fstab_fd:
|
||||
# This gives the order in which this must be mounted
|
||||
for mp in self.state['mount_order']:
|
||||
for mp in state['mount_order']:
|
||||
logger.debug("Writing fstab entry for [%s]", mp)
|
||||
fs_base = self.state['mount'][mp]['base']
|
||||
fs_name = self.state['mount'][mp]['name']
|
||||
fs_val = self.state['filesys'][fs_base]
|
||||
fs_base = state['mount'][mp]['base']
|
||||
fs_name = state['mount'][mp]['name']
|
||||
fs_val = state['filesys'][fs_base]
|
||||
if 'label' in fs_val:
|
||||
diskid = "LABEL=%s" % fs_val['label']
|
||||
else:
|
||||
diskid = "UUID=%s" % fs_val['uuid']
|
||||
|
||||
# If there is no fstab entry - do not write anything
|
||||
if 'fstab' not in self.state:
|
||||
if 'fstab' not in state:
|
||||
continue
|
||||
if fs_name not in self.state['fstab']:
|
||||
if fs_name not in state['fstab']:
|
||||
continue
|
||||
|
||||
options = self.state['fstab'][fs_name]['options']
|
||||
dump_freq = self.state['fstab'][fs_name]['dump-freq']
|
||||
fsck_passno = self.state['fstab'][fs_name]['fsck-passno']
|
||||
options = state['fstab'][fs_name]['options']
|
||||
dump_freq = state['fstab'][fs_name]['dump-freq']
|
||||
fsck_passno = state['fstab'][fs_name]['fsck-passno']
|
||||
|
||||
fstab_fd.write("%s %s %s %s %s %s\n"
|
||||
% (diskid, mp, fs_val['fstype'],
|
||||
@ -292,25 +354,34 @@ class BlockDevice(object):
|
||||
logger.info("create() called")
|
||||
logger.debug("Using config [%s]", self.config)
|
||||
|
||||
self.state = {}
|
||||
rollback = []
|
||||
|
||||
# Create a new, empty state
|
||||
state = BlockDeviceState()
|
||||
try:
|
||||
self.create(self.state, rollback)
|
||||
dg, call_order = create_graph(self.config, self.params)
|
||||
for node in call_order:
|
||||
node.create(state.state, rollback)
|
||||
except Exception:
|
||||
logger.exception("Create failed; rollback initiated")
|
||||
for rollback_cb in reversed(rollback):
|
||||
rollback_cb()
|
||||
sys.exit(1)
|
||||
|
||||
self.write_state(self.state)
|
||||
state.save_state(self.state_json_file_name)
|
||||
|
||||
logger.info("create() finished")
|
||||
return 0
|
||||
|
||||
def cmd_umount(self):
|
||||
"""Unmounts the blockdevice and cleanup resources"""
|
||||
if self.state is None:
|
||||
|
||||
# State should have been created by prior calls; we only need
|
||||
# the dict. If it is not here, it has been cleaned up already
|
||||
# (? more details?)
|
||||
try:
|
||||
state = BlockDeviceState(self.state_json_file_name)
|
||||
state = state.state
|
||||
except BlockDeviceSetupException:
|
||||
logger.info("State already cleaned - no way to do anything here")
|
||||
return 0
|
||||
|
||||
@ -321,36 +392,44 @@ class BlockDevice(object):
|
||||
if dg is None:
|
||||
return 0
|
||||
for node in reverse_order:
|
||||
node.umount(self.state)
|
||||
node.umount(state)
|
||||
|
||||
return 0
|
||||
|
||||
def cmd_cleanup(self):
|
||||
"""Cleanup all remaining relicts - in good case"""
|
||||
# State should have been created by prior calls; we only need
|
||||
# the dict
|
||||
state = BlockDeviceState(self.state_json_file_name)
|
||||
state = state.state
|
||||
|
||||
# Deleting must be done in reverse order
|
||||
dg, call_order = create_graph(self.config, self.params)
|
||||
reverse_order = reversed(call_order)
|
||||
|
||||
for node in reverse_order:
|
||||
node.cleanup(self.state)
|
||||
node.cleanup(state)
|
||||
|
||||
logger.info("Removing temporary dir [%s]", self.state_dir)
|
||||
logger.info("Removing temporary state dir [%s]", self.state_dir)
|
||||
shutil.rmtree(self.state_dir)
|
||||
|
||||
return 0
|
||||
|
||||
def cmd_delete(self):
|
||||
"""Cleanup all remaining relicts - in case of an error"""
|
||||
# State should have been created by prior calls; we only need
|
||||
# the dict
|
||||
state = BlockDeviceState(self.state_json_file_name)
|
||||
state = state.state
|
||||
|
||||
# Deleting must be done in reverse order
|
||||
dg, call_order = create_graph(self.config, self.params)
|
||||
reverse_order = reversed(call_order)
|
||||
|
||||
for node in reverse_order:
|
||||
node.delete(self.state)
|
||||
node.delete(state)
|
||||
|
||||
logger.info("Removing temporary dir [%s]", self.state_dir)
|
||||
logger.info("Removing temporary state dir [%s]", self.state_dir)
|
||||
shutil.rmtree(self.state_dir)
|
||||
|
||||
return 0
|
||||
|
@ -100,7 +100,7 @@ class LocalLoopNode(NodeBase):
|
||||
logger.debug("Gave up trying to detach [%s]", loopdev)
|
||||
return rval
|
||||
|
||||
def create(self, result, rollback):
|
||||
def create(self, state, rollback):
|
||||
logger.debug("[%s] Creating loop on [%s] with size [%d]",
|
||||
self.name, self.filename, self.size)
|
||||
|
||||
@ -110,11 +110,11 @@ class LocalLoopNode(NodeBase):
|
||||
block_device = self._loopdev_attach(self.filename)
|
||||
rollback.append(lambda: self._loopdev_detach(block_device))
|
||||
|
||||
if 'blockdev' not in result:
|
||||
result['blockdev'] = {}
|
||||
if 'blockdev' not in state:
|
||||
state['blockdev'] = {}
|
||||
|
||||
result['blockdev'][self.name] = {"device": block_device,
|
||||
"image": self.filename}
|
||||
state['blockdev'][self.name] = {"device": block_device,
|
||||
"image": self.filename}
|
||||
logger.debug("Created loop name [%s] device [%s] image [%s]",
|
||||
self.name, block_device, self.filename)
|
||||
return
|
||||
|
@ -65,5 +65,5 @@ class PartitionNode(NodeBase):
|
||||
edge_from.append(self.prev_partition.name)
|
||||
return (edge_from, edge_to)
|
||||
|
||||
def create(self, result, rollback):
|
||||
self.partitioning.create(result, rollback)
|
||||
def create(self, state, rollback):
|
||||
self.partitioning.create(state, rollback)
|
||||
|
@ -127,14 +127,13 @@ class Partitioning(PluginBase):
|
||||
|
||||
exec_sudo(["kpartx", "-avs", device_path])
|
||||
|
||||
def create(self, result, rollback):
|
||||
def create(self, state, rollback):
|
||||
# not this is NOT a node and this is not called directly! The
|
||||
# create() calls in the partition nodes this plugin has
|
||||
# created are calling back into this.
|
||||
image_path = result['blockdev'][self.base]['image']
|
||||
device_path = result['blockdev'][self.base]['device']
|
||||
logger.info("Creating partition on [%s] [%s]",
|
||||
self.base, image_path)
|
||||
image_path = state['blockdev'][self.base]['image']
|
||||
device_path = state['blockdev'][self.base]['device']
|
||||
logger.info("Creating partition on [%s] [%s]", self.base, image_path)
|
||||
|
||||
# This is a bit of a hack. Each of the partitions is actually
|
||||
# in the graph, so for every partition we get a create() call
|
||||
@ -167,7 +166,7 @@ class Partitioning(PluginBase):
|
||||
logger.debug("Create partition [%s] [%d]",
|
||||
part_name, part_no)
|
||||
partition_device_name = device_path + "p%d" % part_no
|
||||
result['blockdev'][part_name] \
|
||||
state['blockdev'][part_name] \
|
||||
= {'device': partition_device_name}
|
||||
partition_devices.add(partition_device_name)
|
||||
|
||||
|
@ -102,9 +102,7 @@ class FilesystemNode(NodeBase):
|
||||
edge_to = []
|
||||
return (edge_from, edge_to)
|
||||
|
||||
def create(self, result, rollback):
|
||||
logger.info("create called; result [%s]", result)
|
||||
|
||||
def create(self, state, rollback):
|
||||
cmd = ["mkfs"]
|
||||
|
||||
cmd.extend(['-t', self.type])
|
||||
@ -123,17 +121,17 @@ class FilesystemNode(NodeBase):
|
||||
if self.type in ('ext2', 'ext3', 'ext4', 'xfs'):
|
||||
cmd.append('-q')
|
||||
|
||||
if 'blockdev' not in result:
|
||||
result['blockdev'] = {}
|
||||
device = result['blockdev'][self.base]['device']
|
||||
if 'blockdev' not in state:
|
||||
state['blockdev'] = {}
|
||||
device = state['blockdev'][self.base]['device']
|
||||
cmd.append(device)
|
||||
|
||||
logger.debug("Creating fs command [%s]", cmd)
|
||||
exec_sudo(cmd)
|
||||
|
||||
if 'filesys' not in result:
|
||||
result['filesys'] = {}
|
||||
result['filesys'][self.name] \
|
||||
if 'filesys' not in state:
|
||||
state['filesys'] = {}
|
||||
state['filesys'][self.name] \
|
||||
= {'uuid': self.uuid, 'label': self.label,
|
||||
'fstype': self.type, 'opts': self.opts,
|
||||
'device': device}
|
||||
|
@ -91,9 +91,8 @@ class MountPointNode(NodeBase):
|
||||
edge_from.append(self.base)
|
||||
return (edge_from, edge_to)
|
||||
|
||||
def create(self, result, rollback):
|
||||
def create(self, state, rollback):
|
||||
logger.debug("mount called [%s]", self.mount_point)
|
||||
logger.debug("result [%s]", result)
|
||||
rel_mp = self.mount_point if self.mount_point[0] != '/' \
|
||||
else self.mount_point[1:]
|
||||
mount_point = os.path.join(self.mount_base, rel_mp)
|
||||
@ -102,17 +101,17 @@ class MountPointNode(NodeBase):
|
||||
# file system tree.
|
||||
exec_sudo(['mkdir', '-p', mount_point])
|
||||
logger.info("Mounting [%s] to [%s]", self.name, mount_point)
|
||||
exec_sudo(["mount", result['filesys'][self.base]['device'],
|
||||
exec_sudo(["mount", state['filesys'][self.base]['device'],
|
||||
mount_point])
|
||||
|
||||
if 'mount' not in result:
|
||||
result['mount'] = {}
|
||||
result['mount'][self.mount_point] \
|
||||
if 'mount' not in state:
|
||||
state['mount'] = {}
|
||||
state['mount'][self.mount_point] \
|
||||
= {'name': self.name, 'base': self.base, 'path': mount_point}
|
||||
|
||||
if 'mount_order' not in result:
|
||||
result['mount_order'] = []
|
||||
result['mount_order'].append(self.mount_point)
|
||||
if 'mount_order' not in state:
|
||||
state['mount_order'] = []
|
||||
state['mount_order'].append(self.mount_point)
|
||||
|
||||
def umount(self, state):
|
||||
logger.info("Called for [%s]", self.name)
|
||||
|
@ -34,14 +34,13 @@ class FstabNode(NodeBase):
|
||||
edge_to = []
|
||||
return (edge_from, edge_to)
|
||||
|
||||
def create(self, result, rollback):
|
||||
def create(self, state, rollback):
|
||||
logger.debug("fstab create called [%s]", self.name)
|
||||
logger.debug("result [%s]", result)
|
||||
|
||||
if 'fstab' not in result:
|
||||
result['fstab'] = {}
|
||||
if 'fstab' not in state:
|
||||
state['fstab'] = {}
|
||||
|
||||
result['fstab'][self.base] = {
|
||||
state['fstab'][self.base] = {
|
||||
'name': self.name,
|
||||
'base': self.base,
|
||||
'options': self.options,
|
||||
|
@ -74,7 +74,7 @@ class NodeBase(object):
|
||||
return
|
||||
|
||||
@abc.abstractmethod
|
||||
def create(self, results, rollback):
|
||||
def create(self, state, rollback):
|
||||
"""Main creation driver
|
||||
|
||||
This is the main driver function. After the graph is
|
||||
@ -82,10 +82,11 @@ class NodeBase(object):
|
||||
|
||||
Arguments:
|
||||
|
||||
:param results: A shared dictionary of prior results. This
|
||||
:param state: A shared dictionary of prior results. This
|
||||
dictionary is passed by reference to each call, meaning any
|
||||
entries inserted will be available to subsequent :func:`create`
|
||||
calls of following nodes.
|
||||
calls of following nodes. The ``state`` dictionary will be
|
||||
saved and available to other calls.
|
||||
|
||||
:param rollback: A shared list of functions to be called in
|
||||
the failure case. Nodes should only append to this list.
|
||||
@ -105,7 +106,7 @@ class NodeBase(object):
|
||||
Actions to taken when ``dib-block-device umount`` is called
|
||||
|
||||
:param state: the current state dictionary. This is the
|
||||
`results` dictionary from :func:`create` before this call is
|
||||
`state` dictionary from :func:`create` before this call is
|
||||
made.
|
||||
:return: None
|
||||
"""
|
||||
@ -119,7 +120,7 @@ class NodeBase(object):
|
||||
called in the reverse order to :func:`create`
|
||||
|
||||
:param state: the current state dictionary. This is the
|
||||
`results` dictionary from :func:`create` before this call is
|
||||
`state` dictionary from :func:`create` before this call is
|
||||
made.
|
||||
:return: None
|
||||
"""
|
||||
@ -134,7 +135,7 @@ class NodeBase(object):
|
||||
:func:`create`
|
||||
|
||||
:param state: the current state dictionary. This is the
|
||||
`results` dictionary from :func:`create` before this call is
|
||||
`state` dictionary from :func:`create` before this call is
|
||||
made.
|
||||
:return: None
|
||||
"""
|
||||
|
@ -0,0 +1,6 @@
|
||||
- test_a:
|
||||
name: test_node_a
|
||||
|
||||
- test_b:
|
||||
name: test_node_b
|
||||
base: test_node_a
|
49
diskimage_builder/block_device/tests/plugin/test_a.py
Normal file
49
diskimage_builder/block_device/tests/plugin/test_a.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# plugin test case
|
||||
|
||||
import logging
|
||||
|
||||
from diskimage_builder.block_device.plugin import NodeBase
|
||||
from diskimage_builder.block_device.plugin import PluginBase
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestANode(NodeBase):
|
||||
def __init__(self, name):
|
||||
logger.debug("Create test 1")
|
||||
super(TestANode, self).__init__(name)
|
||||
|
||||
def get_edges(self):
|
||||
# this is like the loop node; it's a root and doesn't have a
|
||||
# base
|
||||
return ([], [])
|
||||
|
||||
def create(self, state, rollback):
|
||||
# put some fake entries into state
|
||||
state['test_a'] = {}
|
||||
state['test_a']['value'] = 'foo'
|
||||
state['test_a']['value2'] = 'bar'
|
||||
return
|
||||
|
||||
|
||||
class TestA(PluginBase):
|
||||
|
||||
def __init__(self, config, defaults):
|
||||
super(PluginBase, self).__init__()
|
||||
self.node = TestANode(config['name'])
|
||||
|
||||
def get_nodes(self):
|
||||
return [self.node]
|
47
diskimage_builder/block_device/tests/plugin/test_b.py
Normal file
47
diskimage_builder/block_device/tests/plugin/test_b.py
Normal file
@ -0,0 +1,47 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# plugin test case
|
||||
|
||||
import logging
|
||||
|
||||
from diskimage_builder.block_device.plugin import NodeBase
|
||||
from diskimage_builder.block_device.plugin import PluginBase
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestBNode(NodeBase):
|
||||
def __init__(self, name, base):
|
||||
logger.debug("Create test 1")
|
||||
super(TestBNode, self).__init__(name)
|
||||
self.base = base
|
||||
|
||||
def get_edges(self):
|
||||
return ([self.base], [])
|
||||
|
||||
def create(self, state, rollback):
|
||||
state['test_b'] = {}
|
||||
state['test_b']['value'] = 'baz'
|
||||
return
|
||||
|
||||
|
||||
class TestB(PluginBase):
|
||||
|
||||
def __init__(self, config, defaults):
|
||||
super(PluginBase, self).__init__()
|
||||
self.node = TestBNode(config['name'],
|
||||
config['base'])
|
||||
|
||||
def get_nodes(self):
|
||||
return [self.node]
|
41
diskimage_builder/block_device/tests/test_base.py
Normal file
41
diskimage_builder/block_device/tests/test_base.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import fixtures
|
||||
import logging
|
||||
import os
|
||||
import testtools
|
||||
import yaml
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestBase(testtools.TestCase):
|
||||
"""Base for all test cases"""
|
||||
def setUp(self):
|
||||
super(TestBase, self).setUp()
|
||||
|
||||
fs = '%(asctime)s %(levelname)s [%(name)s] %(message)s'
|
||||
self.log_fixture = self.useFixture(
|
||||
fixtures.FakeLogger(level=logging.DEBUG, format=fs))
|
||||
|
||||
def get_config_file(self, f):
|
||||
"""Get the full path to sample config file f """
|
||||
logger.debug(os.path.dirname(__file__))
|
||||
return os.path.join(os.path.dirname(__file__), 'config', f)
|
||||
|
||||
def load_config_file(self, f):
|
||||
"""Load f and return it after yaml parsing"""
|
||||
path = self.get_config_file(f)
|
||||
with open(path, 'r') as config:
|
||||
return yaml.safe_load(config)
|
@ -10,30 +10,23 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import fixtures
|
||||
import logging
|
||||
import os
|
||||
import testtools
|
||||
import yaml
|
||||
|
||||
from diskimage_builder.block_device.config import config_tree_to_graph
|
||||
from diskimage_builder.block_device.config import create_graph
|
||||
from diskimage_builder.block_device.exception import \
|
||||
BlockDeviceSetupException
|
||||
from diskimage_builder.block_device.tests.test_base import TestBase
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestConfig(testtools.TestCase):
|
||||
class TestConfig(TestBase):
|
||||
"""Helper for setting up and reading a config"""
|
||||
def setUp(self):
|
||||
super(TestConfig, self).setUp()
|
||||
|
||||
fs = '%(asctime)s %(levelname)s [%(name)s] %(message)s'
|
||||
self.log_fixture = self.useFixture(
|
||||
fixtures.FakeLogger(level=logging.DEBUG, format=fs))
|
||||
|
||||
# reset all globals for each test.
|
||||
# XXX: remove globals :/
|
||||
import diskimage_builder.block_device.level2.mkfs
|
||||
@ -42,12 +35,6 @@ class TestConfig(testtools.TestCase):
|
||||
diskimage_builder.block_device.level3.mount.mount_points = {}
|
||||
diskimage_builder.block_device.level3.mount.sorted_mount_points = None
|
||||
|
||||
def load_config_file(self, f):
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'config', f)
|
||||
with open(path, 'r') as config:
|
||||
return yaml.safe_load(config)
|
||||
|
||||
|
||||
class TestGraphGeneration(TestConfig):
|
||||
"""Extra helper class for testing graph generation"""
|
||||
|
@ -30,14 +30,14 @@ class TestMountOrder(tc.TestGraphGeneration):
|
||||
|
||||
graph, call_order = create_graph(config, self.fake_default_config)
|
||||
|
||||
result = {}
|
||||
result['filesys'] = {}
|
||||
result['filesys']['mkfs_root'] = {}
|
||||
result['filesys']['mkfs_root']['device'] = 'fake'
|
||||
result['filesys']['mkfs_var'] = {}
|
||||
result['filesys']['mkfs_var']['device'] = 'fake'
|
||||
result['filesys']['mkfs_var_log'] = {}
|
||||
result['filesys']['mkfs_var_log']['device'] = 'fake'
|
||||
state = {}
|
||||
state['filesys'] = {}
|
||||
state['filesys']['mkfs_root'] = {}
|
||||
state['filesys']['mkfs_root']['device'] = 'fake'
|
||||
state['filesys']['mkfs_var'] = {}
|
||||
state['filesys']['mkfs_var']['device'] = 'fake'
|
||||
state['filesys']['mkfs_var_log'] = {}
|
||||
state['filesys']['mkfs_var_log']['device'] = 'fake'
|
||||
|
||||
rollback = []
|
||||
|
||||
@ -46,7 +46,7 @@ class TestMountOrder(tc.TestGraphGeneration):
|
||||
# XXX: do we even need to create? We could test the
|
||||
# sudo arguments from the mock in the below asserts
|
||||
# too
|
||||
node.create(result, rollback)
|
||||
node.create(state, rollback)
|
||||
|
||||
# ensure that partitions are mounted in order root->var->var/log
|
||||
self.assertListEqual(result['mount_order'], ['/', '/var', '/var/log'])
|
||||
self.assertListEqual(state['mount_order'], ['/', '/var', '/var/log'])
|
||||
|
107
diskimage_builder/block_device/tests/test_state.py
Normal file
107
diskimage_builder/block_device/tests/test_state.py
Normal file
@ -0,0 +1,107 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import codecs
|
||||
import fixtures
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from stevedore import extension
|
||||
from testtools.matchers import FileExists
|
||||
|
||||
import diskimage_builder.block_device.blockdevice as bd
|
||||
import diskimage_builder.block_device.tests.test_base as tb
|
||||
|
||||
from diskimage_builder.block_device.exception import \
|
||||
BlockDeviceSetupException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestStateBase(tb.TestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestStateBase, self).setUp()
|
||||
|
||||
# override the extensions to the test extensions
|
||||
test_extensions = extension.ExtensionManager(
|
||||
namespace='diskimage_builder.block_device.plugin_test',
|
||||
invoke_on_load=False)
|
||||
extensions_fixture = fixtures.MonkeyPatch(
|
||||
'diskimage_builder.block_device.config._extensions',
|
||||
test_extensions)
|
||||
self.useFixture(extensions_fixture)
|
||||
|
||||
# status and other bits saved here
|
||||
self.build_dir = fixtures.TempDir()
|
||||
self.useFixture(self.build_dir)
|
||||
|
||||
|
||||
class TestState(TestStateBase):
|
||||
|
||||
# The the state generation & saving methods
|
||||
def test_state_create(self):
|
||||
params = {
|
||||
'build-dir': self.build_dir.path,
|
||||
'config': self.get_config_file('cmd_create.yaml')
|
||||
}
|
||||
|
||||
bd_obj = bd.BlockDevice(params)
|
||||
|
||||
bd_obj.cmd_init()
|
||||
bd_obj.cmd_create()
|
||||
|
||||
# cmd_create should have persisted this to disk
|
||||
state_file = os.path.join(self.build_dir.path,
|
||||
'states', 'block-device',
|
||||
'state.json')
|
||||
self.assertThat(state_file, FileExists())
|
||||
|
||||
# ensure we see the values put in by the test extensions
|
||||
# persisted
|
||||
with codecs.open(state_file, encoding='utf-8', mode='r') as fd:
|
||||
state = json.load(fd)
|
||||
self.assertDictEqual(state,
|
||||
{'test_a': {'value': 'foo',
|
||||
'value2': 'bar'},
|
||||
'test_b': {'value': 'baz'}})
|
||||
|
||||
# Test state going missing between phases
|
||||
def test_missing_state(self):
|
||||
params = {
|
||||
'build-dir': self.build_dir.path,
|
||||
'config': self.get_config_file('cmd_create.yaml')
|
||||
}
|
||||
|
||||
bd_obj = bd.BlockDevice(params)
|
||||
bd_obj.cmd_init()
|
||||
bd_obj.cmd_create()
|
||||
|
||||
# cmd_create should have persisted this to disk
|
||||
state_file = os.path.join(self.build_dir.path,
|
||||
'states', 'block-device',
|
||||
'state.json')
|
||||
self.assertThat(state_file, FileExists())
|
||||
|
||||
# simulate the state somehow going missing, and ensure that
|
||||
# later calls notice
|
||||
os.unlink(state_file)
|
||||
self.assertRaisesRegexp(BlockDeviceSetupException,
|
||||
"State dump not found",
|
||||
bd_obj.cmd_cleanup)
|
||||
self.assertRaisesRegexp(BlockDeviceSetupException,
|
||||
"State dump not found",
|
||||
bd_obj.cmd_writefstab)
|
||||
self.assertRaisesRegexp(BlockDeviceSetupException,
|
||||
"State dump not found",
|
||||
bd_obj.cmd_delete)
|
@ -71,3 +71,8 @@ diskimage_builder.block_device.plugin =
|
||||
mkfs = diskimage_builder.block_device.level2.mkfs:Mkfs
|
||||
mount = diskimage_builder.block_device.level3.mount:Mount
|
||||
fstab = diskimage_builder.block_device.level4.fstab:Fstab
|
||||
|
||||
# unit test extensions
|
||||
diskimage_builder.block_device.plugin_test =
|
||||
test_a = diskimage_builder.block_device.tests.plugin.test_a:TestA
|
||||
test_b = diskimage_builder.block_device.tests.plugin.test_b:TestB
|
||||
|
Loading…
Reference in New Issue
Block a user