Add state object, rename "results", add unit tests
A couple of things going on, but I think it makes sense to do them atomically. The NodeBase.create() argument "results" is the global state dictionary that will be saved to "state.json", and re-loaded in later phases and passed to them as the argument "state". So for consistency, call this argument "state" (this fits with the change out to start building the state dictionary earlier in the PluginBase.__init__() calls). Since the "state" is a pretty important part of how everything works, move it into a separate object. This is treated as essentially a singleton. It bundles it nicely together for some added documentation [1]. We move instantiation of this object out of the generic BlockDevice.__init__() call and into the actual cmd_* drivers. This is because there's two distinct instantiation operations -- creating a new state (during cmd_create) and loading an existing state (other cmd_*). This is also safer -- since we know the cmd_* arguments are looking for an existing state.json, we will fail if it somehow goes missing. To more fully unit test this, some testing plugins and new entry-points are added. These add known state values which we check for. These should be a good basis for further tests. [1] as noted, we could probably do some fun things in the future like make this implement a dictionary and have some saftey features like r/o keys. Change-Id: I90eb711b3e9b1ce139eb34bdf3cde641fd06828f
This commit is contained in:
parent
634e9ac043
commit
b85de3cd9e
@ -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