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:
Ian Wienand 2017-05-30 12:06:41 +10:00
parent 634e9ac043
commit b85de3cd9e
17 changed files with 427 additions and 110 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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}

View File

@ -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)

View File

@ -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,

View File

@ -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
"""

View File

@ -0,0 +1,6 @@
- test_a:
name: test_node_a
- test_b:
name: test_node_b
base: test_node_a

View 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]

View 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]

View 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)

View File

@ -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"""

View File

@ -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'])

View 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)

View File

@ -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